From 95d015e834cbdfe6c3d96e220342316d3a4f7c9b Mon Sep 17 00:00:00 2001 From: Abdalrahman Mursi Date: Sun, 17 Dec 2023 20:24:28 +0200 Subject: [PATCH 1/4] feat(refactor): refactor to allow generic usage of komokana --- src/events.rs | 105 +++++++++++ src/kanata.rs | 95 ++++++++++ src/linux.rs | 22 +++ src/main.rs | 462 +++++++------------------------------------------ src/osx.rs | 22 +++ src/windows.rs | 154 +++++++++++++++++ 6 files changed, 463 insertions(+), 397 deletions(-) create mode 100644 src/events.rs create mode 100644 src/kanata.rs create mode 100644 src/linux.rs create mode 100644 src/osx.rs create mode 100644 src/windows.rs diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..d2ba7f7 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,105 @@ +use crate::configuration::Strategy; +use crate::{CONFIG, DEFAULT_LAYER, KANATA}; +use color_eyre::Result; +use serde_json::json; +use std::io::Write; +use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState; + +#[derive(Debug, Copy, Clone)] +pub enum Event { + Show, + FocusChange, +} + +pub fn handle_event(event: Event, exe: &str, title: &str) -> Result<()> { + let target = calculate_target( + event, + exe, + title, + if matches!(event, Event::FocusChange) { + Option::from(DEFAULT_LAYER.get().unwrap().as_ref()) + } else { + None + }, + ); + + if let Some(target) = target { + let stream = &mut KANATA.get().unwrap().get_stream(); + let mut stream = stream.lock(); + let request = json!({ + "ChangeLayer": { + "new": target, + } + }); + + stream.write_all(request.to_string().as_bytes())?; + log::debug!("request sent: {request}"); + }; + + Ok(()) +} + +fn calculate_target(event: Event, exe: &str, title: &str, default: Option<&str>) -> Option { + let configuration = CONFIG.get().unwrap(); + let mut new_layer = default; + for entry in configuration { + if entry.exe == exe { + if matches!(event, Event::FocusChange) { + new_layer = Option::from(entry.target_layer.as_str()); + } + + if let Some(title_overrides) = &entry.title_overrides { + for title_override in title_overrides { + match title_override.strategy { + Strategy::StartsWith => { + if title.starts_with(&title_override.title) { + new_layer = Option::from(title_override.target_layer.as_str()); + } + } + Strategy::EndsWith => { + if title.ends_with(&title_override.title) { + new_layer = Option::from(title_override.target_layer.as_str()); + } + } + Strategy::Contains => { + if title.contains(&title_override.title) { + new_layer = Option::from(title_override.target_layer.as_str()); + } + } + Strategy::Equals => { + if title.eq(&title_override.title) { + new_layer = Option::from(title_override.target_layer.as_str()); + } + } + } + } + + // This acts like a default target layer within the application + // which defaults back to the entry's main target layer + if new_layer.is_none() { + new_layer = Option::from(entry.target_layer.as_str()); + } + } + + if matches!(event, Event::FocusChange) { + if let Some(virtual_key_overrides) = &entry.virtual_key_overrides { + for virtual_key_override in virtual_key_overrides { + if unsafe { GetKeyState(virtual_key_override.virtual_key_code) } < 0 { + new_layer = Option::from(virtual_key_override.targer_layer.as_str()); + } + } + } + + if let Some(virtual_key_ignores) = &entry.virtual_key_ignores { + for virtual_key in virtual_key_ignores { + if unsafe { GetKeyState(*virtual_key) } < 0 { + new_layer = None; + } + } + } + } + } + } + + new_layer.and_then(|new_layer| Option::from(new_layer.to_string())) +} diff --git a/src/kanata.rs b/src/kanata.rs new file mode 100644 index 0000000..103b9e2 --- /dev/null +++ b/src/kanata.rs @@ -0,0 +1,95 @@ +use color_eyre::Result; +use json_dotpath::DotPaths; +use parking_lot::Mutex; +use std::io::Read; +use std::net::TcpStream; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::time::Duration; +use crate::TMPFILE; + +pub static KANATA_DISCONNECTED: AtomicBool = AtomicBool::new(false); + +#[derive(Debug)] +pub struct Kanata { + stream: Arc>, + port: i32, +} + +impl Kanata { + pub fn new(port: i32) -> Result { + Ok(Self { + stream: Arc::new(Mutex::new(Self::connect_to_kanata(port)?)), + port, + }) + } + + pub fn get_stream(&self) -> Arc> { + self.stream.clone() + } + + fn connect_to_kanata(port: i32) -> Result { + Ok(TcpStream::connect(format!("localhost:{port}"))?) + } + + fn re_establish_connection(&self) -> Result { + KANATA_DISCONNECTED.store(true, Ordering::SeqCst); + log::warn!("kanata tcp server is no longer running"); + + let mut result = Self::connect_to_kanata(self.port); + while result.is_err() { + log::warn!("kanata tcp server is not running, retrying connection in 5 seconds"); + std::thread::sleep(Duration::from_secs(5)); + result = Self::connect_to_kanata(self.port); + } + + log::info!("reconnected to kanata on read thread"); + KANATA_DISCONNECTED.store(false, Ordering::SeqCst); + result + } + + pub fn spawn_kanata_listener(&'static self) { + let stream_read = self.get_stream(); + let tmpfile = TMPFILE.get().unwrap().to_owned(); + log::info!("listening"); + + std::thread::spawn(move || -> Result<()> { + let mut reader = stream_read.lock(); + let mut read_stream = reader.try_clone()?; + + loop { + let mut buf = vec![0; 1024]; + match read_stream.read(&mut buf) { + Ok(bytes_read) => { + let data = String::from_utf8(buf[0..bytes_read].to_vec())?; + if data == "\n" { + continue; + } + + let notification: serde_json::Value = serde_json::from_str(&data)?; + + if notification.dot_has("LayerChange.new") { + if let Some(new) = notification.dot_get::("LayerChange.new")? { + log::info!("current layer: {new}"); + if tmpfile { + let mut tmp = std::env::temp_dir(); + tmp.push("kanata_layer"); + std::fs::write(tmp, new)?; + } + } + } + } + Err(error) => { + // Connection reset + if error.raw_os_error().expect("could not get raw os error") == 10054 { + *reader = self.re_establish_connection()?; + read_stream = reader.try_clone()?; + } + } + } + } + }); + } +} diff --git a/src/linux.rs b/src/linux.rs new file mode 100644 index 0000000..c7ba3ba --- /dev/null +++ b/src/linux.rs @@ -0,0 +1,22 @@ +use color_eyre::eyre::Result; +use std::path::PathBuf; +use crate::Provider; + +pub struct Komokana { +} + +impl Provider for Komokana { + fn init() -> Result + where + Self: Sized { + todo!() + } + + fn listen(self) { + todo!() + } + + fn resolve_config_path(config: &str) -> Result { + todo!() + } +} diff --git a/src/main.rs b/src/main.rs index 0a04e12..d057247 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,35 +2,50 @@ #![allow(clippy::missing_errors_doc)] use clap::Parser; -use std::io::Read; -use std::io::Write; -use std::net::TcpStream; -use std::path::PathBuf; -use std::process::Command; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; - -use color_eyre::eyre::anyhow; -use color_eyre::Report; use color_eyre::Result; -use json_dotpath::DotPaths; -use miow::pipe::NamedPipe; -use parking_lot::Mutex; -use serde_json::json; -use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState; - -use crate::configuration::Configuration; -use crate::configuration::Strategy; +use once_cell::sync::OnceCell; +use std::time::Duration; +use std::path::PathBuf; mod configuration; - -static KANATA_DISCONNECTED: AtomicBool = AtomicBool::new(false); -static KANATA_RECONNECT_REQUIRED: AtomicBool = AtomicBool::new(false); - -const PIPE: &str = r#"\\.\pipe\"#; -const NAME: &str = "komokana"; +mod events; +mod kanata; + +use configuration::Configuration; +use kanata::Kanata; + +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +use windows::Komokana; + +#[cfg(target_os = "macos")] +mod osx; +#[cfg(target_os = "macos")] +use osx::Komokana; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +use linux::Komokana; + +static DEFAULT_LAYER: OnceCell = OnceCell::new(); +static CONFIG: OnceCell = OnceCell::new(); +static TMPFILE: OnceCell = OnceCell::new(); +static KANATA: OnceCell = OnceCell::new(); + +/// A Provider (struct named Komokana) has to implement this trait +/// init(): for initialization +/// listen(self): for the thread loop +/// resolve_config_path(&str) -> Result: +/// validates the configuration argument, returns a PathBuf if valid +pub trait Provider { + fn init() -> Result + where + Self: Sized; + fn listen(self); + fn resolve_config_path(config: &str) -> Result; +} #[derive(Debug, Parser)] #[clap(author, about, version, arg_required_else_help = true)] @@ -49,9 +64,28 @@ struct Cli { tmpfile: bool, } +fn init_configuration(config: &str) -> Result<()> { + let config_path = Komokana::resolve_config_path(config)?; + let configuration: Configuration = + serde_yaml::from_str(&std::fs::read_to_string(config_path)?)?; + CONFIG.set(configuration).unwrap(); + Ok(()) +} + +fn init_kanata(port: i32) -> Result<()> { + let kanata = Kanata::new(port)?; + KANATA.set(kanata).unwrap(); + log::debug!("connected to kanata"); + KANATA.get().unwrap().spawn_kanata_listener(); + Ok(()) +} + fn main() -> Result<()> { let cli: Cli = Cli::parse(); - let configuration = resolve_windows_path(&cli.configuration)?; + + init_configuration(&cli.configuration)?; + DEFAULT_LAYER.set(cli.default_layer).unwrap(); + TMPFILE.set(cli.tmpfile).unwrap(); if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "info"); @@ -60,380 +94,14 @@ fn main() -> Result<()> { color_eyre::install()?; env_logger::builder().format_timestamp(None).init(); - let mut komokana = Komokana::init( - configuration, - cli.kanata_port, - cli.default_layer, - cli.tmpfile, - )?; + // initializes static kanata object and starts + // kanata's listening loop + init_kanata(cli.kanata_port)?; + let komokana = Komokana::init()?; komokana.listen(); loop { std::thread::sleep(Duration::from_secs(60)); } } - -struct Komokana { - komorebi: Arc>, - kanata: Arc>, - kanata_port: i32, - configuration: Configuration, - default_layer: String, - tmpfile: bool, -} - -impl Komokana { - pub fn init( - configuration: PathBuf, - kanata_port: i32, - default_layer: String, - tmpfile: bool, - ) -> Result { - let pipe = format!("{}\\{}", PIPE, NAME); - - let configuration: Configuration = - serde_yaml::from_str(&std::fs::read_to_string(configuration)?)?; - - let named_pipe = NamedPipe::new(pipe)?; - - let mut output = Command::new("cmd.exe") - .args(["/C", "komorebic.exe", "subscribe", NAME]) - .output()?; - - while !output.status.success() { - log::warn!( - "komorebic.exe failed with error code {:?}, retrying in 5 seconds...", - output.status.code() - ); - - std::thread::sleep(Duration::from_secs(5)); - - output = Command::new("cmd.exe") - .args(["/C", "komorebic.exe", "subscribe", NAME]) - .output()?; - } - - named_pipe.connect()?; - log::debug!("connected to komorebi"); - - let stream = TcpStream::connect(format!("localhost:{kanata_port}"))?; - log::debug!("connected to kanata"); - - Ok(Self { - komorebi: Arc::new(Mutex::new(named_pipe)), - kanata: Arc::new(Mutex::new(stream)), - kanata_port, - configuration, - default_layer, - tmpfile, - }) - } - - #[allow(clippy::too_many_lines)] - pub fn listen(&mut self) { - let pipe = self.komorebi.clone(); - let mut stream = self.kanata.clone(); - let stream_read = self.kanata.clone(); - let kanata_port = self.kanata_port; - let tmpfile = self.tmpfile; - log::info!("listening"); - - std::thread::spawn(move || -> Result<()> { - let mut read_stream = stream_read.lock().try_clone()?; - drop(stream_read); - - loop { - let mut buf = vec![0; 1024]; - match read_stream.read(&mut buf) { - Ok(bytes_read) => { - let data = String::from_utf8(buf[0..bytes_read].to_vec())?; - if data == "\n" { - continue; - } - - let notification: serde_json::Value = serde_json::from_str(&data)?; - - if notification.dot_has("LayerChange.new") { - if let Some(new) = notification.dot_get::("LayerChange.new")? { - log::info!("current layer: {new}"); - if tmpfile { - let mut tmp = std::env::temp_dir(); - tmp.push("kanata_layer"); - std::fs::write(tmp, new)?; - } - } - } - } - Err(error) => { - // Connection reset - if error.raw_os_error().expect("could not get raw os error") == 10054 { - KANATA_DISCONNECTED.store(true, Ordering::SeqCst); - log::warn!("kanata tcp server is no longer running"); - - let mut result = TcpStream::connect(format!("localhost:{kanata_port}")); - while result.is_err() { - log::warn!("kanata tcp server is not running, retrying connection in 5 seconds"); - std::thread::sleep(Duration::from_secs(5)); - result = TcpStream::connect(format!("localhost:{kanata_port}")); - } - - log::info!("reconnected to kanata on read thread"); - - read_stream = result?; - - KANATA_DISCONNECTED.store(false, Ordering::SeqCst); - KANATA_RECONNECT_REQUIRED.store(true, Ordering::SeqCst); - } - } - } - } - }); - - let config = self.configuration.clone(); - let default_layer = self.default_layer.clone(); - std::thread::spawn(move || -> Result<()> { - let mut buf = vec![0; 8192]; - - loop { - let mut named_pipe = pipe.lock(); - match (*named_pipe).read(&mut buf) { - Ok(bytes_read) => { - let data = String::from_utf8(buf[0..bytes_read].to_vec())?; - if data == "\n" { - continue; - } - - let notification: serde_json::Value = match serde_json::from_str(&data) { - Ok(value) => value, - Err(error) => { - log::debug!("discarding malformed komorebi notification: {error}"); - continue; - } - }; - - if notification.dot_has("event.content.1.exe") { - if let (Some(exe), Some(title), Some(kind)) = ( - notification.dot_get::("event.content.1.exe")?, - notification.dot_get::("event.content.1.title")?, - notification.dot_get::("event.type")?, - ) { - log::debug!("processing komorebi notifcation: {kind}"); - if KANATA_DISCONNECTED.load(Ordering::SeqCst) { - log::info!("kanata is currently disconnected, will not try to send this ChangeLayer request"); - continue; - } - - match kind.as_str() { - "Show" => handle_event( - &config, - &mut stream, - &default_layer, - Event::Show, - &exe, - &title, - kanata_port, - )?, - "FocusChange" => handle_event( - &config, - &mut stream, - &default_layer, - Event::FocusChange, - &exe, - &title, - kanata_port, - )?, - _ => {} - }; - } - } - } - Err(error) => { - // Broken pipe - if error.raw_os_error().expect("could not get raw os error") == 109 { - log::warn!("komorebi is no longer running"); - named_pipe.disconnect()?; - - let mut output = Command::new("cmd.exe") - .args(["/C", "komorebic.exe", "subscribe", NAME]) - .output()?; - - while !output.status.success() { - log::warn!( - "komorebic.exe failed with error code {:?}, retrying in 5 seconds...", - output.status.code() - ); - - std::thread::sleep(Duration::from_secs(5)); - - output = Command::new("cmd.exe") - .args(["/C", "komorebic.exe", "subscribe", NAME]) - .output()?; - } - - log::warn!("reconnected to komorebi"); - named_pipe.connect()?; - } else { - return Err(Report::from(error)); - } - } - } - } - }); - } -} - -fn handle_event( - configuration: &Configuration, - stream: &mut Arc>, - default_layer: &str, - event: Event, - exe: &str, - title: &str, - kanata_port: i32, -) -> Result<()> { - let target = calculate_target( - configuration, - event, - exe, - title, - if matches!(event, Event::FocusChange) { - Option::from(default_layer) - } else { - None - }, - ); - - if let Some(target) = target { - if KANATA_RECONNECT_REQUIRED.load(Ordering::SeqCst) { - let mut result = TcpStream::connect(format!("localhost:{kanata_port}")); - while result.is_err() { - std::thread::sleep(Duration::from_secs(5)); - result = TcpStream::connect(format!("localhost:{kanata_port}")); - } - - log::info!("reconnected to kanata on write thread"); - *stream = Arc::new(Mutex::new(result?)); - KANATA_RECONNECT_REQUIRED.store(false, Ordering::SeqCst); - } - - let mut stream = stream.lock(); - let request = json!({ - "ChangeLayer": { - "new": target, - } - }); - - stream.write_all(request.to_string().as_bytes())?; - log::debug!("request sent: {request}"); - }; - - Ok(()) -} - -#[derive(Debug, Copy, Clone)] -pub enum Event { - Show, - FocusChange, -} - -fn calculate_target( - configuration: &Configuration, - event: Event, - exe: &str, - title: &str, - default: Option<&str>, -) -> Option { - let mut new_layer = default; - for entry in configuration { - if entry.exe == exe { - if matches!(event, Event::FocusChange) { - new_layer = Option::from(entry.target_layer.as_str()); - } - - if let Some(title_overrides) = &entry.title_overrides { - for title_override in title_overrides { - match title_override.strategy { - Strategy::StartsWith => { - if title.starts_with(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } - } - Strategy::EndsWith => { - if title.ends_with(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } - } - Strategy::Contains => { - if title.contains(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } - } - Strategy::Equals => { - if title.eq(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } - } - } - } - - // This acts like a default target layer within the application - // which defaults back to the entry's main target layer - if new_layer.is_none() { - new_layer = Option::from(entry.target_layer.as_str()); - } - } - - if matches!(event, Event::FocusChange) { - if let Some(virtual_key_overrides) = &entry.virtual_key_overrides { - for virtual_key_override in virtual_key_overrides { - if unsafe { GetKeyState(virtual_key_override.virtual_key_code) } < 0 { - new_layer = Option::from(virtual_key_override.targer_layer.as_str()); - } - } - } - - if let Some(virtual_key_ignores) = &entry.virtual_key_ignores { - for virtual_key in virtual_key_ignores { - if unsafe { GetKeyState(*virtual_key) } < 0 { - new_layer = None; - } - } - } - } - } - } - - new_layer.and_then(|new_layer| Option::from(new_layer.to_string())) -} - -fn resolve_windows_path(raw_path: &str) -> Result { - let path = if raw_path.starts_with('~') { - raw_path.replacen( - '~', - &dirs::home_dir() - .ok_or_else(|| anyhow!("there is no home directory"))? - .display() - .to_string(), - 1, - ) - } else { - raw_path.to_string() - }; - - let full_path = PathBuf::from(path); - - let parent = full_path - .parent() - .ok_or_else(|| anyhow!("cannot parse directory"))?; - - let file = full_path - .components() - .last() - .ok_or_else(|| anyhow!("cannot parse filename"))?; - - let mut canonicalized = std::fs::canonicalize(parent)?; - canonicalized.push(file); - - Ok(canonicalized) -} diff --git a/src/osx.rs b/src/osx.rs new file mode 100644 index 0000000..c7ba3ba --- /dev/null +++ b/src/osx.rs @@ -0,0 +1,22 @@ +use color_eyre::eyre::Result; +use std::path::PathBuf; +use crate::Provider; + +pub struct Komokana { +} + +impl Provider for Komokana { + fn init() -> Result + where + Self: Sized { + todo!() + } + + fn listen(self) { + todo!() + } + + fn resolve_config_path(config: &str) -> Result { + todo!() + } +} diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 0000000..62123e3 --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,154 @@ +use color_eyre::eyre::anyhow; +use color_eyre::Report; +use color_eyre::Result; +use json_dotpath::DotPaths; +use miow::pipe::NamedPipe; +use parking_lot::Mutex; +use std::io; +use std::io::Read; +use std::path::PathBuf; +use std::process::Command; +use std::process::Output; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; + +use crate::events::{handle_event, Event}; +use crate::Provider; + +const PIPE: &str = r#"\\.\pipe\"#; +const NAME: &str = "komokana"; + +pub struct Komokana { + komorebi: Arc>, +} + +impl Komokana { + fn connect_to_komorebi() -> io::Result { + let mut output = Command::new("cmd.exe") + .args(["/C", "komorebic.exe", "subscribe", NAME]) + .output()?; + + while !output.status.success() { + log::warn!( + "komorebic.exe failed with error code {:?}, retrying in 5 seconds...", + output.status.code() + ); + + std::thread::sleep(Duration::from_secs(5)); + + output = Command::new("cmd.exe") + .args(["/C", "komorebic.exe", "subscribe", NAME]) + .output()?; + } + Ok(output) + } +} + +impl Provider for Komokana { + fn init() -> Result { + let pipe = format!("{}\\{}", PIPE, NAME); + let named_pipe = NamedPipe::new(pipe)?; + + Self::connect_to_komorebi()?; + + named_pipe.connect()?; + log::debug!("connected to komorebi"); + + Ok(Komokana { + komorebi: Arc::new(Mutex::new(named_pipe)), + }) + } + + fn listen(self) { + let pipe = self.komorebi.clone(); + std::thread::spawn(move || -> Result<()> { + let mut buf = vec![0; 8192]; + + loop { + let mut named_pipe = pipe.lock(); + match (*named_pipe).read(&mut buf) { + Ok(bytes_read) => { + let data = String::from_utf8(buf[0..bytes_read].to_vec())?; + if data == "\n" { + continue; + } + + let notification: serde_json::Value = match serde_json::from_str(&data) { + Ok(value) => value, + Err(error) => { + log::debug!("discarding malformed komorebi notification: {error}"); + continue; + } + }; + + if notification.dot_has("event.content.1.exe") { + if let (Some(exe), Some(title), Some(kind)) = ( + notification.dot_get::("event.content.1.exe")?, + notification.dot_get::("event.content.1.title")?, + notification.dot_get::("event.type")?, + ) { + log::debug!("processing komorebi notifcation: {kind}"); + if crate::kanata::KANATA_DISCONNECTED.load(Ordering::SeqCst) { + log::info!("kanata is currently disconnected, will not try to send this ChangeLayer request"); + continue; + } + + match kind.as_str() { + "Show" => handle_event(Event::Show, &exe, &title)?, + "FocusChange" => { + handle_event(Event::FocusChange, &exe, &title)? + } + _ => {} + }; + } + } + } + Err(error) => { + // Broken pipe + if error.raw_os_error().expect("could not get raw os error") == 109 { + log::warn!("komorebi is no longer running"); + named_pipe.disconnect()?; + Self::connect_to_komorebi()?; + log::warn!("reconnected to komorebi"); + named_pipe.connect()?; + } else { + return Err(Report::from(error)); + } + } + } + } + }); + } + + fn resolve_config_path(raw_path: &str) -> Result { + let path = if raw_path.starts_with('~') { + raw_path.replacen( + '~', + &dirs::home_dir() + .ok_or_else(|| anyhow!("there is no home directory"))? + .display() + .to_string(), + 1, + ) + } else { + raw_path.to_string() + }; + + let full_path = PathBuf::from(path); + + let parent = full_path + .parent() + .ok_or_else(|| anyhow!("cannot parse directory"))?; + + let file = full_path + .components() + .last() + .ok_or_else(|| anyhow!("cannot parse filename"))?; + + let mut canonicalized = std::fs::canonicalize(parent)?; + canonicalized.push(file); + + Ok(canonicalized) + } +} From 0b922d1b3e5675816ce626cb520fe0546979fed9 Mon Sep 17 00:00:00 2001 From: Abdalrahman Mursi Date: Sun, 17 Dec 2023 20:58:13 +0200 Subject: [PATCH 2/4] forgot to commit Cargo.toml --- Cargo.lock | 1 + Cargo.toml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0877106..be2e068 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,6 +324,7 @@ dependencies = [ "json_dotpath", "log", "miow", + "once_cell", "parking_lot", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 1a1a20f..66794a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,14 @@ dirs = "5" env_logger = "0.10" json_dotpath = "1" log = "0.4" -miow = "0.5" parking_lot = "0.12" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +once_cell = "1" + +[target.'cfg(target_os = "windows")'.dependencies] +miow = "0.5" [dependencies.windows] version = "0.52" From 349884f1fcb74006f0dabec844c57d76bfbb5a85 Mon Sep 17 00:00:00 2001 From: Abdalrahman Mursi Date: Thu, 21 Dec 2023 18:13:07 +0200 Subject: [PATCH 3/4] introducing the virtual_keys feature (only for windows for now) and make an event title optional --- Cargo.toml | 7 +++--- src/events.rs | 65 +++++++++++++++++++++++++------------------------- src/main.rs | 8 ++++--- src/windows.rs | 12 ++++++++-- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 66794a3..4e9e636 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ once_cell = "1" [target.'cfg(target_os = "windows")'.dependencies] miow = "0.5" +windows = { version = "0.52", features = ["Win32_UI_Input_KeyboardAndMouse"], optional = true } -[dependencies.windows] -version = "0.52" -features = ["Win32_UI_Input_KeyboardAndMouse"] +[features] +#[cfg(target_os = "windows")] +virtual_keys = [ "dep:windows" ] \ No newline at end of file diff --git a/src/events.rs b/src/events.rs index d2ba7f7..8054a4d 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,9 +1,12 @@ use crate::configuration::Strategy; +#[cfg(feature = "virtual_keys")] +use crate::Komokana; +#[cfg(feature = "virtual_keys")] +use crate::Provider; use crate::{CONFIG, DEFAULT_LAYER, KANATA}; use color_eyre::Result; use serde_json::json; use std::io::Write; -use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState; #[derive(Debug, Copy, Clone)] pub enum Event { @@ -11,7 +14,7 @@ pub enum Event { FocusChange, } -pub fn handle_event(event: Event, exe: &str, title: &str) -> Result<()> { +pub fn handle_event(event: Event, exe: &str, title: Option<&str>) -> Result<()> { let target = calculate_target( event, exe, @@ -39,7 +42,12 @@ pub fn handle_event(event: Event, exe: &str, title: &str) -> Result<()> { Ok(()) } -fn calculate_target(event: Event, exe: &str, title: &str, default: Option<&str>) -> Option { +fn calculate_target( + event: Event, + exe: &str, + title: Option<&str>, + default: Option<&str>, +) -> Option { let configuration = CONFIG.get().unwrap(); let mut new_layer = default; for entry in configuration { @@ -48,43 +56,27 @@ fn calculate_target(event: Event, exe: &str, title: &str, default: Option<&str>) new_layer = Option::from(entry.target_layer.as_str()); } - if let Some(title_overrides) = &entry.title_overrides { - for title_override in title_overrides { - match title_override.strategy { - Strategy::StartsWith => { - if title.starts_with(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } - } - Strategy::EndsWith => { - if title.ends_with(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } - } - Strategy::Contains => { - if title.contains(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } - } - Strategy::Equals => { - if title.eq(&title_override.title) { - new_layer = Option::from(title_override.target_layer.as_str()); - } + if let Some(title) = title { + if let Some(title_overrides) = &entry.title_overrides { + for title_override in title_overrides { + if matches_with_strategy( title, &title_override.title, &title_override.strategy) { + new_layer = Option::from(title_override.target_layer.as_str()); } } - } - // This acts like a default target layer within the application - // which defaults back to the entry's main target layer - if new_layer.is_none() { - new_layer = Option::from(entry.target_layer.as_str()); + // This acts like a default target layer within the application + // which defaults back to the entry's main target layer + if new_layer.is_none() { + new_layer = Option::from(entry.target_layer.as_str()); + } } } + #[cfg(feature = "virtual_keys")] if matches!(event, Event::FocusChange) { if let Some(virtual_key_overrides) = &entry.virtual_key_overrides { for virtual_key_override in virtual_key_overrides { - if unsafe { GetKeyState(virtual_key_override.virtual_key_code) } < 0 { + if Komokana::get_key_state(virtual_key_override.virtual_key_code) < 0 { new_layer = Option::from(virtual_key_override.targer_layer.as_str()); } } @@ -92,7 +84,7 @@ fn calculate_target(event: Event, exe: &str, title: &str, default: Option<&str>) if let Some(virtual_key_ignores) = &entry.virtual_key_ignores { for virtual_key in virtual_key_ignores { - if unsafe { GetKeyState(*virtual_key) } < 0 { + if Komokana::get_key_state(*virtual_key) < 0 { new_layer = None; } } @@ -103,3 +95,12 @@ fn calculate_target(event: Event, exe: &str, title: &str, default: Option<&str>) new_layer.and_then(|new_layer| Option::from(new_layer.to_string())) } + +fn matches_with_strategy(title: &str, config_title: &str, strategy: &Strategy) -> bool { + match strategy { + Strategy::StartsWith => title.starts_with(config_title), + Strategy::EndsWith => title.ends_with(config_title), + Strategy::Contains => title.contains(config_title), + Strategy::Equals => title.eq(config_title), + } +} diff --git a/src/main.rs b/src/main.rs index d057247..68aa12d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,17 +17,17 @@ use kanata::Kanata; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] -use windows::Komokana; +pub use windows::Komokana; #[cfg(target_os = "macos")] mod osx; #[cfg(target_os = "macos")] -use osx::Komokana; +pub use osx::Komokana; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] -use linux::Komokana; +pub use linux::Komokana; static DEFAULT_LAYER: OnceCell = OnceCell::new(); static CONFIG: OnceCell = OnceCell::new(); @@ -45,6 +45,8 @@ pub trait Provider { Self: Sized; fn listen(self); fn resolve_config_path(config: &str) -> Result; + #[cfg(feature = "virtual_keys")] + fn get_key_state(key_code: i32) -> i16; } #[derive(Debug, Parser)] diff --git a/src/windows.rs b/src/windows.rs index 62123e3..82abafe 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -13,6 +13,9 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Duration; +#[cfg(feature = "virtual_keys")] +use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState; + use crate::events::{handle_event, Event}; use crate::Provider; @@ -95,9 +98,9 @@ impl Provider for Komokana { } match kind.as_str() { - "Show" => handle_event(Event::Show, &exe, &title)?, + "Show" => handle_event(Event::Show, &exe, Some(&title))?, "FocusChange" => { - handle_event(Event::FocusChange, &exe, &title)? + handle_event(Event::FocusChange, &exe, Some(&title))? } _ => {} }; @@ -121,6 +124,11 @@ impl Provider for Komokana { }); } + #[cfg(feature = "virtual_keys")] + fn get_key_state(key_code: i32) -> i16 { + unsafe { GetKeyState(key_code) } + } + fn resolve_config_path(raw_path: &str) -> Result { let path = if raw_path.starts_with('~') { raw_path.replacen( From 0ade10260e62b89372a45a9cce7bcb875d283ec2 Mon Sep 17 00:00:00 2001 From: Abdalrahman Mursi Date: Thu, 21 Dec 2023 19:50:32 +0200 Subject: [PATCH 4/4] 'adding a matching strategy to the exe configuration option' --- src/configuration.rs | 1 + src/events.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/configuration.rs b/src/configuration.rs index 12ff3fa..8185e09 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -9,6 +9,7 @@ pub type Configuration = Vec; pub struct Entry { pub exe: String, pub target_layer: String, + pub strategy: Option, pub title_overrides: Option>, pub virtual_key_overrides: Option>, pub virtual_key_ignores: Option>, diff --git a/src/events.rs b/src/events.rs index 8054a4d..9660e8b 100644 --- a/src/events.rs +++ b/src/events.rs @@ -51,7 +51,8 @@ fn calculate_target( let configuration = CONFIG.get().unwrap(); let mut new_layer = default; for entry in configuration { - if entry.exe == exe { + let entry_strategy = entry.strategy.clone().unwrap_or(Strategy::Equals); + if matches_with_strategy(exe, &entry.exe, &entry_strategy) { if matches!(event, Event::FocusChange) { new_layer = Option::from(entry.target_layer.as_str()); }