docs: phase 3 window mode design spec
This commit is contained in:
1 parent
473f8d688d
commit
2dfb7cb7a4
1 file changed
+103
@@ -0,0 +1,103 @@
|
|||||||
|
# valw Phase 3: window mode (design)
|
||||||
|
|
||||||
|
Date: 2026-10-02
|
||||||
|
Status: approved in brainstorming, pending written-spec review
|
||||||
|
Builds on: Phase 0+1 (region/screen), Phase 2 (preview), drag-out (all on `main`)
|
||||||
|
|
||||||
|
## 1. Goal
|
||||||
|
|
||||||
|
Capture a single window the macOS way: `valw window` lets the user click a
|
||||||
|
window and saves exactly that window; in region mode, Space (before
|
||||||
|
dragging) switches to window mode, like the camera icon on macOS.
|
||||||
|
|
||||||
|
Success: any window (tiled or floating, partly covered or not) can be
|
||||||
|
captured with a click, and the shot then goes through the same save,
|
||||||
|
clipboard, preview and drag-out flow as every other capture.
|
||||||
|
|
||||||
|
### Out of scope (later polish phase)
|
||||||
|
|
||||||
|
- The size indicator (`640 × 480`) next to the cursor.
|
||||||
|
- Shift / Alt / Space modifiers while dragging a region.
|
||||||
|
- The window shadow option.
|
||||||
|
- A blue highlight on the window under the pointer (see 2).
|
||||||
|
|
||||||
|
## 2. Decisions
|
||||||
|
|
||||||
|
| Question | Decision | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Who picks the window | niri (`Request::PickWindow`) | niri 26.04's IPC reports positions only for floating windows (`tile_pos_in_workspace_view` is `None` for tiled ones), so valw can't hit-test or highlight tiled windows itself |
|
||||||
|
| Who renders it | niri (`Action::ScreenshotWindow` with an explicit `path`) | Exact window pixels even when covered, rounded corners and transparency kept, correct scale |
|
||||||
|
| How valw gets the pixels | niri writes a temporary PNG; valw waits for the `ScreenshotCaptured` event, decodes the file and deletes it | One path for every target (`--output`, `-`, `--clipboard-only`); the rest of the pipeline stays unchanged |
|
||||||
|
| Highlight | None; niri's pick cursor is the feedback | Not possible for tiled windows (see above) |
|
||||||
|
|
||||||
|
## 3. Flow of `valw window`
|
||||||
|
|
||||||
|
1. As today: load the config, take the capture lock, wait `--delay`, hide
|
||||||
|
the thumbnails (`HideGuard`).
|
||||||
|
2. `niri::pick_window()` sends `Request::PickWindow`; niri changes the
|
||||||
|
cursor and replies after the click. `None` (Esc) → `Cancelled`.
|
||||||
|
3. Open a second niri socket with `Request::EventStream` **before**
|
||||||
|
asking for the screenshot, so the event can't be missed.
|
||||||
|
4. Send `Action::ScreenshotWindow { id: Some(id), write_to_disk: true,
|
||||||
|
show_pointer: cursor, path: Some(tmp) }`, where `tmp` is
|
||||||
|
`$XDG_RUNTIME_DIR/valw-window-<pid>.png` (absolute, as niri requires).
|
||||||
|
5. Read events until `Event::ScreenshotCaptured { path: Some(p) }` with
|
||||||
|
`p == tmp`. A reader thread sends events over a channel; the main side
|
||||||
|
waits at most 5 s, then fails with "niri did not deliver the window
|
||||||
|
screenshot". `ScreenshotCaptured { path: None }` or another path is
|
||||||
|
ignored (niri also reports other screenshots on the same stream).
|
||||||
|
6. Decode `tmp` into an `RgbaImage` and delete it (also on every error
|
||||||
|
after step 4, through a drop guard).
|
||||||
|
7. The preview's output: the output of the window's workspace (from
|
||||||
|
`Request::Workspaces`); if that is unknown, niri's focused output, as
|
||||||
|
in screen mode.
|
||||||
|
8. Continue with the existing `save` → preview → clipboard steps, exactly
|
||||||
|
as for a region shot.
|
||||||
|
|
||||||
|
Side effects owned by niri, accepted: niri sets the clipboard itself and
|
||||||
|
shows its own "Screenshot captured" notification (niri's `dbus` build).
|
||||||
|
valw then sets the clipboard again with the same image (when configured),
|
||||||
|
so the end state is unchanged. The notification's image may be gone,
|
||||||
|
since valw deletes the temporary file.
|
||||||
|
|
||||||
|
## 4. Space in region mode
|
||||||
|
|
||||||
|
- The overlay's keyboard handler: Space while no drag is in progress
|
||||||
|
ends the overlay with the outcome "switch to window mode". Space during
|
||||||
|
a drag does nothing (reserved for "move the selection" in the polish
|
||||||
|
phase).
|
||||||
|
- `region::select` returns `enum Choice { Region(usize, PixelRect), Window }`
|
||||||
|
instead of `Picked`.
|
||||||
|
- `main`: `Window` drops the frozen frames and the Wayland connection and
|
||||||
|
runs steps 2–8 of section 3 in the same process (lock, delay and hidden
|
||||||
|
thumbnails carry over; the delay is not applied twice).
|
||||||
|
|
||||||
|
## 5. Code layout
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `src/niri.rs` | `pick_window()`, `screenshot_window(id, path, cursor)`, `wait_for_capture(...)`, `output_of(window, workspaces)`, `is_our_capture(event, path)` |
|
||||||
|
| `src/window.rs` (new) | steps 2–7: temporary path + drop guard, decode, returns `(RgbaImage, output name)` |
|
||||||
|
| `src/main.rs` | `Command::Window`, `Mode::Window`, `Choice::Window` from region |
|
||||||
|
| `src/region.rs` | `Choice`; `Overlay::switch_to_window()` (no-op while dragging) |
|
||||||
|
| `src/wayland.rs` | Space in `press_key` calls `switch_to_window` |
|
||||||
|
| `src/doctor.rs` | nothing new (the niri version check already covers the IPC) |
|
||||||
|
|
||||||
|
## 6. Testing
|
||||||
|
|
||||||
|
Unit tests (`cargo nextest`):
|
||||||
|
- `niri::is_our_capture`: our path → true; `None`, another path → false.
|
||||||
|
- `niri::output_of`: window on a workspace with an output → that name;
|
||||||
|
no workspace, unknown workspace, workspace without output → `None`.
|
||||||
|
- `region`: Space before a press → `Window`; Space while dragging →
|
||||||
|
nothing.
|
||||||
|
- `window`: the temporary file is removed when the guard drops.
|
||||||
|
|
||||||
|
Manual checks are appended to `docs/test-checklist.md` (tiled, floating,
|
||||||
|
covered, HiDPI/other monitor, Esc during the pick, `--cursor`, Space from
|
||||||
|
region mode, `--clipboard-only`, `-o -`).
|
||||||
|
|
||||||
|
## 7. Definition of done
|
||||||
|
|
||||||
|
`cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo nextest run`
|
||||||
|
and `nix flake check` pass; the checklist section is written.
|
||||||
Reference in new issue
Block a user