GlazeWM is a tiling window manager for macOS and Windows, written in Rust (nightly toolchain). This is a personal fork (jtroeth1/glazewm, branch main) with custom column layout features.
Upstream: glzr-io/glazewm.
- wm (bin): Core window management logic — models, commands, events, IPC, platform sync. Entry point:
main.rs→start_wm()→WindowManager::new()→ event loop. - wm-cli (bin, lib): CLI for IPC with the main application.
- wm-common (lib): Shared types (
AppCommand,WmEvent,ColumnsMode, DTOs, IPC messages), utilities (try_warn!macro), and constants. - wm-platform (lib): Platform-specific API wrappers. Other crates never call Windows/macOS APIs directly. Uses
crate::Error/crate::Result(notanyhow). - wm-ipc-client (lib): WebSocket client for IPC.
- wm-watcher (Windows-only, bin): Watchdog for cleanup on crash.
- wm-macros (lib): Derive macros.
Cross-compile target: x86_64-pc-windows-gnu (MinGW). Cannot run tests natively on Linux (Windows API deps). Tests compile for Windows target.
# Build
cargo build --release --target x86_64-pc-windows-gnu
# Stage, then deploy elevated (stops GlazeWM, backs up, promotes, relaunches).
cp target/x86_64-pc-windows-gnu/release/glazewm.exe \
/mnt/c/Users/jtroeth/.glzr/glazewm/glazewm-new.exe
powershell.exe -NoProfile -Command "Start-Process powershell.exe \
-ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File', \
'C:\Users\jtroeth\.glzr\glazewm\deploy-glazewm.ps1' -Verb RunAs -Wait"- Use
deploy-glazewm.ps1.C:\Program Files\glzr.io\GlazeWM\is not writable from WSL without elevation (ACL grantsBUILTIN\UsersonlyReadAndExecute), and the running.exeis locked besides. The script stopsglazewm-jt+glazewm-watcher, backs upglazewm-jt.exe→glazewm-jt-bak.exe, promotesglazewm-new.exe, deletes the staging file, then runsschtasks /run /tn StartGlazeZsolt. - The staged binary MUST be named
glazewm-new.exe. Verify the promotion withb2sum -l 64on both the build output and the installed exe. - The scheduled task
StartGlazeZsoltrunsglazewm-jt.exedirectly — it does not invokestart-glazewm.ps1, so that script's own staging logic is inert. - GlazeWM must run elevated (Task Scheduler) to reposition windows. Non-elevated instances get "Access is denied" on
SetWindowPos/z-order calls, then exit. - Linker configured in
.cargo/config.toml:x86_64-w64-mingw32-gcc.
- No
.unwrap(). Useanyhowin all crates exceptwm-platform(which usescrate::Error/crate::Result). - Logging:
tracingmacros (tracing::info!,tracing::warn!, etc.).setup_logginginstalls three sinks: stdout,errors.log(ERROR only), andglazewm.log.<date>(daily-rolled, full verbosity level, default INFO). The release build is awindows-subsystem binary launched by Task Scheduler with no console and no redirection, so stdout goes nowhere in production — the rolling file is the only way to seeinfo!/warn!from a real run. File sinks set.with_ansi(false). - Log noise is a bug.
discover_windowsruns every 5s over every visible window; anything logged atinfo!per-window per-tick buries the diagnostics that matter. Log adoptions where they happen (manage_windowalready emitsNew window managed:), not at the call site. - Formatting:
rustfmt.toml— 2-space tabs, 75 char max width, crate-level import granularity. - Linting:
clippy::all+clippy::pedanticat warn level. - Comments: All functions documented. Punctuation at end of all comments. Unsafe blocks get
// SAFETY: .... Type names in backticks. - Tests:
#[cfg(test)]modules. Unit tests for core functionality.
SingleInstance::new()— mutex prevents duplicate instances.UserConfig::new()— parse config.WindowManager::new()→WmState::populate()—CloakJournal::recover()first (see below), then enumerate monitors, thenvisible_windows()in reverse z-order, callingmanage_window()for each, thenjournal_managed_windows().- Register event listeners (window, display, mouse, keybinding).
- Run startup commands (e.g.
shell-exec zebar). - Enter
tokio::select!event loop.
visible_windows()→EnumWindows+is_visible()filter (checksIsWindowVisibleANDDWMWA_CLOAKED).manage_window()→check_is_manageable()filters: not visible → skip,WS_CHILD/WS_EX_NOACTIVATE/WS_EX_TOOLWINDOW→ skip, owner without caption → skip. Acheck_is_manageableerror is logged atwarn!with the handle instead of being swallowed byunwrap_or(None).- Window rules (config
window_rules:) run after management — canignore,set floating, etc. handle_window_shownevent catches windows that appear after startup.- Startup disposition audit.
WmState::populatelogsStartup: found N visible windows to manage., then for every handle that is still absent from the tree after its manage attempt,Startup: window NOT managed: <describe_window> (<describe_window_styles>)— handle, visible, cloaked, child, no_activate, tool_window, caption, owned. The counts must reconcile: visible = managed + NOT managed. This is the tool for "GlazeWM missed my window";describe_window_styleslives at the end ofwm_state.rsand is Windows-only (non-Windows returns an empty string). discover_windows()(5s tick) re-scansvisible_windows()and adopts anything absent from the tree, resolving the nearest monitor's displayed workspace astarget_parent(passingNonewould attach next to the focused container instead). It skips already-managed andignored_windows, but it has no memory of windowscheck_is_manageablerejected, so it re-probes every permanently-unmanageable tool window every tick — cheap, but it must stay silent (see Code Style).WmState::nearest_monitorfalls back to the first monitor whenever the nearest display cannot be resolved or is not in the tree, and returnsNoneonly when there are no monitors. Callers that place windows drop the window silently onNone, so that fallback is load-bearing.
With hide_method: 'cloak', windows on non-displayed workspaces are hidden via IApplicationView::set_cloak. A cloaked window is invisible to visible_windows() and to discover_windows(), and show()/SW_SHOWNA cannot reveal it — only uncloaking can. Previously nothing uncloaked on exit, so a crash orphaned those windows permanently: unmanaged, untileable, and (with title bars hidden) unclosable.
Fix has two layers:
- Uncloak on every exit path.
WindowManager::cleanupuncloaks allstate.windows();WmState::dropuncloaks beforeshow()as an unwind-safe backstop;wm-watcheruncloaks its recorded handles. All idempotent. - Journal for hard kills.
~/.glzr/glazewm/cloaked-windows.txt(config dir), one<handle>\t<process_name>line per managed window, rewritten inpopulateand on the 5scleanup_invalid_windowstick.recover()runs before enumeration and deletes the file after.
Recovery is driven only by journalled handles, never by the cloak flag — DWMWA_CLOAKED returns DWM_CLOAKED_SHELL (2) for suspended UWP apps and windows on other virtual desktops too, so a blanket uncloak-all would drag those onto the screen. Each entry must additionally pass is_valid(), is_cloaked() == true, and a process_name() match against the journal before being uncloaked, which also guards against handle reuse.
Root → Monitor(s) → Workspace(s) → SplitContainer(s)/TilingWindow(s)/NonTilingWindow(s). Focus tracked via child_focus_order deques. set_focused_descendant() propagates focus up the tree.
Declarative column layouts. Two invariants carry the whole feature:
- Window order is derived, never stored. It is the layout read back
row-major (
ColumnGrid::windows), which is exactly the orderdistribute_columnsdeals windows into columns — so read and distribute are inverses and reapplying a layout is a no-op. There is nowindow_orderbuffer to drift out of sync with the tree. - The center (
C) column's occupant is explicit.Workspace::master_window: Option<Uuid>names it. Never inferred from focus, z-order, or column widths.
Both replaced guess-based mechanisms that were the source of the
random master-window flipping: a window_order/grid_affinity pair that
drifted from the tree, and a center_index() that took the widest column
(a tie for C,* at center: 0.5, resolving to the stack).
packages/wm/src/commands/workspace/columns/mod.rs— Commands:apply_columns,apply_grid,reapply_assigned_columns,reapply_columns_for_new_window,reapply_columns_after_move,toggle_columns_mode,apply_rotate,apply_center,move_window_in_columns,focus_in_columns. Internals:ordered_windows,resolve_master,center_column. 30 tests.packages/wm/src/commands/workspace/columns/spec.rs— Pure spec parsing (parse_columns_spec,distribute_columns,row_major,column_widths). No tree dependency. 10 tests.packages/wm/src/commands/workspace/columns/grid.rs—ColumnGridbridge: reads container tree into flat grid, renders grid back to tree. Focus preservation across tree rebuilds.packages/wm/src/models/workspace.rs—master_windowandcolumns_modefields with accessors.
Comma-separated tokens, left-to-right: C = center column (exactly one), * = flexible stack, number = fixed stack count. Examples: C,*, *,C,*, 2,1,C,3.
Side windows are dealt row-major, left to right: one per non-center column, then a second row, etc. A fixed column drops out once full. This makes assignment prefix-stable (window n always lands in the same column) and invertible. There is deliberately no bias knob — dealing from anywhere but the leftmost column is not recoverable from the grid, so it would break idempotence.
MasterStackLeft(default): master window inCcolumn, spec as configured.MasterStackRight: spec reversed (C,*→*,C). A symmetric spec (*,C,*) reverses to itself — its master is already between two stacks, so there is nothing to flip.Grid: Round-robin into equal columns (which is row-major, so also idempotent). Requires ≥4 windows; "armed" with fewer (mode stays Grid, layout falls back to master-stack-left, auto-applies when 4th window arrives).
Toggle cycle via Alt+G: Left → Grid → Right → Left.
- Set explicitly by
apply_center,apply_rotate, andmove_window_in_columns(any move into or out of theCcolumn). - Cleared when it leaves the workspace:
handle_window_destroyed.rs,handle_window_hidden.rs,move_window_in_direction.rs,move_window_to_workspace.rs. resolve_masterrepairs a missing/stale designation by promoting the first window in on-screen order. This is the only implicit change.- New windows:
reapply_columns_for_new_window(workspace, id, ...)inmanage_window.rsforces the new window to the end of the order, so it takes the next free slot and nothing else moves. Same mechanism for the cross-workspace target inreapply_columns_after_move. focus_workspace.rsreapplies unconditionally — safe because reapply is idempotent.
- Command:
toggle-columns-mode(dispatched asInvokeCommand::ToggleColumnsMode). - Query:
query columns-mode→ClientResponseData::ColumnsMode(ColumnsModeData). - Event:
ColumnsModeChanged(subscribable viasub -e columns_mode_changed). - Serde:
ColumnsModeserializes asmaster_stack_left/master_stack_right/grid(snake_case).ColumnsModeDatafields are camelCase (columnsMode,workspace). - IPC messages are parsed via clap. Top-level subcommand for invoking is
command(aliasc), queries usequery, subscriptions usesub -e <event_name>.
general:
default_columns:
- min_aspect_ratio: 2.1 # Ultrawide
spec: '*,C,*'
center: 0.5
- min_aspect_ratio: 1.5 # Standard widescreen
spec: 'C,*'
center: 0.5
- spec: default # Narrower: normal tilingCustom Zebar widget pack at /mnt/c/Users/jtroeth/.glzr/zebar/custom-bar/.
with-glazewm.html— React widget with direct WebSocket to GlazeWM IPC (ws://localhost:6123). Subscribes tocolumns_mode_changedandfocus_changedevents. Re-queries columns mode on focus change (workspace/monitor switch). Click uses Zebar provider'srunCommand().styles.css— Black background (rgba(0 0 0 / 90%)),.columns-modebutton class.zpack.json— 30px height preset forwith-glazewmwidget.settings.json— Points tocustom-barpack.
◧= MasterStackLeft◨= MasterStackRight⊞= Grid
- GlazeWM config:
C:\Users\jtroeth\.glzr\glazewm\config.yaml - GlazeWM deploy script:
C:\Users\jtroeth\.glzr\glazewm\deploy-glazewm.ps1(run elevated; the supported deploy path) - GlazeWM launcher:
C:\Users\jtroeth\.glzr\glazewm\start-glazewm.ps1(not used by the scheduled task) - GlazeWM staging:
C:\Users\jtroeth\.glzr\glazewm\glazewm-new.exe(consumed on next launch) - GlazeWM binary:
C:\Program Files\glzr.io\GlazeWM\glazewm-jt.exe - GlazeWM backup:
C:\Program Files\glzr.io\GlazeWM\glazewm-jt-bak.exe - GlazeWM logs:
C:\Users\jtroeth\.glzr\glazewm\glazewm.log.<YYYY-MM-DD>(daily, INFO+ — the useful one),errors.log(ERROR only). Multiple instances append to the same daily file; split sessions onStarting WM with log level. - GlazeWM cloak journal:
C:\Users\jtroeth\.glzr\glazewm\cloaked-windows.txt(deleted on successful startup recovery) - Zebar config:
C:\Users\jtroeth\.glzr\zebar\custom-bar\ - Zebar settings:
C:\Users\jtroeth\.glzr\zebar\settings.json
- GlazeWM fork:
github.com:jtroeth1/glazewm.git, branchmain. - Config backup:
github.com:jtroeth1/jt.git(main), underconfig/glazewm/andconfig/zebar/.
- Elevation required: GlazeWM must run elevated to reposition windows. Non-elevated → "Access is denied" on every
SetWindowPos. Task Scheduler runs it elevated in production. - Window styles: Some apps (WSLg RAIL windows, Alacritty helper windows) have
WS_EX_TOOLWINDOW/WS_EX_NOACTIVATEand are correctly skipped bycheck_is_manageable. Diagnostic logging shows skip reasons. ColumnGrid::renderfocus corruption:move_container_within_treeandwrap_in_split_containersilently shift the focus chain during tree rebuilds.grid.rssaves/restores focused window ID across Phase 3 to fix this.- Config reload:
default_columnsare re-resolved on every reapply (effective_columns), so moving a workspace to a different-aspect-ratio monitor picks up that monitor's rule. - Cloaking is not hiding:
show()/SW_SHOWNAcannot reveal a window cloaked viaIApplicationView::set_cloak— onlyset_cloaked(false)can. Any new shutdown path must uncloak, or it orphans windows (see Cloak Recovery above). DWMWA_CLOAKED == 2is ambiguous: suspended UWP apps and windows on other virtual desktops reportDWM_CLOAKED_SHELLjust like GlazeWM-hidden windows. Never uncloak based on the flag alone; match against the journal.