tile-manager/GUIDE.md
2026-03-16 00:37:00 -04:00

11 KiB

Build a Rust Tile Manager, Step by Step

This is a small, Codecrafters-style roadmap for building a Wayland desktop tile manager in Rust, in the same general family as Hyprland or niri.

The simplest practical path is:

  1. Build a Wayland compositor, not an X11 window manager.
  2. Use smithay as the foundation.
  3. Start with one monitor, one keyboard, one pointer, one tiled layout.
  4. Add polish only after mapping, focus, input, and rendering work.

What You Are Actually Building

At a high level, your app needs to do five jobs:

  1. Start a Wayland server and accept client connections.
  2. Track outputs, seats, windows, focus, and layout state.
  3. Process input events from keyboard and pointer devices.
  4. Render client surfaces to one or more outputs.
  5. Apply a tiling policy when windows are created, resized, focused, or closed.

If you want something "like Hyprland or niri", that does not mean cloning their code structure. It means building these capabilities in order:

  1. Working compositor loop.
  2. Window mapping and focus.
  3. Basic tiling.
  4. Multi-monitor support.
  5. Animations, gestures, rules, and IPC.

Direct Crates You Will Likely Add

These are the direct dependencies worth adding to your own Cargo.toml.

Core crates

  • smithay What it does: the main compositor toolkit. It provides Wayland protocol helpers, backend abstractions, input handling, output handling, rendering helpers, and optional XWayland support.
  • calloop What it does: callback-based event loop. Smithay is designed around it.
  • tracing What it does: structured logging. Smithay uses it internally, so using it yourself keeps diagnostics consistent.
  • tracing-subscriber What it does: prints and filters tracing logs.
  • anyhow What it does: fast application-level error handling while the project is still evolving.
  • thiserror What it does: typed errors for your own subsystems once the codebase grows.
  • bitflags What it does: ergonomic flag types for key modifiers, window state, output capabilities, and internal options.
  • xkbcommon What it does: keyboard layout and keymap handling through libxkbcommon.

Optional but common

  • serde What it does: config file parsing and IPC payloads.
  • toml What it does: TOML config parsing.
  • clap What it does: command-line flags like --config, --socket, --debug.
  • tracing-appender What it does: file logging.

Optional later-stage crates

  • smithay with XWayland-related features What it does: lets X11 apps run through XWayland.
  • zbus What it does: DBus integration for desktop services.
  • async-channel or crossbeam-channel What it does: internal message passing if you split rendering, IPC, or config reload paths.

Minimum Cargo.toml Shape

Use this as a starting point, then adjust feature flags to match the backend you choose:

[dependencies]
anyhow = "1"
bitflags = "2"
calloop = "0.14"
smithay = "0.7"
thiserror = "2"
tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_info"] }
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
xkbcommon = "0.9"

Important: smithay feature selection matters more than the exact crate list. You will eventually enable only the backends you are actually using.

System Dependencies

Rust crates are not the whole story. A Wayland compositor also needs Linux graphics/input stack libraries installed on the system.

Expect to need some combination of:

  • libwayland
  • libxkbcommon
  • libudev
  • libinput
  • libdrm
  • gbm
  • egl / gles2
  • pixman
  • xwayland if you want X11 app support

The exact set depends on whether you target:

  • winit backend first Easier for learning and debugging.
  • udev/DRM backend Required for a real standalone desktop session.

The Learning Order

Treat each stage as a checkpoint. Do not skip ahead.

Stage 0: Pick the first target

Start with a nested compositor using Smithay's easier development path.

Goal:

  • Run your compositor inside an existing desktop session.
  • Open a test client.
  • See logs and state changes.

Why:

  • This avoids debugging KMS/DRM, seat management, and GPU initialization on day one.

Stage 1: Boot the event loop

Build the smallest app that:

  1. Initializes tracing.
  2. Creates a calloop::EventLoop.
  3. Creates Wayland display state.
  4. Registers the core Smithay globals you need.
  5. Enters the dispatch loop.

Deliverable:

  • The process starts cleanly and stays alive.

Stage 2: Accept clients and map surfaces

Add the core Wayland protocol pieces:

  1. Compositor state.
  2. Shared-memory buffer support.
  3. XDG shell support.
  4. Seat support.

Track:

  • New toplevel windows.
  • Window title and app id.
  • Configure/commit lifecycle.
  • Mapped vs unmapped state.

Deliverable:

  • A client can connect and create a toplevel surface.

Stage 3: Build your internal model

Before fancy rendering, define your own data structures.

You need types roughly like:

  • CompositorState
  • OutputState
  • SeatState
  • Workspace
  • TileTree or ColumnLayout
  • WindowId
  • ManagedWindow

Store:

  • Which workspace a window belongs to.
  • Whether the window is tiled, floating, fullscreen, or urgent.
  • Focus order.
  • Geometry requested by your layout engine.

Deliverable:

  • You can print a stable snapshot of layout state after every window event.

Stage 4: Render one output

Now connect your scene state to rendering.

Keep it simple:

  1. Support one output first.
  2. Draw a background color.
  3. Render mapped client surfaces.
  4. Present frames continuously or when damaged.

Deliverable:

  • One client window is visible on screen.

Stage 5: Keyboard and pointer input

Wire up:

  1. Pointer motion.
  2. Pointer button press.
  3. Keyboard key press.
  4. Modifiers through xkbcommon.

Implement:

  • Focus follows click, or focus follows keyboard only.
  • A compositor modifier key.
  • At least three commands: spawn terminal, close focused window, cycle focus.

Deliverable:

  • You can interact with a client and trigger compositor actions.

Stage 6: First tiling layout

Do not start with dynamic animations or fancy trees. Start with one deterministic layout.

Best beginner layouts:

  1. Master-stack.
  2. Equal vertical columns.
  3. Niri-style scrolling columns.

Recommended first choice:

  • Equal vertical columns or a simple master-stack.

Rules:

  1. When a new tiled window appears, insert it into the active workspace.
  2. Recompute all tile rectangles.
  3. Send configure events with the new size.
  4. Render using those computed rectangles.

Deliverable:

  • Opening a second and third window retile the workspace correctly.

Stage 7: Workspaces

Add:

  1. Multiple workspaces per output.
  2. Active workspace switching.
  3. Move focused window to another workspace.

Deliverable:

  • You can switch workspaces and preserve layout state independently.

Stage 8: Multi-output support

Track:

  • Connected outputs.
  • Per-output current workspace.
  • Output geometry and scale.

Implement:

  • New windows land on the focused output.
  • Workspaces are attached either globally or per output.

Deliverable:

  • Two monitors work without corrupting focus or layout state.

Stage 9: Real standalone session

After the nested version works, move toward a real desktop session.

This is where backend complexity increases:

  1. DRM/KMS
  2. GBM/EGL or another rendering path
  3. Libinput
  4. Session/seat handling
  5. VT switching

Deliverable:

  • The compositor can run on a TTY as the main desktop session.

Stage 10: Quality-of-life features

Only now add polish:

  1. Config file.
  2. IPC socket.
  3. Window rules.
  4. Floating windows.
  5. Fullscreen.
  6. Pointer constraints.
  7. Layer-shell panels and wallpapers.
  8. Screencopy.
  9. Idle inhibit.
  10. Animations.

A Good Project Structure

Once the prototype starts working, split it like this:

src/
  main.rs
  app.rs
  state.rs
  backend/
    mod.rs
    nested.rs
    tty.rs
  input/
    mod.rs
    keyboard.rs
    pointer.rs
    bindings.rs
  layout/
    mod.rs
    columns.rs
    master.rs
  shell/
    mod.rs
    xdg.rs
    window.rs
  render/
    mod.rs
    scene.rs
  workspace/
    mod.rs
  config/
    mod.rs

The important rule is simple:

  • Smithay-facing code should stay near the edges.
  • Your layout and workspace logic should be mostly your own plain Rust types.

What Each Crate Does in the Architecture

Think of the crates like this:

  • smithay: Wayland compositor plumbing and backend integration.
  • calloop: the main event loop that everything runs inside.
  • xkbcommon: keyboard interpretation and modifiers.
  • tracing + tracing-subscriber: logs, spans, filtering, debugging.
  • anyhow: ergonomic top-level errors during rapid iteration.
  • thiserror: precise library-style errors for your own modules.
  • bitflags: compact internal state flags.
  • serde + toml: config loading.
  • clap: startup options.
  • zbus: desktop integration later.

Suggested Milestone Checklist

If you want a clean progression, build in this exact order:

  1. Process boots and enters event loop.
  2. Wayland client connects.
  3. XDG toplevel appears.
  4. One window renders.
  5. Focus works.
  6. Keyboard shortcuts work.
  7. Two windows tile correctly.
  8. Workspaces work.
  9. Multi-output works.
  10. Standalone TTY session works.
  11. XWayland works.
  12. Config and IPC work.

Practical Advice

  • Build a nested compositor first. This is the highest-leverage simplification.
  • Keep layout logic pure. A layout engine should accept windows and output rectangles and return tile rectangles.
  • Add exactly one layout first.
  • Keep a strong debug log for every surface map, unmap, commit, focus change, output change, and configure.
  • Do not implement animations until you trust your state model.
  • Do not add XWayland until native Wayland windows are solid.

These are the most relevant primary sources I used:

Final Recommendation

If your goal is "a simple tile manager similar to Hyprland or niri", the best first version is:

  1. Rust
  2. Wayland
  3. smithay
  4. calloop
  5. one output
  6. one workspace
  7. one tiling layout
  8. nested backend first

That path is realistic. Starting with standalone DRM, multi-monitor, XWayland, animations, and IPC all at once is not.