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; + } + } + } + } + } - let idents = named.iter().map(|f| &f.ident); - let called_arguments = quote! {#(%#idents%) *} + include + }) + .map(|f| &f.ident); + + let argument_idents_clone = argument_idents.clone(); + + 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()?)?;