shellfie-cli 2.0.0 → 2.0.2
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/README.md +1 -1
- package/dist/animator.d.ts +8 -0
- package/dist/animator.d.ts.map +1 -0
- package/dist/animator.js +210 -0
- package/dist/animator.js.map +1 -0
- package/dist/cli.js +16 -1
- package/dist/cli.js.map +1 -1
- package/dist/dvd-executor-v2.d.ts +76 -0
- package/dist/dvd-executor-v2.d.ts.map +1 -0
- package/dist/dvd-executor-v2.js +258 -0
- package/dist/dvd-executor-v2.js.map +1 -0
- package/dist/dvd-executor.d.ts +144 -0
- package/dist/dvd-executor.d.ts.map +1 -0
- package/dist/dvd-executor.js +669 -0
- package/dist/dvd-executor.js.map +1 -0
- package/dist/dvd-parser.d.ts +96 -0
- package/dist/dvd-parser.d.ts.map +1 -0
- package/dist/dvd-parser.js +279 -0
- package/dist/dvd-parser.js.map +1 -0
- package/dist/dvd.d.ts +31 -0
- package/dist/dvd.d.ts.map +1 -0
- package/dist/dvd.js +154 -0
- package/dist/dvd.js.map +1 -0
- package/dist/frame-animator.d.ts +21 -0
- package/dist/frame-animator.d.ts.map +1 -0
- package/dist/frame-animator.js +254 -0
- package/dist/frame-animator.js.map +1 -0
- package/dist/frame-capture.d.ts +16 -0
- package/dist/frame-capture.d.ts.map +1 -0
- package/dist/frame-capture.js +162 -0
- package/dist/frame-capture.js.map +1 -0
- package/dist/svg-animator-v2.d.ts +23 -0
- package/dist/svg-animator-v2.d.ts.map +1 -0
- package/dist/svg-animator-v2.js +134 -0
- package/dist/svg-animator-v2.js.map +1 -0
- package/dist/svg-animator.d.ts +23 -0
- package/dist/svg-animator.d.ts.map +1 -0
- package/dist/svg-animator.js +134 -0
- package/dist/svg-animator.js.map +1 -0
- package/dist/terminal-renderer.d.ts +34 -0
- package/dist/terminal-renderer.d.ts.map +1 -0
- package/dist/terminal-renderer.js +229 -0
- package/dist/terminal-renderer.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DVD Command Executor
|
|
4
|
+
* Executes real commands with typing effect and cursor
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DVDExecutor = void 0;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const terminal_renderer_1 = require("./terminal-renderer");
|
|
12
|
+
const shellfie_1 = require("shellfie");
|
|
13
|
+
class DVDExecutor {
|
|
14
|
+
context;
|
|
15
|
+
options;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.context = {
|
|
19
|
+
lines: [''],
|
|
20
|
+
currentLine: '',
|
|
21
|
+
cursorX: 0,
|
|
22
|
+
cursorY: 0,
|
|
23
|
+
frames: [],
|
|
24
|
+
clipboard: '',
|
|
25
|
+
startTime: Date.now(),
|
|
26
|
+
width: options.width || 800,
|
|
27
|
+
height: options.height || 600,
|
|
28
|
+
fontSize: options.fontSize || 14,
|
|
29
|
+
typingSpeed: 50, // Default 50ms per character
|
|
30
|
+
title: options.title,
|
|
31
|
+
template: options.template || 'macos',
|
|
32
|
+
promptPrefix: '\x1b[95m❯\x1b[0m ', // Default: pink > character
|
|
33
|
+
cursorBlink: true, // Default: cursor blinks
|
|
34
|
+
screenshotCounter: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Capture current terminal state as a frame
|
|
39
|
+
*/
|
|
40
|
+
captureFrame(showCursor = true, activeCursor = false) {
|
|
41
|
+
const buffer = [...this.context.lines];
|
|
42
|
+
buffer[this.context.cursorY] = this.context.currentLine;
|
|
43
|
+
const state = (0, terminal_renderer_1.createTerminalState)(buffer.join('\n'), this.context.cursorX, this.context.cursorY, this.context.width, this.context.height, this.context.fontSize, showCursor, activeCursor, this.context.selectionStart, this.context.selectionEnd);
|
|
44
|
+
const svg = (0, terminal_renderer_1.renderTerminalSVG)(state, {
|
|
45
|
+
title: this.context.title,
|
|
46
|
+
template: this.context.template,
|
|
47
|
+
theme: this.context.theme,
|
|
48
|
+
watermark: this.context.watermark,
|
|
49
|
+
});
|
|
50
|
+
const frame = {
|
|
51
|
+
timestamp: Date.now() - this.context.startTime,
|
|
52
|
+
svg,
|
|
53
|
+
state,
|
|
54
|
+
};
|
|
55
|
+
this.context.frames.push(frame);
|
|
56
|
+
this.options.onFrame?.(frame);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Execute Type command - simulate typing character by character
|
|
60
|
+
*/
|
|
61
|
+
async executeType(text, speed, prefix) {
|
|
62
|
+
const delay = speed || this.context.typingSpeed;
|
|
63
|
+
const promptPrefix = prefix ?? this.context.promptPrefix;
|
|
64
|
+
// If there's a selection, delete it first
|
|
65
|
+
if (this.hasSelection()) {
|
|
66
|
+
this.deleteSelection();
|
|
67
|
+
this.captureFrame(true, true);
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
69
|
+
}
|
|
70
|
+
// Check if we need to add a prefix
|
|
71
|
+
// Add prefix if: line is empty OR line only contains the prefix already
|
|
72
|
+
const shouldAddPrefix = promptPrefix &&
|
|
73
|
+
(this.context.currentLine === '' || this.context.currentLine === promptPrefix);
|
|
74
|
+
if (shouldAddPrefix && this.context.currentLine === '') {
|
|
75
|
+
// Only add prefix if line is completely empty
|
|
76
|
+
this.context.currentLine = promptPrefix;
|
|
77
|
+
this.context.cursorX = this.stripAnsi(promptPrefix).length;
|
|
78
|
+
this.captureFrame(true, true); // active cursor during prefix
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
80
|
+
}
|
|
81
|
+
for (const char of text) {
|
|
82
|
+
this.context.currentLine += char;
|
|
83
|
+
this.context.cursorX++;
|
|
84
|
+
// Capture frame showing the new character with active cursor (no blink during typing)
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
86
|
+
this.captureFrame(true, true);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Strip ANSI escape codes to get actual string length
|
|
91
|
+
*/
|
|
92
|
+
stripAnsi(str) {
|
|
93
|
+
// eslint-disable-next-line no-control-regex
|
|
94
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Execute Enter - run the command and capture streaming output
|
|
98
|
+
*/
|
|
99
|
+
async executeEnter() {
|
|
100
|
+
const fullLine = this.context.currentLine;
|
|
101
|
+
// Strip the prompt prefix from the command before executing
|
|
102
|
+
// Check if the line starts with the prefix and remove it
|
|
103
|
+
let command = fullLine;
|
|
104
|
+
if (this.context.promptPrefix && fullLine.startsWith(this.context.promptPrefix)) {
|
|
105
|
+
command = fullLine.slice(this.context.promptPrefix.length);
|
|
106
|
+
}
|
|
107
|
+
command = command.trim();
|
|
108
|
+
// Finalize current line (the command that was typed - keep the visual prefix)
|
|
109
|
+
this.context.lines[this.context.cursorY] = this.context.currentLine;
|
|
110
|
+
// Move to next line
|
|
111
|
+
this.context.cursorY++;
|
|
112
|
+
this.context.cursorX = 0;
|
|
113
|
+
this.context.currentLine = '';
|
|
114
|
+
if (!this.context.lines[this.context.cursorY]) {
|
|
115
|
+
this.context.lines[this.context.cursorY] = '';
|
|
116
|
+
}
|
|
117
|
+
// Capture frame showing command was submitted (cursor on new line)
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
119
|
+
this.captureFrame(true);
|
|
120
|
+
// Execute the command if it's not empty
|
|
121
|
+
if (command) {
|
|
122
|
+
await this.executeCommandStreaming(command);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Execute command with streaming output support
|
|
127
|
+
*/
|
|
128
|
+
async executeCommandStreaming(command) {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const child = (0, node_child_process_1.spawn)(command, [], {
|
|
131
|
+
shell: true,
|
|
132
|
+
env: { ...process.env, FORCE_COLOR: '1', CLICOLOR_FORCE: '1' },
|
|
133
|
+
});
|
|
134
|
+
let outputBuffer = '';
|
|
135
|
+
let lastFrameTime = Date.now();
|
|
136
|
+
const FRAME_INTERVAL = 100; // Capture frame every 100ms when output is streaming
|
|
137
|
+
const processOutput = (data) => {
|
|
138
|
+
outputBuffer += data;
|
|
139
|
+
// Process complete lines
|
|
140
|
+
const lines = outputBuffer.split('\n');
|
|
141
|
+
outputBuffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
this.context.lines[this.context.cursorY] = line;
|
|
144
|
+
this.context.cursorY++;
|
|
145
|
+
this.context.lines[this.context.cursorY] = '';
|
|
146
|
+
// Capture frame if enough time has passed (for animations)
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (now - lastFrameTime >= FRAME_INTERVAL) {
|
|
149
|
+
this.captureFrame(true);
|
|
150
|
+
lastFrameTime = now;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
child.stdout?.on('data', (data) => {
|
|
155
|
+
processOutput(data.toString());
|
|
156
|
+
});
|
|
157
|
+
child.stderr?.on('data', (data) => {
|
|
158
|
+
processOutput(data.toString());
|
|
159
|
+
});
|
|
160
|
+
child.on('close', () => {
|
|
161
|
+
// Process any remaining buffered output
|
|
162
|
+
if (outputBuffer) {
|
|
163
|
+
this.context.lines[this.context.cursorY] = outputBuffer;
|
|
164
|
+
this.context.cursorY++;
|
|
165
|
+
this.context.lines[this.context.cursorY] = '';
|
|
166
|
+
}
|
|
167
|
+
// Add prompt prefix to the new line after command completes
|
|
168
|
+
this.context.currentLine = this.context.promptPrefix;
|
|
169
|
+
this.context.cursorX = this.stripAnsi(this.context.promptPrefix).length;
|
|
170
|
+
// Capture final frame with cursor on new line with prefix
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
this.captureFrame(true);
|
|
173
|
+
resolve();
|
|
174
|
+
}, 100);
|
|
175
|
+
});
|
|
176
|
+
child.on('error', (err) => {
|
|
177
|
+
this.context.lines[this.context.cursorY] = `Command failed: ${err.message}`;
|
|
178
|
+
this.context.cursorY++;
|
|
179
|
+
this.context.lines[this.context.cursorY] = '';
|
|
180
|
+
// Add prompt prefix to the new line after error
|
|
181
|
+
this.context.currentLine = this.context.promptPrefix;
|
|
182
|
+
this.context.cursorX = this.stripAnsi(this.context.promptPrefix).length;
|
|
183
|
+
this.captureFrame(true);
|
|
184
|
+
resolve();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Execute arrow keys
|
|
190
|
+
*/
|
|
191
|
+
async executeArrow(direction) {
|
|
192
|
+
switch (direction) {
|
|
193
|
+
case 'Left':
|
|
194
|
+
if (this.context.cursorX > 0)
|
|
195
|
+
this.context.cursorX--;
|
|
196
|
+
break;
|
|
197
|
+
case 'Right':
|
|
198
|
+
if (this.context.cursorX < this.context.currentLine.length)
|
|
199
|
+
this.context.cursorX++;
|
|
200
|
+
break;
|
|
201
|
+
case 'Up':
|
|
202
|
+
if (this.context.cursorY > 0) {
|
|
203
|
+
this.context.cursorY--;
|
|
204
|
+
this.context.currentLine = this.context.lines[this.context.cursorY];
|
|
205
|
+
this.context.cursorX = Math.min(this.context.cursorX, this.context.currentLine.length);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
case 'Down':
|
|
209
|
+
if (this.context.cursorY < this.context.lines.length - 1) {
|
|
210
|
+
this.context.cursorY++;
|
|
211
|
+
this.context.currentLine = this.context.lines[this.context.cursorY];
|
|
212
|
+
this.context.cursorX = Math.min(this.context.cursorX, this.context.currentLine.length);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
217
|
+
this.captureFrame(true, true); // active cursor during arrow key movement
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Execute Screenshot - save current terminal state as static SVG using shellfie
|
|
221
|
+
*/
|
|
222
|
+
async executeScreenshot(path) {
|
|
223
|
+
// Determine the screenshot path
|
|
224
|
+
let screenshotPath;
|
|
225
|
+
if (path) {
|
|
226
|
+
screenshotPath = path;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Auto-generate name based on Output path
|
|
230
|
+
const baseName = this.context.outputPath
|
|
231
|
+
? this.context.outputPath.replace(/\.svg$/, '')
|
|
232
|
+
: 'screenshot';
|
|
233
|
+
screenshotPath = `${baseName}_screenshot_${this.context.screenshotCounter}.svg`;
|
|
234
|
+
this.context.screenshotCounter++;
|
|
235
|
+
}
|
|
236
|
+
// Get current terminal content
|
|
237
|
+
const buffer = [...this.context.lines];
|
|
238
|
+
buffer[this.context.cursorY] = this.context.currentLine;
|
|
239
|
+
const content = buffer.join('\n');
|
|
240
|
+
// Use shellfie to generate static SVG with exact dimensions to match animated frames
|
|
241
|
+
// Create a custom template with shadow disabled to match terminal-renderer
|
|
242
|
+
let templateOption = this.context.template;
|
|
243
|
+
if (typeof this.context.template === 'string') {
|
|
244
|
+
// For built-in templates, create a custom version with shadow disabled
|
|
245
|
+
const { templates } = await import('shellfie');
|
|
246
|
+
const baseTemplate = templates[this.context.template];
|
|
247
|
+
if (baseTemplate) {
|
|
248
|
+
templateOption = {
|
|
249
|
+
...baseTemplate,
|
|
250
|
+
shell: {
|
|
251
|
+
...baseTemplate.shell,
|
|
252
|
+
shadow: false, // Disable shadow to match terminal-renderer
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const svg = (0, shellfie_1.shellfie)(content, {
|
|
258
|
+
width: this.context.width,
|
|
259
|
+
height: this.context.height,
|
|
260
|
+
fontSize: this.context.fontSize,
|
|
261
|
+
title: this.context.title,
|
|
262
|
+
template: templateOption,
|
|
263
|
+
theme: this.context.theme,
|
|
264
|
+
watermark: this.context.watermark,
|
|
265
|
+
// Enable title bar border to match terminal-renderer
|
|
266
|
+
header: {
|
|
267
|
+
border: true,
|
|
268
|
+
borderColor: '#d4d4d41a',
|
|
269
|
+
borderWidth: 1,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
// Write to file
|
|
273
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(screenshotPath), svg, 'utf-8');
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Execute Backspace - delete characters with animation
|
|
277
|
+
*/
|
|
278
|
+
async executeBackspace(count = 1) {
|
|
279
|
+
const delay = this.context.typingSpeed;
|
|
280
|
+
// If there's a selection, delete it instead of normal backspace
|
|
281
|
+
if (this.hasSelection()) {
|
|
282
|
+
this.deleteSelection();
|
|
283
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
284
|
+
this.captureFrame(true, true);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
for (let i = 0; i < count; i++) {
|
|
288
|
+
if (this.context.currentLine.length > 0) {
|
|
289
|
+
// Always delete from the end of the line (like a real terminal)
|
|
290
|
+
this.context.currentLine = this.context.currentLine.slice(0, -1);
|
|
291
|
+
this.context.cursorX--;
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
293
|
+
this.captureFrame(true, true); // active cursor during backspace
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Execute keyboard shortcut with modifiers
|
|
299
|
+
*/
|
|
300
|
+
async executeShortcut(ctrl, alt, shift, cmd, key) {
|
|
301
|
+
// Normalize Cmd to Ctrl for cross-platform compatibility
|
|
302
|
+
const metaKey = cmd || ctrl;
|
|
303
|
+
// Handle different shortcut combinations
|
|
304
|
+
if (shift && !alt && !metaKey) {
|
|
305
|
+
// Shift + Arrow keys = Selection
|
|
306
|
+
if (key === 'Left' || key === 'Right') {
|
|
307
|
+
await this.executeSelectionMove(key === 'Right', shift);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (alt && shift && !metaKey) {
|
|
311
|
+
// Alt + Shift + Arrow = Word selection
|
|
312
|
+
if (key === 'Left' || key === 'Right') {
|
|
313
|
+
await this.executeWordSelection(key === 'Right');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (alt && !shift && !metaKey) {
|
|
317
|
+
// Alt + Arrow = Word movement
|
|
318
|
+
if (key === 'Left' || key === 'Right') {
|
|
319
|
+
await this.executeWordMove(key === 'Right');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (metaKey && !alt && !shift) {
|
|
323
|
+
// Cmd/Ctrl + Arrow = Line navigation
|
|
324
|
+
if (key === 'Left' || key === 'Right') {
|
|
325
|
+
await this.executeLineNavigation(key === 'Right');
|
|
326
|
+
}
|
|
327
|
+
else if (key === 'Backspace') {
|
|
328
|
+
await this.executeWordDelete();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Execute selection movement (Shift + Left/Right)
|
|
334
|
+
*/
|
|
335
|
+
async executeSelectionMove(right, shift) {
|
|
336
|
+
const strippedLine = this.stripAnsi(this.context.currentLine);
|
|
337
|
+
// Initialize selection anchor if not already set
|
|
338
|
+
if (!shift || (this.context.selectionStart === undefined && this.context.selectionEnd === undefined)) {
|
|
339
|
+
this.context.selectionStart = this.context.cursorX;
|
|
340
|
+
this.context.selectionEnd = this.context.cursorX;
|
|
341
|
+
}
|
|
342
|
+
// Move cursor
|
|
343
|
+
if (right) {
|
|
344
|
+
if (this.context.cursorX < strippedLine.length) {
|
|
345
|
+
this.context.cursorX++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
if (this.context.cursorX > 0) {
|
|
350
|
+
this.context.cursorX--;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Update selection end
|
|
354
|
+
if (shift) {
|
|
355
|
+
this.context.selectionEnd = this.context.cursorX;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
this.clearSelection();
|
|
359
|
+
}
|
|
360
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
361
|
+
this.captureFrame(true, true);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Execute word movement (Alt + Left/Right)
|
|
365
|
+
*/
|
|
366
|
+
async executeWordMove(right) {
|
|
367
|
+
const strippedLine = this.stripAnsi(this.context.currentLine);
|
|
368
|
+
this.clearSelection(); // Clear any selection
|
|
369
|
+
const direction = right ? 'right' : 'left';
|
|
370
|
+
const newPosition = this.findWordBoundary(direction, this.context.cursorX, this.context.currentLine);
|
|
371
|
+
this.context.cursorX = newPosition;
|
|
372
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
373
|
+
this.captureFrame(true, true);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Execute word selection (Alt + Shift + Left/Right)
|
|
377
|
+
*/
|
|
378
|
+
async executeWordSelection(right) {
|
|
379
|
+
// Initialize selection if not set
|
|
380
|
+
if (this.context.selectionStart === undefined) {
|
|
381
|
+
this.context.selectionStart = this.context.cursorX;
|
|
382
|
+
this.context.selectionEnd = this.context.cursorX;
|
|
383
|
+
}
|
|
384
|
+
const direction = right ? 'right' : 'left';
|
|
385
|
+
const newPosition = this.findWordBoundary(direction, this.context.cursorX, this.context.currentLine);
|
|
386
|
+
this.context.cursorX = newPosition;
|
|
387
|
+
this.context.selectionEnd = newPosition;
|
|
388
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
389
|
+
this.captureFrame(true, true);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Execute line navigation (Cmd/Ctrl + Left/Right)
|
|
393
|
+
*/
|
|
394
|
+
async executeLineNavigation(toEnd) {
|
|
395
|
+
const strippedLine = this.stripAnsi(this.context.currentLine);
|
|
396
|
+
this.clearSelection(); // Clear any selection
|
|
397
|
+
if (toEnd) {
|
|
398
|
+
// Move to end of line
|
|
399
|
+
this.context.cursorX = strippedLine.length;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// Move to beginning of line (after prompt if it exists)
|
|
403
|
+
const promptLength = this.context.promptPrefix ? this.stripAnsi(this.context.promptPrefix).length : 0;
|
|
404
|
+
this.context.cursorX = promptLength;
|
|
405
|
+
}
|
|
406
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
407
|
+
this.captureFrame(true, true);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Execute word deletion (Cmd/Ctrl + Backspace)
|
|
411
|
+
*/
|
|
412
|
+
async executeWordDelete() {
|
|
413
|
+
const delay = this.context.typingSpeed;
|
|
414
|
+
const strippedLine = this.stripAnsi(this.context.currentLine);
|
|
415
|
+
// Find word boundary to the left
|
|
416
|
+
const wordStart = this.findWordBoundary('left', this.context.cursorX, this.context.currentLine);
|
|
417
|
+
// Calculate how many characters to delete
|
|
418
|
+
const deleteCount = this.context.cursorX - wordStart;
|
|
419
|
+
if (deleteCount <= 0)
|
|
420
|
+
return;
|
|
421
|
+
// Animate deletion character by character
|
|
422
|
+
for (let i = 0; i < deleteCount; i++) {
|
|
423
|
+
if (this.context.currentLine.length > 0) {
|
|
424
|
+
this.context.currentLine = this.context.currentLine.slice(0, -1);
|
|
425
|
+
this.context.cursorX--;
|
|
426
|
+
await new Promise((resolve) => setTimeout(resolve, delay / 2)); // Faster than regular backspace
|
|
427
|
+
this.captureFrame(true, true);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Check if there's an active selection
|
|
433
|
+
*/
|
|
434
|
+
hasSelection() {
|
|
435
|
+
return (this.context.selectionStart !== undefined &&
|
|
436
|
+
this.context.selectionEnd !== undefined &&
|
|
437
|
+
this.context.selectionStart !== this.context.selectionEnd);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get selected text
|
|
441
|
+
*/
|
|
442
|
+
getSelectedText() {
|
|
443
|
+
if (!this.hasSelection())
|
|
444
|
+
return '';
|
|
445
|
+
const start = Math.min(this.context.selectionStart, this.context.selectionEnd);
|
|
446
|
+
const end = Math.max(this.context.selectionStart, this.context.selectionEnd);
|
|
447
|
+
const strippedLine = this.stripAnsi(this.context.currentLine);
|
|
448
|
+
return strippedLine.substring(start, end);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Clear selection
|
|
452
|
+
*/
|
|
453
|
+
clearSelection() {
|
|
454
|
+
this.context.selectionStart = undefined;
|
|
455
|
+
this.context.selectionEnd = undefined;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Delete selected text and return true if selection was deleted
|
|
459
|
+
*/
|
|
460
|
+
deleteSelection() {
|
|
461
|
+
if (!this.hasSelection())
|
|
462
|
+
return false;
|
|
463
|
+
const start = Math.min(this.context.selectionStart, this.context.selectionEnd);
|
|
464
|
+
const end = Math.max(this.context.selectionStart, this.context.selectionEnd);
|
|
465
|
+
// Map visual positions to actual positions in currentLine (with ANSI codes)
|
|
466
|
+
const strippedLine = this.stripAnsi(this.context.currentLine);
|
|
467
|
+
const before = strippedLine.substring(0, start);
|
|
468
|
+
const after = strippedLine.substring(end);
|
|
469
|
+
// Rebuild line - this is simplified, should preserve ANSI codes properly
|
|
470
|
+
this.context.currentLine = before + after;
|
|
471
|
+
this.context.cursorX = start;
|
|
472
|
+
this.clearSelection();
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Find word boundary in the given direction
|
|
477
|
+
* Returns the position of the word boundary
|
|
478
|
+
*/
|
|
479
|
+
findWordBoundary(direction, position, text) {
|
|
480
|
+
const stripped = this.stripAnsi(text);
|
|
481
|
+
if (direction === 'left') {
|
|
482
|
+
// Move left to find word boundary
|
|
483
|
+
if (position === 0)
|
|
484
|
+
return 0;
|
|
485
|
+
let pos = position - 1;
|
|
486
|
+
// Skip whitespace
|
|
487
|
+
while (pos > 0 && /\s/.test(stripped[pos])) {
|
|
488
|
+
pos--;
|
|
489
|
+
}
|
|
490
|
+
// Skip word characters
|
|
491
|
+
if (/\w/.test(stripped[pos])) {
|
|
492
|
+
while (pos > 0 && /\w/.test(stripped[pos - 1])) {
|
|
493
|
+
pos--;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (/\S/.test(stripped[pos])) {
|
|
497
|
+
// Skip punctuation (non-whitespace, non-word)
|
|
498
|
+
while (pos > 0 && /[^\w\s]/.test(stripped[pos - 1])) {
|
|
499
|
+
pos--;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return pos;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
// Move right to find word boundary
|
|
506
|
+
if (position >= stripped.length)
|
|
507
|
+
return stripped.length;
|
|
508
|
+
let pos = position;
|
|
509
|
+
// Skip whitespace
|
|
510
|
+
while (pos < stripped.length && /\s/.test(stripped[pos])) {
|
|
511
|
+
pos++;
|
|
512
|
+
}
|
|
513
|
+
// Skip word characters
|
|
514
|
+
if (pos < stripped.length && /\w/.test(stripped[pos])) {
|
|
515
|
+
while (pos < stripped.length && /\w/.test(stripped[pos])) {
|
|
516
|
+
pos++;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else if (pos < stripped.length && /\S/.test(stripped[pos])) {
|
|
520
|
+
// Skip punctuation
|
|
521
|
+
while (pos < stripped.length && /[^\w\s]/.test(stripped[pos])) {
|
|
522
|
+
pos++;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return pos;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Execute a single command
|
|
530
|
+
*/
|
|
531
|
+
async executeCommand(command) {
|
|
532
|
+
switch (command.type) {
|
|
533
|
+
case 'Type':
|
|
534
|
+
await this.executeType(command.text, command.speed, command.prefix);
|
|
535
|
+
break;
|
|
536
|
+
case 'Key':
|
|
537
|
+
if (['Left', 'Right', 'Up', 'Down'].includes(command.key)) {
|
|
538
|
+
const count = command.count || 1;
|
|
539
|
+
for (let i = 0; i < count; i++) {
|
|
540
|
+
await this.executeArrow(command.key);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else if (command.key === 'Enter') {
|
|
544
|
+
await this.executeEnter();
|
|
545
|
+
}
|
|
546
|
+
else if (command.key === 'Backspace') {
|
|
547
|
+
await this.executeBackspace(command.count || 1);
|
|
548
|
+
}
|
|
549
|
+
else if (command.key === 'Space') {
|
|
550
|
+
const count = command.count || 1;
|
|
551
|
+
await this.executeType(' '.repeat(count));
|
|
552
|
+
}
|
|
553
|
+
else if (command.key === 'Tab') {
|
|
554
|
+
const count = command.count || 1;
|
|
555
|
+
await this.executeType(' '.repeat(count)); // 4 spaces per tab
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
case 'Sleep':
|
|
559
|
+
await new Promise((resolve) => setTimeout(resolve, command.duration));
|
|
560
|
+
this.captureFrame(true);
|
|
561
|
+
break;
|
|
562
|
+
case 'Screenshot':
|
|
563
|
+
await this.executeScreenshot(command.path);
|
|
564
|
+
break;
|
|
565
|
+
case 'Copy':
|
|
566
|
+
this.context.clipboard = command.text;
|
|
567
|
+
break;
|
|
568
|
+
case 'Paste':
|
|
569
|
+
if (this.context.clipboard) {
|
|
570
|
+
await this.executeType(this.context.clipboard);
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
case 'Shortcut':
|
|
574
|
+
await this.executeShortcut(command.ctrl, command.alt, command.shift, command.cmd, command.key);
|
|
575
|
+
break;
|
|
576
|
+
case 'Hide':
|
|
577
|
+
case 'Show':
|
|
578
|
+
case 'Output':
|
|
579
|
+
case 'Require':
|
|
580
|
+
case 'Set':
|
|
581
|
+
case 'Source':
|
|
582
|
+
case 'Env':
|
|
583
|
+
case 'Comment':
|
|
584
|
+
case 'Wait':
|
|
585
|
+
// Not implemented in simulation mode
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Execute complete DVD script
|
|
591
|
+
*/
|
|
592
|
+
async execute(script) {
|
|
593
|
+
// Apply settings
|
|
594
|
+
for (const [key, value] of script.settings.entries()) {
|
|
595
|
+
if (key === 'Width')
|
|
596
|
+
this.context.width = parseInt(value, 10);
|
|
597
|
+
if (key === 'Height')
|
|
598
|
+
this.context.height = parseInt(value, 10);
|
|
599
|
+
if (key === 'FontSize')
|
|
600
|
+
this.context.fontSize = parseInt(value, 10);
|
|
601
|
+
if (key === 'TypingSpeed')
|
|
602
|
+
this.context.typingSpeed = parseInt(value, 10);
|
|
603
|
+
if (key === 'Title')
|
|
604
|
+
this.context.title = value;
|
|
605
|
+
if (key === 'Template')
|
|
606
|
+
this.context.template = value;
|
|
607
|
+
if (key === 'Theme') {
|
|
608
|
+
// Look up theme from shellfie themes
|
|
609
|
+
const themeName = value;
|
|
610
|
+
if (shellfie_1.themes[themeName]) {
|
|
611
|
+
this.context.theme = shellfie_1.themes[themeName];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (key === 'PromptPrefix') {
|
|
615
|
+
// Parse the string to handle escape sequences
|
|
616
|
+
this.context.promptPrefix = value
|
|
617
|
+
.replace(/\\e/g, '\x1b')
|
|
618
|
+
.replace(/\\x1b/g, '\x1b')
|
|
619
|
+
.replace(/\\n/g, '\n')
|
|
620
|
+
.replace(/\\t/g, '\t');
|
|
621
|
+
}
|
|
622
|
+
if (key === 'Watermark') {
|
|
623
|
+
// Parse the string to handle escape sequences (same as PromptPrefix)
|
|
624
|
+
this.context.watermark = value
|
|
625
|
+
.replace(/\\e/g, '\x1b')
|
|
626
|
+
.replace(/\\x1b/g, '\x1b')
|
|
627
|
+
.replace(/\\n/g, '\n')
|
|
628
|
+
.replace(/\\t/g, '\t');
|
|
629
|
+
}
|
|
630
|
+
if (key === 'CursorBlink') {
|
|
631
|
+
this.context.cursorBlink = value.toLowerCase() !== 'false';
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Store output path for auto-naming screenshots
|
|
635
|
+
this.context.outputPath = script.output;
|
|
636
|
+
// Capture initial frame
|
|
637
|
+
this.captureFrame(true);
|
|
638
|
+
// Execute commands
|
|
639
|
+
const actionCommands = script.commands.filter((cmd) => !['Output', 'Require', 'Set', 'Env'].includes(cmd.type));
|
|
640
|
+
for (let i = 0; i < actionCommands.length; i++) {
|
|
641
|
+
const cmd = actionCommands[i];
|
|
642
|
+
// Create progress message with command type BEFORE executing
|
|
643
|
+
let cmdDescription = cmd.type;
|
|
644
|
+
if (cmd.type === 'Key') {
|
|
645
|
+
cmdDescription = cmd.key;
|
|
646
|
+
}
|
|
647
|
+
this.options.onProgress?.(i + 1, actionCommands.length, cmdDescription);
|
|
648
|
+
// Now execute the command
|
|
649
|
+
await this.executeCommand(cmd);
|
|
650
|
+
}
|
|
651
|
+
// Capture final frame without cursor
|
|
652
|
+
this.captureFrame(false);
|
|
653
|
+
return this.context.frames;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Get all captured frames
|
|
657
|
+
*/
|
|
658
|
+
getFrames() {
|
|
659
|
+
return this.context.frames;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Cleanup (no-op for simulation)
|
|
663
|
+
*/
|
|
664
|
+
async cleanup() {
|
|
665
|
+
// Nothing to clean up in simulation mode
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
exports.DVDExecutor = DVDExecutor;
|
|
669
|
+
//# sourceMappingURL=dvd-executor.js.map
|