docs(readme): tighten wording and reorder sections
- commit
- 773843275772a48e1d7e72ea77de9714d4c5e3ff
- author
- brice · · 2026-09-26 16:20 UTC
- parent
- d9218d6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +73 -71
M README.md +73 -71
| @@ -16,17 +16,17 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ## Why NeferWL | |
| 18 | 18 | ||
| 19 | - | NeferWL does a few things and does them fast. No animations, no themes, no built-in bar or wallpaper: those are choices, not missing features. Every frame budget goes to your apps. | |
| 19 | + | NeferWL has no animations, themes, built-in bar or wallpaper, by design. The compositor stays small so the CPU and GPU go to your terminal and your games. | |
| 20 | 20 | ||
| 21 | - | - **A terminal, then out of the way.** One workspace opens your terminal at startup. Bars, launchers and notifications are external clients (Waybar, fuzzel, mako…). | |
| 22 | - | - **Columns between tiling and scrolling.** Up to `max-columns` windows share the screen like a classic tiling WM (Sway); past that, columns scroll to the right like Niri, or split in a spiral with `overflow = fixed`. Each workspace picks its own rules. | |
| 23 | - | - **Workspaces you design.** Numbered workspaces are dynamic. Named workspaces can be hidden behind a bind, and **slots** declare their columns (width + command): NeferWL starts the apps in the background and puts each window in its place. | |
| 24 | - | - **Multi-monitor done right.** Each monitor owns its workspaces. Unplug one and its workspaces move to another monitor; plug it back and they return home, without stealing your focus. | |
| 25 | - | - **Built for games.** Fullscreen games skip composition entirely (details below), with tearing and VRR when the game asks. | |
| 26 | - | - **Light.** A running session uses about 40–80 MB of RAM with two 4K monitors, and near-zero CPU when nothing changes on screen. | |
| 27 | - | - **Live config.** One flat `key = value` file. Every key applies on save, keyboard layout included; windows stay open. | |
| 21 | + | - **Terminal first.** NeferWL opens your terminal at startup. Bars, launchers and notifications are external clients such as Waybar, fuzzel and mako. | |
| 22 | + | - **Column tiling.** Up to `max-columns` windows share the screen, as in Sway. Past that, new columns scroll to the right as in Niri, or split in a spiral with `overflow = fixed`. Each workspace can set its own rules. | |
| 23 | + | - **Custom workspaces.** Numbered workspaces appear and disappear as you use them. Named workspaces can be hidden behind a bind. **Slots** declare a workspace's columns (width and command): NeferWL starts the apps in the background and puts each window in its column. | |
| 24 | + | - **Multi-monitor.** Each monitor has its own workspaces. When you unplug a monitor, its workspaces move to another one; when you plug it back, they return, and your focus stays where it was. | |
| 25 | + | - **Games.** A fullscreen game is shown without composition, with tearing and VRR when the game asks for them. Details are in [the performance path](#the-performance-path). | |
| 26 | + | - **Low footprint.** About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | |
| 27 | + | - **Live config.** One `key = value` file. Every key applies when you save, keyboard layout included, and windows stay open. | |
| 28 | 28 | ||
| 29 | - | A complete desktop in a few lines: | |
| 29 | + | An example config: | |
| 30 | 30 | ||
| 31 | 31 | ```text | |
| 32 | 32 | keyboard.layout = fr | |
| @@ -47,71 +47,49 @@ bind.cmd+d = workspace dev | |||
| 47 | 47 | ||
| 48 | 48 | ### The performance path | |
| 49 | 49 | ||
| 50 | - | Every Wayland protocol NeferWL implements is there because it saves work or latency: | |
| 50 | + | These protocols and kernel features cut copies, waits and latency between the app and the screen: | |
| 51 | 51 | ||
| 52 | 52 | - **Zero copy.** GPU clients (`zwp_linux_dmabuf_v1` v4) are sampled in place. Vulkan composes straight into exported scanout images, and the frame fence goes to KMS as `IN_FENCE_FD`: no CPU copy, no CPU wait before a flip. | |
| 53 | 53 | - **Direct scanout.** A fullscreen window's buffer goes straight to the display plane; dmabuf feedback sends the game a scanout-ready format first. A lone opaque window can use an overlay plane. | |
| 54 | 54 | - **Tearing and VRR.** `wp_tearing_control_v1` flips without waiting for vblank when the game requests it; variable refresh runs while a buffer is scanned out. | |
| 55 | - | - **Explicit sync.** `wp_linux_drm_syncobj_v1` passes GPU fences end to end (needed by NVIDIA). | |
| 56 | - | - **Frame pacing.** `wp_presentation` from kernel flip timestamps, `wp_fifo_v1`, `wp_commit_timing_v1` and `wp_content_type_v1`. | |
| 57 | - | - **Only what changed.** Damage-limited redraws, occlusion culling of hidden surfaces, idle outputs never redraw. Atomic KMS: frame, cursor, overlay and VRR in one commit. | |
| 58 | - | - **Input for games.** Relative pointer, pointer constraints, keyboard shortcuts inhibit, 1 kHz+ non-blocking input, hardware cursor plane. | |
| 55 | + | - **Explicit sync.** `wp_linux_drm_syncobj_v1` passes GPU fences from the app to KMS. NVIDIA drivers need it. | |
| 56 | + | - **Frame pacing.** Apps get real flip times through `wp_presentation`, and can queue frames with `wp_fifo_v1` and `wp_commit_timing_v1`. | |
| 57 | + | - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit. | |
| 58 | + | - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. | |
| 59 | 59 | - **X11 games.** Steam and Wine run through xwayland-satellite, started on the first X11 connection. | |
| 60 | 60 | ||
| 61 | - | Requires Linux and Go 1.27. | |
| 62 | - | ||
| 63 | 61 | ## Hardware support | |
| 64 | 62 | ||
| 65 | - | NeferWL needs a GPU and kernel driver with atomic KMS and a Vulkan 1.3 device that can export images as dmabufs; without them it stops at startup with an error. Frames are composed on the GPU only: no CPU copy and no CPU wait for the GPU before a page flip. | |
| 66 | - | ||
| 67 | - | - **AMD GPUs:** developed and tested on AMD (radv). | |
| 68 | - | - **NVIDIA GPUs:** untested. Explicit sync (`linux-drm-syncobj`), which NVIDIA drivers need, is implemented. Testers with NVIDIA hardware are welcome to report issues or contribute. | |
| 69 | - | - **Intel GPUs:** untested. | |
| 70 | - | ||
| 71 | - | Build: `make build`. | |
| 63 | + | NeferWL runs on Linux. It needs a GPU and kernel driver with atomic KMS, and a Vulkan 1.3 device that can export images as dmabufs. Without them, it stops at startup with an error. | |
| 72 | 64 | ||
| 73 | - | Run: `neferwl --backend=headless --timeout 5s` | |
| 65 | + | - **AMD:** developed and tested on radv. | |
| 66 | + | - **NVIDIA:** untested. Explicit sync, which NVIDIA drivers need, is implemented. Reports from NVIDIA users are welcome. | |
| 67 | + | - **Intel:** untested. | |
| 74 | 68 | ||
| 75 | 69 | ## Install (Arch Linux) | |
| 76 | 70 | ||
| 71 | + | Building needs Go 1.27. | |
| 72 | + | ||
| 77 | 73 | ```sh | |
| 78 | 74 | make pkg # builds dist/neferwl-*.pkg.tar.zst from HEAD | |
| 79 | 75 | sudo pacman -U dist/neferwl-*.pkg.tar.zst | |
| 80 | 76 | ``` | |
| 81 | 77 | ||
| 82 | - | The package installs `neferwl` and a **NeferWL** session for display managers (Ly, GDM, SDDM). The session runs `neferwl --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. | |
| 83 | - | ||
| 84 | - | ## Developing the bindings | |
| 85 | - | ||
| 86 | - | NeferWL depends on tagged releases of [purego-libwayland](https://github.com/bnema/purego-libwayland) and [purego-vulkan](https://github.com/bnema/purego-vulkan). To change them together with NeferWL, check them out next to it and use a Go workspace (not committed): | |
| 87 | - | ||
| 88 | - | ```sh | |
| 89 | - | go work init . ../purego-libwayland ../purego-vulkan | |
| 90 | - | ``` | |
| 78 | + | The package installs `neferwl` and a **NeferWL** session for display managers such as Ly, GDM and SDDM. The session runs `neferwl --session`, which passes `WAYLAND_DISPLAY` and `DISPLAY` to D-Bus and systemd user services so that portals and notification daemons can connect. | |
| 91 | 79 | ||
| 92 | - | `make pkg` ignores the workspace and builds the versions in `go.mod`: tag a binding change and update `go.mod` before packaging it. | |
| 80 | + | On other distributions, `make build` builds `bin/neferwl`. | |
| 93 | 81 | ||
| 94 | 82 | ## Configuration | |
| 95 | 83 | ||
| 96 | - | Configuration lives at `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). Use `neferwl --config path` to select another file; `neferwl validate-config [path]` checks it without starting the compositor. | |
| 97 | - | ||
| 98 | - | One `key = value` per line; `#` starts a comment: | |
| 84 | + | NeferWL reads `$XDG_CONFIG_HOME/neferwl/config`, or `~/.config/neferwl/config`. `neferwl --config <path>` selects another file, and `neferwl validate-config [path]` checks a file without starting the compositor. | |
| 99 | 85 | ||
| 100 | - | ```text | |
| 101 | - | keyboard.layout = fr | |
| 102 | - | terminal = foot | |
| 103 | - | bind.cmd+return = spawn-terminal | |
| 104 | - | bind.ctrl+cmd+space = spawn fuzzel | |
| 105 | - | startup = wl-paste --watch cliphist store | |
| 106 | - | ``` | |
| 86 | + | The file has one `key = value` per line, and `#` starts a comment. A missing file or key means the default. An invalid line logs a warning and keeps that key's default; the rest of the file still applies. Changes apply when you save, and each reload logs the keys that changed. | |
| 107 | 87 | ||
| 108 | - | Every key, value and default bind is listed in [docs/config.md](docs/config.md); `examples/config` is a commented copy of the defaults. A missing file means defaults. An invalid line logs a warning and keeps that key's default; the rest of the file still applies. Every key applies live when the file changes, including the keyboard layout; each reload logs the keys that changed. | |
| 88 | + | [docs/config.md](docs/config.md) lists every key, action and default bind. `examples/config` is a commented copy of the defaults. | |
| 109 | 89 | ||
| 110 | 90 | ## Running commands | |
| 111 | 91 | ||
| 112 | - | NeferWL starts a program, not a shell line: `spawn` runs a binary or a script. Anything more than one command with arguments, such as pipes or variables, goes in a script that you write. | |
| 113 | - | ||
| 114 | - | `spawn <command>` binds, `startup = <command>` lines and `terminal = <command>` run one program directly, without a shell. The line is split on spaces: the first word is the program (looked up in `PATH`, or a path such as `/opt/tool/run`), the other words are its arguments. | |
| 92 | + | `spawn <command>` binds, `startup = <command>` lines and `terminal = <command>` run one program directly, without a shell. NeferWL splits the line on spaces: the first word is the program, found in `PATH` or given as a path such as `/opt/tool/run`, and the other words are its arguments. | |
| 115 | 93 | ||
| 116 | 94 | ```text | |
| 117 | 95 | bind.ctrl+cmd+space = spawn fuzzel | |
| @@ -119,9 +97,7 @@ startup = waybar | |||
| 119 | 97 | startup = wl-paste --watch cliphist store | |
| 120 | 98 | ``` | |
| 121 | 99 | ||
| 122 | - | There is no shell, so pipes (`|`), `&&`, redirections, variables (`$HOME`), `~` and quotes have no special meaning: they reach the program as plain arguments. What you write is what runs. | |
| 123 | - | ||
| 124 | - | For anything more complex, write a script and run the script: | |
| 100 | + | Pipes (`|`), `&&`, redirections, variables (`$HOME`), `~` and quotes have no special meaning: they reach the program as plain arguments. For those, write a script and run it: | |
| 125 | 101 | ||
| 126 | 102 | ```sh | |
| 127 | 103 | #!/bin/sh | |
| @@ -134,13 +110,13 @@ grim -g "$(slurp)" "$file" && wl-copy < "$file" | |||
| 134 | 110 | bind.cmd+shift+s = spawn screenshot-area | |
| 135 | 111 | ``` | |
| 136 | 112 | ||
| 137 | - | Make it executable (`chmod +x`) and put it in a directory of your `PATH`, such as `~/.local/bin`. The script can use any shell, including fish (`#!/usr/bin/env fish`). | |
| 113 | + | Make the script executable (`chmod +x`) and put it in a directory of your `PATH`, such as `~/.local/bin`. It can use any shell, including fish (`#!/usr/bin/env fish`). | |
| 138 | 114 | ||
| 139 | - | Programs started by NeferWL get `WAYLAND_DISPLAY`, `DISPLAY` (see [X11 apps](#x11-apps)) and `NEFERWL_STATE` (see below). `startup` commands run once when the session starts; editing them takes effect at the next start. | |
| 115 | + | Programs started by NeferWL get `WAYLAND_DISPLAY`, `DISPLAY` (see [X11 apps](#x11-apps)) and `NEFERWL_STATE` (see below). `startup` commands run once when the session starts, so editing them takes effect at the next start. | |
| 140 | 116 | ||
| 141 | 117 | ## Clipboard | |
| 142 | 118 | ||
| 143 | - | NeferWL supports the clipboard (`wl_data_device`), the primary selection (middle-click paste) and `ext_data_control_v1` for clipboard managers. With [cliphist](https://github.com/sentriz/cliphist), store every copy at startup and bind a picker script (see [Running commands](#running-commands)): | |
| 119 | + | NeferWL supports the clipboard (`wl_data_device`), the primary selection (middle-click paste) and `ext_data_control_v1` for clipboard managers. To keep a history with [cliphist](https://github.com/sentriz/cliphist), store every copy at startup and bind a picker script: | |
| 144 | 120 | ||
| 145 | 121 | ```text | |
| 146 | 122 | startup = wl-paste --watch cliphist store | |
| @@ -158,7 +134,7 @@ Drag and drop is not supported yet. | |||
| 158 | 134 | ||
| 159 | 135 | ## X11 apps | |
| 160 | 136 | ||
| 161 | - | X11 apps such as Steam and Wine run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7 or later, found in `PATH`. NeferWL opens an X11 display, sets `DISPLAY` for the programs it starts, and runs xwayland-satellite when the first X11 app connects. If xwayland-satellite exits, the next X11 app starts it again. | |
| 137 | + | X11 apps such as Steam and Wine run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7 or later, found in `PATH`. NeferWL opens an X11 display, sets `DISPLAY` for the programs it starts, and starts xwayland-satellite when the first X11 app connects. If xwayland-satellite exits, the next X11 app starts it again. | |
| 162 | 138 | ||
| 163 | 139 | ```text | |
| 164 | 140 | xwayland = xwayland-satellite # the default; a path also works | |
| @@ -169,42 +145,68 @@ Do not start xwayland-satellite yourself with `startup`. Changing `xwayland` tak | |||
| 169 | 145 | ||
| 170 | 146 | ## State for scripts | |
| 171 | 147 | ||
| 172 | - | While it runs, NeferWL keeps its state in `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json` and passes that path to the programs it starts as `NEFERWL_STATE`. The file lists every output (active numbered workspace, workspace count, name of the workspace on screen), the focused output and window, and every window with its app ID, PID, output and workspace. | |
| 148 | + | While it runs, NeferWL writes its state to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json` and passes that path to the programs it starts as `NEFERWL_STATE`. The file lists: | |
| 149 | + | ||
| 150 | + | - every output, with its active numbered workspace, workspace count and the name of the workspace on screen; | |
| 151 | + | - the focused output and window; | |
| 152 | + | - every window, with its app ID, PID, output and workspace. | |
| 173 | 153 | ||
| 174 | 154 | ```sh | |
| 175 | 155 | neferwl state # the whole state as JSON | |
| 176 | 156 | neferwl state output-of "$PID" # the output of that process's window, or its nearest parent's | |
| 177 | 157 | ``` | |
| 178 | 158 | ||
| 179 | - | `output-of` finds application windows only; bars have no single output. | |
| 159 | + | `output-of` finds application windows only, because a bar has no single output. | |
| 180 | 160 | ||
| 181 | 161 | ## Try it (headless) | |
| 182 | 162 | ||
| 183 | - | Run `neferwl --backend=headless --screenshot /tmp/neferwl-shots`, then connect with | |
| 184 | - | `WAYLAND_DISPLAY=<logged name> foot`. `/tmp/neferwl-shots/latest.png` shows the current frame. | |
| 185 | - | Use `--input path` (or `--input -` for stdin) to inject a headless script. Each line is | |
| 186 | - | `type text`, `key Super+Return` (also Shift, Ctrl, Alt or a bare key), `sleep 1s`, `move x y` (absolute output coordinates), `click [left|right|middle]`, | |
| 187 | - | `down button`, or `up button`. Cursor rendering is not implemented yet; pointer | |
| 188 | - | focus after layout changes is updated on the next move. | |
| 163 | + | The headless backend runs without a screen, for tests and quick checks: | |
| 164 | + | ||
| 165 | + | ```sh | |
| 166 | + | neferwl --backend=headless --screenshot /tmp/neferwl-shots | |
| 167 | + | WAYLAND_DISPLAY=<name from the log> foot | |
| 168 | + | ``` | |
| 169 | + | ||
| 170 | + | `/tmp/neferwl-shots/latest.png` shows the current frame. `--timeout 5s` stops NeferWL after 5 seconds. | |
| 171 | + | ||
| 172 | + | `--input <path>`, or `--input -` for stdin, plays an input script. One command per line: | |
| 173 | + | ||
| 174 | + | - `type <text>` | |
| 175 | + | - `key Super+Return` (Shift, Ctrl and Alt also work, as does a bare key) | |
| 176 | + | - `sleep 1s` | |
| 177 | + | - `move <x> <y>` (output coordinates) | |
| 178 | + | - `click [left|right|middle]`, `down <button>`, `up <button>` | |
| 179 | + | ||
| 180 | + | The headless backend does not draw the cursor. After a layout change, pointer focus updates on the next move. | |
| 189 | 181 | ||
| 190 | 182 | ## Why not Rust? | |
| 191 | 183 | ||
| 192 | - | Most Wayland compositors are written in C (wlroots, Sway, Mutter), C++ (KWin, Hyprland, gamescope) or Rust (niri, COSMIC). NeferWL is written in Go, by choice: | |
| 184 | + | Most Wayland compositors are written in C (wlroots, Sway, Mutter), C++ (KWin, Hyprland, gamescope) or Rust (niri, COSMIC). NeferWL uses Go on purpose: | |
| 193 | 185 | ||
| 194 | - | - **Simple and readable.** Go is small and explicit. Goroutines and channels fit a compositor where each part (input, outputs, clients) owns its own state. | |
| 195 | - | - **Fast builds, easy tooling.** A full build takes seconds; tests, the race detector, profiling and formatting come with the language. | |
| 186 | + | - **Readable.** Go is small and explicit. Goroutines and channels fit a compositor where input, each output and the clients own their own state. | |
| 187 | + | - **Tooling.** A full build takes seconds, and tests, the race detector, profiling and formatting come with the language. | |
| 196 | 188 | - **No cgo.** NeferWL builds with `CGO_ENABLED=0`: libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego). | |
| 197 | - | - **Fast enough.** With care for allocations and the garbage collector, the hot paths (input, rendering, buffer handling) perform close to native code. | |
| 189 | + | - **Speed.** With care for allocations and the garbage collector, input, rendering and buffer handling run close to native code. | |
| 190 | + | ||
| 191 | + | I like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, [purego-libwayland](https://github.com/bnema/purego-libwayland), [purego-vulkan](https://github.com/bnema/purego-vulkan) and [wlturbo](https://github.com/bnema/wlturbo), and other Go projects can use them. | |
| 192 | + | ||
| 193 | + | Go has no borrow checker, so the compiler does not catch memory and concurrency mistakes as Rust's does. NeferWL compensates with strict ownership (one goroutine owns each piece of state), the race detector on every test run, and protocol tests against real Wayland clients. | |
| 194 | + | ||
| 195 | + | ## Developing the bindings | |
| 198 | 196 | ||
| 199 | - | As a software engineer, my passion is bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way: purego-libwayland, purego-vulkan and wlturbo, to be published for others to reuse. Building them is part of the fun. | |
| 197 | + | NeferWL uses tagged releases of [purego-libwayland](https://github.com/bnema/purego-libwayland) and [purego-vulkan](https://github.com/bnema/purego-vulkan). To change a binding and NeferWL together, clone the binding next to NeferWL and use a Go workspace, which is not committed: | |
| 198 | + | ||
| 199 | + | ```sh | |
| 200 | + | go work init . ../purego-libwayland ../purego-vulkan | |
| 201 | + | ``` | |
| 200 | 202 | ||
| 201 | - | What Go does not give us is Rust's borrow checker: memory and concurrency mistakes are not caught at compile time. NeferWL makes up for it with strict ownership (one goroutine owns each piece of state), the race detector on every test run, and protocol tests against real Wayland clients. | |
| 203 | + | `make pkg` ignores the workspace and builds the versions in `go.mod`. Tag the binding change and update `go.mod` before packaging. | |
| 202 | 204 | ||
| 203 | 205 | ## Contributing | |
| 204 | 206 | ||
| 205 | - | Contributions are welcome. Open an issue first, for a bug fix or a feature, and wait until it is accepted before sending a pull request. Pull requests without an accepted issue will be closed. | |
| 207 | + | Contributions are welcome. Open an issue first, for a bug fix or a feature, and wait until it is accepted before you send a pull request. Pull requests without an accepted issue are closed. | |
| 206 | 208 | ||
| 207 | - | Before a pull request, `make check`, `make race`, `make mocks-check` and `staticcheck ./...` must pass. Repository rules are in [AGENTS.md](AGENTS.md). | |
| 209 | + | Before you open a pull request, `make check`, `make race`, `make mocks-check` and `staticcheck ./...` must pass. Repository rules are in [AGENTS.md](AGENTS.md). | |
| 208 | 210 | ||
| 209 | 211 | ## License | |
| 210 | 212 | ||
| @@ -16,17 +16,17 @@ | |||||
| 16 | 16 | ||||
| 17 | ## Why NeferWL | 17 | ## Why NeferWL | ||
| 18 | 18 | ||||
| 19 | - | NeferWL does a few things and does them fast. No animations, no themes, no built-in bar or wallpaper: those are choices, not missing features. Every frame budget goes to your apps. | 19 | + | NeferWL has no animations, themes, built-in bar or wallpaper, by design. The compositor stays small so the CPU and GPU go to your terminal and your games. |
| 20 | 20 | ||||
| 21 | - | - **A terminal, then out of the way.** One workspace opens your terminal at startup. Bars, launchers and notifications are external clients (Waybar, fuzzel, mako…). | 21 | + | - **Terminal first.** NeferWL opens your terminal at startup. Bars, launchers and notifications are external clients such as Waybar, fuzzel and mako. |
| 22 | - | - **Columns between tiling and scrolling.** Up to `max-columns` windows share the screen like a classic tiling WM (Sway); past that, columns scroll to the right like Niri, or split in a spiral with `overflow = fixed`. Each workspace picks its own rules. | 22 | + | - **Column tiling.** Up to `max-columns` windows share the screen, as in Sway. Past that, new columns scroll to the right as in Niri, or split in a spiral with `overflow = fixed`. Each workspace can set its own rules. |
| 23 | - | - **Workspaces you design.** Numbered workspaces are dynamic. Named workspaces can be hidden behind a bind, and **slots** declare their columns (width + command): NeferWL starts the apps in the background and puts each window in its place. | 23 | + | - **Custom workspaces.** Numbered workspaces appear and disappear as you use them. Named workspaces can be hidden behind a bind. **Slots** declare a workspace's columns (width and command): NeferWL starts the apps in the background and puts each window in its column. |
| 24 | - | - **Multi-monitor done right.** Each monitor owns its workspaces. Unplug one and its workspaces move to another monitor; plug it back and they return home, without stealing your focus. | 24 | + | - **Multi-monitor.** Each monitor has its own workspaces. When you unplug a monitor, its workspaces move to another one; when you plug it back, they return, and your focus stays where it was. |
| 25 | - | - **Built for games.** Fullscreen games skip composition entirely (details below), with tearing and VRR when the game asks. | 25 | + | - **Games.** A fullscreen game is shown without composition, with tearing and VRR when the game asks for them. Details are in [the performance path](#the-performance-path). |
| 26 | - | - **Light.** A running session uses about 40–80 MB of RAM with two 4K monitors, and near-zero CPU when nothing changes on screen. | 26 | + | - **Low footprint.** About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. |
| 27 | - | - **Live config.** One flat `key = value` file. Every key applies on save, keyboard layout included; windows stay open. | 27 | + | - **Live config.** One `key = value` file. Every key applies when you save, keyboard layout included, and windows stay open. |
| 28 | 28 | ||||
| 29 | - | A complete desktop in a few lines: | 29 | + | An example config: |
| 30 | 30 | ||||
| 31 | ```text | 31 | ```text | ||
| 32 | keyboard.layout = fr | 32 | keyboard.layout = fr | ||
| @@ -47,71 +47,49 @@ bind.cmd+d = workspace dev | |||||
| 47 | 47 | ||||
| 48 | ### The performance path | 48 | ### The performance path | ||
| 49 | 49 | ||||
| 50 | - | Every Wayland protocol NeferWL implements is there because it saves work or latency: | 50 | + | These protocols and kernel features cut copies, waits and latency between the app and the screen: |
| 51 | 51 | ||||
| 52 | - **Zero copy.** GPU clients (`zwp_linux_dmabuf_v1` v4) are sampled in place. Vulkan composes straight into exported scanout images, and the frame fence goes to KMS as `IN_FENCE_FD`: no CPU copy, no CPU wait before a flip. | 52 | - **Zero copy.** GPU clients (`zwp_linux_dmabuf_v1` v4) are sampled in place. Vulkan composes straight into exported scanout images, and the frame fence goes to KMS as `IN_FENCE_FD`: no CPU copy, no CPU wait before a flip. | ||
| 53 | - **Direct scanout.** A fullscreen window's buffer goes straight to the display plane; dmabuf feedback sends the game a scanout-ready format first. A lone opaque window can use an overlay plane. | 53 | - **Direct scanout.** A fullscreen window's buffer goes straight to the display plane; dmabuf feedback sends the game a scanout-ready format first. A lone opaque window can use an overlay plane. | ||
| 54 | - **Tearing and VRR.** `wp_tearing_control_v1` flips without waiting for vblank when the game requests it; variable refresh runs while a buffer is scanned out. | 54 | - **Tearing and VRR.** `wp_tearing_control_v1` flips without waiting for vblank when the game requests it; variable refresh runs while a buffer is scanned out. | ||
| 55 | - | - **Explicit sync.** `wp_linux_drm_syncobj_v1` passes GPU fences end to end (needed by NVIDIA). | 55 | + | - **Explicit sync.** `wp_linux_drm_syncobj_v1` passes GPU fences from the app to KMS. NVIDIA drivers need it. |
| 56 | - | - **Frame pacing.** `wp_presentation` from kernel flip timestamps, `wp_fifo_v1`, `wp_commit_timing_v1` and `wp_content_type_v1`. | 56 | + | - **Frame pacing.** Apps get real flip times through `wp_presentation`, and can queue frames with `wp_fifo_v1` and `wp_commit_timing_v1`. |
| 57 | - | - **Only what changed.** Damage-limited redraws, occlusion culling of hidden surfaces, idle outputs never redraw. Atomic KMS: frame, cursor, overlay and VRR in one commit. | 57 | + | - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit. |
| 58 | - | - **Input for games.** Relative pointer, pointer constraints, keyboard shortcuts inhibit, 1 kHz+ non-blocking input, hardware cursor plane. | 58 | + | - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. |
| 59 | - **X11 games.** Steam and Wine run through xwayland-satellite, started on the first X11 connection. | 59 | - **X11 games.** Steam and Wine run through xwayland-satellite, started on the first X11 connection. | ||
| 60 | 60 | ||||
| 61 | - | Requires Linux and Go 1.27. | |||
| 62 | - | ||||
| 63 | ## Hardware support | 61 | ## Hardware support | ||
| 64 | 62 | ||||
| 65 | - | NeferWL needs a GPU and kernel driver with atomic KMS and a Vulkan 1.3 device that can export images as dmabufs; without them it stops at startup with an error. Frames are composed on the GPU only: no CPU copy and no CPU wait for the GPU before a page flip. | 63 | + | NeferWL runs on Linux. It needs a GPU and kernel driver with atomic KMS, and a Vulkan 1.3 device that can export images as dmabufs. Without them, it stops at startup with an error. |
| 66 | - | ||||
| 67 | - | - **AMD GPUs:** developed and tested on AMD (radv). | |||
| 68 | - | - **NVIDIA GPUs:** untested. Explicit sync (`linux-drm-syncobj`), which NVIDIA drivers need, is implemented. Testers with NVIDIA hardware are welcome to report issues or contribute. | |||
| 69 | - | - **Intel GPUs:** untested. | |||
| 70 | - | ||||
| 71 | - | Build: `make build`. | |||
| 72 | 64 | ||||
| 73 | - | Run: `neferwl --backend=headless --timeout 5s` | 65 | + | - **AMD:** developed and tested on radv. |
| 66 | + | - **NVIDIA:** untested. Explicit sync, which NVIDIA drivers need, is implemented. Reports from NVIDIA users are welcome. | |||
| 67 | + | - **Intel:** untested. | |||
| 74 | 68 | ||||
| 75 | ## Install (Arch Linux) | 69 | ## Install (Arch Linux) | ||
| 76 | 70 | ||||
| 71 | + | Building needs Go 1.27. | |||
| 72 | + | ||||
| 77 | ```sh | 73 | ```sh | ||
| 78 | make pkg # builds dist/neferwl-*.pkg.tar.zst from HEAD | 74 | make pkg # builds dist/neferwl-*.pkg.tar.zst from HEAD | ||
| 79 | sudo pacman -U dist/neferwl-*.pkg.tar.zst | 75 | sudo pacman -U dist/neferwl-*.pkg.tar.zst | ||
| 80 | ``` | 76 | ``` | ||
| 81 | 77 | ||||
| 82 | - | The package installs `neferwl` and a **NeferWL** session for display managers (Ly, GDM, SDDM). The session runs `neferwl --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. | 78 | + | The package installs `neferwl` and a **NeferWL** session for display managers such as Ly, GDM and SDDM. The session runs `neferwl --session`, which passes `WAYLAND_DISPLAY` and `DISPLAY` to D-Bus and systemd user services so that portals and notification daemons can connect. |
| 83 | - | ||||
| 84 | - | ## Developing the bindings | |||
| 85 | - | ||||
| 86 | - | NeferWL depends on tagged releases of [purego-libwayland](https://github.com/bnema/purego-libwayland) and [purego-vulkan](https://github.com/bnema/purego-vulkan). To change them together with NeferWL, check them out next to it and use a Go workspace (not committed): | |||
| 87 | - | ||||
| 88 | - | ```sh | |||
| 89 | - | go work init . ../purego-libwayland ../purego-vulkan | |||
| 90 | - | ``` | |||
| 91 | 79 | ||||
| 92 | - | `make pkg` ignores the workspace and builds the versions in `go.mod`: tag a binding change and update `go.mod` before packaging it. | 80 | + | On other distributions, `make build` builds `bin/neferwl`. |
| 93 | 81 | ||||
| 94 | ## Configuration | 82 | ## Configuration | ||
| 95 | 83 | ||||
| 96 | - | Configuration lives at `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). Use `neferwl --config path` to select another file; `neferwl validate-config [path]` checks it without starting the compositor. | 84 | + | NeferWL reads `$XDG_CONFIG_HOME/neferwl/config`, or `~/.config/neferwl/config`. `neferwl --config <path>` selects another file, and `neferwl validate-config [path]` checks a file without starting the compositor. |
| 97 | - | ||||
| 98 | - | One `key = value` per line; `#` starts a comment: | |||
| 99 | 85 | ||||
| 100 | - | ```text | 86 | + | The file has one `key = value` per line, and `#` starts a comment. A missing file or key means the default. An invalid line logs a warning and keeps that key's default; the rest of the file still applies. Changes apply when you save, and each reload logs the keys that changed. |
| 101 | - | keyboard.layout = fr | |||
| 102 | - | terminal = foot | |||
| 103 | - | bind.cmd+return = spawn-terminal | |||
| 104 | - | bind.ctrl+cmd+space = spawn fuzzel | |||
| 105 | - | startup = wl-paste --watch cliphist store | |||
| 106 | - | ``` | |||
| 107 | 87 | ||||
| 108 | - | Every key, value and default bind is listed in [docs/config.md](docs/config.md); `examples/config` is a commented copy of the defaults. A missing file means defaults. An invalid line logs a warning and keeps that key's default; the rest of the file still applies. Every key applies live when the file changes, including the keyboard layout; each reload logs the keys that changed. | 88 | + | [docs/config.md](docs/config.md) lists every key, action and default bind. `examples/config` is a commented copy of the defaults. |
| 109 | 89 | ||||
| 110 | ## Running commands | 90 | ## Running commands | ||
| 111 | 91 | ||||
| 112 | - | NeferWL starts a program, not a shell line: `spawn` runs a binary or a script. Anything more than one command with arguments, such as pipes or variables, goes in a script that you write. | 92 | + | `spawn <command>` binds, `startup = <command>` lines and `terminal = <command>` run one program directly, without a shell. NeferWL splits the line on spaces: the first word is the program, found in `PATH` or given as a path such as `/opt/tool/run`, and the other words are its arguments. |
| 113 | - | ||||
| 114 | - | `spawn <command>` binds, `startup = <command>` lines and `terminal = <command>` run one program directly, without a shell. The line is split on spaces: the first word is the program (looked up in `PATH`, or a path such as `/opt/tool/run`), the other words are its arguments. | |||
| 115 | 93 | ||||
| 116 | ```text | 94 | ```text | ||
| 117 | bind.ctrl+cmd+space = spawn fuzzel | 95 | bind.ctrl+cmd+space = spawn fuzzel | ||
| @@ -119,9 +97,7 @@ startup = waybar | |||||
| 119 | startup = wl-paste --watch cliphist store | 97 | startup = wl-paste --watch cliphist store | ||
| 120 | ``` | 98 | ``` | ||
| 121 | 99 | ||||
| 122 | - | There is no shell, so pipes (`|`), `&&`, redirections, variables (`$HOME`), `~` and quotes have no special meaning: they reach the program as plain arguments. What you write is what runs. | 100 | + | Pipes (`|`), `&&`, redirections, variables (`$HOME`), `~` and quotes have no special meaning: they reach the program as plain arguments. For those, write a script and run it: |
| 123 | - | ||||
| 124 | - | For anything more complex, write a script and run the script: | |||
| 125 | 101 | ||||
| 126 | ```sh | 102 | ```sh | ||
| 127 | #!/bin/sh | 103 | #!/bin/sh | ||
| @@ -134,13 +110,13 @@ grim -g "$(slurp)" "$file" && wl-copy < "$file" | |||||
| 134 | bind.cmd+shift+s = spawn screenshot-area | 110 | bind.cmd+shift+s = spawn screenshot-area | ||
| 135 | ``` | 111 | ``` | ||
| 136 | 112 | ||||
| 137 | - | Make it executable (`chmod +x`) and put it in a directory of your `PATH`, such as `~/.local/bin`. The script can use any shell, including fish (`#!/usr/bin/env fish`). | 113 | + | Make the script executable (`chmod +x`) and put it in a directory of your `PATH`, such as `~/.local/bin`. It can use any shell, including fish (`#!/usr/bin/env fish`). |
| 138 | 114 | ||||
| 139 | - | Programs started by NeferWL get `WAYLAND_DISPLAY`, `DISPLAY` (see [X11 apps](#x11-apps)) and `NEFERWL_STATE` (see below). `startup` commands run once when the session starts; editing them takes effect at the next start. | 115 | + | Programs started by NeferWL get `WAYLAND_DISPLAY`, `DISPLAY` (see [X11 apps](#x11-apps)) and `NEFERWL_STATE` (see below). `startup` commands run once when the session starts, so editing them takes effect at the next start. |
| 140 | 116 | ||||
| 141 | ## Clipboard | 117 | ## Clipboard | ||
| 142 | 118 | ||||
| 143 | - | NeferWL supports the clipboard (`wl_data_device`), the primary selection (middle-click paste) and `ext_data_control_v1` for clipboard managers. With [cliphist](https://github.com/sentriz/cliphist), store every copy at startup and bind a picker script (see [Running commands](#running-commands)): | 119 | + | NeferWL supports the clipboard (`wl_data_device`), the primary selection (middle-click paste) and `ext_data_control_v1` for clipboard managers. To keep a history with [cliphist](https://github.com/sentriz/cliphist), store every copy at startup and bind a picker script: |
| 144 | 120 | ||||
| 145 | ```text | 121 | ```text | ||
| 146 | startup = wl-paste --watch cliphist store | 122 | startup = wl-paste --watch cliphist store | ||
| @@ -158,7 +134,7 @@ Drag and drop is not supported yet. | |||||
| 158 | 134 | ||||
| 159 | ## X11 apps | 135 | ## X11 apps | ||
| 160 | 136 | ||||
| 161 | - | X11 apps such as Steam and Wine run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7 or later, found in `PATH`. NeferWL opens an X11 display, sets `DISPLAY` for the programs it starts, and runs xwayland-satellite when the first X11 app connects. If xwayland-satellite exits, the next X11 app starts it again. | 137 | + | X11 apps such as Steam and Wine run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7 or later, found in `PATH`. NeferWL opens an X11 display, sets `DISPLAY` for the programs it starts, and starts xwayland-satellite when the first X11 app connects. If xwayland-satellite exits, the next X11 app starts it again. |
| 162 | 138 | ||||
| 163 | ```text | 139 | ```text | ||
| 164 | xwayland = xwayland-satellite # the default; a path also works | 140 | xwayland = xwayland-satellite # the default; a path also works | ||
| @@ -169,42 +145,68 @@ Do not start xwayland-satellite yourself with `startup`. Changing `xwayland` tak | |||||
| 169 | 145 | ||||
| 170 | ## State for scripts | 146 | ## State for scripts | ||
| 171 | 147 | ||||
| 172 | - | While it runs, NeferWL keeps its state in `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json` and passes that path to the programs it starts as `NEFERWL_STATE`. The file lists every output (active numbered workspace, workspace count, name of the workspace on screen), the focused output and window, and every window with its app ID, PID, output and workspace. | 148 | + | While it runs, NeferWL writes its state to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json` and passes that path to the programs it starts as `NEFERWL_STATE`. The file lists: |
| 149 | + | ||||
| 150 | + | - every output, with its active numbered workspace, workspace count and the name of the workspace on screen; | |||
| 151 | + | - the focused output and window; | |||
| 152 | + | - every window, with its app ID, PID, output and workspace. | |||
| 173 | 153 | ||||
| 174 | ```sh | 154 | ```sh | ||
| 175 | neferwl state # the whole state as JSON | 155 | neferwl state # the whole state as JSON | ||
| 176 | neferwl state output-of "$PID" # the output of that process's window, or its nearest parent's | 156 | neferwl state output-of "$PID" # the output of that process's window, or its nearest parent's | ||
| 177 | ``` | 157 | ``` | ||
| 178 | 158 | ||||
| 179 | - | `output-of` finds application windows only; bars have no single output. | 159 | + | `output-of` finds application windows only, because a bar has no single output. |
| 180 | 160 | ||||
| 181 | ## Try it (headless) | 161 | ## Try it (headless) | ||
| 182 | 162 | ||||
| 183 | - | Run `neferwl --backend=headless --screenshot /tmp/neferwl-shots`, then connect with | 163 | + | The headless backend runs without a screen, for tests and quick checks: |
| 184 | - | `WAYLAND_DISPLAY=<logged name> foot`. `/tmp/neferwl-shots/latest.png` shows the current frame. | 164 | + | |
| 185 | - | Use `--input path` (or `--input -` for stdin) to inject a headless script. Each line is | 165 | + | ```sh |
| 186 | - | `type text`, `key Super+Return` (also Shift, Ctrl, Alt or a bare key), `sleep 1s`, `move x y` (absolute output coordinates), `click [left|right|middle]`, | 166 | + | neferwl --backend=headless --screenshot /tmp/neferwl-shots |
| 187 | - | `down button`, or `up button`. Cursor rendering is not implemented yet; pointer | 167 | + | WAYLAND_DISPLAY=<name from the log> foot |
| 188 | - | focus after layout changes is updated on the next move. | 168 | + | ``` |
| 169 | + | ||||
| 170 | + | `/tmp/neferwl-shots/latest.png` shows the current frame. `--timeout 5s` stops NeferWL after 5 seconds. | |||
| 171 | + | ||||
| 172 | + | `--input <path>`, or `--input -` for stdin, plays an input script. One command per line: | |||
| 173 | + | ||||
| 174 | + | - `type <text>` | |||
| 175 | + | - `key Super+Return` (Shift, Ctrl and Alt also work, as does a bare key) | |||
| 176 | + | - `sleep 1s` | |||
| 177 | + | - `move <x> <y>` (output coordinates) | |||
| 178 | + | - `click [left|right|middle]`, `down <button>`, `up <button>` | |||
| 179 | + | ||||
| 180 | + | The headless backend does not draw the cursor. After a layout change, pointer focus updates on the next move. | |||
| 189 | 181 | ||||
| 190 | ## Why not Rust? | 182 | ## Why not Rust? | ||
| 191 | 183 | ||||
| 192 | - | Most Wayland compositors are written in C (wlroots, Sway, Mutter), C++ (KWin, Hyprland, gamescope) or Rust (niri, COSMIC). NeferWL is written in Go, by choice: | 184 | + | Most Wayland compositors are written in C (wlroots, Sway, Mutter), C++ (KWin, Hyprland, gamescope) or Rust (niri, COSMIC). NeferWL uses Go on purpose: |
| 193 | 185 | ||||
| 194 | - | - **Simple and readable.** Go is small and explicit. Goroutines and channels fit a compositor where each part (input, outputs, clients) owns its own state. | 186 | + | - **Readable.** Go is small and explicit. Goroutines and channels fit a compositor where input, each output and the clients own their own state. |
| 195 | - | - **Fast builds, easy tooling.** A full build takes seconds; tests, the race detector, profiling and formatting come with the language. | 187 | + | - **Tooling.** A full build takes seconds, and tests, the race detector, profiling and formatting come with the language. |
| 196 | - **No cgo.** NeferWL builds with `CGO_ENABLED=0`: libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego). | 188 | - **No cgo.** NeferWL builds with `CGO_ENABLED=0`: libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego). | ||
| 197 | - | - **Fast enough.** With care for allocations and the garbage collector, the hot paths (input, rendering, buffer handling) perform close to native code. | 189 | + | - **Speed.** With care for allocations and the garbage collector, input, rendering and buffer handling run close to native code. |
| 190 | + | ||||
| 191 | + | I like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, [purego-libwayland](https://github.com/bnema/purego-libwayland), [purego-vulkan](https://github.com/bnema/purego-vulkan) and [wlturbo](https://github.com/bnema/wlturbo), and other Go projects can use them. | |||
| 192 | + | ||||
| 193 | + | Go has no borrow checker, so the compiler does not catch memory and concurrency mistakes as Rust's does. NeferWL compensates with strict ownership (one goroutine owns each piece of state), the race detector on every test run, and protocol tests against real Wayland clients. | |||
| 194 | + | ||||
| 195 | + | ## Developing the bindings | |||
| 198 | 196 | ||||
| 199 | - | As a software engineer, my passion is bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way: purego-libwayland, purego-vulkan and wlturbo, to be published for others to reuse. Building them is part of the fun. | 197 | + | NeferWL uses tagged releases of [purego-libwayland](https://github.com/bnema/purego-libwayland) and [purego-vulkan](https://github.com/bnema/purego-vulkan). To change a binding and NeferWL together, clone the binding next to NeferWL and use a Go workspace, which is not committed: |
| 198 | + | ||||
| 199 | + | ```sh | |||
| 200 | + | go work init . ../purego-libwayland ../purego-vulkan | |||
| 201 | + | ``` | |||
| 200 | 202 | ||||
| 201 | - | What Go does not give us is Rust's borrow checker: memory and concurrency mistakes are not caught at compile time. NeferWL makes up for it with strict ownership (one goroutine owns each piece of state), the race detector on every test run, and protocol tests against real Wayland clients. | 203 | + | `make pkg` ignores the workspace and builds the versions in `go.mod`. Tag the binding change and update `go.mod` before packaging. |
| 202 | 204 | ||||
| 203 | ## Contributing | 205 | ## Contributing | ||
| 204 | 206 | ||||
| 205 | - | Contributions are welcome. Open an issue first, for a bug fix or a feature, and wait until it is accepted before sending a pull request. Pull requests without an accepted issue will be closed. | 207 | + | Contributions are welcome. Open an issue first, for a bug fix or a feature, and wait until it is accepted before you send a pull request. Pull requests without an accepted issue are closed. |
| 206 | 208 | ||||
| 207 | - | Before a pull request, `make check`, `make race`, `make mocks-check` and `staticcheck ./...` must pass. Repository rules are in [AGENTS.md](AGENTS.md). | 209 | + | Before you open a pull request, `make check`, `make race`, `make mocks-check` and `staticcheck ./...` must pass. Repository rules are in [AGENTS.md](AGENTS.md). |
| 208 | 210 | ||||
| 209 | ## License | 211 | ## License | ||
| 210 | 212 | ||||