Diagnostics for focus and layering (--diag / WAYBETTER_DIAG=1): one timestamped line per event in the app's log folder
For the macOS reports (the menu never shows and the window goes inactive on a click; the find field gets no keyboard): off unless the app is started with `--diag` or WAYBETTER_DIAG=1. Then native code and the shell append to `diag.log` in the app's log folder (truncated at launch): menu open requests and every close with its reason, overlay counts, raise/lower and `shell_raise` results, native `shell_focus` calls and their result, the shell document's focus/blur/visibility, and where the find bar's focus is right after it opens and 300 ms later. A line is an event and a few words in plain ASCII (capped); never a URL, a query or page text. `diag_get` / `diag_log` are shell-only (in commands::SHELL and the template's list). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7f9e172196
commit
43fc8a15f5
8 changed files with 147 additions and 69 deletions
89
crates/waybetter-host/src/diag.rs
Normal file
89
crates/waybetter-host/src/diag.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
//! Focus and layering diagnostics, off unless asked for at launch (`--diag` or the environment
|
||||
//! variable `WAYBETTER_DIAG=1`). While on, native code and the shell append one timestamped line per
|
||||
//! event to `diag.log` in the app's log folder: menu open requests and every close with its reason,
|
||||
//! overlays and `shell_raise` calls with their results, native focus calls, and the shell document's
|
||||
//! focus, blur and visibility changes. A line is an event name and a few words: never a URL, a
|
||||
//! query, page text or anything about an account ([`clean`] keeps plain printable ASCII only and
|
||||
//! caps the length). The file is truncated at each launch with diagnostics on.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
pub const ARG: &str = "--diag";
|
||||
pub const ENV: &str = "WAYBETTER_DIAG";
|
||||
pub const FILE: &str = "diag.log";
|
||||
const MAX_LINE: usize = 300;
|
||||
|
||||
static ON: AtomicBool = AtomicBool::new(false);
|
||||
static SINK: Mutex<Option<(std::fs::File, Instant)>> = Mutex::new(None);
|
||||
|
||||
/// Whether this launch asks for diagnostics (the first argument is the program itself).
|
||||
pub fn asked<I, S>(args: I, env: Option<&str>) -> bool
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
env.is_some_and(|value| value == "1") || args.into_iter().skip(1).any(|arg| arg.as_ref() == ARG)
|
||||
}
|
||||
|
||||
/// A line as it is written: printable ASCII only (anything else becomes `?`), one line, capped.
|
||||
pub fn clean(line: &str) -> String {
|
||||
line.chars().take(MAX_LINE).map(|ch| if (' '..='~').contains(&ch) { ch } else { '?' }).collect()
|
||||
}
|
||||
|
||||
/// Starts writing to `dir/diag.log` (created, truncated). Nothing happens without a folder.
|
||||
pub fn start(dir: Option<PathBuf>) {
|
||||
let Some(dir) = dir else { return };
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
if let Ok(file) = std::fs::File::create(dir.join(FILE)) {
|
||||
if let Ok(mut sink) = SINK.lock() {
|
||||
*sink = Some((file, Instant::now()));
|
||||
ON.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
log("host", "diagnostics on");
|
||||
}
|
||||
|
||||
pub fn active() -> bool {
|
||||
ON.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// One event from `source` (`host` or `shell`), with milliseconds since the start.
|
||||
pub fn log(source: &str, line: &str) {
|
||||
if !active() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut sink) = SINK.lock() {
|
||||
if let Some((file, started)) = sink.as_mut() {
|
||||
let _ = writeln!(file, "{:>9} {:<5} {}", started.elapsed().as_millis(), clean(source), clean(line));
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn asked_by_the_argument_or_the_environment_only() {
|
||||
assert!(asked(["app", "--diag"], None));
|
||||
assert!(asked(["app"], Some("1")));
|
||||
assert!(!asked(["app"], Some("yes")));
|
||||
assert!(!asked(["--diag"], None), "the first argument is the program");
|
||||
assert!(!asked(["app", "--diag=1", "diag"], None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_is_plain_short_ascii() {
|
||||
assert_eq!(clean("menu close: blur (hasFocus=false)"), "menu close: blur (hasFocus=false)");
|
||||
assert_eq!(clean("two\nlines\ttab é"), "two?lines?tab ?");
|
||||
assert_eq!(clean(&"x".repeat(1000)).len(), MAX_LINE);
|
||||
// Off by default: nothing is written.
|
||||
assert!(!active());
|
||||
log("shell", "ignored");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
//! Google Fonts, native side, for every app that sets [`crate::app::AppConfig::fonts`]: the
|
||||
//! shell-only `gfonts_*` commands ([`crate::commands::GFONTS`]) over [`crate::fonts`]'s downloads
|
||||
//! and cache (`fonts.googleapis.com` and `fonts.gstatic.com` only, app-data `fonts/`). Site tabs
|
||||
//! never request fonts and gain no command: native code hands them cached bytes
|
||||
//! ([`crate::fonts::page_script_for`]).
|
||||
//!
|
||||
//! When a font is installed or removed the app's [`crate::app::SiteHooks::fonts_changed`] runs, so
|
||||
//! it can give every site tab its theme (and the font bytes) again.
|
||||
|
||||
use crate::fonts::{self, InstalledFont};
|
||||
use crate::gfonts::Weights;
|
||||
use crate::host::shell_only;
|
||||
use tauri::{AppHandle, Manager, Webview};
|
||||
|
||||
/// Opens the font cache in app data. Does nothing unless the app set [`crate::app::AppConfig::fonts`].
|
||||
pub(crate) fn init(app: &AppHandle) {
|
||||
if !crate::app::config(app).fonts {
|
||||
return;
|
||||
}
|
||||
if let Ok(dir) = app.path().app_data_dir() {
|
||||
fonts::init(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn changed(app: &AppHandle) {
|
||||
crate::app::hooks(app).fonts_changed(app);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn gfonts_installed(webview: Webview) -> Result<Vec<InstalledFont>, String> {
|
||||
shell_only(&webview)?;
|
||||
fonts::installed()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn gfonts_install(webview: Webview, app: AppHandle, family: String, weights: Weights) -> Result<InstalledFont, String> {
|
||||
shell_only(&webview)?;
|
||||
let installed = fonts::install(family, weights).await?;
|
||||
changed(&app);
|
||||
Ok(installed)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn gfonts_remove(webview: Webview, app: AppHandle, family: String) -> Result<(), String> {
|
||||
shell_only(&webview)?;
|
||||
fonts::remove(&family)?;
|
||||
changed(&app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn gfonts_faces(webview: Webview, family: String) -> Result<serde_json::Value, String> {
|
||||
shell_only(&webview)?;
|
||||
fonts::faces(&family)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn gfonts_preview(webview: Webview, family: String, weights: Weights, text: String) -> Result<Vec<serde_json::Value>, String> {
|
||||
shell_only(&webview)?;
|
||||
fonts::preview(family, weights, text).await
|
||||
}
|
||||
|
||||
/// The fonts commands ([`crate::commands::GFONTS`]).
|
||||
pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![gfonts_installed, gfonts_install, gfonts_remove, gfonts_faces, gfonts_preview]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue