srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-10-26 22:58:00 +0200
committersrdusr <[email protected]>2025-10-26 22:58:00 +0200
commit304f3a374408bb6a5a04ebb7d4652c631395f5c6 (patch)
tree8a40628fe016bd3e31c295cc76985d65c6354561 /crates
parent9f07715b469a481849ee35ffa33d6dab34356c7d (diff)
downloadsrdwm-304f3a374408bb6a5a04ebb7d4652c631395f5c6.tar.gz
srdwm-304f3a374408bb6a5a04ebb7d4652c631395f5c6.zip
Live-expose monitor split, clean up leftover debug diagnostics
srd.monitor.split only ever ran at Lua config load despite being a plain WindowManager mutation that every backend's monitors() already reads fresh on each call. Adds srd dispatch set output split <name|id> <parts> [rows|columns] (IPC set_monitor_split), same id-resolves-to-name pattern set_output_enabled already uses. Also removes eight log::warn!("XXX-DIAG ...") lines left behind from live debugging in the multi-session shift that landed in 3c41fc4 - the same "temporary, never removed" pattern already fixed twice earlier this session. Several fired on genuinely constant interaction (every title change, every workspace switch, every layer-shell surface hide), not just a one-off leftover. Left xdg_shell.rs's own POPUP-GEOM-DIAG/ POPUP-GRAB-DIAG alone - that one is a still-open, self-documented investigation, not litter. Also documents (docs/TODO.md, not a code change) a live incident where creating a second fake monitor visibly corrupted the real monitor's position and kept drifting with no further input - not root-caused srdwm-side, flagged to the AGS peer session since a fake monitor's real wl_output global is indistinguishable from a real hotplug to GDK/GTK. And documents a deliberate decision not to blind-port window decoration rendering onto the experimental, never-live-tested GPU render path.
Diffstat (limited to 'crates')
-rw-r--r--crates/core/src/manager/windows.rs21
-rw-r--r--crates/ctl/src/main.rs55
-rw-r--r--crates/platform/src/ipc/dispatch.rs32
-rw-r--r--crates/platform/src/ipc/tests.rs71
-rw-r--r--crates/wayland/src/state/layers.rs1
-rw-r--r--crates/wayland/src/state/lifecycle.rs5
-rw-r--r--crates/wayland/src/state/toplevel.rs1
7 files changed, 152 insertions, 34 deletions
diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs
index b2edb6b..5468796 100644
--- a/crates/core/src/manager/windows.rs
+++ b/crates/core/src/manager/windows.rs
@@ -28,14 +28,6 @@ impl WindowManager {
window.corner_radius = self.theme.default_corner_radius;
window.decorated = self.theme.default_decorated && !likely_draws_own_titlebar(&window.app_id);
let actions = self.rules.iter().find(|r| r.matcher.matches(&window)).map(|r| r.actions.clone());
- log::warn!(
- "DECO-DIAG add_window id={id} app_id={:?} title={:?} rules_count={} actions_found={} decorated_before_actions={}",
- window.app_id,
- window.title,
- self.rules.len(),
- actions.is_some(),
- window.decorated
- );
// See `Window::rules_applied`'s doc comment: a native Wayland window
// still has empty title/app_id at this point, so a real (if
// inconclusive) match attempt needs to wait for `reapply_rules_if_pending`.
@@ -212,21 +204,9 @@ impl WindowManager {
pub fn reapply_rules_if_pending(&mut self, id: WindowId) -> bool {
let Some(window) = self.windows.get(&id) else { return false };
if window.rules_applied || (window.title.is_empty() && window.app_id.is_empty()) {
- log::warn!(
- "DECO-DIAG reapply_rules_if_pending id={id} SKIPPED rules_applied={} app_id={:?} title={:?}",
- window.rules_applied,
- window.app_id,
- window.title
- );
return false;
}
let actions = self.rules.iter().find(|r| r.matcher.matches(window)).map(|r| r.actions.clone());
- log::warn!(
- "DECO-DIAG reapply_rules_if_pending id={id} app_id={:?} actions_found={} actions_decorated={:?}",
- window.app_id,
- actions.is_some(),
- actions.as_ref().and_then(|a| a.decorated)
- );
let Some(window) = self.windows.get_mut(&id) else { return false };
window.rules_applied = true;
// `add_window`'s matching fallback only ever sees this once
@@ -246,7 +226,6 @@ impl WindowManager {
if let Some(decorated) = actions.decorated {
window.decorated = decorated;
}
- log::warn!("DECO-DIAG reapply_rules_if_pending id={id} FINAL window.decorated={}", window.decorated);
if let Some(color) = actions.border_color {
window.border_color = color;
}
diff --git a/crates/ctl/src/main.rs b/crates/ctl/src/main.rs
index 3cf3bfd..fb896a3 100644
--- a/crates/ctl/src/main.rs
+++ b/crates/ctl/src/main.rs
@@ -43,6 +43,12 @@
//! disabled output stops presenting
//! and its `wl_output` global goes away
//! until re-enabled
+//! srd dispatch set output split NAME|ID PARTS [rows|columns] divides
+//! one real output into PARTS logical
+//! monitors for placement/tiling --
+//! columns (default) side by side,
+//! rows stacked; PARTS <= 1 clears an
+//! existing split. Live, no restart.
//! srd set border_width 3 live theme values, applied immediately
//! srd set border_color '#cba6f7' (hex string)
//! srd set corner_radius 10
@@ -294,9 +300,9 @@ fn build_dispatch(args: &[String]) -> Result<String, String> {
// back.
"set" => {
if args.get(1).map(String::as_str) != Some("output") {
- return Err(format!("'set' only supports 'output position'/'output enabled' - {usage_hint}"));
+ return Err(format!("'set' only supports 'output position'/'output enabled'/'output split' - {usage_hint}"));
}
- let noun = args.get(2).ok_or("'set output' needs a target: position or enabled")?;
+ let noun = args.get(2).ok_or("'set output' needs a target: position, enabled or split")?;
let target = args.get(3).ok_or("'set output' needs a monitor name or id")?;
match noun.as_str() {
"position" => {
@@ -318,6 +324,25 @@ fn build_dispatch(args: &[String]) -> Result<String, String> {
Err(_) => Ok(format!(r#"{{"cmd":"set_output_enabled","name":"{target}","enabled":{enabled}}}"#)),
}
}
+ // `srd dispatch set output split <name|id> <parts> [rows|columns]`
+ // - the live equivalent of `srd.monitor.split(name, parts,
+ // direction)` in Lua config, which previously only ever took
+ // effect at config load/reload. `parts <= 1` clears an
+ // existing split. `columns` (side-by-side, splitting width)
+ // is the default when the direction is omitted, matching the
+ // Lua function's own default.
+ "split" => {
+ let parts: u64 = args.get(4).ok_or("'set output split' needs a part count")?.parse().map_err(|_| "parts must be a number".to_string())?;
+ let rows = match args.get(5).map(String::as_str) {
+ None | Some("columns") => false,
+ Some("rows") => true,
+ Some(other) => return Err(format!("'set output split' direction must be 'rows' or 'columns', got '{other}'")),
+ };
+ match target.parse::<u64>() {
+ Ok(id) => Ok(format!(r#"{{"cmd":"set_monitor_split","id":{id},"parts":{parts},"rows":{rows}}}"#)),
+ Err(_) => Ok(format!(r#"{{"cmd":"set_monitor_split","name":"{target}","parts":{parts},"rows":{rows}}}"#)),
+ }
+ }
_ => Err(format!("unknown 'set output' target '{noun}' - {usage_hint}")),
}
}
@@ -385,6 +410,7 @@ fn print_usage() {
eprintln!(" srd dispatch cycle keyboard layout");
eprintln!(" srd dispatch set output position <name|id> <x> <y>");
eprintln!(" srd dispatch set output enabled <name|id> <true|false>");
+ eprintln!(" srd dispatch set output split <name|id> <parts> [rows|columns]");
eprintln!(" srd dispatch pin input <pid> <window-id>");
eprintln!(" srd dispatch unpin input <pid>");
eprintln!(" srd dispatch create fake-monitor <name> <width>x<height>");
@@ -503,6 +529,31 @@ mod tests {
}
#[test]
+ fn set_output_split_accepts_a_numeric_id_and_a_name() {
+ assert_eq!(build_request(&args(&["dispatch", "set", "output", "split", "1", "2"])).unwrap(), r#"{"cmd":"set_monitor_split","id":1,"parts":2,"rows":false}"#);
+ assert_eq!(
+ build_request(&args(&["dispatch", "set", "output", "split", "HDMI-A-1", "3", "rows"])).unwrap(),
+ r#"{"cmd":"set_monitor_split","name":"HDMI-A-1","parts":3,"rows":true}"#
+ );
+ }
+
+ #[test]
+ fn set_output_split_defaults_direction_to_columns() {
+ assert_eq!(build_request(&args(&["dispatch", "set", "output", "split", "eDP-1", "2", "columns"])).unwrap(), r#"{"cmd":"set_monitor_split","name":"eDP-1","parts":2,"rows":false}"#);
+ }
+
+ #[test]
+ fn set_output_split_rejects_an_unknown_direction() {
+ assert!(build_request(&args(&["dispatch", "set", "output", "split", "eDP-1", "2", "sideways"])).is_err());
+ }
+
+ #[test]
+ fn set_output_split_needs_a_part_count() {
+ assert!(build_request(&args(&["dispatch", "set", "output", "split", "eDP-1"])).is_err());
+ assert!(build_request(&args(&["dispatch", "set", "output", "split", "eDP-1", "not-a-number"])).is_err());
+ }
+
+ #[test]
fn create_fake_monitor_builds_a_sized_request() {
assert_eq!(
build_request(&args(&["dispatch", "create", "fake-monitor", "FAKE-1", "1920x1080"])).unwrap(),
diff --git a/crates/platform/src/ipc/dispatch.rs b/crates/platform/src/ipc/dispatch.rs
index 313a79c..665cb59 100644
--- a/crates/platform/src/ipc/dispatch.rs
+++ b/crates/platform/src/ipc/dispatch.rs
@@ -219,7 +219,6 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi
// already reads serves both, same as every other dispatch arm.
"activate_workspace" => {
let Some(id) = id else { return (err("missing id"), false) };
- let before = wm.borrow().current_workspace();
// `switch_workspace_on_monitor` falls straight through to the
// ordinary shared-mode `switch_workspace` when `workspace.
// per_monitor` is off, so this is the one call site that works
@@ -239,9 +238,6 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi
.unwrap_or(0);
wm.switch_workspace_on_monitor(id as srdwm_core::WorkspaceId, monitor);
}
- let after = wm.borrow().current_workspace();
- let known: Vec<_> = wm.borrow().workspaces().iter().map(|w| w.id).collect();
- log::warn!("WS-IPC-DIAG requested_id={id} before={before} after={after} known_ids={known:?}");
(ok(), true)
}
// `{"cmd":"set_output_position","id":<monitor id>,"x":<i32>,"y":<i32>}`
@@ -322,6 +318,34 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi
wm.borrow_mut().request_output_enabled(name, enabled);
(ok(), true)
}
+ // `{"cmd":"set_monitor_split","id"|"name":...,"parts":<u32>,
+ // "rows":<bool, optional, default false>}` - the live CLI/IPC path
+ // for `srd.monitor.split(name, parts, direction)` (`crates/config/
+ // src/engine/general.rs`'s own `fn_monitor_split`), which until now
+ // only ever ran once at config load. `WindowManager::
+ // set_monitor_split` just mutates `monitor_splits`, and every
+ // backend's own `monitors()` already reads that map fresh on every
+ // single call (see the udev platform's own `monitors()`) - so,
+ // unlike `set_output_position`/`set_output_enabled` above, this
+ // needs no queue-and-drain at all: the very next `monitors()` query
+ // already reflects it. `parts` <= 1 clears an existing split, same
+ // as the Lua function. Same "resolve id to a name first" fallback
+ // `set_output_enabled` above already uses, since a caller working
+ // from a numeric id shouldn't have to look the name up itself
+ // first just to turn around and split it.
+ "set_monitor_split" => {
+ let name = match req.get("name").and_then(|v| v.as_str()) {
+ Some(name) => Some(name.to_string()),
+ None => id.and_then(|id| wm.borrow().monitors().iter().find(|m| m.id == id as srdwm_core::MonitorId).map(|m| m.name.clone())),
+ };
+ let Some(name) = name else { return (err("missing name, or an id matching a currently-connected monitor"), false) };
+ let Some(parts) = req.get("parts").and_then(|v| v.as_u64()) else {
+ return (err("missing parts"), false);
+ };
+ let rows = req.get("rows").and_then(|v| v.as_bool()).unwrap_or(false);
+ wm.borrow_mut().set_monitor_split(name, parts as u32, rows);
+ (ok(), true)
+ }
// `{"cmd":"capture_workspace","id":<workspace id>,"path":<string>,
// "width":<u32>,"height":<u32>}` - `width`/`height` are optional,
// both or neither. Exists for a workspace switcher's thumbnail
diff --git a/crates/platform/src/ipc/tests.rs b/crates/platform/src/ipc/tests.rs
index cb56a7c..aa86e95 100644
--- a/crates/platform/src/ipc/tests.rs
+++ b/crates/platform/src/ipc/tests.rs
@@ -426,6 +426,77 @@ fn set_output_enabled_with_neither_name_nor_a_resolvable_id_errors() {
}
#[test]
+fn set_monitor_split_accepts_a_name_directly_and_applies_immediately() {
+ // Unlike `set_output_position`/`set_output_enabled`, this one is a
+ // plain `WindowManager` mutation with nothing to drain - the very
+ // next `monitor_split` read already reflects it.
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "eDP-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_monitor_split\",\"name\":\"eDP-1\",\"parts\":2,\"rows\":false}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ let split = wm.borrow().monitor_split("eDP-1").unwrap();
+ assert_eq!(split.parts, 2);
+ assert!(!split.rows);
+}
+
+#[test]
+fn set_monitor_split_resolves_an_id_to_its_name() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(3, "HDMI-A-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_monitor_split\",\"id\":3,\"parts\":3,\"rows\":true}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ let split = wm.borrow().monitor_split("HDMI-A-1").unwrap();
+ assert_eq!(split.parts, 3);
+ assert!(split.rows);
+}
+
+#[test]
+fn set_monitor_split_with_one_part_clears_an_existing_split() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "eDP-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+ wm.borrow_mut().set_monitor_split("eDP-1".to_string(), 2, false);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_monitor_split\",\"name\":\"eDP-1\",\"parts\":1}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ assert!(wm.borrow().monitor_split("eDP-1").is_none());
+}
+
+#[test]
+fn set_monitor_split_with_neither_name_nor_a_resolvable_id_errors() {
+ 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();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_monitor_split\",\"parts\":2}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut reader);
+
+ assert!(line.contains(r#""error""#));
+}
+
+#[test]
fn monitors_query_lists_a_disabled_output_alongside_live_ones() {
// What the AGS peer session asked for directly: a disabled output
// must not just vanish from `srd monitors` - it needs a row
diff --git a/crates/wayland/src/state/layers.rs b/crates/wayland/src/state/layers.rs
index ee4c022..e9c8ac8 100644
--- a/crates/wayland/src/state/layers.rs
+++ b/crates/wayland/src/state/layers.rs
@@ -90,7 +90,6 @@ impl CompState {
let zone_before = map.non_exclusive_zone();
map.unmap_layer(&layer);
let zone_after = map.non_exclusive_zone();
- log::warn!("LAYER-VIS-DIAG unmapped namespace={:?} zone_before={zone_before:?} zone_after={zone_after:?}", layer.namespace());
if zone_after != zone_before {
self.pending.borrow_mut().push(CoreEvent::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0))));
}
diff --git a/crates/wayland/src/state/lifecycle.rs b/crates/wayland/src/state/lifecycle.rs
index 838e308..53a5e86 100644
--- a/crates/wayland/src/state/lifecycle.rs
+++ b/crates/wayland/src/state/lifecycle.rs
@@ -156,11 +156,6 @@ impl CompState {
traffic_light_buttons: theme.traffic_light_buttons,
is_dialog,
};
- log::warn!(
- "DECO-DIAG redraw_decoration_buffer id={id} w.decorated={} early_return={}",
- w.decorated,
- self.decoration_signatures.get(&id) == Some(&signature)
- );
if self.decoration_signatures.get(&id) == Some(&signature) {
return;
}
diff --git a/crates/wayland/src/state/toplevel.rs b/crates/wayland/src/state/toplevel.rs
index 90cf688..85365c8 100644
--- a/crates/wayland/src/state/toplevel.rs
+++ b/crates/wayland/src/state/toplevel.rs
@@ -74,7 +74,6 @@ pub(crate) fn sync_toplevel_metadata(state: &mut CompState, id: WindowId, surfac
// update - reported live as an older window jumping in front of
// a newer, focused one with no user action to explain it.
let reapplied = state.wm.borrow_mut().reapply_rules_if_pending(id);
- log::warn!("DECO-DIAG sync_toplevel_metadata id={id} changed=true reapplied={reapplied}");
if reapplied {
state.redraw_decoration_buffer(id);
state.sync_geometry(id);