srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/ctl/src
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-09-26 22:07:00 +0200
committersrdusr <[email protected]>2025-09-26 22:07:00 +0200
commit9a9aa8fe9d4f4f9b15860b45244e695942c90afd (patch)
treea7a8289731d49713caeaf06f023ab04f78cc663c /crates/ctl/src
parentb46dfa56504f62ecf969914dfed106bf587c4c4a (diff)
downloadsrdwm-9a9aa8fe9d4f4f9b15860b45244e695942c90afd.tar.gz
srdwm-9a9aa8fe9d4f4f9b15860b45244e695942c90afd.zip
Fake (headless) monitors, and fix new windows all opening in one spot
Two independent pieces landed together this pass - both real, both scoped, see docs/TODO.md for the full narrative on each: Fake monitors: a genuinely independent, additional wl_output with no DRM connector/CRTC behind it at all - distinct from srd.monitor.split (divides one real output's own placement rectangle). Researched niri's own Headless backend first (cloned at ~/reference-wms/niri): its render() never actually composites anything, a no-render stub for that project's test suite only. This one is real: it renders whatever is placed on it, on demand, whenever a zwlr_screencopy_manager_v1 client asks for a frame. New crates/wayland/src/udev/virtual_heads.rs (create/remove a real Output + global, render-on-demand for screencopy, integrated into platform.rs's monitors() as a genuine srdwm_core::Monitor so core placement/workspace code needs zero special-casing). New IPC/CLI: srd dispatch create fake-monitor <name> <w>x<h> / remove fake-monitor <name>. Core-side request queue in crates/core/src/manager/fake_monitor.rs. Placement bug, root-caused and fixed: every new window opened alone landed in the exact same spot, "not at all like Windows" (reported live). SmartPlacement::place tried a grid cell first, and grid's own cell count is existing.len() + 1 - with nothing else open (opening one app at a time, the ordinary case), that's always 1, so a 1x1 grid returns the same single cell forever regardless of session history. Cascade had the same bug in a second form (its own step was existing.len() % max_steps, also always 0 with nothing open). Fixed: WindowManager::next_cascade_step (a Cell - add_window's own target_monitor stays borrowed across the call) advances on every real placement and is never reset by a window closing; place() now skips grid entirely when nothing else is open, going straight to cascade, since grid's real job (dividing space among concurrent windows) has nothing to divide when there's no concurrency. Full workspace build/test/clippy clean (223 core / 141 wayland / 29 platform / 24 ctl / 28 config / 10 x11 tests, 0 failed, 0 clippy warnings), built and installed.
Diffstat (limited to 'crates/ctl/src')
-rw-r--r--crates/ctl/src/main.rs42
1 files changed, 42 insertions, 0 deletions
diff --git a/crates/ctl/src/main.rs b/crates/ctl/src/main.rs
index 929d44c..d714405 100644
--- a/crates/ctl/src/main.rs
+++ b/crates/ctl/src/main.rs
@@ -321,6 +321,27 @@ fn build_dispatch(args: &[String]) -> Result<String, String> {
_ => Err(format!("unknown 'set output' target '{noun}' - {usage_hint}")),
}
}
+ // `srd dispatch create fake-monitor <name> <width>x<height>` /
+ // `srd dispatch remove fake-monitor <name>` - a fully virtual
+ // `wl_output` with no real hardware behind it. See
+ // `crates/wayland/src/udev/virtual_heads.rs`'s own module doc
+ // comment for the full design and scope (content is readable via
+ // any `zwlr_screencopy_manager_v1` client - `grim -o <name>`,
+ // concretely - there is no real display to look at directly).
+ "create" | "remove" => {
+ if args.get(1).map(String::as_str) != Some("fake-monitor") {
+ return Err(format!("'{verb}' only supports 'fake-monitor' - {usage_hint}"));
+ }
+ let name = args.get(2).ok_or("'fake-monitor' needs a name")?;
+ if verb == "remove" {
+ return Ok(format!(r#"{{"cmd":"remove_fake_monitor","name":{name:?}}}"#));
+ }
+ let size = args.get(3).ok_or("'create fake-monitor' needs a <width>x<height>")?;
+ let (w, h) = size.split_once('x').ok_or("size must be '<width>x<height>', e.g. 1920x1080")?;
+ let width: u32 = w.parse().map_err(|_| "width must be a number".to_string())?;
+ let height: u32 = h.parse().map_err(|_| "height must be a number".to_string())?;
+ Ok(format!(r#"{{"cmd":"create_fake_monitor","name":{name:?},"width":{width},"height":{height}}}"#))
+ }
// `srd dispatch pin input <pid> <window-id>` / `srd dispatch
// unpin input <pid>` - the CLI surface for `pin_input`, Phase 2
// of the multi-cursor plan (`docs/TODO.md`). `<pid>` is the
@@ -366,6 +387,8 @@ fn print_usage() {
eprintln!(" srd dispatch set output enabled <name|id> <true|false>");
eprintln!(" srd dispatch pin input <pid> <window-id>");
eprintln!(" srd dispatch unpin input <pid>");
+ eprintln!(" srd dispatch create fake-monitor <name> <width>x<height>");
+ eprintln!(" srd dispatch remove fake-monitor <name>");
eprintln!(" srd capture workspace <id> <path> [<width>x<height>]");
eprintln!(" srd set border_width <n>");
eprintln!(" srd set border_color <#hex>");
@@ -472,6 +495,25 @@ mod tests {
}
#[test]
+ fn create_fake_monitor_builds_a_sized_request() {
+ assert_eq!(
+ build_request(&args(&["dispatch", "create", "fake-monitor", "FAKE-1", "1920x1080"])).unwrap(),
+ r#"{"cmd":"create_fake_monitor","name":"FAKE-1","width":1920,"height":1080}"#
+ );
+ }
+
+ #[test]
+ fn remove_fake_monitor_needs_no_size() {
+ assert_eq!(build_request(&args(&["dispatch", "remove", "fake-monitor", "FAKE-1"])).unwrap(), r#"{"cmd":"remove_fake_monitor","name":"FAKE-1"}"#);
+ }
+
+ #[test]
+ fn create_fake_monitor_rejects_a_malformed_size() {
+ assert!(build_request(&args(&["dispatch", "create", "fake-monitor", "FAKE-1", "1920"])).is_err());
+ assert!(build_request(&args(&["dispatch", "create", "fake-monitor", "FAKE-1", "widexhigh"])).is_err());
+ }
+
+ #[test]
fn pin_input_builds_a_request_with_pid_and_window_id() {
assert_eq!(build_request(&args(&["dispatch", "pin", "input", "12345", "7"])).unwrap(), r#"{"cmd":"pin_input","pid":12345,"id":7}"#);
}