srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager
AgeCommit message (Collapse)AuthorFilesLines
2026-08-30Indent doc list continuationssrdusr1-10/+10
2026-08-25Move a window in steps, and let the keyboard resize one at allsrdusr2-2/+197
Two of the owner's reported gaps, both about moving a window without a mouse. Super+Shift+HJKL slammed the window to the far edge of the monitor in one press: "it should move in increments - one side, middle, other side - not just extreme left/up/right/down". It now steps an eighth of the monitor per press, which crosses the screen in eight, passes through the middle at the fourth, and stops flush against the edge rather than short of it. A fraction rather than a pixel count, so the same key feels the same on a laptop panel and a 4K display. Only outside a tiling layout. There a window does not own its geometry -- stepping it would be undone by the next arrange - so the neighbour swap stays, and three tests that assumed swapping on a dynamic workspace now say which layout they mean. Keyboard resize did not exist. Super+right-drag has resized with the mouse for a while, which is why this went unnoticed, but nothing resized a window without one. `srd.window.resize("right", "grow"|"shrink")` grows or shrinks from the far edge in that direction, leaving the top-left where it is, and is bounded by the same minimums a drag is and by the monitor's usable area - a window can be resized neither to nothing nor off the screen, and a test holds each key down forty times to prove it. Wired into the owner's config: Super+Ctrl+arrows resize (the arrow says which way the far edge moves, so Right always widens), and Super+Ctrl+L locks the screen. Hyprland only ever bound the lock to XF86ScreenSaver, which most keyboards do not have; Super+L is the convention everywhere else but is already focus-right here. Ctrl+arrows rather than Ctrl+HJKL because Super+Ctrl+K is already kill-process. Verified through the real path in a nested compositor running that config: 89 bindings, all 89 described, the five new ones registered.
2026-08-24Look for a free spot before piling a new window on the last onesrdusr2-0/+93
Reported: windows spawn predominantly on one side and on top of each other, with no smart placement. Measured first, in a nested compositor: five windows opened at 30,30 then 60,60 then 90,90 then 120,120 then 150,150 -- every pair overlapping, all in the top-left. The cascade was the only thing running. The grid never engaged, and could not. It asked whether a grid CELL was free and then placed the window at that cell's corner at its own, larger size. An ordinary 800x600 window on a 1280x800 screen overlaps every cell of a 2x2 grid, so no cell was ever free, the grid returned nothing, and everything fell through to the cascade. Placement now looks for a position where the window overlaps nothing at all, and only cascades when the screen genuinely cannot fit one - which is what Windows does once its own screen fills up, and what makes the cascade the right last resort rather than the first answer. The candidates are the edges of what is already on screen: every window's left and right edge plus the monitor's own, taken both as "put my left edge here" and "put my right edge here", and the same vertically. That is Openbox's place_overlap reduced to this case (~/reference-wms/openbox), and it works because a rectangle packed against other rectangles is always flush with one of their edges - nothing is gained by testing the space in between. Two things the naive version got wrong, both fixed here: Ties go to the position nearest the middle of the monitor, and the choice rotates through the four most central free spots. Least-overlap placement is deterministic, so opening one window at a time - open, use, close, open the next - put every one of them in exactly the same place, which is this project's own earlier bug report. Every candidate rotated between is free, so variety never costs the guarantee. And placement now runs again once the client's real size is known. It has to happen before the client commits anything, so it was deciding where an 800x600 placeholder should go rather than the window - a small terminal was told it was 800x600, no two of those fit, and it cascaded. Only when the client actually chooses a different size: re-running it otherwise consumed a second cascade step for nothing, and the cascade wraps, which measured as two windows landing on exactly the same spot. 287 core tests pass, clippy clean.
2026-08-11Read the window-memory store at a moment when it can actually matchsrdusr2-0/+101
Reported twice, as two complaints: windows do not remember their size or position across a close or a reboot, and windows spawn stacked on one side with no smart placement. One bug. `add_window` looks the store up by app_id. A Wayland toplevel role exists before its client sends set_app_id, so at the moment srdwm placed a window the app_id was the empty string, every lookup missed, and every window fell through to the cascade - which is exactly what "they all open on top of each other" looks like. The store was being written correctly the whole time and read at the one moment it could not match. The lookup now runs again the instant a real app_id arrives, which is still before the client's first buffer, so nothing is drawn in the wrong place first. It only moves a window still sitting where the cascade put it: a rule's explicit geometry, a maximize, a dialog's centring and a client's own committed size are each more specific than "wherever I last left this app", and a test asserts none of them is overridden. A second bug sat underneath the first and only appeared once it was fixed: the position came back and the size did not, which is stranger than nothing being restored. The backend keeps its own copy of "this size is only a guess" (provisional_size) and adopt_provisional_size reads that one rather than the core flag, so the client's next commit overwrote the size that had just been restored. Cleared with the same call. Verified end to end in a nested compositor, driving a real edge-drag with the virtual-pointer tool: seeded store 400,300 500x400 -> opened at exactly 400,300 500x400 dragged the right edge -> 646 wide, store rewritten to 646 on release closed and reopened -> 400,300 646x400 Before this the same first step opened at 30,30 800x600. Also: window_memory::save_all's nested guard now allows a write when the instance was given its own state directory (SRDWM_STATE_PATH or XDG_STATE_HOME). The blanket refusal added earlier kept the owner's store safe but made the feature impossible to test without pointing a test compositor at the real desktop, which is how this went unverified in the first place.
2026-07-14Workspace capture writes a readable image, and left-edge resize holds its anchorsrdusr1-0/+15
Two things, and the first is smaller than I told anyone. WORKSPACE CAPTURE. I said off-screen workspace capture did not exist and would need building. It already did: udev/capture.rs renders a workspace that is not on screen, at the target monitor's native size, downscaled to a requested size, wallpaper included. Verified on the live DRM session rather than from the source - capturing the active workspace and a non-visible one gave 320x180 images with mean luminance 0.067 and 0.137, so the second is genuinely a different render and not a copy of what is presented. The only thing missing was the container. It wrote PPM, which the shells that want thumbnails cannot decode, so the file was written successfully, returned successfully, and silently not drawn - the same failure class as a capture pass that omits a tier. encode_capture now picks the format from the destination's extension: .ppm still writes PPM so existing callers keep working, .jpg/.jpeg write JPEG, anything else writes PNG. Four tests check the actual magic bytes rather than trusting the call, plus the unfamiliar extension fallback and a size-mismatch error. LEFT-EDGE RESIZE. Reported as content resizing "from the right side even when i resize from left". The window's origin moves the instant the pointer does, but the client only commits a matching buffer some frames later, so its still-old content was being placed at the new origin - which slides the whole window rather than growing it, and leaves the edge that should be nailed down drifting. sync_geometry now derives the origin from the size the client has actually committed when the drag is from a left or top edge, so the opposite edge stays exactly where the drag started and the dragged edge catches up as commits arrive. A right or bottom drag is untouched: its origin never moves. 533 tests pass, clippy clean.
2026-07-05Let the config file take back a setting changed at runtimesrdusr1-0/+9
Reported as windows still being tinted. The tint is the drop shadow, and init.lua sets general.shadows to false - loading that same config in a fresh compositor reports false, while the running session reported true. The reason it could not be corrected is a defect in the live-settings replay added earlier today. That replay re-applies every srd set after a config reload so the titlebar menu's Customize rows survive a save. The unintended half is that a live override then outranked the config file permanently: editing init.lua and saving put the override straight back, which is the state the session was found in. Live-always-wins and config-always-wins are both wrong. The rule is now that the config wins for anything it states, and a live override survives only where the config is silent. That distinction cannot come from `values`, where defaults are seeded before any script runs so every key looks set, so the config engine records which keys srd.set actually touched during the load. That record is cleared and rebuilt on each load and restored along with everything else when a reload fails. Verified both directions: a config-stated key reverts to the file's value on the next reload, and a key the config never mentions keeps its live override. 529 tests pass, clippy clean.
2026-06-02Stop window memory poisoning itself, and publish the decoration side to GTKsrdusr3-1/+59
Two reports, both traced to a cause other than the one being blamed. "Windows still spawn as squares" was not placement. On the live session firefox was 800x632 and so were four other apps, and 800x632 is exactly the placeholder new_managed_window assigns before a client has chosen anything. Firefox's remembered size earlier the same day was 1389x933. The loop: a window closes while still carrying the placeholder, the placeholder is remembered, the next launch therefore has a remembered size and is no longer provisional, a non-provisional window is forced to its size instead of being asked to pick, and on close the placeholder is written back. Every app that ever closed early gets pinned to one identical box, and no amount of placement work can touch it because the size never came from placement. remove_window now refuses to remember a size the client never chose. That alone would have been wrong: adopt_provisional_size cleared its own tracking set but never cleared Window::size_is_provisional, so nothing would ever have been remembered again. Both halves are covered by tests. Five poisoned entries were dropped from the live store and the six real ones kept, with a backup alongside it. "When user sets decorations should override all applications": the earlier answer was true about the protocol and wrong about the outcome. GTK never negotiates decoration, but it does read the desktop's button-layout preference - GTK4 through xdg-desktop-portal, GTK3 through gtk-decoration-layout. srdwm now publishes its own button_side there at startup and after every reload, which is precisely the job kde-gtk-config does for KWin. Verified in both directions from a neutral starting value; testing the second direction is what exposed an ordering bug where the publish ran before apply_general_settings and broadcast the default instead of the configured side. 527 tests pass, clippy clean.
2026-05-13Report whether a listed key binding is actually grabbedsrdusr2-1/+62
Requested by the AGS session while wiring its launcher to srd keybindings: without this the launcher would list a shortcut that does nothing and give no way to tell, which is the same silent-lie class of bug as the rest of the work today. The backend is handed one combo list, once, before connecting - X11 turns it into XGrabKey calls, Wayland into its intercept set. A reload re-registers the actions but cannot re-register the grabs, so a combination added to the config since startup is bound as far as the config engine is concerned and still goes straight to the focused client when pressed. That snapshot is now recorded on the WindowManager at the exact point it is handed to the backend, taken once rather than per-arm so the reported set and the grabbed set cannot drift apart, and each entry in srd keybindings carries a `grabbed` flag. Verified live in a nested instance, both states observed: 46 bindings and 46 grabbed at startup; then appending a new combination to the config gave 47 bindings, 46 grabbed, with the new one reported as not grabbed and carrying its description. Two unit tests cover the set being replaced rather than accumulated, and a combo outside it reading as not grabbed. Also recorded, from the AGS session's own checks: moving a window to a workspace was their bug, not a missing compositor feature - the Overview's previews had a drop target and the bar's workspace dots had none, so the gesture worked on one surface and silently did nothing on the other. And the static half of "are all keybindings working" is clean for the running session: keybindings.lua was last modified 06:02:55 and the compositor started 18:11:13, so every combination in it was grabbed at boot. 517 tests pass, clippy clean.
2026-05-13Expose key bindings over IPC so a launcher can list themsrdusr1-0/+12
Asked whether srdwm's bindings show in the AGS launcher. They could not: srd.bind lives entirely in the Lua engine, and nothing published a binding anywhere a client could read it. There was no IPC command, no field in any response, and bound_keys() was used only by main.rs to register grabs. srd.bind now takes an optional third argument, a description, and the loaded set is copied into the WindowManager after the initial load and after every reload. Core neither owns nor interprets them - it has no Lua state and never dispatches a key - it holds them so the IPC layer, which is handed a WindowManager and nothing else, can serve them. New `srd keybindings` returns combo and description pairs, sorted so a UI listing them does not reshuffle on every refresh. Every binding in the shipped config now carries a description, so the feature is useful without the user writing any. Verified live in a nested instance: 46 bindings published, 0 without a description, and editing the config file updated the list without a restart (which also exercised reload-on-write again). Also verified, for the separate report that windows cannot be moved to another workspace from the AGS workspace pills: the compositor side works. `srd dispatch move workspace <id> 2` moved a window from workspace 1 to 2 and correctly hid it, since workspace 1 was active. Nothing to fix here; the missing piece is on the shell side. 515 tests pass, clippy clean.
2026-05-11Fix spawn placement under the top bar, add per-window minimum sizes, and ↵srdusr5-9/+167
clean up maximize Four reports after restarting into today's build, with a screenshot. The screenshot was measured, not eyeballed: top bar y=0..29, a 4px accent border at y=30..33, titlebar from y=34, bottom border at y=1027..1030, and 49px of bare desktop below it. Windows spawning too close to the top bar. A remembered position was validated only by asking whether it landed on some monitor's full_geometry, which includes the strip a top bar reserves, so an app whose remembered y was small reopened with its titlebar under the bar. That is why it was "sometimes": it depended on the stored value, and the live store holds wezterm at y=44 and firefox at y=69 against a 30px bar. Remembered positions are now clamped into the monitor's usable area. Placement not surviving a logout. Window memory does persist, but five of the eleven entries in the live store were saved with a second monitor attached, at x >= 2000. Those points match no current monitor and were discarded outright, falling back to a fresh cascade, so those apps appeared to remember nothing. Such a position is now clamped onto a monitor that exists instead. Per-window minimum sizes. One global floor is wrong in both directions. Three sources now, in increasing precedence: the global floor, the client's own declared minimum (xdg_toplevel.set_min_size or ICCCM hints), and a min_width/min_height window rule overriding both. A rule wins permanently -- the backend refreshes the client's declared minimum on every decoration redraw and must not undo a deliberate override. Maximize, three faults in one report. A maximized window now draws no border: its edges are the screen's edges, and the only place maximize stops short is the bar strip, which is exactly where the measured line was. maximize_geometry_for no longer subtracts a bottom-anchored dock's exclusive zone, so maximize runs to the bottom of the screen and the dock floats over it; top, left and right are still honoured. general.maximize_covers_dock = false restores the old behaviour. With the border gone the window sits flush under the bar instead of with an accent line crowding it. Verified: seven new tests on the real numbers from the live store, and maximize geometry measured live in a nested instance (a window maximized on a split half reports exactly that half's rect). NOT confirmed on screen: the border removal and the dock behaviour - the nested backend has no bar or dock to reserve a zone, and an attempt to check the border produced a failing control, since srd set border_width only affects windows created after it. 515 tests pass, clippy clean.
2026-04-28Stop a config reload from undoing a change the user just made by handsrdusr2-0/+40
Reloading rebuilds the theme and general settings from the config file. That is right for a file edit, but it also wiped every live `srd set` - and the titlebar right-click menu's "Customize" rows are built entirely out of live `srd set`s. Changing a button style from that menu and then saving init.lua for any unrelated reason silently reverted it. Survivable while reloads only happened on Mod4+Ctrl+r. The reload-on-write support added in the previous commit makes a reload happen on every save, which turns a rare surprise into a reliable one. A control that silently reverts is worse than no control, so this is a defect rather than a documented quirk. The AGS peer session reached the same conclusion from the other side while deciding whether to build Settings controls against these values, and would have had to label them session-only. Every setting changed live is recorded on the WindowManager as key -> raw JSON text, and replayed after each reload through the very same handle_set that applied it, so a replayed setting cannot behave differently from a real one. Recorded only on success, so a rejected value is never replayed, and only for real client calls, so the replay cannot rewrite what it is reading. Last write wins per key. Raw JSON text because core has no serde dependency and no reason to gain one for this; the platform crate parses it back. Verified live in a nested compositor: set button_side left and button_mode fixed, saved an unrelated config edit, both survived, and the log reported re-applying two live settings. Three tests cover the round trip, the rejected-value case and the one-entry-per-key case. A live value is still a session override rather than a persisted setting. That distinction is now written down in DEFAULTS.md instead of being a trap. 515 tests pass, clippy clean.
2026-04-27Build the eight asks recovered from the previous session's transcriptsrdusr6-5/+318
The punch list was not the whole record. These are the owner's own typed requests, read back out of the previous session's transcript rather than guessed at, then each checked against the code before being treated as open. Three they suspected were already done really were: dialogs already had a Close-only titlebar, inactive dimming already existed, and the corner resize hitbox had already been tuned. Snap layouts on drag, asked for twice. Edge snapping worked but committed silently on release with nothing shown first, so there was no way to know it would happen or where. A translucent drop-target preview now follows the drag, and throwing the pointer at a monitor's top edge drops down the existing six-cell grid to aim at. The preview calls the same snap_zone that end_drag does, so the two cannot disagree. Two defects found by screenshot before landing: moving down onto the flyout closed it, and its labels overflowed at a fixed cell width - the same "text goes out of view" fault already fixed once for the context menu. New File now offers real types, chosen by extension, with the de-duplication counter placed before the extension so the file stays what it says it is. Refresh re-reads init.lua and fires a new srd.on("refresh") handler instead of only re-scanning the icon grid. What refresh means beyond srdwm's own config stays the config's decision. general.config_reload_on_write (default on) applies an edited config on save, via an mtime sweep rather than an inotify watch: no new dependency, same behaviour on every target, and unaffected by editors that write through a temp file. A real bug behind "what happens when our config fails": you lost every keybinding. do_reload cleared the binding, handler and repeat tables before re-executing and never restored them, so a syntax error left neither the old config nor the new one, and the only key still working was the reload combo nobody thinks to press. The tables are now restored on any failure and config errors reach notify-send, not just the log. srd.lock() and a default Mod4+Ctrl+l binding: the built-in lock screen could not be reached from Lua at all. Native rather than shelling out, because a lock key that shells out fails silently when the binary is not on PATH. Default bindings added for srd.window.move and a dynamic/tiling toggle, both of which existed with no way to reach them, plus srd.layout.get() so the toggle reads the live workspace rather than the configured default. Dialogs open centred, and are excluded from remembered geometry in both directions - that table is keyed by app_id, which a dialog shares with the window that spawned it, so dialogs inherited an unrelated position and size and then overwrote it with their own. theme.decorations.title_bar.button_mode (dynamic by default, or fixed) drops the Maximize button on a window whose client pinned min == max size, where pressing it can do nothing. Maximize is removed from the slot list rather than skipped in place on both the render and hit-test sides, so the remaining buttons close the gap identically; three tests pin that agreement, which is what fails silently when it drifts. Also: the nested backend's screencopy pass now draws both menus, the flyout and the drag preview. Four investigations in one day started from a screenshot missing a tier, so that pass carries an explicit list of what it still omits and the on-screen loop points at it. 512 tests pass, clippy clean.
2026-01-31Fix desktop-icon deselection and workspace-teleport-on-close; document a ↵srdusr3-1/+103
shadow limit Desktop icons stayed highlighted after clicking a window: select_desktop_icon(None) was only ever called from start_desktop_marquee, never from the one place every focus path (click, Alt-Tab, dock IPC, scratchpad show, snap flyout) already funnels through. Added the deselect there instead of per-caller. Closing a focused window could silently switch the user's active workspace: remove_window's fallback picked self.order.last(), but that list is global, not per-workspace, so it could land on a background window elsewhere - and focus_window already switches workspace to match whatever it's given (a real, separate feature for a deliberate srd dispatch focus). Fixed by preferring a same-workspace window first. New general.close_focus_follows_workspace (default false, live-settable) controls what happens only when nothing is left on the current workspace at all: off leaves focus at nothing, matching Windows/GNOME/macOS; on restores the old always-follow-the-global-fallback behaviour. Three new tests. Also documented, not fixed: shadows can still bleed onto a neighbouring *monitor* near a multi-output seam (shadow_rect has no monitor-boundary awareness), found via a live cross-monitor screenshot. Moot for this session since general.shadows is already off in the live config, but a real, open gap for anyone who re-enables shadows on a multi-monitor setup.
2025-12-01Let a new window pick its own size instead of forcing a guessed placeholdersrdusr3-0/+59
Root-caused "windows spawn small and square, not remembering placement or size": new_managed_window hardcoded a fresh toplevel's geometry to 800x632 before the client had said anything about its own preferred size, and sync_geometry forced that guess onto the client's very first xdg_toplevel::configure unconditionally. Per xdg-shell, size: None on that first configure is how every mainstream compositor lets a client pick its own natural size instead; this one never did, so every app converged on the same placeholder rectangle regardless of what it would have chosen. Window::size_is_provisional marks a size that really was just the guess (not a remembered geometry, a rule's explicit geometry action, or a maximize/phone-mode fill, none of which are guesses). sync_geometry sends size: None for such a window's first configure; a new adopt_provisional_size, called from the commit handler, adopts the client's own real first size into Window::geometry the moment it commits one, clamping only position so a bigger-than-guessed window can't hang off its monitor's edge. Live-verified in a nested compositor: a zenity dialog now renders at its own compact natural size instead of being stretched to the old guess.
2025-11-07Make tiling's master/stack ratio live, add settings readback everywheresrdusr4-7/+303
Investigated the "tiling needs a lot of work" report directly. The MasterStackLayout algorithm itself was already correct; the real gap was that dragging or resizing a tiled window did nothing durable (raw geometry that the next arrange_workspace silently discarded), and master_ratio/master_count had no live path at all (config-file only). A resize-drag on the shared master/stack boundary now live-adjusts TilingConfig::master_ratio and re-arranges the group immediately; srd set master_ratio/master_count do the same for a keybind or script. Found and fixed a real bug while building this: start_resize's own focus_window call re-stacks its target in self.order before the ratio-drag decision used to be made, silently misclassifying real master-column grabs. Fixed by deciding ratio-drag status (and freezing the membership snapshot it depends on) before that raise happens, applying MasterStackLayout directly against the frozen snapshot rather than re-deriving membership from the by-then-reordered live order. Live- verified in a nested compositor, not just unit-tested. Also closes the readback gaps flagged directly by the AGS peer session: border_width/border_color/corner_radius/decoration_mode/gap_inner/ gap_outer/master_ratio/master_count were all live-settable via srd set with no way to read the current value back, and pin_input had no readback at all. SettingsResponse now reports all of them; a new pinned_inputs query (srd pinned inputs) lists every currently pinned pid/window.
2025-11-04Fix window memory never saving on close, and split-screen icon/primary bugssrdusr2-1/+53
Screenshotted the just-split display on request rather than guessing -- it showed why windows never seem to remember placement/size, plus two real split-screen bugs. Window memory (WindowManager::remembered_geometry) was correctly wired on the read side, but the only writes came from end_drag/end_resize in dragresize.rs - a real drag or resize. A window the user opens, looks at, and closes without ever touching its edges had nothing recorded, so reopening it always fell back to a fresh cascade placement, for what is probably most ordinary window lifecycles. remove_window now also snapshots geometry (same app_id-non-empty gate the drag/resize sites use), persisted at both of its wayland-side call sites the same way the drag/resize-release site already does. desktop_icon_origins mirrored the full icon set onto every Monitor entry when general.desktop_icons_all_monitors is on - which, after a srd.monitor.split, is one entry per split part of the same physical screen, not one per real monitor. Extracted into a separately-tested icon_origins_for that collapses split parts of the same connector back to one origin, keeping a genuinely separate monitor's own origin intact. Found while fixing that: every split part also reported primary: true (computed from the connector's name, which doesn't vary per part) -- fixed by gating on part == 0 too.
2025-10-29Fix set_monitor_split never actually reaching srd monitorssrdusr2-0/+35
Live-tested right after shipping it and caught immediately: srd dispatch set output split returned ok, but srd monitors kept reporting the whole, unsplit output. WindowManager::monitors is a passive cache, only refreshed when a backend re-queries and calls set_monitors again - the IPC handler mutated the split map directly but never triggered that requery, unlike set_output_position's own drain site, which already pushes a "just go recompute" event after applying. Makes it a proper queued cross-boundary request instead, the same shape as every other backend-owned effect on this socket: WindowManager:: request_monitor_split/drain_monitor_split_requests, dispatch queues instead of mutating, the udev backend's poll drains it, applies via set_monitor_split, and pushes the same recompute event. srd.monitor. split's Lua config-time path is untouched - it runs before the very first startup query, so it never had this problem.
2025-10-26Live-expose monitor split, clean up leftover debug diagnosticssrdusr1-21/+0
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.
2025-10-03Make the secondary-cursor sprite opt-in and expire stale entriessrdusr1-0/+21
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.
2025-09-26Fake (headless) monitors, and fix new windows all opening in one spotsrdusr4-8/+131
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.
2025-08-29Core window manager: real fixes plus three new rule/placement primitivessrdusr5-61/+443
Several independent, real pieces landed in crates/core this shift - see docs/TODO.md for each one's full root-cause/verification narrative: - "Primary" monitor is now picked by which head sits at physical (0, 0) (the user's own configured anchor), not whichever connector DRM happened to probe first - fixes desktop icons and new-window placement landing on the wrong monitor depending on hotplug/probe order. - A new window's target monitor now prioritizes the pointer's own current monitor over the last-focused window's monitor, which goes stale the moment the user's attention moves to empty desktop, a panel, or a dock. - aspect_ratio window-rule action ("W:H") plus ResizeEdge::apply_aspect_ ratio: holds a floating window's aspect ratio through an interactive resize. The real, scoped "phone monitor" primitive - matches any VM/ emulator/scrcpy window by app_id, nothing Android- or VM-specific here. - general.phone_mode (WindowManager::phone_mode): a new window defaults to maximized instead of floating/tiled small, unless a rule explicitly floats it or sets maximized - the one placement default a phone-shaped screen actually needs. Exposed read-only via IPC so a shell panel can adapt its own chrome to the same signal. - input_pin.rs: the core half of pinning a virtual pointer to a specific window (Multi-cursor Phase 2) - a backend-agnostic request queue, same cross-boundary shape output_position_requests/lock_requested already use, since core has no real Wayland protocol object to reach into itself. Full workspace test suite covers all of the above (aspect-ratio resize math for every edge case, phone-mode default-vs-rule-override behavior, the pin-input request queue, the monitor-picking fixes).
2025-08-17Fix desktop icons v1 regressions: bar overlap, wrong order; add proper menussrdusr1-11/+7
Live testing found v1 genuinely broken, not just rough: 1. Icons weren't rendering reliably at all - ensure_desktop_icons only ever computed the grid's origin once, on whichever render pass happened to be first. AGS's own top bar registers its exclusive zone after that first pass, so origin got permanently baked in at the pre-bar geometry. Confirmed live via a temporary diagnostic log. Fixed by re-deriving origin from the primary monitor's current geometry on every call instead of just the first. 2. Fixed icons (Home/Computer/Trash) always sorted before real files -- confirmed wrong via direct question. The whole list now sorts alphabetically by label, case-insensitive, fixed icons included. 3. "Set as Wallpaper" was the wrong feature: removed entirely (DesktopMenuAction::SetWallpaper, general.wallpaper_command, is_image_path). The user wants that handled by their real file manager once opened, not reimplemented here. Also adds real menu functionality per "where are all the options": Rename (inline text edit, new CompState::renaming_icon field and keyboard redirect mirroring NativeLock::password's existing precedent), Delete (moves to ~/.local/share/Trash per the freedesktop.org spec, new trash.rs module, same-filesystem case, no confirmation - this is the reversible move-to-trash, not a permanent delete), Empty Trash on the Trash icon, and Open Terminal Here / Open in File Manager on the bare-desktop menu (new general.terminal config key). 133 wayland-crate tests (up from 106), full workspace build and clippy clean.
2025-08-15Fix a dragged/resized window rendering wrong on a different-scale monitor ↵srdusr2-11/+91
mid-gesture Reported live: moving a window onto the other monitor "looks very messed up". This machine's two real monitors have genuinely different scales (eDP-1 at 1.0, HDMI-A-1 at ~0.843) - the exact condition needed to expose this. WindowManager::update_drag/update_resize only corrected w.monitor once, at end_drag (update_resize never corrected it at all, not even at the end) - but state/geometry.rs::sync_geometry reads that field on every motion tick to pick which monitor's scale converts the client's physical size into the logical points xdg_toplevel::configure sends it. Crossing onto a different-scale monitor mid-drag kept every configure computed against the origin monitor's stale scale for the gesture's whole remaining duration, only self-correcting once the button came up. Both functions now re-derive w.monitor from which monitor the window's live geometry actually overlaps, every motion tick - the same Rect::overlaps lookup end_drag already used once at the end, now run continuously instead. end_drag's own fixup stays as a final-word safety net for a drag that starts and ends between two motion ticks. Does not close the related, already-documented gap where a client that doesn't speak wp-fractional-scale-v1 still mismatches once settled on a sub-1.0-scaled monitor - this only fixes the stale-during-the-gesture half. Two new tests, full workspace suite and clippy clean.
2025-08-15Add real desktop icons plus right-click desktop/icon context menussrdusr1-0/+39
Closes "right-click on bare desktop" - previously a true no-op, nothing rendered above the wallpaper at all. Requested directly: a real desktop "just like windows does" - Home/Computer/Trash plus one icon per real ~/Desktop entry, individually draggable with persisted grid positions, double-click to open, right-click menus (per-icon "Open" plus "Set as Wallpaper" for image files when general.wallpaper_command is set; bare desktop "New Folder"/"Refresh"). Architecture mirrors the existing context_menu.rs/snap_flyout.rs "compositor-owned floating UI" pattern: desktop_icons.rs (data model, grid layout, filesystem scan, hit-test), desktop_icons_state.rs (JSON persistence, same shape as monitor_layout.rs), desktop_menu.rs (the new right-click menu, reusing decoration::render_context_menu's existing rasterizer), state/desktop_icons.rs (CompState glue: rescan/select/drag/ open/persist), and a new decoration::render_desktop_icon rasterizer -- hand-drawn glyphs, since no PNG/SVG decoding capability exists anywhere in this workspace. Wired into both render loops (udev and winit) above the wallpaper and below every window, and into input/pointer.rs's button/ motion handlers for selection, drag, double-click, and both menus. Four new config keys: general.desktop_icons (default true - a directly requested, purely visual feature, unlike the opt-in-while-experimental general.gpu), general.file_manager, general.desktop_icon_single_click, general.wallpaper_command (all default off/empty). Deliberately out of scope for this pass, stated up front: move-to-trash and "Empty Trash" (destructive, no confirmation-dialog primitive to gate them on yet), filesystem watching, multi-select, per-mimetype icon art, icons on any monitor but the primary one. 124 wayland-crate tests (up from 106), full workspace build and clippy clean.
2025-08-10Make corner resize reachable at the button corner, and widen it everywhere elsesrdusr1-3/+11
Reported live: "even where decorations are corner i should still be able to corner resize, just its... hitbox... does not get in the way of the close icon" - the titlebar corner holding Close/Maximize/Minimize had no resize target at all, by design (competing with the close button was judged worse than losing that one corner). The BUTTON_CLUSTER_MARGIN strip between the button cluster and the frame's true edge was already dead space no button claims, regardless of button_count - its own top DECORATED_TOP_RESIZE_MARGIN rows now register as the diagonal corner (TopLeft/TopRight) instead of falling through to Top/Drag, without touching the button's own hitbox at all. Also requested: widen the other three corners' own resize zone, since a user reaching for a plain edge-resize instinctively aims for the middle of that edge, not its corner - a bigger corner zone doesn't compete with that instinct the way a bigger RESIZE_MARGIN would compete with ordinary content clicks near an edge. CORNER_MARGIN raised from 3 to 5 (18px to 30px at the default resize_margin). Updated one existing test whose own per-window resize_margin override (30px) now put its plain-edge test point inside the widened corner zone on a window too short for the two to stay apart - taller geometry, same edge point relative to center, no change to what it actually verifies. Added coverage for the new button-corner resize target on both sides, and for the dead strip's own non-corner rows still just dragging as before.
2025-05-28Add a real general.gpu config option for the GPU render pathsrdusr1-0/+18
SRDWM_GPU=1 was the only way to opt into the udev backend's GBM+EGL GPU render path - an env var, not a real config option, with no way to enable it from init.lua the way every other general.* flag works. WindowManager::gpu_enabled (plain bool, false by default - unlike rounded_corners_enabled's Option<bool>, GPU rendering has one unambiguous default regardless of which backend ends up connecting, so there's no "let the backend decide" case to preserve) is read from general.gpu in apply_general_settings, same as every other general.* key. gpu::probe now takes an explicit enabled: bool instead of checking the env var itself; udev/platform.rs's call site computes it as wm.gpu_enabled OR SRDWM_GPU=1, so the env var still works as a quick manual override for testing without touching config, on top of the new persistent option. Falls back to the existing software (Pixman) path exactly as before on any failure at any step (no GBM device, no atomic-modesetting support, a software-only EGL renderer, ...) - gpu::probe's own fallback behavior is unchanged, only how the initial enabled/disabled decision gets made.
2025-02-15Checkpoint: preserve all uncommitted rust-rewrite worktree worksrdusr7-29/+1000
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
2025-02-14Fix workspace switches undoing themselves within millisecondssrdusr1-8/+1
sync()'s per-tick platform.focus(id) re-assertion (added to keep real Wayland/X11 keyboard focus following core's own bookkeeping) ran unconditionally on every dirty tick, including when nothing about focus had actually changed. focus_window (core) has its own, separate side effect of switching to the focused window's workspace when it differs from the current one - correct when focus genuinely moves to a window on another workspace, but this call was never gated on focus having changed at all: switching workspace via activate_workspace left the still-focused window's own workspace field untouched, so the very next dirty tick's blind re-assertion of that same focus saw a mismatch against the just-changed current_workspace and switched straight back. Confirmed live via temporary core-side logging: two switch_workspace calls a few milliseconds apart, the second one undoing the first every single time, for every workspace switch that didn't also change which window was focused. Gated the re-assertion on the focused id actually changing since the last sync() call. Real focus-follows-real-platform-focus still happens on every genuine change, which is all the original fix needed.
2025-02-11Add temporary diagnostics for two live-reproduced bugssrdusr1-1/+8
1. A popup's xdg_popup.grab (Firefox's own right-click menu, concretely) receiving zero pointer input at all - no hover highlight, no click effect, not even dismiss-on-miss - logs whether grab_popup actually succeeds, since a silent failure there would explain exactly this. 2. srd dispatch activate_workspace returning {"ok":true} without ever changing the current workspace, confirmed via a raw socket request bypassing the CLI entirely. switch_workspace's own logic reads correct; logs its actual inputs/state to find out why the real process disagrees with it. Remove once both are resolved.
2025-02-03Fix focusing a minimized window not restoring itsrdusr2-0/+37
focus_window marked the target focused without clearing minimized, so a dock icon's Activate (or the plain "focus" IPC command) on a minimized window left it focused but still excluded from visible_windows/rendering - reads exactly like the click did nothing, since the window never actually reappears. Every focus_window caller gets the restore for free now. Added a Super+n keybind for minimize too, since none existed at all before this.
2024-11-30Accumulate core-crate additions: capture requests, focus/workspace fixes, ↵srdusr6-27/+449
test coverage Bundles several related changes to crates/core built up over this session rather than committed incrementally: - WindowManager::request_capture_workspace/drain_capture_requests (new manager/capture.rs) - backend-agnostic queuing for an off-screen workspace render, see the wayland-side commit for why this exists. - focus_window now switches workspace as a side effect when the target isn't on the current one, matching Hyprland's focuswindow convention (manager/focus.rs). - Assorted window/rules/theme field additions and their test coverage. Left less granular than the repo's usual one-purpose-per-commit convention deliberately: these accumulated across a long session without being committed as they landed, and are too entangled line-by-line to safely split apart now without risking mis-attributing changes to the wrong commit message.
2024-11-26Add Monitor::maximize_geometry: maximize covers a dock, still stops at a top barsrdusr2-2/+46
toggle_maximize previously targeted full_geometry outright (past every reserved zone), on an earlier request specifically about the dock - which also silently let it extend behind a top bar's zone, reported back as its own bug once live-tested. maximize_geometry is a third rect distinct from geometry (every zone) and full_geometry (none): full_geometry with only a top-anchored bar's exclusive zone subtracted back out. New test locks in dock-covered/bar-respected together.
2024-10-30Add srdwm's own native session-lock screen (blur, PAM auth)srdusr1-0/+51
A real ext-session-lock-v1 implementation drawn by the compositor itself, not a hand-off to an external locker process: a live-blurred background, PAM authentication on a background thread, and its own config surface (lock_config.rs) rather than hardcoded appearance.
2024-08-24Implement general.focus_follows_mouse/auto_raise; remove the rest as deadsrdusr1-0/+13
Auditing "clicking behavior and basics": general.focus_follows_mouse, general.mouse_follows_focus, general.auto_raise, general.auto_focus, the entire window.* namespace (8 more keys, a full duplicate of the same four plus remember_position/size/state), and general. smart_placement/border_width were all seeded into default_config() and documented in DEFAULTS.md, but none were read anywhere - srd.set()/ srd.get() on any of them silently succeeded while doing nothing. focus_follows_mouse is real, well-defined, and directly relevant to clicking basics - implemented it plus auto_raise (raise, not just focus, on hover) rather than just deleting the promise like the others. WindowManager gained focus_follows_mouse/auto_raise bools, wired from apply_general_settings the same way every other general.* flag is. handle_pointer_position now tracks whichever window (content or decoration) is under the pointer and, when the setting is on and that differs from the currently-focused window, focuses it through the same focus_window() free function every click-driven focus change already uses (real keyboard focus, not just core state) - skipped entirely while dragging/resizing or over a layer-shell surface, so the pointer sweeping over other windows mid-drag or hovering a bar can't steal focus from what's actually being manipulated. mouse_follows_focus (pointer warp on keybinding-driven focus change) and auto_focus (no clear distinct meaning beyond click-to-focus) stay unimplemented and are now undocumented rather than promised.
2024-08-22Add per-window resize-margin override (Hyprland's extend_border_grab_area)srdusr3-1/+25
MISSING.md had listed this as needing to touch srdwm_core::window:: hit_test's signature at every call site, including the honest-stub Windows/macOS backends - overstated on closer inspection: that shared function already takes a plain resize_margin: i32 parameter, agnostic to where the value comes from. The only real change needed was reading Window.resize_margin.unwrap_or(wm-wide default) instead of always the WM-wide value, at the single call site inside WindowManager::hit_test in core - no backend touched at all. Window.resize_margin: Option<i32>, WindowRuleActions.resize_margin to match, applied in add_window/reapply_rules_if_pending the same way opacity already is. Settable via a rule action or srd.window.set_resize_margin(n) on the focused window.
2024-08-09Fix a misleading workspace comment and drop four dead config keyssrdusr2-1/+11
visible_windows' doc comment claimed windows show "on the active workspace of whichever monitor they're assigned to" - the code never reads w.monitor at all; current_workspace is one flat value shared by every monitor, not per-output. Documented that explicitly on both the field and the method, since this is a real behavioral difference from Hyprland worth a reader actually seeing, not just an inaccurate comment to fix quietly. monitor.primary_workspace/monitor.workspace_count describe a per- monitor-workspace design that doesn't exist; workspace.auto_switch/ workspace.persistent were never wired to any behavior. All four were seeded into default_config() and documented in DEFAULTS.md, so srd.set()/srd.get() on them silently succeeded while doing nothing -- removed from both, matching the precedent already set by general. rounded_corners' deliberate absence from default_config for a different reason (backend-dependent default rather than unbuilt).
2024-08-08Fix clicks landing on a window hidden on another workspacesrdusr2-5/+39
WindowManager::hit_test/window_at filtered only by `!w.minimized`, never by workspace - but a window on a workspace that isn't current is not minimized, it's just not shown. Rendering (visible_windows/ visible_windows_front_to_back) already restricted to the current workspace; hit-testing didn't, so a click landing on where an invisible window's stale on-screen geometry happened to sit routed to that window instead of whatever was actually visible underneath. Reported live. Fixed by adding the same workspace check rendering already uses, plus a regression test with two identically-positioned windows on different workspaces.
2024-07-11Split crates/core/src/manager.rs (2048 lines) into manager/srdusr10-0/+2128
Pure reorganization, no behavior change - verified by diffing the function-name set before/after (identical 130 functions) plus a full cargo test pass. WindowManager's struct/field definitions, Default, new(), and the three trivial constructors (add_rule/register_layout/ available_layouts) stay in mod.rs; the rest of the single ~950-line impl block is split into one file per the section comments the file already had (monitors, windows, focus, winops, hittest, dragresize, workspaces, layout). Three methods called across section boundaries (monitor_for, windows_on_workspace, cycle_focus) went from private to pub(super) - Rust's privacy model doesn't let sibling submodules see each other's private items, only a defining module's own descendants. The ~1000-line test module moves to manager/tests.rs unsplit: its helpers (wm_with_monitor, two_monitors, monitor_with_dock) are shared across tests for every section, so splitting further would mean duplicating them or adding another shared-support file for little benefit.