srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/ctl/src
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-10-03 22:59:00 +0200
committersrdusr <[email protected]>2025-10-03 22:59:00 +0200
commit4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704 (patch)
tree657c89f7398f98b0305b9cd69889c232608f3027 /crates/ctl/src
parentd7031dd060ec12e6de334518aa75122c65794047 (diff)
downloadsrdwm-4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704.tar.gz
srdwm-4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704.zip
Make the secondary-cursor sprite opt-in and expire stale entries
Live report: a second cursor appeared uninvited and unusably (frozen, uncontrollable) on screen. Multi-cursor Phase 1 rendered one sprite per physical libinput pointer device that had ever reported a position, with no way to turn it off and no expiry - so a phantom device (a real mouse's side-button/scroll cluster enumerating as its own HID path is a common case) that reports once and never moves again left a frozen ghost sprite with nothing to control or dismiss it. Adds general.multi_cursor (default false, live-settable via `srd set multi_cursor <bool>`) and keys secondary_cursors to (Point, Instant) so both the recording side (udev/session.rs) and the render side (udev/render.rs) drop any entry older than SECONDARY_CURSOR_TIMEOUT (1.5s). The "agent controls a window without interrupting the user" use case this report also raised was never gated on this flag - that's Multi-cursor Phase 2's pinned virtual-pointer delivery, which never shows a visible cursor at all.
Diffstat (limited to 'crates/ctl/src')
-rw-r--r--crates/ctl/src/main.rs12
1 files changed, 10 insertions, 2 deletions
diff --git a/crates/ctl/src/main.rs b/crates/ctl/src/main.rs
index d714405..3cf3bfd 100644
--- a/crates/ctl/src/main.rs
+++ b/crates/ctl/src/main.rs
@@ -178,14 +178,14 @@ 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/shadows/rounded_corners/animations/night_light/reading_mode/phone_mode/decoration_mode)",
+ "set needs a key (border_width/border_color/corner_radius/gap_inner/gap_outer/shadows/rounded_corners/animations/night_light/reading_mode/phone_mode/multi_cursor/decoration_mode)",
)?;
let raw = args.get(2).ok_or("set needs a value")?;
let value = match key.as_str() {
"border_width" | "corner_radius" | "gap_inner" | "gap_outer" => {
raw.parse::<u64>().map_err(|_| format!("{key} needs a numeric value"))?.to_string()
}
- "shadows" | "rounded_corners" | "animations" | "night_light" | "reading_mode" | "phone_mode" => match raw.as_str() {
+ "shadows" | "rounded_corners" | "animations" | "night_light" | "reading_mode" | "phone_mode" | "multi_cursor" => match raw.as_str() {
"true" | "false" => raw.clone(),
_ => return Err(format!("{key} needs 'true' or 'false'")),
},
@@ -401,6 +401,7 @@ fn print_usage() {
eprintln!(" srd set night_light <true|false>");
eprintln!(" srd set reading_mode <true|false>");
eprintln!(" srd set phone_mode <true|false>");
+ eprintln!(" srd set multi_cursor <true|false>");
eprintln!(" srd set decoration_mode <server|client>");
}
@@ -479,6 +480,13 @@ mod tests {
}
#[test]
+ fn set_multi_cursor_accepts_only_true_or_false() {
+ assert_eq!(build_request(&args(&["set", "multi_cursor", "true"])).unwrap(), r#"{"cmd":"set","key":"multi_cursor","value":true}"#);
+ assert_eq!(build_request(&args(&["set", "multi_cursor", "false"])).unwrap(), r#"{"cmd":"set","key":"multi_cursor","value":false}"#);
+ assert!(build_request(&args(&["set", "multi_cursor", "maybe"])).is_err());
+ }
+
+ #[test]
fn set_phone_mode_accepts_only_true_or_false() {
assert_eq!(build_request(&args(&["set", "phone_mode", "true"])).unwrap(), r#"{"cmd":"set","key":"phone_mode","value":true}"#);
assert_eq!(build_request(&args(&["set", "phone_mode", "false"])).unwrap(), r#"{"cmd":"set","key":"phone_mode","value":false}"#);