From 2b7c51b87b3df664d348d6439a8075cd67e06b6b Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 4 Sep 2021 13:14:45 -0700 Subject: [PATCH] refactor(ffm): add selection of ffm implementation This commit adds an optional flag to allow users to select the focus follows mouse implementation that they wish to use (komorebi or windows). The flag defaults to komorebi. The ahk-derive crate has been updated to enable the generation of wrappers fns that require flags. I pushed the ffm check up to listen_for_movements() so that we don't even try to listen to the next event from the message loop unless komorebi-flavoured ffm is enabled. re #7 --- README.md | 18 ++++- derive-ahk/src/lib.rs | 120 +++++++++++++++++++++++++++---- komorebi-core/src/lib.rs | 11 ++- komorebi.sample.with.lib.ahk | 5 +- komorebi/src/process_command.rs | 82 +++++++++++++++++++-- komorebi/src/process_movement.rs | 29 ++++---- komorebi/src/window_manager.rs | 13 ++-- komorebic.lib.sample.ahk | 8 +-- komorebic/src/main.rs | 27 +++++-- 9 files changed, 258 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 95d1e54e..a2ea4359 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,21 @@ komorebic.exe identify-tray-application exe Discord.exe # komorebic.exe identify-tray-application title [TITLE] ``` +#### Focus Follows Mouse + +Komorebi supports two focus-follows-mouse implementations; the native Windows X-Mouse implementation, which treats the +desktop, the task bar and the system tray as windows and switches focus to them eagerly, and a custom `komorebi` +implementation which only considers windows managed by `komorebi` as valid targets to switch focus to when moving the +mouse. + +When calling any of the `komorebic` commands related to focus-follows-mouse functionality, the `komorebi` +implementation will be chosen as the default implementation. You can optionally specify the `windows` implementation by +passing it as an argument to the `--implementation` flag: + +```powershell +komorebic.exe toggle-focus-follows-mouse --implementation windows +``` + ## Configuration with `komorebic` As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I @@ -263,7 +278,8 @@ used [is available here](komorebi.sample.with.lib.ahk). - [x] Toggle floating windows - [x] Toggle monocle window - [x] Toggle native maximization -- [x] Toggle focus follows mouse +- [x] Toggle X-Mouse (Native Windows) focus follows mouse +- [x] Toggle custom Komorebi focus follows mouse (desktop and system tray-aware) - [x] Toggle automatic tiling - [x] Pause all window management - [x] Load configuration on startup diff --git a/derive-ahk/src/lib.rs b/derive-ahk/src/lib.rs index 9c494baf..ddbf3577 100644 --- a/derive-ahk/src/lib.rs +++ b/derive-ahk/src/lib.rs @@ -20,7 +20,10 @@ use ::syn::DeriveInput; use ::syn::Fields; use ::syn::FieldsNamed; use ::syn::FieldsUnnamed; +use ::syn::Meta; +use ::syn::NestedMeta; +#[allow(clippy::too_many_lines)] #[proc_macro_derive(AhkFunction)] pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -29,29 +32,118 @@ pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStre match input.data { Data::Struct(s) => match s.fields { Fields::Named(FieldsNamed { named, .. }) => { - let idents = named.iter().map(|f| &f.ident); - let arguments = quote! {#(#idents), *}.to_string(); + let argument_idents = named + .iter() + // Filter out the flags + .filter(|&f| { + let mut include = true; + for attribute in &f.attrs { + if let ::std::result::Result::Ok(Meta::List(list)) = + attribute.parse_meta() + { + for nested in list.nested { + if let NestedMeta::Meta(Meta::Path(path)) = nested { + if path.is_ident("long") { + include = false; + } + } + } + } + } + + include + }) + .map(|f| &f.ident); + + let argument_idents_clone = argument_idents.clone(); - let idents = named.iter().map(|f| &f.ident); - let called_arguments = quote! {#(%#idents%) *} + let called_arguments = quote! {#(%#argument_idents_clone%) *} .to_string() .replace(" %", "%") .replace("% ", "%") .replace("%%", "% %"); - quote! { - impl AhkFunction for #name { - fn generate_ahk_function() -> String { - ::std::format!(r#" + let flag_idents = named + .iter() + // Filter only the flags + .filter(|f| { + let mut include = false; + + for attribute in &f.attrs { + if let ::std::result::Result::Ok(Meta::List(list)) = + attribute.parse_meta() + { + for nested in list.nested { + if let NestedMeta::Meta(Meta::Path(path)) = nested { + // Identify them using the --long flag name + if path.is_ident("long") { + include = true; + } + } + } + } + } + + include + }) + .map(|f| &f.ident); + + let has_flags = flag_idents.clone().count() != 0; + + if has_flags { + let flag_idents_concat = flag_idents.clone(); + let argument_idents_concat = argument_idents.clone(); + + // Concat the args and flag args if there are flags + let all_arguments = + quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *} + .to_string(); + + let flag_idents_clone = flag_idents.clone(); + let flags = quote! {#(--#flag_idents_clone) *} + .to_string() + .replace("- - ", "--"); + + let called_flag_arguments = quote! {#(%#flag_idents%) *} + .to_string() + .replace(" %", "%") + .replace("% ", "%") + .replace("%%", "% %"); + + quote! { + impl AhkFunction for #name { + fn generate_ahk_function() -> String { + ::std::format!(r#" +{}({}) {{ + Run, komorebic.exe {} {} {} {}, , Hide +}}"#, + ::std::stringify!(#name), + #all_arguments, + ::std::stringify!(#name).to_kebab_case(), + #called_arguments, + #flags, + #called_flag_arguments + ) + } + } + } + } else { + let arguments = quote! {#(#argument_idents), *}.to_string(); + + quote! { + impl AhkFunction for #name { + fn generate_ahk_function() -> String { + ::std::format!(r#" {}({}) {{ Run, komorebic.exe {} {}, , Hide }}"#, - ::std::stringify!(#name), - #arguments, - ::std::stringify!(#name).to_kebab_case(), - #called_arguments - ) - } + ::std::stringify!(#name), + #arguments, + ::std::stringify!(#name).to_kebab_case(), + #called_arguments + ) + } + } } } } diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index eb04c497..445d6a16 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -68,8 +68,8 @@ pub enum SocketMessage { IdentifyTrayApplication(ApplicationIdentifier, String), State, Query(StateQuery), - FocusFollowsMouse(bool), - ToggleFocusFollowsMouse, + FocusFollowsMouse(FocusFollowsMouseImplementation, bool), + ToggleFocusFollowsMouse(FocusFollowsMouseImplementation), } impl SocketMessage { @@ -107,6 +107,13 @@ pub enum ApplicationIdentifier { Title, } +#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] +#[strum(serialize_all = "snake_case")] +pub enum FocusFollowsMouseImplementation { + Komorebi, + Windows, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] #[strum(serialize_all = "snake_case")] pub enum Sizing { diff --git a/komorebi.sample.with.lib.ahk b/komorebi.sample.with.lib.ahk index 5646577a..6f553345 100644 --- a/komorebi.sample.with.lib.ahk +++ b/komorebi.sample.with.lib.ahk @@ -40,6 +40,7 @@ FloatRule("exe", "Wox.exe") ; Identify Minimize-to-Tray Applications IdentifyTrayApplication("exe", "Discord.exe") +IdentifyTrayApplication("exe", "Spotify.exe") ; Change the focused window, Alt + Vim direction keys !h:: @@ -170,9 +171,9 @@ return TogglePause() return -; Toggle focus follows mouse +; Enable focus follows mouse !0:: -ToggleFocusFollowsMouse() +ToggleFocusFollowsMouse("komorebi") return ; Switch to workspace diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index df2447b2..dbdb36c2 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -11,11 +11,13 @@ use parking_lot::Mutex; use uds_windows::UnixStream; use komorebi_core::ApplicationIdentifier; +use komorebi_core::FocusFollowsMouseImplementation; use komorebi_core::SocketMessage; use komorebi_core::StateQuery; use crate::window_manager; use crate::window_manager::WindowManager; +use crate::windows_api::WindowsApi; use crate::FLOAT_IDENTIFIERS; use crate::MANAGE_IDENTIFIERS; use crate::TRAY_AND_MULTI_WINDOW_CLASSES; @@ -211,14 +213,80 @@ impl WindowManager { SocketMessage::ResizeWindow(direction, sizing) => { self.resize_window(direction, sizing, Option::from(50))?; } - SocketMessage::FocusFollowsMouse(enable) => { - if enable { - self.autoraise = true; - } else { - self.autoraise = false; + SocketMessage::FocusFollowsMouse(implementation, enable) => match implementation { + FocusFollowsMouseImplementation::Komorebi => { + if WindowsApi::focus_follows_mouse()? { + tracing::warn!( + "the komorebi implementation of focus follows mouse cannot be enabled while the windows implementation is enabled" + ); + } else if enable { + self.focus_follows_mouse = Option::from(implementation); + } else { + self.focus_follows_mouse = None; + } } - } - SocketMessage::ToggleFocusFollowsMouse => self.autoraise = !self.autoraise, + FocusFollowsMouseImplementation::Windows => { + if let Some(FocusFollowsMouseImplementation::Komorebi) = + self.focus_follows_mouse + { + tracing::warn!( + "the windows implementation of focus follows mouse cannot be enabled while the komorebi implementation is enabled" + ); + } else if enable { + WindowsApi::enable_focus_follows_mouse()?; + self.focus_follows_mouse = + Option::from(FocusFollowsMouseImplementation::Windows); + } else { + WindowsApi::disable_focus_follows_mouse()?; + self.focus_follows_mouse = None; + } + } + }, + SocketMessage::ToggleFocusFollowsMouse(implementation) => match implementation { + FocusFollowsMouseImplementation::Komorebi => { + if WindowsApi::focus_follows_mouse()? { + tracing::warn!( + "the komorebi implementation of focus follows mouse cannot be toggled while the windows implementation is enabled" + ); + } else { + match self.focus_follows_mouse { + None => self.focus_follows_mouse = Option::from(implementation), + Some(FocusFollowsMouseImplementation::Komorebi) => { + self.focus_follows_mouse = None; + } + Some(FocusFollowsMouseImplementation::Windows) => { + tracing::warn!("ignoring command that could mix different focus follow mouse implementations"); + } + } + } + } + FocusFollowsMouseImplementation::Windows => { + if let Some(FocusFollowsMouseImplementation::Komorebi) = + self.focus_follows_mouse + { + tracing::warn!( + "the windows implementation of focus follows mouse cannot be toggled while the komorebi implementation is enabled" + ); + } else { + match self.focus_follows_mouse { + None => { + self.focus_follows_mouse = { + WindowsApi::enable_focus_follows_mouse()?; + Option::from(implementation) + } + } + Some(FocusFollowsMouseImplementation::Windows) => { + WindowsApi::disable_focus_follows_mouse()?; + self.focus_follows_mouse = None; + } + Some(FocusFollowsMouseImplementation::Komorebi) => { + tracing::warn!("ignoring command that could mix different focus follow mouse implementations"); + } + } + } + } + }, + SocketMessage::ReloadConfiguration => { Self::reload_configuration(); } diff --git a/komorebi/src/process_movement.rs b/komorebi/src/process_movement.rs index 0cadf234..7a6ab970 100644 --- a/komorebi/src/process_movement.rs +++ b/komorebi/src/process_movement.rs @@ -5,6 +5,8 @@ use winput::message_loop; use winput::message_loop::Event; use winput::Action; +use komorebi_core::FocusFollowsMouseImplementation; + use crate::window_manager::WindowManager; #[tracing::instrument] @@ -15,21 +17,24 @@ pub fn listen_for_movements(wm: Arc>) { let receiver = message_loop::start().expect("could not start winput message loop"); loop { - match receiver.next_event() { - // Don't want to send any raise events while we are dragging or resizing - Event::MouseButton { action, .. } => match action { - Action::Press => ignore_movement = true, - Action::Release => ignore_movement = false, - }, - Event::MouseMoveRelative { .. } => { - if !ignore_movement { - match wm.lock().raise_window_at_cursor_pos() { - Ok(_) => {} - Err(error) => tracing::error!("{}", error), + let focus_follows_mouse = wm.lock().focus_follows_mouse.clone(); + if let Some(FocusFollowsMouseImplementation::Komorebi) = focus_follows_mouse { + match receiver.next_event() { + // Don't want to send any raise events while we are dragging or resizing + Event::MouseButton { action, .. } => match action { + Action::Press => ignore_movement = true, + Action::Release => ignore_movement = false, + }, + Event::MouseMoveRelative { .. } => { + if !ignore_movement { + match wm.lock().raise_window_at_cursor_pos() { + Ok(_) => {} + Err(error) => tracing::error!("{}", error), + } } } + _ => {} } - _ => {} } } }); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index f5da1244..a9319684 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -17,6 +17,7 @@ use uds_windows::UnixListener; use komorebi_core::CycleDirection; use komorebi_core::Flip; +use komorebi_core::FocusFollowsMouseImplementation; use komorebi_core::Layout; use komorebi_core::OperationDirection; use komorebi_core::Rect; @@ -44,7 +45,7 @@ pub struct WindowManager { pub incoming_events: Arc>>, pub command_listener: UnixListener, pub is_paused: bool, - pub autoraise: bool, + pub focus_follows_mouse: Option, pub hotwatch: Hotwatch, pub virtual_desktop_id: Option, pub has_pending_raise_op: bool, @@ -54,7 +55,7 @@ pub struct WindowManager { pub struct State { pub monitors: Ring, pub is_paused: bool, - pub autoraise: bool, + pub focus_follows_mouse: Option, pub float_identifiers: Vec, pub manage_identifiers: Vec, pub layered_exe_whitelist: Vec, @@ -68,7 +69,7 @@ impl From<&mut WindowManager> for State { Self { monitors: wm.monitors.clone(), is_paused: wm.is_paused, - autoraise: wm.autoraise, + focus_follows_mouse: None, float_identifiers: FLOAT_IDENTIFIERS.lock().clone(), manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(), layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().clone(), @@ -132,7 +133,7 @@ impl WindowManager { incoming_events: incoming, command_listener: listener, is_paused: false, - autoraise: false, + focus_follows_mouse: None, hotwatch: Hotwatch::new()?, virtual_desktop_id, has_pending_raise_op: false, @@ -368,10 +369,6 @@ impl WindowManager { #[tracing::instrument(skip(self))] pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> { - if !self.autoraise { - return Ok(()); - } - if self.has_pending_raise_op { Ok(()) } else { diff --git a/komorebic.lib.sample.ahk b/komorebic.lib.sample.ahk index c11c6b52..157d836e 100644 --- a/komorebic.lib.sample.ahk +++ b/komorebic.lib.sample.ahk @@ -176,12 +176,12 @@ IdentifyTrayApplication(identifier, id) { Run, komorebic.exe identify-tray-application %identifier% %id%, , Hide } -FocusFollowsMouse(boolean_state) { - Run, komorebic.exe focus-follows-mouse %boolean_state%, , Hide +FocusFollowsMouse(boolean_state, implementation) { + Run, komorebic.exe focus-follows-mouse %boolean_state% --implementation %implementation%, , Hide } -ToggleFocusFollowsMouse() { - Run, komorebic.exe toggle-focus-follows-mouse, , Hide +ToggleFocusFollowsMouse(implementation) { + Run, komorebic.exe toggle-focus-follows-mouse --implementation %implementation%, , Hide } AhkLibrary() { diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 9bb78194..401f640b 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -30,6 +30,7 @@ use derive_ahk::AhkLibrary; use komorebi_core::ApplicationIdentifier; use komorebi_core::CycleDirection; use komorebi_core::Flip; +use komorebi_core::FocusFollowsMouseImplementation; use komorebi_core::Layout; use komorebi_core::OperationDirection; use komorebi_core::Sizing; @@ -82,7 +83,6 @@ gen_enum_subcommand_args! { FlipLayout: Flip, ChangeLayout: Layout, WatchConfiguration: BooleanState, - FocusFollowsMouse: BooleanState, Query: StateQuery, } @@ -233,6 +233,20 @@ struct WorkspaceRule { workspace: usize, } +#[derive(Clap, AhkFunction)] +struct ToggleFocusFollowsMouse { + #[clap(arg_enum, short, long, default_value = "komorebi")] + implementation: FocusFollowsMouseImplementation, +} + +#[derive(Clap, AhkFunction)] +struct FocusFollowsMouse { + #[clap(arg_enum, short, long, default_value = "komorebi")] + implementation: FocusFollowsMouseImplementation, + #[clap(arg_enum)] + boolean_state: BooleanState, +} + #[derive(Clap)] #[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)] struct Opts { @@ -361,7 +375,8 @@ enum SubCommand { #[clap(setting = AppSettings::ArgRequiredElseHelp)] FocusFollowsMouse(FocusFollowsMouse), /// Toggle focus follows mouse for the operating system - ToggleFocusFollowsMouse, + #[clap(setting = AppSettings::ArgRequiredElseHelp)] + ToggleFocusFollowsMouse(ToggleFocusFollowsMouse), /// Generate a library of AutoHotKey helper functions AhkLibrary, } @@ -462,8 +477,8 @@ fn main() -> Result<()> { &*SocketMessage::AdjustContainerPadding(arg.sizing, arg.adjustment).as_bytes()?, )?; } - SubCommand::ToggleFocusFollowsMouse => { - send_message(&*SocketMessage::ToggleFocusFollowsMouse.as_bytes()?)?; + SubCommand::ToggleFocusFollowsMouse(arg) => { + send_message(&*SocketMessage::ToggleFocusFollowsMouse(arg.implementation).as_bytes()?)?; } SubCommand::ToggleTiling => { send_message(&*SocketMessage::ToggleTiling.as_bytes()?)?; @@ -667,7 +682,9 @@ fn main() -> Result<()> { BooleanState::Disable => false, }; - send_message(&*SocketMessage::FocusFollowsMouse(enable).as_bytes()?)?; + send_message( + &*SocketMessage::FocusFollowsMouse(arg.implementation, enable).as_bytes()?, + )?; } SubCommand::ReloadConfiguration => { send_message(&*SocketMessage::ReloadConfiguration.as_bytes()?)?;