srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src
AgeCommit message (Collapse)AuthorFilesLines
2026-08-30Indent doc list continuationssrdusr3-19/+19
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 onesrdusr3-14/+231
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-08-05Show a keybinding the way a person writes it, and let bind_repeat be describedsrdusr2-1/+53
Measured with dotfiles-1a, who own the launcher that displays these: `srd keybindings` returned 84 bindings and 84 empty descriptions - 100% - so every entry their launcher showed was a bare key combo with nothing to say what it does. Two causes, one on each side of the boundary. `srd.bind_repeat` never accepted a description. `srd.bind` has taken an optional third argument all along, but its repeating sibling took only two, and mlua drops a surplus argument silently rather than raising - so a config that documented its repeat bindings got no error and no description. It now takes one exactly like `bind`. The combos themselves were reported in their internal dispatch form: `Shift+Mod4+h`. `Mod4` is the X11 modifier's name, not a key's; nothing on a keyboard is labelled Mod4, and the canonical Ctrl/Shift/Alt/Mod4 ordering renders the owner's own `Super+Shift+h` binding back to them inside out. `srd keybindings` now reports a display form - Super, and the order people write - while everything internal keeps the canonical form it dispatches on. The display form parses back to the same binding, so it can be pasted into a config, and a test pins that round trip rather than trusting it. The owner's own config now describes all 84 bindings; verified through the real path, in a nested compositor running that config: 84 of 84 described, zero occurrences of "Mod4".
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-13Give every window an outward resize band, so a borderless one can be grabbedsrdusr1-9/+70
Reported as resizing working "from one direction" and feeling "very cheap". The outward half of the grab zone was `border_width` alone. The inward half is deliberately narrow - 3px on an undecorated window, narrowed on purpose because a wider band was stealing clicks from Nemo's own tab-close and minimize buttons near the edges. So a borderless window's entire resize target was that 3px, which is missed far more often than hit and reads as an edge that only sometimes resizes. Turning borders off for the macOS look made it strictly worse: the border had been quietly providing the only outward reach. The fix belongs outside the frame, not inside it. Pixels beyond a window's own edge have no client content to steal a click from, so the band there can be generous regardless of border width. RESIZE_OUTSET is now a floor on the outward reach, with the drawn border used instead when it is wider. macOS and GNOME both let a pointer grab slightly outside a window's visible edge for the same reason. An existing test asserted the old behaviour - that with no border a point outside the frame is not a hit - so it was rewritten to exercise a border wider than the new band rather than have its assertion quietly flipped. Two tests added: that a borderless window is grabbable across the whole band and one pixel past it is not, and that the band reaches all four edges rather than just the left. 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-06-01Let negotiating clients be forced to server-side decoration, and document ↵srdusr1-0/+23
the GTK half Asked to research how KDE and GNOME make decorations consistent, after being told too quickly that srdwm could only control its own titlebar. Measured against a nested srdwm, one client at a time: Qt/KDE creates a decoration object and asks for server-side; winit creates one and asks for CLIENT-side; GTK never creates one at all. The xdg-decoration spec says the compositor "can decide not to use the client's mode and enforce a different mode instead" and that the client "must obey" - so the first two are srdwm's to decide, and it had simply been deferring. The same spec closes the door on the third: a client that does not negotiate continues to self-decorate, and GTK is not having the conversation. New theme.decorations.force_server_side, default off, overrides the client's requested mode. Verified: with it off Alacritty draws its own content to the window's top edge, with it on the same window gets srdwm's titlebar -- (0,0,0) versus (46,52,64) sampled at three rows. Off by default because it cannot move a GTK button and can stack srdwm's titlebar on a client that draws its own regardless, which is the Firefox case already recorded here. The GTK half needs no compositor code, only the desktop setting GTK actually reads - xdg-desktop-portal's org.gnome.desktop.wm.preferences button-layout for GTK4, gtk-decoration-layout for GTK3. This machine was serving "close,minimize,maximize:" (left) while srdwm's own button_side was right, which is the entire mismatch. Documented in DEFAULTS.md with the mapping from button_side to layout string, and noted that this is exactly what kde-gtk-config does for KWin. 525 tests pass, clippy clean.
2026-05-15Fix the maximize border on the path that actually runs, and three spawn faultssrdusr1-13/+92
The maximize border was still drawn because the earlier fix landed on the wrong branch. udev/render.rs has three border blocks: the SRDWM_GPU=1 path at the top and two Pixman ones below. The patch replaced the first match in the file, which is the GPU branch a real DRM session never runs. All four sites across both backends are now gated on !maximized. The verification had failed twice for a separate reason: winit/capture.rs did not draw border strips at all, so a screenshot could never answer "is there a border here" and the control passed for the wrong reason. Border strips are now drawn into that pass as solid fills - corner rounding is not reproduced, so a capture is not pixel-exact at the corners, but presence, position, thickness and colour are. With that closed the test has a real control: unmaximized gives 6 accent pixels at x=800..805, exactly the configured border_width, and maximized gives none at the right edge or along the top row. That proves the winit path; the Pixman path is the same change at two more sites and is not separately confirmed on screen. Windows spawning as squares, partly off-screen, and always on the left were all SmartPlacement::grid. It returned size.min(cell), shrinking every window to its grid cell whatever size it asked for; it scanned cells in reading order and took the first free one, which is the leftmost; and nothing clamped the result, so a window larger than its cell could hang off the edge with its border out of view. The cell now decides only where a window goes, the scan starts from a rotating cell, and both grid and cascade clamp into the usable area. Four tests, one per reported symptom. 525 tests pass, clippy clean.
2026-05-13Report whether a listed key binding is actually grabbedsrdusr3-1/+63
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 ↵srdusr8-10/+194
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-05-11Three live bug reports after a restart: icon drag, lock cursor, lock boxsrdusr1-0/+16
All three reported directly after the owner restarted into today's build. Desktop icons could not be dragged at all in single-click mode. The press handler opened the icon immediately when general.desktop_icon_single_click was on, so the branch that starts a drag was unreachable and an icon could never be moved. Deciding activation on press cannot distinguish a click from the first instant of a drag. Every press on an icon now starts a potential drag and release decides which it was, using a 4px movement threshold that latches once exceeded. Double-click mode goes through the same path, so both modes now drag identically. The lock screen drew no cursor. The cursor push in the udev render loop sits inside `if !locked`, and a locked head renders only the lock element list, so nothing drew a pointer - and on a bare TTY nothing else does. The on-screen keyboard's clicks were being handled correctly the whole time (native_lock_click); they simply could not be aimed. The pointer is now prepended to the lock element list, above the UI it is used to click. The password field's opaque panel is gone. New LockConfig::box_opacity, default 0.0: no fill, no border, no rounded rectangle, just the dots and status text over the blurred background. Raising it restores the panel at that opacity for anyone who wants a solid field. Drawing text on a transparent surface needed a new blit_glyph_over: the existing blit_glyph blends against one flat opaque colour and writes alpha 255, which would have turned every glyph into a block of the assumed background - the same box with its middle removed. VERIFICATION STATUS, stated plainly: all three are code-complete and the suite passes, but none is confirmed on screen. The nested backend's capture pass does not draw the desktop icon grid (a gap already recorded in winit/capture.rs), so the icon drag cannot be checked by screenshot there, and aiming blind is what this project's own rules forbid. The two lock changes were not visually checked either. 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 transcriptsrdusr10-39/+465
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.
2026-01-31Fix zathura's double titlebar by broadening the CSD-detection heuristicsrdusr1-1/+27
Reported live, found via a screenshot of the user's real open window: srdwm's own server-side titlebar stacked on top of zathura's own girara-drawn header, same class of bug Firefox/Nemo needed a fix for. likely_draws_own_titlebar only matched org.gnome.* app ids; zathura's app id (org.pwmt.zathura) fell through it entirely, with no rules.lua entry to catch it either. Broadened the heuristic to also match org.pwmt.* - PWMT's small toolset (zathura the only one in common use) shares GNOME's own "always draws its own header" property, on the same live evidence that justified org.gnome.* in the first place. No rules.lua entry needed; the heuristic catches it automatically now. Reproduced and confirmed fixed in a disposable nested compositor (built, tested double-decoration, then rebuilt with the fix and retested clean) - never the live session itself.
2026-01-28Redesign the titlebar right-click menu: real separators/headers, live ↵srdusr1-30/+208
customization Reported live: "looks very ugly currently and some of it doesn't make sense." Both were real. Every row, including a bare divider, took one full TITLEBAR_HEIGHT slot, so a separator was a 1px hairline in the middle of 32px of empty space; "Move to Workspace" faked a section caption by embedding box-drawing characters directly in an ordinary item's label, which rendered - and behaved, until the click-dispatch site's own special case - exactly like a clickable row that did nothing. Separately, "Floating" was always offered even though Window::floating only affects the "tiling" layout: toggling it under this project's own default "dynamic" layout visibly changes nothing, reading as a broken control rather than an inapplicable one. ContextMenu (crates/core/src/context_menu.rs) gained real Separator (9px) and Header (22px, non-interactive, dimmed) row kinds with their own small heights, replacing the label-hack outright. Both backends' rendering now sum each row's own real height instead of assuming one uniform value, so hit-testing and pixels can't disagree about where a row is. Floating is omitted entirely outside the tiling layout. New, in direct response to "allow customizing from there as well": a Customize section with live Button Style / Button Side toggles. Each flips the matching ThemeConfig field and immediately redraws every open window's titlebar - not routed through srd set's own path, which is scoped to windows created after the call for lack of a redraw hook it can reach; a menu action that didn't visibly change the titlebar you clicked would be its own "doesn't make sense" bug. Full workspace build/test/clippy clean (242 core tests, +8; 152 wayland, net-even after rewriting the old label-hack tests).
2025-12-01Let a new window pick its own size instead of forcing a guessed placeholdersrdusr4-0/+79
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-20Redesign the native lock screen: clock/avatar header, on-screen keyboard, ↵srdusr1-0/+22
wrong-password shake The native lock UI was a flat bordered rectangle with three left-aligned text lines and no shadow, clock, or identity marker - reported directly as looking unfinished. Splits the redesign across a new transparent-canvas header (time, date, circular avatar, username) above a redesigned, centered password box with a real drop shadow and a dimmed placeholder prompt, plus a genuine on-screen QWERTY-shaped keyboard with working Shift/Backspace/Return/Space and real click hit-testing shared with the render path via one `lock_stack_layout` function, and a damped-sine shake on a failed attempt. LockConfig gains show_clock/show_keyboard/avatar_bg, each independently srd.set-able and documented in a new theme.lock.* section in DEFAULTS.md. native_lock_render_elements now takes one NativeLockFrame struct instead of positional buffer arguments now that it composites five optional layers instead of two. Full workspace build/test/clippy clean (152 wayland tests, +6 new).
2025-11-16Live-expose workspace.per_monitor, titlebar buttons, and desktop iconssrdusr2-1/+33
Closes the remaining "config-file only" gaps from the AGS capability survey. workspace.per_monitor gets srd set per_monitor <bool> - safe to flip live since a monitor with no per-monitor override already falls back to current_workspace regardless of mode, so nothing visually jumps on toggle. theme.decorations.title_bar.button_style/button_side/button_order and general.desktop_icons/desktop_icons_all_monitors all get the same live-set + SettingsResponse readback treatment as everything else this session. New srdwm_core::format_button_order is parse_button_order's exact inverse, so button_order's readback matches the same string shape srd set itself accepts.
2025-11-07Make tiling's master/stack ratio live, add settings readback everywheresrdusr7-8/+340
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-28Fix a fake monitor's layer-shell surfaces misrouting onto the real primarysrdusr1-1/+31
Live incident, root-caused jointly with the AGS peer session: creating a fake monitor visibly shrank the real primary output's usable area (full_y stayed 0 throughout - its true position never moved) each time, tracking almost exactly one bar height per fake monitor created. create_virtual_head registered its new Output in udev.virtual_heads but never in CompState::outputs, the list output_for_wl searches to resolve a client-named wl_output back to anything. new_layer_surface's own fallback for an output it can't resolve is landing on the primary output - so AGS's own per-monitor bar, aimed at the fake monitor it reasonably believed was a new real one, silently landed on the real primary output instead, stacking its own exclusive-zone reservation on top of the real bar already there. Two fake monitors, two misrouted bars, two zone increments, matching the observed climb exactly. Fixed by registering (and, on removal, deregistering) a virtual head's Output in CompState::outputs the same way bring_up_head already does for a real one. Also adds Monitor::is_virtual / MonitorInfo's "virtual" JSON field, requested directly by the AGS peer session as the real discriminator their own temporary FAKE- name-pattern match was standing in for. The X-position half of this same incident was AGS's own remembered- layout restore treating a fake monitor's wl_output as a real hotplug -- already fixed on their side (readArrangeable() now filters split/virtual outputs).
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 spotsrdusr5-23/+221
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 primitivessrdusr7-61/+559
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-25X11 backend: right-click titlebar window menu, matching Wayland's ownsrdusr2-0/+197
Closes the one real gap an X11/Wayland feature-parity audit found this session (desktop icons, window-position memory, and static exclusive-zone reservation were already shared or Wayland-only by nature - see docs/TODO.md's own audit entry for the full breakdown). MenuAction/ContextMenu (row set, labels, row_at hit-testing) move from crates/wayland/src/context_menu.rs into crates/core/src/context_menu.rs -- pure state and geometry with nothing Wayland-specific in it, so X11 needing the same rows is shared data, not duplicated logic. The Wayland crate's own context_menu.rs is now a one-line re-export so every existing crate::context_menu::... call site keeps working unchanged. X11 has no compositor-level input dispatch to intercept every click the way Wayland's input/pointer.rs does, so the X11 side (crates/x11/src/platform/context_menu.rs, new) draws the menu into its own small override-redirect popup window and grabs the pointer for the duration so a click anywhere dismisses it, matching the Wayland backend's own convention. events.rs's ButtonPress handler now reads the real button number instead of hardcoding every press as a left click - a real latent bug (right-clicking a titlebar button would have silently performed its left-click action). Live-verified end to end in an isolated Xvfb + srdwm --x11 instance: full row set including the workspace picker, Minimize runs and closes the menu, a second window's menu dismisses cleanly on outside click, normal focus/click behaviour continues working afterward. See docs/TODO.md for the full investigation and verification narrative.
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 elsesrdusr2-4/+86
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-30Detect XWayland dialogs via WM_TRANSIENT_FOR, not just native xdg_toplevel ↵srdusr1-9/+8
parent Window::is_dialog (close-button-only titlebar, no traffic lights) was only ever set from a native xdg_toplevel's own parent() - redraw_ decoration_buffer's is_dialog computation called dw.toplevel(), which is always None for an XWayland-backed DWindow (X11Surface's own accessor is x11_surface(), a different method), so the .unwrap_or(false) fallback made every XWayland dialog - a GTK "Save As", an app's own "About" box, anything setting the ICCCM transient-for hint - always draw with the full three-button titlebar and traffic-light colours, even though the feature this was built for explicitly wanted the opposite. Documented as a known gap at the time; now closed. redraw_decoration_buffer now also checks X11Surface::is_transient_for() for an XWayland window. property_notify gained a WmWindowProperty:: TransientFor arm that re-runs redraw_decoration_buffer, for a client that sets the hint slightly after its own initial map - the same "read fresh every call" pattern the existing xdg_toplevel::parent() check already relied on, extended to catch a late X11 property the way the Wayland equivalent (set_parent, any time) already was.
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 worksrdusr11-118/+1922
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, ↵srdusr10-30/+533
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 barsrdusr3-3/+64
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-11-01Add Snap-Layouts flyout (Windows 11-style maximize-button hover menu)srdusr1-0/+102
Right-clicking (or hovering) the maximize button opens a flyout of fixed half/quarter positions (snap_flyout.rs renders it); picking one applies that zone via WindowManager::apply_snap_zone, the click-driven equivalent of dragging a window to that edge and releasing.
2024-10-31Add global-menu support (dbusmenu/appmenu) for Wayland and X11 clientssrdusr1-5/+106
Exposes each window's application menu (Firefox/GTK's dbusmenu export, X11's _GTK_APPLICATION_OBJECT_PATH-style menus via global_menu.rs) so an external panel can render it as a system menu bar rather than each window drawing its own, the same convention appmenu.rs/gtk_shell.rs and appmenu_registrar.rs wire up across both backends.
2024-10-30Add srdwm's own native session-lock screen (blur, PAM auth)srdusr2-0/+105
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)srdusr5-1/+34
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-20Give corner resize real priority over a plain edgesrdusr1-11/+118
Reported live, repeatedly: corners felt like they had no priority over sides. They didn't, structurally - a corner only ever registered in the exact pixel square where both edges' own resize_margin zones happened to overlap (6x6px at the default 6px margin), nothing wider. A click a little further along either axis, still clearly aiming for the corner, fell back to a single straight-edge resize instead. resize_edge_at now checks a separate, wider corner zone (CORNER_MARGIN * resize_margin) first, so a corner claims a real, deliberately larger target the way GNOME/KDE already do - diagonal resize is a harder grab than a straight edge and deserves more room, not the same or less. Also fixed a related, previously-unreachable case: a *decorated* window's whole titlebar band returned Drag/Close/Maximize/Minimize unconditionally, so top-left/top-right diagonal resize never had a code path at all, even at the titlebar's own corner pixels. Added top-left resize as a small, genuine corner square (both axes, not just one) checked before drag/buttons. Deliberately did NOT add the matching top-right case: that corner is the close button on every mainstream desktop, and trading a well-known, expected target for a rarely-wanted one at exactly the spot a miss costs the most isn't a trade worth making. Four new regression tests cover: reaching a corner past the old tight margin, falling back to a plain edge just past the new wider one, the newly-reachable decorated top-left corner, and confirming top-right still closes rather than competing with resize.