<feed xmlns='http://www.w3.org/2005/Atom'>
<title>srdwm/crates/core, branch main</title>
<subtitle>Cross-platform window manager written in Rust.
</subtitle>
<id>https://srdusr.com/git/srdwm/atom?h=main</id>
<link rel='self' href='https://srdusr.com/git/srdwm/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/'/>
<updated>2026-08-30T18:41:00+00:00</updated>
<entry>
<title>Indent doc list continuations</title>
<updated>2026-08-30T18:41:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-08-30T18:41:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=f2f9ed1b3f9c49b323ce591eb72a406058d324a4'/>
<id>urn:sha1:f2f9ed1b3f9c49b323ce591eb72a406058d324a4</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Move a window in steps, and let the keyboard resize one at all</title>
<updated>2026-08-25T12:49:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-08-25T12:49:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=8b3ef3c7b28570122126a1c8b3d6695fc3647557'/>
<id>urn:sha1:8b3ef3c7b28570122126a1c8b3d6695fc3647557</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Look for a free spot before piling a new window on the last one</title>
<updated>2026-08-23T22:37:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-08-23T22:37:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=807d0ba1680cdc1322bfe9584781416339f6f76d'/>
<id>urn:sha1:807d0ba1680cdc1322bfe9584781416339f6f76d</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Read the window-memory store at a moment when it can actually match</title>
<updated>2026-08-10T23:35:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-08-10T23:35:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=38d899d19b5e5204062ecab4bb4105d51afddb6b'/>
<id>urn:sha1:38d899d19b5e5204062ecab4bb4105d51afddb6b</id>
<content type='text'>
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 -&gt; opened at exactly 400,300 500x400
  dragged the right edge      -&gt; 646 wide, store rewritten to 646 on release
  closed and reopened          -&gt; 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.
</content>
</entry>
<entry>
<title>Show a keybinding the way a person writes it, and let bind_repeat be described</title>
<updated>2026-08-04T22:55:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-08-04T22:55:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=9c1673fa73bb49433370a60a7b4bb16abee98a9b'/>
<id>urn:sha1:9c1673fa73bb49433370a60a7b4bb16abee98a9b</id>
<content type='text'>
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".
</content>
</entry>
<entry>
<title>Workspace capture writes a readable image, and left-edge resize holds its anchor</title>
<updated>2026-07-14T20:34:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-07-14T20:34:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=aa7b7278b9e758985197170bcee4f0d2d4cb4590'/>
<id>urn:sha1:aa7b7278b9e758985197170bcee4f0d2d4cb4590</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Give every window an outward resize band, so a borderless one can be grabbed</title>
<updated>2026-07-13T17:43:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-07-13T17:43:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=5ef39bd041c64c66f629f9fac53ecc0cdd7aff11'/>
<id>urn:sha1:5ef39bd041c64c66f629f9fac53ecc0cdd7aff11</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Let the config file take back a setting changed at runtime</title>
<updated>2026-07-05T14:06:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-07-05T14:06:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=744757fad22db7a5ebebca9a685cdb00589a1b02'/>
<id>urn:sha1:744757fad22db7a5ebebca9a685cdb00589a1b02</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Stop window memory poisoning itself, and publish the decoration side to GTK</title>
<updated>2026-06-02T18:08:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-06-02T18:08:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=30d45a7b9e0ddcd7ed2836c16e77b423cf3cff28'/>
<id>urn:sha1:30d45a7b9e0ddcd7ed2836c16e77b423cf3cff28</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Let negotiating clients be forced to server-side decoration, and document the GTK half</title>
<updated>2026-06-01T21:18:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-06-01T21:18:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=3ef784e09f6b81b767837e048d691c4e63528e55'/>
<id>urn:sha1:3ef784e09f6b81b767837e048d691c4e63528e55</id>
<content type='text'>
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.
</content>
</entry>
</feed>
