Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b12cd7e
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-rebuild",…
yvette-carlisle Apr 2, 2026
786d4c6
{"schema":"delivery/1","type":"fix","scope":"scroll-capture","summary…
yvette-carlisle Apr 2, 2026
bfa8a08
{"schema":"delivery/1","type":"fix","scope":"ci","summary":"format rs…
yvette-carlisle Apr 2, 2026
8062a1e
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-review","…
yvette-carlisle Apr 2, 2026
9c69884
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-ci","summ…
yvette-carlisle Apr 2, 2026
8906564
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-ci","summ…
yvette-carlisle Apr 2, 2026
3e99bdd
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-ci","summ…
yvette-carlisle Apr 2, 2026
8b58e12
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-ci","summ…
yvette-carlisle Apr 2, 2026
89bb5e8
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-ci","summ…
yvette-carlisle Apr 2, 2026
872330f
{"schema":"delivery/1","type":"fix","scope":"rust-style","summary":"m…
yvette-carlisle Apr 2, 2026
7fb4d5c
{"schema":"delivery/1","type":"fix","scope":"replay-ci","summary":"st…
yvette-carlisle Apr 2, 2026
7923a4f
{"schema":"delivery/1","type":"fix","scope":"replay-ci","summary":"ga…
yvette-carlisle Apr 2, 2026
4a0d6d4
{"schema":"delivery/1","type":"fix","scope":"review-repair","summary"…
yvette-carlisle Apr 2, 2026
bb1d142
{"schema":"delivery/1","type":"fix","scope":"review-repair","summary"…
yvette-carlisle Apr 2, 2026
e1cc760
{"schema":"delivery/1","type":"fix","scope":"scroll-capture","summary…
yvette-carlisle Apr 3, 2026
a8768c8
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-review-re…
yvette-carlisle Apr 3, 2026
9706217
{"schema":"delivery/1","type":"fix","scope":"scroll-capture-review-re…
yvette-carlisle Apr 3, 2026
8ea3c43
{"schema":"delivery/1","type":"fix","scope":"ci-review-repair","summa…
yvette-carlisle Apr 3, 2026
9e13c4e
{"schema":"delivery/1","type":"fix","scope":"review-repair","summary"…
yvette-carlisle Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 40 additions & 9 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,37 +130,68 @@ args = [
# | ---------------------------- | --------- | --- |
# | smoke-macos | composite | |
# | smoke-self-check-macos | composite | |
# | smoke-scroll-capture-macos | command | |
# | replay-scroll-capture | command | |
# | replay-scroll-capture-self-check | command | |
# | smoke-live-loupe-perf-macos | command | |
# | smoke-scroll-capture-self-check-macos | command | |
# | smoke-live-loupe-self-check-macos | command | |
# | analyze-scroll-capture-trace | command | |

[tasks.smoke-macos]
workspace = false
dependencies = [
"smoke-live-loupe-perf-macos",
"smoke-scroll-capture-macos",
"replay-scroll-capture",
]

[tasks.smoke-self-check-macos]
workspace = false
dependencies = [
"smoke-live-loupe-self-check-macos",
"smoke-scroll-capture-self-check-macos",
"replay-scroll-capture-self-check",
]

[tasks.smoke-scroll-capture-macos]
[tasks.replay-scroll-capture]
workspace = false
command = "scripts/scroll-capture-smoke-macos.sh"
command = "cargo"
args = [
"run",
"-p",
"rsnap-overlay",
"--example",
"scroll_capture_replay",
"--",
"--force-worker-pairwise",
]

[tasks.analyze-scroll-capture-trace]
workspace = false
command = "cargo"
args = [
"run",
"-p",
"rsnap-overlay",
"--example",
"scroll_capture_replay",
"--",
"--force-worker-pairwise",
"--json",
"--summary-only",
]

[tasks.smoke-live-loupe-perf-macos]
workspace = false
command = "scripts/live-loupe-perf-smoke-macos.sh"

[tasks.smoke-scroll-capture-self-check-macos]
[tasks.replay-scroll-capture-self-check]
workspace = false
command = "scripts/scroll-capture-smoke-macos.sh"
args = ["--self-check"]
command = "cargo"
args = [
"test",
"-p",
"rsnap-overlay",
"replay_recorded_live_trace_round_trips_one_commit",
"--lib",
]

[tasks.smoke-live-loupe-self-check-macos]
workspace = false
Expand Down
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ Prototype / in active development.

## Capture platform support

- Live sampling path: **macOS 12.3+** via ScreenCaptureKit (`SCStream`) stream samples.
- Live sampling path: **macOS 12.3+** via ScreenCaptureKit. Live loupe/window
sampling uses `SCStream`; downward scroll capture uses discrete
`SCScreenshotManager` region screenshots plus pairwise registration.
- Live mode is stream-first and does not capture full display on cursor movement.
- Frozen capture and scroll-capture imagery on macOS use the native capture stack; `docs/spec/v0.md` is the current contract source of truth.
- Menubar and Dock are not included in live window-outline targeting.
Expand Down Expand Up @@ -93,6 +95,7 @@ cargo run -p rsnap
- In Frozen mode, use Cmd+S (macOS) / Ctrl+S to save a PNG to disk and exit.
- After entering scroll capture from a dragged region on macOS, downward scrolling may append newly proven rows into the side preview.
Upward scrolling never appends. Returning to already-stitched content should not grow the export; only newly proven content may be added.
The scroll-capture commit path uses discrete region screenshots plus pairwise image registration; clipboard and save must match the committed preview the user sees.
`Space` copies the stitched image, Cmd+S (macOS) / Ctrl+S saves it, and `Esc` / `Back`
returns to the original Frozen capture without exiting.
- Output is configured in `settings.toml`:
Expand All @@ -108,16 +111,47 @@ cargo make lint
cargo make test
```

macOS GUI smoke harnesses are also available:
Scroll-capture verification now starts with deterministic replay instead of the old GUI smoke:

```sh
cargo make replay-scroll-capture
cargo make replay-scroll-capture-self-check
```

For semantic trace analysis (first bad frame, under-consumption, overshoot), use:

```sh
cargo make analyze-scroll-capture-trace
```

The remaining macOS GUI smoke harnesses are still available for live-loupe and
desktop-session checks:

```sh
cargo make smoke-self-check-macos
cargo make smoke-macos
```

These scripts drive a logged-in macOS desktop session, require the expected
Screen Recording / automation permissions, and are intended for dedicated smoke
verification runs rather than background CI on a shared desktop session.
`cargo make replay-scroll-capture` and `cargo make analyze-scroll-capture-trace`
now force the latest recorded live trace through the same worker-pairwise
commit path that current macOS production scroll capture uses. They are
trace-driven rather than scenario-driven, so they expect at least one recorded
trace under `~/Library/Application Support/ink.hack.rsnap/scroll-capture-traces/`
unless you pass `--trace <manifest-path>` directly to the example. Use the
direct example without `--force-worker-pairwise` only when you intentionally
want to compare the legacy recorded-source replay mode. `cargo make
replay-scroll-capture-self-check` is the repo-local fallback when you want to
verify the replay harness itself without relying on a user-recorded trace.
`cargo make smoke-self-check-macos` and `cargo make smoke-macos` still drive
the logged-in macOS live-loupe smoke path and require the expected Screen
Recording / automation permissions.

For `XY-185` style downward scroll-capture work, treat the verification order as:

1. deterministic tests and `cargo make check`
2. `cargo make replay-scroll-capture`
3. `cargo make analyze-scroll-capture-trace`
4. one fresh release live touchpad run with a newly recorded trace

Repo-native performance entrypoints are available for deterministic benches and
dedicated smoke:
Expand Down
36 changes: 11 additions & 25 deletions apps/rsnap/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub(crate) enum UserEvent {
#[cfg(target_os = "macos")]
OverlayStreamFrame,
#[cfg(target_os = "macos")]
OverlayScrollInput,
#[cfg(target_os = "macos")]
OverlayWorkerResponse,
}

Expand Down Expand Up @@ -71,12 +73,12 @@ struct App {
#[cfg(target_os = "macos")]
overlay_proxy: EventLoopProxy<UserEvent>,
#[cfg(target_os = "macos")]
overlay_stream_event_pending: Arc<AtomicBool>,
#[cfg(target_os = "macos")]
scroll_input_observer_lifecycle: Arc<ScrollInputObserverLifecycle>,
#[cfg(target_os = "macos")]
scroll_input_shared_state: Arc<SharedScrollInputState>,
#[cfg(target_os = "macos")]
overlay_stream_event_pending: Arc<AtomicBool>,
#[cfg(target_os = "macos")]
startup_permissions_checked: bool,
}
impl App {
Expand All @@ -87,7 +89,6 @@ impl App {
settings_hotkey: Option<HotKey>,
hotkey_manager: Option<GlobalHotKeyManager>,
#[cfg(target_os = "macos")] overlay_proxy: EventLoopProxy<UserEvent>,
#[cfg(target_os = "macos")] overlay_stream_event_pending: Arc<AtomicBool>,
#[cfg(target_os = "macos")] scroll_input_observer_lifecycle: Arc<
ScrollInputObserverLifecycle,
>,
Expand Down Expand Up @@ -121,16 +122,21 @@ impl App {
#[cfg(target_os = "macos")]
overlay_proxy,
#[cfg(target_os = "macos")]
overlay_stream_event_pending,
#[cfg(target_os = "macos")]
scroll_input_observer_lifecycle,
#[cfg(target_os = "macos")]
scroll_input_shared_state,
#[cfg(target_os = "macos")]
overlay_stream_event_pending: Arc::new(AtomicBool::new(false)),
#[cfg(target_os = "macos")]
startup_permissions_checked: false,
}
}

#[cfg(target_os = "macos")]
fn finish_coalesced_overlay_stream_frame_send(&self) {
self.overlay_stream_event_pending.store(false, Ordering::Release);
}

fn open_settings_window(&mut self, event_loop: &ActiveEventLoop, requested_by: &'static str) {
if let Some(window) = self.settings_window.as_ref() {
tracing::info!(requested_by = %requested_by, "Settings already open; focusing.");
Expand Down Expand Up @@ -187,23 +193,3 @@ impl App {
pub fn run() -> Result<()> {
runtime::run()
}

#[cfg(target_os = "macos")]
fn begin_coalesced_overlay_user_event_send(pending: &AtomicBool) -> bool {
pending.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire).is_ok()
}

#[cfg(test)]
mod tests {
#[cfg(target_os = "macos")]
use std::sync::atomic::AtomicBool;

#[cfg(target_os = "macos")]
#[test]
fn begin_coalesced_overlay_user_event_send_only_allows_first_sender_per_flag() {
let pending = AtomicBool::new(false);

assert!(super::begin_coalesced_overlay_user_event_send(&pending));
assert!(!super::begin_coalesced_overlay_user_event_send(&pending));
}
}
Loading
Loading