diff options
| -rw-r--r-- | crates/core/src/manager/windows.rs | 21 | ||||
| -rw-r--r-- | crates/ctl/src/main.rs | 55 | ||||
| -rw-r--r-- | crates/platform/src/ipc/dispatch.rs | 32 | ||||
| -rw-r--r-- | crates/platform/src/ipc/tests.rs | 71 | ||||
| -rw-r--r-- | crates/wayland/src/state/layers.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/state/lifecycle.rs | 5 | ||||
| -rw-r--r-- | crates/wayland/src/state/toplevel.rs | 1 | ||||
| -rw-r--r-- | docs/TODO.md | 26 |
8 files changed, 178 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); diff --git a/docs/TODO.md b/docs/TODO.md index 1392761..0db3a23 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -13,6 +13,32 @@ that has the full story. Keep this list current as items close or open; update the source doc's own entry too, don't let this drift into a second stale copy the way `PANEL_SUPPORT_TODO.md` did. +## Live incident: creating a fake/virtual monitor corrupted the real monitor's position, repeatedly, with no further input (2026-08-27) + +Asked to demo fake monitors live. `srd dispatch create fake-monitor FAKE-1 1920x1080` was harmless (`eDP-1` stayed at `full_x=0`), but creating a *second* one (`FAKE-2`) immediately moved `eDP-1` to `full_x=1920`, and it kept drifting on its own for at least one more tick afterward (`full_x=3840, y` climbing ~31px per tick) with zero further commands issued. Removing both fake monitors stopped the drift but did not self-restore `eDP-1`'s position; fixed by hand via `srd dispatch set output position eDP-1 0 0`, confirmed restored. The user's actual laptop panel visibly shifted during this. + +Not root-caused yet, and not blind-fixed: read `relayout_outputs` (`crates/wayland/src/udev/outputs.rs`) end to end - it only ever iterates `udev.heads` (real heads), never `udev.virtual_heads`, and `create_virtual_head` never touches any other head's position, only computes where to place the *new* one. Nothing found srdwm-side that should reposition a real head just because a fake one appeared, which points outward: a fake monitor is a genuine, independent `wl_output` global (deliberately excluded from `wlr-output-management-v1`'s own listing, per `virtual_heads.rs`'s module doc comment, but *not* excluded from the plain core-protocol registry any GDK/GTK client - AGS included - discovers monitors through). AGS's own `MonitorLayout.tsx` has multiple `restoreRememberedLayout()` call sites for "a monitor being replugged"/"re-enabled" (see this file's own 2026-08-26 entry on the two independent layout-restore systems) - exactly the shape of event a fake monitor's `wl_output` appearing looks like from outside srdwm, and if AGS's own layout matching is index/order-based rather than connector-name-based, a fake monitor inserting itself as "the second output" could explain both the jump and a feedback loop explaining the *continued* drift with no further input. + +Flagged directly to `dotfiles-1a` (the AGS peer session) with the full repro and three concrete questions (does AGS's layout restore fire around this; is its matching index- or name-based; would filtering fake monitors by `make == "srdwm" && model == "virtual"` be a reasonable AGS-side mitigation) rather than guessing further or re-triggering the corruption on the user's live screen a second time to narrow it down. **Fake monitors should not be created again on a live session with AGS running until this is resolved.** + +## GPU render path: decorations investigated, deliberately not blind-ported (2026-08-27) + +Asked directly to finish the GPU (`general.gpu`/`SRDWM_GPU=1`) render path - `gpu.rs`'s own module doc comment already says plainly what's missing: real window content renders, but "square corners, no border or titlebar" - decorations are the real gap. + +Read the Pixman path's own decoration loop (`udev/render.rs`, the `!locked` per-window block) end to end to scope the actual port. It is not a small addition: border top/bottom strips have their own corner-radius-vs-border-width curve-safety logic (`border_curve_is_safe`, tied to whether content masking succeeded that frame), a live-resize crop clamp against the decoration buffer's own last-built size (guards an out-of-bounds sample during an active drag), occlusion-fragment clipping against every window stacked in front (`visible_border_fragments`/`occluders`, needed for correctness, not polish - without it an overlapping window's titlebar bleeds through in front of whatever's on top of it), animation-aware geometry (`window_anims`' interpolated rect, not the model's resting `w.geometry`, or the border visibly detaches from the window mid-tween), and push-order dependencies between the top border, the titlebar, and the shadow that this file's own comments say were each found and fixed only by a live, reported, screenshotted bug (the wedge bug, the "not flush" border, the shadow-over-border smear). Several of the underlying geometry functions (`border_strips`, `visible_border_fragments`) are already renderer-agnostic and reusable as-is; the renderer-specific parts are a straightforward type substitution (`GlesRenderer` for `PixmanRenderer` in the `MemoryRenderBufferRenderElement::from_buffer` calls). + +Not attempted this pass, deliberately: `gpu.rs`'s own doc comment already states `SRDWM_GPU`/`general.gpu` are unset on every machine this was built and tested on, including this one - there is no working GPU-capable KMS+3D path here to visually confirm a single pixel of a port against, on a feature nobody currently has turned on (the live daily-driver session runs the Pixman path). Writing several hundred lines reproducing the above - much of it hard-won from real, previously-misjudged-live bugs - with no way to catch a transposed sign or an off-by-one crop before it ships is the same risk this project's own Chrome-titlebar-heuristic gap was left unfixed over: compiling clean and passing the existing test suite (which has no GPU-backend coverage at all) would prove nothing about whether it actually renders correctly. Left as a scoped, documented gap rather than a guess dressed up as a fix; a real attempt needs either a machine with a working GPU path to check against, or the user accepting an explicitly unverified merge. + +## Live-exposed `srd.monitor.split` and cleaned up eight leftover debug diagnostics (2026-08-27) + +`srd.monitor.split(name, parts, direction)` (divides one real output into N logical monitors for placement/tiling) only ever ran at Lua config load - `WindowManager::set_monitor_split` was already a plain, cheap mutation, and every backend's own `monitors()` already reads it fresh on every call (see the udev platform's own `monitors()`), so there was no real reason it couldn't be live. Added `srd dispatch set output split <name|id> <parts> [rows|columns]` (IPC `set_monitor_split`) following the exact same "resolve id to a name first" pattern `set_output_enabled` already established. `srd.monitor.scale` was investigated too but left alone - its own doc comment is explicit that a backend only applies it "the next time it brings connector `name`'s head up," and `request_output_enabled`'s queue is last-write-wins per name, so a same-tick disable-then-enable to force that collapses to a no-op re-enable; making that genuinely live needs new backend plumbing, not attempted blind here. + +Separately, found and removed eight `log::warn!("XXX-DIAG ...")` lines left behind from live debugging in the multi-session shift that landed in commit `3c41fc4` - the same "temporary, never removed" pattern already fixed twice earlier this session (see the 2026-08-21 POS-DIAG/CURSOR-DIAG entry and the 2026-08-27 TEMP-DIAG entry further down): `DECO-DIAG` (four call sites across `manager/windows.rs::add_window`/`reapply_rules_if_pending`, one in `state/lifecycle.rs::redraw_decoration_buffer`, one in `state/toplevel.rs::sync_toplevel_metadata`), `WS-IPC-DIAG` (`platform/ipc/dispatch.rs`'s `activate_workspace`), and `LAYER-VIS-DIAG` (`state/layers.rs`). Several of these fire on genuinely constant, ordinary interaction - `reapply_rules_if_pending`'s own doc comment says outright it runs "constantly for perfectly ordinary reasons (a browser tab finishing a page load)" - so this was real, continuous log noise on every title change, every workspace switch, every layer surface hide, not just a one-off leftover. + +Deliberately left alone: `protocols/xdg_shell.rs`'s `POPUP-GEOM-DIAG`/`POPUP-GRAB-DIAG` (five call sites). Unlike the eight removed above, this one is self-documented as a live, still-open investigation ("Temporary: live report is that Nemo's right-click context menu never appears at all... Remove once resolved") with no entry anywhere in this file confirming that investigation actually concluded - removing an active diagnostic for a bug nobody has confirmed fixed would be a real regression in debuggability, not a cleanup. Left for whoever is still chasing that one. + +Full workspace build/test/clippy clean (33 platform / 32 ctl tests, both up from before by the new split coverage). + ## Real bug, root-caused and fixed: the cursor itself leaves a "ghost" briefly when crossing between monitors (2026-08-27) Reported live, separately from the secondary-cursor ghost above (same word, different bug - this one is the user's own single, real cursor): "sometimes I recognize ghosting cursor when moving between monitors." |