420 lines
11 KiB
Markdown
420 lines
11 KiB
Markdown
# 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:
|
|
|
|
```toml
|
|
[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:
|
|
|
|
```text
|
|
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.
|
|
|
|
## Recommended References
|
|
|
|
These are the most relevant primary sources I used:
|
|
|
|
- Smithay crate docs: https://docs.rs/smithay/latest/smithay/
|
|
- Smithay project docs: https://smithay.github.io/smithay/smithay/
|
|
- Smithay `wayland` module docs: https://docs.rs/smithay/latest/smithay/wayland/
|
|
- Smithay `xwayland` module docs: https://docs.rs/smithay/latest/smithay/xwayland/
|
|
- `wayland_server` docs: https://smithay.github.io/wayland-rs/wayland_server/
|
|
- `calloop` docs: https://docs.rs/calloop/latest/calloop/
|
|
- `tracing` docs: https://docs.rs/crate/tracing/latest
|
|
- `anyhow` docs: https://docs.rs/crate/anyhow/latest
|
|
- `thiserror` docs: https://docs.rs/crate/thiserror/latest
|
|
- `bitflags` docs: https://docs.rs/bitflags
|
|
- `xkbcommon` docs: https://docs.rs/xkbcommon/latest/xkbcommon/
|
|
|
|
## 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.
|