From e9bb6b43d6e2e79bba216c9d2b4fb2d90b60e084 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sun, 29 Dec 2024 12:28:29 -0800 Subject: [PATCH] feat(cli): add eager-focus command This commit adds a new komorebic command "eager-focus", which takes a full case-sensitive exe identifier as an argument. When komorebi receives this message, it will look through each monitor and workspace for the first matching managed window and then focus it. This allows users who have well defined workspaces and rules to bind semantic hotkeys to commands like "komorebic eager-focus Discord.exe" to immediately jump to applications instead of mentally looking up their assigned workspaces or positions within container stacks. --- komorebi/src/container.rs | 12 +++++++ komorebi/src/core/mod.rs | 1 + komorebi/src/process_command.rs | 60 +++++++++++++++++++++++++++++++++ komorebi/src/window_manager.rs | 6 +++- komorebi/src/workspace.rs | 43 +++++++++++++++++++++++ komorebic/src/main.rs | 12 +++++++ 6 files changed, 133 insertions(+), 1 deletion(-) diff --git a/komorebi/src/container.rs b/komorebi/src/container.rs index 6e9f07ef..fd49d763 100644 --- a/komorebi/src/container.rs +++ b/komorebi/src/container.rs @@ -75,6 +75,18 @@ impl Container { None } + pub fn idx_from_exe(&self, exe: &str) -> Option { + for (idx, window) in self.windows().iter().enumerate() { + if let Ok(window_exe) = window.exe() { + if exe == window_exe { + return Option::from(idx); + } + } + } + + None + } + pub fn contains_window(&self, hwnd: isize) -> bool { for window in self.windows() { if window.hwnd == hwnd { diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 131c55ea..68025d51 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -77,6 +77,7 @@ pub enum SocketMessage { Promote, PromoteFocus, PromoteWindow(OperationDirection), + EagerFocus(String), ToggleFloat, ToggleMonocle, ToggleMaximize, diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index dea13813..7053127a 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -65,6 +65,7 @@ use crate::window_manager; use crate::window_manager::WindowManager; use crate::windows_api::WindowsApi; use crate::winevent_listener; +use crate::workspace::WorkspaceWindowLocation; use crate::GlobalState; use crate::Notification; use crate::NotificationEvent; @@ -229,6 +230,65 @@ impl WindowManager { self.focus_container_in_direction(direction)?; self.promote_container_to_front()? } + SocketMessage::EagerFocus(ref exe) => { + let focused_monitor_idx = self.focused_monitor_idx(); + let focused_workspace_idx = self.focused_workspace_idx()?; + + let mut window_location = None; + let mut monitor_workspace_indices = None; + + 'search: for (monitor_idx, monitor) in self.monitors().iter().enumerate() { + for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() { + if let Some(location) = workspace.location_from_exe(exe) { + window_location = Some(location); + monitor_workspace_indices = Some((monitor_idx, workspace_idx)); + break 'search; + } + } + } + + if let Some((monitor_idx, workspace_idx)) = monitor_workspace_indices { + if monitor_idx != focused_monitor_idx { + self.focus_monitor(monitor_idx)?; + } + + if workspace_idx != focused_workspace_idx { + self.focus_workspace(workspace_idx)?; + } + } + + if let Some(location) = window_location { + match location { + WorkspaceWindowLocation::Monocle(window_idx) => { + self.focus_container_window(window_idx)?; + } + WorkspaceWindowLocation::Maximized => { + if let Some(window) = + self.focused_workspace_mut()?.maximized_window_mut() + { + window.focus(self.mouse_follows_focus)?; + } + } + WorkspaceWindowLocation::Container(container_idx, window_idx) => { + let focused_container_idx = self.focused_container_idx()?; + if container_idx != focused_container_idx { + self.focused_workspace_mut()?.focus_container(container_idx); + } + + self.focus_container_window(window_idx)?; + } + WorkspaceWindowLocation::Floating(window_idx) => { + if let Some(window) = self + .focused_workspace_mut()? + .floating_windows_mut() + .get_mut(window_idx) + { + window.focus(self.mouse_follows_focus)?; + } + } + } + } + } SocketMessage::FocusWindow(direction) => { self.focus_container_in_direction(direction)?; } diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 2c80cdad..512026d5 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -2245,7 +2245,7 @@ impl WindowManager { let len = NonZeroUsize::new(container.windows().len()) .ok_or_else(|| anyhow!("there must be at least one window in a container"))?; - if len.get() == 1 { + if len.get() == 1 && idx != 0 { bail!("there is only one window in this container"); } @@ -3270,6 +3270,10 @@ impl WindowManager { .ok_or_else(|| anyhow!("there is no container")) } + pub fn focused_container_idx(&self) -> Result { + Ok(self.focused_workspace()?.focused_container_idx()) + } + pub fn focused_container_mut(&mut self) -> Result<&mut Container> { self.focused_workspace_mut()? .focused_container_mut() diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index b0dc72d6..c3180a3e 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -119,6 +119,14 @@ impl Default for Workspace { } } +#[derive(Debug)] +pub enum WorkspaceWindowLocation { + Monocle(usize), // window_idx + Maximized, + Container(usize, usize), // container_idx, window_idx + Floating(usize), // idx in floating_windows +} + impl Workspace { pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> Result<()> { self.name = Option::from(config.name.clone()); @@ -579,6 +587,41 @@ impl Workspace { None } + pub fn location_from_exe(&self, exe: &str) -> Option { + for (container_idx, container) in self.containers().iter().enumerate() { + if let Some(window_idx) = container.idx_from_exe(exe) { + return Some(WorkspaceWindowLocation::Container( + container_idx, + window_idx, + )); + } + } + + if let Some(window) = self.maximized_window() { + if let Ok(window_exe) = window.exe() { + if exe == window_exe { + return Some(WorkspaceWindowLocation::Maximized); + } + } + } + + if let Some(container) = self.monocle_container() { + if let Some(window_idx) = container.idx_from_exe(exe) { + return Some(WorkspaceWindowLocation::Monocle(window_idx)); + } + } + + for (window_idx, window) in self.floating_windows().iter().enumerate() { + if let Ok(window_exe) = window.exe() { + if exe == window_exe { + return Some(WorkspaceWindowLocation::Floating(window_idx)); + } + } + } + + None + } + pub fn contains_managed_window(&self, hwnd: isize) -> bool { for container in self.containers() { if container.contains_window(hwnd) { diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 6b32c60b..fd6fb145 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -927,6 +927,12 @@ struct ReplaceConfiguration { path: PathBuf, } +#[derive(Parser)] +struct EagerFocus { + /// Case-sensitive exe identifier + exe: String, +} + #[derive(Parser)] #[clap(author, about, version = build::CLAP_LONG_VERSION)] struct Opts { @@ -1020,6 +1026,9 @@ enum SubCommand { /// Move the focused window in the specified cycle direction #[clap(arg_required_else_help = true)] CycleMove(CycleMove), + /// Focus the first managed window matching the given exe + #[clap(arg_required_else_help = true)] + EagerFocus(EagerFocus), /// Stack the focused window in the specified direction #[clap(arg_required_else_help = true)] Stack(Stack), @@ -1726,6 +1735,9 @@ fn main() -> Result<()> { SubCommand::CycleMove(arg) => { send_message(&SocketMessage::CycleMoveWindow(arg.cycle_direction))?; } + SubCommand::EagerFocus(arg) => { + send_message(&SocketMessage::EagerFocus(arg.exe))?; + } SubCommand::MoveToMonitor(arg) => { send_message(&SocketMessage::MoveContainerToMonitorNumber(arg.target))?; }