Repository navigation
Architecture
This describes the window manager in ../swinewm/. The scope
and roadmap are in project_summary.md; per-command
status is in compatibility.md.
swinewm/ mirrors the sway repository so that a file can be compared with
its counterpart (swinewm/swinewm/tree/container.c ↔
sway/sway/tree/container.c):
swinewm/
CMakeLists.txt builds the core everywhere, the .exe files on Windows
config.in default config (sway's, adapted for Windows)
common/ list_t, logging, string helpers, JSON writer, fopen
include/ headers; include/swinewm/ mirrors sway/include/sway/
swinewm/ the window manager
commands.c command parser and handler table
commands/ one file per command or small group of commands
config.c config reader: blocks, $variables, include, deferral
tree/ root, output, workspace, container, view, arrange
input/ seat (focus stack), key names and binding lookup
desktop/ transaction: push layout changes to the platform
ipc-server.c portable IPC request handling
ipc-json.c get_tree / get_workspaces / ... replies
bar/ the built-in Waybar: config, modules, CSS, formats
notify/ the built-in Mako: notifications, config, modes
main.c command-line options and start-up order
win32/ the only code that includes <windows.h>
resources/ installed defaults: Waybar and mako configs,
wallpaper, icon (also compiled in as fallbacks)
cmake/ packaging (CPack/WiX MSI) and build helpers
swinemsg/ IPC client (swaymsg)
swinemenu/ launcher (wofi/wmenu): lists apps, asks swinewm
to exec the choice, themed by get_bar_config
tests/ unit tests with a fake platform
The core is portable C11 with no Windows headers. It talks to the OS only
through platform.h:
| Core asks the platform to | Platform tells the core |
|---|---|
| set a window's geometry, show/hide it, activate it, close it | a window appeared / disappeared / was minimized / was focused / changed title |
start a process (exec) |
an output (monitor) was added, removed or resized |
list config file locations, expand ~ and %VAR%
|
a key binding fired, an IPC request arrived |
The tests link tests/fake_platform.c
instead of win32/, which records the requests. Everything about layout,
focus and commands can therefore be tested on Linux in CI, and the
behaviour can be compared with sway directly.
Same model as sway:
root
├─ output (one per monitor) ── workspace ── container ── container (view)
│ │ └── container (view)
│ └─ floating: container (view)
├─ fallback output (holds workspaces while no monitor is connected)
└─ scratchpad (hidden containers)
- A view is one managed native window (
HWND). A container holds either one view or child containers with a layout (splith,splitv,tabbed,stacking). - The seat focus stack is a most-recently-focused list of nodes. As in sway it answers "which child of this node was focused last", which drives directional focus, where new windows are inserted, which tab is visible and where focus goes when a window closes.
- Functions keep sway's names and semantics (
container_split,container_reap_empty,workspace_wrap_children,workspace_squash,container_move_in_direction, ...). When in doubt, the sway source is the reference; deviations are commented.
sway separates pending from current state and commits changes in transactions that wait for Wayland clients to acknowledge new sizes. Native windows have no such handshake, so swinewm simplifies this:
- Commands and events change the tree.
-
transaction_commit_dirty()runsarrange_root(), which computes each view's desired box and visibility (view->pending). - It then diffs against what was last applied (
view->current) and calls the platform: hide first, then move/resize, then show, then activate the focused window if focus changed.
The whole tree is arranged every time; at desktop scale this is cheap and it avoids dirty-tracking bugs.
All of it is in ../swinewm/swinewm/win32/.
Everything that touches the tree runs on the main thread. The others only post or send messages to the shell's hidden top-level window:
-
WinEvent hooks (out of context) only queue events and post
WM_SWINE_EVENTS; the queue is drained on the main thread. -
The low-level keyboard hook must return quickly or Windows removes it,
so it only looks the key up and posts
WM_SWINE_BINDINGwith a copy of the command. - IPC client threads pass each request to the main thread and write the reply back. Subscribed event frames use a per-client writer queue.
-
The tray thread owns swinewm's
Shell_TrayWnd(see the tray section below) and postsWM_SWINE_TRAYwhen icons change. - A hidden top-level window (not a message-only window) is used because only
top-level windows receive
WM_DISPLAYCHANGEandWM_SETTINGCHANGE.
Roughly what Alt+Tab shows: visible, not cloaked, top-level, not a tool
window unless it has WS_EX_APPWINDOW, has a caption, not one of the shell's
own classes (taskbar, desktop). Owned windows, standard dialogs (#32770)
and windows without a sizing border start out floating, matching sway's
handling of dialogs and fixed-size windows.
Windows on inactive workspaces are hidden with ShowWindowAsync(SW_HIDE),
the same approach as the earlier C++ prototype (removed; see git history).
Each view counts the show/hide events it caused, so they are not mistaken for the application
hiding itself (minimizing to the tray). An application that shows a hidden
window by itself (activated from its tray icon) pulls the user to that
workspace.
Hidden windows would be lost if swinewm crashed, so:
- the handles (with process ids) of hidden windows are written to
%LOCALAPPDATA%\swinewm\hidden-windows.txt; -
swinewm --recoverand every start of swinewm show them again; - an unhandled-exception filter and the normal exit path show every hidden window.
- a separate watchdog process checks the shell window's responsiveness and
watches the manager process. After three missed heartbeats it terminates a
hung manager before running
--recover; a clean-shutdown event prevents recovery on an intentional exit. When SwineWM is the login shell, the watchdog starts Explorer after recovery.
Windows 10 and 11 windows have invisible resize borders outside the visible
frame. The layout is in terms of the visible frame (from
DWMWA_EXTENDED_FRAME_BOUNDS), and the rectangle passed to SetWindowPos
is grown by the measured borders. Maximized windows are restored first.
Outputs use the monitor's work area for tiling (so the taskbar, and later
swinebar, stay visible) and the full monitor for fullscreen.
A hung application must not freeze the window manager. Calls on other
processes' windows are asynchronous: SetWindowPos(... SWP_ASYNCWINDOWPOS),
ShowWindowAsync, PostMessage(WM_CLOSE). Nothing uses SendMessage on a
foreign window.
Windows restricts SetForegroundWindow to the process that received the
last input. When the first attempt is refused, swinewm sends a zero-length
synthetic mouse move and retries. Activation can still be refused, so
platform_view_activate returns the outcome and the core treats focus as a
request (see the archived design review). Foreground changes made by the
user (clicking a window) update the seat focus; changes involving windows on
hidden workspaces are ignored, because Windows briefly activates such windows
while a workspace is being hidden.
RegisterHotKey cannot claim most Win+key combinations, so a
WH_KEYBOARD_LL hook checks every key press against the current mode's
bindings and swallows matches (and their key releases). Modifier state is
read with GetAsyncKeyState on each press, so it cannot get stuck after
Win+L. When Win or Alt was part of a binding, an unassigned key is injected
before its release so that the Start menu or a menu bar does not open.
Key names are xkb keysym names mapped to virtual-key codes; punctuation
follows the US layout for now (layout-aware lookup with VkKeyScanEx is a
to-do).
A WH_MOUSE_LL hook (win32/mouse.c) passes pointer input to the core
(input/cursor.c, a port of sway's seatop_* files). Motion is only
recorded in the hook and handled from a posted message, at most one pending
at a time. A press is handed to the core inside the hook, because the hook
has to answer at once whether the application gets it; the core starts a
move or resize there, and posts the commands of mouse bindings instead of
running them. A swallowed press has its release swallowed too. What is under
the pointer is WindowFromPoint, so menus and other windows swinewm does
not manage are seen as such. While swinewm code runs on the main thread
(win32_busy), the hook leaves input alone: a COM call that pumps messages
must not re-enter the core.
The core works out each view's decoration at every commit
(view_update_decoration: border colour from the client.* class, title
bar or not, square corners for tiled windows, opacity) and hands changes to
platform_view_set_decoration. win32/decor.c applies them on a worker
thread, because changing another application's window style waits for that
application: the DWM border colour (Windows 11), the title bar colours for
border normal, WS_DLGFRAME removed for pixel and none, and
WS_EX_LAYERED for opacity. What it changed per window goes into the
recovery file and is undone on exit, by the crash handler and by
--recover.
exec uses CreateProcessW, which searches PATH and app aliases such as
wt.exe; if that fails the command is handed to ShellExecuteExW, which
opens documents, URLs and shell: folders. There is no implicit shell:
write exec cmd /c ... for shell syntax. SWINESOCK is set in the
environment of started programs, like SWAYSOCK.
The i3/sway wire format ("i3-ipc", length, type, JSON payload) over the
named pipe \\.\pipe\swinewm-ipc.<user>, overridable with SWINESOCK.
Remote clients are rejected. The first pipe instance is created with
FILE_FLAG_FIRST_PIPE_INSTANCE, which also prevents a second swinewm from
starting. Subscribed clients receive events through per-client writer queues,
so a slow IPC reader cannot block the window-manager thread.
The manager starts a second swinewm.exe --watch <pid> <shell-mode> process. It probes
the hidden shell window every two seconds and watches the manager process
handle. Three consecutive missed probes cause the watchdog to terminate the
unresponsive manager and run --recover; an unexpected process exit also
triggers recovery. Intentional exit and restart signal a named event before
teardown, so they do not trigger recovery (the normal shutdown path restores
the desktop). If the manager was the login shell, the watchdog also starts
Explorer after recovery.
Waybar is a separate program that gets workspace and window state from Sway over IPC. In swinewm the bar is part of the window manager and reads the tree directly; only the configuration is Waybar's.
-
Enabling. A sway
bar { }block (any content),barlines, orexec waybarturn the bar on;bar mode invisiblehides it.position top|bottomfrom the sway block applies unless the Waybar config setsposition. Other swaybar options are accepted and ignored. -
Config.
waybar/config.jsonc(orwaybar/config) andwaybar/style.cssin the swinewm config directories, in Waybar's formats. Without them, built-in defaults adapted from Waybar's own are used. The JSONC reader accepts comments and trailing commas. -
Modules (
swinewm/bar/bar.c) turn state into labels using Waybar's{name}/{:%H:%M}format syntax,format-icons,format-alt,states,max-lengthandon-click. Unsupported modules are skipped with a log message. -
Styling (
swinewm/bar/css.c) implements the CSS subset that Waybar themes rely on: type/#id/.class selectors with descendant combinators,@define-color,alpha(), colours, fonts, padding, margin, borders, andbox-shadow: insetunderlines. Rules with pseudo-classes such as:hovernever match. Elements mirror Waybar's widget tree:window#waybar, alabelper module with the module id (#clock,#custom-media) and state classes, and#workspaces button.focused|visible|urgent. - Space. The bar's height (scaled by the monitor's DPI) is reserved on each output, and tiling uses the remaining area. The bar hides while the output shows a fullscreen window.
-
Display changes. Display and DPI notifications are coalesced briefly
before outputs are re-enumerated and the layout, bars and wallpaper are
refreshed. This lets resolution and mixed-DPI changes settle before the
bar and wallpaper windows are placed again. Windows-only smoke checks:
change a monitor's resolution or refresh rate while a game is fullscreen,
exit the game, and verify the layout, bar and wallpaper recover; repeat
with monitors at different scaling percentages and verify display settings
are restored by normal exit and
swinewm --recoverafter a configured mode change. -
Rendering (
win32/bar.c): one topmost, non-activating tool window per output, painted with GDI into a back buffer. Clicks run the item's command (for exampleworkspace "2"); the wheel switches workspaces. Translucent colours are blended over black for now.
On Linux, Waybar's tray shows StatusNotifierItems that programs such as
nm-applet register over D-Bus. On Windows the equivalent is the
notification area: programs call Shell_NotifyIcon, which sends
WM_COPYDATA to the first window of class Shell_TrayWnd that
FindWindow returns. When the bar has a tray module, swinewm becomes
that window (win32/tray.c, after ManagedShell's TrayService):
- It creates
Shell_TrayWnd(never shown) with aTrayNotifyWndchild and keeps it topmost, re-raising it every 100 ms, soFindWindowfinds it before Explorer's. It then broadcasts the registeredTaskbarCreatedmessage, and running programs add their icons again, now to swinewm. - The window runs on its own thread:
Shell_NotifyIconwaits for the answer, selected AppBar queries are handled directly when SwineWM is the shell, and other messages swinewm does not handle (including Explorer's private ones) are passed on to Explorer's taskbar withSendMessageTimeout. Neither can block the main thread. The icon table (window, id or GUID, callback message, version, a copy of the icon, tooltip, hidden state) is shared under a lock; the main thread copies it into the core (swinewm/bar/tray.c) onWM_SWINE_TRAY, and the bar module (swinewm/bar/bar.c) applies Waybar's options. Icons whose window is gone are pruned every 2 s. - Clicks on an icon are sent to its program with
SendNotifyMessage, encoded as Explorer does: button down/up (double click when quick enough), thenNIN_SELECTafter a left click andWM_CONTEXTMENUafter a right click; version-4 icons get the position inwParam.AllowSetForegroundWindowlets the program show its menu.Shell_NotifyIconGetRectis answered with where the bar drew the icon. - Balloon tips (
NIF_INFO) become notifications of the built-in Mako. - As the shell, swinewm also loads the SysTray shell service object
(
stobject.dll), which adds the volume, network and power icons that Explorer would otherwise show. - Undo: when the tray module goes away or swinewm exits, the window is
destroyed and
TaskbarCreatedis broadcast again, so programs give their icons back to Explorer. After a crash the window is dropped to the bottom of the z-order instead (its thread may be stuck) before the broadcast;swinewm --recoverbroadcasts too.
Explorer keeps showing the icons it had when swinewm took over (it never
learns they moved), so its own tray looks stale until swinewm exits;
explorer_taskbar autohide hides it anyway.
sway starts swaybg for output … bg …; swinewm draws the wallpaper itself
(win32/wallpaper.c). Each output with a background gets a non-activating
window covering the monitor at the bottom of the stacking order. Images are
decoded and scaled with the Windows Imaging Component (Fant
interpolation) and composed once per size; the swaybg modes stretch,
fill, fit, center, tile and solid_color are supported, with the
fallback colour around the image.
While Explorer runs, its desktop windows (Progman, WorkerW) are at the
very bottom. The wallpaper window is kept directly above them instead of
moving Explorer's windows, so exiting swinewm leaves the normal desktop as
it was.
Mako receives notifications over D-Bus. In swinewm, notify-send is a
command (so it works in bindings, over swinemsg, and as
exec notify-send ... from scripts started by swinewm), and makoctl dismiss|restore|invoke|mode|reload likewise. exec makoctl ... and
exec mako lines in sway configs are routed to the built-in versions.
-
Config (
swinewm/notify/notify.c):mako/configin the swinewm config directories, in mako's format: global options, then[criteria]sections (app-name,summary,body,urgency,category,mode,expiring) applied in order. Styles are resolved when a notification arrives and again when modes change or the config is reloaded. -
Behaviour follows mako(5):
default-timeout(0 = never), history ofmax-historydismissed or expired notifications formakoctl restore,max-visible,sort,invisible, modes such asdo-not-disturb, theformatspecifiers%a %s %b %h %t, and button actions. -
Rendering (
win32/notify.c): one topmost, non-activating window per visible notification, stacked from the configuredanchorinside the usable area of the focused (or configured) output. Expiry uses a one-shot timer set for the next deadline. -
swinemsg -t get_notifications/get_notification_historylist them (swinewm IPC extensions 1000 and 1001).
-
Urgency. Windows has no urgency hint, but an application that wants
attention flashes its taskbar button (
FlashWindowEx), which reaches shell-hook windows asHSHELL_FLASH. swinewm registers its window withRegisterShellHookWindowand turns that into sway's view urgency: the workspace gets theurgentclass in the bar until the window is focused. sway'surgent enable|disable|allow|denycommand works too. -
Fullscreen. An application that makes itself fullscreen (F11 in a
browser) is detected from location changes and
HSHELL_FULLSCREENENTER/EXIT: its "effective" rectangle (client area if it has a frame) equals a monitor. A managed window then becomes a sway fullscreen container, as an xdg-toplevel fullscreen request does in sway, and returns to the layout when it leaves fullscreen. An unmanaged fullscreen window in front (a game) only hides the bar on its monitor.
win32/shell.c. swinewm acts as the shell when no shell window exists at
start-up, typically after swinewm --set-shell made it the user's logon
shell. --shell and --no-shell override the detection, but --shell is
refused while Explorer (or any other shell) runs: two shells would fight
over the work area. Then it:
- logs to
%LOCALAPPDATA%\swinewm\swinewm.loginstead of a console when started without a terminal (by Winlogon); - creates a bottom-most desktop window and registers it with
SetShellWindow(some programs look for the shell throughGetShellWindow); - sets the
ShellDesktopSwitchEventevent so Windows completes the logon; - starts the user's startup programs (Run/RunOnce keys and Startup folders, honouring entries disabled in Task Manager), once per logon session;
- owns the work area: each monitor's work area is set to the monitor minus
the bar, so maximized windows outside the tiling respect the bar, and
reset on exit. The change is announced with a posted
WM_SETTINGCHANGE(notSPIF_SENDCHANGE, which waits for every window), and swinewm does not follow work area changes made by others; - makes
exitlog off, as sway's does; - after a crash, starts Explorer (with a short delay, so that Explorer finds no shell window and becomes the shell) so the user is never stranded.
Next to Explorer, explorer_taskbar autohide switches Explorer's taskbar to
auto-hide and keeps it hidden, restoring the original setting on exit,
after a crash and with --recover.
The approach follows Cairo Desktop's ManagedShell library; see
research/windows-shell.md for the details
and what remains (notably registering bars as AppBars while running next to
Explorer).
The feature-by-feature status lives in
compatibility.md. The larger remaining areas include
decorations, IPC subscriptions/events, script-driven
Waybar modules, Windows toast notifications and registering bars as AppBars
when running beside Explorer. These are separate projects with different
Windows dependencies; do not infer support from the upstream format alone.