Commit Graph
1668 Commits
Author SHA1 Message Date
LGUG2Z 2a1716bbff docs(schema): regenerate the bar schema for the pan commands
schema.bar.json embeds the socket messages, so it gets Pan, with the PanDirection that it
takes, and PanGesture, with what the fingers of a gesture do. The other two schemas are
the same as they were.
2026-10-30 15:45:37 -07:00
LGUG2Z a65b5c231e feat(komorebic): pan the Scrolling layout with the fingers on a touchpad
komorebic pan --stream reads the gestures of fingers on a touchpad from standard input, as
whkd writes them for a swipe of three or four fingers (docs/gesture-stream.md in its
repository): a line for each begin, move, end and cancel, with the distances in centimetres,
and any field or message that it does not know ignored. The row follows the fingers while they
are down, and when they lift it comes to rest with a column against an edge of the screen,
going at the speed that the times of the moves say that the fingers were. Focus goes to the
nearest column in view if the focused one is not. A gesture that is given up, or that is
not finished when the stream stops, puts the row back to what it was.

komorebic works out nothing but what it is asked to: --sensitivity multiplies the distances,
--reversed flips the way that the row goes, and --paging takes a step for every two
centimetres that the fingers go, as komorebic pan does, and not move the row with them. Moves
that wait while one is told are added up, as whkd says that they may be.

A gesture is told to komorebi on one connection, which it handles in order, as messages
that are sent on a connection each are not. With ghost movement the pictures that cover the
windows near the screen are taken when the fingers go down, a move of the fingers sets them
where the row puts the windows and does nothing else, and when the fingers lift they glide to
where it comes to rest, over the time that an animation takes, and the windows are shown. Without
it the workspace is laid out a step at a time, as it is for a drag. Ten centimetres of the
fingers move the row the width of the screen. The golden transcript of whkd and the swipes
of its demo are fixtures of the tests that read the stream.
2026-10-30 15:45:37 -07:00
LGUG2Z a63057c232 feat(komorebic): pan the Scrolling layout to the left or the right
komorebic pan <left|right> moves the view along the row of the Scrolling layout by a column
without moving focus first, and lets it come to rest with a column against an edge of the
screen. Focus goes to the nearest column on the screen if the focused one has gone off it. It
does nothing in the other layouts.

The way that the view goes is the way that focus goes for the commands that have directions:
panning to the left brings what is to the left of the screen into view, which is to move the
row to the right.

This is a command that a wheel can be bound to, as it is with whkd:
alt + wheelup : komorebic pan left.
2026-10-30 15:45:37 -07:00
LGUG2Z dd961f50f6 fix(border): draw a border again for a window that is not where it was drawn
A border follows its window from its own thread, which draws it again when the window has
changed its size or its place, and compares the window with window_rect to see whether
it has. The border manager sets window_rect too, from its thread, to where it finds the
window when it is told that the layout has changed, which is before windows that are moved
without waiting for them (window_handling_behaviour Async) have moved, or after, and when it
is after, the border sees a window that is where it already is and is not drawn again: in
a recording of three commands that resize windows the border of the focused terminal stays
the size that it was while the window grows by 200 pixels, and those of the others follow.

A border remembers the rect that it was last drawn around, and is drawn again for a window that
is not there, whatever window_rect says.
2026-10-30 15:45:37 -07:00
LGUG2Z 0811292de6 fix(resize): lay windows out in place for the commands that resize them
The pictures that stand for windows while they are moved by an animation are dimmer
and softer than the windows: text in the main terminal of a recording of resize commands
has its brightest pixels at 180 where the window has 215, and half as many pixels brighter
than 140 while the picture is up. Every resize command lays out the windows that it makes
bigger or smaller, and repeating them puts a picture over each of them every time, so it
is the commands that show it.

Windows are put where they go at once while the commands that resize them are handled, as
they are with animations off, and one that is on its way somewhere is stopped where it is
first. Dragging the edge of a window to resize it is not one of them, and has the pictures
that cover the windows that follow it, as it did. Animations of other commands are as they
were.
2026-10-30 15:45:37 -07:00
LGUG2Z 57084c4ddf fix(scrolling): put the left edge of a column where it is dragged to, however far
A column that is resized by its left edge is made wider and the strip is panned by what
it was made wider by, so that its left edge is where it was moved to and its right edge
where it was. But a column that is made wider than the room that it has to the right is
brought into view as it is made wider, which moves the strip already, so that for what
was beyond the room the strip moved twice. A recording of a drag of the last column
of a strip, 1342 pixels, has the strip 1008 pixels too far to the left at the end of it,
and every step with the strip moved by twice what the drag was less the room there was.

Say where the right edge of the column is before and after it is made wider, and pan by
the difference, which is what is left to put back whether the strip moved or not.
2026-10-30 15:45:37 -07:00
LGUG2Z 1a36f39870 fix(animation): make ghosts on a host that DWM does not crop
DWM draws a thumbnail that is stretched on a host that a thread that is aware of the
scaling of the display made with only part of its picture: a picture 1227 pixels wide
that was asked to be shown 1767 wide came out 342 wide on a host as wide as the screen at
125%, the further to the right the less of it, and all of it from a thread that is not
aware. A recording of a drag, where a tile grows while the pictures of its windows are
what is seen of them, shows File Explorer windows cut off at the right, and the trace of
what is asked of DWM shows the sizes and the picture to be the right ones.

When every display is scaled alike, and it is not 100%, the thread that owns the host
makes it, and the windows that show the pictures, as a thread that is not aware, whose
units are those of the screen divided by that scale. Where a ghost goes is worked out in
pixels as before and turned into those units where it is asked of DWM, and the part of a
picture that is shown into the units that DWM has the picture window in. Displays that are
scaled differently, or not at all, are as they were.

Measured by drawing pictures through the host and recording the screen, from a process that
is aware, at 125%: the pictures that were drawn 1226, 1074, 902, 742, 598, 460 and 342 wide
for sizes of 1227 to 1767 are now drawn 1225, 1315, 1405, 1495, 1585, 1675 and 1765 wide,
with the picture in the proportions that it has.
2026-10-30 15:45:37 -07:00
LGUG2Z 2560ee6364 feat(resize): cover the windows that a drag resizes with a picture of them
Resizing the tiles next to the edge of a window that is dragged lays out every window
that is not the one dragged again at every frame. A window that has to lay itself out
at a new size has nothing to show for a frame, a terminal that renders through a swap
chain is not there at all and the wallpaper shows through where it was, and one that is
slow to take a move, which File Explorer is, is seen at every size that it is on the
way through. A recording of a drag shows whole terminals gone for a frame, and
Explorer windows drawn twice, each of them at two sizes.

With ghost movement, the first time a step has to resize a window a picture of it is
taken before it is touched and put over it, the window is cloaked and its border is left
to the picture. Every step moves the picture, and the border with it, to where the window
is to be, with one call to DWM, and sends the window nowhere: the window is where it
was, hidden. When the end of the drag has laid the workspace out, each of the windows
that were covered is put, on a thread of its own, where the last step put its picture, made
to paint itself, given the size change that a swap chain needs to be built again, and
shown, and its picture fades out over it, as for a move that is animated.

A new drag, before that, puts them where they were to go and shows them at once. The look
at the windows after a drag leaves covered ones to this, and a window that is hidden by its
workspace is not shown by it. Every step traces where the picture is to go and what part
of it is shown there, and a picture that could not be moved says so at debug level.
2026-10-30 15:45:37 -07:00
LGUG2Z 18708d6db8 fix(resize): measure a drag from where the tile was, not where the window had got to
The start of a drag is handled after the drag has begun, so the rect read
then is already some way into it, and a mouse resize came out short by
that much: dragging an edge by 60px moved it 53px, and the longer the
drag the more was lost.

An edge that has moved since the start was read was taken from the edge
of the window's tile, so measure from that. An edge that has not moved
is not being dragged, and an edge a long way from its tile is a window
that could not be as small as it, so both stay as they were read.
2026-10-30 15:45:37 -07:00
LGUG2Z 48e30d9585 fix(scrolling): resizing a column by its left edge moves that edge
A column was made wider, or narrower, from its left edge whichever edge was
resized: dragging the left edge of the last window of a strip to the left made it
wider to the right, which is where its right edge went, and its left edge stayed
where it was. In the recording of the drag, it was the same window, 1651 wide
before and 1982 wide after, still at the same left edge, to the right of where the
user had put it.

The strip is panned by what the column was made wider by, so that the left edge is
where it was moved to and the right edge stays, and the columns to the left of the
column follow it. The right edge is the one that moves for any other direction, as
it was.
2026-10-30 15:45:37 -07:00
LGUG2Z f9610dd373 fix(logging): do not look up what a window is for every location change
A window that is moved sends a location change for each of the windows in it, and
File Explorer sends about eighteen for every move. Each one had the hook look up the
exe, the title and the class of the window, a call into it for each, to write a trace
line that says that nothing is done with it: about a millisecond and a half each,
holding up the events behind it, which include those of the window being dragged.

They are no longer logged.
2026-10-30 15:45:37 -07:00
LGUG2Z 105777bfda fix(resize): put windows that did not get where a drag left them
After a drag Explorer was left 48 pixels short of where the layout puts it, for as
long as nothing laid the workspace out again: a window that is busy with what an
earlier move made it do is left where that put it, by what was sent while it was.
Which of the moves it is that a window does not make is not something that is known,
but that it is not where it is to be is, and is something that is cheap to look at.

A quarter of a second after the end of a drag that was followed, when the windows
have done what they were going to, the windows of the workspace are looked at, and
any that are not where the layout puts them, give or take a couple of pixels, are
sent there again, and looked at once more after that. It stops when none are, when
another drag begins, or when the workspace is not the one on show. What it found is
logged at debug level each time, so that a check that found nothing is not the same
in the log as one that was never made.
2026-10-30 15:45:37 -07:00
LGUG2Z b24ebe42ad feat(resize): the neighbours follow the edge of a window dragged to resize it
The tiles next to the edge of a window that the user drags to resize it follow the
edge while it is dragged, and not only once it is let go.

Windows tells komorebi of every step of the drag with a location change of the
window, far faster than anything can be laid out. They are only a signal that the
window has moved: at most one waits, and what is laid out is where the window is
when it is looked at, which is the newest. Steps are laid out from a thread of
their own, so that the handling of events is not held up, straight into place and
not animated, as an animation of each step would only trail behind the pointer.

When the user takes hold of a window the resize dimensions, the layout and the
scrolling space of the workspace are kept. Every step, and the end of the drag, is
worked out from those, so a drag ends the same with its steps as without them, and
one that goes back to where it began puts the neighbours back. The window being
dragged is the user's until they let go, so a layout leaves it where it is. The
neighbours are where the last step put them, a few pixels from where the end of the
drag does, so the end is laid out at once as well, and a layout in which windows are
moved with animations does not take the width of a neighbour that has yet to catch
up for the least that its column is to be.

What a window is made to do is not always as quick as the steps: moving File
Explorer does the layout of eighteen windows, and a move sent for every step waits
in its queue, which is busy for as long as there are any, so it falls behind and
goes on catching up after the drag. A window that has yet to get to where it was
last sent is left alone by a step, for no more than a tenth of a second, and looked
at again six milliseconds after the step when a window was left, which is sooner
than the next step for the windows that are quick to move.

A window that opens or closes in the middle of a drag changes what the layout is
made of, so the neighbours stop following until the drag ends. Monocle and maximized
windows, and windows that are not tiled, are not followed. Every window that a layout
for a drag moves is logged, at debug level, with where it was sent from and to, or
why it was left alone.
2026-10-30 15:45:37 -07:00
LGUG2Z 3d9f581b96 feat(animation): draw the border of a window into its ghost
A border is a window of its own, which cannot be cut off at the edge of the
monitor as a ghost is, so the borders of the windows that slide in and out
were left out of it: they were gone before the windows left and did not come
back until they were shown.

The picture a ghost shows is now given the border its window has, drawn in
room that is made for it around the window and over its edge, in the colour of
the kind of border the window has (single, stack, monocle, unfocused...). The
ghost is bigger than the window by that room, and is clipped to the monitor as
it was, so the window and its border slide and are cut off as one.

The pictures that are kept of the windows that leave are of the windows alone,
as the kind of border a window has when it comes back is not the one it had when
it left.
2026-10-30 15:45:37 -07:00
LGUG2Z 65bf21c11a feat(animation): slide workspaces in and out
When the workspace of a monitor is switched, the windows of the workspace that
is left slide out of the monitor and those of the one that is switched to slide
in from the other side, as ghosts, together: from the right when going to a
higher workspace (or right across the edge of a workspace with the Workspace
cross boundary behaviour) and from the left when going to a lower one (or
left). It is one of the animation kinds, so it is on with animations for
everything and can be turned off with the workspace entry of
animation.enabled, and it needs ghost movement.

It is not done when either workspace has a maximized window or windows are not
hidden by cloaking them, and a window that cannot be pictured is hidden or
shown at once.

None of a ghost is drawn outside of its monitor, so nothing is shown on a
monitor next to it. The border of a window that slides in is held until the
window is shown, as it cannot be cut off at the edge of the monitor.

Windows of Chromium and Electron apps, and other apps that render through a
swap chain, are left with a stale render target by being laid out, or asked to
render, while they are hidden, so a window that slides in:

 - shows the picture that was kept of it when it left, and is not asked for a
   new one while hidden (one with no picture, if it is such an app, is shown at
   once);
 - is not laid out at all if it is where it is to be, and is laid out once it is
   shown, and not before, if it is such an app;
 - is not focused until it is shown, as focusing a hidden window shows it;
 - is shown before it is laid out when it has to be shown at once, and is held
   hidden against the layouts that follow the switch until then.

The container that is to be focused on a workspace that is crossed into is
focused before it is laid out, and not after, which would place its windows
twice.
2026-10-30 15:45:37 -07:00
LGUG2Z 794a0bcd82 feat(animation): pending workspace transitions
Let a workspace that is being switched say which of its windows are going to
slide out of the monitor and which are going to slide in, so that hiding and
showing them is left to the animations that do that.

A window with a pending exit is not hidden, and one that is to slide in is not
shown, by the usual paths. It is the animation that takes it over, and it
stays held from then on until the animation shows it, however many times the
layout of its workspace is done meanwhile: taking what is pending only takes
what was asked for. Whatever is still pending when the switch is over, and has
no animation, is hidden or shown at once, so nothing can be left behind, and
what is pending expires after a couple of seconds as well.

The border of a window that is sliding in cannot be cut off at the edge of the
monitor as its picture is, so it can be held: hidden, and not moved or shown by
anything that happens to the window, until it is let go of.
2026-10-30 15:45:37 -07:00
LGUG2Z 38e4f04900 feat(animation): keep the picture of a window that leaves
Keep the picture that is taken of a window as its ghost is made, for the ghost
of the window to show when it is back, so that a window that is hidden is
not asked to render itself to get one. Asking a hidden Chromium or Electron
window to has been seen to leave it blank, or drawn off its place, until its
process is restarted.

At most 12 pictures, 256 MiB and ten minutes of them are kept, and a picture
is only given back for a window that is within a few pixels of the size it
was. Nothing uses them yet.
2026-10-30 15:45:37 -07:00
LGUG2Z 730f5d82ec feat(animation): a workspace animation prefix
Workspaces are going to slide in and out of the monitor when they are
switched, and that is its own kind of animation (workspace) next to moving
and transparency: it is turned on, and given a duration and an easing, the
same way (animation.enabled, duration and style take a workspace entry, as do
komorebic animation, animation-duration and animation-style with
--animation-type workspace), with the duration and the easing of the other
animations when it has none of its own.

Animations being enabled for everything enables it too, and it can be turned
off in particular (animation.enabled with workspace set to false), so that
enabled: true keeps meaning everything. It needs ghost movement, as the
windows slide as ghosts. Nothing uses it yet.
2026-10-30 15:45:37 -07:00
LGUG2Z 5cc8e43765 feat(animation): clip a ghost to a rect
A ghost can now be given a rect that none of it is drawn outside of: the part
of it that is outside is cut off, with the part of its picture that goes
there, and it is not shown at all when all of it is. A host that covers every
monitor would otherwise draw a ghost that slides out of a monitor on the one
next to it.

What a ghost is made to show now includes which part of its picture goes
where, so that a ghost that is already showing what it is asked to is still
left alone. Checked against DWM with real pixels (on a host that is not
layered, whose thumbnails the screen can be read back from): inside the clip
the window, outside it the host, and nothing at all when it is all outside.
2026-10-30 15:45:37 -07:00
LGUG2Z 388299b6e9 feat(animation): geometry for sliding and clipping ghosts
Pure arithmetic on rects, in virtual screen coordinates, for workspaces that
slide in and out of a monitor: which side the workspace that is entered comes
from (from the indices of the two workspaces, or from the direction of moving
across the edge of one), how far each of the two sets of windows slides, and
what to draw of a ghost so that none of it is outside a rect: the part of
where the ghost is that is inside it, and the part of the picture that goes
there. A ghost is drawn on a host that covers every monitor, and one that
slides out of a monitor would otherwise be drawn on the one next to it.
2026-10-30 15:45:37 -07:00
LGUG2Z 8bb6fed332 refactor(animation): move the ghost dispatcher out of window.rs
The animation of a window by a ghost is not about windows in general, and the
workspace transitions that follow need to use it for more than moving a window
from one place in a layout to another. No change in behaviour.
2026-10-30 15:45:37 -07:00
LGUG2Z 699f31f8bc perf(animation): do not update a ghost that has not changed
Asking DWM to put a thumbnail where it already is, as opaque as it already
is, still has it compose the frame again, and an eased animation spends its
first and last frames where it is. Remember where a ghost was last put and
how opaque it was made, and leave it alone when that is asked of it again.
2026-10-30 15:45:37 -07:00
LGUG2Z fd6fb3047b perf(animation): do not wake the ghost thread every 8ms for nothing
The thread that owns the ghost host window looked for messages every 8ms for
as long as komorebi ran once ghost movement was on, about 125 wakeups a second
doing nothing, which keeps a processor from sleeping. Poll at that rate only
while there are ghosts, and every 250ms while there are none: it is woken at
once by a command either way.
2026-10-30 15:45:37 -07:00
LGUG2Z 388a17d682 perf(border): coalesce the rects an animation sends to a border
An animation sent the border of each window it moves a rect for every frame:
it took the global border state lock, which the border manager holds while it
handles a notification, cloned the id of the border, boxed the rect and posted
it, and the border drew every one of them in turn, resizing its render target
and drawing again for each, even when it had fallen behind, so that it lagged
and then caught up in a burst.

Look up the border once, when the ghost is made, and keep the newest rect for
each border where the animation and the border can both get at it: the border
is only told when it has none waiting, and draws the newest when it gets
there. No lock the border manager holds is taken in the middle of an animation
any more, and no box is allocated for each frame.
2026-10-30 15:45:37 -07:00
LGUG2Z 70176ee167 perf(animation): keep the swap chain nudge off the animation thread
Windows that render through a swap chain get a one pixel size change when
they are shown again, which takes as long as the window takes to answer twice
(over a hundred milliseconds for a slow one), and it was done by the
animation thread in the middle of the animation, as a hitch in every move:
drawing a frame took up to 154ms, 45ms for the worst frame of an animation on
average. Do it on the thread that resizes the window, after the window has
been resized and made to paint itself, and while it is hidden; showing the
window is then only uncloaking it. It has to come after rendering the window:
rendering leaves the compositor of a swap chain window as stale as it was, and
the window took over a hundred milliseconds after being shown to show
anything, as a gap of wallpaper where it was meant to be.
2026-10-30 15:45:37 -07:00
LGUG2Z cd1c9ce9ce perf(animation): do not build a picture to make a window paint itself
Making a window paint itself (a render of it is synchronous) went through
Snapshot::capture, which also allocated and filled a second picture of the
visible frame, copied it out of the first and cut its corners, to throw it all
away: about a quarter of what taking a picture costs, on the thread that
gates showing the window again.

Split rendering a window out of taking a picture of it and make paint_now only
render. And when taking a picture, do not run over millions of pixels to make
them opaque when a window has rendered them that way, as it does: look at a
grid of them across the picture, and only fall back to making every pixel
opaque if one of those is not.
2026-10-30 15:45:37 -07:00
LGUG2Z 126acaa074 chore(animation): trace what ghosts and animation frames cost
Time the picture a ghost takes of its window (allocating, PrintWindow and
turning it into a picture), the render a window is asked for to lay itself
out (paint_now), how long a ghost takes to make, and what the frames of a
group of animations cost: how long drawing them takes, how far apart they
come and how many are late. All at trace level, the frame statistics at
debug.
2026-10-30 15:45:37 -07:00
LGUG2Z 0b17aae86b fix(border): leave the border of a ghosted window to the animation
While a ghost stands in for a window the window is hidden and not where the
ghost is, and an animation moves its border along with the ghost. But the
border also follows the real window: every event komorebi handles ends with
a border update that reads the rect of the real window, and so does every
location change of it, and komorebi's own cloak, uncloak and resize of the
window produce both. Each of them put the border where the window is going to
be, or was, for a frame, until the next frame of the animation moved it back:
the border jumped ahead of the window, by as much as the whole width of the
screen when moves overlapped.

Say which windows a ghost stands in for, from when the ghost is made until it
is gone (or the animation is cancelled), and leave the borders of those to the
animation.

Recorded over 16 moves of mixed pace, a border was moved by something other
than its animation 21 times before; on a swap in the right column, 6 times
before (up to 127px off) and none after.
2026-10-30 15:45:37 -07:00
LGUG2Z 94cd37f6d0 feat(animation): show ghosts as a still picture on one shared host window
A ghost was a live thumbnail of its window, so it showed whatever the window
painted: the stretched old frame, then the blank frame a window paints while
it lays itself out again (Explorer shows its toolbar and nothing else for a
few hundred milliseconds), then the new layout, replacing the picture all at
once after the animation was over, and it stayed on screen about 300ms past
the end of the move while it waited for that. Every ghost also had a host
window of its own that was moved with SetWindowPos on every frame through a
channel to a thread that polls for commands every 8ms.

Take a picture of the window with PrintWindow before anything is done to it
(cropped to the visible frame, with the corners rounded) and show it through
a cloaked window of our own that DWM can take a thumbnail of and scale on the
GPU. All ghosts are composed onto one layered, click-through host window that
covers the virtual screen, and is only above other windows while a ghost is
alive so that it does not get in the way of the taskbar's auto-hide:
animating a ghost is one DwmUpdateThumbnailProperties call on the animating
thread, which has no thread affinity. The number of registered thumbnails is
counted and traced so that a leak of compositor handles can be seen.

The real window is cloaked and resized at the start of the animation, on a
thread of its own, and asked to paint itself at its new size straight away
(taking a picture of it makes it render synchronously) while the ghost is on
screen, so that it has the whole animation to lay itself out unseen. At 55%
of the way through, the window is shown again under the ghost (with the one
pixel size change that windows which render through a swap chain need for
their compositor to rebuild its render target) and the ghost fades out over it
while still moving, so the picture dissolves into the window instead of being
replaced after. A window that is slow to take its new size is not waited for:
it is shown once it is ready, or, if the animation ends first, waited for with
the ghost over it and then faded out over a few frames. Windows that are
minimized or gone are not ghosted.
2026-10-30 15:45:37 -07:00
LGUG2Z b50e21e5cd feat(animation): run the movement animations of a layout pass together
Each window that moved in a layout pass was animated on a thread of its own,
each preparing itself, drawing its frames and finishing by itself, so the
windows of a swap could be a little ahead of or behind one another, and a
ghost could appear over its window noticeably later than the ghost of the
window it was swapped with.

Collect the animations started while a workspace is being updated in an
AnimationBatch and run them on a single thread: they are prepared at once, so
that starting takes as long as the slowest of them (a ghost takes a picture of
its window) and not as long as all of them, every one is prepared before the
first frame, every frame of all of them is drawn before the compositor is
asked to present it (waiting for it with DwmFlush when a ghost is being
animated), and they finish and fade out together. Animations cancelled by a
newer one still drop out one at a time, and the wait for a cancelled one to
stop is checked every 4ms instead of every 125ms: it notices once per frame,
so the longer wait only delayed the animation replacing it.
2026-10-30 15:45:37 -07:00
LGUG2Z e3dd3c60de fix(scrolling): refocus after a window is closed with Hide
Closing a window sends Hide and not Destroy to many applications. The Hide
arm re-tiled without asking Windows to focus the surviving window, so no
window had the foreground and the border did not follow focus. Use the
same refocus rule as the Destroy arm.
2026-10-30 15:45:37 -07:00
LGUG2Z ae8e982380 fix(scrolling): show every window when a stack is spread out into a column
Spreading a stack out into a column left two things wrong, both seen in a
recording of toggle-column-stack:

- The window that stays in the original container was never restored. When it
  was the tab at the back it stayed cloaked, and its column was empty until
  focus reached it. Only the windows that moved to new containers were
  restored.
- Each new container took focus when it was inserted, so the strip scrolled a
  column to bring it into view, and did not scroll back once the containers
  were put in the one column again. The strip was left a column to the left,
  with an empty third of the screen. Focus now stays on the column that they
  are put in.

Test both, with windows that are hidden as stacking hides them.
2026-10-30 15:45:37 -07:00
LGUG2Z 5daca12e28 feat(scrolling): support more than one monitor
Remove the single-monitor restrictions on the Scrolling layout. Each
workspace keeps a strip of its own, and a window is seen on whichever monitor
it overlaps, so placement now looks at the other monitors: a window that
scrolled out of view past an edge that another monitor is next to is hidden
instead of parked, and a partly visible one is cut down to its own work area.

A window that comes back into view is shown again first, but only the one at
the front of its container: the others are the tabs at the back of a stack,
which are hidden because they are at the back.
2026-10-30 15:45:37 -07:00
LGUG2Z b8a444df5c docs(scrolling): describe the scrolling layout 2026-10-30 15:45:37 -07:00
LGUG2Z e8364f7b5b feat(scrolling): rest windows that scroll out of view at the edge
A window that scrolls out of view comes to rest at the edge of the work area
that it left by, with 5 pixels of it still on screen, keeping its size so that
nothing is resized to bring it back and an animation back into view is short.
Reuses the existing movement animations.

Also test the Workspace glue: widths, consume, expel, moving a column, and
that retile forgets the widths that windows were found to refuse.
2026-10-30 15:45:37 -07:00
LGUG2Z f170e8e2ac feat(scrolling): make room for windows that will not get as narrow
After windows are placed, ask each how wide it really is. A window that is
wider than its column was made to be widens the column to fit, and the columns
after it move along. A column never gets narrower again because of this, and
never wider than the screen.

What a window says about its size is only used once it can be trusted: with
synchronous window handling and no animation in flight, and for an overhang
of more than 16px, so that rounding, a terminal's cell grid, or a window that
is still moving does not widen a column for good. Adopting a width is logged
at info level.
2026-10-30 15:45:37 -07:00
LGUG2Z 7c79625ba7 docs(schema): regenerate the schemas for the scrolling layout
schema.json gets the center_focused_column layout option as a boolean or one
of Never, Always and OnOverflow, and schema.bar.json, which embeds the socket
messages, gets the new scrolling commands.
2026-10-30 15:45:37 -07:00
LGUG2Z 598390d9cb feat(scrolling): add commands and config for the scrolling layout
Focus, move, resize, retile and closing a window now go through the strip of
columns in the Scrolling layout. New commands: consume, expel,
toggle-column-stack, scrolling-layout-full-width and center-focus. A top
level scrolling section configures centering, centring a lone column, and
the minimum height of a window in a column of several.
2026-10-30 15:45:37 -07:00
LGUG2Z 8c7327fd31 feat(layouts): share the platform-free half of the scrolling glue
komorebi-layouts now carries what a host needs of the Scrolling layout
without touching an operating system: the scrolling config section and its
checked settings, the layout options for the model, placement of containers
in a work area, resizing a column or tile, and the hand-over of column
records between workspaces. It re-exports the model as
komorebi_layouts::scrolling::model, so komorebi-for-mac can pick all of it up
through its existing komorebi-layouts git dependency.

Also add the Workspace methods for columns, consume/expel/stack, focus and
ordering, which sit on top of it.
2026-10-30 15:45:37 -07:00
LGUG2Z 3b93035d48 feat(scrolling): lay out the scrolling layout with the scrolling model
The Scrolling layout now keeps a ScrollingSpace per workspace, keyed by
container id, instead of recomputing equal-width columns from the previous
layout on every update. Column widths follow layout_options.scrolling.columns
and persist as windows come and go.
2026-10-30 15:45:37 -07:00
LGUG2Z 6c4eb621d3 feat(scrolling): add a platform-free model of a scrollable strip of columns
Ported from komorebi-for-mac (feature/moar-scrolling). The crate has no
dependencies and no knowledge of windows; it is not used by komorebi yet.
2026-10-30 15:45:37 -07:00
LGUG2Z a4e03e3d76 test(resize): cover the top edge of the lower tile of a BSP layout
Dragging the edge two stacked tiles share moves it by the same amount from
either tile, in the layout and in the window manager. A drag of the top edge
of the lower tile that moves nothing is not for want of this.
2026-10-30 15:45:37 -07:00
LGUG2Z dc9b2e9539 fix(window): uncloak the windows a killed daemon left cloaked
A daemon that is killed does not get to uncloak the windows it hid, and the
next one skips them as cloaked, so they stay out of sight and unmanaged.

A cloak by komorebi reads the same as one for another virtual desktop or a
suspended app, so keep a record of the windows that are cloaked and uncloak
those that the last daemon left behind, before looking for windows.
2026-10-30 15:45:37 -07:00
LGUG2Z 0b57006053 chore(deps): update libz-sys to 1.1.31 for RUSTSEC-2026-0418
libz-sys before 1.1.31 logs the compiler command of its zlib probe, which
with cc 1.6.0 or later includes every environment variable of the build,
and cargo stores that output under target/. It is a build dependency of
shadow-rs via git2 and libgit2-sys; bump it in the lockfile.
2026-10-30 12:13:42 -07:00
LGUG2Z 5c8e4770dc fix(monitor): do not mistake an unplugged display for one that is still attached
Displays were matched on serial_number_id.eq(..) || device_id.eq(..), so
two displays with no serial number matched each other through None == None,
and two with no device path through the shared "UNKNOWN" device id. When
a display without either was unplugged, the one that stayed attached
"matched" it, nothing was removed, and every display change started the
3s verification again; state kept listing the display for good.

A serial number now identifies a display only when both have one and it is
not empty, and a display without a device path is told apart by its name.
The removal takes the monitors out by index instead of by id, so that two
displays sharing the id UNKNOWN are not removed together, and adding a
connected monitor no longer skips a display for sharing that id.
2026-10-30 12:13:42 -07:00
LGUG2Z 3b424dd707 fix(config): accept an integer for an initial window placement rule
PlacementTarget deserialized through String, so the integer the docs
promise ("initial_window_placement_rules": 2) made the daemon exit at
start with an untagged enum error while "2" worked. Accept integers as
well as strings.

The derived schema described an externally tagged object that neither
form matches, so describe the type by hand and regenerate both schemas.
2026-10-30 12:13:42 -07:00
LGUG2Z 837ca257d4 fix(monitor): validate the target before moving a container or workspace
move_container_to_monitor and move_workspace_to_monitor took the container
or workspace out of its place and only then looked the target monitor up,
so a monitor index that does not exist left the window on screen but in no
workspace. Check the index first so the command fails and nothing moves.
2026-10-30 12:13:42 -07:00
LGUG2Z 9b9cad997c fix(rules): keep a rule whose id another kind of rule already has
The socket handlers for manage, ignore, object-name-change, tray, layered
and title bar rules skipped a new rule when any simple rule had the same
id, whatever its kind, so ignore-rule exe foo after ignore-rule class foo
was dropped. Share one helper that compares the kind as well.
2026-10-30 12:13:42 -07:00
LGUG2Z a599e9ecc1 chore: keep the line endings LF
Everything but dependencies.json is stored with LF, which nothing said, so
a CRLF file could get in unnoticed (it did, for two files, in a round trip
that shows as a rewrite of every line of both).

* .gitattributes: normalize every text file to LF in the repository and
  check it out with LF, whatever core.autocrlf is (the Windows runner
  defaults it to true)
* rustfmt.toml: newline_style = "Unix", so cargo fmt --check fails on
  a Rust file with CRLF
* CI: fail when a file is stored with CRLF
* dependencies.json: the one file that was stored with CRLF
2026-10-30 12:13:42 -07:00
LGUG2Z 21a821aca0 fix(nix): get the build working again 2026-10-28 19:30:00 -07:00