| Age | Commit message (Collapse) | Author | Files | Lines |
|
My own testing changed the owner's GTK button style. Nested instances were
started with a scratch srdwm config, which naturally does not set
button_style, so the built-in default applied and publish_gtk_stylesheet
wrote traffic-light CSS into the real ~/.config/gtk-3.0/srdwm-buttons.css --
changing how every GTK app looked in the actual session, from a throwaway
compositor that was testing something unrelated. Their own config and the
running compositor both said "traditional" the whole time; only the
generated file disagreed, which is why it looked like the feature had
regressed.
Both publishers now return early when running nested, using the same test
srdwm_wayland::connect uses to choose the winit backend over udev/DRM: a
host WAYLAND_DISPLAY or DISPLAY means there is already a session and this
process is a window inside it. A nested instance is a test window, not the
shell, and has no business rewriting settings the real session is using.
Verified: with the guard in place a nested run leaves the file byte-identical
(md5 before and after), where before it would have rewritten it from the
default.
This is the same class of mistake as sending synthetic clicks to the wrong
compositor earlier - a test instance reaching outside its own sandbox --
and it wants a structural guard rather than remembering to set HOME.
533 tests pass, clippy clean.
|
|
Reported as the titlebar not resizing at the same time as the window, and
as resizing feeling cheap.
effective_frame_of returned the live drag target while a resize was active,
so the titlebar and border tracked the pointer while the client's actual
pixels were still whatever it last committed. The two disagreed for the
whole drag, and the decoration leading its own content is what reads as
broken.
It now returns the committed size anchored to whichever edge the drag is
holding still - exactly the rect the content occupies, since sync_geometry
positions it the same way, so the two agree by construction rather than by
timing. The consequence is that the frame sits one commit behind the
pointer instead of ahead of its own content. That is the trade every other
compositor makes, and it is the right way round: a frame glued to its
content and slightly behind the cursor reads as solid.
The committed-size correction was extracted into committed_frame so the
resize path and the ordinary path share it rather than having two versions
that can drift, and so the resize path is no longer short-circuited by the
pending-configure branch, which fires constantly during a drag precisely
because every tick sends a configure.
533 tests pass, clippy clean. The arithmetic is covered where it is
testable; how it feels mid-drag is a judgement only real hardware can make,
and a nested drag did not reproduce a clean enough scenario to claim it.
|
|
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.
|
|
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.
|
|
Three reports, three separate causes.
Alternate characters were invisible. A keycap drew only the character the
current shift state types, so there was no way to find a symbol without
pressing Shift and hunting for it - a physical key is labelled with both.
Each cap now draws the other state's character small and dimmed in its
top-right, skipped where the two are the same (every letter differs only by
case, which the cap already shows) and for named keys like Enter.
"Enter Password" rendered in a monospace font. find_system_font
deliberately prefers a mono face, which is right for a titlebar title or a
menu row sitting in columns and wrong for a sentence; on this machine it
resolves to DejaVu Sans Mono. New find_ui_font prefers a proportional face
from a ranked list of widely-installed families, falling back to the mono
one, and the lock screen's prose - prompt, clock, date, username, status --
uses it. Ranked rather than first-found so the result does not depend on
directory order, which is how the mono scan once picked an italic face and
rendered every titlebar in italic.
The password dots had no spacing. They were drawn as a plain string, so a
run of identical bullets separated only by their own advance read as one
smeared blob rather than countable characters. Added explicit tracking,
excluded from the centring width so the row does not sit half a gap left.
531 tests pass, clippy clean.
|
|
An earlier cleanup deleted five of them from window-memory.json by hand and
they came back within minutes. The reason is that a running compositor holds
the whole table in memory and save_all writes all of it back on the next
window close, so a hand-edited file cannot survive a running session.
Filtering on load is the only point where the fix sticks.
remove_window already refuses to record a size the client never chose, so
nothing new is captured this way; this clears what was written before that
landed. Those entries are self-perpetuating - a remembered size makes the
next launch non-provisional, which forces the client to that size rather
than asking it to pick, which writes the same value back on close - so an
affected app can never escape on its own.
A window genuinely sized exactly 800x632 loses its remembered size once and
gets it back at the next real resize or close. Two tests cover the exact
match and that sharing only one dimension is not enough.
531 tests pass, clippy clean.
|
|
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.
|
|
character
Two gaps, both found by being asked about them.
~/.face was never read. The file exists here, a 300x300 JPEG, and a grep for
.face/AccountsService/avatar_path returned nothing anywhere in the codebase:
the lock screen drew a coloured circle with the user's initial
unconditionally. It now looks for ~/.face, ~/.face.icon, then
/var/lib/AccountsService/icons/$USER, which is where GNOME and KDE keep the
picture their settings UI sets. Scaled to cover the circle and
centre-cropped rather than letterboxed, and masked with a soft edge. Falls
back to the initial when nothing is set or the file will not decode.
That needed a raster decoder, since ~/.face is JPEG and the only image code
here was resvg, which is SVG-only. Added image with default features off and
only jpeg and png.
The on-screen keyboard could not type most passwords. It had letters,
digits, and the digits' own shifted symbols, and nothing else - no -, _, .,
/, =, [, ], ;, ', comma, backslash or backtick. For the case that keyboard
exists for, a session with no reachable physical keyboard, a password
containing any of those meant no way in at all. Every printable ASCII
character now has a key, with a test asserting the whole 0x20..0x7f range
rather than spot-checking.
The lock screen itself could not be screenshotted: locking a nested instance
hits the pre-existing EGL context-loss crash already recorded in docs/TODO.md,
confirmed again here. That is environmental and predates this change, so the
on-screen appearance still needs the real session. The avatar path is covered
by four tests including one that decodes the real ~/.face through the same
function the lock screen calls.
529 tests pass, clippy clean.
|
|
Asked for after the previous entry declined to write GTK CSS. The objection
was to clobbering a file full of hand-written work, not to the feature, and
splitting ownership solves both.
srdwm owns srdwm-buttons.css, generated from
theme.decorations.title_bar.button_style and rewritten on every start and
config reload, and adds exactly one @import line to the user's gtk.css if it
is missing. Nothing else in that file is ever touched. The import goes first
because CSS only permits @import ahead of other rules, which also leaves the
user's own rules last and therefore able to override the generated style.
The generated CSS sets a background per button rather than un-hiding a child
image, for the reason the earlier hand-written attempt found by screenshot:
WhiteSur paints the control as the button's own background-image from a
compiled gresource, so clearing that background leaves a blank button rather
than revealing a glyph.
Verified end to end on a real GTK app, both directions, through the
generated file: traffic_lights gives Nemo coloured dots, traditional gives a
dash, a square and an X, each matching srdwm's own titlebar directly above
it. Also verified the import is added once and not duplicated, that
switching style rewrites only the generated file, and that a home without
GTK config directories has nothing written to it.
527 tests pass, clippy clean.
|
|
Firefox and Nemo were reported as still showing traffic lights after the
button side was fixed. Neither was srdwm's doing.
Nemo, and every GTK app: ~/.config/gtk-3.0/gtk.css contained a deliberate
override from 2026-08-22 painting each titlebutton as a glossy macOS dot,
with the glyph hidden by opacity: 0. Its own comment records that it was
added when srdwm's own decoration drew traffic lights, so that every window
matched. srdwm's style has since changed to traditional and the stylesheet
was still enforcing the old look.
Firefox was already on its traditional variant, byte-identical to
userChrome-traditional.css with the legacy stylesheet pref enabled. It needs
only a Firefox restart.
A first attempt at the GTK fix produced invisible buttons, confirmed by
screenshot: clearing the coloured backgrounds and un-hiding the child image
left blank space, because WhiteSur paints the control as the button's own
background-image from its compiled gresource and there is no child image to
reveal. The working version supplies the icon explicitly via
-gtk-icontheme(). Verified by screenshot: Nemo's header now draws a dash, a
square and an X directly under srdwm's own titlebar drawing the same three.
Kept as a swappable pair, gtk-traditional.css and gtk-traffic-lights.css,
matching the convention Firefox's chrome directory already uses.
srdwm publishes the button layout itself but deliberately does not write
this CSS: the file is the user's, already held hand-written work, and a
compositor silently overwriting it would destroy customisation it cannot
understand.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
The seam bleed the owner reported as "windows show a bit in the other
monitor" was fixed and unit-tested but never seen. With monitor split now
working in a nested instance it can be, and it is.
Same window, same settings, same instance, same scanline. Control, right
edge at x=320 with no seam nearby: a real shadow, (9,9,13) two pixels out,
fading through (11,11,17) and (12,12,19) to the bare desktop (13,13,20) by
23px, a full 24px falloff. Seam, right edge exactly on the boundary at
x=640: (13,13,20) at every sample from two pixels out onward. Nothing
crosses.
Retracting the "SSD versus CSD" lead recorded in the previous commit. The
first attempt at this test looked like a pass until the negative control
failed - the same window off the seam had no shadow either. Narrowing it to
"Alacritty gets a shadow, Nemo does not" pointed at decoration, and that was
written down as a lead. It was neither: one temporary diagnostic in the build
path reported max=true. Nemo restores its own maximized state on startup and
the shadow gate correctly excludes a maximized window, which has no
neighbour to separate from. No bug, and decoration had nothing to do with
it. One log line beat two rounds of reasoning from symptoms; the lead is
retracted in the same file that stated it.
515 tests pass, clippy clean.
|
|
Earlier today I wrote that a nested compositor cannot produce a second
monitor because split and fake monitors "need real head machinery that only
the DRM backend has". That was inferred from both commands returning ok and
changing nothing, not read from the code, and the first half is false.
udev/platform.rs was simply the only backend draining those request queues.
The winit poll never took them off, so the request sat there forever and the
dispatch looked like it had worked. Nothing about split is DRM-bound:
MonitorSplit is bookkeeping in WindowManager and split_rect is pure geometry
in core. The winit poll now drains split requests the same way, and its
monitors() expands a split into one Monitor per part with its own
full_geometry and maximize_geometry, matching the udev expansion. Splitting
the nested output into two 640x800 monitors with a seam now works, which is
what a multi-monitor repro needs. Fake monitors stay udev-only; that half was
not re-checked and is not claimed either way.
Also corrected: the capture pass measured the shadow rect from w.geometry
while both on-screen loops measure it from effective_frame, the client's real
committed size. src indexes into a buffer rasterised at the frame's size, so
the two disagreeing reads the wrong region whenever a client settles on a
different size than it was asked for.
The seam check this was meant to unblock is still not done. With the split
working, the negative control failed: a floating window off the seam had no
shadow either. Running both clients in one instance showed Alacritty renders
a shadow in a capture and Nemo renders none, same settings, both floating,
either focus. Nemo is server-side decorated and Alacritty is not, which is a
lead and not a conclusion. Recorded in docs/TODO.md as open rather than
guessed at.
515 tests pass, clippy clean.
|
|
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.
|
|
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.
|
|
across a monitor seam
Nemo's right-click context menu was the last open punch-list item, parked
twice as untestable. It works: verified end to end in a throwaway nested
compositor, menu and submenu both, at the correct position and stacking.
The popup path itself needed no fix, so the POPUP-GEOM-DIAG/POPUP-GRAB-DIAG
diagnostics are removed.
Two real bugs turned up in the way of testing it.
zwlr_virtual_pointer was a silent no-op on the winit backend. Every
Motion/MotionAbsolute handler read UdevState::bounds() behind an early
return when state.udev was None, and that field is Some only for the DRM
backend. The protocol advertised its global, accepted create_virtual_pointer
and accepted every request, then discarded all motion with no error and no
log. That is the backend a nested instance runs on, so the only safe way to
drive a throwaway compositor - a Wayland client of that compositor, which
cannot reach any other session, unlike ydotool's /dev/uinput writes - did
not work at all. Bounds now come from WindowManager::monitors() when udev is
absent; both backends fill that list from Platform::monitors().
The winit backend's screencopy pass rendered no popups and no shadows. It
re-renders the scene offscreen, and that second scene was missing tiers, so
grim on a nested instance reported the opposite of the truth: a menu drawing
perfectly on screen photographed as absent. The DRM backend never had this,
since it serves screencopy from the on-screen frame it just drew. Border
strips are still missing from that pass, called out in the code rather than
left silent.
Also fixed, from the "windows show a bit in the other monitor" report:
shadow_rect expanded by SHADOW_SIZE on every side with no monitor-boundary
awareness, so a window flush against a seam put its 24px shadow strip on the
neighbouring screen. shadow_rect_clipped clips to the bounding box of the
monitors the window's geometry actually touches - not just its assigned
one, since a window straddling a seam really does occupy both and clipping
there would cut its shadow off mid-body. The bitmap's own extent stays
unclipped, because the src rectangle indexes into it; only the fragment list
is clipped. Six tests on the incident's own numbers. Not confirmed on
screen: the nested backend cannot produce a second monitor.
New tool: tools/virtual-pointer-click, a scriptable virtual-pointer driver
that acknowledges each command after its round-trip, so a test script can
put a screenshot between a move and the click that follows it.
489 tests pass, clippy clean.
|
|
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.
|
|
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.
|
|
toggle_floating
The tiled-shadow-tint fix earlier today gated the shadow on Window::floating
alone. arrange_workspace only reads floating under the "tiling" layout, so
every window on this project's own default "dynamic" layout starts, and
stays, floating: false - the gate misread that as "tiled, no shadow"
regardless of which layout was actually running, so shadows silently
vanished under dynamic mode entirely, recoverable only by pressing Super+S
(toggle_floating), which then looked like that key toggles a tint rather
than floating. Fixed by checking the workspace's own layout name first:
a window is only "currently tiled" when its workspace runs "tiling" AND
it hasn't opted out via floating. DecorationSignature's floating field is
now currently_tiled, since a layout switch changes this for every window
on a workspace without touching any of their own floating fields.
Also disabled general.shadows in the user's own config per direct
request - never asked for, on by default, and a real problem for
color-accuracy work regardless of how correctly it renders otherwise.
Also fixed both context menus (titlebar and desktop) silently truncating
labels past a fixed 170px width with no indication - widened dynamically
to each menu's own real widest label via a new measure_text_width helper.
|
|
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).
|
|
Diagnosed by a peer session (dotfiles-1a): SHADOW_SIZE is 24px, and a
tiling layout with a small gap_inner (as little as 1px live) leaves the
shadow nowhere to fall except onto the adjacent tile, darkening it by up
to SHADOW_MAX_ALPHA (~35%) on whichever side is unfocused. Not a content
tint or an opacity rule - verified against the actual rasteriser and the
live rule set before accepting the diagnosis.
A drop shadow separates a window from what's behind it; tiled windows are
coplanar and adjacent by construction, with nothing behind them to
separate from. redraw_decoration_buffer's shadow gate now requires
w.floating in addition to the existing !maximized/!fullscreen checks.
DecorationSignature gained a floating field so toggling floating on its
own invalidates the decoration cache instead of waiting for an unrelated
field to force a rebuild.
Live-verified in a nested compositor: two tiled windows show a clean
shared edge with no gradient bleeding across; floating a window still
detaches it from the tile group with its shadow intact; shadows still
toggle globally both ways.
|
|
The intro read as a marketing claim ("aiming to feel like a native window
manager... rather than a compromise") instead of stating what the project
actually does. Replaced with concrete, verifiable facts: every backend
draws a real title bar with drag/resize/minimize/maximize/close.
The status table implied Windows and macOS were unplanned ("designed, not
built") rather than real, in-progress work blocked only on hardware
access. All four backends are equal in design intent; Linux is verified
because it is the only platform with a working development machine right
now, not because the others are lower priority. Windows/macOS rows now
name the real cfg-gated code that already exists for each and state
plainly why it has never been built or run for real.
|
|
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.
|
|
Attempted to screenshot the redesigned lock screen in a disposable nested
compositor instance; the nested instance's own EGL context was lost the
moment the lock engaged, before any frame rendered. Reproduced the exact
same crash on the immediately prior commit, which never touches the lock
screen, confirming this is pre-existing sandbox/EGL flakiness (already
visible as transient BAD_ALLOC errors during ordinary nested startup, not
something this change introduced) rather than a lock-screen regression.
|
|
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).
|
|
The GPU (SRDWM_GPU=1/general.gpu) path had real window content but
square corners and no border/titlebar. A prior pass investigated a full
port of the Pixman path's decoration rendering and deliberately did not
attempt it blind, given no working GPU-capable hardware on this machine
to verify a single pixel of it against. Asked directly, twice, to build
it anyway rather than leave it.
Scoped smaller than a full port: border top/bottom strips and the
titlebar bitmap now render, reusing the exact cached MemoryRenderBuffers
the Pixman path already builds (renderer-agnostic pixel buffers,
imported for GlesRenderer the same generic way cursor::render_elements
already does for either renderer). Left out on purpose: occlusion-
fragment clipping against overlapping windows, and the left/right border
side strips plus the drop shadow.
Full workspace build/test/clippy clean. Explicitly not visually
verified - same reason as before, no GPU-capable hardware on this
machine.
|
|
Chrome/Chromium: launched a real google-chrome-stable in a nested
compositor and confirmed no double-titlebar - Chrome negotiates
ClientSide decoration itself, srdwm correctly doesn't stack its own SSD
on top, and the Unity-style menu row it draws is Chrome's own chrome,
not evidence of a bug. likely_draws_own_titlebar needs no new entry for
it.
Nemo: confirmed the same no-double-decoration result, but could not
safely test the actual right-click-shows-a-popup symptom - ydotool is
a uinput-level daemon shared with the live session, not scoped to the
nested compositor, so a blind synthetic click there risks landing in
the user's real desktop. Parked rather than guessed at; the existing
POPUP-GEOM-DIAG/POPUP-GRAB-DIAG diagnostics stay since the underlying
bug's status is still genuinely unknown.
|
|
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.
|
|
question
Investigated a live srd.monitor.scale path, the obvious next candidate
after monitor.split's own live path. Unlike split (a pure
placement computation), scale is only ever read by disable_connector_by_
name/enable_connector_by_name - applying it live means a real disable-
then-re-enable cycle on the physical connector, the same screen blank a
genuine unplug/replug causes. Parked the "is that an acceptable cost"
question rather than deciding it alone; left monitor.scale as Lua-
config/restart-only for now.
|
|
Reported by the aegis-fc peer session testing srdwm's own layer-shell
strut handling: a maximized X11 client sat 4-8px past the right and
bottom screen edges whenever its border was nonzero.
set_border_width sets the frame's native X11 border-width attribute,
which the X server draws outside a window's own declared width/height on
all four sides - unlike every other backend's own border in this
compositor (rendered as ordinary pixels inside the allocated geometry
rect). apply_geometry configured the frame at geometry's own x/y/width/
height verbatim, so a nonzero native border pushed the frame's true
visible footprint 2*border_width past every edge of what geometry
actually promised.
Fixed by shifting the configured origin inward and the configured size
down by border_width on both axes (frame_geometry_for, pulled out as a
pure function so it's unit-tested without a real X11 connection) - the
visible footprint, native border included, now lands exactly on
geometry. border_width == 0 reduces to the prior behavior exactly.
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
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.
|
|
Live report: the real cursor sometimes leaves a brief ghost behind right
after moving between monitors. The bare-metal render loop already forces
a full repaint (ages = [0, 0]) on a workspace switch or any window move/
resize/open/close/restack, both added earlier for the same underlying gap:
the damage tracker's own element diffing doesn't always catch a vacated
region on its own. Neither reset noticed the pointer leaving one monitor
for another - no window moved, no workspace changed - so that head's own
vacated cursor-sized region was left entirely to the tracker's diffing,
intermittently.
Adds UdevState::last_cursor_head, compared each frame the same way the
other two resets are; only the head the pointer just left gets forced back
to ages = [0, 0] (the newly-entered head draws a genuinely new element
there and diffs correctly on its own).
|
|
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.
|
|
Codebase modularization, requested directly. Surveyed the whole
workspace first: at ~38k lines it's already organized by topic
(crates/core/src/manager/, crates/wayland/src/{state,udev,decoration}/
already split into small per-concern files) - crates/platform/src/
ipc.rs was the one real outlier, 1894 lines holding the socket
lifecycle, every payload type, both dispatch match statements, and its
own tests all in one file.
Split into ipc/{mod,types,dispatch,tests}.rs by concern, matching the
established pattern exactly - mod.rs keeps IpcServer itself, types.rs
the response/event structs and snapshot functions, dispatch.rs
handle_request/handle_set, tests.rs the existing suite moved verbatim.
Extracted via exact line-range copies against git's own HEAD content
(not retyped), specifically to rule out a transcription bug in a file
this central. Pure reorganization: build/test/clippy clean before and
after, exact same test count (29 in crates/platform) both times.
README.md separately corrected: it still linked to legacy-cpp/ (deleted
this shift) and described the Wayland backend as the smaller, less-done
one - backwards from current reality, where Wayland is the daily-driver
target and by far the more complete backend.
|
|
Real web research (KDE's own source tree, current as of Plasma
6.6.5/2026): com.canonical.AppMenu.Registrar + dbusmenu is still the
current, unreplaced global-menu mechanism in KDE Plasma 6, and generic
Qt apps still export via the same QGenericUnixTheme path since Qt 5.7 --
exactly what srdwm's own appmenu_registrar.rs/appmenu.rs already
implement. No newer protocol to catch up to, no code gap found. srdwm's
own scope (discovery/registration) is correctly split from AGS's
(rendering) - see the FEATURE_GAP.md entry from the previous commit.
|
|
Two research-only entries, no code changes:
- docs/FEATURE_GAP.md gains a "vs. full desktop environments" section
(KDE/GNOME/XFCE/macOS/Windows), requested directly and distinct from
the file's existing tiling-WM (niri/sway/Hyprland) comparison. Verified
rather than assumed: real app-to-app clipboard already works
(delegate_data_device!), drag-and-drop between real windows already
works; genuine gaps are compositor-level blur-behind, the
already-tracked fractional-scale wl_pointer bug's real-world cost, and
no PipeWire screencasting - with an explicit line drawn between
srdwm's own scope and AGS's (notifications, applets, alt-tab UI,
screenshot tooling are shell concerns, not compositor gaps).
- docs/TODO.md: researched "different titlebars, non-traffic-light,
right side, especially firefox/chrome" and found the requested system
already exists and is already documented (button_style, button_side/
order, glyph-always, and a real, already-correct xdg-decoration
negotiation with Firefox's own specific behavior already documented).
One real, unverified gap found via actual web research into
Chromium's own Wayland decoration history: likely_draws_own_titlebar
only matches org.gnome.* today, and Chromium's xdg-decoration support
has a documented history of inconsistency vs Firefox/GTK. Deliberately
not blind-fixed - forcing decorated=false for Chrome would be worse
than doing nothing if it already negotiates correctly; needs a live
screenshot check with a real Chrome/Chromium install first.
|
|
Reported live: "looks weird and unpolished... need a lot more items".
Compared directly against the exact AGS reference this project's own
menu rebuild already targets rather than guessing:
- Highlighted rows used a flat, fully-saturated fill instead of the
reference's subtle 22%-accent-into-background wash. New decoration::
color::mix_rgb (channel-wise linear blend, generalizing brighten/
darken's fixed-target blends to an arbitrary second colour/ratio) lets
render_context_menu reproduce that same ratio.
- Every separator row was a label string of Unicode box-drawing
characters rendered as text glyphs, which render inconsistently at
small sizes - a label that's entirely U+2500 now draws a real 1px
hairline instead; a label that mixes it with real text ("--- Move to
Workspace ---", a deliberate section-header convention) still renders
as text, unchanged.
- "Select All" added to the bare-desktop menu, the one action every
mainstream desktop's own menu offers that this one lacked.
New tests needed real care: the panel's own rounded-corner distance
field softens alpha within its radius of any canvas edge, not just the
visible corners, so a naive full-row pixel scan against bg picked that
up as a false positive on the first attempt - fixed by scanning only
rows/columns confirmed (via a throwaway debug dump) to sit inside the
panel's genuinely flat interior.
Full workspace build/test/clippy clean, built and installed. Real
submenus and per-row icons remain real, separate scope - this
project's floating-menu UI has no nested-panel concept yet.
|
|
Reported live: "try move desktop items all at once somewhere else"
didn't work. Two compounding bugs, both real: CompState::
desktop_icon_drag only ever tracked one icon id, and the click handler
that starts a drag unconditionally collapsed any existing multi-
selection down to just the grabbed icon before the drag even began.
desktop_icon_drag is now Option<DesktopIconDrag> (crates/wayland/src/
desktop_icons.rs, new type): a grab offset, the grabbed icon's own live
position, and a members list - every currently-selected icon (the
grabbed one included), each a fixed offset from the grabbed icon's own
top-left at drag start, so the group moves as one rigid unit.
input/pointer.rs's click handler now only resets to single-selection
when the grabbed icon isn't already part of the current selection --
grabbing one inside an existing multi-selection keeps the whole group
selected and dragging, matching Windows/GNOME/macOS/KDE convention.
end_desktop_icon_drag snaps every dragged icon to its own nearest free
grid cell independently, tracking newly-claimed cells across the group
so two icons landing near each other never claim the same one.
Full workspace build/test/clippy clean, built and installed. Not unit-
testable (this module has no CompState test fixture for its own
selection/drag logic, an already-documented, accepted gap) - needs a
live drag to confirm.
|
|
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.
|
|
docs/TODO.md is this shift's single consolidated pending-work list (see
its own header for why it exists alongside PANEL_SUPPORT_TODO.md/
SESSION_HANDOFF.md rather than replacing them) - every commit in this
batch has its own dated entry there with the full root-cause/
verification narrative. docs/DEFAULTS.md corrected against the real
config engine and extended for every new general.* key this shift added
(aspect_ratio rule action, phone_mode). docs/FEATURE_GAP.md is a new
survey against niri/Hyprland/sway, requested directly. docs/
IMPLEMENTATION_STATUS.md and docs/PRIOR_ART.md updated to match.
Cargo.lock reflects the new resvg/usvg/tiny-skia dependencies (real
icon-theme SVG rendering).
|