Skip to content

Commit

Permalink
feat(wm): add dynamic layout selection rules
Browse files Browse the repository at this point in the history
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
  • Loading branch information
LGUG2Z committed Mar 28, 2022
1 parent 31b8be1 commit 75234ca
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions komorebi-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
29 changes: 29 additions & 0 deletions komorebi/src/process_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
121 changes: 121 additions & 0 deletions komorebi/src/window_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions komorebi/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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() {
Expand Down
12 changes: 12 additions & 0 deletions komorebic.lib.sample.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
75 changes: 75 additions & 0 deletions komorebic/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)]
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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())
Expand Down

0 comments on commit 75234ca

Please sign in to comment.