rust-dwm
A guided map of the rewrite

Understand the desktop.
Then design the types.

X11 provides the machinery. DWM chooses how windows behave. The Rust rewrite makes those decisions and their ownership explicit.

01
The mental model

Three participants. One conversation.

An application and DWM each connect to an X server. DWM is itself an X client with permission to manage other windows.

Protocol model: Xlib overview. The application renders its content; DWM does not draw terminal text or browser pages. A separate compositor, if present, handles compositing effects.

A

A window is a resource

A window exists in the server and has a numeric ID. DWM’s Client is an additional local record containing policy state for a managed window.

WindowIdserver resourceClientIdlocal managed record
B

The root is the parent

The root window represents an X screen’s top-level window tree. Ordinary application windows are children. DWM selects management events there.

An X screen is not necessarily one physical monitor. DWM discovers monitor rectangles separately.

C

Showing is a request

A newly created window is initially unmapped. The application requests mapping. With root redirection selected, the server asks DWM to handle eligible map requests.

override_redirect windows bypass this management path.

The key handoff

SubstructureRedirectMask lets the WM intercept eligible map/configure requests on the root’s children. Only one client can select it on that root. That is why a second WM cannot simply take over the same screen.

Four things travelling over the connection

Request
Client → server: map, configure, focus or read a property.
Reply
Server → requesting client: requested data, such as attributes.
Event
Server → interested client: something happened, or policy is needed.
Error
A request failed; for example, the target window disappeared.

Metadata gives the WM context

Properties are typed data attached to windows. An atom is a numeric handle for a name such as WM_NAME. It identifies the property; it is not the property’s value.

  • WM_NORMAL_HINTSSize limits, increments and aspect constraints
  • WM_TRANSIENT_FORWhich window a dialog belongs to
  • WM_PROTOCOLSSupported messages such as graceful close
  • _NET_WM_STATEExtended states such as fullscreen

ICCCM and EWMH specify conventions between clients and WMs. The rewrite targets the subset DWM 6.8 supports.

Sending is not completion

Requests may be buffered. flush() sends pending requests; it does not prove another client has processed its events. Replies, checked errors and observable conditions matter for reliable tests.

02
From an event to a visible result

Follow the code, one change at a time.

Connect→Claim root events→Set up monitors & bar→Scan existing windows→Run event loop

The startup path is main → setup → scan → run. The scan adopts eligible windows that already exist. After that, the event loop waits for changes rather than repeatedly polling every application.

The actual C loop

dwm.c · run()C
void
run(void)
{
	XEvent ev;
	/* main event loop */
	XSync(dpy, False);
	while (running && !XNextEvent(dpy, &ev))
		if (handler[ev.type])
			handler[ev.type](&ev); /* call handler */
}

handler[ev.type] chooses a callback. State such as mons, selmon and dpy lives in globals.

The Rust shape

Proposed event dispatch · illustrativeRUST
fn run(&mut self) -> Result<(), WmError> {
    while self.running {
        let event = self.backend.next_event()?;
        match event {
            Event::MapRequest(request) =>
                self.manage_window(request.window)?,
            Event::KeyPress(key) =>
                self.handle_key(key)?,
            other => self.handle_other(other)?,
        }
        self.backend.flush()?;
    }
    Ok(())
}

Illustrative integration sketch: methods own their dependencies; an enum match makes event dispatch explicit. This snippet is not a complete implementation.

LifecycleYou open a terminalMapRequest
  1. Application creates and asks to map

    The server resource exists, but DWM does not yet have a managed-client record.

    application → X server
  2. Server redirects the request

    DWM receives MapRequest. It checks attributes and ignores override-redirect or already-managed cases.

    run → maprequest
  3. Build the client’s policy state

    Read title, hints and transient parent. Inherit a managed parent’s tags/monitor, or apply ordered rules. Decide floating placement.

    manage → Client
  4. Attach and arrange

    Insert into the monitor’s layout order and focus history. Calculate visible window geometry and apply hints.

    attach → arrange → tile → resize
  5. Issue management requests

    Configure geometry and borders, publish state, map the window and negotiate focus. The application draws its own content.

    DWM → X server → application
InputYou switch to tag 2KeyPress
  1. Resolve a grabbed binding

    The X server delivers the matching key event to DWM. Normalize lock modifiers and look up its binding.

    keypress → view
  2. Change the monitor’s view

    view changes the selected tag slot and preserves the other slot. Clients keep their existing tag memberships.

    TagView::select(tags)
  3. Reconcile focus and visibility

    Choose an eligible visible client. C DWM moves hidden windows offscreen while leaving them mapped.

    focus → arrange → showhide
  4. Place the visible clients

    Apply the current layout, update stacking and redraw the bar.

    arrangemon → tile/monocle → restack
ProtocolAn application enters fullscreenClientMessage
  1. Decode the requested state

    The application sends an EWMH state message through the X server. Validate its format and target.

    clientmessage → setfullscreen
  2. Preserve normal state

    Save placement, geometry and border once. A repeated entry must not overwrite the saved state.

    Client::enter_fullscreen
  3. Use the full screen rectangle

    Set zero border, resize to the monitor’s full area, publish fullscreen state and raise the window.

    _NET_WM_STATE → resizeclient
  4. Restore and arrange on exit

    Recover saved normal state. A tiled client can then receive newly calculated tiled geometry.

    Client::leave_fullscreen → arrange
CleanupA window disappearsDestroyNotify
  1. Resolve the window ID

    An unknown or already-removed window must be handled safely.

    destroynotify → wintoclient
  2. Remove all local relationships

    Clear lookup, layout membership and focus-history entries. Cancel an active drag for that client.

    unmanage → detach → detachstack
  3. Repair focus and layout

    Choose remaining focus, update the client list and rearrange. Do not send restoration requests to a destroyed window.

    focus → updateclientlist → arrange

A quick event map

ConfigureRequestApplication asks for geometryRespect floating requests or enforce managed layout
PropertyNotifyMetadata changesUpdate the relevant title, hints or state
ExposeA region needs repaintingRedraw DWM’s bar
MappingNotifyKeyboard mapping changesRefresh key translation and grabs
03
Make ownership visible

The server has windows. The WM has policy.

Rust structs represent the decisions DWM remembers between events. They do not replace the server’s window resources.

Read the original C structsClient and Monitor from the frozen baseline
dwm.c · ClientC
struct Client {
	char name[256];
	float mina, maxa;
	int x, y, w, h;
	int oldx, oldy, oldw, oldh;
	int basew, baseh, incw, inch, maxw, maxh, minw, minh, hintsvalid;
	int bw, oldbw;
	unsigned int tags;
	int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen;
	Client *next;
	Client *snext;
	Monitor *mon;
	Window win;
};
dwm.c · MonitorC
struct Monitor {
	char ltsymbol[16];
	float mfact;
	int nmaster;
	int num;
	int by;               /* bar geometry */
	int mx, my, mw, mh;   /* screen size */
	int wx, wy, ww, wh;   /* window area  */
	unsigned int seltags;
	unsigned int sellt;
	unsigned int tagset[2];
	int showbar;
	int topbar;
	Client *clients;
	Client *sel;
	Client *stack;
	Monitor *next;
	Window barwin;
	const Layout *lt[2];
};

next and snext are different linked orders. mon is a back pointer. In Rust, stores own the values and typed IDs express these relationships.

Identity

WindowId / ClientId

A server window ID can be reused. A managed-client ID names this local lifetime. Use distinct types so the compiler catches accidental mixing.

Distinct identity typesRUST
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WindowId(u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ClientId(u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MonitorId(u64);
InvariantRemove the window lookup when the client is removed.
Local state

Client

Remembers one window’s tags, monitor and presentation. Its methods protect local transitions; the manager coordinates effects. This compact sketch omits title and size-hint fields.

Client · compiled policy sketchRUST
#[derive(Debug)]
pub struct Client {
    window: WindowId,
    monitor: MonitorId,
    tags: TagSet,
    presentation: Presentation,
    urgent: bool,
}
Owns behavioris_visible_on, enter_fullscreen, leave_fullscreen
Per-monitor policy

Monitor

Owns a view and two distinct orders: tiling order and focus history. A monitor-wide floating layout does not change every client’s saved placement.

Monitor · compiled policy sketchRUST
#[derive(Debug)]
pub struct Monitor {
    screen: Rect,
    work_area: Rect,
    view: TagView,
    layouts: LayoutSelection,
    layout_order: Vec<ClientId>,
    focus_history: Vec<ClientId>,
    selected_client: Option<ClientId>,
}
InvariantEvery listed client belongs to this live monitor; lists have no duplicate IDs.
Transitions

TagView

Owns the current and previous nonempty tag sets. Selecting a view preserves the old one; toggling edits only the current view.

TagView · compiled transitionsRUST
/// Mirrors dwm's two tag slots. Toggle edits the current slot in place.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TagView {
    current: TagSet,
    previous: TagSet,
}
impl TagView {
    pub fn new(tags: TagSet) -> Self {
        Self {
            current: tags,
            previous: tags,
        }
    }
    pub fn current(&self) -> TagSet {
        self.current
    }
    pub fn select(&mut self, tags: TagSet) -> bool {
        if tags == self.current {
            return false;
        }
        self.previous = self.current;
        self.current = tags;
        true
    }
    pub fn toggle(&mut self, tags: TagSet) -> bool {
        match self.current.toggled(tags) {
            Some(next) => {
                self.current = next;
                true
            }
            None => false,
        }
    }
    pub fn restore_previous(&mut self) {
        std::mem::swap(&mut self.current, &mut self.previous);
    }
}
InvariantToggling away the last visible tag is rejected.

Turn an argument union into intent

C bindings pair a callback with a generic Arg. The callback must interpret the right union member. An enum makes each action’s input explicit.

Arg · current C representationC
typedef union {
	int i;
	unsigned int ui;
	float f;
	const void *v;
} Arg;

Read the action at the call site

Action · typed variantsRUST
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
    ViewTags(TagSet),
    ToggleView(TagSet),
    PreviousView,
    SetLayout(Layout),
}

PreviousView directly expresses the special behavior represented by a zero-valued argument in C.

Methods protect local rules. The manager coordinates shared work.

Client::enter_fullscreen() saves state. WindowManager updates geometry, focus and protocol properties. Pure helpers remain useful when an algorithm has no natural receiver.

Encode restoration in the type

Presentation · state carries restorationRUST
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Placement {
    Tiled,
    Floating,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NormalState {
    placement: Placement,
    geometry: Rect,
    border: BorderWidth,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Presentation {
    Normal(NormalState),
    Fullscreen {
        restore: NormalState,
        geometry: Rect,
    },
}

Fullscreen carries its restore state. Urgency stays independent; it is not another exclusive presentation mode.

Use named reconciliation

Orchestration · illustrativeRUST
fn handle_action(&mut self, action: Action)
    -> Result<(), WmError>
{
    if let Some(monitor) =
        self.state.apply_action(action)?
    {
        self.reconcile_monitor(monitor)?;
    }
    self.backend.flush()?;
    Ok(())
}
Why this shape reads well

The action names intent, the state method names the mutation, and reconciliation names the effects. The call path is visible without searching mutable globals.

A subtle dependency inside tiling

Ideal size→Apply size hints→Actual outer height→Next client’s Y position

C DWM advances the tile cursor using the size after constraints. Applying hints only after calculating every rectangle changes placement. Keep this dependency explicit and test it.

04
Build confidence before replacing the desktop

Port behavior in verifiable slices.

0–1

Baseline & model

Freeze C 6.8, record behavior, implement tags, geometry, state and pure layouts.

2–3

A working vertical slice

Connect, adopt a terminal, tile, focus, close and exit. Add rules and input.

4–5

Protocols & presentation

Fullscreen, dialogs, monitors, bar drawing and validated configuration.

6

Release candidate

Differential tests, real applications and recorded session trials pass.

Shared input

One scenario

Same windows, properties, input and configuration.

C DWM 6.8fresh X server
Rust WMfresh X server
Observable result

Compare behavior

Geometry, focus, visibility, stacking and protocol properties.

Start with characterization

The C archive has no automated suite. Convert transient.c into a bounded, asserted external scenario first.

Keep expectations independent

Use the actual C functions for policy fixtures and an external Xlib client for integration. Keep the baseline unchanged.

Prove the tests can fail

Inject wrong previous-view handling, fullscreen restore and hint placement. Each relevant assertion must catch its defect.

Four Rust policy-sketch tests currently pass. The runnable Rust WM and full X11 regression harness are planned work.

05
Keep the complete specification close

The full plan and source reference.

Use the guided sections above for the mental model. Expand these documents for the complete design decisions, compatibility matrix and test execution strategy.

Complete rewrite planExpand the full specification

DWM rewrite in Rust

The goal is a small X11 window manager with DWM's familiar behavior, implemented in readable, idiomatic Rust. Optimize for understanding ownership and behavior before minimizing line count. This is a rewrite guided by observable behavior, rather than a function-by-function translation.

The local C baseline is now DWM 6.8 in reference/dwm-6.8/, unpacked unchanged from the user-provided download and identified by archive and file checksums. No local AGENTS.md was found. Record any personal patches and configuration separately before implementation. No Rust-specific skill was found among the installed skills; the Rust API Guidelines are the design reference.

1. Scope and compatibility baseline

First release: X11, tagging, tiled/monocle/floating layouts, keyboard and mouse actions, rules, focus, fullscreen, multiple monitors, and a status bar. Preserve relevant ICCCM and EWMH behavior supported by the pinned baseline. Wayland, a plugin framework, runtime configuration reload, and arbitrary DWM patch compatibility are separate projects.

Write a compatibility matrix before coding. Cover startup with existing windows, transient/dialog handling, override-redirect windows, focus history, layout order, current/previous tag views, current/previous layouts, window size hints, fullscreen restoration, urgency, status text, bar clicks, spawning, and clean shutdown. Mark intentional changes explicitly.

DWM tags must remain sets: a client can belong to multiple tags and a monitor can show multiple tags. Do not replace this with one workspace per window. Record rule matching and precedence, geometry rounding, and focus behavior as examples that can become tests.

2. Architecture: one crate, a few explicit boundaries

Start with one Cargo package containing a library for testable policy and a thin executable. Avoid a workspace of tiny crates.

Source exampleTEXT
src/
  main.rs          startup, diagnostics, exit handling
  wm.rs            event orchestration and cross-object operations
  model/           clients, monitors, tags, geometry, layouts
  x11/             connection, atoms, properties, protocol requests
  input.rs         bindings, modifier handling, pointer interaction
  config.rs        typed defaults, bindings, rules, validation
  bar.rs           bar content, geometry, hit testing, drawing
  process.rs       spawning and child reaping

The normal path should be easy to follow:

Source exampleTEXT
X11 event → handler → policy/state update → X11 requests → flush

Keep X11-generated types near the protocol boundary. Extract typed window metadata before changing policy. Layout calculation should return placements without talking to X11. Introduce a small effects enum only if it makes event ordering and testing clearer; do not build an elaborate command bus.

Begin with a single-threaded synchronous event loop. A pointer drag is explicit interaction state processed by that loop, so window destruction and other events remain handled during a drag.

3. Types and the behavior they own

TypeResponsibility and useful methods
WindowId, ClientId, MonitorIdDistinguish X11 resources, managed clients, and monitor identities.
Tag, TagSetValidate configured tag bounds; contains, intersects, union, toggle.
TagViewMaintain current and previous nonempty selections; select, toggle, restore_previous.
Rect, Size, BorderWidthExpress geometry; intersection_area, outer_size, checked protocol conversion.
SizeHintsApply base size, increments, aspect and min/max constraints through constrain.
ClientOwn title, metadata, tags, placement and hints; is_visible_on, enter_fullscreen, leave_fullscreen.
MonitorOwn screen/work areas, client order, focus history, tag view and layout selection.
LayoutClosed enum for Tile, Monocle, Floating; placements returns computed geometry.
MasterRatioValidate finite supported ratios; expose intentional adjustment behavior.
ActionTyped variants such as ViewTags(TagSet), Focus(Direction), SetLayout(Layout), Spawn(CommandSpec).
RuleMatch window metadata and describe intended changes. Preserve baseline rule combination semantics.
X11BackendOwn connection and protocol resources; query metadata, configure, focus, publish properties.
WindowManagerCoordinate manage, unmanage, focus_client, move_client_to_monitor, dispatch_action.

Prefer methods when a type owns the behavior or invariant. A client may update its fullscreen state, but the manager coordinates resulting monitor layout and X11 requests. Free functions remain appropriate for stateless algorithms without a natural receiver.

Use descriptive names: master_count, master_ratio, selected_client, work_area. Replace the C Arg union and function-pointer bindings with Action variants carrying their actual argument types.

Use enums where alternatives exclude each other. Represent normal placement as tiled or floating and fullscreen as a state carrying the previous placement, geometry and border width. Keep independent facts such as urgency as independent fields. Do not combine unrelated flags into one enormous enum.

Keep fields private where mutation could break an invariant. Use Option<ClientId> for absent focus, rather than a zero identifier. Validate configuration at startup. A rule may have no tag override; a managed client and active view must have a nonempty valid tag set. Model DWM's zero-argument “previous view” operation as a distinct action.

Start with concrete types and exhaustive matches. Introduce a trait only for a real interchangeable behavior or a useful test boundary. A trait for every struct, broad dyn dispatch, or a generic backend threaded through every type would increase reading effort without a demonstrated benefit.

4. Ownership and collections

WindowManager owns the client store, monitor store and selected monitor. Monitors contain client IDs rather than references; clients identify their monitor by ID. Keep layout order and focus history as separate ID vectors, because they serve different behaviors.

Start with a typed ID and standard map plus a window-to-client lookup. Never use unordered map iteration to determine layout or focus. Allocate client IDs independently from reusable X11 window IDs. Add a generational arena only if stale references or complexity justify it.

Maintain these invariants through manager methods:

  • Every managed client belongs to exactly one live monitor.
  • Monitor orders contain no duplicate or missing client IDs.
  • Removing a client removes all membership and focus-history entries.
  • The selected client belongs to its monitor and is eligible under the focus policy.
  • Removing a monitor migrates its clients before deleting its state.

Avoid self-referential structures, mutable globals, and routine Rc<RefCell<_>>. Resolve borrowing conflicts by splitting fields, narrowing borrow scopes, or collecting IDs before mutation. Copying a small ID or geometry value is reasonable; cloning the entire manager to bypass borrowing is not.

5. X11 and rendering decisions

Use x11rb with its pure-Rust RustConnection as the first backend candidate. Pin the dependency after a small startup/event-loop spike. Check extension availability and provide a single-monitor fallback. Begin with monitor discovery compatible with the chosen DWM baseline; treat RandR hotplug support as an explicit addition if it exceeds that baseline.

For the bar, time-box a rendering spike comparing retaining Xft behind a narrow FFI wrapper with Cairo/Pango rendering uploaded to the X11 bar window. Decide using Unicode rendering, fallback fonts, dependency footprint, and code readability. Do not promise pixel-identical text rendering before this spike. Keep unsafe code, if needed, contained and documented.

Classify errors deliberately: another window manager owning root redirection is a startup failure; a client disappearing during a request is an expected race; a broken X connection ends the session. Surface checked request errors and asynchronous errors. Validate property types, formats and lengths. Do not use blanket error suppression or unwrap() on external input.

Use explicit shutdown for restoring windows and best-effort cleanup. Resource wrappers can use Drop for local cleanup, but shutdown requests that can fail should have an explicit path. Spawn argument vectors without implicit shell interpolation, and reap children.

6. Implementation milestones

StageDeliverableAcceptance gate
0. BaselinePinned upstream source, patch inventory, compatibility matrix and design decisions.Every required behavior has a scenario; scope differences are recorded.
1. ModelGeometry, tags/views, client/monitor state, layouts and typed actions.Pure tests cover invariants, tag transitions, layout ordering, borders and rounding.
2. X11 lifecycleConnect, claim root, scan existing windows, manage/map/unmanage, clean exit.Nested X server scenarios pass; competing WM and disappearing clients are handled.
3. Daily interactionFocus, bindings, rules, spawning, tile/monocle/floating, size hints, dragging.Deterministic scenarios match the pinned baseline; event handling continues during dragging.
4. Protocol and monitorsRelevant ICCCM/EWMH behavior, fullscreen, transients, monitor transfer and topology updates.Real applications, fullscreen restore, focus negotiation and monitor changes behave correctly.
5. Bar and configurationUnicode bar, status updates, click regions, validated typed config.Drawing and hit testing agree; malformed configuration has useful diagnostics.
6. Release candidateDocumentation, session entry, build instructions and compatibility report.Required tests pass; extended nested and real-session trials reveal no outstanding critical failures.

The first runnable milestone is startup → manage a terminal → tile it → focus it → close it → exit cleanly. Build that vertical slice before completing every abstraction.

Keep configuration in readable Rust initially: Config::default() plus explicit binding/rule constructors. Recompilation preserves DWM's customization approach. Add a file format only if users need it; keep policy and validation independent of the format.

7. Verification and readability gates

Use pure unit tests for tags, views, rules, fullscreen transitions, geometry and layouts. Add property tests where they catch meaningful invariant failures, such as arbitrary tag toggles or client removal sequences.

Run integration scenarios under Xephyr or Xvfb with small controlled X clients. Compare behavior with the pinned C baseline: window geometry, visibility, focus, stacking and published properties. Compare observable outcomes, not internal structures. Check hidden-window behavior carefully: changing visibility implementation can alter client lifecycle events.

Exercise real terminals, browsers, dialogs and fullscreen applications. Include keyboard mapping changes, lock modifiers, Unicode titles, rapid map/unmap/destroy sequences, negative monitor coordinates and a window disappearing during dragging.

Before accepting each milestone, run cargo fmt --check, appropriate tests and Clippy. Review readability separately: can a reader trace an event to its outcome, find who owns an invariant, and understand a public method without consulting distant global state? Explain protocol quirks in comments; avoid comments that merely restate the code.

Success means the agreed DWM behaviors pass the compatibility matrix, state invariants survive lifecycle races, and policy code can be understood without learning X11 request encoding. Performance should be measured for event handling and layout before adding caching or concurrency.

References

Detailed design and validation

The detailed documents use the downloaded 6.8 source as the reference. The sketches demonstrate policy only; they are not a runnable window manager.

Detailed Rust designExpand the full specification

Types and flow, grounded in DWM 6.8

Read alongside ../reference/dwm-6.8/dwm.c and the compilable ../examples/type_sketch.rs. The reference is unchanged. The Rust code is a policy sketch, not the beginning of a runnable WM. Unused-field warnings are expected because protocol integration, constructors and many operations are deliberately absent.

Ownership graph

The ownership diagram describes where values live; cross-object relationships use ID lookups, not Rust references. State has no X connection, so its transitions can be tested directly.

Ownership: values live in their owner; relationships use ID lookupsWindowManager owns State, X11Backend, Config, InteractionState owns Client and Monitor stores; indexes use typed IDsMonitor owns TagView, LayoutSelection and ordered client IDsClient owns tags, SizeHints and PresentationClient ↔ Monitor relationships use IDs, not borrowed references
Ownership: values live in their owner; relationships use ID lookups

screen includes the full monitor; work_area excludes the bar. Fullscreen uses screen; tiling uses work_area. Rect coordinates are signed because monitors may sit left of the origin. Use wider intermediates for intersection and border arithmetic; convert to X11's wire ranges only at the backend boundary.

Preserve two independent lists from C Client.next and Client.snext: layout order and focus history. New clients are prepended in the baseline. HashMap traversal must never decide either order. monitor_order also makes keyboard monitor traversal deterministic. Fresh internal client IDs prevent accidental confusion with reused X window IDs; remove pending interaction state when a client disappears.

A production State constructor establishes at least one monitor and validates tag sets against one configured mask. The sketch's TagSet construction enforces valid bits, but values created with different TagMask instances can still be mixed: production accepts only the startup configuration's mask and validates configuration/actions at that boundary. This is a runtime invariant, not a claim of compile-time proof.

Behavior belongs where its inputs and invariants live

C locationRust responsibilityWhy
view at dwm.c:2054; toggleview:1754TagView::select, toggle, restore_previousOwn both view slots and nonempty-view behavior.
setlayout:1511LayoutSelection::select, restore_previousPreserve both layout slots without pointer comparisons.
setfullscreen:1483Client transition, followed by manager reconciliationSaved client state is local; arranging and X requests involve other objects.
applysizehints:314SizeHints::constrainReturn geometry instead of four mutable output parameters.
tile:1688; monocle:1114Layout::placements plus hint-aware geometry calculationPure computation is inspectable and testable.
applyrules:278Rule::matches, ordered rule applicationMatch metadata locally; choose monitor/tags through state.
manage:1032; unmanage:1779Manager lifecycle orchestration plus State membership methodsMembership spans client store, lookup and monitor lists.
focus:789State selects a candidate; backend performs focus negotiationFocus history and X11 input protocol are separate concerns.
showhide:1630Manager visibility reconciliationBaseline moves hidden clients offscreen rather than unmapping them.

Do not move every C function into one giant impl WindowManager. Use methods for meaningful receivers and keep plain algorithm helpers when no receiver is natural. Do not make Client::focus() reach into global monitor state or issue X11 requests.

Event and action graph

Event flow; pointer interaction uses the same loopX11 event → exhaustive handler matchRead metadata OR resolve binding to typed ActionState lifecycle update OR State.apply_actionReconcile affected monitor: visibility and focus candidateCalculate layout with size hintsSend visibility, geometry, stacking, focus and bar requestsHandle errors and flush → next event
Event flow; pointer interaction uses the same loop

Reconciliation is a named sequence with explicit ordering, not a magical full-state diff. Map/startup operations may need requests before and after insertion. Retain those phases instead of forcing every lifecycle operation through the same generic pipeline. Release mutable state borrows before protocol I/O; collect small IDs/placements, not clones of the entire state.

An illustrative integration outline (not part of the compiled sketch):

Source exampleRUST
fn handle_action(&mut self, action: Action) -> Result<(), WmError> {
    if let Some(monitor) = self.state.apply_action(action)? {
        self.reconcile_monitor(monitor)?;
    }
    self.backend.flush()?;
    Ok(())
}

The sketch's PolicyError::MissingMonitor must become a structured WmError conversion. Recover an expected client-disappearance race at the lifecycle boundary; do not silently swallow all protocol errors. State and server are not one atomic transaction: partial requests require cleanup or another reconciliation.

Managing a new window

The baseline maprequest queries attributes, ignores override-redirect, then calls manage only for unknown windows. manage performs an ordered mix of metadata, state and protocol work. The Rust implementation should make those phases explicit.

New-window lifecycle; vanished or already-managed clients branch out before insertionApplication requests mapping → X server sends MapRequestRead attributes and metadata; skip unmanaged override-redirectResolve transient parent or apply ordered rulesConstruct client; configure border, events and button grabsInsert membership; publish client list and NormalStateArrange → map window → negotiate focus → flush
New-window lifecycle; vanished or already-managed clients branch out before insertion

A known managed transient inherits its parent's monitor and tags before ordinary rules; transient/fixed-size behavior affects floating placement. Fullscreen metadata also participates during management. Track these cases as separate tests rather than reducing all metadata to a single is_floating rule.

Rules use substring matches. Matching rules accumulate tag bits, while later matches overwrite floating status and valid monitor choice. An invalid monitor target does not erase an earlier valid choice. No resulting tag bits means use the chosen monitor's active view. An idiomatic rewrite must preserve this order explicitly.

Fullscreen state graph

Fullscreen transitions are idempotent; restoration survives repeated requestsNormal: tiled or floating, with geometry and borderEnter fullscreen: save NormalState; use screen geometryRepeated enter: keep the original restore stateLeave fullscreen: restore NormalState, then arrangeRepeated leave: no change
Fullscreen transitions are idempotent; restoration survives repeated requests

Use Presentation::Fullscreen { restore: NormalState, geometry: Rect } so saved state exists exactly when needed. Keep urgency and input-focus eligibility separate. Floating layout at monitor level does not mean every client's persistent placement should become floating.

For monitor resizing while fullscreen, update current fullscreen geometry without overwriting the saved normal state. On leave, a tiled client may immediately receive new tiled geometry during arrangement; the saved rectangle is not a promise of its final tiled position.

Layout flow and code quality

Hint-aware layout: each constrained height affects the following clientMonitor layout_orderFilter visible clients eligible for tilingCompute widths and remaining-height sharesConstrain each size using its hints and borderAdvance cursor using actual outer height → next clientConfigure placements through X11Backend
Hint-aware layout: each constrained height affects the following client

The C tile advances offsets using actual HEIGHT(c) after resize, which can apply size hints. Computing all ideal rectangles and constraining them only afterward can produce different placement. Either integrate pure hint calculation into the layout pass or return placements iteratively with actual constrained sizes. Preserve float ratio rounding/truncation before considering a different numeric representation.

Readable code should express this dependency in names such as remaining_height, clients_remaining, constrained_size, and outer_height. Use explicit loops where each result feeds the next iteration. A long iterator chain would hide that dependency.

The standalone sketch below supplies working definitions for validated tags, view transitions, IDs, presentation state, typed actions and the ownership container. It deliberately omits layout algorithms, X11, rules and bar drawing. It is a design example, not evidence of WM compatibility.

Compilable Rust examples

These excerpts are copied from examples/type_sketch.rs; run its tests using the commands at the end.

Tag view owns its transitions

Source exampleRUST
/// Mirrors dwm's two tag slots. Toggle edits the current slot in place.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TagView {
    current: TagSet,
    previous: TagSet,
}
impl TagView {
    pub fn new(tags: TagSet) -> Self {
        Self {
            current: tags,
            previous: tags,
        }
    }
    pub fn current(&self) -> TagSet {
        self.current
    }
    pub fn select(&mut self, tags: TagSet) -> bool {
        if tags == self.current {
            return false;
        }
        self.previous = self.current;
        self.current = tags;
        true
    }
    pub fn toggle(&mut self, tags: TagSet) -> bool {
        match self.current.toggled(tags) {
            Some(next) => {
                self.current = next;
                true
            }
            None => false,
        }
    }
    pub fn restore_previous(&mut self) {
        std::mem::swap(&mut self.current, &mut self.previous);
    }
}

Client owns fullscreen state

Source exampleRUST
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Presentation {
    Normal(NormalState),
    Fullscreen {
        restore: NormalState,
        geometry: Rect,
    },
}
#[derive(Debug)]
pub struct Client {
    window: WindowId,
    monitor: MonitorId,
    tags: TagSet,
    presentation: Presentation,
    urgent: bool,
}
impl Client {
    pub fn is_visible_on(&self, view: &TagView) -> bool {
        self.tags.intersects(view.current())
    }
    /// Repeated fullscreen requests do not overwrite the saved state.
    pub fn enter_fullscreen(&mut self, screen: Rect) -> bool {
        match self.presentation {
            Presentation::Normal(restore) => {
                self.presentation = Presentation::Fullscreen {
                    restore,
                    geometry: screen,
                };
                true
            }
            Presentation::Fullscreen { .. } => false,
        }
    }
    pub fn leave_fullscreen(&mut self) -> bool {
        match self.presentation {
            Presentation::Fullscreen { restore, .. } => {
                self.presentation = Presentation::Normal(restore);
                true
            }
            Presentation::Normal(_) => false,
        }
    }
}

Typed action dispatch returns affected monitor

Source exampleRUST
impl State {
    /// Returns the monitor whose focus/layout/bar must be reconciled.
    /// No X11 calls occur while this method holds a mutable monitor borrow.
    pub fn apply_action(&mut self, action: Action) -> Result<Option<MonitorId>, PolicyError> {
        let monitor = self
            .monitors
            .get_mut(&self.selected_monitor)
            .ok_or(PolicyError::MissingMonitor)?;
        let changed = match action {
            Action::ViewTags(tags) => monitor.view.select(tags),
            Action::ToggleView(tags) => monitor.view.toggle(tags),
            Action::PreviousView => {
                monitor.view.restore_previous();
                true
            }
            Action::SetLayout(layout) => {
                monitor.layouts.select(layout);
                true
            }
        };
        Ok(changed.then_some(self.selected_monitor))
    }
}
Source exampleSH
rustfmt --edition 2021 --check examples/type_sketch.rs
rustc --edition=2021 --test examples/type_sketch.rs -o /tmp/dwm-type-tests
/tmp/dwm-type-tests

Four tests exercise tag validation, previous-view semantics, fullscreen restoration/idempotence, and layout selection. They validate only the sketches. Full regression validation is described in REGRESSION_PLAN.md.

Full regression strategyExpand the full specification

Regression validation and migrating C tests

Existing tests and limits

DWM 6.8 has no automated test suite, test directory, assertions or Makefile test target. transient.c is a manual Xlib exerciser with no assertions or bounded termination. There is therefore no suite to directly translate into Rust tests.

Establish executable characterization tests against the frozen C implementation first. Then run identical fixtures and external scenarios against Rust. Passing establishes compatibility for covered scenarios, not proof that every possible application/event ordering is regression-free.

The current repository has four standalone Rust sketch tests only. The C oracle, X11 clients, runners, CI and Rust WM below are planned deliverables. No WM regression result is claimed yet.

Shared test architecture

Source exampleTEXT
tests/fixtures/                 inputs and explicit expected observations
tests/scenarios/                language-independent external scenarios
tests/support/xclient.c         controllable Xlib client
tests/support/c_oracle.c        test-only access to actual C policy
tests/support/reference.patch  minimal observation hooks if required
tests/runner/                   isolated-server lifecycle and comparison
build/reference/               disposable baseline build copy
artifacts/regressions/          logs, snapshots, seeds and failure images
Differential validation: the same scenario drives both implementationsShared scenario + identical configurationRun C DWM and Rust WM on separate fresh X serversCapture logical geometry, visibility, focus, stacking, propertiesNormalize only resource IDs and timestampsCompare → pass / reviewed difference / failure reproduction
Differential validation: the same scenario drives both implementations

Keep reference sources unchanged and retain their license. Build copies under build/reference/; record source hashes, compiler options, scenario configuration and server/font versions. Test-hook patches need review: changing the oracle can hide a regression.

1. Build a reproducible C baseline

Verify SHA256SUMS from the reference directory, copy the source into build/reference/, and generate a fixed test config.h there. Match Rust tag count, bindings, borders, master ratio/count, resize hints, rules, bar position and fonts.

The C build needs a compiler, Xlib, Xft, fontconfig, FreeType and Xinerama development files. Build using the original Makefile in the copy; build the manual client separately if useful:

Source exampleSH
make -C build/reference
cc build/reference/transient.c -o build/reference/transient -lX11

These are future commands after creating the build copy. Do not install the baseline or run it on the desktop display. A runner owns a dedicated nested display. Xvfb and Xephyr were not found on PATH in this planning pass; no C build or nested-X suite was run. Missing environment support must be reported, not counted as passing.

2. Port the intent of transient.c

Leave the original intact. Create a controlled client in tests/support/xclient.c reproducing these operations:

  1. Create a parent titled floating, size 400×400, with min/max size hints fixed at 400×400.
  2. Wait for mapping and observed expected geometry.
  3. Move its tag/monitor through normal bindings, then create a 100×100 child with WM_TRANSIENT_FOR=parent.
  4. Wait for mapping and settled lifecycle behavior; assert floating placement and inherited tag/monitor behavior through visible outcomes.
  5. Switch tags away and back; verify parent and child leave/return to the visible area without recreation.
  6. Destroy child and parent; verify managed-list/focus cleanup and bounded exit.

Use a pipe/socket command protocol with acknowledgements and logical window names. Replace the original five-second sleep with event-driven conditions and deadlines. The tags are internal: do not infer them from _NET_WM_DESKTOP, which is not the baseline's tagging model. Use a policy hook for exact internal assertions when necessary.

Run the scenario against C first and review its observations, then run unchanged against Rust. Add cases for an unmanaged parent, parent destruction during metadata lookup, a non-fixed parent, and a transient hint changed after mapping. Follow actual C handler behavior instead of assuming all property changes reapply rules.

The test client can stay in C. Keeping an independent Xlib driver avoids testing the Rust backend solely through itself.

3. Characterize and port policy tests

Target view, toggleview, setlayout, rule precedence, fullscreen, size hints and tiling first. Use actual C functions as the oracle, not a second rewritten algorithm.

A test-only harness can rename main, include the copied dwm.c in its translation unit, restore main, construct Client/Monitor globals, call static functions and serialize results. This alone does not make the functions pure: they may issue X requests. Use a nested display or narrow documented hooks to suppress unrelated drawing and capture requests. Keep real hint application and cursor advancement. Check harness results against external C scenarios to ensure hooks preserve behavior.

Feed the same fixture schema to C and Rust:

Source exampleJSON
{
  "case": "view-toggle-keeps-other-slot",
  "tag_count": 9,
  "initial_slots": [1, 1],
  "operations": [{"view": 2}, {"toggle_view": 1}, {"previous_view": true}],
  "expected_current": 1,
  "expected_previous": 3
}

Fixtures retain both view/layout slots, selected slot, ordering, geometry, hints and relevant flags. Compare logical values, not raw memory or pointer addresses. Translate stable pure assertions to Rust #[test] functions while retaining fixture IDs and expected outputs.

If later patches include real C tests: inventory inputs/assertions/dependencies/licenses, run them on C before translation, port pure assertions, keep black-box tests shared, and translate pointer identity to logical IDs. Existing failures must be distinguished from new migration failures.

4. Required comparison matrix

AreaCases and assertions
TagsMultiple/all tags, same-view no-op, previous alternation, empty toggle rejection; both slots, visible/offscreen clients and focus.
Layouts0/1/many clients, master count 0/1/above count, odd dimensions, borders, hints/aspect/increments; exact rectangles, order and both layout slots.
FocusMap/cycle order, hidden selected window, pointer entry, urgency, never-focus/WM_TAKE_FOCUS; input focus and active-window property.
RulesSubstring matches, tag union, later floating overwrite, valid then invalid monitor target, no match; monitor/tags/placement.
LifecycleStartup scan, duplicate map, override-redirect, real/synthetic unmap, destruction races; client list, WM_STATE, restored borders and no stale membership.
FullscreenTiled/floating, repeated add/remove/toggle, resize and destruction; zero border, full-screen geometry, restored normal state and rearrangement.
MonitorsNegative origin, transfer/cycling order, removal migration, resize, missing extension; membership, geometry, focus and bar area.
InputLock/NumLock, mapping changes, grabs, bar click edges, move/resize interrupted by destroy; correct actions and no stuck pointer grabs.
BarUnicode/fallback, title/status, urgency and selection, show/hide; semantic content and hit regions, with separate visual checks.
Process/exitArgument vectors, failed exec, child reaping, competing WM, clean shutdown; exit status, diagnostics, restored windows and no zombies.

Match geometry exactly under equal configuration. The C layout uses actual size-hint-constrained height to place the next client and truncates float-derived master width; a general pixel tolerance can hide real defects. Use a fixed bar height for policy tests if font metrics differ, and validate typography separately. Intended differences require explicit approval and specific expectations.

5. Deterministic X11 execution

Use fresh separate servers for C and Rust with identical screen, extensions, locale, fonts and keyboard settings. Wait for WM readiness through ownership/root-property observations rather than fixed sleeps.

Normalize server-specific XIDs to logical names and remove irrelevant timestamps. Preserve geometry, borders, map state, logical stacking, focus and protocol contents. Map state alone cannot prove visibility: C hides clients by moving them offscreen while keeping them mapped.

Drive bindings with real input injection such as XTEST where supported; synthetic XSendEvent is not universally equivalent. Script metadata/protocol changes through the support client. Observer-side XSync does not guarantee the WM processed another connection's events. Use condition-based waits with deadlines, stable observations and, when required, a narrow test-only acknowledgement. Retain traces on timeout.

Verify the server actually supports the multi-monitor topology under test. Xvfb screen configuration is not automatically equivalent to Xinerama monitor changes. Use a verified Xephyr/Xorg environment for real topology tests and a protocol fixture backend for policy tests. Report unsupported cases explicitly; required scenarios cannot silently skip.

6. Check that the tests detect regressions

On a disposable implementation copy, inject these defects and require the relevant test to fail:

  • Toggle overwrites previous view.
  • Layout order is substituted for focus history.
  • Hints are applied only after computing all rectangles.
  • Repeated fullscreen entry overwrites restore state.
  • Hidden windows are unmapped.
  • Removal leaves a stale ID in one list.

A passing test after its intended defect means its assertions need strengthening. Add a permanent fixture for every bug fixed during migration.

After deterministic tests pass, use seeded random lifecycle/action sequences. Compare defined policy outcomes to C and assert independent Rust invariants. Save seeds and minimize failures into short fixtures. C is a compatibility oracle, not a correctness proof: inherited defects or undefined behavior need a reviewed compatibility decision, not blind reproduction.

7. Milestones and release gate

  1. Build C and automate the transient scenario before implementing complex Rust behavior.
  2. Establish fixtures for views, layouts/hints, rules and fullscreen.
  3. Port policy incrementally and run fixtures/invariants with each change.
  4. Add shared lifecycle/input/protocol integration once the Rust vertical slice runs.
  5. Add real application trials and verified multi-monitor cases.
  6. Require formatting/lint checks, all required differential scenarios, race cases and recorded real-session trials before release.

Future Cargo gates: cargo fmt --check, cargo clippy --all-targets -- -D warnings, and cargo test. The standalone sketch is not a Cargo WM project yet. Implement a runner with explicit executable/scenario arguments so the same scenarios select C or Rust.

Publish pass/fail/skip per scenario, baseline identity, environment, intended differences and failure artifacts. Failures block promotion until fixed or explicitly reviewed. Never automatically accept new golden output.

Checks completed now

Both archives have identical SHA-256; every extracted regular file matches its archive bytes; all 13 reference file checksums pass. Four standalone Rust tests compile and pass on Rust 1.82.0. No automated C tests existed to port. No C build, X11 differential validation or WM regression suite has run.

Complete compilable Rust policy sketchAll types and four focused tests
examples/type_sketch.rs · complete sourceRUST
//! Standalone policy sketch, not a window manager or protocol implementation.
//! Run: rustc --edition=2021 --test examples/type_sketch.rs -o /tmp/dwm-type-tests
use std::collections::HashMap;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WindowId(u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ClientId(u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MonitorId(u64);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TagSet(u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TagMask(u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TagError {
    InvalidCount,
    Empty,
    OutsideConfiguredTags,
}
impl TagMask {
    pub fn new(count: u8) -> Result<Self, TagError> {
        if !(1..=31).contains(&count) {
            return Err(TagError::InvalidCount);
        }
        Ok(Self((1u32 << count) - 1))
    }
    pub fn set(self, bits: u32) -> Result<TagSet, TagError> {
        if bits == 0 {
            return Err(TagError::Empty);
        }
        if bits & !self.0 != 0 {
            return Err(TagError::OutsideConfiguredTags);
        }
        Ok(TagSet(bits))
    }
}
impl TagSet {
    pub fn intersects(self, other: Self) -> bool {
        self.0 & other.0 != 0
    }
    pub fn toggled(self, other: Self) -> Option<Self> {
        let bits = self.0 ^ other.0;
        (bits != 0).then_some(Self(bits))
    }
}

/// Mirrors dwm's two tag slots. Toggle edits the current slot in place.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TagView {
    current: TagSet,
    previous: TagSet,
}
impl TagView {
    pub fn new(tags: TagSet) -> Self {
        Self {
            current: tags,
            previous: tags,
        }
    }
    pub fn current(&self) -> TagSet {
        self.current
    }
    pub fn select(&mut self, tags: TagSet) -> bool {
        if tags == self.current {
            return false;
        }
        self.previous = self.current;
        self.current = tags;
        true
    }
    pub fn toggle(&mut self, tags: TagSet) -> bool {
        match self.current.toggled(tags) {
            Some(next) => {
                self.current = next;
                true
            }
            None => false,
        }
    }
    pub fn restore_previous(&mut self) {
        std::mem::swap(&mut self.current, &mut self.previous);
    }
}

/// Coordinates may be negative. Construction checks positive dimensions.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rect {
    x: i32,
    y: i32,
    width: u32,
    height: u32,
}
impl Rect {
    pub fn new(x: i32, y: i32, width: u32, height: u32) -> Option<Self> {
        (width > 0 && height > 0).then_some(Self {
            x,
            y,
            width,
            height,
        })
    }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BorderWidth(u16);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Placement {
    Tiled,
    Floating,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NormalState {
    placement: Placement,
    geometry: Rect,
    border: BorderWidth,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Presentation {
    Normal(NormalState),
    Fullscreen {
        restore: NormalState,
        geometry: Rect,
    },
}
#[derive(Debug)]
pub struct Client {
    window: WindowId,
    monitor: MonitorId,
    tags: TagSet,
    presentation: Presentation,
    urgent: bool,
}
impl Client {
    pub fn is_visible_on(&self, view: &TagView) -> bool {
        self.tags.intersects(view.current())
    }
    /// Repeated fullscreen requests do not overwrite the saved state.
    pub fn enter_fullscreen(&mut self, screen: Rect) -> bool {
        match self.presentation {
            Presentation::Normal(restore) => {
                self.presentation = Presentation::Fullscreen {
                    restore,
                    geometry: screen,
                };
                true
            }
            Presentation::Fullscreen { .. } => false,
        }
    }
    pub fn leave_fullscreen(&mut self) -> bool {
        match self.presentation {
            Presentation::Fullscreen { restore, .. } => {
                self.presentation = Presentation::Normal(restore);
                true
            }
            Presentation::Normal(_) => false,
        }
    }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Layout {
    Tile,
    Monocle,
    Floating,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LayoutSelection {
    current: Layout,
    previous: Layout,
}
impl LayoutSelection {
    pub fn select(&mut self, layout: Layout) {
        if layout != self.current {
            std::mem::swap(&mut self.current, &mut self.previous);
        }
        self.current = layout;
    }
    pub fn restore_previous(&mut self) {
        std::mem::swap(&mut self.current, &mut self.previous);
    }
}
#[derive(Debug)]
pub struct Monitor {
    screen: Rect,
    work_area: Rect,
    view: TagView,
    layouts: LayoutSelection,
    layout_order: Vec<ClientId>,
    focus_history: Vec<ClientId>,
    selected_client: Option<ClientId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
    ViewTags(TagSet),
    ToggleView(TagSet),
    PreviousView,
    SetLayout(Layout),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PolicyError {
    MissingMonitor,
}
#[derive(Debug)]
pub struct State {
    clients: HashMap<ClientId, Client>,
    monitors: HashMap<MonitorId, Monitor>,
    monitor_order: Vec<MonitorId>,
    window_to_client: HashMap<WindowId, ClientId>,
    selected_monitor: MonitorId,
}
impl State {
    /// Returns the monitor whose focus/layout/bar must be reconciled.
    /// No X11 calls occur while this method holds a mutable monitor borrow.
    pub fn apply_action(&mut self, action: Action) -> Result<Option<MonitorId>, PolicyError> {
        let monitor = self
            .monitors
            .get_mut(&self.selected_monitor)
            .ok_or(PolicyError::MissingMonitor)?;
        let changed = match action {
            Action::ViewTags(tags) => monitor.view.select(tags),
            Action::ToggleView(tags) => monitor.view.toggle(tags),
            Action::PreviousView => {
                monitor.view.restore_previous();
                true
            }
            Action::SetLayout(layout) => {
                monitor.layouts.select(layout);
                true
            }
        };
        Ok(changed.then_some(self.selected_monitor))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn toggling_view_preserves_the_other_slot_and_rejects_empty() {
        let mask = TagMask::new(9).unwrap();
        let first = mask.set(1).unwrap();
        let second = mask.set(2).unwrap();
        let mut view = TagView::new(first);
        assert!(view.select(second));
        assert!(view.toggle(first));
        assert_eq!(view.current(), mask.set(3).unwrap());
        assert!(!view.toggle(mask.set(3).unwrap()));
        view.restore_previous();
        assert_eq!(view.current(), first);
        view.restore_previous();
        assert_eq!(view.current(), mask.set(3).unwrap());
    }
    #[test]
    fn fullscreen_is_idempotent_and_restores_floating_geometry_and_border() {
        let original = NormalState {
            placement: Placement::Floating,
            geometry: Rect::new(-100, 30, 400, 300).unwrap(),
            border: BorderWidth(2),
        };
        let mut client = Client {
            window: WindowId(10),
            monitor: MonitorId(1),
            tags: TagMask::new(9).unwrap().set(1).unwrap(),
            presentation: Presentation::Normal(original),
            urgent: false,
        };
        assert!(client.enter_fullscreen(Rect::new(-1920, 0, 1920, 1080).unwrap()));
        assert!(!client.enter_fullscreen(Rect::new(0, 0, 800, 600).unwrap()));
        assert!(client.leave_fullscreen());
        assert_eq!(client.presentation, Presentation::Normal(original));
        assert!(!client.leave_fullscreen());
    }
    #[test]
    fn selecting_current_layout_does_not_replace_previous_layout() {
        let mut selection = LayoutSelection {
            current: Layout::Tile,
            previous: Layout::Monocle,
        };
        selection.select(Layout::Tile);
        selection.restore_previous();
        assert_eq!(selection.current, Layout::Monocle);
    }
    #[test]
    fn tag_boundaries_are_validated() {
        assert_eq!(TagMask::new(0), Err(TagError::InvalidCount));
        let mask = TagMask::new(9).unwrap();
        assert_eq!(mask.set(0), Err(TagError::Empty));
        assert_eq!(mask.set(1 << 9), Err(TagError::OutsideConfiguredTags));
        assert!(mask.set(1 << 8).is_ok());
    }
}