There are a bunch of edge cases coming up with this behaviour. Setting
the default to false for now so people on the nightly can opt in as they
feel comfortable.
5f629e1f made directional focus cycle the monocle container in the
underlying ring. Useful on a single monitor, but undesired when focusing
across monitors — the focus stays trapped on the monocle workspace
instead of crossing the boundary.
This commit adds a new top-level monocle_focus_behaviour option with two
variants, Cycle (the post-5f629e1f behaviour) and NoOp (the pre-5f629e1f
behaviour, now the default), along with a ToggleMonocleFocusBehaviour
socket message and corresponding komorebic subcommands to flip between
them at runtime.
This commit tries to render move/resize animations on a DWM-thumbnail
"ghost" window instead of calling MoveWindow per-frame on the real HWND.
The source is cloaked via IApplicationView::SetCloak, the thumbnail is
animated via DwmUpdateThumbnailProperties on a layered host owned by a
single "ghost owner" thread, the border for the source follows the
lerped rect via a new WM_ANIMATE_RECT message handled on the border's
own WndProc thread (preserving today's per-frame border tracking), and
the real SetWindowPos happens once at the end of the animation.
Apps repaint exactly once per animation instead of N times, which is a
substantial win for heavy renderers (browsers, IDEs, Office). For
non-Chromium sources the source is also pre-positioned to target_rect
before the thumbnail is registered so the captured texture is target-
sized and downscales to native 1:1 at the end of the animation rather
than upscaling to a stretched/blurry final frame.
Chromium-shell sources skip the pre-paint step: their
NativeWindowOcclusionTrackerWin reads DWMWA_CLOAKED and treats any cloak
value as hidden, suspending the renderer; WM_SIZE while cloaked produces
no new frame and the post-uncloak swap chain shows stale or black
content.
For those apps we keep the source cloaked at start_rect for the whole
animation and do the SetWindowPos in post_render after uncloak, where
the visibility flip is what wakes Viz back up.
A short ease-in opacity crossfade in post_render masks the texture
transition for the Chromium path and gives slow renderers time to
present their first post-resize frame before the overlay is removed.
This commit fixes a cross-thread use-after-free crash with exception
code 0xc000041d (FATAL_USER_CALLBACK_EXCEPTION), identified via WinDbg
analysis of a minidump where rax=0xfeeefeeefeeefeee at the crash site in
d2d1!HwndPresenter::Present - the Windows heap freed-memory fill pattern
confirming Direct2D dereferenced a previously freed object.
The root cause was a data race between the border manager thread and the
border's own message loop thread. Border::create() spawns a dedicated
thread (Thread B) for the HWND message loop and sends Box<Border> back
to the border manager thread (Thread A) via a channel. After this point
both threads accessed Border::render_target concurrently without any
synchronisation:
Thread A called update_brushes() directly on the Box<Border>, replacing
render_target with a new ID2D1HwndRenderTarget and dropping the old one.
Dropping the old RenderTarget decremented the COM refcount to zero,
causing D2D to free its internal HwndPresenter.
Thread B was concurrently mid-render in a WM_PAINT or
EVENT_OBJECT_LOCATIONCHANGE handler, holding a reference to that same
old render target obtained via the GWLP_USERDATA raw pointer. Calling
EndDraw() after HwndPresenter was freed produced the crash. The process
uptime of two seconds in the dump confirmed this happened during startup
workspace initialisation, when a ForceUpdate notification triggered
update_brushes() while the newly-shown border window was processing its
first WM_PAINT.
The fix routes all brush update requests through the border's own
message loop by posting a custom WM_UPDATE_BRUSHES (WM_USER + 1) message
instead of calling update_brushes() cross-thread. The three call sites
in the border manager that previously called border.update_brushes()?
directly now call border.request_brush_update(), which posts the message
via PostMessageW. The WndProc handler for WM_UPDATE_BRUSHES calls
update_brushes() and invalidate() entirely on Thread B, eliminating the
race.
A secondary bug in destroy() was also fixed: it was clearing
GWLP_USERDATA before posting WM_CLOSE, which caused WM_DESTROY's null-
pointer guard to skip the render_target = None cleanup. This left the
ID2D1HwndRenderTarget alive past HWND destruction, and D2D freed its
HwndPresenter during WM_NCDESTROY while the COM wrapper still held a
reference - a second path to the same crash for any message queued
between WM_NCDESTROY and WM_QUIT. The premature GWLP_USERDATA clear has
been removed; WM_DESTROY already handles it correctly after releasing
the render target.
This commit implements something I've found myself wanting while working
on the Macbook.
When I have a monocle container active, I want to quickly be able to
switch back and forth with an adjacent window in the underlying layout
without having to toggle monocle mode off and back on again.
This is now accomplished by using focus left/down to promote the
previous window in the Ring to monocle, and by using focus right/down to
focus the next window in the Ring to monocle.
Borders were being funny so I just ended up nuking them whenever we
cycle.
This commit moves layout-related code into a new workspace crate
komorebi-layouts, with the intention of re-using it all in komorebi for
Mac instead of maintaining two separate implementations.
This commit attempts tofixfixes a use-after-free bug in the
border_manager that was causing crashes with exception code 0xc000041d
(FATAL_USER_CALLBACK_EXCEPTION) when borders were being destroyed during
config updates.
The root cause was a race condition between the main thread destroying a
border window and the border's window thread still processing queued
window messages (EVENT_OBJECT_LOCATIONCHANGE, WM_PAINT).
The crash occurred when these callbacks invoked render_target.EndDraw(),
which internally calls HwndPresenter::Present() to present the rendered
frame to the HWND. By this point, the HWND and its associated Direct2D
surfaces had been freed by WM_DESTROY, resulting in Direct2D attempting
to dereference freed memory (0xbaadf00dbaadf00d - debug heap poison
value).
The previous attempts at fixing this issue (bdef1448, dbde351e)
addressed symptoms but not the fundamental race condition. bdef1448
attempted to release resources on the main thread before destruction,
but this created a cross-thread race. dbde351e moved resource cleanup to
WM_DESTROY, but this still allowed EVENT_OBJECT_LOCATIONCHANGE/WM_PAINT
handlers to check `render_target.is_some()`, context-switch to
WM_DESTROY which clears it, then context-switch back and call EndDraw()
on a now-invalid reference.
This commit attempts to eliminate the race condition by introducing an
atomic destruction flag that serves as a memory barrier between the
destruction path and the rendering paths:
- Added `is_destroying: Arc<AtomicBool>` field to the Border struct
- In destroy(): Sets the flag with Release ordering, sleeps 10ms to
allow in-flight operations to complete, then proceeds with cleanup
- In EVENT_OBJECT_LOCATIONCHANGE and WM_PAINT: Checks the flag with
Acquire ordering both at handler entry and immediately before calling
BeginDraw/EndDraw, exiting early if destruction is in progress
The Acquire/Release memory ordering creates a synchronizes-with
relationship that ensures:
1. When the destruction flag is set, all subsequent handler checks will
see it (no stale cached values)
2. Handlers that pass the first check but race with destruction will be
caught by the second check before touching D2D resources
3. The 10ms sleep window allows any handler already past both checks to
complete its EndDraw() before resources are freed
This is a lock-free solution with zero overhead on the hot rendering
path (atomic loads are nearly free) and provides defense-in-depth with
multiple barriers against the use-after-free condition.
This commit ensures that we check if a window is already managed in any
workspaces even after checking known_hwnds, because windows moved as
part of the earlier ensure_workspace_rules call can slip through the
cracks.
This commit fixes a use-after-free bug in the border_manager that was
causing crashes with exception code 0xc000041d when borders were being
destroyed during workspace/monitor changes.
The root cause was a race condition between the main thread destroying a
border window and the border's window thread still processing queued
window messages (WM_PAINT, EVENT_OBJECT_LOCATIONCHANGE). The crash
occurred when these callbacks invoked render_target.EndDraw(), which
internally calls HwndPresenter::Present() to present the rendered frame
to the HWND. By this point, the HWND and its associated DirectX surfaces
had already been freed, resulting in Direct2D attempting to dereference
freed memory (0xFEEEFEEEFEEEFEEE - Windows heap poison value).
The issue stemmed from two problems in destroy_border():
1. Direct2D resources (render_target and brushes) were not being
released before closing the window. These COM objects hold internal
pointers to the HWND and its DirectX swap chain/surfaces. When
close_window() was called, Windows began tearing down these resources
while the Direct2D objects still held dangling pointers to them.
2. GWLP_USERDATA was not being cleared before closing the window. This
meant that any messages already queued in the window's message queue
could still retrieve the border_pointer and attempt to render using
the now-invalid Direct2D resources.
This commit addresses both issues:
- In destroy_border() (mod.rs): Explicitly set render_target to None and
clear the brushes HashMap before calling destroy(). This ensures that
Direct2D COM objects are properly released while the HWND is still
valid, preventing EndDraw() from accessing freed HWND resources.
- In destroy() (border.rs): Clear GWLP_USERDATA before calling
close_window(). This ensures that any pending window messages will see
a null pointer and exit early from the callbacks (which already have
null pointer checks in place).
These changes create two layers of defense against the race condition:
the callbacks won't access the border_pointer (it's null), and even if
they somehow did, the render_target would be None so no rendering
operations would occur.
I think this is the root cause of a lot of crash tickets that people are
mistakenly attributing to specific applications which lack
reproducibility across different users/machines, i.e. #1626, #1624.
The WorkspaceTenantName is populated far more consistently than MdmUrl.
This commit switches to extracting that instead and passing it on to the
MDM splash screen so that users on non-corporate devices who may have
unintentionally enrolled themselves into BYOD MDM by logging into an
account a clicking "Yes" on some dark pattern pop-up have a clear
indication of why they are seeing the splash, and can take the
appropriate steps to remove the MDM profile from their system if
desired.
I think this got broken as part of the automatic Rust version syntax
upgrades which joined two if clauses together with && which should have
been kept separate.
Now, if the user gives the --bar flag to the start command, and a static
config file is not resolved, or if the static config file does not have
a bar_configurations stanza, it will fallthrough to the default
PowerShell snippet to try and start komorebi-bar without an explicit
--config flag.
This commit fixes the stupidest of stupid bugs. Column width
calculations on the Scrolling layout should take the number of windows
into account, especially when lower than the configured column count.
I was getting really tired of having to switch between display inputs to
different platform-specific machines to be able to make and test changes
on komorebi for Windows and komorebi for Mac.
With this commit, the `flake.nix` provides a Nix devShell and crane
build for users to make and validate changes with `cargo check`, `cargo
clippy` and `cargo build` with the Windows MSVC toolchain on Linux and
macOS.
Getting tired of making little changes in both this and the komorebi for
Mac repo - I think eventually either komorebi-themes will live in its
own repo or komorebi for Mac will be integrated here.
But for now, at least everything is defined in komorebi-themes and I
don't have to redefine any theme-related stuff in komorebi for Mac.
This commit ensures that the various default values that the different
Option<T> config properties can be unwrapped to are encoded by schemars
so that they can be picked up by docgen.
This commit ensures that we are using show_total_activity and
show_activity instead of show_total_data_transmitted and
show_network_activity in the komorebi.bar.example.json file which is
populated for users in the quickstart command.
JSONSchema is not smart enough to resolve aliases for backwards compat,
which results in confusing behaviour for new users trying to edit this
file.
fix#1596
This commit makes the komorebic promote-swap command reversible by
storing the previous container index in the Workspace state.
If the current container index is the same as the layout's primary
index, and there is a previous promotion swap container index available,
the two containers at those indices will be swapped, effectively making
a second call to promote-swap an undo.
This commit adds a new SocketMessage variant PromoteSwap and
corresponding komorebic promote-swap command.
PromoteSwap will also promote the focused window container to the
largest tile in the layout, but instead of removing the focused
container x from the Ring and inserting it into position 0, possibly
changing the positions of other windows between 0 and x, the indices x
and 0 in the Ring will be swapped directly.
This commit adds visual feedback in the form of a ghost tile for
preselections made by the preselect-direction command.
A container with the id "PRESELECT" will be added to the workspace, and
replaced when the next manage-able window is spawned.
A new command, cancel-preselect, has been added to remove both the
preselection index and the ghost tile if the user changes their mind.
This commit adds a new feature to preselect the direction of the next
spawned window with a corresponding komorebic preselect-direction
command which takes an OperationDirection.
If the OperationDirection is valid from the current position, it will be
stored in the Workspace state, and then read, applied, and deleted when
the next manage-able window is spawned.
Direction preselection does not (yet?) support the Grid layout.
Marking all --ahk flags in the start/stop/kill/enable-autostart commands
as EOL; there are any number of ways that users can manage their own
launching of AutoHotKey scripts and the complexity of the different ways
that AHK can be installed is not worth the maintenance burden for this
project.
This commit adds a new LayoutOptions option, GridLayoutOptions,
currently with a single configurable "rows" opt which can be use to
constrain the grid by number of rows.
This commit adds a new option under layout_options.scrolling -
"center_focused_column", which defaults to false. When
set to true, and when the number of scrolling columns is an odd number
>=3, komorebi will, if there are enough windows being managed on the
workspace, and if the focused window is not too close to either the
beginning or the end of the workspace ring, keep the focused window in a
centered position in the layout
This commit standardizes the codebase to disallow usage of the raw eyre!
macro for creating errors, instead using ok_or_eyre() when constructing
ad-hoc errors from Result and Option types, and otherwise using the
bail! macro in response to failed boolean conditions.