From 6fff86fd535f92e59e505fb6fc379c26bc58ea27 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 18 Apr 2024 23:04:53 +0700 Subject: [PATCH 001/154] win-tray: add icon --- assets/kanata.ico | Bin 0 -> 10366 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/kanata.ico diff --git a/assets/kanata.ico b/assets/kanata.ico new file mode 100644 index 0000000000000000000000000000000000000000..cc5945c629bf9fc210064a16c2af7642e9941aa2 GIT binary patch literal 10366 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 Date: Sun, 28 Apr 2024 18:15:26 +0700 Subject: [PATCH 002/154] win-tray: move main to m_cli --- src/m_cli.rs | 230 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 247 ++++----------------------------------------------- 2 files changed, 246 insertions(+), 231 deletions(-) create mode 100644 src/m_cli.rs diff --git a/src/m_cli.rs b/src/m_cli.rs new file mode 100644 index 000000000..90cf78721 --- /dev/null +++ b/src/m_cli.rs @@ -0,0 +1,230 @@ +use anyhow::{bail, Result}; +use clap::Parser; +use kanata_parser::cfg; +use kanata_state_machine::*; +use log::info; +use simplelog::{format_description, *}; +use std::path::PathBuf; + +#[cfg(test)] +mod tests; + +#[derive(Parser, Debug)] +#[command(author, version, verbatim_doc_comment)] +/// kanata: an advanced software key remapper +/// +/// kanata remaps key presses to other keys or complex actions depending on the +/// configuration for that key. You can find the guide for creating a config +/// file here: +/// +/// https://github.com/jtroo/kanata/blob/main/docs/config.adoc +/// +/// If you need help, please feel welcome to create an issue or discussion in +/// the kanata repository: +/// +/// https://github.com/jtroo/kanata +struct Args { + // Display different platform specific paths based on the target OS + #[cfg_attr( + target_os = "windows", + doc = r"Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" + )] + #[cfg_attr( + target_os = "macos", + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$HOME/Library/Application Support/kanata/kanata.kbd.'" + )] + #[cfg_attr( + not(any(target_os = "macos", target_os = "windows")), + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$XDG_CONFIG_HOME/kanata/kanata.kbd'" + )] + #[arg(short, long, verbatim_doc_comment)] + cfg: Option>, + + /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be + /// listened on. + #[cfg(feature = "tcp_server")] + #[arg( + short = 'p', + long = "port", + value_name = "PORT or IP:PORT", + verbatim_doc_comment + )] + tcp_server_address: Option, + /// Path for the symlink pointing to the newly-created device. If blank, no + /// symlink will be created. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + symlink_path: Option, + + /// List the keyboards available for grabbing and exit. + #[cfg(target_os = "macos")] + #[arg(short, long)] + list: bool, + + /// Enable debug logging. + #[arg(short, long)] + debug: bool, + + /// Enable trace logging; implies --debug as well. + #[arg(short, long)] + trace: bool, + + /// Remove the startup delay on kanata. + /// In some cases, removing the delay may cause keyboard issues on startup. + #[arg(short, long, verbatim_doc_comment)] + nodelay: bool, + + /// Milliseconds to wait before attempting to register a newly connected + /// device. The default is 200. + /// + /// You may wish to increase this if you have a device that is failing + /// to register - the device may be taking too long to become ready. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + wait_device_ms: Option, + + /// Validate configuration file and exit + #[arg(long, verbatim_doc_comment)] + check: bool, +} + +/// Parse CLI arguments and initialize logging. +fn cli_init() -> Result { + let args = Args::parse(); + + #[cfg(target_os = "macos")] + if args.list { + karabiner_driverkit::list_keyboards(); + std::process::exit(0); + } + + let cfg_paths = args.cfg.unwrap_or_else(default_cfg); + + let log_lvl = match (args.debug, args.trace) { + (_, true) => LevelFilter::Trace, + (true, false) => LevelFilter::Debug, + (false, false) => LevelFilter::Info, + }; + + let mut log_cfg = ConfigBuilder::new(); + if let Err(e) = log_cfg.set_time_offset_to_local() { + eprintln!("WARNING: could not set log TZ to local: {e:?}"); + }; + log_cfg.set_time_format_rfc3339(); + CombinedLogger::init(vec![TermLogger::new( + log_lvl, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + )]) + .expect("logger can init"); + log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); + #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] + log::info!("using LLHOOK+SendInput for keyboard IO"); + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + log::info!("using the Interception driver for keyboard IO"); + + if let Some(config_file) = cfg_paths.first() { + if !config_file.exists() { + bail!( + "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", + cfg_paths[0].to_str().unwrap_or("?") + ) + } + } else { + bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); + } + + if args.check { + log::info!("validating config only and exiting"); + let status = match cfg::new_from_file(&cfg_paths[0]) { + Ok(_) => 0, + Err(e) => { + log::error!("{e:?}"); + 1 + } + }; + std::process::exit(status); + } + + #[cfg(target_os = "linux")] + if let Some(wait) = args.wait_device_ms { + use std::sync::atomic::Ordering; + log::info!("Setting device registration wait time to {wait} ms."); + oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); + } + + Ok(ValidatedArgs { + paths: cfg_paths, + #[cfg(feature = "tcp_server")] + tcp_server_address: args.tcp_server_address, + #[cfg(target_os = "linux")] + symlink_path: args.symlink_path, + nodelay: args.nodelay, + }) +} + +fn main_impl() -> Result<()> { + let args = cli_init()?; + let kanata_arc = Kanata::new_arc(&args)?; + + if !args.nodelay { + info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); + std::thread::sleep(std::time::Duration::from_secs(2)); + } + + // Start a processing loop in another thread and run the event loop in this thread. + // + // The reason for two different event loops is that the "event loop" only listens for keyboard + // events, which it sends to the "processing loop". The processing loop handles keyboard events + // while also maintaining `tick()` calls to keyberon. + + let (tx, rx) = std::sync::mpsc::sync_channel(100); + + let (server, ntx, nrx) = if let Some(address) = { + #[cfg(feature = "tcp_server")] + { + args.tcp_server_address + } + #[cfg(not(feature = "tcp_server"))] + { + None:: + } + } { + let mut server = TcpServer::new(address.into_inner(), tx.clone()); + server.start(kanata_arc.clone()); + let (ntx, nrx) = std::sync::mpsc::sync_channel(100); + (Some(server), Some(ntx), Some(nrx)) + } else { + (None, None, None) + }; + + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); + + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } + + #[cfg(target_os = "linux")] + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; + + Kanata::event_loop(kanata_arc, tx)?; + + Ok(()) +} +pub fn main_cli() -> Result<()> { + let ret = main_impl(); + if let Err(ref e) = ret { + log::error!("{e}\n"); + } + eprintln!("\nPress enter to exit"); + let _ = std::io::stdin().read_line(&mut String::new()); + ret +} diff --git a/src/main.rs b/src/main.rs index cac3a156e..76ef43695 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,234 +1,19 @@ -use anyhow::{bail, Result}; -use clap::Parser; -use kanata_parser::cfg; -use kanata_state_machine::*; -use log::info; -use simplelog::{format_description, *}; -use std::path::PathBuf; - -#[cfg(test)] -mod tests; - -#[derive(Parser, Debug)] -#[command(author, version, verbatim_doc_comment)] -/// kanata: an advanced software key remapper -/// -/// kanata remaps key presses to other keys or complex actions depending on the -/// configuration for that key. You can find the guide for creating a config -/// file here: -/// -/// https://github.com/jtroo/kanata/blob/main/docs/config.adoc -/// -/// If you need help, please feel welcome to create an issue or discussion in -/// the kanata repository: -/// -/// https://github.com/jtroo/kanata -struct Args { - // Display different platform specific paths based on the target OS - #[cfg_attr( - target_os = "windows", - doc = r"Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" - )] - #[cfg_attr( - target_os = "macos", - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$HOME/Library/Application Support/kanata/kanata.kbd.'" - )] - #[cfg_attr( - not(any(target_os = "macos", target_os = "windows")), - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$XDG_CONFIG_HOME/kanata/kanata.kbd'" - )] - #[arg(short, long, verbatim_doc_comment)] - cfg: Option>, - - /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be - /// listened on. - #[cfg(feature = "tcp_server")] - #[arg( - short = 'p', - long = "port", - value_name = "PORT or IP:PORT", - verbatim_doc_comment - )] - tcp_server_address: Option, - /// Path for the symlink pointing to the newly-created device. If blank, no - /// symlink will be created. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - symlink_path: Option, - - /// List the keyboards available for grabbing and exit. - #[cfg(target_os = "macos")] - #[arg(short, long)] - list: bool, - - /// Enable debug logging. - #[arg(short, long)] - debug: bool, - - /// Enable trace logging; implies --debug as well. - #[arg(short, long)] - trace: bool, - - /// Remove the startup delay on kanata. - /// In some cases, removing the delay may cause keyboard issues on startup. - #[arg(short, long, verbatim_doc_comment)] - nodelay: bool, - - /// Milliseconds to wait before attempting to register a newly connected - /// device. The default is 200. - /// - /// You may wish to increase this if you have a device that is failing - /// to register - the device may be taking too long to become ready. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - wait_device_ms: Option, - - /// Validate configuration file and exit - #[arg(long, verbatim_doc_comment)] - check: bool, -} - -/// Parse CLI arguments and initialize logging. -fn cli_init() -> Result { - let args = Args::parse(); - - #[cfg(target_os = "macos")] - if args.list { - karabiner_driverkit::list_keyboards(); - std::process::exit(0); - } - - let cfg_paths = args.cfg.unwrap_or_else(default_cfg); - - let log_lvl = match (args.debug, args.trace) { - (_, true) => LevelFilter::Trace, - (true, false) => LevelFilter::Debug, - (false, false) => LevelFilter::Info, - }; - - let mut log_cfg = ConfigBuilder::new(); - if let Err(e) = log_cfg.set_time_offset_to_local() { - eprintln!("WARNING: could not set log TZ to local: {e:?}"); - }; - log_cfg.set_time_format_custom(format_description!( - version = 2, - "[hour]:[minute]:[second].[subsecond digits:4]" - )); - CombinedLogger::init(vec![TermLogger::new( - log_lvl, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - )]) - .expect("logger can init"); - log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); - #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] - log::info!("using LLHOOK+SendInput for keyboard IO"); - #[cfg(all(feature = "interception_driver", target_os = "windows"))] - log::info!("using the Interception driver for keyboard IO"); - - if let Some(config_file) = cfg_paths.first() { - if !config_file.exists() { - bail!( - "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", - cfg_paths[0].to_str().unwrap_or("?") - ) - } - } else { - bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); - } - - if args.check { - log::info!("validating config only and exiting"); - let status = match cfg::new_from_file(&cfg_paths[0]) { - Ok(_) => 0, - Err(e) => { - log::error!("{e:?}"); - 1 - } - }; - std::process::exit(status); - } - - #[cfg(target_os = "linux")] - if let Some(wait) = args.wait_device_ms { - use std::sync::atomic::Ordering; - log::info!("Setting device registration wait time to {wait} ms."); - oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); - } - - Ok(ValidatedArgs { - paths: cfg_paths, - #[cfg(feature = "tcp_server")] - tcp_server_address: args.tcp_server_address, - #[cfg(target_os = "linux")] - symlink_path: args.symlink_path, - nodelay: args.nodelay, - }) -} - -fn main_impl() -> Result<()> { - let args = cli_init()?; - let kanata_arc = Kanata::new_arc(&args)?; - - if !args.nodelay { - info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); - std::thread::sleep(std::time::Duration::from_secs(2)); - } - - // Start a processing loop in another thread and run the event loop in this thread. - // - // The reason for two different event loops is that the "event loop" only listens for keyboard - // events, which it sends to the "processing loop". The processing loop handles keyboard events - // while also maintaining `tick()` calls to keyberon. - - let (tx, rx) = std::sync::mpsc::sync_channel(100); - - let (server, ntx, nrx) = if let Some(address) = { - #[cfg(feature = "tcp_server")] - { - args.tcp_server_address - } - #[cfg(not(feature = "tcp_server"))] - { - None:: - } - } { - let mut server = TcpServer::new(address.into_inner(), tx.clone()); - server.start(kanata_arc.clone()); - let (ntx, nrx) = std::sync::mpsc::sync_channel(100); - (Some(server), Some(ntx), Some(nrx)) - } else { - (None, None, None) - }; - - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } - - #[cfg(target_os = "linux")] - sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - - Kanata::event_loop(kanata_arc, tx)?; - - Ok(()) -} - +#[cfg(feature = "gui")] +pub mod m_gui; +#[cfg(feature = "gui")] +use m_gui::main_gui; +#[cfg(not(feature = "gui"))] +pub mod m_cli; +#[cfg(not(feature = "gui"))] +use crate::m_cli::main_cli; + +#[cfg(not(feature = "gui"))] fn main() -> Result<()> { - let ret = main_impl(); - if let Err(ref e) = ret { - log::error!("{e}\n"); - } - eprintln!("\nPress enter to exit"); - let _ = std::io::stdin().read_line(&mut String::new()); + let ret = main_cli(); ret } + +#[cfg(feature = "gui")] +fn main() { + main_gui() +} From 3a71ddd12b1fe5b0d7badae27d28fd10b5a47136 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 18 Apr 2024 23:04:19 +0700 Subject: [PATCH 003/154] win-tray: add main_gui attach console if launched from console init logging to win debug str otherwise --- src/kanata/windows/llhook.rs | 2 + src/m_gui.rs | 287 +++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 3 files changed, 290 insertions(+) create mode 100644 src/m_gui.rs diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index e501fc49e..5edf0e21d 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -68,6 +68,8 @@ impl Kanata { // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); + eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel + let _ = std::io::stdin().read_line(&mut String::new()); Ok(()) } } diff --git a/src/m_gui.rs b/src/m_gui.rs new file mode 100644 index 000000000..82e960149 --- /dev/null +++ b/src/m_gui.rs @@ -0,0 +1,287 @@ +#![allow(unused_imports,unused_variables,unreachable_code,dead_code,non_upper_case_globals)] +// #![allow(non_upper_case_globals)] + +extern crate native_windows_gui as nwg; +extern crate native_windows_derive as nwd; +use nwd::NwgUi; +use nwg::NativeUi; + +#[derive(Default, NwgUi)] pub struct SystemTray { + #[nwg_control] window : nwg::MessageWindow, + #[nwg_resource(source_file:Some("../assets/kanata.ico"))] icon : nwg::Icon, + #[nwg_control(icon:Some(&data.icon), tip: Some("Hello"))] // + #[nwg_events(MousePressLeftUp:[SystemTray::show_menu] // + , OnContextMenu :[SystemTray::show_menu])] tray : nwg::TrayNotification, + #[nwg_control(parent:window , popup: true)] tray_menu : nwg::Menu, + #[nwg_control(parent:tray_menu, text:"&1 Hello")] // + #[nwg_events(OnMenuItemSelected:[SystemTray::hello1])] tray_item1 : nwg::MenuItem, + #[nwg_control(parent:tray_menu, text:"&2 Popup")] // + #[nwg_events(OnMenuItemSelected:[SystemTray::hello2])] tray_item2 : nwg::MenuItem, + #[nwg_control(parent:tray_menu, text:"&X Exit")] // + #[nwg_events(OnMenuItemSelected:[SystemTray::exit ])] tray_item3 : nwg::MenuItem, +} +impl SystemTray { + fn show_menu(&self) { + let (x, y) = nwg::GlobalCursor::position(); + self.tray_menu.popup(x, y); } + fn hello1(&self) {nwg::simple_message("Hello", "Hello World!");} + fn hello2(&self) { + let flags = nwg::TrayNotificationFlags::USER_ICON | nwg::TrayNotificationFlags::LARGE_ICON; + self.tray.show("Hello World", Some("Welcome to my application"), Some(flags), Some(&self.icon)); } + fn exit(&self) {nwg::stop_thread_dispatch();} +} + + + + +use anyhow::{bail, Result}; +use clap::Parser; +use kanata_parser::cfg; +use crate::*; +use log::info; +use simplelog::*; + +use std::path::PathBuf; + +// #[cfg(test)] +// mod tests; + +#[derive(Parser, Debug)] +// #[command(author, version, verbatim_doc_comment)] +/// kanata: an advanced software key remapper +/// +/// kanata remaps key presses to other keys or complex actions depending on the +/// configuration for that key. You can find the guide for creating a config +/// file here: +/// +/// https://github.com/jtroo/kanata/blob/main/docs/config.adoc +/// +/// If you need help, please feel welcome to create an issue or discussion in +/// the kanata repository: +/// +/// https://github.com/jtroo/kanata +struct Args { + // Display different platform specific paths based on the target OS + #[cfg_attr( + target_os = "windows", + doc = r"Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" + )] + #[cfg_attr( + target_os = "macos", + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$HOME/Library/Application Support/kanata/kanata.kbd.'" + )] + #[cfg_attr( + not(any(target_os = "macos", target_os = "windows")), + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$XDG_CONFIG_HOME/kanata/kanata.kbd'" + )] + #[arg(short, long, verbatim_doc_comment)] + cfg: Option>, + + /// Port to run the optional TCP server on. If blank, no TCP port will be + /// listened on. + #[cfg(feature = "tcp_server")] + #[arg( + short = 'p', + long = "port", + value_name = "PORT or IP:PORT", + verbatim_doc_comment + )] + tcp_server_address: Option, + + /// Path for the symlink pointing to the newly-created device. If blank, no + /// symlink will be created. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + symlink_path: Option, + + /// List the keyboards available for grabbing and exit. + #[cfg(target_os = "macos")] + #[arg(short, long)] + list: bool, + + /// Enable debug logging. + #[arg(short, long)] + debug: bool, + + /// Enable trace logging; implies --debug as well. + #[arg(short, long)] + trace: bool, + + /// Remove the startup delay on kanata. + /// In some cases, removing the delay may cause keyboard issues on startup. + #[arg(short, long, verbatim_doc_comment)] + nodelay: bool, + + /// Milliseconds to wait before attempting to register a newly connected + /// device. The default is 200. + /// + /// You may wish to increase this if you have a device that is failing + /// to register - the device may be taking too long to become ready. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + wait_device_ms: Option, + + /// Validate configuration file and exit + #[arg(long, verbatim_doc_comment)] + check: bool, +} + +/// Parse CLI arguments and initialize logging. +fn cli_init() -> Result { + let args = Args::parse(); + + #[cfg(target_os = "macos")] + if args.list { + karabiner_driverkit::list_keyboards(); + std::process::exit(0); + } + + let cfg_paths = args.cfg.unwrap_or_else(default_cfg); + + let log_lvl = match (args.debug, args.trace) { + (_, true) => LevelFilter::Trace, + (true, false) => LevelFilter::Debug, + (false, false) => LevelFilter::Info, + }; + + let mut log_cfg = ConfigBuilder::new(); + if let Err(e) = log_cfg.set_time_offset_to_local() { + eprintln!("WARNING: could not set log TZ to local: {e:?}"); + }; + log_cfg.set_time_format_rfc3339(); + // todo: use this logger with WinDbg + if *IS_TERM { + CombinedLogger::init(vec![ + TermLogger ::new(log_lvl,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi,), + WriteLogger::new(log_lvl,log_cfg.build(),log_win::WINDBG_LOGGER), + ]).expect("logger can init"); + } + log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); + #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] + log::info!("using LLHOOK+SendInput for keyboard IO"); + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + log::info!("using the Interception driver for keyboard IO"); + + if let Some(config_file) = cfg_paths.first() { + if !config_file.exists() { + bail!( + "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", + cfg_paths[0].to_str().unwrap_or("?") + ) + } + } else { + bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); + } + + if args.check { + log::info!("validating config only and exiting"); + let status = match cfg::new_from_file(&cfg_paths[0]) { + Ok(_) => 0, + Err(e) => { + log::error!("{e:?}"); + 1 + } + }; + std::process::exit(status); + } + + #[cfg(target_os = "linux")] + if let Some(wait) = args.wait_device_ms { + use std::sync::atomic::Ordering; + log::info!("Setting device registration wait time to {wait} ms."); + oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); + } + + Ok(ValidatedArgs { + paths: cfg_paths, + #[cfg(feature = "tcp_server")] + tcp_server_address: args.tcp_server_address, + #[cfg(target_os = "linux")] + symlink_path: args.symlink_path, + nodelay: args.nodelay, + }) +} + +fn main_impl() -> Result<()> { + let args = cli_init()?; // parse CLI arguments and initialize logging + #[cfg(not(feature = "passthru_ahk"))] + let cfg_arc = Kanata::new_arc(&args)?; // new configuration from a file + #[cfg(feature = "passthru_ahk")] + let cfg_arc = Kanata::new_arc(&args, None)?; // new configuration from a file + if !args.nodelay { + info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); + std::thread::sleep(std::time::Duration::from_secs(2)); + } + + // Start a processing loop in another thread and run the event loop in this thread. + // The reason for two different event loops is that the "event loop" only listens for keyboard events, which it sends to the "processing loop". The processing loop handles keyboard events while also maintaining `tick()` calls to keyberon. + let (tx, rx) = std::sync::mpsc::sync_channel(100); + + let (server, ntx, nrx) = if let Some(address) = { + #[cfg(feature = "tcp_server")]{args.tcp_server_address} + #[cfg(not(feature = "tcp_server"))]{None::} + } { + let mut server = TcpServer::new(address.into_inner(), tx.clone()); + server.start(cfg_arc.clone()); + let (ntx, nrx) = std::sync::mpsc::sync_channel(100); + (Some(server), Some(ntx), Some(nrx)) + } else { + (None, None, None) + }; + + Kanata::start_processing_loop(cfg_arc.clone(), rx, ntx, args.nodelay); // 2 handles keyboard events while also maintaining `tick()` calls to keyberon + + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } + #[cfg(target_os = "linux")] + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; + + Kanata::event_loop(cfg_arc, tx)?; // 1 only listens for keyboard events + + Ok(()) +} + +use log::*; +use win_dbg_logger as log_win; +fn log_init(max_lvl: &i8) { + let _ = log_win::init(); + let a = log_win::set_thread_state(true); + let log_lvl = match max_lvl { + 1 => log::LevelFilter::Error, + 2 => log::LevelFilter::Warn, + 3 => log::LevelFilter::Info, + 4 => log::LevelFilter::Debug, + 5 => log::LevelFilter::Trace, + _ => log::LevelFilter::Info, + }; + log::set_max_level(log_lvl); +} + +use once_cell::sync::Lazy; +static IS_TERM:Lazy = Lazy::new(||stdout().is_terminal()); + +use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; +use winapi::shared::minwindef::BOOL; +use std::io::{stdout, IsTerminal}; +pub fn main_gui() { + let is_attached:BOOL; // doesn't attach in GUI launch mode + unsafe {is_attached = AttachConsole(ATTACH_PARENT_PROCESS);}; + if *IS_TERM { + println!("println terminal; is_attached console = {:?}",is_attached); // GUI launch will have no console + log::info!("log::info terminal; is_attached console = {:?}",is_attached); // isn't ready yet + } else { + log_init(&4); + info!("I'm not a terminal"); + } + let ret = main_impl(); + if let Err(ref e) = ret {log::error!("{e}\n");} + unsafe {FreeConsole();} +} diff --git a/src/main.rs b/src/main.rs index 76ef43695..2c020b120 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ pub mod m_cli; #[cfg(not(feature = "gui"))] use crate::m_cli::main_cli; +use anyhow::{Result}; #[cfg(not(feature = "gui"))] fn main() -> Result<()> { let ret = main_cli(); From a7f9a81666fabe96865bf56b7e87c6ebe6963b9d Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 19 Apr 2024 01:15:16 +0700 Subject: [PATCH 004/154] win-tray: fix guards --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 2c020b120..253bb73c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ pub mod m_cli; #[cfg(not(feature = "gui"))] use crate::m_cli::main_cli; +use anyhow::{Result}; +#[cfg(not(feature = "gui"))] use anyhow::{Result}; #[cfg(not(feature = "gui"))] fn main() -> Result<()> { @@ -16,5 +18,5 @@ fn main() -> Result<()> { #[cfg(feature = "gui")] fn main() { - main_gui() + main_gui(); } From 9221e6a5e33f8d13e7cf81bed2daa67a8490c451 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 19 Apr 2024 22:54:08 +0700 Subject: [PATCH 005/154] win-tray: move code to the lib --- src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 253bb73c0..123b95388 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ #[cfg(feature = "gui")] -pub mod m_gui; -#[cfg(feature = "gui")] -use m_gui::main_gui; +use kanata_state_machine::m_gui::main_gui; + #[cfg(not(feature = "gui"))] pub mod m_cli; #[cfg(not(feature = "gui"))] From 8928aff7c2db41776025b5790093526b1850bbf6 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:23:45 +0700 Subject: [PATCH 006/154] win-tray: update main --- src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 123b95388..15c8a6190 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,19 @@ #[cfg(feature = "gui")] use kanata_state_machine::m_gui::main_gui; -#[cfg(not(feature = "gui"))] -pub mod m_cli; -#[cfg(not(feature = "gui"))] -use crate::m_cli::main_cli; +#[cfg(not(feature = "gui"))] use kanata_state_machine::lib_main::lib_main_cli; +#[cfg( feature = "gui" )] use kanata_state_machine::lib_main::lib_main_gui; use anyhow::{Result}; #[cfg(not(feature = "gui"))] use anyhow::{Result}; #[cfg(not(feature = "gui"))] fn main() -> Result<()> { - let ret = main_cli(); + let ret = lib_main_cli(); ret } #[cfg(feature = "gui")] fn main() { - main_gui(); + lib_main_gui(); } From 5802433fe22b7ce9ac3a468ed441a4aad2cff100 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:17:15 +0700 Subject: [PATCH 007/154] win-tray: disable console with a gui app --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 15c8a6190..6d70ae3ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(feature = "gui", windows_subsystem = "windows")] //disable console on Windows #[cfg(feature = "gui")] use kanata_state_machine::m_gui::main_gui; From 6b06a90b6af11eb788716511b301dfedf3a6ff50 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Mon, 22 Apr 2024 03:39:23 +0700 Subject: [PATCH 008/154] win-tray: add IS_CONSOLE as a lazy static --- src/m_gui.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/m_gui.rs b/src/m_gui.rs index 82e960149..0420db4ba 100644 --- a/src/m_gui.rs +++ b/src/m_gui.rs @@ -145,9 +145,9 @@ fn cli_init() -> Result { let cfg_paths = args.cfg.unwrap_or_else(default_cfg); let log_lvl = match (args.debug, args.trace) { - (_, true) => LevelFilter::Trace, - (true, false) => LevelFilter::Debug, - (false, false) => LevelFilter::Info, + (_ , true ) => LevelFilter::Trace, + (true , false) => LevelFilter::Debug, + (false, false) => LevelFilter::Info, }; let mut log_cfg = ConfigBuilder::new(); @@ -267,19 +267,19 @@ fn log_init(max_lvl: &i8) { use once_cell::sync::Lazy; static IS_TERM:Lazy = Lazy::new(||stdout().is_terminal()); +static IS_CONSOLE:Lazy = Lazy::new(|| unsafe{ + if AttachConsole(ATTACH_PARENT_PROCESS)== 0i32 {return false} else {return true}}); use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; use winapi::shared::minwindef::BOOL; use std::io::{stdout, IsTerminal}; pub fn main_gui() { - let is_attached:BOOL; // doesn't attach in GUI launch mode - unsafe {is_attached = AttachConsole(ATTACH_PARENT_PROCESS);}; if *IS_TERM { - println!("println terminal; is_attached console = {:?}",is_attached); // GUI launch will have no console - log::info!("log::info terminal; is_attached console = {:?}",is_attached); // isn't ready yet + println!("println terminal; is_attached console = {}",*IS_CONSOLE); // GUI launch will have no console + info!("info! terminal; is_attached console = {}",*IS_CONSOLE); // isn't ready yet } else { - log_init(&4); - info!("I'm not a terminal"); + log_init(&5); + info!("info! I'm not a terminal, is_attached console = {}",*IS_CONSOLE);trace!("trace");debug!("debug"); } let ret = main_impl(); if let Err(ref e) = ret {log::error!("{e}\n");} From 0682b0fd27b625105122a09abce85c31be987931 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 19 Apr 2024 22:52:16 +0700 Subject: [PATCH 009/154] win-tray: update import --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 075d8d921..441930de4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,10 @@ use std::str::FromStr; pub mod kanata; pub mod oskbd; pub mod tcp_server; +#[cfg(feature = "gui")] +pub mod m_gui; +#[cfg(feature = "gui")] +use win_dbg_logger as log_win; pub use kanata::*; pub use tcp_server::TcpServer; From c55eca3f5365b304fa382165dfcaa056e3a049c6 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 19 Apr 2024 01:25:04 +0700 Subject: [PATCH 010/154] gui build: add manifest to avoid STATUS_ENTRYPOINT_NOT_FOUND error https://github.com/gabdube/native-windows-gui/issues/251 --- build.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 4d53b5bab..93ec08ca8 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,5 @@ fn main() -> std::io::Result<()> { - #[cfg(all(target_os = "windows", feature = "win_manifest"))] + #[cfg(all(target_os = "windows", any(feature = "win_manifest",feature = "gui")))] { windows::build()?; } @@ -59,3 +59,61 @@ mod windows { Ok(()) } } + +#[cfg(all(target_os = "windows", feature = "gui"))] +mod windows { + use indoc::formatdoc; + use regex::Regex; + use std::fs::File; + use std::io::Write; + extern crate embed_resource; + + // println! during build + macro_rules! pb { + ($($tokens:tt)*) => {println!("cargo:warning={}", format!($($tokens)*))}} + + pub(super) fn build() -> std::io::Result<()> { + let manifest_path: &str = "./target/kanata.exe.manifest"; + + // Note about expected version format: + // MS says "Use the four-part version format: mmmmm.nnnnn.ooooo.ppppp" + // https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests + + let re_ver_build = Regex::new(r"^(?(\d+\.){2}\d+)[-a-zA-Z]+(?\d+)$").unwrap(); + let re_version3 = Regex::new(r"^(\d+\.){2}\d+$").unwrap(); + let mut version: String = env!("CARGO_PKG_VERSION").to_string(); + + if re_version3.find(&version).is_some() { + version = format!("{}.0", version); + } else if re_ver_build.find(&version).is_some() { + version = re_ver_build + .replace_all(&version, r"$vpre.$vpos") + .to_string(); + } else { + pb!("unknown version format '{}', using '0.0.0.0'", version); + version = "0.0.0.0".to_string(); + } + + let manifest_str = formatdoc!( + r#" + + + + + + + + + + + + "#, + version + ); + let mut manifest_f = File::create(manifest_path)?; + write!(manifest_f, "{}", manifest_str)?; + embed_resource::compile("./src/kanata.exe.manifest.rc", embed_resource::NONE); + Ok(()) + } +} From a1890d9f11df1f5f2933d3d25a2d07554d854ed9 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:16:53 +0700 Subject: [PATCH 011/154] win-tray: add tray icon construction before llhook's gui loop --- src/kanata/windows/llhook.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 5edf0e21d..859a3d05b 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -6,6 +6,16 @@ use std::time; use super::PRESSED_KEYS; use crate::kanata::*; +#[cfg(feature = "gui")] +use crate::m_gui; +#[cfg(feature = "gui")] +extern crate native_windows_gui as nwg; +#[cfg(feature = "gui")] +extern crate native_windows_derive as nwd; +#[cfg(feature = "gui")] +use nwd::NwgUi; +#[cfg(feature = "gui")] +use nwg::NativeUi; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events @@ -66,6 +76,8 @@ impl Kanata { true }); + #[cfg(feature = "gui")] + let _ui = m_gui::SystemTray::build_ui(Default::default()).expect("Failed to build UI"); // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel From 9830ba36cb1ba779128a21d445f904dbc5b4905a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Mon, 22 Apr 2024 03:46:08 +0700 Subject: [PATCH 012/154] win-tray: combine common things in lib_main need to move cli impl to lib as well so that it can be used in the gui impl gui impl needs to be in the lib so that it can use the llhook's GUI event loop --- src/{m_cli.rs => lib_main.rs} | 0 src/m_gui.rs | 257 +--------------------------------- 2 files changed, 6 insertions(+), 251 deletions(-) rename src/{m_cli.rs => lib_main.rs} (100%) diff --git a/src/m_cli.rs b/src/lib_main.rs similarity index 100% rename from src/m_cli.rs rename to src/lib_main.rs diff --git a/src/m_gui.rs b/src/m_gui.rs index 0420db4ba..463bdaf87 100644 --- a/src/m_gui.rs +++ b/src/m_gui.rs @@ -31,257 +31,12 @@ impl SystemTray { fn exit(&self) {nwg::stop_thread_dispatch();} } - - - -use anyhow::{bail, Result}; -use clap::Parser; -use kanata_parser::cfg; -use crate::*; -use log::info; -use simplelog::*; - -use std::path::PathBuf; - -// #[cfg(test)] -// mod tests; - -#[derive(Parser, Debug)] -// #[command(author, version, verbatim_doc_comment)] -/// kanata: an advanced software key remapper -/// -/// kanata remaps key presses to other keys or complex actions depending on the -/// configuration for that key. You can find the guide for creating a config -/// file here: -/// -/// https://github.com/jtroo/kanata/blob/main/docs/config.adoc -/// -/// If you need help, please feel welcome to create an issue or discussion in -/// the kanata repository: -/// -/// https://github.com/jtroo/kanata -struct Args { - // Display different platform specific paths based on the target OS - #[cfg_attr( - target_os = "windows", - doc = r"Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" - )] - #[cfg_attr( - target_os = "macos", - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$HOME/Library/Application Support/kanata/kanata.kbd.'" - )] - #[cfg_attr( - not(any(target_os = "macos", target_os = "windows")), - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$XDG_CONFIG_HOME/kanata/kanata.kbd'" - )] - #[arg(short, long, verbatim_doc_comment)] - cfg: Option>, - - /// Port to run the optional TCP server on. If blank, no TCP port will be - /// listened on. - #[cfg(feature = "tcp_server")] - #[arg( - short = 'p', - long = "port", - value_name = "PORT or IP:PORT", - verbatim_doc_comment - )] - tcp_server_address: Option, - - /// Path for the symlink pointing to the newly-created device. If blank, no - /// symlink will be created. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - symlink_path: Option, - - /// List the keyboards available for grabbing and exit. - #[cfg(target_os = "macos")] - #[arg(short, long)] - list: bool, - - /// Enable debug logging. - #[arg(short, long)] - debug: bool, - - /// Enable trace logging; implies --debug as well. - #[arg(short, long)] - trace: bool, - - /// Remove the startup delay on kanata. - /// In some cases, removing the delay may cause keyboard issues on startup. - #[arg(short, long, verbatim_doc_comment)] - nodelay: bool, - - /// Milliseconds to wait before attempting to register a newly connected - /// device. The default is 200. - /// - /// You may wish to increase this if you have a device that is failing - /// to register - the device may be taking too long to become ready. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - wait_device_ms: Option, - - /// Validate configuration file and exit - #[arg(long, verbatim_doc_comment)] - check: bool, -} - -/// Parse CLI arguments and initialize logging. -fn cli_init() -> Result { - let args = Args::parse(); - - #[cfg(target_os = "macos")] - if args.list { - karabiner_driverkit::list_keyboards(); - std::process::exit(0); - } - - let cfg_paths = args.cfg.unwrap_or_else(default_cfg); - - let log_lvl = match (args.debug, args.trace) { - (_ , true ) => LevelFilter::Trace, - (true , false) => LevelFilter::Debug, - (false, false) => LevelFilter::Info, - }; - - let mut log_cfg = ConfigBuilder::new(); - if let Err(e) = log_cfg.set_time_offset_to_local() { - eprintln!("WARNING: could not set log TZ to local: {e:?}"); - }; - log_cfg.set_time_format_rfc3339(); - // todo: use this logger with WinDbg - if *IS_TERM { - CombinedLogger::init(vec![ - TermLogger ::new(log_lvl,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi,), - WriteLogger::new(log_lvl,log_cfg.build(),log_win::WINDBG_LOGGER), - ]).expect("logger can init"); - } - log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); - #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] - log::info!("using LLHOOK+SendInput for keyboard IO"); - #[cfg(all(feature = "interception_driver", target_os = "windows"))] - log::info!("using the Interception driver for keyboard IO"); - - if let Some(config_file) = cfg_paths.first() { - if !config_file.exists() { - bail!( - "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", - cfg_paths[0].to_str().unwrap_or("?") - ) - } - } else { - bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); - } - - if args.check { - log::info!("validating config only and exiting"); - let status = match cfg::new_from_file(&cfg_paths[0]) { - Ok(_) => 0, - Err(e) => { - log::error!("{e:?}"); - 1 - } - }; - std::process::exit(status); - } - - #[cfg(target_os = "linux")] - if let Some(wait) = args.wait_device_ms { - use std::sync::atomic::Ordering; - log::info!("Setting device registration wait time to {wait} ms."); - oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); - } - - Ok(ValidatedArgs { - paths: cfg_paths, - #[cfg(feature = "tcp_server")] - tcp_server_address: args.tcp_server_address, - #[cfg(target_os = "linux")] - symlink_path: args.symlink_path, - nodelay: args.nodelay, - }) -} - -fn main_impl() -> Result<()> { - let args = cli_init()?; // parse CLI arguments and initialize logging - #[cfg(not(feature = "passthru_ahk"))] - let cfg_arc = Kanata::new_arc(&args)?; // new configuration from a file - #[cfg(feature = "passthru_ahk")] - let cfg_arc = Kanata::new_arc(&args, None)?; // new configuration from a file - if !args.nodelay { - info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); - std::thread::sleep(std::time::Duration::from_secs(2)); - } - - // Start a processing loop in another thread and run the event loop in this thread. - // The reason for two different event loops is that the "event loop" only listens for keyboard events, which it sends to the "processing loop". The processing loop handles keyboard events while also maintaining `tick()` calls to keyberon. - let (tx, rx) = std::sync::mpsc::sync_channel(100); - - let (server, ntx, nrx) = if let Some(address) = { - #[cfg(feature = "tcp_server")]{args.tcp_server_address} - #[cfg(not(feature = "tcp_server"))]{None::} - } { - let mut server = TcpServer::new(address.into_inner(), tx.clone()); - server.start(cfg_arc.clone()); - let (ntx, nrx) = std::sync::mpsc::sync_channel(100); - (Some(server), Some(ntx), Some(nrx)) - } else { - (None, None, None) - }; - - Kanata::start_processing_loop(cfg_arc.clone(), rx, ntx, args.nodelay); // 2 handles keyboard events while also maintaining `tick()` calls to keyberon - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } - #[cfg(target_os = "linux")] - sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - - Kanata::event_loop(cfg_arc, tx)?; // 1 only listens for keyboard events - - Ok(()) -} - -use log::*; -use win_dbg_logger as log_win; -fn log_init(max_lvl: &i8) { - let _ = log_win::init(); - let a = log_win::set_thread_state(true); - let log_lvl = match max_lvl { - 1 => log::LevelFilter::Error, - 2 => log::LevelFilter::Warn, - 3 => log::LevelFilter::Info, - 4 => log::LevelFilter::Debug, - 5 => log::LevelFilter::Trace, - _ => log::LevelFilter::Info, - }; - log::set_max_level(log_lvl); -} +pub use log::*; +pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; +pub use winapi::shared::minwindef::BOOL; +pub use std::io::{stdout, IsTerminal}; use once_cell::sync::Lazy; -static IS_TERM:Lazy = Lazy::new(||stdout().is_terminal()); -static IS_CONSOLE:Lazy = Lazy::new(|| unsafe{ +pub static IS_TERM:Lazy = Lazy::new(||stdout().is_terminal()); +pub static IS_CONSOLE:Lazy = Lazy::new(|| unsafe{ if AttachConsole(ATTACH_PARENT_PROCESS)== 0i32 {return false} else {return true}}); - -use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; -use winapi::shared::minwindef::BOOL; -use std::io::{stdout, IsTerminal}; -pub fn main_gui() { - if *IS_TERM { - println!("println terminal; is_attached console = {}",*IS_CONSOLE); // GUI launch will have no console - info!("info! terminal; is_attached console = {}",*IS_CONSOLE); // isn't ready yet - } else { - log_init(&5); - info!("info! I'm not a terminal, is_attached console = {}",*IS_CONSOLE);trace!("trace");debug!("debug"); - } - let ret = main_impl(); - if let Err(ref e) = ret {log::error!("{e}\n");} - unsafe {FreeConsole();} -} From 13a0aa74e63905bf5e50abbdb8fe4ddd7b68edfc Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Mon, 22 Apr 2024 22:38:40 +0700 Subject: [PATCH 013/154] win-tray: update lib --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 441930de4..189cd509b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,14 @@ use std::str::FromStr; pub mod kanata; pub mod oskbd; pub mod tcp_server; +pub mod lib_main; #[cfg(feature = "gui")] pub mod m_gui; #[cfg(feature = "gui")] -use win_dbg_logger as log_win; +pub use m_gui::*; +#[cfg(feature = "gui")] +pub use win_dbg_logger as log_win; +pub use win_dbg_logger::WINDBG_LOGGER; pub use kanata::*; pub use tcp_server::TcpServer; From 9356724751f4a057125dca6d04467e0c5a94b4b1 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:36:45 +0700 Subject: [PATCH 014/154] win-tray: update lib main --- src/lib_main.rs | 65 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/src/lib_main.rs b/src/lib_main.rs index 90cf78721..39cc7d2fe 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -1,7 +1,9 @@ use anyhow::{bail, Result}; use clap::Parser; +#[cfg(all(target_os = "windows", feature = "gui"))] +use clap::{CommandFactory,error::ErrorKind}; use kanata_parser::cfg; -use kanata_state_machine::*; +use crate::*; use log::info; use simplelog::{format_description, *}; use std::path::PathBuf; @@ -96,7 +98,29 @@ kanata.kbd in the current working directory and /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { + #[cfg(all(not(target_os = "windows"), not(feature = "gui")))] let args = Args::parse(); + #[cfg(all( target_os = "windows", feature = "gui" ))] + let args = match Args::try_parse() { + Ok (args ) => args, + Err(e) => { + if *IS_TERM { // init loggers without config so '-help' "error" or real ones can be printed + let mut log_cfg = ConfigBuilder::new(); + CombinedLogger::init(vec![TermLogger::new(LevelFilter::Debug,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi,), + log_win::windbg_simple_combo(LevelFilter::Debug),]).expect("logger can init"); + } else {log_win::init();log::set_max_level(LevelFilter::Debug);} // doesn't panic + match e.kind() { + ErrorKind::DisplayHelp => { + let mut cmd = lib_main::Args::command(); + let help = cmd.render_help(); + info!("{help}"); + log::set_max_level(LevelFilter::Off); + return Err(anyhow!("")) + }, + _ => return Err(e.into()), + } + } + }; #[cfg(target_os = "macos")] if args.list { @@ -117,13 +141,14 @@ fn cli_init() -> Result { eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; log_cfg.set_time_format_rfc3339(); - CombinedLogger::init(vec![TermLogger::new( - log_lvl, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - )]) - .expect("logger can init"); + #[cfg(all(not(target_os = "windows"), not(feature = "gui")))] + CombinedLogger::init(vec![TermLogger::new(log_lvl,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi, + )]).expect("logger can init"); + #[cfg(all( target_os = "windows", feature = "gui" ))] + if *IS_TERM { + CombinedLogger::init(vec![TermLogger::new(log_lvl,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi,), + log_win::windbg_simple_combo(log_lvl),]).expect("logger can init"); + } else {CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl),]).expect("logger can init");} log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] log::info!("using LLHOOK+SendInput for keyboard IO"); @@ -174,6 +199,9 @@ fn main_impl() -> Result<()> { let args = cli_init()?; let kanata_arc = Kanata::new_arc(&args)?; + #[cfg(all( target_os = "windows", feature = "gui" ))] + if CFG.set(kanata_arc.clone()).is_err() {warn!("Someone else set our ‘CFG’");}; // store a clone of cfg so that we can ask it to reset itself + if !args.nodelay { info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); std::thread::sleep(std::time::Duration::from_secs(2)); @@ -219,7 +247,7 @@ fn main_impl() -> Result<()> { Ok(()) } -pub fn main_cli() -> Result<()> { +pub fn lib_main_cli() -> Result<()> { let ret = main_impl(); if let Err(ref e) = ret { log::error!("{e}\n"); @@ -228,3 +256,22 @@ pub fn main_cli() -> Result<()> { let _ = std::io::stdin().read_line(&mut String::new()); ret } +#[cfg(all( target_os = "windows", feature = "gui" ))] +use parking_lot::Mutex; +#[cfg(all( target_os = "windows", feature = "gui" ))] +use std::sync::{Arc, OnceLock}; +#[cfg(all( target_os = "windows", feature = "gui" ))] +pub static CFG: OnceLock>> = OnceLock::new(); + +#[cfg(all( target_os = "windows", feature = "gui" ))] +pub fn lib_main_gui() { + let _attach_console = *IS_CONSOLE; + let ret = main_impl(); + if let Err(ref e) = ret {log::error!("{e}\n");} + // if *IS_TERM { + // eprintln!("\nPress enter to exit"); + // let _ = std::io::stdin().read_line(&mut String::new()); // TODO: panics on Err(TryRecvError::Disconnected) @ Win/llhook, move to llhook OR coordinate with exit(&self) {nwg::stop_thread_dispatch();}? OR just ignore, why do we need this at all? + // } + + unsafe {FreeConsole();} +} From 856997529888688215310e534f259ae3c2ac2fc6 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:40:53 +0700 Subject: [PATCH 015/154] win-tray: rename to gui_win for Win --- src/kanata/windows/llhook.rs | 4 ++-- src/lib.rs | 9 +++++---- src/{m_gui.rs => m_gui_win.rs} | 0 3 files changed, 7 insertions(+), 6 deletions(-) rename src/{m_gui.rs => m_gui_win.rs} (100%) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 859a3d05b..a008e16e7 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -7,7 +7,7 @@ use std::time; use super::PRESSED_KEYS; use crate::kanata::*; #[cfg(feature = "gui")] -use crate::m_gui; +use crate::m_gui_win::*; #[cfg(feature = "gui")] extern crate native_windows_gui as nwg; #[cfg(feature = "gui")] @@ -77,7 +77,7 @@ impl Kanata { }); #[cfg(feature = "gui")] - let _ui = m_gui::SystemTray::build_ui(Default::default()).expect("Failed to build UI"); + let _ui = SystemTray::build_ui(Default::default()).expect("Failed to build UI"); // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel diff --git a/src/lib.rs b/src/lib.rs index 189cd509b..66944c65e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,12 +7,13 @@ pub mod kanata; pub mod oskbd; pub mod tcp_server; pub mod lib_main; -#[cfg(feature = "gui")] -pub mod m_gui; -#[cfg(feature = "gui")] -pub use m_gui::*; +#[cfg(all(target_os = "windows", feature = "gui"))] +pub mod m_gui_win; +#[cfg(all(target_os = "windows", feature = "gui"))] +pub use m_gui_win::*; #[cfg(feature = "gui")] pub use win_dbg_logger as log_win; +#[cfg(feature = "gui")] pub use win_dbg_logger::WINDBG_LOGGER; pub use kanata::*; diff --git a/src/m_gui.rs b/src/m_gui_win.rs similarity index 100% rename from src/m_gui.rs rename to src/m_gui_win.rs From b3cd5a41040ac96693bc00eebd86a58be83e9765 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Wed, 24 Apr 2024 16:53:45 +0700 Subject: [PATCH 016/154] win-tray: handle exit sequence and stop dispatch to hide the tray icon --- src/kanata/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index bba8d8e41..791ef5c77 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1935,14 +1935,16 @@ fn check_for_exit(event: &KeyEvent) { } const EXIT_MSG: &str = "pressed LControl+Space+Escape, exiting"; if IS_ESC_PRESSED.load(SeqCst) && IS_SPC_PRESSED.load(SeqCst) && IS_LCL_PRESSED.load(SeqCst) { - #[cfg(not(target_os = "linux"))] + log::info!("{EXIT_MSG}"); + #[cfg(all(target_os = "windows", feature = "gui"))]{ + native_windows_gui::stop_thread_dispatch(); + } + #[cfg(all(not(target_os = "linux"),not(target_os = "windows"),not(feature = "gui")))] { - log::info!("{EXIT_MSG}"); panic!("{EXIT_MSG}"); } #[cfg(target_os = "linux")] { - log::info!("{EXIT_MSG}"); signal_hook::low_level::raise(signal_hook::consts::SIGTERM).expect("raise signal"); } } From 3deba08db6b9d3ea960644ae5a8bc8857215847d Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 23 Apr 2024 15:52:50 +0700 Subject: [PATCH 017/154] win-tray: add dpi awareness to the manifest --- build.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.rs b/build.rs index 93ec08ca8..39bfd0312 100644 --- a/build.rs +++ b/build.rs @@ -103,6 +103,12 @@ mod windows { + + + true + PerMonitorV2 + + From 79fa0ec5cfc8d9a6b413a63176e243b8ac46ee95 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 23 Apr 2024 16:19:18 +0700 Subject: [PATCH 018/154] gui-tray: fix manifest format --- build.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/build.rs b/build.rs index 39bfd0312..42e5d290e 100644 --- a/build.rs +++ b/build.rs @@ -96,23 +96,19 @@ mod windows { let manifest_str = formatdoc!( r#" - - - - - - - - - + + + + + + true PerMonitorV2 - - - - - + + + + "#, version From f2873403cb9dd027668a5ba80768b26de97bfdf5 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 23 Apr 2024 22:14:24 +0700 Subject: [PATCH 019/154] win-tray: update manifest --- build.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/build.rs b/build.rs index 42e5d290e..825ae91b1 100644 --- a/build.rs +++ b/build.rs @@ -96,20 +96,22 @@ mod windows { let manifest_str = formatdoc!( r#" - - - - - - - true - PerMonitorV2 - - - - - + + + + + + + + true + PerMonitorV2 + + + + + + "#, version ); From 40f8370c2c9bd403130c73a8404aa8ec46b1478f Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:49:12 +0700 Subject: [PATCH 020/154] win-tray: attempt to fix some manifest signing issues, replace EnableUIAccess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1→v2 and newser library --- EnableUIAccess/EnableUIAccess.ahk | 346 -------------------- EnableUIAccess/EnableUIAccess_launch.ahk | 134 ++++++++ EnableUIAccess/Lib/Cert.ahk | 384 ----------------------- EnableUIAccess/Lib/Crypt.ahk | 161 ---------- EnableUIAccess/Lib/EnableUIAccess.ahk | 272 ++++++++++++++++ EnableUIAccess/Lib/SignFile.ahk | 52 --- EnableUIAccess/Lib/SystemTime.ahk | 101 ------ 7 files changed, 406 insertions(+), 1044 deletions(-) delete mode 100644 EnableUIAccess/EnableUIAccess.ahk create mode 100644 EnableUIAccess/EnableUIAccess_launch.ahk delete mode 100644 EnableUIAccess/Lib/Cert.ahk delete mode 100644 EnableUIAccess/Lib/Crypt.ahk create mode 100644 EnableUIAccess/Lib/EnableUIAccess.ahk delete mode 100644 EnableUIAccess/Lib/SignFile.ahk delete mode 100644 EnableUIAccess/Lib/SystemTime.ahk diff --git a/EnableUIAccess/EnableUIAccess.ahk b/EnableUIAccess/EnableUIAccess.ahk deleted file mode 100644 index 5d75f0d4a..000000000 --- a/EnableUIAccess/EnableUIAccess.ahk +++ /dev/null @@ -1,346 +0,0 @@ -/* - EnableUIAccess.ahk v1.01 by Lexikos - - USE AT YOUR OWN RISK - - Enables the uiAccess flag in an application's embedded manifest - and signs the file with a self-signed digital certificate. If the - file is in a trusted location (A_ProgramFiles or A_WinDir), this - allows the application to bypass UIPI (User Interface Privilege - Isolation, a part of User Account Control in Vista/7). It also - enables the journal playback hook (SendPlay). - - Command line params (mutually exclusive): - SkipWarning - don't display the initial warning - "" "" - attempt to run silently using the given file(s) - - This script and the provided Lib files may be used, modified, - copied, etc. without restriction. - -*/ - -#NoEnv - -#Include -#Include -#Include -#Include - -; Command line args: -in_file = %1% -out_file = %2% - -if (in_file = "") -MsgBox 49,, -(Join -This script enables the selected AutoHotkey.exe to bypass restrictions - imposed by UIPI, a component of UAC in Windows Vista and 7. To do this - it modifies an attribute in the file's embedded manifest and signs the - file using a self-signed digital certificate, which is then installed - in the local machine's Trusted Root Certification Authorities store.`n -`n -THE RESULTING EXECUTABLE MAY BE UNUSABLE ON ANY SYSTEM WHERE THIS - CERTIFICATE IS NOT INSTALLED.`n -`n -Continue at your own risk. -) -ifMsgBox Cancel - ExitApp - -if !A_IsAdmin -{ - if (in_file = "") - in_file := "SkipWarning" - cmd = "%A_ScriptFullPath%" - if !A_IsCompiled - { ; Use A_AhkPath in case the "runas" verb isn't registered for ahk files. - cmd = "%A_AhkPath%" %cmd% - } - Run *RunAs %cmd% "%in_file%" "%out_file%",, UseErrorLevel - ExitApp -} - -if (in_file = "" || in_file = "SkipWarning") -{ - ; Find AutoHotkey installation. - RegRead InstallDir, HKEY_LOCAL_MACHINE, SOFTWARE\AutoHotkey, InstallDir - if ErrorLevel && A_PtrSize=8 - RegRead InstallDir, HKLM, SOFTWARE\Wow6432Node\AutoHotkey, InstallDir - - ; Let user confirm or select file(s). - FileSelectFile in_file, 1, %InstallDir%\AutoHotkey.exe - , Select Source File, Executable Files (*.exe) - if ErrorLevel - ExitApp - FileSelectFile out_file, S16, %in_file% - , Select Destination File, Executable Files (*.exe) - if ErrorLevel - ExitApp - user_specified_files := true -} - -; Convert short paths to long paths. -Loop %in_file%, 0 - in_file := A_LoopFileLongPath -if (out_file = "") ; i.e. only one file was given via command line. - out_file := in_file -else - Loop %out_file%, 0 - out_file := A_LoopFileLongPath - -if Crypt.IsSigned(in_file) -{ - MsgBox 48,, Input file is already signed. The script will now exit. - ExitApp -} - -if user_specified_files && !IsTrustedLocation(out_file) -{ - MsgBox 49,, - (LTrim Join`s - The path you have selected is not a trusted location. If you choose - to continue, the uiAccess attribute will be set but will not have - any effect until the file is moved to a trusted location. Trusted - locations include \Program Files\ and \Windows\System32\. - ) - ifMsgBox Cancel - ExitApp -} - -if (in_file = out_file) -{ - ; The following should typically work even if the file is in use: - bak_file := in_file "~" A_Now ".bak" - FileMove %in_file%, %bak_file%, 1 - if ErrorLevel - Fail("Failed to rename selected file.") - in_file := bak_file -} -FileCopy %in_file%, %out_file%, 1 -if ErrorLevel - Fail("Failed to copy file to destination.") - - -; Set the uiAccess attribute in the file's manifest to "true". -if !EXE_uiAccess_set(out_file, true) - Fail("Failed to set uiAccess attribute in manifest.") - - -; Open the current user's "Personal" certificate store. -my := Cert.OpenStore(Cert.STORE_PROV_SYSTEM, 0, Cert.SYSTEM_STORE_CURRENT_USER, "wstr", "My") -if !my - Warn("Failed to open 'Personal' certificate store.") - -; Locate "AutoHotkey" certificate created by a previous run of this script. -ahk_cert := my.FindCertificates(0, Cert.FIND_SUBJECT_STR, "wstr", "AutoHotkey")[1] - -if !ahk_cert -{ - ; Create key container. - cr := Crypt.AcquireContext("AutoHotkey", 0, Crypt.PROV_RSA_FULL, Crypt.NEWKEYSET) - if !cr - Fail("Failed to create 'AutoHotkey' key container.") - - ; Generate key for certificate. - key := cr.GenerateKey(Crypt.AT_SIGNATURE, 1024, Crypt.EXPORTABLE) - - ; Create simple certificate name. - cn := new Cert.Name({CommonName: "AutoHotkey"}) - - ; Set end time to 10 years from now. - end_time := SystemTime.Now() - end_time.Year += 10 - - ; Create certificate using the parameters created above. - ahk_cert := cr.CreateSelfSignCertificate(cn, 0, end_time) - if !ahk_cert - Fail("Failed to create 'AutoHotkey' certificate.") - - ; Add certificate to current user's "Personal" store so they won't - ; need to create it again if they need to update the executable. - if !(my.AddCertificate(ahk_cert, Cert.STORE_ADD_NEW)) - Warn("Failed to add certificate to 'Personal' store.") - ; Proceed even if above failed, since it probably doesn't matter. - - ; Attempt to install certificate in trusted store. - root := Cert.OpenStore(Cert.STORE_PROV_SYSTEM, 0, Cert.SYSTEM_STORE_LOCAL_MACHINE, "wstr", "Root") - if !(root && root.AddCertificate(ahk_cert, Cert.STORE_ADD_USE_EXISTING)) - { - if (%True% != "Silent") - MsgBox 49,, - (LTrim Join`s - Failed to install certificate. If you continue, the executable - may become unusable until the certificate is manually installed. - This can typically be done via Digital Signatures tab on the - file's Properties dialog. - ) - ifMsgBox Cancel - ExitApp - } - - key.Dispose() - cr.Dispose() -} - -; Sign the file. -if !SignFile(out_file, ahk_cert, "AutoHotkey") - Fail("Failed to sign file.") - - -; In interactive mode, if not overwriting the original file, offer -; to create an additional context menu item for AHK files. -if (user_specified_files && in_file != out_file) -{ - RegRead uiAccessVerb, HKCR, AutoHotkeyScript\Shell\uiAccess\Command - if ErrorLevel - { - MsgBox 3,, Register "Run Script with UI Access" context menu item? - ifMsgBox Yes - { - RegWrite REG_SZ, HKCR, AutoHotkeyScript\Shell\uiAccess - ,, Run with UI Access - RegWrite REG_SZ, HKCR, AutoHotkeyScript\Shell\uiAccess\Command - ,, "%out_file%" "`%1" `%* - } - ifMsgBox Cancel - ExitApp - } -} - - -; IsTrustedLocation -; Returns true if path is a valid location for uiAccess="true". -IsTrustedLocation(path) -{ - ; http://msdn.microsoft.com/en-us/library/bb756929 - ; MSDN: "\Program Files\ and \windows\system32\ are currently the - ; two allowable protected locations." - ; However, \Program Files (x86)\ also appears to be allowed. - if InStr(path, A_ProgramFiles "\") = 1 - return true - if InStr(path, A_WinDir "\System32\") = 1 - return true - - ; On 64-bit systems, if this script is 32-bit, A_ProgramFiles is - ; %ProgramFiles(x86)%, otherwise it is %ProgramW6432%. So check - ; the opposite "Program Files" folder: - EnvGet other, % A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432" - if (other != "" && InStr(path, other "\") = 1) - return true - - return false -} - - -; EXE_uiAccess_set -; Sets the uiAccess attribute in an executable file's manifest. -; file - Path of file. -; value - New value; must be boolean (0 or 1). -EXE_uiAccess_set(file, value) -{ - ; Load manifest from EXE file. - xml := ComObjCreate("Msxml2.DOMDocument") - xml.async := false - xml.setProperty("SelectionLanguage", "XPath") - xml.setProperty("SelectionNamespaces" - , "xmlns:v1='urn:schemas-microsoft-com:asm.v1' " - . "xmlns:v3='urn:schemas-microsoft-com:asm.v3'") - if !xml.load("res://" file "/#24/#1") - { - ; This will happen if the file doesn't exist or can't be opened, - ; or if it doesn't have an embedded manifest. - ErrorLevel := "load" - return false - } - - ; Check if any change is necessary. If the uiAccess attribute is - ; not present, it is effectively "false": - node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/security" - . "/requestedPrivileges/requestedExecutionLevel") - if ((node && node.getAttribute("uiAccess") = "true") = value) - { - ErrorLevel := "already set" - return true - } - - ; The follow "IF" section should be unnecessary for AutoHotkey_L. - if !node - { - ; Get assembly node, which should always exist. - if !last := xml.selectSingleNode("/v1:assembly") - { - ErrorLevel := "invalid manifest" - return 0 - } - for _, name in ["trustInfo", "security", "requestedPrivileges" - , "requestedExecutionLevel"] - { - if !(node := last.selectSingleNode("*[local-name()='" name "']")) - { - static NODE_ELEMENT := 1 - node := xml.createNode(NODE_ELEMENT, name - , "urn:schemas-microsoft-com:asm.v3") - last.appendChild(node) - } - last := node - } - ; Since the requestedExecutionLevel node didn't exist before, - ; we must have just created it. Although this attribute *might* - ; not actually be required, it seems best to set it: - node.setAttribute("level", "asInvoker") - } - - ; Set the uiAccess attribute! - node.setAttribute("uiAccess", value ? "true" : "false") - - ; Retrieve XML text. - xml := RTrim(xml.xml, "`r`n") - - ; Convert to UTF-8. - VarSetCapacity(data, data_size := StrPut(xml, "utf-8") - 1) - StrPut(xml, &data, "utf-8") - - ; - ; Replace manifest resource. - ; - - hupd := DllCall("BeginUpdateResource", "str", file, "int", false) - if !hupd - { - ErrorLevel := "BeginUpdateResource" - return false - } - - ; Res type RT_MANIFEST (24), resource ID 1, language English (US) - r := DllCall("UpdateResource", "ptr", hupd, "ptr", 24, "ptr", 1 - , "ushort", 1033, "ptr", &data, "uint", data_size) - - if !DllCall("EndUpdateResource", "ptr", hupd, "int", !r) - { - ErrorLevel := "EndUpdateResource" - return false - } - if !r ; i.e. above succeeded only in discarding the failed changes. - { - ErrorLevel := "UpdateResource" - return false - } - ; Success! - ErrorLevel := 0 - return true -} - - -Fail(msg) -{ - if (%True% != "Silent") - MsgBox 16,, %msg%`n`nErrorLevel: %ErrorLevel%`nA_LastError: %A_LastError% - ExitApp -} - -Warn(msg) -{ - msg .= " (Err " ErrorLevel "; " A_LastError ")`n" - OutputDebug %msg% - FileAppend %msg%, * -} \ No newline at end of file diff --git a/EnableUIAccess/EnableUIAccess_launch.ahk b/EnableUIAccess/EnableUIAccess_launch.ahk new file mode 100644 index 000000000..891193edd --- /dev/null +++ b/EnableUIAccess/EnableUIAccess_launch.ahk @@ -0,0 +1,134 @@ +#requires AutoHotkey v2.0 +#SingleInstance Off ; Needed for elevation with *runas. +/* v2 based on EnableUIAccess.ahk v1.01 by Lexikos USE AT YOUR OWN RISK + Enables the uiAccess flag in an application's embedded manifest and signs the file with a self-signed digital certificate. If the file is in a trusted location (A_ProgramFiles or A_WinDir), this allows the application to bypass UIPI (User Interface Privilege Isolation, a part of User Account Control in Vista/7). It also enables the journal playback hook (SendPlay). + Command line params (mutually exclusive): + SkipWarning - don't display the initial warning + "" "" - attempt to run silently using the given file(s) + This script and the provided Lib files may be used, modified, copied, etc. without restriction. +*/ +#include + +in_file := (A_Args.Has(1))?A_Args[1]:'' ; Command line args +out_file := (A_Args.Has(2))?A_Args[2]:'' + +if (in_file = ""){ + msgResult := MsgBox("Enable the selected EXE to bypass UAC-UIPI security restrictions imposed by modifying 'UIAccess' attribute in the file's embedded manifest and signing the file using a self-signed digital certificate, which is then installed in the local machine's Trusted Root Certification Authorities store.`n`nThe resulting EXE is unusable on a system without this certificate installed!`n`nContinue at your own risk", "", 49) + if (msgResult = "Cancel"){ + ExitApp() + } +} + +if !A_IsAdmin { + if (in_file = "") { + in_file := "SkipWarning" + } + cmd := "`"" . A_ScriptFullPath . "`"" + if !A_IsCompiled { ; Use A_AhkPath in case the "runas" verb isn't registered for ahk files. + cmd := "`"" . A_AhkPath . "`" " . cmd + } + Try Run("*RunAs " cmd " `"" in_file "`" `"" out_file "`"", , "", ) + ExitApp() +} +global user_specified_files := false +if (in_file = "" || in_file = "SkipWarning") { ; Find AutoHotkey installation. + InstallDir := RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\AutoHotkey", "InstallDir") + if A_LastError && A_PtrSize=8 { + InstallDir := RegRead("HKLM\SOFTWARE\Wow6432Node\AutoHotkey", "InstallDir") + } + ; Let user confirm or select file(s). + in_file := FileSelect(1, InstallDir "\AutoHotkey.exe", "Select Source File", "Executable Files (*.exe)") + if A_LastError { + ExitApp() + } + out_file := FileSelect("S16", in_file, "Select Destination File", "Executable Files (*.exe)") + if A_LastError { + ExitApp() + } + user_specified_files := true +} + +Loop in_file { ; Convert short paths to long paths + in_file := A_LoopFileFullPath +} +if (out_file = "") { ; i.e. only one file was given via command line + out_file := in_file +} else { + Loop out_file { + out_file := A_LoopFileFullPath + } +} +if Crypt.IsSigned(in_file) { + msgResult := MsgBox("Input file is already signed. The script will now exit" in_file,"", 48) + ExitApp() +} + +if user_specified_files && !IsTrustedLocation(out_file) { + msgResult := MsgBox("Target path is not a trusted location (Program Files or Windows\System32), so 'uiAccess' will have no effect until the file is moved there","", 49) + if (msgResult = "Cancel") { + ExitApp() + } +} + +if (in_file = out_file) { ; The following should typically work even if the file is in use + bak_file := in_file "~" A_Now ".bak" + FileMove(in_file, bak_file, 1) + if A_LastError { + Fail("Failed to rename selected file.") + } + in_file := bak_file +} +Try { + FileCopy(in_file, out_file, 1) +} Catch as Err { + throw OSError(Err) +} +if A_LastError { + Fail("Failed to copy file to destination.") +} + +if !EnableUIAccess(out_file) { ; Set the uiAccess attribute in the file's manifest + Fail("Failed to set uiAccess attribute in manifest") +} + + +if (user_specified_files && in_file != out_file) { ; in interactive mode, if not overwriting the original file, offer to create an additional context menu item for AHK files + uiAccessVerb := RegRead("HKCR\AutoHotkeyScript\Shell\uiAccess\Command") + if A_LastError { + msgResult := MsgBox("Register `"Run Script with UI Access`" context menu item?", "", 3) + if (msgResult = "Yes") { + RegWrite("Run with UI Access", "REG_SZ", "HKCR\AutoHotkeyScript\Shell\uiAccess") + RegWrite("`"" out_file "`" `"`%1`" `%*", "REG_SZ", "HKCR\AutoHotkeyScript\Shell\uiAccess\Command") + } + if (msgResult = "Cancel") + ExitApp() + } +} + +IsTrustedLocation(path) { ; IsTrustedLocation →true if path is a valid location for uiAccess="true" + ; http://msdn.microsoft.com/en-us/library/bb756929 "\Program Files\ and \windows\system32\ are currently 2 allowable protected locations." However, \Program Files (x86)\ also appears to be allowed + if InStr(path, A_ProgramFiles "\") = 1 { + return true + } + if InStr(path, A_WinDir "\System32\") = 1 { + return true + } + other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432") ; On 64-bit systems, if this script is 32-bit, A_ProgramFiles is %ProgramFiles(x86)%, otherwise it is %ProgramW6432%. So check the opposite "Program Files" folder: + if (other != "" && InStr(path, other "\") = 1) { + return true + } + return false +} + +Fail(msg) { + ; if (%True% != "Silent") { ;??? + MsgBox(msg "`nA_LastError: " A_LastError, "", 16) + ; } + ExitApp() +} + +Warn(msg) { + msg .= " (Err " A_LastError ")`n" + OutputDebug(msg) + FileAppend(msg, "*") +} diff --git a/EnableUIAccess/Lib/Cert.ahk b/EnableUIAccess/Lib/Cert.ahk deleted file mode 100644 index 7acba6d6a..000000000 --- a/EnableUIAccess/Lib/Cert.ahk +++ /dev/null @@ -1,384 +0,0 @@ -class Cert -{ - ; Encoding Types -static X509_ASN_ENCODING := 0x00000001 - , PKCS_7_ASN_ENCODING := 0x00010000 - - ; Certificate Information Flags (CERT_INFO_*) -static INFO_VERSION_FLAG := 1 - , INFO_SERIAL_NUMBER_FLAG := 2 - , INFO_SIGNATURE_ALGORITHM_FLAG := 3 - , INFO_ISSUER_FLAG := 4 - , INFO_NOT_BEFORE_FLAG := 5 - , INFO_NOT_AFTER_FLAG := 6 - , INFO_SUBJECT_FLAG := 7 - , INFO_SUBJECT_PUBLIC_KEY_INFO_FLAG := 8 - , INFO_ISSUER_UNIQUE_ID_FLAG := 9 - , INFO_SUBJECT_UNIQUE_ID_FLAG := 10 - , INFO_EXTENSION_FLAG := 11 - - ; Certificate Comparison Functions (CERT_COMPARE_*) -static COMPARE_MASK := 0xFFFF - , COMPARE_SHIFT := (_ := 16) - , COMPARE_ANY := 0 - , COMPARE_SHA1_HASH := 1 - , COMPARE_NAME := 2 - , COMPARE_ATTR := 3 - , COMPARE_MD5_HASH := 4 - , COMPARE_PROPERTY := 5 - , COMPARE_PUBLIC_KEY := 6 - , COMPARE_HASH := Cert.COMPARE_SHA1_HASH - , COMPARE_NAME_STR_A := 7 - , COMPARE_NAME_STR_W := 8 - , COMPARE_KEY_SPEC := 9 - , COMPARE_ENHKEY_USAGE := 10 - , COMPARE_CTL_USAGE := Cert.COMPARE_ENHKEY_USAGE - , COMPARE_SUBJECT_CERT := 11 - , COMPARE_ISSUER_OF := 12 - , COMPARE_EXISTING := 13 - , COMPARE_SIGNATURE_HASH := 14 - , COMPARE_KEY_IDENTIFIER := 15 - , COMPARE_CERT_ID := 16 - , COMPARE_CROSS_CERT_DIST_POINTS := 17 - , COMPARE_PUBKEY_MD5_HASH := 18 - , COMPARE_SUBJECT_INFO_ACCESS := 19 - - ; dwFindType Flags (CERT_FIND_*) -static FIND_ANY := Cert.COMPARE_ANY << _ - , FIND_SHA1_HASH := Cert.COMPARE_SHA1_HASH << _ - , FIND_MD5_HASH := Cert.COMPARE_MD5_HASH << _ - , FIND_SIGNATURE_HASH := Cert.COMPARE_SIGNATURE_HASH << _ - , FIND_KEY_IDENTIFIER := Cert.COMPARE_KEY_IDENTIFIER << _ - , FIND_HASH := Cert.FIND_SHA1_HASH - , FIND_PROPERTY := Cert.COMPARE_PROPERTY << _ - , FIND_PUBLIC_KEY := Cert.COMPARE_PUBLIC_KEY << _ - , FIND_SUBJECT_NAME := (Cert.COMPARE_NAME << _) | Cert.INFO_SUBJECT_FLAG - , FIND_SUBJECT_ATTR := (Cert.COMPARE_ATTR << _) | Cert.INFO_SUBJECT_FLAG - , FIND_ISSUER_NAME := (Cert.COMPARE_NAME << _) | Cert.INFO_ISSUER_FLAG - , FIND_ISSUER_ATTR := (Cert.COMPARE_ATTR << _) | Cert.INFO_ISSUER_FLAG - , FIND_SUBJECT_STR := (Cert.COMPARE_NAME_STR_W << _) | Cert.INFO_SUBJECT_FLAG - , FIND_ISSUER_STR := (Cert.COMPARE_NAME_STR_W << _) | Cert.INFO_ISSUER_FLAG - , FIND_KEY_SPEC := Cert.COMPARE_KEY_SPEC << _ - , FIND_ENHKEY_USAGE := Cert.COMPARE_ENHKEY_USAGE << _ - , FIND_CTL_USAGE := Cert.FIND_ENHKEY_USAGE - , FIND_SUBJECT_CERT := Cert.COMPARE_SUBJECT_CERT << _ - , FIND_ISSUER_OF := Cert.COMPARE_ISSUER_OF << _ - , FIND_EXISTING := Cert.COMPARE_EXISTING << _ - , FIND_CERT_ID := Cert.COMPARE_CERT_ID << _ - , FIND_CROSS_CERT_DIST_POINTS := Cert.COMPARE_CROSS_CERT_DIST_POINTS << _ - , FIND_PUBKEY_MD5_HASH := Cert.COMPARE_PUBKEY_MD5_HASH << _ - , FIND_SUBJECT_INFO_ACCESS := Cert.COMPARE_SUBJECT_INFO_ACCESS << _ - - ; Certificate Store Provider Types (CERT_STORE_PROV_*) -static STORE_PROV_MSG := 1 - , STORE_PROV_MEMORY := 2 - , STORE_PROV_FILE := 3 - , STORE_PROV_REG := 4 - , STORE_PROV_PKCS7 := 5 - , STORE_PROV_SERIALIZED := 6 - , STORE_PROV_FILENAME_A := 7 - , STORE_PROV_FILENAME_W := 8 - , STORE_PROV_FILENAME := Cert.STORE_PROV_FILENAME_W - , STORE_PROV_SYSTEM_A := 9 - , STORE_PROV_SYSTEM_W := 10 - , STORE_PROV_SYSTEM := Cert.STORE_PROV_SYSTEM_W - , STORE_PROV_COLLECTION := 11 - , STORE_PROV_SYSTEM_REGISTRY_A := 12 - , STORE_PROV_SYSTEM_REGISTRY_W := 13 - , STORE_PROV_SYSTEM_REGISTRY := Cert.STORE_PROV_SYSTEM_REGISTRY_W - , STORE_PROV_PHYSICAL_W := 14 - , STORE_PROV_PHYSICAL := Cert.STORE_PROV_PHYSICAL_W - , STORE_PROV_LDAP_W := 16 - , STORE_PROV_LDAP := Cert.STORE_PROV_LDAP_W - , STORE_PROV_PKCS12 := 17 - - ; Certificate Store open/property flags (low-word; CERT_STORE_*) -static STORE_NO_CRYPT_RELEASE_FLAG := 0x0001 - , STORE_SET_LOCALIZED_NAME_FLAG := 0x0002 - , STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG := 0x0004 - , STORE_DELETE_FLAG := 0x0010 - , STORE_UNSAFE_PHYSICAL_FLAG := 0x0020 - , STORE_SHARE_STORE_FLAG := 0x0040 - , STORE_SHARE_CONTEXT_FLAG := 0x0080 - , STORE_MANIFOLD_FLAG := 0x0100 - , STORE_ENUM_ARCHIVED_FLAG := 0x0200 - , STORE_UPDATE_KEYID_FLAG := 0x0400 - , STORE_BACKUP_RESTORE_FLAG := 0x0800 - , STORE_READONLY_FLAG := 0x8000 - , STORE_OPEN_EXISTING_FLAG := 0x4000 - , STORE_CREATE_NEW_FLAG := 0x2000 - , STORE_MAXIMUM_ALLOWED_FLAG := 0x1000 - - ; Certificate System Store Flag Values (high-word; CERT_SYSTEM_STORE_*) -static SYSTEM_STORE_MASK := 0xFFFF0000 - , SYSTEM_STORE_RELOCATE_FLAG := 0x80000000 - , SYSTEM_STORE_UNPROTECTED_FLAG := 0x40000000 - ; Location of the system store: - , SYSTEM_STORE_LOCATION_MASK := 0x00FF0000 - , SYSTEM_STORE_LOCATION_SHIFT := (_ := 16) - ; Registry: HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE - , SYSTEM_STORE_CURRENT_USER_ID := 1 - , SYSTEM_STORE_LOCAL_MACHINE_ID := 2 - ; Registry: HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Services - , SYSTEM_STORE_CURRENT_SERVICE_ID := 4 - , SYSTEM_STORE_SERVICES_ID := 5 - ; Registry: HKEY_USERS - , SYSTEM_STORE_USERS_ID := 6 - ; Registry: HKEY_CURRENT_USER\Software\Policies\Microsoft\SystemCertificates - , SYSTEM_STORE_CURRENT_USER_GROUP_POLICY_ID := 7 - ; Registry: HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates - , SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY_ID := 8 - ; Registry: HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates - , SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE_ID := 9 - , SYSTEM_STORE_CURRENT_USER := (Cert.SYSTEM_STORE_CURRENT_USER_ID << _) - , SYSTEM_STORE_LOCAL_MACHINE := (Cert.SYSTEM_STORE_LOCAL_MACHINE_ID << _) - , SYSTEM_STORE_CURRENT_SERVICE := (Cert.SYSTEM_STORE_CURRENT_SERVICE_ID << _) - , SYSTEM_STORE_SERVICES := (Cert.SYSTEM_STORE_SERVICES_ID << _) - , SYSTEM_STORE_USERS := (Cert.SYSTEM_STORE_USERS_ID << _) - , SYSTEM_STORE_CURRENT_USER_GROUP_POLICY := (Cert.SYSTEM_STORE_CURRENT_USER_GROUP_POLICY_ID << _) - , SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY := (Cert.SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY_ID << _) - , SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE := (Cert.SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE_ID << _) - - ; Certificate name types (CERT_NAME_*) -static NAME_EMAIL_TYPE := 1 - , NAME_RDN_TYPE := 2 - , NAME_ATTR_TYPE := 3 - , NAME_SIMPLE_DISPLAY_TYPE := 4 - , NAME_FRIENDLY_DISPLAY_TYPE := 5 - , NAME_DNS_TYPE := 6 - , NAME_URL_TYPE := 7 - , NAME_UPN_TYPE := 8 - ; Certificate name flags - , NAME_ISSUER_FLAG := 0x00000001 - , NAME_DISABLE_IE4_UTF8_FLAG := 0x00010000 - , NAME_STR_ENABLE_PUNYCODE_FLAG := 0x00200000 - - ; dwAddDisposition values (CERT_STORE_ADD_*) -static STORE_ADD_NEW := 1 - , STORE_ADD_USE_EXISTING := 2 - , STORE_ADD_REPLACE_EXISTING := 3 - , STORE_ADD_ALWAYS := 4 - , STORE_ADD_REPLACE_EXISTING_INHERIT_PROPERTIES := 5 - , STORE_ADD_NEWER := 6 - , STORE_ADD_NEWER_INHERIT_PROPERTIES := 7 - - ; - ; Static Methods - ; - - OpenStore(pStoreProvider, dwMsgAndCertEncodingType, dwFlags, ParamType="Ptr", Param=0) - { - hCertStore := DllCall("Crypt32\CertOpenStore" - , "ptr", pStoreProvider - , "uint", dwMsgAndCertEncodingType - , "ptr", 0 ; hCryptProv - , "uint", dwFlags - , ParamType, Param) - if hCertStore - hCertStore := new this.Store(hCertStore) - return hCertStore - } - - GetStoreNames(dwFlags) - { - static cb := RegisterCallback("Cert_GetStoreNames_Callback", "F") - global Cert - DllCall("Crypt32\CertEnumSystemStore", "uint", dwFlags - , "ptr", 0, "ptr", &(names := []), "ptr", cb) - return names - } - - - ; - ; Certificate Name - ; - class Name - { - __New(Props) - { - static Fields := { - (Join, - CommonName: "CN" - LocalityName: "L" - Organization: "O" - OrganizationalUnit: "OU" - Email: "E" - Country: "C" - State: "ST" - StreetAddress: "STREET" - Title: "T" - GivenName: "G" - Initials: "I" - Surname: "SN" - Doman: "DC" - )} - static CERT_X500_NAME_STR := 3, Q := """" ; For readability. - - if IsObject(Props) - { - ; Build name string from caller-supplied object. - name_string := "" - for field_name, field_code in Fields - { - if Props.HasKey(field_name) - { - if (name_string != "") - name_string .= ";" - name_string .= field_code "=" Q RegExReplace(Props[field_name], Q, Q Q) Q - } - } - } - else - name_string := Props - - Loop 2 - { - if A_Index=1 - { ; First iteration: retrieve required size. - pbEncoded := 0 - cbEncoded := 0 - } - else - { ; Second iteration: retrieve encoded name. - this.SetCapacity("data", cbEncoded) - pbEncoded := this.GetAddress("data") - } - global Cert - if !DllCall("Crypt32\CertStrToName" - , "uint", Cert.X509_ASN_ENCODING - , "str", name_string - , "uint", CERT_X500_NAME_STR - , "ptr", 0 ; Reserved - , "ptr", pbEncoded - , "uint*", cbEncoded - , "str*", ErrorString) - { - ErrorLevel := ErrorString - return false - } - } - this.SetCapacity("blob", A_PtrSize*2) ; CERT_NAME_BLOB - NumPut(pbEncoded, NumPut(cbEncoded, this.p := this.GetAddress("blob"))) - } - } - - - ; - ; Certificate Store - ; - class Store - { - FindCertificates(dwFindFlags, dwFindType, FindParamType="ptr", FindParam=0) - { - global Cert - hStore := this.h - , dwCertEncodingType := Cert.X509_ASN_ENCODING | Cert.PKCS_7_ASN_ENCODING - , ctx := new Cert.Context(0) - , certs := [] - while ctx.p := DllCall("Crypt32\CertFindCertificateInStore" - , "ptr", hStore - , "uint", dwCertEncodingType - , "uint", dwFindFlags - , "uint", dwFindType - , FindParamType, FindParam - , "ptr", ctx.p ; If non-NULL, this context is freed. - , "ptr") - { - ; Each certificate context must be duplicated since the next - ; call will free it. - certs.Insert(ctx.Duplicate()) - } - ctx.p := 0 ; Above freed it already. - return certs - } - - AddCertificate(Certificate, dwAddDisposition) - { - if !DllCall("Crypt32\CertAddCertificateContextToStore" - , "ptr", this.h - , "ptr", Certificate.p - , "uint", dwAddDisposition - , "ptr*", pStoreContext) - return 0 - global Cert - return pStoreContext ? new Cert.Context(pStoreContext) : 0 - } - - __New(handle) - { - this.h := handle - } - - __Delete() - { - if this.h && DllCall("Crypt32\CertCloseStore", "ptr", this.h, "uint", 0) - this.h := 0 - } - - static Dispose := Cert.Store.__Delete ; Alias - } - - - ; - ; Certificate Context - ; - class Context - { - __New(ptr) - { - this.p := ptr - } - - __Delete() - { - if this.p && DllCall("Crypt32\CertFreeCertificateContext", "ptr", this.p) - this.p := 0 - } - - ; CertGetNameString - ; http://msdn.microsoft.com/en-us/library/aa376086 - GetNameString(dwType, dwFlags=0, pvTypePara=0) - { - if !this.p - return - cc := DllCall("Crypt32\CertGetNameString", "ptr", this.p, "uint", dwType, "uint", dwFlags, "ptr", pvTypePara, "ptr", 0, "uint", 0) - if cc <= 1 ; i.e. empty string. - return - VarSetCapacity(name, cc*2) - DllCall("Crypt32\CertGetNameString", "ptr", this.p, "uint", dwType, "uint", dwFlags, "ptr", pvTypePara, "str", name, "uint", cc) - return name - } - - ; CertDuplicateCertificateContext - ; http://msdn.microsoft.com/en-us/library/aa376045 - Duplicate() - { - return this.p && (p := DllCall("Crypt32\CertDuplicateCertificateContext", "ptr", this.p)) - ? new this.base(p) : p - } - - static Dispose := Cert.Context.__Delete ; Alias - } - - - ; - ; Error Detection - ; - __Get(name) - { - ListLines - MsgBox 16,, Attempt to access invalid property Cert.%name%. - Pause - } -} - - -; -; Internal -; - -Cert_GetStoreNames_Callback(pvSystemStore, dwFlags, pStoreInfo, pvReserved, pvArg) -{ - Object(pvArg).Insert(StrGet(pvSystemStore, "utf-16")) - return true -} \ No newline at end of file diff --git a/EnableUIAccess/Lib/Crypt.ahk b/EnableUIAccess/Lib/Crypt.ahk deleted file mode 100644 index 6e6770b52..000000000 --- a/EnableUIAccess/Lib/Crypt.ahk +++ /dev/null @@ -1,161 +0,0 @@ -class Crypt -{ - ; Provider Types -static PROV_RSA_FULL := 1 - , PROV_RSA_SIG := 2 - , PROV_DSS := 3 - , PROV_FORTEZZA := 4 - , PROV_MS_EXCHANGE := 5 - , PROV_SSL := 6 - , PROV_STT_MER := 7 ; <= XP - , PROV_STT_ACQ := 8 ; <= XP - , PROV_STT_BRND := 9 ; <= XP - , PROV_STT_ROOT := 10 ; <= XP - , PROV_STT_ISS := 11 ; <= XP - , PROV_RSA_SCHANNEL := 12 - , PROV_DSS_DH := 13 - , PROV_EC_ECDSA_SIG := 14 - , PROV_EC_ECNRA_SIG := 15 - , PROV_EC_ECDSA_FULL := 16 - , PROV_EC_ECNRA_FULL := 17 - , PROV_DH_SCHANNEL := 18 - , PROV_SPYRUS_LYNKS := 20 - , PROV_RNG := 21 - , PROV_INTEL_SEC := 22 - , PROV_REPLACE_OWF := 23 ; >= XP - , PROV_RSA_AES := 24 ; >= XP - - ; CryptAcquireContext - dwFlags - ; http://msdn.microsoft.com/en-us/library/aa379886 -static VERIFYCONTEXT := 0xF0000000 - , NEWKEYSET := 0x00000008 - , DELETEKEYSET := 0x00000010 - , MACHINE_KEYSET := 0x00000020 - , SILENT := 0x00000040 - , CRYPT_DEFAULT_CONTAINER_OPTIONAL := 0x00000080 - - ; CryptGenKey - dwFlag - ; http://msdn.microsoft.com/en-us/library/aa379941 -static EXPORTABLE := 0x00000001 - , USER_PROTECTED := 0x00000002 - , CREATE_SALT := 0x00000004 - , UPDATE_KEY := 0x00000008 - , NO_SALT := 0x00000010 - , PREGEN := 0x00000040 - , ARCHIVABLE := 0x00004000 - , FORCE_KEY_PROTECTION_HIGH := 0x00008000 - - ; Key Types -static AT_KEYEXCHANGE := 1 - , AT_SIGNATURE := 2 - - ; - ; METHODS - ; - - AcquireContext(Container, Provider, dwProvType, dwFlags) - { - if DllCall("Advapi32\CryptAcquireContext" - , "ptr*", hProv - , "ptr", Container ? &Container : 0 - , "ptr", Provider ? &Provider : 0 - , "uint", dwProvType - , "uint", dwFlags) - { - if (dwFlags & this.DELETEKEYSET) - ; Success, but hProv is invalid in this case. - return 1 - ; Wrap it up so it'll be released at some point. - return new this.Context(hProv) - } - return 0 - } - - IsSigned(FilePath) - { - return DllCall("Crypt32\CryptQueryObject" - , "uint", CERT_QUERY_OBJECT_FILE := 1 - , "wstr", FilePath - , "uint", CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED := 1<<10 - , "uint", CERT_QUERY_FORMAT_FLAG_BINARY := 2 - , "uint", 0 - , "uint*", dwEncoding - , "uint*", dwContentType - , "uint*", dwFormatType - , "ptr", 0 - , "ptr", 0 - , "ptr", 0) - } - - ; - ; Error Detection - ; - __Get(name) - { - ListLines - MsgBox 16,, Attempt to access invalid property Crypt.%name%. - Pause - } - - ; - ; CLASSES - ; - - class _Handle - { - __New(handle) - { - this.h := handle - } - - __Delete() - { - this.Dispose() - } - } - - class Context extends Crypt._Handle - { - GenerateKey(KeyType, KeyBitLength, dwFlags) - { - if DllCall("Advapi32\CryptGenKey" - , "ptr", this.h - , "uint", KeyType - , "uint", (KeyBitLength << 16) | dwFlags - , "ptr*", hKey) - { - global Crypt - return new Crypt.Key(hKey) - } - return 0 - } - - CreateSelfSignCertificate(NameObject, StartTime, EndTime) - { - ctx := DllCall("Crypt32\CertCreateSelfSignCertificate" - , "ptr", this.h - , "ptr", IsObject(NameObject) ? NameObject.p : NameObject - , "uint", 0, "ptr", 0, "ptr", 0 - , "ptr", IsObject(StartTime) ? StartTime.p : StartTime - , "ptr", IsObject(EndTime) ? EndTime.p : EndTime - , "ptr", 0, "ptr") - global Cert - return ctx ? new Cert.Context(ctx) : 0 - } - - Dispose() - { - if this.h && DllCall("Advapi32\CryptReleaseContext", "ptr", this.h, "uint", 0) - this.h := 0 - } - } - - class Key extends Crypt._Handle - { - Dispose() - { - if this.h && DllCall("Advapi32\CryptDestroyKey", "ptr", this.h) - this.h := 0 - } - } -} \ No newline at end of file diff --git a/EnableUIAccess/Lib/EnableUIAccess.ahk b/EnableUIAccess/Lib/EnableUIAccess.ahk new file mode 100644 index 000000000..043a5e740 --- /dev/null +++ b/EnableUIAccess/Lib/EnableUIAccess.ahk @@ -0,0 +1,272 @@ +#requires AutoHotkey v2.0 + +EnableUIAccess(ExePath) { + static CertName := "AutoHotkey" + hStore := DllCall("Crypt32\CertOpenStore", "ptr",10 ; STORE_PROV_SYSTEM_W + , "uint",0, "ptr",0, "uint",0x20000 ; SYSTEM_STORE_LOCAL_MACHINE + , "wstr","Root", "ptr") + if !hStore { + throw OSError() + } + store := CertStore(hStore) + cert := CertContext() ; Find or create certificate for signing. + while (cert.ptr := DllCall("Crypt32\CertFindCertificateInStore", "ptr",hStore + , "uint",0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING + , "uint",0, "uint",0x80007 ; FIND_SUBJECT_STR + , "wstr", CertName, "ptr",cert.ptr, "ptr")) + && !(DllCall("Crypt32\CryptAcquireCertificatePrivateKey" + , "ptr",cert, "uint",5 ; CRYPT_ACQUIRE_CACHE_FLAG|CRYPT_ACQUIRE_COMPARE_KEY_FLAG + , "ptr",0, "ptr*", 0, "uint*", &keySpec:=0, "ptr",0) + && (keySpec & 2)) { ; AT_SIGNATURE ; Keep looking for a certificate with a private key. + } + if !cert.ptr { + cert := EnableUIAccess_CreateCert(CertName, hStore) + } + EnableUIAccess_SetManifest(ExePath) ; Set uiAccess attribute in manifest + EnableUIAccess_SignFile(ExePath, cert, CertName) ; Sign the file (otherwise uiAccess attribute is ignored) + return true +} + +EnableUIAccess_SetManifest(ExePath) { + xml := ComObject("Msxml2.DOMDocument") + xml.async := false + xml.setProperty("SelectionLanguage", "XPath") + xml.setProperty("SelectionNamespaces" + , "xmlns:v1='urn:schemas-microsoft-com:asm.v1' " + . "xmlns:v3='urn:schemas-microsoft-com:asm.v3'") + try { + if !xml.loadXML(EnableUIAccess_ReadManifest(ExePath)) { + throw Error("Invalid manifest") + } + } catch as e { + throw Error("Error loading manifest from " ExePath,, e.Message "`n @ " e.File ":" e.Line) + } + + + node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/v3:security" + . "/v3:requestedPrivileges/v3:requestedExecutionLevel") + if !node ; Not AutoHotkey? + throw Error("Manifest is missing required elements") + + node.setAttribute("uiAccess", "true") + xml := RTrim(xml.xml, "`r`n") + + data := Buffer(StrPut(xml, "utf-8") - 1) + StrPut(xml, data, "utf-8") + + if !(hupd := DllCall("BeginUpdateResource", "str",ExePath, "int",false)) + throw OSError() + r := DllCall("UpdateResource", "ptr",hupd, "ptr",24, "ptr",1 + , "ushort", 1033, "ptr",data, "uint",data.size) + + ; Retry loop to work around file locks (especially by antivirus) + for delay in [0, 100, 500, 1000, 3500] { + Sleep delay + if DllCall("EndUpdateResource", "ptr",hupd, "int",!r) || !r + return + if !(A_LastError = 5 || A_LastError = 110) ; ERROR_ACCESS_DENIED || ERROR_OPEN_FAILED + break + } + throw OSError(A_LastError, "EndUpdateResource") +} + +EnableUIAccess_ReadManifest(ExePath) { + if !(hmod := DllCall("LoadLibraryEx", "str",ExePath, "ptr",0, "uint",2, "ptr")) + throw OSError() + try { + if !(hres := DllCall("FindResource", "ptr",hmod, "ptr",1, "ptr",24, "ptr")) { + throw OSError() + } + size := DllCall("SizeofResource", "ptr",hmod, "ptr",hres, "uint") + if !(hglb := DllCall("LoadResource", "ptr",hmod, "ptr",hres, "ptr")) { + throw OSError() + } + if !(pres := DllCall("LockResource", "ptr",hglb, "ptr")) { + throw OSError() + } + return StrGet(pres, size, "utf-8") + } + finally { + DllCall("FreeLibrary", "ptr",hmod) + } +} + +EnableUIAccess_CreateCert(Name, hStore) { + prov := CryptContext() ; Here Name is used as the key container name. + if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov + , "str",Name, "ptr",0, "uint",1, "uint",0) { ; PROV_RSA_FULL=1, open existing=0 + if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov + , "str",Name, "ptr",0, "uint",1, "uint",8) { ; PROV_RSA_FULL=1, CRYPT_NEWKEYSET=8 + throw OSError() + } + if !DllCall("Advapi32\CryptGenKey", "ptr",prov + , "uint",2, "uint",0x4000001, "ptr*", CryptKey()) { ; AT_SIGNATURE=2, EXPORTABLE=..01 + throw OSError() + } + } + + ; Here Name is used as the certificate subject and name. + Loop 2 { + if A_Index = 1 { + pbName := cbName := 0 + } else { + bName := Buffer(cbName), pbName := bName.ptr + } + if !DllCall("Crypt32\CertStrToName", "uint",1, "str","CN=" Name + , "uint",3, "ptr",0, "ptr",pbName, "uint*", &cbName, "ptr",0) ; X509_ASN_ENCODING=1, CERT_X500_NAME_STR=3 + throw OSError() + } + cnb := Buffer(2*A_PtrSize), NumPut("ptr",cbName, "ptr",pbName, cnb) + + ; Set expiry to 9999-01-01 12pm +0. + NumPut("short", 9999, "sort", 1, "short", 5, "short", 1, "short", 12, endTime := Buffer(16, 0)) + + StrPut("2.5.29.4", szOID_KEY_USAGE_RESTRICTION := Buffer(9),, "cp0") + StrPut("2.5.29.37", szOID_ENHANCED_KEY_USAGE := Buffer(10),, "cp0") + StrPut("1.3.6.1.5.5.7.3.3", szOID_PKIX_KP_CODE_SIGNING := Buffer(18),, "cp0") + + ; CERT_KEY_USAGE_RESTRICTION_INFO key_usage; + key_usage := Buffer(6*A_PtrSize, 0) + NumPut('ptr', 0, 'ptr', 0, 'ptr', 1, 'ptr', key_usage.ptr + 5*A_PtrSize, 'ptr', 0 + , 'uchar', (CERT_DATA_ENCIPHERMENT_KEY_USAGE := 0x10) + | (CERT_DIGITAL_SIGNATURE_KEY_USAGE := 0x80), key_usage) + + ; CERT_ENHKEY_USAGE enh_usage; + enh_usage := Buffer(3*A_PtrSize) + NumPut("ptr",1, "ptr",enh_usage.ptr + 2*A_PtrSize, "ptr",szOID_PKIX_KP_CODE_SIGNING.ptr, enh_usage) + + key_usage_data := EncodeObject(szOID_KEY_USAGE_RESTRICTION, key_usage) + enh_usage_data := EncodeObject(szOID_ENHANCED_KEY_USAGE, enh_usage) + + EncodeObject(structType, structInfo) { + encoder := DllCall.Bind("Crypt32\CryptEncodeObject", "uint",X509_ASN_ENCODING := 1 + , "ptr",structType, "ptr",structInfo) + if !encoder("ptr",0, "uint*", &enc_size := 0) + throw OSError() + enc_data := Buffer(enc_size) + if !encoder("ptr",enc_data, "uint*", &enc_size) + throw OSError() + enc_data.Size := enc_size + return enc_data + } + + ; CERT_EXTENSION extension[2]; CERT_EXTENSIONS extensions; + NumPut("ptr",szOID_KEY_USAGE_RESTRICTION.ptr, "ptr",true, "ptr",key_usage_data.size, "ptr",key_usage_data.ptr + , "ptr",szOID_ENHANCED_KEY_USAGE.ptr , "ptr",true, "ptr",enh_usage_data.size, "ptr",enh_usage_data.ptr + , extension := Buffer(8*A_PtrSize)) + NumPut("ptr",2, "ptr",extension.ptr, extensions := Buffer(2*A_PtrSize)) + + if !hCert := DllCall("Crypt32\CertCreateSelfSignCertificate" + , "ptr",prov, "ptr",cnb, "uint",0, "ptr",0 + , "ptr",0, "ptr",0, "ptr",endTime, "ptr",extensions, "ptr") { + throw OSError() + } + cert := CertContext(hCert) + + if !DllCall("Crypt32\CertAddCertificateContextToStore", "ptr",hStore + , "ptr",hCert, "uint",1, "ptr",0) { ; STORE_ADD_NEW=1 + throw OSError() + } + + return cert +} + +EnableUIAccess_DeleteCertAndKey(Name) { + ; This first call "acquires" the key container but also deletes it. + DllCall("Advapi32\CryptAcquireContext", "ptr*", 0, "str",Name + , "ptr",0, "uint",1, "uint",16) ; PROV_RSA_FULL=1, CRYPT_DELETEKEYSET=16 + if !hStore := DllCall("Crypt32\CertOpenStore", "ptr",10 ; STORE_PROV_SYSTEM_W + , "uint",0, "ptr",0, "uint",0x20000 ; SYSTEM_STORE_LOCAL_MACHINE + , "wstr", "Root", "ptr") + throw OSError() + store := CertStore(hStore) + deleted := 0 + ; Multiple certificates might be created over time as keys become inaccessible + while p := DllCall("Crypt32\CertFindCertificateInStore", "ptr",hStore + , "uint",0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING + , "uint",0, "uint",0x80007 ; FIND_SUBJECT_STR + , "wstr", Name, "ptr",0, "ptr") { + if !DllCall("Crypt32\CertDeleteCertificateFromStore", "ptr",p) { + throw OSError() + } + deleted++ + } + return deleted +} + +class Crypt { + static IsSigned(FilePath) { + return DllCall("Crypt32\CryptQueryObject" + ,"uint" , CERT_QUERY_OBJECT_FILE := 1 + ,"wstr" , FilePath + ,"uint" , CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED := 1<<10 + ,"uint" , CERT_QUERY_FORMAT_FLAG_BINARY := 2 + ,"uint" , 0 + ,"uint*" , &dwEncoding:=0 + ,"uint*" , &dwContentType:=0 + ,"uint*" , &dwFormatType:=0 + ,"ptr" , 0 + ,"ptr" , 0 + ,"ptr" , 0) + } +} +class CryptPtrBase { + __new(p:=0) => this.ptr := p + __delete() => this.ptr && this.Dispose() +} +class CryptContext extends CryptPtrBase { + Dispose() => DllCall("Advapi32\CryptReleaseContext", "ptr",this, "uint",0) +} +class CertContext extends CryptPtrBase { + Dispose() => DllCall("Crypt32\CertFreeCertificateContext", "ptr",this) +} +class CertStore extends CryptPtrBase { + Dispose() => DllCall("Crypt32\CertCloseStore", "ptr",this, "uint",0) +} +class CryptKey extends CryptPtrBase { + Dispose() => DllCall("Advapi32\CryptDestroyKey", "ptr",this) +} + +EnableUIAccess_SignFile(ExePath, CertCtx, Name) { + file_info := struct( ; SIGNER_FILE_INFO + "ptr",A_PtrSize*3, "ptr",StrPtr(ExePath)) + dwIndex := Buffer(4, 0) ; DWORD + subject_info := struct( ; SIGNER_SUBJECT_INFO + "ptr",A_PtrSize*4, "ptr",dwIndex.ptr, "ptr",SIGNER_SUBJECT_FILE:=1, + "ptr",file_info.ptr) + cert_store_info := struct( ; SIGNER_CERT_STORE_INFO + "ptr",A_PtrSize*4, "ptr",CertCtx.ptr, "ptr",SIGNER_CERT_POLICY_CHAIN:=2) + cert_info := struct( ; SIGNER_CERT + "uint",8+A_PtrSize*2, "uint",SIGNER_CERT_STORE:=2, + "ptr",cert_store_info.ptr) + authcode_attr := struct( ; SIGNER_ATTR_AUTHCODE + "uint",8+A_PtrSize*3, "int",false, "ptr",true, "ptr",StrPtr(Name)) + sig_info := struct( ; SIGNER_SIGNATURE_INFO + "uint",8+A_PtrSize*4, "uint",CALG_SHA1:=0x8004, + "ptr",SIGNER_AUTHCODE_ATTR:=1, "ptr",authcode_attr.ptr) + + hr := DllCall("MSSign32\SignerSign" + , "ptr",subject_info, "ptr",cert_info, "ptr",sig_info + , "ptr",0, "ptr",0, "ptr",0, "ptr",0, "hresult") ; pProviderInfo pwszHttpTimeStamp psRequest pSipData + msgbox(' hr= ' hr) + + struct(args*) => ( + args.Push(b := Buffer(args[2], 0)), + NumPut(args*), + b + ) +} + +EnableUIAccess_Verify(ExePath) { ; Verifies a signed executable file. Returns 0 on success, or a standard OS error number. + wfi := Buffer(4*A_PtrSize) ; WINTRUST_FILE_INFO + NumPut('ptr', wfi.size, 'ptr', StrPtr(ExePath), 'ptr', 0, 'ptr', 0, wfi) + NumPut('int64', 0x11d0cd4400aac56b, 'int64', 0xee95c24fc000c28c, actionID := Buffer(16)) ; WINTRUST_ACTION_GENERIC_VERIFY_V2 + + wtd := Buffer(9*A_PtrSize+16) ; WINTRUST_DATA + NumPut( + 'ptr', wtd.Size, 'ptr', 0, 'ptr', 0, 'int', WTD_UI_NONE:=2, 'int', WTD_REVOKE_NONE:=0, + 'ptr', WTD_CHOICE_FILE:=1, 'ptr', wfi.ptr, 'ptr', WTD_STATEACTION_VERIFY:=1, + 'ptr', 0, 'ptr', 0, 'int', 0, 'int', 0, 'ptr', 0, wtd + ) + return DllCall('wintrust\WinVerifyTrust', 'ptr', 0, 'ptr', actionID, 'ptr', wtd, 'int') +} diff --git a/EnableUIAccess/Lib/SignFile.ahk b/EnableUIAccess/Lib/SignFile.ahk deleted file mode 100644 index 85e1ce879..000000000 --- a/EnableUIAccess/Lib/SignFile.ahk +++ /dev/null @@ -1,52 +0,0 @@ -SignFile(File, CertCtx, Name) -{ - VarSetCapacity(wfile, 2 * StrPut(File, "utf-16")), StrPut(File, &wfile, "utf-16") - VarSetCapacity(wname, 2 * StrPut(Name, "utf-16")), StrPut(Name, &wname, "utf-16") - cert_ptr := IsObject(CertCtx) ? CertCtx.p : CertCtx - - VarSetCapacity(file_info, A_PtrSize*3, 0) ; SIGNER_FILE_INFO - NumPut(3*A_PtrSize, file_info, 0) - NumPut(&wfile, file_info, A_PtrSize) - - VarSetCapacity(dwIndex, 4, 0) - - VarSetCapacity(subject_info, A_PtrSize*4, 0) ; SIGNER_SUBJECT_INFO - NumPut(A_PtrSize*4, subject_info, 0) - NumPut(&dwIndex, subject_info, A_PtrSize) ; MSDN: "must be set to zero" in this case means "must be set to the address of a field containing zero". - NumPut(SIGNER_SUBJECT_FILE:=1, subject_info, A_PtrSize*2) - NumPut(&file_info, subject_info, A_PtrSize*3) - - VarSetCapacity(cert_store_info, A_PtrSize*4, 0) ; SIGNER_CERT_STORE_INFO - NumPut(A_PtrSize*4, cert_store_info, 0) - NumPut(cert_ptr, cert_store_info, A_PtrSize) - NumPut(SIGNER_CERT_POLICY_CHAIN:=2, cert_store_info, A_PtrSize*3) - - VarSetCapacity(cert_info, 8+A_PtrSize*2, 0) ; SIGNER_CERT - NumPut(8+A_PtrSize*2, cert_info, 0, "uint") - NumPut(SIGNER_CERT_STORE:=2, cert_info, 4, "uint") - NumPut(&cert_store_info, cert_info, 8) - - VarSetCapacity(authcode_attr, 8+A_PtrSize*3, 0) ; SIGNER_ATTR_AUTHCODE - NumPut(8+A_PtrSize*3, authcode_attr, 0, "uint") - NumPut(false, authcode_attr, 4, "int") ; fCommercial - NumPut(true, authcode_attr, 8) ; fIndividual - NumPut(&wname, authcode_attr, 8+A_PtrSize) - - VarSetCapacity(sig_info, 8+A_PtrSize*4, 0) ; SIGNER_SIGNATURE_INFO - NumPut(8+A_PtrSize*4, sig_info, 0, "uint") - NumPut(CALG_SHA1:=0x8004, sig_info, 4, "uint") - NumPut(SIGNER_AUTHCODE_ATTR:=1, sig_info, 8) - NumPut(&authcode_attr, sig_info, 8+A_PtrSize) - - hr := DllCall("MSSign32\SignerSign" - , "ptr", &subject_info - , "ptr", &cert_info - , "ptr", &sig_info - , "ptr", 0 ; pProviderInfo - , "ptr", 0 ; pwszHttpTimeStamp - , "ptr", 0 ; psRequest - , "ptr", 0 ; pSipData - , "uint") - - return 0 == (ErrorLevel := hr) -} \ No newline at end of file diff --git a/EnableUIAccess/Lib/SystemTime.ahk b/EnableUIAccess/Lib/SystemTime.ahk deleted file mode 100644 index 446e0b2ea..000000000 --- a/EnableUIAccess/Lib/SystemTime.ahk +++ /dev/null @@ -1,101 +0,0 @@ -/* - SystemTime - Wrapper for Win32 SYSTEMTIME Structure - http://msdn.microsoft.com/en-us/library/ms724950 - - Usage Examples: - - ; Create structure from string. - st := SystemTime.FromString(A_Now) - - ; Shortcut: - st := SystemTime.Now() - - ; Update values. - st.FromString(A_Now) - - ; Retrieve components. - year := st.Year - month := st.Month - weekday := st.DayOfWeek - day := st.Day - hour := st.Hour - minute := st.Minute - second := st.Second - ms := st.Milliseconds - - ; Set or perform math on component. - st.Year += 10 - - ; Create structure to receive output from DllCall. - st := new SystemTime - DllCall("GetSystemTime", "ptr", st.p) - MsgBox % st.ToString() - - ; Fill external structure. - st := SystemTime.FromPointer(externalPointer) - st.FromString(A_Now) - - ; Convert external structure to string. - MsgBox % SystemTime.ToString(externalPointer) - -*/ - -class SystemTime -{ - FromString(str) - { - if this.p - st := this - else - st := new this - if !(p := st.p) - return 0 - FormatTime wday, %str%, WDay - wday -= 1 - FormatTime str, %str%, yyyy M '%wday%' d H m s '0' - Loop Parse, str, %A_Space% - NumPut(A_LoopField, p+(A_Index-1)*2, "ushort") - return st - } - - FromPointer(pointer) - { - return { p: pointer, base: this } ; Does not call __New. - } - - ToString(st = 0) - { - if !(p := (st ? (IsObject(st) ? st.p : st) : this.p)) - return "" - VarSetCapacity(s, 28), s := SubStr("000" NumGet(p+0, "ushort"), -3) - Loop 6 - if A_Index != 2 - s .= SubStr("0" NumGet(p+A_Index*2, "ushort"), -1) - return s - } - - Now() - { - return this.FromString(A_Now) - } - - __New() - { - if !(this.SetCapacity("struct", 16)) - || !(this.p := this.GetAddress("struct")) - return 0 - NumPut(0, NumPut(0, this.p, "int64"), "int64") - } - - __GetSet(name, value="") - { - static fields := {Year:0, Month:2, DayOfWeek:4, Day:6, Hour:8 - , Minute:10, Second:12, Milliseconds:14} - if fields.HasKey(name) - return value="" - ? NumGet( this.p + fields[name], "ushort") - : NumPut(value, this.p + fields[name], "ushort") - } - static __Get := SystemTime.__GetSet - static __Set := SystemTime.__GetSet -} \ No newline at end of file From 56bad59fba6665c59cffb75918c0f9b7bf3a7e18 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Wed, 24 Apr 2024 04:34:02 +0700 Subject: [PATCH 021/154] win-tray: update manifest --- build.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.rs b/build.rs index 825ae91b1..ba9525240 100644 --- a/build.rs +++ b/build.rs @@ -98,9 +98,9 @@ mod windows { r#" - - - + + + true From 3880663a4c459002b0a1443e91614f2aff362b77 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:51:38 +0700 Subject: [PATCH 022/154] win-tray: add request_live_reload --- src/kanata/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 791ef5c77..c125bb7cf 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -455,6 +455,11 @@ impl Kanata { }) } + #[cfg(all(target_os = "windows", feature = "gui"))] + pub fn request_live_reload(&mut self) -> Result<()> { + self.live_reload_requested = true; + Ok(()) + } fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, From d0c199f1f6886be70386ac3e587c4e48f1301d19 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:53:34 +0700 Subject: [PATCH 023/154] win-tray: update guards --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 66944c65e..1184af23b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,9 +11,9 @@ pub mod lib_main; pub mod m_gui_win; #[cfg(all(target_os = "windows", feature = "gui"))] pub use m_gui_win::*; -#[cfg(feature = "gui")] +#[cfg(all(target_os = "windows", feature = "gui"))] pub use win_dbg_logger as log_win; -#[cfg(feature = "gui")] +#[cfg(all(target_os = "windows", feature = "gui"))] pub use win_dbg_logger::WINDBG_LOGGER; pub use kanata::*; From 9f001b156eeb73e769ed29eb0e3824e8acc8369e Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 26 Apr 2024 00:37:30 +0700 Subject: [PATCH 024/154] win-tray: add request live reload n function --- src/kanata/mod.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index c125bb7cf..2f7be4c0a 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -456,9 +456,21 @@ impl Kanata { } #[cfg(all(target_os = "windows", feature = "gui"))] - pub fn request_live_reload(&mut self) -> Result<()> { + pub fn request_live_reload(&mut self) { self.live_reload_requested = true; - Ok(()) + } + #[cfg(all(target_os = "windows", feature = "gui"))] + pub fn request_live_reload_n(&mut self,n:usize) { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow + self.live_reload_requested = true; + match self.cfg_paths.get(n) { + Some(path) => { + self.cur_cfg_idx = n; + log::info!("Requested live reload of file: {}", path.display(),); + } + None => { + log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); + } + } } fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { From c8977421088df871875d9e14b8bd1ee396360c86 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 26 Apr 2024 19:42:48 +0700 Subject: [PATCH 025/154] doc(gui): update AHK installer guide --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index d5657637a..85e83193d 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3393,7 +3393,7 @@ The default `kanata.exe` binary doesn't work in elevated windows (run with admin e.g., `Control Panel`. However, you can use AutoHotkey's "EnableUIAccess" script to self-sign the binary, move it to "Program Files", then launching kanata from there will also work in these elevated windows. See https://github.com/jtroo/kanata/blob/main/EnableUIAccess[EnableUIAccess] folder with the script -and its requires libraries (needs https://www.autohotkey.com/download/ahk-install.exe[AutoHotkey v1] installed) +and its requires libraries (needs https://www.autohotkey.com/download/ahk-v2.exe[AutoHotkey v2] installed) If compiling yourself, you should add the feature flag `win_manifest` to enable the use of the `EnableUIAccess` script: From 6e402e292b136954d4a19ee28a456eb66da2cc09 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:43:42 +0700 Subject: [PATCH 026/154] log: update error context message --- src/kanata/windows/llhook.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index a008e16e7..499207b08 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use parking_lot::Mutex; use std::convert::TryFrom; use std::sync::mpsc::{sync_channel, Receiver, SyncSender as Sender, TryRecvError}; @@ -28,7 +29,7 @@ impl Kanata { panic!("Could not attach to console"); } }; - native_windows_gui::init()?; + native_windows_gui::init().context("Failed to init Native Windows GUI")?; let (preprocess_tx, preprocess_rx) = sync_channel(100); start_event_preprocessor(preprocess_rx, tx); @@ -77,7 +78,7 @@ impl Kanata { }); #[cfg(feature = "gui")] - let _ui = SystemTray::build_ui(Default::default()).expect("Failed to build UI"); + let _ui = SystemTray::build_ui(Default::default()).context("Failed to build UI")?; // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel From 610f62f276192eefd0ee37163c335828abffea14 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:19:54 +0700 Subject: [PATCH 027/154] win-tray: add tray builder to llhook near main thread loop --- src/kanata/windows/llhook.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 499207b08..78b103441 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -1,3 +1,4 @@ +use core::cell::RefCell; use anyhow::Context; use parking_lot::Mutex; use std::convert::TryFrom; @@ -7,15 +8,15 @@ use std::time; use super::PRESSED_KEYS; use crate::kanata::*; -#[cfg(feature = "gui")] +#[cfg(all(target_os = "windows", feature = "gui"))] use crate::m_gui_win::*; -#[cfg(feature = "gui")] +#[cfg(all(target_os = "windows", feature = "gui"))] extern crate native_windows_gui as nwg; -#[cfg(feature = "gui")] +#[cfg(all(target_os = "windows", feature = "gui"))] extern crate native_windows_derive as nwd; -#[cfg(feature = "gui")] +#[cfg(all(target_os = "windows", feature = "gui"))] use nwd::NwgUi; -#[cfg(feature = "gui")] +#[cfg(all(target_os = "windows", feature = "gui"))] use nwg::NativeUi; impl Kanata { @@ -23,6 +24,7 @@ impl Kanata { /// and run the native_windows_gui event loop. pub fn event_loop(_kanata: Arc>, tx: Sender) -> Result<()> { // Display debug and panic output when launched from a terminal. + #[cfg(not(feature = "gui"))] unsafe { use winapi::um::wincon::*; if AttachConsole(ATTACH_PARENT_PROCESS) != 0 { @@ -76,13 +78,14 @@ impl Kanata { try_send_panic(&preprocess_tx, key_event); true }); + #[cfg(all(target_os = "windows", feature = "gui"))] + let _ui = build_tray(&_cfg)?; - #[cfg(feature = "gui")] - let _ui = SystemTray::build_ui(Default::default()).context("Failed to build UI")?; - // The event loop is also required for the low-level keyboard hook to work. - native_windows_gui::dispatch_thread_events(); - eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel - let _ = std::io::stdin().read_line(&mut String::new()); + native_windows_gui::dispatch_thread_events(); // The event loop is also required for the low-level keyboard hook to work. + // if *IS_TERM { + // eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel + // let _ = std::io::stdin().read_line(&mut String::new()); + // } Ok(()) } } From 235000cfc601cb7a310deba295b46f353ce86163 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:59:55 +0700 Subject: [PATCH 028/154] win-tray: add win-tray-icon support to user config --- parser/src/cfg/defcfg.rs | 14 ++++++++++++++ parser/src/cfg/tests.rs | 1 + src/kanata/mod.rs | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index afbf49b37..c47d61e0b 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -53,6 +53,8 @@ pub struct CfgOptions { pub windows_interception_keyboard_hwids: Option>, #[cfg(any(target_os = "macos", target_os = "unknown"))] pub macos_dev_names_include: Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] + pub win_tray_icon: Option, } impl Default for CfgOptions { @@ -105,6 +107,8 @@ impl Default for CfgOptions { windows_interception_keyboard_hwids: None, #[cfg(any(target_os = "macos", target_os = "unknown"))] macos_dev_names_include: None, + #[cfg(all(target_os = "windows", feature = "gui"))] + win_tray_icon: None, } } } @@ -389,6 +393,16 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { cfg.macos_dev_names_include = Some(dev_names); } } + "win-tray-icon" => { + #[cfg(all(target_os = "windows", feature = "gui"))] + { + let icon_path = sexpr_to_str_or_err(val, label)?; + if icon_path.is_empty() { + log::warn!("win-tray-icon is empty"); + } + cfg.win_tray_icon = Some(icon_path.to_string()); + } + } "process-unmapped-keys" => { cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 01c664fa9..d9bfd501f 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1287,6 +1287,7 @@ fn parse_all_defcfg() { linux-unicode-u-code v linux-unicode-termination space linux-x11-repeat-delay-rate 400,50 + win-tray-icon symbols.ico 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/kanata/mod.rs b/src/kanata/mod.rs index 2f7be4c0a..8e9dd0a47 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -197,6 +197,8 @@ pub struct Kanata { pub switch_max_key_timing: u16, #[cfg(feature = "tcp_server")] tcp_server_address: Option, + #[cfg(all(target_os = "windows", feature = "gui"))] + pub win_tray_icon: Option, } #[derive(PartialEq, Clone, Copy)] @@ -359,6 +361,8 @@ impl Kanata { switch_max_key_timing: cfg.switch_max_key_timing, #[cfg(feature = "tcp_server")] tcp_server_address: args.tcp_server_address.clone(), + #[cfg(all(target_os = "windows", feature = "gui"))] + win_tray_icon: cfg.options.win_tray_icon, }) } @@ -452,6 +456,8 @@ impl Kanata { switch_max_key_timing: cfg.switch_max_key_timing, #[cfg(feature = "tcp_server")] tcp_server_address: None, + #[cfg(all(target_os = "windows", feature = "gui"))] + win_tray_icon: None, }) } From 4417eb7105f63cc1e621e8189d2aeaf259064fc3 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 18 Apr 2024 23:04:33 +0700 Subject: [PATCH 029/154] dep(gui): add gui feature --- Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6690fe34f..86866a660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,10 @@ winapi = { version = "0.3.9", features = [ "timeapi", "mmsystem", ] } -native-windows-gui = { version = "1.0.12", default_features = false } +native-windows-gui = { version = "1.0.12", default_features = false, features=["tray-notification","message-window","menu","cursor"] } +native-windows-derive = { version = "1.0.5", default_features = false, optional = true } +lazy_static = { version = "1.4.0", optional = true } +regex = { version = "1.10.4", optional = true } kanata-interception = { version = "0.2.0", optional = true } [target.'cfg(target_os = "windows")'.build-dependencies] @@ -98,6 +101,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] +gui = ["native-windows-derive", "embed-resource", "indoc", "regex","lazy_static"] [profile.release] opt-level = "z" From 100e4926fff9b529dc7541ddc950b4ac4c1ef291 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:12:34 +0700 Subject: [PATCH 030/154] dep: add win_dbg_logger to the workspace --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 86866a660..6aa4df0b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "tcp_protocol", "simulated_input", "windows_key_tester", + "win_dbg_logger", ] exclude = [ "wasm", @@ -79,6 +80,7 @@ winapi = { version = "0.3.9", features = [ "timeapi", "mmsystem", ] } +win_dbg_logger = { path = "win_dbg_logger", optional = true } native-windows-gui = { version = "1.0.12", default_features = false, features=["tray-notification","message-window","menu","cursor"] } native-windows-derive = { version = "1.0.5", default_features = false, optional = true } lazy_static = { version = "1.4.0", optional = true } @@ -101,7 +103,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] -gui = ["native-windows-derive", "embed-resource", "indoc", "regex","lazy_static"] +gui = ["native-windows-derive", "embed-resource", "indoc", "regex","lazy_static","win_dbg_logger","win_dbg_logger/simple_shared"] [profile.release] opt-level = "z" From ea17ae664fd8dc3b6a16c8c31a7decf7a64c6949 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:42:29 +0700 Subject: [PATCH 031/154] dep(gui-tray): add highdpi, allow embed, parsing non.ico images --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6aa4df0b2..a1f2b98eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ winapi = { version = "0.3.9", features = [ "mmsystem", ] } win_dbg_logger = { path = "win_dbg_logger", optional = true } -native-windows-gui = { version = "1.0.12", default_features = false, features=["tray-notification","message-window","menu","cursor"] } +native-windows-gui = { version = "1.0.13", default_features = false, features=["tray-notification","message-window","menu","cursor","high-dpi","embed-resource","image-decoder"] } native-windows-derive = { version = "1.0.5", default_features = false, optional = true } lazy_static = { version = "1.4.0", optional = true } regex = { version = "1.10.4", optional = true } From 8becb147092e2f67b2e5226dfbdf8fb8bd71fc98 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 18:41:39 +0700 Subject: [PATCH 032/154] win-tray: extend existing winmanifest instead of appendign a few lines and duplicating the rest of the code --- Cargo.toml | 2 +- build.rs | 56 +----------------------------------------------------- 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a1f2b98eb..b635f821e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] -gui = ["native-windows-derive", "embed-resource", "indoc", "regex","lazy_static","win_dbg_logger","win_dbg_logger/simple_shared"] +gui = ["win_manifest","native-windows-derive", "embed-resource", "indoc", "regex","lazy_static","win_dbg_logger","win_dbg_logger/simple_shared"] [profile.release] opt-level = "z" diff --git a/build.rs b/build.rs index ba9525240..e21229f95 100644 --- a/build.rs +++ b/build.rs @@ -6,61 +6,7 @@ fn main() -> std::io::Result<()> { Ok(()) } -#[cfg(all(target_os = "windows", feature = "win_manifest"))] -mod windows { - use indoc::formatdoc; - use regex::Regex; - use std::fs::File; - use std::io::Write; - extern crate embed_resource; - - // println! during build - macro_rules! pb { - ($($tokens:tt)*) => {println!("cargo:warning={}", format!($($tokens)*))}} - - pub(super) fn build() -> std::io::Result<()> { - let manifest_path: &str = "./target/kanata.exe.manifest"; - - // Note about expected version format: - // MS says "Use the four-part version format: mmmmm.nnnnn.ooooo.ppppp" - // https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests - - let re_ver_build = Regex::new(r"^(?(\d+\.){2}\d+)[-a-zA-Z]+(?\d+)$").unwrap(); - let re_version3 = Regex::new(r"^(\d+\.){2}\d+$").unwrap(); - let mut version: String = env!("CARGO_PKG_VERSION").to_string(); - - if re_version3.find(&version).is_some() { - version = format!("{}.0", version); - } else if re_ver_build.find(&version).is_some() { - version = re_ver_build - .replace_all(&version, r"$vpre.$vpos") - .to_string(); - } else { - pb!("unknown version format '{}', using '0.0.0.0'", version); - version = "0.0.0.0".to_string(); - } - - let manifest_str = formatdoc!( - r#" - - - - - - - - - "#, - version - ); - let mut manifest_f = File::create(manifest_path)?; - write!(manifest_f, "{}", manifest_str)?; - embed_resource::compile("./src/kanata.exe.manifest.rc", embed_resource::NONE); - Ok(()) - } -} - -#[cfg(all(target_os = "windows", feature = "gui"))] +#[cfg(all(target_os = "windows", any(feature = "win_manifest",feature = "gui")))] mod windows { use indoc::formatdoc; use regex::Regex; From 9e0084dd9fb33a65c5df4656b5c219e96f3fe619 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 16:51:28 +0700 Subject: [PATCH 033/154] dep(gui): add gui feature to parser --- Cargo.toml | 2 +- parser/Cargo.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b635f821e..e0eb1299b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] -gui = ["win_manifest","native-windows-derive", "embed-resource", "indoc", "regex","lazy_static","win_dbg_logger","win_dbg_logger/simple_shared"] +gui = ["win_manifest","native-windows-derive","lazy_static","win_dbg_logger","win_dbg_logger/simple_shared","kanata-parser/gui"] [profile.release] opt-level = "z" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 23c038144..e9daed839 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -31,3 +31,4 @@ bytemuck = "1.15.0" [features] cmd = [] interception_driver = [] +gui = [] From 7b9f66b8f542863405982be7a07ce67ed38bbb44 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 01:20:38 +0700 Subject: [PATCH 034/154] win-tray: update live reload function and win_tray_icon config --- src/kanata/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 8e9dd0a47..17f11715e 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -462,11 +462,13 @@ impl Kanata { } #[cfg(all(target_os = "windows", feature = "gui"))] - pub fn request_live_reload(&mut self) { + pub fn live_reload(&mut self) -> Result<()> { self.live_reload_requested = true; + self.do_live_reload(&None)?; + Ok(()) } #[cfg(all(target_os = "windows", feature = "gui"))] - pub fn request_live_reload_n(&mut self,n:usize) { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow + pub fn live_reload_n(&mut self,n:usize) -> Result<()> { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow self.live_reload_requested = true; match self.cfg_paths.get(n) { Some(path) => { @@ -477,6 +479,8 @@ impl Kanata { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } + self.do_live_reload(&None)?; + Ok(()) } fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { @@ -510,6 +514,9 @@ impl Kanata { self.virtual_keys = cfg.fake_keys; } self.switch_max_key_timing = cfg.switch_max_key_timing; + #[cfg(all(target_os = "windows", feature = "gui"))] { + self.win_tray_icon = cfg.options.win_tray_icon; + } *MAPPED_KEYS.lock() = cfg.mapped_keys; #[cfg(target_os = "linux")] From 202d9e9e43b7a76a984dc01b0f57ac2d4b574c45 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 27 Apr 2024 21:37:46 +0700 Subject: [PATCH 035/154] doc(gui-tray): update config adoc --- docs/config.adoc | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/config.adoc b/docs/config.adoc index 85e83193d..8316ddd45 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2390,6 +2390,33 @@ they are an arbitrary length and can be very long. ) ---- +[[windows-only-win-tray-icon]] +=== Windows only: win-tray-icon +<> + +Show a custom tray icon file for a gui-enabled build of kanata on Windows. +Accepts either the full path (including the file name with extension) to the icon file +or just the file name, which is then searched in the following locations: + +* Default parent folders: +** config file's, executable's +** env vars: `XDG_CONFIG_HOME`, `APPDATA` (`C:\Users\\AppData\Roaming`), `USERPROFILE` `/.config` (`C:\Users\\.config`) +* Default config subfolders: `kanata` `kanata-tray` +* Default image subfolders (optional): `icon` `img` `icons` +* Supported image file formats: `ico` `jpg` `jpeg` `png` `bmp` `dds` `tiff` + +If not specified, ttries to load any icon file from the same locations with the name matching +config name with extension replaced by one of the supported ones. + +.Example: +[source] +---- +;; in a config file C:\Users\\AppData\Roaming\kanata\kanata.kbd +(defcfg + win-tray-icon base.png ;; will load C:\Users\\AppData\Roaming\kanata\base.png +) +---- + [[using-multiple-defcfg-options]] === Using multiple defcfg options <> From 8208910daee446d8520fa7e81c7959c5279c38cf Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Mon, 22 Apr 2024 19:59:54 +0700 Subject: [PATCH 036/154] dep: add windbg log for win logging without console --- win_dbg_logger/Cargo.toml | 20 ++++ win_dbg_logger/src/lib.rs | 207 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 win_dbg_logger/Cargo.toml create mode 100644 win_dbg_logger/src/lib.rs diff --git a/win_dbg_logger/Cargo.toml b/win_dbg_logger/Cargo.toml new file mode 100644 index 000000000..48cb9c6d9 --- /dev/null +++ b/win_dbg_logger/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "win_dbg_logger" +version = "0.1.0" +authors = ["Arlie Davis "] +edition = "2018" +license = "MIT OR Apache-2.0" +repository = "https://github.com/sivadeilra/win_dbg_logger" +description = "A logger for use with Windows debuggers." + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.*" +lazy_static = {version="1.4.0" } +winapi = {version="0.3.9", features=["processthreadsapi",]} +regex = {version="1.10.4"} +simplelog = {version="0.12.0", optional=true} + +[features] +simple_shared = ["simplelog"] diff --git a/win_dbg_logger/src/lib.rs b/win_dbg_logger/src/lib.rs new file mode 100644 index 000000000..7268a74c5 --- /dev/null +++ b/win_dbg_logger/src/lib.rs @@ -0,0 +1,207 @@ +#![allow(non_upper_case_globals)] +//! A logger for use with Windows debuggers. +//! +//! This crate integrates with the ubiquitous [`log`] crate and can be used with the [`simplelog`] crate. +//! +//! Windows allows applications to output a string directly to debuggers. This is very useful in situations where other forms of logging are not available. +//! For example, stderr is not available for GUI apps. +//! +//! Windows provides the `OutputDebugString` entry point, which allows apps to print a debug string. Internally, `OutputDebugString` is implemented by raising an SEH exception, which the debugger catches and handles. +//! +//! Raising an exception has a significant cost, when run under a debugger, because the debugger halts all threads in the target process. So you should avoid using this logger for high rates of output, because doing so will slow down your app. +//! +//! Like many Windows entry points, `OutputDebugString` is actually two entry points: `OutputDebugStringA` (multi-byte encodings) and +//! `OutputDebugStringW` (UTF-16). In most cases, the `*A` version is implemented using a "thunk" which converts its arguments to UTF-16 and then calls the `*W` version. However, `OutputDebugStringA` is one of the few entry points where the opposite is true. +//! +//! This crate can be compiled and used on non-Windows platforms, but it does nothing. This is intended to minimize the impact on code that takes a dependency on this crate. +//! +//! # Example +//! +//! ```rust +//! use log::{debug, info}; +//! +//! fn do_cool_stuff() { +//! info!("Hello, world!"); +//! debug!("Hello, world, in detail!"); +//! } +//! +//! fn main() { +//! log::set_logger(&win_dbg_logger::WINDBG_LOGGER).unwrap(); +//! log::set_max_level(log::LevelFilter::Debug); +//! +//! do_cool_stuff(); +//! } +//! ``` + +use log::{Level, LevelFilter, Metadata, Record}; + +/// This implements `log::Log`, and so can be used as a logging provider. +/// It forwards log messages to the Windows `OutputDebugString` API. +#[derive(Copy, Clone)] +pub struct WinDbgLogger { + level: LevelFilter, + /// Allow for `WinDbgLogger` to possibly have more fields in the future + _priv: (), +} + +/// This is a static instance of `WinDbgLogger`. Since `WinDbgLogger` contains no state, this can be directly registered using `log::set_logger`, e.g.: +/// +/// ``` +/// log::set_logger(&win_dbg_logger::WINDBG_LOGGER).unwrap(); // Initialize +/// log::set_max_level(log::LevelFilter::Debug); +/// +/// use log::{info, debug}; // Import +/// +/// info!("Hello, world!"); debug!("Hello, world, in detail!"); // Use to log +/// ``` +pub static WINDBG_LOGGER :WinDbgLogger = WinDbgLogger {level:LevelFilter::Trace , _priv:()}; +pub static WINDBG_L1 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Error , _priv:()}; +pub static WINDBG_L2 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Warn , _priv:()}; +pub static WINDBG_L3 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Info , _priv:()}; +pub static WINDBG_L4 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Debug , _priv:()}; +pub static WINDBG_L5 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Trace , _priv:()}; +pub static WINDBG_L0 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Off , _priv:()}; + +#[cfg(feature = "simple_shared")] +pub fn windbg_simple_combo(log_lvl:LevelFilter) -> Box { + match log_lvl { + LevelFilter::Error => Box::new(WINDBG_L1), + LevelFilter::Warn => Box::new(WINDBG_L2), + LevelFilter::Info => Box::new(WINDBG_L3), + LevelFilter::Debug => Box::new(WINDBG_L4), + LevelFilter::Trace => Box::new(WINDBG_L5), + LevelFilter::Off => Box::new(WINDBG_L0), + } +} +#[cfg(feature = "simple_shared")] +impl simplelog::SharedLogger for WinDbgLogger { // allows using with simplelog's CombinedLogger + fn level (&self ) -> LevelFilter {self.level} + fn config(&self ) -> Option<&simplelog::Config> {None} + fn as_log( self:Box) -> Box {Box::new(*self)} +} + +/// Convert logging levels to shorter and more visible icons +pub fn iconify(lvl: log::Level) -> char { match lvl { + Level::Error => '❗', + Level::Warn => '⚠', + Level::Info => 'ⓘ', + Level::Debug => 'ⓓ', + Level::Trace => 'ⓣ', +}} + +use std::sync::OnceLock; +pub fn is_thread_state() -> &'static bool {set_thread_state(false)} +pub fn set_thread_state(is: bool) -> &'static bool { // accessor function to avoid get_or_init on every call (lazycell allows doing that without an extra function) + static CELL: OnceLock = OnceLock::new(); + CELL.get_or_init(|| is) +} + +use lazy_static::lazy_static; +use regex::Regex; +lazy_static! { // shorten source file name, no src/ no .rs ext + static ref reExt:Regex = Regex::new(r"\..*$" ).unwrap(); + static ref reSrc:Regex = Regex::new(r"src[\\/]").unwrap(); +} +fn clean_name(path: Option<&str>) -> String { // remove extension and src paths + if let Some(p) = path {reSrc.replace(&reExt.replace(p, ""), "").to_string() + } else {"?".to_string() + } +} + +#[cfg(target_os = "windows")] +use winapi::um::processthreadsapi::GetCurrentThreadId; +impl log::Log for WinDbgLogger { + fn enabled(&self, metadata: &Metadata) -> bool {metadata.level() <= self.level} + + fn log(&self, record: &Record) { + #[cfg(not(target_os = "windows"))] + let thread_id = ""; + #[cfg( target_os = "windows" )] + let thread_id = if *is_thread_state() {format!("{}¦", unsafe { GetCurrentThreadId() }) + } else {"".to_string()}; + if self.enabled(record.metadata()) { + let s = format!( + "{}{}{}:{} {}", + thread_id, + iconify(record.level()), + clean_name(record.file()), + record.line().unwrap_or(0), + record.args() + ); + output_debug_string(&s); + } + } + + fn flush(&self) {} +} + +/// Calls the `OutputDebugString` API to log a string. +/// +/// On non-Windows platforms, this function does nothing. +/// +/// See [`OutputDebugStringW`](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-outputdebugstringw). +pub fn output_debug_string(s: &str) { + #[cfg(windows)]{ + let len = s.encode_utf16().count() + 1; + let mut s_utf16: Vec = Vec::with_capacity(len); + s_utf16.extend(s.encode_utf16()); + s_utf16.push(0); + unsafe {OutputDebugStringW(&s_utf16[0]);} + } + #[cfg(not(windows))]{let _ = s;} +} + +#[cfg(windows)] extern "stdcall" { + fn OutputDebugStringW(chars: *const u16); + fn IsDebuggerPresent() -> i32; +} + +/// Checks whether a debugger is attached to the current process. +/// +/// On non-Windows platforms, this function always returns `false`. +/// +/// See [`IsDebuggerPresent`](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-isdebuggerpresent). +pub fn is_debugger_present() -> bool { + #[cfg( windows) ]{unsafe {IsDebuggerPresent() != 0 }} + #[cfg(not(windows))]{ false } +} + +/// Sets the `WinDbgLogger` as the currently-active logger. +/// +/// If an error occurs when registering `WinDbgLogger` as the current logger, this function will output a warning and will return normally. It will not panic. +/// This behavior was chosen because `WinDbgLogger` is intended for use in debugging. +/// Panicking would disrupt debugging and introduce new failure modes. It would also create problems for mixed-mode debugging, where Rust code is linked with C/C++ code. +pub fn init() { + match log::set_logger(&WINDBG_LOGGER) { + Ok(()) => {} //↓ there's really nothing we can do about it. + Err(_) => {output_debug_string("Warning: Failed to register WinDbgLogger as the current Rust logger.\r\n",);} + } +} + +macro_rules! define_init_at_level {($func:ident, $level:ident) => { + /// This can be called from C/C++ code to register the debug logger. + /// + /// For Windows DLLs that have statically linked an instance of `win_dbg_logger` into them, `DllMain` should call `win_dbg_logger_init_()` from the `DLL_PROCESS_ATTACH` handler, e.g.: + /// + /// ```ignore + /// extern "C" void __cdecl rust_win_dbg_logger_init_debug(); // Calls into Rust code + /// BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD reason, LPVOID reserved) { + /// switch (reason) { + /// case DLL_PROCESS_ATTACH: + /// rust_win_dbg_logger_init_debug(); + /// // ... + /// } + /// // ... + /// } + /// ``` + /// + /// For Windows executables that have statically linked an instance of `win_dbg_logger` into them, call `win_dbg_logger_init_()` during app startup. + #[no_mangle] pub extern "C" fn $func() {init();log::set_max_level(LevelFilter::$level);} + }; +} + +define_init_at_level!(rust_win_dbg_logger_init_trace , Trace); +define_init_at_level!(rust_win_dbg_logger_init_info , Info); +define_init_at_level!(rust_win_dbg_logger_init_debug , Debug); +define_init_at_level!(rust_win_dbg_logger_init_warn , Warn); +define_init_at_level!(rust_win_dbg_logger_init_error , Error); From 781c492aafc7db90757a5f0dceb39a37ac5d4c19 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 23 Apr 2024 17:23:52 +0700 Subject: [PATCH 037/154] win-tray: embed icon --- src/kanata.exe.manifest.rc | 1 + src/m_gui_win.rs | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/kanata.exe.manifest.rc b/src/kanata.exe.manifest.rc index 9c6384d3d..e2e9d95f4 100644 --- a/src/kanata.exe.manifest.rc +++ b/src/kanata.exe.manifest.rc @@ -1,2 +1,3 @@ #define RT_MANIFEST 24 1 RT_MANIFEST "./target/kanata.exe.manifest" +iconMain ICON "../assets/kanata.ico" diff --git a/src/m_gui_win.rs b/src/m_gui_win.rs index 463bdaf87..b1dadec2d 100644 --- a/src/m_gui_win.rs +++ b/src/m_gui_win.rs @@ -7,18 +7,21 @@ use nwd::NwgUi; use nwg::NativeUi; #[derive(Default, NwgUi)] pub struct SystemTray { - #[nwg_control] window : nwg::MessageWindow, - #[nwg_resource(source_file:Some("../assets/kanata.ico"))] icon : nwg::Icon, - #[nwg_control(icon:Some(&data.icon), tip: Some("Hello"))] // - #[nwg_events(MousePressLeftUp:[SystemTray::show_menu] // - , OnContextMenu :[SystemTray::show_menu])] tray : nwg::TrayNotification, - #[nwg_control(parent:window , popup: true)] tray_menu : nwg::Menu, - #[nwg_control(parent:tray_menu, text:"&1 Hello")] // - #[nwg_events(OnMenuItemSelected:[SystemTray::hello1])] tray_item1 : nwg::MenuItem, - #[nwg_control(parent:tray_menu, text:"&2 Popup")] // - #[nwg_events(OnMenuItemSelected:[SystemTray::hello2])] tray_item2 : nwg::MenuItem, - #[nwg_control(parent:tray_menu, text:"&X Exit")] // - #[nwg_events(OnMenuItemSelected:[SystemTray::exit ])] tray_item3 : nwg::MenuItem, + #[nwg_resource] embed : nwg::EmbedResource, + #[nwg_control] window : nwg::MessageWindow, + #[nwg_resource(source_embed :Some(&data.embed) // + , source_embed_str:Some("iconMain"))] icon : nwg::Icon, + #[nwg_control(icon:Some(&data.icon) // + , tip :Some("Hello"))] // + #[nwg_events(MousePressLeftUp:[SystemTray::show_menu] // + , OnContextMenu :[SystemTray::show_menu])] tray : nwg::TrayNotification, + #[nwg_control(parent:window , popup: true)] tray_menu : nwg::Menu, + #[nwg_control(parent:tray_menu, text:"&1 Hello")] // + #[nwg_events(OnMenuItemSelected:[SystemTray::hello1])] tray_item1 : nwg::MenuItem, + #[nwg_control(parent:tray_menu, text:"&2 Popup")] // + #[nwg_events(OnMenuItemSelected:[SystemTray::hello2])] tray_item2 : nwg::MenuItem, + #[nwg_control(parent:tray_menu, text:"&X Exit")] // + #[nwg_events(OnMenuItemSelected:[SystemTray::exit ])] tray_item3 : nwg::MenuItem, } impl SystemTray { fn show_menu(&self) { From cff15fe964a2f6a8b75e4e5caa0784a1b087e184 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 23 Apr 2024 23:40:24 +0700 Subject: [PATCH 038/154] win-tray: replace derive with manual builder --- src/m_gui_win.rs | 124 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 22 deletions(-) diff --git a/src/m_gui_win.rs b/src/m_gui_win.rs index b1dadec2d..f5f319033 100644 --- a/src/m_gui_win.rs +++ b/src/m_gui_win.rs @@ -1,39 +1,119 @@ -#![allow(unused_imports,unused_variables,unreachable_code,dead_code,non_upper_case_globals)] -// #![allow(non_upper_case_globals)] - +use crate::Kanata; +use parking_lot::Mutex; +use anyhow::{Result,Context}; extern crate native_windows_gui as nwg; extern crate native_windows_derive as nwd; +use std::sync::Arc; +use core::cell::RefCell; use nwd::NwgUi; -use nwg::NativeUi; - -#[derive(Default, NwgUi)] pub struct SystemTray { - #[nwg_resource] embed : nwg::EmbedResource, - #[nwg_control] window : nwg::MessageWindow, - #[nwg_resource(source_embed :Some(&data.embed) // - , source_embed_str:Some("iconMain"))] icon : nwg::Icon, - #[nwg_control(icon:Some(&data.icon) // - , tip :Some("Hello"))] // - #[nwg_events(MousePressLeftUp:[SystemTray::show_menu] // - , OnContextMenu :[SystemTray::show_menu])] tray : nwg::TrayNotification, - #[nwg_control(parent:window , popup: true)] tray_menu : nwg::Menu, - #[nwg_control(parent:tray_menu, text:"&1 Hello")] // - #[nwg_events(OnMenuItemSelected:[SystemTray::hello1])] tray_item1 : nwg::MenuItem, - #[nwg_control(parent:tray_menu, text:"&2 Popup")] // - #[nwg_events(OnMenuItemSelected:[SystemTray::hello2])] tray_item2 : nwg::MenuItem, - #[nwg_control(parent:tray_menu, text:"&X Exit")] // - #[nwg_events(OnMenuItemSelected:[SystemTray::exit ])] tray_item3 : nwg::MenuItem, +use nwg::{NativeUi,ControlHandle}; + +#[derive(Default,Debug,Clone)] pub struct SystemTrayData { + pub tooltip:String, } +#[derive(Default)] pub struct SystemTray { + pub app_data : RefCell, + /// Store dynamically created tray menu items + pub tray_item_dyn : RefCell>, + /// Store dynamically created tray menu items' handlers + pub handlers_dyn : RefCell>, + /// Store embedded-in-the-binary resources like icons not to load them from a file + pub embed : nwg::EmbedResource, + pub icon : nwg::Icon, + pub window : nwg::MessageWindow, + pub tray : nwg::TrayNotification, + pub tray_menu : nwg::Menu, + pub tray_item1 : nwg::MenuItem, + pub tray_item2 : nwg::MenuItem, + pub tray_item3 : nwg::MenuItem, +} +use winapi::shared::windef::{HWND, HMENU}; impl SystemTray { fn show_menu(&self) { let (x, y) = nwg::GlobalCursor::position(); self.tray_menu.popup(x, y); } - fn hello1(&self) {nwg::simple_message("Hello", "Hello World!");} + fn hello1(&self) {nwg::simple_message("HelloMsg", "Hello World!");} fn hello2(&self) { let flags = nwg::TrayNotificationFlags::USER_ICON | nwg::TrayNotificationFlags::LARGE_ICON; self.tray.show("Hello World", Some("Welcome to my application"), Some(flags), Some(&self.icon)); } fn exit(&self) {nwg::stop_thread_dispatch();} } +pub mod system_tray_ui { + use native_windows_gui::{self as nwg, MousePressEvent}; + use super::*; + use std::rc::Rc; + use std::cell::RefCell; + use std::ops::{Deref, DerefMut}; + + pub struct SystemTrayUi { + inner : Rc, + handler_def : RefCell> + } + + impl nwg::NativeUi for SystemTray { + fn build_ui(mut d: SystemTray) -> Result { + use nwg::Event as E; + + let app_data = d.app_data.borrow().clone(); + // d.app_data = RefCell::new(Default::default()); + d.tray_item_dyn = RefCell::new(Default::default()); + d.handlers_dyn = RefCell::new(Default::default()); + // Resources + d.embed = Default::default(); + d.embed = nwg::EmbedResource::load(Some("kanata.exe"))?; + nwg::Icon::builder().source_embed(Some(&d.embed)).source_embed_str(Some("iconMain")).strict(true)/*use sys, not panic, if missing*/ + .build(&mut d.icon)?; + + + // Controls + nwg::MessageWindow ::builder() + . build( &mut d.window )? ; + nwg::TrayNotification ::builder().parent(&d.window) .icon(Some(&d.icon)) .tip(Some(&app_data.tooltip)) + . build( &mut d.tray )? ; + nwg::Menu ::builder().parent(&d.window) .popup(true)/*context menu*/ // + . build( &mut d.tray_menu )? ; + nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&1 Hello") // + . build( &mut d.tray_item1 )? ; + nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&2 Popup") // + . build( &mut d.tray_item2 )? ; + nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&X Exit\t‹⎈␠⎋") // + . build( &mut d.tray_item3 )? ; + + let ui = SystemTrayUi { // Wrap-up + inner : Rc::new(d), + handler_def : Default::default(), + }; + + let evt_ui = Rc::downgrade(&ui.inner); // Events + let handle_events = move |evt, _evt_data, handle| { + if let Some(evt_ui) = evt_ui.upgrade() { + match evt { + E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnMenuItemSelected => + if &handle == &evt_ui.tray_item1 {SystemTray::hello1(&evt_ui); + } else if &handle == &evt_ui.tray_item2 {SystemTray::hello2(&evt_ui); + } else if &handle == &evt_ui.tray_item3 {SystemTray::exit (&evt_ui); + }, + _ => {} + } + } + }; + ui.handler_def.borrow_mut().push(nwg::full_bind_event_handler(&ui.window.handle, handle_events)); + return Ok(ui); + } + } + + impl Drop for SystemTrayUi { /// To make sure that everything is freed without issues, the default handler must be unbound. + fn drop(&mut self) { + let mut handlers = self.handler_def.borrow_mut(); + for handler in handlers.drain(0..) {nwg::unbind_event_handler(&handler);} + } + } + impl Deref for SystemTrayUi {type Target = SystemTray;fn deref (& self) -> & Self::Target {& self.inner}} +} + pub use log::*; pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; pub use winapi::shared::minwindef::BOOL; From 529283654964a99226d7b8ca9d100d4143432757 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Wed, 24 Apr 2024 16:56:37 +0700 Subject: [PATCH 039/154] win-tray: handle window close --- src/m_gui_win.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/m_gui_win.rs b/src/m_gui_win.rs index f5f319033..241c9766d 100644 --- a/src/m_gui_win.rs +++ b/src/m_gui_win.rs @@ -89,6 +89,7 @@ pub mod system_tray_ui { let handle_events = move |evt, _evt_data, handle| { if let Some(evt_ui) = evt_ui.upgrade() { match evt { + E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnMenuItemSelected => From 6960da4ed4708c5f1112f279e985ae2021ae7b7f Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 19:20:57 +0700 Subject: [PATCH 040/154] win-tray: add tray builder function --- src/m_gui_win.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/m_gui_win.rs b/src/m_gui_win.rs index 241c9766d..9a97109c8 100644 --- a/src/m_gui_win.rs +++ b/src/m_gui_win.rs @@ -115,6 +115,15 @@ pub mod system_tray_ui { impl Deref for SystemTrayUi {type Target = SystemTray;fn deref (& self) -> & Self::Target {& self.inner}} } +pub fn build_tray(cfg: &Arc>) -> Result { + let k = cfg.lock(); + let paths = &k.cfg_paths; + let path_cur = &paths[0]; + let app_data = SystemTrayData {tooltip:path_cur.display().to_string()}; + let app = SystemTray {app_data:RefCell::new(app_data), ..Default::default()}; + SystemTray::build_ui(app).context("Failed to build UI") +} + pub use log::*; pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; pub use winapi::shared::minwindef::BOOL; From a614ed77e02e68a9fc9c7f94139a67f234bed26a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 19:25:17 +0700 Subject: [PATCH 041/154] win-tray: add config reloading support including tooltip/icon changes --- src/m_gui_win.rs | 245 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 219 insertions(+), 26 deletions(-) diff --git a/src/m_gui_win.rs b/src/m_gui_win.rs index 9a97109c8..cce1544bf 100644 --- a/src/m_gui_win.rs +++ b/src/m_gui_win.rs @@ -1,4 +1,9 @@ -use crate::Kanata; +use std::env::{var_os,current_exe}; +use std::path::{Path,PathBuf}; +use std::collections::HashMap; +use std::ffi::OsStr; +use log::Level::Debug; +use crate::{Kanata,kanata}; use parking_lot::Mutex; use anyhow::{Result,Context}; extern crate native_windows_gui as nwg; @@ -9,7 +14,9 @@ use nwd::NwgUi; use nwg::{NativeUi,ControlHandle}; #[derive(Default,Debug,Clone)] pub struct SystemTrayData { - pub tooltip:String, + pub tooltip :String, + pub cfg_p :Vec, + pub cfg_icon :Option, } #[derive(Default)] pub struct SystemTray { pub app_data : RefCell, @@ -18,33 +25,158 @@ use nwg::{NativeUi,ControlHandle}; /// Store dynamically created tray menu items' handlers pub handlers_dyn : RefCell>, /// Store embedded-in-the-binary resources like icons not to load them from a file + pub icon_dyn : RefCell>>, + /// Store embedded-in-the-binary resources like icons not to load them from a file pub embed : nwg::EmbedResource, pub icon : nwg::Icon, pub window : nwg::MessageWindow, pub tray : nwg::TrayNotification, pub tray_menu : nwg::Menu, - pub tray_item1 : nwg::MenuItem, - pub tray_item2 : nwg::MenuItem, - pub tray_item3 : nwg::MenuItem, + pub tray_1cfg_m : nwg::Menu, + pub tray_2reload : nwg::MenuItem, + pub tray_3exit : nwg::MenuItem, } +pub fn get_appdata() -> Option {var_os("APPDATA").map(PathBuf::from)} +pub fn get_user_home() -> Option {var_os("USERPROFILE").map(PathBuf::from)} +pub fn get_xdg_home() -> Option {var_os("XDG_CONFIG_HOME").map(PathBuf::from)} + +const CFG_FD: [&str; 3] = ["","kanata","kanata-tray"]; // blank "" allow checking directly for user passed values +const ASSET_FD: [&str; 4] = ["","icon","img","icons"]; +const IMG_EXT: [&str; 7] = ["ico","jpg","jpeg","png","bmp","dds","tiff"]; +use crate::lib_main::CFG; use winapi::shared::windef::{HWND, HMENU}; impl SystemTray { fn show_menu(&self) { let (x, y) = nwg::GlobalCursor::position(); self.tray_menu.popup(x, y); } - fn hello1(&self) {nwg::simple_message("HelloMsg", "Hello World!");} - fn hello2(&self) { - let flags = nwg::TrayNotificationFlags::USER_ICON | nwg::TrayNotificationFlags::LARGE_ICON; - self.tray.show("Hello World", Some("Welcome to my application"), Some(flags), Some(&self.icon)); } - fn exit(&self) {nwg::stop_thread_dispatch();} + fn get_icon_p(&self, i:I , s:P ) -> Option + where I:AsRef, P:AsRef {self.get_icon_p_impl(i.as_ref(),s.as_ref())} + fn get_icon_p_impl(&self, icn:&str, p:&Path) -> Option { + let mut icon_file = PathBuf::new(); + let blank_p = Path::new(""); + let icn_p = Path::new(&icn); + let pre_p = p.parent ().unwrap_or_else(| |Path ::new("")); + let nameext = &p.file_name ().unwrap_or_else(| |OsStr ::new("")); + let cur_exe = current_exe ().unwrap_or_else(|_|PathBuf::new( )); + let xdg_cfg = get_xdg_home ().unwrap_or_else(| |PathBuf::new( )); + let app_data = get_appdata ().unwrap_or_else(| |PathBuf::new( )); + let mut user_cfg = get_user_home().unwrap_or_else(| |PathBuf::new( )); user_cfg.push(".config"); + let icn_ext = &icn_p.extension().unwrap_or_else(||OsStr::new("")).to_string_lossy().to_string(); + let is_icn_ext_valid = if ! IMG_EXT.iter().any(|&i| {i==icn_ext}) {warn!("user extension \"{}\" isn't valid!",icn_ext); false} else {trace!("icn_ext={:?}",icn_ext);true}; + let parents = [Path::new(""),pre_p,&cur_exe,&xdg_cfg,&app_data,&user_cfg]; // empty path to allow no prefixes when icon path is explictily set in case it's a full path already + let f_name = [icn_p.as_os_str(),nameext]; + 'p:for p_par in parents {trace!("{}p_par={:?}" ,"" ,p_par); + for p_kan in CFG_FD {trace!("{}p_kan={:?}" ," " ,p_kan); + for p_icn in ASSET_FD {trace!("{}p_icn={:?}" ," " ,p_icn); + for nm in f_name {trace!("{} nm={:?}" ," " ,nm); + for ext in IMG_EXT {trace!("{} ext={:?}" ," " ,ext); + if !(p_par == blank_p){icon_file.push(p_par);} // folders + if ! p_kan.is_empty() {icon_file.push(p_kan);} + if ! p_icn.is_empty() {icon_file.push(p_icn);} + if ! nm.is_empty() {icon_file.push(nm );} + if !( nm == icn_p ){icon_file.push(ext); // no icon name passed, iterate extensions + } else if ! is_icn_ext_valid {icon_file.push(ext);} else{trace!("skip ext");} // replace invalid icon extension + if icon_file == blank_p {continue;} + trace!("testing icon file {:?}",icon_file); + if ! icon_file.is_file() {icon_file.clear(); + if p_par == blank_p && p_kan.is_empty() && p_icn.is_empty() && nm == icn_p {trace!("skipping further iters {:?}",nm); continue 'p} + } else {info!("✓ found icon file: {}",icon_file.display().to_string()); + return Some(icon_file.display().to_string()) + } } } } } } + debug!("✗ no icon file found");return None + } + fn check_active(&self) { + if let Some(cfg) = CFG.get() {let k = cfg.lock(); + let idx_cfg = k.cur_cfg_idx; + let tray_item_dyn = &self.tray_item_dyn.borrow(); // + for (i, h_cfg_i) in tray_item_dyn.iter().enumerate() { + if h_cfg_i.checked(){trace!("✓checked {} active {} eq? {} !eq? {}",i,idx_cfg,i==idx_cfg,!(i==idx_cfg));} + if h_cfg_i.checked() && !(i==idx_cfg){debug!("uncheck i{} act{}",i,idx_cfg);h_cfg_i.set_checked(false);} // uncheck inactive + if ! h_cfg_i.checked() && i==idx_cfg {debug!(" check i{} act{}",i,idx_cfg);h_cfg_i.set_checked(true );} // check active + }; + } else {error!("no CFG var that contains active kanata config"); + }; + } + fn reload(&self,i:Option) { + use nwg::TrayNotificationFlags as f_tray; + let mut msg_title :String = "".to_string(); + let mut msg_content:String = "".to_string(); + let mut flags:f_tray = f_tray::empty(); + if let Some(cfg) = CFG.get() {let mut k = cfg.lock(); + let paths = &k.cfg_paths; + let idx_cfg = match i { + Some(idx) => {if idx k.cur_cfg_idx}; + let path_cur = &paths[idx_cfg]; let path_cur_s = path_cur.display().to_string(); + let path_cur_cc = path_cur.clone(); + msg_content += &path_cur_s; + let cfg_name = &path_cur.file_name().unwrap_or_else(||OsStr::new("")).to_string_lossy().to_string(); + if log_enabled!(Debug) {let cfg_icon = &k.win_tray_icon;debug!("pre reload win_tray_icon={:?}",cfg_icon);} + match i { + Some(idx) => {if let Ok(()) = k.live_reload_n(idx) { + msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" loaded"); flags |= f_tray::USER_ICON; + } else { + msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" NOT loaded"); flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; + self.tray.show(&msg_content, Some(&msg_title), Some(flags), Some(&self.icon)); + return + } + } + None => {if let Ok(()) = k.live_reload ( ) { + msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" reloaded"); flags |= f_tray::USER_ICON; + } else { + msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" NOT reloaded"); flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; + self.tray.show(&msg_content, Some(&msg_title), Some(flags), Some(&self.icon)); + return + } + } + }; + let cfg_icon = &k.win_tray_icon; debug!("pos reload win_tray_icon={:?}",cfg_icon); + let mut app_data = self.app_data.borrow_mut(); + app_data.cfg_icon = cfg_icon.clone(); + // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading + self.tray.set_tip(&path_cur_s); // update tooltip to point to the newer config + // self.tray.set_visibility(true); + + let mut icon_dyn = self.icon_dyn.borrow_mut(); // update the tray icon + if icon_dyn.contains_key(&path_cur_cc) { + if let Some(icon) = icon_dyn.get(&path_cur_cc).unwrap() {self.tray.set_icon(&icon); // + } else {info!("this config has no associated icon, using default: {}",path_cur_cc.display().to_string()); + self.tray.set_icon(&self.icon)} + } else { + let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon {cfg_icon} else {""}; + if let Some(ico_p) = &self.get_icon_p(&cfg_icon_p, &path_cur_cc) { + let mut temp_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).build(&mut temp_icon_bitmap) { + info!("✓ Using an icon from this config: {}",path_cur_cc.display().to_string()); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + let _ = icon_dyn.insert(path_cur_cc.clone(),Some(temp_icon)); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + self.tray.set_icon(&temp_icon); + } else {warn!("✗ Invalid/no icon from this config: {}",path_cur_cc.display().to_string()); + self.tray.set_icon(&self.icon); + } + } else {warn!("✗ Invalid icon path from this config: {}",path_cur_cc.display().to_string()); + self.tray.set_icon(&self.icon); + } + } + } else {msg_title+="✗ Config NOT reloaded, no CFG";warn!("{}", msg_title); flags |= f_tray::ERROR_ICON; + }; + flags |= f_tray::LARGE_ICON; // todo: fails without this, must have SM_CXICON x SM_CYICON? + self.tray.show(&msg_content, Some(&msg_title), Some(flags), Some(&self.icon)); + } + fn exit(&self) { + let handlers = self.handlers_dyn.borrow(); + for handler in handlers.iter() {nwg::unbind_event_handler(&handler);} + nwg::stop_thread_dispatch();} } pub mod system_tray_ui { - use native_windows_gui::{self as nwg, MousePressEvent}; use super::*; + use core::cmp; use std::rc::Rc; use std::cell::RefCell; use std::ops::{Deref, DerefMut}; + use native_windows_gui::{self as nwg, MousePressEvent}; pub struct SystemTrayUi { inner : Rc, @@ -68,17 +200,58 @@ pub mod system_tray_ui { // Controls nwg::MessageWindow ::builder() - . build( &mut d.window )? ; - nwg::TrayNotification ::builder().parent(&d.window) .icon(Some(&d.icon)) .tip(Some(&app_data.tooltip)) - . build( &mut d.tray )? ; - nwg::Menu ::builder().parent(&d.window) .popup(true)/*context menu*/ // - . build( &mut d.tray_menu )? ; - nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&1 Hello") // - . build( &mut d.tray_item1 )? ; - nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&2 Popup") // - . build( &mut d.tray_item2 )? ; - nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&X Exit\t‹⎈␠⎋") // - . build( &mut d.tray_item3 )? ; + . build( &mut d.window )? ; + nwg::TrayNotification ::builder().parent(&d.window) .icon(Some(&d.icon)) .tip(Some(&app_data.tooltip)) + . build( &mut d.tray )? ; + nwg::Menu ::builder().parent(&d.window) .popup(true)/*context menu*/ // + . build( &mut d.tray_menu )? ; + nwg::Menu ::builder().parent(&d.tray_menu) .text("&F Load config") // + . build( &mut d.tray_1cfg_m )? ; + nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&R Reload config") // + . build( &mut d.tray_2reload )? ; + nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&X Exit\t‹⎈␠⎋") // + . build( &mut d.tray_3exit )? ; + + {let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut + let mut icon_dyn = d.icon_dyn .borrow_mut(); + const menu_acc:&str = "ASDFGQWERTZXCVBYUIOPHJKLNM"; + let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon {cfg_icon} else {""}; + if (app_data.cfg_p).len() > 0 { + for (i, cfg_p) in app_data.cfg_p.iter().enumerate() { + let i_acc = match i { // menu accelerators from 1–0 then A–Z starting from home row for easier presses + 0..= 8 => format!("&{} ",i+1), + 9 => format!("&{} ",0), + 10..=35 => format!("&{} ",&menu_acc[(i-10)..cmp::min(i-10+1,menu_acc.len())]), + _ => format!(" "), + }; + let cfg_name = &cfg_p.file_name().unwrap_or_else(||OsStr::new("")).to_string_lossy().to_string(); //kanata.kbd + // let menu_text = i_acc + cfg_name; // &1 kanata.kbd + let menu_text = format!("{cfg_name}\t{i_acc}"); // kanata.kbd &1 + let mut menu_item = Default::default(); + if i == 0 {nwg::MenuItem::builder().parent(&d.tray_1cfg_m).text(&menu_text).check(true) .build(&mut menu_item)?; + } else {nwg::MenuItem::builder().parent(&d.tray_1cfg_m).text(&menu_text) .build(&mut menu_item)?; + } + tray_item_dyn.push(menu_item); + if i == 0 { // add icons if exists, hashed by config path (for active config, others will create on load) + if let Some(ico_p) = &d.get_icon_p(&cfg_icon_p, &cfg_p) { + let mut temp_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).build(&mut temp_icon_bitmap) { + debug!("✓ main 0 config: using icon for {:?}",cfg_p); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + let _ = icon_dyn.insert(cfg_p.clone(),Some(temp_icon)); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + d.tray.set_icon(&temp_icon); + } else {info!("✗ main 0 icon ✓ icon path, using DEFAULT icon for {:?}",cfg_p);} + } else { + debug!("✗ main 0 config: using DEFAULT icon for {:?}",cfg_p); + let mut temp_icon = Default::default(); + nwg::Icon::builder().source_embed(Some(&d.embed)).source_embed_str(Some("iconMain")).strict(true).build(&mut temp_icon)?; + let _ = icon_dyn.insert(cfg_p.clone(),Some(temp_icon)); + } + } + } + } else {warn!("Didn't get any config paths from Kanata!")} + } let ui = SystemTrayUi { // Wrap-up inner : Rc::new(d), @@ -92,10 +265,26 @@ pub mod system_tray_ui { E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnMenuHover => + if &handle == &evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} E::OnMenuItemSelected => - if &handle == &evt_ui.tray_item1 {SystemTray::hello1(&evt_ui); - } else if &handle == &evt_ui.tray_item2 {SystemTray::hello2(&evt_ui); - } else if &handle == &evt_ui.tray_item3 {SystemTray::exit (&evt_ui); + if &handle == &evt_ui.tray_2reload {SystemTray::reload(&evt_ui,None); + } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); + } else { + match handle { + ControlHandle::MenuItem(parent, id) => { + let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // + for (i, h_cfg) in tray_item_dyn.iter().enumerate() { + if &handle == h_cfg { //info!("CONFIG handle i={:?} {:?}",i,&handle); + for (j, h_cfg_j) in tray_item_dyn.iter().enumerate() { + if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others + h_cfg.set_checked(true); // check self + SystemTray::reload(&evt_ui,Some(i)); + } + } + }, + _ => {}, + } }, _ => {} } @@ -118,8 +307,12 @@ pub mod system_tray_ui { pub fn build_tray(cfg: &Arc>) -> Result { let k = cfg.lock(); let paths = &k.cfg_paths; + let cfg_icon = &k.win_tray_icon; let path_cur = &paths[0]; - let app_data = SystemTrayData {tooltip:path_cur.display().to_string()}; + let app_data = SystemTrayData { + tooltip : path_cur.display().to_string(), + cfg_p : paths.clone(), + cfg_icon : cfg_icon.clone(),}; let app = SystemTray {app_data:RefCell::new(app_data), ..Default::default()}; SystemTray::build_ui(app).context("Failed to build UI") } From f0d11d7479e0fc20b8b1ca0f8d520b1dacff5301 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 20:13:48 +0700 Subject: [PATCH 042/154] clippy fmt --- build.rs | 4 +- src/kanata/mod.rs | 21 +- src/kanata/windows/llhook.rs | 17 +- src/lib.rs | 4 +- src/lib_main.rs | 101 +++-- src/m_gui_win.rs | 764 +++++++++++++++++++++++------------ src/main.rs | 14 +- win_dbg_logger/src/lib.rs | 263 +++++++----- 8 files changed, 757 insertions(+), 431 deletions(-) diff --git a/build.rs b/build.rs index e21229f95..21b17555d 100644 --- a/build.rs +++ b/build.rs @@ -1,12 +1,12 @@ fn main() -> std::io::Result<()> { - #[cfg(all(target_os = "windows", any(feature = "win_manifest",feature = "gui")))] + #[cfg(all(target_os = "windows", any(feature = "win_manifest", feature = "gui")))] { windows::build()?; } Ok(()) } -#[cfg(all(target_os = "windows", any(feature = "win_manifest",feature = "gui")))] +#[cfg(all(target_os = "windows", any(feature = "win_manifest", feature = "gui")))] mod windows { use indoc::formatdoc; use regex::Regex; diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 17f11715e..c09c18cb3 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -468,7 +468,8 @@ impl Kanata { Ok(()) } #[cfg(all(target_os = "windows", feature = "gui"))] - pub fn live_reload_n(&mut self,n:usize) -> Result<()> { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow + pub fn live_reload_n(&mut self, n: usize) -> Result<()> { + // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow self.live_reload_requested = true; match self.cfg_paths.get(n) { Some(path) => { @@ -479,8 +480,8 @@ impl Kanata { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } - self.do_live_reload(&None)?; - Ok(()) + self.do_live_reload(&None)?; + Ok(()) } fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { @@ -514,8 +515,9 @@ impl Kanata { self.virtual_keys = cfg.fake_keys; } self.switch_max_key_timing = cfg.switch_max_key_timing; - #[cfg(all(target_os = "windows", feature = "gui"))] { - self.win_tray_icon = cfg.options.win_tray_icon; + #[cfg(all(target_os = "windows", feature = "gui"))] + { + self.win_tray_icon = cfg.options.win_tray_icon; } *MAPPED_KEYS.lock() = cfg.mapped_keys; @@ -1966,10 +1968,15 @@ fn check_for_exit(event: &KeyEvent) { const EXIT_MSG: &str = "pressed LControl+Space+Escape, exiting"; if IS_ESC_PRESSED.load(SeqCst) && IS_SPC_PRESSED.load(SeqCst) && IS_LCL_PRESSED.load(SeqCst) { log::info!("{EXIT_MSG}"); - #[cfg(all(target_os = "windows", feature = "gui"))]{ + #[cfg(all(target_os = "windows", feature = "gui"))] + { native_windows_gui::stop_thread_dispatch(); } - #[cfg(all(not(target_os = "linux"),not(target_os = "windows"),not(feature = "gui")))] + #[cfg(all( + not(target_os = "linux"), + not(target_os = "windows"), + not(feature = "gui") + ))] { panic!("{EXIT_MSG}"); } diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 78b103441..3ad5325e6 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -1,4 +1,3 @@ -use core::cell::RefCell; use anyhow::Context; use parking_lot::Mutex; use std::convert::TryFrom; @@ -11,13 +10,9 @@ use crate::kanata::*; #[cfg(all(target_os = "windows", feature = "gui"))] use crate::m_gui_win::*; #[cfg(all(target_os = "windows", feature = "gui"))] -extern crate native_windows_gui as nwg; -#[cfg(all(target_os = "windows", feature = "gui"))] extern crate native_windows_derive as nwd; #[cfg(all(target_os = "windows", feature = "gui"))] -use nwd::NwgUi; -#[cfg(all(target_os = "windows", feature = "gui"))] -use nwg::NativeUi; +extern crate native_windows_gui as nwg; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events @@ -79,13 +74,13 @@ impl Kanata { true }); #[cfg(all(target_os = "windows", feature = "gui"))] - let _ui = build_tray(&_cfg)?; + let _ui = build_tray(&_kanata)?; native_windows_gui::dispatch_thread_events(); // The event loop is also required for the low-level keyboard hook to work. - // if *IS_TERM { - // eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel - // let _ = std::io::stdin().read_line(&mut String::new()); - // } + // if *IS_TERM { + // eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel + // let _ = std::io::stdin().read_line(&mut String::new()); + // } Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 1184af23b..aca6a3185 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,11 +4,11 @@ use std::path::PathBuf; use std::str::FromStr; pub mod kanata; -pub mod oskbd; -pub mod tcp_server; pub mod lib_main; #[cfg(all(target_os = "windows", feature = "gui"))] pub mod m_gui_win; +pub mod oskbd; +pub mod tcp_server; #[cfg(all(target_os = "windows", feature = "gui"))] pub use m_gui_win::*; #[cfg(all(target_os = "windows", feature = "gui"))] diff --git a/src/lib_main.rs b/src/lib_main.rs index 39cc7d2fe..a791ff924 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -1,9 +1,9 @@ +use crate::*; use anyhow::{bail, Result}; use clap::Parser; #[cfg(all(target_os = "windows", feature = "gui"))] -use clap::{CommandFactory,error::ErrorKind}; +use clap::{error::ErrorKind, CommandFactory}; use kanata_parser::cfg; -use crate::*; use log::info; use simplelog::{format_description, *}; use std::path::PathBuf; @@ -98,26 +98,38 @@ kanata.kbd in the current working directory and /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { - #[cfg(all(not(target_os = "windows"), not(feature = "gui")))] + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] let args = Args::parse(); - #[cfg(all( target_os = "windows", feature = "gui" ))] + #[cfg(all(target_os = "windows", feature = "gui"))] let args = match Args::try_parse() { - Ok (args ) => args, - Err(e) => { - if *IS_TERM { // init loggers without config so '-help' "error" or real ones can be printed + Ok(args) => args, + Err(e) => { + if *IS_TERM { + // init loggers without config so '-help' "error" or real ones can be printed let mut log_cfg = ConfigBuilder::new(); - CombinedLogger::init(vec![TermLogger::new(LevelFilter::Debug,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi,), - log_win::windbg_simple_combo(LevelFilter::Debug),]).expect("logger can init"); - } else {log_win::init();log::set_max_level(LevelFilter::Debug);} // doesn't panic + CombinedLogger::init(vec![ + TermLogger::new( + LevelFilter::Debug, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + ), + log_win::windbg_simple_combo(LevelFilter::Debug), + ]) + .expect("logger can init"); + } else { + log_win::init(); + log::set_max_level(LevelFilter::Debug); + } // doesn't panic match e.kind() { - ErrorKind::DisplayHelp => { + ErrorKind::DisplayHelp => { let mut cmd = lib_main::Args::command(); let help = cmd.render_help(); info!("{help}"); log::set_max_level(LevelFilter::Off); - return Err(anyhow!("")) - }, - _ => return Err(e.into()), + return Err(anyhow!("")); + } + _ => return Err(e.into()), } } }; @@ -141,14 +153,29 @@ fn cli_init() -> Result { eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; log_cfg.set_time_format_rfc3339(); - #[cfg(all(not(target_os = "windows"), not(feature = "gui")))] - CombinedLogger::init(vec![TermLogger::new(log_lvl,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi, - )]).expect("logger can init"); - #[cfg(all( target_os = "windows", feature = "gui" ))] - if *IS_TERM { - CombinedLogger::init(vec![TermLogger::new(log_lvl,log_cfg.build(),TerminalMode::Mixed,ColorChoice::AlwaysAnsi,), - log_win::windbg_simple_combo(log_lvl),]).expect("logger can init"); - } else {CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl),]).expect("logger can init");} + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + CombinedLogger::init(vec![TermLogger::new( + log_lvl, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + )]) + .expect("logger can init"); + #[cfg(all(target_os = "windows", feature = "gui"))] + if *IS_TERM { + CombinedLogger::init(vec![ + TermLogger::new( + log_lvl, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + ), + log_win::windbg_simple_combo(log_lvl), + ]) + .expect("logger can init"); + } else { + CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl)]).expect("logger can init"); + } log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] log::info!("using LLHOOK+SendInput for keyboard IO"); @@ -199,8 +226,10 @@ fn main_impl() -> Result<()> { let args = cli_init()?; let kanata_arc = Kanata::new_arc(&args)?; - #[cfg(all( target_os = "windows", feature = "gui" ))] - if CFG.set(kanata_arc.clone()).is_err() {warn!("Someone else set our ‘CFG’");}; // store a clone of cfg so that we can ask it to reset itself + #[cfg(all(target_os = "windows", feature = "gui"))] + if CFG.set(kanata_arc.clone()).is_err() { + warn!("Someone else set our ‘CFG’"); + }; // store a clone of cfg so that we can ask it to reset itself if !args.nodelay { info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); @@ -256,22 +285,26 @@ pub fn lib_main_cli() -> Result<()> { let _ = std::io::stdin().read_line(&mut String::new()); ret } -#[cfg(all( target_os = "windows", feature = "gui" ))] +#[cfg(all(target_os = "windows", feature = "gui"))] use parking_lot::Mutex; -#[cfg(all( target_os = "windows", feature = "gui" ))] +#[cfg(all(target_os = "windows", feature = "gui"))] use std::sync::{Arc, OnceLock}; -#[cfg(all( target_os = "windows", feature = "gui" ))] +#[cfg(all(target_os = "windows", feature = "gui"))] pub static CFG: OnceLock>> = OnceLock::new(); -#[cfg(all( target_os = "windows", feature = "gui" ))] +#[cfg(all(target_os = "windows", feature = "gui"))] pub fn lib_main_gui() { - let _attach_console = *IS_CONSOLE; - let ret = main_impl(); - if let Err(ref e) = ret {log::error!("{e}\n");} - // if *IS_TERM { + let _attach_console = *IS_CONSOLE; + let ret = main_impl(); + if let Err(ref e) = ret { + log::error!("{e}\n"); + } + // if *IS_TERM { // eprintln!("\nPress enter to exit"); // let _ = std::io::stdin().read_line(&mut String::new()); // TODO: panics on Err(TryRecvError::Disconnected) @ Win/llhook, move to llhook OR coordinate with exit(&self) {nwg::stop_thread_dispatch();}? OR just ignore, why do we need this at all? - // } + // } - unsafe {FreeConsole();} + unsafe { + FreeConsole(); + } } diff --git a/src/m_gui_win.rs b/src/m_gui_win.rs index cce1544bf..a0eb65d29 100644 --- a/src/m_gui_win.rs +++ b/src/m_gui_win.rs @@ -1,267 +1,475 @@ -use std::env::{var_os,current_exe}; -use std::path::{Path,PathBuf}; -use std::collections::HashMap; -use std::ffi::OsStr; +use crate::Kanata; +use anyhow::{Context, Result}; use log::Level::Debug; -use crate::{Kanata,kanata}; use parking_lot::Mutex; -use anyhow::{Result,Context}; -extern crate native_windows_gui as nwg; +use std::collections::HashMap; +use std::env::{current_exe, var_os}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; extern crate native_windows_derive as nwd; -use std::sync::Arc; +extern crate native_windows_gui as nwg; use core::cell::RefCell; -use nwd::NwgUi; -use nwg::{NativeUi,ControlHandle}; +use std::sync::Arc; -#[derive(Default,Debug,Clone)] pub struct SystemTrayData { - pub tooltip :String, - pub cfg_p :Vec, - pub cfg_icon :Option, +use nwg::{ControlHandle, NativeUi}; + +#[derive(Default, Debug, Clone)] +pub struct SystemTrayData { + pub tooltip: String, + pub cfg_p: Vec, + pub cfg_icon: Option, +} +#[derive(Default)] +pub struct SystemTray { + pub app_data: RefCell, + /// Store dynamically created tray menu items + pub tray_item_dyn: RefCell>, + /// Store dynamically created tray menu items' handlers + pub handlers_dyn: RefCell>, + /// Store embedded-in-the-binary resources like icons not to load them from a file + pub icon_dyn: RefCell>>, + /// Store embedded-in-the-binary resources like icons not to load them from a file + pub embed: nwg::EmbedResource, + pub icon: nwg::Icon, + pub window: nwg::MessageWindow, + pub tray: nwg::TrayNotification, + pub tray_menu: nwg::Menu, + pub tray_1cfg_m: nwg::Menu, + pub tray_2reload: nwg::MenuItem, + pub tray_3exit: nwg::MenuItem, } -#[derive(Default)] pub struct SystemTray { - pub app_data : RefCell, - /// Store dynamically created tray menu items - pub tray_item_dyn : RefCell>, - /// Store dynamically created tray menu items' handlers - pub handlers_dyn : RefCell>, - /// Store embedded-in-the-binary resources like icons not to load them from a file - pub icon_dyn : RefCell>>, - /// Store embedded-in-the-binary resources like icons not to load them from a file - pub embed : nwg::EmbedResource, - pub icon : nwg::Icon, - pub window : nwg::MessageWindow, - pub tray : nwg::TrayNotification, - pub tray_menu : nwg::Menu, - pub tray_1cfg_m : nwg::Menu, - pub tray_2reload : nwg::MenuItem, - pub tray_3exit : nwg::MenuItem, +pub fn get_appdata() -> Option { + var_os("APPDATA").map(PathBuf::from) +} +pub fn get_user_home() -> Option { + var_os("USERPROFILE").map(PathBuf::from) +} +pub fn get_xdg_home() -> Option { + var_os("XDG_CONFIG_HOME").map(PathBuf::from) } -pub fn get_appdata() -> Option {var_os("APPDATA").map(PathBuf::from)} -pub fn get_user_home() -> Option {var_os("USERPROFILE").map(PathBuf::from)} -pub fn get_xdg_home() -> Option {var_os("XDG_CONFIG_HOME").map(PathBuf::from)} -const CFG_FD: [&str; 3] = ["","kanata","kanata-tray"]; // blank "" allow checking directly for user passed values -const ASSET_FD: [&str; 4] = ["","icon","img","icons"]; -const IMG_EXT: [&str; 7] = ["ico","jpg","jpeg","png","bmp","dds","tiff"]; +const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow checking directly for user passed values +const ASSET_FD: [&str; 4] = ["", "icon", "img", "icons"]; +const IMG_EXT: [&str; 7] = ["ico", "jpg", "jpeg", "png", "bmp", "dds", "tiff"]; use crate::lib_main::CFG; -use winapi::shared::windef::{HWND, HMENU}; + impl SystemTray { - fn show_menu(&self) { - let (x, y) = nwg::GlobalCursor::position(); - self.tray_menu.popup(x, y); } - fn get_icon_p(&self, i:I , s:P ) -> Option - where I:AsRef, P:AsRef {self.get_icon_p_impl(i.as_ref(),s.as_ref())} - fn get_icon_p_impl(&self, icn:&str, p:&Path) -> Option { - let mut icon_file = PathBuf::new(); - let blank_p = Path::new(""); - let icn_p = Path::new(&icn); - let pre_p = p.parent ().unwrap_or_else(| |Path ::new("")); - let nameext = &p.file_name ().unwrap_or_else(| |OsStr ::new("")); - let cur_exe = current_exe ().unwrap_or_else(|_|PathBuf::new( )); - let xdg_cfg = get_xdg_home ().unwrap_or_else(| |PathBuf::new( )); - let app_data = get_appdata ().unwrap_or_else(| |PathBuf::new( )); - let mut user_cfg = get_user_home().unwrap_or_else(| |PathBuf::new( )); user_cfg.push(".config"); - let icn_ext = &icn_p.extension().unwrap_or_else(||OsStr::new("")).to_string_lossy().to_string(); - let is_icn_ext_valid = if ! IMG_EXT.iter().any(|&i| {i==icn_ext}) {warn!("user extension \"{}\" isn't valid!",icn_ext); false} else {trace!("icn_ext={:?}",icn_ext);true}; - let parents = [Path::new(""),pre_p,&cur_exe,&xdg_cfg,&app_data,&user_cfg]; // empty path to allow no prefixes when icon path is explictily set in case it's a full path already - let f_name = [icn_p.as_os_str(),nameext]; - 'p:for p_par in parents {trace!("{}p_par={:?}" ,"" ,p_par); - for p_kan in CFG_FD {trace!("{}p_kan={:?}" ," " ,p_kan); - for p_icn in ASSET_FD {trace!("{}p_icn={:?}" ," " ,p_icn); - for nm in f_name {trace!("{} nm={:?}" ," " ,nm); - for ext in IMG_EXT {trace!("{} ext={:?}" ," " ,ext); - if !(p_par == blank_p){icon_file.push(p_par);} // folders - if ! p_kan.is_empty() {icon_file.push(p_kan);} - if ! p_icn.is_empty() {icon_file.push(p_icn);} - if ! nm.is_empty() {icon_file.push(nm );} - if !( nm == icn_p ){icon_file.push(ext); // no icon name passed, iterate extensions - } else if ! is_icn_ext_valid {icon_file.push(ext);} else{trace!("skip ext");} // replace invalid icon extension - if icon_file == blank_p {continue;} - trace!("testing icon file {:?}",icon_file); - if ! icon_file.is_file() {icon_file.clear(); - if p_par == blank_p && p_kan.is_empty() && p_icn.is_empty() && nm == icn_p {trace!("skipping further iters {:?}",nm); continue 'p} - } else {info!("✓ found icon file: {}",icon_file.display().to_string()); - return Some(icon_file.display().to_string()) - } } } } } } - debug!("✗ no icon file found");return None - } - fn check_active(&self) { - if let Some(cfg) = CFG.get() {let k = cfg.lock(); - let idx_cfg = k.cur_cfg_idx; - let tray_item_dyn = &self.tray_item_dyn.borrow(); // - for (i, h_cfg_i) in tray_item_dyn.iter().enumerate() { - if h_cfg_i.checked(){trace!("✓checked {} active {} eq? {} !eq? {}",i,idx_cfg,i==idx_cfg,!(i==idx_cfg));} - if h_cfg_i.checked() && !(i==idx_cfg){debug!("uncheck i{} act{}",i,idx_cfg);h_cfg_i.set_checked(false);} // uncheck inactive - if ! h_cfg_i.checked() && i==idx_cfg {debug!(" check i{} act{}",i,idx_cfg);h_cfg_i.set_checked(true );} // check active - }; - } else {error!("no CFG var that contains active kanata config"); - }; - } - fn reload(&self,i:Option) { - use nwg::TrayNotificationFlags as f_tray; - let mut msg_title :String = "".to_string(); - let mut msg_content:String = "".to_string(); - let mut flags:f_tray = f_tray::empty(); - if let Some(cfg) = CFG.get() {let mut k = cfg.lock(); - let paths = &k.cfg_paths; - let idx_cfg = match i { - Some(idx) => {if idx k.cur_cfg_idx}; - let path_cur = &paths[idx_cfg]; let path_cur_s = path_cur.display().to_string(); - let path_cur_cc = path_cur.clone(); - msg_content += &path_cur_s; - let cfg_name = &path_cur.file_name().unwrap_or_else(||OsStr::new("")).to_string_lossy().to_string(); - if log_enabled!(Debug) {let cfg_icon = &k.win_tray_icon;debug!("pre reload win_tray_icon={:?}",cfg_icon);} - match i { - Some(idx) => {if let Ok(()) = k.live_reload_n(idx) { - msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" loaded"); flags |= f_tray::USER_ICON; - } else { - msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" NOT loaded"); flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; - self.tray.show(&msg_content, Some(&msg_title), Some(flags), Some(&self.icon)); - return - } - } - None => {if let Ok(()) = k.live_reload ( ) { - msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" reloaded"); flags |= f_tray::USER_ICON; - } else { - msg_title+=&("🔄 \"".to_owned() + cfg_name + "\" NOT reloaded"); flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; - self.tray.show(&msg_content, Some(&msg_title), Some(flags), Some(&self.icon)); - return - } + fn show_menu(&self) { + let (x, y) = nwg::GlobalCursor::position(); + self.tray_menu.popup(x, y); + } + fn get_icon_p(&self, i: I, s: P) -> Option + where + I: AsRef, + P: AsRef, + { + self.get_icon_p_impl(i.as_ref(), s.as_ref()) + } + fn get_icon_p_impl(&self, icn: &str, p: &Path) -> Option { + let mut icon_file = PathBuf::new(); + let blank_p = Path::new(""); + let icn_p = Path::new(&icn); + let pre_p = p.parent().unwrap_or_else(|| Path::new("")); + let nameext = &p.file_name().unwrap_or_else(|| OsStr::new("")); + let cur_exe = current_exe().unwrap_or_else(|_| PathBuf::new()); + let xdg_cfg = get_xdg_home().unwrap_or_else(|| PathBuf::new()); + let app_data = get_appdata().unwrap_or_else(|| PathBuf::new()); + let mut user_cfg = get_user_home().unwrap_or_else(|| PathBuf::new()); + user_cfg.push(".config"); + let icn_ext = &icn_p + .extension() + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy() + .to_string(); + let is_icn_ext_valid = if !IMG_EXT.iter().any(|&i| i == icn_ext) { + warn!("user extension \"{}\" isn't valid!", icn_ext); + false + } else { + trace!("icn_ext={:?}", icn_ext); + true + }; + let parents = [ + Path::new(""), + pre_p, + &cur_exe, + &xdg_cfg, + &app_data, + &user_cfg, + ]; // empty path to allow no prefixes when icon path is explictily set in case it's a full path already + let f_name = [icn_p.as_os_str(), nameext]; + 'p: for p_par in parents { + trace!("{}p_par={:?}", "", p_par); + for p_kan in CFG_FD { + trace!("{}p_kan={:?}", " ", p_kan); + for p_icn in ASSET_FD { + trace!("{}p_icn={:?}", " ", p_icn); + for nm in f_name { + trace!("{} nm={:?}", " ", nm); + for ext in IMG_EXT { + trace!("{} ext={:?}", " ", ext); + if !(p_par == blank_p) { + icon_file.push(p_par); + } // folders + if !p_kan.is_empty() { + icon_file.push(p_kan); + } + if !p_icn.is_empty() { + icon_file.push(p_icn); + } + if !nm.is_empty() { + icon_file.push(nm); + } + if !(nm == icn_p) { + icon_file.push(ext); // no icon name passed, iterate extensions + } else if !is_icn_ext_valid { + icon_file.push(ext); + } else { + trace!("skip ext"); + } // replace invalid icon extension + if icon_file == blank_p { + continue; + } + trace!("testing icon file {:?}", icon_file); + if !icon_file.is_file() { + icon_file.clear(); + if p_par == blank_p + && p_kan.is_empty() + && p_icn.is_empty() + && nm == icn_p + { + trace!("skipping further iters {:?}", nm); + continue 'p; + } + } else { + info!("✓ found icon file: {}", icon_file.display().to_string()); + return Some(icon_file.display().to_string()); + } + } + } + } + } } - }; - let cfg_icon = &k.win_tray_icon; debug!("pos reload win_tray_icon={:?}",cfg_icon); - let mut app_data = self.app_data.borrow_mut(); - app_data.cfg_icon = cfg_icon.clone(); - // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading - self.tray.set_tip(&path_cur_s); // update tooltip to point to the newer config - // self.tray.set_visibility(true); + debug!("✗ no icon file found"); + return None; + } + fn check_active(&self) { + if let Some(cfg) = CFG.get() { + let k = cfg.lock(); + let idx_cfg = k.cur_cfg_idx; + let tray_item_dyn = &self.tray_item_dyn.borrow(); // + for (i, h_cfg_i) in tray_item_dyn.iter().enumerate() { + if h_cfg_i.checked() { + trace!( + "✓checked {} active {} eq? {} !eq? {}", + i, + idx_cfg, + i == idx_cfg, + !(i == idx_cfg) + ); + } + if h_cfg_i.checked() && !(i == idx_cfg) { + debug!("uncheck i{} act{}", i, idx_cfg); + h_cfg_i.set_checked(false); + } // uncheck inactive + if !h_cfg_i.checked() && i == idx_cfg { + debug!(" check i{} act{}", i, idx_cfg); + h_cfg_i.set_checked(true); + } // check active + } + } else { + error!("no CFG var that contains active kanata config"); + }; + } + fn reload(&self, i: Option) { + use nwg::TrayNotificationFlags as f_tray; + let mut msg_title: String = "".to_string(); + let mut msg_content: String = "".to_string(); + let mut flags: f_tray = f_tray::empty(); + if let Some(cfg) = CFG.get() { + let mut k = cfg.lock(); + let paths = &k.cfg_paths; + let idx_cfg = match i { + Some(idx) => { + if idx < paths.len() { + idx + } else { + error!( + "Invalid config index {} while kanata has only {} configs loaded", + idx + 1, + paths.len() + ); + k.cur_cfg_idx + } + } + None => k.cur_cfg_idx, + }; + let path_cur = &paths[idx_cfg]; + let path_cur_s = path_cur.display().to_string(); + let path_cur_cc = path_cur.clone(); + msg_content += &path_cur_s; + let cfg_name = &path_cur + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy() + .to_string(); + if log_enabled!(Debug) { + let cfg_icon = &k.win_tray_icon; + debug!("pre reload win_tray_icon={:?}", cfg_icon); + } + match i { + Some(idx) => { + if let Ok(()) = k.live_reload_n(idx) { + msg_title += &("🔄 \"".to_owned() + cfg_name + "\" loaded"); + flags |= f_tray::USER_ICON; + } else { + msg_title += &("🔄 \"".to_owned() + cfg_name + "\" NOT loaded"); + flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; + self.tray.show( + &msg_content, + Some(&msg_title), + Some(flags), + Some(&self.icon), + ); + return; + } + } + None => { + if let Ok(()) = k.live_reload() { + msg_title += &("🔄 \"".to_owned() + cfg_name + "\" reloaded"); + flags |= f_tray::USER_ICON; + } else { + msg_title += &("🔄 \"".to_owned() + cfg_name + "\" NOT reloaded"); + flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; + self.tray.show( + &msg_content, + Some(&msg_title), + Some(flags), + Some(&self.icon), + ); + return; + } + } + }; + let cfg_icon = &k.win_tray_icon; + debug!("pos reload win_tray_icon={:?}", cfg_icon); + let mut app_data = self.app_data.borrow_mut(); + app_data.cfg_icon = cfg_icon.clone(); + // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading + self.tray.set_tip(&path_cur_s); // update tooltip to point to the newer config + // self.tray.set_visibility(true); - let mut icon_dyn = self.icon_dyn.borrow_mut(); // update the tray icon - if icon_dyn.contains_key(&path_cur_cc) { - if let Some(icon) = icon_dyn.get(&path_cur_cc).unwrap() {self.tray.set_icon(&icon); // - } else {info!("this config has no associated icon, using default: {}",path_cur_cc.display().to_string()); - self.tray.set_icon(&self.icon)} - } else { - let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon {cfg_icon} else {""}; - if let Some(ico_p) = &self.get_icon_p(&cfg_icon_p, &path_cur_cc) { - let mut temp_icon_bitmap = Default::default(); - if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).build(&mut temp_icon_bitmap) { - info!("✓ Using an icon from this config: {}",path_cur_cc.display().to_string()); - let temp_icon = temp_icon_bitmap.copy_as_icon(); - let _ = icon_dyn.insert(path_cur_cc.clone(),Some(temp_icon)); - let temp_icon = temp_icon_bitmap.copy_as_icon(); - self.tray.set_icon(&temp_icon); - } else {warn!("✗ Invalid/no icon from this config: {}",path_cur_cc.display().to_string()); - self.tray.set_icon(&self.icon); - } - } else {warn!("✗ Invalid icon path from this config: {}",path_cur_cc.display().to_string()); - self.tray.set_icon(&self.icon); + let mut icon_dyn = self.icon_dyn.borrow_mut(); // update the tray icon + if icon_dyn.contains_key(&path_cur_cc) { + if let Some(icon) = icon_dyn.get(&path_cur_cc).unwrap() { + self.tray.set_icon(&icon); // + } else { + info!( + "this config has no associated icon, using default: {}", + path_cur_cc.display().to_string() + ); + self.tray.set_icon(&self.icon) + } + } else { + let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon { + cfg_icon + } else { + "" + }; + if let Some(ico_p) = &self.get_icon_p(&cfg_icon_p, &path_cur_cc) { + let mut temp_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder() + .source_file(Some(&ico_p)) + .strict(false) + .build(&mut temp_icon_bitmap) + { + info!( + "✓ Using an icon from this config: {}", + path_cur_cc.display().to_string() + ); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + let _ = icon_dyn.insert(path_cur_cc.clone(), Some(temp_icon)); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + self.tray.set_icon(&temp_icon); + } else { + warn!( + "✗ Invalid/no icon from this config: {}", + path_cur_cc.display().to_string() + ); + self.tray.set_icon(&self.icon); + } + } else { + warn!( + "✗ Invalid icon path from this config: {}", + path_cur_cc.display().to_string() + ); + self.tray.set_icon(&self.icon); + } + } + } else { + msg_title += "✗ Config NOT reloaded, no CFG"; + warn!("{}", msg_title); + flags |= f_tray::ERROR_ICON; + }; + flags |= f_tray::LARGE_ICON; // todo: fails without this, must have SM_CXICON x SM_CYICON? + self.tray.show( + &msg_content, + Some(&msg_title), + Some(flags), + Some(&self.icon), + ); + } + fn exit(&self) { + let handlers = self.handlers_dyn.borrow(); + for handler in handlers.iter() { + nwg::unbind_event_handler(&handler); } - } - } else {msg_title+="✗ Config NOT reloaded, no CFG";warn!("{}", msg_title); flags |= f_tray::ERROR_ICON; - }; - flags |= f_tray::LARGE_ICON; // todo: fails without this, must have SM_CXICON x SM_CYICON? - self.tray.show(&msg_content, Some(&msg_title), Some(flags), Some(&self.icon)); - } - fn exit(&self) { - let handlers = self.handlers_dyn.borrow(); - for handler in handlers.iter() {nwg::unbind_event_handler(&handler);} - nwg::stop_thread_dispatch();} + nwg::stop_thread_dispatch(); + } } pub mod system_tray_ui { - use super::*; - use core::cmp; - use std::rc::Rc; - use std::cell::RefCell; - use std::ops::{Deref, DerefMut}; - use native_windows_gui::{self as nwg, MousePressEvent}; + use super::*; + use core::cmp; + use native_windows_gui::{self as nwg, MousePressEvent}; + use std::cell::RefCell; + use std::ops::Deref; + use std::rc::Rc; - pub struct SystemTrayUi { - inner : Rc, - handler_def : RefCell> - } - - impl nwg::NativeUi for SystemTray { - fn build_ui(mut d: SystemTray) -> Result { - use nwg::Event as E; + pub struct SystemTrayUi { + inner: Rc, + handler_def: RefCell>, + } - let app_data = d.app_data.borrow().clone(); - // d.app_data = RefCell::new(Default::default()); - d.tray_item_dyn = RefCell::new(Default::default()); - d.handlers_dyn = RefCell::new(Default::default()); - // Resources - d.embed = Default::default(); - d.embed = nwg::EmbedResource::load(Some("kanata.exe"))?; - nwg::Icon::builder().source_embed(Some(&d.embed)).source_embed_str(Some("iconMain")).strict(true)/*use sys, not panic, if missing*/ - .build(&mut d.icon)?; + impl nwg::NativeUi for SystemTray { + fn build_ui(mut d: SystemTray) -> Result { + use nwg::Event as E; + let app_data = d.app_data.borrow().clone(); + // d.app_data = RefCell::new(Default::default()); + d.tray_item_dyn = RefCell::new(Default::default()); + d.handlers_dyn = RefCell::new(Default::default()); + // Resources + d.embed = Default::default(); + d.embed = nwg::EmbedResource::load(Some("kanata.exe"))?; + nwg::Icon::builder() + .source_embed(Some(&d.embed)) + .source_embed_str(Some("iconMain")) + .strict(true) /*use sys, not panic, if missing*/ + .build(&mut d.icon)?; - // Controls - nwg::MessageWindow ::builder() - . build( &mut d.window )? ; - nwg::TrayNotification ::builder().parent(&d.window) .icon(Some(&d.icon)) .tip(Some(&app_data.tooltip)) - . build( &mut d.tray )? ; - nwg::Menu ::builder().parent(&d.window) .popup(true)/*context menu*/ // - . build( &mut d.tray_menu )? ; - nwg::Menu ::builder().parent(&d.tray_menu) .text("&F Load config") // - . build( &mut d.tray_1cfg_m )? ; - nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&R Reload config") // - . build( &mut d.tray_2reload )? ; - nwg::MenuItem ::builder().parent(&d.tray_menu) .text("&X Exit\t‹⎈␠⎋") // - . build( &mut d.tray_3exit )? ; + // Controls + nwg::MessageWindow::builder().build(&mut d.window)?; + nwg::TrayNotification::builder() + .parent(&d.window) + .icon(Some(&d.icon)) + .tip(Some(&app_data.tooltip)) + .build(&mut d.tray)?; + nwg::Menu::builder() + .parent(&d.window) + .popup(true) /*context menu*/ // + .build(&mut d.tray_menu)?; + nwg::Menu::builder() + .parent(&d.tray_menu) + .text("&F Load config") // + .build(&mut d.tray_1cfg_m)?; + nwg::MenuItem::builder() + .parent(&d.tray_menu) + .text("&R Reload config") // + .build(&mut d.tray_2reload)?; + nwg::MenuItem::builder() + .parent(&d.tray_menu) + .text("&X Exit\t‹⎈␠⎋") // + .build(&mut d.tray_3exit)?; - {let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut - let mut icon_dyn = d.icon_dyn .borrow_mut(); - const menu_acc:&str = "ASDFGQWERTZXCVBYUIOPHJKLNM"; - let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon {cfg_icon} else {""}; - if (app_data.cfg_p).len() > 0 { - for (i, cfg_p) in app_data.cfg_p.iter().enumerate() { - let i_acc = match i { // menu accelerators from 1–0 then A–Z starting from home row for easier presses - 0..= 8 => format!("&{} ",i+1), - 9 => format!("&{} ",0), - 10..=35 => format!("&{} ",&menu_acc[(i-10)..cmp::min(i-10+1,menu_acc.len())]), - _ => format!(" "), - }; - let cfg_name = &cfg_p.file_name().unwrap_or_else(||OsStr::new("")).to_string_lossy().to_string(); //kanata.kbd - // let menu_text = i_acc + cfg_name; // &1 kanata.kbd - let menu_text = format!("{cfg_name}\t{i_acc}"); // kanata.kbd &1 - let mut menu_item = Default::default(); - if i == 0 {nwg::MenuItem::builder().parent(&d.tray_1cfg_m).text(&menu_text).check(true) .build(&mut menu_item)?; - } else {nwg::MenuItem::builder().parent(&d.tray_1cfg_m).text(&menu_text) .build(&mut menu_item)?; - } - tray_item_dyn.push(menu_item); - if i == 0 { // add icons if exists, hashed by config path (for active config, others will create on load) - if let Some(ico_p) = &d.get_icon_p(&cfg_icon_p, &cfg_p) { - let mut temp_icon_bitmap = Default::default(); - if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).build(&mut temp_icon_bitmap) { - debug!("✓ main 0 config: using icon for {:?}",cfg_p); - let temp_icon = temp_icon_bitmap.copy_as_icon(); - let _ = icon_dyn.insert(cfg_p.clone(),Some(temp_icon)); - let temp_icon = temp_icon_bitmap.copy_as_icon(); - d.tray.set_icon(&temp_icon); - } else {info!("✗ main 0 icon ✓ icon path, using DEFAULT icon for {:?}",cfg_p);} - } else { - debug!("✗ main 0 config: using DEFAULT icon for {:?}",cfg_p); - let mut temp_icon = Default::default(); - nwg::Icon::builder().source_embed(Some(&d.embed)).source_embed_str(Some("iconMain")).strict(true).build(&mut temp_icon)?; - let _ = icon_dyn.insert(cfg_p.clone(),Some(temp_icon)); + { + let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut + let mut icon_dyn = d.icon_dyn.borrow_mut(); + const MENU_ACC: &str = "ASDFGQWERTZXCVBYUIOPHJKLNM"; + let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon { + cfg_icon + } else { + "" + }; + if (app_data.cfg_p).len() > 0 { + for (i, cfg_p) in app_data.cfg_p.iter().enumerate() { + let i_acc = match i { + // menu accelerators from 1–0 then A–Z starting from home row for easier presses + 0..=8 => format!("&{} ", i + 1), + 9 => format!("&{} ", 0), + 10..=35 => format!( + "&{} ", + &MENU_ACC[(i - 10)..cmp::min(i - 10 + 1, MENU_ACC.len())] + ), + _ => format!(" "), + }; + let cfg_name = &cfg_p + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy() + .to_string(); //kanata.kbd + // let menu_text = i_acc + cfg_name; // &1 kanata.kbd + let menu_text = format!("{cfg_name}\t{i_acc}"); // kanata.kbd &1 + let mut menu_item = Default::default(); + if i == 0 { + nwg::MenuItem::builder() + .parent(&d.tray_1cfg_m) + .text(&menu_text) + .check(true) + .build(&mut menu_item)?; + } else { + nwg::MenuItem::builder() + .parent(&d.tray_1cfg_m) + .text(&menu_text) + .build(&mut menu_item)?; + } + tray_item_dyn.push(menu_item); + if i == 0 { + // add icons if exists, hashed by config path (for active config, others will create on load) + if let Some(ico_p) = &d.get_icon_p(&cfg_icon_p, &cfg_p) { + let mut temp_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder() + .source_file(Some(&ico_p)) + .strict(false) + .build(&mut temp_icon_bitmap) + { + debug!("✓ main 0 config: using icon for {:?}", cfg_p); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + d.tray.set_icon(&temp_icon); + } else { + info!( + "✗ main 0 icon ✓ icon path, using DEFAULT icon for {:?}", + cfg_p + ); + } + } else { + debug!("✗ main 0 config: using DEFAULT icon for {:?}", cfg_p); + let mut temp_icon = Default::default(); + nwg::Icon::builder() + .source_embed(Some(&d.embed)) + .source_embed_str(Some("iconMain")) + .strict(true) + .build(&mut temp_icon)?; + let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); + } + } + } + } else { + warn!("Didn't get any config paths from Kanata!") + } } - } - } - } else {warn!("Didn't get any config paths from Kanata!")} - } - let ui = SystemTrayUi { // Wrap-up - inner : Rc::new(d), - handler_def : Default::default(), - }; + let ui = SystemTrayUi { + // Wrap-up + inner: Rc::new(d), + handler_def: Default::default(), + }; - let evt_ui = Rc::downgrade(&ui.inner); // Events - let handle_events = move |evt, _evt_data, handle| { - if let Some(evt_ui) = evt_ui.upgrade() { - match evt { + let evt_ui = Rc::downgrade(&ui.inner); // Events + let handle_events = move |evt, _evt_data, handle| { + if let Some(evt_ui) = evt_ui.upgrade() { + match evt { E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} @@ -272,11 +480,11 @@ pub mod system_tray_ui { } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); } else { match handle { - ControlHandle::MenuItem(parent, id) => { + ControlHandle::MenuItem(_parent, _id) => { let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // for (i, h_cfg) in tray_item_dyn.iter().enumerate() { if &handle == h_cfg { //info!("CONFIG handle i={:?} {:?}",i,&handle); - for (j, h_cfg_j) in tray_item_dyn.iter().enumerate() { + for (_j, h_cfg_j) in tray_item_dyn.iter().enumerate() { if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others h_cfg.set_checked(true); // check self SystemTray::reload(&evt_ui,Some(i)); @@ -288,41 +496,63 @@ pub mod system_tray_ui { }, _ => {} } + } + }; + ui.handler_def + .borrow_mut() + .push(nwg::full_bind_event_handler( + &ui.window.handle, + handle_events, + )); + return Ok(ui); } - }; - ui.handler_def.borrow_mut().push(nwg::full_bind_event_handler(&ui.window.handle, handle_events)); - return Ok(ui); } - } - impl Drop for SystemTrayUi { /// To make sure that everything is freed without issues, the default handler must be unbound. - fn drop(&mut self) { - let mut handlers = self.handler_def.borrow_mut(); - for handler in handlers.drain(0..) {nwg::unbind_event_handler(&handler);} + impl Drop for SystemTrayUi { + /// To make sure that everything is freed without issues, the default handler must be unbound. + fn drop(&mut self) { + let mut handlers = self.handler_def.borrow_mut(); + for handler in handlers.drain(0..) { + nwg::unbind_event_handler(&handler); + } + } + } + impl Deref for SystemTrayUi { + type Target = SystemTray; + fn deref(&self) -> &Self::Target { + &self.inner + } } - } - impl Deref for SystemTrayUi {type Target = SystemTray;fn deref (& self) -> & Self::Target {& self.inner}} } pub fn build_tray(cfg: &Arc>) -> Result { - let k = cfg.lock(); - let paths = &k.cfg_paths; - let cfg_icon = &k.win_tray_icon; - let path_cur = &paths[0]; - let app_data = SystemTrayData { - tooltip : path_cur.display().to_string(), - cfg_p : paths.clone(), - cfg_icon : cfg_icon.clone(),}; - let app = SystemTray {app_data:RefCell::new(app_data), ..Default::default()}; - SystemTray::build_ui(app).context("Failed to build UI") + let k = cfg.lock(); + let paths = &k.cfg_paths; + let cfg_icon = &k.win_tray_icon; + let path_cur = &paths[0]; + let app_data = SystemTrayData { + tooltip: path_cur.display().to_string(), + cfg_p: paths.clone(), + cfg_icon: cfg_icon.clone(), + }; + let app = SystemTray { + app_data: RefCell::new(app_data), + ..Default::default() + }; + SystemTray::build_ui(app).context("Failed to build UI") } pub use log::*; -pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; -pub use winapi::shared::minwindef::BOOL; pub use std::io::{stdout, IsTerminal}; +pub use winapi::shared::minwindef::BOOL; +pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; use once_cell::sync::Lazy; -pub static IS_TERM:Lazy = Lazy::new(||stdout().is_terminal()); -pub static IS_CONSOLE:Lazy = Lazy::new(|| unsafe{ - if AttachConsole(ATTACH_PARENT_PROCESS)== 0i32 {return false} else {return true}}); +pub static IS_TERM: Lazy = Lazy::new(|| stdout().is_terminal()); +pub static IS_CONSOLE: Lazy = Lazy::new(|| unsafe { + if AttachConsole(ATTACH_PARENT_PROCESS) == 0i32 { + return false; + } else { + return true; + } +}); diff --git a/src/main.rs b/src/main.rs index 6d70ae3ad..1ec850200 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,15 @@ #![cfg_attr(feature = "gui", windows_subsystem = "windows")] //disable console on Windows -#[cfg(feature = "gui")] -use kanata_state_machine::m_gui::main_gui; -#[cfg(not(feature = "gui"))] use kanata_state_machine::lib_main::lib_main_cli; -#[cfg( feature = "gui" )] use kanata_state_machine::lib_main::lib_main_gui; +#[cfg(not(feature = "gui"))] +use kanata_state_machine::lib_main::lib_main_cli; +#[cfg(feature = "gui")] +use kanata_state_machine::lib_main::lib_main_gui; -use anyhow::{Result}; #[cfg(not(feature = "gui"))] -use anyhow::{Result}; +use anyhow::Result; #[cfg(not(feature = "gui"))] fn main() -> Result<()> { - let ret = lib_main_cli(); - ret + lib_main_cli() } #[cfg(feature = "gui")] diff --git a/win_dbg_logger/src/lib.rs b/win_dbg_logger/src/lib.rs index 7268a74c5..ab277f082 100644 --- a/win_dbg_logger/src/lib.rs +++ b/win_dbg_logger/src/lib.rs @@ -39,9 +39,9 @@ use log::{Level, LevelFilter, Metadata, Record}; /// It forwards log messages to the Windows `OutputDebugString` API. #[derive(Copy, Clone)] pub struct WinDbgLogger { - level: LevelFilter, - /// Allow for `WinDbgLogger` to possibly have more fields in the future - _priv: (), + level: LevelFilter, + /// Allow for `WinDbgLogger` to possibly have more fields in the future + _priv: (), } /// This is a static instance of `WinDbgLogger`. Since `WinDbgLogger` contains no state, this can be directly registered using `log::set_logger`, e.g.: @@ -54,46 +54,79 @@ pub struct WinDbgLogger { /// /// info!("Hello, world!"); debug!("Hello, world, in detail!"); // Use to log /// ``` -pub static WINDBG_LOGGER :WinDbgLogger = WinDbgLogger {level:LevelFilter::Trace , _priv:()}; -pub static WINDBG_L1 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Error , _priv:()}; -pub static WINDBG_L2 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Warn , _priv:()}; -pub static WINDBG_L3 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Info , _priv:()}; -pub static WINDBG_L4 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Debug , _priv:()}; -pub static WINDBG_L5 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Trace , _priv:()}; -pub static WINDBG_L0 :WinDbgLogger = WinDbgLogger {level:LevelFilter::Off , _priv:()}; +pub static WINDBG_LOGGER: WinDbgLogger = WinDbgLogger { + level: LevelFilter::Trace, + _priv: (), +}; +pub static WINDBG_L1: WinDbgLogger = WinDbgLogger { + level: LevelFilter::Error, + _priv: (), +}; +pub static WINDBG_L2: WinDbgLogger = WinDbgLogger { + level: LevelFilter::Warn, + _priv: (), +}; +pub static WINDBG_L3: WinDbgLogger = WinDbgLogger { + level: LevelFilter::Info, + _priv: (), +}; +pub static WINDBG_L4: WinDbgLogger = WinDbgLogger { + level: LevelFilter::Debug, + _priv: (), +}; +pub static WINDBG_L5: WinDbgLogger = WinDbgLogger { + level: LevelFilter::Trace, + _priv: (), +}; +pub static WINDBG_L0: WinDbgLogger = WinDbgLogger { + level: LevelFilter::Off, + _priv: (), +}; #[cfg(feature = "simple_shared")] -pub fn windbg_simple_combo(log_lvl:LevelFilter) -> Box { - match log_lvl { - LevelFilter::Error => Box::new(WINDBG_L1), - LevelFilter::Warn => Box::new(WINDBG_L2), - LevelFilter::Info => Box::new(WINDBG_L3), - LevelFilter::Debug => Box::new(WINDBG_L4), - LevelFilter::Trace => Box::new(WINDBG_L5), - LevelFilter::Off => Box::new(WINDBG_L0), - } +pub fn windbg_simple_combo(log_lvl: LevelFilter) -> Box { + match log_lvl { + LevelFilter::Error => Box::new(WINDBG_L1), + LevelFilter::Warn => Box::new(WINDBG_L2), + LevelFilter::Info => Box::new(WINDBG_L3), + LevelFilter::Debug => Box::new(WINDBG_L4), + LevelFilter::Trace => Box::new(WINDBG_L5), + LevelFilter::Off => Box::new(WINDBG_L0), + } } #[cfg(feature = "simple_shared")] -impl simplelog::SharedLogger for WinDbgLogger { // allows using with simplelog's CombinedLogger - fn level (&self ) -> LevelFilter {self.level} - fn config(&self ) -> Option<&simplelog::Config> {None} - fn as_log( self:Box) -> Box {Box::new(*self)} +impl simplelog::SharedLogger for WinDbgLogger { + // allows using with simplelog's CombinedLogger + fn level(&self) -> LevelFilter { + self.level + } + fn config(&self) -> Option<&simplelog::Config> { + None + } + fn as_log(self: Box) -> Box { + Box::new(*self) + } } /// Convert logging levels to shorter and more visible icons -pub fn iconify(lvl: log::Level) -> char { match lvl { - Level::Error => '❗', - Level::Warn => '⚠', - Level::Info => 'ⓘ', - Level::Debug => 'ⓓ', - Level::Trace => 'ⓣ', -}} +pub fn iconify(lvl: log::Level) -> char { + match lvl { + Level::Error => '❗', + Level::Warn => '⚠', + Level::Info => 'ⓘ', + Level::Debug => 'ⓓ', + Level::Trace => 'ⓣ', + } +} use std::sync::OnceLock; -pub fn is_thread_state() -> &'static bool {set_thread_state(false)} -pub fn set_thread_state(is: bool) -> &'static bool { // accessor function to avoid get_or_init on every call (lazycell allows doing that without an extra function) - static CELL: OnceLock = OnceLock::new(); - CELL.get_or_init(|| is) +pub fn is_thread_state() -> &'static bool { + set_thread_state(false) +} +pub fn set_thread_state(is: bool) -> &'static bool { + // accessor function to avoid get_or_init on every call (lazycell allows doing that without an extra function) + static CELL: OnceLock = OnceLock::new(); + CELL.get_or_init(|| is) } use lazy_static::lazy_static; @@ -102,37 +135,45 @@ lazy_static! { // shorten source file name, no src/ no .rs ext static ref reExt:Regex = Regex::new(r"\..*$" ).unwrap(); static ref reSrc:Regex = Regex::new(r"src[\\/]").unwrap(); } -fn clean_name(path: Option<&str>) -> String { // remove extension and src paths - if let Some(p) = path {reSrc.replace(&reExt.replace(p, ""), "").to_string() - } else {"?".to_string() - } +fn clean_name(path: Option<&str>) -> String { + // remove extension and src paths + if let Some(p) = path { + reSrc.replace(&reExt.replace(p, ""), "").to_string() + } else { + "?".to_string() + } } #[cfg(target_os = "windows")] use winapi::um::processthreadsapi::GetCurrentThreadId; impl log::Log for WinDbgLogger { - fn enabled(&self, metadata: &Metadata) -> bool {metadata.level() <= self.level} - - fn log(&self, record: &Record) { - #[cfg(not(target_os = "windows"))] - let thread_id = ""; - #[cfg( target_os = "windows" )] - let thread_id = if *is_thread_state() {format!("{}¦", unsafe { GetCurrentThreadId() }) - } else {"".to_string()}; - if self.enabled(record.metadata()) { - let s = format!( - "{}{}{}:{} {}", - thread_id, - iconify(record.level()), - clean_name(record.file()), - record.line().unwrap_or(0), - record.args() - ); - output_debug_string(&s); + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= self.level } - } - fn flush(&self) {} + fn log(&self, record: &Record) { + #[cfg(not(target_os = "windows"))] + let thread_id = ""; + #[cfg(target_os = "windows")] + let thread_id = if *is_thread_state() { + format!("{}¦", unsafe { GetCurrentThreadId() }) + } else { + "".to_string() + }; + if self.enabled(record.metadata()) { + let s = format!( + "{}{}{}:{} {}", + thread_id, + iconify(record.level()), + clean_name(record.file()), + record.line().unwrap_or(0), + record.args() + ); + output_debug_string(&s); + } + } + + fn flush(&self) {} } /// Calls the `OutputDebugString` API to log a string. @@ -141,19 +182,26 @@ impl log::Log for WinDbgLogger { /// /// See [`OutputDebugStringW`](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-outputdebugstringw). pub fn output_debug_string(s: &str) { - #[cfg(windows)]{ - let len = s.encode_utf16().count() + 1; - let mut s_utf16: Vec = Vec::with_capacity(len); - s_utf16.extend(s.encode_utf16()); - s_utf16.push(0); - unsafe {OutputDebugStringW(&s_utf16[0]);} - } - #[cfg(not(windows))]{let _ = s;} + #[cfg(windows)] + { + let len = s.encode_utf16().count() + 1; + let mut s_utf16: Vec = Vec::with_capacity(len); + s_utf16.extend(s.encode_utf16()); + s_utf16.push(0); + unsafe { + OutputDebugStringW(&s_utf16[0]); + } + } + #[cfg(not(windows))] + { + let _ = s; + } } -#[cfg(windows)] extern "stdcall" { - fn OutputDebugStringW(chars: *const u16); - fn IsDebuggerPresent() -> i32; +#[cfg(windows)] +extern "stdcall" { + fn OutputDebugStringW(chars: *const u16); + fn IsDebuggerPresent() -> i32; } /// Checks whether a debugger is attached to the current process. @@ -162,8 +210,14 @@ pub fn output_debug_string(s: &str) { /// /// See [`IsDebuggerPresent`](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-isdebuggerpresent). pub fn is_debugger_present() -> bool { - #[cfg( windows) ]{unsafe {IsDebuggerPresent() != 0 }} - #[cfg(not(windows))]{ false } + #[cfg(windows)] + { + unsafe { IsDebuggerPresent() != 0 } + } + #[cfg(not(windows))] + { + false + } } /// Sets the `WinDbgLogger` as the currently-active logger. @@ -172,36 +226,45 @@ pub fn is_debugger_present() -> bool { /// This behavior was chosen because `WinDbgLogger` is intended for use in debugging. /// Panicking would disrupt debugging and introduce new failure modes. It would also create problems for mixed-mode debugging, where Rust code is linked with C/C++ code. pub fn init() { - match log::set_logger(&WINDBG_LOGGER) { - Ok(()) => {} //↓ there's really nothing we can do about it. - Err(_) => {output_debug_string("Warning: Failed to register WinDbgLogger as the current Rust logger.\r\n",);} - } + match log::set_logger(&WINDBG_LOGGER) { + Ok(()) => {} //↓ there's really nothing we can do about it. + Err(_) => { + output_debug_string( + "Warning: Failed to register WinDbgLogger as the current Rust logger.\r\n", + ); + } + } } -macro_rules! define_init_at_level {($func:ident, $level:ident) => { - /// This can be called from C/C++ code to register the debug logger. - /// - /// For Windows DLLs that have statically linked an instance of `win_dbg_logger` into them, `DllMain` should call `win_dbg_logger_init_()` from the `DLL_PROCESS_ATTACH` handler, e.g.: - /// - /// ```ignore - /// extern "C" void __cdecl rust_win_dbg_logger_init_debug(); // Calls into Rust code - /// BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD reason, LPVOID reserved) { - /// switch (reason) { - /// case DLL_PROCESS_ATTACH: - /// rust_win_dbg_logger_init_debug(); - /// // ... - /// } - /// // ... - /// } - /// ``` - /// - /// For Windows executables that have statically linked an instance of `win_dbg_logger` into them, call `win_dbg_logger_init_()` during app startup. - #[no_mangle] pub extern "C" fn $func() {init();log::set_max_level(LevelFilter::$level);} - }; +macro_rules! define_init_at_level { + ($func:ident, $level:ident) => { + /// This can be called from C/C++ code to register the debug logger. + /// + /// For Windows DLLs that have statically linked an instance of `win_dbg_logger` into them, `DllMain` should call `win_dbg_logger_init_()` from the `DLL_PROCESS_ATTACH` handler, e.g.: + /// + /// ```ignore + /// extern "C" void __cdecl rust_win_dbg_logger_init_debug(); // Calls into Rust code + /// BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD reason, LPVOID reserved) { + /// switch (reason) { + /// case DLL_PROCESS_ATTACH: + /// rust_win_dbg_logger_init_debug(); + /// // ... + /// } + /// // ... + /// } + /// ``` + /// + /// For Windows executables that have statically linked an instance of `win_dbg_logger` into them, call `win_dbg_logger_init_()` during app startup. + #[no_mangle] + pub extern "C" fn $func() { + init(); + log::set_max_level(LevelFilter::$level); + } + }; } -define_init_at_level!(rust_win_dbg_logger_init_trace , Trace); -define_init_at_level!(rust_win_dbg_logger_init_info , Info); -define_init_at_level!(rust_win_dbg_logger_init_debug , Debug); -define_init_at_level!(rust_win_dbg_logger_init_warn , Warn); -define_init_at_level!(rust_win_dbg_logger_init_error , Error); +define_init_at_level!(rust_win_dbg_logger_init_trace, Trace); +define_init_at_level!(rust_win_dbg_logger_init_info, Info); +define_init_at_level!(rust_win_dbg_logger_init_debug, Debug); +define_init_at_level!(rust_win_dbg_logger_init_warn, Warn); +define_init_at_level!(rust_win_dbg_logger_init_error, Error); From b50362fb495e7290180dc96d83c6c77fa4f84dab Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 20:29:08 +0700 Subject: [PATCH 043/154] clippy --- src/lib.rs | 2 ++ src/lib_main.rs | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index aca6a3185..fa25aa6fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ use std::str::FromStr; pub mod kanata; pub mod lib_main; +#[cfg(test)] +pub mod tests; #[cfg(all(target_os = "windows", feature = "gui"))] pub mod m_gui_win; pub mod oskbd; diff --git a/src/lib_main.rs b/src/lib_main.rs index a791ff924..849769345 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -8,9 +8,6 @@ use log::info; use simplelog::{format_description, *}; use std::path::PathBuf; -#[cfg(test)] -mod tests; - #[derive(Parser, Debug)] #[command(author, version, verbatim_doc_comment)] /// kanata: an advanced software key remapper From 4e0d51c8829ef123fdac8c9b31f4a65c16fb54b6 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 20:37:50 +0700 Subject: [PATCH 044/154] clippy fix test import --- src/tests/sim_tests/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/sim_tests/mod.rs b/src/tests/sim_tests/mod.rs index 5d013a55c..12f55a308 100644 --- a/src/tests/sim_tests/mod.rs +++ b/src/tests/sim_tests/mod.rs @@ -5,7 +5,7 @@ //! and see if the real output looks sensible according to what is expected. use crate::tests::*; -use kanata_state_machine::{ +use crate::{ oskbd::{KeyEvent, KeyValue}, str_to_oscode, Kanata, }; From 4ed6e22966c70486bdf6930796821d329a78653e Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 20:41:44 +0700 Subject: [PATCH 045/154] ahk: silence debug msgbox --- EnableUIAccess/Lib/EnableUIAccess.ahk | 1 - 1 file changed, 1 deletion(-) diff --git a/EnableUIAccess/Lib/EnableUIAccess.ahk b/EnableUIAccess/Lib/EnableUIAccess.ahk index 043a5e740..b584efc37 100644 --- a/EnableUIAccess/Lib/EnableUIAccess.ahk +++ b/EnableUIAccess/Lib/EnableUIAccess.ahk @@ -248,7 +248,6 @@ EnableUIAccess_SignFile(ExePath, CertCtx, Name) { hr := DllCall("MSSign32\SignerSign" , "ptr",subject_info, "ptr",cert_info, "ptr",sig_info , "ptr",0, "ptr",0, "ptr",0, "ptr",0, "hresult") ; pProviderInfo pwszHttpTimeStamp psRequest pSipData - msgbox(' hr= ' hr) struct(args*) => ( args.Push(b := Buffer(args[2], 0)), From d1681e9ed871db5aa7e74fda4977817d36b35d3b Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 20:59:44 +0700 Subject: [PATCH 046/154] doc: update 'gui' feature --- docs/config.adoc | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 8316ddd45..91d68048d 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2394,8 +2394,8 @@ they are an arbitrary length and can be very long. === Windows only: win-tray-icon <> -Show a custom tray icon file for a gui-enabled build of kanata on Windows. -Accepts either the full path (including the file name with extension) to the icon file +Show a custom tray icon file for a <> gui-enabled build of kanata on Windows. +Accepts either the full path (including the file name with an extension) to the icon file or just the file name, which is then searched in the following locations: * Default parent folders: @@ -2405,7 +2405,7 @@ or just the file name, which is then searched in the following locations: * Default image subfolders (optional): `icon` `img` `icons` * Supported image file formats: `ico` `jpg` `jpeg` `png` `bmp` `dds` `tiff` -If not specified, ttries to load any icon file from the same locations with the name matching +If not specified, tries to load any icon file from the same locations with the name matching config name with extension replaced by one of the supported ones. .Example: @@ -3429,6 +3429,22 @@ to enable the use of the `EnableUIAccess` script: cargo build --win_manifest ``` +[[windows-only-win-tray]] +=== Windows only: win-tray +<> + +Kanata can be compiled as a Windows GUI tray app with the feature flag `gui`. +This can simplify launching the app on user login by placing a `.lnk` +at `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, show custom icon indicator per config +(see <>). It also supports (re)loading configs. + +Currently the only configuration supported is tray icon per profile, all other configuration should +be done by passing cli flags in the `Target` field of `.lnk`, e.g., `"C:\Program Files\kanata\kanata.exe" -d -n` +to launch kanata without a delay in a debug mode + +When launched from a command line, the app outputs log to the console, but otherwise the logs are currently +only available via an app capable of viewing `OutputDebugString` debugs, e.g., https://github.com/smourier/TraceSpy[TraceSpy]. + [[test-your-config]] === Test your config <> From 145a57cdc6bf74a830bbbbd562357b512c08b7bc Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 20:33:57 +0700 Subject: [PATCH 047/154] __fix weird doc test fail --- src/lib_main.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib_main.rs b/src/lib_main.rs index 849769345..8e0134dec 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -14,14 +14,10 @@ use std::path::PathBuf; /// /// kanata remaps key presses to other keys or complex actions depending on the /// configuration for that key. You can find the guide for creating a config -/// file here: -/// -/// https://github.com/jtroo/kanata/blob/main/docs/config.adoc +/// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc /// /// If you need help, please feel welcome to create an issue or discussion in -/// the kanata repository: -/// -/// https://github.com/jtroo/kanata +/// the kanata repository: https://github.com/jtroo/kanata struct Args { // Display different platform specific paths based on the target OS #[cfg_attr( From 663c1ef3d7604f5796a8bb10f0db22df86eb6be8 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 28 Apr 2024 21:24:22 +0700 Subject: [PATCH 048/154] fmt --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fa25aa6fa..1c3848eb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,12 +5,12 @@ use std::str::FromStr; pub mod kanata; pub mod lib_main; -#[cfg(test)] -pub mod tests; #[cfg(all(target_os = "windows", feature = "gui"))] pub mod m_gui_win; pub mod oskbd; pub mod tcp_server; +#[cfg(test)] +pub mod tests; #[cfg(all(target_os = "windows", feature = "gui"))] pub use m_gui_win::*; #[cfg(all(target_os = "windows", feature = "gui"))] From 684def041c5391e5c5183463dfe8af4657db13f5 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 30 Apr 2024 14:25:59 +0700 Subject: [PATCH 049/154] doc: update AHK install instructions Co-authored-by: jtroo --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index 91d68048d..d3c0a435d 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3420,7 +3420,7 @@ The default `kanata.exe` binary doesn't work in elevated windows (run with admin e.g., `Control Panel`. However, you can use AutoHotkey's "EnableUIAccess" script to self-sign the binary, move it to "Program Files", then launching kanata from there will also work in these elevated windows. See https://github.com/jtroo/kanata/blob/main/EnableUIAccess[EnableUIAccess] folder with the script -and its requires libraries (needs https://www.autohotkey.com/download/ahk-v2.exe[AutoHotkey v2] installed) +and its required libraries (needs https://www.autohotkey.com/download/[AutoHotkey v2] installed) If compiling yourself, you should add the feature flag `win_manifest` to enable the use of the `EnableUIAccess` script: From 6a1d414bec407b4f67275c3fbd0c199a1bda6158 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:17:04 +0700 Subject: [PATCH 050/154] format --- src/kanata/windows/llhook.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 3ad5325e6..601770c32 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -76,11 +76,8 @@ impl Kanata { #[cfg(all(target_os = "windows", feature = "gui"))] let _ui = build_tray(&_kanata)?; - native_windows_gui::dispatch_thread_events(); // The event loop is also required for the low-level keyboard hook to work. - // if *IS_TERM { - // eprintln!("\nPress enter to exit"); // moved from main to not panic on a disconnected channel - // let _ = std::io::stdin().read_line(&mut String::new()); - // } + // The event loop is also required for the low-level keyboard hook to work. + native_windows_gui::dispatch_thread_events(); Ok(()) } } From 3249fc5f37c964b54de88fc031db4070646f7cd2 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:19:55 +0700 Subject: [PATCH 051/154] format --- src/kanata/windows/llhook.rs | 4 ++-- src/m_gui_win.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 601770c32..af7a0065c 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -10,9 +10,9 @@ use crate::kanata::*; #[cfg(all(target_os = "windows", feature = "gui"))] use crate::m_gui_win::*; #[cfg(all(target_os = "windows", feature = "gui"))] -extern crate native_windows_derive as nwd; +crate native_windows_derive as nwd; #[cfg(all(target_os = "windows", feature = "gui"))] -extern crate native_windows_gui as nwg; +crate native_windows_gui as nwg; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events diff --git a/src/m_gui_win.rs b/src/m_gui_win.rs index a0eb65d29..91d672142 100644 --- a/src/m_gui_win.rs +++ b/src/m_gui_win.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; use std::env::{current_exe, var_os}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -extern crate native_windows_derive as nwd; -extern crate native_windows_gui as nwg; +crate native_windows_derive as nwd; +crate native_windows_gui as nwg; use core::cell::RefCell; use std::sync::Arc; From d8a0ba30f1cc90755549e2446736c9feb4f49041 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:24:09 +0700 Subject: [PATCH 052/154] dep: move NWG features needed for win-tray to the "gui" feature --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e0eb1299b..9e712d045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ winapi = { version = "0.3.9", features = [ "mmsystem", ] } win_dbg_logger = { path = "win_dbg_logger", optional = true } -native-windows-gui = { version = "1.0.13", default_features = false, features=["tray-notification","message-window","menu","cursor","high-dpi","embed-resource","image-decoder"] } +native-windows-gui = { version = "1.0.13", default_features = false} native-windows-derive = { version = "1.0.5", default_features = false, optional = true } lazy_static = { version = "1.4.0", optional = true } regex = { version = "1.10.4", optional = true } @@ -103,7 +103,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] -gui = ["win_manifest","native-windows-derive","lazy_static","win_dbg_logger","win_dbg_logger/simple_shared","kanata-parser/gui"] +gui = ["win_manifest","native-windows-derive","lazy_static","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"] [profile.release] opt-level = "z" From 8eeec543dfc2c24103456a54e4dd295716b00680 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:32:10 +0700 Subject: [PATCH 053/154] remove unused cfg guards --- src/kanata/windows/llhook.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index af7a0065c..ba0aa84d1 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -7,11 +7,11 @@ use std::time; use super::PRESSED_KEYS; use crate::kanata::*; -#[cfg(all(target_os = "windows", feature = "gui"))] +#[cfg(feature = "gui")] use crate::m_gui_win::*; -#[cfg(all(target_os = "windows", feature = "gui"))] +#[cfg(feature = "gui")] crate native_windows_derive as nwd; -#[cfg(all(target_os = "windows", feature = "gui"))] +#[cfg(feature = "gui")] crate native_windows_gui as nwg; impl Kanata { @@ -73,7 +73,7 @@ impl Kanata { try_send_panic(&preprocess_tx, key_event); true }); - #[cfg(all(target_os = "windows", feature = "gui"))] + #[cfg(feature = "gui")] let _ui = build_tray(&_kanata)?; // The event loop is also required for the low-level keyboard hook to work. From 61ee020e147181ec647fed52c37721ddda43f826 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:32:43 +0700 Subject: [PATCH 054/154] move win-specific functions to Windows module --- src/kanata/mod.rs | 22 ---------------------- src/kanata/windows/mod.rs | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index c09c18cb3..ca07572be 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -461,28 +461,6 @@ impl Kanata { }) } - #[cfg(all(target_os = "windows", feature = "gui"))] - pub fn live_reload(&mut self) -> Result<()> { - self.live_reload_requested = true; - self.do_live_reload(&None)?; - Ok(()) - } - #[cfg(all(target_os = "windows", feature = "gui"))] - pub fn live_reload_n(&mut self, n: usize) -> Result<()> { - // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow - self.live_reload_requested = true; - match self.cfg_paths.get(n) { - Some(path) => { - self.cur_cfg_idx = n; - log::info!("Requested live reload of file: {}", path.display(),); - } - None => { - log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); - } - } - self.do_live_reload(&None)?; - Ok(()) - } fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index 513999d93..c9ba949d3 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -127,4 +127,27 @@ impl Kanata { pub fn check_release_non_physical_shift(&mut self) -> Result<()> { Ok(()) } + + #[cfg(feature = "gui")] + pub fn live_reload(&mut self) -> Result<()> { + self.live_reload_requested = true; + self.do_live_reload(&None)?; + Ok(()) + } + #[cfg(feature = "gui")] + pub fn live_reload_n(&mut self, n: usize) -> Result<()> { + // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow + self.live_reload_requested = true; + match self.cfg_paths.get(n) { + Some(path) => { + self.cur_cfg_idx = n; + log::info!("Requested live reload of file: {}", path.display(),); + } + None => { + log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); + } + } + self.do_live_reload(&None)?; + Ok(()) + } } From e2ac98fdba8c041b6afd990a9f9abecffdd250c7 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:36:52 +0700 Subject: [PATCH 055/154] add unknown to support WASM in new wintray cfg --- parser/src/cfg/defcfg.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index c47d61e0b..ada235190 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -53,7 +53,7 @@ pub struct CfgOptions { pub windows_interception_keyboard_hwids: Option>, #[cfg(any(target_os = "macos", target_os = "unknown"))] pub macos_dev_names_include: Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] + #[cfg(all(any(target_os = "windows",target_os = "unknown"), feature = "gui"))] pub win_tray_icon: Option, } @@ -107,7 +107,7 @@ impl Default for CfgOptions { windows_interception_keyboard_hwids: None, #[cfg(any(target_os = "macos", target_os = "unknown"))] macos_dev_names_include: None, - #[cfg(all(target_os = "windows", feature = "gui"))] + #[cfg(all(any(target_os = "windows",target_os = "unknown"), feature = "gui"))] win_tray_icon: None, } } @@ -394,7 +394,7 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { } } "win-tray-icon" => { - #[cfg(all(target_os = "windows", feature = "gui"))] + #[cfg(all(any(target_os = "windows",target_os = "unknown"), feature = "gui"))] { let icon_path = sexpr_to_str_or_err(val, label)?; if icon_path.is_empty() { From 8b72e1f3b9c29142482448145d0755b57f04751f Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:38:02 +0700 Subject: [PATCH 056/154] update icon --- assets/kanata.ico | Bin 10366 -> 15086 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/kanata.ico b/assets/kanata.ico index cc5945c629bf9fc210064a16c2af7642e9941aa2..80838d3d6d5569d0e95e5e0faae305731d391f3a 100644 GIT binary patch literal 15086 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 Date: Tue, 30 Apr 2024 14:39:56 +0700 Subject: [PATCH 057/154] remove m_ --- src/{m_gui_win.rs => gui_win.rs} | 0 src/kanata/windows/llhook.rs | 2 +- src/lib.rs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{m_gui_win.rs => gui_win.rs} (100%) diff --git a/src/m_gui_win.rs b/src/gui_win.rs similarity index 100% rename from src/m_gui_win.rs rename to src/gui_win.rs diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index ba0aa84d1..8fedd3e51 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -8,7 +8,7 @@ use std::time; use super::PRESSED_KEYS; use crate::kanata::*; #[cfg(feature = "gui")] -use crate::m_gui_win::*; +use crate::gui_win::*; #[cfg(feature = "gui")] crate native_windows_derive as nwd; #[cfg(feature = "gui")] diff --git a/src/lib.rs b/src/lib.rs index 1c3848eb9..87d7785f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,13 +6,13 @@ use std::str::FromStr; pub mod kanata; pub mod lib_main; #[cfg(all(target_os = "windows", feature = "gui"))] -pub mod m_gui_win; +pub mod gui_win; pub mod oskbd; pub mod tcp_server; #[cfg(test)] pub mod tests; #[cfg(all(target_os = "windows", feature = "gui"))] -pub use m_gui_win::*; +pub use gui_win::*; #[cfg(all(target_os = "windows", feature = "gui"))] pub use win_dbg_logger as log_win; #[cfg(all(target_os = "windows", feature = "gui"))] From e82bd9da81f8b02f62a53d64d9bd8933686a0468 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:41:47 +0700 Subject: [PATCH 058/154] cargo fmt --- parser/src/cfg/defcfg.rs | 7 +++++-- src/gui_win.rs | 6 +++--- src/kanata/windows/llhook.rs | 6 +++--- src/lib.rs | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index ada235190..46b1f80ad 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -53,7 +53,7 @@ pub struct CfgOptions { pub windows_interception_keyboard_hwids: Option>, #[cfg(any(target_os = "macos", target_os = "unknown"))] pub macos_dev_names_include: Option>, - #[cfg(all(any(target_os = "windows",target_os = "unknown"), feature = "gui"))] + #[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))] pub win_tray_icon: Option, } @@ -394,7 +394,10 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { } } "win-tray-icon" => { - #[cfg(all(any(target_os = "windows",target_os = "unknown"), feature = "gui"))] + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] { let icon_path = sexpr_to_str_or_err(val, label)?; if icon_path.is_empty() { diff --git a/src/gui_win.rs b/src/gui_win.rs index 91d672142..7c35a1c40 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -1,14 +1,14 @@ use crate::Kanata; use anyhow::{Context, Result}; +use core::cell::RefCell; use log::Level::Debug; +use native_windows_derive as nwd; +use native_windows_gui as nwg; use parking_lot::Mutex; use std::collections::HashMap; use std::env::{current_exe, var_os}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -crate native_windows_derive as nwd; -crate native_windows_gui as nwg; -use core::cell::RefCell; use std::sync::Arc; use nwg::{ControlHandle, NativeUi}; diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 8fedd3e51..3cc25365e 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -6,13 +6,13 @@ use std::sync::Arc; use std::time; use super::PRESSED_KEYS; -use crate::kanata::*; #[cfg(feature = "gui")] use crate::gui_win::*; +use crate::kanata::*; #[cfg(feature = "gui")] -crate native_windows_derive as nwd; +use native_windows_derive as nwd; #[cfg(feature = "gui")] -crate native_windows_gui as nwg; +use native_windows_gui as nwg; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events diff --git a/src/lib.rs b/src/lib.rs index 87d7785f2..b94660d86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,10 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; -pub mod kanata; -pub mod lib_main; #[cfg(all(target_os = "windows", feature = "gui"))] pub mod gui_win; +pub mod kanata; +pub mod lib_main; pub mod oskbd; pub mod tcp_server; #[cfg(test)] From 81b212efdaf6003477574c3cdfc0a550aff33920 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:45:38 +0700 Subject: [PATCH 059/154] remove unused imports --- src/gui_win.rs | 1 - src/kanata/windows/llhook.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 7c35a1c40..ae71935a1 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -2,7 +2,6 @@ use crate::Kanata; use anyhow::{Context, Result}; use core::cell::RefCell; use log::Level::Debug; -use native_windows_derive as nwd; use native_windows_gui as nwg; use parking_lot::Mutex; use std::collections::HashMap; diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 3cc25365e..f5af17b58 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -9,10 +9,6 @@ use super::PRESSED_KEYS; #[cfg(feature = "gui")] use crate::gui_win::*; use crate::kanata::*; -#[cfg(feature = "gui")] -use native_windows_derive as nwd; -#[cfg(feature = "gui")] -use native_windows_gui as nwg; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events From c81d4bbc24d6dfc6f9d5583415cc777b2ff235a9 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Tue, 30 Apr 2024 14:49:39 +0700 Subject: [PATCH 060/154] remove warning on empty icon config --- src/gui_win.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index ae71935a1..f67cc0a2d 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -80,13 +80,14 @@ impl SystemTray { .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); - let is_icn_ext_valid = if !IMG_EXT.iter().any(|&i| i == icn_ext) { - warn!("user extension \"{}\" isn't valid!", icn_ext); - false - } else { - trace!("icn_ext={:?}", icn_ext); - true - }; + let is_icn_ext_valid = + if !IMG_EXT.iter().any(|&i| i == icn_ext) && icn_p.extension().is_some() { + warn!("user extension \"{}\" isn't valid!", icn_ext); + false + } else { + trace!("icn_ext={:?}", icn_ext); + true + }; let parents = [ Path::new(""), pre_p, From 72d974e439a9ddbbc86c19c8a0c7e941c2d7915d Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:19:24 +0700 Subject: [PATCH 061/154] gui: dep: add notice NWG feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9e712d045..9fb5ab01e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] -gui = ["win_manifest","native-windows-derive","lazy_static","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"] +gui = ["win_manifest","native-windows-derive","lazy_static","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"] [profile.release] opt-level = "z" From 9218979c97d0dfc566f02c5888ead8cb613db3ad Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:22:25 +0700 Subject: [PATCH 062/154] gui: move gui creation from llhook to lib main to allow passing gui notifier to the loop --- src/kanata/windows/llhook.rs | 3 --- src/lib_main.rs | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index f5af17b58..65db78b16 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -22,7 +22,6 @@ impl Kanata { panic!("Could not attach to console"); } }; - native_windows_gui::init().context("Failed to init Native Windows GUI")?; let (preprocess_tx, preprocess_rx) = sync_channel(100); start_event_preprocessor(preprocess_rx, tx); @@ -69,8 +68,6 @@ impl Kanata { try_send_panic(&preprocess_tx, key_event); true }); - #[cfg(feature = "gui")] - let _ui = build_tray(&_kanata)?; // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); diff --git a/src/lib_main.rs b/src/lib_main.rs index 8e0134dec..6401ee114 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -255,8 +255,25 @@ fn main_impl() -> Result<()> { (None, None, None) }; + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); + #[cfg(all(target_os = "windows", feature = "gui"))] + use crate native_windows_gui as nwg; + #[cfg(all(target_os = "windows", feature = "gui"))] + use anyhow::Context; + #[cfg(all(target_os = "windows", feature = "gui"))] + native_windows_gui::init().context("Failed to init Native Windows GUI")?; + #[cfg(all(target_os = "windows", feature = "gui"))] + let ui = build_tray(&kanata_arc)?; + #[cfg(all(target_os = "windows", feature = "gui"))] + let noticer:&nwg::Notice = &ui.layer_notice; + #[cfg(all(target_os = "windows", feature = "gui"))] + let gui_tx = noticer.sender(); + #[cfg(all(target_os = "windows", feature = "gui"))] + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, gui_tx, args.nodelay); + + if let (Some(server), Some(nrx)) = (server, nrx) { #[allow(clippy::unit_arg)] Kanata::start_notification_loop(nrx, server.connections); From b9c440bcf8164a2b552137f83b0ca7d1ddf78bd7 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:29:23 +0700 Subject: [PATCH 063/154] gui: let processing loop notify on layer changes --- src/kanata/mod.rs | 43 +++++++++++++++++++++++++++++++-------- src/kanata/windows/mod.rs | 11 ++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index ca07572be..be3fdfa0d 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -461,7 +461,8 @@ impl Kanata { }) } - fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { + fn do_live_reload(&mut self, _tx: &Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx:nwg::NoticeSender) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, Err(e) => { @@ -539,6 +540,8 @@ impl Kanata { } } } + #[cfg(all(target_os = "windows", feature = "gui"))] + gui_tx.notice(); Ok(()) } @@ -581,7 +584,8 @@ impl Kanata { /// Advance keyberon layout state and send events based on changes to its state. /// Returns the number of ticks that elapsed. - fn handle_time_ticks(&mut self, tx: &Option>) -> Result { + fn handle_time_ticks(&mut self, tx: &Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx:nwg::NoticeSender) -> Result { const NS_IN_MS: u128 = 1_000_000; let now = instant::Instant::now(); let ns_elapsed = now.duration_since(self.last_tick).as_nanos(); @@ -604,7 +608,10 @@ impl Kanata { _ => instant::Instant::now(), }; + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] self.check_handle_layer_change(tx); + #[cfg(all(target_os = "windows", feature = "gui"))] + self.check_handle_layer_change(tx,gui_tx); if self.live_reload_requested && ((self.prev_keys.is_empty() && self.cur_keys.is_empty()) @@ -620,9 +627,10 @@ impl Kanata { // activate. Having this fallback allows live reload to happen which resets the // kanata states. self.live_reload_requested = false; - if let Err(e) = self.do_live_reload(tx) { - log::error!("live reload failed {e}"); - } + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + if let Err(e) = self.do_live_reload(tx) {log::error!("live reload failed {e}");} + #[cfg(all(target_os = "windows", feature = "gui"))] + if let Err(e) = self.do_live_reload(tx,gui_tx) {log::error!("live reload failed {e}");} } #[cfg(feature = "perf_logging")] @@ -1499,7 +1507,8 @@ impl Kanata { #[allow(unused_variables)] /// Prints the layer. If the TCP server is enabled, then this will also send a notification to /// all connected clients. - fn check_handle_layer_change(&mut self, tx: &Option>) { + fn check_handle_layer_change(&mut self, tx: &Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx:nwg::NoticeSender) { let cur_layer = self.layout.bm().current_layer(); if cur_layer != self.prev_layer { let new = self.layer_info[cur_layer].name.clone(); @@ -1515,6 +1524,8 @@ impl Kanata { } } } + #[cfg(all(target_os = "windows", feature = "gui"))] + gui_tx.notice(); } } @@ -1578,6 +1589,8 @@ impl Kanata { kanata: Arc>, rx: Receiver, tx: Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] + gui_tx: nwg::NoticeSender, nodelay: bool, ) { info!("entering the processing loop"); @@ -1718,7 +1731,11 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); - match k.handle_time_ticks(&tx) { + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + let res_time_tick = k.handle_time_ticks(&tx); + #[cfg(all(target_os = "windows", feature = "gui"))] + let res_time_tick = k.handle_time_ticks(&tx,gui_tx); + match res_time_tick { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; @@ -1767,7 +1784,11 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); - match k.handle_time_ticks(&tx) { + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + let res_time_tick = k.handle_time_ticks(&tx); + #[cfg(all(target_os = "windows", feature = "gui"))] + let res_time_tick = k.handle_time_ticks(&tx,gui_tx); + match res_time_tick { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; @@ -1782,7 +1803,11 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); - match k.handle_time_ticks(&tx) { + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + let res_time_tick = k.handle_time_ticks(&tx); + #[cfg(all(target_os = "windows", feature = "gui"))] + let res_time_tick = k.handle_time_ticks(&tx,gui_tx); + match res_time_tick { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index c9ba949d3..fc15ab08f 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -10,6 +10,9 @@ mod llhook; #[cfg(feature = "interception_driver")] mod interception; +cfg(feature = "gui")] +use crate native_windows_gui as nwg; + pub static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); @@ -129,13 +132,13 @@ impl Kanata { } #[cfg(feature = "gui")] - pub fn live_reload(&mut self) -> Result<()> { + pub fn live_reload(&mut self,gui_tx:nwg::NoticeSender) -> Result<()> { self.live_reload_requested = true; - self.do_live_reload(&None)?; + self.do_live_reload(&None,gui_tx)?; Ok(()) } #[cfg(feature = "gui")] - pub fn live_reload_n(&mut self, n: usize) -> Result<()> { + pub fn live_reload_n(&mut self,n:usize,gui_tx:nwg::NoticeSender) -> Result<()> { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow self.live_reload_requested = true; match self.cfg_paths.get(n) { @@ -147,7 +150,7 @@ impl Kanata { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } - self.do_live_reload(&None)?; + self.do_live_reload(&None,gui_tx)?; Ok(()) } } From 1a4a7a1bdb0a8312d1e8439c235b7a847dc58e09 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:34:07 +0700 Subject: [PATCH 064/154] gui: add layer_notice notification receiver to get notified on layer changes --- src/gui_win.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index f67cc0a2d..67f34e9f6 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -31,6 +31,7 @@ pub struct SystemTray { pub embed: nwg::EmbedResource, pub icon: nwg::Icon, pub window: nwg::MessageWindow, + pub layer_notice : nwg::Notice, pub tray: nwg::TrayNotification, pub tray_menu: nwg::Menu, pub tray_1cfg_m: nwg::Menu, @@ -358,7 +359,10 @@ pub mod system_tray_ui { .build(&mut d.icon)?; // Controls - nwg::MessageWindow::builder().build(&mut d.window)?; + nwg::MessageWindow ::builder() + . build( &mut d.window )? ; + nwg::Notice ::builder().parent(&d.window) + . build( &mut d.layer_notice )? ; nwg::TrayNotification::builder() .parent(&d.window) .icon(Some(&d.icon)) @@ -470,6 +474,7 @@ pub mod system_tray_ui { let handle_events = move |evt, _evt_data, handle| { if let Some(evt_ui) = evt_ui.upgrade() { match evt { + E::OnNotice => if &handle == &evt_ui.layer_notice {info!("got noticed layer_notice");} E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} From ff1242665262d5002df2883cb0bd032975daa141 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:35:22 +0700 Subject: [PATCH 065/154] gui: cfg: initial support for specifying icons in config as part of the layers --- parser/src/cfg/mod.rs | 56 +++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 5f0558871..3f9913116 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -319,6 +319,8 @@ pub type MappedKeys = HashSet; pub struct LayerInfo { pub name: String, pub cfg_text: String, + #[cfg(all(target_os = "windows", feature = "gui"))] + pub layer_icon: Option, } #[allow(clippy::type_complexity)] // return type is not pub @@ -500,6 +502,7 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; +const DEFLAYER_ICON: &str = "icon"; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -647,7 +650,7 @@ pub fn parse_cfg_raw_string( bail!("No deflayer expressions exist. At least one layer must be defined.") } - let layer_idxs = parse_layer_indexes(&layer_exprs, mapping_order.len())?; + let (layer_idxs,layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; let mut sorted_idxs: Vec<(&String, &usize)> = layer_idxs.iter().map(|tuple| (tuple.0, tuple.1)).collect(); @@ -680,7 +683,7 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { name, cfg_text }) + .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, layer_icon: layer_icons.get(&name).unwrap_or(&None).clone()}) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1031,21 +1034,27 @@ fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec< } type LayerIndexes = HashMap; +type LayerIcons = HashMap>; type Aliases = HashMap; -/// Returns layer names and their indexes into the keyberon layout. This also checks that: +/// Returns layer names and their indexes into the keyberon layout, and their tray icons. This also checks that: /// - All layers have the same number of items as the defsrc, /// - There are no duplicate layer names /// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. -fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result { +fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result<(LayerIndexes,LayerIcons)> { let mut layer_indexes = HashMap::default(); + let mut layer_icons = HashMap::default(); for (i, expr_type) in exprs.iter().enumerate() { + let mut icon:Option = None; let (mut subexprs, expr, do_element_count_check) = match expr_type { - SpannedLayerExprs::DefsrcMapping(e) => { - (check_first_expr(e.t.iter(), DEFLAYER)?, e, true) - } - SpannedLayerExprs::CustomMapping(e) => { - (check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?, e, false) + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)? , e, true)} + #[cfg(all(target_os = "windows", feature = "gui"))] + SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true)} + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?, e, false)} + #[cfg(all(target_os = "windows", feature = "gui"))] + SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), e, false) } }; let layer_expr = subexprs.next().ok_or_else(|| { @@ -1080,6 +1089,28 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu if layer_indexes.contains_key(&layer_name) { bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); } + let mut third_list_count = 0; + #[cfg(all(target_os = "windows", feature = "gui"))] + if let Some(third) = subexprs.peek() {println!("next is list? = {:?} {:?}",third.list(None),third); + if let Some(third_list) = third.list(None) { + third_list_count = 1; + let third_list_1st = &third_list[0]; + if let Some(third_list_1st_s) = &third_list[0].atom(None) { + if *third_list_1st_s != DEFLAYER_ICON { + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st_s}"); + } else { + if let Some(third_list_2nd_s) = &third_list[1].atom(None) { + icon = Some(third_list_2nd_s.to_string()); + } else { + bail!("deflayer failed to parse an icon name in its 3rd element"); + } + } + } else { + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st:?}"); + } + } + subexprs.next(); // advance over the 3rd list + } // Check if user tried to use parentheses directly - `(` and `)` // or escaped them like in kmonad - `\(` and `\)`. for subexpr in subexprs { @@ -1107,7 +1138,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } if do_element_count_check { - let num_actions = expr.t.len() - 2; + let num_actions = expr.t.len() - 2 - third_list_count; if num_actions != expected_len { bail_span!( expr, @@ -1118,10 +1149,11 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu ) } } - layer_indexes.insert(layer_name, i); + layer_indexes.insert(layer_name.clone(), i); + layer_icons.insert(layer_name, icon); } - Ok(layer_indexes) + Ok((layer_indexes,layer_icons)) } #[derive(Debug, Clone)] From 3383c190732202bbf983d339b7d9ce1e6f7db40a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:37:31 +0700 Subject: [PATCH 066/154] gui: add cfg support for icon --- parser/src/cfg/mod.rs | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 3f9913116..6c1a5e0d7 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -502,7 +502,7 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; -const DEFLAYER_ICON: &str = "icon"; +const DEFLAYER_ICON: [&str;3] = ["icon","🖻","🖼"]; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -1096,8 +1096,8 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu third_list_count = 1; let third_list_1st = &third_list[0]; if let Some(third_list_1st_s) = &third_list[0].atom(None) { - if *third_list_1st_s != DEFLAYER_ICON { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st_s}"); + if ! DEFLAYER_ICON.iter().any(|&i| {i==*third_list_1st_s}) { + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { icon = Some(third_list_2nd_s.to_string()); @@ -1106,7 +1106,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } } else { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st:?}"); + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st:?}"); } } subexprs.next(); // advance over the 3rd list @@ -1419,6 +1419,24 @@ fn parse_action(expr: &SExpr, s: &ParserState) -> Result<&'static KanataAction> }) } +/// Check if an `SExpr` is a layer config. +fn parse_action_as_cfg(expr: &SExpr) -> bool { + if let Some(expr_list) = expr.list(None) { + if let Some(expr_list_1st) = &expr_list[0].atom(None) { + if ! DEFLAYER_ICON.iter().any(|&i| {i==*expr_list_1st}) { + return false + } else { + if expr_list[1].atom(None).is_some() { + return true + } else { + return false + } + } + } + } + false +} + /// Returns a single custom action in the proper wrapped type. fn custom(ca: CustomAction, a: &Allocations) -> Result<&'static KanataAction> { Ok(a.sref(Action::Custom(a.sref(a.sref_slice(ca))))) @@ -2873,6 +2891,14 @@ fn parse_layers( LayerExprs::DefsrcMapping(layer) => { // Parse actions in the layer and place them appropriately according // to defsrc mapping order. + #[cfg(all(target_os = "windows", feature = "gui"))] + let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action + #[cfg(all(target_os = "windows", feature = "gui"))] + for (i, ac) in layer.iter().skip(skip).enumerate() { + let ac = parse_action(ac, s)?; + layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; + } + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] for (i, ac) in layer.iter().skip(2).enumerate() { let ac = parse_action(ac, s)?; layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; @@ -2880,6 +2906,12 @@ fn parse_layers( } LayerExprs::CustomMapping(layer) => { // Parse actions as input output pairs + #[cfg(all(target_os = "windows", feature = "gui"))] + let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action + // Parse actions as input -> output triplets + #[cfg(all(target_os = "windows", feature = "gui"))] + let mut pairs = layer[skip..].chunks_exact(2); + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] let mut pairs = layer[2..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; From 3576920934250f10add8fa9e4e441e2200b31b42 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:40:21 +0700 Subject: [PATCH 067/154] gui: cfg: remove feature flags as configs should work everywhere --- parser/src/cfg/mod.rs | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 6c1a5e0d7..e275dfa86 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -319,8 +319,7 @@ pub type MappedKeys = HashSet; pub struct LayerInfo { pub name: String, pub cfg_text: String, - #[cfg(all(target_os = "windows", feature = "gui"))] - pub layer_icon: Option, + pub icon: Option, } #[allow(clippy::type_complexity)] // return type is not pub @@ -683,7 +682,7 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, layer_icon: layer_icons.get(&name).unwrap_or(&None).clone()}) + .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, icon: layer_icons.get(&name).unwrap_or(&None).clone()}) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1047,13 +1046,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu for (i, expr_type) in exprs.iter().enumerate() { let mut icon:Option = None; let (mut subexprs, expr, do_element_count_check) = match expr_type { - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)? , e, true)} - #[cfg(all(target_os = "windows", feature = "gui"))] SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true)} - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?, e, false)} - #[cfg(all(target_os = "windows", feature = "gui"))] SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), e, false) } }; @@ -1089,15 +1082,14 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu if layer_indexes.contains_key(&layer_name) { bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); } - let mut third_list_count = 0; - #[cfg(all(target_os = "windows", feature = "gui"))] - if let Some(third) = subexprs.peek() {println!("next is list? = {:?} {:?}",third.list(None),third); + let mut cfg_third = 0; + if let Some(third) = subexprs.peek() { if let Some(third_list) = third.list(None) { - third_list_count = 1; + cfg_third = 1; let third_list_1st = &third_list[0]; if let Some(third_list_1st_s) = &third_list[0].atom(None) { if ! DEFLAYER_ICON.iter().any(|&i| {i==*third_list_1st_s}) { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st_s}"); + bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { icon = Some(third_list_2nd_s.to_string()); @@ -1106,7 +1098,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } } else { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st:?}"); + bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st:?}"); } } subexprs.next(); // advance over the 3rd list @@ -1138,17 +1130,18 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } if do_element_count_check { - let num_actions = expr.t.len() - 2 - third_list_count; + let num_actions = expr.t.len() - 2 - cfg_third; + let dbg_cfg_third = if cfg_third == 1 {" (excluding 1 config)"} else {""}; if num_actions != expected_len { bail_span!( expr, - "Layer {} has {} item(s), but requires {} to match defsrc", + "Layer {} has {} item(s){}, but requires {} to match defsrc", layer_name, num_actions, + dbg_cfg_third, expected_len ) - } - } + } } layer_indexes.insert(layer_name.clone(), i); layer_icons.insert(layer_name, icon); } @@ -2891,28 +2884,16 @@ fn parse_layers( LayerExprs::DefsrcMapping(layer) => { // Parse actions in the layer and place them appropriately according // to defsrc mapping order. - #[cfg(all(target_os = "windows", feature = "gui"))] let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action - #[cfg(all(target_os = "windows", feature = "gui"))] for (i, ac) in layer.iter().skip(skip).enumerate() { let ac = parse_action(ac, s)?; layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; } - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - for (i, ac) in layer.iter().skip(2).enumerate() { - let ac = parse_action(ac, s)?; - layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; - } } LayerExprs::CustomMapping(layer) => { - // Parse actions as input output pairs - #[cfg(all(target_os = "windows", feature = "gui"))] let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action // Parse actions as input -> output triplets - #[cfg(all(target_os = "windows", feature = "gui"))] let mut pairs = layer[skip..].chunks_exact(2); - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - let mut pairs = layer[2..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; let mut unmapped_anykey_used = false; From 7725397b4959d1cc32d732353b868a0f3f61b48e Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Wed, 1 May 2024 20:26:52 +0700 Subject: [PATCH 068/154] gui: cfg trim icon from quotes --- parser/src/cfg/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index e275dfa86..a29583e62 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1092,7 +1092,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { - icon = Some(third_list_2nd_s.to_string()); + icon = Some(third_list_2nd_s.trim_matches('"').to_string()); } else { bail!("deflayer failed to parse an icon name in its 3rd element"); } From 5b4efec3829f661c045ea8b55c52a18aeb001306 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:46:18 +0700 Subject: [PATCH 069/154] gui: rename to tray_icon allows not changing configs in case support is extended past Windows --- parser/src/cfg/defcfg.rs | 19 ++++++++++++++++--- parser/src/cfg/tests.rs | 3 ++- src/kanata/mod.rs | 17 +++++++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 46b1f80ad..3eb75fd3b 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -54,7 +54,9 @@ pub struct CfgOptions { #[cfg(any(target_os = "macos", target_os = "unknown"))] pub macos_dev_names_include: Option>, #[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))] - pub win_tray_icon: Option, + pub tray_icon: Option, + #[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))] + pub icon_match_layer_name: bool, } impl Default for CfgOptions { @@ -108,7 +110,9 @@ impl Default for CfgOptions { #[cfg(any(target_os = "macos", target_os = "unknown"))] macos_dev_names_include: None, #[cfg(all(any(target_os = "windows",target_os = "unknown"), feature = "gui"))] - win_tray_icon: None, + tray_icon: None, + #[cfg(all(any(target_os = "windows",target_os = "unknown"), feature = "gui"))] + icon_match_layer_name: true, } } } @@ -401,11 +405,20 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { { let icon_path = sexpr_to_str_or_err(val, label)?; if icon_path.is_empty() { - log::warn!("win-tray-icon is empty"); + log::warn!("tray-icon is empty"); } cfg.win_tray_icon = Some(icon_path.to_string()); } } + "icon-match-layer-name" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] { + cfg.icon_match_layer_name = parse_defcfg_val_bool(val, label)? + } + } + "process-unmapped-keys" => { cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index d9bfd501f..f58a1f552 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1287,7 +1287,8 @@ fn parse_all_defcfg() { linux-unicode-u-code v linux-unicode-termination space linux-x11-repeat-delay-rate 400,50 - win-tray-icon symbols.ico + tray-icon symbols.ico + icon-match-layer-name no 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/kanata/mod.rs b/src/kanata/mod.rs index be3fdfa0d..a930c51df 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -198,7 +198,11 @@ pub struct Kanata { #[cfg(feature = "tcp_server")] tcp_server_address: Option, #[cfg(all(target_os = "windows", feature = "gui"))] - pub win_tray_icon: Option, + /// File name / path to the tray icon file. + pub tray_icon: Option, + #[cfg(all(target_os = "windows", feature = "gui"))] + /// Whether to match layer names to icon files without an explicit 'icon' field + pub icon_match_layer_name: bool, } #[derive(PartialEq, Clone, Copy)] @@ -362,7 +366,9 @@ impl Kanata { #[cfg(feature = "tcp_server")] tcp_server_address: args.tcp_server_address.clone(), #[cfg(all(target_os = "windows", feature = "gui"))] - win_tray_icon: cfg.options.win_tray_icon, + tray_icon: cfg.options.tray_icon, + #[cfg(all(target_os = "windows", feature = "gui"))] + icon_match_layer_name: cfg.options.icon_match_layer_name, }) } @@ -457,7 +463,9 @@ impl Kanata { #[cfg(feature = "tcp_server")] tcp_server_address: None, #[cfg(all(target_os = "windows", feature = "gui"))] - win_tray_icon: None, + tray_icon: None, + #[cfg(all(target_os = "windows", feature = "gui"))] + icon_match_layer_name: true, }) } @@ -496,7 +504,8 @@ impl Kanata { self.switch_max_key_timing = cfg.switch_max_key_timing; #[cfg(all(target_os = "windows", feature = "gui"))] { - self.win_tray_icon = cfg.options.win_tray_icon; + self.tray_icon = cfg.options.tray_icon; + self.icon_match_layer_name = cfg.options.icon_match_layer_name; } *MAPPED_KEYS.lock() = cfg.mapped_keys; From 795f421028e75ddaf5269b84dfb7e56e4733bcba Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Wed, 1 May 2024 23:03:39 +0700 Subject: [PATCH 070/154] gui: doc: add icon config example --- cfg_samples/tray-icon/3trans.parent.png | Bin 0 -> 960 bytes cfg_samples/tray-icon/6name-match.png | Bin 0 -> 931 bytes cfg_samples/tray-icon/_custom-icons/s.png | Bin 0 -> 6207 bytes cfg_samples/tray-icon/icons/1symbols.ico | Bin 0 -> 102281 bytes cfg_samples/tray-icon/img/2Nav Num.png | Bin 0 -> 3670 bytes cfg_samples/tray-icon/license_icons.txt | 24 ++++++++++++++++++++++ cfg_samples/tray-icon/tray-icon.kbd | 21 +++++++++++++++++++ cfg_samples/tray-icon/tray-icon.png | Bin 0 -> 1020 bytes docs/config.adoc | 19 ++++++++++++++--- 9 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 cfg_samples/tray-icon/3trans.parent.png create mode 100644 cfg_samples/tray-icon/6name-match.png create mode 100644 cfg_samples/tray-icon/_custom-icons/s.png create mode 100644 cfg_samples/tray-icon/icons/1symbols.ico create mode 100644 cfg_samples/tray-icon/img/2Nav Num.png create mode 100644 cfg_samples/tray-icon/license_icons.txt create mode 100644 cfg_samples/tray-icon/tray-icon.kbd create mode 100644 cfg_samples/tray-icon/tray-icon.png diff --git a/cfg_samples/tray-icon/3trans.parent.png b/cfg_samples/tray-icon/3trans.parent.png new file mode 100644 index 0000000000000000000000000000000000000000..a199266def9d868aa007a3db0c330df12b71b45c GIT binary patch literal 960 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6%U;1OBOz`%C|gc+x5^GP!> zFc*2cIEGZrd3*h!FSDVD>qUEw+9gw#eDTy=sdPH=;4}p#MV%ebLq4cPhkr9OKF;8o zznY!lH3vfrCn;!d7=Ojlr$sYs?}wjn(2rezedpc0)#)|e;-nZtl8yKkgWdD^b z_f;KRZ#n<`YV|)Imu8-=xpR4c((IYZUpef`W&S_T*p?e@+o)c{sNlmeibg{K5dt~j kM3TU(&%hx}BC2K=z9X6QE7$t~FiSFcy85}Sb4q9e0FUdwssI20 literal 0 HcmV?d00001 diff --git a/cfg_samples/tray-icon/6name-match.png b/cfg_samples/tray-icon/6name-match.png new file mode 100644 index 0000000000000000000000000000000000000000..cd0a00efa5c95fa06a525fa7557e6d68c9d6e4f1 GIT binary patch literal 931 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 literal 0 HcmV?d00001 diff --git a/cfg_samples/tray-icon/icons/1symbols.ico b/cfg_samples/tray-icon/icons/1symbols.ico new file mode 100644 index 0000000000000000000000000000000000000000..0d2c5fd23ee24fc332f00e59e0e3bbc1f6fd8243 GIT binary patch literal 102281 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 literal 0 HcmV?d00001 diff --git a/cfg_samples/tray-icon/img/2Nav Num.png b/cfg_samples/tray-icon/img/2Nav Num.png new file mode 100644 index 0000000000000000000000000000000000000000..b70b4415e85d2217c302b6d66a9aad001984dc08 GIT binary patch literal 3670 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 WuW71WK(4?aMPO$|v8=K1O8O^eT1%<` literal 0 HcmV?d00001 diff --git a/cfg_samples/tray-icon/license_icons.txt b/cfg_samples/tray-icon/license_icons.txt new file mode 100644 index 000000000..e45a2e224 --- /dev/null +++ b/cfg_samples/tray-icon/license_icons.txt @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2024, Fred Vatin + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cfg_samples/tray-icon/tray-icon.kbd b/cfg_samples/tray-icon/tray-icon.kbd new file mode 100644 index 000000000..9660c4a0a --- /dev/null +++ b/cfg_samples/tray-icon/tray-icon.kbd @@ -0,0 +1,21 @@ +(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 + 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 +) +(defalias l1 (layer-while-held 1emoji)) +(defalias l2 (layer-while-held 2icon-quote)) +(defalias l3 (layer-while-held 3emoji_alt)) +(defalias l4 (layer-while-held 4my-lmap)) +(defalias l5 (layer-while-held 5no-icn)) +(defalias l6 (layer-while-held 6name-match)) + +(defsrc 1 2 3 4 5 6) +(deflayer ⌂ (icon base.png) @l1 @l2 @l3 @l4 @l5 @l6) ;; find in the 'icon' subfolder +(deflayer 1emoji (🖻 1symbols.ico) q q q q q q) ;; find in the 'icons' subfolder +(deflayer 2icon-quote (🖻 "2Nav Num.png") w w w w w w) ;; find in the 'img' subfolder +(deflayer 3emoji_alt (🖼 3trans.parent) e e e e e e) ;; find '.png' +(deflayermap (4my-lmap) (🖻 "..\..\assets\kanata.ico") 1 r 2 r 3 r 4 r 5 r 6 r) ;; find in relative path +(deflayer 5no-icn t t t t t t) ;; match file name from 'tray-icon' config, whithout which would fall back to 'tray-icon.png' as it's the only valid icon matching 'tray-icon.kbd' name +(deflayer 6name-match y y y y y y) ;; uses '6name-match' with any valid extension since 'icon-match-layer-name' is set to 'yes' diff --git a/cfg_samples/tray-icon/tray-icon.png b/cfg_samples/tray-icon/tray-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7508e5c82cbc81e3088440de9e217c382df7770f GIT binary patch literal 1020 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6%U;1OBOz`%C|gc+x5^GP!> zFt77;aSW-L^Y*%7)|~_qx4@Gt3?(jQut)apc8J+^`r4&5_sByNwAL=pImo>2-i_~b zL~9fZf8O6Ee|jT>XZdC321_P}M3T_$FAbYxuDxWvvu6G2P5`Nss5^X0cVUFW0UaQiVUi5Ul&%l> also allows specifying +layer icons in `+deflayer+` and `+deflayermap+` to show in the tray menu on layer activation, +see https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] + ==== deflayermap An alternative method for defining a layer exists: `deflayermap`. @@ -2390,8 +2394,8 @@ they are an arbitrary length and can be very long. ) ---- -[[windows-only-win-tray-icon]] -=== Windows only: win-tray-icon +[[windows-only-tray-icon]] +=== Windows only: tray-icon <> Show a custom tray icon file for a <> gui-enabled build of kanata on Windows. @@ -2407,16 +2411,25 @@ or just the file name, which is then searched in the following locations: If not specified, tries to load any icon file from the same locations with the name matching config name with extension replaced by one of the supported ones. +See https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] for more details. .Example: [source] ---- ;; in a config file C:\Users\\AppData\Roaming\kanata\kanata.kbd (defcfg - win-tray-icon base.png ;; will load C:\Users\\AppData\Roaming\kanata\base.png + tray-icon base.png ;; will load C:\Users\\AppData\Roaming\kanata\base.png ) ---- +[[windows-only-icon-match-layer-name]] +=== Windows only: icon-match-layer-name +<> + +Show a custom tray icon that matches the name of the active layer if it doesn't specify an explicit icon. +Otherwise <> will be used. Defaults to true. File search rules are the same as in <>. +See https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] for more details. + [[using-multiple-defcfg-options]] === Using multiple defcfg options <> From 3ad6f222647fde5a777caf8f9f55a1ba300fe313 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:55:27 +0700 Subject: [PATCH 071/154] rename to tray_icon --- parser/src/cfg/defcfg.rs | 4 ++-- src/gui_win.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 3eb75fd3b..f3ca360c4 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -397,7 +397,7 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { cfg.macos_dev_names_include = Some(dev_names); } } - "win-tray-icon" => { + "tray-icon" => { #[cfg(all( any(target_os = "windows", target_os = "unknown"), feature = "gui" @@ -407,7 +407,7 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { if icon_path.is_empty() { log::warn!("tray-icon is empty"); } - cfg.win_tray_icon = Some(icon_path.to_string()); + cfg.tray_icon = Some(icon_path.to_string()); } } "icon-match-layer-name" => { diff --git a/src/gui_win.rs b/src/gui_win.rs index 67f34e9f6..251f560d0 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -214,8 +214,8 @@ impl SystemTray { .to_string_lossy() .to_string(); if log_enabled!(Debug) { - let cfg_icon = &k.win_tray_icon; - debug!("pre reload win_tray_icon={:?}", cfg_icon); + let cfg_icon = &k.tray_icon; + debug!("pre reload tray_icon={:?}", cfg_icon); } match i { Some(idx) => { @@ -251,8 +251,8 @@ impl SystemTray { } } }; - let cfg_icon = &k.win_tray_icon; - debug!("pos reload win_tray_icon={:?}", cfg_icon); + let cfg_icon = &k.tray_icon; + debug!("pos reload tray_icon={:?}", cfg_icon); let mut app_data = self.app_data.borrow_mut(); app_data.cfg_icon = cfg_icon.clone(); // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading @@ -533,7 +533,7 @@ pub mod system_tray_ui { pub fn build_tray(cfg: &Arc>) -> Result { let k = cfg.lock(); let paths = &k.cfg_paths; - let cfg_icon = &k.win_tray_icon; + let cfg_icon = &k.tray_icon; let path_cur = &paths[0]; let app_data = SystemTrayData { tooltip: path_cur.display().to_string(), From 095e8f5e3e18fd780dab4de0b5164179c08d1407 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:54:41 +0700 Subject: [PATCH 072/154] fmt --- parser/src/cfg/defcfg.rs | 6 ++-- parser/src/cfg/mod.rs | 63 +++++++++++++++++++++++++----------- src/kanata/mod.rs | 40 +++++++++++++++-------- src/kanata/windows/llhook.rs | 3 -- src/kanata/windows/mod.rs | 12 +++---- src/lib_main.rs | 7 ++-- 6 files changed, 82 insertions(+), 49 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index f3ca360c4..85ca2f151 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -414,12 +414,12 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { #[cfg(all( any(target_os = "windows", target_os = "unknown"), feature = "gui" - ))] { - cfg.icon_match_layer_name = parse_defcfg_val_bool(val, label)? + ))] + { + cfg.icon_match_layer_name = parse_defcfg_val_bool(val, label)? } } - "process-unmapped-keys" => { cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? } diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a29583e62..51799cd87 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -501,7 +501,7 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; -const DEFLAYER_ICON: [&str;3] = ["icon","🖻","🖼"]; +const DEFLAYER_ICON: [&str; 3] = ["icon", "🖻", "🖼"]; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -649,7 +649,7 @@ pub fn parse_cfg_raw_string( bail!("No deflayer expressions exist. At least one layer must be defined.") } - let (layer_idxs,layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; + let (layer_idxs, layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; let mut sorted_idxs: Vec<(&String, &usize)> = layer_idxs.iter().map(|tuple| (tuple.0, tuple.1)).collect(); @@ -682,7 +682,11 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, icon: layer_icons.get(&name).unwrap_or(&None).clone()}) + .map(|(name, cfg_text)| LayerInfo { + name: name.clone(), + cfg_text, + icon: layer_icons.get(&name).unwrap_or(&None).clone(), + }) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1040,15 +1044,23 @@ type Aliases = HashMap; /// - All layers have the same number of items as the defsrc, /// - There are no duplicate layer names /// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. -fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result<(LayerIndexes,LayerIcons)> { +fn parse_layer_indexes( + exprs: &[SpannedLayerExprs], + expected_len: usize, +) -> Result<(LayerIndexes, LayerIcons)> { let mut layer_indexes = HashMap::default(); let mut layer_icons = HashMap::default(); for (i, expr_type) in exprs.iter().enumerate() { - let mut icon:Option = None; + let mut icon: Option = None; let (mut subexprs, expr, do_element_count_check) = match expr_type { - SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true)} - SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), e, false) + SpannedLayerExprs::DefsrcMapping(e) => { + (check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true) } + SpannedLayerExprs::CustomMapping(e) => ( + check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), + e, + false, + ), }; let layer_expr = subexprs.next().ok_or_else(|| { anyhow_span!(expr, "deflayer requires a name and {expected_len} item(s)") @@ -1082,13 +1094,13 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu if layer_indexes.contains_key(&layer_name) { bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); } - let mut cfg_third = 0; + let mut cfg_third = 0; if let Some(third) = subexprs.peek() { if let Some(third_list) = third.list(None) { cfg_third = 1; let third_list_1st = &third_list[0]; if let Some(third_list_1st_s) = &third_list[0].atom(None) { - if ! DEFLAYER_ICON.iter().any(|&i| {i==*third_list_1st_s}) { + if !DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { @@ -1131,7 +1143,11 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } if do_element_count_check { let num_actions = expr.t.len() - 2 - cfg_third; - let dbg_cfg_third = if cfg_third == 1 {" (excluding 1 config)"} else {""}; + let dbg_cfg_third = if cfg_third == 1 { + " (excluding 1 config)" + } else { + "" + }; if num_actions != expected_len { bail_span!( expr, @@ -1141,12 +1157,13 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu dbg_cfg_third, expected_len ) - } } + } + } layer_indexes.insert(layer_name.clone(), i); layer_icons.insert(layer_name, icon); } - Ok((layer_indexes,layer_icons)) + Ok((layer_indexes, layer_icons)) } #[derive(Debug, Clone)] @@ -1416,13 +1433,13 @@ fn parse_action(expr: &SExpr, s: &ParserState) -> Result<&'static KanataAction> fn parse_action_as_cfg(expr: &SExpr) -> bool { if let Some(expr_list) = expr.list(None) { if let Some(expr_list_1st) = &expr_list[0].atom(None) { - if ! DEFLAYER_ICON.iter().any(|&i| {i==*expr_list_1st}) { - return false + if !DEFLAYER_ICON.iter().any(|&i| i == *expr_list_1st) { + return false; } else { if expr_list[1].atom(None).is_some() { - return true + return true; } else { - return false + return false; } } } @@ -2884,15 +2901,23 @@ fn parse_layers( LayerExprs::DefsrcMapping(layer) => { // Parse actions in the layer and place them appropriately according // to defsrc mapping order. - let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action + let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) { + 3 + } else { + 2 + }; // skip the 3rd list that's used to configure a layer rather than define an action for (i, ac) in layer.iter().skip(skip).enumerate() { let ac = parse_action(ac, s)?; layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; } } LayerExprs::CustomMapping(layer) => { - let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action - // Parse actions as input -> output triplets + let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) { + 3 + } else { + 2 + }; // skip the 3rd list that's used to configure a layer rather than define an action + // Parse actions as input -> output triplets let mut pairs = layer[skip..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index a930c51df..70b326d23 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -469,8 +469,11 @@ impl Kanata { }) } - fn do_live_reload(&mut self, _tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx:nwg::NoticeSender) -> Result<()> { + fn do_live_reload( + &mut self, + _tx: &Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, + ) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, Err(e) => { @@ -593,8 +596,11 @@ impl Kanata { /// Advance keyberon layout state and send events based on changes to its state. /// Returns the number of ticks that elapsed. - fn handle_time_ticks(&mut self, tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx:nwg::NoticeSender) -> Result { + fn handle_time_ticks( + &mut self, + tx: &Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, + ) -> Result { const NS_IN_MS: u128 = 1_000_000; let now = instant::Instant::now(); let ns_elapsed = now.duration_since(self.last_tick).as_nanos(); @@ -620,7 +626,7 @@ impl Kanata { #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] self.check_handle_layer_change(tx); #[cfg(all(target_os = "windows", feature = "gui"))] - self.check_handle_layer_change(tx,gui_tx); + self.check_handle_layer_change(tx, gui_tx); if self.live_reload_requested && ((self.prev_keys.is_empty() && self.cur_keys.is_empty()) @@ -637,9 +643,13 @@ impl Kanata { // kanata states. self.live_reload_requested = false; #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - if let Err(e) = self.do_live_reload(tx) {log::error!("live reload failed {e}");} + if let Err(e) = self.do_live_reload(tx) { + log::error!("live reload failed {e}"); + } #[cfg(all(target_os = "windows", feature = "gui"))] - if let Err(e) = self.do_live_reload(tx,gui_tx) {log::error!("live reload failed {e}");} + if let Err(e) = self.do_live_reload(tx, gui_tx) { + log::error!("live reload failed {e}"); + } } #[cfg(feature = "perf_logging")] @@ -1516,8 +1526,11 @@ impl Kanata { #[allow(unused_variables)] /// Prints the layer. If the TCP server is enabled, then this will also send a notification to /// all connected clients. - fn check_handle_layer_change(&mut self, tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx:nwg::NoticeSender) { + fn check_handle_layer_change( + &mut self, + tx: &Option>, + #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, + ) { let cur_layer = self.layout.bm().current_layer(); if cur_layer != self.prev_layer { let new = self.layer_info[cur_layer].name.clone(); @@ -1598,8 +1611,7 @@ impl Kanata { kanata: Arc>, rx: Receiver, tx: Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] - gui_tx: nwg::NoticeSender, + #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, nodelay: bool, ) { info!("entering the processing loop"); @@ -1743,7 +1755,7 @@ impl Kanata { #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] let res_time_tick = k.handle_time_ticks(&tx); #[cfg(all(target_os = "windows", feature = "gui"))] - let res_time_tick = k.handle_time_ticks(&tx,gui_tx); + let res_time_tick = k.handle_time_ticks(&tx, gui_tx); match res_time_tick { Ok(ms) => ms_elapsed = ms, Err(e) => break e, @@ -1796,7 +1808,7 @@ impl Kanata { #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] let res_time_tick = k.handle_time_ticks(&tx); #[cfg(all(target_os = "windows", feature = "gui"))] - let res_time_tick = k.handle_time_ticks(&tx,gui_tx); + let res_time_tick = k.handle_time_ticks(&tx, gui_tx); match res_time_tick { Ok(ms) => ms_elapsed = ms, Err(e) => break e, @@ -1815,7 +1827,7 @@ impl Kanata { #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] let res_time_tick = k.handle_time_ticks(&tx); #[cfg(all(target_os = "windows", feature = "gui"))] - let res_time_tick = k.handle_time_ticks(&tx,gui_tx); + let res_time_tick = k.handle_time_ticks(&tx, gui_tx); match res_time_tick { Ok(ms) => ms_elapsed = ms, Err(e) => break e, diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 65db78b16..7becbedd5 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -1,4 +1,3 @@ -use anyhow::Context; use parking_lot::Mutex; use std::convert::TryFrom; use std::sync::mpsc::{sync_channel, Receiver, SyncSender as Sender, TryRecvError}; @@ -6,8 +5,6 @@ use std::sync::Arc; use std::time; use super::PRESSED_KEYS; -#[cfg(feature = "gui")] -use crate::gui_win::*; use crate::kanata::*; impl Kanata { diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index fc15ab08f..c2fdd019d 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -10,8 +10,8 @@ mod llhook; #[cfg(feature = "interception_driver")] mod interception; -cfg(feature = "gui")] -use crate native_windows_gui as nwg; +#[cfg(feature = "gui")] +use native_windows_gui as nwg; pub static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); @@ -132,13 +132,13 @@ impl Kanata { } #[cfg(feature = "gui")] - pub fn live_reload(&mut self,gui_tx:nwg::NoticeSender) -> Result<()> { + pub fn live_reload(&mut self, gui_tx: nwg::NoticeSender) -> Result<()> { self.live_reload_requested = true; - self.do_live_reload(&None,gui_tx)?; + self.do_live_reload(&None, gui_tx)?; Ok(()) } #[cfg(feature = "gui")] - pub fn live_reload_n(&mut self,n:usize,gui_tx:nwg::NoticeSender) -> Result<()> { + pub fn live_reload_n(&mut self, n: usize, gui_tx: nwg::NoticeSender) -> Result<()> { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow self.live_reload_requested = true; match self.cfg_paths.get(n) { @@ -150,7 +150,7 @@ impl Kanata { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } - self.do_live_reload(&None,gui_tx)?; + self.do_live_reload(&None, gui_tx)?; Ok(()) } } diff --git a/src/lib_main.rs b/src/lib_main.rs index 6401ee114..4463db8d8 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -258,22 +258,21 @@ fn main_impl() -> Result<()> { #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - #[cfg(all(target_os = "windows", feature = "gui"))] - use crate native_windows_gui as nwg; #[cfg(all(target_os = "windows", feature = "gui"))] use anyhow::Context; #[cfg(all(target_os = "windows", feature = "gui"))] + use native_windows_gui as nwg; + #[cfg(all(target_os = "windows", feature = "gui"))] native_windows_gui::init().context("Failed to init Native Windows GUI")?; #[cfg(all(target_os = "windows", feature = "gui"))] let ui = build_tray(&kanata_arc)?; #[cfg(all(target_os = "windows", feature = "gui"))] - let noticer:&nwg::Notice = &ui.layer_notice; + let noticer: &nwg::Notice = &ui.layer_notice; #[cfg(all(target_os = "windows", feature = "gui"))] let gui_tx = noticer.sender(); #[cfg(all(target_os = "windows", feature = "gui"))] Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, gui_tx, args.nodelay); - if let (Some(server), Some(nrx)) = (server, nrx) { #[allow(clippy::unit_arg)] Kanata::start_notification_loop(nrx, server.connections); From 7045bbff8edce8f74943bc3dae38da0ebb0849f5 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 15:58:37 +0700 Subject: [PATCH 073/154] gui: update config/layer icon functions --- src/gui_win.rs | 514 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 394 insertions(+), 120 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 251f560d0..c203e7078 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -1,22 +1,44 @@ use crate::Kanata; use anyhow::{Context, Result}; -use core::cell::RefCell; -use log::Level::Debug; -use native_windows_gui as nwg; +use log::Level::*; use parking_lot::Mutex; use std::collections::HashMap; use std::env::{current_exe, var_os}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use native_windows_derive as nwd; +use native_windows_gui as nwg; +use core::cell::RefCell; use nwg::{ControlHandle, NativeUi}; +use std::sync::Arc; + +trait PathExt { + fn add_ext(&mut self, ext_o: impl AsRef); +} +impl PathExt for PathBuf { + fn add_ext(&mut self, ext_o: impl AsRef) { + match self.extension() { + Some(ext) => { + let mut ext = ext.to_os_string(); + ext.push("."); + ext.push(ext_o.as_ref()); + self.set_extension(ext) + } + None => self.set_extension(ext_o.as_ref()), + }; + } +} #[derive(Default, Debug, Clone)] pub struct SystemTrayData { + // todo: check that on config reload this is updated from the new config data pub tooltip: String, pub cfg_p: Vec, pub cfg_icon: Option, + pub layer0_name: String, + pub layer0_icon: Option, + pub icon_match_layer_name: bool, } #[derive(Default)] pub struct SystemTray { @@ -25,13 +47,15 @@ pub struct SystemTray { pub tray_item_dyn: RefCell>, /// Store dynamically created tray menu items' handlers pub handlers_dyn: RefCell>, - /// Store embedded-in-the-binary resources like icons not to load them from a file + /// Store dynamically created icons to not load them from a file every time pub icon_dyn: RefCell>>, + /// Store 'icon_dyn' hashmap key for the currently active icon ('cfg_path:layer_name' format) + pub icon_active: RefCell>, /// Store embedded-in-the-binary resources like icons not to load them from a file pub embed: nwg::EmbedResource, pub icon: nwg::Icon, pub window: nwg::MessageWindow, - pub layer_notice : nwg::Notice, + pub layer_notice: nwg::Notice, pub tray: nwg::TrayNotification, pub tray_menu: nwg::Menu, pub tray_1cfg_m: nwg::Menu, @@ -51,6 +75,7 @@ pub fn get_xdg_home() -> Option { const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow checking directly for user passed values const ASSET_FD: [&str; 4] = ["", "icon", "img", "icons"]; const IMG_EXT: [&str; 7] = ["ico", "jpg", "jpeg", "png", "bmp", "dds", "tiff"]; +const PRE_LAYER: &str = "🗍: "; // : invalid path marker, so should be safe to use as a separator use crate::lib_main::CFG; impl SystemTray { @@ -58,37 +83,74 @@ impl SystemTray { let (x, y) = nwg::GlobalCursor::position(); self.tray_menu.popup(x, y); } - fn get_icon_p(&self, i: I, s: P) -> Option + /// 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` or a config file name at various locations (where config file is, where executable is, in user config folders) + fn get_icon_p( + &self, + lyr_icn: S1, + lyr_nm: S2, + cfg_icn: S3, + cfg_p: P, + match_name: &bool, + ) -> Option where - I: AsRef, + S1: AsRef, + S2: AsRef, + S3: AsRef, P: AsRef, { - self.get_icon_p_impl(i.as_ref(), s.as_ref()) + self.get_icon_p_impl( + lyr_icn.as_ref(), + lyr_nm.as_ref(), + cfg_icn.as_ref(), + cfg_p.as_ref(), + match_name, + ) } - fn get_icon_p_impl(&self, icn: &str, p: &Path) -> Option { + fn get_icon_p_impl( + &self, + lyr_icn: &str, + lyr_nm: &str, + cfg_icn: &str, + p: &Path, + match_name: &bool, + ) -> Option { + trace!("lyr_icn={lyr_icn} lyr_nm={lyr_nm} cfg_icn={cfg_icn} cfg_p={p:?} match_name={match_name}"); let mut icon_file = PathBuf::new(); let blank_p = Path::new(""); - let icn_p = Path::new(&icn); + let lyr_icn_p = Path::new(&lyr_icn); + let lyr_nm_p = Path::new(&lyr_nm); + let cfg_icn_p = Path::new(&cfg_icn); + let cfg_stem = &p.file_stem().unwrap_or_else(|| OsStr::new("")); + let cfg_name = &p.file_name().unwrap_or_else(|| OsStr::new("")); + let f_name = [ + lyr_icn_p.as_os_str(), + if *match_name { + lyr_nm_p.as_os_str() + } else { + OsStr::new("") + }, + cfg_icn_p.as_os_str(), + cfg_stem, + cfg_name, + ] + .into_iter(); + let f_ext = [ + lyr_icn_p.extension(), + if *match_name { + lyr_nm_p.extension() + } else { + None + }, + cfg_icn_p.extension(), + None, + None, + ]; let pre_p = p.parent().unwrap_or_else(|| Path::new("")); - let nameext = &p.file_name().unwrap_or_else(|| OsStr::new("")); let cur_exe = current_exe().unwrap_or_else(|_| PathBuf::new()); let xdg_cfg = get_xdg_home().unwrap_or_else(|| PathBuf::new()); let app_data = get_appdata().unwrap_or_else(|| PathBuf::new()); let mut user_cfg = get_user_home().unwrap_or_else(|| PathBuf::new()); user_cfg.push(".config"); - let icn_ext = &icn_p - .extension() - .unwrap_or_else(|| OsStr::new("")) - .to_string_lossy() - .to_string(); - let is_icn_ext_valid = - if !IMG_EXT.iter().any(|&i| i == icn_ext) && icn_p.extension().is_some() { - warn!("user extension \"{}\" isn't valid!", icn_ext); - false - } else { - trace!("icn_ext={:?}", icn_ext); - true - }; let parents = [ Path::new(""), pre_p, @@ -97,15 +159,44 @@ impl SystemTray { &app_data, &user_cfg, ]; // empty path to allow no prefixes when icon path is explictily set in case it's a full path already - let f_name = [icn_p.as_os_str(), nameext]; - 'p: for p_par in parents { - trace!("{}p_par={:?}", "", p_par); - for p_kan in CFG_FD { - trace!("{}p_kan={:?}", " ", p_kan); - for p_icn in ASSET_FD { - trace!("{}p_icn={:?}", " ", p_icn); - for nm in f_name { - trace!("{} nm={:?}", " ", nm); + + for (i, nm) in f_name.enumerate() { + trace!("{}nm={:?}", "", nm); + if nm.is_empty() { + trace!("no file name to test, skip"); + continue; + } + let mut is_full_p = false; + if nm == lyr_icn_p { + is_full_p = true + }; // user configs can have full paths, so test them even if all parent folders are emtpy + if nm == cfg_icn_p { + is_full_p = true + }; + let icn_ext = &f_ext[i] + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy() + .to_string(); + let is_icn_ext_valid = if !IMG_EXT.iter().any(|&i| i == icn_ext) && f_ext[i].is_some() { + warn!( + "user icon extension \"{}\" might be invalid (or just not an extension)!", + icn_ext + ); + false + } else { + trace!("icn_ext={:?}", icn_ext); + true + }; + 'p: for p_par in parents { + trace!("{}p_par={:?}", " ", p_par); + if p_par == blank_p && !is_full_p { + trace!("blank parent for non-user, skip"); + continue; + } + for p_kan in CFG_FD { + trace!("{}p_kan={:?}", " ", p_kan); + for p_icn in ASSET_FD { + trace!("{}p_icn={:?}", " ", p_icn); for ext in IMG_EXT { trace!("{} ext={:?}", " ", ext); if !(p_par == blank_p) { @@ -120,25 +211,22 @@ impl SystemTray { if !nm.is_empty() { icon_file.push(nm); } - if !(nm == icn_p) { - icon_file.push(ext); // no icon name passed, iterate extensions + if !is_full_p { + icon_file.set_extension(ext); // no icon name passed, iterate extensions } else if !is_icn_ext_valid { - icon_file.push(ext); + icon_file.add_ext(ext); } else { trace!("skip ext"); } // replace invalid icon extension - if icon_file == blank_p { - continue; - } trace!("testing icon file {:?}", icon_file); if !icon_file.is_file() { icon_file.clear(); if p_par == blank_p && p_kan.is_empty() && p_icn.is_empty() - && nm == icn_p + && is_full_p { - trace!("skipping further iters {:?}", nm); + trace!("skipping further sub-iters on an empty parent with user config {:?}",nm); continue 'p; } } else { @@ -181,11 +269,12 @@ impl SystemTray { error!("no CFG var that contains active kanata config"); }; } - fn reload(&self, i: Option) { + /// Reload config file, currently active (`i=None`) or matching a given `i` index + fn reload_cfg(&self, i: Option) { use nwg::TrayNotificationFlags as f_tray; - let mut msg_title: String = "".to_string(); - let mut msg_content: String = "".to_string(); - let mut flags: f_tray = f_tray::empty(); + let mut msg_title = "".to_string(); + let mut msg_content = "".to_string(); + let mut flags = f_tray::empty(); if let Some(cfg) = CFG.get() { let mut k = cfg.lock(); let paths = &k.cfg_paths; @@ -215,11 +304,21 @@ impl SystemTray { .to_string(); if log_enabled!(Debug) { let cfg_icon = &k.tray_icon; - debug!("pre reload tray_icon={:?}", cfg_icon); + let cfg_icon_s = cfg_icon.clone().unwrap_or("✗".to_string()); + let layer_id = k.layout.b().current_layer(); + let layer_name = &k.layer_info[layer_id].name; + let layer_icon = &k.layer_info[layer_id].icon; + let layer_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); + debug!( + "pre reload tray_icon={} layer_name={} layer_icon={}", + cfg_icon_s, layer_name, layer_icon_s + ); } + let noticer: &nwg::Notice = &self.layer_notice; + let gui_tx = noticer.sender(); match i { Some(idx) => { - if let Ok(()) = k.live_reload_n(idx) { + if let Ok(()) = k.live_reload_n(idx, gui_tx) { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" loaded"); flags |= f_tray::USER_ICON; } else { @@ -235,7 +334,7 @@ impl SystemTray { } } None => { - if let Ok(()) = k.live_reload() { + if let Ok(()) = k.live_reload(gui_tx) { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" reloaded"); flags |= f_tray::USER_ICON; } else { @@ -252,60 +351,37 @@ impl SystemTray { } }; let cfg_icon = &k.tray_icon; - debug!("pos reload tray_icon={:?}", cfg_icon); - let mut app_data = self.app_data.borrow_mut(); - app_data.cfg_icon = cfg_icon.clone(); - // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading - self.tray.set_tip(&path_cur_s); // update tooltip to point to the newer config - // self.tray.set_visibility(true); + let layer_id = k.layout.b().current_layer(); + let layer_name = &k.layer_info[layer_id].name; + let layer_icon = &k.layer_info[layer_id].icon; + let mut cfg_layer_pkey = PathBuf::new(); // path key + cfg_layer_pkey.push(path_cur_cc.clone()); + cfg_layer_pkey.push(PRE_LAYER.to_owned() + &layer_name); //:invalid path marker, so should be safe to use as a separator + let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); + if log_enabled!(Debug) { + let layer_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); + debug!( + "pos reload tray_icon={:?} layer_name={:?} layer_icon={:?}", + cfg_icon, layer_name, layer_icon_s + ); + } - let mut icon_dyn = self.icon_dyn.borrow_mut(); // update the tray icon - if icon_dyn.contains_key(&path_cur_cc) { - if let Some(icon) = icon_dyn.get(&path_cur_cc).unwrap() { - self.tray.set_icon(&icon); // - } else { - info!( - "this config has no associated icon, using default: {}", - path_cur_cc.display().to_string() - ); - self.tray.set_icon(&self.icon) - } - } else { - let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon { - cfg_icon - } else { - "" - }; - if let Some(ico_p) = &self.get_icon_p(&cfg_icon_p, &path_cur_cc) { - let mut temp_icon_bitmap = Default::default(); - if let Ok(()) = nwg::Bitmap::builder() - .source_file(Some(&ico_p)) - .strict(false) - .build(&mut temp_icon_bitmap) - { - info!( - "✓ Using an icon from this config: {}", - path_cur_cc.display().to_string() - ); - let temp_icon = temp_icon_bitmap.copy_as_icon(); - let _ = icon_dyn.insert(path_cur_cc.clone(), Some(temp_icon)); - let temp_icon = temp_icon_bitmap.copy_as_icon(); - self.tray.set_icon(&temp_icon); - } else { - warn!( - "✗ Invalid/no icon from this config: {}", - path_cur_cc.display().to_string() - ); - self.tray.set_icon(&self.icon); - } - } else { - warn!( - "✗ Invalid icon path from this config: {}", - path_cur_cc.display().to_string() - ); - self.tray.set_icon(&self.icon); - } + { + let mut app_data = self.app_data.borrow_mut(); + app_data.cfg_icon = cfg_icon.clone(); + // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading + self.tray.set_tip(&cfg_layer_pkey_s); // update tooltip to point to the newer config + // self.tray.set_visibility(true); } + let clear = if i.is_none() { true } else { false }; + self.update_tray_icon( + cfg_layer_pkey, + &cfg_layer_pkey_s, + &layer_name, + &layer_icon, + path_cur_cc, + clear, + ) } else { msg_title += "✗ Config NOT reloaded, no CFG"; warn!("{}", msg_title); @@ -319,6 +395,189 @@ impl SystemTray { Some(&self.icon), ); } + /// Update tray icon data on layer change + fn reload_layer_icon(&self) { + if let Some(cfg) = CFG.get() { + if let Some(k) = cfg.try_lock() { + let paths = &k.cfg_paths; + let idx_cfg = k.cur_cfg_idx; + let path_cur = &paths[idx_cfg]; + let path_cur_cc = path_cur.clone(); + let cfg_icon = &k.tray_icon; + let layer_id = k.layout.b().current_layer(); + let layer_name = &k.layer_info[layer_id].name; + let layer_icon = &k.layer_info[layer_id].icon; + + let mut cfg_layer_pkey = PathBuf::new(); // path key + cfg_layer_pkey.push(path_cur_cc.clone()); + cfg_layer_pkey.push(PRE_LAYER.to_owned() + &layer_name); //:invalid path marker, so should be safe to use as a separator + let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); + if log_enabled!(Debug) { + let cfg_name = &path_cur + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy() + .to_string(); + let cfg_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); + let layer_icon_s = cfg_icon.clone().unwrap_or("✗".to_string()); + info!( + "✓ layer changed to ‘{}’ with icon ‘{}’ @ ‘{}’ tray_icon ‘{}’", + layer_name, layer_icon_s, cfg_name, cfg_icon_s + ); + } + + self.tray.set_tip(&cfg_layer_pkey_s); // update tooltip to point to the newer config + let clear = false; + self.update_tray_icon( + cfg_layer_pkey, + &cfg_layer_pkey_s, + &layer_name, + &layer_icon, + path_cur_cc, + clear, + ) + } else { + debug!("✗ kanata config is locked, can't get current layer (likely the gui changed the layer and is still holding the lock, it will update the icon)"); + } + } else { + warn!("✗ Layer indicator NOT changed, no CFG"); + }; + } + /// Update tray icon data given various config/layer info + fn update_tray_icon( + &self, + cfg_layer_pkey: PathBuf, + cfg_layer_pkey_s: &str, + layer_name: &str, + layer_icon: &Option, + path_cur_cc: PathBuf, + clear: bool, + ) { + let mut icon_dyn = self.icon_dyn.borrow_mut(); // update the tray icon + let mut icon_active = self.icon_active.borrow_mut(); // update the tray icon active path + if clear { + *icon_dyn = Default::default(); + *icon_active = Default::default(); + debug!("reloading active config, clearing icon_dyn/_active cache"); + } + let app_data = self.app_data.borrow(); + if let Some(icon_opt) = icon_dyn.get(&cfg_layer_pkey) { + // 1a config+layer path has already been checked + if let Some(icon) = icon_opt { + self.tray.set_icon(&icon); + *icon_active = Some(cfg_layer_pkey); + } else { + info!( + "no icon found, using default for config+layer = {}", + cfg_layer_pkey_s + ); + self.tray.set_icon(&self.icon); + *icon_active = Some(cfg_layer_pkey); + } + } else if let Some(layer_icon) = layer_icon { + // 1b cfg+layer path hasn't been checked, but layer has an icon configured, so check it + if let Some(ico_p) = &self.get_icon_p( + &layer_icon, + &layer_name, + "", + &path_cur_cc, + &app_data.icon_match_layer_name, + ) { + let mut temp_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder() + .source_file(Some(&ico_p)) + .strict(false) + .build(&mut temp_icon_bitmap) + { + info!( + "✓ Using an icon from this config+layer: {}", + cfg_layer_pkey_s + ); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + let _ = icon_dyn.insert(cfg_layer_pkey.clone(), Some(temp_icon)); + *icon_active = Some(cfg_layer_pkey); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + self.tray.set_icon(&temp_icon); + } else { + warn!( + "✗ Invalid icon file \"{layer_icon}\" from this config+layer: {}", + cfg_layer_pkey_s + ); + let _ = icon_dyn.insert(cfg_layer_pkey.clone(), None); + *icon_active = Some(cfg_layer_pkey); + self.tray.set_icon(&self.icon); + } + } else { + warn!( + "✗ Invalid icon path \"{layer_icon}\" from this config+layer: {}", + cfg_layer_pkey_s + ); + let _ = icon_dyn.insert(cfg_layer_pkey.clone(), None); + *icon_active = Some(cfg_layer_pkey); + self.tray.set_icon(&self.icon); + } + } else if icon_dyn.contains_key(&path_cur_cc) { + // 2a no layer icon configured, but config icon exists, use it + if let Some(icon) = icon_dyn.get(&path_cur_cc).unwrap() { + self.tray.set_icon(&icon); + *icon_active = Some(path_cur_cc); + } else { + info!( + "no icon found, using default for config: {}", + path_cur_cc.display().to_string() + ); + self.tray.set_icon(&self.icon); + *icon_active = Some(path_cur_cc); + } + } else { + // 2a no layer icon configured, no config icon, use config path + let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon { + cfg_icon + } else { + "" + }; + if let Some(ico_p) = &self.get_icon_p( + "", + &layer_name, + &cfg_icon_p, + &path_cur_cc, + &app_data.icon_match_layer_name, + ) { + let mut temp_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder() + .source_file(Some(&ico_p)) + .strict(false) + .build(&mut temp_icon_bitmap) + { + info!( + "✓ Using an icon from this config: {}", + path_cur_cc.display().to_string() + ); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + let _ = icon_dyn.insert(cfg_layer_pkey.clone(), Some(temp_icon)); + *icon_active = Some(cfg_layer_pkey); + let temp_icon = temp_icon_bitmap.copy_as_icon(); + self.tray.set_icon(&temp_icon); + } else { + warn!( + "✗ Invalid icon file \"{cfg_icon_p}\" from this config: {}", + path_cur_cc.display().to_string() + ); + let _ = icon_dyn.insert(cfg_layer_pkey.clone(), None); + *icon_active = Some(cfg_layer_pkey); + self.tray.set_icon(&self.icon); + } + } else { + warn!( + "✗ Invalid icon path \"{cfg_icon_p}\" from this config: {}", + path_cur_cc.display().to_string() + ); + let _ = icon_dyn.insert(cfg_layer_pkey.clone(), None); + *icon_active = Some(cfg_layer_pkey); + self.tray.set_icon(&self.icon); + } + } + } fn exit(&self) { let handlers = self.handlers_dyn.borrow(); for handler in handlers.iter() { @@ -359,10 +618,10 @@ pub mod system_tray_ui { .build(&mut d.icon)?; // Controls - nwg::MessageWindow ::builder() - . build( &mut d.window )? ; - nwg::Notice ::builder().parent(&d.window) - . build( &mut d.layer_notice )? ; + nwg::MessageWindow::builder().build(&mut d.window)?; + nwg::Notice::builder() + .parent(&d.window) + .build(&mut d.layer_notice)?; nwg::TrayNotification::builder() .parent(&d.window) .icon(Some(&d.icon)) @@ -388,12 +647,10 @@ pub mod system_tray_ui { { let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut let mut icon_dyn = d.icon_dyn.borrow_mut(); + let mut icon_active = d.icon_active.borrow_mut(); const MENU_ACC: &str = "ASDFGQWERTZXCVBYUIOPHJKLNM"; - let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon { - cfg_icon - } else { - "" - }; + let layer0_icon_s = &app_data.layer0_icon.clone().unwrap_or("".to_string()); + let cfg_icon_s = &app_data.cfg_icon.clone().unwrap_or("".to_string()); if (app_data.cfg_p).len() > 0 { for (i, cfg_p) in app_data.cfg_p.iter().enumerate() { let i_acc = match i { @@ -429,23 +686,31 @@ pub mod system_tray_ui { tray_item_dyn.push(menu_item); if i == 0 { // add icons if exists, hashed by config path (for active config, others will create on load) - if let Some(ico_p) = &d.get_icon_p(&cfg_icon_p, &cfg_p) { + if let Some(ico_p) = &d.get_icon_p( + &layer0_icon_s, + &app_data.layer0_name, + &cfg_icon_s, + &cfg_p, + &app_data.icon_match_layer_name, + ) { + let mut cfg_layer_pkey = PathBuf::new(); // path key + cfg_layer_pkey.push(cfg_p.clone()); + cfg_layer_pkey.push(PRE_LAYER.to_owned() + &app_data.layer0_name); + let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); let mut temp_icon_bitmap = Default::default(); if let Ok(()) = nwg::Bitmap::builder() .source_file(Some(&ico_p)) .strict(false) .build(&mut temp_icon_bitmap) { - debug!("✓ main 0 config: using icon for {:?}", cfg_p); + debug!("✓ main 0 config: using icon for {}", cfg_layer_pkey_s); let temp_icon = temp_icon_bitmap.copy_as_icon(); - let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); + let _ = icon_dyn.insert(cfg_layer_pkey, Some(temp_icon)); let temp_icon = temp_icon_bitmap.copy_as_icon(); d.tray.set_icon(&temp_icon); } else { - info!( - "✗ main 0 icon ✓ icon path, using DEFAULT icon for {:?}", - cfg_p - ); + info!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p); + let _ = icon_dyn.insert(cfg_layer_pkey, None); } } else { debug!("✗ main 0 config: using DEFAULT icon for {:?}", cfg_p); @@ -456,6 +721,7 @@ pub mod system_tray_ui { .strict(true) .build(&mut temp_icon)?; let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); + *icon_active = Some(cfg_p.clone()); } } } @@ -474,14 +740,14 @@ pub mod system_tray_ui { let handle_events = move |evt, _evt_data, handle| { if let Some(evt_ui) = evt_ui.upgrade() { match evt { - E::OnNotice => if &handle == &evt_ui.layer_notice {info!("got noticed layer_notice");} + E::OnNotice => if &handle == &evt_ui.layer_notice {SystemTray::reload_layer_icon(&evt_ui);} E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnMenuHover => if &handle == &evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} E::OnMenuItemSelected => - if &handle == &evt_ui.tray_2reload {SystemTray::reload(&evt_ui,None); + if &handle == &evt_ui.tray_2reload {SystemTray::reload_cfg(&evt_ui,None); } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); } else { match handle { @@ -492,7 +758,7 @@ pub mod system_tray_ui { for (_j, h_cfg_j) in tray_item_dyn.iter().enumerate() { if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others h_cfg.set_checked(true); // check self - SystemTray::reload(&evt_ui,Some(i)); + SystemTray::reload_cfg(&evt_ui,Some(i)); } } }, @@ -535,11 +801,19 @@ pub fn build_tray(cfg: &Arc>) -> Result Date: Thu, 2 May 2024 16:43:08 +0700 Subject: [PATCH 074/154] gui: update layer tooltip format move it to a new line for better readability --- src/gui_win.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index c203e7078..34dcfaf62 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -75,7 +75,7 @@ pub fn get_xdg_home() -> Option { const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow checking directly for user passed values const ASSET_FD: [&str; 4] = ["", "icon", "img", "icons"]; const IMG_EXT: [&str; 7] = ["ico", "jpg", "jpeg", "png", "bmp", "dds", "tiff"]; -const PRE_LAYER: &str = "🗍: "; // : invalid path marker, so should be safe to use as a separator +const PRE_LAYER: &str = "\n🗍: "; // : invalid path marker, so should be safe to use as a separator use crate::lib_main::CFG; impl SystemTray { From 208f73fabc8627d332233c241a9775fdd7f0a785 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 16:43:43 +0700 Subject: [PATCH 075/154] gui: update debug --- src/gui_win.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 34dcfaf62..54cde3f89 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -420,7 +420,7 @@ impl SystemTray { .to_string(); let cfg_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); let layer_icon_s = cfg_icon.clone().unwrap_or("✗".to_string()); - info!( + debug!( "✓ layer changed to ‘{}’ with icon ‘{}’ @ ‘{}’ tray_icon ‘{}’", layer_name, layer_icon_s, cfg_name, cfg_icon_s ); From 642e4d49efe90fe5e136c8d5419aa3d7169c657a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 16:44:56 +0700 Subject: [PATCH 076/154] gui: use up-to-date icon_match_layer_name --- src/gui_win.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 54cde3f89..2d9d7bdb8 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -32,7 +32,6 @@ impl PathExt for PathBuf { #[derive(Default, Debug, Clone)] pub struct SystemTrayData { - // todo: check that on config reload this is updated from the new config data pub tooltip: String, pub cfg_p: Vec, pub cfg_icon: Option, @@ -369,6 +368,9 @@ impl SystemTray { { let mut app_data = self.app_data.borrow_mut(); app_data.cfg_icon = cfg_icon.clone(); + app_data.layer0_name = k.layer_info[0].name.clone(); + app_data.layer0_icon = Some(k.layer_info[0].name.clone()); + app_data.icon_match_layer_name = k.icon_match_layer_name; // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading self.tray.set_tip(&cfg_layer_pkey_s); // update tooltip to point to the newer config // self.tray.set_visibility(true); From de39eff0e87078484e23ab4fd51ea8ee45e26141 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 16:45:37 +0700 Subject: [PATCH 077/154] fmt --- src/gui_win.rs | 6 +++--- src/kanata/mod.rs | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 2d9d7bdb8..8e7c1a35d 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -1,14 +1,14 @@ use crate::Kanata; use anyhow::{Context, Result}; +use core::cell::RefCell; use log::Level::*; + +use native_windows_gui as nwg; use parking_lot::Mutex; use std::collections::HashMap; use std::env::{current_exe, var_os}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use native_windows_derive as nwd; -use native_windows_gui as nwg; -use core::cell::RefCell; use nwg::{ControlHandle, NativeUi}; use std::sync::Arc; diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 70b326d23..94ddfbbdf 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -472,7 +472,8 @@ impl Kanata { fn do_live_reload( &mut self, _tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, + #[cfg(all(target_os = "windows", feature = "gui"))] + gui_tx: native_windows_gui::NoticeSender, ) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, @@ -599,7 +600,8 @@ impl Kanata { fn handle_time_ticks( &mut self, tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, + #[cfg(all(target_os = "windows", feature = "gui"))] + gui_tx: native_windows_gui::NoticeSender, ) -> Result { const NS_IN_MS: u128 = 1_000_000; let now = instant::Instant::now(); @@ -1529,7 +1531,8 @@ impl Kanata { fn check_handle_layer_change( &mut self, tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, + #[cfg(all(target_os = "windows", feature = "gui"))] + gui_tx: native_windows_gui::NoticeSender, ) { let cur_layer = self.layout.bm().current_layer(); if cur_layer != self.prev_layer { @@ -1611,7 +1614,8 @@ impl Kanata { kanata: Arc>, rx: Receiver, tx: Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] gui_tx: native_windows_gui::NoticeSender, + #[cfg(all(target_os = "windows", feature = "gui"))] + gui_tx: native_windows_gui::NoticeSender, nodelay: bool, ) { info!("entering the processing loop"); From 3d47cdefbc634e91ea367c3cae120c1c21378988 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 16:49:51 +0700 Subject: [PATCH 078/154] gui: make icon log messages a debug level --- src/gui_win.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 8e7c1a35d..56855512a 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -229,7 +229,7 @@ impl SystemTray { continue 'p; } } else { - info!("✓ found icon file: {}", icon_file.display().to_string()); + debug!("✓ found icon file: {}", icon_file.display().to_string()); return Some(icon_file.display().to_string()); } } @@ -469,7 +469,7 @@ impl SystemTray { self.tray.set_icon(&icon); *icon_active = Some(cfg_layer_pkey); } else { - info!( + debug!( "no icon found, using default for config+layer = {}", cfg_layer_pkey_s ); @@ -491,7 +491,7 @@ impl SystemTray { .strict(false) .build(&mut temp_icon_bitmap) { - info!( + debug!( "✓ Using an icon from this config+layer: {}", cfg_layer_pkey_s ); @@ -524,7 +524,7 @@ impl SystemTray { self.tray.set_icon(&icon); *icon_active = Some(path_cur_cc); } else { - info!( + debug!( "no icon found, using default for config: {}", path_cur_cc.display().to_string() ); @@ -551,7 +551,7 @@ impl SystemTray { .strict(false) .build(&mut temp_icon_bitmap) { - info!( + debug!( "✓ Using an icon from this config: {}", path_cur_cc.display().to_string() ); @@ -711,7 +711,7 @@ pub mod system_tray_ui { let temp_icon = temp_icon_bitmap.copy_as_icon(); d.tray.set_icon(&temp_icon); } else { - info!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p); + debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p); let _ = icon_dyn.insert(cfg_layer_pkey, None); } } else { From da40fde0f4b2b7eced15374161fda9c490690665 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 17:39:39 +0700 Subject: [PATCH 079/154] gui: cfg icon fix wrong conditional 3rd element can be a non-icon list --- parser/src/cfg/mod.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 51799cd87..6d77614b0 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1097,23 +1097,20 @@ fn parse_layer_indexes( let mut cfg_third = 0; if let Some(third) = subexprs.peek() { if let Some(third_list) = third.list(None) { - cfg_third = 1; - let third_list_1st = &third_list[0]; - if let Some(third_list_1st_s) = &third_list[0].atom(None) { - if !DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { - bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); - } else { - if let Some(third_list_2nd_s) = &third_list[1].atom(None) { - icon = Some(third_list_2nd_s.trim_matches('"').to_string()); - } else { - bail!("deflayer failed to parse an icon name in its 3rd element"); + if third_list.len() > 0 { + if let Some(third_list_1st_s) = &third_list[0].atom(None) { + if DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { + cfg_third = 1; + if let Some(third_list_2nd_s) = &third_list[1].atom(None) { + icon = Some(third_list_2nd_s.trim_matches('"').to_string()); + subexprs.next(); // advance over the 3rd list + } else { + bail!("deflayer failed to parse an icon name in its 3rd element"); + } } } - } else { - bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st:?}"); } } - subexprs.next(); // advance over the 3rd list } // Check if user tried to use parentheses directly - `(` and `)` // or escaped them like in kmonad - `\(` and `\)`. From c7653c83e05af5e4b555993838ebda5013d2145b Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 17:52:17 +0700 Subject: [PATCH 080/154] clippy --- parser/src/cfg/mod.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 6d77614b0..a398303c6 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1097,7 +1097,7 @@ fn parse_layer_indexes( let mut cfg_third = 0; if let Some(third) = subexprs.peek() { if let Some(third_list) = third.list(None) { - if third_list.len() > 0 { + if !third_list.is_empty() { if let Some(third_list_1st_s) = &third_list[0].atom(None) { if DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { cfg_third = 1; @@ -1433,11 +1433,7 @@ fn parse_action_as_cfg(expr: &SExpr) -> bool { if !DEFLAYER_ICON.iter().any(|&i| i == *expr_list_1st) { return false; } else { - if expr_list[1].atom(None).is_some() { - return true; - } else { - return false; - } + return expr_list[1].atom(None).is_some(); } } } From 2e87eea56fa5cb4a26c0dc5c6e7b46f7eeb194f5 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:02:43 +0700 Subject: [PATCH 081/154] gui: dep: add menu icons --- Cargo.toml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9fb5ab01e..96156b74a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,19 @@ winapi = { version = "0.3.9", features = [ "timeapi", "mmsystem", ] } +windows-sys = { version = "0.52.0", features = [ + "Win32_Devices_DeviceAndDriverInstallation", + "Win32_Devices_Usb", + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_Security", + "Win32_System_Diagnostics_Debug", + "Win32_System_Registry", + "Win32_System_Threading", + "Win32_UI_Controls", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", +], 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 } @@ -103,7 +116,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] -gui = ["win_manifest","native-windows-derive","lazy_static","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"] +gui = ["win_manifest","native-windows-derive","lazy_static","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","dep:windows-sys"] [profile.release] opt-level = "z" From 987191dbaf97cf424d56c4b008ee62611155305f Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Thu, 2 May 2024 18:20:23 +0700 Subject: [PATCH 082/154] gui: vendor nwg extension --- license-tray-icons | 26 +++++++++ src/gui_nwg_ext.rs | 130 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 license-tray-icons create mode 100644 src/gui_nwg_ext.rs diff --git a/license-tray-icons b/license-tray-icons new file mode 100644 index 000000000..a9d02d7ff --- /dev/null +++ b/license-tray-icons @@ -0,0 +1,26 @@ +The MIT License (MIT) +===================== + +Copyright © `2024` `Niccolò Betto` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/gui_nwg_ext.rs b/src/gui_nwg_ext.rs new file mode 100644 index 000000000..0bce155dc --- /dev/null +++ b/src/gui_nwg_ext.rs @@ -0,0 +1,130 @@ +// based on https://github.com/lynxnb/wsl-usb-manager/blob/master/src/gui/nwg_ext.rs +use native_windows_gui as nwg; + +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::Graphics::Gdi::DeleteObject; +use windows_sys::Win32::UI::Shell::{ + SHGetStockIconInfo, SHGSI_ICON, SHGSI_SMALLICON, SHSTOCKICONID, SHSTOCKICONINFO, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CopyImage, DestroyIcon, GetIconInfoExW, SetMenuItemInfoW, HMENU, ICONINFOEXW, IMAGE_BITMAP, + LR_CREATEDIBSECTION, MENUITEMINFOW, MF_BYCOMMAND, MIIM_BITMAP, +}; + +/// Extends [`nwg::Bitmap`] with additional functionality. +pub trait BitmapEx { + fn from_system_icon(icon: SHSTOCKICONID) -> nwg::Bitmap; +} + +impl BitmapEx for nwg::Bitmap { + /// Creates a bitmap from a [`SHSTOCKICONID`] system icon ID. + fn from_system_icon(icon: SHSTOCKICONID) -> nwg::Bitmap { + // Retrieve the icon + let mut stock_icon_info = SHSTOCKICONINFO { + cbSize: std::mem::size_of::() as u32, + hIcon: 0, + iSysImageIndex: 0, + iIcon: 0, + szPath: [0; 260], + }; + unsafe { + SHGetStockIconInfo( + icon, + SHGSI_ICON | SHGSI_SMALLICON, + &mut stock_icon_info as *mut _, + ); + } + + // Retrieve the bitmap for the icon + let mut icon_info = ICONINFOEXW { + cbSize: std::mem::size_of::() as u32, + fIcon: 0, + xHotspot: 0, + yHotspot: 0, + hbmMask: 0, + hbmColor: 0, + wResID: 0, + szModName: [0; 260], + szResName: [0; 260], + }; + unsafe { + GetIconInfoExW(stock_icon_info.hIcon, &mut icon_info as *mut _); + } + + // Create a copy of the bitmap with transparent background from the icon bitmap + let hbitmap = unsafe { + CopyImage( + icon_info.hbmColor as HANDLE, + IMAGE_BITMAP, + 0, + 0, + LR_CREATEDIBSECTION, + ) + }; + + // Delete the unused icon and bitmaps + unsafe { + DeleteObject(icon_info.hbmMask); + DeleteObject(icon_info.hbmColor); + DestroyIcon(stock_icon_info.hIcon); + }; + + if hbitmap == 0 { + panic!("Failed to create bitmap from system icon"); + } else { + #[allow(unused)] + struct Bitmap { + handle: HANDLE, + owned: bool, + } + + let bitmap = Bitmap { + handle: hbitmap as HANDLE, + owned: true, + }; + + // Ugly hack to set the private `owned` field inside nwg::Bitmap to true + unsafe { std::mem::transmute(bitmap) } + } + } +} + +/// Extends [`nwg::MenuItem`] with additional functionality. +pub trait MenuItemEx { + fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>); +} + +impl MenuItemEx for nwg::MenuItem { + /// Sets a bitmap to be displayed on a menu item. Pass `None` to remove the bitmap. + fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>) { + let (hmenu, item_id) = self.handle.hmenu_item().unwrap(); + let hbitmap = match bitmap { + Some(b) => b.handle as HANDLE, + None => 0, + }; + + let menu_item_info = MENUITEMINFOW { + cbSize: std::mem::size_of::() as u32, + fMask: MIIM_BITMAP, + fType: 0, + fState: 0, + wID: 0, + hSubMenu: 0, + hbmpChecked: 0, + hbmpUnchecked: 0, + dwItemData: 0, + dwTypeData: std::ptr::null_mut(), + cch: 0, + hbmpItem: hbitmap, + }; + + unsafe { + SetMenuItemInfoW( + hmenu as HMENU, + item_id, + MF_BYCOMMAND as i32, + &menu_item_info as *const _, + ); + } + } +} From e386f3f4ae49c378819a6be4830d48040e564cc0 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 3 May 2024 16:03:56 +0700 Subject: [PATCH 083/154] gui: extend nwg Menu to show icons in Menu nwg supports only MenuItems --- src/gui_nwg_ext.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/src/gui_nwg_ext.rs b/src/gui_nwg_ext.rs index 0bce155dc..5d8cfce01 100644 --- a/src/gui_nwg_ext.rs +++ b/src/gui_nwg_ext.rs @@ -2,13 +2,15 @@ use native_windows_gui as nwg; use windows_sys::Win32::Foundation::HANDLE; -use windows_sys::Win32::Graphics::Gdi::DeleteObject; +use windows_sys::Win32::Graphics::Gdi::{DeleteObject,HBRUSH}; use windows_sys::Win32::UI::Shell::{ SHGetStockIconInfo, SHGSI_ICON, SHGSI_SMALLICON, SHSTOCKICONID, SHSTOCKICONINFO, }; use windows_sys::Win32::UI::WindowsAndMessaging::{ - CopyImage, DestroyIcon, GetIconInfoExW, SetMenuItemInfoW, HMENU, ICONINFOEXW, IMAGE_BITMAP, - LR_CREATEDIBSECTION, MENUITEMINFOW, MF_BYCOMMAND, MIIM_BITMAP, + CopyImage, DestroyIcon, GetIconInfoExW, SetMenuInfo, SetMenuItemInfoW, SetMenuItemBitmaps, + HMENU, ICONINFOEXW, IMAGE_BITMAP, + LR_CREATEDIBSECTION, MENUINFO, MENUITEMINFOW, MF_BYCOMMAND, MIIM_BITMAP,MIM_STYLE,MIM_BACKGROUND + ,MNS_NOTIFYBYPOS,MNS_CHECKORBMP,MNS_AUTODISMISS,MF_BYPOSITION,MENU_ITEM_FLAGS }; /// Extends [`nwg::Bitmap`] with additional functionality. @@ -89,6 +91,70 @@ impl BitmapEx for nwg::Bitmap { } } +/// Extends [`nwg::Menu`] with additional functionality. +pub trait MenuEx {fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>);} +impl MenuEx for nwg::Menu { /// Sets a bitmap to be displayed on a menu. Pass `None` to remove the bitmap + fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>) { + use std::{ptr,mem::size_of}; + let (hmenu_par, hmenu) = self.handle.hmenu().unwrap(); + let hbitmap = match bitmap {Some(b) => b.handle as HANDLE, None => 0,}; + + let menu_item_info = MENUITEMINFOW { + cbSize : size_of::() as u32 ,//u32 byte-size of the structure, must be set by the caller to sizeof(MENUITEMINFO) + fMask : MIIM_BITMAP ,//MENU_ITEM_MASK members to be retrieved or set: + // MIIM_BITMAP hbmpItem + // MIIM_STATE fState + // MIIM_CHECKMARKS hbmpChecked and hbmpUnchecked + // MIIM_DATA dwItemData + // MIIM_FTYPE fType + // MIIM_ID wID + // MIIM_STRING dwTypeData + // MIIM_SUBMENU hSubMenu + // MIIM_TYPE fType and dwTypeData, replaced by MIIM_BITMAP, MIIM_FTYPE, and MIIM_STRING + hbmpItem : hbitmap ,//HBITMAP bitmap to display or one of: (fMask==MIIM_BITMAP) + // HBMMENU_CALLBACK Bitmap that is drawn by the window that owns the menu. The application must process the WM_MEASUREITEM and WM_DRAWITEM messages + // HBMMENU_MBAR_MINIMIZE | _D | HBMMENU_MBAR_RESTORE | HBMMENU_MBAR_CLOSE | _D + // ↑ Min|dis Min | Restore | Close|Disabled close button for the menu bar + // ↓ Min|Max|Close|Restore button for the submenu + // HBMMENU_POPUP_MINIMIZE | HBMMENU_POPUP_MAXIMIZE | HBMMENU_POPUP_CLOSE | HBMMENU_POPUP_RESTORE + // HBMMENU_SYSTEM Windows icon or the icon of the window specified in dwItemData + fType : 0 ,//MENU_ITEM_TYPE requires fMask=MIIM_FTYPE, MFT_BITMAP/MFT_SEPARATOR/MFT_STRING can't be combined, set fMask to MIIM_TYPE to use fType + // MFT_OWNERDRAW Assigns responsibility for drawing the menu item to the window that owns the menu. The window receives a WM_MEASUREITEM message before the menu is displayed for the first time, and a WM_DRAWITEM message whenever the appearance of the menu item must be updated. If this value is specified, the dwTypeData member contains an application-defined value + // MFT_MENUBARBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, a vertical line separates the new column from the old + // MFT_MENUBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, the columns are not separated by a vertical line + // MFT_RADIOCHECK Displays selected menu items using a radio-button mark instead of a check mark if the hbmpChecked member is NULL + // MFT_RIGHTJUSTIFY Right-justifies the menu item and any subsequent items. This value is valid only if the menu item is in a menu bar + // MFT_RIGHTORDER Specifies that menus cascade right-to-left (the default is left-to-right). This is used to support right-to-left languages, such as Arabic and Hebrew + // MFT_SEPARATOR Specifies that the menu item is a separator. A menu item separator appears as a horizontal dividing line. The dwTypeData and cch members are ignored. This value is valid only in a drop-down menu, submenu, or shortcut menu + // MFT_STRING Displays the menu item using a text string. dwTypeData member is the pointer to a null-terminated string, and the cch member is the length of the string. replaced by MIIM_STRING + // MFT_BITMAP Displays the menu item using a bitmap . low-order word of the dwTypeData member is the bitmap handle, and the cch member is ignored. replaced by MIIM_BITMAP and hbmpItem + fState : 0 ,//MENU_ITEM_STATE requires fMask=MIIM_STATE + // MFS_ENABLED | MFS_DISABLED==MFS_GRAYED ≝Enables|disables&grays so it can|can't be selected + // MFS_CHECKED | MFS_UNCHECKED Checks|Unchecks see hbmpChecked for info + // MFS_HILITE | MFS_UNHILITE Highlights|≝No highlight + // MFS_DEFAULT Set as default (only 1), displayed in bold + // + hSubMenu : 0 ,//HMENU handle to the drop-down (sub)menu associated with the menu item. NULL if no drop-down. fMask==MIIM_SUBMENU + hbmpChecked : 0 ,//HBITMAP bitmap to display @ selected item. 0→default bitmap (✓ or • if fType=MFT_RADIOCHECK). fMask==MIIM_CHECKMARKS + hbmpUnchecked : 0 ,//HBITMAP bitmap to display @ not selected item. 0→no bitmap. fMask==MIIM_CHECKMARKS + dwTypeData : ptr::null_mut() ,//PWSTR item contents, depends on fType, used if fMask has MIIM_TYPE + wID : 0 ,//u32 app-defined item value ID. fMask==MIIM_ID, + dwItemData : 0 ,//usize app-defined item value. fMask==MIIM_DATA + cch : 0 ,//u32 . + }; + unsafe {SetMenuItemInfoW( + hmenu_par as HMENU ,//hmenu handle to the menu that contains the menu item + hmenu as u32 ,//item id/pos of the menu item to get infor about, meaning depends on the value of fByPosition + MF_BYCOMMAND as i32 ,//fByPosition meaning of uItem + // FALSE it's a menu item id + // it's a menu item position (see learn.microsoft.com/en-us/windows/desktop/menurc/about-menus) + &menu_item_info as *const _ ,//lpmii pointer to a MENUITEMINFO structure that specifies the information to retrieve and receives information about the menu item (cbSize member must be set to sizeof(MENUITEMINFO) before calling this function) + ); + } + } +} + + /// Extends [`nwg::MenuItem`] with additional functionality. pub trait MenuItemEx { fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>); From 472f5d52f4e8530b03abb47d3984768faf22ee4a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:06:26 +0700 Subject: [PATCH 084/154] gui: add nwg ext module --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b94660d86..2088fa8af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,10 @@ pub mod tests; #[cfg(all(target_os = "windows", feature = "gui"))] pub use gui_win::*; #[cfg(all(target_os = "windows", feature = "gui"))] +pub mod gui_nwg_ext; +#[cfg(all(target_os = "windows", feature = "gui"))] +pub use gui_nwg_ext::*; +#[cfg(all(target_os = "windows", feature = "gui"))] pub use win_dbg_logger as log_win; #[cfg(all(target_os = "windows", feature = "gui"))] pub use win_dbg_logger::WINDBG_LOGGER; From b41cc4cb07484ccf20acc8dc891e0766ad34104a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:09:02 +0700 Subject: [PATCH 085/154] gui: add menu item icons --- src/gui_win.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/gui_win.rs b/src/gui_win.rs index 56855512a..af51bee4a 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -12,6 +12,7 @@ use std::path::{Path, PathBuf}; use nwg::{ControlHandle, NativeUi}; use std::sync::Arc; +use core::cell::{Cell,RefCell}; trait PathExt { fn add_ext(&mut self, ext_o: impl AsRef); @@ -60,6 +61,8 @@ pub struct SystemTray { pub tray_1cfg_m: nwg::Menu, pub tray_2reload: nwg::MenuItem, pub tray_3exit: nwg::MenuItem, + pub img_reload : nwg::Bitmap, + pub img_exit : nwg::Bitmap, } pub fn get_appdata() -> Option { var_os("APPDATA").map(PathBuf::from) @@ -593,6 +596,9 @@ pub mod system_tray_ui { use super::*; use core::cmp; use native_windows_gui::{self as nwg, MousePressEvent}; + use crate::gui_nwg_ext::{BitmapEx, MenuItemEx, MenuEx}; + use windows_sys::Win32::UI::{Controls::LVSCW_AUTOSIZE_USEHEADER, + Shell::{SIID_SHIELD,SIID_DELETE,SIID_DOCASSOC}}; use std::cell::RefCell; use std::ops::Deref; use std::rc::Rc; @@ -646,6 +652,15 @@ pub mod system_tray_ui { .text("&X Exit\t‹⎈␠⎋") // .build(&mut d.tray_3exit)?; + let mut tmp_bitmap = Default::default(); + nwg::Bitmap::builder().source_embed(Some(&d.embed)).source_embed_str(Some("imgReload")).strict(true).size(Some((24,24))) + .build(&mut tmp_bitmap)?; + let img_exit = nwg::Bitmap::from_system_icon(SIID_DELETE); + d.tray_2reload .set_bitmap(Some(&tmp_bitmap)); + d.tray_3exit .set_bitmap(Some(&img_exit)); + d.img_reload = tmp_bitmap; + d.img_exit = img_exit; + { let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut let mut icon_dyn = d.icon_dyn.borrow_mut(); From fb0fb5633f5fae2d455f070599813ef515c6b4db Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Fri, 3 May 2024 01:32:40 +0700 Subject: [PATCH 086/154] gui: add reload icon --- assets/reload_32px.png | Bin 0 -> 612 bytes src/kanata.exe.manifest.rc | 1 + 2 files changed, 1 insertion(+) create mode 100644 assets/reload_32px.png diff --git a/assets/reload_32px.png b/assets/reload_32px.png new file mode 100644 index 0000000000000000000000000000000000000000..63601649d5e6b7b7bffab48f67fc53606c585684 GIT binary patch literal 612 zcmV-q0-ODbP)t$q9TerLGXh6f+&3k>rO;;=>=Q~w#9{)oo*CCOE>DOK7kj!->dZm{(+EArkPTP zBK_fGCX<}xBqwtcO_S(N{E!x&nS~5>W^Kwo#x%QXm$)X?uI9Xc@a&Pzn^%}f7=Jv&s4OJO`^(WZn5$SN1%x|MMXgZRxF+!AXN zW3S2I*=kg_nN)fu9jz1BW)*%IG8f$z)A%43WR(HTQ>GaV;!%kR`}=C}Uc+UmaTVhV zY#e?~*seLgr$2Cv1QF>aAVL^JxG$Evb^FNsaj*ar{IJP(U^Rt2$@ zJAwTvR#r(q#4L>%x^vtEG4#P^-RtBA4k0(N!S8WR$xFf-A)XN1IF|>aD5toP-4J8> y&V(LZBnF9YqMoRPQ!Eo6A`>1d-13Vm9Q^@(UYd#%{zNDM0000 Date: Sat, 4 May 2024 02:11:41 +0700 Subject: [PATCH 087/154] gui: add a loader for menu tray icons per config file --- src/gui_win.rs | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index af51bee4a..4374cdedf 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -43,15 +43,17 @@ pub struct SystemTrayData { #[derive(Default)] pub struct SystemTray { pub app_data: RefCell, - /// Store dynamically created tray menu items + /// Store dynamically created tray menu items pub tray_item_dyn: RefCell>, - /// Store dynamically created tray menu items' handlers + /// Store dynamically created tray menu items' handlers pub handlers_dyn: RefCell>, - /// Store dynamically created icons to not load them from a file every time + /// Store dynamically created icons to not load them from a file every time pub icon_dyn: RefCell>>, - /// Store 'icon_dyn' hashmap key for the currently active icon ('cfg_path:layer_name' format) + /// Store dynamically created icons to not load them from a file every time (bitmap format needed to set MenuItem's icons) + pub img_dyn : RefCell>>, + /// Store 'icon_dyn' hashmap key for the currently active icon ('cfg_path:layer_name' format) pub icon_active: RefCell>, - /// Store embedded-in-the-binary resources like icons not to load them from a file + /// Store embedded-in-the-binary resources like icons not to load them from a file pub embed: nwg::EmbedResource, pub icon: nwg::Icon, pub window: nwg::MessageWindow, @@ -664,6 +666,7 @@ pub mod system_tray_ui { { let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut let mut icon_dyn = d.icon_dyn.borrow_mut(); + let mut img_dyn = d.img_dyn .borrow_mut(); let mut icon_active = d.icon_active.borrow_mut(); const MENU_ACC: &str = "ASDFGQWERTZXCVBYUIOPHJKLNM"; let layer0_icon_s = &app_data.layer0_icon.clone().unwrap_or("".to_string()); @@ -700,7 +703,6 @@ pub mod system_tray_ui { .text(&menu_text) .build(&mut menu_item)?; } - tray_item_dyn.push(menu_item); if i == 0 { // add icons if exists, hashed by config path (for active config, others will create on load) if let Some(ico_p) = &d.get_icon_p( @@ -739,9 +741,15 @@ pub mod system_tray_ui { .build(&mut temp_icon)?; let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); *icon_active = Some(cfg_p.clone()); + // Set tray menu config item icons, ignores layers since these are per config + if let Some(temp_icon_bitmap) = set_menu_item_cfg_icon(&mut menu_item, &cfg_icon_s, &cfg_p) { + let _ = img_dyn.insert(cfg_p.clone(),Some(temp_icon_bitmap)); + } else { + let _ = img_dyn.insert(cfg_p.clone(),None); + } + } + tray_item_dyn.push(menu_item); } - } - } } else { warn!("Didn't get any config paths from Kanata!") } @@ -796,6 +804,18 @@ pub mod system_tray_ui { } } + fn set_menu_item_cfg_icon(menu_item:&mut nwg::MenuItem, cfg_icon_s:&str, cfg_p:&PathBuf) -> Option{ + if let Some(ico_p) = get_icon_p("","", &cfg_icon_s, &cfg_p, &false) { + let cfg_pkey_s = cfg_p.display().to_string(); + let mut temp_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).size(Some((24,24))).build(&mut temp_icon_bitmap) { + debug!("✓ main 0 config: using icon for {}",cfg_pkey_s); + menu_item.set_bitmap(Some(&temp_icon_bitmap)); return Some(temp_icon_bitmap) + } else {debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p);} + } + menu_item.set_bitmap(None); None + } + impl Drop for SystemTrayUi { /// To make sure that everything is freed without issues, the default handler must be unbound. fn drop(&mut self) { From a3d217259fc806cff1ef0cd73eac0d93b4d5ca0d Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:13:30 +0700 Subject: [PATCH 088/154] gui: move icon path parsing functions outside of SystemTray impl as they're more general --- src/gui_win.rs | 312 ++++++++++++++++++++++++------------------------- 1 file changed, 156 insertions(+), 156 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 4374cdedf..e957a0ea2 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -82,168 +82,168 @@ 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 use crate::lib_main::CFG; -impl SystemTray { - fn show_menu(&self) { - let (x, y) = nwg::GlobalCursor::position(); - self.tray_menu.popup(x, y); - } - /// 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` or a config file name at various locations (where config file is, where executable is, in user config folders) - fn get_icon_p( - &self, - lyr_icn: S1, - lyr_nm: S2, - cfg_icn: S3, - cfg_p: P, - match_name: &bool, - ) -> Option - where - S1: AsRef, - S2: AsRef, - S3: AsRef, - P: AsRef, - { - self.get_icon_p_impl( - lyr_icn.as_ref(), - lyr_nm.as_ref(), - cfg_icn.as_ref(), - cfg_p.as_ref(), - match_name, - ) - } - fn get_icon_p_impl( - &self, - lyr_icn: &str, - lyr_nm: &str, - cfg_icn: &str, - p: &Path, - match_name: &bool, - ) -> Option { - trace!("lyr_icn={lyr_icn} lyr_nm={lyr_nm} cfg_icn={cfg_icn} cfg_p={p:?} match_name={match_name}"); - let mut icon_file = PathBuf::new(); - let blank_p = Path::new(""); - let lyr_icn_p = Path::new(&lyr_icn); - let lyr_nm_p = Path::new(&lyr_nm); - let cfg_icn_p = Path::new(&cfg_icn); - let cfg_stem = &p.file_stem().unwrap_or_else(|| OsStr::new("")); - let cfg_name = &p.file_name().unwrap_or_else(|| OsStr::new("")); - let f_name = [ - lyr_icn_p.as_os_str(), - if *match_name { - lyr_nm_p.as_os_str() - } else { - OsStr::new("") - }, - cfg_icn_p.as_os_str(), - cfg_stem, - cfg_name, - ] - .into_iter(); - let f_ext = [ - lyr_icn_p.extension(), - if *match_name { - lyr_nm_p.extension() - } else { - None - }, - cfg_icn_p.extension(), - None, - None, - ]; - let pre_p = p.parent().unwrap_or_else(|| Path::new("")); - let cur_exe = current_exe().unwrap_or_else(|_| PathBuf::new()); - let xdg_cfg = get_xdg_home().unwrap_or_else(|| PathBuf::new()); - let app_data = get_appdata().unwrap_or_else(|| PathBuf::new()); - let mut user_cfg = get_user_home().unwrap_or_else(|| PathBuf::new()); - user_cfg.push(".config"); - let parents = [ - Path::new(""), - pre_p, - &cur_exe, - &xdg_cfg, - &app_data, - &user_cfg, - ]; // empty path to allow no prefixes when icon path is explictily set in case it's a full path already +/// 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` or a config file name at various locations (where config file is, where executable is, in user config folders) +fn get_icon_p( + &self, + lyr_icn: S1, + lyr_nm: S2, + cfg_icn: S3, + cfg_p: P, + match_name: &bool, +) -> Option +where + S1: AsRef, + S2: AsRef, + S3: AsRef, + P: AsRef, +{ + self.get_icon_p_impl( + lyr_icn.as_ref(), + lyr_nm.as_ref(), + cfg_icn.as_ref(), + cfg_p.as_ref(), + match_name, + ) +} +fn get_icon_p_impl( + &self, + lyr_icn: &str, + lyr_nm: &str, + cfg_icn: &str, + p: &Path, + match_name: &bool, +) -> Option { + trace!("lyr_icn={lyr_icn} lyr_nm={lyr_nm} cfg_icn={cfg_icn} cfg_p={p:?} match_name={match_name}"); + let mut icon_file = PathBuf::new(); + let blank_p = Path::new(""); + let lyr_icn_p = Path::new(&lyr_icn); + let lyr_nm_p = Path::new(&lyr_nm); + let cfg_icn_p = Path::new(&cfg_icn); + let cfg_stem = &p.file_stem().unwrap_or_else(|| OsStr::new("")); + let cfg_name = &p.file_name().unwrap_or_else(|| OsStr::new("")); + let f_name = [ + lyr_icn_p.as_os_str(), + if *match_name { + lyr_nm_p.as_os_str() + } else { + OsStr::new("") + }, + cfg_icn_p.as_os_str(), + cfg_stem, + cfg_name, + ] + .into_iter(); + let f_ext = [ + lyr_icn_p.extension(), + if *match_name { + lyr_nm_p.extension() + } else { + None + }, + cfg_icn_p.extension(), + None, + None, + ]; + let pre_p = p.parent().unwrap_or_else(|| Path::new("")); + let cur_exe = current_exe().unwrap_or_else(|_| PathBuf::new()); + let xdg_cfg = get_xdg_home().unwrap_or_else(|| PathBuf::new()); + let app_data = get_appdata().unwrap_or_else(|| PathBuf::new()); + let mut user_cfg = get_user_home().unwrap_or_else(|| PathBuf::new()); + user_cfg.push(".config"); + let parents = [ + Path::new(""), + pre_p, + &cur_exe, + &xdg_cfg, + &app_data, + &user_cfg, + ]; // empty path to allow no prefixes when icon path is explictily set in case it's a full path already - for (i, nm) in f_name.enumerate() { - trace!("{}nm={:?}", "", nm); - if nm.is_empty() { - trace!("no file name to test, skip"); + for (i, nm) in f_name.enumerate() { + trace!("{}nm={:?}", "", nm); + if nm.is_empty() { + trace!("no file name to test, skip"); + continue; + } + let mut is_full_p = false; + if nm == lyr_icn_p { + is_full_p = true + }; // user configs can have full paths, so test them even if all parent folders are emtpy + if nm == cfg_icn_p { + is_full_p = true + }; + let icn_ext = &f_ext[i] + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy() + .to_string(); + let is_icn_ext_valid = if !IMG_EXT.iter().any(|&i| i == icn_ext) && f_ext[i].is_some() { + warn!( + "user icon extension \"{}\" might be invalid (or just not an extension)!", + icn_ext + ); + false + } else { + trace!("icn_ext={:?}", icn_ext); + true + }; + 'p: for p_par in parents { + trace!("{}p_par={:?}", " ", p_par); + if p_par == blank_p && !is_full_p { + trace!("blank parent for non-user, skip"); continue; } - let mut is_full_p = false; - if nm == lyr_icn_p { - is_full_p = true - }; // user configs can have full paths, so test them even if all parent folders are emtpy - if nm == cfg_icn_p { - is_full_p = true - }; - let icn_ext = &f_ext[i] - .unwrap_or_else(|| OsStr::new("")) - .to_string_lossy() - .to_string(); - let is_icn_ext_valid = if !IMG_EXT.iter().any(|&i| i == icn_ext) && f_ext[i].is_some() { - warn!( - "user icon extension \"{}\" might be invalid (or just not an extension)!", - icn_ext - ); - false - } else { - trace!("icn_ext={:?}", icn_ext); - true - }; - 'p: for p_par in parents { - trace!("{}p_par={:?}", " ", p_par); - if p_par == blank_p && !is_full_p { - trace!("blank parent for non-user, skip"); - continue; - } - for p_kan in CFG_FD { - trace!("{}p_kan={:?}", " ", p_kan); - for p_icn in ASSET_FD { - trace!("{}p_icn={:?}", " ", p_icn); - for ext in IMG_EXT { - trace!("{} ext={:?}", " ", ext); - if !(p_par == blank_p) { - icon_file.push(p_par); - } // folders - if !p_kan.is_empty() { - icon_file.push(p_kan); - } - if !p_icn.is_empty() { - icon_file.push(p_icn); - } - if !nm.is_empty() { - icon_file.push(nm); - } - if !is_full_p { - icon_file.set_extension(ext); // no icon name passed, iterate extensions - } else if !is_icn_ext_valid { - icon_file.add_ext(ext); - } else { - trace!("skip ext"); - } // replace invalid icon extension - trace!("testing icon file {:?}", icon_file); - if !icon_file.is_file() { - icon_file.clear(); - if p_par == blank_p - && p_kan.is_empty() - && p_icn.is_empty() - && is_full_p - { - trace!("skipping further sub-iters on an empty parent with user config {:?}",nm); - continue 'p; - } - } else { - debug!("✓ found icon file: {}", icon_file.display().to_string()); - return Some(icon_file.display().to_string()); + for p_kan in CFG_FD { + trace!("{}p_kan={:?}", " ", p_kan); + for p_icn in ASSET_FD { + trace!("{}p_icn={:?}", " ", p_icn); + for ext in IMG_EXT { + trace!("{} ext={:?}", " ", ext); + if !(p_par == blank_p) { + icon_file.push(p_par); + } // folders + if !p_kan.is_empty() { + icon_file.push(p_kan); + } + if !p_icn.is_empty() { + icon_file.push(p_icn); + } + if !nm.is_empty() { + icon_file.push(nm); + } + if !is_full_p { + icon_file.set_extension(ext); // no icon name passed, iterate extensions + } else if !is_icn_ext_valid { + icon_file.add_ext(ext); + } else { + trace!("skip ext"); + } // replace invalid icon extension + trace!("testing icon file {:?}", icon_file); + if !icon_file.is_file() { + icon_file.clear(); + if p_par == blank_p + && p_kan.is_empty() + && p_icn.is_empty() + && is_full_p + { + trace!("skipping further sub-iters on an empty parent with user config {:?}",nm); + continue 'p; } + } else { + debug!("✓ found icon file: {}", icon_file.display().to_string()); + return Some(icon_file.display().to_string()); } } } } } - debug!("✗ no icon file found"); - return None; + } + debug!("✗ no icon file found"); + return None; +} +impl SystemTray { + fn show_menu(&self) { + let (x, y) = nwg::GlobalCursor::position(); + self.tray_menu.popup(x, y); } fn check_active(&self) { if let Some(cfg) = CFG.get() { @@ -483,7 +483,7 @@ impl SystemTray { } } else if let Some(layer_icon) = layer_icon { // 1b cfg+layer path hasn't been checked, but layer has an icon configured, so check it - if let Some(ico_p) = &self.get_icon_p( + if let Some(ico_p) = get_icon_p( &layer_icon, &layer_name, "", @@ -543,7 +543,7 @@ impl SystemTray { } else { "" }; - if let Some(ico_p) = &self.get_icon_p( + if let Some(ico_p) = get_icon_p( "", &layer_name, &cfg_icon_p, @@ -705,7 +705,7 @@ pub mod system_tray_ui { } if i == 0 { // add icons if exists, hashed by config path (for active config, others will create on load) - if let Some(ico_p) = &d.get_icon_p( + if let Some(ico_p) = get_icon_p( &layer0_icon_s, &app_data.layer0_name, &cfg_icon_s, From 759d74318faeff8f6e0264e738d6d912ffe87f5c Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:14:09 +0700 Subject: [PATCH 089/154] gui: reformat --- src/gui_win.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index e957a0ea2..0428a2af3 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -490,20 +490,20 @@ impl SystemTray { &path_cur_cc, &app_data.icon_match_layer_name, ) { - let mut temp_icon_bitmap = Default::default(); + let mut cfg_icon_bitmap = Default::default(); if let Ok(()) = nwg::Bitmap::builder() .source_file(Some(&ico_p)) .strict(false) - .build(&mut temp_icon_bitmap) + .build(&mut cfg_icon_bitmap) { debug!( "✓ Using an icon from this config+layer: {}", cfg_layer_pkey_s ); - let temp_icon = temp_icon_bitmap.copy_as_icon(); + let temp_icon = cfg_icon_bitmap.copy_as_icon(); let _ = icon_dyn.insert(cfg_layer_pkey.clone(), Some(temp_icon)); *icon_active = Some(cfg_layer_pkey); - let temp_icon = temp_icon_bitmap.copy_as_icon(); + let temp_icon = cfg_icon_bitmap.copy_as_icon(); self.tray.set_icon(&temp_icon); } else { warn!( @@ -550,20 +550,20 @@ impl SystemTray { &path_cur_cc, &app_data.icon_match_layer_name, ) { - let mut temp_icon_bitmap = Default::default(); + let mut cfg_icon_bitmap = Default::default(); if let Ok(()) = nwg::Bitmap::builder() .source_file(Some(&ico_p)) .strict(false) - .build(&mut temp_icon_bitmap) + .build(&mut cfg_icon_bitmap) { debug!( "✓ Using an icon from this config: {}", path_cur_cc.display().to_string() ); - let temp_icon = temp_icon_bitmap.copy_as_icon(); + let temp_icon = cfg_icon_bitmap.copy_as_icon(); let _ = icon_dyn.insert(cfg_layer_pkey.clone(), Some(temp_icon)); *icon_active = Some(cfg_layer_pkey); - let temp_icon = temp_icon_bitmap.copy_as_icon(); + let temp_icon = cfg_icon_bitmap.copy_as_icon(); self.tray.set_icon(&temp_icon); } else { warn!( @@ -716,16 +716,16 @@ pub mod system_tray_ui { cfg_layer_pkey.push(cfg_p.clone()); cfg_layer_pkey.push(PRE_LAYER.to_owned() + &app_data.layer0_name); let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); - let mut temp_icon_bitmap = Default::default(); + let mut cfg_icon_bitmap = Default::default(); if let Ok(()) = nwg::Bitmap::builder() .source_file(Some(&ico_p)) .strict(false) - .build(&mut temp_icon_bitmap) + .build(&mut cfg_icon_bitmap) { debug!("✓ main 0 config: using icon for {}", cfg_layer_pkey_s); - let temp_icon = temp_icon_bitmap.copy_as_icon(); + let temp_icon = cfg_icon_bitmap.copy_as_icon(); let _ = icon_dyn.insert(cfg_layer_pkey, Some(temp_icon)); - let temp_icon = temp_icon_bitmap.copy_as_icon(); + let temp_icon = cfg_icon_bitmap.copy_as_icon(); d.tray.set_icon(&temp_icon); } else { debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p); @@ -742,8 +742,8 @@ pub mod system_tray_ui { let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); *icon_active = Some(cfg_p.clone()); // Set tray menu config item icons, ignores layers since these are per config - if let Some(temp_icon_bitmap) = set_menu_item_cfg_icon(&mut menu_item, &cfg_icon_s, &cfg_p) { - let _ = img_dyn.insert(cfg_p.clone(),Some(temp_icon_bitmap)); + if let Some(cfg_icon_bitmap) = set_menu_item_cfg_icon(&mut menu_item, &cfg_icon_s, &cfg_p) { + let _ = img_dyn.insert(cfg_p.clone(),Some(cfg_icon_bitmap)); } else { let _ = img_dyn.insert(cfg_p.clone(),None); } @@ -807,10 +807,10 @@ pub mod system_tray_ui { fn set_menu_item_cfg_icon(menu_item:&mut nwg::MenuItem, cfg_icon_s:&str, cfg_p:&PathBuf) -> Option{ if let Some(ico_p) = get_icon_p("","", &cfg_icon_s, &cfg_p, &false) { let cfg_pkey_s = cfg_p.display().to_string(); - let mut temp_icon_bitmap = Default::default(); - if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).size(Some((24,24))).build(&mut temp_icon_bitmap) { + let mut cfg_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).size(Some((24,24))).build(&mut cfg_icon_bitmap) { debug!("✓ main 0 config: using icon for {}",cfg_pkey_s); - menu_item.set_bitmap(Some(&temp_icon_bitmap)); return Some(temp_icon_bitmap) + menu_item.set_bitmap(Some(&cfg_icon_bitmap)); return Some(cfg_icon_bitmap) } else {debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p);} } menu_item.set_bitmap(None); None From 9c4e5561d9b236b5d111a369cc110bcf0276c523 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:15:12 +0700 Subject: [PATCH 090/154] gui: show currently active icon in the cfg group menu --- src/gui_win.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui_win.rs b/src/gui_win.rs index 0428a2af3..32b3d3909 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -743,6 +743,7 @@ pub mod system_tray_ui { *icon_active = Some(cfg_p.clone()); // Set tray menu config item icons, ignores layers since these are per config if let Some(cfg_icon_bitmap) = set_menu_item_cfg_icon(&mut menu_item, &cfg_icon_s, &cfg_p) { + d.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); // show currently active config's icon in the combo menu let _ = img_dyn.insert(cfg_p.clone(),Some(cfg_icon_bitmap)); } else { let _ = img_dyn.insert(cfg_p.clone(),None); From c490e0b975b9164299e4ac25d1482798865a1b96 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:17:37 +0700 Subject: [PATCH 091/154] future fix: revert index on failed config reload --- src/kanata/windows/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index c2fdd019d..32aabcb64 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -141,6 +141,7 @@ impl Kanata { pub fn live_reload_n(&mut self, n: usize, gui_tx: nwg::NoticeSender) -> Result<()> { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow self.live_reload_requested = true; + // let backup_cfg_idx = self.cur_cfg_idx; match self.cfg_paths.get(n) { Some(path) => { self.cur_cfg_idx = n; @@ -150,6 +151,10 @@ impl Kanata { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } + // if let Err(e) = self.do_live_reload(&None,gui_tx) { + // self.cur_cfg_idx = backup_cfg_idx; // restore index on fail when. TODO: add when a similar reversion is added to other custom actions + // return Err(e) + // } self.do_live_reload(&None, gui_tx)?; Ok(()) } From 81caca1b8c7bf9d9a3896a81211de07c9f21ba89 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:22:49 +0700 Subject: [PATCH 092/154] gui: return Result from reload_cfg --- src/gui_win.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 32b3d3909..95a592f36 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -1,5 +1,5 @@ use crate::Kanata; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use core::cell::RefCell; use log::Level::*; @@ -274,7 +274,7 @@ impl SystemTray { }; } /// Reload config file, currently active (`i=None`) or matching a given `i` index - fn reload_cfg(&self, i: Option) { + fn reload_cfg(&self, i:Option) -> Result<()> { use nwg::TrayNotificationFlags as f_tray; let mut msg_title = "".to_string(); let mut msg_content = "".to_string(); @@ -334,7 +334,7 @@ impl SystemTray { Some(flags), Some(&self.icon), ); - return; + bail!("{msg_content}"); } } None => { @@ -350,7 +350,7 @@ impl SystemTray { Some(flags), Some(&self.icon), ); - return; + bail!("{msg_content}"); } } }; @@ -401,6 +401,7 @@ impl SystemTray { Some(flags), Some(&self.icon), ); + Ok(()) } /// Update tray icon data on layer change fn reload_layer_icon(&self) { @@ -773,18 +774,19 @@ pub mod system_tray_ui { E::OnMenuHover => if &handle == &evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} E::OnMenuItemSelected => - if &handle == &evt_ui.tray_2reload {SystemTray::reload_cfg(&evt_ui,None); - } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); + if &handle == &evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None); + } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); } else { match handle { ControlHandle::MenuItem(_parent, _id) => { let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // for (i, h_cfg) in tray_item_dyn.iter().enumerate() { - if &handle == h_cfg { //info!("CONFIG handle i={:?} {:?}",i,&handle); + // if SystemTray::reload_cfg(&evt_ui,Some(i)).is_ok() { for (_j, h_cfg_j) in tray_item_dyn.iter().enumerate() { if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others h_cfg.set_checked(true); // check self - SystemTray::reload_cfg(&evt_ui,Some(i)); + let _ = SystemTray::reload_cfg(&evt_ui,Some(i)); // depends on future fix in kanata that would revert index on failed config changes + // } else {info!("OnMenuItemSelected: checkmarks not changed since config wasn't reloaded");} } } }, From 1686e7d9aee1ba9f1d0fa7c85e7ea8f69caa0a46 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:26:09 +0700 Subject: [PATCH 093/154] gui: update config tray icons on menu hover --- src/gui_win.rs | 58 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 95a592f36..fb8431123 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -245,29 +245,51 @@ impl SystemTray { let (x, y) = nwg::GlobalCursor::position(); self.tray_menu.popup(x, y); } + /// Add a ✓ (or highlight the icon) to the currently active config. Runs on opening of the list of configs menu + fn update_tray_icon_cfg(&self,menu_item_cfg:&mut nwg::MenuItem,cfg_p:&PathBuf,is_active:bool) -> Result<()> { + let mut img_dyn = self.img_dyn.borrow_mut(); + if img_dyn.contains_key(cfg_p) { // check if menu group icon needs to be updated to match active + if is_active { + if let Some(cfg_icon_bitmap) = img_dyn.get(cfg_p) { + self.tray_1cfg_m.set_bitmap(cfg_icon_bitmap.as_ref()); + } + } + } else {trace!("config menu item icon missing, read config and add it (or nothing) {cfg_p:?}"); + if let Ok(cfg) = cfg::new_from_file(&cfg_p) { + if let Some(cfg_icon_s) = cfg.options.tray_icon {debug!("loaded config without a tray icon {cfg_p:?}"); + if let Some(cfg_icon_bitmap) = set_menu_item_cfg_icon(menu_item_cfg, &cfg_icon_s, &cfg_p) { + if is_active {self.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap));} // update currently active config's icon in the combo menu + debug!("✓set icon {cfg_p:?}"); + let _ = img_dyn.insert(cfg_p.clone(),Some(cfg_icon_bitmap)); + } else {bail!("✗couldn't get a valid icon")} + } else {bail!("✗icon not configured")} + } else {bail!("✗couldn't load config")} + } + Ok(()) + } fn check_active(&self) { - if let Some(cfg) = CFG.get() { - let k = cfg.lock(); - let idx_cfg = k.cur_cfg_idx; - let tray_item_dyn = &self.tray_item_dyn.borrow(); // - for (i, h_cfg_i) in tray_item_dyn.iter().enumerate() { - if h_cfg_i.checked() { - trace!( - "✓checked {} active {} eq? {} !eq? {}", - i, - idx_cfg, - i == idx_cfg, - !(i == idx_cfg) - ); - } - if h_cfg_i.checked() && !(i == idx_cfg) { + if let Some(cfg) = CFG.get() {let k = cfg.lock(); + let idx_cfg = k.cur_cfg_idx; + let mut tray_item_dyn = self.tray_item_dyn .borrow_mut(); + for (i, mut h_cfg_i) in tray_item_dyn.iter_mut().enumerate() { + // 1 if missing an icon, read config to get one + let cfg_p = &k.cfg_paths[i]; trace!(" →→→→ i={i:?} {:?} cfg_p={cfg_p:?}",h_cfg_i.handle); // change to trace todo + let is_active = i==idx_cfg; + if let Err(e) = self.update_tray_icon_cfg(&mut h_cfg_i,&cfg_p,is_active){ + info!("{e:?} {cfg_p:?}"); + let mut img_dyn = self.img_dyn.borrow_mut(); + img_dyn.insert(cfg_p.clone(),None); + if i==idx_cfg {self.tray_1cfg_m.set_bitmap(None);} // update currently active config's icon in the combo menu + }; + // 2 if wrong GUI checkmark, correct it + if h_cfg_i.checked() && !is_active { debug!("uncheck i{} act{}", i, idx_cfg); h_cfg_i.set_checked(false); - } // uncheck inactive - if !h_cfg_i.checked() && i == idx_cfg { + } + if !h_cfg_i.checked() && is_active { debug!(" check i{} act{}", i, idx_cfg); h_cfg_i.set_checked(true); - } // check active + } } } else { error!("no CFG var that contains active kanata config"); From 071195ae03d1f93184f59b2f9e4e392b9ef87fd9 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:27:55 +0700 Subject: [PATCH 094/154] gui: move set_menu_item_cfg_icon out of UI --- src/gui_win.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index fb8431123..f85472524 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -11,6 +11,8 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use nwg::{ControlHandle, NativeUi}; +use crate::gui_nwg_ext::{BitmapEx, MenuItemEx, MenuEx}; +use kanata_parser::cfg; use std::sync::Arc; use core::cell::{Cell,RefCell}; @@ -240,6 +242,19 @@ fn get_icon_p_impl( debug!("✗ no icon file found"); return None; } + +fn set_menu_item_cfg_icon(menu_item:&mut nwg::MenuItem, cfg_icon_s:&str, cfg_p:&PathBuf) -> Option{ + if let Some(ico_p) = get_icon_p("","", &cfg_icon_s, &cfg_p, &false) { + let cfg_pkey_s = cfg_p.display().to_string(); + let mut cfg_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).size(Some((24,24))).build(&mut cfg_icon_bitmap) { + debug!("✓ main 0 config: using icon for {}",cfg_pkey_s); + menu_item.set_bitmap(Some(&cfg_icon_bitmap)); return Some(cfg_icon_bitmap) + } else {debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p);} + } + menu_item.set_bitmap(None); None +} + impl SystemTray { fn show_menu(&self) { let (x, y) = nwg::GlobalCursor::position(); @@ -621,7 +636,6 @@ pub mod system_tray_ui { use super::*; use core::cmp; use native_windows_gui::{self as nwg, MousePressEvent}; - use crate::gui_nwg_ext::{BitmapEx, MenuItemEx, MenuEx}; use windows_sys::Win32::UI::{Controls::LVSCW_AUTOSIZE_USEHEADER, Shell::{SIID_SHIELD,SIID_DELETE,SIID_DOCASSOC}}; use std::cell::RefCell; @@ -829,18 +843,6 @@ pub mod system_tray_ui { } } - fn set_menu_item_cfg_icon(menu_item:&mut nwg::MenuItem, cfg_icon_s:&str, cfg_p:&PathBuf) -> Option{ - if let Some(ico_p) = get_icon_p("","", &cfg_icon_s, &cfg_p, &false) { - let cfg_pkey_s = cfg_p.display().to_string(); - let mut cfg_icon_bitmap = Default::default(); - if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).size(Some((24,24))).build(&mut cfg_icon_bitmap) { - debug!("✓ main 0 config: using icon for {}",cfg_pkey_s); - menu_item.set_bitmap(Some(&cfg_icon_bitmap)); return Some(cfg_icon_bitmap) - } else {debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p);} - } - menu_item.set_bitmap(None); None - } - impl Drop for SystemTrayUi { /// To make sure that everything is freed without issues, the default handler must be unbound. fn drop(&mut self) { From 580143186160a48a20376b2f12b331a96390a3fe Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:30:42 +0700 Subject: [PATCH 095/154] gui: add a function to update config group icon and test it on each popup and after reloads --- src/gui_win.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index f85472524..59bea4ec1 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -257,6 +257,7 @@ fn set_menu_item_cfg_icon(menu_item:&mut nwg::MenuItem, cfg_icon_s:&str, cfg_p:& impl SystemTray { fn show_menu(&self) { + self.update_tray_icon_cfg_group(false); let (x, y) = nwg::GlobalCursor::position(); self.tray_menu.popup(x, y); } @@ -282,6 +283,25 @@ impl SystemTray { } Ok(()) } + fn update_tray_icon_cfg_group(&self,force:bool) { + if let Some(cfg) = CFG.get() {if let Some(k) = cfg.try_lock() { + let idx_cfg = k.cur_cfg_idx; + let mut tray_item_dyn = self.tray_item_dyn .borrow_mut(); + let h_cfg_i = &mut tray_item_dyn[idx_cfg]; + let is_check = h_cfg_i.checked(); + if ! is_check || force { + let cfg_p = &k.cfg_paths[idx_cfg]; debug!("✗ mismatch idx_cfg={idx_cfg:?} {} {:?} cfg_p={cfg_p:?}",if is_check {"✓"}else{"✗"}, h_cfg_i.handle); + h_cfg_i.set_checked(true); + if let Err(e) = self.update_tray_icon_cfg(h_cfg_i,&cfg_p,true){ + debug!("{e:?} {cfg_p:?}"); + let mut img_dyn = self.img_dyn.borrow_mut(); + img_dyn.insert(cfg_p.clone(),None); + self.tray_1cfg_m.set_bitmap(None); // can't update menu, so remove combo menu icon + }; + } else {debug!("gui cfg selection matches active config");}; + } else {debug!("✗ kanata config is locked, can't get current config (likely the gui changed the layer and is still holding the lock, it will update the icon)");} + }; + } fn check_active(&self) { if let Some(cfg) = CFG.get() {let k = cfg.lock(); let idx_cfg = k.cur_cfg_idx; @@ -810,12 +830,12 @@ pub mod system_tray_ui { E::OnMenuHover => if &handle == &evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} E::OnMenuItemSelected => - if &handle == &evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None); + if &handle == &evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None);SystemTray::update_tray_icon_cfg_group(&evt_ui,true); } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); } else { match handle { ControlHandle::MenuItem(_parent, _id) => { - let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // + {let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // for (i, h_cfg) in tray_item_dyn.iter().enumerate() { // if SystemTray::reload_cfg(&evt_ui,Some(i)).is_ok() { for (_j, h_cfg_j) in tray_item_dyn.iter().enumerate() { @@ -825,6 +845,8 @@ pub mod system_tray_ui { // } else {info!("OnMenuItemSelected: checkmarks not changed since config wasn't reloaded");} } } + } + SystemTray::update_tray_icon_cfg_group(&evt_ui,true); }, _ => {}, } From a5e2c53fc563d3f734954986218904a39f9c2579 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:31:43 +0700 Subject: [PATCH 096/154] gui: reset menu item image cache as well on config reload --- src/gui_win.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui_win.rs b/src/gui_win.rs index 59bea4ec1..9ac8af4c4 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -520,9 +520,11 @@ impl SystemTray { ) { let mut icon_dyn = self.icon_dyn.borrow_mut(); // update the tray icon let mut icon_active = self.icon_active.borrow_mut(); // update the tray icon active path + let mut img_dyn = self.img_dyn .borrow_mut(); // update the tray images if clear { *icon_dyn = Default::default(); *icon_active = Default::default(); + *img_dyn = Default::default(); debug!("reloading active config, clearing icon_dyn/_active cache"); } let app_data = self.app_data.borrow(); From bfed72d1cb023c890e39f2e8dc00c5937463875b Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:33:19 +0700 Subject: [PATCH 097/154] gui: avoid main tray icon flashing from config icon to layer icon --- src/gui_win.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 9ac8af4c4..5460009bc 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -691,11 +691,6 @@ pub mod system_tray_ui { nwg::Notice::builder() .parent(&d.window) .build(&mut d.layer_notice)?; - nwg::TrayNotification::builder() - .parent(&d.window) - .icon(Some(&d.icon)) - .tip(Some(&app_data.tooltip)) - .build(&mut d.tray)?; nwg::Menu::builder() .parent(&d.window) .popup(true) /*context menu*/ // @@ -722,6 +717,8 @@ pub mod system_tray_ui { d.img_reload = tmp_bitmap; d.img_exit = img_exit; + let mut main_tray_icon_l = Default::default(); + let mut main_tray_icon_is = false; { let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut let mut icon_dyn = d.icon_dyn.borrow_mut(); @@ -785,7 +782,8 @@ pub mod system_tray_ui { let temp_icon = cfg_icon_bitmap.copy_as_icon(); let _ = icon_dyn.insert(cfg_layer_pkey, Some(temp_icon)); let temp_icon = cfg_icon_bitmap.copy_as_icon(); - d.tray.set_icon(&temp_icon); + main_tray_icon_l = temp_icon; + main_tray_icon_is = true; } else { debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p); let _ = icon_dyn.insert(cfg_layer_pkey, None); @@ -814,6 +812,11 @@ pub mod system_tray_ui { warn!("Didn't get any config paths from Kanata!") } } + let main_tray_icon = match main_tray_icon_is { + true => Some(&main_tray_icon_l), + false => Some(&d.icon),}; + nwg::TrayNotification ::builder().parent(&d.window) .icon(main_tray_icon) .tip(Some(&app_data.tooltip)) + . build( &mut d.tray )? ; let ui = SystemTrayUi { // Wrap-up From 5fa298d0321bac145cec70ff2fd78e946636e519 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:35:48 +0700 Subject: [PATCH 098/154] gui: move ui back to event loop to avoid panicks on exits via GUI --- src/kanata/windows/llhook.rs | 4 +++- src/lib_main.rs | 30 ++++++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 7becbedd5..83945533e 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -10,7 +10,7 @@ use crate::kanata::*; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events /// and run the native_windows_gui event loop. - pub fn event_loop(_kanata: Arc>, tx: Sender) -> Result<()> { + pub fn event_loop(_cfg: Arc>, tx: Sender, #[cfg(all(target_os = "windows", feature = "gui"))] ui:system_tray_ui::SystemTrayUi) -> Result<()> { // Display debug and panic output when launched from a terminal. #[cfg(not(feature = "gui"))] unsafe { @@ -66,6 +66,8 @@ impl Kanata { true }); + #[cfg(all(target_os = "windows", feature = "gui"))] + let _ui = ui; // prevents thread from panicking on exiting via a GUI // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); Ok(()) diff --git a/src/lib_main.rs b/src/lib_main.rs index 4463db8d8..2ce434c1a 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -255,22 +255,26 @@ fn main_impl() -> Result<()> { (None, None, None) }; - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))]{ Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - #[cfg(all(target_os = "windows", feature = "gui"))] + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } + + #[cfg(target_os = "linux")] + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; + + Kanata::event_loop(kanata_arc, tx)?; + } + + #[cfg(all(target_os = "windows", feature = "gui"))] { use anyhow::Context; - #[cfg(all(target_os = "windows", feature = "gui"))] use native_windows_gui as nwg; - #[cfg(all(target_os = "windows", feature = "gui"))] native_windows_gui::init().context("Failed to init Native Windows GUI")?; - #[cfg(all(target_os = "windows", feature = "gui"))] let ui = build_tray(&kanata_arc)?; - #[cfg(all(target_os = "windows", feature = "gui"))] - let noticer: &nwg::Notice = &ui.layer_notice; - #[cfg(all(target_os = "windows", feature = "gui"))] - let gui_tx = noticer.sender(); - #[cfg(all(target_os = "windows", feature = "gui"))] + let gui_tx = ui.layer_notice.sender(); Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, gui_tx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) { @@ -278,10 +282,8 @@ fn main_impl() -> Result<()> { Kanata::start_notification_loop(nrx, server.connections); } - #[cfg(target_os = "linux")] - sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - - Kanata::event_loop(kanata_arc, tx)?; + Kanata::event_loop(kanata_arc, tx, ui)?; + } Ok(()) } From 2bd54318d6dd7d000c82448c8a05b99ad014584a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 02:28:23 +0700 Subject: [PATCH 099/154] fix formatting --- src/gui_nwg_ext.rs | 132 ++++++++------- src/gui_win.rs | 320 ++++++++++++++++++++--------------- src/kanata/windows/llhook.rs | 8 +- src/kanata/windows/mod.rs | 4 +- src/lib_main.rs | 46 ++--- 5 files changed, 288 insertions(+), 222 deletions(-) diff --git a/src/gui_nwg_ext.rs b/src/gui_nwg_ext.rs index 5d8cfce01..cfb9bbf6a 100644 --- a/src/gui_nwg_ext.rs +++ b/src/gui_nwg_ext.rs @@ -2,15 +2,13 @@ use native_windows_gui as nwg; use windows_sys::Win32::Foundation::HANDLE; -use windows_sys::Win32::Graphics::Gdi::{DeleteObject,HBRUSH}; +use windows_sys::Win32::Graphics::Gdi::DeleteObject; use windows_sys::Win32::UI::Shell::{ SHGetStockIconInfo, SHGSI_ICON, SHGSI_SMALLICON, SHSTOCKICONID, SHSTOCKICONINFO, }; use windows_sys::Win32::UI::WindowsAndMessaging::{ - CopyImage, DestroyIcon, GetIconInfoExW, SetMenuInfo, SetMenuItemInfoW, SetMenuItemBitmaps, - HMENU, ICONINFOEXW, IMAGE_BITMAP, - LR_CREATEDIBSECTION, MENUINFO, MENUITEMINFOW, MF_BYCOMMAND, MIIM_BITMAP,MIM_STYLE,MIM_BACKGROUND - ,MNS_NOTIFYBYPOS,MNS_CHECKORBMP,MNS_AUTODISMISS,MF_BYPOSITION,MENU_ITEM_FLAGS + CopyImage, DestroyIcon, GetIconInfoExW, SetMenuItemInfoW, HMENU, ICONINFOEXW, IMAGE_BITMAP, + LR_CREATEDIBSECTION, MENUITEMINFOW, MF_BYCOMMAND, MIIM_BITMAP, }; /// Extends [`nwg::Bitmap`] with additional functionality. @@ -92,69 +90,75 @@ impl BitmapEx for nwg::Bitmap { } /// Extends [`nwg::Menu`] with additional functionality. -pub trait MenuEx {fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>);} -impl MenuEx for nwg::Menu { /// Sets a bitmap to be displayed on a menu. Pass `None` to remove the bitmap - fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>) { - use std::{ptr,mem::size_of}; - let (hmenu_par, hmenu) = self.handle.hmenu().unwrap(); - let hbitmap = match bitmap {Some(b) => b.handle as HANDLE, None => 0,}; +pub trait MenuEx { + fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>); +} +impl MenuEx for nwg::Menu { + /// Sets a bitmap to be displayed on a menu. Pass `None` to remove the bitmap + fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>) { + use std::{mem::size_of, ptr}; + let (hmenu_par, hmenu) = self.handle.hmenu().unwrap(); + let hbitmap = match bitmap { + Some(b) => b.handle as HANDLE, + None => 0, + }; - let menu_item_info = MENUITEMINFOW { - cbSize : size_of::() as u32 ,//u32 byte-size of the structure, must be set by the caller to sizeof(MENUITEMINFO) - fMask : MIIM_BITMAP ,//MENU_ITEM_MASK members to be retrieved or set: - // MIIM_BITMAP hbmpItem - // MIIM_STATE fState - // MIIM_CHECKMARKS hbmpChecked and hbmpUnchecked - // MIIM_DATA dwItemData - // MIIM_FTYPE fType - // MIIM_ID wID - // MIIM_STRING dwTypeData - // MIIM_SUBMENU hSubMenu - // MIIM_TYPE fType and dwTypeData, replaced by MIIM_BITMAP, MIIM_FTYPE, and MIIM_STRING - hbmpItem : hbitmap ,//HBITMAP bitmap to display or one of: (fMask==MIIM_BITMAP) - // HBMMENU_CALLBACK Bitmap that is drawn by the window that owns the menu. The application must process the WM_MEASUREITEM and WM_DRAWITEM messages - // HBMMENU_MBAR_MINIMIZE | _D | HBMMENU_MBAR_RESTORE | HBMMENU_MBAR_CLOSE | _D - // ↑ Min|dis Min | Restore | Close|Disabled close button for the menu bar - // ↓ Min|Max|Close|Restore button for the submenu - // HBMMENU_POPUP_MINIMIZE | HBMMENU_POPUP_MAXIMIZE | HBMMENU_POPUP_CLOSE | HBMMENU_POPUP_RESTORE - // HBMMENU_SYSTEM Windows icon or the icon of the window specified in dwItemData - fType : 0 ,//MENU_ITEM_TYPE requires fMask=MIIM_FTYPE, MFT_BITMAP/MFT_SEPARATOR/MFT_STRING can't be combined, set fMask to MIIM_TYPE to use fType - // MFT_OWNERDRAW Assigns responsibility for drawing the menu item to the window that owns the menu. The window receives a WM_MEASUREITEM message before the menu is displayed for the first time, and a WM_DRAWITEM message whenever the appearance of the menu item must be updated. If this value is specified, the dwTypeData member contains an application-defined value - // MFT_MENUBARBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, a vertical line separates the new column from the old - // MFT_MENUBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, the columns are not separated by a vertical line - // MFT_RADIOCHECK Displays selected menu items using a radio-button mark instead of a check mark if the hbmpChecked member is NULL - // MFT_RIGHTJUSTIFY Right-justifies the menu item and any subsequent items. This value is valid only if the menu item is in a menu bar - // MFT_RIGHTORDER Specifies that menus cascade right-to-left (the default is left-to-right). This is used to support right-to-left languages, such as Arabic and Hebrew - // MFT_SEPARATOR Specifies that the menu item is a separator. A menu item separator appears as a horizontal dividing line. The dwTypeData and cch members are ignored. This value is valid only in a drop-down menu, submenu, or shortcut menu - // MFT_STRING Displays the menu item using a text string. dwTypeData member is the pointer to a null-terminated string, and the cch member is the length of the string. replaced by MIIM_STRING - // MFT_BITMAP Displays the menu item using a bitmap . low-order word of the dwTypeData member is the bitmap handle, and the cch member is ignored. replaced by MIIM_BITMAP and hbmpItem - fState : 0 ,//MENU_ITEM_STATE requires fMask=MIIM_STATE - // MFS_ENABLED | MFS_DISABLED==MFS_GRAYED ≝Enables|disables&grays so it can|can't be selected - // MFS_CHECKED | MFS_UNCHECKED Checks|Unchecks see hbmpChecked for info - // MFS_HILITE | MFS_UNHILITE Highlights|≝No highlight - // MFS_DEFAULT Set as default (only 1), displayed in bold - // - hSubMenu : 0 ,//HMENU handle to the drop-down (sub)menu associated with the menu item. NULL if no drop-down. fMask==MIIM_SUBMENU - hbmpChecked : 0 ,//HBITMAP bitmap to display @ selected item. 0→default bitmap (✓ or • if fType=MFT_RADIOCHECK). fMask==MIIM_CHECKMARKS - hbmpUnchecked : 0 ,//HBITMAP bitmap to display @ not selected item. 0→no bitmap. fMask==MIIM_CHECKMARKS - dwTypeData : ptr::null_mut() ,//PWSTR item contents, depends on fType, used if fMask has MIIM_TYPE - wID : 0 ,//u32 app-defined item value ID. fMask==MIIM_ID, - dwItemData : 0 ,//usize app-defined item value. fMask==MIIM_DATA - cch : 0 ,//u32 . - }; - unsafe {SetMenuItemInfoW( - hmenu_par as HMENU ,//hmenu handle to the menu that contains the menu item - hmenu as u32 ,//item id/pos of the menu item to get infor about, meaning depends on the value of fByPosition - MF_BYCOMMAND as i32 ,//fByPosition meaning of uItem - // FALSE it's a menu item id - // it's a menu item position (see learn.microsoft.com/en-us/windows/desktop/menurc/about-menus) - &menu_item_info as *const _ ,//lpmii pointer to a MENUITEMINFO structure that specifies the information to retrieve and receives information about the menu item (cbSize member must be set to sizeof(MENUITEMINFO) before calling this function) - ); + let menu_item_info = MENUITEMINFOW { + cbSize: size_of::() as u32, //u32 byte-size of the structure, must be set by the caller to sizeof(MENUITEMINFO) + fMask: MIIM_BITMAP, //MENU_ITEM_MASK members to be retrieved or set: + // MIIM_BITMAP hbmpItem + // MIIM_STATE fState + // MIIM_CHECKMARKS hbmpChecked and hbmpUnchecked + // MIIM_DATA dwItemData + // MIIM_FTYPE fType + // MIIM_ID wID + // MIIM_STRING dwTypeData + // MIIM_SUBMENU hSubMenu + // MIIM_TYPE fType and dwTypeData, replaced by MIIM_BITMAP, MIIM_FTYPE, and MIIM_STRING + hbmpItem: hbitmap, //HBITMAP bitmap to display or one of: (fMask==MIIM_BITMAP) + // HBMMENU_CALLBACK Bitmap that is drawn by the window that owns the menu. The application must process the WM_MEASUREITEM and WM_DRAWITEM messages + // HBMMENU_MBAR_MINIMIZE | _D | HBMMENU_MBAR_RESTORE | HBMMENU_MBAR_CLOSE | _D + // ↑ Min|dis Min | Restore | Close|Disabled close button for the menu bar + // ↓ Min|Max|Close|Restore button for the submenu + // HBMMENU_POPUP_MINIMIZE | HBMMENU_POPUP_MAXIMIZE | HBMMENU_POPUP_CLOSE | HBMMENU_POPUP_RESTORE + // HBMMENU_SYSTEM Windows icon or the icon of the window specified in dwItemData + fType: 0, //MENU_ITEM_TYPE requires fMask=MIIM_FTYPE, MFT_BITMAP/MFT_SEPARATOR/MFT_STRING can't be combined, set fMask to MIIM_TYPE to use fType + // MFT_OWNERDRAW Assigns responsibility for drawing the menu item to the window that owns the menu. The window receives a WM_MEASUREITEM message before the menu is displayed for the first time, and a WM_DRAWITEM message whenever the appearance of the menu item must be updated. If this value is specified, the dwTypeData member contains an application-defined value + // MFT_MENUBARBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, a vertical line separates the new column from the old + // MFT_MENUBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, the columns are not separated by a vertical line + // MFT_RADIOCHECK Displays selected menu items using a radio-button mark instead of a check mark if the hbmpChecked member is NULL + // MFT_RIGHTJUSTIFY Right-justifies the menu item and any subsequent items. This value is valid only if the menu item is in a menu bar + // MFT_RIGHTORDER Specifies that menus cascade right-to-left (the default is left-to-right). This is used to support right-to-left languages, such as Arabic and Hebrew + // MFT_SEPARATOR Specifies that the menu item is a separator. A menu item separator appears as a horizontal dividing line. The dwTypeData and cch members are ignored. This value is valid only in a drop-down menu, submenu, or shortcut menu + // MFT_STRING Displays the menu item using a text string. dwTypeData member is the pointer to a null-terminated string, and the cch member is the length of the string. replaced by MIIM_STRING + // MFT_BITMAP Displays the menu item using a bitmap . low-order word of the dwTypeData member is the bitmap handle, and the cch member is ignored. replaced by MIIM_BITMAP and hbmpItem + fState: 0, //MENU_ITEM_STATE requires fMask=MIIM_STATE + // MFS_ENABLED | MFS_DISABLED==MFS_GRAYED ≝Enables|disables&grays so it can|can't be selected + // MFS_CHECKED | MFS_UNCHECKED Checks|Unchecks see hbmpChecked for info + // MFS_HILITE | MFS_UNHILITE Highlights|≝No highlight + // MFS_DEFAULT Set as default (only 1), displayed in bold + // + hSubMenu: 0, //HMENU handle to the drop-down (sub)menu associated with the menu item. NULL if no drop-down. fMask==MIIM_SUBMENU + hbmpChecked: 0, //HBITMAP bitmap to display @ selected item. 0→default bitmap (✓ or • if fType=MFT_RADIOCHECK). fMask==MIIM_CHECKMARKS + hbmpUnchecked: 0, //HBITMAP bitmap to display @ not selected item. 0→no bitmap. fMask==MIIM_CHECKMARKS + dwTypeData: ptr::null_mut(), //PWSTR item contents, depends on fType, used if fMask has MIIM_TYPE + wID: 0, //u32 app-defined item value ID. fMask==MIIM_ID, + dwItemData: 0, //usize app-defined item value. fMask==MIIM_DATA + cch: 0, //u32 . + }; + unsafe { + SetMenuItemInfoW( + hmenu_par as HMENU, //hmenu handle to the menu that contains the menu item + hmenu as u32, //item id/pos of the menu item to get infor about, meaning depends on the value of fByPosition + MF_BYCOMMAND as i32, //fByPosition meaning of uItem + // FALSE it's a menu item id + // it's a menu item position (see learn.microsoft.com/en-us/windows/desktop/menurc/about-menus) + &menu_item_info as *const _, //lpmii pointer to a MENUITEMINFO structure that specifies the information to retrieve and receives information about the menu item (cbSize member must be set to sizeof(MENUITEMINFO) before calling this function) + ); + } } - } } - /// Extends [`nwg::MenuItem`] with additional functionality. pub trait MenuItemEx { fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>); diff --git a/src/gui_win.rs b/src/gui_win.rs index 5460009bc..4c34184b7 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -1,5 +1,5 @@ use crate::Kanata; -use anyhow::{Context, Result, bail}; +use anyhow::{bail, Context, Result}; use core::cell::RefCell; use log::Level::*; @@ -10,11 +10,10 @@ use std::env::{current_exe, var_os}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use nwg::{ControlHandle, NativeUi}; -use crate::gui_nwg_ext::{BitmapEx, MenuItemEx, MenuEx}; +use crate::gui_nwg_ext::{BitmapEx, MenuEx, MenuItemEx}; use kanata_parser::cfg; +use nwg::{ControlHandle, NativeUi}; use std::sync::Arc; -use core::cell::{Cell,RefCell}; trait PathExt { fn add_ext(&mut self, ext_o: impl AsRef); @@ -52,7 +51,7 @@ pub struct SystemTray { /// Store dynamically created icons to not load them from a file every time pub icon_dyn: RefCell>>, /// Store dynamically created icons to not load them from a file every time (bitmap format needed to set MenuItem's icons) - pub img_dyn : RefCell>>, + pub img_dyn: RefCell>>, /// Store 'icon_dyn' hashmap key for the currently active icon ('cfg_path:layer_name' format) pub icon_active: RefCell>, /// Store embedded-in-the-binary resources like icons not to load them from a file @@ -65,8 +64,8 @@ pub struct SystemTray { pub tray_1cfg_m: nwg::Menu, pub tray_2reload: nwg::MenuItem, pub tray_3exit: nwg::MenuItem, - pub img_reload : nwg::Bitmap, - pub img_exit : nwg::Bitmap, + pub img_reload: nwg::Bitmap, + pub img_exit: nwg::Bitmap, } pub fn get_appdata() -> Option { var_os("APPDATA").map(PathBuf::from) @@ -86,7 +85,6 @@ use crate::lib_main::CFG; /// 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` or a config file name at various locations (where config file is, where executable is, in user config folders) fn get_icon_p( - &self, lyr_icn: S1, lyr_nm: S2, cfg_icn: S3, @@ -99,7 +97,7 @@ where S3: AsRef, P: AsRef, { - self.get_icon_p_impl( + get_icon_p_impl( lyr_icn.as_ref(), lyr_nm.as_ref(), cfg_icn.as_ref(), @@ -107,15 +105,17 @@ where match_name, ) } + fn get_icon_p_impl( - &self, lyr_icn: &str, lyr_nm: &str, cfg_icn: &str, p: &Path, match_name: &bool, ) -> Option { - trace!("lyr_icn={lyr_icn} lyr_nm={lyr_nm} cfg_icn={cfg_icn} cfg_p={p:?} match_name={match_name}"); + trace!( + "lyr_icn={lyr_icn} lyr_nm={lyr_nm} cfg_icn={cfg_icn} cfg_p={p:?} match_name={match_name}" + ); let mut icon_file = PathBuf::new(); let blank_p = Path::new(""); let lyr_icn_p = Path::new(&lyr_icn); @@ -222,10 +222,7 @@ fn get_icon_p_impl( trace!("testing icon file {:?}", icon_file); if !icon_file.is_file() { icon_file.clear(); - if p_par == blank_p - && p_kan.is_empty() - && p_icn.is_empty() - && is_full_p + if p_par == blank_p && p_kan.is_empty() && p_icn.is_empty() && is_full_p { trace!("skipping further sub-iters on an empty parent with user config {:?}",nm); continue 'p; @@ -243,16 +240,32 @@ fn get_icon_p_impl( return None; } -fn set_menu_item_cfg_icon(menu_item:&mut nwg::MenuItem, cfg_icon_s:&str, cfg_p:&PathBuf) -> Option{ - if let Some(ico_p) = get_icon_p("","", &cfg_icon_s, &cfg_p, &false) { - let cfg_pkey_s = cfg_p.display().to_string(); - let mut cfg_icon_bitmap = Default::default(); - if let Ok(()) = nwg::Bitmap::builder().source_file(Some(&ico_p)).strict(false).size(Some((24,24))).build(&mut cfg_icon_bitmap) { - debug!("✓ main 0 config: using icon for {}",cfg_pkey_s); - menu_item.set_bitmap(Some(&cfg_icon_bitmap)); return Some(cfg_icon_bitmap) - } else {debug!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p);} - } - menu_item.set_bitmap(None); None +fn set_menu_item_cfg_icon( + menu_item: &mut nwg::MenuItem, + cfg_icon_s: &str, + cfg_p: &PathBuf, +) -> Option { + if let Some(ico_p) = get_icon_p("", "", &cfg_icon_s, &cfg_p, &false) { + let cfg_pkey_s = cfg_p.display().to_string(); + let mut cfg_icon_bitmap = Default::default(); + if let Ok(()) = nwg::Bitmap::builder() + .source_file(Some(&ico_p)) + .strict(false) + .size(Some((24, 24))) + .build(&mut cfg_icon_bitmap) + { + debug!("✓ main 0 config: using icon for {}", cfg_pkey_s); + menu_item.set_bitmap(Some(&cfg_icon_bitmap)); + return Some(cfg_icon_bitmap); + } else { + debug!( + "✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}", + cfg_p + ); + } + } + menu_item.set_bitmap(None); + None } impl SystemTray { @@ -262,61 +275,93 @@ impl SystemTray { self.tray_menu.popup(x, y); } /// Add a ✓ (or highlight the icon) to the currently active config. Runs on opening of the list of configs menu - fn update_tray_icon_cfg(&self,menu_item_cfg:&mut nwg::MenuItem,cfg_p:&PathBuf,is_active:bool) -> Result<()> { - let mut img_dyn = self.img_dyn.borrow_mut(); - if img_dyn.contains_key(cfg_p) { // check if menu group icon needs to be updated to match active - if is_active { - if let Some(cfg_icon_bitmap) = img_dyn.get(cfg_p) { - self.tray_1cfg_m.set_bitmap(cfg_icon_bitmap.as_ref()); - } + fn update_tray_icon_cfg( + &self, + menu_item_cfg: &mut nwg::MenuItem, + cfg_p: &PathBuf, + is_active: bool, + ) -> Result<()> { + let mut img_dyn = self.img_dyn.borrow_mut(); + if img_dyn.contains_key(cfg_p) { + // check if menu group icon needs to be updated to match active + if is_active { + if let Some(cfg_icon_bitmap) = img_dyn.get(cfg_p) { + self.tray_1cfg_m.set_bitmap(cfg_icon_bitmap.as_ref()); + } + } + } else { + trace!("config menu item icon missing, read config and add it (or nothing) {cfg_p:?}"); + if let Ok(cfg) = cfg::new_from_file(&cfg_p) { + if let Some(cfg_icon_s) = cfg.options.tray_icon { + debug!("loaded config without a tray icon {cfg_p:?}"); + if let Some(cfg_icon_bitmap) = + set_menu_item_cfg_icon(menu_item_cfg, &cfg_icon_s, &cfg_p) + { + if is_active { + self.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); + } // update currently active config's icon in the combo menu + debug!("✓set icon {cfg_p:?}"); + let _ = img_dyn.insert(cfg_p.clone(), Some(cfg_icon_bitmap)); + } else { + bail!("✗couldn't get a valid icon") + } + } else { + bail!("✗icon not configured") + } + } else { + bail!("✗couldn't load config") + } } - } else {trace!("config menu item icon missing, read config and add it (or nothing) {cfg_p:?}"); - if let Ok(cfg) = cfg::new_from_file(&cfg_p) { - if let Some(cfg_icon_s) = cfg.options.tray_icon {debug!("loaded config without a tray icon {cfg_p:?}"); - if let Some(cfg_icon_bitmap) = set_menu_item_cfg_icon(menu_item_cfg, &cfg_icon_s, &cfg_p) { - if is_active {self.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap));} // update currently active config's icon in the combo menu - debug!("✓set icon {cfg_p:?}"); - let _ = img_dyn.insert(cfg_p.clone(),Some(cfg_icon_bitmap)); - } else {bail!("✗couldn't get a valid icon")} - } else {bail!("✗icon not configured")} - } else {bail!("✗couldn't load config")} - } - Ok(()) + Ok(()) } - fn update_tray_icon_cfg_group(&self,force:bool) { - if let Some(cfg) = CFG.get() {if let Some(k) = cfg.try_lock() { - let idx_cfg = k.cur_cfg_idx; - let mut tray_item_dyn = self.tray_item_dyn .borrow_mut(); - let h_cfg_i = &mut tray_item_dyn[idx_cfg]; - let is_check = h_cfg_i.checked(); - if ! is_check || force { - let cfg_p = &k.cfg_paths[idx_cfg]; debug!("✗ mismatch idx_cfg={idx_cfg:?} {} {:?} cfg_p={cfg_p:?}",if is_check {"✓"}else{"✗"}, h_cfg_i.handle); - h_cfg_i.set_checked(true); - if let Err(e) = self.update_tray_icon_cfg(h_cfg_i,&cfg_p,true){ - debug!("{e:?} {cfg_p:?}"); - let mut img_dyn = self.img_dyn.borrow_mut(); - img_dyn.insert(cfg_p.clone(),None); - self.tray_1cfg_m.set_bitmap(None); // can't update menu, so remove combo menu icon - }; - } else {debug!("gui cfg selection matches active config");}; - } else {debug!("✗ kanata config is locked, can't get current config (likely the gui changed the layer and is still holding the lock, it will update the icon)");} - }; + fn update_tray_icon_cfg_group(&self, force: bool) { + if let Some(cfg) = CFG.get() { + if let Some(k) = cfg.try_lock() { + let idx_cfg = k.cur_cfg_idx; + let mut tray_item_dyn = self.tray_item_dyn.borrow_mut(); + let h_cfg_i = &mut tray_item_dyn[idx_cfg]; + let is_check = h_cfg_i.checked(); + if !is_check || force { + let cfg_p = &k.cfg_paths[idx_cfg]; + debug!( + "✗ mismatch idx_cfg={idx_cfg:?} {} {:?} cfg_p={cfg_p:?}", + if is_check { "✓" } else { "✗" }, + h_cfg_i.handle + ); + h_cfg_i.set_checked(true); + if let Err(e) = self.update_tray_icon_cfg(h_cfg_i, &cfg_p, true) { + debug!("{e:?} {cfg_p:?}"); + let mut img_dyn = self.img_dyn.borrow_mut(); + img_dyn.insert(cfg_p.clone(), None); + self.tray_1cfg_m.set_bitmap(None); // can't update menu, so remove combo menu icon + }; + } else { + debug!("gui cfg selection matches active config"); + }; + } else { + debug!("✗ kanata config is locked, can't get current config (likely the gui changed the layer and is still holding the lock, it will update the icon)"); + } + }; } fn check_active(&self) { - if let Some(cfg) = CFG.get() {let k = cfg.lock(); - let idx_cfg = k.cur_cfg_idx; - let mut tray_item_dyn = self.tray_item_dyn .borrow_mut(); - for (i, mut h_cfg_i) in tray_item_dyn.iter_mut().enumerate() { - // 1 if missing an icon, read config to get one - let cfg_p = &k.cfg_paths[i]; trace!(" →→→→ i={i:?} {:?} cfg_p={cfg_p:?}",h_cfg_i.handle); // change to trace todo - let is_active = i==idx_cfg; - if let Err(e) = self.update_tray_icon_cfg(&mut h_cfg_i,&cfg_p,is_active){ - info!("{e:?} {cfg_p:?}"); - let mut img_dyn = self.img_dyn.borrow_mut(); - img_dyn.insert(cfg_p.clone(),None); - if i==idx_cfg {self.tray_1cfg_m.set_bitmap(None);} // update currently active config's icon in the combo menu - }; - // 2 if wrong GUI checkmark, correct it + if let Some(cfg) = CFG.get() { + let k = cfg.lock(); + let idx_cfg = k.cur_cfg_idx; + let mut tray_item_dyn = self.tray_item_dyn.borrow_mut(); + for (i, mut h_cfg_i) in tray_item_dyn.iter_mut().enumerate() { + // 1 if missing an icon, read config to get one + let cfg_p = &k.cfg_paths[i]; + trace!(" →→→→ i={i:?} {:?} cfg_p={cfg_p:?}", h_cfg_i.handle); + let is_active = i == idx_cfg; + if let Err(e) = self.update_tray_icon_cfg(&mut h_cfg_i, &cfg_p, is_active) { + debug!("{e:?} {cfg_p:?}"); + let mut img_dyn = self.img_dyn.borrow_mut(); + img_dyn.insert(cfg_p.clone(), None); + if is_active { + self.tray_1cfg_m.set_bitmap(None); + } // update currently active config's icon in the combo menu + }; + // 2 if wrong GUI checkmark, correct it if h_cfg_i.checked() && !is_active { debug!("uncheck i{} act{}", i, idx_cfg); h_cfg_i.set_checked(false); @@ -331,7 +376,7 @@ impl SystemTray { }; } /// Reload config file, currently active (`i=None`) or matching a given `i` index - fn reload_cfg(&self, i:Option) -> Result<()> { + fn reload_cfg(&self, i: Option) -> Result<()> { use nwg::TrayNotificationFlags as f_tray; let mut msg_title = "".to_string(); let mut msg_content = "".to_string(); @@ -437,7 +482,7 @@ impl SystemTray { self.tray.set_tip(&cfg_layer_pkey_s); // update tooltip to point to the newer config // self.tray.set_visibility(true); } - let clear = if i.is_none() { true } else { false }; + let clear = i.is_none(); self.update_tray_icon( cfg_layer_pkey, &cfg_layer_pkey_s, @@ -520,7 +565,7 @@ impl SystemTray { ) { let mut icon_dyn = self.icon_dyn.borrow_mut(); // update the tray icon let mut icon_active = self.icon_active.borrow_mut(); // update the tray icon active path - let mut img_dyn = self.img_dyn .borrow_mut(); // update the tray images + let mut img_dyn = self.img_dyn.borrow_mut(); // update the tray images if clear { *icon_dyn = Default::default(); *icon_active = Default::default(); @@ -658,11 +703,10 @@ pub mod system_tray_ui { use super::*; use core::cmp; use native_windows_gui::{self as nwg, MousePressEvent}; - use windows_sys::Win32::UI::{Controls::LVSCW_AUTOSIZE_USEHEADER, - Shell::{SIID_SHIELD,SIID_DELETE,SIID_DOCASSOC}}; use std::cell::RefCell; use std::ops::Deref; use std::rc::Rc; + use windows_sys::Win32::UI::Shell::SIID_DELETE; pub struct SystemTrayUi { inner: Rc, @@ -709,20 +753,24 @@ pub mod system_tray_ui { .build(&mut d.tray_3exit)?; let mut tmp_bitmap = Default::default(); - nwg::Bitmap::builder().source_embed(Some(&d.embed)).source_embed_str(Some("imgReload")).strict(true).size(Some((24,24))) - .build(&mut tmp_bitmap)?; - let img_exit = nwg::Bitmap::from_system_icon(SIID_DELETE); - d.tray_2reload .set_bitmap(Some(&tmp_bitmap)); - d.tray_3exit .set_bitmap(Some(&img_exit)); - d.img_reload = tmp_bitmap; - d.img_exit = img_exit; + nwg::Bitmap::builder() + .source_embed(Some(&d.embed)) + .source_embed_str(Some("imgReload")) + .strict(true) + .size(Some((24, 24))) + .build(&mut tmp_bitmap)?; + let img_exit = nwg::Bitmap::from_system_icon(SIID_DELETE); + d.tray_2reload.set_bitmap(Some(&tmp_bitmap)); + d.tray_3exit.set_bitmap(Some(&img_exit)); + d.img_reload = tmp_bitmap; + d.img_exit = img_exit; let mut main_tray_icon_l = Default::default(); let mut main_tray_icon_is = false; { let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut let mut icon_dyn = d.icon_dyn.borrow_mut(); - let mut img_dyn = d.img_dyn .borrow_mut(); + let mut img_dyn = d.img_dyn.borrow_mut(); let mut icon_active = d.icon_active.borrow_mut(); const MENU_ACC: &str = "ASDFGQWERTZXCVBYUIOPHJKLNM"; let layer0_icon_s = &app_data.layer0_icon.clone().unwrap_or("".to_string()); @@ -744,7 +792,7 @@ pub mod system_tray_ui { .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); //kanata.kbd - // let menu_text = i_acc + cfg_name; // &1 kanata.kbd + // let menu_text = i_acc + cfg_name; // &1 kanata.kbd let menu_text = format!("{cfg_name}\t{i_acc}"); // kanata.kbd &1 let mut menu_item = Default::default(); if i == 0 { @@ -798,25 +846,32 @@ pub mod system_tray_ui { .build(&mut temp_icon)?; let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); *icon_active = Some(cfg_p.clone()); - // Set tray menu config item icons, ignores layers since these are per config - if let Some(cfg_icon_bitmap) = set_menu_item_cfg_icon(&mut menu_item, &cfg_icon_s, &cfg_p) { - d.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); // show currently active config's icon in the combo menu - let _ = img_dyn.insert(cfg_p.clone(),Some(cfg_icon_bitmap)); - } else { - let _ = img_dyn.insert(cfg_p.clone(),None); - } - } - tray_item_dyn.push(menu_item); } + // Set tray menu config item icons, ignores layers since these are per config + if let Some(cfg_icon_bitmap) = + set_menu_item_cfg_icon(&mut menu_item, &cfg_icon_s, &cfg_p) + { + d.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); // show currently active config's icon in the combo menu + let _ = img_dyn.insert(cfg_p.clone(), Some(cfg_icon_bitmap)); + } else { + let _ = img_dyn.insert(cfg_p.clone(), None); + } + } + tray_item_dyn.push(menu_item); + } } else { warn!("Didn't get any config paths from Kanata!") } } let main_tray_icon = match main_tray_icon_is { - true => Some(&main_tray_icon_l), - false => Some(&d.icon),}; - nwg::TrayNotification ::builder().parent(&d.window) .icon(main_tray_icon) .tip(Some(&app_data.tooltip)) - . build( &mut d.tray )? ; + true => Some(&main_tray_icon_l), + false => Some(&d.icon), + }; + nwg::TrayNotification::builder() + .parent(&d.window) + .icon(main_tray_icon) + .tip(Some(&app_data.tooltip)) + .build(&mut d.tray)?; let ui = SystemTrayUi { // Wrap-up @@ -828,36 +883,37 @@ pub mod system_tray_ui { let handle_events = move |evt, _evt_data, handle| { if let Some(evt_ui) = evt_ui.upgrade() { match evt { - E::OnNotice => if &handle == &evt_ui.layer_notice {SystemTray::reload_layer_icon(&evt_ui);} - E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} - E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} - E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} - E::OnMenuHover => - if &handle == &evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} - E::OnMenuItemSelected => - if &handle == &evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None);SystemTray::update_tray_icon_cfg_group(&evt_ui,true); - } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); - } else { - match handle { - ControlHandle::MenuItem(_parent, _id) => { - {let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // - for (i, h_cfg) in tray_item_dyn.iter().enumerate() { - // if SystemTray::reload_cfg(&evt_ui,Some(i)).is_ok() { - for (_j, h_cfg_j) in tray_item_dyn.iter().enumerate() { - if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others - h_cfg.set_checked(true); // check self - let _ = SystemTray::reload_cfg(&evt_ui,Some(i)); // depends on future fix in kanata that would revert index on failed config changes - // } else {info!("OnMenuItemSelected: checkmarks not changed since config wasn't reloaded");} - } - } + E::OnNotice => if &handle == &evt_ui.layer_notice {SystemTray::reload_layer_icon(&evt_ui);} + E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} + E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnMenuHover => + if &handle == &evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} + E::OnMenuItemSelected => + if &handle == &evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None);SystemTray::update_tray_icon_cfg_group(&evt_ui,true); + } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); + } else { + match handle { + ControlHandle::MenuItem(_parent, _id) => { + {let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // + for (i, h_cfg) in tray_item_dyn.iter().enumerate() { + if &handle == h_cfg { //info!("CONFIG handle i={:?} {:?}",i,&handle); + // if SystemTray::reload_cfg(&evt_ui,Some(i)).is_ok() { + for (_j, h_cfg_j) in tray_item_dyn.iter().enumerate() { + if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others + h_cfg.set_checked(true); // check self + let _ = SystemTray::reload_cfg(&evt_ui,Some(i)); // depends on future fix in kanata that would revert index on failed config changes + // } else {info!("OnMenuItemSelected: checkmarks not changed since config wasn't reloaded");} + } + } + } + SystemTray::update_tray_icon_cfg_group(&evt_ui,true); + }, + _ => {}, + } + }, + _ => {} } - SystemTray::update_tray_icon_cfg_group(&evt_ui,true); - }, - _ => {}, - } - }, - _ => {} - } } }; ui.handler_def diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 83945533e..b99c7fb51 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -10,7 +10,11 @@ use crate::kanata::*; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events /// and run the native_windows_gui event loop. - pub fn event_loop(_cfg: Arc>, tx: Sender, #[cfg(all(target_os = "windows", feature = "gui"))] ui:system_tray_ui::SystemTrayUi) -> Result<()> { + pub fn event_loop( + _cfg: Arc>, + tx: Sender, + #[cfg(all(target_os = "windows", feature = "gui"))] ui: crate::system_tray_ui::SystemTrayUi, + ) -> Result<()> { // Display debug and panic output when launched from a terminal. #[cfg(not(feature = "gui"))] unsafe { @@ -68,7 +72,7 @@ impl Kanata { #[cfg(all(target_os = "windows", feature = "gui"))] let _ui = ui; // prevents thread from panicking on exiting via a GUI - // The event loop is also required for the low-level keyboard hook to work. + // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); Ok(()) } diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index 32aabcb64..d342a0666 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -152,8 +152,8 @@ impl Kanata { } } // if let Err(e) = self.do_live_reload(&None,gui_tx) { - // self.cur_cfg_idx = backup_cfg_idx; // restore index on fail when. TODO: add when a similar reversion is added to other custom actions - // return Err(e) + // self.cur_cfg_idx = backup_cfg_idx; // restore index on fail when. TODO: add when a similar reversion is added to other custom actions + // return Err(e) // } self.do_live_reload(&None, gui_tx)?; Ok(()) diff --git a/src/lib_main.rs b/src/lib_main.rs index 2ce434c1a..e650e9762 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -255,34 +255,36 @@ fn main_impl() -> Result<()> { (None, None, None) }; - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))]{ - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + { + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } - #[cfg(target_os = "linux")] - sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; + #[cfg(target_os = "linux")] + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - Kanata::event_loop(kanata_arc, tx)?; + Kanata::event_loop(kanata_arc, tx)?; } - #[cfg(all(target_os = "windows", feature = "gui"))] { - use anyhow::Context; - use native_windows_gui as nwg; - native_windows_gui::init().context("Failed to init Native Windows GUI")?; - let ui = build_tray(&kanata_arc)?; - let gui_tx = ui.layer_notice.sender(); - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, gui_tx, args.nodelay); - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } + #[cfg(all(target_os = "windows", feature = "gui"))] + { + use anyhow::Context; + + native_windows_gui::init().context("Failed to init Native Windows GUI")?; + let ui = build_tray(&kanata_arc)?; + let gui_tx = ui.layer_notice.sender(); + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, gui_tx, args.nodelay); + + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } - Kanata::event_loop(kanata_arc, tx, ui)?; + Kanata::event_loop(kanata_arc, tx, ui)?; } Ok(()) From b095dc35c513f71d69e65ca54b499a90b058f3bb Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 03:09:15 +0700 Subject: [PATCH 100/154] doc: use smaller icons --- cfg_samples/tray-icon/icons/1symbols.ico | Bin 102281 -> 0 bytes cfg_samples/tray-icon/icons/1symbols.png | Bin 0 -> 3524 bytes cfg_samples/tray-icon/tray-icon.kbd | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 cfg_samples/tray-icon/icons/1symbols.ico create mode 100644 cfg_samples/tray-icon/icons/1symbols.png diff --git a/cfg_samples/tray-icon/icons/1symbols.ico b/cfg_samples/tray-icon/icons/1symbols.ico deleted file mode 100644 index 0d2c5fd23ee24fc332f00e59e0e3bbc1f6fd8243..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102281 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 diff --git a/cfg_samples/tray-icon/icons/1symbols.png b/cfg_samples/tray-icon/icons/1symbols.png new file mode 100644 index 0000000000000000000000000000000000000000..f55da33299bbbb3a37bbaec6d08a64dd2aa7a347 GIT binary patch literal 3524 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 literal 0 HcmV?d00001 diff --git a/cfg_samples/tray-icon/tray-icon.kbd b/cfg_samples/tray-icon/tray-icon.kbd index 9660c4a0a..2a317f30a 100644 --- a/cfg_samples/tray-icon/tray-icon.kbd +++ b/cfg_samples/tray-icon/tray-icon.kbd @@ -13,7 +13,7 @@ (defsrc 1 2 3 4 5 6) (deflayer ⌂ (icon base.png) @l1 @l2 @l3 @l4 @l5 @l6) ;; find in the 'icon' subfolder -(deflayer 1emoji (🖻 1symbols.ico) q q q q q q) ;; find in the 'icons' subfolder +(deflayer 1emoji (🖻 1symbols.png) q q q q q q) ;; find in the 'icons' subfolder (deflayer 2icon-quote (🖻 "2Nav Num.png") w w w w w w) ;; find in the 'img' subfolder (deflayer 3emoji_alt (🖼 3trans.parent) e e e e e e) ;; find '.png' (deflayermap (4my-lmap) (🖻 "..\..\assets\kanata.ico") 1 r 2 r 3 r 4 r 5 r 6 r) ;; find in relative path From 1999abb2846e3dfc4d59dc07ecc8fceafd0f34c1 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 18:17:24 +0700 Subject: [PATCH 101/154] revert gui: let processing loop notify on layer changes --- src/kanata/mod.rs | 38 +++----------------------------------- src/kanata/windows/mod.rs | 13 +++++-------- 2 files changed, 8 insertions(+), 43 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 94ddfbbdf..071a6e9a7 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -472,8 +472,6 @@ impl Kanata { fn do_live_reload( &mut self, _tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] - gui_tx: native_windows_gui::NoticeSender, ) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, @@ -553,8 +551,6 @@ impl Kanata { } } } - #[cfg(all(target_os = "windows", feature = "gui"))] - gui_tx.notice(); Ok(()) } @@ -600,8 +596,6 @@ impl Kanata { fn handle_time_ticks( &mut self, tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] - gui_tx: native_windows_gui::NoticeSender, ) -> Result { const NS_IN_MS: u128 = 1_000_000; let now = instant::Instant::now(); @@ -625,10 +619,7 @@ impl Kanata { _ => instant::Instant::now(), }; - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] self.check_handle_layer_change(tx); - #[cfg(all(target_os = "windows", feature = "gui"))] - self.check_handle_layer_change(tx, gui_tx); if self.live_reload_requested && ((self.prev_keys.is_empty() && self.cur_keys.is_empty()) @@ -644,14 +635,9 @@ impl Kanata { // activate. Having this fallback allows live reload to happen which resets the // kanata states. self.live_reload_requested = false; - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] if let Err(e) = self.do_live_reload(tx) { log::error!("live reload failed {e}"); } - #[cfg(all(target_os = "windows", feature = "gui"))] - if let Err(e) = self.do_live_reload(tx, gui_tx) { - log::error!("live reload failed {e}"); - } } #[cfg(feature = "perf_logging")] @@ -1531,8 +1517,6 @@ impl Kanata { fn check_handle_layer_change( &mut self, tx: &Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] - gui_tx: native_windows_gui::NoticeSender, ) { let cur_layer = self.layout.bm().current_layer(); if cur_layer != self.prev_layer { @@ -1549,8 +1533,6 @@ impl Kanata { } } } - #[cfg(all(target_os = "windows", feature = "gui"))] - gui_tx.notice(); } } @@ -1614,8 +1596,6 @@ impl Kanata { kanata: Arc>, rx: Receiver, tx: Option>, - #[cfg(all(target_os = "windows", feature = "gui"))] - gui_tx: native_windows_gui::NoticeSender, nodelay: bool, ) { info!("entering the processing loop"); @@ -1756,11 +1736,7 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - let res_time_tick = k.handle_time_ticks(&tx); - #[cfg(all(target_os = "windows", feature = "gui"))] - let res_time_tick = k.handle_time_ticks(&tx, gui_tx); - match res_time_tick { + match k.handle_time_ticks(&tx) { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; @@ -1809,11 +1785,7 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - let res_time_tick = k.handle_time_ticks(&tx); - #[cfg(all(target_os = "windows", feature = "gui"))] - let res_time_tick = k.handle_time_ticks(&tx, gui_tx); - match res_time_tick { + match k.handle_time_ticks(&tx) { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; @@ -1828,11 +1800,7 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - let res_time_tick = k.handle_time_ticks(&tx); - #[cfg(all(target_os = "windows", feature = "gui"))] - let res_time_tick = k.handle_time_ticks(&tx, gui_tx); - match res_time_tick { + match k.handle_time_ticks(&tx) { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index d342a0666..8b42186b4 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -10,9 +10,6 @@ mod llhook; #[cfg(feature = "interception_driver")] mod interception; -#[cfg(feature = "gui")] -use native_windows_gui as nwg; - pub static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); @@ -132,13 +129,13 @@ impl Kanata { } #[cfg(feature = "gui")] - pub fn live_reload(&mut self, gui_tx: nwg::NoticeSender) -> Result<()> { + pub fn live_reload(&mut self) -> Result<()> { self.live_reload_requested = true; - self.do_live_reload(&None, gui_tx)?; + self.do_live_reload(&None)?; Ok(()) } #[cfg(feature = "gui")] - pub fn live_reload_n(&mut self, n: usize, gui_tx: nwg::NoticeSender) -> Result<()> { + pub fn live_reload_n(&mut self, n: usize) -> Result<()> { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow self.live_reload_requested = true; // let backup_cfg_idx = self.cur_cfg_idx; @@ -151,11 +148,11 @@ impl Kanata { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } - // if let Err(e) = self.do_live_reload(&None,gui_tx) { + // if let Err(e) = self.do_live_reload(&None) { // self.cur_cfg_idx = backup_cfg_idx; // restore index on fail when. TODO: add when a similar reversion is added to other custom actions // return Err(e) // } - self.do_live_reload(&None, gui_tx)?; + self.do_live_reload(&None)?; Ok(()) } } From 53e30db95622557296d98d1da93f83967903ace1 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 18:19:09 +0700 Subject: [PATCH 102/154] win-tray: store GUI notifier in a static var --- src/gui_win.rs | 6 ++---- src/lib_main.rs | 5 ++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/gui_win.rs b/src/gui_win.rs index 4c34184b7..e5f3b42b9 100644 --- a/src/gui_win.rs +++ b/src/gui_win.rs @@ -420,11 +420,9 @@ impl SystemTray { cfg_icon_s, layer_name, layer_icon_s ); } - let noticer: &nwg::Notice = &self.layer_notice; - let gui_tx = noticer.sender(); match i { Some(idx) => { - if let Ok(()) = k.live_reload_n(idx, gui_tx) { + if let Ok(()) = k.live_reload_n(idx) { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" loaded"); flags |= f_tray::USER_ICON; } else { @@ -440,7 +438,7 @@ impl SystemTray { } } None => { - if let Ok(()) = k.live_reload(gui_tx) { + if let Ok(()) = k.live_reload() { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" reloaded"); flags |= f_tray::USER_ICON; } else { diff --git a/src/lib_main.rs b/src/lib_main.rs index e650e9762..833ef0357 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -277,7 +277,8 @@ fn main_impl() -> Result<()> { native_windows_gui::init().context("Failed to init Native Windows GUI")?; let ui = build_tray(&kanata_arc)?; let gui_tx = ui.layer_notice.sender(); - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, gui_tx, args.nodelay); + if GUI_TX.set(gui_tx).is_err() {warn!("Someone else set our ‘GUI_TX’");}; + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) { #[allow(clippy::unit_arg)] @@ -304,6 +305,8 @@ use parking_lot::Mutex; use std::sync::{Arc, OnceLock}; #[cfg(all(target_os = "windows", feature = "gui"))] pub static CFG: OnceLock>> = OnceLock::new(); +#[cfg(all(target_os = "windows", feature = "gui"))] +pub static GUI_TX:OnceLock = OnceLock::new(); #[cfg(all(target_os = "windows", feature = "gui"))] pub fn lib_main_gui() { From c99ab7622111fc69336105fcdaa6e274e410bbfd Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 18:20:17 +0700 Subject: [PATCH 103/154] win-tray: use static GUI notifier --- src/kanata/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 071a6e9a7..a7a604003 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -61,6 +61,9 @@ mod unknown; #[cfg(target_os = "unknown")] use unknown::*; +#[cfg(all(target_os = "windows", feature = "gui"))] +use crate::lib_main::GUI_TX; + mod caps_word; pub use caps_word::*; @@ -551,6 +554,9 @@ impl Kanata { } } } + #[cfg(all(target_os = "windows", feature = "gui"))] + if let Some(gui_tx) = GUI_TX.get() {gui_tx.notice(); + } else {error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes");} Ok(()) } @@ -1533,6 +1539,9 @@ impl Kanata { } } } + #[cfg(all(target_os = "windows", feature = "gui"))] + if let Some(gui_tx) = GUI_TX.get() {gui_tx.notice(); + } else {error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes");} } } From 67c1075fd6f7c0fd325ea69a35f4722911011460 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 18:21:51 +0700 Subject: [PATCH 104/154] format --- src/kanata/mod.rs | 29 +++++++++++++---------------- src/lib_main.rs | 6 ++++-- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index a7a604003..fab401ce4 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -472,10 +472,7 @@ impl Kanata { }) } - fn do_live_reload( - &mut self, - _tx: &Option>, - ) -> Result<()> { + fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, Err(e) => { @@ -555,8 +552,11 @@ impl Kanata { } } #[cfg(all(target_os = "windows", feature = "gui"))] - if let Some(gui_tx) = GUI_TX.get() {gui_tx.notice(); - } else {error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes");} + if let Some(gui_tx) = GUI_TX.get() { + gui_tx.notice(); + } else { + error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes"); + } Ok(()) } @@ -599,10 +599,7 @@ impl Kanata { /// Advance keyberon layout state and send events based on changes to its state. /// Returns the number of ticks that elapsed. - fn handle_time_ticks( - &mut self, - tx: &Option>, - ) -> Result { + fn handle_time_ticks(&mut self, tx: &Option>) -> Result { const NS_IN_MS: u128 = 1_000_000; let now = instant::Instant::now(); let ns_elapsed = now.duration_since(self.last_tick).as_nanos(); @@ -1520,10 +1517,7 @@ impl Kanata { #[allow(unused_variables)] /// Prints the layer. If the TCP server is enabled, then this will also send a notification to /// all connected clients. - fn check_handle_layer_change( - &mut self, - tx: &Option>, - ) { + fn check_handle_layer_change(&mut self, tx: &Option>) { let cur_layer = self.layout.bm().current_layer(); if cur_layer != self.prev_layer { let new = self.layer_info[cur_layer].name.clone(); @@ -1540,8 +1534,11 @@ impl Kanata { } } #[cfg(all(target_os = "windows", feature = "gui"))] - if let Some(gui_tx) = GUI_TX.get() {gui_tx.notice(); - } else {error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes");} + if let Some(gui_tx) = GUI_TX.get() { + gui_tx.notice(); + } else { + error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes"); + } } } diff --git a/src/lib_main.rs b/src/lib_main.rs index 833ef0357..e3e003e57 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -277,7 +277,9 @@ fn main_impl() -> Result<()> { native_windows_gui::init().context("Failed to init Native Windows GUI")?; let ui = build_tray(&kanata_arc)?; let gui_tx = ui.layer_notice.sender(); - if GUI_TX.set(gui_tx).is_err() {warn!("Someone else set our ‘GUI_TX’");}; + if GUI_TX.set(gui_tx).is_err() { + warn!("Someone else set our ‘GUI_TX’"); + }; Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) { @@ -306,7 +308,7 @@ use std::sync::{Arc, OnceLock}; #[cfg(all(target_os = "windows", feature = "gui"))] pub static CFG: OnceLock>> = OnceLock::new(); #[cfg(all(target_os = "windows", feature = "gui"))] -pub static GUI_TX:OnceLock = OnceLock::new(); +pub static GUI_TX: OnceLock = OnceLock::new(); #[cfg(all(target_os = "windows", feature = "gui"))] pub fn lib_main_gui() { From 32e858259d2861d7695cea6a66d5b07ff3157f95 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 18:26:49 +0700 Subject: [PATCH 105/154] fix a comment --- parser/src/cfg/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a398303c6..f59bc686e 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -2910,7 +2910,7 @@ fn parse_layers( } else { 2 }; // skip the 3rd list that's used to configure a layer rather than define an action - // Parse actions as input -> output triplets + // Parse actions as input output pairs let mut pairs = layer[skip..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; From 9999e31a935d9ad6ddf83bac3289bdae082d4b80 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 19:17:05 +0700 Subject: [PATCH 106/154] parser: convert parse_action_as_cfg to functional notation --- parser/src/cfg/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index f59bc686e..cd6758f07 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1428,16 +1428,16 @@ fn parse_action(expr: &SExpr, s: &ParserState) -> Result<&'static KanataAction> /// Check if an `SExpr` is a layer config. fn parse_action_as_cfg(expr: &SExpr) -> bool { - if let Some(expr_list) = expr.list(None) { - if let Some(expr_list_1st) = &expr_list[0].atom(None) { - if !DEFLAYER_ICON.iter().any(|&i| i == *expr_list_1st) { - return false; + expr.list(None) + .and_then(|l| l[0].atom(None)) + .and_then(|l0| { + if !DEFLAYER_ICON.iter().any(|&i| i == l0) { + None } else { - return expr_list[1].atom(None).is_some(); + expr.list(None).and_then(|l| l[1].atom(None)) } - } - } - false + }) + .is_some() } /// Returns a single custom action in the proper wrapped type. From e2c00359c8d0a674715994271d3e95a8e6c8bae6 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 4 May 2024 23:19:35 +0700 Subject: [PATCH 107/154] Update src/kanata/mod.rs Co-authored-by: jtroo --- src/kanata/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index fab401ce4..bc8796889 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -555,7 +555,7 @@ impl Kanata { if let Some(gui_tx) = GUI_TX.get() { gui_tx.notice(); } else { - error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes"); + error!("no GUI_TX to notify GUI thread of layer changes"); } Ok(()) } From 1be460c56bd9f6f71943f5e77966117598d039f1 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 4 May 2024 23:20:01 +0700 Subject: [PATCH 108/154] Update src/kanata/mod.rs Co-authored-by: jtroo --- src/kanata/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index bc8796889..12bdaddb7 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1537,7 +1537,7 @@ impl Kanata { if let Some(gui_tx) = GUI_TX.get() { gui_tx.notice(); } else { - error!("no ‘GUI_TX’ var that can notify GUI thread of layer changes"); + error!("no GUI_TX to notify GUI thread of layer changes"); } } } From 1eb70c7a015b7cd22be8228a19d4e097b305a373 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 23:36:39 +0700 Subject: [PATCH 109/154] win-tray: fix default value to read from options --- src/kanata/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 12bdaddb7..bb100e355 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -468,7 +468,7 @@ impl Kanata { #[cfg(all(target_os = "windows", feature = "gui"))] tray_icon: None, #[cfg(all(target_os = "windows", feature = "gui"))] - icon_match_layer_name: true, + icon_match_layer_name: cfg.options.icon_match_layer_name, }) } From 23b819fc7f0663aa63f5bab756887fa9c7cf3221 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 23:37:59 +0700 Subject: [PATCH 110/154] win-tray: remove unneeded console no-close-on-error guard when there is no console --- src/lib_main.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib_main.rs b/src/lib_main.rs index e3e003e57..ccc7825dc 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -317,10 +317,6 @@ pub fn lib_main_gui() { if let Err(ref e) = ret { log::error!("{e}\n"); } - // if *IS_TERM { - // eprintln!("\nPress enter to exit"); - // let _ = std::io::stdin().read_line(&mut String::new()); // TODO: panics on Err(TryRecvError::Disconnected) @ Win/llhook, move to llhook OR coordinate with exit(&self) {nwg::stop_thread_dispatch();}? OR just ignore, why do we need this at all? - // } unsafe { FreeConsole(); From 9dee8bb3927a4be8169a402c4d9aa98f36b66b9a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sat, 4 May 2024 23:38:14 +0700 Subject: [PATCH 111/154] cargo fmt --- src/kanata/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index bb100e355..d4c4dd80e 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -555,7 +555,7 @@ impl Kanata { if let Some(gui_tx) = GUI_TX.get() { gui_tx.notice(); } else { - error!("no GUI_TX to notify GUI thread of layer changes"); + error!("no GUI_TX to notify GUI thread of layer changes"); } Ok(()) } From 02b0cd5629095d893d71abf1692d54c096131108 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:12:54 +0700 Subject: [PATCH 112/154] Revert "parser: convert parse_action_as_cfg to functional notation" This reverts commit cd0d57e65ea2df30cce2a505f1fb7ae134f4b02a. --- parser/src/cfg/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index cd6758f07..f59bc686e 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1428,16 +1428,16 @@ fn parse_action(expr: &SExpr, s: &ParserState) -> Result<&'static KanataAction> /// Check if an `SExpr` is a layer config. fn parse_action_as_cfg(expr: &SExpr) -> bool { - expr.list(None) - .and_then(|l| l[0].atom(None)) - .and_then(|l0| { - if !DEFLAYER_ICON.iter().any(|&i| i == l0) { - None + if let Some(expr_list) = expr.list(None) { + if let Some(expr_list_1st) = &expr_list[0].atom(None) { + if !DEFLAYER_ICON.iter().any(|&i| i == *expr_list_1st) { + return false; } else { - expr.list(None).and_then(|l| l[1].atom(None)) + return expr_list[1].atom(None).is_some(); } - }) - .is_some() + } + } + false } /// Returns a single custom action in the proper wrapped type. From e56215e546ae5996362aff9b2f5458d1b1067056 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:13:10 +0700 Subject: [PATCH 113/154] Revert "fix a comment" This reverts commit d5d742f3422b4a8f09f0b4c2d6a9d19df014cabc. --- parser/src/cfg/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index f59bc686e..a398303c6 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -2910,7 +2910,7 @@ fn parse_layers( } else { 2 }; // skip the 3rd list that's used to configure a layer rather than define an action - // Parse actions as input output pairs + // Parse actions as input -> output triplets let mut pairs = layer[skip..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; From 3340060f4657812a9030be20ad37167239b1263a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:13:27 +0700 Subject: [PATCH 114/154] Revert "clippy" This reverts commit 209b0c36509421495da10793afcfa388c1329c08. --- parser/src/cfg/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a398303c6..6d77614b0 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1097,7 +1097,7 @@ fn parse_layer_indexes( let mut cfg_third = 0; if let Some(third) = subexprs.peek() { if let Some(third_list) = third.list(None) { - if !third_list.is_empty() { + if third_list.len() > 0 { if let Some(third_list_1st_s) = &third_list[0].atom(None) { if DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { cfg_third = 1; @@ -1433,7 +1433,11 @@ fn parse_action_as_cfg(expr: &SExpr) -> bool { if !DEFLAYER_ICON.iter().any(|&i| i == *expr_list_1st) { return false; } else { - return expr_list[1].atom(None).is_some(); + if expr_list[1].atom(None).is_some() { + return true; + } else { + return false; + } } } } From aaac2d55eb4c1caa3a24efe501f0b73ab879fe71 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:13:57 +0700 Subject: [PATCH 115/154] Revert "gui: cfg icon fix wrong conditional" This reverts commit 2dc82ba131457e242e59d1174ef7d0653ca4c2d3. --- parser/src/cfg/mod.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 6d77614b0..51799cd87 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1097,20 +1097,23 @@ fn parse_layer_indexes( let mut cfg_third = 0; if let Some(third) = subexprs.peek() { if let Some(third_list) = third.list(None) { - if third_list.len() > 0 { - if let Some(third_list_1st_s) = &third_list[0].atom(None) { - if DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { - cfg_third = 1; - if let Some(third_list_2nd_s) = &third_list[1].atom(None) { - icon = Some(third_list_2nd_s.trim_matches('"').to_string()); - subexprs.next(); // advance over the 3rd list - } else { - bail!("deflayer failed to parse an icon name in its 3rd element"); - } + cfg_third = 1; + let third_list_1st = &third_list[0]; + if let Some(third_list_1st_s) = &third_list[0].atom(None) { + if !DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { + bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); + } else { + if let Some(third_list_2nd_s) = &third_list[1].atom(None) { + icon = Some(third_list_2nd_s.trim_matches('"').to_string()); + } else { + bail!("deflayer failed to parse an icon name in its 3rd element"); } } + } else { + bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st:?}"); } } + subexprs.next(); // advance over the 3rd list } // Check if user tried to use parentheses directly - `(` and `)` // or escaped them like in kmonad - `\(` and `\)`. From 85bb5cd22135bb550dfd5308cad5028764bd5961 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:14:45 +0700 Subject: [PATCH 116/154] revert fmt --- parser/src/cfg/mod.rs | 63 +++++++++++++------------------------------ 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 51799cd87..a29583e62 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -501,7 +501,7 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; -const DEFLAYER_ICON: [&str; 3] = ["icon", "🖻", "🖼"]; +const DEFLAYER_ICON: [&str;3] = ["icon","🖻","🖼"]; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -649,7 +649,7 @@ pub fn parse_cfg_raw_string( bail!("No deflayer expressions exist. At least one layer must be defined.") } - let (layer_idxs, layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; + let (layer_idxs,layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; let mut sorted_idxs: Vec<(&String, &usize)> = layer_idxs.iter().map(|tuple| (tuple.0, tuple.1)).collect(); @@ -682,11 +682,7 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { - name: name.clone(), - cfg_text, - icon: layer_icons.get(&name).unwrap_or(&None).clone(), - }) + .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, icon: layer_icons.get(&name).unwrap_or(&None).clone()}) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1044,23 +1040,15 @@ type Aliases = HashMap; /// - All layers have the same number of items as the defsrc, /// - There are no duplicate layer names /// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. -fn parse_layer_indexes( - exprs: &[SpannedLayerExprs], - expected_len: usize, -) -> Result<(LayerIndexes, LayerIcons)> { +fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result<(LayerIndexes,LayerIcons)> { let mut layer_indexes = HashMap::default(); let mut layer_icons = HashMap::default(); for (i, expr_type) in exprs.iter().enumerate() { - let mut icon: Option = None; + let mut icon:Option = None; let (mut subexprs, expr, do_element_count_check) = match expr_type { - SpannedLayerExprs::DefsrcMapping(e) => { - (check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true) + SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true)} + SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), e, false) } - SpannedLayerExprs::CustomMapping(e) => ( - check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), - e, - false, - ), }; let layer_expr = subexprs.next().ok_or_else(|| { anyhow_span!(expr, "deflayer requires a name and {expected_len} item(s)") @@ -1094,13 +1082,13 @@ fn parse_layer_indexes( if layer_indexes.contains_key(&layer_name) { bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); } - let mut cfg_third = 0; + let mut cfg_third = 0; if let Some(third) = subexprs.peek() { if let Some(third_list) = third.list(None) { cfg_third = 1; let third_list_1st = &third_list[0]; if let Some(third_list_1st_s) = &third_list[0].atom(None) { - if !DEFLAYER_ICON.iter().any(|&i| i == *third_list_1st_s) { + if ! DEFLAYER_ICON.iter().any(|&i| {i==*third_list_1st_s}) { bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { @@ -1143,11 +1131,7 @@ fn parse_layer_indexes( } if do_element_count_check { let num_actions = expr.t.len() - 2 - cfg_third; - let dbg_cfg_third = if cfg_third == 1 { - " (excluding 1 config)" - } else { - "" - }; + let dbg_cfg_third = if cfg_third == 1 {" (excluding 1 config)"} else {""}; if num_actions != expected_len { bail_span!( expr, @@ -1157,13 +1141,12 @@ fn parse_layer_indexes( dbg_cfg_third, expected_len ) - } - } + } } layer_indexes.insert(layer_name.clone(), i); layer_icons.insert(layer_name, icon); } - Ok((layer_indexes, layer_icons)) + Ok((layer_indexes,layer_icons)) } #[derive(Debug, Clone)] @@ -1433,13 +1416,13 @@ fn parse_action(expr: &SExpr, s: &ParserState) -> Result<&'static KanataAction> fn parse_action_as_cfg(expr: &SExpr) -> bool { if let Some(expr_list) = expr.list(None) { if let Some(expr_list_1st) = &expr_list[0].atom(None) { - if !DEFLAYER_ICON.iter().any(|&i| i == *expr_list_1st) { - return false; + if ! DEFLAYER_ICON.iter().any(|&i| {i==*expr_list_1st}) { + return false } else { if expr_list[1].atom(None).is_some() { - return true; + return true } else { - return false; + return false } } } @@ -2901,23 +2884,15 @@ fn parse_layers( LayerExprs::DefsrcMapping(layer) => { // Parse actions in the layer and place them appropriately according // to defsrc mapping order. - let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) { - 3 - } else { - 2 - }; // skip the 3rd list that's used to configure a layer rather than define an action + let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action for (i, ac) in layer.iter().skip(skip).enumerate() { let ac = parse_action(ac, s)?; layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; } } LayerExprs::CustomMapping(layer) => { - let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) { - 3 - } else { - 2 - }; // skip the 3rd list that's used to configure a layer rather than define an action - // Parse actions as input -> output triplets + let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action + // Parse actions as input -> output triplets let mut pairs = layer[skip..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; From 6c0a0ac99295b8702894880ab63da39fab13c60f Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:15:44 +0700 Subject: [PATCH 117/154] Revert "gui: cfg trim icon from quotes" This reverts commit f1b5528193ded14a4227a4ce93ad0dc7a7480a2a. --- parser/src/cfg/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a29583e62..e275dfa86 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1092,7 +1092,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { - icon = Some(third_list_2nd_s.trim_matches('"').to_string()); + icon = Some(third_list_2nd_s.to_string()); } else { bail!("deflayer failed to parse an icon name in its 3rd element"); } From 105af4ca447d2cec91fe5ecc67cbee6b69f97508 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:16:19 +0700 Subject: [PATCH 118/154] Revert "gui: cfg: remove feature flags as configs should work everywhere" This reverts commit 8b32aa349c94a3f5bf0039cdfeb77c3e90468b37. --- parser/src/cfg/mod.rs | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index e275dfa86..6c1a5e0d7 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -319,7 +319,8 @@ pub type MappedKeys = HashSet; pub struct LayerInfo { pub name: String, pub cfg_text: String, - pub icon: Option, + #[cfg(all(target_os = "windows", feature = "gui"))] + pub layer_icon: Option, } #[allow(clippy::type_complexity)] // return type is not pub @@ -682,7 +683,7 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, icon: layer_icons.get(&name).unwrap_or(&None).clone()}) + .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, layer_icon: layer_icons.get(&name).unwrap_or(&None).clone()}) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1046,7 +1047,13 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu for (i, expr_type) in exprs.iter().enumerate() { let mut icon:Option = None; let (mut subexprs, expr, do_element_count_check) = match expr_type { + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)? , e, true)} + #[cfg(all(target_os = "windows", feature = "gui"))] SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true)} + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?, e, false)} + #[cfg(all(target_os = "windows", feature = "gui"))] SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), e, false) } }; @@ -1082,14 +1089,15 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu if layer_indexes.contains_key(&layer_name) { bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); } - let mut cfg_third = 0; - if let Some(third) = subexprs.peek() { + let mut third_list_count = 0; + #[cfg(all(target_os = "windows", feature = "gui"))] + if let Some(third) = subexprs.peek() {println!("next is list? = {:?} {:?}",third.list(None),third); if let Some(third_list) = third.list(None) { - cfg_third = 1; + third_list_count = 1; let third_list_1st = &third_list[0]; if let Some(third_list_1st_s) = &third_list[0].atom(None) { if ! DEFLAYER_ICON.iter().any(|&i| {i==*third_list_1st_s}) { - bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st_s}"); + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { icon = Some(third_list_2nd_s.to_string()); @@ -1098,7 +1106,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } } else { - bail!("deflayer with a list as its 3rd element expects it to start with one of {DEFLAYER_ICON:?}, not {third_list_1st:?}"); + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st:?}"); } } subexprs.next(); // advance over the 3rd list @@ -1130,18 +1138,17 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } if do_element_count_check { - let num_actions = expr.t.len() - 2 - cfg_third; - let dbg_cfg_third = if cfg_third == 1 {" (excluding 1 config)"} else {""}; + let num_actions = expr.t.len() - 2 - third_list_count; if num_actions != expected_len { bail_span!( expr, - "Layer {} has {} item(s){}, but requires {} to match defsrc", + "Layer {} has {} item(s), but requires {} to match defsrc", layer_name, num_actions, - dbg_cfg_third, expected_len ) - } } + } + } layer_indexes.insert(layer_name.clone(), i); layer_icons.insert(layer_name, icon); } @@ -2884,16 +2891,28 @@ fn parse_layers( LayerExprs::DefsrcMapping(layer) => { // Parse actions in the layer and place them appropriately according // to defsrc mapping order. + #[cfg(all(target_os = "windows", feature = "gui"))] let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action + #[cfg(all(target_os = "windows", feature = "gui"))] for (i, ac) in layer.iter().skip(skip).enumerate() { let ac = parse_action(ac, s)?; layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; } + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + for (i, ac) in layer.iter().skip(2).enumerate() { + let ac = parse_action(ac, s)?; + layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; + } } LayerExprs::CustomMapping(layer) => { + // Parse actions as input output pairs + #[cfg(all(target_os = "windows", feature = "gui"))] let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action // Parse actions as input -> output triplets + #[cfg(all(target_os = "windows", feature = "gui"))] let mut pairs = layer[skip..].chunks_exact(2); + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + let mut pairs = layer[2..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; let mut unmapped_anykey_used = false; From f6695d0b720b37a9a3aec237f08125a7482b48ab Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:17:34 +0700 Subject: [PATCH 119/154] Revert "gui: add cfg support for icon" This reverts commit 7585f37ea03bf0c0cd8f6b5928140aff3bdf9a40. --- parser/src/cfg/mod.rs | 40 ++++------------------------------------ 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 6c1a5e0d7..3f9913116 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -502,7 +502,7 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; -const DEFLAYER_ICON: [&str;3] = ["icon","🖻","🖼"]; +const DEFLAYER_ICON: &str = "icon"; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -1096,8 +1096,8 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu third_list_count = 1; let third_list_1st = &third_list[0]; if let Some(third_list_1st_s) = &third_list[0].atom(None) { - if ! DEFLAYER_ICON.iter().any(|&i| {i==*third_list_1st_s}) { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st_s}"); + if *third_list_1st_s != DEFLAYER_ICON { + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st_s}"); } else { if let Some(third_list_2nd_s) = &third_list[1].atom(None) { icon = Some(third_list_2nd_s.to_string()); @@ -1106,7 +1106,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } } else { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON:?} as its 1st element, not {third_list_1st:?}"); + bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st:?}"); } } subexprs.next(); // advance over the 3rd list @@ -1419,24 +1419,6 @@ fn parse_action(expr: &SExpr, s: &ParserState) -> Result<&'static KanataAction> }) } -/// Check if an `SExpr` is a layer config. -fn parse_action_as_cfg(expr: &SExpr) -> bool { - if let Some(expr_list) = expr.list(None) { - if let Some(expr_list_1st) = &expr_list[0].atom(None) { - if ! DEFLAYER_ICON.iter().any(|&i| {i==*expr_list_1st}) { - return false - } else { - if expr_list[1].atom(None).is_some() { - return true - } else { - return false - } - } - } - } - false -} - /// Returns a single custom action in the proper wrapped type. fn custom(ca: CustomAction, a: &Allocations) -> Result<&'static KanataAction> { Ok(a.sref(Action::Custom(a.sref(a.sref_slice(ca))))) @@ -2891,14 +2873,6 @@ fn parse_layers( LayerExprs::DefsrcMapping(layer) => { // Parse actions in the layer and place them appropriately according // to defsrc mapping order. - #[cfg(all(target_os = "windows", feature = "gui"))] - let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action - #[cfg(all(target_os = "windows", feature = "gui"))] - for (i, ac) in layer.iter().skip(skip).enumerate() { - let ac = parse_action(ac, s)?; - layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; - } - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] for (i, ac) in layer.iter().skip(2).enumerate() { let ac = parse_action(ac, s)?; layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; @@ -2906,12 +2880,6 @@ fn parse_layers( } LayerExprs::CustomMapping(layer) => { // Parse actions as input output pairs - #[cfg(all(target_os = "windows", feature = "gui"))] - let skip = if layer.len() >= 3 && parse_action_as_cfg(&layer[2]) {3} else {2}; // skip the 3rd list that's used to configure a layer rather than define an action - // Parse actions as input -> output triplets - #[cfg(all(target_os = "windows", feature = "gui"))] - let mut pairs = layer[skip..].chunks_exact(2); - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] let mut pairs = layer[2..].chunks_exact(2); let mut layer_mapped_keys = HashSet::default(); let mut defsrc_anykey_used = false; From ff58f7463298d10c744f3469a85463a453004eab Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:20:38 +0700 Subject: [PATCH 120/154] Revert "gui: cfg: initial support for specifying icons in config as part of the layers" This reverts commit 4e44185db85d6e73b030d1c26c4ef00883b7e777. --- parser/src/cfg/mod.rs | 56 ++++++++++--------------------------------- 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 3f9913116..5f0558871 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -319,8 +319,6 @@ pub type MappedKeys = HashSet; pub struct LayerInfo { pub name: String, pub cfg_text: String, - #[cfg(all(target_os = "windows", feature = "gui"))] - pub layer_icon: Option, } #[allow(clippy::type_complexity)] // return type is not pub @@ -502,7 +500,6 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; -const DEFLAYER_ICON: &str = "icon"; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -650,7 +647,7 @@ pub fn parse_cfg_raw_string( bail!("No deflayer expressions exist. At least one layer must be defined.") } - let (layer_idxs,layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; + let layer_idxs = parse_layer_indexes(&layer_exprs, mapping_order.len())?; let mut sorted_idxs: Vec<(&String, &usize)> = layer_idxs.iter().map(|tuple| (tuple.0, tuple.1)).collect(); @@ -683,7 +680,7 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, layer_icon: layer_icons.get(&name).unwrap_or(&None).clone()}) + .map(|(name, cfg_text)| LayerInfo { name, cfg_text }) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1034,27 +1031,21 @@ fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec< } type LayerIndexes = HashMap; -type LayerIcons = HashMap>; type Aliases = HashMap; -/// Returns layer names and their indexes into the keyberon layout, and their tray icons. This also checks that: +/// Returns layer names and their indexes into the keyberon layout. This also checks that: /// - All layers have the same number of items as the defsrc, /// - There are no duplicate layer names /// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. -fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result<(LayerIndexes,LayerIcons)> { +fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result { let mut layer_indexes = HashMap::default(); - let mut layer_icons = HashMap::default(); for (i, expr_type) in exprs.iter().enumerate() { - let mut icon:Option = None; let (mut subexprs, expr, do_element_count_check) = match expr_type { - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)? , e, true)} - #[cfg(all(target_os = "windows", feature = "gui"))] - SpannedLayerExprs::DefsrcMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER)?.peekable(), e, true)} - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?, e, false)} - #[cfg(all(target_os = "windows", feature = "gui"))] - SpannedLayerExprs::CustomMapping(e) => {(check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?.peekable(), e, false) + SpannedLayerExprs::DefsrcMapping(e) => { + (check_first_expr(e.t.iter(), DEFLAYER)?, e, true) + } + SpannedLayerExprs::CustomMapping(e) => { + (check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?, e, false) } }; let layer_expr = subexprs.next().ok_or_else(|| { @@ -1089,28 +1080,6 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu if layer_indexes.contains_key(&layer_name) { bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); } - let mut third_list_count = 0; - #[cfg(all(target_os = "windows", feature = "gui"))] - if let Some(third) = subexprs.peek() {println!("next is list? = {:?} {:?}",third.list(None),third); - if let Some(third_list) = third.list(None) { - third_list_count = 1; - let third_list_1st = &third_list[0]; - if let Some(third_list_1st_s) = &third_list[0].atom(None) { - if *third_list_1st_s != DEFLAYER_ICON { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st_s}"); - } else { - if let Some(third_list_2nd_s) = &third_list[1].atom(None) { - icon = Some(third_list_2nd_s.to_string()); - } else { - bail!("deflayer failed to parse an icon name in its 3rd element"); - } - } - } else { - bail!("deflayer with a list as its 3rd element only accepts {DEFLAYER_ICON} as its 1st element, not {third_list_1st:?}"); - } - } - subexprs.next(); // advance over the 3rd list - } // Check if user tried to use parentheses directly - `(` and `)` // or escaped them like in kmonad - `\(` and `\)`. for subexpr in subexprs { @@ -1138,7 +1107,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu } } if do_element_count_check { - let num_actions = expr.t.len() - 2 - third_list_count; + let num_actions = expr.t.len() - 2; if num_actions != expected_len { bail_span!( expr, @@ -1149,11 +1118,10 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu ) } } - layer_indexes.insert(layer_name.clone(), i); - layer_icons.insert(layer_name, icon); + layer_indexes.insert(layer_name, i); } - Ok((layer_indexes,layer_icons)) + Ok(layer_indexes) } #[derive(Debug, Clone)] From 317dca0ffbabc2eeaea34ca92d48d80c200bf21e Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:21:55 +0700 Subject: [PATCH 121/154] cfg: add initial support for specifying layer icons in config --- parser/src/cfg/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 5f0558871..b46f97925 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -319,6 +319,7 @@ pub type MappedKeys = HashSet; pub struct LayerInfo { pub name: String, pub cfg_text: String, + pub icon: Option, } #[allow(clippy::type_complexity)] // return type is not pub @@ -500,6 +501,7 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; +const DEFLAYER_ICON: [&str;3] = ["icon","🖻","🖼"]; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -647,7 +649,7 @@ pub fn parse_cfg_raw_string( bail!("No deflayer expressions exist. At least one layer must be defined.") } - let layer_idxs = parse_layer_indexes(&layer_exprs, mapping_order.len())?; + let (layer_idxs,layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; let mut sorted_idxs: Vec<(&String, &usize)> = layer_idxs.iter().map(|tuple| (tuple.0, tuple.1)).collect(); @@ -680,7 +682,7 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { name, cfg_text }) + .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, icon: layer_icons.get(&name).unwrap_or(&None).clone()}) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1031,6 +1033,7 @@ fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec< } type LayerIndexes = HashMap; +type LayerIcons = HashMap>; type Aliases = HashMap; /// Returns layer names and their indexes into the keyberon layout. This also checks that: @@ -1118,10 +1121,11 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu ) } } - layer_indexes.insert(layer_name, i); + layer_indexes.insert(layer_name.clone(), i); + layer_icons.insert(layer_name, icon); } - Ok(layer_indexes) + Ok((layer_indexes,layer_icons)) } #[derive(Debug, Clone)] From 515f96d70cb6f8e18c98a1a00d77acb0cef3c5b5 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:27:59 +0700 Subject: [PATCH 122/154] win-tray: move icon config to a layer name instead of having a special 3rd position --- parser/src/cfg/mod.rs | 65 +++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index b46f97925..88eb63b55 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1040,8 +1040,9 @@ type Aliases = HashMap; /// - All layers have the same number of items as the defsrc, /// - There are no duplicate layer names /// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. -fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result { +fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result<(LayerIndexes,LayerIcons)> { let mut layer_indexes = HashMap::default(); + let mut layer_icons = HashMap::default(); for (i, expr_type) in exprs.iter().enumerate() { let (mut subexprs, expr, do_element_count_check) = match expr_type { SpannedLayerExprs::DefsrcMapping(e) => { @@ -1054,13 +1055,33 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu let layer_expr = subexprs.next().ok_or_else(|| { anyhow_span!(expr, "deflayer requires a name and {expected_len} item(s)") })?; - let layer_name = match expr_type { - SpannedLayerExprs::DefsrcMapping(_) => layer_expr - .atom(None) - .ok_or_else(|| { - anyhow_expr!(layer_expr, "layer name after {DEFLAYER} must be a string") - })? - .to_owned(), + let (layer_name, icon) = match expr_type { + SpannedLayerExprs::DefsrcMapping(_) => match layer_expr { + SExpr::Atom(name_span ) => (name_span.t.to_owned(),None), + SExpr::List(name_opts_span) => { + let listt = &name_opts_span.t; let mut list = listt.into_iter(); + let name = list.next().ok_or_else(|| {anyhow_span!(name_opts_span,"deflayer requires a string name within this pair of parenthesis (or a string name without any)")})? + .atom(None).ok_or_else(|| {anyhow_expr!(layer_expr, "layer name after {DEFLAYER} must be a string when enclosed within one pair of parentheses")})?; + let mut icon = ""; + //todo: add hashmap for future options, currently only parse icons + let mut layer_opts:HashMap = HashMap::default(); + while let Some(key_expr) = list.next() { // Read k-v pairs from the configuration + let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")}).and_then(|opt_key| { + if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { + if layer_opts.contains_key(DEFLAYER_ICON[0]) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER}: {opt_key}");} + Ok(DEFLAYER_ICON[0]) + } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} + })?; + if layer_opts.contains_key(opt_key) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER}: {opt_key}");} + let opt_val = match list.next() { + Some(v) => v.atom(None).ok_or_else(|| anyhow_expr!(v, "No lists are allowed in {DEFLAYER}'s option values")).and_then(|opt_val| {icon=opt_val.trim_matches('"'); Ok(opt_val)})?, + None => bail_expr!(key_expr, "Option without a value in {DEFLAYER} {name}"), + }; + layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); + } + (name.to_owned(),Some(icon.to_owned())) + } + }, SpannedLayerExprs::CustomMapping(_) => { let list = layer_expr .list(None) @@ -1071,13 +1092,29 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu ) })? .to_owned(); - if list.len() != 1 { - bail_expr!( - layer_expr, - "layer name after {DEFLAYER_MAPPED} must be a string within one pair of parentheses" - ); + let mut list = list.iter(); + let name = list.next().ok_or_else(|| {anyhow_expr!(layer_expr,"layer name after {DEFLAYER_MAPPED} must be a string within one pair of parentheses")})? + .atom(None).ok_or_else(|| {anyhow_expr!(layer_expr, "layer name after {DEFLAYER_MAPPED} must be a string within one pair of parentheses")})?; + + let mut icon = ""; + // add hashmap for future options, currently only parse icons + let mut layer_opts:HashMap = HashMap::default(); + while let Some(key_expr) = list.next() { // Read k-v pairs from the configuration + let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(&key_expr, "No lists are allowed in {DEFLAYER_MAPPED} options")}) + .and_then(|opt_key| { + if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { + if layer_opts.contains_key(DEFLAYER_ICON[0]) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER_MAPPED}: {opt_key}");} + Ok(DEFLAYER_ICON[0]) + } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER_MAPPED}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} + })?; + if layer_opts.contains_key(opt_key) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER_MAPPED}: {opt_key}");} + let opt_val = match list.next() { + Some(v) => v.atom(None).ok_or_else(|| anyhow_expr!(&v, "No lists are allowed in {DEFLAYER_MAPPED}'s option values")).and_then(|opt_val| {icon=opt_val.trim_matches('"'); Ok(opt_val)})?, + None => bail_expr!(&key_expr, "Option without a value in {DEFLAYER_MAPPED} {name}"), + }; + layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); } - list[0].atom(None).ok_or_else(|| anyhow_expr!(layer_expr, "layer name after {DEFLAYER_MAPPED} must be a string within one pair of parentheses"))?.to_owned() + (name.to_owned(),Some(icon.to_owned())) } }; if layer_indexes.contains_key(&layer_name) { From 6ca811a788be88fddc6f253b223ff0d5b48a6502 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:42:35 +0700 Subject: [PATCH 123/154] cargo fmt, clippy --- parser/src/cfg/mod.rs | 94 +++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 88eb63b55..6076cd194 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -501,7 +501,7 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; -const DEFLAYER_ICON: [&str;3] = ["icon","🖻","🖼"]; +const DEFLAYER_ICON: [&str; 3] = ["icon", "🖻", "🖼"]; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -649,7 +649,7 @@ pub fn parse_cfg_raw_string( bail!("No deflayer expressions exist. At least one layer must be defined.") } - let (layer_idxs,layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; + let (layer_idxs, layer_icons) = parse_layer_indexes(&layer_exprs, mapping_order.len())?; let mut sorted_idxs: Vec<(&String, &usize)> = layer_idxs.iter().map(|tuple| (tuple.0, tuple.1)).collect(); @@ -682,7 +682,11 @@ pub fn parse_cfg_raw_string( let layer_info: Vec = layer_names .into_iter() .zip(layer_strings) - .map(|(name, cfg_text)| LayerInfo { name: name.clone(), cfg_text, icon: layer_icons.get(&name).unwrap_or(&None).clone()}) + .map(|(name, cfg_text)| LayerInfo { + name: name.clone(), + cfg_text, + icon: layer_icons.get(&name).unwrap_or(&None).clone(), + }) .collect(); let defsrc_layer = create_defsrc_layer(); @@ -1040,7 +1044,10 @@ type Aliases = HashMap; /// - All layers have the same number of items as the defsrc, /// - There are no duplicate layer names /// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. -fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Result<(LayerIndexes,LayerIcons)> { +fn parse_layer_indexes( + exprs: &[SpannedLayerExprs], + expected_len: usize, +) -> Result<(LayerIndexes, LayerIcons)> { let mut layer_indexes = HashMap::default(); let mut layer_icons = HashMap::default(); for (i, expr_type) in exprs.iter().enumerate() { @@ -1057,29 +1064,46 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu })?; let (layer_name, icon) = match expr_type { SpannedLayerExprs::DefsrcMapping(_) => match layer_expr { - SExpr::Atom(name_span ) => (name_span.t.to_owned(),None), + SExpr::Atom(name_span) => (name_span.t.to_owned(), None), SExpr::List(name_opts_span) => { - let listt = &name_opts_span.t; let mut list = listt.into_iter(); + let listt = &name_opts_span.t; + let mut list = listt.iter(); let name = list.next().ok_or_else(|| {anyhow_span!(name_opts_span,"deflayer requires a string name within this pair of parenthesis (or a string name without any)")})? .atom(None).ok_or_else(|| {anyhow_expr!(layer_expr, "layer name after {DEFLAYER} must be a string when enclosed within one pair of parentheses")})?; - let mut icon = ""; - //todo: add hashmap for future options, currently only parse icons - let mut layer_opts:HashMap = HashMap::default(); - while let Some(key_expr) = list.next() { // Read k-v pairs from the configuration - let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")}).and_then(|opt_key| { + let mut icon = ""; + //todo: add hashmap for future options, currently only parse icons + let mut layer_opts: HashMap = HashMap::default(); + while let Some(key_expr) = list.next() { + // Read k-v pairs from the configuration + let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")}).and_then(|opt_key| { if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { if layer_opts.contains_key(DEFLAYER_ICON[0]) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER}: {opt_key}");} Ok(DEFLAYER_ICON[0]) } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} })?; - if layer_opts.contains_key(opt_key) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER}: {opt_key}");} - let opt_val = match list.next() { - Some(v) => v.atom(None).ok_or_else(|| anyhow_expr!(v, "No lists are allowed in {DEFLAYER}'s option values")).and_then(|opt_val| {icon=opt_val.trim_matches('"'); Ok(opt_val)})?, - None => bail_expr!(key_expr, "Option without a value in {DEFLAYER} {name}"), - }; - layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); + if layer_opts.contains_key(opt_key) { + bail_expr!(key_expr, "Duplicate option found in {DEFLAYER}: {opt_key}"); } - (name.to_owned(),Some(icon.to_owned())) + let opt_val = match list.next() { + Some(v) => v + .atom(None) + .ok_or_else(|| { + anyhow_expr!( + v, + "No lists are allowed in {DEFLAYER}'s option values" + ) + }) + .map(|opt_val| { + icon = opt_val.trim_matches('"'); + opt_val + })?, + None => { + bail_expr!(key_expr, "Option without a value in {DEFLAYER} {name}") + } + }; + layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); + } + (name.to_owned(), Some(icon.to_owned())) } }, SpannedLayerExprs::CustomMapping(_) => { @@ -1098,8 +1122,9 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu let mut icon = ""; // add hashmap for future options, currently only parse icons - let mut layer_opts:HashMap = HashMap::default(); - while let Some(key_expr) = list.next() { // Read k-v pairs from the configuration + let mut layer_opts: HashMap = HashMap::default(); + while let Some(key_expr) = list.next() { + // Read k-v pairs from the configuration let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(&key_expr, "No lists are allowed in {DEFLAYER_MAPPED} options")}) .and_then(|opt_key| { if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { @@ -1107,14 +1132,33 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu Ok(DEFLAYER_ICON[0]) } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER_MAPPED}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} })?; - if layer_opts.contains_key(opt_key) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER_MAPPED}: {opt_key}");} + if layer_opts.contains_key(opt_key) { + bail_expr!( + key_expr, + "Duplicate option found in {DEFLAYER_MAPPED}: {opt_key}" + ); + } let opt_val = match list.next() { - Some(v) => v.atom(None).ok_or_else(|| anyhow_expr!(&v, "No lists are allowed in {DEFLAYER_MAPPED}'s option values")).and_then(|opt_val| {icon=opt_val.trim_matches('"'); Ok(opt_val)})?, - None => bail_expr!(&key_expr, "Option without a value in {DEFLAYER_MAPPED} {name}"), + Some(v) => v + .atom(None) + .ok_or_else(|| { + anyhow_expr!( + &v, + "No lists are allowed in {DEFLAYER_MAPPED}'s option values" + ) + }) + .map(|opt_val| { + icon = opt_val.trim_matches('"'); + opt_val + })?, + None => bail_expr!( + &key_expr, + "Option without a value in {DEFLAYER_MAPPED} {name}" + ), }; layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); } - (name.to_owned(),Some(icon.to_owned())) + (name.to_owned(), Some(icon.to_owned())) } }; if layer_indexes.contains_key(&layer_name) { @@ -1162,7 +1206,7 @@ fn parse_layer_indexes(exprs: &[SpannedLayerExprs], expected_len: usize) -> Resu layer_icons.insert(layer_name, icon); } - Ok((layer_indexes,layer_icons)) + Ok((layer_indexes, layer_icons)) } #[derive(Debug, Clone)] From 8f2be35ec7e6073185394be9bc9b9a5836a8bd9a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 01:48:38 +0700 Subject: [PATCH 124/154] doc: update tray icon format --- cfg_samples/tray-icon/tray-icon.kbd | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cfg_samples/tray-icon/tray-icon.kbd b/cfg_samples/tray-icon/tray-icon.kbd index 2a317f30a..5cca181a5 100644 --- a/cfg_samples/tray-icon/tray-icon.kbd +++ b/cfg_samples/tray-icon/tray-icon.kbd @@ -11,11 +11,11 @@ (defalias l5 (layer-while-held 5no-icn)) (defalias l6 (layer-while-held 6name-match)) -(defsrc 1 2 3 4 5 6) -(deflayer ⌂ (icon base.png) @l1 @l2 @l3 @l4 @l5 @l6) ;; find in the 'icon' subfolder -(deflayer 1emoji (🖻 1symbols.png) q q q q q q) ;; find in the 'icons' subfolder -(deflayer 2icon-quote (🖻 "2Nav Num.png") w w w w w w) ;; find in the 'img' subfolder -(deflayer 3emoji_alt (🖼 3trans.parent) e e e e e e) ;; find '.png' -(deflayermap (4my-lmap) (🖻 "..\..\assets\kanata.ico") 1 r 2 r 3 r 4 r 5 r 6 r) ;; find in relative path -(deflayer 5no-icn t t t t t t) ;; match file name from 'tray-icon' config, whithout which would fall back to 'tray-icon.png' as it's the only valid icon matching 'tray-icon.kbd' name -(deflayer 6name-match y y y y y y) ;; uses '6name-match' with any valid extension since 'icon-match-layer-name' is set to 'yes' +(defsrc 1 2 3 4 5 6) +(deflayer (⌂ icon base.png) @l1 @l2 @l3 @l4 @l5 @l6) ;; find in the 'icon' subfolder +(deflayer (1emoji 🖻 1symbols.png) q q q q q q) ;; find in the 'icons' subfolder +(deflayer (2icon-quote 🖻 "2Nav Num.png") w w w w w w) ;; find in the 'img' subfolder +(deflayer (3emoji_alt 🖼 3trans.parent) e e e e e e) ;; find '.png' +(deflayermap (4my-lmap 🖻 "..\..\assets\kanata.ico") 1 r 2 r 3 r 4 r 5 r 6 r) ;; find in relative path +(deflayer 5no-icn t t t t t t) ;; match file name from 'tray-icon' config, whithout which would fall back to 'tray-icon.png' as it's the only valid icon matching 'tray-icon.kbd' name +(deflayer 6name-match y y y y y y) ;; uses '6name-match' with any valid extension since 'icon-match-layer-name' is set to 'yes' From 1d2a22b7980a53b59f2b89169794962c5aa5da5e Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 02:15:57 +0700 Subject: [PATCH 125/154] fix missing logging --- src/lib_main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib_main.rs b/src/lib_main.rs index ccc7825dc..93d45e4bb 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -145,7 +145,10 @@ fn cli_init() -> Result { if let Err(e) = log_cfg.set_time_offset_to_local() { eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; - log_cfg.set_time_format_rfc3339(); + log_cfg.set_time_format_custom(format_description!( + version = 2, + "[hour]:[minute]:[second].[subsecond digits:4]" + )); #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] CombinedLogger::init(vec![TermLogger::new( log_lvl, From 05e6e1fbe6a9fa5344e5be9d0fd959a170de89cd Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 02:20:32 +0700 Subject: [PATCH 126/154] win-tray: dep: update lock file --- Cargo.lock | 81 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed2cc94ea..6bcdef40b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,7 +214,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -280,7 +280,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml", + "toml 0.8.12", "vswhom", "winreg", ] @@ -501,9 +501,11 @@ dependencies = [ "kanata-parser", "kanata-tcp-protocol", "karabiner-driverkit", + "lazy_static", "log", "miette", "mio", + "native-windows-derive", "native-windows-gui", "nix 0.26.4", "once_cell", @@ -516,7 +518,9 @@ dependencies = [ "signal-hook", "simplelog", "time", + "win_dbg_logger", "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -703,7 +707,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -727,6 +731,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "muldiv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" + +[[package]] +name = "native-windows-derive" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76134ae81020d89d154f619fd2495a2cecad204276b1dc21174b55e4d0975edd" +dependencies = [ + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "native-windows-gui" version = "1.0.13" @@ -735,6 +757,7 @@ checksum = "4f7003a669f68deb6b7c57d74fff4f8e533c44a3f0b297492440ef4ff5a28454" dependencies = [ "bitflags 1.3.2", "lazy_static", + "muldiv", "winapi", "winapi-build", ] @@ -795,10 +818,10 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -892,6 +915,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1047,7 +1079,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -1161,6 +1193,17 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.55" @@ -1225,7 +1268,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -1261,6 +1304,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.12" @@ -1377,7 +1429,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.55", "wasm-bindgen-shared", ] @@ -1399,7 +1451,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1420,6 +1472,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "win_dbg_logger" +version = "0.1.0" +dependencies = [ + "lazy_static", + "log", + "regex", + "simplelog", + "winapi", +] + [[package]] name = "winapi" version = "0.3.9" From 352faaf734f92e8c30850140afbb8a2985d3a91c Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 19:36:27 +0700 Subject: [PATCH 127/154] make Args pub to be used in gui --- src/lib_main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib_main.rs b/src/lib_main.rs index 93d45e4bb..9a65e867c 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -18,7 +18,7 @@ use std::path::PathBuf; /// /// If you need help, please feel welcome to create an issue or discussion in /// the kanata repository: https://github.com/jtroo/kanata -struct Args { +pub struct Args { // Display different platform specific paths based on the target OS #[cfg_attr( target_os = "windows", @@ -39,7 +39,7 @@ kanata.kbd in the current working directory and '$XDG_CONFIG_HOME/kanata/kanata.kbd'" )] #[arg(short, long, verbatim_doc_comment)] - cfg: Option>, + pub cfg: Option>, /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be /// listened on. @@ -50,30 +50,30 @@ kanata.kbd in the current working directory and value_name = "PORT or IP:PORT", verbatim_doc_comment )] - tcp_server_address: Option, + pub tcp_server_address: Option, /// Path for the symlink pointing to the newly-created device. If blank, no /// symlink will be created. #[cfg(target_os = "linux")] #[arg(short, long, verbatim_doc_comment)] - symlink_path: Option, + pub symlink_path: Option, /// List the keyboards available for grabbing and exit. #[cfg(target_os = "macos")] #[arg(short, long)] - list: bool, + pub list: bool, /// Enable debug logging. #[arg(short, long)] - debug: bool, + pub debug: bool, /// Enable trace logging; implies --debug as well. #[arg(short, long)] - trace: bool, + pub trace: bool, /// Remove the startup delay on kanata. /// In some cases, removing the delay may cause keyboard issues on startup. #[arg(short, long, verbatim_doc_comment)] - nodelay: bool, + pub nodelay: bool, /// Milliseconds to wait before attempting to register a newly connected /// device. The default is 200. @@ -82,11 +82,11 @@ kanata.kbd in the current working directory and /// to register - the device may be taking too long to become ready. #[cfg(target_os = "linux")] #[arg(short, long, verbatim_doc_comment)] - wait_device_ms: Option, + pub wait_device_ms: Option, /// Validate configuration file and exit #[arg(long, verbatim_doc_comment)] - check: bool, + pub check: bool, } /// Parse CLI arguments and initialize logging. From 253464665385e8390d1741b3ff9d365a12e1b28d Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 19:08:47 +0700 Subject: [PATCH 128/154] win-tray: move gui win to a separate folder --- src/gui/[gui].rs | 206 ++++++++++++++++++ src/{gui_win.rs => gui/win.rs} | 0 .../gui/win_nwg_ext-license | 0 src/{gui_nwg_ext.rs => gui/win_nwg_ext.rs} | 0 src/lib_main.rs | 117 +--------- 5 files changed, 215 insertions(+), 108 deletions(-) create mode 100644 src/gui/[gui].rs rename src/{gui_win.rs => gui/win.rs} (100%) rename license-tray-icons => src/gui/win_nwg_ext-license (100%) rename src/{gui_nwg_ext.rs => gui/win_nwg_ext.rs} (100%) diff --git a/src/gui/[gui].rs b/src/gui/[gui].rs new file mode 100644 index 000000000..4bea22624 --- /dev/null +++ b/src/gui/[gui].rs @@ -0,0 +1,206 @@ +use crate::*; +use anyhow::{bail, Result, Context}; +use clap::Parser; +use clap::{error::ErrorKind, CommandFactory}; +use kanata_parser::cfg; +use simplelog::{format_description, *}; + +pub mod win; +pub use win::*; +pub mod win_nwg_ext; +pub use win_nwg_ext::*; +use lib_main::*; + +pub use win_dbg_logger as log_win; +pub use win_dbg_logger::WINDBG_LOGGER; + +use parking_lot::Mutex; +use std::sync::{Arc, OnceLock}; +pub static CFG: OnceLock>> = OnceLock::new(); +pub static GUI_TX: OnceLock = OnceLock::new(); + +/// Parse CLI arguments and initialize logging. +fn cli_init() -> Result { + let args = match Args::try_parse() { + Ok(args) => args, + Err(e) => { + if *IS_TERM { + // init loggers without config so '-help' "error" or real ones can be printed + let mut log_cfg = ConfigBuilder::new(); + CombinedLogger::init(vec![ + TermLogger::new( + LevelFilter::Debug, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + ), + log_win::windbg_simple_combo(LevelFilter::Debug), + ]) + .expect("logger can init"); + } else { + log_win::init(); + log::set_max_level(LevelFilter::Debug); + } // doesn't panic + match e.kind() { + ErrorKind::DisplayHelp => { + let mut cmd = lib_main::Args::command(); + let help = cmd.render_help(); + info!("{help}"); + log::set_max_level(LevelFilter::Off); + return Err(anyhow!("")); + } + _ => return Err(e.into()), + } + } + }; + + #[cfg(target_os = "macos")] + if args.list { + karabiner_driverkit::list_keyboards(); + std::process::exit(0); + } + + let cfg_paths = args.cfg.unwrap_or_else(default_cfg); + + let log_lvl = match (args.debug, args.trace) { + (_, true) => LevelFilter::Trace, + (true, false) => LevelFilter::Debug, + (false, false) => LevelFilter::Info, + }; + + let mut log_cfg = ConfigBuilder::new(); + if let Err(e) = log_cfg.set_time_offset_to_local() { + eprintln!("WARNING: could not set log TZ to local: {e:?}"); + }; + log_cfg.set_time_format_custom(format_description!( + version = 2, + "[hour]:[minute]:[second].[subsecond digits:4]" + )); + if *IS_TERM { + CombinedLogger::init(vec![ + TermLogger::new( + log_lvl, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + ), + log_win::windbg_simple_combo(log_lvl), + ]) + .expect("logger can init"); + } else { + CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl)]).expect("logger can init"); + } + log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); + #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] + log::info!("using LLHOOK+SendInput for keyboard IO"); + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + log::info!("using the Interception driver for keyboard IO"); + + if let Some(config_file) = cfg_paths.first() { + if !config_file.exists() { + bail!( + "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", + cfg_paths[0].to_str().unwrap_or("?") + ) + } + } else { + bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); + } + + if args.check { + log::info!("validating config only and exiting"); + let status = match cfg::new_from_file(&cfg_paths[0]) { + Ok(_) => 0, + Err(e) => { + log::error!("{e:?}"); + 1 + } + }; + std::process::exit(status); + } + + #[cfg(target_os = "linux")] + if let Some(wait) = args.wait_device_ms { + use std::sync::atomic::Ordering; + log::info!("Setting device registration wait time to {wait} ms."); + oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); + } + + Ok(ValidatedArgs { + paths: cfg_paths, + #[cfg(feature = "tcp_server")] + tcp_server_address: args.tcp_server_address, + #[cfg(target_os = "linux")] + symlink_path: args.symlink_path, + nodelay: args.nodelay, + }) +} + +fn main_impl() -> Result<()> { + let args = cli_init()?; + let kanata_arc = Kanata::new_arc(&args)?; + + if CFG.set(kanata_arc.clone()).is_err() { + warn!("Someone else set our ‘CFG’"); + }; // store a clone of cfg so that we can ask it to reset itself + + if !args.nodelay { + info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); + std::thread::sleep(std::time::Duration::from_secs(2)); + } + + // Start a processing loop in another thread and run the event loop in this thread. + // + // The reason for two different event loops is that the "event loop" only listens for keyboard + // events, which it sends to the "processing loop". The processing loop handles keyboard events + // while also maintaining `tick()` calls to keyberon. + + let (tx, rx) = std::sync::mpsc::sync_channel(100); + + let (server, ntx, nrx) = if let Some(address) = { + #[cfg(feature = "tcp_server")] + { + args.tcp_server_address + } + #[cfg(not(feature = "tcp_server"))] + { + None:: + } + } { + let mut server = TcpServer::new(address.into_inner(), tx.clone()); + server.start(kanata_arc.clone()); + let (ntx, nrx) = std::sync::mpsc::sync_channel(100); + (Some(server), Some(ntx), Some(nrx)) + } else { + (None, None, None) + }; + + native_windows_gui::init().context("Failed to init Native Windows GUI")?; + let ui = build_tray(&kanata_arc)?; + let gui_tx = ui.layer_notice.sender(); + if GUI_TX.set(gui_tx).is_err() { + warn!("Someone else set our ‘GUI_TX’"); + }; + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); + + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } + + Kanata::event_loop(kanata_arc, tx, ui)?; + + Ok(()) +} + +pub fn lib_main_gui() { + let _attach_console = *IS_CONSOLE; + let ret = main_impl(); + if let Err(ref e) = ret { + log::error!("{e}\n"); + } + + unsafe { + FreeConsole(); + } +} diff --git a/src/gui_win.rs b/src/gui/win.rs similarity index 100% rename from src/gui_win.rs rename to src/gui/win.rs diff --git a/license-tray-icons b/src/gui/win_nwg_ext-license similarity index 100% rename from license-tray-icons rename to src/gui/win_nwg_ext-license diff --git a/src/gui_nwg_ext.rs b/src/gui/win_nwg_ext.rs similarity index 100% rename from src/gui_nwg_ext.rs rename to src/gui/win_nwg_ext.rs diff --git a/src/lib_main.rs b/src/lib_main.rs index 9a65e867c..835386ecb 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -1,8 +1,6 @@ use crate::*; use anyhow::{bail, Result}; use clap::Parser; -#[cfg(all(target_os = "windows", feature = "gui"))] -use clap::{error::ErrorKind, CommandFactory}; use kanata_parser::cfg; use log::info; use simplelog::{format_description, *}; @@ -91,41 +89,7 @@ kanata.kbd in the current working directory and /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] let args = Args::parse(); - #[cfg(all(target_os = "windows", feature = "gui"))] - let args = match Args::try_parse() { - Ok(args) => args, - Err(e) => { - if *IS_TERM { - // init loggers without config so '-help' "error" or real ones can be printed - let mut log_cfg = ConfigBuilder::new(); - CombinedLogger::init(vec![ - TermLogger::new( - LevelFilter::Debug, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - ), - log_win::windbg_simple_combo(LevelFilter::Debug), - ]) - .expect("logger can init"); - } else { - log_win::init(); - log::set_max_level(LevelFilter::Debug); - } // doesn't panic - match e.kind() { - ErrorKind::DisplayHelp => { - let mut cmd = lib_main::Args::command(); - let help = cmd.render_help(); - info!("{help}"); - log::set_max_level(LevelFilter::Off); - return Err(anyhow!("")); - } - _ => return Err(e.into()), - } - } - }; #[cfg(target_os = "macos")] if args.list { @@ -149,7 +113,6 @@ fn cli_init() -> Result { version = 2, "[hour]:[minute]:[second].[subsecond digits:4]" )); - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] CombinedLogger::init(vec![TermLogger::new( log_lvl, log_cfg.build(), @@ -157,21 +120,7 @@ fn cli_init() -> Result { ColorChoice::AlwaysAnsi, )]) .expect("logger can init"); - #[cfg(all(target_os = "windows", feature = "gui"))] - if *IS_TERM { - CombinedLogger::init(vec![ - TermLogger::new( - log_lvl, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - ), - log_win::windbg_simple_combo(log_lvl), - ]) - .expect("logger can init"); - } else { - CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl)]).expect("logger can init"); - } + log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] log::info!("using LLHOOK+SendInput for keyboard IO"); @@ -222,11 +171,6 @@ fn main_impl() -> Result<()> { let args = cli_init()?; let kanata_arc = Kanata::new_arc(&args)?; - #[cfg(all(target_os = "windows", feature = "gui"))] - if CFG.set(kanata_arc.clone()).is_err() { - warn!("Someone else set our ‘CFG’"); - }; // store a clone of cfg so that we can ask it to reset itself - if !args.nodelay { info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); std::thread::sleep(std::time::Duration::from_secs(2)); @@ -258,40 +202,18 @@ fn main_impl() -> Result<()> { (None, None, None) }; - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - { - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - #[cfg(target_os = "linux")] - sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - - Kanata::event_loop(kanata_arc, tx)?; + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); } - #[cfg(all(target_os = "windows", feature = "gui"))] - { - use anyhow::Context; - - native_windows_gui::init().context("Failed to init Native Windows GUI")?; - let ui = build_tray(&kanata_arc)?; - let gui_tx = ui.layer_notice.sender(); - if GUI_TX.set(gui_tx).is_err() { - warn!("Someone else set our ‘GUI_TX’"); - }; - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } + #[cfg(target_os = "linux")] + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - Kanata::event_loop(kanata_arc, tx, ui)?; - } + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + Kanata::event_loop(kanata_arc, tx)?; Ok(()) } @@ -304,24 +226,3 @@ pub fn lib_main_cli() -> Result<()> { let _ = std::io::stdin().read_line(&mut String::new()); ret } -#[cfg(all(target_os = "windows", feature = "gui"))] -use parking_lot::Mutex; -#[cfg(all(target_os = "windows", feature = "gui"))] -use std::sync::{Arc, OnceLock}; -#[cfg(all(target_os = "windows", feature = "gui"))] -pub static CFG: OnceLock>> = OnceLock::new(); -#[cfg(all(target_os = "windows", feature = "gui"))] -pub static GUI_TX: OnceLock = OnceLock::new(); - -#[cfg(all(target_os = "windows", feature = "gui"))] -pub fn lib_main_gui() { - let _attach_console = *IS_CONSOLE; - let ret = main_impl(); - if let Err(ref e) = ret { - log::error!("{e}\n"); - } - - unsafe { - FreeConsole(); - } -} From 2bafd0bb1212a24a2e17aca4d49ffb6ebfd39a0f Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 19:39:59 +0700 Subject: [PATCH 129/154] win-tray: update gui references to the new mod --- src/gui/win.rs | 4 ++-- src/kanata/mod.rs | 2 +- src/kanata/windows/llhook.rs | 2 +- src/lib.rs | 12 +----------- src/main.rs | 2 +- 5 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/gui/win.rs b/src/gui/win.rs index e5f3b42b9..2a9d5d176 100644 --- a/src/gui/win.rs +++ b/src/gui/win.rs @@ -10,7 +10,7 @@ use std::env::{current_exe, var_os}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use crate::gui_nwg_ext::{BitmapEx, MenuEx, MenuItemEx}; +use crate::gui::win_nwg_ext::{BitmapEx, MenuEx, MenuItemEx}; use kanata_parser::cfg; use nwg::{ControlHandle, NativeUi}; use std::sync::Arc; @@ -81,7 +81,7 @@ const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow check 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 -use crate::lib_main::CFG; +use crate::gui::CFG; /// 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` or a config file name at various locations (where config file is, where executable is, in user config folders) fn get_icon_p( diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index d4c4dd80e..dcf04bf52 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -62,7 +62,7 @@ mod unknown; use unknown::*; #[cfg(all(target_os = "windows", feature = "gui"))] -use crate::lib_main::GUI_TX; +use crate::gui::GUI_TX; mod caps_word; pub use caps_word::*; diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index b99c7fb51..ef8b5840e 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -13,7 +13,7 @@ impl Kanata { pub fn event_loop( _cfg: Arc>, tx: Sender, - #[cfg(all(target_os = "windows", feature = "gui"))] ui: crate::system_tray_ui::SystemTrayUi, + #[cfg(all(target_os = "windows", feature = "gui"))] ui: crate::gui::system_tray_ui::SystemTrayUi, ) -> Result<()> { // Display debug and panic output when launched from a terminal. #[cfg(not(feature = "gui"))] diff --git a/src/lib.rs b/src/lib.rs index 2088fa8af..e0abe01e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,23 +4,13 @@ use std::path::PathBuf; use std::str::FromStr; #[cfg(all(target_os = "windows", feature = "gui"))] -pub mod gui_win; +#[path="gui/[gui].rs"] pub mod gui; pub mod kanata; pub mod lib_main; pub mod oskbd; pub mod tcp_server; #[cfg(test)] pub mod tests; -#[cfg(all(target_os = "windows", feature = "gui"))] -pub use gui_win::*; -#[cfg(all(target_os = "windows", feature = "gui"))] -pub mod gui_nwg_ext; -#[cfg(all(target_os = "windows", feature = "gui"))] -pub use gui_nwg_ext::*; -#[cfg(all(target_os = "windows", feature = "gui"))] -pub use win_dbg_logger as log_win; -#[cfg(all(target_os = "windows", feature = "gui"))] -pub use win_dbg_logger::WINDBG_LOGGER; pub use kanata::*; pub use tcp_server::TcpServer; diff --git a/src/main.rs b/src/main.rs index 1ec850200..f3de63431 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ #[cfg(not(feature = "gui"))] use kanata_state_machine::lib_main::lib_main_cli; #[cfg(feature = "gui")] -use kanata_state_machine::lib_main::lib_main_gui; +use kanata_state_machine::gui::lib_main_gui; #[cfg(not(feature = "gui"))] use anyhow::Result; From 39ee0dbf652c2f738604efa0954b5f7a819d8d2a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 19:44:10 +0700 Subject: [PATCH 130/154] cargo fmt --- src/gui/[gui].rs | 4 ++-- src/kanata/windows/llhook.rs | 3 ++- src/lib.rs | 3 ++- src/main.rs | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/gui/[gui].rs b/src/gui/[gui].rs index 4bea22624..3633bb476 100644 --- a/src/gui/[gui].rs +++ b/src/gui/[gui].rs @@ -1,5 +1,5 @@ use crate::*; -use anyhow::{bail, Result, Context}; +use anyhow::{bail, Context, Result}; use clap::Parser; use clap::{error::ErrorKind, CommandFactory}; use kanata_parser::cfg; @@ -8,8 +8,8 @@ use simplelog::{format_description, *}; pub mod win; pub use win::*; pub mod win_nwg_ext; -pub use win_nwg_ext::*; use lib_main::*; +pub use win_nwg_ext::*; pub use win_dbg_logger as log_win; pub use win_dbg_logger::WINDBG_LOGGER; diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index ef8b5840e..b29aeb46f 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -13,7 +13,8 @@ impl Kanata { pub fn event_loop( _cfg: Arc>, tx: Sender, - #[cfg(all(target_os = "windows", feature = "gui"))] ui: crate::gui::system_tray_ui::SystemTrayUi, + #[cfg(all(target_os = "windows", feature = "gui"))] + ui: crate::gui::system_tray_ui::SystemTrayUi, ) -> Result<()> { // Display debug and panic output when launched from a terminal. #[cfg(not(feature = "gui"))] diff --git a/src/lib.rs b/src/lib.rs index e0abe01e1..e5851a22c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,8 @@ use std::path::PathBuf; use std::str::FromStr; #[cfg(all(target_os = "windows", feature = "gui"))] -#[path="gui/[gui].rs"] pub mod gui; +#[path = "gui/[gui].rs"] +pub mod gui; pub mod kanata; pub mod lib_main; pub mod oskbd; diff --git a/src/main.rs b/src/main.rs index f3de63431..74ed03c19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ #![cfg_attr(feature = "gui", windows_subsystem = "windows")] //disable console on Windows -#[cfg(not(feature = "gui"))] -use kanata_state_machine::lib_main::lib_main_cli; #[cfg(feature = "gui")] use kanata_state_machine::gui::lib_main_gui; +#[cfg(not(feature = "gui"))] +use kanata_state_machine::lib_main::lib_main_cli; #[cfg(not(feature = "gui"))] use anyhow::Result; From a5cff362d0b3e6caa07c2ff5997a2dbd725634e8 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 19:55:23 +0700 Subject: [PATCH 131/154] win-tray: move gui notice sender to gui mode --- src/gui/win.rs | 10 +++++++++- src/kanata/mod.rs | 17 ++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/gui/win.rs b/src/gui/win.rs index 2a9d5d176..528ca0e52 100644 --- a/src/gui/win.rs +++ b/src/gui/win.rs @@ -81,7 +81,15 @@ const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow check 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 -use crate::gui::CFG; +use crate::gui::{CFG, GUI_TX}; + +pub fn send_gui_notice() { + if let Some(gui_tx) = GUI_TX.get() { + gui_tx.notice(); + } else { + error!("no GUI_TX to notify GUI thread of layer changes"); + } +} /// 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` or a config file name at various locations (where config file is, where executable is, in user config folders) fn get_icon_p( diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index dcf04bf52..81d50f781 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1,5 +1,7 @@ //! Implements the glue between OS input/output and keyberon state management. +#[cfg(all(target_os = "windows", feature = "gui"))] +use crate::gui::win::*; use anyhow::{bail, Result}; use kanata_parser::sequences::*; use log::{error, info}; @@ -61,9 +63,6 @@ mod unknown; #[cfg(target_os = "unknown")] use unknown::*; -#[cfg(all(target_os = "windows", feature = "gui"))] -use crate::gui::GUI_TX; - mod caps_word; pub use caps_word::*; @@ -552,11 +551,7 @@ impl Kanata { } } #[cfg(all(target_os = "windows", feature = "gui"))] - if let Some(gui_tx) = GUI_TX.get() { - gui_tx.notice(); - } else { - error!("no GUI_TX to notify GUI thread of layer changes"); - } + send_gui_notice(); Ok(()) } @@ -1534,11 +1529,7 @@ impl Kanata { } } #[cfg(all(target_os = "windows", feature = "gui"))] - if let Some(gui_tx) = GUI_TX.get() { - gui_tx.notice(); - } else { - error!("no GUI_TX to notify GUI thread of layer changes"); - } + send_gui_notice(); } } From 902248b38b9b4e46f4d39694b740e955120db03d Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 19:59:08 +0700 Subject: [PATCH 132/154] Revert "make Args pub to be used in gui" This reverts commit 352faaf734f92e8c30850140afbb8a2985d3a91c. --- src/lib_main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib_main.rs b/src/lib_main.rs index 835386ecb..52d696018 100644 --- a/src/lib_main.rs +++ b/src/lib_main.rs @@ -16,7 +16,7 @@ use std::path::PathBuf; /// /// If you need help, please feel welcome to create an issue or discussion in /// the kanata repository: https://github.com/jtroo/kanata -pub struct Args { +struct Args { // Display different platform specific paths based on the target OS #[cfg_attr( target_os = "windows", @@ -37,7 +37,7 @@ kanata.kbd in the current working directory and '$XDG_CONFIG_HOME/kanata/kanata.kbd'" )] #[arg(short, long, verbatim_doc_comment)] - pub cfg: Option>, + cfg: Option>, /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be /// listened on. @@ -48,30 +48,30 @@ kanata.kbd in the current working directory and value_name = "PORT or IP:PORT", verbatim_doc_comment )] - pub tcp_server_address: Option, + tcp_server_address: Option, /// Path for the symlink pointing to the newly-created device. If blank, no /// symlink will be created. #[cfg(target_os = "linux")] #[arg(short, long, verbatim_doc_comment)] - pub symlink_path: Option, + symlink_path: Option, /// List the keyboards available for grabbing and exit. #[cfg(target_os = "macos")] #[arg(short, long)] - pub list: bool, + list: bool, /// Enable debug logging. #[arg(short, long)] - pub debug: bool, + debug: bool, /// Enable trace logging; implies --debug as well. #[arg(short, long)] - pub trace: bool, + trace: bool, /// Remove the startup delay on kanata. /// In some cases, removing the delay may cause keyboard issues on startup. #[arg(short, long, verbatim_doc_comment)] - pub nodelay: bool, + nodelay: bool, /// Milliseconds to wait before attempting to register a newly connected /// device. The default is 200. @@ -80,11 +80,11 @@ kanata.kbd in the current working directory and /// to register - the device may be taking too long to become ready. #[cfg(target_os = "linux")] #[arg(short, long, verbatim_doc_comment)] - pub wait_device_ms: Option, + wait_device_ms: Option, /// Validate configuration file and exit #[arg(long, verbatim_doc_comment)] - pub check: bool, + check: bool, } /// Parse CLI arguments and initialize logging. From 4a89fe00a2eb4fc9377e1bc95371f361d6b02885 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 20:00:34 +0700 Subject: [PATCH 133/154] win-tray: dupe Args from cli --- src/gui/[gui].rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/src/gui/[gui].rs b/src/gui/[gui].rs index 3633bb476..f22e45ce0 100644 --- a/src/gui/[gui].rs +++ b/src/gui/[gui].rs @@ -8,7 +8,7 @@ use simplelog::{format_description, *}; pub mod win; pub use win::*; pub mod win_nwg_ext; -use lib_main::*; + pub use win_nwg_ext::*; pub use win_dbg_logger as log_win; @@ -19,6 +19,87 @@ use std::sync::{Arc, OnceLock}; pub static CFG: OnceLock>> = OnceLock::new(); pub static GUI_TX: OnceLock = OnceLock::new(); +#[derive(Parser, Debug)] +#[command(author, version, verbatim_doc_comment)] +/// kanata: an advanced software key remapper +/// +/// kanata remaps key presses to other keys or complex actions depending on the +/// configuration for that key. You can find the guide for creating a config +/// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc +/// +/// If you need help, please feel welcome to create an issue or discussion in +/// the kanata repository: https://github.com/jtroo/kanata +struct Args { + // Display different platform specific paths based on the target OS + #[cfg_attr( + target_os = "windows", + doc = r"Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" + )] + #[cfg_attr( + target_os = "macos", + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$HOME/Library/Application Support/kanata/kanata.kbd.'" + )] + #[cfg_attr( + not(any(target_os = "macos", target_os = "windows")), + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$XDG_CONFIG_HOME/kanata/kanata.kbd'" + )] + #[arg(short, long, verbatim_doc_comment)] + cfg: Option>, + + /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be + /// listened on. + #[cfg(feature = "tcp_server")] + #[arg( + short = 'p', + long = "port", + value_name = "PORT or IP:PORT", + verbatim_doc_comment + )] + tcp_server_address: Option, + /// Path for the symlink pointing to the newly-created device. If blank, no + /// symlink will be created. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + symlink_path: Option, + + /// List the keyboards available for grabbing and exit. + #[cfg(target_os = "macos")] + #[arg(short, long)] + list: bool, + + /// Enable debug logging. + #[arg(short, long)] + debug: bool, + + /// Enable trace logging; implies --debug as well. + #[arg(short, long)] + trace: bool, + + /// Remove the startup delay on kanata. + /// In some cases, removing the delay may cause keyboard issues on startup. + #[arg(short, long, verbatim_doc_comment)] + nodelay: bool, + + /// Milliseconds to wait before attempting to register a newly connected + /// device. The default is 200. + /// + /// You may wish to increase this if you have a device that is failing + /// to register - the device may be taking too long to become ready. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + wait_device_ms: Option, + + /// Validate configuration file and exit + #[arg(long, verbatim_doc_comment)] + check: bool, +} + /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { let args = match Args::try_parse() { @@ -43,7 +124,7 @@ fn cli_init() -> Result { } // doesn't panic match e.kind() { ErrorKind::DisplayHelp => { - let mut cmd = lib_main::Args::command(); + let mut cmd = Args::command(); let help = cmd.render_help(); info!("{help}"); log::set_max_level(LevelFilter::Off); From f1b46285f7596c149f29ba838c020fa02c6e3cf8 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 20:05:37 +0700 Subject: [PATCH 134/154] win-tray: move cli back to main parts needed in gui were duped, so cli doesn't need to be a lib anymore --- src/lib.rs | 1 - src/lib_main.rs | 228 ---------------------------------------------- src/main.rs | 238 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 234 insertions(+), 233 deletions(-) delete mode 100644 src/lib_main.rs diff --git a/src/lib.rs b/src/lib.rs index e5851a22c..d36a3dcb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ use std::str::FromStr; #[path = "gui/[gui].rs"] pub mod gui; pub mod kanata; -pub mod lib_main; pub mod oskbd; pub mod tcp_server; #[cfg(test)] diff --git a/src/lib_main.rs b/src/lib_main.rs deleted file mode 100644 index 52d696018..000000000 --- a/src/lib_main.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::*; -use anyhow::{bail, Result}; -use clap::Parser; -use kanata_parser::cfg; -use log::info; -use simplelog::{format_description, *}; -use std::path::PathBuf; - -#[derive(Parser, Debug)] -#[command(author, version, verbatim_doc_comment)] -/// kanata: an advanced software key remapper -/// -/// kanata remaps key presses to other keys or complex actions depending on the -/// configuration for that key. You can find the guide for creating a config -/// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc -/// -/// If you need help, please feel welcome to create an issue or discussion in -/// the kanata repository: https://github.com/jtroo/kanata -struct Args { - // Display different platform specific paths based on the target OS - #[cfg_attr( - target_os = "windows", - doc = r"Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" - )] - #[cfg_attr( - target_os = "macos", - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$HOME/Library/Application Support/kanata/kanata.kbd.'" - )] - #[cfg_attr( - not(any(target_os = "macos", target_os = "windows")), - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$XDG_CONFIG_HOME/kanata/kanata.kbd'" - )] - #[arg(short, long, verbatim_doc_comment)] - cfg: Option>, - - /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be - /// listened on. - #[cfg(feature = "tcp_server")] - #[arg( - short = 'p', - long = "port", - value_name = "PORT or IP:PORT", - verbatim_doc_comment - )] - tcp_server_address: Option, - /// Path for the symlink pointing to the newly-created device. If blank, no - /// symlink will be created. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - symlink_path: Option, - - /// List the keyboards available for grabbing and exit. - #[cfg(target_os = "macos")] - #[arg(short, long)] - list: bool, - - /// Enable debug logging. - #[arg(short, long)] - debug: bool, - - /// Enable trace logging; implies --debug as well. - #[arg(short, long)] - trace: bool, - - /// Remove the startup delay on kanata. - /// In some cases, removing the delay may cause keyboard issues on startup. - #[arg(short, long, verbatim_doc_comment)] - nodelay: bool, - - /// Milliseconds to wait before attempting to register a newly connected - /// device. The default is 200. - /// - /// You may wish to increase this if you have a device that is failing - /// to register - the device may be taking too long to become ready. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - wait_device_ms: Option, - - /// Validate configuration file and exit - #[arg(long, verbatim_doc_comment)] - check: bool, -} - -/// Parse CLI arguments and initialize logging. -fn cli_init() -> Result { - let args = Args::parse(); - - #[cfg(target_os = "macos")] - if args.list { - karabiner_driverkit::list_keyboards(); - std::process::exit(0); - } - - let cfg_paths = args.cfg.unwrap_or_else(default_cfg); - - let log_lvl = match (args.debug, args.trace) { - (_, true) => LevelFilter::Trace, - (true, false) => LevelFilter::Debug, - (false, false) => LevelFilter::Info, - }; - - let mut log_cfg = ConfigBuilder::new(); - if let Err(e) = log_cfg.set_time_offset_to_local() { - eprintln!("WARNING: could not set log TZ to local: {e:?}"); - }; - log_cfg.set_time_format_custom(format_description!( - version = 2, - "[hour]:[minute]:[second].[subsecond digits:4]" - )); - CombinedLogger::init(vec![TermLogger::new( - log_lvl, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - )]) - .expect("logger can init"); - - log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); - #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] - log::info!("using LLHOOK+SendInput for keyboard IO"); - #[cfg(all(feature = "interception_driver", target_os = "windows"))] - log::info!("using the Interception driver for keyboard IO"); - - if let Some(config_file) = cfg_paths.first() { - if !config_file.exists() { - bail!( - "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", - cfg_paths[0].to_str().unwrap_or("?") - ) - } - } else { - bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); - } - - if args.check { - log::info!("validating config only and exiting"); - let status = match cfg::new_from_file(&cfg_paths[0]) { - Ok(_) => 0, - Err(e) => { - log::error!("{e:?}"); - 1 - } - }; - std::process::exit(status); - } - - #[cfg(target_os = "linux")] - if let Some(wait) = args.wait_device_ms { - use std::sync::atomic::Ordering; - log::info!("Setting device registration wait time to {wait} ms."); - oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); - } - - Ok(ValidatedArgs { - paths: cfg_paths, - #[cfg(feature = "tcp_server")] - tcp_server_address: args.tcp_server_address, - #[cfg(target_os = "linux")] - symlink_path: args.symlink_path, - nodelay: args.nodelay, - }) -} - -fn main_impl() -> Result<()> { - let args = cli_init()?; - let kanata_arc = Kanata::new_arc(&args)?; - - if !args.nodelay { - info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); - std::thread::sleep(std::time::Duration::from_secs(2)); - } - - // Start a processing loop in another thread and run the event loop in this thread. - // - // The reason for two different event loops is that the "event loop" only listens for keyboard - // events, which it sends to the "processing loop". The processing loop handles keyboard events - // while also maintaining `tick()` calls to keyberon. - - let (tx, rx) = std::sync::mpsc::sync_channel(100); - - let (server, ntx, nrx) = if let Some(address) = { - #[cfg(feature = "tcp_server")] - { - args.tcp_server_address - } - #[cfg(not(feature = "tcp_server"))] - { - None:: - } - } { - let mut server = TcpServer::new(address.into_inner(), tx.clone()); - server.start(kanata_arc.clone()); - let (ntx, nrx) = std::sync::mpsc::sync_channel(100); - (Some(server), Some(ntx), Some(nrx)) - } else { - (None, None, None) - }; - - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } - - #[cfg(target_os = "linux")] - sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - Kanata::event_loop(kanata_arc, tx)?; - - Ok(()) -} -pub fn lib_main_cli() -> Result<()> { - let ret = main_impl(); - if let Err(ref e) = ret { - log::error!("{e}\n"); - } - eprintln!("\nPress enter to exit"); - let _ = std::io::stdin().read_line(&mut String::new()); - ret -} diff --git a/src/main.rs b/src/main.rs index 74ed03c19..000fcdf2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,245 @@ #![cfg_attr(feature = "gui", windows_subsystem = "windows")] //disable console on Windows +#[cfg(not(feature = "gui"))] +use anyhow::{bail, Result}; +#[cfg(not(feature = "gui"))] +use clap::Parser; +#[cfg(not(feature = "gui"))] +use kanata_parser::cfg; #[cfg(feature = "gui")] use kanata_state_machine::gui::lib_main_gui; #[cfg(not(feature = "gui"))] -use kanata_state_machine::lib_main::lib_main_cli; +use kanata_state_machine::*; +#[cfg(not(feature = "gui"))] +use log::info; +#[cfg(not(feature = "gui"))] +use simplelog::{format_description, *}; +#[cfg(not(feature = "gui"))] +use std::path::PathBuf; + +#[cfg(not(feature = "gui"))] +#[derive(Parser, Debug)] +#[command(author, version, verbatim_doc_comment)] +/// kanata: an advanced software key remapper +/// +/// kanata remaps key presses to other keys or complex actions depending on the +/// configuration for that key. You can find the guide for creating a config +/// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc +/// +/// If you need help, please feel welcome to create an issue or discussion in +/// the kanata repository: https://github.com/jtroo/kanata +struct Args { + // Display different platform specific paths based on the target OS + #[cfg_attr( + target_os = "windows", + doc = r"Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" + )] + #[cfg_attr( + target_os = "macos", + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$HOME/Library/Application Support/kanata/kanata.kbd.'" + )] + #[cfg_attr( + not(any(target_os = "macos", target_os = "windows")), + doc = "Configuration file(s) to use with kanata. If not specified, defaults to +kanata.kbd in the current working directory and +'$XDG_CONFIG_HOME/kanata/kanata.kbd'" + )] + #[arg(short, long, verbatim_doc_comment)] + cfg: Option>, + + /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be + /// listened on. + #[cfg(feature = "tcp_server")] + #[arg( + short = 'p', + long = "port", + value_name = "PORT or IP:PORT", + verbatim_doc_comment + )] + tcp_server_address: Option, + /// Path for the symlink pointing to the newly-created device. If blank, no + /// symlink will be created. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + symlink_path: Option, + + /// List the keyboards available for grabbing and exit. + #[cfg(target_os = "macos")] + #[arg(short, long)] + list: bool, + + /// Enable debug logging. + #[arg(short, long)] + debug: bool, + + /// Enable trace logging; implies --debug as well. + #[arg(short, long)] + trace: bool, + + /// Remove the startup delay on kanata. + /// In some cases, removing the delay may cause keyboard issues on startup. + #[arg(short, long, verbatim_doc_comment)] + nodelay: bool, + + /// Milliseconds to wait before attempting to register a newly connected + /// device. The default is 200. + /// + /// You may wish to increase this if you have a device that is failing + /// to register - the device may be taking too long to become ready. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + wait_device_ms: Option, + + /// Validate configuration file and exit + #[arg(long, verbatim_doc_comment)] + check: bool, +} + +#[cfg(not(feature = "gui"))] +/// Parse CLI arguments and initialize logging. +fn cli_init() -> Result { + let args = Args::parse(); + + #[cfg(target_os = "macos")] + if args.list { + karabiner_driverkit::list_keyboards(); + std::process::exit(0); + } + + let cfg_paths = args.cfg.unwrap_or_else(default_cfg); + + let log_lvl = match (args.debug, args.trace) { + (_, true) => LevelFilter::Trace, + (true, false) => LevelFilter::Debug, + (false, false) => LevelFilter::Info, + }; + + let mut log_cfg = ConfigBuilder::new(); + if let Err(e) = log_cfg.set_time_offset_to_local() { + eprintln!("WARNING: could not set log TZ to local: {e:?}"); + }; + log_cfg.set_time_format_custom(format_description!( + version = 2, + "[hour]:[minute]:[second].[subsecond digits:4]" + )); + CombinedLogger::init(vec![TermLogger::new( + log_lvl, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + )]) + .expect("logger can init"); + + log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); + #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] + log::info!("using LLHOOK+SendInput for keyboard IO"); + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + log::info!("using the Interception driver for keyboard IO"); + + if let Some(config_file) = cfg_paths.first() { + if !config_file.exists() { + bail!( + "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", + cfg_paths[0].to_str().unwrap_or("?") + ) + } + } else { + bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); + } + + if args.check { + log::info!("validating config only and exiting"); + let status = match cfg::new_from_file(&cfg_paths[0]) { + Ok(_) => 0, + Err(e) => { + log::error!("{e:?}"); + 1 + } + }; + std::process::exit(status); + } + + #[cfg(target_os = "linux")] + if let Some(wait) = args.wait_device_ms { + use std::sync::atomic::Ordering; + log::info!("Setting device registration wait time to {wait} ms."); + oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); + } + + Ok(ValidatedArgs { + paths: cfg_paths, + #[cfg(feature = "tcp_server")] + tcp_server_address: args.tcp_server_address, + #[cfg(target_os = "linux")] + symlink_path: args.symlink_path, + nodelay: args.nodelay, + }) +} #[cfg(not(feature = "gui"))] -use anyhow::Result; +fn main_impl() -> Result<()> { + let args = cli_init()?; + let kanata_arc = Kanata::new_arc(&args)?; + + if !args.nodelay { + info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); + std::thread::sleep(std::time::Duration::from_secs(2)); + } + + // Start a processing loop in another thread and run the event loop in this thread. + // + // The reason for two different event loops is that the "event loop" only listens for keyboard + // events, which it sends to the "processing loop". The processing loop handles keyboard events + // while also maintaining `tick()` calls to keyberon. + + let (tx, rx) = std::sync::mpsc::sync_channel(100); + + let (server, ntx, nrx) = if let Some(address) = { + #[cfg(feature = "tcp_server")] + { + args.tcp_server_address + } + #[cfg(not(feature = "tcp_server"))] + { + None:: + } + } { + let mut server = TcpServer::new(address.into_inner(), tx.clone()); + server.start(kanata_arc.clone()); + let (ntx, nrx) = std::sync::mpsc::sync_channel(100); + (Some(server), Some(ntx), Some(nrx)) + } else { + (None, None, None) + }; + + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); + + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } + + #[cfg(target_os = "linux")] + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; + + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + Kanata::event_loop(kanata_arc, tx)?; + + Ok(()) +} #[cfg(not(feature = "gui"))] -fn main() -> Result<()> { - lib_main_cli() +pub fn main() -> Result<()> { + let ret = main_impl(); + if let Err(ref e) = ret { + log::error!("{e}\n"); + } + eprintln!("\nPress enter to exit"); + let _ = std::io::stdin().read_line(&mut String::new()); + ret } #[cfg(feature = "gui")] From 8079198a240cd27b4ce9350e5731f16114350688 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 20:40:57 +0700 Subject: [PATCH 135/154] dep: update lock --- Cargo.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bcdef40b..e8d890312 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,7 +501,6 @@ dependencies = [ "kanata-parser", "kanata-tcp-protocol", "karabiner-driverkit", - "lazy_static", "log", "miette", "mio", @@ -1476,8 +1475,8 @@ dependencies = [ name = "win_dbg_logger" version = "0.1.0" dependencies = [ - "lazy_static", "log", + "once_cell", "regex", "simplelog", "winapi", From d91b97fcd62a512125117b6b1ac0dd3b07d42fa0 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 20:41:07 +0700 Subject: [PATCH 136/154] dep: remove lazy static --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96156b74a..1c3590ced 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,7 +96,6 @@ windows-sys = { version = "0.52.0", features = [ 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 } -lazy_static = { version = "1.4.0", optional = true } regex = { version = "1.10.4", optional = true } kanata-interception = { version = "0.2.0", optional = true } @@ -116,7 +115,7 @@ cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] wasm = [ "instant/wasm-bindgen" ] -gui = ["win_manifest","native-windows-derive","lazy_static","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","dep:windows-sys"] +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","dep:windows-sys"] [profile.release] opt-level = "z" From 81331dfbaee27099711eb4a06bba67ba1453d6c9 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 20:44:26 +0700 Subject: [PATCH 137/154] windbg: replace lazy static with std and a macro --- win_dbg_logger/Cargo.toml | 9 ++++----- win_dbg_logger/src/lib.rs | 13 ++++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/win_dbg_logger/Cargo.toml b/win_dbg_logger/Cargo.toml index 48cb9c6d9..8e9533653 100644 --- a/win_dbg_logger/Cargo.toml +++ b/win_dbg_logger/Cargo.toml @@ -10,11 +10,10 @@ description = "A logger for use with Windows debuggers." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log = "0.4.*" -lazy_static = {version="1.4.0" } -winapi = {version="0.3.9", features=["processthreadsapi",]} -regex = {version="1.10.4"} -simplelog = {version="0.12.0", optional=true} +log = "0.4.*" +winapi = {version="0.3.9", features=["processthreadsapi",]} +regex = {version="1.10.4"} +simplelog = {version="0.12.0", optional=true} [features] simple_shared = ["simplelog"] diff --git a/win_dbg_logger/src/lib.rs b/win_dbg_logger/src/lib.rs index ab277f082..c5a8b6220 100644 --- a/win_dbg_logger/src/lib.rs +++ b/win_dbg_logger/src/lib.rs @@ -129,16 +129,19 @@ pub fn set_thread_state(is: bool) -> &'static bool { CELL.get_or_init(|| is) } -use lazy_static::lazy_static; use regex::Regex; -lazy_static! { // shorten source file name, no src/ no .rs ext - static ref reExt:Regex = Regex::new(r"\..*$" ).unwrap(); - static ref reSrc:Regex = Regex::new(r"src[\\/]").unwrap(); +macro_rules! regex { + ($re:literal $(,)?) => {{ + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| regex::Regex::new($re).unwrap()) + }}; } fn clean_name(path: Option<&str>) -> String { + let re_ext: &Regex = regex!(r"\..*$"); // shorten source file name, no src/ no .rs ext + let re_src: &Regex = regex!(r"src[\\/]"); // remove extension and src paths if let Some(p) = path { - reSrc.replace(&reExt.replace(p, ""), "").to_string() + re_src.replace(&re_ext.replace(p, ""), "").to_string() } else { "?".to_string() } From 3314c55dd2cd2ab0e6894c3325c74bba8e21d4b7 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 20:44:30 +0700 Subject: [PATCH 138/154] dep: update lock --- Cargo.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e8d890312..7314b7ed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,7 +1476,6 @@ name = "win_dbg_logger" version = "0.1.0" dependencies = [ "log", - "once_cell", "regex", "simplelog", "winapi", From 430330c73e3313402c87aafe365e7276fa01adf5 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 21:10:15 +0700 Subject: [PATCH 139/154] refactor(parser): extract parse_layer_opts into a separate function --- parser/src/cfg/layer_opts.rs | 34 ++++++++++++++++++++++++++++++ parser/src/cfg/mod.rs | 41 +++++++----------------------------- 2 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 parser/src/cfg/layer_opts.rs diff --git a/parser/src/cfg/layer_opts.rs b/parser/src/cfg/layer_opts.rs new file mode 100644 index 000000000..69676171a --- /dev/null +++ b/parser/src/cfg/layer_opts.rs @@ -0,0 +1,34 @@ +use crate::cfg::*; +use crate::*; +use std::collections::HashMap; +pub fn parse_layer_opts<'a>( + mut list: impl Iterator, +) -> Result> { + let mut layer_opts: HashMap = HashMap::default(); + while let Some(key_expr) = list.next() { + // Read k-v pairs from the configuration + // todo: add hashmap for future options, currently only parse icons + let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")}).and_then(|opt_key| { + if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { + if layer_opts.contains_key(DEFLAYER_ICON[0]) { + bail_expr!(key_expr,"Duplicate option found in {DEFLAYER}: {opt_key}, one of {DEFLAYER_ICON:?} already exists");} + // separate dupe check since multi-keys are stored with one "canonical" repr, so '🖻' → 'icon' + // and this info will be lost after the loop + Ok(DEFLAYER_ICON[0]) + } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} + })?; + if layer_opts.contains_key(opt_key) { + bail_expr!(key_expr, "Duplicate option found in {DEFLAYER}: {opt_key}"); + } + let opt_val = match list.next() { + Some(v) => v.atom(None).ok_or_else(|| { + anyhow_expr!(v, "No lists are allowed in {DEFLAYER}'s option values") + })?, + None => { + bail_expr!(key_expr, "Option without a value in {DEFLAYER}") + } + }; + layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); + } + Ok(layer_opts) +} diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 6076cd194..68c669c8f 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -50,6 +50,9 @@ pub use key_override::*; mod custom_tap_hold; use custom_tap_hold::*; +pub mod layer_opts; +use layer_opts::*; + pub mod list_actions; use list_actions::*; @@ -1070,39 +1073,11 @@ fn parse_layer_indexes( let mut list = listt.iter(); let name = list.next().ok_or_else(|| {anyhow_span!(name_opts_span,"deflayer requires a string name within this pair of parenthesis (or a string name without any)")})? .atom(None).ok_or_else(|| {anyhow_expr!(layer_expr, "layer name after {DEFLAYER} must be a string when enclosed within one pair of parentheses")})?; - let mut icon = ""; - //todo: add hashmap for future options, currently only parse icons - let mut layer_opts: HashMap = HashMap::default(); - while let Some(key_expr) = list.next() { - // Read k-v pairs from the configuration - let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")}).and_then(|opt_key| { - if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { - if layer_opts.contains_key(DEFLAYER_ICON[0]) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER}: {opt_key}");} - Ok(DEFLAYER_ICON[0]) - } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} - })?; - if layer_opts.contains_key(opt_key) { - bail_expr!(key_expr, "Duplicate option found in {DEFLAYER}: {opt_key}"); - } - let opt_val = match list.next() { - Some(v) => v - .atom(None) - .ok_or_else(|| { - anyhow_expr!( - v, - "No lists are allowed in {DEFLAYER}'s option values" - ) - }) - .map(|opt_val| { - icon = opt_val.trim_matches('"'); - opt_val - })?, - None => { - bail_expr!(key_expr, "Option without a value in {DEFLAYER} {name}") - } - }; - layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); - } + let layer_opts = parse_layer_opts(list)?; + let icon = layer_opts + .get(DEFLAYER_ICON[0]) + .map(|icon_s| icon_s.trim_matches('"')) + .unwrap_or(""); (name.to_owned(), Some(icon.to_owned())) } }, From 9a386880b49f323fcbe49ad73e9a5d53ac10be00 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 21:44:24 +0700 Subject: [PATCH 140/154] test: icon layer opts --- parser/src/cfg/tests.rs | 14 ++++++++++++++ parser/test_cfgs/icon_bad_dupe.kbd | 4 ++++ parser/test_cfgs/icon_good.kbd | 6 ++++++ 3 files changed, 24 insertions(+) create mode 100644 parser/test_cfgs/icon_bad_dupe.kbd create mode 100644 parser/test_cfgs/icon_good.kbd diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index f58a1f552..e0e7d1a1f 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1846,3 +1846,17 @@ fn parse_defseq_overlap_too_many() { .map_err(|e| eprintln!("{:?}", miette::Error::from(e))) .expect_err("fails"); } + +#[test] +fn parse_layer_opts_icon() { + let _lk = lock(&CFG_PARSE_LOCK); + new_from_file(&std::path::PathBuf::from("./test_cfgs/icon_good.kbd")).unwrap(); +} + +#[test] +fn disallow_dupe_layer_opts_icon() { + let _lk = lock(&CFG_PARSE_LOCK); + new_from_file(&std::path::PathBuf::from("./test_cfgs/icon_bad_dupe.kbd")) + .map(|_| ()) + .expect_err("fails"); +} diff --git a/parser/test_cfgs/icon_bad_dupe.kbd b/parser/test_cfgs/icon_bad_dupe.kbd new file mode 100644 index 000000000..0fa0c83cc --- /dev/null +++ b/parser/test_cfgs/icon_bad_dupe.kbd @@ -0,0 +1,4 @@ +;; This config file is invalid and should be rejected +(defcfg) +(defsrc 1) +(deflayer (base icon base.png 🖻 n.ico ) 1) diff --git a/parser/test_cfgs/icon_good.kbd b/parser/test_cfgs/icon_good.kbd new file mode 100644 index 000000000..d1c3ebe3c --- /dev/null +++ b/parser/test_cfgs/icon_good.kbd @@ -0,0 +1,6 @@ +(defcfg) +(defsrc 1) +(deflayer (base icon base.png ) 1) +(deflayer (1emoji 🖻 1symbols.png ) 1) +(deflayer (2icon-quote 🖻 "2Nav Num.png" ) 1) +(deflayer (3emoji_alt 🖼 3trans.parent ) 1) From 602c76c32df1cf54ae1e70a8f3f9260c08a26f2b Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Sun, 5 May 2024 23:16:27 +0700 Subject: [PATCH 141/154] doc(win-tray): add screenshot/video --- docs/config.adoc | 8 ++++++++ docs/win-tray/win-tray-layer-change.gif | Bin 0 -> 5571 bytes docs/win-tray/win-tray-screen.png | Bin 0 -> 14019 bytes 3 files changed, 8 insertions(+) create mode 100644 docs/win-tray/win-tray-layer-change.gif create mode 100644 docs/win-tray/win-tray-screen.png diff --git a/docs/config.adoc b/docs/config.adoc index 011703610..70a101a3e 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3449,6 +3449,14 @@ cargo build --win_manifest Kanata can be compiled as a Windows GUI tray app with the feature flag `gui`. This can simplify launching the app on user login by placing a `.lnk` at `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, show custom icon indicator per config + + +image:https://github.com/jtroo/kanata/blob/main/docs/win-tray/win-tray-screen.png[icon indicator per config,477,129] + +as well as dynamic icon indicator per layer (might need to click on the gif below to play) + +image:https://github.com/jtroo/kanata/blob/main/docs/win-tray/win-tray-layer-change.gif[icon indicator per layer,33,35,opts=autoplay] + (see <>). It also supports (re)loading configs. Currently the only configuration supported is tray icon per profile, all other configuration should diff --git a/docs/win-tray/win-tray-layer-change.gif b/docs/win-tray/win-tray-layer-change.gif new file mode 100644 index 0000000000000000000000000000000000000000..62b1f4a5d6dbfb37e53edc618e29609481e809b7 GIT binary patch literal 5571 zcmeH~X;72r7RM8UkO+h&tQA`@R2C7?Vhgfwf+7$QK`KgFr;r#NC7uQ35t*e2mzCj^?4e+o$Hrg=XU1KZSP#qb3VN9oSA3locH{H z=e+J)w^&(+=_BxnC5TwIdjN?<%1G@VM4?b}a&q!g8-NM}P-PIRVpVZ!>gt0~1E;B_ zuBD@?vr1~iP*+#?!(phm`olFJ;SGktTD*Y~-q6HoowYYn{Sl0-MDe%X8DcI zOKqKt;0_1&)ZOmmg8rJG`*rt*dGPTPeA4sq`M~Ie0K5{w>FH_u$Q1qY6tin;W{@%S zlsWr=!5W!k(Pr65$VLM;7O-((!4NEruolE%aW!1T!^Kx@&PK?w1{^!U*#bFUfJ=}% zcrzmq0-p6kG&?QLcc=5eP-X1}N;~3J1BuIlkx`6!kBO zdw{r?D;@^o=X@~@hyfHsAi2z!^est-mLyMvl4&4e0)S=$!aN9IgG3G} zhSL&A7eS^N&Pd=aAI?hPoCLB&a9#`-rG*0!LIgzULj`X){~az)1h?fxXF|N&T+yAOg5P%jF`+XlLuMs9 z;;vI4NlVYtKZ%xvOqEwLjn>|P%G6L-l#gH-Pze=wiR=-oONMuu?NJ%)>z2&yQ_qc#oGgmF?> zs=Lj)j=dYy)O-WYXMYSBKYCA8|6_FFtP%t+ZjHc$3d(J%l6JP?1tT33rW#3$Pl>!R@qThr8rjCCx2*s$sm6`)xb;t;!A} z8u@-2$6x-m4XeG<2ZuTEwZdjbLwBAT`!>1U=K7W4Jd>-m!>LJ!3kkW%dOJ9EvyaDB(3cSDS-!I}{Un>+s4xueQ2s8tM zddmPs60Mi4E3?z8syQZ9{sbsPkG*K}PtB4E2~}LvoanLZD;_Hf#Ef7B* z{zx0`PTD6(Il~0Mj?xErW;H?+QW#+?o$wFLsLyh|Tt01B)e%CP7-|bl$!JXQyQbrL zurbIomV$@D`mU=xMjAtVMd^yG+6T8WBw0?bA$HWK!QahnY8q_~N_KB4JU8hnT~a_xsazF>*JLr<5B1cRrg|PfMJDpA(#fBCN7%(bA>gsZHu0I; z>Q{dm&NdgZFDu&%`0mOv?5i&aY=oBiKd+}(UB=p6@5G&sXVHheZR%(#3K`5Xl$(^s z_)6>PyYU0LR?n^Qx$Q*#Ex9&w#*;bI?cHuVj#%ns&b5;*cg_&{PUY$R6U2qYxZgos z9C;&xMlxd}(9(2tECXti$7CpB-iJaW+9w!EacdjFl-2zUG1-wF$^BtWfruH%q&oC||lTEXN1B)Ym z?v{Znm$c(AhNX}kgX3nETRZKmEa%f0K@?w7<@!QR{tmOf59Cy@g#=qv+;Wh|ITge= z|MXQd68Ycd!Fv(E7xBAg|1Wp0ck|$FW1U1m2@3I#>7ZuoF({k8yw0l=2zpM4BjU)h zyZQTTWE3>^&3YVu*^{Q_UTBc8@na{Wo%;gxyYq8?DOL|*7IgWh50slF9O7WE)Dm)C zvwutE(O?eN1?2DZ>K!qDcxktJijqQT^sdXhe?4A(v~cbrS`|-HTBY*t u9N(SFAHne5Re77d^sirn`_GTYiShTN@ozL5-`x#=s(=2{tNu_2HopOa6rytg literal 0 HcmV?d00001 diff --git a/docs/win-tray/win-tray-screen.png b/docs/win-tray/win-tray-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7c5aa6730a5777ee97cd7812fc419d9d3f3055 GIT binary patch literal 14019 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 literal 0 HcmV?d00001 From 1f4fd41523c17d037e2e76e5e8c0b511e1991442 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 14:42:50 -0700 Subject: [PATCH 142/154] move/rename files to match conventions better --- src/gui/{[gui].rs => mod.rs} | 0 src/gui/{win_nwg_ext-license => win_nwg_ext/license-MIT} | 0 src/gui/{win_nwg_ext.rs => win_nwg_ext/mod.rs} | 0 src/lib.rs | 1 - 4 files changed, 1 deletion(-) rename src/gui/{[gui].rs => mod.rs} (100%) rename src/gui/{win_nwg_ext-license => win_nwg_ext/license-MIT} (100%) rename src/gui/{win_nwg_ext.rs => win_nwg_ext/mod.rs} (100%) diff --git a/src/gui/[gui].rs b/src/gui/mod.rs similarity index 100% rename from src/gui/[gui].rs rename to src/gui/mod.rs diff --git a/src/gui/win_nwg_ext-license b/src/gui/win_nwg_ext/license-MIT similarity index 100% rename from src/gui/win_nwg_ext-license rename to src/gui/win_nwg_ext/license-MIT diff --git a/src/gui/win_nwg_ext.rs b/src/gui/win_nwg_ext/mod.rs similarity index 100% rename from src/gui/win_nwg_ext.rs rename to src/gui/win_nwg_ext/mod.rs diff --git a/src/lib.rs b/src/lib.rs index d36a3dcb2..5473d6436 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,6 @@ use std::path::PathBuf; use std::str::FromStr; #[cfg(all(target_os = "windows", feature = "gui"))] -#[path = "gui/[gui].rs"] pub mod gui; pub mod kanata; pub mod oskbd; From 6e76660c415b6202ae370df2298ed7642d691837 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 14:57:31 -0700 Subject: [PATCH 143/154] reduce cfg usage in main.rs --- src/main.rs | 403 ++++++++++++++++++++++++++-------------------------- 1 file changed, 198 insertions(+), 205 deletions(-) diff --git a/src/main.rs b/src/main.rs index 000fcdf2f..1dbda5972 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,236 +1,228 @@ -#![cfg_attr(feature = "gui", windows_subsystem = "windows")] //disable console on Windows - -#[cfg(not(feature = "gui"))] -use anyhow::{bail, Result}; -#[cfg(not(feature = "gui"))] -use clap::Parser; -#[cfg(not(feature = "gui"))] -use kanata_parser::cfg; -#[cfg(feature = "gui")] -use kanata_state_machine::gui::lib_main_gui; -#[cfg(not(feature = "gui"))] -use kanata_state_machine::*; -#[cfg(not(feature = "gui"))] -use log::info; #[cfg(not(feature = "gui"))] -use simplelog::{format_description, *}; -#[cfg(not(feature = "gui"))] -use std::path::PathBuf; - -#[cfg(not(feature = "gui"))] -#[derive(Parser, Debug)] -#[command(author, version, verbatim_doc_comment)] -/// kanata: an advanced software key remapper -/// -/// kanata remaps key presses to other keys or complex actions depending on the -/// configuration for that key. You can find the guide for creating a config -/// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc -/// -/// If you need help, please feel welcome to create an issue or discussion in -/// the kanata repository: https://github.com/jtroo/kanata -struct Args { - // Display different platform specific paths based on the target OS - #[cfg_attr( - target_os = "windows", - doc = r"Configuration file(s) to use with kanata. If not specified, defaults to +mod cli { + pub(crate) use anyhow::{bail, Result}; + pub(crate) use clap::Parser; + pub(crate) use kanata_parser::cfg; + pub(crate) use kanata_state_machine::*; + pub(crate) use log::info; + pub(crate) use simplelog::{format_description, *}; + pub(crate) use std::path::PathBuf; + + #[derive(Parser, Debug)] + #[command(author, version, verbatim_doc_comment)] + /// kanata: an advanced software key remapper + /// + /// kanata remaps key presses to other keys or complex actions depending on the + /// configuration for that key. You can find the guide for creating a config + /// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc + /// + /// If you need help, please feel welcome to create an issue or discussion in + /// the kanata repository: https://github.com/jtroo/kanata + struct Args { + // Display different platform specific paths based on the target OS + #[cfg_attr( + target_os = "windows", + doc = r"Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and 'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" - )] - #[cfg_attr( - target_os = "macos", - doc = "Configuration file(s) to use with kanata. If not specified, defaults to + )] + #[cfg_attr( + target_os = "macos", + doc = "Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and '$HOME/Library/Application Support/kanata/kanata.kbd.'" - )] - #[cfg_attr( - not(any(target_os = "macos", target_os = "windows")), - doc = "Configuration file(s) to use with kanata. If not specified, defaults to + )] + #[cfg_attr( + not(any(target_os = "macos", target_os = "windows")), + doc = "Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and '$XDG_CONFIG_HOME/kanata/kanata.kbd'" - )] - #[arg(short, long, verbatim_doc_comment)] - cfg: Option>, - - /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be - /// listened on. - #[cfg(feature = "tcp_server")] - #[arg( - short = 'p', - long = "port", - value_name = "PORT or IP:PORT", - verbatim_doc_comment - )] - tcp_server_address: Option, - /// Path for the symlink pointing to the newly-created device. If blank, no - /// symlink will be created. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - symlink_path: Option, - - /// List the keyboards available for grabbing and exit. - #[cfg(target_os = "macos")] - #[arg(short, long)] - list: bool, - - /// Enable debug logging. - #[arg(short, long)] - debug: bool, - - /// Enable trace logging; implies --debug as well. - #[arg(short, long)] - trace: bool, - - /// Remove the startup delay on kanata. - /// In some cases, removing the delay may cause keyboard issues on startup. - #[arg(short, long, verbatim_doc_comment)] - nodelay: bool, - - /// Milliseconds to wait before attempting to register a newly connected - /// device. The default is 200. - /// - /// You may wish to increase this if you have a device that is failing - /// to register - the device may be taking too long to become ready. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - wait_device_ms: Option, - - /// Validate configuration file and exit - #[arg(long, verbatim_doc_comment)] - check: bool, -} + )] + #[arg(short, long, verbatim_doc_comment)] + cfg: Option>, -#[cfg(not(feature = "gui"))] -/// Parse CLI arguments and initialize logging. -fn cli_init() -> Result { - let args = Args::parse(); - - #[cfg(target_os = "macos")] - if args.list { - karabiner_driverkit::list_keyboards(); - std::process::exit(0); + /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be + /// listened on. + #[cfg(feature = "tcp_server")] + #[arg( + short = 'p', + long = "port", + value_name = "PORT or IP:PORT", + verbatim_doc_comment + )] + tcp_server_address: Option, + /// Path for the symlink pointing to the newly-created device. If blank, no + /// symlink will be created. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + symlink_path: Option, + + /// List the keyboards available for grabbing and exit. + #[cfg(target_os = "macos")] + #[arg(short, long)] + list: bool, + + /// Enable debug logging. + #[arg(short, long)] + debug: bool, + + /// Enable trace logging; implies --debug as well. + #[arg(short, long)] + trace: bool, + + /// Remove the startup delay on kanata. + /// In some cases, removing the delay may cause keyboard issues on startup. + #[arg(short, long, verbatim_doc_comment)] + nodelay: bool, + + /// Milliseconds to wait before attempting to register a newly connected + /// device. The default is 200. + /// + /// You may wish to increase this if you have a device that is failing + /// to register - the device may be taking too long to become ready. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + wait_device_ms: Option, + + /// Validate configuration file and exit + #[arg(long, verbatim_doc_comment)] + check: bool, } - let cfg_paths = args.cfg.unwrap_or_else(default_cfg); - - let log_lvl = match (args.debug, args.trace) { - (_, true) => LevelFilter::Trace, - (true, false) => LevelFilter::Debug, - (false, false) => LevelFilter::Info, - }; - - let mut log_cfg = ConfigBuilder::new(); - if let Err(e) = log_cfg.set_time_offset_to_local() { - eprintln!("WARNING: could not set log TZ to local: {e:?}"); - }; - log_cfg.set_time_format_custom(format_description!( - version = 2, - "[hour]:[minute]:[second].[subsecond digits:4]" - )); - CombinedLogger::init(vec![TermLogger::new( - log_lvl, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - )]) - .expect("logger can init"); - - log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); - #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] - log::info!("using LLHOOK+SendInput for keyboard IO"); - #[cfg(all(feature = "interception_driver", target_os = "windows"))] - log::info!("using the Interception driver for keyboard IO"); - - if let Some(config_file) = cfg_paths.first() { - if !config_file.exists() { - bail!( + /// Parse CLI arguments and initialize logging. + fn cli_init() -> Result { + let args = Args::parse(); + + #[cfg(target_os = "macos")] + if args.list { + karabiner_driverkit::list_keyboards(); + std::process::exit(0); + } + + let cfg_paths = args.cfg.unwrap_or_else(default_cfg); + + let log_lvl = match (args.debug, args.trace) { + (_, true) => LevelFilter::Trace, + (true, false) => LevelFilter::Debug, + (false, false) => LevelFilter::Info, + }; + + let mut log_cfg = ConfigBuilder::new(); + if let Err(e) = log_cfg.set_time_offset_to_local() { + eprintln!("WARNING: could not set log TZ to local: {e:?}"); + }; + log_cfg.set_time_format_custom(format_description!( + version = 2, + "[hour]:[minute]:[second].[subsecond digits:4]" + )); + CombinedLogger::init(vec![TermLogger::new( + log_lvl, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + )]) + .expect("logger can init"); + + log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); + #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] + log::info!("using LLHOOK+SendInput for keyboard IO"); + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + log::info!("using the Interception driver for keyboard IO"); + + if let Some(config_file) = cfg_paths.first() { + if !config_file.exists() { + bail!( "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", cfg_paths[0].to_str().unwrap_or("?") ) + } + } else { + bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); } - } else { - bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); - } - if args.check { - log::info!("validating config only and exiting"); - let status = match cfg::new_from_file(&cfg_paths[0]) { - Ok(_) => 0, - Err(e) => { - log::error!("{e:?}"); - 1 - } - }; - std::process::exit(status); - } + if args.check { + log::info!("validating config only and exiting"); + let status = match cfg::new_from_file(&cfg_paths[0]) { + Ok(_) => 0, + Err(e) => { + log::error!("{e:?}"); + 1 + } + }; + std::process::exit(status); + } - #[cfg(target_os = "linux")] - if let Some(wait) = args.wait_device_ms { - use std::sync::atomic::Ordering; - log::info!("Setting device registration wait time to {wait} ms."); - oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); + #[cfg(target_os = "linux")] + if let Some(wait) = args.wait_device_ms { + use std::sync::atomic::Ordering; + log::info!("Setting device registration wait time to {wait} ms."); + oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); + } + + Ok(ValidatedArgs { + paths: cfg_paths, + #[cfg(feature = "tcp_server")] + tcp_server_address: args.tcp_server_address, + #[cfg(target_os = "linux")] + symlink_path: args.symlink_path, + nodelay: args.nodelay, + }) } - Ok(ValidatedArgs { - paths: cfg_paths, - #[cfg(feature = "tcp_server")] - tcp_server_address: args.tcp_server_address, - #[cfg(target_os = "linux")] - symlink_path: args.symlink_path, - nodelay: args.nodelay, - }) -} + pub(crate) fn main_impl() -> Result<()> { + let args = cli_init()?; + let kanata_arc = Kanata::new_arc(&args)?; -#[cfg(not(feature = "gui"))] -fn main_impl() -> Result<()> { - let args = cli_init()?; - let kanata_arc = Kanata::new_arc(&args)?; + if !args.nodelay { + info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); + std::thread::sleep(std::time::Duration::from_secs(2)); + } - if !args.nodelay { - info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); - std::thread::sleep(std::time::Duration::from_secs(2)); - } + // Start a processing loop in another thread and run the event loop in this thread. + // + // The reason for two different event loops is that the "event loop" only listens for keyboard + // events, which it sends to the "processing loop". The processing loop handles keyboard events + // while also maintaining `tick()` calls to keyberon. - // Start a processing loop in another thread and run the event loop in this thread. - // - // The reason for two different event loops is that the "event loop" only listens for keyboard - // events, which it sends to the "processing loop". The processing loop handles keyboard events - // while also maintaining `tick()` calls to keyberon. + let (tx, rx) = std::sync::mpsc::sync_channel(100); - let (tx, rx) = std::sync::mpsc::sync_channel(100); + let (server, ntx, nrx) = if let Some(address) = { + #[cfg(feature = "tcp_server")] + { + args.tcp_server_address + } + #[cfg(not(feature = "tcp_server"))] + { + None:: + } + } { + let mut server = TcpServer::new(address.into_inner(), tx.clone()); + server.start(kanata_arc.clone()); + let (ntx, nrx) = std::sync::mpsc::sync_channel(100); + (Some(server), Some(ntx), Some(nrx)) + } else { + (None, None, None) + }; - let (server, ntx, nrx) = if let Some(address) = { - #[cfg(feature = "tcp_server")] - { - args.tcp_server_address - } - #[cfg(not(feature = "tcp_server"))] - { - None:: + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); + + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); } - } { - let mut server = TcpServer::new(address.into_inner(), tx.clone()); - server.start(kanata_arc.clone()); - let (ntx, nrx) = std::sync::mpsc::sync_channel(100); - (Some(server), Some(ntx), Some(nrx)) - } else { - (None, None, None) - }; - - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } - #[cfg(target_os = "linux")] - sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; + #[cfg(target_os = "linux")] + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; - #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] - Kanata::event_loop(kanata_arc, tx)?; + #[cfg(any(not(target_os = "windows"), not(feature = "gui")))] + Kanata::event_loop(kanata_arc, tx)?; - Ok(()) + Ok(()) + } } + +#[cfg(not(feature = "gui"))] +use cli::*; #[cfg(not(feature = "gui"))] pub fn main() -> Result<()> { let ret = main_impl(); @@ -244,5 +236,6 @@ pub fn main() -> Result<()> { #[cfg(feature = "gui")] fn main() { + use kanata_state_machine::gui::lib_main_gui; lib_main_gui(); } From 7d0bfb9b8a7625233c9ce14e6202f2eeb7129fc9 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 15:47:29 -0700 Subject: [PATCH 144/154] move main code to main --- src/gui/mod.rs | 279 +--------------------------------------- src/main.rs | 173 +++++++++++++------------ src/main_lib/mod.rs | 2 + src/main_lib/win_gui.rs | 176 +++++++++++++++++++++++++ 4 files changed, 268 insertions(+), 362 deletions(-) create mode 100644 src/main_lib/mod.rs create mode 100644 src/main_lib/win_gui.rs diff --git a/src/gui/mod.rs b/src/gui/mod.rs index f22e45ce0..ddf5257f4 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,287 +1,12 @@ -use crate::*; -use anyhow::{bail, Context, Result}; -use clap::Parser; -use clap::{error::ErrorKind, CommandFactory}; -use kanata_parser::cfg; -use simplelog::{format_description, *}; - pub mod win; pub use win::*; pub mod win_nwg_ext; - -pub use win_nwg_ext::*; - pub use win_dbg_logger as log_win; pub use win_dbg_logger::WINDBG_LOGGER; +pub use win_nwg_ext::*; +use crate::*; use parking_lot::Mutex; use std::sync::{Arc, OnceLock}; pub static CFG: OnceLock>> = OnceLock::new(); pub static GUI_TX: OnceLock = OnceLock::new(); - -#[derive(Parser, Debug)] -#[command(author, version, verbatim_doc_comment)] -/// kanata: an advanced software key remapper -/// -/// kanata remaps key presses to other keys or complex actions depending on the -/// configuration for that key. You can find the guide for creating a config -/// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc -/// -/// If you need help, please feel welcome to create an issue or discussion in -/// the kanata repository: https://github.com/jtroo/kanata -struct Args { - // Display different platform specific paths based on the target OS - #[cfg_attr( - target_os = "windows", - doc = r"Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" - )] - #[cfg_attr( - target_os = "macos", - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$HOME/Library/Application Support/kanata/kanata.kbd.'" - )] - #[cfg_attr( - not(any(target_os = "macos", target_os = "windows")), - doc = "Configuration file(s) to use with kanata. If not specified, defaults to -kanata.kbd in the current working directory and -'$XDG_CONFIG_HOME/kanata/kanata.kbd'" - )] - #[arg(short, long, verbatim_doc_comment)] - cfg: Option>, - - /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be - /// listened on. - #[cfg(feature = "tcp_server")] - #[arg( - short = 'p', - long = "port", - value_name = "PORT or IP:PORT", - verbatim_doc_comment - )] - tcp_server_address: Option, - /// Path for the symlink pointing to the newly-created device. If blank, no - /// symlink will be created. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - symlink_path: Option, - - /// List the keyboards available for grabbing and exit. - #[cfg(target_os = "macos")] - #[arg(short, long)] - list: bool, - - /// Enable debug logging. - #[arg(short, long)] - debug: bool, - - /// Enable trace logging; implies --debug as well. - #[arg(short, long)] - trace: bool, - - /// Remove the startup delay on kanata. - /// In some cases, removing the delay may cause keyboard issues on startup. - #[arg(short, long, verbatim_doc_comment)] - nodelay: bool, - - /// Milliseconds to wait before attempting to register a newly connected - /// device. The default is 200. - /// - /// You may wish to increase this if you have a device that is failing - /// to register - the device may be taking too long to become ready. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - wait_device_ms: Option, - - /// Validate configuration file and exit - #[arg(long, verbatim_doc_comment)] - check: bool, -} - -/// Parse CLI arguments and initialize logging. -fn cli_init() -> Result { - let args = match Args::try_parse() { - Ok(args) => args, - Err(e) => { - if *IS_TERM { - // init loggers without config so '-help' "error" or real ones can be printed - let mut log_cfg = ConfigBuilder::new(); - CombinedLogger::init(vec![ - TermLogger::new( - LevelFilter::Debug, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - ), - log_win::windbg_simple_combo(LevelFilter::Debug), - ]) - .expect("logger can init"); - } else { - log_win::init(); - log::set_max_level(LevelFilter::Debug); - } // doesn't panic - match e.kind() { - ErrorKind::DisplayHelp => { - let mut cmd = Args::command(); - let help = cmd.render_help(); - info!("{help}"); - log::set_max_level(LevelFilter::Off); - return Err(anyhow!("")); - } - _ => return Err(e.into()), - } - } - }; - - #[cfg(target_os = "macos")] - if args.list { - karabiner_driverkit::list_keyboards(); - std::process::exit(0); - } - - let cfg_paths = args.cfg.unwrap_or_else(default_cfg); - - let log_lvl = match (args.debug, args.trace) { - (_, true) => LevelFilter::Trace, - (true, false) => LevelFilter::Debug, - (false, false) => LevelFilter::Info, - }; - - let mut log_cfg = ConfigBuilder::new(); - if let Err(e) = log_cfg.set_time_offset_to_local() { - eprintln!("WARNING: could not set log TZ to local: {e:?}"); - }; - log_cfg.set_time_format_custom(format_description!( - version = 2, - "[hour]:[minute]:[second].[subsecond digits:4]" - )); - if *IS_TERM { - CombinedLogger::init(vec![ - TermLogger::new( - log_lvl, - log_cfg.build(), - TerminalMode::Mixed, - ColorChoice::AlwaysAnsi, - ), - log_win::windbg_simple_combo(log_lvl), - ]) - .expect("logger can init"); - } else { - CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl)]).expect("logger can init"); - } - log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); - #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] - log::info!("using LLHOOK+SendInput for keyboard IO"); - #[cfg(all(feature = "interception_driver", target_os = "windows"))] - log::info!("using the Interception driver for keyboard IO"); - - if let Some(config_file) = cfg_paths.first() { - if !config_file.exists() { - bail!( - "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", - cfg_paths[0].to_str().unwrap_or("?") - ) - } - } else { - bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); - } - - if args.check { - log::info!("validating config only and exiting"); - let status = match cfg::new_from_file(&cfg_paths[0]) { - Ok(_) => 0, - Err(e) => { - log::error!("{e:?}"); - 1 - } - }; - std::process::exit(status); - } - - #[cfg(target_os = "linux")] - if let Some(wait) = args.wait_device_ms { - use std::sync::atomic::Ordering; - log::info!("Setting device registration wait time to {wait} ms."); - oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); - } - - Ok(ValidatedArgs { - paths: cfg_paths, - #[cfg(feature = "tcp_server")] - tcp_server_address: args.tcp_server_address, - #[cfg(target_os = "linux")] - symlink_path: args.symlink_path, - nodelay: args.nodelay, - }) -} - -fn main_impl() -> Result<()> { - let args = cli_init()?; - let kanata_arc = Kanata::new_arc(&args)?; - - if CFG.set(kanata_arc.clone()).is_err() { - warn!("Someone else set our ‘CFG’"); - }; // store a clone of cfg so that we can ask it to reset itself - - if !args.nodelay { - info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); - std::thread::sleep(std::time::Duration::from_secs(2)); - } - - // Start a processing loop in another thread and run the event loop in this thread. - // - // The reason for two different event loops is that the "event loop" only listens for keyboard - // events, which it sends to the "processing loop". The processing loop handles keyboard events - // while also maintaining `tick()` calls to keyberon. - - let (tx, rx) = std::sync::mpsc::sync_channel(100); - - let (server, ntx, nrx) = if let Some(address) = { - #[cfg(feature = "tcp_server")] - { - args.tcp_server_address - } - #[cfg(not(feature = "tcp_server"))] - { - None:: - } - } { - let mut server = TcpServer::new(address.into_inner(), tx.clone()); - server.start(kanata_arc.clone()); - let (ntx, nrx) = std::sync::mpsc::sync_channel(100); - (Some(server), Some(ntx), Some(nrx)) - } else { - (None, None, None) - }; - - native_windows_gui::init().context("Failed to init Native Windows GUI")?; - let ui = build_tray(&kanata_arc)?; - let gui_tx = ui.layer_notice.sender(); - if GUI_TX.set(gui_tx).is_err() { - warn!("Someone else set our ‘GUI_TX’"); - }; - Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); - - if let (Some(server), Some(nrx)) = (server, nrx) { - #[allow(clippy::unit_arg)] - Kanata::start_notification_loop(nrx, server.connections); - } - - Kanata::event_loop(kanata_arc, tx, ui)?; - - Ok(()) -} - -pub fn lib_main_gui() { - let _attach_console = *IS_CONSOLE; - let ret = main_impl(); - if let Err(ref e) = ret { - log::error!("{e}\n"); - } - - unsafe { - FreeConsole(); - } -} diff --git a/src/main.rs b/src/main.rs index 1dbda5972..eb75106c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,93 +1,96 @@ -#[cfg(not(feature = "gui"))] -mod cli { - pub(crate) use anyhow::{bail, Result}; - pub(crate) use clap::Parser; - pub(crate) use kanata_parser::cfg; - pub(crate) use kanata_state_machine::*; - pub(crate) use log::info; - pub(crate) use simplelog::{format_description, *}; - pub(crate) use std::path::PathBuf; - - #[derive(Parser, Debug)] - #[command(author, version, verbatim_doc_comment)] - /// kanata: an advanced software key remapper - /// - /// kanata remaps key presses to other keys or complex actions depending on the - /// configuration for that key. You can find the guide for creating a config - /// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc - /// - /// If you need help, please feel welcome to create an issue or discussion in - /// the kanata repository: https://github.com/jtroo/kanata - struct Args { - // Display different platform specific paths based on the target OS - #[cfg_attr( - target_os = "windows", - doc = r"Configuration file(s) to use with kanata. If not specified, defaults to +mod main_lib; + +use anyhow::{bail, Result}; +use clap::Parser; +use kanata_parser::cfg; +use kanata_state_machine::*; +use simplelog::{format_description, *}; +use std::path::PathBuf; + +#[derive(Parser, Debug)] +#[command(author, version, verbatim_doc_comment)] +/// kanata: an advanced software key remapper +/// +/// kanata remaps key presses to other keys or complex actions depending on the +/// configuration for that key. You can find the guide for creating a config +/// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc +/// +/// If you need help, please feel welcome to create an issue or discussion in +/// the kanata repository: https://github.com/jtroo/kanata +struct Args { + // Display different platform specific paths based on the target OS + #[cfg_attr( + target_os = "windows", + doc = r"Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and 'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'" - )] - #[cfg_attr( - target_os = "macos", - doc = "Configuration file(s) to use with kanata. If not specified, defaults to + )] + #[cfg_attr( + target_os = "macos", + doc = "Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and '$HOME/Library/Application Support/kanata/kanata.kbd.'" - )] - #[cfg_attr( - not(any(target_os = "macos", target_os = "windows")), - doc = "Configuration file(s) to use with kanata. If not specified, defaults to + )] + #[cfg_attr( + not(any(target_os = "macos", target_os = "windows")), + doc = "Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and '$XDG_CONFIG_HOME/kanata/kanata.kbd'" - )] - #[arg(short, long, verbatim_doc_comment)] - cfg: Option>, - - /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be - /// listened on. - #[cfg(feature = "tcp_server")] - #[arg( - short = 'p', - long = "port", - value_name = "PORT or IP:PORT", - verbatim_doc_comment - )] - tcp_server_address: Option, - /// Path for the symlink pointing to the newly-created device. If blank, no - /// symlink will be created. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - symlink_path: Option, - - /// List the keyboards available for grabbing and exit. - #[cfg(target_os = "macos")] - #[arg(short, long)] - list: bool, - - /// Enable debug logging. - #[arg(short, long)] - debug: bool, - - /// Enable trace logging; implies --debug as well. - #[arg(short, long)] - trace: bool, - - /// Remove the startup delay on kanata. - /// In some cases, removing the delay may cause keyboard issues on startup. - #[arg(short, long, verbatim_doc_comment)] - nodelay: bool, - - /// Milliseconds to wait before attempting to register a newly connected - /// device. The default is 200. - /// - /// You may wish to increase this if you have a device that is failing - /// to register - the device may be taking too long to become ready. - #[cfg(target_os = "linux")] - #[arg(short, long, verbatim_doc_comment)] - wait_device_ms: Option, + )] + #[arg(short, long, verbatim_doc_comment)] + cfg: Option>, + + /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be + /// listened on. + #[cfg(feature = "tcp_server")] + #[arg( + short = 'p', + long = "port", + value_name = "PORT or IP:PORT", + verbatim_doc_comment + )] + tcp_server_address: Option, + /// Path for the symlink pointing to the newly-created device. If blank, no + /// symlink will be created. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + symlink_path: Option, + + /// List the keyboards available for grabbing and exit. + #[cfg(target_os = "macos")] + #[arg(short, long)] + list: bool, + + /// Enable debug logging. + #[arg(short, long)] + debug: bool, + + /// Enable trace logging; implies --debug as well. + #[arg(short, long)] + trace: bool, + + /// Remove the startup delay on kanata. + /// In some cases, removing the delay may cause keyboard issues on startup. + #[arg(short, long, verbatim_doc_comment)] + nodelay: bool, + + /// Milliseconds to wait before attempting to register a newly connected + /// device. The default is 200. + /// + /// You may wish to increase this if you have a device that is failing + /// to register - the device may be taking too long to become ready. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + wait_device_ms: Option, + + /// Validate configuration file and exit + #[arg(long, verbatim_doc_comment)] + check: bool, +} - /// Validate configuration file and exit - #[arg(long, verbatim_doc_comment)] - check: bool, - } +#[cfg(not(feature = "gui"))] +mod cli { + use super::*; /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { @@ -174,7 +177,7 @@ kanata.kbd in the current working directory and let kanata_arc = Kanata::new_arc(&args)?; if !args.nodelay { - info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); + log::info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); std::thread::sleep(std::time::Duration::from_secs(2)); } @@ -236,6 +239,6 @@ pub fn main() -> Result<()> { #[cfg(feature = "gui")] fn main() { - use kanata_state_machine::gui::lib_main_gui; + use main_lib::win_gui::*; lib_main_gui(); } diff --git a/src/main_lib/mod.rs b/src/main_lib/mod.rs new file mode 100644 index 000000000..bc545bf97 --- /dev/null +++ b/src/main_lib/mod.rs @@ -0,0 +1,2 @@ +#[cfg(all(target_os = "windows", feature = "gui"))] +pub(crate) mod win_gui; diff --git a/src/main_lib/win_gui.rs b/src/main_lib/win_gui.rs new file mode 100644 index 000000000..bad866df4 --- /dev/null +++ b/src/main_lib/win_gui.rs @@ -0,0 +1,176 @@ +use crate::*; +use anyhow::{anyhow, Context}; +use clap::{error::ErrorKind, CommandFactory}; +use kanata_state_machine::gui::*; +use kanata_state_machine::*; + +/// Parse CLI arguments and initialize logging. +fn cli_init() -> Result { + let args = match Args::try_parse() { + Ok(args) => args, + Err(e) => { + if *IS_TERM { + // init loggers without config so '-help' "error" or real ones can be printed + let mut log_cfg = ConfigBuilder::new(); + CombinedLogger::init(vec![ + TermLogger::new( + LevelFilter::Debug, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + ), + log_win::windbg_simple_combo(LevelFilter::Debug), + ]) + .expect("logger can init"); + } else { + log_win::init(); + log::set_max_level(LevelFilter::Debug); + } // doesn't panic + match e.kind() { + ErrorKind::DisplayHelp => { + let mut cmd = Args::command(); + let help = cmd.render_help(); + info!("{help}"); + log::set_max_level(LevelFilter::Off); + return Err(anyhow!("")); + } + _ => return Err(e.into()), + } + } + }; + + let cfg_paths = args.cfg.unwrap_or_else(default_cfg); + + let log_lvl = match (args.debug, args.trace) { + (_, true) => LevelFilter::Trace, + (true, false) => LevelFilter::Debug, + (false, false) => LevelFilter::Info, + }; + + let mut log_cfg = ConfigBuilder::new(); + if let Err(e) = log_cfg.set_time_offset_to_local() { + eprintln!("WARNING: could not set log TZ to local: {e:?}"); + }; + log_cfg.set_time_format_custom(format_description!( + version = 2, + "[hour]:[minute]:[second].[subsecond digits:4]" + )); + if *IS_TERM { + CombinedLogger::init(vec![ + TermLogger::new( + log_lvl, + log_cfg.build(), + TerminalMode::Mixed, + ColorChoice::AlwaysAnsi, + ), + log_win::windbg_simple_combo(log_lvl), + ]) + .expect("logger can init"); + } else { + CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl)]).expect("logger can init"); + } + log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); + #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] + log::info!("using LLHOOK+SendInput for keyboard IO"); + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + log::info!("using the Interception driver for keyboard IO"); + + if let Some(config_file) = cfg_paths.first() { + if !config_file.exists() { + bail!( + "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", + cfg_paths[0].to_str().unwrap_or("?") + ) + } + } else { + bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); + } + + if args.check { + log::info!("validating config only and exiting"); + let status = match cfg::new_from_file(&cfg_paths[0]) { + Ok(_) => 0, + Err(e) => { + log::error!("{e:?}"); + 1 + } + }; + std::process::exit(status); + } + + Ok(ValidatedArgs { + paths: cfg_paths, + #[cfg(feature = "tcp_server")] + tcp_server_address: args.tcp_server_address, + nodelay: args.nodelay, + }) +} + +fn main_impl() -> Result<()> { + let args = cli_init()?; + let kanata_arc = Kanata::new_arc(&args)?; + + if CFG.set(kanata_arc.clone()).is_err() { + warn!("Someone else set our ‘CFG’"); + }; // store a clone of cfg so that we can ask it to reset itself + + if !args.nodelay { + info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); + std::thread::sleep(std::time::Duration::from_secs(2)); + } + + // Start a processing loop in another thread and run the event loop in this thread. + // + // The reason for two different event loops is that the "event loop" only listens for keyboard + // events, which it sends to the "processing loop". The processing loop handles keyboard events + // while also maintaining `tick()` calls to keyberon. + + let (tx, rx) = std::sync::mpsc::sync_channel(100); + + let (server, ntx, nrx) = if let Some(address) = { + #[cfg(feature = "tcp_server")] + { + args.tcp_server_address + } + #[cfg(not(feature = "tcp_server"))] + { + None:: + } + } { + let mut server = TcpServer::new(address.into_inner(), tx.clone()); + server.start(kanata_arc.clone()); + let (ntx, nrx) = std::sync::mpsc::sync_channel(100); + (Some(server), Some(ntx), Some(nrx)) + } else { + (None, None, None) + }; + + native_windows_gui::init().context("Failed to init Native Windows GUI")?; + let ui = build_tray(&kanata_arc)?; + let gui_tx = ui.layer_notice.sender(); + if GUI_TX.set(gui_tx).is_err() { + warn!("Someone else set our ‘GUI_TX’"); + }; + Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); + + if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] + Kanata::start_notification_loop(nrx, server.connections); + } + + Kanata::event_loop(kanata_arc, tx, ui)?; + + Ok(()) +} + +pub fn lib_main_gui() { + let _attach_console = *IS_CONSOLE; + let ret = main_impl(); + if let Err(ref e) = ret { + log::error!("{e}\n"); + } + + unsafe { + FreeConsole(); + } +} From a1fe3e49a2fa5fa18f48659740dc16ca2d8c277f Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 15:49:34 -0700 Subject: [PATCH 145/154] apply clippy auto-fixes --- src/gui/win.rs | 72 ++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/src/gui/win.rs b/src/gui/win.rs index 528ca0e52..d0acc4ac0 100644 --- a/src/gui/win.rs +++ b/src/gui/win.rs @@ -156,9 +156,9 @@ fn get_icon_p_impl( ]; let pre_p = p.parent().unwrap_or_else(|| Path::new("")); let cur_exe = current_exe().unwrap_or_else(|_| PathBuf::new()); - let xdg_cfg = get_xdg_home().unwrap_or_else(|| PathBuf::new()); - let app_data = get_appdata().unwrap_or_else(|| PathBuf::new()); - let mut user_cfg = get_user_home().unwrap_or_else(|| PathBuf::new()); + let xdg_cfg = get_xdg_home().unwrap_or_default(); + let app_data = get_appdata().unwrap_or_default(); + let mut user_cfg = get_user_home().unwrap_or_default(); user_cfg.push(".config"); let parents = [ Path::new(""), @@ -208,7 +208,7 @@ fn get_icon_p_impl( trace!("{}p_icn={:?}", " ", p_icn); for ext in IMG_EXT { trace!("{} ext={:?}", " ", ext); - if !(p_par == blank_p) { + if p_par != blank_p { icon_file.push(p_par); } // folders if !p_kan.is_empty() { @@ -245,7 +245,7 @@ fn get_icon_p_impl( } } debug!("✗ no icon file found"); - return None; + None } fn set_menu_item_cfg_icon( @@ -253,7 +253,7 @@ fn set_menu_item_cfg_icon( cfg_icon_s: &str, cfg_p: &PathBuf, ) -> Option { - if let Some(ico_p) = get_icon_p("", "", &cfg_icon_s, &cfg_p, &false) { + if let Some(ico_p) = get_icon_p("", "", cfg_icon_s, cfg_p, &false) { let cfg_pkey_s = cfg_p.display().to_string(); let mut cfg_icon_bitmap = Default::default(); if let Ok(()) = nwg::Bitmap::builder() @@ -299,11 +299,11 @@ impl SystemTray { } } else { trace!("config menu item icon missing, read config and add it (or nothing) {cfg_p:?}"); - if let Ok(cfg) = cfg::new_from_file(&cfg_p) { + if let Ok(cfg) = cfg::new_from_file(cfg_p) { if let Some(cfg_icon_s) = cfg.options.tray_icon { debug!("loaded config without a tray icon {cfg_p:?}"); if let Some(cfg_icon_bitmap) = - set_menu_item_cfg_icon(menu_item_cfg, &cfg_icon_s, &cfg_p) + set_menu_item_cfg_icon(menu_item_cfg, &cfg_icon_s, cfg_p) { if is_active { self.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); @@ -337,7 +337,7 @@ impl SystemTray { h_cfg_i.handle ); h_cfg_i.set_checked(true); - if let Err(e) = self.update_tray_icon_cfg(h_cfg_i, &cfg_p, true) { + if let Err(e) = self.update_tray_icon_cfg(h_cfg_i, cfg_p, true) { debug!("{e:?} {cfg_p:?}"); let mut img_dyn = self.img_dyn.borrow_mut(); img_dyn.insert(cfg_p.clone(), None); @@ -356,12 +356,12 @@ impl SystemTray { let k = cfg.lock(); let idx_cfg = k.cur_cfg_idx; let mut tray_item_dyn = self.tray_item_dyn.borrow_mut(); - for (i, mut h_cfg_i) in tray_item_dyn.iter_mut().enumerate() { + for (i, h_cfg_i) in tray_item_dyn.iter_mut().enumerate() { // 1 if missing an icon, read config to get one let cfg_p = &k.cfg_paths[i]; trace!(" →→→→ i={i:?} {:?} cfg_p={cfg_p:?}", h_cfg_i.handle); let is_active = i == idx_cfg; - if let Err(e) = self.update_tray_icon_cfg(&mut h_cfg_i, &cfg_p, is_active) { + if let Err(e) = self.update_tray_icon_cfg(h_cfg_i, cfg_p, is_active) { debug!("{e:?} {cfg_p:?}"); let mut img_dyn = self.img_dyn.borrow_mut(); img_dyn.insert(cfg_p.clone(), None); @@ -468,7 +468,7 @@ impl SystemTray { let layer_icon = &k.layer_info[layer_id].icon; let mut cfg_layer_pkey = PathBuf::new(); // path key cfg_layer_pkey.push(path_cur_cc.clone()); - cfg_layer_pkey.push(PRE_LAYER.to_owned() + &layer_name); //:invalid path marker, so should be safe to use as a separator + cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, so should be safe to use as a separator let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); if log_enabled!(Debug) { let layer_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); @@ -480,8 +480,8 @@ impl SystemTray { { let mut app_data = self.app_data.borrow_mut(); - app_data.cfg_icon = cfg_icon.clone(); - app_data.layer0_name = k.layer_info[0].name.clone(); + app_data.cfg_icon.clone_from(cfg_icon); + app_data.layer0_name.clone_from(&k.layer_info[0].name); app_data.layer0_icon = Some(k.layer_info[0].name.clone()); app_data.icon_match_layer_name = k.icon_match_layer_name; // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading @@ -492,8 +492,8 @@ impl SystemTray { self.update_tray_icon( cfg_layer_pkey, &cfg_layer_pkey_s, - &layer_name, - &layer_icon, + layer_name, + layer_icon, path_cur_cc, clear, ) @@ -526,7 +526,7 @@ impl SystemTray { let mut cfg_layer_pkey = PathBuf::new(); // path key cfg_layer_pkey.push(path_cur_cc.clone()); - cfg_layer_pkey.push(PRE_LAYER.to_owned() + &layer_name); //:invalid path marker, so should be safe to use as a separator + cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, so should be safe to use as a separator let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); if log_enabled!(Debug) { let cfg_name = &path_cur @@ -547,8 +547,8 @@ impl SystemTray { self.update_tray_icon( cfg_layer_pkey, &cfg_layer_pkey_s, - &layer_name, - &layer_icon, + layer_name, + layer_icon, path_cur_cc, clear, ) @@ -582,7 +582,7 @@ impl SystemTray { if let Some(icon_opt) = icon_dyn.get(&cfg_layer_pkey) { // 1a config+layer path has already been checked if let Some(icon) = icon_opt { - self.tray.set_icon(&icon); + self.tray.set_icon(icon); *icon_active = Some(cfg_layer_pkey); } else { debug!( @@ -595,8 +595,8 @@ impl SystemTray { } else if let Some(layer_icon) = layer_icon { // 1b cfg+layer path hasn't been checked, but layer has an icon configured, so check it if let Some(ico_p) = get_icon_p( - &layer_icon, - &layer_name, + layer_icon, + layer_name, "", &path_cur_cc, &app_data.icon_match_layer_name, @@ -637,7 +637,7 @@ impl SystemTray { } else if icon_dyn.contains_key(&path_cur_cc) { // 2a no layer icon configured, but config icon exists, use it if let Some(icon) = icon_dyn.get(&path_cur_cc).unwrap() { - self.tray.set_icon(&icon); + self.tray.set_icon(icon); *icon_active = Some(path_cur_cc); } else { debug!( @@ -656,8 +656,8 @@ impl SystemTray { }; if let Some(ico_p) = get_icon_p( "", - &layer_name, - &cfg_icon_p, + layer_name, + cfg_icon_p, &path_cur_cc, &app_data.icon_match_layer_name, ) { @@ -699,7 +699,7 @@ impl SystemTray { fn exit(&self) { let handlers = self.handlers_dyn.borrow(); for handler in handlers.iter() { - nwg::unbind_event_handler(&handler); + nwg::unbind_event_handler(handler); } nwg::stop_thread_dispatch(); } @@ -781,7 +781,7 @@ pub mod system_tray_ui { const MENU_ACC: &str = "ASDFGQWERTZXCVBYUIOPHJKLNM"; let layer0_icon_s = &app_data.layer0_icon.clone().unwrap_or("".to_string()); let cfg_icon_s = &app_data.cfg_icon.clone().unwrap_or("".to_string()); - if (app_data.cfg_p).len() > 0 { + if !(app_data.cfg_p).is_empty() { for (i, cfg_p) in app_data.cfg_p.iter().enumerate() { let i_acc = match i { // menu accelerators from 1–0 then A–Z starting from home row for easier presses @@ -791,7 +791,7 @@ pub mod system_tray_ui { "&{} ", &MENU_ACC[(i - 10)..cmp::min(i - 10 + 1, MENU_ACC.len())] ), - _ => format!(" "), + _ => " ".to_string(), }; let cfg_name = &cfg_p .file_name() @@ -816,10 +816,10 @@ pub mod system_tray_ui { if i == 0 { // add icons if exists, hashed by config path (for active config, others will create on load) if let Some(ico_p) = get_icon_p( - &layer0_icon_s, + layer0_icon_s, &app_data.layer0_name, - &cfg_icon_s, - &cfg_p, + cfg_icon_s, + cfg_p, &app_data.icon_match_layer_name, ) { let mut cfg_layer_pkey = PathBuf::new(); // path key @@ -855,7 +855,7 @@ pub mod system_tray_ui { } // Set tray menu config item icons, ignores layers since these are per config if let Some(cfg_icon_bitmap) = - set_menu_item_cfg_icon(&mut menu_item, &cfg_icon_s, &cfg_p) + set_menu_item_cfg_icon(&mut menu_item, cfg_icon_s, cfg_p) { d.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); // show currently active config's icon in the combo menu let _ = img_dyn.insert(cfg_p.clone(), Some(cfg_icon_bitmap)); @@ -928,7 +928,7 @@ pub mod system_tray_ui { &ui.window.handle, handle_events, )); - return Ok(ui); + Ok(ui) } } @@ -982,9 +982,5 @@ pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; use once_cell::sync::Lazy; pub static IS_TERM: Lazy = Lazy::new(|| stdout().is_terminal()); pub static IS_CONSOLE: Lazy = Lazy::new(|| unsafe { - if AttachConsole(ATTACH_PARENT_PROCESS) == 0i32 { - return false; - } else { - return true; - } + AttachConsole(ATTACH_PARENT_PROCESS) != 0i32 }); From 5f524873ea96a7df0baf528fe204a7ab7b44a12c Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 15:53:54 -0700 Subject: [PATCH 146/154] clippy/fmt gui --- src/gui/win.rs | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/gui/win.rs b/src/gui/win.rs index d0acc4ac0..c4e65ffb6 100644 --- a/src/gui/win.rs +++ b/src/gui/win.rs @@ -889,35 +889,30 @@ pub mod system_tray_ui { let handle_events = move |evt, _evt_data, handle| { if let Some(evt_ui) = evt_ui.upgrade() { match evt { - E::OnNotice => if &handle == &evt_ui.layer_notice {SystemTray::reload_layer_icon(&evt_ui);} - E::OnWindowClose => if &handle == &evt_ui.window {SystemTray::exit (&evt_ui);} - E::OnMousePress(MousePressEvent::MousePressLeftUp) => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} - E::OnContextMenu/*🖰›*/ => if &handle == &evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnNotice => if handle == evt_ui.layer_notice {SystemTray::reload_layer_icon(&evt_ui);} + E::OnWindowClose => if handle == evt_ui.window {SystemTray::exit (&evt_ui);} + E::OnMousePress(MousePressEvent::MousePressLeftUp) => if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnContextMenu/*🖰›*/ => if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnMenuHover => - if &handle == &evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} + if handle == evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} E::OnMenuItemSelected => - if &handle == &evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None);SystemTray::update_tray_icon_cfg_group(&evt_ui,true); - } else if &handle == &evt_ui.tray_3exit {SystemTray::exit (&evt_ui); - } else { - match handle { - ControlHandle::MenuItem(_parent, _id) => { + if handle == evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None);SystemTray::update_tray_icon_cfg_group(&evt_ui,true); + } else if handle == evt_ui.tray_3exit {SystemTray::exit (&evt_ui); + } else if let + ControlHandle::MenuItem(_parent, _id) = handle { {let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // for (i, h_cfg) in tray_item_dyn.iter().enumerate() { if &handle == h_cfg { //info!("CONFIG handle i={:?} {:?}",i,&handle); // if SystemTray::reload_cfg(&evt_ui,Some(i)).is_ok() { - for (_j, h_cfg_j) in tray_item_dyn.iter().enumerate() { + for h_cfg_j in tray_item_dyn.iter() { if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others h_cfg.set_checked(true); // check self let _ = SystemTray::reload_cfg(&evt_ui,Some(i)); // depends on future fix in kanata that would revert index on failed config changes // } else {info!("OnMenuItemSelected: checkmarks not changed since config wasn't reloaded");} } } - } - SystemTray::update_tray_icon_cfg_group(&evt_ui,true); - }, - _ => {}, + } } - }, _ => {} } } @@ -981,6 +976,5 @@ pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; use once_cell::sync::Lazy; pub static IS_TERM: Lazy = Lazy::new(|| stdout().is_terminal()); -pub static IS_CONSOLE: Lazy = Lazy::new(|| unsafe { - AttachConsole(ATTACH_PARENT_PROCESS) != 0i32 -}); +pub static IS_CONSOLE: Lazy = + Lazy::new(|| unsafe { AttachConsole(ATTACH_PARENT_PROCESS) != 0i32 }); From 66e4b76a9d1d10899d4ff89e6c45102903c1f2b5 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 15:56:27 -0700 Subject: [PATCH 147/154] add gui to ci --- .github/workflows/rust.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8f4930e1e..0e6a3846f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -112,6 +112,11 @@ jobs: - name: Run clippy simulated output run: cargo clippy --all --features=simulated_output,cmd -- -D warnings + - name: Run tests gui + run: cargo test --all --features=gui + - name: Run clippy gui + run: cargo clippy --all --features=gui -- -D warnings + build-test-clippy-macos: runs-on: ${{ matrix.os }} strategy: From 02645cde8289b51fed2c73b1ae5d318a4d5e137f Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 16:18:56 -0700 Subject: [PATCH 148/154] use slice instead of iter, deduplicate, use None --- parser/src/cfg/layer_opts.rs | 10 +++-- parser/src/cfg/mod.rs | 74 +++++++++++------------------------- 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/parser/src/cfg/layer_opts.rs b/parser/src/cfg/layer_opts.rs index 69676171a..f28e44d42 100644 --- a/parser/src/cfg/layer_opts.rs +++ b/parser/src/cfg/layer_opts.rs @@ -1,10 +1,12 @@ use crate::cfg::*; use crate::*; -use std::collections::HashMap; -pub fn parse_layer_opts<'a>( - mut list: impl Iterator, -) -> Result> { + +pub(crate) const DEFLAYER_ICON: [&str; 3] = ["icon", "🖻", "🖼"]; +pub(crate) type LayerIcons = HashMap>; + +pub fn parse_layer_opts(list: &[SExpr]) -> Result> { let mut layer_opts: HashMap = HashMap::default(); + let mut list = list.iter(); while let Some(key_expr) = list.next() { // Read k-v pairs from the configuration // todo: add hashmap for future options, currently only parse icons diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 68c669c8f..0c6a7ca17 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -504,7 +504,6 @@ fn expand_includes( const DEFLAYER: &str = "deflayer"; const DEFLAYER_MAPPED: &str = "deflayermap"; -const DEFLAYER_ICON: [&str; 3] = ["icon", "🖻", "🖼"]; const DEFLOCALKEYS_VARIANTS: &[&str] = &[ "deflocalkeys-win", "deflocalkeys-winiov2", @@ -1040,7 +1039,6 @@ fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec< } type LayerIndexes = HashMap; -type LayerIcons = HashMap>; type Aliases = HashMap; /// Returns layer names and their indexes into the keyberon layout. This also checks that: @@ -1069,16 +1067,20 @@ fn parse_layer_indexes( SpannedLayerExprs::DefsrcMapping(_) => match layer_expr { SExpr::Atom(name_span) => (name_span.t.to_owned(), None), SExpr::List(name_opts_span) => { - let listt = &name_opts_span.t; - let mut list = listt.iter(); - let name = list.next().ok_or_else(|| {anyhow_span!(name_opts_span,"deflayer requires a string name within this pair of parenthesis (or a string name without any)")})? - .atom(None).ok_or_else(|| {anyhow_expr!(layer_expr, "layer name after {DEFLAYER} must be a string when enclosed within one pair of parentheses")})?; - let layer_opts = parse_layer_opts(list)?; + let list = &name_opts_span.t; + let name = list.get(0).ok_or_else(|| anyhow_span!( + name_opts_span, + "deflayer requires a string name within this pair of parenthesis (or a string name without any)" + ))? + .atom(None).ok_or_else(|| anyhow_expr!( + layer_expr, + "layer name after {DEFLAYER} must be a string when enclosed within one pair of parentheses" + ))?; + let layer_opts = parse_layer_opts(&list[1..])?; let icon = layer_opts .get(DEFLAYER_ICON[0]) - .map(|icon_s| icon_s.trim_matches('"')) - .unwrap_or(""); - (name.to_owned(), Some(icon.to_owned())) + .map(|icon_s| icon_s.trim_matches('"').to_owned()); + (name.to_owned(), icon) } }, SpannedLayerExprs::CustomMapping(_) => { @@ -1091,49 +1093,19 @@ fn parse_layer_indexes( ) })? .to_owned(); - let mut list = list.iter(); - let name = list.next().ok_or_else(|| {anyhow_expr!(layer_expr,"layer name after {DEFLAYER_MAPPED} must be a string within one pair of parentheses")})? - .atom(None).ok_or_else(|| {anyhow_expr!(layer_expr, "layer name after {DEFLAYER_MAPPED} must be a string within one pair of parentheses")})?; + let name = list.get(0) + .and_then(|s| s.atom(None)) + .ok_or_else(|| anyhow_expr!( + layer_expr, + "layer name after {DEFLAYER_MAPPED} must be a string within one pair of parentheses" + ))?; - let mut icon = ""; // add hashmap for future options, currently only parse icons - let mut layer_opts: HashMap = HashMap::default(); - while let Some(key_expr) = list.next() { - // Read k-v pairs from the configuration - let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(&key_expr, "No lists are allowed in {DEFLAYER_MAPPED} options")}) - .and_then(|opt_key| { - if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { - if layer_opts.contains_key(DEFLAYER_ICON[0]) {bail_expr!(key_expr,"Duplicate option found in {DEFLAYER_MAPPED}: {opt_key}");} - Ok(DEFLAYER_ICON[0]) - } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER_MAPPED}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} - })?; - if layer_opts.contains_key(opt_key) { - bail_expr!( - key_expr, - "Duplicate option found in {DEFLAYER_MAPPED}: {opt_key}" - ); - } - let opt_val = match list.next() { - Some(v) => v - .atom(None) - .ok_or_else(|| { - anyhow_expr!( - &v, - "No lists are allowed in {DEFLAYER_MAPPED}'s option values" - ) - }) - .map(|opt_val| { - icon = opt_val.trim_matches('"'); - opt_val - })?, - None => bail_expr!( - &key_expr, - "Option without a value in {DEFLAYER_MAPPED} {name}" - ), - }; - layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); - } - (name.to_owned(), Some(icon.to_owned())) + let layer_opts = parse_layer_opts(&list[1..])?; + let icon = layer_opts + .get(DEFLAYER_ICON[0]) + .map(|icon_s| icon_s.trim_matches('"').to_owned()); + (name.to_owned(), icon) } }; if layer_indexes.contains_key(&layer_name) { From 0bd4643cd4728653543881a1339ff25b0560db9b Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 16:28:44 -0700 Subject: [PATCH 149/154] random fix I noticed while refactoring --- parser/src/cfg/mod.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 0c6a7ca17..971dcfd1d 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -2971,18 +2971,8 @@ fn parse_layers( } } let rem = pairs.remainder(); - match rem.len() { - 0 => {} - 1 => { - bail_expr!( - &rem[0], - "an input must be followed by a map string and an action" - ); - } - 2 => { - bail_expr!(&rem[1], "map string must be followed by an action"); - } - _ => unreachable!(), + if !rem.is_empty() { + bail_expr!(&rem[0], "input must by followed by an action"); } } } From af1ea80ac847f86bbb404a5da34b343e23b575fc Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 16:30:42 -0700 Subject: [PATCH 150/154] refactor --- parser/src/cfg/layer_opts.rs | 43 +++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/parser/src/cfg/layer_opts.rs b/parser/src/cfg/layer_opts.rs index f28e44d42..872999190 100644 --- a/parser/src/cfg/layer_opts.rs +++ b/parser/src/cfg/layer_opts.rs @@ -6,31 +6,44 @@ pub(crate) type LayerIcons = HashMap>; pub fn parse_layer_opts(list: &[SExpr]) -> Result> { let mut layer_opts: HashMap = HashMap::default(); - let mut list = list.iter(); - while let Some(key_expr) = list.next() { + let mut opts = list.chunks_exact(2); + for kv in opts.by_ref() { + let key_expr = &kv[0]; + let val_expr = &kv[1]; // Read k-v pairs from the configuration // todo: add hashmap for future options, currently only parse icons - let opt_key = key_expr.atom(None).ok_or_else(|| {anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")}).and_then(|opt_key| { + let opt_key = key_expr.atom(None) + .ok_or_else(|| anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")) + .and_then(|opt_key| { if DEFLAYER_ICON.iter().any(|&i| i == opt_key) { if layer_opts.contains_key(DEFLAYER_ICON[0]) { - bail_expr!(key_expr,"Duplicate option found in {DEFLAYER}: {opt_key}, one of {DEFLAYER_ICON:?} already exists");} - // separate dupe check since multi-keys are stored with one "canonical" repr, so '🖻' → 'icon' + // separate dupe check since multi-keys are stored + // with one "canonical" repr, so '🖻' → 'icon' // and this info will be lost after the loop + bail_expr!( + key_expr, + "Duplicate option found in {DEFLAYER}: {opt_key}, one of {DEFLAYER_ICON:?} already exists" + ); + } Ok(DEFLAYER_ICON[0]) - } else {bail_expr!(key_expr, "Invalid option in {DEFLAYER}: {opt_key}, expected one of {DEFLAYER_ICON:?}")} - })?; + } else { + bail_expr!(key_expr, "Invalid option in {DEFLAYER}: {opt_key}, expected one of {DEFLAYER_ICON:?}") + } + })?; if layer_opts.contains_key(opt_key) { bail_expr!(key_expr, "Duplicate option found in {DEFLAYER}: {opt_key}"); } - let opt_val = match list.next() { - Some(v) => v.atom(None).ok_or_else(|| { - anyhow_expr!(v, "No lists are allowed in {DEFLAYER}'s option values") - })?, - None => { - bail_expr!(key_expr, "Option without a value in {DEFLAYER}") - } - }; + let opt_val = val_expr.atom(None).ok_or_else(|| { + anyhow_expr!( + val_expr, + "No lists are allowed in {DEFLAYER}'s option values" + ) + })?; layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); } + let rem = opts.remainder(); + if !rem.is_empty() { + bail_expr!(&rem[0], "This option is missing a value."); + } Ok(layer_opts) } From fddd7c068b40479bdf45932b12df17fa9d68e39f Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 May 2024 16:36:06 -0700 Subject: [PATCH 151/154] add to tests, use chunk_exact --- parser/src/cfg/mod.rs | 4 ++-- parser/src/cfg/tests.rs | 12 +++++++++++- parser/test_cfgs/icon_good.kbd | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 971dcfd1d..e0e6bb5ce 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1068,7 +1068,7 @@ fn parse_layer_indexes( SExpr::Atom(name_span) => (name_span.t.to_owned(), None), SExpr::List(name_opts_span) => { let list = &name_opts_span.t; - let name = list.get(0).ok_or_else(|| anyhow_span!( + let name = list.first().ok_or_else(|| anyhow_span!( name_opts_span, "deflayer requires a string name within this pair of parenthesis (or a string name without any)" ))? @@ -1093,7 +1093,7 @@ fn parse_layer_indexes( ) })? .to_owned(); - let name = list.get(0) + let name = list.first() .and_then(|s| s.atom(None)) .ok_or_else(|| anyhow_expr!( layer_expr, diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index e0e7d1a1f..864a86a8c 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1854,9 +1854,19 @@ fn parse_layer_opts_icon() { } #[test] -fn disallow_dupe_layer_opts_icon() { +fn disallow_dupe_layer_opts_icon_layernonmap() { let _lk = lock(&CFG_PARSE_LOCK); new_from_file(&std::path::PathBuf::from("./test_cfgs/icon_bad_dupe.kbd")) .map(|_| ()) .expect_err("fails"); } + +#[test] +fn disallow_dupe_layer_opts_icon_layermap() { + let source = " +(defcfg) +(defsrc) +(deflayermap (base icon base.png 🖻 n.ico) 0 0) +"; + parse_cfg(source).map(|_| ()).expect_err("fails"); +} diff --git a/parser/test_cfgs/icon_good.kbd b/parser/test_cfgs/icon_good.kbd index d1c3ebe3c..668e0e686 100644 --- a/parser/test_cfgs/icon_good.kbd +++ b/parser/test_cfgs/icon_good.kbd @@ -4,3 +4,4 @@ (deflayer (1emoji 🖻 1symbols.png ) 1) (deflayer (2icon-quote 🖻 "2Nav Num.png" ) 1) (deflayer (3emoji_alt 🖼 3trans.parent ) 1) +(deflayermap (4layermap 🖼 3trans.parent ) 0 0) From a3b1db8e84242ce3a075b2c54b8e1ae7a6905a7a Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Mon, 6 May 2024 16:30:11 +0700 Subject: [PATCH 152/154] split long comments+code --- parser/src/cfg/mod.rs | 9 ++-- src/gui/win.rs | 84 +++++++++++++++++++++++--------------- src/gui/win_nwg_ext/mod.rs | 63 ++++++++-------------------- src/main.rs | 10 ++--- src/tcp_server.rs | 10 +++-- 5 files changed, 86 insertions(+), 90 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index e0e6bb5ce..cdccff58c 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -184,7 +184,8 @@ macro_rules! anyhow_span { pub struct FileContentProvider<'a> { /// A function to load content of a file from a filepath. - /// Optionally, it could implement caching and a mechanism preventing "file" and "./file" from loading twice. + /// Optionally, it could implement caching and a mechanism preventing "file" and "./file" + /// from loading twice. get_file_content_fn: &'a mut dyn FnMut(&Path) -> std::result::Result, } @@ -2084,7 +2085,8 @@ static KEYMODI: &[(&str, KeyCode)] = &[ ("RA-", KeyCode::RAlt), ("⎇›", KeyCode::RAlt), ("⌥›", KeyCode::RAlt), - ("⎈", KeyCode::LCtrl), // Shorter indicators should be at the end to only get matched after indicators with sides have had a chance + ("⎈", KeyCode::LCtrl), // Shorter indicators should be at the end to only get matched after + // indicators with sides have had a chance ("⌥", KeyCode::LAlt), ("⎇", KeyCode::LAlt), ("◆", KeyCode::LGui), @@ -3166,7 +3168,8 @@ fn parse_sequence_keys(exprs: &[SExpr], s: &ParserState) -> Result> { ); } if *pressed != KEY_OVERLAP { - // Note: key overlap item is special and goes at the end, not the beginning + // Note: key overlap item is special and goes at the end, + // not the beginning seq.push(seq_num); } } diff --git a/src/gui/win.rs b/src/gui/win.rs index c4e65ffb6..adea34bf4 100644 --- a/src/gui/win.rs +++ b/src/gui/win.rs @@ -50,7 +50,8 @@ pub struct SystemTray { pub handlers_dyn: RefCell>, /// Store dynamically created icons to not load them from a file every time pub icon_dyn: RefCell>>, - /// Store dynamically created icons to not load them from a file every time (bitmap format needed to set MenuItem's icons) + /// Store dynamically created icons to not load them from a file every time + /// (bitmap format needed to set MenuItem's icons) pub img_dyn: RefCell>>, /// Store 'icon_dyn' hashmap key for the currently active icon ('cfg_path:layer_name' format) pub icon_active: RefCell>, @@ -77,7 +78,8 @@ pub fn get_xdg_home() -> Option { var_os("XDG_CONFIG_HOME").map(PathBuf::from) } -const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow checking directly for user passed values +const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow checking directly for + // user passed values 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 @@ -91,7 +93,10 @@ pub fn send_gui_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` or a config file name at various locations (where config file is, where executable is, in user config folders) +/// 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` +/// or a config file name at various locations (where config file is, where executable is, +/// in user config folders) fn get_icon_p( lyr_icn: S1, lyr_nm: S2, @@ -167,7 +172,8 @@ fn get_icon_p_impl( &xdg_cfg, &app_data, &user_cfg, - ]; // empty path to allow no prefixes when icon path is explictily set in case it's a full path already + ]; // empty path to allow no prefixes when icon path is explictily set in case it's a full + // path already for (i, nm) in f_name.enumerate() { trace!("{}nm={:?}", "", nm); @@ -282,7 +288,8 @@ impl SystemTray { let (x, y) = nwg::GlobalCursor::position(); self.tray_menu.popup(x, y); } - /// Add a ✓ (or highlight the icon) to the currently active config. Runs on opening of the list of configs menu + /// Add a ✓ (or highlight the icon) to the currently active config. + /// Runs on opening of the list of configs menu fn update_tray_icon_cfg( &self, menu_item_cfg: &mut nwg::MenuItem, @@ -341,7 +348,8 @@ impl SystemTray { debug!("{e:?} {cfg_p:?}"); let mut img_dyn = self.img_dyn.borrow_mut(); img_dyn.insert(cfg_p.clone(), None); - self.tray_1cfg_m.set_bitmap(None); // can't update menu, so remove combo menu icon + self.tray_1cfg_m.set_bitmap(None); // can't update menu, so remove combo + // menu icon }; } else { debug!("gui cfg selection matches active config"); @@ -468,7 +476,9 @@ impl SystemTray { let layer_icon = &k.layer_info[layer_id].icon; let mut cfg_layer_pkey = PathBuf::new(); // path key cfg_layer_pkey.push(path_cur_cc.clone()); - cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, so should be safe to use as a separator + cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, + // so should be safe to use as + // a separator let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); if log_enabled!(Debug) { let layer_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); @@ -484,9 +494,7 @@ impl SystemTray { app_data.layer0_name.clone_from(&k.layer_info[0].name); app_data.layer0_icon = Some(k.layer_info[0].name.clone()); app_data.icon_match_layer_name = k.icon_match_layer_name; - // self.tray.set_visibility(false); // flash the icon, but might be confusing as the app isn't restarting, just reloading self.tray.set_tip(&cfg_layer_pkey_s); // update tooltip to point to the newer config - // self.tray.set_visibility(true); } let clear = i.is_none(); self.update_tray_icon( @@ -526,7 +534,9 @@ impl SystemTray { let mut cfg_layer_pkey = PathBuf::new(); // path key cfg_layer_pkey.push(path_cur_cc.clone()); - cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, so should be safe to use as a separator + cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, + // so should be safe + // to use as a separator let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); if log_enabled!(Debug) { let cfg_name = &path_cur @@ -724,7 +734,6 @@ pub mod system_tray_ui { use nwg::Event as E; let app_data = d.app_data.borrow().clone(); - // d.app_data = RefCell::new(Default::default()); d.tray_item_dyn = RefCell::new(Default::default()); d.handlers_dyn = RefCell::new(Default::default()); // Resources @@ -774,7 +783,7 @@ pub mod system_tray_ui { let mut main_tray_icon_l = Default::default(); let mut main_tray_icon_is = false; { - let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed mut + let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); //extra scope to drop borrowed let mut icon_dyn = d.icon_dyn.borrow_mut(); let mut img_dyn = d.img_dyn.borrow_mut(); let mut icon_active = d.icon_active.borrow_mut(); @@ -784,7 +793,7 @@ pub mod system_tray_ui { if !(app_data.cfg_p).is_empty() { for (i, cfg_p) in app_data.cfg_p.iter().enumerate() { let i_acc = match i { - // menu accelerators from 1–0 then A–Z starting from home row for easier presses + // accelerators from 1–0, A–Z starting from home row for easier presses 0..=8 => format!("&{} ", i + 1), 9 => format!("&{} ", 0), 10..=35 => format!( @@ -798,7 +807,6 @@ pub mod system_tray_ui { .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); //kanata.kbd - // let menu_text = i_acc + cfg_name; // &1 kanata.kbd let menu_text = format!("{cfg_name}\t{i_acc}"); // kanata.kbd &1 let mut menu_item = Default::default(); if i == 0 { @@ -814,7 +822,8 @@ pub mod system_tray_ui { .build(&mut menu_item)?; } if i == 0 { - // add icons if exists, hashed by config path (for active config, others will create on load) + // add icons if exists, hashed by config path + // (for active config, others will create on load) if let Some(ico_p) = get_icon_p( layer0_icon_s, &app_data.layer0_name, @@ -853,11 +862,15 @@ pub mod system_tray_ui { let _ = icon_dyn.insert(cfg_p.clone(), Some(temp_icon)); *icon_active = Some(cfg_p.clone()); } - // Set tray menu config item icons, ignores layers since these are per config + // Set tray menu config item icons, ignores layers since these + // are per config if let Some(cfg_icon_bitmap) = set_menu_item_cfg_icon(&mut menu_item, cfg_icon_s, cfg_p) { - d.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); // show currently active config's icon in the combo menu + d.tray_1cfg_m.set_bitmap(Some(&cfg_icon_bitmap)); // show currently + // active config's + // icon in the + // combo menu let _ = img_dyn.insert(cfg_p.clone(), Some(cfg_icon_bitmap)); } else { let _ = img_dyn.insert(cfg_p.clone(), None); @@ -889,26 +902,33 @@ pub mod system_tray_ui { let handle_events = move |evt, _evt_data, handle| { if let Some(evt_ui) = evt_ui.upgrade() { match evt { - E::OnNotice => if handle == evt_ui.layer_notice {SystemTray::reload_layer_icon(&evt_ui);} - E::OnWindowClose => if handle == evt_ui.window {SystemTray::exit (&evt_ui);} - E::OnMousePress(MousePressEvent::MousePressLeftUp) => if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} - E::OnContextMenu/*🖰›*/ => if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} - E::OnMenuHover => - if handle == evt_ui.tray_1cfg_m {SystemTray::check_active(&evt_ui);} - E::OnMenuItemSelected => - if handle == evt_ui.tray_2reload {let _ = SystemTray::reload_cfg(&evt_ui,None);SystemTray::update_tray_icon_cfg_group(&evt_ui,true); + E::OnNotice => + if handle == evt_ui.layer_notice { + SystemTray::reload_layer_icon(&evt_ui);} + E::OnWindowClose => + if handle == evt_ui.window {SystemTray::exit (&evt_ui);} + E::OnMousePress(MousePressEvent::MousePressLeftUp) => + if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnContextMenu/*🖰›*/ => + if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} + E::OnMenuHover => + if handle == evt_ui.tray_1cfg_m { + SystemTray::check_active(&evt_ui);} + E::OnMenuItemSelected => + if handle == evt_ui.tray_2reload { + let _ = SystemTray::reload_cfg(&evt_ui,None); + SystemTray::update_tray_icon_cfg_group(&evt_ui,true); } else if handle == evt_ui.tray_3exit {SystemTray::exit (&evt_ui); } else if let ControlHandle::MenuItem(_parent, _id) = handle { {let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // for (i, h_cfg) in tray_item_dyn.iter().enumerate() { - if &handle == h_cfg { //info!("CONFIG handle i={:?} {:?}",i,&handle); - // if SystemTray::reload_cfg(&evt_ui,Some(i)).is_ok() { + if &handle == h_cfg { for h_cfg_j in tray_item_dyn.iter() { - if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others + if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck + // others h_cfg.set_checked(true); // check self - let _ = SystemTray::reload_cfg(&evt_ui,Some(i)); // depends on future fix in kanata that would revert index on failed config changes - // } else {info!("OnMenuItemSelected: checkmarks not changed since config wasn't reloaded");} + let _ = SystemTray::reload_cfg(&evt_ui,Some(i)); // depends } } } @@ -928,7 +948,8 @@ pub mod system_tray_ui { } impl Drop for SystemTrayUi { - /// To make sure that everything is freed without issues, the default handler must be unbound. + /// To make sure that everything is freed without issues, the default handler + /// must be unbound. fn drop(&mut self) { let mut handlers = self.handler_def.borrow_mut(); for handler in handlers.drain(0..) { @@ -961,7 +982,6 @@ pub fn build_tray(cfg: &Arc>) -> Result() as u32, //u32 byte-size of the structure, must be set by the caller to sizeof(MENUITEMINFO) - fMask: MIIM_BITMAP, //MENU_ITEM_MASK members to be retrieved or set: - // MIIM_BITMAP hbmpItem - // MIIM_STATE fState - // MIIM_CHECKMARKS hbmpChecked and hbmpUnchecked - // MIIM_DATA dwItemData - // MIIM_FTYPE fType - // MIIM_ID wID - // MIIM_STRING dwTypeData - // MIIM_SUBMENU hSubMenu - // MIIM_TYPE fType and dwTypeData, replaced by MIIM_BITMAP, MIIM_FTYPE, and MIIM_STRING - hbmpItem: hbitmap, //HBITMAP bitmap to display or one of: (fMask==MIIM_BITMAP) - // HBMMENU_CALLBACK Bitmap that is drawn by the window that owns the menu. The application must process the WM_MEASUREITEM and WM_DRAWITEM messages - // HBMMENU_MBAR_MINIMIZE | _D | HBMMENU_MBAR_RESTORE | HBMMENU_MBAR_CLOSE | _D - // ↑ Min|dis Min | Restore | Close|Disabled close button for the menu bar - // ↓ Min|Max|Close|Restore button for the submenu - // HBMMENU_POPUP_MINIMIZE | HBMMENU_POPUP_MAXIMIZE | HBMMENU_POPUP_CLOSE | HBMMENU_POPUP_RESTORE - // HBMMENU_SYSTEM Windows icon or the icon of the window specified in dwItemData - fType: 0, //MENU_ITEM_TYPE requires fMask=MIIM_FTYPE, MFT_BITMAP/MFT_SEPARATOR/MFT_STRING can't be combined, set fMask to MIIM_TYPE to use fType - // MFT_OWNERDRAW Assigns responsibility for drawing the menu item to the window that owns the menu. The window receives a WM_MEASUREITEM message before the menu is displayed for the first time, and a WM_DRAWITEM message whenever the appearance of the menu item must be updated. If this value is specified, the dwTypeData member contains an application-defined value - // MFT_MENUBARBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, a vertical line separates the new column from the old - // MFT_MENUBREAK Places the menu item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu). For a drop-down menu, submenu, or shortcut menu, the columns are not separated by a vertical line - // MFT_RADIOCHECK Displays selected menu items using a radio-button mark instead of a check mark if the hbmpChecked member is NULL - // MFT_RIGHTJUSTIFY Right-justifies the menu item and any subsequent items. This value is valid only if the menu item is in a menu bar - // MFT_RIGHTORDER Specifies that menus cascade right-to-left (the default is left-to-right). This is used to support right-to-left languages, such as Arabic and Hebrew - // MFT_SEPARATOR Specifies that the menu item is a separator. A menu item separator appears as a horizontal dividing line. The dwTypeData and cch members are ignored. This value is valid only in a drop-down menu, submenu, or shortcut menu - // MFT_STRING Displays the menu item using a text string. dwTypeData member is the pointer to a null-terminated string, and the cch member is the length of the string. replaced by MIIM_STRING - // MFT_BITMAP Displays the menu item using a bitmap . low-order word of the dwTypeData member is the bitmap handle, and the cch member is ignored. replaced by MIIM_BITMAP and hbmpItem - fState: 0, //MENU_ITEM_STATE requires fMask=MIIM_STATE - // MFS_ENABLED | MFS_DISABLED==MFS_GRAYED ≝Enables|disables&grays so it can|can't be selected - // MFS_CHECKED | MFS_UNCHECKED Checks|Unchecks see hbmpChecked for info - // MFS_HILITE | MFS_UNHILITE Highlights|≝No highlight - // MFS_DEFAULT Set as default (only 1), displayed in bold - // - hSubMenu: 0, //HMENU handle to the drop-down (sub)menu associated with the menu item. NULL if no drop-down. fMask==MIIM_SUBMENU - hbmpChecked: 0, //HBITMAP bitmap to display @ selected item. 0→default bitmap (✓ or • if fType=MFT_RADIOCHECK). fMask==MIIM_CHECKMARKS - hbmpUnchecked: 0, //HBITMAP bitmap to display @ not selected item. 0→no bitmap. fMask==MIIM_CHECKMARKS - dwTypeData: ptr::null_mut(), //PWSTR item contents, depends on fType, used if fMask has MIIM_TYPE - wID: 0, //u32 app-defined item value ID. fMask==MIIM_ID, - dwItemData: 0, //usize app-defined item value. fMask==MIIM_DATA - cch: 0, //u32 . + cbSize: size_of::() as u32, + fMask: MIIM_BITMAP, + hbmpItem: hbitmap, + fType: 0, + fState: 0, + hSubMenu: 0, + hbmpChecked: 0, + hbmpUnchecked: 0, + dwTypeData: ptr::null_mut(), + wID: 0, + dwItemData: 0, + cch: 0, }; unsafe { SetMenuItemInfoW( - hmenu_par as HMENU, //hmenu handle to the menu that contains the menu item - hmenu as u32, //item id/pos of the menu item to get infor about, meaning depends on the value of fByPosition - MF_BYCOMMAND as i32, //fByPosition meaning of uItem - // FALSE it's a menu item id - // it's a menu item position (see learn.microsoft.com/en-us/windows/desktop/menurc/about-menus) - &menu_item_info as *const _, //lpmii pointer to a MENUITEMINFO structure that specifies the information to retrieve and receives information about the menu item (cbSize member must be set to sizeof(MENUITEMINFO) before calling this function) + hmenu_par as HMENU, + hmenu as u32, + MF_BYCOMMAND as i32, + &menu_item_info as *const _, ); } } diff --git a/src/main.rs b/src/main.rs index eb75106c1..7afe03bd3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,8 +40,8 @@ kanata.kbd in the current working directory and #[arg(short, long, verbatim_doc_comment)] cfg: Option>, - /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, no TCP port will be - /// listened on. + /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, + /// no TCP port will be listened on. #[cfg(feature = "tcp_server")] #[arg( short = 'p', @@ -183,9 +183,9 @@ mod cli { // Start a processing loop in another thread and run the event loop in this thread. // - // The reason for two different event loops is that the "event loop" only listens for keyboard - // events, which it sends to the "processing loop". The processing loop handles keyboard events - // while also maintaining `tick()` calls to keyberon. + // The reason for two different event loops is that the "event loop" only listens for + // keyboard events, which it sends to the "processing loop". The processing loop handles + // keyboard events while also maintaining `tick()` calls to keyberon. let (tx, rx) = std::sync::mpsc::sync_channel(100); diff --git a/src/tcp_server.rs b/src/tcp_server.rs index ebaebbf5f..0bcb1b2ea 100755 --- a/src/tcp_server.rs +++ b/src/tcp_server.rs @@ -127,7 +127,8 @@ impl TcpServer { .lock() .layer_info .iter() - .step_by(2) // skip every other name, which is a duplicate + .step_by(2) // skip every other name, + // which is a duplicate .map(|info| info.name.clone()) .collect::>(), }; @@ -176,14 +177,17 @@ impl TcpServer { match kanata.lock().kbd_out.set_mouse(x, y) { Ok(_) => { log::info!("sucessfully did set mouse position to: x {x} y {y}"); - // Optionally send a success message to the client + // Optionally send a success message to the + // client } Err(e) => { log::error!( "Failed to set mouse position: {}", e ); - // Implement any error handling logic here, such as sending an error response to the client + // Implement any error handling logic here, + // such as sending an error response to + // the client } } } From 4efe19e368bc2cf7b6a1548499c0b33eacccede9 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Mon, 6 May 2024 16:48:20 +0700 Subject: [PATCH 153/154] split long comments+code --- keyberon/src/multikey_buffer.rs | 3 ++- parser/src/cfg/mod.rs | 3 ++- src/oskbd/simulated.rs | 3 ++- win_dbg_logger/src/lib.rs | 40 +++++++++++++++++++++++---------- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/keyberon/src/multikey_buffer.rs b/keyberon/src/multikey_buffer.rs index b36c3d741..77404f7ef 100644 --- a/keyberon/src/multikey_buffer.rs +++ b/keyberon/src/multikey_buffer.rs @@ -67,7 +67,8 @@ impl<'a, T> MultiKeyBuffer<'a, T> { /// # Safety /// /// The program should not have any references to the inner buffer before calling. - /// The program should not mutate the buffer after calling this function until after the returned reference is dropped. + /// The program should not mutate the buffer after calling this function until after the + /// returned reference is dropped. pub(crate) unsafe fn get_ref(&self) -> &'a Action<'a, T> { *self.ac = Action::NoOp; *self.ptr = slice::from_raw_parts(self.buf.as_ptr(), self.size); diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index cdccff58c..960abf95a 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -491,7 +491,8 @@ fn expand_includes( ) }; let include_file_path = spanned_filepath.t.trim_matches('"'); - let file_content = file_content_provider.get_file_content(Path::new(include_file_path)).map_err(|e| anyhow_span!(spanned_filepath, "{e}"))?; + let file_content = file_content_provider.get_file_content(Path::new(include_file_path)) + .map_err(|e| anyhow_span!(spanned_filepath, "{e}"))?; let tree = sexpr::parse(&file_content, include_file_path)?; acc.extend(tree); diff --git a/src/oskbd/simulated.rs b/src/oskbd/simulated.rs index 4913056f1..5e5cce3f5 100644 --- a/src/oskbd/simulated.rs +++ b/src/oskbd/simulated.rs @@ -96,7 +96,8 @@ impl LogFmt { let mut pad = value.len(); let mut time = "".to_string(); if self.ticks > 0 { - pad = std::cmp::max(value.len(), self.ticks.to_string().len()); // add extra padding if event tick is wider + pad = std::cmp::max(value.len(), self.ticks.to_string().len()); // add extra padding if + // event tick is wider time = format!(" {: &'static bool { set_thread_state(false) } pub fn set_thread_state(is: bool) -> &'static bool { - // accessor function to avoid get_or_init on every call (lazycell allows doing that without an extra function) + // accessor function to avoid get_or_init on every call + // (lazycell allows doing that without an extra function) static CELL: OnceLock = OnceLock::new(); CELL.get_or_init(|| is) } @@ -225,9 +236,11 @@ pub fn is_debugger_present() -> bool { /// Sets the `WinDbgLogger` as the currently-active logger. /// -/// If an error occurs when registering `WinDbgLogger` as the current logger, this function will output a warning and will return normally. It will not panic. +/// If an error occurs when registering `WinDbgLogger` as the current logger, this function will +/// output a warning and will return normally. It will not panic. /// This behavior was chosen because `WinDbgLogger` is intended for use in debugging. -/// Panicking would disrupt debugging and introduce new failure modes. It would also create problems for mixed-mode debugging, where Rust code is linked with C/C++ code. +/// Panicking would disrupt debugging and introduce new failure modes. It would also create +/// problems for mixed-mode debugging, where Rust code is linked with C/C++ code. pub fn init() { match log::set_logger(&WINDBG_LOGGER) { Ok(()) => {} //↓ there's really nothing we can do about it. @@ -243,7 +256,9 @@ macro_rules! define_init_at_level { ($func:ident, $level:ident) => { /// This can be called from C/C++ code to register the debug logger. /// - /// For Windows DLLs that have statically linked an instance of `win_dbg_logger` into them, `DllMain` should call `win_dbg_logger_init_()` from the `DLL_PROCESS_ATTACH` handler, e.g.: + /// For Windows DLLs that have statically linked an instance of `win_dbg_logger` into + /// them, `DllMain` should call `win_dbg_logger_init_()` from the `DLL_PROCESS_ATTACH` + /// handler, e.g.: /// /// ```ignore /// extern "C" void __cdecl rust_win_dbg_logger_init_debug(); // Calls into Rust code @@ -257,7 +272,8 @@ macro_rules! define_init_at_level { /// } /// ``` /// - /// For Windows executables that have statically linked an instance of `win_dbg_logger` into them, call `win_dbg_logger_init_()` during app startup. + /// For Windows executables that have statically linked an instance of `win_dbg_logger` + /// into them, call `win_dbg_logger_init_()` during app startup. #[no_mangle] pub extern "C" fn $func() { init(); From eb658e2397c614fadbf33f2877589b1c274e50d2 Mon Sep 17 00:00:00 2001 From: eugenesvk Date: Mon, 6 May 2024 17:00:47 +0700 Subject: [PATCH 154/154] chore(justfile): add gui check/format commands --- justfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/justfile b/justfile index c93e6d23e..083d8ab15 100644 --- a/justfile +++ b/justfile @@ -33,6 +33,12 @@ test: fmt: cargo fmt --all +guic: + cargo check --features=gui +guif: + cargo fmt --all + cargo clippy --all --fix --features=gui -- -D warnings + use_cratesio_deps: sed -i 's/^# \(kanata-\(keyberon\|parser\|tcp-protocol\) = ".*\)$/\1/' Cargo.toml parser/Cargo.toml sed -i 's/^\(kanata-\(keyberon\|parser\|tcp-protocol\) = .*path.*\)$/# \1/' Cargo.toml parser/Cargo.toml