From c165172b5a636f383739a99dc51a35fe4a3a77ef Mon Sep 17 00:00:00 2001 From: Csaba Date: Tue, 10 Feb 2026 23:25:29 +0100 Subject: [PATCH] feat(cli): new layout-ratios command The implementation adds a new layout-ratio CLI command to komorebi that allows users to dynamically set column and row ratios for layouts at runtime via komorebic layout-ratio --columns 0.3 0.4 --rows 0.5. A public validate_ratios function was added to komorebi-layouts that clamps ratio values between 0.1 and 0.9, truncates when the cumulative sum would reach or exceed 1.0, and limits to a maximum of 5 ratios. This function is shared between config file deserialization and the new CLI command, ensuring consistent validation behavior. The SocketMessage enum in komorebi/src/core/mod.rs has a new LayoutRatios variant. The handler in process_command.rs uses the shared validate_ratios function to process the ratios before applying them to the focused workspace's layout options. When the CLI command is called without any arguments, it prints a helpful message instead of returning an error. --- .gitattributes | 1 + komorebi-layouts/src/default_layout.rs | 55 +++++++++++++++----------- komorebi/src/core/mod.rs | 2 + komorebi/src/process_command.rs | 23 +++++++++++ komorebic/src/main.rs | 21 ++++++++++ schema.bar.json | 40 +++++++++++++++++++ 6 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a43336b5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.json text diff \ No newline at end of file diff --git a/komorebi-layouts/src/default_layout.rs b/komorebi-layouts/src/default_layout.rs index 45a22663..f5955cce 100644 --- a/komorebi-layouts/src/default_layout.rs +++ b/komorebi-layouts/src/default_layout.rs @@ -23,6 +23,36 @@ pub const DEFAULT_RATIO: f32 = 0.5; /// Default secondary ratio value for UltrawideVerticalStack layout pub const DEFAULT_SECONDARY_RATIO: f32 = 0.25; +/// Validates and converts a Vec of ratios into a fixed-size array. +/// - Clamps values to MIN_RATIO..MAX_RATIO range +/// - Truncates when cumulative sum reaches or exceeds 1.0 +/// - Limits to MAX_RATIOS values +#[must_use] +pub fn validate_ratios(ratios: &[f32]) -> [Option; MAX_RATIOS] { + let mut arr = [None; MAX_RATIOS]; + let mut cumulative_sum = 0.0_f32; + + for (i, &val) in ratios.iter().take(MAX_RATIOS).enumerate() { + let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO); + + // Only add this ratio if cumulative sum stays below 1.0 + if cumulative_sum + clamped_val < 1.0 { + arr[i] = Some(clamped_val); + cumulative_sum += clamped_val; + } else { + // Stop adding ratios - cumulative sum would reach or exceed 1.0 + tracing::debug!( + "Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0", + i, + cumulative_sum, + clamped_val + ); + break; + } + } + arr +} + #[derive( Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum, )] @@ -137,30 +167,7 @@ where D: serde::Deserializer<'de>, { let opt: Option> = Option::deserialize(deserializer)?; - Ok(opt.map(|vec| { - let mut arr = [None; MAX_RATIOS]; - let mut cumulative_sum = 0.0_f32; - - for (i, &val) in vec.iter().take(MAX_RATIOS).enumerate() { - let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO); - - // Only add this ratio if cumulative sum stays below 1.0 - if cumulative_sum + clamped_val < 1.0 { - arr[i] = Some(clamped_val); - cumulative_sum += clamped_val; - } else { - // Stop adding ratios - cumulative sum would reach or exceed 1.0 - tracing::debug!( - "Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0", - i, - cumulative_sum, - clamped_val - ); - break; - } - } - arr - })) + Ok(opt.map(|vec| validate_ratios(&vec))) } /// Helper to serialize [Option; MAX_RATIOS] as a compact array (without trailing nulls) diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 364bcd4c..54bbc3db 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -39,6 +39,7 @@ pub use komorebi_layouts::OperationDirection; pub use komorebi_layouts::Rect; pub use komorebi_layouts::ScrollingLayoutOptions; pub use komorebi_layouts::Sizing; +pub use komorebi_layouts::validate_ratios; // Local modules and exports pub use animation::AnimationStyle; @@ -118,6 +119,7 @@ pub enum SocketMessage { AdjustWorkspacePadding(Sizing, i32), ChangeLayout(DefaultLayout), CycleLayout(CycleDirection), + LayoutRatios(Option>, Option>), ScrollingLayoutColumns(NonZeroUsize), ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf), FlipLayout(Axis), diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 136c048f..3cbabefe 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -957,6 +957,29 @@ impl WindowManager { } SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?, SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?, + SocketMessage::LayoutRatios(ref columns, ref rows) => { + use crate::core::validate_ratios; + + let focused_workspace = self.focused_workspace_mut()?; + + let mut options = focused_workspace.layout_options.unwrap_or(LayoutOptions { + scrolling: None, + grid: None, + column_ratios: None, + row_ratios: None, + }); + + if let Some(cols) = columns { + options.column_ratios = Some(validate_ratios(cols)); + } + + if let Some(rws) = rows { + options.row_ratios = Some(validate_ratios(rws)); + } + + focused_workspace.layout_options = Some(options); + self.update_focused_workspace(false, false)?; + } SocketMessage::ChangeLayoutCustom(ref path) => { self.change_workspace_custom_layout(path)?; } diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 4742efa6..40c2c009 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -1001,6 +1001,16 @@ struct ScrollingLayoutColumns { count: NonZeroUsize, } +#[derive(Parser)] +struct LayoutRatios { + /// Column width ratios (space-separated values between 0.1 and 0.9) + #[clap(short, long, num_args = 1..)] + columns: Option>, + /// Row height ratios (space-separated values between 0.1 and 0.9) + #[clap(short, long, num_args = 1..)] + rows: Option>, +} + #[derive(Parser)] struct License { /// Email address associated with an Individual Commercial Use License @@ -1267,6 +1277,8 @@ enum SubCommand { /// Set the number of visible columns for the Scrolling layout on the focused workspace #[clap(arg_required_else_help = true)] ScrollingLayoutColumns(ScrollingLayoutColumns), + /// Set the layout column and row ratios for the focused workspace + LayoutRatios(LayoutRatios), /// Load a custom layout from file for the focused workspace #[clap(hide = true)] #[clap(arg_required_else_help = true)] @@ -2934,6 +2946,15 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { SubCommand::ScrollingLayoutColumns(args) => { send_message(&SocketMessage::ScrollingLayoutColumns(args.count))?; } + SubCommand::LayoutRatios(args) => { + if args.columns.is_none() && args.rows.is_none() { + println!( + "No ratios provided, nothing to change. Use --columns or --rows to specify ratios." + ); + } else { + send_message(&SocketMessage::LayoutRatios(args.columns, args.rows))?; + } + } SubCommand::LoadCustomLayout(args) => { send_message(&SocketMessage::ChangeLayoutCustom(args.path))?; } diff --git a/schema.bar.json b/schema.bar.json index 6c7396a7..6bc7d36f 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -5417,6 +5417,46 @@ "content" ] }, + { + "type": "object", + "properties": { + "content": { + "type": "array", + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "type": [ + "array", + "null" + ], + "items": { + "type": "number", + "format": "float" + } + }, + { + "type": [ + "array", + "null" + ], + "items": { + "type": "number", + "format": "float" + } + } + ] + }, + "type": { + "type": "string", + "const": "LayoutRatios" + } + }, + "required": [ + "type", + "content" + ] + }, { "type": "object", "properties": {