docs: trim user guides to essential, actionable content
- commit
- 2d8529beadef772cf8641e1275c05d0567d3bc45
- author
- brice · · 2026-10-14 03:48 UTC
- parent
- a67deb8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +267 -549
M README.md +1 -1
| @@ -362,7 +362,7 @@ The command comes from `terminal`, then `$TERMINAL`, then `foot`. | |||
| 362 | 362 | - [Session locking](docs/session-lock.md): external lockers, recovery and security limits. | |
| 363 | 363 | - [Screen capture](docs/capture.md): protocols, indicator, executable allowlist and portals. | |
| 364 | 364 | - [Headless mode](docs/headless.md): screenshots, input scripts and input-method testing without a screen. | |
| 365 | - | - [Performance](docs/performance.md): rendering, frame pacing, profiling and reference measurements. | |
| 365 | + | - [Performance](docs/performance.md): VRR, real-time priority, profiling and frame stats. | |
| 366 | 366 | ||
| 367 | 367 | ## Contributing | |
| 368 | 368 | ||
| @@ -362,7 +362,7 @@ The command comes from `terminal`, then `$TERMINAL`, then `foot`. | |||||
| 362 | - [Session locking](docs/session-lock.md): external lockers, recovery and security limits. | 362 | - [Session locking](docs/session-lock.md): external lockers, recovery and security limits. | ||
| 363 | - [Screen capture](docs/capture.md): protocols, indicator, executable allowlist and portals. | 363 | - [Screen capture](docs/capture.md): protocols, indicator, executable allowlist and portals. | ||
| 364 | - [Headless mode](docs/headless.md): screenshots, input scripts and input-method testing without a screen. | 364 | - [Headless mode](docs/headless.md): screenshots, input scripts and input-method testing without a screen. | ||
| 365 | - | - [Performance](docs/performance.md): rendering, frame pacing, profiling and reference measurements. | 365 | + | - [Performance](docs/performance.md): VRR, real-time priority, profiling and frame stats. |
| 366 | 366 | ||||
| 367 | ## Contributing | 367 | ## Contributing | ||
| 368 | 368 | ||||
M docs/capture.md +20 -73
| @@ -1,58 +1,25 @@ | |||
| 1 | 1 | # Screen capture | |
| 2 | 2 | ||
| 3 | - | NeferWL supports `zwlr_screencopy_manager_v1` (used by `grim`) and `ext_image_copy_capture_manager_v1` (used by recorders). Three rules apply to every capture: | |
| 3 | + | NeferWL supports `zwlr_screencopy_manager_v1` (used by `grim`) and `ext_image_copy_capture_manager_v1` (used by recorders). | |
| 4 | 4 | ||
| 5 | - | 1. A red indicator shows on screen while something is captured. | |
| 6 | - | 2. Only allowed executables may capture. | |
| 7 | - | 3. Sandboxed apps go through the desktop portal. | |
| 5 | + | - A red indicator shows on screen while something is captured. | |
| 6 | + | - Only allowed executables may capture. See [the allowlist](#the-allowlist). | |
| 7 | + | - Sandboxed apps (Flatpak) must go through the [portal](#portals). | |
| 8 | 8 | ||
| 9 | 9 | ## Sources | |
| 10 | 10 | ||
| 11 | - | With ext-image-copy-capture, a client can capture: | |
| 11 | + | With ext-image-copy-capture, a client can capture an output, a window, a workspace (even off screen, without bars) or a region of an output. The workspace and region sources use the NeferWL protocol in `internal/adapters/wayland/imagecapture/`. | |
| 12 | 12 | ||
| 13 | - | | Source | Protocol | Notes | | |
| 14 | - | |---|---|---| | |
| 15 | - | | An output | `ext_output_image_capture_source_manager_v1` | | | |
| 16 | - | | A window | `ext_foreign_toplevel_image_capture_source_manager_v1` | Rendered at its own size, even on another workspace or under a fullscreen window | | |
| 17 | - | | A workspace | `neferwl_image_capture_source_manager_v1.create_workspace_source` | Its real layout, without bars, even when not on screen | | |
| 18 | - | | A region of an output | `neferwl_image_capture_source_manager_v1.create_output_region_source` | In logical pixels, clipped to the output | | |
| 19 | - | ||
| 20 | - | The NeferWL protocol XML is in `internal/adapters/wayland/imagecapture/`. | |
| 21 | - | ||
| 22 | - | - Captures need Vulkan sync-file export; on devices without it, such as lavapipe, capture requests fail. | |
| 23 | - | - Captures are opaque 8-bit sRGB. HDR content is tone-mapped on the GPU. | |
| 13 | + | - Captures are 8-bit sRGB. HDR content is tone-mapped. | |
| 24 | 14 | - The cursor is never included. | |
| 25 | - | - Only one off-screen target (a window or hidden workspace) renders at a time. Other sessions wait for it, then fail. Off-screen targets are limited to 16384 px per side and 256 MiB per image. | |
| 26 | - | - A session stops when its output, workspace or window goes away. A window that unmaps and maps again is a new window: capture it again. | |
| 27 | - | - A frame whose target is briefly unavailable is retried for up to one second, so a short hiccup does not end a screencast. | |
| 28 | - | - `neferwl_workspace_frame_v1` (from `get_workspace_frame`) reports a workspace's position and size, which ext-workspace does not carry. | |
| 15 | + | - GPUs without Vulkan sync-file export (such as lavapipe) cannot capture. | |
| 16 | + | - `neferwl_capture_exclusion_manager_v1` lets a recorder keep its own HUD out of its frames. | |
| 29 | 17 | ||
| 30 | 18 | ## The indicator | |
| 31 | 19 | ||
| 32 | - | Every capture is visible, and no client can turn that off. NeferWL draws a 2-logical-pixel red border (`#ff3b30`) around the captured output, region or on-screen workspace. A captured window always shows a small red square in the top-right corner of its output, plus its border while it is on screen. A hidden workspace shows only the square. | |
| 33 | - | ||
| 34 | - | - A live session keeps the indicator on. A one-shot screenshot flashes it for one second. | |
| 35 | - | - The indicator is never in the captured image. | |
| 36 | - | - The indicator is on screen before or with the frame it captures. If it cannot be shown, a screencopy frame fails after 250 ms; an ext-image-copy-capture frame keeps retrying for up to 1 s. | |
| 37 | - | - While it shows, the output is composed (no direct scanout), and capturing the displayed frame costs a second composition. | |
| 38 | - | ||
| 39 | - | ## Hiding a recorder's own HUD | |
| 40 | - | ||
| 41 | - | `neferwl_capture_exclusion_manager_v1` keeps a recorder's HUD out of its own frames: | |
| 42 | - | ||
| 43 | - | 1. The recorder calls `get_exclusion` on its capture session and gets a token. It fails with `busy` if another exclusion is live. | |
| 44 | - | 2. The HUD attaches up to 4 layer surfaces (top or overlay, not yet mapped) with that token. | |
| 45 | - | 3. The HUD stays visible over fullscreen windows without taking keyboard focus. That session's frames leave it out; every other capture keeps it. | |
| 46 | - | ||
| 47 | - | The HUD must run as the same user and be allowed to capture itself. The exclusion ends with the session, or when the recorder destroys it; its HUD surfaces are then closed. | |
| 48 | - | ||
| 49 | - | ## Who may capture | |
| 50 | - | ||
| 51 | - | A client that may not capture gets its protocol's failure (`failed`, or `stopped` for a session). In order: | |
| 20 | + | A 2-pixel red border surrounds the captured output, region or workspace. A captured window adds a small red square in the top-right corner of its output. No client can turn it off, and it never appears in the captured image. | |
| 52 | 21 | ||
| 53 | - | 1. **Locked session:** nobody captures. See [Session locking](session-lock.md). | |
| 54 | - | 2. **Sandboxed client:** a client connected through `wp_security_context_v1` (Flatpak, for instance) never captures directly; it uses the portal. Sandboxed clients still see the capture globals, but their requests are refused. Other privileged protocols are not restricted. A sandbox engine must call `set_sandbox_engine` before `commit`, and a sandboxed client cannot create its own security context; both are protocol errors. | |
| 55 | - | 3. **Allowlist:** the executable must be listed. | |
| 22 | + | A screenshot flashes the indicator for one second; a recording keeps it on. | |
| 56 | 23 | ||
| 57 | 24 | ## The allowlist | |
| 58 | 25 | ||
| @@ -80,43 +47,23 @@ sudoedit /etc/neferwl/capture-allow | |||
| 80 | 47 | /opt/tools/bin/recorder | |
| 81 | 48 | ``` | |
| 82 | 49 | ||
| 83 | - | - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink. | |
| 84 | - | - Paths must match exactly; there are no globs. | |
| 50 | + | - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink. No globs. | |
| 85 | 51 | - A line with only `*` lets every client capture. | |
| 86 | - | - Changes apply within a fraction of a second, or about 2 s after creating `/etc/neferwl`. They apply to the next request and to the next frame of running sessions; images already delivered cannot be taken back. If the file cannot be watched, NeferWL logs a warning and changes need a restart. | |
| 87 | - | - If the file or `/etc/neferwl` is missing, the built-in list applies. | |
| 88 | - | - If the file is invalid or unsafe, no client may capture. The log names the problem. | |
| 52 | + | - Changes apply without a restart. | |
| 53 | + | - The file and every directory above it must be owned by root, not writable by group or others, and not symlinks. | |
| 54 | + | - If the file is invalid or unsafe, nobody may capture. The log says why. | |
| 55 | + | - Refused clients are logged as `capture refused`, with the pid and path. | |
| 56 | + | - Client checks need Linux 6.5 or later. On older kernels, nobody may capture unless the list is `*`. | |
| 89 | 57 | ||
| 90 | - | The path is fixed and not a config key: any program running as you could edit your config, but not a root-owned file. | |
| 58 | + | Never list an interpreter (`python`, `bash`, `node`): every script it runs could capture. Avoid listing files you own: any program running as you could replace them. | |
| 91 | 59 | ||
| 92 | - | ### The file must belong to root | |
| 93 | - | ||
| 94 | - | The file must be a regular file. It and every directory above it (`/`, `/etc`, `/etc/neferwl`) must be owned by root, not writable by group or others (sticky directories are accepted), and not symlinks. Otherwise no client may capture, even if `/` or `/etc` is the problem: the built-in list does not apply. | |
| 95 | - | ||
| 96 | - | ### How a client is identified | |
| 97 | - | ||
| 98 | - | By the executable actually running, not its name or app ID. NeferWL takes a pidfd of the client (`SO_PEERPIDFD`, Linux 6.5+) and checks `/proc/<pid>/exe` at every screencopy frame, every new ext session and every HUD attach. A process that exits, or whose executable was deleted or replaced, is refused. | |
| 99 | - | ||
| 100 | - | Frames of an open ext session reuse the executable found when the client was last checked. A program that `exec`s into another binary keeps its open sessions, and is judged by the new binary at its next request. | |
| 101 | - | ||
| 102 | - | On kernels older than 6.5, no client may capture unless the list is `*`. | |
| 103 | - | ||
| 104 | - | Each refused executable is logged once: `capture refused`, with the pid, path and reason. | |
| 105 | - | ||
| 106 | - | ### Limits | |
| 107 | - | ||
| 108 | - | - **Binaries you own.** Listing a file under your home trusts every program that can overwrite it. Prefer root-owned paths. | |
| 109 | - | - **Interpreters.** A script runs as its interpreter. Never list `python`, `bash` or `node`: every script would be allowed. | |
| 110 | - | - **Same-user attacks.** The allowlist stops unknown programs. It does not stop a program running as you from driving a listed one (`LD_PRELOAD`, `ptrace`, or simply asking it). The indicator still shows. | |
| 111 | - | - **What happens next.** A listed program can send its images anywhere. | |
| 60 | + | The allowlist stops unknown programs, not a program running as you that drives a listed one. The indicator still shows. | |
| 112 | 61 | ||
| 113 | 62 | ## Portals | |
| 114 | 63 | ||
| 115 | 64 | - **Screen sharing** in browsers and apps: install `xdg-desktop-portal-wlr`. | |
| 116 | 65 | - **Screenshots and recordings:** install [nefercap](https://github.com/bnema/nefercap). | |
| 117 | 66 | ||
| 118 | - | The packaged `neferwl-portals.conf` sends `ScreenCast` and `Screenshot` to the wlr portal and everything else to the GTK portal. You can share a whole output or a single window. | |
| 119 | - | ||
| 120 | - | NeferWL has no window picker. The wlr portal starts one itself, such as slurp, wofi or bemenu: see `chooser_type` and `chooser_cmd` in `man xdg-desktop-portal-wlr`. | |
| 67 | + | The packaged `neferwl-portals.conf` routes `ScreenCast` and `Screenshot` to the wlr portal. NeferWL has no window picker: set `chooser_type` and `chooser_cmd` in the wlr portal config to use slurp, wofi or bemenu (see `man xdg-desktop-portal-wlr`). | |
| 121 | 68 | ||
| 122 | - | Any program running as you can ask the portal over D-Bus, and the wlr portal takes screenshots without a dialog. The indicator still flashes each time. | |
| 69 | + | The wlr portal takes screenshots without asking, for any program running as you. The indicator still flashes. | |
| @@ -1,58 +1,25 @@ | |||||
| 1 | # Screen capture | 1 | # Screen capture | ||
| 2 | 2 | ||||
| 3 | - | NeferWL supports `zwlr_screencopy_manager_v1` (used by `grim`) and `ext_image_copy_capture_manager_v1` (used by recorders). Three rules apply to every capture: | 3 | + | NeferWL supports `zwlr_screencopy_manager_v1` (used by `grim`) and `ext_image_copy_capture_manager_v1` (used by recorders). |
| 4 | 4 | ||||
| 5 | - | 1. A red indicator shows on screen while something is captured. | 5 | + | - A red indicator shows on screen while something is captured. |
| 6 | - | 2. Only allowed executables may capture. | 6 | + | - Only allowed executables may capture. See [the allowlist](#the-allowlist). |
| 7 | - | 3. Sandboxed apps go through the desktop portal. | 7 | + | - Sandboxed apps (Flatpak) must go through the [portal](#portals). |
| 8 | 8 | ||||
| 9 | ## Sources | 9 | ## Sources | ||
| 10 | 10 | ||||
| 11 | - | With ext-image-copy-capture, a client can capture: | 11 | + | With ext-image-copy-capture, a client can capture an output, a window, a workspace (even off screen, without bars) or a region of an output. The workspace and region sources use the NeferWL protocol in `internal/adapters/wayland/imagecapture/`. |
| 12 | 12 | ||||
| 13 | - | | Source | Protocol | Notes | | 13 | + | - Captures are 8-bit sRGB. HDR content is tone-mapped. |
| 14 | - | |---|---|---| | |||
| 15 | - | | An output | `ext_output_image_capture_source_manager_v1` | | | |||
| 16 | - | | A window | `ext_foreign_toplevel_image_capture_source_manager_v1` | Rendered at its own size, even on another workspace or under a fullscreen window | | |||
| 17 | - | | A workspace | `neferwl_image_capture_source_manager_v1.create_workspace_source` | Its real layout, without bars, even when not on screen | | |||
| 18 | - | | A region of an output | `neferwl_image_capture_source_manager_v1.create_output_region_source` | In logical pixels, clipped to the output | | |||
| 19 | - | ||||
| 20 | - | The NeferWL protocol XML is in `internal/adapters/wayland/imagecapture/`. | |||
| 21 | - | ||||
| 22 | - | - Captures need Vulkan sync-file export; on devices without it, such as lavapipe, capture requests fail. | |||
| 23 | - | - Captures are opaque 8-bit sRGB. HDR content is tone-mapped on the GPU. | |||
| 24 | - The cursor is never included. | 14 | - The cursor is never included. | ||
| 25 | - | - Only one off-screen target (a window or hidden workspace) renders at a time. Other sessions wait for it, then fail. Off-screen targets are limited to 16384 px per side and 256 MiB per image. | 15 | + | - GPUs without Vulkan sync-file export (such as lavapipe) cannot capture. |
| 26 | - | - A session stops when its output, workspace or window goes away. A window that unmaps and maps again is a new window: capture it again. | 16 | + | - `neferwl_capture_exclusion_manager_v1` lets a recorder keep its own HUD out of its frames. |
| 27 | - | - A frame whose target is briefly unavailable is retried for up to one second, so a short hiccup does not end a screencast. | |||
| 28 | - | - `neferwl_workspace_frame_v1` (from `get_workspace_frame`) reports a workspace's position and size, which ext-workspace does not carry. | |||
| 29 | 17 | ||||
| 30 | ## The indicator | 18 | ## The indicator | ||
| 31 | 19 | ||||
| 32 | - | Every capture is visible, and no client can turn that off. NeferWL draws a 2-logical-pixel red border (`#ff3b30`) around the captured output, region or on-screen workspace. A captured window always shows a small red square in the top-right corner of its output, plus its border while it is on screen. A hidden workspace shows only the square. | 20 | + | A 2-pixel red border surrounds the captured output, region or workspace. A captured window adds a small red square in the top-right corner of its output. No client can turn it off, and it never appears in the captured image. |
| 33 | - | ||||
| 34 | - | - A live session keeps the indicator on. A one-shot screenshot flashes it for one second. | |||
| 35 | - | - The indicator is never in the captured image. | |||
| 36 | - | - The indicator is on screen before or with the frame it captures. If it cannot be shown, a screencopy frame fails after 250 ms; an ext-image-copy-capture frame keeps retrying for up to 1 s. | |||
| 37 | - | - While it shows, the output is composed (no direct scanout), and capturing the displayed frame costs a second composition. | |||
| 38 | - | ||||
| 39 | - | ## Hiding a recorder's own HUD | |||
| 40 | - | ||||
| 41 | - | `neferwl_capture_exclusion_manager_v1` keeps a recorder's HUD out of its own frames: | |||
| 42 | - | ||||
| 43 | - | 1. The recorder calls `get_exclusion` on its capture session and gets a token. It fails with `busy` if another exclusion is live. | |||
| 44 | - | 2. The HUD attaches up to 4 layer surfaces (top or overlay, not yet mapped) with that token. | |||
| 45 | - | 3. The HUD stays visible over fullscreen windows without taking keyboard focus. That session's frames leave it out; every other capture keeps it. | |||
| 46 | - | ||||
| 47 | - | The HUD must run as the same user and be allowed to capture itself. The exclusion ends with the session, or when the recorder destroys it; its HUD surfaces are then closed. | |||
| 48 | - | ||||
| 49 | - | ## Who may capture | |||
| 50 | - | ||||
| 51 | - | A client that may not capture gets its protocol's failure (`failed`, or `stopped` for a session). In order: | |||
| 52 | 21 | ||||
| 53 | - | 1. **Locked session:** nobody captures. See [Session locking](session-lock.md). | 22 | + | A screenshot flashes the indicator for one second; a recording keeps it on. |
| 54 | - | 2. **Sandboxed client:** a client connected through `wp_security_context_v1` (Flatpak, for instance) never captures directly; it uses the portal. Sandboxed clients still see the capture globals, but their requests are refused. Other privileged protocols are not restricted. A sandbox engine must call `set_sandbox_engine` before `commit`, and a sandboxed client cannot create its own security context; both are protocol errors. | |||
| 55 | - | 3. **Allowlist:** the executable must be listed. | |||
| 56 | 23 | ||||
| 57 | ## The allowlist | 24 | ## The allowlist | ||
| 58 | 25 | ||||
| @@ -80,43 +47,23 @@ sudoedit /etc/neferwl/capture-allow | |||||
| 80 | /opt/tools/bin/recorder | 47 | /opt/tools/bin/recorder | ||
| 81 | ``` | 48 | ``` | ||
| 82 | 49 | ||||
| 83 | - | - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink. | 50 | + | - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink. No globs. |
| 84 | - | - Paths must match exactly; there are no globs. | |||
| 85 | - A line with only `*` lets every client capture. | 51 | - A line with only `*` lets every client capture. | ||
| 86 | - | - Changes apply within a fraction of a second, or about 2 s after creating `/etc/neferwl`. They apply to the next request and to the next frame of running sessions; images already delivered cannot be taken back. If the file cannot be watched, NeferWL logs a warning and changes need a restart. | 52 | + | - Changes apply without a restart. |
| 87 | - | - If the file or `/etc/neferwl` is missing, the built-in list applies. | 53 | + | - The file and every directory above it must be owned by root, not writable by group or others, and not symlinks. |
| 88 | - | - If the file is invalid or unsafe, no client may capture. The log names the problem. | 54 | + | - If the file is invalid or unsafe, nobody may capture. The log says why. |
| 55 | + | - Refused clients are logged as `capture refused`, with the pid and path. | |||
| 56 | + | - Client checks need Linux 6.5 or later. On older kernels, nobody may capture unless the list is `*`. | |||
| 89 | 57 | ||||
| 90 | - | The path is fixed and not a config key: any program running as you could edit your config, but not a root-owned file. | 58 | + | Never list an interpreter (`python`, `bash`, `node`): every script it runs could capture. Avoid listing files you own: any program running as you could replace them. |
| 91 | 59 | ||||
| 92 | - | ### The file must belong to root | 60 | + | The allowlist stops unknown programs, not a program running as you that drives a listed one. The indicator still shows. |
| 93 | - | ||||
| 94 | - | The file must be a regular file. It and every directory above it (`/`, `/etc`, `/etc/neferwl`) must be owned by root, not writable by group or others (sticky directories are accepted), and not symlinks. Otherwise no client may capture, even if `/` or `/etc` is the problem: the built-in list does not apply. | |||
| 95 | - | ||||
| 96 | - | ### How a client is identified | |||
| 97 | - | ||||
| 98 | - | By the executable actually running, not its name or app ID. NeferWL takes a pidfd of the client (`SO_PEERPIDFD`, Linux 6.5+) and checks `/proc/<pid>/exe` at every screencopy frame, every new ext session and every HUD attach. A process that exits, or whose executable was deleted or replaced, is refused. | |||
| 99 | - | ||||
| 100 | - | Frames of an open ext session reuse the executable found when the client was last checked. A program that `exec`s into another binary keeps its open sessions, and is judged by the new binary at its next request. | |||
| 101 | - | ||||
| 102 | - | On kernels older than 6.5, no client may capture unless the list is `*`. | |||
| 103 | - | ||||
| 104 | - | Each refused executable is logged once: `capture refused`, with the pid, path and reason. | |||
| 105 | - | ||||
| 106 | - | ### Limits | |||
| 107 | - | ||||
| 108 | - | - **Binaries you own.** Listing a file under your home trusts every program that can overwrite it. Prefer root-owned paths. | |||
| 109 | - | - **Interpreters.** A script runs as its interpreter. Never list `python`, `bash` or `node`: every script would be allowed. | |||
| 110 | - | - **Same-user attacks.** The allowlist stops unknown programs. It does not stop a program running as you from driving a listed one (`LD_PRELOAD`, `ptrace`, or simply asking it). The indicator still shows. | |||
| 111 | - | - **What happens next.** A listed program can send its images anywhere. | |||
| 112 | 61 | ||||
| 113 | ## Portals | 62 | ## Portals | ||
| 114 | 63 | ||||
| 115 | - **Screen sharing** in browsers and apps: install `xdg-desktop-portal-wlr`. | 64 | - **Screen sharing** in browsers and apps: install `xdg-desktop-portal-wlr`. | ||
| 116 | - **Screenshots and recordings:** install [nefercap](https://github.com/bnema/nefercap). | 65 | - **Screenshots and recordings:** install [nefercap](https://github.com/bnema/nefercap). | ||
| 117 | 66 | ||||
| 118 | - | The packaged `neferwl-portals.conf` sends `ScreenCast` and `Screenshot` to the wlr portal and everything else to the GTK portal. You can share a whole output or a single window. | 67 | + | The packaged `neferwl-portals.conf` routes `ScreenCast` and `Screenshot` to the wlr portal. NeferWL has no window picker: set `chooser_type` and `chooser_cmd` in the wlr portal config to use slurp, wofi or bemenu (see `man xdg-desktop-portal-wlr`). |
| 119 | - | ||||
| 120 | - | NeferWL has no window picker. The wlr portal starts one itself, such as slurp, wofi or bemenu: see `chooser_type` and `chooser_cmd` in `man xdg-desktop-portal-wlr`. | |||
| 121 | 68 | ||||
| 122 | - | Any program running as you can ask the portal over D-Bus, and the wlr portal takes screenshots without a dialog. The indicator still flashes each time. | 69 | + | The wlr portal takes screenshots without asking, for any program running as you. The indicator still flashes. |
M docs/config.md +168 -308
| @@ -1,91 +1,93 @@ | |||
| 1 | 1 | # Configuration | |
| 2 | 2 | ||
| 3 | - | NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). `neferwl --config <path>` selects another file; `neferwl validate-config [path]` checks one without starting the compositor. | |
| 3 | + | NeferWL reads `~/.config/neferwl/config` (or `$XDG_CONFIG_HOME/neferwl/config`). Changes apply when you save, except `startup` and `xwayland`, which apply at the next start. | |
| 4 | + | ||
| 5 | + | ```sh | |
| 6 | + | neferwl --config <path> # use another file | |
| 7 | + | neferwl validate-config [path] # check a file | |
| 8 | + | ``` | |
| 4 | 9 | ||
| 5 | 10 | ## Format | |
| 6 | 11 | ||
| 7 | - | - One `key = value` per line. A line starting with `#` is a comment, and so is a `#` after a space, unless it is inside a quoted word (`"a # b"`). | |
| 12 | + | - One `key = value` per line. `#` starts a comment. | |
| 8 | 13 | - Lists are comma-separated; booleans are `on` / `off`. | |
| 9 | - | - 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. A duplicate key warns and the last one wins. | |
| 10 | - | - Every key applies live when the file is saved; windows stay open. `startup` and `xwayland` take effect at the next start. | |
| 14 | + | - An invalid line logs a warning and keeps the default. If a key appears twice, the last one wins. | |
| 15 | + | ||
| 16 | + | See [examples/config](../examples/config) for every key with its default. | |
| 11 | 17 | ||
| 12 | 18 | ## Keys | |
| 13 | 19 | ||
| 14 | 20 | | Key | Default | Meaning | | |
| 15 | 21 | |---|---|---| | |
| 16 | 22 | | `keyboard.layout`, `.variant`, `.options` | empty | xkb names, e.g. `fr`, `caps:escape` | | |
| 17 | - | | `keyboard.repeat-rate` / `repeat-delay` | `25` / `600` | Key repeat per second / delay in ms | | |
| 18 | - | | `keyboard.cmd` | `super` | Modifier that `cmd` means in binds: `super`, `alt` or `ctrl` | | |
| 19 | - | | `terminal` | `$TERMINAL`, or `foot` if unset/empty | Command for automatic requests and `spawn-terminal`; whitespace-separated | | |
| 20 | - | | `terminal.auto-open` | `first` | `first`: one request for the initial numbered workspace per session; `all`: visible empty workspaces without slots, with retry protection; `off`: none. `--no-terminal` disables automatic requests for this run only | | |
| 21 | - | | `startup` | none | Commands run once at session start; separate several with `;` or repeat the key | | |
| 23 | + | | `keyboard.repeat-rate` / `.repeat-delay` | `25` / `600` | Repeats per second / delay in ms | | |
| 24 | + | | `keyboard.cmd` | `super` | Modifier meant by `cmd` in binds: `super`, `alt` or `ctrl` | | |
| 25 | + | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | |
| 26 | + | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | |
| 27 | + | | `startup` | none | Commands run at session start, separated by `;` | | |
| 22 | 28 | | `xwayland` | `xwayland-satellite` | X11 support; `off` disables it | | |
| 23 | - | | `background` | `#111111` | Solid background color | | |
| 24 | - | | `border.width` | `2` | Lines between visible neighboring tiles and around non-fullscreen floats, in logical pixels; `0` hides them | | |
| 25 | - | | `floating.dim` | `0.3` | Black veil over tiles and lower layers when a native floating window above the tiles or the stash is visible, and over the background and lower layers behind the overview; opacity 0 to 1 | | |
| 26 | - | | `stash.width` | `80` | Width of the selected stashed window, over the tiles, in percent of the usable width; 10 to 90 (`80%` works too). The rest is split between both sides, where the neighbors show. Its height is 80% of the usable height | | |
| 27 | - | | `stash.gap` | `2` | Space between the selected stashed window and its neighbors, in percent of the usable width; 0 to 10. The neighbors show from there to the screen edge: with the defaults, 8% of each. A gap as wide as the side margin hides them | | |
| 28 | - | | `stash.dim` | `0.5` | Black veil over the stash neighbors, borders included; opacity 0 to 1 | | |
| 29 | - | | `stash.capture` | `on` | `on`: a window opened while the stash is shown joins its end, selected; `toggle-window-stash` tiles it as a new column. `off`: new windows always tile | | |
| 30 | - | | `border.active` / `border.inactive` | `#808080` / `#111111` | Colors for focused / other window lines; only the focused output lights up | | |
| 29 | + | | `background` | `#111111` | Background color | | |
| 30 | + | | `border.width` | `2` | Border width in pixels; `0` hides borders | | |
| 31 | + | | `border.active` / `.inactive` | `#808080` / `#111111` | Border colors of the focused / other windows | | |
| 32 | + | | `floating.dim` | `0.3` | Darkening behind floating windows and the overview, 0 to 1 | | |
| 31 | 33 | | `layout.gaps` | `0` | Space between windows | | |
| 32 | - | | `layout.max-columns` | `2` | Columns that share the screen before the overflow applies. It also sets the cell: a column is `N` cells wide, a cell being `1/max-columns` of the usable width (see [Column widths](#column-widths)) | | |
| 33 | - | | `layout.overflow` | `cascade` | Past the max: `cascade` into vertically scrolling bands, `scroll` right, or `fixed` spiral split | | |
| 34 | - | | `layout.<output>.max-columns`, `.overflow` | layout values | Per-screen layout: connector (`DP-2`) or monitor key | | |
| 35 | - | | `touchpad.natural-scroll` | `off` | Content follows the fingers, for two-finger scroll and three-finger swipes (see [Touchpad](#touchpad)) | | |
| 36 | - | | `touchpad.tap` | `on` | Tap to click: one finger left, two right, three middle | | |
| 37 | - | | `touchpad.accel-speed` | `0` | Touchpad pointer speed, from `-1` (slowest) to `1` (fastest) | | |
| 38 | - | | `touchpad.accel-profile` | `adaptive` | `adaptive`: faster finger moves go further; `flat`: constant speed | | |
| 39 | - | | `touchpad.left-handed` | `off` | Swaps the left and right touchpad buttons | | |
| 40 | - | | `touchpad.scroll-factor` | `1` | Multiplies two-finger scroll distance, above `0` up to `10` | | |
| 41 | - | | `mouse.natural-scroll` | `off` | Wheel scrolling moves the content with the wheel instead of against it. `mouse.*` applies to every pointer that is not a touchpad: mice, trackballs, trackpoints | | |
| 42 | - | | `mouse.accel-speed` | `0` | Mouse pointer speed, from `-1` (slowest) to `1` (fastest) | | |
| 43 | - | | `mouse.accel-profile` | `adaptive` | `adaptive`: faster moves go further; `flat`: constant speed | | |
| 44 | - | | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons | | |
| 45 | - | | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` | | |
| 46 | - | | `animations` | `off` | **Experimental.** `on` animates changes; `off` makes every change instant and the three-finger swipe no longer follows the fingers: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview and the column switcher cards, windows opening and closing, the stash showing and hiding, the focus pulse | | |
| 47 | - | | `animations.speed` | `normal` | Speed of every spring (not the focus pulse): `slow`, `normal` or `fast` (a view move lands in about 260, 180 or 130 ms), or a number relative to `normal`, from `0.1` (10× slower) to `10` (10× faster) | | |
| 34 | + | | `layout.max-columns` | `2` | Columns that share the screen | | |
| 35 | + | | `layout.overflow` | `cascade` | What happens past `max-columns`: [`cascade`](#cascade) into bands below, `scroll` right, or `fixed` spiral split | | |
| 36 | + | | `layout.<output>.max-columns`, `.overflow` | layout values | Per-output layout | | |
| 37 | + | | `stash.width` | `80` | Width of the selected [stashed](#stash) window, in % of the screen (10–90) | | |
| 38 | + | | `stash.gap` | `2` | Gap around the selected stashed window, in % (0–10) | | |
| 39 | + | | `stash.dim` | `0.5` | Darkening of the stash neighbors, 0 to 1 | | |
| 40 | + | | `stash.capture` | `on` | A window opened while the stash shows joins the stash | | |
| 41 | + | | `touchpad.natural-scroll` | `off` | Content follows the fingers | | |
| 42 | + | | `touchpad.tap` | `on` | Tap to click | | |
| 43 | + | | `touchpad.accel-speed` | `0` | Pointer speed, `-1` to `1` | | |
| 44 | + | | `touchpad.accel-profile` | `adaptive` | `adaptive` or `flat` | | |
| 45 | + | | `touchpad.left-handed` | `off` | Swap left and right buttons | | |
| 46 | + | | `touchpad.scroll-factor` | `1` | Scroll speed multiplier, up to `10` | | |
| 47 | + | | `mouse.natural-scroll`, `.accel-speed`, `.accel-profile`, `.left-handed` | as touchpad | Same, for mice, trackballs and trackpoints | | |
| 48 | + | | `cursor.hide-after` | `5s` | Hide the cursor after this long without motion, or `off` | | |
| 49 | + | | `animations` | `off` | **Experimental.** Animate layout changes and make swipes follow the fingers | | |
| 50 | + | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | |
| 48 | 51 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | |
| 49 | - | | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone outside panels (e.g. a maximized column), the overview, the column switcher cards and a window focused as it opens. Off while `animations = off` | | |
| 50 | - | | `focus.effect` | `screen` | What the focus indicator draws: `screen` brightens the window slightly, visible on black too. HDR (PQ) client content is not brightened on HDR outputs | | |
| 51 | - | | `focus.strength` | `0.04` | Peak of the focus effect, `0.01` to `0.2`: black reaches about 10/255 at `0.04` | | |
| 52 | - | | `workspace.<name>.*` | none | Named workspaces are outside the numbered list; use the overview to show an occupied one, or a `workspace <name>` bind to show it and toggle back | | |
| 53 | - | | `workspace.<name>.monitor` | focused output | Home monitor: connector (`DP-2`) or monitor key. A `workspace <name>` bind focuses it there. Unset, or while home is absent, the workspace comes to the focused output; it returns home when it reconnects | | |
| 54 | - | | `workspace.<name>.size` | `inherit` | Logical `WxH` viewport, centered on the monitor; inherits monitor size by default and always keeps its scale | | |
| 55 | - | | `workspace.<name>.max-columns`, `.overflow` | screen values | Per-workspace layout | | |
| 56 | - | | `workspace.<name>.column.<N>` | none | Slot: `<cells>, <command>`; cells is a whole number from 1 to 16 | | |
| 57 | - | | `rule.<name>.*` | none | Window rules: place an app's windows by app ID when they first open; see [Window rules](#window-rules) | | |
| 58 | - | | `output.<name>` | preferred | `WxH`, `WxH@Hz`, `preferred` or `off` | | |
| 52 | + | | `focus.animation` | `pulse` | Brief brightening of a newly focused window, or `off`. Needs `animations = on` | | |
| 53 | + | | `focus.strength` | `0.04` | Strength of the focus pulse, `0.01` to `0.2` | | |
| 54 | + | | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace | | |
| 55 | + | | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` | | |
| 56 | + | | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout | | |
| 57 | + | | `workspace.<name>.column.<N>` | none | Column slot: `<cells>, <command>` | | |
| 58 | + | | `rule.<name>.*` | none | [Window rules](#window-rules) | | |
| 59 | + | | `output.<name>` | `preferred` | `WxH`, `WxH@Hz`, `preferred` or `off` | | |
| 59 | 60 | | `output.<name>.scale` | `1` | 1 to 4, e.g. `1.5` or `4/3` | | |
| 60 | - | | `output.<name>.transform` | `normal` | `normal`, `90`, `180`, `270`, `flipped`, `flipped-90`, `flipped-180`, `flipped-270`; 90/270 swap the output's width and height | | |
| 61 | - | | `output.<name>.primary` | `off` | Gets focus and pointer at startup | | |
| 62 | - | | `output.<name>.right-of` / `.left-of` / `.above` / `.below` | automatic | Connector to place the output against, e.g. `DP-1` | | |
| 63 | - | | `output.<name>.offset` | `0` | Logical px along the shared edge, −65536 to 65536; see [Output placement](#output-placement) | | |
| 64 | - | | `output.<name>.hdr` | `off` | Turns on HDR10 on capable outputs; otherwise falls back to SDR (reason in the log) | | |
| 65 | - | | `output.<name>.sdr-brightness` | `203` | How bright SDR desktop content appears in HDR, in nits (80–1000) | | |
| 66 | - | | `render.direct-scanout` | `on` | Fullscreen buffers straight to the display | | |
| 67 | - | | `render.tearing` / `render.vrr` | `on` / `on` | Tearing: games in direct scanout. VRR: any fullscreen window covering the output | | |
| 68 | - | | `render.vrr-flip-gap` | `1ms` | Under VRR, a game frame commits at least this long after the previous flip, so it does not wait for the panel's slowest refresh (0 to 10ms; `0` turns it off) | | |
| 69 | - | | `performance.realtime` | `on` | Request real-time scheduling for output and input threads | | |
| 70 | - | | `log.level` / `log.debug` | `info` / empty | Log level / debug components or `all` | | |
| 71 | - | | `bind.<keys>` | see below | Action for a key combo; `none` removes a default | | |
| 61 | + | | `output.<name>.transform` | `normal` | `90`, `180`, `270`, `flipped`, `flipped-90`, … | | |
| 62 | + | | `output.<name>.primary` | `off` | Focused at startup | | |
| 63 | + | | `output.<name>.right-of`, `.left-of`, `.above`, `.below`, `.offset` | automatic | [Output placement](#output-placement) | | |
| 64 | + | | `output.<name>.hdr` | `off` | [HDR](#hdr) on capable displays | | |
| 65 | + | | `output.<name>.sdr-brightness` | `203` | Brightness of SDR content in HDR, in nits (80–1000) | | |
| 66 | + | | `render.direct-scanout` | `on` | Send fullscreen windows straight to the display | | |
| 67 | + | | `render.tearing` | `on` | Allow tearing for fullscreen games | | |
| 68 | + | | `render.vrr` | `on` | Variable refresh rate for fullscreen windows | | |
| 69 | + | | `render.vrr-flip-gap` | `1ms` | See [Performance](performance.md#vrr) | | |
| 70 | + | | `performance.realtime` | `on` | Real-time scheduling | | |
| 71 | + | | `log.level` / `log.debug` | `info` / empty | Log level / debug categories or `all` | | |
| 72 | + | | `bind.<keys>` | see [Default binds](#default-binds) | [Binds](#binds) | | |
| 73 | + | ||
| 74 | + | `<output>` is a connector such as `DP-2`, or a monitor key (`make model serial`). | |
| 72 | 75 | ||
| 73 | 76 | ## Window rules | |
| 74 | 77 | ||
| 75 | - | A window rule places the windows of an app when they first open. Rules are flat keys named by a rule name (letters, digits, `-` or `_`): | |
| 78 | + | Rules place an app's windows when they first open. | |
| 76 | 79 | ||
| 77 | 80 | | Key | Meaning | | |
| 78 | 81 | |---|---| | |
| 79 | - | | `rule.<name>.app-id` | Required. A [Go regular expression](https://pkg.go.dev/regexp/syntax) matched against the whole app ID: `steam` does not match `steam_app_1`, `steam.*` does | | |
| 80 | - | | `rule.<name>.floating` | `on` floats the window (centred, free floating, sized by NeferWL); `off` tiles a fixed-size window that would float | | |
| 81 | - | | `rule.<name>.workspace` | A workspace number (1-based, past the last one means the last) or the name of a declared `workspace.<name>.*` workspace | | |
| 82 | - | | `rule.<name>.monitor` | A connector (`DP-2`) or a monitor key (`make model serial`); the focused output when it is not connected | | |
| 83 | - | | `rule.<name>.width` | Width of the window's new column in cells, a whole number from 1 to 16 (see [Column widths](#column-widths)); tiled windows only | | |
| 82 | + | | `rule.<name>.app-id` | Required. [Regular expression](https://pkg.go.dev/regexp/syntax) matching the whole app ID | | |
| 83 | + | | `rule.<name>.floating` | `on` to float, `off` to tile | | |
| 84 | + | | `rule.<name>.workspace` | Workspace number or name | | |
| 85 | + | | `rule.<name>.monitor` | Output | | |
| 86 | + | | `rule.<name>.width` | Column width in [cells](#column-widths), 1 to 16 | | |
| 84 | 87 | ||
| 85 | 88 | ```ini | |
| 86 | 89 | rule.games.app-id = steam_app_.* | |
| 87 | 90 | rule.games.workspace = 3 | |
| 88 | - | rule.games.monitor = DP-2 | |
| 89 | 91 | rule.chat.app-id = discord|org\.telegram\..* | |
| 90 | 92 | rule.chat.workspace = chat | |
| 91 | 93 | rule.chat.width = 1 | |
| @@ -93,22 +95,17 @@ rule.calc.app-id = org\.gnome\.Calculator | |||
| 93 | 95 | rule.calc.floating = on | |
| 94 | 96 | ``` | |
| 95 | 97 | ||
| 96 | - | - Every rule that matches applies, in file order; a later rule wins for each field it sets. A generic rule can set `monitor` and a specific one `width`. | |
| 97 | - | - Rules apply once, when the window first maps. A later app ID change, a window that maps again, or a config reload moves nothing. Reloading changes rules for windows that open afterwards. | |
| 98 | - | - A rule never moves you. The window opens on its target workspace without taking the focus or switching the workspace or monitor you are on. When the target is the workspace on screen of the focused output, the window opens as any new window does. | |
| 99 | - | - A named workspace stays on its own monitor: `monitor` is ignored next to a named `workspace`. A name nobody declared warns and the window opens as usual. | |
| 100 | - | - A window started for a slot (`workspace.<name>.column.N`) goes to its slot, and a dialog stays over its parent: rules do not apply to them. | |
| 101 | - | - An invalid value, a bad regular expression or a missing `app-id` logs a warning with the line, and the rule is ignored. An invalid value that repeats a key already set keeps the earlier value, like any other key. | |
| 102 | - | ||
| 103 | - | To find an app ID, look at `app_id` in `neferwl state | jq '.windows[] | {id, app_id}'`, or at the `window mapped` line in the log. X11 apps run through xwayland-satellite get their app ID from `WM_CLASS`; it may be empty, and an app that sets it after opening is not matched. | |
| 98 | + | - Every matching rule applies, in file order; later rules win. | |
| 99 | + | - A rule never moves your focus or view. | |
| 100 | + | - To find an app ID: `neferwl state | jq '.windows[].app_id'`. | |
| 104 | 101 | ||
| 105 | 102 | ## Output placement | |
| 106 | 103 | ||
| 107 | - | Without relations, outputs sit left to right in config order, top-aligned. A relation places an output against another one; `offset` slides it along the shared edge so the pointer crosses where the screens really meet. | |
| 104 | + | By default, outputs sit left to right in config order. Place one against another, and use `offset` (logical pixels) to align the edges: | |
| 108 | 105 | ||
| 109 | 106 | ```ini | |
| 110 | 107 | output.DP-2.right-of = DP-1 | |
| 111 | - | output.DP-2.offset = 360 # DP-2 starts 360 px lower than DP-1's top | |
| 108 | + | output.DP-2.offset = 360 # DP-2 starts 360 px lower than DP-1 | |
| 112 | 109 | ``` | |
| 113 | 110 | ||
| 114 | 111 | ``` | |
| @@ -120,136 +117,55 @@ output.DP-2.offset = 360 # DP-2 starts 360 px lower than DP-1's top | |||
| 120 | 117 | └────────────┘ | |
| 121 | 118 | ``` | |
| 122 | 119 | ||
| 123 | - | Placement, offsets and monitor navigation use the rotated size: a 2560x1440 output with `transform = 90` is 1440 wide and 2560 tall at scale 1. Rotated outputs always compose, with no direct scanout or overlay planes. | |
| 124 | - | ||
| 125 | - | Rules: | |
| 126 | - | ||
| 127 | - | - `offset` moves down for `right-of`/`left-of` and right for `above`/`below`; negative moves the other way. | |
| 128 | - | - Sizes are logical: the layout follows scale changes on its own. | |
| 129 | - | - An output whose reference is not connected is placed automatically until the reference returns. | |
| 130 | - | - Cycles and invalid values are ignored with a warning in the log. | |
| 131 | - | - Positions and transforms set by kanshi, wdisplays or wlr-randr win until the next config reload. | |
| 132 | - | - `focus-monitor-*`, `move-workspace-to-monitor-*` and column moves past the edge go to the output on that side. | |
| 133 | - | ||
| 134 | - | Two rows of three, with DP-1 as the base: | |
| 135 | - | ||
| 136 | - | ``` | |
| 137 | - | ┌────────┐┌────────────┐┌────────┐ | |
| 138 | - | │ DP-4 ││ HDMI-1 ││ DP-5 │ | |
| 139 | - | └────────┘└────────────┘└────────┘ | |
| 140 | - | ┌────────┐┌────────────┐┌────────┐ | |
| 141 | - | │ DP-2 ││ DP-1 ││ DP-3 │ | |
| 142 | - | └────────┘└────────────┘└────────┘ | |
| 143 | - | ``` | |
| 144 | - | ||
| 145 | - | ```ini | |
| 146 | - | # Bottom row: 1080p sides, bottoms aligned with DP-1 (1440 - 1080 = 360) | |
| 147 | - | output.DP-2.left-of = DP-1 | |
| 148 | - | output.DP-2.offset = 360 | |
| 149 | - | output.DP-3.right-of = DP-1 | |
| 150 | - | output.DP-3.offset = 360 | |
| 151 | - | ||
| 152 | - | # Top row: 1200p sides, bottoms aligned with HDMI-1 (1440 - 1200 = 240) | |
| 153 | - | output.HDMI-1.above = DP-1 | |
| 154 | - | output.DP-4.left-of = HDMI-1 | |
| 155 | - | output.DP-4.offset = 240 | |
| 156 | - | output.DP-5.right-of = HDMI-1 | |
| 157 | - | output.DP-5.offset = 240 | |
| 158 | - | ``` | |
| 120 | + | `offset` moves down for `right-of`/`left-of`, and right for `above`/`below`. kanshi, wdisplays and wlr-randr override these settings until the next reload. | |
| 159 | 121 | ||
| 160 | 122 | ## Workspace size | |
| 161 | 123 | ||
| 162 | - | A workspace inherits its monitor size unless `workspace.<name>.size` sets a logical `WxH`. Its viewport is centered, and columns, maximization, floating windows and fullscreen use that real layout size—not scaled overview previews. Oversized dimensions are limited to the monitor; reserved layer zones still reduce usable space. Monitor mode and scale stay unchanged. Omit `size` or use `inherit` to restore the default. Workspace transitions involving a smaller viewport switch without the vertical slide, keeping scrolling columns inside their frame. Fullscreen inside a smaller viewport uses composition rather than output-covering direct scanout, so fullscreen-game VRR/tearing eligibility does not apply. | |
| 124 | + | `workspace.<name>.size` gives a named workspace a smaller centered viewport, for example to present at 1080p on a 4K screen: | |
| 163 | 125 | ||
| 164 | 126 | ```ini | |
| 165 | 127 | workspace.presentation.size = 1920x1080 | |
| 166 | - | bind.Cmd+p = workspace presentation | |
| 128 | + | bind.cmd+p = workspace presentation | |
| 167 | 129 | ``` | |
| 168 | 130 | ||
| 169 | 131 | ## Touchpad | |
| 170 | 132 | ||
| 171 | - | With `animations = on`, a three-finger swipe follows the fingers and moves one step at most, so a quick swipe never skips a column or a workspace: | |
| 172 | - | ||
| 173 | - | - Left or right scrolls the columns (`scroll` overflow) to the next column edge; the focus moves to a column fully on screen. | |
| 174 | - | - Up or down slides to the next numbered workspace in scroll and fixed layouts. | |
| 175 | - | - In [Cascade](#cascade), up/down scrolls one band and left/right switches workspaces. | |
| 176 | - | ||
| 177 | - | The view sticks a little near each step and catches up between them. Past the next step it resists; lift and swipe again to go further. When the fingers lift, a quick swipe goes on to the next step and a slow one settles on the closest. | |
| 178 | - | ||
| 179 | - | In scroll and fixed layouts with `natural-scroll = off`, a swipe left shows the columns to the left and a swipe up shows the workspace above. `natural-scroll = on` moves the content with the fingers, so both are reversed. | |
| 180 | - | ||
| 181 | - | With `animations = off`, nothing moves during the swipe: when the fingers lift, a quick or long enough swipe navigates content or workspaces along the layout's axes. Cascade band swipes stop at the first and last band. | |
| 182 | - | ||
| 183 | - | Over the shown stash a sideways swipe slides it with the fingers, with a stop on each window, and moves one window at most. In scroll and fixed layouts where the view cannot scroll (a floating or fullscreen window, fixed overflow, or a named workspace), a quick swipe runs `focus-column-left/right` or `focus-workspace-prev/next` when the fingers lift. Cascade keeps horizontal workspace navigation; vertical band swipes do nothing while a floating or fullscreen window has focus. | |
| 184 | - | ||
| 185 | - | Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia. | |
| 133 | + | | Gesture | Action | | |
| 134 | + | |---|---| | |
| 135 | + | | Three fingers left/right | Scroll columns (switch workspace in cascade) | | |
| 136 | + | | Three fingers up/down | Switch workspace (scroll bands in cascade) | | |
| 137 | + | | Four fingers up | Open the overview, or hide the stash | | |
| 138 | + | | Four fingers down | Close the overview, or show/hide the stash | | |
| 186 | 139 | ||
| 187 | - | Three- and four-finger swipes belong to the compositor. Every other touchpad gesture goes to the window under the pointer through `zwp_pointer_gestures_v1` (version 3): swipes of other finger counts, pinch (zoom and rotation) and hold. A gesture stays with the window that had the pointer when it began; it ends as cancelled if the pointer moves to another window or the window closes. While the session is locked, only the lock screen gets gestures. | |
| 140 | + | A swipe moves one step at most. Other gestures (pinch, hold, two-finger scroll) go to the app under the pointer. | |
| 188 | 141 | ||
| 189 | 142 | ## HDR | |
| 190 | 143 | ||
| 191 | - | `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits. | |
| 192 | - | ||
| 193 | - | HDR-aware Wayland clients, including games through Vulkan WSI, browsers, and video players, can present HDR through `wp_color_manager_v1` and `wp_color_representation_v1`. Some apps require HDR to be enabled in their own settings. X11 clients stay SDR. | |
| 144 | + | `output.<name>.hdr = on` sends HDR10 to displays that support it. If the display, driver or GPU cannot, NeferWL falls back to SDR and logs why. NVIDIA is not supported yet. | |
| 194 | 145 | ||
| 195 | - | Hardware-decoded video can use NV12 (8-bit) or P010 (10-bit) DMA-BUF subsurfaces with BT.601/709/2020 coefficients, full or limited range, and chroma-location type 0. The Vulkan device must support sampling the format/modifier with per-plane views; planes in one DMA-BUF use one memory import, while separate plane buffers additionally require Vulkan DISJOINT support. Unsupported formats are not offered. Confirm hardware decoding and the advertised DMA-BUF formats when testing an app. | |
| 196 | - | ||
| 197 | - | - Fullscreen HDR buffers (10-bit, PQ) go straight to the display unchanged. | |
| 198 | - | - Opaque SDR (sRGB) buffers also skip composition on HDR outputs when the KMS plane has a colour pipeline (`DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE`): sRGB curve, `sdr-brightness`, BT.709 to BT.2020, PQ, the same maths as composition. This applies to a fullscreen window (direct scanout) and to a lone window on an overlay plane. The compositor looks for that chain on each plane, whatever the driver; amdgpu on recent kernels has it. Without it, with alpha, with YUV or extended-linear content, SDR windows are composed, and the log gives the reason (`hdr_sdr_content`, `yuv`). When the driver refuses a pipeline (tested once per plane, buffer format and cursor state), the window is composed with reason `color_refused`. | |
| 199 | - | - Composed frames and every modeset leave the planes' pipeline on Bypass. | |
| 200 | - | - Every modeset and exit clear the CRTC's `DEGAMMA_LUT`, `CTM` and `GAMMA_LUT`, which this compositor never uses: a gamma ramp left by another DRM master would otherwise distort every frame. | |
| 201 | - | - A VT switch resets the pipeline first: before the seat is disabled, each output that applies a pipeline commits Bypass (at most 100 ms are spent waiting for it, then the switch goes on and a warning is logged). The pipeline survives a master change, and a client without `DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE` cannot clear it. A crash or `kill -9` cannot do this reset: the next DRM master inherits the pipeline, with its colours distorted, until a client that sets the cap commits a modeset (or this compositor starts again). | |
| 202 | - | - Windowed and otherwise composed HDR content keeps its full range in a linear fp16 image before PQ output; the display tone-maps it according to its EDID metadata. SDR surfaces are blended in linear light on HDR outputs, so antialiased edges may look slightly different than sRGB-space blending. Dim veils (`floating.dim`, `stash.dim`) darken SDR white as much as on an SDR output; brighter HDR highlights stay a little brighter. | |
| 203 | - | - Screenshots and capture use opaque 8-bit sRGB. HDR capture applies GPU tone mapping and reduces out-of-sRGB gamut: values below 90% of SDR reference white stay unchanged, while brighter values are compressed to preserve highlight differences (SDR reference white maps to about 245/255 when capturing an HDR output, versus 255/255 on an SDR output). This capture-only transform does not change HDR display output. | |
| 204 | - | - The display gets the metadata from its own EDID, not the client's MaxCLL/MaxFALL. | |
| 205 | - | ||
| 206 | - | HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 composition and 10-bit export formats/features. Unsupported capabilities fall back to SDR. NVIDIA is not supported yet (tested status). | |
| 146 | + | - HDR-aware apps (games, browsers, video players) can show HDR content. Some need HDR turned on in their own settings. X11 apps stay SDR. | |
| 147 | + | - The desktop stays SDR, shown at `sdr-brightness` nits. | |
| 148 | + | - Fullscreen HDR windows go straight to the display. | |
| 149 | + | - Screenshots and captures are SDR (tone-mapped). | |
| 150 | + | - If NeferWL crashes, the next compositor may show distorted colours until it sets its own colour pipeline. | |
| 207 | 151 | ||
| 208 | 152 | ## Binds | |
| 209 | 153 | ||
| 210 | 154 | `bind.<keys> = <action>`. Modifiers: `cmd` (see `keyboard.cmd`), `shift`, `ctrl`, `alt`, `super`. | |
| 211 | 155 | ||
| 212 | - | - Keys are what the active layout prints: `cmd+é` on AZERTY. Write `=`, `#` and space by name: `cmd+equal`, `cmd+numbersign`, `cmd+space`. | |
| 213 | - | - `code:N` is a physical key (evdev code), the same on every layout: `code:2` to `code:10` are the digit row 1 to 9. | |
| 214 | - | - Other keys use their xkb keysym name, case-sensitive (`wev` shows it). Media keys have no default bind: | |
| 215 | - | ||
| 216 | - | ``` | |
| 217 | - | bind.XF86AudioRaiseVolume = spawn wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+ | |
| 218 | - | bind.XF86AudioLowerVolume = spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- | |
| 219 | - | bind.XF86AudioMute = spawn wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle | |
| 220 | - | bind.XF86MonBrightnessUp = spawn brightnessctl set +5% | |
| 221 | - | bind.XF86MonBrightnessDown = spawn brightnessctl set 5%- | |
| 222 | - | ``` | |
| 156 | + | - Keys are what your layout prints: `cmd+é` on AZERTY. Write `=`, `#` and space by name: `cmd+equal`, `cmd+numbersign`, `cmd+space`. | |
| 157 | + | - `code:N` is a physical key, the same on every layout: `code:2` to `code:10` are the digits 1 to 9. | |
| 158 | + | - Other keys use their xkb name (`wev` shows it). | |
| 223 | 159 | - `none` removes a default bind. | |
| 224 | - | - `spawn <command>` runs a program without a shell; `spawn a; b` runs several. See [Running commands](desktop.md#running-commands). | |
| 225 | - | ||
| 226 | - | ### Cascade | |
| 227 | - | ||
| 228 | - | Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | |
| 229 | - | ||
| 230 | - | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. A maximized column takes a band of its own: the columns after it continue in the following bands, and unmaximizing packs them back. It stays maximized while focus moves. | |
| 231 | - | ||
| 232 | - | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | |
| 233 | - | ||
| 234 | - | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. | |
| 235 | - | ||
| 236 | - | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next occupied workspace at the band's edge, then to the neighbor monitor, then to the trailing empty workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | |
| 237 | - | ||
| 238 | - | Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade. | |
| 160 | + | - `spawn <command>` runs a program without a shell. See [Running commands](desktop.md#running-commands). | |
| 239 | 161 | ||
| 240 | - | Cascade supports dropping onto tiles, but not gap insertion or edge scrolling during a drag. | |
| 241 | - | ||
| 242 | - | ### Column widths | |
| 243 | - | ||
| 244 | - | A cell is `1/max-columns` of the usable width. A column is N cells wide, from 1 to 16; an auto column (no cells set) shares the width equally. The same model serves every overflow: | |
| 245 | - | ||
| 246 | - | - `cascade`: the columns of a band share its row of cells. | |
| 247 | - | - `fixed`: the columns share the row while there are at most `max-columns` of them. Past that, the spiral applies and cells are ignored. | |
| 248 | - | - `scroll`: each column is sized on its own, up to `max-columns` cells; an auto column is the equal share of the columns in view. | |
| 249 | - | ||
| 250 | - | In a row, the auto columns share the cells the set ones leave; when no cell is left, an auto column counts for one. Columns with more cells than the row holds (after a smaller `max-columns`, or moved to another workspace) share the width in proportion. All-auto rows are laid out as equal shares. | |
| 251 | - | ||
| 252 | - | `cycle-column-width` gives the focused column one more cell, taking it from the widest other column of its row (the nearest, right side first, on a tie). A row that does not add up to `max-columns` cells is first spread evenly, the focused column taking the remainder, and that counts as the step. With no cell left to take, the row returns to equal shares. `set-column-width +N` repeats the grow without cycling; `-N` gives a cell to the right neighbor (the left one at the row's end), down to one cell. | |
| 162 | + | ```ini | |
| 163 | + | bind.XF86AudioRaiseVolume = spawn wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+ | |
| 164 | + | bind.XF86AudioLowerVolume = spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- | |
| 165 | + | bind.XF86AudioMute = spawn wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle | |
| 166 | + | bind.XF86MonBrightnessUp = spawn brightnessctl set +5% | |
| 167 | + | bind.XF86MonBrightnessDown = spawn brightnessctl set 5%- | |
| 168 | + | ``` | |
| 253 | 169 | ||
| 254 | 170 | ### Actions | |
| 255 | 171 | ||
| @@ -258,149 +174,93 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||
| 258 | 174 | | `spawn <command>` | Run a program | | |
| 259 | 175 | | `spawn-terminal` | Run `terminal` | | |
| 260 | 176 | | `close-window` | Close the focused window | | |
| 261 | - | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, and the app's own fullscreen requests are then refused until it is focused again. An activation of a window that can show over it keeps it fullscreen instead: one of its dialogs, or a stashed window or a dialog of one (and their dialogs), which shows the stash over it as `toggle-stash-visible` does, unless that stashed window asked for fullscreen while hidden: it then replaces the fullscreen window. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | |
| 262 | - | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | |
| 263 | - | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | |
| 264 | - | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | |
| 265 | - | | `toggle-overview` | Open the [overview](#overview), or close it on the selected window | | |
| 266 | - | | `switch-column-next` | Switch to the column used before this one on the workspace. Hold Cmd and press again to go further back; after a short hold a strip of cards shows the columns, most recent first, and releasing Cmd focuses the selected one. Escape cancels; clicking a card picks it, and a click elsewhere cancels and is not passed to windows. A click before the cards show cancels and acts as usual. Outside cascade, a maximized column hands its maximization to the target. A workspace capture session pauses while the strip shows, as in the overview | | |
| 267 | - | | `switch-column-prev` | The same, in the opposite direction | | |
| 268 | - | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off; in cascade, the column takes a band of its own. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | | |
| 269 | - | | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes | | |
| 270 | - | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it comes the previous or next occupied workspace, then the neighbor monitor, then the trailing empty workspace. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays | | |
| 271 | - | | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends | | |
| 272 | - | | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen | | |
| 273 | - | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes | | |
| 274 | - | | `set-column-width <+N\|-N>` | Widen or narrow the focused column by N cells (1-16), never below one cell and without cycling back to equal shares. In `cascade` and `fixed` overflow the cells come from, or go to, the other columns of the row; in `scroll` overflow the column grows on its own, up to `max-columns` cells | | |
| 275 | - | | `set-window-height <+N%\|-N%>` | Give the focused window N% more or less of its column's height; the other rows give or take it in proportion. Each row keeps at least 10%. A window joining or leaving the column resets equal rows. Does nothing past 9 rows | | |
| 276 | - | | `consume-or-expel-window-left/right` | A lone window joins the neighbor column; a stacked one leaves for a new column | | |
| 277 | - | | `focus-workspace <N>` / `focus-workspace-prev/next` | Show a numbered or neighbor workspace | | |
| 278 | - | | `workspace <name>` | Toggle a named workspace | | |
| 279 | - | | `move-column-to-workspace <N>` / `-prev/-next` | Move the focused column | | |
| 280 | - | | `move-window-to-workspace <N>` / `-prev/-next` | Move only the focused window | | |
| 281 | - | | `focus-monitor-left/right` / `focus-monitor-up/down` | Focus the neighbor monitor in that direction | | |
| 282 | - | | `move-workspace-to-monitor-left/right` / `move-workspace-to-monitor-up/down` | Move the workspace to the neighbor monitor in that direction; it gets a new home | | |
| 283 | - | | `move-workspace-prev/next` | Swap the current numbered workspace with its neighbor; the view follows it. It never passes the empty workspace at the end | | |
| 284 | - | | `scale-up` / `scale-down` | Zoom the whole display; one second after the last press, the scale is saved to `output.<name>.scale` and a notification confirms it (`notify-send`) | | |
| 177 | + | | `toggle-fullscreen` | Fullscreen the focused window. Nothing shows above it except its own dialogs | | |
| 178 | + | | `toggle-floating` | Make the focused window [float](#free-floating-windows), or put it back in its column | | |
| 179 | + | | `toggle-window-stash` | Move the focused window to the [stash](#stash), or back to its column | | |
| 180 | + | | `toggle-stash-visible` | Show or hide the stash | | |
| 181 | + | | `toggle-overview` | Open or close the [overview](#overview) | | |
| 182 | + | | `switch-column-next` / `-prev` | Switch to the previously used column. Hold `cmd` and press again to go further back | | |
| 183 | + | | `maximize-column` | Toggle full width for the focused column | | |
| 184 | + | | `cycle-column-width` | Grow the focused column by one [cell](#column-widths), then back to equal widths | | |
| 185 | + | | `set-column-width <+N\|-N>` | Widen or narrow the focused column by N cells | | |
| 186 | + | | `set-window-height <+N%\|-N%>` | Change the focused window's share of its column | | |
| 187 | + | | `focus-column-left/right` | Focus the neighbor column, then the neighbor monitor | | |
| 188 | + | | `focus-window-up/down` | Focus the neighbor window in the column, then the neighbor workspace (band in cascade) | | |
| 189 | + | | `move-column-left/right` | Move the focused column, then to the neighbor monitor | | |
| 190 | + | | `move-window-up/down` | Move the focused window in its column | | |
| 191 | + | | `consume-or-expel-window-left/right` | Join the neighbor column, or leave the current one | | |
| 192 | + | | `focus-workspace <N>` / `-prev/-next` | Show a workspace | | |
| 193 | + | | `workspace <name>` | Show or hide a named workspace | | |
| 194 | + | | `move-column-to-workspace <N>` / `-prev/-next` | Move the focused column to a workspace | | |
| 195 | + | | `move-window-to-workspace <N>` / `-prev/-next` | Move the focused window to a workspace | | |
| 196 | + | | `move-workspace-prev/next` | Swap the workspace with its neighbor | | |
| 197 | + | | `focus-monitor-left/right/up/down` | Focus the neighbor monitor | | |
| 198 | + | | `move-workspace-to-monitor-left/right/up/down` | Move the workspace to the neighbor monitor | | |
| 199 | + | | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config | | |
| 285 | 200 | | `quit` | Exit NeferWL | | |
| 286 | 201 | ||
| 287 | - | ### Stash | |
| 288 | - | ||
| 289 | - | Each workspace has a stash: a horizontal strip of windows set aside with `toggle-window-stash`, in the order they arrived. The selected window is centred over the tiles, `stash.width` of the usable width (80% by default) and 80% of its height. Its left and right neighbors sit `stash.gap` beside it and show up to the screen edges, dimmed (`stash.dim`); a click on one selects it. The others wait off screen. | |
| 290 | - | ||
| 291 | - | While the stash has the focus, `focus-column-left/right` and the three-finger swipe move through it, sliding the view, and stop at its ends; `focus-window-up/down` do nothing. Hide it with `toggle-stash-visible`, or with a four-finger swipe down or up, to get back to the tiles; a swipe down shows it again. Scripts see each stashed window's place, and whether the stash is hidden, in the [state file](desktop.md#state-for-scripts). | |
| 292 | - | ||
| 293 | - | Native floating windows are not in the stash: they stay centred in the usable output area. A window-sized float that fills that area (within two border widths plus two logical pixels per axis) stays below the columns when you focus a tile; `focus-window-up` at the top of a column raises it again. It does not hide bars or pin focus. Small dialogs and file pickers always stay above the columns; shrinking a float promotes it above them. Real fullscreen remains exclusive, except for the dialogs shown over it: the fullscreen window's own, those of stashed windows while the stash shows over it, and their own dialogs. A dialog belongs to a window through `xdg_toplevel.set_parent`, or through `zxdg_importer_v2` when another program opens it, as the desktop portal does. A dialog the app marks modal through `xdg_dialog_v1` (GTK4 does for "Save as…") blocks its window: clicking it, activating it, picking it in the overview or moving the focus onto it within its workspace focuses the dialog instead, and so do nested modal dialogs. A focus move away from the dialog reaches its window once, so the next move can go past it. | |
| 294 | - | ||
| 295 | - | ### Free floating windows | |
| 296 | - | ||
| 297 | - | `toggle-floating` lifts the focused tile out of its column into a free floating window. NeferWL sets its size, and it stays where you put it, kept inside the usable area. Its place is relative to the screen, so it keeps it on another workspace or monitor of any size. | |
| 202 | + | ### Default binds | |
| 298 | 203 | ||
| 299 | - | On a free floating window, `move-column-left/right` and `move-window-up/down` move it by 50 logical pixels, and `set-column-width`/`set-window-height` resize it: a width step moves it by N cells (`Usable.W / max-columns` each), a height step by N% of the usable height. `toggle-window-stash` stashes it and brings it back floating at the same place. `toggle-floating` returns it to its column. | |
| 204 | + | | Keys | Action | | |
| 205 | + | |---|---| | |
| 206 | + | | `cmd+return` | `spawn-terminal` | | |
| 207 | + | | `ctrl+cmd+space` | `spawn fuzzel` | | |
| 208 | + | | `cmd+q` | `close-window` | | |
| 209 | + | | `cmd+left/right`, `cmd+h/l` | `focus-column-left/right` | | |
| 210 | + | | `cmd+up/down`, `cmd+k/j` | `focus-window-up/down` | | |
| 211 | + | | `cmd+shift+left/right`, `cmd+shift+h/l` | `move-column-left/right` | | |
| 212 | + | | `cmd+shift+up/down`, `cmd+shift+k/j` | `move-window-up/down` | | |
| 213 | + | | `cmd+bracketleft/bracketright` | `consume-or-expel-window-left/right` | | |
| 214 | + | | `cmd+alt+left/right`, `cmd+alt+h/l` | `set-column-width -1/+1` | | |
| 215 | + | | `cmd+alt+up/down`, `cmd+alt+k/j` | `set-window-height -10%/+10%` | | |
| 216 | + | | `cmd+r` | `cycle-column-width` | | |
| 217 | + | | `cmd+f` | `maximize-column` | | |
| 218 | + | | `cmd+shift+f` | `toggle-fullscreen` | | |
| 219 | + | | `cmd+shift+space` | `toggle-floating` | | |
| 220 | + | | `cmd+s` | `toggle-stash-visible` | | |
| 221 | + | | `cmd+shift+s` | `toggle-window-stash` | | |
| 222 | + | | `cmd+o` | `toggle-overview` | | |
| 223 | + | | `cmd+tab` / `cmd+shift+tab` | `switch-column-next/prev` | | |
| 224 | + | | `cmd+1`…`9` (`code:2`…`10`) | `focus-workspace 1`…`9` | | |
| 225 | + | | `cmd+shift+1`…`9` | `move-column-to-workspace 1`…`9` | | |
| 226 | + | | `cmd+pageup/pagedown` | `focus-workspace-prev/next` | | |
| 227 | + | | `cmd+shift+pageup/pagedown` | `move-column-to-workspace-prev/next` | | |
| 228 | + | | `cmd+ctrl+arrows`, `cmd+ctrl+h/j/k/l` | `focus-monitor-*` | | |
| 229 | + | | `cmd+ctrl+shift+left/right`, `cmd+ctrl+shift+h/l` | `move-workspace-to-monitor-left/right` | | |
| 230 | + | | `cmd+ctrl+shift+up/down`, `cmd+ctrl+shift+k/j` | `move-workspace-prev/next` | | |
| 231 | + | | `cmd+code:12` / `cmd+code:13` (`-` / `=` on US) | `scale-down` / `scale-up` | | |
| 232 | + | | `ctrl+alt+backspace` | `quit` | | |
| 300 | 233 | ||
| 301 | - | ### Moving windows with the pointer | |
| 234 | + | With `keyboard.cmd = alt` or `ctrl`, the defaults that would combine `cmd` with that same modifier are dropped. Bind those actions to other keys. | |
| 302 | 235 | ||
| 303 | - | Hold `cmd` and drag with the left button to move the window under the pointer; drag with the right button to resize a floating window from its bottom-right corner. Apps that drag their own title bar or edges (`xdg_toplevel.move`/`resize`) do the same without `cmd`. The window gets no pointer events while you drag, and `Escape` cancels the drag. | |
| 236 | + | ## Layout | |
| 304 | 237 | ||
| 305 | - | A floating window follows the pointer and becomes a [free floating window](#free-floating-windows). A tile stays in place until you release the button. Until then, a bar or frame in `border.active` color shows where it will land: | |
| 238 | + | ### Cascade | |
| 306 | 239 | ||
| 307 | - | - top or bottom quarter of a window: stack it above or below that window; | |
| 308 | - | - middle of a window: swap the two columns (a window from a stacked column becomes a new column there); | |
| 309 | - | - in scroll overflow, the gap between two columns: a new column there. Moving the pointer against the left or right edge scrolls the workspace one column at a time. | |
| 240 | + | The default layout. Columns fill the screen side by side up to `max-columns`, then continue in a new band below. Each band fills the screen height. | |
| 310 | 241 | ||
| 311 | - | The drop can be on another monitor; focus follows the window. There, the middle of a window puts it in a new column at that place: only columns of the same workspace swap. A full `fixed` workspace has no room for a new column, so the window stacks in the outlined one. | |
| 242 | + | - Left/right focus moves within a band, then to the neighbor workspace. | |
| 243 | + | - Up/down moves through the windows of a column, then to the band above or below. | |
| 244 | + | - A maximized column gets a band of its own. | |
| 312 | 245 | ||
| 313 | - | ### Overview | |
| 246 | + | ### Column widths | |
| 314 | 247 | ||
| 315 | - | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows, over a background darkened by `floating.dim`. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, and in a vertical overview up to two cards behind peek above it and passed cards peek below (a Cascade overview fans covering floats left and right). Neighbor workspaces show dimmed stacks. Each monitor has one overview, laid out along the navigation axis of the current workspace's layout: neighbors appear above and below a `scroll` or `fixed` workspace and left and right of a `cascade` one (see [Cascade](#cascade)); stepping to a workspace with another layout turns the overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with rules across the axis (horizontal in a vertical overview, vertical in a side-by-side one) separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear beside it along that axis; in a vertical overview, rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal previous/next workspace navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. | |
| 248 | + | A cell is `1/max-columns` of the screen width. A column is 1 to 16 cells wide. Columns without a set width share the space equally. | |
| 316 | 249 | ||
| 317 | - | | Key | Action | | |
| 318 | - | | --- | --- | | |
| 319 | - | | `h` / `l`, `left` / `right` | Move within a front column-group card; left of its first tile enters the stash. Left on a single-window card sends it behind, or enters the stash when none is behind; right does nothing on a single card. | | |
| 320 | - | | `k` / `j`, `up` / `down` | Browse cards within the selected stash pile; otherwise bring the next card above / previous card below to the front, then change workspace at the stack boundary | | |
| 321 | - | | `return` | Show the selected front card; picking a hidden column moves the maximization to it. Other rows shown keep their focus, maximization and float order | | |
| 322 | - | | `escape` | Return to the original workspace; every row shown keeps its focus, maximization and float order | | |
| 250 | + | ### Stash | |
| 323 | 251 | ||
| 324 | - | The focus binds (`focus-column-left/right`, `focus-window-up/down`, `cmd+arrows` by default) move the selection like these keys. In the overview, `focus-workspace-prev/next` also traverse cards before crossing workspace rows, including named ones, except inside a selected stash pile. The keys above describe vertical overviews; in a [Cascade](#cascade) overview they move as described there. | |
| 252 | + | Each workspace has a stash: a strip of windows set aside with `toggle-window-stash`. The selected one shows centered over your columns, with its neighbors dimmed on the sides. `focus-column-left/right` or a three-finger swipe browse it. `toggle-stash-visible` or a four-finger swipe hides it. | |
| 325 | 253 | ||
| 326 | - | Each workspace's [stash](#stash), hidden or not, shows as a pile of cards on the left of its row, which stays centred unless it would overlap the pile (in a Cascade overview the pile extends its workspace's slot to the left): its selected window in front, up to three others behind it, dimmed. In the pile, `k` / `j` or up/down browse stash cards and stop at either end without changing workspace. In a vertical overview, right (`l`) leaves the pile immediately, left (`h`) stays in the pile, and on a stash-only workspace right deselects the pile so up/down can change workspace. In a Cascade overview, left from the pile goes to the previous workspace, right returns to the band's first column (or to the next workspace on a stash-only workspace), and up/down browse the pile. Browsing brings the selected card to the front without reordering the stash. `return` or a click on a card closes the overview with the stash shown on that window. | |
| 254 | + | ### Free floating windows | |
| 327 | 255 | ||
| 328 | - | A four-finger swipe up opens the overview, or hides the [stash](#stash) when it is shown, and a swipe down closes the overview on the selection (with the overview closed, a swipe down shows or hides the stash), whatever `touchpad.natural-scroll` says. Two-finger scrolling belongs to applications and never navigates the overview; previews do not accept application input. In a vertical overview, three fingers move the selection during the gesture, following `touchpad.natural-scroll`: horizontally, a short distance selects the next app and further movement repeats at a longer distance; vertically, one step per gesture selects a stack card or changes workspace. Inside a selected stash pile, vertical movement browses its cards and horizontal movement enters or leaves the pile. Lifting resets the gesture without an extra step; cancellation keeps selections already made. A Cascade overview follows the fingers instead (see [Cascade](#cascade)). The mouse wheel moves one step per notch, following `mouse.natural-scroll`. Three-finger navigation, the wheel and keyboard navigation share the same pile boundaries. | |
| 256 | + | `toggle-floating` turns the focused window into a floating window that stays where you put it. On a floating window, move binds move it by 50 pixels and resize binds resize it. | |
| 329 | 257 | ||
| 330 | - | Return or a click on a front tile or peeking card commits it; card changes remain provisional until then. `close-window` targets the selected preview. Move-to-workspace binds (`move-column-to-workspace*`, `move-window-to-workspace*`) move the selected preview at once and keep the overview open; with `focus.follow-move`, the selection follows it. Escape does not undo a move, and the row that received the window keeps it focused. A selected stash card, or a row with no preview, does not move. Other window mutation binds (moving within the row, resizing, maximizing, fullscreen and stash toggles), moving a workspace to another monitor and `switch-column-next` / `switch-column-prev` are disabled while the overview is open. Workspace and monitor navigation, launch and quit binds remain active. | |
| 258 | + | ### Moving windows with the pointer | |
| 331 | 259 | ||
| 332 | - | ### Default binds | |
| 260 | + | - `cmd` + left drag moves a window. Drop it on the top or bottom of another window to stack it there, or in the middle to swap. | |
| 261 | + | - `cmd` + right drag resizes a floating window. | |
| 262 | + | - `Escape` cancels the drag. | |
| 333 | 263 | ||
| 334 | - | With `keyboard.cmd = alt`, the `cmd+alt+…` binds have the same keys as `cmd+…` and are dropped with a warning; with `keyboard.cmd = ctrl`, so are the `cmd+ctrl+…` ones. Bind those actions to other keys. | |
| 264 | + | ### Overview | |
| 335 | 265 | ||
| 336 | - | | Keys | Action | | |
| 337 | - | |---|---| | |
| 338 | - | | `cmd+return` | `spawn-terminal` | | |
| 339 | - | | `cmd+left` | `focus-column-left` | | |
| 340 | - | | `cmd+right` | `focus-column-right` | | |
| 341 | - | | `cmd+up` | `focus-window-up` | | |
| 342 | - | | `cmd+down` | `focus-window-down` | | |
| 343 | - | | `cmd+shift+left` | `move-column-left` | | |
| 344 | - | | `cmd+shift+right` | `move-column-right` | | |
| 345 | - | | `cmd+h` | `focus-column-left` | | |
| 346 | - | | `cmd+l` | `focus-column-right` | | |
| 347 | - | | `cmd+k` | `focus-window-up` | | |
| 348 | - | | `cmd+j` | `focus-window-down` | | |
| 349 | - | | `cmd+shift+h` | `move-column-left` | | |
| 350 | - | | `cmd+shift+l` | `move-column-right` | | |
| 351 | - | | `cmd+bracketleft` | `consume-or-expel-window-left` | | |
| 352 | - | | `cmd+bracketright` | `consume-or-expel-window-right` | | |
| 353 | - | | `cmd+shift+up` / `cmd+shift+k` | `move-window-up` | | |
| 354 | - | | `cmd+shift+down` / `cmd+shift+j` | `move-window-down` | | |
| 355 | - | | `cmd+alt+left` / `cmd+alt+h` | `set-column-width -1` | | |
| 356 | - | | `cmd+alt+right` / `cmd+alt+l` | `set-column-width +1` | | |
| 357 | - | | `cmd+alt+up` / `cmd+alt+k` | `set-window-height -10%` | | |
| 358 | - | | `cmd+alt+down` / `cmd+alt+j` | `set-window-height +10%` | | |
| 359 | - | | `cmd+r` | `cycle-column-width` | | |
| 360 | - | | `cmd+f` | `maximize-column` | | |
| 361 | - | | `cmd+shift+f` | `toggle-fullscreen` | | |
| 362 | - | | `cmd+s` | `toggle-stash-visible` | | |
| 363 | - | | `cmd+shift+s` | `toggle-window-stash` | | |
| 364 | - | | `cmd+shift+space` | `toggle-floating` | | |
| 365 | - | | `cmd+o` | `toggle-overview` | | |
| 366 | - | | `cmd+tab` | `switch-column-next` | | |
| 367 | - | | `cmd+shift+tab` | `switch-column-prev` | | |
| 368 | - | | `cmd+q` | `close-window` | | |
| 369 | - | | `ctrl+alt+backspace` | `quit` | | |
| 370 | - | | `ctrl+cmd+space` | `spawn fuzzel` | | |
| 371 | - | | `cmd+pageup` | `focus-workspace-prev` | | |
| 372 | - | | `cmd+pagedown` | `focus-workspace-next` | | |
| 373 | - | | `cmd+shift+pageup` | `move-column-to-workspace-prev` | | |
| 374 | - | | `cmd+shift+pagedown` | `move-column-to-workspace-next` | | |
| 375 | - | | `cmd+code:2` | `focus-workspace 1` | | |
| 376 | - | | `cmd+code:3` | `focus-workspace 2` | | |
| 377 | - | | `cmd+code:4` | `focus-workspace 3` | | |
| 378 | - | | `cmd+code:5` | `focus-workspace 4` | | |
| 379 | - | | `cmd+code:6` | `focus-workspace 5` | | |
| 380 | - | | `cmd+code:7` | `focus-workspace 6` | | |
| 381 | - | | `cmd+code:8` | `focus-workspace 7` | | |
| 382 | - | | `cmd+code:9` | `focus-workspace 8` | | |
| 383 | - | | `cmd+code:10` | `focus-workspace 9` | | |
| 384 | - | | `cmd+shift+code:2` | `move-column-to-workspace 1` | | |
| 385 | - | | `cmd+shift+code:3` | `move-column-to-workspace 2` | | |
| 386 | - | | `cmd+shift+code:4` | `move-column-to-workspace 3` | | |
| 387 | - | | `cmd+shift+code:5` | `move-column-to-workspace 4` | | |
| 388 | - | | `cmd+shift+code:6` | `move-column-to-workspace 5` | | |
| 389 | - | | `cmd+shift+code:7` | `move-column-to-workspace 6` | | |
| 390 | - | | `cmd+shift+code:8` | `move-column-to-workspace 7` | | |
| 391 | - | | `cmd+shift+code:9` | `move-column-to-workspace 8` | | |
| 392 | - | | `cmd+shift+code:10` | `move-column-to-workspace 9` | | |
| 393 | - | | `cmd+ctrl+left` | `focus-monitor-left` | | |
| 394 | - | | `cmd+ctrl+right` | `focus-monitor-right` | | |
| 395 | - | | `cmd+ctrl+up` / `cmd+ctrl+k` | `focus-monitor-up` | | |
| 396 | - | | `cmd+ctrl+down` / `cmd+ctrl+j` | `focus-monitor-down` | | |
| 397 | - | | `cmd+ctrl+h` | `focus-monitor-left` | | |
| 398 | - | | `cmd+ctrl+l` | `focus-monitor-right` | | |
| 399 | - | | `cmd+ctrl+shift+left` | `move-workspace-to-monitor-left` | | |
| 400 | - | | `cmd+ctrl+shift+right` | `move-workspace-to-monitor-right` | | |
| 401 | - | | `cmd+ctrl+shift+h` | `move-workspace-to-monitor-left` | | |
| 402 | - | | `cmd+ctrl+shift+l` | `move-workspace-to-monitor-right` | | |
| 403 | - | | `cmd+ctrl+shift+up` / `cmd+ctrl+shift+k` | `move-workspace-prev` | | |
| 404 | - | | `cmd+ctrl+shift+down` / `cmd+ctrl+shift+j` | `move-workspace-next` | | |
| 405 | - | | `cmd+code:13` | `scale-up` | | |
| 406 | - | | `cmd+code:12` | `scale-down` | | |
| 266 | + | `toggle-overview` (or a four-finger swipe up) shows previews of your workspaces. Arrows, `h/j/k/l` or three-finger swipes move the selection; `Return` or a click opens it; `Escape` goes back. `close-window` and the move-to-workspace binds work on the selected preview. | |
| @@ -1,91 +1,93 @@ | |||||
| 1 | # Configuration | 1 | # Configuration | ||
| 2 | 2 | ||||
| 3 | - | NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). `neferwl --config <path>` selects another file; `neferwl validate-config [path]` checks one without starting the compositor. | 3 | + | NeferWL reads `~/.config/neferwl/config` (or `$XDG_CONFIG_HOME/neferwl/config`). Changes apply when you save, except `startup` and `xwayland`, which apply at the next start. |
| 4 | + | ||||
| 5 | + | ```sh | |||
| 6 | + | neferwl --config <path> # use another file | |||
| 7 | + | neferwl validate-config [path] # check a file | |||
| 8 | + | ``` | |||
| 4 | 9 | ||||
| 5 | ## Format | 10 | ## Format | ||
| 6 | 11 | ||||
| 7 | - | - One `key = value` per line. A line starting with `#` is a comment, and so is a `#` after a space, unless it is inside a quoted word (`"a # b"`). | 12 | + | - One `key = value` per line. `#` starts a comment. |
| 8 | - Lists are comma-separated; booleans are `on` / `off`. | 13 | - Lists are comma-separated; booleans are `on` / `off`. | ||
| 9 | - | - 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. A duplicate key warns and the last one wins. | 14 | + | - An invalid line logs a warning and keeps the default. If a key appears twice, the last one wins. |
| 10 | - | - Every key applies live when the file is saved; windows stay open. `startup` and `xwayland` take effect at the next start. | 15 | + | |
| 16 | + | See [examples/config](../examples/config) for every key with its default. | |||
| 11 | 17 | ||||
| 12 | ## Keys | 18 | ## Keys | ||
| 13 | 19 | ||||
| 14 | | Key | Default | Meaning | | 20 | | Key | Default | Meaning | | ||
| 15 | |---|---|---| | 21 | |---|---|---| | ||
| 16 | | `keyboard.layout`, `.variant`, `.options` | empty | xkb names, e.g. `fr`, `caps:escape` | | 22 | | `keyboard.layout`, `.variant`, `.options` | empty | xkb names, e.g. `fr`, `caps:escape` | | ||
| 17 | - | | `keyboard.repeat-rate` / `repeat-delay` | `25` / `600` | Key repeat per second / delay in ms | | 23 | + | | `keyboard.repeat-rate` / `.repeat-delay` | `25` / `600` | Repeats per second / delay in ms | |
| 18 | - | | `keyboard.cmd` | `super` | Modifier that `cmd` means in binds: `super`, `alt` or `ctrl` | | 24 | + | | `keyboard.cmd` | `super` | Modifier meant by `cmd` in binds: `super`, `alt` or `ctrl` | |
| 19 | - | | `terminal` | `$TERMINAL`, or `foot` if unset/empty | Command for automatic requests and `spawn-terminal`; whitespace-separated | | 25 | + | | `terminal` | `$TERMINAL` or `foot` | Terminal command | |
| 20 | - | | `terminal.auto-open` | `first` | `first`: one request for the initial numbered workspace per session; `all`: visible empty workspaces without slots, with retry protection; `off`: none. `--no-terminal` disables automatic requests for this run only | | 26 | + | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | |
| 21 | - | | `startup` | none | Commands run once at session start; separate several with `;` or repeat the key | | 27 | + | | `startup` | none | Commands run at session start, separated by `;` | |
| 22 | | `xwayland` | `xwayland-satellite` | X11 support; `off` disables it | | 28 | | `xwayland` | `xwayland-satellite` | X11 support; `off` disables it | | ||
| 23 | - | | `background` | `#111111` | Solid background color | | 29 | + | | `background` | `#111111` | Background color | |
| 24 | - | | `border.width` | `2` | Lines between visible neighboring tiles and around non-fullscreen floats, in logical pixels; `0` hides them | | 30 | + | | `border.width` | `2` | Border width in pixels; `0` hides borders | |
| 25 | - | | `floating.dim` | `0.3` | Black veil over tiles and lower layers when a native floating window above the tiles or the stash is visible, and over the background and lower layers behind the overview; opacity 0 to 1 | | 31 | + | | `border.active` / `.inactive` | `#808080` / `#111111` | Border colors of the focused / other windows | |
| 26 | - | | `stash.width` | `80` | Width of the selected stashed window, over the tiles, in percent of the usable width; 10 to 90 (`80%` works too). The rest is split between both sides, where the neighbors show. Its height is 80% of the usable height | | 32 | + | | `floating.dim` | `0.3` | Darkening behind floating windows and the overview, 0 to 1 | |
| 27 | - | | `stash.gap` | `2` | Space between the selected stashed window and its neighbors, in percent of the usable width; 0 to 10. The neighbors show from there to the screen edge: with the defaults, 8% of each. A gap as wide as the side margin hides them | | |||
| 28 | - | | `stash.dim` | `0.5` | Black veil over the stash neighbors, borders included; opacity 0 to 1 | | |||
| 29 | - | | `stash.capture` | `on` | `on`: a window opened while the stash is shown joins its end, selected; `toggle-window-stash` tiles it as a new column. `off`: new windows always tile | | |||
| 30 | - | | `border.active` / `border.inactive` | `#808080` / `#111111` | Colors for focused / other window lines; only the focused output lights up | | |||
| 31 | | `layout.gaps` | `0` | Space between windows | | 33 | | `layout.gaps` | `0` | Space between windows | | ||
| 32 | - | | `layout.max-columns` | `2` | Columns that share the screen before the overflow applies. It also sets the cell: a column is `N` cells wide, a cell being `1/max-columns` of the usable width (see [Column widths](#column-widths)) | | 34 | + | | `layout.max-columns` | `2` | Columns that share the screen | |
| 33 | - | | `layout.overflow` | `cascade` | Past the max: `cascade` into vertically scrolling bands, `scroll` right, or `fixed` spiral split | | 35 | + | | `layout.overflow` | `cascade` | What happens past `max-columns`: [`cascade`](#cascade) into bands below, `scroll` right, or `fixed` spiral split | |
| 34 | - | | `layout.<output>.max-columns`, `.overflow` | layout values | Per-screen layout: connector (`DP-2`) or monitor key | | 36 | + | | `layout.<output>.max-columns`, `.overflow` | layout values | Per-output layout | |
| 35 | - | | `touchpad.natural-scroll` | `off` | Content follows the fingers, for two-finger scroll and three-finger swipes (see [Touchpad](#touchpad)) | | 37 | + | | `stash.width` | `80` | Width of the selected [stashed](#stash) window, in % of the screen (10–90) | |
| 36 | - | | `touchpad.tap` | `on` | Tap to click: one finger left, two right, three middle | | 38 | + | | `stash.gap` | `2` | Gap around the selected stashed window, in % (0–10) | |
| 37 | - | | `touchpad.accel-speed` | `0` | Touchpad pointer speed, from `-1` (slowest) to `1` (fastest) | | 39 | + | | `stash.dim` | `0.5` | Darkening of the stash neighbors, 0 to 1 | |
| 38 | - | | `touchpad.accel-profile` | `adaptive` | `adaptive`: faster finger moves go further; `flat`: constant speed | | 40 | + | | `stash.capture` | `on` | A window opened while the stash shows joins the stash | |
| 39 | - | | `touchpad.left-handed` | `off` | Swaps the left and right touchpad buttons | | 41 | + | | `touchpad.natural-scroll` | `off` | Content follows the fingers | |
| 40 | - | | `touchpad.scroll-factor` | `1` | Multiplies two-finger scroll distance, above `0` up to `10` | | 42 | + | | `touchpad.tap` | `on` | Tap to click | |
| 41 | - | | `mouse.natural-scroll` | `off` | Wheel scrolling moves the content with the wheel instead of against it. `mouse.*` applies to every pointer that is not a touchpad: mice, trackballs, trackpoints | | 43 | + | | `touchpad.accel-speed` | `0` | Pointer speed, `-1` to `1` | |
| 42 | - | | `mouse.accel-speed` | `0` | Mouse pointer speed, from `-1` (slowest) to `1` (fastest) | | 44 | + | | `touchpad.accel-profile` | `adaptive` | `adaptive` or `flat` | |
| 43 | - | | `mouse.accel-profile` | `adaptive` | `adaptive`: faster moves go further; `flat`: constant speed | | 45 | + | | `touchpad.left-handed` | `off` | Swap left and right buttons | |
| 44 | - | | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons | | 46 | + | | `touchpad.scroll-factor` | `1` | Scroll speed multiplier, up to `10` | |
| 45 | - | | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` | | 47 | + | | `mouse.natural-scroll`, `.accel-speed`, `.accel-profile`, `.left-handed` | as touchpad | Same, for mice, trackballs and trackpoints | |
| 46 | - | | `animations` | `off` | **Experimental.** `on` animates changes; `off` makes every change instant and the three-finger swipe no longer follows the fingers: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview and the column switcher cards, windows opening and closing, the stash showing and hiding, the focus pulse | | 48 | + | | `cursor.hide-after` | `5s` | Hide the cursor after this long without motion, or `off` | |
| 47 | - | | `animations.speed` | `normal` | Speed of every spring (not the focus pulse): `slow`, `normal` or `fast` (a view move lands in about 260, 180 or 130 ms), or a number relative to `normal`, from `0.1` (10× slower) to `10` (10× faster) | | 49 | + | | `animations` | `off` | **Experimental.** Animate layout changes and make swipes follow the fingers | |
| 50 | + | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | |||
| 48 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | 51 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | ||
| 49 | - | | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone outside panels (e.g. a maximized column), the overview, the column switcher cards and a window focused as it opens. Off while `animations = off` | | 52 | + | | `focus.animation` | `pulse` | Brief brightening of a newly focused window, or `off`. Needs `animations = on` | |
| 50 | - | | `focus.effect` | `screen` | What the focus indicator draws: `screen` brightens the window slightly, visible on black too. HDR (PQ) client content is not brightened on HDR outputs | | 53 | + | | `focus.strength` | `0.04` | Strength of the focus pulse, `0.01` to `0.2` | |
| 51 | - | | `focus.strength` | `0.04` | Peak of the focus effect, `0.01` to `0.2`: black reaches about 10/255 at `0.04` | | 54 | + | | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace | |
| 52 | - | | `workspace.<name>.*` | none | Named workspaces are outside the numbered list; use the overview to show an occupied one, or a `workspace <name>` bind to show it and toggle back | | 55 | + | | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` | |
| 53 | - | | `workspace.<name>.monitor` | focused output | Home monitor: connector (`DP-2`) or monitor key. A `workspace <name>` bind focuses it there. Unset, or while home is absent, the workspace comes to the focused output; it returns home when it reconnects | | 56 | + | | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout | |
| 54 | - | | `workspace.<name>.size` | `inherit` | Logical `WxH` viewport, centered on the monitor; inherits monitor size by default and always keeps its scale | | 57 | + | | `workspace.<name>.column.<N>` | none | Column slot: `<cells>, <command>` | |
| 55 | - | | `workspace.<name>.max-columns`, `.overflow` | screen values | Per-workspace layout | | 58 | + | | `rule.<name>.*` | none | [Window rules](#window-rules) | |
| 56 | - | | `workspace.<name>.column.<N>` | none | Slot: `<cells>, <command>`; cells is a whole number from 1 to 16 | | 59 | + | | `output.<name>` | `preferred` | `WxH`, `WxH@Hz`, `preferred` or `off` | |
| 57 | - | | `rule.<name>.*` | none | Window rules: place an app's windows by app ID when they first open; see [Window rules](#window-rules) | | |||
| 58 | - | | `output.<name>` | preferred | `WxH`, `WxH@Hz`, `preferred` or `off` | | |||
| 59 | | `output.<name>.scale` | `1` | 1 to 4, e.g. `1.5` or `4/3` | | 60 | | `output.<name>.scale` | `1` | 1 to 4, e.g. `1.5` or `4/3` | | ||
| 60 | - | | `output.<name>.transform` | `normal` | `normal`, `90`, `180`, `270`, `flipped`, `flipped-90`, `flipped-180`, `flipped-270`; 90/270 swap the output's width and height | | 61 | + | | `output.<name>.transform` | `normal` | `90`, `180`, `270`, `flipped`, `flipped-90`, … | |
| 61 | - | | `output.<name>.primary` | `off` | Gets focus and pointer at startup | | 62 | + | | `output.<name>.primary` | `off` | Focused at startup | |
| 62 | - | | `output.<name>.right-of` / `.left-of` / `.above` / `.below` | automatic | Connector to place the output against, e.g. `DP-1` | | 63 | + | | `output.<name>.right-of`, `.left-of`, `.above`, `.below`, `.offset` | automatic | [Output placement](#output-placement) | |
| 63 | - | | `output.<name>.offset` | `0` | Logical px along the shared edge, −65536 to 65536; see [Output placement](#output-placement) | | 64 | + | | `output.<name>.hdr` | `off` | [HDR](#hdr) on capable displays | |
| 64 | - | | `output.<name>.hdr` | `off` | Turns on HDR10 on capable outputs; otherwise falls back to SDR (reason in the log) | | 65 | + | | `output.<name>.sdr-brightness` | `203` | Brightness of SDR content in HDR, in nits (80–1000) | |
| 65 | - | | `output.<name>.sdr-brightness` | `203` | How bright SDR desktop content appears in HDR, in nits (80–1000) | | 66 | + | | `render.direct-scanout` | `on` | Send fullscreen windows straight to the display | |
| 66 | - | | `render.direct-scanout` | `on` | Fullscreen buffers straight to the display | | 67 | + | | `render.tearing` | `on` | Allow tearing for fullscreen games | |
| 67 | - | | `render.tearing` / `render.vrr` | `on` / `on` | Tearing: games in direct scanout. VRR: any fullscreen window covering the output | | 68 | + | | `render.vrr` | `on` | Variable refresh rate for fullscreen windows | |
| 68 | - | | `render.vrr-flip-gap` | `1ms` | Under VRR, a game frame commits at least this long after the previous flip, so it does not wait for the panel's slowest refresh (0 to 10ms; `0` turns it off) | | 69 | + | | `render.vrr-flip-gap` | `1ms` | See [Performance](performance.md#vrr) | |
| 69 | - | | `performance.realtime` | `on` | Request real-time scheduling for output and input threads | | 70 | + | | `performance.realtime` | `on` | Real-time scheduling | |
| 70 | - | | `log.level` / `log.debug` | `info` / empty | Log level / debug components or `all` | | 71 | + | | `log.level` / `log.debug` | `info` / empty | Log level / debug categories or `all` | |
| 71 | - | | `bind.<keys>` | see below | Action for a key combo; `none` removes a default | | 72 | + | | `bind.<keys>` | see [Default binds](#default-binds) | [Binds](#binds) | |
| 73 | + | ||||
| 74 | + | `<output>` is a connector such as `DP-2`, or a monitor key (`make model serial`). | |||
| 72 | 75 | ||||
| 73 | ## Window rules | 76 | ## Window rules | ||
| 74 | 77 | ||||
| 75 | - | A window rule places the windows of an app when they first open. Rules are flat keys named by a rule name (letters, digits, `-` or `_`): | 78 | + | Rules place an app's windows when they first open. |
| 76 | 79 | ||||
| 77 | | Key | Meaning | | 80 | | Key | Meaning | | ||
| 78 | |---|---| | 81 | |---|---| | ||
| 79 | - | | `rule.<name>.app-id` | Required. A [Go regular expression](https://pkg.go.dev/regexp/syntax) matched against the whole app ID: `steam` does not match `steam_app_1`, `steam.*` does | | 82 | + | | `rule.<name>.app-id` | Required. [Regular expression](https://pkg.go.dev/regexp/syntax) matching the whole app ID | |
| 80 | - | | `rule.<name>.floating` | `on` floats the window (centred, free floating, sized by NeferWL); `off` tiles a fixed-size window that would float | | 83 | + | | `rule.<name>.floating` | `on` to float, `off` to tile | |
| 81 | - | | `rule.<name>.workspace` | A workspace number (1-based, past the last one means the last) or the name of a declared `workspace.<name>.*` workspace | | 84 | + | | `rule.<name>.workspace` | Workspace number or name | |
| 82 | - | | `rule.<name>.monitor` | A connector (`DP-2`) or a monitor key (`make model serial`); the focused output when it is not connected | | 85 | + | | `rule.<name>.monitor` | Output | |
| 83 | - | | `rule.<name>.width` | Width of the window's new column in cells, a whole number from 1 to 16 (see [Column widths](#column-widths)); tiled windows only | | 86 | + | | `rule.<name>.width` | Column width in [cells](#column-widths), 1 to 16 | |
| 84 | 87 | ||||
| 85 | ```ini | 88 | ```ini | ||
| 86 | rule.games.app-id = steam_app_.* | 89 | rule.games.app-id = steam_app_.* | ||
| 87 | rule.games.workspace = 3 | 90 | rule.games.workspace = 3 | ||
| 88 | - | rule.games.monitor = DP-2 | |||
| 89 | rule.chat.app-id = discord|org\.telegram\..* | 91 | rule.chat.app-id = discord|org\.telegram\..* | ||
| 90 | rule.chat.workspace = chat | 92 | rule.chat.workspace = chat | ||
| 91 | rule.chat.width = 1 | 93 | rule.chat.width = 1 | ||
| @@ -93,22 +95,17 @@ rule.calc.app-id = org\.gnome\.Calculator | |||||
| 93 | rule.calc.floating = on | 95 | rule.calc.floating = on | ||
| 94 | ``` | 96 | ``` | ||
| 95 | 97 | ||||
| 96 | - | - Every rule that matches applies, in file order; a later rule wins for each field it sets. A generic rule can set `monitor` and a specific one `width`. | 98 | + | - Every matching rule applies, in file order; later rules win. |
| 97 | - | - Rules apply once, when the window first maps. A later app ID change, a window that maps again, or a config reload moves nothing. Reloading changes rules for windows that open afterwards. | 99 | + | - A rule never moves your focus or view. |
| 98 | - | - A rule never moves you. The window opens on its target workspace without taking the focus or switching the workspace or monitor you are on. When the target is the workspace on screen of the focused output, the window opens as any new window does. | 100 | + | - To find an app ID: `neferwl state | jq '.windows[].app_id'`. |
| 99 | - | - A named workspace stays on its own monitor: `monitor` is ignored next to a named `workspace`. A name nobody declared warns and the window opens as usual. | |||
| 100 | - | - A window started for a slot (`workspace.<name>.column.N`) goes to its slot, and a dialog stays over its parent: rules do not apply to them. | |||
| 101 | - | - An invalid value, a bad regular expression or a missing `app-id` logs a warning with the line, and the rule is ignored. An invalid value that repeats a key already set keeps the earlier value, like any other key. | |||
| 102 | - | ||||
| 103 | - | To find an app ID, look at `app_id` in `neferwl state | jq '.windows[] | {id, app_id}'`, or at the `window mapped` line in the log. X11 apps run through xwayland-satellite get their app ID from `WM_CLASS`; it may be empty, and an app that sets it after opening is not matched. | |||
| 104 | 101 | ||||
| 105 | ## Output placement | 102 | ## Output placement | ||
| 106 | 103 | ||||
| 107 | - | Without relations, outputs sit left to right in config order, top-aligned. A relation places an output against another one; `offset` slides it along the shared edge so the pointer crosses where the screens really meet. | 104 | + | By default, outputs sit left to right in config order. Place one against another, and use `offset` (logical pixels) to align the edges: |
| 108 | 105 | ||||
| 109 | ```ini | 106 | ```ini | ||
| 110 | output.DP-2.right-of = DP-1 | 107 | output.DP-2.right-of = DP-1 | ||
| 111 | - | output.DP-2.offset = 360 # DP-2 starts 360 px lower than DP-1's top | 108 | + | output.DP-2.offset = 360 # DP-2 starts 360 px lower than DP-1 |
| 112 | ``` | 109 | ``` | ||
| 113 | 110 | ||||
| 114 | ``` | 111 | ``` | ||
| @@ -120,136 +117,55 @@ output.DP-2.offset = 360 # DP-2 starts 360 px lower than DP-1's top | |||||
| 120 | └────────────┘ | 117 | └────────────┘ | ||
| 121 | ``` | 118 | ``` | ||
| 122 | 119 | ||||
| 123 | - | Placement, offsets and monitor navigation use the rotated size: a 2560x1440 output with `transform = 90` is 1440 wide and 2560 tall at scale 1. Rotated outputs always compose, with no direct scanout or overlay planes. | 120 | + | `offset` moves down for `right-of`/`left-of`, and right for `above`/`below`. kanshi, wdisplays and wlr-randr override these settings until the next reload. |
| 124 | - | ||||
| 125 | - | Rules: | |||
| 126 | - | ||||
| 127 | - | - `offset` moves down for `right-of`/`left-of` and right for `above`/`below`; negative moves the other way. | |||
| 128 | - | - Sizes are logical: the layout follows scale changes on its own. | |||
| 129 | - | - An output whose reference is not connected is placed automatically until the reference returns. | |||
| 130 | - | - Cycles and invalid values are ignored with a warning in the log. | |||
| 131 | - | - Positions and transforms set by kanshi, wdisplays or wlr-randr win until the next config reload. | |||
| 132 | - | - `focus-monitor-*`, `move-workspace-to-monitor-*` and column moves past the edge go to the output on that side. | |||
| 133 | - | ||||
| 134 | - | Two rows of three, with DP-1 as the base: | |||
| 135 | - | ||||
| 136 | - | ``` | |||
| 137 | - | ┌────────┐┌────────────┐┌────────┐ | |||
| 138 | - | │ DP-4 ││ HDMI-1 ││ DP-5 │ | |||
| 139 | - | └────────┘└────────────┘└────────┘ | |||
| 140 | - | ┌────────┐┌────────────┐┌────────┐ | |||
| 141 | - | │ DP-2 ││ DP-1 ││ DP-3 │ | |||
| 142 | - | └────────┘└────────────┘└────────┘ | |||
| 143 | - | ``` | |||
| 144 | - | ||||
| 145 | - | ```ini | |||
| 146 | - | # Bottom row: 1080p sides, bottoms aligned with DP-1 (1440 - 1080 = 360) | |||
| 147 | - | output.DP-2.left-of = DP-1 | |||
| 148 | - | output.DP-2.offset = 360 | |||
| 149 | - | output.DP-3.right-of = DP-1 | |||
| 150 | - | output.DP-3.offset = 360 | |||
| 151 | - | ||||
| 152 | - | # Top row: 1200p sides, bottoms aligned with HDMI-1 (1440 - 1200 = 240) | |||
| 153 | - | output.HDMI-1.above = DP-1 | |||
| 154 | - | output.DP-4.left-of = HDMI-1 | |||
| 155 | - | output.DP-4.offset = 240 | |||
| 156 | - | output.DP-5.right-of = HDMI-1 | |||
| 157 | - | output.DP-5.offset = 240 | |||
| 158 | - | ``` | |||
| 159 | 121 | ||||
| 160 | ## Workspace size | 122 | ## Workspace size | ||
| 161 | 123 | ||||
| 162 | - | A workspace inherits its monitor size unless `workspace.<name>.size` sets a logical `WxH`. Its viewport is centered, and columns, maximization, floating windows and fullscreen use that real layout size—not scaled overview previews. Oversized dimensions are limited to the monitor; reserved layer zones still reduce usable space. Monitor mode and scale stay unchanged. Omit `size` or use `inherit` to restore the default. Workspace transitions involving a smaller viewport switch without the vertical slide, keeping scrolling columns inside their frame. Fullscreen inside a smaller viewport uses composition rather than output-covering direct scanout, so fullscreen-game VRR/tearing eligibility does not apply. | 124 | + | `workspace.<name>.size` gives a named workspace a smaller centered viewport, for example to present at 1080p on a 4K screen: |
| 163 | 125 | ||||
| 164 | ```ini | 126 | ```ini | ||
| 165 | workspace.presentation.size = 1920x1080 | 127 | workspace.presentation.size = 1920x1080 | ||
| 166 | - | bind.Cmd+p = workspace presentation | 128 | + | bind.cmd+p = workspace presentation |
| 167 | ``` | 129 | ``` | ||
| 168 | 130 | ||||
| 169 | ## Touchpad | 131 | ## Touchpad | ||
| 170 | 132 | ||||
| 171 | - | With `animations = on`, a three-finger swipe follows the fingers and moves one step at most, so a quick swipe never skips a column or a workspace: | 133 | + | | Gesture | Action | |
| 172 | - | 134 | + | |---|---| | |
| 173 | - | - Left or right scrolls the columns (`scroll` overflow) to the next column edge; the focus moves to a column fully on screen. | 135 | + | | Three fingers left/right | Scroll columns (switch workspace in cascade) | |
| 174 | - | - Up or down slides to the next numbered workspace in scroll and fixed layouts. | 136 | + | | Three fingers up/down | Switch workspace (scroll bands in cascade) | |
| 175 | - | - In [Cascade](#cascade), up/down scrolls one band and left/right switches workspaces. | 137 | + | | Four fingers up | Open the overview, or hide the stash | |
| 176 | - | 138 | + | | Four fingers down | Close the overview, or show/hide the stash | | |
| 177 | - | The view sticks a little near each step and catches up between them. Past the next step it resists; lift and swipe again to go further. When the fingers lift, a quick swipe goes on to the next step and a slow one settles on the closest. | |||
| 178 | - | ||||
| 179 | - | In scroll and fixed layouts with `natural-scroll = off`, a swipe left shows the columns to the left and a swipe up shows the workspace above. `natural-scroll = on` moves the content with the fingers, so both are reversed. | |||
| 180 | - | ||||
| 181 | - | With `animations = off`, nothing moves during the swipe: when the fingers lift, a quick or long enough swipe navigates content or workspaces along the layout's axes. Cascade band swipes stop at the first and last band. | |||
| 182 | - | ||||
| 183 | - | Over the shown stash a sideways swipe slides it with the fingers, with a stop on each window, and moves one window at most. In scroll and fixed layouts where the view cannot scroll (a floating or fullscreen window, fixed overflow, or a named workspace), a quick swipe runs `focus-column-left/right` or `focus-workspace-prev/next` when the fingers lift. Cascade keeps horizontal workspace navigation; vertical band swipes do nothing while a floating or fullscreen window has focus. | |||
| 184 | - | ||||
| 185 | - | Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia. | |||
| 186 | 139 | ||||
| 187 | - | Three- and four-finger swipes belong to the compositor. Every other touchpad gesture goes to the window under the pointer through `zwp_pointer_gestures_v1` (version 3): swipes of other finger counts, pinch (zoom and rotation) and hold. A gesture stays with the window that had the pointer when it began; it ends as cancelled if the pointer moves to another window or the window closes. While the session is locked, only the lock screen gets gestures. | 140 | + | A swipe moves one step at most. Other gestures (pinch, hold, two-finger scroll) go to the app under the pointer. |
| 188 | 141 | ||||
| 189 | ## HDR | 142 | ## HDR | ||
| 190 | 143 | ||||
| 191 | - | `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits. | 144 | + | `output.<name>.hdr = on` sends HDR10 to displays that support it. If the display, driver or GPU cannot, NeferWL falls back to SDR and logs why. NVIDIA is not supported yet. |
| 192 | - | ||||
| 193 | - | HDR-aware Wayland clients, including games through Vulkan WSI, browsers, and video players, can present HDR through `wp_color_manager_v1` and `wp_color_representation_v1`. Some apps require HDR to be enabled in their own settings. X11 clients stay SDR. | |||
| 194 | 145 | ||||
| 195 | - | Hardware-decoded video can use NV12 (8-bit) or P010 (10-bit) DMA-BUF subsurfaces with BT.601/709/2020 coefficients, full or limited range, and chroma-location type 0. The Vulkan device must support sampling the format/modifier with per-plane views; planes in one DMA-BUF use one memory import, while separate plane buffers additionally require Vulkan DISJOINT support. Unsupported formats are not offered. Confirm hardware decoding and the advertised DMA-BUF formats when testing an app. | 146 | + | - HDR-aware apps (games, browsers, video players) can show HDR content. Some need HDR turned on in their own settings. X11 apps stay SDR. |
| 196 | - | 147 | + | - The desktop stays SDR, shown at `sdr-brightness` nits. | |
| 197 | - | - Fullscreen HDR buffers (10-bit, PQ) go straight to the display unchanged. | 148 | + | - Fullscreen HDR windows go straight to the display. |
| 198 | - | - Opaque SDR (sRGB) buffers also skip composition on HDR outputs when the KMS plane has a colour pipeline (`DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE`): sRGB curve, `sdr-brightness`, BT.709 to BT.2020, PQ, the same maths as composition. This applies to a fullscreen window (direct scanout) and to a lone window on an overlay plane. The compositor looks for that chain on each plane, whatever the driver; amdgpu on recent kernels has it. Without it, with alpha, with YUV or extended-linear content, SDR windows are composed, and the log gives the reason (`hdr_sdr_content`, `yuv`). When the driver refuses a pipeline (tested once per plane, buffer format and cursor state), the window is composed with reason `color_refused`. | 149 | + | - Screenshots and captures are SDR (tone-mapped). |
| 199 | - | - Composed frames and every modeset leave the planes' pipeline on Bypass. | 150 | + | - If NeferWL crashes, the next compositor may show distorted colours until it sets its own colour pipeline. |
| 200 | - | - Every modeset and exit clear the CRTC's `DEGAMMA_LUT`, `CTM` and `GAMMA_LUT`, which this compositor never uses: a gamma ramp left by another DRM master would otherwise distort every frame. | |||
| 201 | - | - A VT switch resets the pipeline first: before the seat is disabled, each output that applies a pipeline commits Bypass (at most 100 ms are spent waiting for it, then the switch goes on and a warning is logged). The pipeline survives a master change, and a client without `DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE` cannot clear it. A crash or `kill -9` cannot do this reset: the next DRM master inherits the pipeline, with its colours distorted, until a client that sets the cap commits a modeset (or this compositor starts again). | |||
| 202 | - | - Windowed and otherwise composed HDR content keeps its full range in a linear fp16 image before PQ output; the display tone-maps it according to its EDID metadata. SDR surfaces are blended in linear light on HDR outputs, so antialiased edges may look slightly different than sRGB-space blending. Dim veils (`floating.dim`, `stash.dim`) darken SDR white as much as on an SDR output; brighter HDR highlights stay a little brighter. | |||
| 203 | - | - Screenshots and capture use opaque 8-bit sRGB. HDR capture applies GPU tone mapping and reduces out-of-sRGB gamut: values below 90% of SDR reference white stay unchanged, while brighter values are compressed to preserve highlight differences (SDR reference white maps to about 245/255 when capturing an HDR output, versus 255/255 on an SDR output). This capture-only transform does not change HDR display output. | |||
| 204 | - | - The display gets the metadata from its own EDID, not the client's MaxCLL/MaxFALL. | |||
| 205 | - | ||||
| 206 | - | HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 composition and 10-bit export formats/features. Unsupported capabilities fall back to SDR. NVIDIA is not supported yet (tested status). | |||
| 207 | 151 | ||||
| 208 | ## Binds | 152 | ## Binds | ||
| 209 | 153 | ||||
| 210 | `bind.<keys> = <action>`. Modifiers: `cmd` (see `keyboard.cmd`), `shift`, `ctrl`, `alt`, `super`. | 154 | `bind.<keys> = <action>`. Modifiers: `cmd` (see `keyboard.cmd`), `shift`, `ctrl`, `alt`, `super`. | ||
| 211 | 155 | ||||
| 212 | - | - Keys are what the active layout prints: `cmd+é` on AZERTY. Write `=`, `#` and space by name: `cmd+equal`, `cmd+numbersign`, `cmd+space`. | 156 | + | - Keys are what your layout prints: `cmd+é` on AZERTY. Write `=`, `#` and space by name: `cmd+equal`, `cmd+numbersign`, `cmd+space`. |
| 213 | - | - `code:N` is a physical key (evdev code), the same on every layout: `code:2` to `code:10` are the digit row 1 to 9. | 157 | + | - `code:N` is a physical key, the same on every layout: `code:2` to `code:10` are the digits 1 to 9. |
| 214 | - | - Other keys use their xkb keysym name, case-sensitive (`wev` shows it). Media keys have no default bind: | 158 | + | - Other keys use their xkb name (`wev` shows it). |
| 215 | - | ||||
| 216 | - | ``` | |||
| 217 | - | bind.XF86AudioRaiseVolume = spawn wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+ | |||
| 218 | - | bind.XF86AudioLowerVolume = spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- | |||
| 219 | - | bind.XF86AudioMute = spawn wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle | |||
| 220 | - | bind.XF86MonBrightnessUp = spawn brightnessctl set +5% | |||
| 221 | - | bind.XF86MonBrightnessDown = spawn brightnessctl set 5%- | |||
| 222 | - | ``` | |||
| 223 | - `none` removes a default bind. | 159 | - `none` removes a default bind. | ||
| 224 | - | - `spawn <command>` runs a program without a shell; `spawn a; b` runs several. See [Running commands](desktop.md#running-commands). | 160 | + | - `spawn <command>` runs a program without a shell. See [Running commands](desktop.md#running-commands). |
| 225 | - | ||||
| 226 | - | ### Cascade | |||
| 227 | - | ||||
| 228 | - | Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | |||
| 229 | - | ||||
| 230 | - | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. A maximized column takes a band of its own: the columns after it continue in the following bands, and unmaximizing packs them back. It stays maximized while focus moves. | |||
| 231 | - | ||||
| 232 | - | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | |||
| 233 | - | ||||
| 234 | - | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. | |||
| 235 | - | ||||
| 236 | - | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next occupied workspace at the band's edge, then to the neighbor monitor, then to the trailing empty workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | |||
| 237 | - | ||||
| 238 | - | Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade. | |||
| 239 | 161 | ||||
| 240 | - | Cascade supports dropping onto tiles, but not gap insertion or edge scrolling during a drag. | 162 | + | ```ini |
| 241 | - | 163 | + | bind.XF86AudioRaiseVolume = spawn wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+ | |
| 242 | - | ### Column widths | 164 | + | bind.XF86AudioLowerVolume = spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- |
| 243 | - | 165 | + | bind.XF86AudioMute = spawn wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle | |
| 244 | - | A cell is `1/max-columns` of the usable width. A column is N cells wide, from 1 to 16; an auto column (no cells set) shares the width equally. The same model serves every overflow: | 166 | + | bind.XF86MonBrightnessUp = spawn brightnessctl set +5% |
| 245 | - | 167 | + | bind.XF86MonBrightnessDown = spawn brightnessctl set 5%- | |
| 246 | - | - `cascade`: the columns of a band share its row of cells. | 168 | + | ``` |
| 247 | - | - `fixed`: the columns share the row while there are at most `max-columns` of them. Past that, the spiral applies and cells are ignored. | |||
| 248 | - | - `scroll`: each column is sized on its own, up to `max-columns` cells; an auto column is the equal share of the columns in view. | |||
| 249 | - | ||||
| 250 | - | In a row, the auto columns share the cells the set ones leave; when no cell is left, an auto column counts for one. Columns with more cells than the row holds (after a smaller `max-columns`, or moved to another workspace) share the width in proportion. All-auto rows are laid out as equal shares. | |||
| 251 | - | ||||
| 252 | - | `cycle-column-width` gives the focused column one more cell, taking it from the widest other column of its row (the nearest, right side first, on a tie). A row that does not add up to `max-columns` cells is first spread evenly, the focused column taking the remainder, and that counts as the step. With no cell left to take, the row returns to equal shares. `set-column-width +N` repeats the grow without cycling; `-N` gives a cell to the right neighbor (the left one at the row's end), down to one cell. | |||
| 253 | 169 | ||||
| 254 | ### Actions | 170 | ### Actions | ||
| 255 | 171 | ||||
| @@ -258,149 +174,93 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||||
| 258 | | `spawn <command>` | Run a program | | 174 | | `spawn <command>` | Run a program | | ||
| 259 | | `spawn-terminal` | Run `terminal` | | 175 | | `spawn-terminal` | Run `terminal` | | ||
| 260 | | `close-window` | Close the focused window | | 176 | | `close-window` | Close the focused window | | ||
| 261 | - | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, and the app's own fullscreen requests are then refused until it is focused again. An activation of a window that can show over it keeps it fullscreen instead: one of its dialogs, or a stashed window or a dialog of one (and their dialogs), which shows the stash over it as `toggle-stash-visible` does, unless that stashed window asked for fullscreen while hidden: it then replaces the fullscreen window. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | 177 | + | | `toggle-fullscreen` | Fullscreen the focused window. Nothing shows above it except its own dialogs | |
| 262 | - | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | 178 | + | | `toggle-floating` | Make the focused window [float](#free-floating-windows), or put it back in its column | |
| 263 | - | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | 179 | + | | `toggle-window-stash` | Move the focused window to the [stash](#stash), or back to its column | |
| 264 | - | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | 180 | + | | `toggle-stash-visible` | Show or hide the stash | |
| 265 | - | | `toggle-overview` | Open the [overview](#overview), or close it on the selected window | | 181 | + | | `toggle-overview` | Open or close the [overview](#overview) | |
| 266 | - | | `switch-column-next` | Switch to the column used before this one on the workspace. Hold Cmd and press again to go further back; after a short hold a strip of cards shows the columns, most recent first, and releasing Cmd focuses the selected one. Escape cancels; clicking a card picks it, and a click elsewhere cancels and is not passed to windows. A click before the cards show cancels and acts as usual. Outside cascade, a maximized column hands its maximization to the target. A workspace capture session pauses while the strip shows, as in the overview | | 182 | + | | `switch-column-next` / `-prev` | Switch to the previously used column. Hold `cmd` and press again to go further back | |
| 267 | - | | `switch-column-prev` | The same, in the opposite direction | | 183 | + | | `maximize-column` | Toggle full width for the focused column | |
| 268 | - | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off; in cascade, the column takes a band of its own. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | | 184 | + | | `cycle-column-width` | Grow the focused column by one [cell](#column-widths), then back to equal widths | |
| 269 | - | | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes | | 185 | + | | `set-column-width <+N\|-N>` | Widen or narrow the focused column by N cells | |
| 270 | - | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it comes the previous or next occupied workspace, then the neighbor monitor, then the trailing empty workspace. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays | | 186 | + | | `set-window-height <+N%\|-N%>` | Change the focused window's share of its column | |
| 271 | - | | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends | | 187 | + | | `focus-column-left/right` | Focus the neighbor column, then the neighbor monitor | |
| 272 | - | | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen | | 188 | + | | `focus-window-up/down` | Focus the neighbor window in the column, then the neighbor workspace (band in cascade) | |
| 273 | - | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes | | 189 | + | | `move-column-left/right` | Move the focused column, then to the neighbor monitor | |
| 274 | - | | `set-column-width <+N\|-N>` | Widen or narrow the focused column by N cells (1-16), never below one cell and without cycling back to equal shares. In `cascade` and `fixed` overflow the cells come from, or go to, the other columns of the row; in `scroll` overflow the column grows on its own, up to `max-columns` cells | | 190 | + | | `move-window-up/down` | Move the focused window in its column | |
| 275 | - | | `set-window-height <+N%\|-N%>` | Give the focused window N% more or less of its column's height; the other rows give or take it in proportion. Each row keeps at least 10%. A window joining or leaving the column resets equal rows. Does nothing past 9 rows | | 191 | + | | `consume-or-expel-window-left/right` | Join the neighbor column, or leave the current one | |
| 276 | - | | `consume-or-expel-window-left/right` | A lone window joins the neighbor column; a stacked one leaves for a new column | | 192 | + | | `focus-workspace <N>` / `-prev/-next` | Show a workspace | |
| 277 | - | | `focus-workspace <N>` / `focus-workspace-prev/next` | Show a numbered or neighbor workspace | | 193 | + | | `workspace <name>` | Show or hide a named workspace | |
| 278 | - | | `workspace <name>` | Toggle a named workspace | | 194 | + | | `move-column-to-workspace <N>` / `-prev/-next` | Move the focused column to a workspace | |
| 279 | - | | `move-column-to-workspace <N>` / `-prev/-next` | Move the focused column | | 195 | + | | `move-window-to-workspace <N>` / `-prev/-next` | Move the focused window to a workspace | |
| 280 | - | | `move-window-to-workspace <N>` / `-prev/-next` | Move only the focused window | | 196 | + | | `move-workspace-prev/next` | Swap the workspace with its neighbor | |
| 281 | - | | `focus-monitor-left/right` / `focus-monitor-up/down` | Focus the neighbor monitor in that direction | | 197 | + | | `focus-monitor-left/right/up/down` | Focus the neighbor monitor | |
| 282 | - | | `move-workspace-to-monitor-left/right` / `move-workspace-to-monitor-up/down` | Move the workspace to the neighbor monitor in that direction; it gets a new home | | 198 | + | | `move-workspace-to-monitor-left/right/up/down` | Move the workspace to the neighbor monitor | |
| 283 | - | | `move-workspace-prev/next` | Swap the current numbered workspace with its neighbor; the view follows it. It never passes the empty workspace at the end | | 199 | + | | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config | |
| 284 | - | | `scale-up` / `scale-down` | Zoom the whole display; one second after the last press, the scale is saved to `output.<name>.scale` and a notification confirms it (`notify-send`) | | |||
| 285 | | `quit` | Exit NeferWL | | 200 | | `quit` | Exit NeferWL | | ||
| 286 | 201 | ||||
| 287 | - | ### Stash | 202 | + | ### Default binds |
| 288 | - | ||||
| 289 | - | Each workspace has a stash: a horizontal strip of windows set aside with `toggle-window-stash`, in the order they arrived. The selected window is centred over the tiles, `stash.width` of the usable width (80% by default) and 80% of its height. Its left and right neighbors sit `stash.gap` beside it and show up to the screen edges, dimmed (`stash.dim`); a click on one selects it. The others wait off screen. | |||
| 290 | - | ||||
| 291 | - | While the stash has the focus, `focus-column-left/right` and the three-finger swipe move through it, sliding the view, and stop at its ends; `focus-window-up/down` do nothing. Hide it with `toggle-stash-visible`, or with a four-finger swipe down or up, to get back to the tiles; a swipe down shows it again. Scripts see each stashed window's place, and whether the stash is hidden, in the [state file](desktop.md#state-for-scripts). | |||
| 292 | - | ||||
| 293 | - | Native floating windows are not in the stash: they stay centred in the usable output area. A window-sized float that fills that area (within two border widths plus two logical pixels per axis) stays below the columns when you focus a tile; `focus-window-up` at the top of a column raises it again. It does not hide bars or pin focus. Small dialogs and file pickers always stay above the columns; shrinking a float promotes it above them. Real fullscreen remains exclusive, except for the dialogs shown over it: the fullscreen window's own, those of stashed windows while the stash shows over it, and their own dialogs. A dialog belongs to a window through `xdg_toplevel.set_parent`, or through `zxdg_importer_v2` when another program opens it, as the desktop portal does. A dialog the app marks modal through `xdg_dialog_v1` (GTK4 does for "Save as…") blocks its window: clicking it, activating it, picking it in the overview or moving the focus onto it within its workspace focuses the dialog instead, and so do nested modal dialogs. A focus move away from the dialog reaches its window once, so the next move can go past it. | |||
| 294 | - | ||||
| 295 | - | ### Free floating windows | |||
| 296 | - | ||||
| 297 | - | `toggle-floating` lifts the focused tile out of its column into a free floating window. NeferWL sets its size, and it stays where you put it, kept inside the usable area. Its place is relative to the screen, so it keeps it on another workspace or monitor of any size. | |||
| 298 | 203 | ||||
| 299 | - | On a free floating window, `move-column-left/right` and `move-window-up/down` move it by 50 logical pixels, and `set-column-width`/`set-window-height` resize it: a width step moves it by N cells (`Usable.W / max-columns` each), a height step by N% of the usable height. `toggle-window-stash` stashes it and brings it back floating at the same place. `toggle-floating` returns it to its column. | 204 | + | | Keys | Action | |
| 205 | + | |---|---| | |||
| 206 | + | | `cmd+return` | `spawn-terminal` | | |||
| 207 | + | | `ctrl+cmd+space` | `spawn fuzzel` | | |||
| 208 | + | | `cmd+q` | `close-window` | | |||
| 209 | + | | `cmd+left/right`, `cmd+h/l` | `focus-column-left/right` | | |||
| 210 | + | | `cmd+up/down`, `cmd+k/j` | `focus-window-up/down` | | |||
| 211 | + | | `cmd+shift+left/right`, `cmd+shift+h/l` | `move-column-left/right` | | |||
| 212 | + | | `cmd+shift+up/down`, `cmd+shift+k/j` | `move-window-up/down` | | |||
| 213 | + | | `cmd+bracketleft/bracketright` | `consume-or-expel-window-left/right` | | |||
| 214 | + | | `cmd+alt+left/right`, `cmd+alt+h/l` | `set-column-width -1/+1` | | |||
| 215 | + | | `cmd+alt+up/down`, `cmd+alt+k/j` | `set-window-height -10%/+10%` | | |||
| 216 | + | | `cmd+r` | `cycle-column-width` | | |||
| 217 | + | | `cmd+f` | `maximize-column` | | |||
| 218 | + | | `cmd+shift+f` | `toggle-fullscreen` | | |||
| 219 | + | | `cmd+shift+space` | `toggle-floating` | | |||
| 220 | + | | `cmd+s` | `toggle-stash-visible` | | |||
| 221 | + | | `cmd+shift+s` | `toggle-window-stash` | | |||
| 222 | + | | `cmd+o` | `toggle-overview` | | |||
| 223 | + | | `cmd+tab` / `cmd+shift+tab` | `switch-column-next/prev` | | |||
| 224 | + | | `cmd+1`…`9` (`code:2`…`10`) | `focus-workspace 1`…`9` | | |||
| 225 | + | | `cmd+shift+1`…`9` | `move-column-to-workspace 1`…`9` | | |||
| 226 | + | | `cmd+pageup/pagedown` | `focus-workspace-prev/next` | | |||
| 227 | + | | `cmd+shift+pageup/pagedown` | `move-column-to-workspace-prev/next` | | |||
| 228 | + | | `cmd+ctrl+arrows`, `cmd+ctrl+h/j/k/l` | `focus-monitor-*` | | |||
| 229 | + | | `cmd+ctrl+shift+left/right`, `cmd+ctrl+shift+h/l` | `move-workspace-to-monitor-left/right` | | |||
| 230 | + | | `cmd+ctrl+shift+up/down`, `cmd+ctrl+shift+k/j` | `move-workspace-prev/next` | | |||
| 231 | + | | `cmd+code:12` / `cmd+code:13` (`-` / `=` on US) | `scale-down` / `scale-up` | | |||
| 232 | + | | `ctrl+alt+backspace` | `quit` | | |||
| 300 | 233 | ||||
| 301 | - | ### Moving windows with the pointer | 234 | + | With `keyboard.cmd = alt` or `ctrl`, the defaults that would combine `cmd` with that same modifier are dropped. Bind those actions to other keys. |
| 302 | 235 | ||||
| 303 | - | Hold `cmd` and drag with the left button to move the window under the pointer; drag with the right button to resize a floating window from its bottom-right corner. Apps that drag their own title bar or edges (`xdg_toplevel.move`/`resize`) do the same without `cmd`. The window gets no pointer events while you drag, and `Escape` cancels the drag. | 236 | + | ## Layout |
| 304 | 237 | ||||
| 305 | - | A floating window follows the pointer and becomes a [free floating window](#free-floating-windows). A tile stays in place until you release the button. Until then, a bar or frame in `border.active` color shows where it will land: | 238 | + | ### Cascade |
| 306 | 239 | ||||
| 307 | - | - top or bottom quarter of a window: stack it above or below that window; | 240 | + | The default layout. Columns fill the screen side by side up to `max-columns`, then continue in a new band below. Each band fills the screen height. |
| 308 | - | - middle of a window: swap the two columns (a window from a stacked column becomes a new column there); | |||
| 309 | - | - in scroll overflow, the gap between two columns: a new column there. Moving the pointer against the left or right edge scrolls the workspace one column at a time. | |||
| 310 | 241 | ||||
| 311 | - | The drop can be on another monitor; focus follows the window. There, the middle of a window puts it in a new column at that place: only columns of the same workspace swap. A full `fixed` workspace has no room for a new column, so the window stacks in the outlined one. | 242 | + | - Left/right focus moves within a band, then to the neighbor workspace. |
| 243 | + | - Up/down moves through the windows of a column, then to the band above or below. | |||
| 244 | + | - A maximized column gets a band of its own. | |||
| 312 | 245 | ||||
| 313 | - | ### Overview | 246 | + | ### Column widths |
| 314 | 247 | ||||
| 315 | - | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows, over a background darkened by `floating.dim`. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, and in a vertical overview up to two cards behind peek above it and passed cards peek below (a Cascade overview fans covering floats left and right). Neighbor workspaces show dimmed stacks. Each monitor has one overview, laid out along the navigation axis of the current workspace's layout: neighbors appear above and below a `scroll` or `fixed` workspace and left and right of a `cascade` one (see [Cascade](#cascade)); stepping to a workspace with another layout turns the overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with rules across the axis (horizontal in a vertical overview, vertical in a side-by-side one) separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear beside it along that axis; in a vertical overview, rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal previous/next workspace navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. | 248 | + | A cell is `1/max-columns` of the screen width. A column is 1 to 16 cells wide. Columns without a set width share the space equally. |
| 316 | 249 | ||||
| 317 | - | | Key | Action | | 250 | + | ### Stash |
| 318 | - | | --- | --- | | |||
| 319 | - | | `h` / `l`, `left` / `right` | Move within a front column-group card; left of its first tile enters the stash. Left on a single-window card sends it behind, or enters the stash when none is behind; right does nothing on a single card. | | |||
| 320 | - | | `k` / `j`, `up` / `down` | Browse cards within the selected stash pile; otherwise bring the next card above / previous card below to the front, then change workspace at the stack boundary | | |||
| 321 | - | | `return` | Show the selected front card; picking a hidden column moves the maximization to it. Other rows shown keep their focus, maximization and float order | | |||
| 322 | - | | `escape` | Return to the original workspace; every row shown keeps its focus, maximization and float order | | |||
| 323 | 251 | ||||
| 324 | - | The focus binds (`focus-column-left/right`, `focus-window-up/down`, `cmd+arrows` by default) move the selection like these keys. In the overview, `focus-workspace-prev/next` also traverse cards before crossing workspace rows, including named ones, except inside a selected stash pile. The keys above describe vertical overviews; in a [Cascade](#cascade) overview they move as described there. | 252 | + | Each workspace has a stash: a strip of windows set aside with `toggle-window-stash`. The selected one shows centered over your columns, with its neighbors dimmed on the sides. `focus-column-left/right` or a three-finger swipe browse it. `toggle-stash-visible` or a four-finger swipe hides it. |
| 325 | 253 | ||||
| 326 | - | Each workspace's [stash](#stash), hidden or not, shows as a pile of cards on the left of its row, which stays centred unless it would overlap the pile (in a Cascade overview the pile extends its workspace's slot to the left): its selected window in front, up to three others behind it, dimmed. In the pile, `k` / `j` or up/down browse stash cards and stop at either end without changing workspace. In a vertical overview, right (`l`) leaves the pile immediately, left (`h`) stays in the pile, and on a stash-only workspace right deselects the pile so up/down can change workspace. In a Cascade overview, left from the pile goes to the previous workspace, right returns to the band's first column (or to the next workspace on a stash-only workspace), and up/down browse the pile. Browsing brings the selected card to the front without reordering the stash. `return` or a click on a card closes the overview with the stash shown on that window. | 254 | + | ### Free floating windows |
| 327 | 255 | ||||
| 328 | - | A four-finger swipe up opens the overview, or hides the [stash](#stash) when it is shown, and a swipe down closes the overview on the selection (with the overview closed, a swipe down shows or hides the stash), whatever `touchpad.natural-scroll` says. Two-finger scrolling belongs to applications and never navigates the overview; previews do not accept application input. In a vertical overview, three fingers move the selection during the gesture, following `touchpad.natural-scroll`: horizontally, a short distance selects the next app and further movement repeats at a longer distance; vertically, one step per gesture selects a stack card or changes workspace. Inside a selected stash pile, vertical movement browses its cards and horizontal movement enters or leaves the pile. Lifting resets the gesture without an extra step; cancellation keeps selections already made. A Cascade overview follows the fingers instead (see [Cascade](#cascade)). The mouse wheel moves one step per notch, following `mouse.natural-scroll`. Three-finger navigation, the wheel and keyboard navigation share the same pile boundaries. | 256 | + | `toggle-floating` turns the focused window into a floating window that stays where you put it. On a floating window, move binds move it by 50 pixels and resize binds resize it. |
| 329 | 257 | ||||
| 330 | - | Return or a click on a front tile or peeking card commits it; card changes remain provisional until then. `close-window` targets the selected preview. Move-to-workspace binds (`move-column-to-workspace*`, `move-window-to-workspace*`) move the selected preview at once and keep the overview open; with `focus.follow-move`, the selection follows it. Escape does not undo a move, and the row that received the window keeps it focused. A selected stash card, or a row with no preview, does not move. Other window mutation binds (moving within the row, resizing, maximizing, fullscreen and stash toggles), moving a workspace to another monitor and `switch-column-next` / `switch-column-prev` are disabled while the overview is open. Workspace and monitor navigation, launch and quit binds remain active. | 258 | + | ### Moving windows with the pointer |
| 331 | 259 | ||||
| 332 | - | ### Default binds | 260 | + | - `cmd` + left drag moves a window. Drop it on the top or bottom of another window to stack it there, or in the middle to swap. |
| 261 | + | - `cmd` + right drag resizes a floating window. | |||
| 262 | + | - `Escape` cancels the drag. | |||
| 333 | 263 | ||||
| 334 | - | With `keyboard.cmd = alt`, the `cmd+alt+…` binds have the same keys as `cmd+…` and are dropped with a warning; with `keyboard.cmd = ctrl`, so are the `cmd+ctrl+…` ones. Bind those actions to other keys. | 264 | + | ### Overview |
| 335 | 265 | ||||
| 336 | - | | Keys | Action | | 266 | + | `toggle-overview` (or a four-finger swipe up) shows previews of your workspaces. Arrows, `h/j/k/l` or three-finger swipes move the selection; `Return` or a click opens it; `Escape` goes back. `close-window` and the move-to-workspace binds work on the selected preview. |
| 337 | - | |---|---| | |||
| 338 | - | | `cmd+return` | `spawn-terminal` | | |||
| 339 | - | | `cmd+left` | `focus-column-left` | | |||
| 340 | - | | `cmd+right` | `focus-column-right` | | |||
| 341 | - | | `cmd+up` | `focus-window-up` | | |||
| 342 | - | | `cmd+down` | `focus-window-down` | | |||
| 343 | - | | `cmd+shift+left` | `move-column-left` | | |||
| 344 | - | | `cmd+shift+right` | `move-column-right` | | |||
| 345 | - | | `cmd+h` | `focus-column-left` | | |||
| 346 | - | | `cmd+l` | `focus-column-right` | | |||
| 347 | - | | `cmd+k` | `focus-window-up` | | |||
| 348 | - | | `cmd+j` | `focus-window-down` | | |||
| 349 | - | | `cmd+shift+h` | `move-column-left` | | |||
| 350 | - | | `cmd+shift+l` | `move-column-right` | | |||
| 351 | - | | `cmd+bracketleft` | `consume-or-expel-window-left` | | |||
| 352 | - | | `cmd+bracketright` | `consume-or-expel-window-right` | | |||
| 353 | - | | `cmd+shift+up` / `cmd+shift+k` | `move-window-up` | | |||
| 354 | - | | `cmd+shift+down` / `cmd+shift+j` | `move-window-down` | | |||
| 355 | - | | `cmd+alt+left` / `cmd+alt+h` | `set-column-width -1` | | |||
| 356 | - | | `cmd+alt+right` / `cmd+alt+l` | `set-column-width +1` | | |||
| 357 | - | | `cmd+alt+up` / `cmd+alt+k` | `set-window-height -10%` | | |||
| 358 | - | | `cmd+alt+down` / `cmd+alt+j` | `set-window-height +10%` | | |||
| 359 | - | | `cmd+r` | `cycle-column-width` | | |||
| 360 | - | | `cmd+f` | `maximize-column` | | |||
| 361 | - | | `cmd+shift+f` | `toggle-fullscreen` | | |||
| 362 | - | | `cmd+s` | `toggle-stash-visible` | | |||
| 363 | - | | `cmd+shift+s` | `toggle-window-stash` | | |||
| 364 | - | | `cmd+shift+space` | `toggle-floating` | | |||
| 365 | - | | `cmd+o` | `toggle-overview` | | |||
| 366 | - | | `cmd+tab` | `switch-column-next` | | |||
| 367 | - | | `cmd+shift+tab` | `switch-column-prev` | | |||
| 368 | - | | `cmd+q` | `close-window` | | |||
| 369 | - | | `ctrl+alt+backspace` | `quit` | | |||
| 370 | - | | `ctrl+cmd+space` | `spawn fuzzel` | | |||
| 371 | - | | `cmd+pageup` | `focus-workspace-prev` | | |||
| 372 | - | | `cmd+pagedown` | `focus-workspace-next` | | |||
| 373 | - | | `cmd+shift+pageup` | `move-column-to-workspace-prev` | | |||
| 374 | - | | `cmd+shift+pagedown` | `move-column-to-workspace-next` | | |||
| 375 | - | | `cmd+code:2` | `focus-workspace 1` | | |||
| 376 | - | | `cmd+code:3` | `focus-workspace 2` | | |||
| 377 | - | | `cmd+code:4` | `focus-workspace 3` | | |||
| 378 | - | | `cmd+code:5` | `focus-workspace 4` | | |||
| 379 | - | | `cmd+code:6` | `focus-workspace 5` | | |||
| 380 | - | | `cmd+code:7` | `focus-workspace 6` | | |||
| 381 | - | | `cmd+code:8` | `focus-workspace 7` | | |||
| 382 | - | | `cmd+code:9` | `focus-workspace 8` | | |||
| 383 | - | | `cmd+code:10` | `focus-workspace 9` | | |||
| 384 | - | | `cmd+shift+code:2` | `move-column-to-workspace 1` | | |||
| 385 | - | | `cmd+shift+code:3` | `move-column-to-workspace 2` | | |||
| 386 | - | | `cmd+shift+code:4` | `move-column-to-workspace 3` | | |||
| 387 | - | | `cmd+shift+code:5` | `move-column-to-workspace 4` | | |||
| 388 | - | | `cmd+shift+code:6` | `move-column-to-workspace 5` | | |||
| 389 | - | | `cmd+shift+code:7` | `move-column-to-workspace 6` | | |||
| 390 | - | | `cmd+shift+code:8` | `move-column-to-workspace 7` | | |||
| 391 | - | | `cmd+shift+code:9` | `move-column-to-workspace 8` | | |||
| 392 | - | | `cmd+shift+code:10` | `move-column-to-workspace 9` | | |||
| 393 | - | | `cmd+ctrl+left` | `focus-monitor-left` | | |||
| 394 | - | | `cmd+ctrl+right` | `focus-monitor-right` | | |||
| 395 | - | | `cmd+ctrl+up` / `cmd+ctrl+k` | `focus-monitor-up` | | |||
| 396 | - | | `cmd+ctrl+down` / `cmd+ctrl+j` | `focus-monitor-down` | | |||
| 397 | - | | `cmd+ctrl+h` | `focus-monitor-left` | | |||
| 398 | - | | `cmd+ctrl+l` | `focus-monitor-right` | | |||
| 399 | - | | `cmd+ctrl+shift+left` | `move-workspace-to-monitor-left` | | |||
| 400 | - | | `cmd+ctrl+shift+right` | `move-workspace-to-monitor-right` | | |||
| 401 | - | | `cmd+ctrl+shift+h` | `move-workspace-to-monitor-left` | | |||
| 402 | - | | `cmd+ctrl+shift+l` | `move-workspace-to-monitor-right` | | |||
| 403 | - | | `cmd+ctrl+shift+up` / `cmd+ctrl+shift+k` | `move-workspace-prev` | | |||
| 404 | - | | `cmd+ctrl+shift+down` / `cmd+ctrl+shift+j` | `move-workspace-next` | | |||
| 405 | - | | `cmd+code:13` | `scale-up` | | |||
| 406 | - | | `cmd+code:12` | `scale-down` | | |||
M docs/desktop.md +30 -69
| @@ -1,29 +1,19 @@ | |||
| 1 | 1 | # Desktop integration | |
| 2 | 2 | ||
| 3 | - | How NeferWL runs programs and works with bars, clipboard managers, X11 apps, idle tools and scripts. | |
| 4 | - | ||
| 5 | 3 | ## Running commands | |
| 6 | 4 | ||
| 7 | - | `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. | |
| 5 | + | `spawn` binds, `startup` and `terminal` run a program directly, **without a shell**. The line is split on spaces: the first word is the program (from `PATH` or a full path), the rest are its arguments. `;` separates several commands, which all start at once. | |
| 8 | 6 | ||
| 9 | 7 | ```text | |
| 10 | 8 | bind.ctrl+cmd+space = spawn fuzzel | |
| 11 | - | startup = waybar | |
| 12 | - | startup = wl-paste --watch cliphist store | |
| 13 | - | ``` | |
| 14 | - | ||
| 15 | - | `spawn` binds and `startup` lines can hold several commands separated by `;`. They all start right away, in order; one does not wait for the previous one to finish. Empty commands are ignored. A `;` always separates commands, so put an argument containing `;` in a script. `terminal` and workspace slot commands (`column.N`) take a single command. | |
| 16 | - | ||
| 17 | - | ```text | |
| 18 | 9 | startup = waybar; mako; wl-paste --watch cliphist store | |
| 19 | - | bind.cmd+shift+s = spawn grim; notify-send Screenshot saved | |
| 20 | 10 | ``` | |
| 21 | 11 | ||
| 22 | - | Apart from `;`, 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: | |
| 12 | + | Pipes, `&&`, redirections, `$VARIABLES`, `~` and quotes are passed as plain text. For those, write a script in your `PATH` and run it: | |
| 23 | 13 | ||
| 24 | 14 | ```sh | |
| 25 | 15 | #!/bin/sh | |
| 26 | - | # ~/.local/bin/screenshot-main: save the main monitor with a dated name. | |
| 16 | + | # ~/.local/bin/screenshot-main | |
| 27 | 17 | nefercap screenshot -output DP-1 -file "$HOME/Pictures/$(date +%F-%T).png" | |
| 28 | 18 | ``` | |
| 29 | 19 | ||
| @@ -31,23 +21,20 @@ nefercap screenshot -output DP-1 -file "$HOME/Pictures/$(date +%F-%T).png" | |||
| 31 | 21 | bind.cmd+shift+p = spawn screenshot-main | |
| 32 | 22 | ``` | |
| 33 | 23 | ||
| 34 | - | 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`). | |
| 35 | - | ||
| 36 | - | 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. | |
| 24 | + | Programs started by NeferWL get `WAYLAND_DISPLAY`, `DISPLAY` and `NEFERWL_STATE`. `startup` changes apply at the next session. | |
| 37 | 25 | ||
| 38 | 26 | ## Clipboard | |
| 39 | 27 | ||
| 40 | - | 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: | |
| 28 | + | The clipboard, primary selection (middle-click paste) and clipboard managers work. A [cliphist](https://github.com/sentriz/cliphist) history: | |
| 41 | 29 | ||
| 42 | 30 | ```text | |
| 43 | 31 | startup = wl-paste --watch cliphist store | |
| 44 | 32 | bind.cmd+v = spawn cliphist-pick | |
| 45 | 33 | ``` | |
| 46 | 34 | ||
| 47 | - | `~/.local/bin/cliphist-pick`: | |
| 48 | - | ||
| 49 | 35 | ```sh | |
| 50 | 36 | #!/bin/sh | |
| 37 | + | # ~/.local/bin/cliphist-pick | |
| 51 | 38 | cliphist list | fuzzel --dmenu | cliphist decode | wl-copy | |
| 52 | 39 | ``` | |
| 53 | 40 | ||
| @@ -55,102 +42,76 @@ Drag and drop is not supported yet. | |||
| 55 | 42 | ||
| 56 | 43 | ## Input methods | |
| 57 | 44 | ||
| 58 | - | NeferWL supports `zwp_text_input_v3` for apps and `zwp_input_method_v2` for one input method, such as [fcitx5](https://fcitx-im.org) or IBus. Start the input method at session start: | |
| 45 | + | [fcitx5](https://fcitx-im.org) and IBus work through `zwp_input_method_v2`. Start one at session start: | |
| 59 | 46 | ||
| 60 | 47 | ```text | |
| 61 | 48 | startup = fcitx5 -d | |
| 62 | 49 | ``` | |
| 63 | 50 | ||
| 64 | - | Text input follows keyboard focus. The input method can grab the keyboard to compose text from keys (CJK, dead keys); the focused app then receives the composed text. A second input method gets `unavailable` until the first one exits. Candidate popups (`zwp_input_popup_surface_v2`) receive the text cursor position but are not drawn yet. | |
| 51 | + | Candidate popups are not drawn yet. | |
| 65 | 52 | ||
| 66 | 53 | ## X11 apps | |
| 67 | 54 | ||
| 68 | - | X11 apps such as Steam 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. | |
| 55 | + | X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. | |
| 69 | 56 | ||
| 70 | 57 | ```text | |
| 71 | - | xwayland = xwayland-satellite # the default; a path also works | |
| 72 | - | xwayland = off # no X11 display | |
| 58 | + | xwayland = off # no X11 support | |
| 73 | 59 | ``` | |
| 74 | 60 | ||
| 75 | - | Do not start xwayland-satellite yourself with `startup`. Changing `xwayland` takes effect at the next start. | |
| 76 | - | ||
| 77 | 61 | ## Session locking | |
| 78 | 62 | ||
| 79 | - | External lockers use `ext-session-lock-v1`. NeferWL isolates desktop input and captures, covers every output, and stays protected if the locker dies. Authentication belongs to the locker; screen-off and a visual cover do not replace locking. See [Session locking](session-lock.md) for confirmation, recovery and security limits. | |
| 63 | + | Use an external locker such as swaylock or hyprlock. See [Session locking](session-lock.md). | |
| 80 | 64 | ||
| 81 | 65 | ## Screen capture | |
| 82 | 66 | ||
| 83 | - | nefercap, `grim` and screen sharing through `xdg-desktop-portal-wlr` work out of the box. Other recorders must be added to `/etc/neferwl/capture-allow`. Every capture shows a red border. See [Screen capture](capture.md). | |
| 67 | + | nefercap, `grim` and screen sharing through `xdg-desktop-portal-wlr` work out of the box. Other tools must be added to the allowlist. See [Screen capture](capture.md). | |
| 84 | 68 | ||
| 85 | 69 | ## Idle and screen off | |
| 86 | 70 | ||
| 87 | - | NeferWL supports `ext_idle_notifier_v1` and `zwlr_output_power_management_v1`, so [swayidle](https://github.com/swaywm/swayidle) and [wlopm](https://git.sr.ht/~leon_plickat/wlopm) turn the screens off after a delay. A window that inhibits idle, such as a video player or a game, keeps them on. A display that disconnects in deep sleep comes back off when it reconnects, unless there was keyboard, pointer or touch input in the meantime. | |
| 88 | - | ||
| 89 | - | Programs that inhibit idle over D-Bus instead (browsers, Electron apps, VLC) are covered too: NeferWL serves `org.freedesktop.ScreenSaver` on the session bus. The desktop portal's `Inhibit` reaches it only through a portal backend that forwards to that name, such as `xdg-desktop-portal-gtk`; `xdg-desktop-portal-wlr` does not implement `Inhibit`. An inhibition ends when the program releases it or leaves the bus, and `SimulateUserActivity` counts as input. If another program owns the name, or the session bus is missing, NeferWL logs a warning and tries again later, so it takes over once the name is free or the bus is back. | |
| 71 | + | [swayidle](https://github.com/swaywm/swayidle) and [wlopm](https://git.sr.ht/~leon_plickat/wlopm) turn the screens off after a delay: | |
| 90 | 72 | ||
| 91 | 73 | ```text | |
| 92 | 74 | startup = swayidle -w timeout 300 wlopm-off resume wlopm-on | |
| 93 | 75 | ``` | |
| 94 | 76 | ||
| 95 | - | `swayidle` runs its commands through a shell; NeferWL does not, so the quoted form `timeout 300 'wlopm --off "*"'` does not fit on a `startup` line. Put each command in a script (see [Running commands](#running-commands)): | |
| 96 | - | ||
| 97 | 77 | ```sh | |
| 98 | 78 | #!/bin/sh | |
| 99 | 79 | # ~/.local/bin/wlopm-off (wlopm-on is the same with --on) | |
| 100 | 80 | exec wlopm --off '*' | |
| 101 | 81 | ``` | |
| 102 | 82 | ||
| 103 | - | ## Power keys | |
| 83 | + | Video players, games and browsers keep the screens on while they ask to, through Wayland or D-Bus (`org.freedesktop.ScreenSaver`). | |
| 104 | 84 | ||
| 105 | - | NeferWL takes logind's `handle-power-key`, `handle-suspend-key`, `handle-hibernate-key` and `handle-reboot-key` inhibitor locks. logind then no longer powers off, suspends, hibernates or reboots when one of those keys is pressed: the key reaches NeferWL, and nothing happens unless a bind uses it. Binds are off while the session is locked, so these keys do nothing on the lock screen. | |
| 85 | + | ## Power keys | |
| 106 | 86 | ||
| 107 | - | | logind key | Kernel key | Bind | | |
| 108 | - | |---|---|---| | |
| 109 | - | | power | `KEY_POWER` | `XF86PowerOff` | | |
| 110 | - | | suspend | `KEY_SLEEP` | `XF86Sleep` | | |
| 111 | - | | hibernate | `KEY_SUSPEND` | `XF86Suspend` | | |
| 112 | - | | reboot | `KEY_RESTART` | `code:408` (no keysym) | | |
| 87 | + | NeferWL stops logind from acting on the power, suspend, hibernate and reboot keys. They do nothing unless you bind them: | |
| 113 | 88 | ||
| 114 | 89 | ```text | |
| 115 | 90 | bind.XF86PowerOff = spawn systemctl suspend | |
| 116 | 91 | bind.XF86Suspend = spawn systemctl hibernate | |
| 117 | 92 | ``` | |
| 118 | 93 | ||
| 119 | - | The lid switch is not covered: logind still handles it. The locks last as long as NeferWL runs; if it crashes, logind handles the keys again. When logind restarts, NeferWL keeps the old locks until it has taken new ones. If the system bus or logind is missing, NeferWL logs a warning and tries again later, and the keys keep logind's behaviour until it succeeds. | |
| 120 | - | ||
| 121 | - | ## Bars | |
| 122 | - | ||
| 123 | - | NeferWL supports `ext_workspace_manager_v1` for bars such as Waybar 0.13+ (`ext/workspaces`) and ironbar. Bars get workspace updates and can switch workspaces without polling. | |
| 94 | + | | Key | Bind name | | |
| 95 | + | |---|---| | |
| 96 | + | | Power | `XF86PowerOff` | | |
| 97 | + | | Sleep | `XF86Sleep` | | |
| 98 | + | | Suspend | `XF86Suspend` | | |
| 99 | + | | Reboot | `code:408` | | |
| 124 | 100 | ||
| 125 | - | Each workspace has an `id`, the same as `workspace_id` in the [state file](#state-for-scripts): | |
| 101 | + | The lid switch is still handled by logind. | |
| 126 | 102 | ||
| 127 | - | | Workspace | `id` | Stable | | |
| 128 | - | |---|---|---| | |
| 129 | - | | Named | `name:<configured name>` | Across launches | | |
| 130 | - | | Numbered | `<prefix>-<n>`, with a random prefix per launch | For the current launch only | | |
| 103 | + | ## Bars | |
| 131 | 104 | ||
| 132 | - | Only store settings keyed on named workspace IDs. | |
| 105 | + | Waybar 0.13+ (`ext/workspaces`) and ironbar show and switch workspaces. Taskbars can list, focus and close windows. | |
| 133 | 106 | ||
| 134 | - | Taskbars can list, focus, close and fullscreen windows through `zwlr_foreign_toplevel_manager_v1`, and notification daemons such as Dunst can detect fullscreen windows. `ext_foreign_toplevel_list_v1` lists the same windows with an `identifier` (`neferwl-<window>-<mapping>`) that is never reused in a session (a window that maps again gets a new one); portals use it to share single windows. While the session is locked, bars and taskbars get no updates. | |
| 107 | + | Workspace IDs are `name:<name>` for named workspaces, stable across launches. Numbered workspaces get a random prefix per launch, so do not save settings keyed on them. | |
| 135 | 108 | ||
| 136 | 109 | ## State for scripts | |
| 137 | 110 | ||
| 138 | - | 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: | |
| 139 | - | ||
| 140 | - | - every output, with its active numbered workspace, workspace count, and the name (`workspace`) and ID string (`workspace_id`) of the workspace on screen; | |
| 141 | - | - the focused output and window; | |
| 142 | - | - every window, with its app ID, PID, output, workspace, `workspace_id` (the ID string of its workspace, numbered or hidden) and whether it is on screen (`visible`); | |
| 143 | - | - `floating: true` for floating windows. Stashed windows also have their 1-based place in their workspace's stash (`stash_index` of `stash_count`, both `0` outside it) and `hidden: true` while the stash is hidden. | |
| 144 | - | - for tiled windows, their 1-based `column` (layout order, left to right with `overflow = "scroll"`; with `overflow = "fixed"`, the spiral breaks that, so a later column can sit below or beside an earlier one) and `row` (top to bottom in the column); both are `0` when `floating` is true. Sort by `column`, `row` for layout order. | |
| 145 | - | ||
| 146 | - | ```sh | |
| 147 | - | neferwl state # the whole state as JSON | |
| 148 | - | neferwl state output-of "$PID" # the output of that process's window, or its nearest parent's | |
| 149 | - | ``` | |
| 150 | - | ||
| 151 | - | `output-of` finds application windows only, because a bar has no single output. | |
| 111 | + | NeferWL writes its state as JSON to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json`, also given to started programs as `$NEFERWL_STATE`. It lists outputs, workspaces, the focused window, and every window with its app ID, PID, workspace, column and row. | |
| 152 | 112 | ||
| 153 | 113 | ```sh | |
| 154 | - | # Windows waiting in hidden stashes | |
| 155 | - | neferwl state | jq '[.windows[] | select(.hidden)] | length' | |
| 114 | + | neferwl state # the whole state | |
| 115 | + | neferwl state output-of "$PID" # the output showing that process's window | |
| 116 | + | neferwl state | jq '[.windows[] | select(.hidden)] | length' # windows in hidden stashes | |
| 156 | 117 | ``` | |
| @@ -1,29 +1,19 @@ | |||||
| 1 | # Desktop integration | 1 | # Desktop integration | ||
| 2 | 2 | ||||
| 3 | - | How NeferWL runs programs and works with bars, clipboard managers, X11 apps, idle tools and scripts. | |||
| 4 | - | ||||
| 5 | ## Running commands | 3 | ## Running commands | ||
| 6 | 4 | ||||
| 7 | - | `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. | 5 | + | `spawn` binds, `startup` and `terminal` run a program directly, **without a shell**. The line is split on spaces: the first word is the program (from `PATH` or a full path), the rest are its arguments. `;` separates several commands, which all start at once. |
| 8 | 6 | ||||
| 9 | ```text | 7 | ```text | ||
| 10 | bind.ctrl+cmd+space = spawn fuzzel | 8 | bind.ctrl+cmd+space = spawn fuzzel | ||
| 11 | - | startup = waybar | |||
| 12 | - | startup = wl-paste --watch cliphist store | |||
| 13 | - | ``` | |||
| 14 | - | ||||
| 15 | - | `spawn` binds and `startup` lines can hold several commands separated by `;`. They all start right away, in order; one does not wait for the previous one to finish. Empty commands are ignored. A `;` always separates commands, so put an argument containing `;` in a script. `terminal` and workspace slot commands (`column.N`) take a single command. | |||
| 16 | - | ||||
| 17 | - | ```text | |||
| 18 | startup = waybar; mako; wl-paste --watch cliphist store | 9 | startup = waybar; mako; wl-paste --watch cliphist store | ||
| 19 | - | bind.cmd+shift+s = spawn grim; notify-send Screenshot saved | |||
| 20 | ``` | 10 | ``` | ||
| 21 | 11 | ||||
| 22 | - | Apart from `;`, 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: | 12 | + | Pipes, `&&`, redirections, `$VARIABLES`, `~` and quotes are passed as plain text. For those, write a script in your `PATH` and run it: |
| 23 | 13 | ||||
| 24 | ```sh | 14 | ```sh | ||
| 25 | #!/bin/sh | 15 | #!/bin/sh | ||
| 26 | - | # ~/.local/bin/screenshot-main: save the main monitor with a dated name. | 16 | + | # ~/.local/bin/screenshot-main |
| 27 | nefercap screenshot -output DP-1 -file "$HOME/Pictures/$(date +%F-%T).png" | 17 | nefercap screenshot -output DP-1 -file "$HOME/Pictures/$(date +%F-%T).png" | ||
| 28 | ``` | 18 | ``` | ||
| 29 | 19 | ||||
| @@ -31,23 +21,20 @@ nefercap screenshot -output DP-1 -file "$HOME/Pictures/$(date +%F-%T).png" | |||||
| 31 | bind.cmd+shift+p = spawn screenshot-main | 21 | bind.cmd+shift+p = spawn screenshot-main | ||
| 32 | ``` | 22 | ``` | ||
| 33 | 23 | ||||
| 34 | - | 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`). | 24 | + | Programs started by NeferWL get `WAYLAND_DISPLAY`, `DISPLAY` and `NEFERWL_STATE`. `startup` changes apply at the next session. |
| 35 | - | ||||
| 36 | - | 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. | |||
| 37 | 25 | ||||
| 38 | ## Clipboard | 26 | ## Clipboard | ||
| 39 | 27 | ||||
| 40 | - | 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: | 28 | + | The clipboard, primary selection (middle-click paste) and clipboard managers work. A [cliphist](https://github.com/sentriz/cliphist) history: |
| 41 | 29 | ||||
| 42 | ```text | 30 | ```text | ||
| 43 | startup = wl-paste --watch cliphist store | 31 | startup = wl-paste --watch cliphist store | ||
| 44 | bind.cmd+v = spawn cliphist-pick | 32 | bind.cmd+v = spawn cliphist-pick | ||
| 45 | ``` | 33 | ``` | ||
| 46 | 34 | ||||
| 47 | - | `~/.local/bin/cliphist-pick`: | |||
| 48 | - | ||||
| 49 | ```sh | 35 | ```sh | ||
| 50 | #!/bin/sh | 36 | #!/bin/sh | ||
| 37 | + | # ~/.local/bin/cliphist-pick | |||
| 51 | cliphist list | fuzzel --dmenu | cliphist decode | wl-copy | 38 | cliphist list | fuzzel --dmenu | cliphist decode | wl-copy | ||
| 52 | ``` | 39 | ``` | ||
| 53 | 40 | ||||
| @@ -55,102 +42,76 @@ Drag and drop is not supported yet. | |||||
| 55 | 42 | ||||
| 56 | ## Input methods | 43 | ## Input methods | ||
| 57 | 44 | ||||
| 58 | - | NeferWL supports `zwp_text_input_v3` for apps and `zwp_input_method_v2` for one input method, such as [fcitx5](https://fcitx-im.org) or IBus. Start the input method at session start: | 45 | + | [fcitx5](https://fcitx-im.org) and IBus work through `zwp_input_method_v2`. Start one at session start: |
| 59 | 46 | ||||
| 60 | ```text | 47 | ```text | ||
| 61 | startup = fcitx5 -d | 48 | startup = fcitx5 -d | ||
| 62 | ``` | 49 | ``` | ||
| 63 | 50 | ||||
| 64 | - | Text input follows keyboard focus. The input method can grab the keyboard to compose text from keys (CJK, dead keys); the focused app then receives the composed text. A second input method gets `unavailable` until the first one exits. Candidate popups (`zwp_input_popup_surface_v2`) receive the text cursor position but are not drawn yet. | 51 | + | Candidate popups are not drawn yet. |
| 65 | 52 | ||||
| 66 | ## X11 apps | 53 | ## X11 apps | ||
| 67 | 54 | ||||
| 68 | - | X11 apps such as Steam 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. | 55 | + | X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. |
| 69 | 56 | ||||
| 70 | ```text | 57 | ```text | ||
| 71 | - | xwayland = xwayland-satellite # the default; a path also works | 58 | + | xwayland = off # no X11 support |
| 72 | - | xwayland = off # no X11 display | |||
| 73 | ``` | 59 | ``` | ||
| 74 | 60 | ||||
| 75 | - | Do not start xwayland-satellite yourself with `startup`. Changing `xwayland` takes effect at the next start. | |||
| 76 | - | ||||
| 77 | ## Session locking | 61 | ## Session locking | ||
| 78 | 62 | ||||
| 79 | - | External lockers use `ext-session-lock-v1`. NeferWL isolates desktop input and captures, covers every output, and stays protected if the locker dies. Authentication belongs to the locker; screen-off and a visual cover do not replace locking. See [Session locking](session-lock.md) for confirmation, recovery and security limits. | 63 | + | Use an external locker such as swaylock or hyprlock. See [Session locking](session-lock.md). |
| 80 | 64 | ||||
| 81 | ## Screen capture | 65 | ## Screen capture | ||
| 82 | 66 | ||||
| 83 | - | nefercap, `grim` and screen sharing through `xdg-desktop-portal-wlr` work out of the box. Other recorders must be added to `/etc/neferwl/capture-allow`. Every capture shows a red border. See [Screen capture](capture.md). | 67 | + | nefercap, `grim` and screen sharing through `xdg-desktop-portal-wlr` work out of the box. Other tools must be added to the allowlist. See [Screen capture](capture.md). |
| 84 | 68 | ||||
| 85 | ## Idle and screen off | 69 | ## Idle and screen off | ||
| 86 | 70 | ||||
| 87 | - | NeferWL supports `ext_idle_notifier_v1` and `zwlr_output_power_management_v1`, so [swayidle](https://github.com/swaywm/swayidle) and [wlopm](https://git.sr.ht/~leon_plickat/wlopm) turn the screens off after a delay. A window that inhibits idle, such as a video player or a game, keeps them on. A display that disconnects in deep sleep comes back off when it reconnects, unless there was keyboard, pointer or touch input in the meantime. | 71 | + | [swayidle](https://github.com/swaywm/swayidle) and [wlopm](https://git.sr.ht/~leon_plickat/wlopm) turn the screens off after a delay: |
| 88 | - | ||||
| 89 | - | Programs that inhibit idle over D-Bus instead (browsers, Electron apps, VLC) are covered too: NeferWL serves `org.freedesktop.ScreenSaver` on the session bus. The desktop portal's `Inhibit` reaches it only through a portal backend that forwards to that name, such as `xdg-desktop-portal-gtk`; `xdg-desktop-portal-wlr` does not implement `Inhibit`. An inhibition ends when the program releases it or leaves the bus, and `SimulateUserActivity` counts as input. If another program owns the name, or the session bus is missing, NeferWL logs a warning and tries again later, so it takes over once the name is free or the bus is back. | |||
| 90 | 72 | ||||
| 91 | ```text | 73 | ```text | ||
| 92 | startup = swayidle -w timeout 300 wlopm-off resume wlopm-on | 74 | startup = swayidle -w timeout 300 wlopm-off resume wlopm-on | ||
| 93 | ``` | 75 | ``` | ||
| 94 | 76 | ||||
| 95 | - | `swayidle` runs its commands through a shell; NeferWL does not, so the quoted form `timeout 300 'wlopm --off "*"'` does not fit on a `startup` line. Put each command in a script (see [Running commands](#running-commands)): | |||
| 96 | - | ||||
| 97 | ```sh | 77 | ```sh | ||
| 98 | #!/bin/sh | 78 | #!/bin/sh | ||
| 99 | # ~/.local/bin/wlopm-off (wlopm-on is the same with --on) | 79 | # ~/.local/bin/wlopm-off (wlopm-on is the same with --on) | ||
| 100 | exec wlopm --off '*' | 80 | exec wlopm --off '*' | ||
| 101 | ``` | 81 | ``` | ||
| 102 | 82 | ||||
| 103 | - | ## Power keys | 83 | + | Video players, games and browsers keep the screens on while they ask to, through Wayland or D-Bus (`org.freedesktop.ScreenSaver`). |
| 104 | 84 | ||||
| 105 | - | NeferWL takes logind's `handle-power-key`, `handle-suspend-key`, `handle-hibernate-key` and `handle-reboot-key` inhibitor locks. logind then no longer powers off, suspends, hibernates or reboots when one of those keys is pressed: the key reaches NeferWL, and nothing happens unless a bind uses it. Binds are off while the session is locked, so these keys do nothing on the lock screen. | 85 | + | ## Power keys |
| 106 | 86 | ||||
| 107 | - | | logind key | Kernel key | Bind | | 87 | + | NeferWL stops logind from acting on the power, suspend, hibernate and reboot keys. They do nothing unless you bind them: |
| 108 | - | |---|---|---| | |||
| 109 | - | | power | `KEY_POWER` | `XF86PowerOff` | | |||
| 110 | - | | suspend | `KEY_SLEEP` | `XF86Sleep` | | |||
| 111 | - | | hibernate | `KEY_SUSPEND` | `XF86Suspend` | | |||
| 112 | - | | reboot | `KEY_RESTART` | `code:408` (no keysym) | | |||
| 113 | 88 | ||||
| 114 | ```text | 89 | ```text | ||
| 115 | bind.XF86PowerOff = spawn systemctl suspend | 90 | bind.XF86PowerOff = spawn systemctl suspend | ||
| 116 | bind.XF86Suspend = spawn systemctl hibernate | 91 | bind.XF86Suspend = spawn systemctl hibernate | ||
| 117 | ``` | 92 | ``` | ||
| 118 | 93 | ||||
| 119 | - | The lid switch is not covered: logind still handles it. The locks last as long as NeferWL runs; if it crashes, logind handles the keys again. When logind restarts, NeferWL keeps the old locks until it has taken new ones. If the system bus or logind is missing, NeferWL logs a warning and tries again later, and the keys keep logind's behaviour until it succeeds. | 94 | + | | Key | Bind name | |
| 120 | - | 95 | + | |---|---| | |
| 121 | - | ## Bars | 96 | + | | Power | `XF86PowerOff` | |
| 122 | - | 97 | + | | Sleep | `XF86Sleep` | | |
| 123 | - | NeferWL supports `ext_workspace_manager_v1` for bars such as Waybar 0.13+ (`ext/workspaces`) and ironbar. Bars get workspace updates and can switch workspaces without polling. | 98 | + | | Suspend | `XF86Suspend` | |
| 99 | + | | Reboot | `code:408` | | |||
| 124 | 100 | ||||
| 125 | - | Each workspace has an `id`, the same as `workspace_id` in the [state file](#state-for-scripts): | 101 | + | The lid switch is still handled by logind. |
| 126 | 102 | ||||
| 127 | - | | Workspace | `id` | Stable | | 103 | + | ## Bars |
| 128 | - | |---|---|---| | |||
| 129 | - | | Named | `name:<configured name>` | Across launches | | |||
| 130 | - | | Numbered | `<prefix>-<n>`, with a random prefix per launch | For the current launch only | | |||
| 131 | 104 | ||||
| 132 | - | Only store settings keyed on named workspace IDs. | 105 | + | Waybar 0.13+ (`ext/workspaces`) and ironbar show and switch workspaces. Taskbars can list, focus and close windows. |
| 133 | 106 | ||||
| 134 | - | Taskbars can list, focus, close and fullscreen windows through `zwlr_foreign_toplevel_manager_v1`, and notification daemons such as Dunst can detect fullscreen windows. `ext_foreign_toplevel_list_v1` lists the same windows with an `identifier` (`neferwl-<window>-<mapping>`) that is never reused in a session (a window that maps again gets a new one); portals use it to share single windows. While the session is locked, bars and taskbars get no updates. | 107 | + | Workspace IDs are `name:<name>` for named workspaces, stable across launches. Numbered workspaces get a random prefix per launch, so do not save settings keyed on them. |
| 135 | 108 | ||||
| 136 | ## State for scripts | 109 | ## State for scripts | ||
| 137 | 110 | ||||
| 138 | - | 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: | 111 | + | NeferWL writes its state as JSON to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json`, also given to started programs as `$NEFERWL_STATE`. It lists outputs, workspaces, the focused window, and every window with its app ID, PID, workspace, column and row. |
| 139 | - | ||||
| 140 | - | - every output, with its active numbered workspace, workspace count, and the name (`workspace`) and ID string (`workspace_id`) of the workspace on screen; | |||
| 141 | - | - the focused output and window; | |||
| 142 | - | - every window, with its app ID, PID, output, workspace, `workspace_id` (the ID string of its workspace, numbered or hidden) and whether it is on screen (`visible`); | |||
| 143 | - | - `floating: true` for floating windows. Stashed windows also have their 1-based place in their workspace's stash (`stash_index` of `stash_count`, both `0` outside it) and `hidden: true` while the stash is hidden. | |||
| 144 | - | - for tiled windows, their 1-based `column` (layout order, left to right with `overflow = "scroll"`; with `overflow = "fixed"`, the spiral breaks that, so a later column can sit below or beside an earlier one) and `row` (top to bottom in the column); both are `0` when `floating` is true. Sort by `column`, `row` for layout order. | |||
| 145 | - | ||||
| 146 | - | ```sh | |||
| 147 | - | neferwl state # the whole state as JSON | |||
| 148 | - | neferwl state output-of "$PID" # the output of that process's window, or its nearest parent's | |||
| 149 | - | ``` | |||
| 150 | - | ||||
| 151 | - | `output-of` finds application windows only, because a bar has no single output. | |||
| 152 | 112 | ||||
| 153 | ```sh | 113 | ```sh | ||
| 154 | - | # Windows waiting in hidden stashes | 114 | + | neferwl state # the whole state |
| 155 | - | neferwl state | jq '[.windows[] | select(.hidden)] | length' | 115 | + | neferwl state output-of "$PID" # the output showing that process's window |
| 116 | + | neferwl state | jq '[.windows[] | select(.hidden)] | length' # windows in hidden stashes | |||
| 156 | ``` | 117 | ``` | ||
M docs/headless.md +13 -18
| @@ -1,23 +1,20 @@ | |||
| 1 | 1 | # Headless mode | |
| 2 | 2 | ||
| 3 | - | The headless backend runs without a screen, for tests and quick checks: | |
| 3 | + | Runs NeferWL without a screen, for tests and quick checks: | |
| 4 | 4 | ||
| 5 | 5 | ```sh | |
| 6 | 6 | neferwl --backend=headless --screenshot /tmp/neferwl-shots | |
| 7 | 7 | WAYLAND_DISPLAY=<name from the log> foot | |
| 8 | 8 | ``` | |
| 9 | 9 | ||
| 10 | - | - `/tmp/neferwl-shots/latest.png` shows the current frame, cursor included. A frame that cannot be read back is skipped, never written black. | |
| 11 | - | - Every frame is also saved as `frame-NNNNNN.png`, so the directory keeps growing. With several outputs, each writes to `<dir>/HEADLESS-<n>/`. | |
| 10 | + | - `latest.png` shows the current frame. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. | |
| 11 | + | - With several outputs, each writes to `<dir>/HEADLESS-<n>/`. | |
| 12 | 12 | - `--timeout 5s` stops NeferWL after 5 seconds. | |
| 13 | - | - While the session is locked, the PNG stops updating and keeps the last unlocked frame. See [Session locking](session-lock.md). | |
| 14 | - | - The cursor is not drawn on screen. After a layout change, pointer focus updates on the next move. | |
| 15 | - | ||
| 16 | - | `grim` and recorders also work here; see [Screen capture](capture.md). | |
| 13 | + | - `grim` and recorders work too; see [Screen capture](capture.md). | |
| 17 | 14 | ||
| 18 | 15 | ## Input scripts | |
| 19 | 16 | ||
| 20 | - | `--input <path>`, or `--input -` for stdin, plays one command per line: | |
| 17 | + | `--input <file>`, or `--input -` for stdin, plays one command per line: | |
| 21 | 18 | ||
| 22 | 19 | | Command | Effect | | |
| 23 | 20 | |---|---| | |
| @@ -26,27 +23,25 @@ WAYLAND_DISPLAY=<name from the log> foot | |||
| 26 | 23 | | `sleep 1s` | Wait | | |
| 27 | 24 | | `move <x> <y>` | Move the pointer, in output coordinates | | |
| 28 | 25 | | `click [left\|right\|middle]`, `down <button>`, `up <button>` | Mouse buttons | | |
| 29 | - | | `swipe up\|down\|left\|right` | Quick three-finger touchpad flick | | |
| 26 | + | | `swipe up\|down\|left\|right` | Three-finger flick | | |
| 30 | 27 | | `swipe4 up\|down` | Four-finger flick | | |
| 31 | 28 | ||
| 32 | 29 | ## Testing input methods | |
| 33 | 30 | ||
| 34 | - | `examples/testime` is a minimal input method. Each time an app enables text input, it sends a preedit string, then commits the final text: | |
| 31 | + | `examples/testime` sends a preedit string, then commits text, each time an app enables text input: | |
| 35 | 32 | ||
| 36 | 33 | ```sh | |
| 37 | - | neferwl --backend=headless | |
| 38 | - | WAYLAND_DISPLAY=<name from the log> go run ./examples/testime -preedit nihon -commit 日本 | |
| 34 | + | WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本 | |
| 39 | 35 | ``` | |
| 40 | 36 | ||
| 41 | 37 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | |
| 42 | 38 | ||
| 43 | 39 | ## Colour check | |
| 44 | 40 | ||
| 45 | - | `make color-check` checks that known colours come out right. It needs a GPU and `/dev/udmabuf`, and is not part of `make check` or CI (build tag `colorcheck`, test in `internal/app/colorcheck_test.go`). It runs a headless 640×360 output with `examples/testpattern -fullscreen` and compares 5×5 boxes at the centre of each patch: | |
| 46 | - | ||
| 47 | - | - `TestColorCheckSDR`: an sRGB `wl_shm` client on an SDR output. `latest.png` must match the sRGB values within ±1 per channel. | |
| 48 | - | - `TestColorCheckHDR`: a virtual HDR output (`--headless-hdr`), read back as raw PQ codes in `latest-pq.png`. An sRGB client must give `PQ(BT.709→BT.2020 · linear · 203 nits)`; a PQ client (`-mode hdr`) its own codes. Tolerance is ±0.012 PQ, or 0.03 nit for channels below 0.05 nit, where PQ is very steep. The test fails if the output falls back to SDR. | |
| 41 | + | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. | |
| 49 | 42 | ||
| 50 | - | `--screenshot-raw`, with `--screenshot` and `--headless-hdr`, also writes `latest-pq.png`: a 16-bit PNG of the 10-bit PQ codes, without cursor or tone mapping. It is a test-only flag. | |
| 43 | + | On a real screen, compare by eye: | |
| 51 | 44 | ||
| 52 | - | The check cannot see hardware scanout or the KMS colour pipeline. On a real HDR screen, run `WAYLAND_DISPLAY=<name> go run ./examples/testpattern -mode hdr -fullscreen` (or `-mode sdr`) and compare by eye. The client asks for fullscreen 1.2 s after its first commit, because earlier requests are ignored. | |
| 45 | + | ```sh | |
| 46 | + | WAYLAND_DISPLAY=<name> go run ./examples/testpattern -mode hdr -fullscreen # or -mode sdr | |
| 47 | + | ``` | |
| @@ -1,23 +1,20 @@ | |||||
| 1 | # Headless mode | 1 | # Headless mode | ||
| 2 | 2 | ||||
| 3 | - | The headless backend runs without a screen, for tests and quick checks: | 3 | + | Runs NeferWL without a screen, for tests and quick checks: |
| 4 | 4 | ||||
| 5 | ```sh | 5 | ```sh | ||
| 6 | neferwl --backend=headless --screenshot /tmp/neferwl-shots | 6 | neferwl --backend=headless --screenshot /tmp/neferwl-shots | ||
| 7 | WAYLAND_DISPLAY=<name from the log> foot | 7 | WAYLAND_DISPLAY=<name from the log> foot | ||
| 8 | ``` | 8 | ``` | ||
| 9 | 9 | ||||
| 10 | - | - `/tmp/neferwl-shots/latest.png` shows the current frame, cursor included. A frame that cannot be read back is skipped, never written black. | 10 | + | - `latest.png` shows the current frame. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. |
| 11 | - | - Every frame is also saved as `frame-NNNNNN.png`, so the directory keeps growing. With several outputs, each writes to `<dir>/HEADLESS-<n>/`. | 11 | + | - With several outputs, each writes to `<dir>/HEADLESS-<n>/`. |
| 12 | - `--timeout 5s` stops NeferWL after 5 seconds. | 12 | - `--timeout 5s` stops NeferWL after 5 seconds. | ||
| 13 | - | - While the session is locked, the PNG stops updating and keeps the last unlocked frame. See [Session locking](session-lock.md). | 13 | + | - `grim` and recorders work too; see [Screen capture](capture.md). |
| 14 | - | - The cursor is not drawn on screen. After a layout change, pointer focus updates on the next move. | |||
| 15 | - | ||||
| 16 | - | `grim` and recorders also work here; see [Screen capture](capture.md). | |||
| 17 | 14 | ||||
| 18 | ## Input scripts | 15 | ## Input scripts | ||
| 19 | 16 | ||||
| 20 | - | `--input <path>`, or `--input -` for stdin, plays one command per line: | 17 | + | `--input <file>`, or `--input -` for stdin, plays one command per line: |
| 21 | 18 | ||||
| 22 | | Command | Effect | | 19 | | Command | Effect | | ||
| 23 | |---|---| | 20 | |---|---| | ||
| @@ -26,27 +23,25 @@ WAYLAND_DISPLAY=<name from the log> foot | |||||
| 26 | | `sleep 1s` | Wait | | 23 | | `sleep 1s` | Wait | | ||
| 27 | | `move <x> <y>` | Move the pointer, in output coordinates | | 24 | | `move <x> <y>` | Move the pointer, in output coordinates | | ||
| 28 | | `click [left\|right\|middle]`, `down <button>`, `up <button>` | Mouse buttons | | 25 | | `click [left\|right\|middle]`, `down <button>`, `up <button>` | Mouse buttons | | ||
| 29 | - | | `swipe up\|down\|left\|right` | Quick three-finger touchpad flick | | 26 | + | | `swipe up\|down\|left\|right` | Three-finger flick | |
| 30 | | `swipe4 up\|down` | Four-finger flick | | 27 | | `swipe4 up\|down` | Four-finger flick | | ||
| 31 | 28 | ||||
| 32 | ## Testing input methods | 29 | ## Testing input methods | ||
| 33 | 30 | ||||
| 34 | - | `examples/testime` is a minimal input method. Each time an app enables text input, it sends a preedit string, then commits the final text: | 31 | + | `examples/testime` sends a preedit string, then commits text, each time an app enables text input: |
| 35 | 32 | ||||
| 36 | ```sh | 33 | ```sh | ||
| 37 | - | neferwl --backend=headless | 34 | + | WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本 |
| 38 | - | WAYLAND_DISPLAY=<name from the log> go run ./examples/testime -preedit nihon -commit 日本 | |||
| 39 | ``` | 35 | ``` | ||
| 40 | 36 | ||||
| 41 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | 37 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | ||
| 42 | 38 | ||||
| 43 | ## Colour check | 39 | ## Colour check | ||
| 44 | 40 | ||||
| 45 | - | `make color-check` checks that known colours come out right. It needs a GPU and `/dev/udmabuf`, and is not part of `make check` or CI (build tag `colorcheck`, test in `internal/app/colorcheck_test.go`). It runs a headless 640×360 output with `examples/testpattern -fullscreen` and compares 5×5 boxes at the centre of each patch: | 41 | + | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. |
| 46 | - | ||||
| 47 | - | - `TestColorCheckSDR`: an sRGB `wl_shm` client on an SDR output. `latest.png` must match the sRGB values within ±1 per channel. | |||
| 48 | - | - `TestColorCheckHDR`: a virtual HDR output (`--headless-hdr`), read back as raw PQ codes in `latest-pq.png`. An sRGB client must give `PQ(BT.709→BT.2020 · linear · 203 nits)`; a PQ client (`-mode hdr`) its own codes. Tolerance is ±0.012 PQ, or 0.03 nit for channels below 0.05 nit, where PQ is very steep. The test fails if the output falls back to SDR. | |||
| 49 | 42 | ||||
| 50 | - | `--screenshot-raw`, with `--screenshot` and `--headless-hdr`, also writes `latest-pq.png`: a 16-bit PNG of the 10-bit PQ codes, without cursor or tone mapping. It is a test-only flag. | 43 | + | On a real screen, compare by eye: |
| 51 | 44 | ||||
| 52 | - | The check cannot see hardware scanout or the KMS colour pipeline. On a real HDR screen, run `WAYLAND_DISPLAY=<name> go run ./examples/testpattern -mode hdr -fullscreen` (or `-mode sdr`) and compare by eye. The client asks for fullscreen 1.2 s after its first commit, because earlier requests are ignored. | 45 | + | ```sh |
| 46 | + | WAYLAND_DISPLAY=<name> go run ./examples/testpattern -mode hdr -fullscreen # or -mode sdr | |||
| 47 | + | ``` | |||
M docs/performance.md +23 -60
| @@ -1,48 +1,36 @@ | |||
| 1 | 1 | # Performance | |
| 2 | 2 | ||
| 3 | - | NeferWL uses about 40–80 MB of RAM with two 4K monitors and almost no CPU while the screen does not change. | |
| 3 | + | NeferWL uses about 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | |
| 4 | 4 | ||
| 5 | - | ## How it stays light | |
| 6 | - | ||
| 7 | - | - **No reflection across the C boundary.** libwayland and Vulkan calls go through fixed-arity `purego` syscalls and callbacks, with zero allocations per call. | |
| 8 | - | - **Few allocations per frame.** Frame callbacks, flips, Vulkan submissions and surface updates reuse buffers; scene publication and `Render` stay within small fixed allocation counts. `make perf-check` fails if a hot path allocates more than its budget. | |
| 9 | - | - **Paced frame callbacks.** Clients are told to draw on each output's flip or refresh, so a 165 Hz output paces them at 165 Hz. Cursors and surfaces without an output get 60 Hz. The pacer sleeps while no callback waits. | |
| 10 | - | - **Hidden windows rest.** A window nobody sees (another workspace, a hidden stash, scrolled off screen, under a fullscreen window) gets frame callbacks once per second, and the `suspended` state if its client supports xdg-shell v6. A captured window keeps its output's rate. | |
| 11 | - | - **Redraw only what changed.** An unchanged output is not recomposed. A content change redraws only its damage, and a moving window only its old and new area. A change to layers, scale, transform, dim or capture redraws the whole output. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M. | |
| 12 | - | - **Direct scanout.** A fullscreen buffer goes straight to the display, with tearing for games, when `render.direct-scanout` and `render.tearing` allow it (see [Configuration](config.md#keys)). | |
| 13 | - | - **Cheap animations** (experimental, off by default: `animations = on`). Springs reuse the composition pass: no extra GPU pass, blur or texture copy. Clients get one configure, at the final size. A window that is fading or zooming is composed, not scanned out directly. | |
| 14 | - | - **Compressed render targets.** Output images use the best modifier the display plane accepts (AMD DCC, then tiled, then linear). The log line `zero-copy output` shows the choice. | |
| 15 | - | - **Bounded capture.** At most 32 capture requests are in flight. A new request is never queued: wlr-screencopy fails it at once, and ext-image-copy retries for up to 1 s, then fails. Display capture uses up to 128 MiB of host memory per output (two 5K frames; larger outputs cannot be captured). Capturing a hidden workspace adds a separate renderer with its own 128 MiB, freed when idle. The capture indicator also costs composition; see [Screen capture](capture.md#the-indicator). | |
| 16 | - | - **GC settings.** The Go heap is a few MB because client buffers live in shared and GPU memory. NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit; `GOGC` and `GOMEMLIMIT` from the environment override them. | |
| 17 | - | ||
| 18 | - | A full composed frame at 2560×1600 costs about 0.16 ms on an RX 9070 XT, 1.4 ms on a Raphael iGPU and 3 ms on a Radeon 680M, against 11.1 ms per frame at 90 Hz. To measure a GPU yourself: `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU). | |
| 5 | + | - Only changed areas are redrawn. Windows nobody sees get one frame callback per second. | |
| 6 | + | - Fullscreen windows go straight to the display (direct scanout), with tearing for games. See `render.direct-scanout` and `render.tearing` in [Configuration](config.md#keys). | |
| 7 | + | - A full frame at 2560×1600 takes about 0.16 ms on an RX 9070 XT and 3 ms on a Radeon 680M. Measure yours with `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU). | |
| 8 | + | - NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit. `GOGC` and `GOMEMLIMIT` from the environment override them. | |
| 19 | 9 | ||
| 20 | 10 | ## VRR | |
| 21 | 11 | ||
| 22 | - | With a fullscreen game under VRR: | |
| 23 | - | ||
| 24 | - | - The cursor moves with the game's next frame instead of forcing its own refresh, which would drop the panel to its slowest rate. If the game stops drawing, the cursor updates alone at 24 Hz. | |
| 25 | - | - A game frame commits at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can hold the panel at its slowest refresh (48 Hz on the measured panel) while the game renders at 150 fps. Measurements are in `internal/adapters/drm/vrr_flip_gap.go`. | |
| 12 | + | With a fullscreen game under VRR, each game frame is committed at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can hold the panel at its slowest refresh rate. | |
| 26 | 13 | ||
| 27 | 14 | ## Real-time priority | |
| 28 | 15 | ||
| 29 | - | Output and input threads request real-time scheduling, which needs `CAP_SYS_NICE`. The Arch and Debian packages set it on `/usr/bin/neferwl`. Apps started by NeferWL do not inherit it. The Vulkan queue also requests realtime GPU priority, falling back to high. | |
| 16 | + | Output and input threads ask for real-time scheduling, which needs `CAP_SYS_NICE`. The Arch and Debian packages set it. For a local build: | |
| 30 | 17 | ||
| 31 | - | - Turn off real-time threads: `performance.realtime = off`. The GPU queue priority is still requested. | |
| 32 | - | - Grant the capability to a local build: `sudo setcap cap_sys_nice+ep bin/neferwl`. | |
| 18 | + | ```sh | |
| 19 | + | sudo setcap cap_sys_nice+ep bin/neferwl | |
| 20 | + | ``` | |
| 33 | 21 | ||
| 34 | - | With the capability, the loader ignores `LD_PRELOAD` and `LD_LIBRARY_PATH`, and debuggers need root to attach. | |
| 22 | + | `performance.realtime = off` turns it off. With the capability set, the loader ignores `LD_PRELOAD` and `LD_LIBRARY_PATH`, and debuggers need root to attach. | |
| 35 | 23 | ||
| 36 | - | ## Measuring | |
| 24 | + | ## Profiling | |
| 37 | 25 | ||
| 38 | - | Profile a running compositor from another TTY or over SSH: | |
| 26 | + | Run these from another TTY or over SSH: | |
| 39 | 27 | ||
| 40 | 28 | ```sh | |
| 41 | 29 | # CPU, Go and C frames | |
| 42 | 30 | perf record -g -p (pgrep -f bin/neferwl) -- sleep 8 | |
| 43 | 31 | perf report --children --percent-limit 1 | |
| 44 | 32 | ||
| 45 | - | # Allocations per Go line | |
| 33 | + | # Allocations | |
| 46 | 34 | neferwl --backend=drm --pprof=localhost:6060 | |
| 47 | 35 | go tool pprof -sample_index=alloc_objects -top http://localhost:6060/debug/pprof/heap | |
| 48 | 36 | ||
| @@ -50,45 +38,20 @@ go tool pprof -sample_index=alloc_objects -top http://localhost:6060/debug/pprof | |||
| 50 | 38 | GODEBUG=gctrace=1 neferwl --backend=drm | |
| 51 | 39 | ``` | |
| 52 | 40 | ||
| 53 | - | `make tty` serves pprof on `localhost:6060` (`make tty PPROF=` turns it off). It also serves `/debug/pprof/profile` and `/debug/pprof/trace`. | |
| 41 | + | `make tty` serves pprof on `localhost:6060` by default (`make tty PPROF=` turns it off). | |
| 54 | 42 | ||
| 55 | - | Logs are in `$XDG_STATE_HOME/neferwl/runs/<backend>/` (default `~/.local/state`), with `latest.log` pointing at the newest run. Each backend keeps 20 runs. `--debug=all` leaves out the noisy categories `drm-flip`, `input-motion` and `input-keys`; name them to turn them on. | |
| 43 | + | Logs are in `~/.local/state/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. `--debug=all` enables every category except the noisy `drm-flip`, `input-motion` and `input-keys`; name them to turn them on. | |
| 56 | 44 | ||
| 57 | - | ### Frame stats | |
| 45 | + | ## Frame stats | |
| 58 | 46 | ||
| 59 | - | Every 10 s, each output logs a `stats` entry for the last interval. The main fields: | |
| 47 | + | Every 10 s, each output logs a `stats` entry: | |
| 60 | 48 | ||
| 61 | 49 | | Field | Meaning | | |
| 62 | 50 | |---|---| | |
| 63 | - | | `missed_vblanks` | Due frames that flipped more than 1.5 refresh periods after the previous one. Not counted under VRR | | |
| 64 | - | | `max_flip_interval_ms` | Longest gap between two flips of due frames, VRR included. Restarts after a modeset | | |
| 51 | + | | `missed_vblanks` | Frames that flipped more than 1.5 refresh periods late (not counted under VRR) | | |
| 52 | + | | `max_flip_interval_ms` | Longest gap between two flips | | |
| 65 | 53 | | `max_commit_delay_ms` | Longest wait from "frame due" to commit | | |
| 66 | - | | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event | | |
| 67 | - | | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` | | |
| 68 | - | | `redrawn_pixels` | Pixels the renderer drew | | |
| 69 | - | ||
| 70 | - | It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`. | |
| 71 | - | ||
| 72 | - | To find the cause of missed vblanks: | |
| 73 | - | ||
| 74 | - | - With `late_fences`: the GPU composition was late. | |
| 75 | - | - Without `late_fences` but with a large `max_commit_delay_ms`: the commit was late (CPU work or a late event read). | |
| 76 | - | ||
| 77 | - | Few `missed_vblanks` mean flips were on time only if `max_commit_delay_ms` is also small. | |
| 78 | - | ||
| 79 | - | `--debug=drm-flip` (or `make tty TTY_DEBUG=drm-flip`) logs every flip as a `flip` entry with `commit_to_flip_ms`, `flip_interval_ms`, `fence_ready_at_commit`, and, when the client fence reports it, `commit_to_fence_ms` and `fence_to_flip_ms`. A negative `commit_to_fence_ms` means the client finished before the commit. `input stats` shows coalesced pointer motion and the longest wait for core. | |
| 80 | - | ||
| 81 | - | ## Reference: 4K HDR video with tiled subsurfaces | |
| 82 | - | ||
| 83 | - | A browser playing 4K HDR video with 65 subsurfaces committing every frame, measured with `perf record -g` for 8 s on the DRM backend: | |
| 84 | - | ||
| 85 | - | | Metric | Before (issue #19) | 2026-09-27 | | |
| 86 | - | |---|---|---| | |
| 87 | - | | CPU | 20–30 % | 13–14 % | | |
| 88 | - | | GC (`gcBgMarkWorker`) | 20 % | 8.7 % | | |
| 89 | - | | Go → C reflection | 46.7 % | 0 | | |
| 90 | - | | C → Go reflection | ~30 % | 0 | | |
| 91 | - | | `Render` allocations per frame | 1321 | 10 | | |
| 92 | - | | GC cycles on the playback test | 13 (91 ms) | 2 (3 ms) | | |
| 54 | + | | `late_fences` | Missed vblanks where the GPU finished too late (needs `--debug=drm-flip`) | | |
| 55 | + | | `redrawn_pixels` | Pixels drawn | | |
| 93 | 56 | ||
| 94 | - | The remaining CPU time is real work: surface commits and fifo logic (~23 %), C calls (~5 %) and the Go scheduler (~8 %). | |
| 57 | + | Missed vblanks with `late_fences` mean the GPU was late. Without `late_fences` but with a large `max_commit_delay_ms`, the CPU was late. | |
| @@ -1,48 +1,36 @@ | |||||
| 1 | # Performance | 1 | # Performance | ||
| 2 | 2 | ||||
| 3 | - | NeferWL uses about 40–80 MB of RAM with two 4K monitors and almost no CPU while the screen does not change. | 3 | + | NeferWL uses about 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. |
| 4 | 4 | ||||
| 5 | - | ## How it stays light | 5 | + | - Only changed areas are redrawn. Windows nobody sees get one frame callback per second. |
| 6 | - | 6 | + | - Fullscreen windows go straight to the display (direct scanout), with tearing for games. See `render.direct-scanout` and `render.tearing` in [Configuration](config.md#keys). | |
| 7 | - | - **No reflection across the C boundary.** libwayland and Vulkan calls go through fixed-arity `purego` syscalls and callbacks, with zero allocations per call. | 7 | + | - A full frame at 2560×1600 takes about 0.16 ms on an RX 9070 XT and 3 ms on a Radeon 680M. Measure yours with `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU). |
| 8 | - | - **Few allocations per frame.** Frame callbacks, flips, Vulkan submissions and surface updates reuse buffers; scene publication and `Render` stay within small fixed allocation counts. `make perf-check` fails if a hot path allocates more than its budget. | 8 | + | - NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit. `GOGC` and `GOMEMLIMIT` from the environment override them. |
| 9 | - | - **Paced frame callbacks.** Clients are told to draw on each output's flip or refresh, so a 165 Hz output paces them at 165 Hz. Cursors and surfaces without an output get 60 Hz. The pacer sleeps while no callback waits. | |||
| 10 | - | - **Hidden windows rest.** A window nobody sees (another workspace, a hidden stash, scrolled off screen, under a fullscreen window) gets frame callbacks once per second, and the `suspended` state if its client supports xdg-shell v6. A captured window keeps its output's rate. | |||
| 11 | - | - **Redraw only what changed.** An unchanged output is not recomposed. A content change redraws only its damage, and a moving window only its old and new area. A change to layers, scale, transform, dim or capture redraws the whole output. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M. | |||
| 12 | - | - **Direct scanout.** A fullscreen buffer goes straight to the display, with tearing for games, when `render.direct-scanout` and `render.tearing` allow it (see [Configuration](config.md#keys)). | |||
| 13 | - | - **Cheap animations** (experimental, off by default: `animations = on`). Springs reuse the composition pass: no extra GPU pass, blur or texture copy. Clients get one configure, at the final size. A window that is fading or zooming is composed, not scanned out directly. | |||
| 14 | - | - **Compressed render targets.** Output images use the best modifier the display plane accepts (AMD DCC, then tiled, then linear). The log line `zero-copy output` shows the choice. | |||
| 15 | - | - **Bounded capture.** At most 32 capture requests are in flight. A new request is never queued: wlr-screencopy fails it at once, and ext-image-copy retries for up to 1 s, then fails. Display capture uses up to 128 MiB of host memory per output (two 5K frames; larger outputs cannot be captured). Capturing a hidden workspace adds a separate renderer with its own 128 MiB, freed when idle. The capture indicator also costs composition; see [Screen capture](capture.md#the-indicator). | |||
| 16 | - | - **GC settings.** The Go heap is a few MB because client buffers live in shared and GPU memory. NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit; `GOGC` and `GOMEMLIMIT` from the environment override them. | |||
| 17 | - | ||||
| 18 | - | A full composed frame at 2560×1600 costs about 0.16 ms on an RX 9070 XT, 1.4 ms on a Raphael iGPU and 3 ms on a Radeon 680M, against 11.1 ms per frame at 90 Hz. To measure a GPU yourself: `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU). | |||
| 19 | 9 | ||||
| 20 | ## VRR | 10 | ## VRR | ||
| 21 | 11 | ||||
| 22 | - | With a fullscreen game under VRR: | 12 | + | With a fullscreen game under VRR, each game frame is committed at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can hold the panel at its slowest refresh rate. |
| 23 | - | ||||
| 24 | - | - The cursor moves with the game's next frame instead of forcing its own refresh, which would drop the panel to its slowest rate. If the game stops drawing, the cursor updates alone at 24 Hz. | |||
| 25 | - | - A game frame commits at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can hold the panel at its slowest refresh (48 Hz on the measured panel) while the game renders at 150 fps. Measurements are in `internal/adapters/drm/vrr_flip_gap.go`. | |||
| 26 | 13 | ||||
| 27 | ## Real-time priority | 14 | ## Real-time priority | ||
| 28 | 15 | ||||
| 29 | - | Output and input threads request real-time scheduling, which needs `CAP_SYS_NICE`. The Arch and Debian packages set it on `/usr/bin/neferwl`. Apps started by NeferWL do not inherit it. The Vulkan queue also requests realtime GPU priority, falling back to high. | 16 | + | Output and input threads ask for real-time scheduling, which needs `CAP_SYS_NICE`. The Arch and Debian packages set it. For a local build: |
| 30 | 17 | ||||
| 31 | - | - Turn off real-time threads: `performance.realtime = off`. The GPU queue priority is still requested. | 18 | + | ```sh |
| 32 | - | - Grant the capability to a local build: `sudo setcap cap_sys_nice+ep bin/neferwl`. | 19 | + | sudo setcap cap_sys_nice+ep bin/neferwl |
| 20 | + | ``` | |||
| 33 | 21 | ||||
| 34 | - | With the capability, the loader ignores `LD_PRELOAD` and `LD_LIBRARY_PATH`, and debuggers need root to attach. | 22 | + | `performance.realtime = off` turns it off. With the capability set, the loader ignores `LD_PRELOAD` and `LD_LIBRARY_PATH`, and debuggers need root to attach. |
| 35 | 23 | ||||
| 36 | - | ## Measuring | 24 | + | ## Profiling |
| 37 | 25 | ||||
| 38 | - | Profile a running compositor from another TTY or over SSH: | 26 | + | Run these from another TTY or over SSH: |
| 39 | 27 | ||||
| 40 | ```sh | 28 | ```sh | ||
| 41 | # CPU, Go and C frames | 29 | # CPU, Go and C frames | ||
| 42 | perf record -g -p (pgrep -f bin/neferwl) -- sleep 8 | 30 | perf record -g -p (pgrep -f bin/neferwl) -- sleep 8 | ||
| 43 | perf report --children --percent-limit 1 | 31 | perf report --children --percent-limit 1 | ||
| 44 | 32 | ||||
| 45 | - | # Allocations per Go line | 33 | + | # Allocations |
| 46 | neferwl --backend=drm --pprof=localhost:6060 | 34 | neferwl --backend=drm --pprof=localhost:6060 | ||
| 47 | go tool pprof -sample_index=alloc_objects -top http://localhost:6060/debug/pprof/heap | 35 | go tool pprof -sample_index=alloc_objects -top http://localhost:6060/debug/pprof/heap | ||
| 48 | 36 | ||||
| @@ -50,45 +38,20 @@ go tool pprof -sample_index=alloc_objects -top http://localhost:6060/debug/pprof | |||||
| 50 | GODEBUG=gctrace=1 neferwl --backend=drm | 38 | GODEBUG=gctrace=1 neferwl --backend=drm | ||
| 51 | ``` | 39 | ``` | ||
| 52 | 40 | ||||
| 53 | - | `make tty` serves pprof on `localhost:6060` (`make tty PPROF=` turns it off). It also serves `/debug/pprof/profile` and `/debug/pprof/trace`. | 41 | + | `make tty` serves pprof on `localhost:6060` by default (`make tty PPROF=` turns it off). |
| 54 | 42 | ||||
| 55 | - | Logs are in `$XDG_STATE_HOME/neferwl/runs/<backend>/` (default `~/.local/state`), with `latest.log` pointing at the newest run. Each backend keeps 20 runs. `--debug=all` leaves out the noisy categories `drm-flip`, `input-motion` and `input-keys`; name them to turn them on. | 43 | + | Logs are in `~/.local/state/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. `--debug=all` enables every category except the noisy `drm-flip`, `input-motion` and `input-keys`; name them to turn them on. |
| 56 | 44 | ||||
| 57 | - | ### Frame stats | 45 | + | ## Frame stats |
| 58 | 46 | ||||
| 59 | - | Every 10 s, each output logs a `stats` entry for the last interval. The main fields: | 47 | + | Every 10 s, each output logs a `stats` entry: |
| 60 | 48 | ||||
| 61 | | Field | Meaning | | 49 | | Field | Meaning | | ||
| 62 | |---|---| | 50 | |---|---| | ||
| 63 | - | | `missed_vblanks` | Due frames that flipped more than 1.5 refresh periods after the previous one. Not counted under VRR | | 51 | + | | `missed_vblanks` | Frames that flipped more than 1.5 refresh periods late (not counted under VRR) | |
| 64 | - | | `max_flip_interval_ms` | Longest gap between two flips of due frames, VRR included. Restarts after a modeset | | 52 | + | | `max_flip_interval_ms` | Longest gap between two flips | |
| 65 | | `max_commit_delay_ms` | Longest wait from "frame due" to commit | | 53 | | `max_commit_delay_ms` | Longest wait from "frame due" to commit | | ||
| 66 | - | | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event | | 54 | + | | `late_fences` | Missed vblanks where the GPU finished too late (needs `--debug=drm-flip`) | |
| 67 | - | | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` | | 55 | + | | `redrawn_pixels` | Pixels drawn | |
| 68 | - | | `redrawn_pixels` | Pixels the renderer drew | | |||
| 69 | - | ||||
| 70 | - | It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`. | |||
| 71 | - | ||||
| 72 | - | To find the cause of missed vblanks: | |||
| 73 | - | ||||
| 74 | - | - With `late_fences`: the GPU composition was late. | |||
| 75 | - | - Without `late_fences` but with a large `max_commit_delay_ms`: the commit was late (CPU work or a late event read). | |||
| 76 | - | ||||
| 77 | - | Few `missed_vblanks` mean flips were on time only if `max_commit_delay_ms` is also small. | |||
| 78 | - | ||||
| 79 | - | `--debug=drm-flip` (or `make tty TTY_DEBUG=drm-flip`) logs every flip as a `flip` entry with `commit_to_flip_ms`, `flip_interval_ms`, `fence_ready_at_commit`, and, when the client fence reports it, `commit_to_fence_ms` and `fence_to_flip_ms`. A negative `commit_to_fence_ms` means the client finished before the commit. `input stats` shows coalesced pointer motion and the longest wait for core. | |||
| 80 | - | ||||
| 81 | - | ## Reference: 4K HDR video with tiled subsurfaces | |||
| 82 | - | ||||
| 83 | - | A browser playing 4K HDR video with 65 subsurfaces committing every frame, measured with `perf record -g` for 8 s on the DRM backend: | |||
| 84 | - | ||||
| 85 | - | | Metric | Before (issue #19) | 2026-09-27 | | |||
| 86 | - | |---|---|---| | |||
| 87 | - | | CPU | 20–30 % | 13–14 % | | |||
| 88 | - | | GC (`gcBgMarkWorker`) | 20 % | 8.7 % | | |||
| 89 | - | | Go → C reflection | 46.7 % | 0 | | |||
| 90 | - | | C → Go reflection | ~30 % | 0 | | |||
| 91 | - | | `Render` allocations per frame | 1321 | 10 | | |||
| 92 | - | | GC cycles on the playback test | 13 (91 ms) | 2 (3 ms) | | |||
| 93 | 56 | ||||
| 94 | - | The remaining CPU time is real work: surface commits and fifo logic (~23 %), C calls (~5 %) and the Go scheduler (~8 %). | 57 | + | Missed vblanks with `late_fences` mean the GPU was late. Without `late_fences` but with a large `max_commit_delay_ms`, the CPU was late. |
M docs/session-lock.md +12 -20
| @@ -1,34 +1,26 @@ | |||
| 1 | 1 | # Session locking | |
| 2 | 2 | ||
| 3 | - | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL blocks input and hides the desktop. A fullscreen window or a layer-shell cover is not a lock. | |
| 3 | + | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. | |
| 4 | 4 | ||
| 5 | 5 | ## While locked | |
| 6 | 6 | ||
| 7 | - | - Only the locker gets input. Binds, activation, virtual keyboards, input-method grabs, clipboard requests and new captures are refused. | |
| 8 | - | - Outputs show only black and the locker's surfaces. Direct scanout, overlays, tearing and hardware cursors are off. | |
| 9 | - | - Every running capture session stops and the capture indicator clears. Clients start new sessions after unlock. | |
| 10 | - | - Keys held during lock or unlock do not reach the desktop afterwards. | |
| 11 | - | - `Ctrl+Alt+Backspace` and the `--debug=input-keys` log are disabled. VT switching still works. | |
| 12 | - | - A second locker is refused while the first is alive. Activity can wake outputs but never unlocks. | |
| 13 | - | - A locker that uses a screenshot as background (hyprlock `screenshot`, swaylock-effects `--screenshots`) must be in the [capture allowlist](capture.md#the-allowlist), and capture is refused once locked. | |
| 7 | + | - Only the locker gets input. Binds, clipboard requests and new captures are refused. | |
| 8 | + | - Outputs show black and the locker's surfaces only. | |
| 9 | + | - Running captures stop. | |
| 10 | + | - `Ctrl+Alt+Backspace` is disabled. VT switching still works. | |
| 11 | + | - A locker that uses a screenshot as background (hyprlock `screenshot`, swaylock-effects `--screenshots`) must be in the [capture allowlist](capture.md#the-allowlist). | |
| 14 | 12 | ||
| 15 | - | The locker gets `locked` only once every output really shows a protected frame (or is off), DRM leases are revoked and captures started before the lock have finished. If that cannot be confirmed, `locked` is withheld. | |
| 13 | + | The locker is told `locked` only once every output shows a protected frame or is off. | |
| 16 | 14 | ||
| 17 | 15 | ## If the locker dies | |
| 18 | 16 | ||
| 19 | - | The session stays locked and outputs stay black. Start a new locker to take over. There is no emergency unlock key and no unlock timeout. | |
| 17 | + | The session stays locked and black. Start a new locker to take over. There is no emergency unlock key and no timeout. | |
| 20 | 18 | ||
| 21 | - | Only the current locker, after `locked`, can unlock. A locker that exits right after unlocking must do a Wayland roundtrip first; otherwise its exit may count as a crash and keep the session locked. | |
| 19 | + | A locker that exits right after unlocking must do a Wayland roundtrip first, or its exit counts as a crash and the session stays locked. | |
| 22 | 20 | ||
| 23 | 21 | ## Limits | |
| 24 | 22 | ||
| 25 | - | - NeferWL does not authenticate anything: no PAM, no stored credentials. | |
| 26 | - | - A process running as your user can start its own locker and unlock. Root, the kernel and other VTs are outside the compositor's control. | |
| 23 | + | - NeferWL does not authenticate: no PAM, no stored credentials. | |
| 24 | + | - Any process running as your user can start its own locker and unlock. | |
| 27 | 25 | - Clipboard data and window contents a client received before the lock stay with that client. | |
| 28 | - | - The state file for scripts is not cleared while locked. | |
| 29 | - | - A stalled GPU or KMS call delays `locked`; lock latency is not guaranteed. | |
| 30 | - | - If KMS refuses a protected frame, the output stays registered and retries every 50 ms to 2 s. If the driver refuses to turn an output off, its old content can stay lit, and `locked` is withheld. | |
| 31 | - | ||
| 32 | - | ## Testing | |
| 33 | - | ||
| 34 | - | Headless tests cover two outputs, input isolation, capture refusal, locker death and takeover, and unlock. KMS behaviour is tested with mocks only; hotplug, DPMS, VT switching and suspend still need testing on real hardware. | |
| 26 | + | - The [state file](desktop.md#state-for-scripts) is not cleared while locked. | |
| @@ -1,34 +1,26 @@ | |||||
| 1 | # Session locking | 1 | # Session locking | ||
| 2 | 2 | ||||
| 3 | - | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL blocks input and hides the desktop. A fullscreen window or a layer-shell cover is not a lock. | 3 | + | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. |
| 4 | 4 | ||||
| 5 | ## While locked | 5 | ## While locked | ||
| 6 | 6 | ||||
| 7 | - | - Only the locker gets input. Binds, activation, virtual keyboards, input-method grabs, clipboard requests and new captures are refused. | 7 | + | - Only the locker gets input. Binds, clipboard requests and new captures are refused. |
| 8 | - | - Outputs show only black and the locker's surfaces. Direct scanout, overlays, tearing and hardware cursors are off. | 8 | + | - Outputs show black and the locker's surfaces only. |
| 9 | - | - Every running capture session stops and the capture indicator clears. Clients start new sessions after unlock. | 9 | + | - Running captures stop. |
| 10 | - | - Keys held during lock or unlock do not reach the desktop afterwards. | 10 | + | - `Ctrl+Alt+Backspace` is disabled. VT switching still works. |
| 11 | - | - `Ctrl+Alt+Backspace` and the `--debug=input-keys` log are disabled. VT switching still works. | 11 | + | - A locker that uses a screenshot as background (hyprlock `screenshot`, swaylock-effects `--screenshots`) must be in the [capture allowlist](capture.md#the-allowlist). |
| 12 | - | - A second locker is refused while the first is alive. Activity can wake outputs but never unlocks. | |||
| 13 | - | - A locker that uses a screenshot as background (hyprlock `screenshot`, swaylock-effects `--screenshots`) must be in the [capture allowlist](capture.md#the-allowlist), and capture is refused once locked. | |||
| 14 | 12 | ||||
| 15 | - | The locker gets `locked` only once every output really shows a protected frame (or is off), DRM leases are revoked and captures started before the lock have finished. If that cannot be confirmed, `locked` is withheld. | 13 | + | The locker is told `locked` only once every output shows a protected frame or is off. |
| 16 | 14 | ||||
| 17 | ## If the locker dies | 15 | ## If the locker dies | ||
| 18 | 16 | ||||
| 19 | - | The session stays locked and outputs stay black. Start a new locker to take over. There is no emergency unlock key and no unlock timeout. | 17 | + | The session stays locked and black. Start a new locker to take over. There is no emergency unlock key and no timeout. |
| 20 | 18 | ||||
| 21 | - | Only the current locker, after `locked`, can unlock. A locker that exits right after unlocking must do a Wayland roundtrip first; otherwise its exit may count as a crash and keep the session locked. | 19 | + | A locker that exits right after unlocking must do a Wayland roundtrip first, or its exit counts as a crash and the session stays locked. |
| 22 | 20 | ||||
| 23 | ## Limits | 21 | ## Limits | ||
| 24 | 22 | ||||
| 25 | - | - NeferWL does not authenticate anything: no PAM, no stored credentials. | 23 | + | - NeferWL does not authenticate: no PAM, no stored credentials. |
| 26 | - | - A process running as your user can start its own locker and unlock. Root, the kernel and other VTs are outside the compositor's control. | 24 | + | - Any process running as your user can start its own locker and unlock. |
| 27 | - Clipboard data and window contents a client received before the lock stay with that client. | 25 | - Clipboard data and window contents a client received before the lock stay with that client. | ||
| 28 | - | - The state file for scripts is not cleared while locked. | 26 | + | - The [state file](desktop.md#state-for-scripts) is not cleared while locked. |
| 29 | - | - A stalled GPU or KMS call delays `locked`; lock latency is not guaranteed. | |||
| 30 | - | - If KMS refuses a protected frame, the output stays registered and retries every 50 ms to 2 s. If the driver refuses to turn an output off, its old content can stay lit, and `locked` is withheld. | |||
| 31 | - | ||||
| 32 | - | ## Testing | |||
| 33 | - | ||||
| 34 | - | Headless tests cover two outputs, input isolation, capture refusal, locker death and takeover, and unlock. KMS behaviour is tested with mocks only; hotplug, DPMS, VT switching and suspend still need testing on real hardware. | |||