@reshotdev/screenshot 0.0.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +388 -0
- package/package.json +64 -0
- package/src/commands/auth.js +259 -0
- package/src/commands/chrome.js +140 -0
- package/src/commands/ci-run.js +123 -0
- package/src/commands/ci-setup.js +288 -0
- package/src/commands/drifts.js +423 -0
- package/src/commands/import-tests.js +309 -0
- package/src/commands/ingest.js +458 -0
- package/src/commands/init.js +633 -0
- package/src/commands/publish.js +1721 -0
- package/src/commands/pull.js +303 -0
- package/src/commands/record.js +94 -0
- package/src/commands/run.js +476 -0
- package/src/commands/setup-wizard.js +740 -0
- package/src/commands/setup.js +137 -0
- package/src/commands/status.js +275 -0
- package/src/commands/sync.js +621 -0
- package/src/commands/ui.js +248 -0
- package/src/commands/validate-docs.js +529 -0
- package/src/index.js +462 -0
- package/src/lib/api-client.js +815 -0
- package/src/lib/capture-engine.js +1623 -0
- package/src/lib/capture-script-runner.js +3120 -0
- package/src/lib/ci-detect.js +137 -0
- package/src/lib/config.js +1240 -0
- package/src/lib/diff-engine.js +642 -0
- package/src/lib/hash.js +74 -0
- package/src/lib/image-crop.js +396 -0
- package/src/lib/matrix.js +89 -0
- package/src/lib/output-path-template.js +318 -0
- package/src/lib/playwright-runner.js +252 -0
- package/src/lib/polished-clip.js +553 -0
- package/src/lib/privacy-engine.js +408 -0
- package/src/lib/progress-tracker.js +142 -0
- package/src/lib/record-browser-injection.js +654 -0
- package/src/lib/record-cdp.js +612 -0
- package/src/lib/record-clip.js +343 -0
- package/src/lib/record-config.js +623 -0
- package/src/lib/record-screenshot.js +360 -0
- package/src/lib/record-terminal.js +123 -0
- package/src/lib/recorder-service.js +781 -0
- package/src/lib/secrets.js +51 -0
- package/src/lib/selector-strategies.js +859 -0
- package/src/lib/standalone-mode.js +400 -0
- package/src/lib/storage-providers.js +569 -0
- package/src/lib/style-engine.js +684 -0
- package/src/lib/ui-api.js +4677 -0
- package/src/lib/ui-assets.js +373 -0
- package/src/lib/ui-executor.js +587 -0
- package/src/lib/variant-injector.js +591 -0
- package/src/lib/viewport-presets.js +454 -0
- package/src/lib/worker-pool.js +118 -0
- package/web/cropper/index.html +436 -0
- package/web/manager/dist/assets/index--ZgioErz.js +507 -0
- package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
- package/web/manager/dist/index.html +27 -0
- package/web/subtitle-editor/index.html +295 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// record-screenshot.js - Screenshot capture flow with cropping
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const inquirer = require("inquirer");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs-extra");
|
|
6
|
+
const express = require("express");
|
|
7
|
+
const { updateBrowserMode } = require("./record-browser-injection");
|
|
8
|
+
const { saveScenarioProgress } = require("./record-config");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Start screenshot capture flow
|
|
12
|
+
* @param {Object} sessionState - Recording session state
|
|
13
|
+
* @param {Page} page - Playwright page object
|
|
14
|
+
*/
|
|
15
|
+
async function startCaptureFlow(sessionState, page) {
|
|
16
|
+
const { captureType } = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: "list",
|
|
19
|
+
name: "captureType",
|
|
20
|
+
message: "What do you want to capture?",
|
|
21
|
+
choices: [
|
|
22
|
+
{ name: "Single-Step Screenshot (PNG)", value: "screenshot" },
|
|
23
|
+
{ name: "Multi-Step Animated Clip (GIF/MP4)", value: "clip" },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
if (captureType === "screenshot") {
|
|
29
|
+
const screenshotStep = await captureScreenshotInteractive(
|
|
30
|
+
sessionState,
|
|
31
|
+
page
|
|
32
|
+
);
|
|
33
|
+
await registerScreenshotStep(sessionState, page, screenshotStep);
|
|
34
|
+
} else if (captureType === "clip") {
|
|
35
|
+
const { startClipRecording } = require("./record-clip");
|
|
36
|
+
await startClipRecording(sessionState, page);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Capture screenshot with optional element selection and cropping
|
|
42
|
+
* @param {Object} sessionState - Recording session state
|
|
43
|
+
* @param {Page} page - Playwright page object
|
|
44
|
+
* @param {Object} options - Additional options
|
|
45
|
+
* @param {boolean} options.uiMode - If true, skip inquirer prompts
|
|
46
|
+
* @param {string} options.areaType - 'full' or 'element' (required if uiMode)
|
|
47
|
+
* @param {string} options.selector - Element selector (if areaType is 'element')
|
|
48
|
+
* @param {string} options.outputFilename - Output filename
|
|
49
|
+
* @param {Object} options.clip - Crop box coordinates
|
|
50
|
+
*/
|
|
51
|
+
async function captureScreenshotInteractive(sessionState, page, options = {}) {
|
|
52
|
+
const {
|
|
53
|
+
uiMode = false,
|
|
54
|
+
selector: providedSelector,
|
|
55
|
+
clip: providedClip,
|
|
56
|
+
outputFilename: providedFilename,
|
|
57
|
+
} = options;
|
|
58
|
+
let { areaType: providedAreaType } = options;
|
|
59
|
+
|
|
60
|
+
let areaType = providedAreaType;
|
|
61
|
+
|
|
62
|
+
if (!uiMode) {
|
|
63
|
+
// CLI mode - use inquirer
|
|
64
|
+
const answers = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: "list",
|
|
67
|
+
name: "areaType",
|
|
68
|
+
message: "What area should be captured?",
|
|
69
|
+
choices: [
|
|
70
|
+
{ name: "Full Page", value: "full" },
|
|
71
|
+
{ name: "Select a Specific Element", value: "element" },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
areaType = answers.areaType;
|
|
76
|
+
} else {
|
|
77
|
+
// UI mode - use provided options or defaults
|
|
78
|
+
areaType = areaType || "full";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let selector = providedSelector || null;
|
|
82
|
+
|
|
83
|
+
if (areaType === "element" && !selector) {
|
|
84
|
+
if (uiMode) {
|
|
85
|
+
// In UI mode without selector, default to full page
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.yellow(
|
|
88
|
+
"[Recorder] Element mode without selector, falling back to full page"
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
areaType = "full";
|
|
92
|
+
} else {
|
|
93
|
+
// Set mode to element selection
|
|
94
|
+
sessionState.mode = "select-element-for-screenshot";
|
|
95
|
+
sessionState.pendingCapture = { selector: null };
|
|
96
|
+
|
|
97
|
+
await updateBrowserMode(page, "select-element-for-screenshot");
|
|
98
|
+
|
|
99
|
+
console.log(
|
|
100
|
+
chalk.cyan(
|
|
101
|
+
"\n Return to your browser and click on the element you want to capture...\n"
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Wait for element to be selected
|
|
106
|
+
await new Promise((resolve) => {
|
|
107
|
+
sessionState.onElementSelected = resolve;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
selector = sessionState.pendingCapture.selector;
|
|
111
|
+
await updateBrowserMode(page, "normal");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Get output filename
|
|
116
|
+
let outputFilename = providedFilename;
|
|
117
|
+
|
|
118
|
+
if (!outputFilename && !uiMode) {
|
|
119
|
+
const answers = await inquirer.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: "input",
|
|
122
|
+
name: "outputFilename",
|
|
123
|
+
message: "Output filename:",
|
|
124
|
+
default: `${sessionState.visualKey}-view.png`,
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
outputFilename = answers.outputFilename;
|
|
128
|
+
} else if (!outputFilename) {
|
|
129
|
+
outputFilename = `${sessionState.visualKey}-${Date.now()}.png`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Capture temporary screenshot
|
|
133
|
+
const tempDir = path.join(process.cwd(), ".reshot", "tmp");
|
|
134
|
+
fs.ensureDirSync(tempDir);
|
|
135
|
+
const tempImagePath = path.join(tempDir, `temp-screenshot-${Date.now()}.png`);
|
|
136
|
+
|
|
137
|
+
if (areaType === "full") {
|
|
138
|
+
await page.screenshot({ path: tempImagePath, fullPage: true });
|
|
139
|
+
} else {
|
|
140
|
+
const element = await page.locator(selector).first();
|
|
141
|
+
await element.screenshot({ path: tempImagePath });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(chalk.green(` ā Screenshot captured\n`));
|
|
145
|
+
|
|
146
|
+
// Ask about cropping (only in CLI mode unless clip is provided)
|
|
147
|
+
let cropConfig = null;
|
|
148
|
+
let cropBox = providedClip || null;
|
|
149
|
+
|
|
150
|
+
if (!uiMode && !cropBox) {
|
|
151
|
+
const { wantsCrop } = await inquirer.prompt([
|
|
152
|
+
{
|
|
153
|
+
type: "confirm",
|
|
154
|
+
name: "wantsCrop",
|
|
155
|
+
message:
|
|
156
|
+
"Do you want to crop this screenshot (e.g., to focus on a specific modal or element)?",
|
|
157
|
+
default: false,
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
if (wantsCrop) {
|
|
162
|
+
cropConfig = await runCropperServer(tempImagePath, page);
|
|
163
|
+
|
|
164
|
+
// Check if the cropper returned the new format or legacy format
|
|
165
|
+
if (cropConfig && cropConfig.region) {
|
|
166
|
+
// New format with full crop config
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.green(
|
|
169
|
+
`\n ā Crop defined: ${JSON.stringify(cropConfig.region)}`
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
if (cropConfig.persistToScenario) {
|
|
173
|
+
console.log(
|
|
174
|
+
chalk.cyan(
|
|
175
|
+
` ā Will apply to all subsequent captures in this scenario\n`
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
console.log("");
|
|
180
|
+
}
|
|
181
|
+
// Convert region to clip format for backward compatibility
|
|
182
|
+
cropBox = cropConfig.region;
|
|
183
|
+
} else if (cropConfig && cropConfig.x !== undefined) {
|
|
184
|
+
// Legacy format - just x, y, width, height
|
|
185
|
+
cropBox = cropConfig;
|
|
186
|
+
cropConfig = {
|
|
187
|
+
enabled: true,
|
|
188
|
+
region: cropBox,
|
|
189
|
+
scaleMode: "none",
|
|
190
|
+
preserveAspectRatio: true,
|
|
191
|
+
persistToScenario: false,
|
|
192
|
+
};
|
|
193
|
+
console.log(
|
|
194
|
+
chalk.green(`\n ā Crop defined: ${JSON.stringify(cropBox)}\n`)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Clean up temp image
|
|
201
|
+
fs.removeSync(tempImagePath);
|
|
202
|
+
|
|
203
|
+
// Get device scale factor from page
|
|
204
|
+
let deviceScaleFactor = null;
|
|
205
|
+
try {
|
|
206
|
+
deviceScaleFactor = await page.evaluate(() => window.devicePixelRatio || 1);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Fallback to 1 if evaluation fails
|
|
209
|
+
deviceScaleFactor = 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Generate stable step ID
|
|
213
|
+
const stepId = `${sessionState.visualKey}-${Date.now()}-${Math.random()
|
|
214
|
+
.toString(36)
|
|
215
|
+
.substr(2, 9)}`;
|
|
216
|
+
|
|
217
|
+
// Create screenshot step with full metadata
|
|
218
|
+
const screenshotStep = {
|
|
219
|
+
action: "screenshot",
|
|
220
|
+
key: buildCaptureKey(sessionState.visualKey, outputFilename),
|
|
221
|
+
path: outputFilename,
|
|
222
|
+
id: stepId,
|
|
223
|
+
deviceScaleFactor: deviceScaleFactor !== 1 ? deviceScaleFactor : undefined,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (selector) {
|
|
227
|
+
screenshotStep.selector = selector;
|
|
228
|
+
// Add default selector padding (can be customized later)
|
|
229
|
+
// Default to 10px padding on all sides
|
|
230
|
+
screenshotStep.selectorPadding = {
|
|
231
|
+
top: 10,
|
|
232
|
+
right: 10,
|
|
233
|
+
bottom: 10,
|
|
234
|
+
left: 10,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle crop configuration
|
|
239
|
+
if (cropConfig && cropConfig.enabled) {
|
|
240
|
+
// Use the new crop format
|
|
241
|
+
screenshotStep.crop = {
|
|
242
|
+
enabled: true,
|
|
243
|
+
region: cropConfig.region,
|
|
244
|
+
scaleMode: cropConfig.scaleMode || "none",
|
|
245
|
+
preserveAspectRatio: cropConfig.preserveAspectRatio !== false,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (cropConfig.padding) {
|
|
249
|
+
screenshotStep.crop.padding = cropConfig.padding;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Flag to indicate this crop should be persisted to scenario level
|
|
253
|
+
if (cropConfig.persistToScenario) {
|
|
254
|
+
screenshotStep._persistCropToScenario = true;
|
|
255
|
+
}
|
|
256
|
+
} else if (cropBox) {
|
|
257
|
+
// Legacy format - convert to clip
|
|
258
|
+
screenshotStep.clip = cropBox;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return screenshotStep;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Run cropper mini-server with HTML UI
|
|
266
|
+
* @param {string} tempImagePath - Path to temporary screenshot
|
|
267
|
+
* @param {Page} page - Playwright page object for opening cropper UI
|
|
268
|
+
* @returns {Promise<Object>} Crop box coordinates
|
|
269
|
+
*/
|
|
270
|
+
async function runCropperServer(tempImagePath, page) {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const app = express();
|
|
273
|
+
app.use(express.json());
|
|
274
|
+
|
|
275
|
+
let cropResult = null;
|
|
276
|
+
let cropperPage = null;
|
|
277
|
+
|
|
278
|
+
// Serve cropper HTML
|
|
279
|
+
app.get("/", (req, res) => {
|
|
280
|
+
const htmlPath = path.join(__dirname, "../../web/cropper/index.html");
|
|
281
|
+
res.sendFile(htmlPath);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Serve temp image
|
|
285
|
+
app.get("/image", (req, res) => {
|
|
286
|
+
res.sendFile(tempImagePath);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Receive crop coordinates
|
|
290
|
+
app.post("/crop", (req, res) => {
|
|
291
|
+
cropResult = req.body;
|
|
292
|
+
res.json({ ok: true });
|
|
293
|
+
|
|
294
|
+
// Close cropper and resolve
|
|
295
|
+
setTimeout(async () => {
|
|
296
|
+
if (cropperPage) {
|
|
297
|
+
await cropperPage.close();
|
|
298
|
+
}
|
|
299
|
+
server.close();
|
|
300
|
+
resolve(cropResult);
|
|
301
|
+
}, 500);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Start server on ephemeral port
|
|
305
|
+
const server = app.listen(0, async () => {
|
|
306
|
+
const port = server.address().port;
|
|
307
|
+
const url = `http://localhost:${port}`;
|
|
308
|
+
|
|
309
|
+
console.log(chalk.cyan(`\n Opening cropper UI at ${url}...\n`));
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// Open cropper in new tab
|
|
313
|
+
const context = page.context();
|
|
314
|
+
cropperPage = await context.newPage();
|
|
315
|
+
await cropperPage.goto(url);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
server.close();
|
|
318
|
+
reject(error);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Timeout after 5 minutes
|
|
323
|
+
setTimeout(() => {
|
|
324
|
+
if (!cropResult) {
|
|
325
|
+
server.close();
|
|
326
|
+
if (cropperPage) {
|
|
327
|
+
cropperPage.close();
|
|
328
|
+
}
|
|
329
|
+
reject(new Error("Cropper timeout"));
|
|
330
|
+
}
|
|
331
|
+
}, 5 * 60 * 1000);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function buildCaptureKey(visualKey, filename) {
|
|
336
|
+
const base = filename
|
|
337
|
+
.replace(path.extname(filename), "")
|
|
338
|
+
.replace(/[^a-zA-Z0-9-]/g, "-")
|
|
339
|
+
.toLowerCase()
|
|
340
|
+
.replace(/-+/g, "-")
|
|
341
|
+
.replace(/^-|-$/g, "");
|
|
342
|
+
const sanitized = base || `capture-${Date.now()}`;
|
|
343
|
+
return `${visualKey}-${sanitized}`.replace(/-+/g, "-");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function registerScreenshotStep(sessionState, page, screenshotStep) {
|
|
347
|
+
sessionState.capturedSteps.push(screenshotStep);
|
|
348
|
+
await saveScenarioProgress(sessionState, page, { finalize: false });
|
|
349
|
+
console.log(
|
|
350
|
+
chalk.green(
|
|
351
|
+
` ā Screenshot step added: ${sessionState.visualKey}/${screenshotStep.key} ā ${screenshotStep.path}\n`
|
|
352
|
+
)
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
startCaptureFlow,
|
|
358
|
+
captureScreenshot: captureScreenshotInteractive,
|
|
359
|
+
captureScreenshotInteractive,
|
|
360
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// record-terminal.js - Terminal hotkey handling for record command
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Set up terminal hotkey listener with raw mode
|
|
7
|
+
* @param {Object} sessionState - Recording session state
|
|
8
|
+
* @param {Function} onCapture - Callback when 'C' is pressed
|
|
9
|
+
* @returns {Function} Cleanup function to restore terminal
|
|
10
|
+
*/
|
|
11
|
+
function setupTerminalHotkeys(sessionState, onCapture) {
|
|
12
|
+
if (!process.stdin.isTTY) {
|
|
13
|
+
console.warn(chalk.yellow('ā Terminal is not in TTY mode, hotkeys may not work'));
|
|
14
|
+
return () => {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
escapeCodeTimeout: 50
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
23
|
+
process.stdin.setRawMode(true);
|
|
24
|
+
|
|
25
|
+
// Print hotkey instructions
|
|
26
|
+
console.log(chalk.cyan('šÆ Recording Controls:'));
|
|
27
|
+
console.log(chalk.gray(' Navigate to the state you want, then press C to capture (screenshot or clip).'));
|
|
28
|
+
console.log(chalk.gray(' Press Q - Quit and save the visual'));
|
|
29
|
+
console.log(chalk.gray(' Press Ctrl+C - Quit without saving\n'));
|
|
30
|
+
|
|
31
|
+
console.log(chalk.green('ā Setup complete. When you are ready, press C to start capturing.\n'));
|
|
32
|
+
|
|
33
|
+
const keypressHandler = (str, key) => {
|
|
34
|
+
if (!key) return;
|
|
35
|
+
|
|
36
|
+
// Ctrl+C - hard quit
|
|
37
|
+
if (key.ctrl && key.name === 'c') {
|
|
38
|
+
console.log(chalk.yellow('\n\nā Interrupted. Exiting without saving...'));
|
|
39
|
+
sessionState.quit = true;
|
|
40
|
+
sessionState.saveOnQuit = false;
|
|
41
|
+
if (sessionState.onChange) {
|
|
42
|
+
sessionState.onChange();
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Q - quit and save
|
|
48
|
+
if (key.name === 'q' && !key.ctrl) {
|
|
49
|
+
console.log(chalk.cyan('\n\nš Quitting recording session...'));
|
|
50
|
+
sessionState.quit = true;
|
|
51
|
+
sessionState.saveOnQuit = true;
|
|
52
|
+
if (sessionState.onChange) {
|
|
53
|
+
sessionState.onChange();
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// C - capture (screenshot or clip)
|
|
59
|
+
if (key.name === 'c' && !key.ctrl) {
|
|
60
|
+
// Check if we're stopping a clip recording
|
|
61
|
+
if (sessionState.mode === 'recording-clip') {
|
|
62
|
+
console.log(chalk.cyan('\n\nš Stopping clip recording...'));
|
|
63
|
+
sessionState.stopClipRecording = true;
|
|
64
|
+
if (sessionState.onChange) {
|
|
65
|
+
sessionState.onChange();
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Otherwise, start capture flow
|
|
71
|
+
if (sessionState.mode === 'normal') {
|
|
72
|
+
if (sessionState.phase === 'idle') {
|
|
73
|
+
sessionState.phase = 'capturing';
|
|
74
|
+
}
|
|
75
|
+
console.log(chalk.cyan('\n\nšø Starting capture flow...'));
|
|
76
|
+
if (onCapture) {
|
|
77
|
+
// Let the capture flow manage terminal settings as needed.
|
|
78
|
+
onCapture().catch((error) => {
|
|
79
|
+
console.error(chalk.red('Capture error:'), error.message);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
process.stdin.on('keypress', keypressHandler);
|
|
87
|
+
|
|
88
|
+
// Return cleanup function
|
|
89
|
+
return () => {
|
|
90
|
+
process.stdin.removeListener('keypress', keypressHandler);
|
|
91
|
+
if (process.stdin.isTTY) {
|
|
92
|
+
process.stdin.setRawMode(false);
|
|
93
|
+
}
|
|
94
|
+
rl.close();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Main event loop that waits for session to complete
|
|
100
|
+
* @param {Object} sessionState - Recording session state
|
|
101
|
+
*/
|
|
102
|
+
async function runEventLoop(sessionState) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
sessionState.onChange = () => {
|
|
105
|
+
if (sessionState.quit) {
|
|
106
|
+
resolve();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Keep process alive
|
|
111
|
+
const interval = setInterval(() => {
|
|
112
|
+
if (sessionState.quit) {
|
|
113
|
+
clearInterval(interval);
|
|
114
|
+
}
|
|
115
|
+
}, 100);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
setupTerminalHotkeys,
|
|
121
|
+
runEventLoop
|
|
122
|
+
};
|
|
123
|
+
|