diff --git a/README.md b/README.md index bb192eee..d7e5f349 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,47 @@ You can run `komorebic.exe` to get a full list of the commands that you can use keybindings with. You can run `komorebic.exe --help` to get a full explanation of the arguments required for each command. +``` +start Start komorebi.exe as a background process +stop Stop the komorebi.exe process and restore all hidden windows +state Show a JSON representation of the current window manager state +log Tail komorebi.exe's process logs (cancel with Ctrl-C) +focus Change focus to the window in the specified direction +move Move the focused window in the specified direction +stack Stack the focused window in the specified direction +resize Resize the focused window in the specified direction +unstack Unstack the focused window +cycle-stack Cycle the focused stack in the specified cycle direction +move-to-monitor Move the focused window to the specified monitor +move-to-workspace Move the focused window to the specified workspace +focus-monitor Focus the specified monitor +focus-workspace Focus the specified workspace on the focused monitor +new-workspace Create and append a new workspace on the focused monitor +adjust-container-padding Adjust container padding on the focused workspace +adjust-workspace-padding Adjust workspace padding on the focused workspace +flip-layout Flip the layout on the focused workspace (BSP only) +promote Promote the focused window to the top of the tree +retile Force the retiling of all managed windows +ensure-workspaces Create at least this many workspaces for the specified monitor +container-padding Set the container padding for the specified workspace +workspace-padding Set the workspace padding for the specified workspace +workspace-layout Set the layout for the specified workspace +workspace-tiling Enable or disable window tiling for the specified workspace +workspace-name Set the workspace name for the specified workspace +toggle-pause Toggle the window manager on and off across all monitors +toggle-tiling Toggle window tiling on the focused workspace +toggle-float Toggle floating mode for the focused window +toggle-monocle Toggle monocle mode for the focused container +toggle-maximize Toggle native window fullscreen for the focused window +restore-windows Restore all hidden windows (debugging command) +reload-configuration Reload ~/komorebi.ahk (if it exists) +watch-configuration Toggle the automatic reloading of ~/komorebi.ahk (if it exists) +float-rule Add a rule to always float the specified application +identify-tray-application Identify an application that closes to the system tray +focus-follows-mouse Enable or disable focus follows mouse for the operating system +help Print this message or the help of the given subcommand(s) +``` + ## Features - [x] Multi-monitor @@ -158,6 +199,7 @@ each command. - [x] Identify 'close/minimize to tray' applications - [x] Toggle floating windows - [x] Toggle monocle window +- [x] Toggle native maximization - [x] Toggle focus follows mouse - [x] Toggle automatic tiling - [x] Pause all window management diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index 631b7407..6f368c61 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -32,6 +32,7 @@ pub enum SocketMessage { Promote, ToggleFloat, ToggleMonocle, + ToggleMaximize, // Current Workspace Commands AdjustContainerPadding(Sizing, i32), AdjustWorkspacePadding(Sizing, i32), diff --git a/komorebi.sample.ahk b/komorebi.sample.ahk index 4dea28f6..82f4b32b 100644 --- a/komorebi.sample.ahk +++ b/komorebi.sample.ahk @@ -129,6 +129,11 @@ return Run, komorebic.exe toggle-monocle, , Hide return +; Toggle native maximize for the focused window, Alt + Shift + = +!+=:: +Run, komorebic.exe toggle-maximize, , Hide +return + ; Flip horizontally, Alt + X !x:: Run, komorebic.exe flip-layout horizontal, , Hide diff --git a/komorebi/src/monitor.rs b/komorebi/src/monitor.rs index f2da1532..dd569975 100644 --- a/komorebi/src/monitor.rs +++ b/komorebi/src/monitor.rs @@ -70,14 +70,23 @@ impl Monitor { } } + #[tracing::instrument(skip(self))] pub fn move_container_to_workspace( &mut self, target_workspace_idx: usize, follow: bool, ) -> Result<()> { - let container = self + let workspace = self .focused_workspace_mut() - .context("there is no workspace")? + .context("there is no workspace")?; + + if workspace.maximized_window().is_some() { + return Err(eyre::anyhow!( + "cannot move native maximized window to another monitor or workspace" + )); + } + + let container = workspace .remove_focused_container() .context("there is no container")?; diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 9fa9fda7..01824bee 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -66,6 +66,7 @@ impl WindowManager { } SocketMessage::ToggleFloat => self.toggle_float()?, SocketMessage::ToggleMonocle => self.toggle_monocle()?, + SocketMessage::ToggleMaximize => self.toggle_maximize()?, SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => { self.set_container_padding(monitor_idx, workspace_idx, size)?; } diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 08a1be92..5bb816dc 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -170,7 +170,7 @@ impl WindowManager { let workspace = self.focused_workspace_mut()?; - if workspace.containers().is_empty() || !workspace.contains_window(window.hwnd) { + if !workspace.contains_window(window.hwnd) { workspace.new_container_for_window(*window); self.update_focused_workspace(false)?; } diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index fe5e71e9..24fb5c03 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -130,6 +130,18 @@ impl Window { WindowsApi::restore_window(self.hwnd()); } + pub fn maximize(self) { + let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock().unwrap(); + if let Some(idx) = programmatically_hidden_hwnds + .iter() + .position(|&hwnd| hwnd == self.hwnd) + { + programmatically_hidden_hwnds.remove(idx); + } + + WindowsApi::maximize_window(self.hwnd()); + } + pub fn focus(self) -> Result<()> { // Attach komorebi thread to Window thread let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd()); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index eda822d1..66a3faf0 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -196,7 +196,9 @@ impl WindowManager { .update_focused_workspace()?; if mouse_follows_focus { - if let Ok(window) = self.focused_window_mut() { + if let Some(window) = self.focused_workspace()?.maximized_window() { + window.focus()?; + } else if let Ok(window) = self.focused_window_mut() { window.focus()?; } else { let desktop_window = Window { @@ -310,9 +312,17 @@ impl WindowManager { tracing::info!("moving container"); let monitor = self.focused_monitor_mut().context("there is no monitor")?; - let container = monitor + let workspace = monitor .focused_workspace_mut() - .context("there is no workspace")? + .context("there is no workspace")?; + + if workspace.maximized_window().is_some() { + return Err(eyre::anyhow!( + "cannot move native maximized window to another monitor or workspace" + )); + } + + let container = workspace .remove_focused_container() .context("there is no container")?; @@ -541,6 +551,34 @@ impl WindowManager { workspace.reintegrate_monocle_container() } + #[tracing::instrument(skip(self))] + pub fn toggle_maximize(&mut self) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + + match workspace.maximized_window() { + None => self.maximize_window()?, + Some(_) => self.unmaximize_window()?, + } + + self.update_focused_workspace(false) + } + + #[tracing::instrument(skip(self))] + pub fn maximize_window(&mut self) -> Result<()> { + tracing::info!("maximizing windowj"); + + let workspace = self.focused_workspace_mut()?; + workspace.new_maximized_window() + } + + #[tracing::instrument(skip(self))] + pub fn unmaximize_window(&mut self) -> Result<()> { + tracing::info!("unmaximizing window"); + + let workspace = self.focused_workspace_mut()?; + workspace.reintegrate_maximized_window() + } + #[tracing::instrument(skip(self))] pub fn flip_layout(&mut self, layout_flip: LayoutFlip) -> Result<()> { tracing::info!("flipping layout"); diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index f3216af6..5db45071 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -68,6 +68,7 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD; use bindings::Windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE; use bindings::Windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING; use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_HIDE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE; use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE; use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION; use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS; @@ -259,6 +260,10 @@ impl WindowsApi { Self::show_window(hwnd, SW_RESTORE); } + pub fn maximize_window(hwnd: HWND) { + Self::show_window(hwnd, SW_MAXIMIZE); + } + pub fn set_foreground_window(hwnd: HWND) -> Result<()> { match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) { WindowsResult::Ok(_) => Ok(()), diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index edc79689..baaa496d 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -28,7 +28,12 @@ pub struct Workspace { monocle_container: Option, #[serde(skip_serializing)] #[getset(get_copy = "pub", set = "pub")] - monocle_restore_idx: Option, + monocle_container_restore_idx: Option, + #[getset(get = "pub", get_mut = "pub", set = "pub")] + maximized_window: Option, + #[serde(skip_serializing)] + #[getset(get_copy = "pub", set = "pub")] + maximized_window_restore_idx: Option, #[getset(get = "pub", get_mut = "pub")] floating_windows: Vec, #[getset(get_copy = "pub", set = "pub")] @@ -57,7 +62,9 @@ impl Default for Workspace { name: None, containers: Ring::default(), monocle_container: None, - monocle_restore_idx: None, + maximized_window: None, + maximized_window_restore_idx: None, + monocle_container_restore_idx: None, floating_windows: Vec::default(), layout: Layout::BSP, layout_flip: None, @@ -77,6 +84,20 @@ impl Workspace { window.hide(); } } + + if let Some(window) = self.maximized_window() { + window.hide(); + } + + if let Some(container) = self.monocle_container_mut() { + for window in container.windows_mut() { + window.hide(); + } + } + + for window in self.floating_windows() { + window.hide(); + } } pub fn restore(&mut self) -> Result<()> { @@ -87,14 +108,31 @@ impl Workspace { window.restore(); if idx == i { - to_focus = Option::from(window); + to_focus = Option::from(*window); } } } + if let Some(window) = self.maximized_window() { + window.maximize(); + } + + if let Some(container) = self.monocle_container_mut() { + for window in container.windows_mut() { + window.restore(); + } + } + + for window in self.floating_windows() { + window.restore(); + } + // Do this here to make sure that an error doesn't stop the restoration of other windows + // Maximised windows should always be drawn at the top of the Z order if let Some(window) = to_focus { - window.focus()?; + if self.maximized_window().is_none() { + window.focus()?; + } } Ok(()) @@ -110,7 +148,9 @@ impl Workspace { if let Some(container) = self.monocle_container_mut() { if let Some(window) = container.focused_window_mut() { window.set_position(&adjusted_work_area, true)?; - } + }; + } else if let Some(window) = self.maximized_window_mut() { + window.maximize(); } else if !self.containers().is_empty() { let layouts = self.layout().calculate( &adjusted_work_area, @@ -223,8 +263,26 @@ impl Workspace { } pub fn contains_window(&self, hwnd: isize) -> bool { - for x in self.containers() { - if x.contains_window(hwnd) { + for container in self.containers() { + if container.contains_window(hwnd) { + return true; + } + } + + if let Some(window) = self.maximized_window() { + if hwnd == window.hwnd { + return true; + } + } + + if let Some(container) = self.monocle_container() { + if container.contains_window(hwnd) { + return true; + } + } + + for window in self.floating_windows() { + if hwnd == window.hwnd { return true; } } @@ -497,7 +555,7 @@ impl Workspace { // it had before self.set_monocle_container(Option::from(container)); - self.set_monocle_restore_idx(Option::from(focused_idx)); + self.set_monocle_container_restore_idx(Option::from(focused_idx)); if focused_idx != 0 { self.focus_container(focused_idx - 1); @@ -513,7 +571,7 @@ impl Workspace { pub fn reintegrate_monocle_container(&mut self) -> Result<()> { let restore_idx = self - .monocle_restore_idx() + .monocle_container_restore_idx() .context("there is no monocle restore index")?; let container = self @@ -534,6 +592,71 @@ impl Workspace { .load_focused_window(); self.set_monocle_container(None); + self.set_monocle_container_restore_idx(None); + + Ok(()) + } + + pub fn new_maximized_window(&mut self) -> Result<()> { + let focused_idx = self.focused_container_idx(); + + let container = self + .focused_container_mut() + .context("there is no container")?; + + let window = container + .remove_focused_window() + .context("there is no window")?; + + if container.windows().is_empty() { + self.containers_mut().remove(focused_idx); + self.resize_dimensions_mut().remove(focused_idx); + } else { + container.load_focused_window(); + } + + self.set_maximized_window(Option::from(window)); + self.set_maximized_window_restore_idx(Option::from(focused_idx)); + + if let Some(window) = self.maximized_window() { + window.maximize(); + } + + if focused_idx != 0 { + self.focus_container(focused_idx - 1); + } + + Ok(()) + } + + pub fn reintegrate_maximized_window(&mut self) -> Result<()> { + let restore_idx = self + .maximized_window_restore_idx() + .context("there is no monocle restore index")?; + + let window = self + .maximized_window() + .as_ref() + .context("there is no monocle container")?; + + let window = *window; + if !self.containers().is_empty() && restore_idx > self.containers().len() - 1 { + self.containers_mut() + .resize(restore_idx, Container::default()); + } + + let mut container = Container::default(); + container.windows_mut().push_back(window); + self.containers_mut().insert(restore_idx, container); + + self.focus_container(restore_idx); + + self.focused_container_mut() + .context("there is no container")? + .load_focused_window(); + + self.set_maximized_window(None); + self.set_maximized_window_restore_idx(None); Ok(()) } diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index ae489150..9b4934e3 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -250,6 +250,8 @@ enum SubCommand { ToggleFloat, /// Toggle monocle mode for the focused container ToggleMonocle, + /// Toggle native maximization for the focused window + ToggleMaximize, /// Restore all hidden windows (debugging command) RestoreWindows, /// Reload ~/komorebi.ahk (if it exists) @@ -341,6 +343,9 @@ fn main() -> Result<()> { SubCommand::ToggleMonocle => { send_message(&*SocketMessage::ToggleMonocle.as_bytes()?)?; } + SubCommand::ToggleMaximize => { + send_message(&*SocketMessage::ToggleMaximize.as_bytes()?)?; + } SubCommand::WorkspaceLayout(arg) => { send_message( &*SocketMessage::WorkspaceLayout(arg.monitor, arg.workspace, arg.value)