docs: phase 2 preview design spec
This commit is contained in:
1 parent
a9d979d38f
commit
f52cb2a430
1 file changed
+280
@@ -0,0 +1,280 @@
|
|||||||
|
# valw: Phase 2 design (floating preview)
|
||||||
|
|
||||||
|
Date: 2026-09-30
|
||||||
|
Status: approved in brainstorming, pending written-spec review
|
||||||
|
Builds on: `2026-09-29-valw-phase-0-1-design.md` (Phase 0+1, shipped as `main`)
|
||||||
|
|
||||||
|
## 1. Context and goal
|
||||||
|
|
||||||
|
After a capture, a small thumbnail slides in at the bottom-right corner of
|
||||||
|
the monitor, like macOS's floating screenshot thumbnail. Clicking it opens
|
||||||
|
the screenshot in Satty; swiping it right dismisses it; otherwise it slides
|
||||||
|
away after a few seconds.
|
||||||
|
|
||||||
|
Phase 2 is deliberately minimal. Once it ships, valw is wired into copland
|
||||||
|
and the user runs it as their only screenshot tool for at least a day. The
|
||||||
|
bugs and gaps found then decide what comes next.
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
- Floating thumbnail per capture, stacking up to 5.
|
||||||
|
- Click → Satty; swipe right → dismiss; timeout → dismiss.
|
||||||
|
- Thumbnails never appear in screenshots and never take keyboard focus.
|
||||||
|
- `[preview]` config (`enabled`, `timeout_secs`) and `--no-preview`.
|
||||||
|
- Satty on valw's PATH through the Nix package.
|
||||||
|
- Wiring valw into copland for the test day (section 9).
|
||||||
|
|
||||||
|
### Out of scope (candidates after the test day)
|
||||||
|
|
||||||
|
- Hover pauses the timer.
|
||||||
|
- Notifications (error notifications, preview-off fallback).
|
||||||
|
- Right-click menu, drag-and-drop out of the thumbnail.
|
||||||
|
- Rounded corners, shadow, configurable corner or size.
|
||||||
|
- Configurable editor command; valw's own editor (Phase 5).
|
||||||
|
- home-manager / NixOS modules (end of the project).
|
||||||
|
|
||||||
|
## 2. Decisions
|
||||||
|
|
||||||
|
| Question | Decision | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| When is the file written? | At capture time, as in Phase 1. The preview is only a shortcut. | Nothing is lost if the preview crashes or the session ends. |
|
||||||
|
| Next capture while a preview is visible | Previews **stack** (max 5, oldest evicted). | User choice. |
|
||||||
|
| Who owns the stack? | One on-demand **preview host** process. | A single owner avoids position races between processes. |
|
||||||
|
| Editing | Satty, hardcoded: `satty --filename <path> --output-filename <path>`. | Satty 0.22.0 has both flags; saving overwrites the screenshot. |
|
||||||
|
|
||||||
|
## 3. Preview host
|
||||||
|
|
||||||
|
### 3.1 Startup
|
||||||
|
|
||||||
|
After a capture has saved its file and set up the clipboard, and if the
|
||||||
|
preview is enabled (section 7):
|
||||||
|
|
||||||
|
1. Connect to `$XDG_RUNTIME_DIR/valw.sock`.
|
||||||
|
2. If that fails, start a host: `std::process::Command` on
|
||||||
|
`/proc/self/exe` with the hidden subcommand `__preview-host`, `setsid` in
|
||||||
|
`pre_exec`, stdio to `/dev/null`. A fresh exec inherits no Wayland
|
||||||
|
connection, lock or threads from the capture process.
|
||||||
|
3. Retry connecting every 20 ms for up to 1 s. If it still fails, log a
|
||||||
|
warning; the capture itself has already succeeded.
|
||||||
|
|
||||||
|
A new host takes `$XDG_RUNTIME_DIR/valw-preview.lock` (the existing `Lock`
|
||||||
|
type) before touching the socket. If the lock is busy, another host is
|
||||||
|
starting or running, and this one exits 0 without output. Holding the lock
|
||||||
|
makes it safe to delete a stale socket file (left by a crash) before
|
||||||
|
binding.
|
||||||
|
|
||||||
|
### 3.2 Protocol
|
||||||
|
|
||||||
|
Unix stream socket, one JSON object per line in each direction
|
||||||
|
(`serde_json`). Every request gets exactly one reply.
|
||||||
|
|
||||||
|
| Request | Host action | Reply |
|
||||||
|
|---|---|---|
|
||||||
|
| `{"cmd":"hide"}` | Mark this connection as hiding; unmap all thumbnails if this is the first; Wayland round trip so the compositor has processed the unmap | `{"ok":true}` |
|
||||||
|
| `{"cmd":"add","path":"…","output":"HDMI-A-1"}` | Load and downscale the PNG, add a thumbnail on that output | `{"ok":true}` |
|
||||||
|
|
||||||
|
Any malformed or unknown request gets `{"ok":false,"error":"…"}` and the
|
||||||
|
connection stays open.
|
||||||
|
|
||||||
|
A connection that sent `hide` stops hiding when it **closes**, for any
|
||||||
|
reason: normal end, Esc, an error, or the process being killed. There is no
|
||||||
|
explicit `show` message. Thumbnails are visible only while no connection is
|
||||||
|
hiding.
|
||||||
|
|
||||||
|
### 3.3 Lifetime
|
||||||
|
|
||||||
|
The host exits when its stack is empty and no client connection is open. It
|
||||||
|
removes the socket file on the way out. If it crashes, the compositor
|
||||||
|
removes its surfaces and the next capture starts a new host.
|
||||||
|
|
||||||
|
The host writes its own per-run log (`log::init`), like every valw process.
|
||||||
|
|
||||||
|
### 3.4 Event loop
|
||||||
|
|
||||||
|
calloop (SCTK's default `calloop` feature, `WaylandSource`), one loop for:
|
||||||
|
|
||||||
|
- the Wayland connection,
|
||||||
|
- the listening socket and every client connection (line-buffered),
|
||||||
|
- a timer per thumbnail (`timeout_secs`),
|
||||||
|
- animation, paced by frame callbacks.
|
||||||
|
|
||||||
|
## 4. Thumbnail
|
||||||
|
|
||||||
|
### 4.1 Surface
|
||||||
|
|
||||||
|
One layer-shell surface per thumbnail:
|
||||||
|
|
||||||
|
- namespace `valw-preview`, layer `Overlay`, anchored bottom + right,
|
||||||
|
- `keyboard_interactivity = None`: the compositor never gives it keyboard
|
||||||
|
focus,
|
||||||
|
- input region = the image rectangle only (transparent padding lets clicks
|
||||||
|
through),
|
||||||
|
- buffer at the output's physical scale, `wp_viewporter` to logical size
|
||||||
|
(same approach as the region overlay).
|
||||||
|
|
||||||
|
### 4.2 Size and look
|
||||||
|
|
||||||
|
- Long edge 220 logical px, aspect ratio kept. Images smaller than that are
|
||||||
|
not upscaled.
|
||||||
|
- Downscaled once on `add` with `image::imageops::resize` (CatmullRom).
|
||||||
|
- 1 px light border (`#e0e0e0` at 80% opacity). No rounded corners or
|
||||||
|
shadow in Phase 2.
|
||||||
|
|
||||||
|
### 4.3 Stack layout
|
||||||
|
|
||||||
|
- Newest thumbnail in the corner: the image sits 16 px from the right and
|
||||||
|
bottom screen edges. (The surface itself touches the right edge, see 4.4;
|
||||||
|
the 16 px is padding inside it, outside the input region.)
|
||||||
|
- Older thumbnails above it, 12 px apart.
|
||||||
|
- When one closes, those above it move down (no animation).
|
||||||
|
- A 6th thumbnail evicts the oldest (slides out as on timeout).
|
||||||
|
- Stacks are per output.
|
||||||
|
|
||||||
|
### 4.4 Animation
|
||||||
|
|
||||||
|
- Slide in from the right edge: 200 ms, ease-out cubic.
|
||||||
|
- Slide out to the right: 150 ms, ease-in cubic.
|
||||||
|
- Implemented by moving the image's x offset inside the surface (the surface
|
||||||
|
reaches the screen edge; its right margin is 0). The layer position never
|
||||||
|
changes during an animation.
|
||||||
|
|
||||||
|
### 4.5 Input
|
||||||
|
|
||||||
|
- **Click:** press and release with less than 4 px of movement. Spawns
|
||||||
|
`satty --filename <path> --output-filename <path>` (detached, stdio to
|
||||||
|
`/dev/null`) and slides the thumbnail out. A failed spawn is logged.
|
||||||
|
- **Swipe:** press, then horizontal drag moves the image with the pointer
|
||||||
|
(right only; leftward drag is clamped to 0). On release, if the offset is
|
||||||
|
at least 40% of the image width, it slides out; otherwise it animates
|
||||||
|
back.
|
||||||
|
- **Timeout:** after `timeout_secs`, slide out. The timer keeps running
|
||||||
|
during a drag; if it fires mid-drag, the thumbnail closes on release.
|
||||||
|
It also keeps running while thumbnails are hidden for a capture: after a
|
||||||
|
long region selection, older thumbnails may already be gone. Simplest
|
||||||
|
rule; revisit after the test day if it annoys.
|
||||||
|
|
||||||
|
## 5. Capture integration
|
||||||
|
|
||||||
|
In `main.rs`'s capture flow:
|
||||||
|
|
||||||
|
1. After the lock and `--delay`, **immediately before screencopy**, open a
|
||||||
|
`HideGuard`: connect to the host socket if it exists, send `hide`, wait
|
||||||
|
up to 300 ms for the reply. No host: no-op. Timeout: log a warning and
|
||||||
|
carry on.
|
||||||
|
2. Keep the guard (and its connection) alive through region selection, so
|
||||||
|
thumbnails can't sit above the overlay or take its clicks.
|
||||||
|
3. After `deliver` has written the file: if a preview applies (5.1), send
|
||||||
|
`add` over the guard's connection, or, without a running host, start one
|
||||||
|
(3.1) and send `add` there.
|
||||||
|
4. Drop the guard: the connection closes and the stack shows again, the new
|
||||||
|
thumbnail sliding in.
|
||||||
|
|
||||||
|
Cost: one extra round trip before screencopy, only when a host is running.
|
||||||
|
|
||||||
|
### 5.1 When a preview is shown
|
||||||
|
|
||||||
|
- Only when a file was written: default save location or `-o PATH` to a
|
||||||
|
regular file.
|
||||||
|
- Not for `-o -` (stdout), `--clipboard-only`, or `-o` onto a device or
|
||||||
|
FIFO.
|
||||||
|
- `--all`: one preview, for the focused output's file (the same one that
|
||||||
|
goes to the clipboard).
|
||||||
|
- Target output: `region` → the anchor output; `screen` → the captured
|
||||||
|
(focused) output; `--all` → the focused output.
|
||||||
|
|
||||||
|
## 6. Code layout
|
||||||
|
|
||||||
|
Flat files, pure logic kept separate:
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/stack.rs` | Pure: stack positions, eviction, swipe/click decisions, easing, thumbnail size |
|
||||||
|
| `src/ipc.rs` | Message types (serde), `HideGuard`, `add`, starting the host |
|
||||||
|
| `src/host.rs` | Host entry point, lock, socket server, calloop loop, hide counter |
|
||||||
|
| `src/thumbnail.rs` | One thumbnail: surface, drawing, animation state, pointer handling |
|
||||||
|
|
||||||
|
`wayland.rs` gains what the host needs (a calloop-based dispatch path and
|
||||||
|
routing of pointer/frame/configure events to thumbnails), following the
|
||||||
|
existing `overlay` pattern. `main.rs` gains the hidden `__preview-host`
|
||||||
|
subcommand and the capture integration.
|
||||||
|
|
||||||
|
New dependency: `serde_json`. calloop comes with SCTK's default features.
|
||||||
|
|
||||||
|
## 7. Config and CLI
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[preview]
|
||||||
|
enabled = true
|
||||||
|
timeout_secs = 5
|
||||||
|
```
|
||||||
|
|
||||||
|
- `timeout_secs` must be at least 1; 0 or negative is a config error with a
|
||||||
|
hint.
|
||||||
|
- `--no-preview` (on `screen` and `region`) disables the preview for that
|
||||||
|
run.
|
||||||
|
- `valw doctor`: no new checks in Phase 2 (Satty ships in the package).
|
||||||
|
|
||||||
|
## 8. Packaging
|
||||||
|
|
||||||
|
`nix/package.ulu.nix` wraps the binary with `makeWrapper`:
|
||||||
|
`--suffix PATH : ${lib.makeBinPath [ pkgs.satty ]}`. A user's own Satty on
|
||||||
|
PATH still wins because of `--suffix`.
|
||||||
|
|
||||||
|
## 9. copland wiring (test day)
|
||||||
|
|
||||||
|
In the separate `~/copland` repo, done with the user's go-ahead:
|
||||||
|
|
||||||
|
- Flake input: `valw.url = "path:/home/elars/Projects/valw";` with
|
||||||
|
`inputs.nixpkgs.follows = "nixpkgs"`. After valw changes:
|
||||||
|
`nix flake update valw`.
|
||||||
|
- Package: add `inputs.valw.packages.${system}.default` to the user's
|
||||||
|
packages (a small `modules/home/home-valw.ulu.nix`, no module options).
|
||||||
|
- Binds in `modules/home/niri/desktop-niri-keybinds.ulu.nix`:
|
||||||
|
- `Mod+Shift+S` → `valw region` (replaces Noctalia's `screenshot-region`)
|
||||||
|
- `Print` → `valw screen`
|
||||||
|
- `Mod+Shift+3/4` stay workspace moves.
|
||||||
|
|
||||||
|
## 10. Testing
|
||||||
|
|
||||||
|
### 10.1 Unit tests (`cargo nextest`)
|
||||||
|
|
||||||
|
- `stack`: positions for 1–5 thumbnails; eviction at 6; swipe 39% → back,
|
||||||
|
40% → close, leftward → back; click vs drag (< 4 px); easing endpoints
|
||||||
|
and monotonicity; thumbnail size (1920×1080 → 220×124, 1080×1920 →
|
||||||
|
124×220, 100×50 unchanged).
|
||||||
|
- `ipc`: JSON round trip for each message; malformed and unknown requests
|
||||||
|
→ `ok:false`.
|
||||||
|
- host state: hide counter (hide, hide, close, close → visible only at 0);
|
||||||
|
exit condition (empty stack and no connections).
|
||||||
|
- host startup: a second host fails the lock and exits quietly.
|
||||||
|
- config: `[preview]` defaults, `timeout_secs = 0` rejected.
|
||||||
|
|
||||||
|
### 10.2 Headless sway check (`nix flake check`)
|
||||||
|
|
||||||
|
The headless background is a solid colour, which makes these deterministic:
|
||||||
|
|
||||||
|
1. `valw screen` with default target → `valw.sock` appears within 1 s.
|
||||||
|
2. While the thumbnail is visible, `valw screen -o -` → the bottom-right
|
||||||
|
300×200 px of the PNG is entirely the background colour (thumbnails
|
||||||
|
never end up in screenshots).
|
||||||
|
3. With `timeout_secs = 1`, the socket is gone within 3 s (host exits).
|
||||||
|
4. `--no-preview` → no socket appears.
|
||||||
|
|
||||||
|
Click and swipe need real pointer input and stay manual.
|
||||||
|
|
||||||
|
### 10.3 Test-day checklist (manual, niri)
|
||||||
|
|
||||||
|
- Thumbnail on the correct monitor; sharp at scale 1.25.
|
||||||
|
- Typing is never interrupted while a thumbnail is visible.
|
||||||
|
- Click opens Satty; saving there overwrites the screenshot file.
|
||||||
|
- Swipe right closes; a short drag snaps back.
|
||||||
|
- Several captures in a row stack up to 5; none appear in later
|
||||||
|
screenshots.
|
||||||
|
- During region selection thumbnails are hidden and not clickable.
|
||||||
|
- After the timeout, `pgrep -f __preview-host` finds nothing.
|
||||||
|
|
||||||
|
## 11. Definition of done
|
||||||
|
|
||||||
|
`cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo nextest run`
|
||||||
|
and `nix flake check` pass; the section 10.3 checklist is done on niri;
|
||||||
|
valw is wired into copland and the test day can start.
|
||||||
Reference in new issue
Block a user