<feed xmlns='http://www.w3.org/2005/Atom'>
<title>srdwm/crates/core/src/manager, 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>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>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>Report whether a listed key binding is actually grabbed</title>
<updated>2026-05-13T18:11:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-05-13T18:11:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=8174ced0d9d42343b18072c64491ccb06632a75f'/>
<id>urn:sha1:8174ced0d9d42343b18072c64491ccb06632a75f</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Expose key bindings over IPC so a launcher can list them</title>
<updated>2026-05-13T17:47:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-05-13T17:47:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=670c9845129fc0c4b6e0192d657eea05e1a64b49'/>
<id>urn:sha1:670c9845129fc0c4b6e0192d657eea05e1a64b49</id>
<content type='text'>
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 &lt;id&gt; 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.
</content>
</entry>
<entry>
<title>Fix spawn placement under the top bar, add per-window minimum sizes, and clean up maximize</title>
<updated>2026-05-11T14:57:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-05-11T14:57:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=646b37e7e3aa5931079c6b9e804f420bb43c74d5'/>
<id>urn:sha1:646b37e7e3aa5931079c6b9e804f420bb43c74d5</id>
<content type='text'>
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 &gt;= 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.
</content>
</entry>
</feed>
