diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/core/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/core/src/window.rs | 32 | ||||
| -rw-r--r-- | crates/ctl/src/main.rs | 56 | ||||
| -rw-r--r-- | crates/platform/src/ipc/dispatch.rs | 105 | ||||
| -rw-r--r-- | crates/platform/src/ipc/tests.rs | 97 | ||||
| -rw-r--r-- | crates/platform/src/ipc/types.rs | 18 |
6 files changed, 305 insertions, 5 deletions
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 7b728d9..ed24e78 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -24,7 +24,7 @@ pub use regex::Regex; pub use rules::{WindowMatch, WindowRule, WindowRuleActions}; pub use theme::{format_hex_color, parse_hex_color, ThemeConfig}; pub use window::{ - classify_menu_source, parse_button_order, ButtonOrder, GlobalMenu, MenuSource, ResizeEdge, TitlebarButton, TitlebarHit, Window, WindowId, + classify_menu_source, format_button_order, parse_button_order, ButtonOrder, GlobalMenu, MenuSource, ResizeEdge, TitlebarButton, TitlebarHit, Window, WindowId, BUTTON_CLUSTER_MARGIN, BUTTON_PITCH, RESIZE_MARGIN, TITLEBAR_HEIGHT, }; pub use workspace::{Workspace, WorkspaceId}; diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs index a8de352..21faeff 100644 --- a/crates/core/src/window.rs +++ b/crates/core/src/window.rs @@ -838,6 +838,23 @@ pub fn parse_button_order(s: &str) -> Option<ButtonOrder> { Some(order) } +/// [`parse_button_order`]'s exact inverse - `"close,minimize,maximize"`, +/// lowercase, comma-separated in the order the buttons actually render. +/// Exists for settings readback, same reasoning as `format_hex_color`: +/// a caller reading `button_order` back over IPC should get the identical +/// string shape `srd set button_order` itself accepts. +pub fn format_button_order(order: ButtonOrder) -> String { + order + .iter() + .map(|b| match b { + TitlebarButton::Close => "close", + TitlebarButton::Minimize => "minimize", + TitlebarButton::Maximize => "maximize", + }) + .collect::<Vec<_>>() + .join(",") +} + #[cfg(test)] mod button_order_tests { use super::*; @@ -869,6 +886,21 @@ mod button_order_tests { } #[test] + fn format_button_order_round_trips_through_parse_button_order() { + for order in [ + [TitlebarButton::Close, TitlebarButton::Minimize, TitlebarButton::Maximize], + [TitlebarButton::Maximize, TitlebarButton::Minimize, TitlebarButton::Close], + ] { + assert_eq!(parse_button_order(&format_button_order(order)), Some(order)); + } + } + + #[test] + fn format_button_order_matches_the_exact_shape_set_accepts() { + assert_eq!(format_button_order([TitlebarButton::Close, TitlebarButton::Minimize, TitlebarButton::Maximize]), "close,minimize,maximize"); + } + + #[test] fn rejects_an_unknown_token() { assert_eq!(parse_button_order("close,minimize,help"), None); } diff --git a/crates/ctl/src/main.rs b/crates/ctl/src/main.rs index fb7196c..1751483 100644 --- a/crates/ctl/src/main.rs +++ b/crates/ctl/src/main.rs @@ -186,7 +186,7 @@ fn build_request(args: &[String]) -> Result<String, String> { // as booleans at all, not a string it then has to reject. Some("set") => { let key = args.get(1).ok_or( - "set needs a key (border_width/border_color/corner_radius/gap_inner/gap_outer/master_ratio/master_count/shadows/rounded_corners/animations/night_light/reading_mode/phone_mode/multi_cursor/decoration_mode)", + "set needs a key (border_width/border_color/corner_radius/gap_inner/gap_outer/master_ratio/master_count/shadows/rounded_corners/animations/night_light/reading_mode/phone_mode/multi_cursor/per_monitor/decoration_mode/button_style/button_side/button_order/title_centered/button_glyph_always/desktop_icons/desktop_icons_all_monitors)", )?; let raw = args.get(2).ok_or("set needs a value")?; let value = match key.as_str() { @@ -194,7 +194,8 @@ fn build_request(args: &[String]) -> Result<String, String> { raw.parse::<u64>().map_err(|_| format!("{key} needs a numeric value"))?.to_string() } "master_ratio" => raw.parse::<f64>().map_err(|_| format!("{key} needs a numeric value"))?.to_string(), - "shadows" | "rounded_corners" | "animations" | "night_light" | "reading_mode" | "phone_mode" | "multi_cursor" => match raw.as_str() { + "shadows" | "rounded_corners" | "animations" | "night_light" | "reading_mode" | "phone_mode" | "multi_cursor" | "per_monitor" | "title_centered" | "button_glyph_always" + | "desktop_icons" | "desktop_icons_all_monitors" => match raw.as_str() { "true" | "false" => raw.clone(), _ => return Err(format!("{key} needs 'true' or 'false'")), }, @@ -202,7 +203,15 @@ fn build_request(args: &[String]) -> Result<String, String> { "server" | "client" => format!("{:?}", raw), _ => return Err(format!("{key} needs 'server' or 'client'")), }, - "border_color" => format!("{:?}", raw), + "button_style" => match raw.as_str() { + "traffic_lights" | "traditional" => format!("{:?}", raw), + _ => return Err(format!("{key} needs 'traffic_lights' or 'traditional'")), + }, + "button_side" => match raw.as_str() { + "left" | "right" => format!("{:?}", raw), + _ => return Err(format!("{key} needs 'left' or 'right'")), + }, + "border_color" | "button_order" => format!("{:?}", raw), _ => return Err(format!("unknown set key '{key}'")), }; Ok(format!(r#"{{"cmd":"set","key":"{key}","value":{value}}}"#)) @@ -432,7 +441,17 @@ fn print_usage() { eprintln!(" srd set reading_mode <true|false>"); eprintln!(" srd set phone_mode <true|false>"); eprintln!(" srd set multi_cursor <true|false>"); + eprintln!(" srd set per_monitor <true|false>"); + eprintln!(" srd set master_ratio <0.1-0.9>"); + eprintln!(" srd set master_count <n>"); eprintln!(" srd set decoration_mode <server|client>"); + eprintln!(" srd set button_style <traffic_lights|traditional>"); + eprintln!(" srd set button_side <left|right>"); + eprintln!(" srd set button_order <close,minimize,maximize order>"); + eprintln!(" srd set title_centered <true|false>"); + eprintln!(" srd set button_glyph_always <true|false>"); + eprintln!(" srd set desktop_icons <true|false>"); + eprintln!(" srd set desktop_icons_all_monitors <true|false>"); } #[cfg(test)] @@ -676,6 +695,37 @@ mod tests { assert_eq!(build_request(&args(&["set", "master_count", "2"])).unwrap(), r#"{"cmd":"set","key":"master_count","value":2}"#); assert!(build_request(&args(&["set", "master_count", "not-a-number"])).is_err()); } + + #[test] + fn set_per_monitor_accepts_only_true_or_false() { + assert_eq!(build_request(&args(&["set", "per_monitor", "true"])).unwrap(), r#"{"cmd":"set","key":"per_monitor","value":true}"#); + assert_eq!(build_request(&args(&["set", "per_monitor", "false"])).unwrap(), r#"{"cmd":"set","key":"per_monitor","value":false}"#); + assert!(build_request(&args(&["set", "per_monitor", "maybe"])).is_err()); + } + + #[test] + fn set_button_style_accepts_only_the_two_known_values() { + assert_eq!(build_request(&args(&["set", "button_style", "traditional"])).unwrap(), r#"{"cmd":"set","key":"button_style","value":"traditional"}"#); + assert_eq!(build_request(&args(&["set", "button_style", "traffic_lights"])).unwrap(), r#"{"cmd":"set","key":"button_style","value":"traffic_lights"}"#); + assert!(build_request(&args(&["set", "button_style", "square"])).is_err()); + } + + #[test] + fn set_button_side_accepts_only_left_or_right() { + assert_eq!(build_request(&args(&["set", "button_side", "right"])).unwrap(), r#"{"cmd":"set","key":"button_side","value":"right"}"#); + assert!(build_request(&args(&["set", "button_side", "top"])).is_err()); + } + + #[test] + fn set_button_order_passes_the_string_through() { + assert_eq!(build_request(&args(&["set", "button_order", "close,minimize,maximize"])).unwrap(), r#"{"cmd":"set","key":"button_order","value":"close,minimize,maximize"}"#); + } + + #[test] + fn set_desktop_icons_accepts_only_true_or_false() { + assert_eq!(build_request(&args(&["set", "desktop_icons", "false"])).unwrap(), r#"{"cmd":"set","key":"desktop_icons","value":false}"#); + assert!(build_request(&args(&["set", "desktop_icons", "maybe"])).is_err()); + } } #[cfg(unix)] diff --git a/crates/platform/src/ipc/dispatch.rs b/crates/platform/src/ipc/dispatch.rs index 3dcca57..c5e70be 100644 --- a/crates/platform/src/ipc/dispatch.rs +++ b/crates/platform/src/ipc/dispatch.rs @@ -36,6 +36,14 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi gap_outer: wm.tiling.gap_outer, master_ratio: wm.tiling.master_ratio, master_count: wm.tiling.master_count, + per_monitor: wm.per_monitor_workspaces, + button_style: if wm.theme.traffic_light_buttons { "traffic_lights" } else { "traditional" }.to_string(), + button_side: if wm.theme.buttons_left { "left" } else { "right" }.to_string(), + button_order: wm.theme.button_order.map(srdwm_core::format_button_order), + title_centered: wm.theme.title_centered, + button_glyph_always: wm.theme.button_glyph_always, + desktop_icons: wm.desktop_icons_enabled, + desktop_icons_all_monitors: wm.desktop_icons_all_monitors, }; (serde_json::to_vec(&settings).unwrap_or_default(), false) } @@ -498,6 +506,86 @@ fn handle_set(req: &serde_json::Value, wm: &std::rc::Rc<std::cell::RefCell<Windo wm.borrow_mut().theme.default_decorated = mode != "client"; (ok(), true) } + // `srd set button_style <traffic_lights|traditional>` - live + // equivalent of `theme.decorations.title_bar.button_style` + // (`ThemeConfig::traffic_light_buttons`), previously config-file/ + // restart-only. Same "only affects windows created (or + // redecorated) after this call" scope as `decoration_mode` above + // - retroactively repainting every already-open window's titlebar + // is real, separate work (a redraw-buffer invalidation this + // backend-agnostic crate has no way to trigger itself). + "button_style" => { + let Some(v) = value.and_then(|v| v.as_str()) else { return (err("button_style needs \"traffic_lights\" or \"traditional\""), false) }; + if v != "traffic_lights" && v != "traditional" { + return (err("button_style needs \"traffic_lights\" or \"traditional\""), false); + } + wm.borrow_mut().theme.traffic_light_buttons = v == "traffic_lights"; + (ok(), true) + } + // `srd set button_side <left|right>` - live equivalent of `theme. + // decorations.title_bar.button_side`. Same scope note as + // `button_style` above. + "button_side" => { + let Some(v) = value.and_then(|v| v.as_str()) else { return (err("button_side needs \"left\" or \"right\""), false) }; + if v != "left" && v != "right" { + return (err("button_side needs \"left\" or \"right\""), false); + } + wm.borrow_mut().theme.buttons_left = v == "left"; + (ok(), true) + } + // `srd set button_order "close,minimize,maximize"` - live + // equivalent of `theme.decorations.title_bar.button_order`. + // `None` (the built-in default order for whichever side `button_ + // side` selects) is not reachable through this live path - only + // a config reload clears an explicit override back to that, + // matching the same asymmetry `rounded_corners`'s own live toggle + // already has (an explicit live `Some`/`Some` only, never back to + // an unset default). + "button_order" => { + let Some(raw) = value.and_then(|v| v.as_str()) else { return (err("button_order needs a string value"), false) }; + let Some(order) = srdwm_core::parse_button_order(raw) else { + return (err("button_order must name close, minimize and maximize exactly once each, comma-separated"), false); + }; + wm.borrow_mut().theme.button_order = Some(order); + (ok(), true) + } + // `srd set title_centered <bool>` - live equivalent of `theme. + // decorations.title_bar.title_centered`. + "title_centered" => { + let Some(v) = value.and_then(|v| v.as_bool()) else { return (err("title_centered needs a boolean value"), false) }; + wm.borrow_mut().theme.title_centered = v; + (ok(), true) + } + // `srd set button_glyph_always <bool>` - live equivalent of + // `theme.decorations.title_bar.button_glyph_always` (GNOME/Adwaita's + // "always visible" convention vs classic macOS's "hidden until + // hover"). + "button_glyph_always" => { + let Some(v) = value.and_then(|v| v.as_bool()) else { return (err("button_glyph_always needs a boolean value"), false) }; + wm.borrow_mut().theme.button_glyph_always = v; + (ok(), true) + } + // `srd set desktop_icons <bool>` - live equivalent of `general. + // desktop_icons`. Unlike the theme keys above, this one *is* + // immediately visible either way: `ensure_desktop_icons`'s own + // early `if !self.wm.borrow().desktop_icons_enabled { return }` + // check runs on every dirty tick, so turning icons off actually + // stops drawing them (and back on redraws them) on this compositor's + // very next redraw, not just for icons created after the call. + "desktop_icons" => { + let Some(v) = value.and_then(|v| v.as_bool()) else { return (err("desktop_icons needs a boolean value"), false) }; + wm.borrow_mut().desktop_icons_enabled = v; + (ok(), true) + } + // `srd set desktop_icons_all_monitors <bool>` - live equivalent + // of `general.desktop_icons_all_monitors`. Same immediacy as + // `desktop_icons` above - `desktop_icon_origins` (this session's + // own split-screen fix) reads this fresh every call. + "desktop_icons_all_monitors" => { + let Some(v) = value.and_then(|v| v.as_bool()) else { return (err("desktop_icons_all_monitors needs a boolean value"), false) }; + wm.borrow_mut().desktop_icons_all_monitors = v; + (ok(), true) + } // Tiling-only: `arrange_workspace` skips floating/fullscreen // windows regardless, and under `"dynamic"` (the no-op default // layout) nothing reads `tiling.gap_*` at all - so setting these @@ -588,6 +676,23 @@ fn handle_set(req: &serde_json::Value, wm: &std::rc::Rc<std::cell::RefCell<Windo wm.borrow_mut().phone_mode = v; (ok(), true) } + // `srd set per_monitor <bool>` - live equivalent of `workspace. + // per_monitor`, previously restart-only (flagged in the same + // capability survey monitor split/scale were). Safe to flip live + // with no reconciliation step needed: `monitor_workspaces` (the + // per-monitor override map) starts empty and a monitor with no + // entry in it always falls back to `current_workspace` regardless + // of mode (`WindowManager::workspace_for_monitor`'s own doc + // comment) - so turning this *on* changes nothing visually until + // a monitor's workspace is switched independently for the first + // time, and turning it back *off* simply resumes every monitor + // showing `current_workspace`, the same shared value they'd + // already fall back to individually. + "per_monitor" => { + let Some(v) = value.and_then(|v| v.as_bool()) else { return (err("per_monitor needs a boolean value"), false) }; + wm.borrow_mut().per_monitor_workspaces = v; + (ok(), true) + } // `srd set multi_cursor <bool>` - live equivalent of `general. // multi_cursor`. See `WindowManager::multi_cursor_enabled`'s own // doc comment for why this is opt-in rather than always-on. diff --git a/crates/platform/src/ipc/tests.rs b/crates/platform/src/ipc/tests.rs index 13ab4c2..c2a678c 100644 --- a/crates/platform/src/ipc/tests.rs +++ b/crates/platform/src/ipc/tests.rs @@ -751,7 +751,24 @@ fn settings_reports_the_readback_fields_flagged_as_missing() { server.poll(&wm); let line = read_line(&mut std::io::BufReader::new(client)); let parsed: serde_json::Value = serde_json::from_str(&line).unwrap(); - for field in ["border_width", "border_color", "corner_radius", "decoration_mode_server", "gap_inner", "gap_outer", "master_ratio", "master_count"] { + for field in [ + "border_width", + "border_color", + "corner_radius", + "decoration_mode_server", + "gap_inner", + "gap_outer", + "master_ratio", + "master_count", + "per_monitor", + "button_style", + "button_side", + "button_order", + "title_centered", + "button_glyph_always", + "desktop_icons", + "desktop_icons_all_monitors", + ] { assert!(parsed.get(field).is_some(), "settings response is missing '{field}'"); } assert_eq!(parsed["border_color"].as_str().unwrap().chars().next(), Some('#'), "border_color must be a hex string, matching what srd set border_color itself accepts"); @@ -791,6 +808,84 @@ fn set_master_ratio_clamps_to_a_sane_range() { } #[test] +fn set_per_monitor_is_reflected_immediately_by_settings() { + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + + let mut set_client = UnixStream::connect(&server.path).unwrap(); + set_client.write_all(b"{\"cmd\":\"set\",\"key\":\"per_monitor\",\"value\":true}\n").unwrap(); + server.poll(&wm); + let _ = read_line(&mut std::io::BufReader::new(set_client)); + + let mut settings_client = UnixStream::connect(&server.path).unwrap(); + settings_client.write_all(b"{\"cmd\":\"settings\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(settings_client)); + assert!(line.contains(r#""per_monitor":true"#)); +} + +#[test] +fn button_and_desktop_icon_settings_are_readable_and_live_settable() { + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + + for (key, value) in [ + ("button_style", "\"traditional\""), + ("button_side", "\"left\""), + ("button_order", "\"maximize,minimize,close\""), + ("title_centered", "true"), + ("button_glyph_always", "true"), + ("desktop_icons", "false"), + ("desktop_icons_all_monitors", "false"), + ] { + let mut client = UnixStream::connect(&server.path).unwrap(); + client.write_all(format!("{{\"cmd\":\"set\",\"key\":\"{key}\",\"value\":{value}}}\n").as_bytes()).unwrap(); + server.poll(&wm); + let _ = read_line(&mut std::io::BufReader::new(client)); + } + + let mut settings_client = UnixStream::connect(&server.path).unwrap(); + settings_client.write_all(b"{\"cmd\":\"settings\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(settings_client)); + assert!(line.contains(r#""button_style":"traditional""#)); + assert!(line.contains(r#""button_side":"left""#)); + assert!(line.contains(r#""button_order":"maximize,minimize,close""#)); + assert!(line.contains(r#""title_centered":true"#)); + assert!(line.contains(r#""button_glyph_always":true"#)); + assert!(line.contains(r#""desktop_icons":false"#)); + assert!(line.contains(r#""desktop_icons_all_monitors":false"#)); +} + +#[test] +fn button_order_is_null_until_explicitly_set() { + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + + let mut client = UnixStream::connect(&server.path).unwrap(); + client.write_all(b"{\"cmd\":\"settings\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(client)); + assert!(line.contains(r#""button_order":null"#)); +} + +#[test] +fn set_button_order_rejects_an_invalid_ordering() { + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + + let mut client = UnixStream::connect(&server.path).unwrap(); + client.write_all(b"{\"cmd\":\"set\",\"key\":\"button_order\",\"value\":\"close,close,maximize\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(client)); + assert!(line.contains(r#""error""#)); +} + +#[test] fn pinned_inputs_lists_nothing_before_any_pin_is_applied() { let dir = tempfile::tempdir().unwrap(); let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); diff --git a/crates/platform/src/ipc/types.rs b/crates/platform/src/ipc/types.rs index 24e3148..bf1526e 100644 --- a/crates/platform/src/ipc/types.rs +++ b/crates/platform/src/ipc/types.rs @@ -198,6 +198,24 @@ pub(crate) struct SettingsResponse { /// `master_count`. pub(crate) master_ratio: f32, pub(crate) master_count: usize, + /// `WindowManager::per_monitor_workspaces`'s own doc comment - `srd + /// set per_monitor <bool>`'s readback. + pub(crate) per_monitor: bool, + /// `ThemeConfig::traffic_light_buttons`'s readback, as the same + /// `"traffic_lights"`/`"traditional"` string `srd set button_style` + /// itself accepts. + pub(crate) button_style: String, + /// `ThemeConfig::buttons_left`'s readback, as `"left"`/`"right"`. + pub(crate) button_side: String, + /// `ThemeConfig::button_order`'s readback - `null` when unset (the + /// built-in default for whichever side `button_side` selects), the + /// same `"close,minimize,maximize"` string shape `srd set button_ + /// order` accepts otherwise. + pub(crate) button_order: Option<String>, + pub(crate) title_centered: bool, + pub(crate) button_glyph_always: bool, + pub(crate) desktop_icons: bool, + pub(crate) desktop_icons_all_monitors: bool, } /// `"keyboard_layout"`'s one-shot reply shape - the active XKB layout's |