Files
valw/docs/superpowers/specs/2026-10-02-valw-phase-3-window-mode-design.md
T

104 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.