diff --git a/README.md b/README.md index 1c16ec8a..722feb37 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,31 @@ YAML configuration: Horizontal ``` +## Dynamically Changing Layouts Based on Number of Visible Window Containers + +With `komorebi` it is possible to define rules to automatically change the layout on a specified workspace when a +threshold of window containers is met. + +```powershell +# On the first workspace of the first monitor (0 0) +# When there are one or more window containers visible on the screen (1) +# Use the bsp layout (bsp) +komorebic workspace-layout-rule 0 0 1 bsp + +# On the first workspace of the first monitor (0 0) +# When there are five or more window containers visible on the screen (five) +# Use the custom layout stored in the home directory (~/custom.yaml) +komorebic workspace-custom-layout-rule 0 0 5 ~/custom.yaml +``` + +However, if you add workspace layout rules, you will not be able to manually change the layout of a workspace until all +layout rules for that workspace have been cleared. + +```powershell +# If you decide that workspace layout rules are not for you, you can remove them from that same workspace like this +komorebic clear-workspace-layout-rules 0 0 +``` + ## Configuration with `komorebic` As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I @@ -362,6 +387,9 @@ container-padding Set the container padding for the spe workspace-padding Set the workspace padding for the specified workspace workspace-layout Set the layout for the specified workspace workspace-custom-layout Set a custom layout for the specified workspace +workspace-layout-rule Add a dynamic layout rule for the specified workspace +workspace-custom-layout-rule Add a dynamic custom layout for the specified workspace +clear-workspace-layout-rules Clear all dynamic layout rules 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-window-container-behaviour Toggle the behaviour for new windows (stacking or dynamic tiling) @@ -432,6 +460,7 @@ used [is available here](komorebi.sample.with.lib.ahk). - [x] Main half-width window with horizontal stack layout (`vertical-stack`) - [x] 2x Main window (half and quarter-width) with horizontal stack layout (`ultrawide-vertical-stack`) - [x] Load custom layouts from JSON and YAML representations +- [x] Dynamically select layout based on the number of open windows - [x] Floating rules based on exe name, window title and class - [x] Workspace rules based on exe name and window class - [x] Additional manage rules based on exe name and window class diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index d694369e..cd983b2d 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -86,6 +86,9 @@ pub enum SocketMessage { WorkspaceName(usize, usize, String), WorkspaceLayout(usize, usize, DefaultLayout), WorkspaceLayoutCustom(usize, usize, PathBuf), + WorkspaceLayoutRule(usize, usize, usize, DefaultLayout), + WorkspaceLayoutCustomRule(usize, usize, usize, PathBuf), + ClearWorkspaceLayoutRules(usize, usize), // Configuration ReloadConfiguration, WatchConfiguration(bool), diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 2a80b19a..2cd5cd0f 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -244,6 +244,35 @@ impl WindowManager { SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => { self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?; } + SocketMessage::WorkspaceLayoutRule( + monitor_idx, + workspace_idx, + at_container_count, + layout, + ) => { + self.add_workspace_layout_default_rule( + monitor_idx, + workspace_idx, + at_container_count, + layout, + )?; + } + SocketMessage::WorkspaceLayoutCustomRule( + monitor_idx, + workspace_idx, + at_container_count, + path, + ) => { + self.add_workspace_layout_custom_rule( + monitor_idx, + workspace_idx, + at_container_count, + path, + )?; + } + SocketMessage::ClearWorkspaceLayoutRules(monitor_idx, workspace_idx) => { + self.clear_workspace_layout_rules(monitor_idx, workspace_idx)?; + } SocketMessage::CycleFocusWorkspace(direction) => { // This is to ensure that even on an empty workspace on a secondary monitor, the // secondary monitor where the cursor is focused will be used as the target for diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 68d61321..92a59a69 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -1277,6 +1277,127 @@ impl WindowManager { self.update_focused_workspace(false) } + #[tracing::instrument(skip(self))] + pub fn add_workspace_layout_default_rule( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + at_container_count: usize, + layout: DefaultLayout, + ) -> Result<()> { + tracing::info!("setting workspace layout"); + + let invisible_borders = self.invisible_borders; + let offset = self.work_area_offset; + let focused_monitor_idx = self.focused_monitor_idx(); + + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + let work_area = *monitor.work_area_size(); + let focused_workspace_idx = monitor.focused_workspace_idx(); + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut(); + rules.retain(|pair| pair.0 != at_container_count); + rules.push((at_container_count, Layout::Default(layout))); + rules.sort_by(|a, b| a.0.cmp(&b.0)); + + // If this is the focused workspace on a non-focused screen, let's update it + if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx { + workspace.update(&work_area, offset, &invisible_borders)?; + Ok(()) + } else { + Ok(self.update_focused_workspace(false)?) + } + } + + #[tracing::instrument(skip(self))] + pub fn add_workspace_layout_custom_rule( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + at_container_count: usize, + path: PathBuf, + ) -> Result<()> { + tracing::info!("setting workspace layout"); + + let invisible_borders = self.invisible_borders; + let offset = self.work_area_offset; + let focused_monitor_idx = self.focused_monitor_idx(); + + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + let work_area = *monitor.work_area_size(); + let focused_workspace_idx = monitor.focused_workspace_idx(); + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + let layout = CustomLayout::from_path_buf(path)?; + + let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut(); + rules.retain(|pair| pair.0 != at_container_count); + rules.push((at_container_count, Layout::Custom(layout))); + rules.sort_by(|a, b| a.0.cmp(&b.0)); + + // If this is the focused workspace on a non-focused screen, let's update it + if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx { + workspace.update(&work_area, offset, &invisible_borders)?; + Ok(()) + } else { + Ok(self.update_focused_workspace(false)?) + } + } + + #[tracing::instrument(skip(self))] + pub fn clear_workspace_layout_rules( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + ) -> Result<()> { + tracing::info!("setting workspace layout"); + + let invisible_borders = self.invisible_borders; + let offset = self.work_area_offset; + let focused_monitor_idx = self.focused_monitor_idx(); + + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + let work_area = *monitor.work_area_size(); + let focused_workspace_idx = monitor.focused_workspace_idx(); + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut(); + rules.clear(); + + // If this is the focused workspace on a non-focused screen, let's update it + if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx { + workspace.update(&work_area, offset, &invisible_borders)?; + Ok(()) + } else { + Ok(self.update_focused_workspace(false)?) + } + } + #[tracing::instrument(skip(self))] pub fn set_workspace_layout_default( &mut self, diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 7b674b29..89870baa 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -41,6 +41,8 @@ pub struct Workspace { floating_windows: Vec, #[getset(get = "pub", get_mut = "pub", set = "pub")] layout: Layout, + #[getset(get = "pub", get_mut = "pub", set = "pub")] + layout_rules: Vec<(usize, Layout)>, #[getset(get_copy = "pub", set = "pub")] layout_flip: Option, #[getset(get_copy = "pub", set = "pub")] @@ -69,6 +71,7 @@ impl Default for Workspace { monocle_container_restore_idx: None, floating_windows: Vec::default(), layout: Layout::Default(DefaultLayout::BSP), + layout_rules: vec![], layout_flip: None, workspace_padding: Option::from(10), container_padding: Option::from(10), @@ -164,6 +167,20 @@ impl Workspace { self.enforce_resize_constraints(); + if !self.layout_rules().is_empty() { + let mut updated_layout = None; + + for rule in self.layout_rules() { + if self.containers().len() >= rule.0 { + updated_layout = Option::from(rule.1.clone()); + } + } + + if let Some(updated_layout) = updated_layout { + self.set_layout(updated_layout); + } + } + if *self.tile() { if let Some(container) = self.monocle_container_mut() { if let Some(window) = container.focused_window_mut() { diff --git a/komorebic.lib.sample.ahk b/komorebic.lib.sample.ahk index 7510e7d1..ed72f11f 100644 --- a/komorebic.lib.sample.ahk +++ b/komorebic.lib.sample.ahk @@ -188,6 +188,18 @@ WorkspaceCustomLayout(monitor, workspace, path) { Run, komorebic.exe workspace-custom-layout %monitor% %workspace% %path%, , Hide } +WorkspaceLayoutRule(monitor, workspace, at_container_count, layout) { + Run, komorebic.exe workspace-layout-rule %monitor% %workspace% %at_container_count% %layout%, , Hide +} + +WorkspaceCustomLayoutRule(monitor, workspace, at_container_count, path) { + Run, komorebic.exe workspace-custom-layout-rule %monitor% %workspace% %at_container_count% %path%, , Hide +} + +ClearWorkspaceLayoutRules(monitor, workspace) { + Run, komorebic.exe clear-workspace-layout-rules %monitor% %workspace%, , Hide +} + WorkspaceTiling(monitor, workspace, value) { Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide } diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 9d53ee8d..4c231b6b 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -171,6 +171,15 @@ gen_workspace_subcommand_args! { Tiling: #[enum] BooleanState, } +#[derive(Parser, AhkFunction)] +pub struct ClearWorkspaceLayoutRules { + /// Monitor index (zero-indexed) + monitor: usize, + + /// Workspace index on the specified monitor (zero-indexed) + workspace: usize, +} + #[derive(Parser, AhkFunction)] pub struct WorkspaceCustomLayout { /// Monitor index (zero-indexed) @@ -183,6 +192,35 @@ pub struct WorkspaceCustomLayout { path: String, } +#[derive(Parser, AhkFunction)] +pub struct WorkspaceLayoutRule { + /// Monitor index (zero-indexed) + monitor: usize, + + /// Workspace index on the specified monitor (zero-indexed) + workspace: usize, + + /// The number of window containers on-screen required to trigger this layout rule + at_container_count: usize, + + layout: DefaultLayout, +} + +#[derive(Parser, AhkFunction)] +pub struct WorkspaceCustomLayoutRule { + /// Monitor index (zero-indexed) + monitor: usize, + + /// Workspace index on the specified monitor (zero-indexed) + workspace: usize, + + /// The number of window containers on-screen required to trigger this layout rule + at_container_count: usize, + + /// JSON or YAML file from which the custom layout definition should be loaded + path: String, +} + #[derive(Parser, AhkFunction)] struct Resize { #[clap(arg_enum)] @@ -526,6 +564,15 @@ enum SubCommand { /// Set a custom layout for the specified workspace #[clap(arg_required_else_help = true)] WorkspaceCustomLayout(WorkspaceCustomLayout), + /// Add a dynamic layout rule for the specified workspace + #[clap(arg_required_else_help = true)] + WorkspaceLayoutRule(WorkspaceLayoutRule), + /// Add a dynamic custom layout for the specified workspace + #[clap(arg_required_else_help = true)] + WorkspaceCustomLayoutRule(WorkspaceCustomLayoutRule), + /// Clear all dynamic layout rules for the specified workspace + #[clap(arg_required_else_help = true)] + ClearWorkspaceLayoutRules(ClearWorkspaceLayoutRules), /// Enable or disable window tiling for the specified workspace #[clap(arg_required_else_help = true)] WorkspaceTiling(WorkspaceTiling), @@ -760,6 +807,34 @@ fn main() -> Result<()> { .as_bytes()?, )?; } + SubCommand::WorkspaceLayoutRule(arg) => { + send_message( + &*SocketMessage::WorkspaceLayoutRule( + arg.monitor, + arg.workspace, + arg.at_container_count, + arg.layout, + ) + .as_bytes()?, + )?; + } + SubCommand::WorkspaceCustomLayoutRule(arg) => { + send_message( + &*SocketMessage::WorkspaceLayoutCustomRule( + arg.monitor, + arg.workspace, + arg.at_container_count, + resolve_windows_path(&arg.path)?, + ) + .as_bytes()?, + )?; + } + SubCommand::ClearWorkspaceLayoutRules(arg) => { + send_message( + &*SocketMessage::ClearWorkspaceLayoutRules(arg.monitor, arg.workspace) + .as_bytes()?, + )?; + } SubCommand::WorkspaceTiling(arg) => { send_message( &*SocketMessage::WorkspaceTiling(arg.monitor, arg.workspace, arg.value.into())