latchshot 0.1.0

A lightweight yet intelligent window-aware screenshot tool
Documentation
# Latchshot 设计草案

> A lightweight yet intelligent window-aware screenshot tool.

本文档记录 Latchshot 的产品目标、交互原则和第一版架构约束。它不是一份已经冻结的规格;尚未决定的部分会继续在这里收敛。

## 项目目标

Latchshot 希望让最常见的截图操作足够自然:默认识别并吸附到光标下的窗口,需要精确控制时则直接拖动进入自由选区。它应该启动迅速、反馈清楚,并且在完成截图前尽量少打扰用户。

第一版需要支持:

- 光标下窗口的自动吸附。
- 单击或按 Enter 截取当前候选窗口。
- 拖动超过一个很小的阈值后进入自由选区。
- Shift 临时禁用吸附,Esc 取消。
- 多输出、缩放和输出变换下正确的坐标与裁切。
- 将结果写入文件、剪贴板或 stdout。
- 通过 Niri 后端获得窗口信息。

窗口截图在第一版中的含义是:裁切冻结画面中窗口几何范围内的实际像素。因此,窗口被其他内容遮挡时,遮挡也会进入截图。捕获独立且完整的窗口内容需要 compositor 提供额外能力,未来可以作为可选 capability 加入。

## 后端必须从第一版抽象

Niri 是第一个后端,但不能成为核心架构的一部分。后端抽象不是未来重构项,而是 MVP 的起点。

核心代码不得:

- 直接依赖 `niri-ipc` 类型。
- 根据 Niri 的 workspace、output 或 window ID 组织状态机。
- 在选择、动画、裁切或输出代码中调用 Niri IPC。
- 假设所有 compositor 都能提供完全相同的能力。

项目自己的领域模型至少需要表达:

- 输出的逻辑几何、像素尺寸、scale 和 transform。
- 窗口的稳定标识、可见几何、所属输出与焦点状态。
- 可用时的窗口层叠顺序或等价的命中测试信息。
- 后端当前具备的 capability。

坐标和尺寸在核心层保留足够精度,只在最终像素裁切或协议边界处舍入,避免 fractional scaling 产生一两个像素的漂移。

建议的代码边界是:

```text
Application
├── Core
│   ├── selection state machine
│   ├── hit testing
│   ├── animation
│   └── crop and output pipeline
├── Backend API
│   ├── scene/window discovery
│   ├── frame capture
│   ├── pointer/output information
│   └── capability reporting
├── Backends
│   ├── niri
│   └── future compositor implementations
└── Overlay renderer
```

窗口发现与画面捕获是不同的能力。某个后端可以用 compositor IPC 获取窗口几何,同时通过通用 Wayland 协议、portal 或外部工具捕获画面。因此实现上应使用小而独立的接口组合,而不是一个不断膨胀的 `Backend` trait。

后端应返回一次性的场景快照,核心只消费 Latchshot 自己的类型。窗口命中、重叠窗口优先级、多输出坐标转换和降级行为直接针对核心状态机做单元测试;当出现必须注入后端的集成测试时,再为测试提供最小 mock 实现。

后端选择最终应支持显式指定和自动探测。自动探测失败时应给出简短明确的错误,不能静默假装没有窗口并退化为自由选区。

## 截图流程

理想流程如下:

1. 获取输出画面,并取得与其时间尽量接近的场景快照。
2. 显示冻结画面和选择 overlay,避免 overlay 自己进入截图。
3. 根据场景快照确定光标下的候选窗口。
4. 用户确认窗口,或拖动进入自由选区。
5. 将逻辑选择区域转换为对应输出上的物理像素区域并裁切。
6. 按请求写入文件、剪贴板或 stdout,然后立即退出。

场景快照和捕获画面之间可能存在短暂竞态。核心接口应允许将来使用带同步保证的 compositor 后端,但 MVP 不需要为此引入复杂的持续追踪机制。

## 吸附动画

动画的目标是帮助用户理解当前选中了什么,而不是展示动画本身。

- 候选窗口使用细、清晰的圆角轮廓表示。
- 候选从无到有时做一次很短的淡入;窗口之间切换不重复淡入。
- 光标短暂经过窗口间隙时保留当前候选约 80 ms,以维持连续过渡;实际点击命中不使用这段宽限。
- 切换候选窗口时,轮廓在约 100–140 ms 内移动并改变尺寸。
- 使用接近临界阻尼的运动,不弹跳、不呼吸、不循环发光。
- 快速跨过多个窗口时始终追踪最新目标,不排队播放中间动画。
- 一旦进入自由选区,吸附动画立刻停止,选择框严格跟手。
- 完成时只提供很短的收束或亮度反馈,不使用刺眼的全屏闪光。
- 尊重 reduced-motion 设置;启用后改为极短淡入或直接切换。

如果运动中的边框让候选范围产生歧义,点击命中应以最新目标几何为准,而不是动画当时显示的中间几何。

## 暂不进入 MVP

- 截图后的完整标注编辑器。
- OCR、上传和云端历史。
- 长截图或滚动拼接。
- 为每个 compositor 复制一套 UI 或选择逻辑。
- 为了动画效果引入常驻后台进程。

## 待讨论

- overlay 和渲染层使用哪套 Wayland/Rust 组件。
- 第一版画面捕获使用通用协议、portal,还是可配置的外部 helper。
- 自由选区是否允许跨输出,以及不同 scale 输出交界处的像素语义。
- 重叠或浮动窗口缺少可靠层叠顺序时,候选窗口如何切换。
- 默认输出策略是“剪贴板优先”还是“文件与剪贴板同时写入”。