ubuntu not working. idk I'm just gonna use windows anyway
This commit is contained in:
@@ -62,7 +62,7 @@ using only Node built-ins.
|
|||||||
|
|
||||||
For a windows installation, see [docs/windows_installation](docs/windows_installation.md) or for a developer/more in depth walkthrough, see [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md).
|
For a windows installation, see [docs/windows_installation](docs/windows_installation.md) or for a developer/more in depth walkthrough, see [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md).
|
||||||
|
|
||||||
On **Linux** (⚠️ work in progress — X11 vs Wayland, enabling per-click capture, the screen-share prompt), see [GETTING_STARTED_WITH_LINUX.md](GETTING_STARTED_WITH_LINUX.md).
|
On **Linux** (⚠️ work in progress — X11 vs Wayland, enabling per-click capture, the screen-share prompt), see [docs/GETTING_STARTED_WITH_LINUX.md](docs/GETTING_STARTED_WITH_LINUX.md).
|
||||||
|
|
||||||
Requirements: Node.js 20+ and npm (Electron is the only dependency).
|
Requirements: Node.js 20+ and npm (Electron is the only dependency).
|
||||||
|
|
||||||
|
|||||||
+31
-7
@@ -113,8 +113,13 @@ function hasBinary(name) {
|
|||||||
// Treating it as "available" would leave the session with no click capture AND
|
// Treating it as "available" would leave the session with no click capture AND
|
||||||
// no interval fallback, so zero steps get captured.
|
// no interval fallback, so zero steps get captured.
|
||||||
function isWayland() {
|
function isWayland() {
|
||||||
return process.platform === 'linux'
|
if (process.platform !== 'linux') return false;
|
||||||
&& (process.env.XDG_SESSION_TYPE === 'wayland' || Boolean(process.env.WAYLAND_DISPLAY));
|
// XDG_SESSION_TYPE is the authoritative session hint when present. Some
|
||||||
|
// desktops still export WAYLAND_DISPLAY even when the active session is X11,
|
||||||
|
// so only fall back to it when XDG_SESSION_TYPE is unavailable.
|
||||||
|
const sessionType = String(process.env.XDG_SESSION_TYPE || '').toLowerCase();
|
||||||
|
if (sessionType) return sessionType === 'wayland';
|
||||||
|
return Boolean(process.env.WAYLAND_DISPLAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- evdev (Linux kernel input) click reader --------------------------------
|
// ---- evdev (Linux kernel input) click reader --------------------------------
|
||||||
@@ -263,6 +268,16 @@ class CaptureService {
|
|||||||
return this.settings.get('capture.strictClickFrames') !== false;
|
return this.settings.get('capture.strictClickFrames') !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fallbackCaptureTrigger() {
|
||||||
|
const raw = String(this.settings.get('capture.fallbackTrigger') || 'interval').toLowerCase();
|
||||||
|
return raw === 'hotkey' ? 'hotkey' : 'interval';
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackIntervalSec() {
|
||||||
|
const raw = Number(this.settings.get('capture.autoIntervalSec'));
|
||||||
|
return Number.isFinite(raw) && raw > 0 ? raw : 5;
|
||||||
|
}
|
||||||
|
|
||||||
clickCaptureAvailable() {
|
clickCaptureAvailable() {
|
||||||
if (this._clickAvail === undefined) {
|
if (this._clickAvail === undefined) {
|
||||||
// Three click sources, in order of fidelity:
|
// Three click sources, in order of fidelity:
|
||||||
@@ -296,12 +311,19 @@ class CaptureService {
|
|||||||
|
|
||||||
startSession(guideId, { intervalSec = null } = {}) {
|
startSession(guideId, { intervalSec = null } = {}) {
|
||||||
this.finishSession();
|
this.finishSession();
|
||||||
// Default trigger: clicks when the platform supports it, otherwise an
|
// Default trigger: clicks when the platform supports it, otherwise the
|
||||||
// interval so a session always produces steps even if the global hotkey
|
// user-selected fallback (timer or hotkey-only). That keeps Linux from
|
||||||
// never fires (common under Wayland/WSLg).
|
// silently dropping into an unwanted 5-second timer when click capture
|
||||||
|
// is unavailable.
|
||||||
let interval = intervalSec;
|
let interval = intervalSec;
|
||||||
if (interval == null) {
|
if (interval == null) {
|
||||||
interval = this.clickCaptureAvailable() ? 0 : (this.settings.get('capture.autoIntervalSec') || 5);
|
if (this.clickCaptureAvailable()) {
|
||||||
|
interval = 0;
|
||||||
|
} else if (this.fallbackCaptureTrigger() === 'hotkey') {
|
||||||
|
interval = 0;
|
||||||
|
} else {
|
||||||
|
interval = this.fallbackIntervalSec();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Sessions start paused: nothing hides and no capturing happens until
|
// Sessions start paused: nothing hides and no capturing happens until
|
||||||
// the user explicitly presses "Start recording" in the capture bar, so
|
// the user explicitly presses "Start recording" in the capture bar, so
|
||||||
@@ -1381,7 +1403,9 @@ public static class SFHook {
|
|||||||
console.error(`[stepforge] click watcher stopped${detail ? `: ${detail}` : ''}`);
|
console.error(`[stepforge] click watcher stopped${detail ? `: ${detail}` : ''}`);
|
||||||
if (!this.session) return;
|
if (!this.session) return;
|
||||||
if (!this.session.intervalSec) {
|
if (!this.session.intervalSec) {
|
||||||
this.session.intervalSec = this.settings.get('capture.autoIntervalSec') || 5;
|
this.session.intervalSec = this.fallbackCaptureTrigger() === 'hotkey'
|
||||||
|
? 0
|
||||||
|
: this.fallbackIntervalSec();
|
||||||
this.applyInterval();
|
this.applyInterval();
|
||||||
}
|
}
|
||||||
this.notify('capture:state', this.state());
|
this.notify('capture:state', this.state());
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class StepForgeApp {
|
|||||||
api.library.trashList(),
|
api.library.trashList(),
|
||||||
]);
|
]);
|
||||||
this.state.info = info;
|
this.state.info = info;
|
||||||
|
document.body.classList.toggle('platform-linux', info.platform === 'linux');
|
||||||
this.state.settings = settings;
|
this.state.settings = settings;
|
||||||
this.state.library = {
|
this.state.library = {
|
||||||
guides: library.guides || [],
|
guides: library.guides || [],
|
||||||
|
|||||||
@@ -312,6 +312,11 @@ function showSettingsDialog({
|
|||||||
const previewCount = makeInput(settings.exports?.previewStepCount ?? 3, 'number', { min: 1, step: 1 });
|
const previewCount = makeInput(settings.exports?.previewStepCount ?? 3, 'number', { min: 1, step: 1 });
|
||||||
const openFolder = el('input', { type: 'checkbox', checked: Boolean(settings.exports?.openFolderAfterExport) });
|
const openFolder = el('input', { type: 'checkbox', checked: Boolean(settings.exports?.openFolderAfterExport) });
|
||||||
const captureOutside = el('input', { type: 'checkbox', checked: Boolean(settings.capture?.captureOutsideClicks) });
|
const captureOutside = el('input', { type: 'checkbox', checked: Boolean(settings.capture?.captureOutsideClicks) });
|
||||||
|
const fallbackTrigger = makeSelect(settings.capture?.fallbackTrigger || 'interval', [
|
||||||
|
{ value: 'interval', label: 'Timed interval' },
|
||||||
|
{ value: 'hotkey', label: 'Hotkey only' },
|
||||||
|
]);
|
||||||
|
const autoIntervalSec = makeInput(settings.capture?.autoIntervalSec ?? 5, 'number', { min: 1, step: 1 });
|
||||||
const confirmSimple = el('input', { type: 'checkbox', checked: Boolean(settings.capture?.confirmSimpleCapture) });
|
const confirmSimple = el('input', { type: 'checkbox', checked: Boolean(settings.capture?.confirmSimpleCapture) });
|
||||||
const keepLast = makeInput(settings.backups?.keepLast ?? 10, 'number', { min: 0, step: 1 });
|
const keepLast = makeInput(settings.backups?.keepLast ?? 10, 'number', { min: 0, step: 1 });
|
||||||
const aiEnabled = el('input', { type: 'checkbox', checked: Boolean(settings.ai?.enabled) });
|
const aiEnabled = el('input', { type: 'checkbox', checked: Boolean(settings.ai?.enabled) });
|
||||||
@@ -326,6 +331,12 @@ function showSettingsDialog({
|
|||||||
void api.settings.set({ keyPath: 'ai.ollama.model', value: model }).catch(() => {});
|
void api.settings.set({ keyPath: 'ai.ollama.model', value: model }).catch(() => {});
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
|
const syncFallbackUi = () => {
|
||||||
|
autoIntervalSec.disabled = fallbackTrigger.value === 'hotkey';
|
||||||
|
};
|
||||||
|
fallbackTrigger.addEventListener('change', syncFallbackUi);
|
||||||
|
syncFallbackUi();
|
||||||
|
|
||||||
const updateAiStatus = (message, { error = false } = {}) => {
|
const updateAiStatus = (message, { error = false } = {}) => {
|
||||||
aiStatus.textContent = message;
|
aiStatus.textContent = message;
|
||||||
aiStatus.classList.toggle('error', Boolean(error));
|
aiStatus.classList.toggle('error', Boolean(error));
|
||||||
@@ -403,9 +414,14 @@ function showSettingsDialog({
|
|||||||
labeledRow('Delay (ms)', delayMs),
|
labeledRow('Delay (ms)', delayMs),
|
||||||
labeledRow('Click marker', clickMarker),
|
labeledRow('Click marker', clickMarker),
|
||||||
labeledRow('Capture outside clicks', captureOutside),
|
labeledRow('Capture outside clicks', captureOutside),
|
||||||
|
labeledRow('When clicks are unavailable', fallbackTrigger),
|
||||||
|
labeledRow('Timer interval (seconds)', autoIntervalSec),
|
||||||
labeledRow('Confirm simple capture', confirmSimple),
|
labeledRow('Confirm simple capture', confirmSimple),
|
||||||
labeledRow('Capture hotkey', captureHotkey),
|
labeledRow('Capture hotkey', captureHotkey),
|
||||||
labeledRow('Pause / resume hotkey', pauseHotkey),
|
labeledRow('Pause / resume hotkey', pauseHotkey),
|
||||||
|
el('div.muted', {},
|
||||||
|
'Hotkey fallback uses the Capture hotkey. Timer fallback uses the interval above.',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
el('fieldset', {},
|
el('fieldset', {},
|
||||||
el('legend', {}, 'Editor'),
|
el('legend', {}, 'Editor'),
|
||||||
@@ -455,6 +471,8 @@ function showSettingsDialog({
|
|||||||
delayMs: Number(delayMs.value || 0),
|
delayMs: Number(delayMs.value || 0),
|
||||||
mode: captureMode.value,
|
mode: captureMode.value,
|
||||||
clickMarker: clickMarker.checked,
|
clickMarker: clickMarker.checked,
|
||||||
|
fallbackTrigger: fallbackTrigger.value === 'hotkey' ? 'hotkey' : 'interval',
|
||||||
|
autoIntervalSec: Math.max(1, Number(autoIntervalSec.value || 5)),
|
||||||
hotkeyCapture: captureHotkey.value.trim(),
|
hotkeyCapture: captureHotkey.value.trim(),
|
||||||
hotkeyPauseResume: pauseHotkey.value.trim(),
|
hotkeyPauseResume: pauseHotkey.value.trim(),
|
||||||
captureOutsideClicks: captureOutside.checked,
|
captureOutsideClicks: captureOutside.checked,
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.platform-linux button {
|
||||||
|
padding: 5px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.platform-linux input,
|
||||||
|
body.platform-linux select,
|
||||||
|
body.platform-linux textarea {
|
||||||
|
padding: 6px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
*::selection { background: rgba(0, 104, 255, 0.2); }
|
*::selection { background: rgba(0, 104, 255, 0.2); }
|
||||||
|
|
||||||
#app { display: flex; flex-direction: column; height: 100vh; }
|
#app { display: flex; flex-direction: column; height: 100vh; }
|
||||||
@@ -183,6 +193,21 @@ kbd {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.platform-linux #topbar {
|
||||||
|
height: 52px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.platform-linux #capture-status {
|
||||||
|
padding: 5px 9px;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.platform-linux #capture-status button {
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
.library, .editor { flex: 1; min-height: 0; display: flex; }
|
.library, .editor { flex: 1; min-height: 0; display: flex; }
|
||||||
|
|
||||||
.lib-side {
|
.lib-side {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ const DEFAULT_SETTINGS = {
|
|||||||
hotkeyPauseResume: 'CommandOrControl+Shift+2',
|
hotkeyPauseResume: 'CommandOrControl+Shift+2',
|
||||||
captureOutsideClicks: true,
|
captureOutsideClicks: true,
|
||||||
confirmSimpleCapture: false,
|
confirmSimpleCapture: false,
|
||||||
|
// Fallback trigger when click capture is unavailable: keep the old timer
|
||||||
|
// fallback by default, but let users switch to hotkey-only recordings.
|
||||||
|
fallbackTrigger: 'interval', // interval | hotkey
|
||||||
// Leading-edge click debounce (ms): clicks of the same button closer
|
// Leading-edge click debounce (ms): clicks of the same button closer
|
||||||
// together than this collapse into one step, so accidental fast/double
|
// together than this collapse into one step, so accidental fast/double
|
||||||
// clicks don't each become a step. Clicks spaced further apart always
|
// clicks don't each become a step. Clicks spaced further apart always
|
||||||
|
|||||||
@@ -97,8 +97,13 @@ captures a screenshot on each click.
|
|||||||
### Adjusting the timed-capture interval
|
### Adjusting the timed-capture interval
|
||||||
|
|
||||||
If you don't enable the `input` group, StepForge captures on a timer. Change the
|
If you don't enable the `input` group, StepForge captures on a timer. Change the
|
||||||
interval in **Settings → Capture** (`capture.autoIntervalSec`, default 5
|
fallback in **Settings → Capture**:
|
||||||
seconds).
|
|
||||||
|
- `When clicks are unavailable` -> `Hotkey only` to use the Capture hotkey
|
||||||
|
instead of a timer.
|
||||||
|
- `When clicks are unavailable` -> `Timed interval`, then set
|
||||||
|
`Timer interval (seconds)` (`capture.autoIntervalSec`, default 5 seconds)
|
||||||
|
if you want timed captures.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node scripts/start-electron.js",
|
"start": "node scripts/start-electron.js",
|
||||||
"test": "node --test tests/unit/",
|
"test": "node --test \"tests/unit/*.test.js\"",
|
||||||
"sample": "node scripts/make-sample-guide.js",
|
"sample": "node scripts/make-sample-guide.js",
|
||||||
"package:windows": "node scripts/package-windows.js",
|
"package:windows": "node scripts/package-windows.js",
|
||||||
"build": "bash scripts/build-release.sh",
|
"build": "bash scripts/build-release.sh",
|
||||||
|
|||||||
@@ -60,6 +60,26 @@ function sanitizeElectronEnv(baseEnv = process.env) {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function linuxSandboxLaunchArgs({
|
||||||
|
electronPath,
|
||||||
|
platform = process.platform,
|
||||||
|
statSync = fs.statSync,
|
||||||
|
} = {}) {
|
||||||
|
if (platform !== 'linux') return [];
|
||||||
|
if (!electronPath) return ['--no-sandbox'];
|
||||||
|
|
||||||
|
const helperPath = path.join(path.dirname(electronPath), 'chrome-sandbox');
|
||||||
|
try {
|
||||||
|
const stat = statSync(helperPath);
|
||||||
|
const ownedByRoot = stat.uid === 0;
|
||||||
|
const hasSetuid = Boolean(stat.mode & 0o4000);
|
||||||
|
if (ownedByRoot && hasSetuid) return [];
|
||||||
|
} catch {
|
||||||
|
// Missing or unreadable helper: fall back to the unsandboxed launcher.
|
||||||
|
}
|
||||||
|
return ['--no-sandbox'];
|
||||||
|
}
|
||||||
|
|
||||||
function electronBinaryCandidates({ packageRoot, distDir, platform }) {
|
function electronBinaryCandidates({ packageRoot, distDir, platform }) {
|
||||||
const candidatePaths = [];
|
const candidatePaths = [];
|
||||||
const pathHint = packageRoot ? readElectronPathHint(packageRoot) : null;
|
const pathHint = packageRoot ? readElectronPathHint(packageRoot) : null;
|
||||||
@@ -292,6 +312,7 @@ module.exports = {
|
|||||||
runNpmRebuild,
|
runNpmRebuild,
|
||||||
runNpmInstall,
|
runNpmInstall,
|
||||||
sanitizeElectronEnv,
|
sanitizeElectronEnv,
|
||||||
|
linuxSandboxLaunchArgs,
|
||||||
resolveElectronBinary,
|
resolveElectronBinary,
|
||||||
resolveElectronPackageRoot,
|
resolveElectronPackageRoot,
|
||||||
platformBinaryCandidates,
|
platformBinaryCandidates,
|
||||||
|
|||||||
@@ -46,8 +46,21 @@ fi
|
|||||||
cat > "$WORK_DIR/usr/bin/stepforge" <<'EOF'
|
cat > "$WORK_DIR/usr/bin/stepforge" <<'EOF'
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
APP_DIR=/opt/stepforge
|
APP_DIR=/opt/stepforge
|
||||||
|
ELECTRON="$APP_DIR/node_modules/.bin/electron"
|
||||||
|
SANDBOX_HELPER="$APP_DIR/node_modules/electron/dist/chrome-sandbox"
|
||||||
cd "$APP_DIR" || exit 1
|
cd "$APP_DIR" || exit 1
|
||||||
exec "$APP_DIR/node_modules/.bin/electron" "$APP_DIR" "$@"
|
if command -v stat >/dev/null 2>&1 && [ -e "$SANDBOX_HELPER" ]; then
|
||||||
|
helper_uid="$(stat -c '%u' "$SANDBOX_HELPER" 2>/dev/null || echo '')"
|
||||||
|
helper_mode="$(stat -c '%a' "$SANDBOX_HELPER" 2>/dev/null || echo '')"
|
||||||
|
if [ "$helper_uid" = "0" ] && [ -n "$helper_mode" ]; then
|
||||||
|
helper_mode_num=$((8#$helper_mode))
|
||||||
|
if [ $((helper_mode_num & 04000)) -ne 0 ]; then
|
||||||
|
exec "$ELECTRON" "$APP_DIR" "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf '%s\n' '[stepforge] Electron sandbox helper is not configured for this install; starting with --no-sandbox' >&2
|
||||||
|
exec "$ELECTRON" --no-sandbox "$APP_DIR" "$@"
|
||||||
EOF
|
EOF
|
||||||
chmod 0755 "$WORK_DIR/usr/bin/stepforge"
|
chmod 0755 "$WORK_DIR/usr/bin/stepforge"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
const { spawn } = require('node:child_process');
|
const { spawn } = require('node:child_process');
|
||||||
|
|
||||||
const { resolveElectronBinary, sanitizeElectronEnv } = require('./electron-launcher');
|
const {
|
||||||
|
linuxSandboxLaunchArgs,
|
||||||
|
resolveElectronBinary,
|
||||||
|
sanitizeElectronEnv,
|
||||||
|
} = require('./electron-launcher');
|
||||||
|
|
||||||
let electronPath;
|
let electronPath;
|
||||||
try {
|
try {
|
||||||
@@ -13,11 +17,16 @@ try {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const env = sanitizeElectronEnv();
|
const env = sanitizeElectronEnv();
|
||||||
|
const sandboxArgs = linuxSandboxLaunchArgs({ electronPath });
|
||||||
|
if (sandboxArgs.includes('--no-sandbox')) {
|
||||||
|
console.warn('[stepforge] Electron sandbox helper is not configured for this install; starting with --no-sandbox');
|
||||||
|
}
|
||||||
|
|
||||||
// On Linux/Wayland, enable PipeWire-based screen capture so desktopCapturer
|
// On Linux, prefer the native Ozone path when available and enable PipeWire-
|
||||||
// can go through the XDG Desktop Portal when XWayland isn't the only option.
|
// based screen capture so desktopCapturer can go through the XDG Desktop
|
||||||
|
// Portal on Wayland without affecting X11.
|
||||||
const extraArgs = process.platform === 'linux'
|
const extraArgs = process.platform === 'linux'
|
||||||
? ['--enable-features=WebRTCPipeWireCapturer']
|
? ['--enable-features=WebRTCPipeWireCapturer', '--ozone-platform-hint=auto', ...sandboxArgs]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const child = spawn(electronPath, [...extraArgs, '.'], {
|
const child = spawn(electronPath, [...extraArgs, '.'], {
|
||||||
|
|||||||
@@ -33,6 +33,22 @@ function makeService({ settings: settingsOverrides, screenApi } = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('XDG_SESSION_TYPE=x11 wins over a stray WAYLAND_DISPLAY value', () => {
|
||||||
|
const prevSessionType = process.env.XDG_SESSION_TYPE;
|
||||||
|
const prevWaylandDisplay = process.env.WAYLAND_DISPLAY;
|
||||||
|
try {
|
||||||
|
process.env.XDG_SESSION_TYPE = 'x11';
|
||||||
|
process.env.WAYLAND_DISPLAY = 'wayland-0';
|
||||||
|
const service = makeService();
|
||||||
|
assert.equal(service.onWayland(), false);
|
||||||
|
} finally {
|
||||||
|
if (prevSessionType === undefined) delete process.env.XDG_SESSION_TYPE;
|
||||||
|
else process.env.XDG_SESSION_TYPE = prevSessionType;
|
||||||
|
if (prevWaylandDisplay === undefined) delete process.env.WAYLAND_DISPLAY;
|
||||||
|
else process.env.WAYLAND_DISPLAY = prevWaylandDisplay;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// The raw/regular twin window plus margin: how long a test must wait for a
|
// The raw/regular twin window plus margin: how long a test must wait for a
|
||||||
// held Linux raw press to fire when no coordinate twin arrives.
|
// held Linux raw press to fire when no coordinate twin arrives.
|
||||||
const TWIN_FLUSH_MS = 60;
|
const TWIN_FLUSH_MS = 60;
|
||||||
@@ -840,6 +856,49 @@ test('losing the click watcher mid-session falls back to interval capture', () =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('losing the click watcher mid-session can fall back to hotkey-only', () => {
|
||||||
|
const service = makeService();
|
||||||
|
service.settings.get = (key) => {
|
||||||
|
if (key === 'capture.fallbackTrigger') return 'hotkey';
|
||||||
|
if (key === 'capture.autoIntervalSec') return 3;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
service.session = { guideId: 'guide-loss-hotkey', paused: false, count: 0, intervalSec: 0 };
|
||||||
|
const states = [];
|
||||||
|
service.notify = (channel, payload) => {
|
||||||
|
states.push({ channel, payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.handleClickWatcherLoss('exited with code 1');
|
||||||
|
|
||||||
|
assert.equal(service.session.intervalSec, 0,
|
||||||
|
'hotkey fallback must not silently start the 5-second timer');
|
||||||
|
assert.equal(service.intervalTimer, null);
|
||||||
|
assert.ok(states.some((s) => s.channel === 'capture:state'));
|
||||||
|
} finally {
|
||||||
|
service.finishSession();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starting a session with hotkey fallback keeps the timer off when clicks are unavailable', () => {
|
||||||
|
const service = makeService();
|
||||||
|
service.clickCaptureAvailable = () => false;
|
||||||
|
service.settings.get = (key) => {
|
||||||
|
if (key === 'capture.fallbackTrigger') return 'hotkey';
|
||||||
|
if (key === 'capture.autoIntervalSec') return 7;
|
||||||
|
if (key === 'capture.captureOutsideClicks') return false;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.startSession('guide-hotkey-fallback');
|
||||||
|
|
||||||
|
assert.equal(service.session.intervalSec, 0);
|
||||||
|
assert.equal(service.state().clickCaptureAvailable, false);
|
||||||
|
assert.equal(service.state().clickCapture, false);
|
||||||
|
service.finishSession();
|
||||||
|
});
|
||||||
|
|
||||||
// ---- strict frame selection -----------------------------------------------------
|
// ---- strict frame selection -----------------------------------------------------
|
||||||
|
|
||||||
test('a click is served instantly from the freshly buffered frame', async () => {
|
test('a click is served instantly from the freshly buffered frame', async () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const path = require('node:path');
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
buildMissingElectronError,
|
buildMissingElectronError,
|
||||||
|
linuxSandboxLaunchArgs,
|
||||||
repairElectronInstall,
|
repairElectronInstall,
|
||||||
resolveElectronBinary,
|
resolveElectronBinary,
|
||||||
} = require('../../scripts/electron-launcher');
|
} = require('../../scripts/electron-launcher');
|
||||||
@@ -171,3 +172,21 @@ test('reports a helpful error when the runtime is missing', (t) => {
|
|||||||
assert.match(message, /Electron could not be started/);
|
assert.match(message, /Electron could not be started/);
|
||||||
assert.match(message, /Expected the binary in:/);
|
assert.match(message, /Expected the binary in:/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('uses --no-sandbox when the Linux sandbox helper is not root-owned and setuid', () => {
|
||||||
|
const args = linuxSandboxLaunchArgs({
|
||||||
|
electronPath: '/tmp/stepforge/node_modules/electron/dist/electron',
|
||||||
|
platform: 'linux',
|
||||||
|
statSync: () => ({ uid: 1000, mode: 0o100755 }),
|
||||||
|
});
|
||||||
|
assert.deepEqual(args, ['--no-sandbox']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the sandbox enabled when the Linux helper is root-owned and setuid', () => {
|
||||||
|
const args = linuxSandboxLaunchArgs({
|
||||||
|
electronPath: '/tmp/stepforge/node_modules/electron/dist/electron',
|
||||||
|
platform: 'linux',
|
||||||
|
statSync: () => ({ uid: 0, mode: 0o104755 }),
|
||||||
|
});
|
||||||
|
assert.deepEqual(args, []);
|
||||||
|
});
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ test('settings persist, deep-merge with defaults, and store global placeholders'
|
|||||||
assert.equal(s2.get('capture.delayMs'), 1500);
|
assert.equal(s2.get('capture.delayMs'), 1500);
|
||||||
assert.equal(s2.get('ai.ollama.model'), 'qwen3:0.6b');
|
assert.equal(s2.get('ai.ollama.model'), 'qwen3:0.6b');
|
||||||
assert.equal(s2.get('capture.clickMarker'), DEFAULT_SETTINGS.capture.clickMarker);
|
assert.equal(s2.get('capture.clickMarker'), DEFAULT_SETTINGS.capture.clickMarker);
|
||||||
|
assert.equal(s2.get('capture.fallbackTrigger'), DEFAULT_SETTINGS.capture.fallbackTrigger);
|
||||||
assert.deepEqual(s2.getGlobalPlaceholders(), { Company: 'Acme', Author: 'Tyler' });
|
assert.deepEqual(s2.getGlobalPlaceholders(), { Company: 'Acme', Author: 'Tyler' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user