From fe6ce675bd6ca2e89c51358d6a0cec9686f515f1 Mon Sep 17 00:00:00 2001 From: Iisyourdad Date: Thu, 11 Jun 2026 08:15:50 -0500 Subject: [PATCH] reduce click capture latency Co-Authored-By: Claude Fable 5 --- app/capture.js | 24 ++++++++++++++---- tests/unit/capture.test.js | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 tests/unit/capture.test.js diff --git a/app/capture.js b/app/capture.js index 2af6238..4cc08fa 100644 --- a/app/capture.js +++ b/app/capture.js @@ -23,6 +23,7 @@ const { encodePng } = require('../core/png'); */ const CLICK_DEBOUNCE_MS = 700; +const CLICK_CAPTURE_HIDE_DELAY_MS = 25; function hasBinary(name) { try { @@ -241,6 +242,7 @@ class CaptureService { guideId: this.session.guideId, mode: mode === 'region' ? 'fullscreen' : mode, delayMs: 0, + hideWindowDelayMs: trigger === 'click' ? CLICK_CAPTURE_HIDE_DELAY_MS : null, refocus: false, // don't steal focus from the app the user is documenting }); if (result.ok) { @@ -285,7 +287,7 @@ while ($true) { $s = [W.U]::GetAsyncKeyState(0x01) -band 0x8000 if ($s -and -not $down) { Write-Output CLICK } $down = [bool]$s - Start-Sleep -Milliseconds 40 + Start-Sleep -Milliseconds 10 }`; this.clickWatcher = spawn('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: ['ignore', 'pipe', 'ignore'] }); this.clickWatcher.stdout.on('data', (chunk) => { @@ -365,12 +367,14 @@ while ($true) { * Hide the app window while `fn` runs so screenshots show the user's work, * not StepForge itself. Restores visibility afterwards. */ - async withWindowHidden(fn, { refocus = true } = {}) { + async withWindowHidden(fn, { refocus = true, pauseMs = 350 } = {}) { const win = this.getWindow(); const wasVisible = win && !win.isDestroyed() && win.isVisible() && !win.isMinimized(); if (wasVisible) { win.hide(); - await new Promise((r) => setTimeout(r, 350)); // let the compositor repaint + if (pauseMs > 0) { + await new Promise((r) => setTimeout(r, pauseMs)); // let the compositor repaint + } } try { return await fn(); @@ -390,13 +394,23 @@ while ($true) { * Take a screenshot and append it to the guide as a new image step. * Adds a click-marker annotation at the cursor position when enabled. */ - async shoot({ guideId, mode = 'fullscreen', delayMs = null, hideWindow = true, refocus = true }) { + async shoot({ + guideId, + mode = 'fullscreen', + delayMs = null, + hideWindow = true, + refocus = true, + hideWindowDelayMs = null, + }) { const delay = delayMs == null ? this.settings.get('capture.delayMs') || 0 : delayMs; if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay)); let grabbed; try { grabbed = hideWindow - ? await this.withWindowHidden(() => this.grab(mode), { refocus }) + ? await this.withWindowHidden(() => this.grab(mode), { + refocus, + pauseMs: hideWindowDelayMs == null ? 350 : hideWindowDelayMs, + }) : await this.grab(mode); } catch (err) { return { ok: false, reason: err.message }; diff --git a/tests/unit/capture.test.js b/tests/unit/capture.test.js new file mode 100644 index 0000000..0ca3826 --- /dev/null +++ b/tests/unit/capture.test.js @@ -0,0 +1,50 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const CaptureService = require('../../app/capture'); + +function makeService() { + const store = { + addStep() { + throw new Error('not used in this test'); + }, + }; + const settings = { + get(key) { + if (key === 'capture.mode') return 'fullscreen'; + if (key === 'capture.delayMs') return 0; + return null; + }, + }; + return new CaptureService({ + store, + settings, + getWindow: () => null, + notify: () => {}, + }); +} + +test('click-triggered session capture uses the low-latency hide pause', async () => { + const service = makeService(); + service.session = { guideId: 'guide-1', paused: false, count: 0, intervalSec: 0 }; + + let seenOptions = null; + service.shoot = async (options) => { + seenOptions = options; + return { ok: true, step: { stepId: 'step-1' } }; + }; + + const result = await service.sessionCapture('click'); + + assert.equal(result.ok, true); + assert.equal(service.session.count, 1); + assert.deepEqual(seenOptions, { + guideId: 'guide-1', + mode: 'fullscreen', + delayMs: 0, + hideWindowDelayMs: 25, + refocus: false, + }); +});