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
This commit is contained in:
LGUG2Z
2022-03-28 14:49:13 -07:00
parent 31b8be1481
commit 75234caa98
7 changed files with 286 additions and 0 deletions

View File

@@ -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

View File

@@ -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),

View File

@@ -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

View File

@@ -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,

View File

@@ -41,6 +41,8 @@ pub struct Workspace {
floating_windows: Vec<Window>,
#[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<Axis>,
#[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() {

View File

@@ -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
}

View File

@@ -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())