From 75234caa983d02e48ec625c40f27b4d485bcedf9 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Mon, 28 Mar 2022 14:49:13 -0700 Subject: [PATCH] feat(wm): add dynamic layout selection rules This commit adds a new feature which allows the user to specify a set of rules for a specific workspace that will be used to calculate which layout to apply to that workspace at any given time. The rule consists of a usize, which identifies the threshold of window containers which need to be visible on the workspace to activate the rule, and a layout, which will be applied to the workspace when the rule is activated. Both default and custom layouts can be used in workspace layout rules. When a workspace has layout rules in effect, manually changing the layout will not work again until the rules for that workspace have been cleared. This feature came about after trying but failing to modify the custom layout code in such a way that the width percentage of a primary column in a custom layout might be propagated to the fallback columnar layout when the tertiary column threshold is not met. Although this new feature introduces more complexity, it is strictly opt-in and can be completely ignored if the user has no interest in adjusting layouts based on the visible window count. re #121 --- README.md | 29 ++++++++ komorebi-core/src/lib.rs | 3 + komorebi/src/process_command.rs | 29 ++++++++ komorebi/src/window_manager.rs | 121 ++++++++++++++++++++++++++++++++ komorebi/src/workspace.rs | 17 +++++ komorebic.lib.sample.ahk | 12 ++++ komorebic/src/main.rs | 75 ++++++++++++++++++++ 7 files changed, 286 insertions(+) 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())