diff --git a/Cargo.lock b/Cargo.lock index bf85eb168..0b392c5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -585,8 +585,8 @@ dependencies = [ "serde_json", "signal-hook", "simplelog", + "strip-ansi-escapes", "time", - "win_dbg_logger 0.1.0", "winapi", "windows-sys 0.52.0", ] @@ -1221,7 +1221,7 @@ dependencies = [ "parking_lot", "regex", "widestring", - "win_dbg_logger 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "win_dbg_logger", "winapi", ] @@ -1252,6 +1252,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.0" @@ -1495,6 +1504,26 @@ dependencies = [ "libc", ] +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1571,16 +1600,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" -[[package]] -name = "win_dbg_logger" -version = "0.1.0" -dependencies = [ - "log", - "regex", - "simplelog", - "winapi", -] - [[package]] name = "win_dbg_logger" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d52c8661e..42372fbe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "example_tcp_client", "tcp_protocol", "windows_key_tester", - "win_dbg_logger", "simulated_input", "simulated_passthru", ] @@ -101,12 +100,12 @@ windows-sys = { version = "0.52.0", features = [ "Wdk_System", "Wdk_System_SystemServices", ], optional=true } -win_dbg_logger = { path = "win_dbg_logger", optional = true } native-windows-gui = { version = "1.0.13", default_features = false} native-windows-derive = { version = "1.0.5", default_features = false, optional = true } regex = { version = "1.10.4", optional = true } kanata-interception = { version = "0.2.0", optional = true } muldiv = { version = "1.0.1", optional = true } +strip-ansi-escapes = { version = "0.2.0", optional = true } [target.'cfg(target_os = "windows")'.build-dependencies] embed-resource = { version = "2.4.2", optional = true } @@ -126,7 +125,9 @@ simulated_output = ["indoc"] simulated_input = ["indoc"] passthru_ahk = ["simulated_input","simulated_output"] wasm = [ "instant/wasm-bindgen" ] -gui = ["win_manifest","native-windows-derive","win_dbg_logger","win_dbg_logger/simple_shared","kanata-parser/gui","native-windows-gui/tray-notification","native-windows-gui/message-window","native-windows-gui/menu","native-windows-gui/cursor","native-windows-gui/high-dpi","native-windows-gui/embed-resource","native-windows-gui/image-decoder","native-windows-gui/notice","native-windows-gui/animation-timer","muldiv","dep:windows-sys","win_sendinput_send_scancodes","win_llhook_read_scancodes"] +gui = ["win_manifest","native-windows-derive","kanata-parser/gui","native-windows-gui/tray-notification","native-windows-gui/message-window","native-windows-gui/menu","native-windows-gui/cursor","native-windows-gui/high-dpi","native-windows-gui/embed-resource","native-windows-gui/image-decoder","native-windows-gui/notice","native-windows-gui/animation-timer","muldiv","strip-ansi-escapes","dep:windows-sys","win_sendinput_send_scancodes","win_llhook_read_scancodes", + "winapi/processthreadsapi", +] [profile.release] opt-level = "z" diff --git a/cfg_samples/tray-icon/tray-icon.kbd b/cfg_samples/tray-icon/tray-icon.kbd index 35eca9072..1c7bfdfaf 100644 --- a/cfg_samples/tray-icon/tray-icon.kbd +++ b/cfg_samples/tray-icon/tray-icon.kbd @@ -1,12 +1,16 @@ (defcfg - process-unmapped-keys yes ;;|no| enable processing of keys that are not in defsrc, useful if mapping a few keys in defsrc instead of most of the keys on your keyboard. Without this, the tap-hold-release and tap-hold-press actions will not activate for keys that are not in defsrc. Disabled because some keys may not work correctly if they are intercepted. E.g. rctl/altgr on Windows; see the windows-altgr configuration item above for context. - log-layer-changes yes ;;|no| overhead + process-unmapped-keys yes ;;|no| enable processing of keys that are not in defsrc, useful if mapping a few keys in defsrc instead of most of the keys on your keyboard. Without this, the tap-hold-release and tap-hold-press actions will not activate for keys that are not in defsrc. Disabled because some keys may not work correctly if they are intercepted. E.g. rctl/altgr on Windows; see the windows-altgr configuration item above for context. + log-layer-changes yes ;;|no| overhead tray-icon "./_custom-icons/s.png" ;; should activate for layers without icons like '5no-icn' - icon-match-layer-name yes ;;|yes| match layer name to icon files even without an explicit (icon name.ico) config - tooltip-layer-changes yes ;;|false| - tooltip-show-blank yes ;;|no| - tooltip-duration 500 ;;|500| - tooltip-size 24,24 ;;|24 24| + ;;opt val |≝| + icon-match-layer-name yes ;;|yes| match layer name to icon files even without an explicit (icon name.ico) config + tooltip-layer-changes yes ;;|false| + tooltip-show-blank yes ;;|no| + tooltip-duration 500 ;;|500| + tooltip-size 24,24 ;;|24 24| + notify-cfg-reload yes ;;|yes| + notify-cfg-reload-silent no ;;|no| + notify-error yes ;;|yes| ) (defalias l1 (layer-while-held 1emoji)) (defalias l2 (layer-while-held 2icon-quote)) diff --git a/docs/config.adoc b/docs/config.adoc index 9c063882a..31541ec13 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2566,6 +2566,12 @@ Show system notification message on config reload. Defaults to true. Requires << Disable sound for the system notification message on config reload. Defaults to false. Requires <> gui-enabled build. +[[windows-only-notify-error]] +=== Windows only: notify-error +<> + +Show system notification message on kanata errors. Defaults to true. Requires <> gui-enabled build. + [[using-multiple-defcfg-options]] === Using multiple defcfg options <> diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 1d4b354a4..3ebbad9dc 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -32,6 +32,9 @@ pub struct CfgOptionsGui { /// Disable sound for the system notification message on config reload #[cfg(all(target_os = "windows", feature = "gui"))] pub notify_cfg_reload_silent: bool, + /// Show system notification message on errors + #[cfg(all(target_os = "windows", feature = "gui"))] + pub notify_error: bool, /// Set tooltip size (width, height) #[cfg(all(target_os = "windows", feature = "gui"))] pub tooltip_size: (u16, u16), @@ -48,6 +51,7 @@ impl Default for CfgOptionsGui { tooltip_duration: 500, notify_cfg_reload: true, notify_cfg_reload_silent: false, + notify_error: true, tooltip_size: (24, 24), } } @@ -527,6 +531,15 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { parse_defcfg_val_bool(val, label)? } } + "notify-error" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.notify_error = parse_defcfg_val_bool(val, label)? + } + } "tooltip-size" => { #[cfg(all( any(target_os = "windows", target_os = "unknown"), diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index f7d420fc7..9381d63d9 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1340,6 +1340,7 @@ fn parse_all_defcfg() { tooltip-size 24,24 notify-cfg-reload yes notify-cfg-reload-silent no + notify-error yes windows-altgr add-lctl-release windows-interception-mouse-hwid "70, 0, 60, 0" windows-interception-mouse-hwids ("0, 0, 0" "1, 1, 1") diff --git a/src/gui/mod.rs b/src/gui/mod.rs index a75f79207..cbfe02483 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,5 +1,6 @@ pub mod win; pub use win::*; +pub mod win_dbg_logger; pub mod win_nwg_ext; pub use win_dbg_logger as log_win; pub use win_dbg_logger::WINDBG_LOGGER; @@ -7,7 +8,10 @@ pub use win_nwg_ext::*; use crate::*; use parking_lot::Mutex; +use std::sync::mpsc::Sender as ASender; use std::sync::{Arc, OnceLock}; pub static CFG: OnceLock>> = OnceLock::new(); pub static GUI_TX: OnceLock = OnceLock::new(); pub static GUI_CFG_TX: OnceLock = OnceLock::new(); +pub static GUI_ERR_TX: OnceLock = OnceLock::new(); +pub static GUI_ERR_MSG_TX: OnceLock> = OnceLock::new(); diff --git a/src/gui/win.rs b/src/gui/win.rs index 50f257b9e..d090c8c74 100644 --- a/src/gui/win.rs +++ b/src/gui/win.rs @@ -96,7 +96,12 @@ pub struct SystemTray { win_tt_timer: nwg::AnimationTimer, pub layer_notice: nwg::Notice, pub cfg_notice: nwg::Notice, + pub err_notice: nwg::Notice, pub tt_notice: nwg::Notice, + /// Receiver of error message content sent from other threads + /// (e.g., from key event thread via WinDbgLogger that will also notify our GUI + /// (but not pass data) after sending data to this receiver) + pub err_recv: Option>, pub tt2m_channel: Option<(ASender, Receiver)>, // receiver will be created before a thread is spawned and moved there pub m2tt_sender: RefCell>>, @@ -125,7 +130,7 @@ const ASSET_FD: [&str; 4] = ["", "icon", "img", "icons"]; const IMG_EXT: [&str; 7] = ["ico", "jpg", "jpeg", "png", "bmp", "dds", "tiff"]; const PRE_LAYER: &str = "\n🗍: "; // : invalid path marker, so should be safe to use as a separator const TTTIMER_L: u16 = 9; // lifetime delta to duration for a tooltip timer -use crate::gui::{CFG, GUI_CFG_TX, GUI_TX}; +use crate::gui::{CFG, GUI_CFG_TX, GUI_ERR_MSG_TX, GUI_ERR_TX, GUI_TX}; pub fn send_gui_notice() { if let Some(gui_tx) = GUI_TX.get() { @@ -141,6 +146,26 @@ pub fn send_gui_cfg_notice() { error!("no GUI_CFG_TX to notify GUI thread of layer changes"); } } +pub fn send_gui_err_notice() { + if let Some(gui_tx) = GUI_ERR_TX.get() { + gui_tx.notice(); + } else { + error!("no GUI_ERR_TX to notify GUI thread of errors"); + } +} +pub fn show_err_msg_nofail(title: String, msg: String) { + // log gets insalized before gui, so some errors might have no target to log to, ignore them + if let Some(gui_msg_tx) = GUI_ERR_MSG_TX.get() { + if gui_msg_tx.send((title, msg)).is_err() { + warn!("send_gui_err_msg_notice failed to use OS notifications") + } else { + // can't Error to avoid an ∞ error loop ↑ + if let Some(gui_tx) = GUI_ERR_TX.get() { + gui_tx.notice(); + } + } + } +} /// Find an icon file that matches a given config icon name for a layer `lyr_icn` or a layer name /// `lyr_nm` (if `match_name` is `true`) or a given config icon name for the whole config `cfg_p` @@ -921,6 +946,48 @@ impl SystemTray { fn reload_layer_icon(&self) { let _ = self.reload_cfg_or_layer_icon(false); } + /// Show OS notification message with an error coming from WinDbgLogger + fn notify_error(&self) { + let app_data = self.app_data.borrow(); + if !app_data.gui_opts.notify_error { + return; + }; + use nwg::TrayNotificationFlags as f_tray; + let mut msg_title = "".to_string(); + let mut msg_content = "".to_string(); + let mut flags = f_tray::empty(); + if let Some(gui_msg_rx) = &self.err_recv { + match gui_msg_rx.try_recv() { + Ok((title, msg)) => { + msg_title += &title; + msg_content += &msg; + } + Err(TryRecvError::Empty) => { + msg_title += "internal"; + msg_content += "channel to receive errors is Empty"; + } + Err(TryRecvError::Disconnected) => { + msg_title += "internal"; + msg_content += "channel to receive errors is Disconnected"; + } + } + } else { + msg_title += "internal"; + msg_content += "SystemTray is supposed to have a valid 'err_recv' field value" + } + flags |= f_tray::ERROR_ICON; + if app_data.gui_opts.notify_cfg_reload_silent { + flags |= f_tray::SILENT; + } + let msg_title = strip_ansi_escapes::strip_str(&msg_title); + let msg_content = strip_ansi_escapes::strip_str(&msg_content); + self.tray.show( + &msg_content, + Some(&msg_title), + Some(flags), + Some(&self.icon), + ); + } /// Update tray icon data on config reload fn reload_cfg_icon(&self) { let _ = self.reload_cfg_or_layer_icon(true); @@ -1227,6 +1294,11 @@ pub mod system_tray_ui { let (sndr, rcvr) = std::sync::mpsc::channel(); d.tt2m_channel = Some((sndr, rcvr)); + let (sndr, rcvr) = std::sync::mpsc::channel(); + d.err_recv = Some(rcvr); + if GUI_ERR_MSG_TX.set(sndr).is_err() { + warn!("Someone else set our ‘GUI_ERR_MSG_TX’"); + }; // Controls nwg::MessageWindow::builder().build(&mut d.window)?; @@ -1243,6 +1315,9 @@ pub mod system_tray_ui { nwg::Notice::builder() .parent(&d.window) .build(&mut d.tt_notice)?; + nwg::Notice::builder() + .parent(&d.window) + .build(&mut d.err_notice)?; nwg::Menu::builder() .parent(&d.tray_menu) .text("&F Load config") // @@ -1442,6 +1517,8 @@ pub mod system_tray_ui { SystemTray::reload_layer_icon(&evt_ui); } else if handle == evt_ui.cfg_notice { SystemTray::reload_cfg_icon(&evt_ui); + } else if handle == evt_ui.err_notice { + SystemTray::notify_error(&evt_ui); } else if handle == evt_ui.tt_notice { SystemTray::update_tooltip_pos(&evt_ui);} E::OnWindowClose => diff --git a/win_dbg_logger/src/lib.rs b/src/gui/win_dbg_logger/mod.rs similarity index 88% rename from win_dbg_logger/src/lib.rs rename to src/gui/win_dbg_logger/mod.rs index d70e53591..68d651c24 100644 --- a/win_dbg_logger/src/lib.rs +++ b/src/gui/win_dbg_logger/mod.rs @@ -35,7 +35,7 @@ //! } //! //! fn main() { -//! log::set_logger(&win_dbg_logger::WINDBG_LOGGER).unwrap(); +//! log::set_logger(&kanata_state_machine::gui::WINDBG_LOGGER).unwrap(); //! log::set_max_level(log::LevelFilter::Debug); //! //! do_cool_stuff(); @@ -57,7 +57,7 @@ pub struct WinDbgLogger { /// this can be directly registered using `log::set_logger`, e.g.: /// /// ``` -/// log::set_logger(&win_dbg_logger::WINDBG_LOGGER).unwrap(); // Initialize +/// log::set_logger(&kanata_state_machine::gui::WINDBG_LOGGER).unwrap(); // Initialize /// log::set_max_level(log::LevelFilter::Debug); /// /// use log::{info, debug}; // Import @@ -93,8 +93,12 @@ pub static WINDBG_L0: WinDbgLogger = WinDbgLogger { _priv: (), }; -#[cfg(feature = "simple_shared")] -pub fn windbg_simple_combo(log_lvl: LevelFilter) -> Box { +#[cfg(all(target_os = "windows", feature = "gui"))] +pub fn windbg_simple_combo( + log_lvl: LevelFilter, + noti_lvl: LevelFilter, +) -> Box { + set_noti_lvl(noti_lvl); match log_lvl { LevelFilter::Error => Box::new(WINDBG_L1), LevelFilter::Warn => Box::new(WINDBG_L2), @@ -104,7 +108,7 @@ pub fn windbg_simple_combo(log_lvl: LevelFilter) -> Box Box::new(WINDBG_L0), } } -#[cfg(feature = "simple_shared")] +#[cfg(all(target_os = "windows", feature = "gui"))] impl simplelog::SharedLogger for WinDbgLogger { // allows using with simplelog's CombinedLogger fn level(&self) -> LevelFilter { @@ -139,6 +143,13 @@ pub fn set_thread_state(is: bool) -> &'static bool { static CELL: OnceLock = OnceLock::new(); CELL.get_or_init(|| is) } +pub fn get_noti_lvl() -> &'static LevelFilter { + set_noti_lvl(LevelFilter::Off) +} +pub fn set_noti_lvl(lvl: LevelFilter) -> &'static LevelFilter { + static CELL: OnceLock = OnceLock::new(); + CELL.get_or_init(|| lvl) +} use regex::Regex; macro_rules! regex { @@ -183,6 +194,20 @@ impl log::Log for WinDbgLogger { record.line().unwrap_or(0), record.args() ); + #[cfg(all(target_os = "windows", feature = "gui"))] + { + use crate::gui::win::*; + let title = format!( + "{}{}:{}", + thread_id, + clean_name(record.file()), + record.line().unwrap_or(0) + ); + let msg = format!("{}", record.args()); + if record.level() <= *get_noti_lvl() { + show_err_msg_nofail(title, msg); + } + } output_debug_string(&s); } } diff --git a/win_dbg_logger/Cargo.toml b/src/gui/win_dbg_logger/win_dbg_logger.toml similarity index 100% rename from win_dbg_logger/Cargo.toml rename to src/gui/win_dbg_logger/win_dbg_logger.toml diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 3b2369984..967eb5a70 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -527,6 +527,7 @@ impl Kanata { self.gui_opts.tooltip_duration = cfg.options.gui_opts.tooltip_duration; self.gui_opts.notify_cfg_reload = cfg.options.gui_opts.notify_cfg_reload; self.gui_opts.notify_cfg_reload_silent = cfg.options.gui_opts.notify_cfg_reload_silent; + self.gui_opts.notify_error = cfg.options.gui_opts.notify_error; self.gui_opts.tooltip_size = cfg.options.gui_opts.tooltip_size; } diff --git a/src/main_lib/win_gui.rs b/src/main_lib/win_gui.rs index 0eb230fc9..85281632e 100644 --- a/src/main_lib/win_gui.rs +++ b/src/main_lib/win_gui.rs @@ -6,6 +6,7 @@ use kanata_state_machine::*; /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { + let noti_lvl = LevelFilter::Error; // min lvl above which to use Win system notifications let args = match Args::try_parse() { Ok(args) => args, Err(e) => { @@ -19,7 +20,7 @@ fn cli_init() -> Result { TerminalMode::Mixed, ColorChoice::AlwaysAnsi, ), - log_win::windbg_simple_combo(LevelFilter::Debug), + log_win::windbg_simple_combo(LevelFilter::Debug, noti_lvl), ]) .expect("logger can init"); } else { @@ -63,11 +64,12 @@ fn cli_init() -> Result { TerminalMode::Mixed, ColorChoice::AlwaysAnsi, ), - log_win::windbg_simple_combo(log_lvl), + log_win::windbg_simple_combo(log_lvl, noti_lvl), ]) .expect("logger can init"); } else { - CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl)]).expect("logger can init"); + CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl, noti_lvl)]) + .expect("logger can init"); } log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] @@ -149,12 +151,16 @@ fn main_impl() -> Result<()> { let ui = build_tray(&kanata_arc)?; let gui_tx = ui.layer_notice.sender(); let gui_cfg_tx = ui.cfg_notice.sender(); // allows notifying GUI on config reloads + let gui_err_tx = ui.err_notice.sender(); // allows notifying GUI on erorrs (from logger) if GUI_TX.set(gui_tx).is_err() { warn!("Someone else set our ‘GUI_TX’"); }; if GUI_CFG_TX.set(gui_cfg_tx).is_err() { warn!("Someone else set our ‘GUI_CFG_TX’"); }; + if GUI_ERR_TX.set(gui_err_tx).is_err() { + warn!("Someone else set our ‘GUI_ERR_TX’"); + }; Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) {