shellfie-cli 2.0.1 → 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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/animator.d.ts +8 -0
  3. package/dist/animator.d.ts.map +1 -0
  4. package/dist/animator.js +210 -0
  5. package/dist/animator.js.map +1 -0
  6. package/dist/dvd-executor-v2.d.ts +76 -0
  7. package/dist/dvd-executor-v2.d.ts.map +1 -0
  8. package/dist/dvd-executor-v2.js +258 -0
  9. package/dist/dvd-executor-v2.js.map +1 -0
  10. package/dist/dvd-executor.d.ts +144 -0
  11. package/dist/dvd-executor.d.ts.map +1 -0
  12. package/dist/dvd-executor.js +669 -0
  13. package/dist/dvd-executor.js.map +1 -0
  14. package/dist/dvd-parser.d.ts +96 -0
  15. package/dist/dvd-parser.d.ts.map +1 -0
  16. package/dist/dvd-parser.js +279 -0
  17. package/dist/dvd-parser.js.map +1 -0
  18. package/dist/dvd.d.ts +31 -0
  19. package/dist/dvd.d.ts.map +1 -0
  20. package/dist/dvd.js +154 -0
  21. package/dist/dvd.js.map +1 -0
  22. package/dist/frame-animator.d.ts +21 -0
  23. package/dist/frame-animator.d.ts.map +1 -0
  24. package/dist/frame-animator.js +254 -0
  25. package/dist/frame-animator.js.map +1 -0
  26. package/dist/frame-capture.d.ts +16 -0
  27. package/dist/frame-capture.d.ts.map +1 -0
  28. package/dist/frame-capture.js +162 -0
  29. package/dist/frame-capture.js.map +1 -0
  30. package/dist/svg-animator-v2.d.ts +23 -0
  31. package/dist/svg-animator-v2.d.ts.map +1 -0
  32. package/dist/svg-animator-v2.js +134 -0
  33. package/dist/svg-animator-v2.js.map +1 -0
  34. package/dist/svg-animator.d.ts +23 -0
  35. package/dist/svg-animator.d.ts.map +1 -0
  36. package/dist/svg-animator.js +134 -0
  37. package/dist/svg-animator.js.map +1 -0
  38. package/dist/terminal-renderer.d.ts +34 -0
  39. package/dist/terminal-renderer.d.ts.map +1 -0
  40. package/dist/terminal-renderer.js +229 -0
  41. package/dist/terminal-renderer.js.map +1 -0
  42. package/package.json +2 -2
@@ -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