docs: phase 0+1 implementation plan; spec updated with prototype findings
This commit is contained in:
1 parent
da46fe6f0e
commit
3976ce6fd9
2 files changed
+4365
-42
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -91,18 +91,23 @@ src/
|
||||
error.rs # user-facing error type and rendering
|
||||
log.rs # tracing setup, per-run log files
|
||||
lock.rs # single-instance lock
|
||||
detach.rs # fork a background child (clipboard server)
|
||||
wayland.rs # SCTK state: registry, outputs, seat, shm
|
||||
frame.rs # Frame type, pixel format conversion, transforms
|
||||
capture.rs # Capturer trait + wlr-screencopy implementation
|
||||
niri.rs # niri IPC wrapper (focused output)
|
||||
region.rs # region selection overlay + state machine
|
||||
render.rs # tiny-skia helpers
|
||||
niri.rs # niri IPC wrapper (focused output, version)
|
||||
selection.rs # selection state machine, logical → pixel math
|
||||
region.rs # region selection overlay (layer-shell surfaces)
|
||||
render.rs # overlay pixels: dim, paste selection, outline
|
||||
output.rs # PNG saving, filename template, clipboard
|
||||
doctor.rs # `valw doctor`
|
||||
docs/
|
||||
```
|
||||
|
||||
Files stay flat for now; `overlay/` and `editor/` subdirectories appear once
|
||||
Phases 3 to 5 give them enough content.
|
||||
Phases 3 to 5 give them enough content. Pure logic (`frame`, `selection`,
|
||||
`render`, `output`, `config`, `error`) is kept apart from Wayland code so it
|
||||
can be unit tested without a compositor.
|
||||
|
||||
### 4.1 Nix
|
||||
|
||||
@@ -179,16 +184,23 @@ is incomplete.
|
||||
| Purpose | Crate |
|
||||
|---|---|
|
||||
| Wayland client | `smithay-client-toolkit`, `wayland-client`, `wayland-protocols`, `wayland-protocols-wlr` |
|
||||
| Drawing | `tiny-skia` |
|
||||
| PNG | `image` (png feature only) |
|
||||
| Clipboard | `wl-clipboard-rs` |
|
||||
| niri IPC | `niri-ipc` (version pinned to match niri 26.04) |
|
||||
| niri IPC | `niri-ipc` (`=26.4.0`, matches niri 26.04) |
|
||||
| CLI | `clap` (derive) |
|
||||
| Config | `serde`, `toml`, `serde_ignored` |
|
||||
| Time / filenames | `chrono` |
|
||||
| Errors | `anyhow` |
|
||||
| Logging | `tracing`, `tracing-subscriber`, `tracing-appender` |
|
||||
| Lock | `rustix` (flock) |
|
||||
| Logging | `tracing`, `tracing-subscriber` |
|
||||
| Lock, poll, stdio | `rustix` |
|
||||
| fork | `libc` |
|
||||
|
||||
Not needed in Phase 1: `tiny-skia` (the overlay is plain byte copies; shapes
|
||||
and text arrive in Phase 3) and `tracing-appender` (one file per run needs no
|
||||
rolling writer).
|
||||
|
||||
Dependencies are built with `opt-level = 3` in the dev profile; unoptimised
|
||||
PNG encoding is too slow to test with.
|
||||
|
||||
## 6. Capture pipeline
|
||||
|
||||
@@ -205,12 +217,20 @@ Every capture command runs these steps in order.
|
||||
screencopy and layer-shell. A missing required global is an error that
|
||||
names the protocol.
|
||||
5. **Capture.** `Capturer::capture(&[Output], cursor: bool) -> Vec<Frame>`
|
||||
issues one screencopy request per output in parallel and waits for every
|
||||
`ready`. A `Frame` holds RGBA8 pixels at physical resolution plus its output
|
||||
name, logical geometry and scale. `XRGB8888`, `ARGB8888`, `XBGR8888` and
|
||||
issues one screencopy request per output in parallel and waits up to 5 s
|
||||
for every `ready`. A `Frame` holds upright, opaque RGBA8 pixels at physical
|
||||
resolution plus its output name and logical geometry; the scale is
|
||||
`frame width / logical width`. `XRGB8888`, `ARGB8888`, `XBGR8888` and
|
||||
`ABGR8888` are converted; `y_invert` is honoured; any other format is an
|
||||
error that names the format. `cursor` comes from `--cursor` or
|
||||
error that names the format. Alpha is always 255, even where the
|
||||
compositor's buffer says 0. `cursor` comes from `--cursor` or
|
||||
`capture.show_cursor`.
|
||||
- **niri quirk (verified in its source):** niri rejects a shm buffer
|
||||
with `invalid buffer` unless the whole `wl_shm_pool` is exactly the
|
||||
buffer's size. Every capture therefore gets its own pool.
|
||||
- **Rotated outputs:** the buffer arrives in the output's native
|
||||
orientation. It is turned upright the way grim does it: y-invert,
|
||||
rotate clockwise by the transform's angle, then mirror if flipped.
|
||||
|
||||
### 6.1 `valw screen`
|
||||
|
||||
@@ -241,10 +261,11 @@ dies. No overlay can outlive the process.
|
||||
|
||||
### 7.2 HiDPI
|
||||
|
||||
- Each surface asks for its preferred scale via `wp_fractional_scale`. The
|
||||
buffer is allocated at physical size, and `wp_viewporter` sets the
|
||||
destination to the logical size. The frozen frame is shown 1:1 with no
|
||||
resampling.
|
||||
- Each surface's buffer is the frozen frame's size (physical pixels), and
|
||||
`wp_viewporter` sets the destination to the logical size from the layer
|
||||
configure. The frame is shown 1:1 with no resampling. The frame already
|
||||
says how many physical pixels the output has, so `wp_fractional_scale` is
|
||||
not needed.
|
||||
- Pointer events arrive in surface-local logical coordinates. They are
|
||||
converted to global logical coordinates by adding the output's logical
|
||||
position; the selection is stored in global logical coordinates.
|
||||
@@ -267,8 +288,9 @@ Idle --press--> Dragging{start} --release--> Done(rect)
|
||||
|
||||
### 7.4 Logical to physical conversion
|
||||
|
||||
For an output at logical position `(ox, oy)` with scale `s` and a selection
|
||||
`(x, y, w, h)` in global logical coordinates:
|
||||
For an output at logical position `(ox, oy)` with scale `s` (per axis:
|
||||
frame size / logical size) and a selection `(x, y, w, h)` in global logical
|
||||
coordinates:
|
||||
|
||||
```
|
||||
x0 = floor((x - ox) * s) y0 = floor((y - oy) * s)
|
||||
@@ -312,10 +334,22 @@ show_cursor = false
|
||||
|
||||
### 8.3 Clipboard
|
||||
|
||||
`wl-clipboard-rs` copies the PNG as `image/png` and forks into the background
|
||||
to serve paste requests. The forked child would inherit the lock file
|
||||
descriptor and keep the lock held, so **the lock is released explicitly before
|
||||
copying**. A test covers this.
|
||||
A Wayland clipboard needs a process that stays alive to answer paste
|
||||
requests. `wl-clipboard-rs` does **not** fork: its background mode serves from
|
||||
a thread, which dies when valw exits. So valw does what `wl-copy` does:
|
||||
|
||||
1. `Options::foreground(true).prepare_copy(...)` offers the PNG as
|
||||
`image/png` (no threads are started).
|
||||
2. `detach::spawn` releases the capture lock, then forks. The child calls
|
||||
`setsid`, points stdin/stdout/stderr at `/dev/null`, serves paste requests
|
||||
until something else is copied, and exits with `_exit`.
|
||||
3. The parent exits right away.
|
||||
|
||||
The lock is released before the fork because the child would otherwise
|
||||
inherit it and block the next capture. stdio goes to `/dev/null` because an
|
||||
inherited stdout pipe would keep `valw region -o - | consumer` waiting until
|
||||
the clipboard changes. A unit test covers both, and the headless test runs
|
||||
`screen -o -` through a pipe.
|
||||
|
||||
### 8.4 CLI
|
||||
|
||||
@@ -326,15 +360,16 @@ valw doctor
|
||||
|
||||
COMMON:
|
||||
--clipboard-only copy to clipboard, do not save a file
|
||||
--output <path> write to this path instead of directory + template;
|
||||
"-" writes PNG to stdout (not allowed with --all)
|
||||
--delay <secs> wait before capturing
|
||||
-o, --output <path> write to this path instead of directory + template;
|
||||
"-" writes PNG to stdout
|
||||
--delay <secs> wait before capturing (non-negative, fractions allowed)
|
||||
--cursor include the cursor in the capture
|
||||
```
|
||||
|
||||
Flag interactions:
|
||||
Flag interactions (enforced by clap, so they are usage errors, exit 2):
|
||||
|
||||
- `--clipboard-only` conflicts with `--output` and `--all`.
|
||||
- `--clipboard-only` conflicts with `--output`.
|
||||
- `--all` conflicts with `--output` and `--clipboard-only`.
|
||||
- Otherwise the clipboard follows `save.copy_to_clipboard`, whatever the
|
||||
output target (including `--output -`).
|
||||
- `--clipboard-only` copies even if `save.copy_to_clipboard = false`.
|
||||
@@ -372,13 +407,19 @@ error: could not capture output HDMI-A-1
|
||||
|
||||
- `tracing` writes one file per run to `$XDG_STATE_HOME/valw/logs/`; the 10
|
||||
most recent are kept.
|
||||
- Each log starts with an environment summary: valw version, niri version,
|
||||
bound globals and their versions, outputs (name, position, size, scale), and
|
||||
the buffer formats screencopy offered.
|
||||
- Each log starts with an environment summary: valw version and arguments,
|
||||
niri version, all globals and their versions (one line), outputs (name,
|
||||
position, size, transform). Each capture logs the buffer formats offered
|
||||
and the one used, and how long capturing took.
|
||||
- `region` logs the time from process start to the first overlay commit, and
|
||||
the selected rectangle in both logical and pixel coordinates.
|
||||
- The default level is `info`; `VALW_LOG=debug` (EnvFilter syntax) raises it.
|
||||
- stderr gets warnings and errors. Processes spawned by niri log stderr to
|
||||
niri's journal (`journalctl --user -u niri`), but the log file is the
|
||||
- stderr gets warnings from tracing (with colour only on a terminal) and
|
||||
errors in the three-part format above. Processes spawned by niri log stderr
|
||||
to niri's journal (`journalctl --user -u niri`), but the log file is the
|
||||
primary record.
|
||||
- If the log directory can't be written, valw keeps working and logs to
|
||||
stderr only; a broken log dir must not block a screenshot.
|
||||
|
||||
### 9.3 Panics
|
||||
|
||||
@@ -390,10 +431,10 @@ same three-part format to stderr, with "this is a bug in valw" as the hint.
|
||||
Prints one `ok` / `warn` / `fail` line per check and exits 1 if any check
|
||||
fails:
|
||||
|
||||
- required globals present with a sufficient version (screencopy, layer-shell,
|
||||
xdg-output, shm, seat), optional ones reported (fractional-scale, viewporter,
|
||||
cursor-shape, data-control)
|
||||
- outputs with position, size and scale
|
||||
- required globals present with a sufficient version (compositor, shm, seat,
|
||||
xdg-output v2, layer-shell, screencopy, viewporter); optional ones warn
|
||||
when missing (cursor-shape, ext/wlr data-control)
|
||||
- outputs with name, logical size, position and transform
|
||||
- niri IPC reachable, and niri's version compatible with the pinned `niri-ipc`
|
||||
- config file parses
|
||||
- save directory is writable
|
||||
@@ -417,17 +458,21 @@ Wayland-independent logic lives in pure functions:
|
||||
format error
|
||||
- filename template, collision suffix, `--output -`
|
||||
- config: defaults when missing, unknown key warning, invalid TOML line number
|
||||
- lock: acquire, busy error, forked child does not hold the lock
|
||||
- error rendering: snapshot of the three-part output
|
||||
- lock: acquire, busy error; forked child holds neither the lock nor stdout
|
||||
- output transforms (all eight) against grim's rules
|
||||
- error rendering: exact text of the three-part output
|
||||
- `niri-ipc` pin in Cargo.toml matches `niri::IPC_VERSION`
|
||||
|
||||
### 10.2 Headless integration test (`nix flake check`)
|
||||
|
||||
A check starts **sway** with `WLR_BACKENDS=headless` and
|
||||
`WLR_RENDERER=pixman` inside the build sandbox, then:
|
||||
A check starts **sway** (`sway-unwrapped`; the wrapped one needs D-Bus) with
|
||||
`WLR_BACKENDS=headless` and `WLR_RENDERER=pixman` inside the build sandbox,
|
||||
then:
|
||||
|
||||
- runs `valw doctor` and asserts no `fail` lines
|
||||
- runs `valw screen -o -` and asserts the PNG size equals the headless
|
||||
output's size
|
||||
- runs `valw screen -o - | cat` under a 10 s timeout (so a clipboard child
|
||||
holding the pipe fails the check) and asserts the PNG is 1280×720, the
|
||||
headless output's size
|
||||
|
||||
This exercises the real screencopy path on every build. It runs on wlroots,
|
||||
not niri. `region` is not covered because it needs synthetic pointer input.
|
||||
@@ -443,6 +488,8 @@ not niri. `region` is not covered because it needs synthetic pointer input.
|
||||
- **No stuck overlay:** `kill -9` during selection; the overlay disappears and
|
||||
the next run gets the lock.
|
||||
- **Noctalia:** the overlay covers the bar.
|
||||
- **Rotated output:** `niri msg output HDMI-A-1 transform 90`, then
|
||||
`valw screen` and `grim -o HDMI-A-1` must match pixel for pixel.
|
||||
|
||||
## 11. Phase 0 exit criteria
|
||||
|
||||
|
||||
Reference in new issue
Block a user