Your application
Creates windows, sets hints and renders its content.
terminal · browser · dialogX11 provides the machinery. DWM chooses how windows behave. The Rust rewrite makes those decisions and their ownership explicit.
An application and DWM each connect to an X server. DWM is itself an X client with permission to manage other windows.
Creates windows, sets hints and renders its content.
terminal · browser · dialogTracks window resources and geometry, routes input, and handles drawing requests.
window IDs · root window · event queuesChooses placement, visible tags, focus and stacking. Draws its own bar.
tile · focus · view · arrangeProtocol 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 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 recordThe 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.
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.
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.
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 constraintsWM_TRANSIENT_FORWhich window a dialog belongs toWM_PROTOCOLSSupported messages such as graceful close_NET_WM_STATEExtended states such as fullscreenICCCM and EWMH specify conventions between clients and WMs. The rewrite targets the subset DWM 6.8 supports.
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.
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.
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.
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.
The server resource exists, but DWM does not yet have a managed-client record.
application → X serverDWM receives MapRequest. It checks attributes and ignores override-redirect or already-managed cases.
run → maprequestRead title, hints and transient parent. Inherit a managed parent’s tags/monitor, or apply ordered rules. Decide floating placement.
manage → ClientInsert into the monitor’s layout order and focus history. Calculate visible window geometry and apply hints.
attach → arrange → tile → resizeConfigure geometry and borders, publish state, map the window and negotiate focus. The application draws its own content.
DWM → X server → applicationThe X server delivers the matching key event to DWM. Normalize lock modifiers and look up its binding.
keypress → viewview changes the selected tag slot and preserves the other slot. Clients keep their existing tag memberships.
TagView::select(tags)Choose an eligible visible client. C DWM moves hidden windows offscreen while leaving them mapped.
focus → arrange → showhideApply the current layout, update stacking and redraw the bar.
arrangemon → tile/monocle → restackThe application sends an EWMH state message through the X server. Validate its format and target.
clientmessage → setfullscreenSave placement, geometry and border once. A repeated entry must not overwrite the saved state.
Client::enter_fullscreenSet zero border, resize to the monitor’s full area, publish fullscreen state and raise the window.
_NET_WM_STATE → resizeclientRecover saved normal state. A tiled client can then receive newly calculated tiled geometry.
Client::leave_fullscreen → arrangeAn unknown or already-removed window must be handled safely.
destroynotify → wintoclientClear lookup, layout membership and focus-history entries. Cancel an active drag for that client.
unmanage → detach → detachstackChoose remaining focus, update the client list and rearrange. Do not send restoration requests to a destroyed window.
focus → updateclientlist → arrangeConfigureRequestApplication asks for geometryRespect floating requests or enforce managed layoutPropertyNotifyMetadata changesUpdate the relevant title, hints or stateExposeA region needs repaintingRedraw DWM’s barMappingNotifyKeyboard mapping changesRefresh key translation and grabsRust structs represent the decisions DWM remembers between events. They do not replace the server’s window resources.
WindowManagerevent orchestrationStatepolicy and invariantsMonitorview · layouts · client ID listsClienttags · hints · presentationRelationships use IDs. No borrowed pointer cycle.
X11Backendconnection and protocol resourcesRustConnection + Atomsqueries · requests · errorsMutable policy borrows end before protocol I/O.
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;
};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.
A server window ID can be reused. A managed-client ID names this local lifetime. Use distinct types so the compiler catches accidental mixing.
#[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);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.
#[derive(Debug)]
pub struct Client {
window: WindowId,
monitor: MonitorId,
tags: TagSet,
presentation: Presentation,
urgent: bool,
}is_visible_on, enter_fullscreen, leave_fullscreenOwns a view and two distinct orders: tiling order and focus history. A monitor-wide floating layout does not change every client’s saved placement.
#[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>,
}Owns the current and previous nonempty tag sets. Selecting a view preserves the old one; toggling edits only the current view.
/// 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);
}
}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.
typedef union {
int i;
unsigned int ui;
float f;
const void *v;
} Arg;#[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.
Client::enter_fullscreen() saves state. WindowManager updates geometry, focus and protocol properties. Pure helpers remain useful when an algorithm has no natural receiver.
#[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.
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 action names intent, the state method names the mutation, and reconciliation names the effects. The call path is visible without searching mutable globals.
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.
Freeze C 6.8, record behavior, implement tags, geometry, state and pure layouts.
Connect, adopt a terminal, tile, focus, close and exit. Add rules and input.
Fullscreen, dialogs, monitors, bar drawing and validated configuration.
Differential tests, real applications and recorded session trials pass.
Same windows, properties, input and configuration.
C DWM 6.8fresh X serverRust WMfresh X serverGeometry, focus, visibility, stacking and protocol properties.
The C archive has no automated suite. Convert transient.c into a bounded, asserted external scenario first.
Use the actual C functions for policy fixtures and an external Xlib client for integration. Keep the baseline unchanged.
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.
Use the guided sections above for the mental model. Expand these documents for the complete design decisions, compatibility matrix and test execution strategy.
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.
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.
Start with one Cargo package containing a library for testable policy and a thin executable. Avoid a workspace of tiny crates.
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 reapingThe normal path should be easy to follow:
X11 event → handler → policy/state update → X11 requests → flushKeep 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.
| Type | Responsibility and useful methods |
|---|---|
WindowId, ClientId, MonitorId | Distinguish X11 resources, managed clients, and monitor identities. |
Tag, TagSet | Validate configured tag bounds; contains, intersects, union, toggle. |
TagView | Maintain current and previous nonempty selections; select, toggle, restore_previous. |
Rect, Size, BorderWidth | Express geometry; intersection_area, outer_size, checked protocol conversion. |
SizeHints | Apply base size, increments, aspect and min/max constraints through constrain. |
Client | Own title, metadata, tags, placement and hints; is_visible_on, enter_fullscreen, leave_fullscreen. |
Monitor | Own screen/work areas, client order, focus history, tag view and layout selection. |
Layout | Closed enum for Tile, Monocle, Floating; placements returns computed geometry. |
MasterRatio | Validate finite supported ratios; expose intentional adjustment behavior. |
Action | Typed variants such as ViewTags(TagSet), Focus(Direction), SetLayout(Layout), Spawn(CommandSpec). |
Rule | Match window metadata and describe intended changes. Preserve baseline rule combination semantics. |
X11Backend | Own connection and protocol resources; query metadata, configure, focus, publish properties. |
WindowManager | Coordinate 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.
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:
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.
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.
| Stage | Deliverable | Acceptance gate |
|---|---|---|
| 0. Baseline | Pinned upstream source, patch inventory, compatibility matrix and design decisions. | Every required behavior has a scenario; scope differences are recorded. |
| 1. Model | Geometry, tags/views, client/monitor state, layouts and typed actions. | Pure tests cover invariants, tag transitions, layout ordering, borders and rounding. |
| 2. X11 lifecycle | Connect, claim root, scan existing windows, manage/map/unmanage, clean exit. | Nested X server scenarios pass; competing WM and disappearing clients are handled. |
| 3. Daily interaction | Focus, bindings, rules, spawning, tile/monocle/floating, size hints, dragging. | Deterministic scenarios match the pinned baseline; event handling continues during dragging. |
| 4. Protocol and monitors | Relevant ICCCM/EWMH behavior, fullscreen, transients, monitor transfer and topology updates. | Real applications, fullscreen restore, focus negotiation and monitor changes behave correctly. |
| 5. Bar and configuration | Unicode bar, status updates, click regions, validated typed config. | Drawing and hit testing agree; malformed configuration has useful diagnostics. |
| 6. Release candidate | Documentation, 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.
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.
The detailed documents use the downloaded 6.8 source as the reference. The sketches demonstrate policy only; they are not a runnable window manager.
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.
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.
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.
| C location | Rust responsibility | Why |
|---|---|---|
view at dwm.c:2054; toggleview:1754 | TagView::select, toggle, restore_previous | Own both view slots and nonempty-view behavior. |
setlayout:1511 | LayoutSelection::select, restore_previous | Preserve both layout slots without pointer comparisons. |
setfullscreen:1483 | Client transition, followed by manager reconciliation | Saved client state is local; arranging and X requests involve other objects. |
applysizehints:314 | SizeHints::constrain | Return geometry instead of four mutable output parameters. |
tile:1688; monocle:1114 | Layout::placements plus hint-aware geometry calculation | Pure computation is inspectable and testable. |
applyrules:278 | Rule::matches, ordered rule application | Match metadata locally; choose monitor/tags through state. |
manage:1032; unmanage:1779 | Manager lifecycle orchestration plus State membership methods | Membership spans client store, lookup and monitor lists. |
focus:789 | State selects a candidate; backend performs focus negotiation | Focus history and X11 input protocol are separate concerns. |
showhide:1630 | Manager visibility reconciliation | Baseline 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.
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):
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.
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.
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.
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.
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.
These excerpts are copied from examples/type_sketch.rs; run its tests using the commands at the end.
/// 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);
}
}#[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,
}
}
}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))
}
}rustfmt --edition 2021 --check examples/type_sketch.rs
rustc --edition=2021 --test examples/type_sketch.rs -o /tmp/dwm-type-tests
/tmp/dwm-type-testsFour 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.
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.
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 imagesKeep 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.
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:
make -C build/reference
cc build/reference/transient.c -o build/reference/transient -lX11These 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.
Leave the original intact. Create a controlled client in tests/support/xclient.c reproducing these operations:
floating, size 400×400, with min/max size hints fixed at 400×400.WM_TRANSIENT_FOR=parent.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.
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:
{
"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.
| Area | Cases and assertions |
|---|---|
| Tags | Multiple/all tags, same-view no-op, previous alternation, empty toggle rejection; both slots, visible/offscreen clients and focus. |
| Layouts | 0/1/many clients, master count 0/1/above count, odd dimensions, borders, hints/aspect/increments; exact rectangles, order and both layout slots. |
| Focus | Map/cycle order, hidden selected window, pointer entry, urgency, never-focus/WM_TAKE_FOCUS; input focus and active-window property. |
| Rules | Substring matches, tag union, later floating overwrite, valid then invalid monitor target, no match; monitor/tags/placement. |
| Lifecycle | Startup scan, duplicate map, override-redirect, real/synthetic unmap, destruction races; client list, WM_STATE, restored borders and no stale membership. |
| Fullscreen | Tiled/floating, repeated add/remove/toggle, resize and destruction; zero border, full-screen geometry, restored normal state and rearrangement. |
| Monitors | Negative origin, transfer/cycling order, removal migration, resize, missing extension; membership, geometry, focus and bar area. |
| Input | Lock/NumLock, mapping changes, grabs, bar click edges, move/resize interrupted by destroy; correct actions and no stuck pointer grabs. |
| Bar | Unicode/fallback, title/status, urgency and selection, show/hide; semantic content and hit regions, with separate visual checks. |
| Process/exit | Argument 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.
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.
On a disposable implementation copy, inject these defects and require the relevant test to fail:
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.
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.
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.
//! 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());
}
}