diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 0cb88d720..3b6509134 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -60,6 +60,8 @@ pub enum SocketMessage { FocusStackWindow(usize), StackAll, UnstackAll, + FocusExe(Option, Option), + DisplayMonitorWorkspaceNumber(usize, usize), ResizeWindowEdge(OperationDirection, Sizing), ResizeWindowAxis(Axis, Sizing), MoveContainerToMonitorNumber(usize), @@ -87,6 +89,7 @@ pub enum SocketMessage { ToggleFloat, ToggleMonocle, ToggleMaximize, + ToggleAlwaysOnTop, ToggleWindowContainerBehaviour, ToggleFloatOverride, WindowHidingBehaviour(HidingBehaviour), diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 9fca4b839..4f2486bd1 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -230,6 +230,14 @@ impl WindowManager { self.focus_container_in_direction(direction)?; self.promote_container_to_front()? } + SocketMessage::DisplayMonitorWorkspaceNumber(monitor_idx, workspace_idx) => { + self.send_always_on_top( + Option::from(monitor_idx), + Option::from(workspace_idx), + None, + )?; + self.display_monitor_workspace(monitor_idx, workspace_idx)?; + } SocketMessage::EagerFocus(ref exe) => { let focused_monitor_idx = self.focused_monitor_idx(); let focused_workspace_idx = self.focused_workspace_idx()?; @@ -300,6 +308,9 @@ impl WindowManager { } } } + SocketMessage::FocusExe(ref exe, hwnd) => { + self.focus_window_from_exe(exe, hwnd)?; + } SocketMessage::MoveWindow(direction) => { let focused_workspace = self.focused_workspace()?; match focused_workspace.layer() { @@ -360,6 +371,7 @@ impl WindowManager { SocketMessage::ToggleFloat => self.toggle_float()?, SocketMessage::ToggleMonocle => self.toggle_monocle()?, SocketMessage::ToggleMaximize => self.toggle_maximize()?, + SocketMessage::ToggleAlwaysOnTop => self.toggle_always_on_top()?, SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => { self.set_container_padding(monitor_idx, workspace_idx, size)?; } @@ -604,6 +616,7 @@ impl WindowManager { } SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => { self.move_container_to_workspace(workspace_idx, true, None)?; + self.send_always_on_top(None, Some(workspace_idx), Some(true))?; } SocketMessage::CycleMoveContainerToWorkspace(direction) => { let focused_monitor = self @@ -620,8 +633,10 @@ impl WindowManager { ); self.move_container_to_workspace(workspace_idx, true, None)?; + self.send_always_on_top(None, Some(workspace_idx), Some(true))?; } SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => { + self.send_always_on_top(Some(monitor_idx), None, Some(true))?; let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor(monitor_idx, None, true, direction)?; } @@ -635,11 +650,14 @@ impl WindowManager { .ok_or_else(|| anyhow!("there must be at least one monitor"))?, ); + self.send_always_on_top(Some(monitor_idx), None, Some(true))?; + let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor(monitor_idx, None, true, direction)?; } SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => { self.move_container_to_workspace(workspace_idx, false, None)?; + self.send_always_on_top(None, Some(workspace_idx), Some(false))?; } SocketMessage::CycleSendContainerToWorkspace(direction) => { let focused_monitor = self @@ -656,8 +674,10 @@ impl WindowManager { ); self.move_container_to_workspace(workspace_idx, false, None)?; + self.send_always_on_top(None, Some(workspace_idx), Some(false))?; } SocketMessage::SendContainerToMonitorNumber(monitor_idx) => { + self.send_always_on_top(Some(monitor_idx), None, Some(false))?; let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor(monitor_idx, None, false, direction)?; } @@ -668,10 +688,13 @@ impl WindowManager { .ok_or_else(|| anyhow!("there must be at least one monitor"))?, ); + self.send_always_on_top(Some(monitor_idx), None, Some(false))?; + let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor(monitor_idx, None, false, direction)?; } SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => { + self.send_always_on_top(Some(monitor_idx), Some(workspace_idx), Some(false))?; let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor( monitor_idx, @@ -681,6 +704,7 @@ impl WindowManager { )?; } SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => { + self.send_always_on_top(Some(monitor_idx), Some(workspace_idx), Some(true))?; let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor( monitor_idx, @@ -693,6 +717,7 @@ impl WindowManager { if let Some((monitor_idx, workspace_idx)) = self.monitor_workspace_index_by_name(workspace) { + self.send_always_on_top(Some(monitor_idx), Some(workspace_idx), Some(false))?; let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor( monitor_idx, @@ -706,6 +731,7 @@ impl WindowManager { if let Some((monitor_idx, workspace_idx)) = self.monitor_workspace_index_by_name(workspace) { + self.send_always_on_top(Some(monitor_idx), Some(workspace_idx), Some(true))?; let direction = self.direction_from_monitor_idx(monitor_idx); self.move_container_to_monitor( monitor_idx, @@ -897,6 +923,8 @@ impl WindowManager { .ok_or_else(|| anyhow!("there must be at least one workspace"))?, ); + self.send_always_on_top(None, Option::from(workspace_idx), None)?; + self.focus_workspace(workspace_idx)?; } SocketMessage::CycleFocusEmptyWorkspace(direction) => { @@ -1015,6 +1043,7 @@ impl WindowManager { if let Some(monitor) = self.focused_monitor_mut() { if let Some(last_focused_workspace) = monitor.last_focused_workspace() { + self.send_always_on_top(None, Option::from(last_focused_workspace), None)?; self.focus_workspace(last_focused_workspace)?; } } @@ -1040,6 +1069,7 @@ impl WindowManager { } if self.focused_workspace_idx().unwrap_or_default() != workspace_idx { + self.send_always_on_top(None, Option::from(workspace_idx), None)?; self.focus_workspace(workspace_idx)?; } } @@ -1061,6 +1091,16 @@ impl WindowManager { let focused_monitor_idx = self.focused_monitor_idx(); + for i in 0..self.monitors.elements().len() { + if i != focused_monitor_idx { + self.send_always_on_top( + Option::from(i), + Option::from(workspace_idx), + None, + )?; + } + } + for (i, monitor) in self.monitors_mut().iter_mut().enumerate() { if i != focused_monitor_idx { monitor.focus_workspace(workspace_idx)?; @@ -1068,6 +1108,11 @@ impl WindowManager { } } + self.send_always_on_top( + Option::from(focused_monitor_idx), + Some(workspace_idx), + None, + )?; self.focus_workspace(workspace_idx)?; } SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => { @@ -1077,6 +1122,7 @@ impl WindowManager { let focused_pair = (focused_monitor_idx, focused_workspace_idx); if focused_pair != (monitor_idx, workspace_idx) { + self.send_always_on_top(Option::from(monitor_idx), Some(workspace_idx), None)?; self.focus_monitor(monitor_idx)?; self.focus_workspace(workspace_idx)?; } @@ -1085,6 +1131,7 @@ impl WindowManager { if let Some((monitor_idx, workspace_idx)) = self.monitor_workspace_index_by_name(name) { + self.send_always_on_top(Option::from(monitor_idx), Some(workspace_idx), None)?; self.focus_monitor(monitor_idx)?; self.focus_workspace(workspace_idx)?; } diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 298d6d8d6..88e5b980a 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -191,6 +191,13 @@ impl WindowManager { already_moved_window_handles.remove(&window.hwnd); } + + if let Some(aot) = self.always_on_top.as_mut() { + if aot.contains(&window.hwnd) { + let idx = aot.iter().position(|x| *x == window.hwnd).unwrap(); + aot.remove(idx); + } + } } WindowManagerEvent::Minimize(_, window) => { let mut hide = false; @@ -707,6 +714,7 @@ impl WindowManager { } WindowManagerEvent::MouseCapture(..) | WindowManagerEvent::Cloak(..) + | WindowManagerEvent::LocationChange(..) | WindowManagerEvent::TitleUpdate(..) => {} }; @@ -734,6 +742,7 @@ impl WindowManager { if !matches!( event, WindowManagerEvent::Show(WinEvent::ObjectNameChange, _) + | WindowManagerEvent::LocationChange(_, _) ) { tracing::info!("processed: {}", event.window().to_string()); } else { diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 06c674c77..95529bcdc 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -1156,6 +1156,7 @@ impl StaticConfig { already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())), uncloack_to_ignore: 0, known_hwnds: HashMap::new(), + always_on_top: None, }; match value.focus_follows_mouse { diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 23cc72f1e..14df50e3a 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -122,6 +122,7 @@ pub struct WindowManager { pub uncloack_to_ignore: usize, /// Maps each known window hwnd to the (monitor, workspace) index pair managing it pub known_hwnds: HashMap, + pub always_on_top: Option>, } #[allow(clippy::struct_excessive_bools)] @@ -438,6 +439,7 @@ impl WindowManager { already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())), uncloack_to_ignore: 0, known_hwnds: HashMap::new(), + always_on_top: None, }) } @@ -1786,6 +1788,302 @@ impl WindowManager { self.update_focused_workspace(mouse_follows_focus, true) } + #[tracing::instrument(skip(self))] + pub fn focus_window_from_exe( + &mut self, + exe: &Option, + hwnd: Option, + ) -> Result<()> { + let mut monitor_idx = 0; + let mut workspace_idx = 0; + let mut hwnd_from_exe: isize = 0; + let mut bool = false; + let mouse_follows_focus = self.mouse_follows_focus; + let offset = self.work_area_offset; + + 'outer: for (i, m) in self.monitors.elements().iter().enumerate() { + for (j, w) in m.workspaces().iter().enumerate() { + if let Some(hwnd) = hwnd { + if w.contains_managed_window(hwnd) { + monitor_idx = i; + workspace_idx = j; + hwnd_from_exe = hwnd; + bool = true; + break 'outer; + } + } + if let Some(exe) = exe { + if let Some(hwndexe) = w.hwnd_from_exe(&exe) { + monitor_idx = i; + workspace_idx = j; + hwnd_from_exe = hwndexe; + bool = true; + if !hwnd.is_some() { + break 'outer; + } + } + } + } + } + + if bool { + if self.focused_monitor_idx() != monitor_idx { + self.focus_monitor(monitor_idx)?; + } + if self.focused_workspace_idx()? != workspace_idx { + self.focused_monitor_mut() + .ok_or_else(|| anyhow!("there is no monitor"))? + .focus_workspace(workspace_idx)?; + } + let target_monitor = self + .focused_monitor_mut() + .ok_or_else(|| anyhow!("there is no monitor"))?; + target_monitor + .workspaces_mut() + .get_mut(workspace_idx) + .ok_or_else(|| anyhow!("there is no workspace"))? + .focus_container_by_window(hwnd_from_exe)?; + target_monitor.load_focused_workspace(mouse_follows_focus)?; + target_monitor.update_focused_workspace(offset)?; + self.update_focused_workspace(self.mouse_follows_focus, true)?; + } else { + Err(anyhow!("there is no window with that exe"))? + } + Ok(()) + } + + #[tracing::instrument(skip(self))] + pub fn display_monitor_workspace( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + ) -> Result<()> { + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_else(|| anyhow!("There is no monitor"))?; + + monitor.focus_workspace(workspace_idx)?; + monitor.load_focused_workspace(false)?; + + let focused_workspace_idx = self.focused_workspace_idx()?; + self.focus_workspace(focused_workspace_idx)?; + + Ok(()) + } + + #[tracing::instrument(skip(self))] + pub fn send_always_on_top( + &mut self, + monitor_idx: Option, + workspace_idx: Option, + follows: Option, + ) -> Result<()> { + let mut contains_always_on_top = false; + let last_window = if let Ok(window) = self.focused_window() { + window.hwnd + } else { + if self.focused_workspace()?.floating_windows().len() > 0 { + self.focused_workspace()?.floating_windows()[0].hwnd + } else { + return Ok(()); + } + }; + let aot = self.always_on_top.clone(); + let mut windows_vec = vec![]; + + let flw_contains = if let Some(aot) = self.always_on_top.as_ref() { + if aot.len() == 0 { + return Ok(()); + } + if let Some(flw) = follows { + if let Ok(fc) = self.focused_container() { + let contains = fc.windows().iter().any(|w| aot.contains(&w.hwnd)); + if flw && contains { + windows_vec = fc.windows().into_iter().map(|w| w.hwnd).collect::>(); + true + } else if !flw && !contains { + return Ok(()); + } else if !flw && contains { + Err(anyhow!("cannot send an always on top window"))? + } else { + false + } + } else { + false + } + } else { + false + } + } else { + return Ok(()); + }; + self.check_aot_windows()?; + + aot.ok_or_else(|| anyhow!("there is no always on Top windows"))? + .iter() + .filter(|&&window| { + let mut is_window = false; + if flw_contains { + if windows_vec.contains(&&window) { + is_window = true; + } + } + !is_window + }) + .try_for_each(|&window| { + if let Some(monitor_idx) = monitor_idx { + if self + .monitors() + .get(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .focused_workspace() + .unwrap() + .contains_managed_window(window) + { + let idx = self + .monitors() + .get(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .focused_workspace() + .unwrap() + .container_idx_for_window(window) + .ok_or_else(|| anyhow!("there is no container at this index"))?; + + let con = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .focused_workspace_mut() + .unwrap() + .remove_container_by_idx(idx) + .unwrap(); + + self.monitors_mut() + .get_mut(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .add_container(con, workspace_idx)?; + } + } else { + if self + .focused_workspace() + .unwrap() + .contains_managed_window(window) + { + let idx = self + .focused_workspace() + .unwrap() + .container_idx_for_window(window); + let con = self + .focused_workspace_mut() + .unwrap() + .remove_container_by_idx(idx.unwrap()) + .ok_or_else(|| anyhow!("there is no container at this index"))?; + + self.focused_monitor_mut() + .ok_or_else(|| anyhow!("there is no focused monitor"))? + .add_container(con, workspace_idx)?; + + contains_always_on_top = true; + + //self.update_focused_workspace(mff, false).unwrap() + } else if self + .focused_workspace()? + .floating_windows() + .iter() + .any(|w| w.hwnd == window) + { + let idx = self + .focused_workspace_mut()? + .floating_windows() + .iter() + .position(|x| x.hwnd == window) + .unwrap(); + let float_window = self + .focused_workspace_mut()? + .floating_windows_mut() + .remove(idx); + self.focused_monitor_mut() + .ok_or_else(|| anyhow!("there is no focused workspace"))? + .workspaces_mut() + .get_mut(workspace_idx.unwrap()) + .ok_or_else(|| anyhow!("there is no workspace at this index"))? + .floating_windows_mut() + .push(float_window); + } + } + + Ok::<(), color_eyre::eyre::Error>(()) + })?; + + if contains_always_on_top + && self.focused_workspace()?.containers().len() != 0 + && self + .focused_workspace()? + .contains_managed_window(last_window) + { + self.focused_workspace_mut()? + .focus_container_by_window(last_window)?; + } + Ok(()) + } + + pub fn check_aot_windows(&mut self) -> Result<()> { + let mut not_contains = vec![]; + if self.always_on_top.is_none() { + return Ok(()); + } + for (i, hwnd) in self + .always_on_top + .as_ref() + .ok_or_else(|| anyhow!("there is no always on top windows"))? + .iter() + .enumerate() + { + let mut not_contains_bool = true; + 'monitor: for monitor in self.monitors.elements().iter() { + for workspace in monitor.workspaces().iter() { + if workspace.contains_managed_window(*hwnd) { + not_contains_bool = false; + break 'monitor; + } + } + } + + if not_contains_bool { + not_contains.push(i); + } + } + not_contains.iter().for_each(|&i| { + self.always_on_top.as_mut().unwrap().remove(i); + }); + Ok(()) + } + + pub fn toggle_always_on_top(&mut self) -> Result<()> { + self.check_aot_windows()?; + + let focused_con = self.focused_container().unwrap().clone(); + + focused_con.windows().iter().try_for_each(|window| { + if let Some(always_on_top) = self.always_on_top.as_mut() { + if always_on_top.contains(&window.hwnd) { + let idx = always_on_top + .iter() + .position(|x| *x == window.hwnd) + .unwrap(); + always_on_top.remove(idx); + } else { + always_on_top.push(window.hwnd.clone()) + } + } else { + self.always_on_top = Some(vec![window.hwnd.clone()]) + } + Ok::<(), color_eyre::eyre::Error>(()) + })?; + Ok(()) + } + #[tracing::instrument(skip(self))] pub fn move_container_to_monitor( &mut self, diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index dec2a0b17..cd4754f94 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -29,6 +29,7 @@ pub enum WindowManagerEvent { Unmanage(Window), Raise(Window), TitleUpdate(WinEvent, Window), + LocationChange(WinEvent, Window), } impl Display for WindowManagerEvent { @@ -79,6 +80,9 @@ impl Display for WindowManagerEvent { Self::TitleUpdate(winevent, window) => { write!(f, "TitleUpdate (WinEvent: {winevent}, Window: {window})") } + Self::LocationChange(winevent, window) => { + write!(f, "LocationChange (WinEvent: {winevent}, Window: {window})") + } } } } @@ -99,7 +103,8 @@ impl WindowManagerEvent { | Self::Raise(window) | Self::Manage(window) | Self::Unmanage(window) - | Self::TitleUpdate(_, window) => window, + | Self::TitleUpdate(_, window) + | Self::LocationChange(_, window) => window, } } @@ -123,6 +128,7 @@ impl WindowManagerEvent { WindowManagerEvent::Unmanage(_) => "Unmanage", WindowManagerEvent::Raise(_) => "Raise", WindowManagerEvent::TitleUpdate(_, _) => "TitleUpdate", + WindowManagerEvent::LocationChange(_, _) => "LocationChange", } } @@ -138,6 +144,7 @@ impl WindowManagerEvent { | WindowManagerEvent::MoveResizeStart(event, _) | WindowManagerEvent::MoveResizeEnd(event, _) | WindowManagerEvent::MouseCapture(event, _) + | WindowManagerEvent::LocationChange(event, _) | WindowManagerEvent::TitleUpdate(event, _) => Some(event.to_string()), WindowManagerEvent::Manage(_) | WindowManagerEvent::Unmanage(_) @@ -213,6 +220,7 @@ impl WindowManagerEvent { Option::from(Self::TitleUpdate(winevent, window)) } } + WinEvent::ObjectLocationChange => Option::from(Self::LocationChange(winevent, window)), _ => None, } } diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 68e7cca51..a3335fb31 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -835,7 +835,7 @@ impl Workspace { None } - fn container_idx_for_window(&self, hwnd: isize) -> Option { + pub(crate) fn container_idx_for_window(&self, hwnd: isize) -> Option { let mut idx = None; for (i, x) in self.containers().iter().enumerate() { if x.contains_window(hwnd) { diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 185d69463..fa9feb681 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -829,6 +829,23 @@ struct Kill { masir: bool, } +#[derive(Parser)] +struct Exe { + /// executable name + #[clap(short, long)] + exe: Option, + /// hwnd handle of the window + #[clap(long)] + hwnd: Option, +} + +#[derive(Parser)] +struct DisplayMonitorWorkspace { + /// Monitor index (zero-indexed) + monitor: usize, + /// Workspace index on the specified monitor (zero-indexed) + workspace: usize, +} #[derive(Parser)] struct SaveResize { /// File to which the resize layout dimensions should be saved @@ -1022,9 +1039,15 @@ enum SubCommand { #[clap(arg_required_else_help = true)] #[clap(alias = "load")] LoadResize(LoadResize), + /// Display the workspace index at monitor index + #[clap(arg_required_else_help = true)] + DisplayMonitorWorkspace(DisplayMonitorWorkspace), /// Change focus to the window in the specified direction #[clap(arg_required_else_help = true)] Focus(Focus), + /// Change focus to the window with the specified executable + #[clap(arg_required_else_help = true)] + FocusExe(Exe), /// Move the focused window in the specified direction #[clap(arg_required_else_help = true)] Move(Move), @@ -1283,6 +1306,8 @@ enum SubCommand { ToggleMonocle, /// Toggle native maximization for the focused window ToggleMaximize, + /// Toggle Always on top mode for the focused window + ToggleAlwaysOnTop, /// Restore all hidden windows (debugging command) RestoreWindows, /// Force komorebi to manage the focused window @@ -1758,9 +1783,18 @@ fn main() -> Result<()> { println!("{line}"); } } + SubCommand::DisplayMonitorWorkspace(arg) => { + send_message(&SocketMessage::DisplayMonitorWorkspaceNumber( + arg.monitor, + arg.workspace, + ))?; + } SubCommand::Focus(arg) => { send_message(&SocketMessage::FocusWindow(arg.operation_direction))?; } + SubCommand::FocusExe(arg) => { + send_message(&SocketMessage::FocusExe(arg.exe, arg.hwnd))?; + } SubCommand::ForceFocus => { send_message(&SocketMessage::ForceFocus)?; } @@ -1947,6 +1981,9 @@ fn main() -> Result<()> { SubCommand::ToggleMaximize => { send_message(&SocketMessage::ToggleMaximize)?; } + SubCommand::ToggleAlwaysOnTop => { + send_message(&SocketMessage::ToggleAlwaysOnTop)?; + } SubCommand::WorkspaceLayout(arg) => { send_message(&SocketMessage::WorkspaceLayout( arg.monitor,