@pep/term-deck 1.0.14 → 1.0.16

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 (37) hide show
  1. package/dist/bin/term-deck.d.ts +1 -0
  2. package/dist/bin/term-deck.js +1916 -0
  3. package/dist/bin/term-deck.js.map +1 -0
  4. package/dist/index.d.ts +670 -0
  5. package/dist/index.js +159 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +16 -13
  8. package/bin/term-deck.js +0 -14
  9. package/bin/term-deck.ts +0 -45
  10. package/src/cli/__tests__/errors.test.ts +0 -201
  11. package/src/cli/__tests__/help.test.ts +0 -157
  12. package/src/cli/__tests__/init.test.ts +0 -110
  13. package/src/cli/commands/export.ts +0 -33
  14. package/src/cli/commands/init.ts +0 -125
  15. package/src/cli/commands/present.ts +0 -29
  16. package/src/cli/errors.ts +0 -77
  17. package/src/core/__tests__/slide.test.ts +0 -1759
  18. package/src/core/__tests__/theme.test.ts +0 -1103
  19. package/src/core/slide.ts +0 -509
  20. package/src/core/theme.ts +0 -388
  21. package/src/export/__tests__/recorder.test.ts +0 -566
  22. package/src/export/recorder.ts +0 -639
  23. package/src/index.ts +0 -36
  24. package/src/presenter/__tests__/main.test.ts +0 -244
  25. package/src/presenter/main.ts +0 -658
  26. package/src/renderer/__tests__/screen-extended.test.ts +0 -801
  27. package/src/renderer/__tests__/screen.test.ts +0 -525
  28. package/src/renderer/screen.ts +0 -671
  29. package/src/schemas/__tests__/config.test.ts +0 -429
  30. package/src/schemas/__tests__/slide.test.ts +0 -349
  31. package/src/schemas/__tests__/theme.test.ts +0 -970
  32. package/src/schemas/__tests__/validation.test.ts +0 -256
  33. package/src/schemas/config.ts +0 -58
  34. package/src/schemas/slide.ts +0 -56
  35. package/src/schemas/theme.ts +0 -203
  36. package/src/schemas/validation.ts +0 -64
  37. package/src/themes/matrix/index.ts +0 -53
@@ -1,566 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'
2
- import { VirtualTerminal, captureScreen, createRecordingSession, saveFrame, cleanupSession, checkFfmpeg, detectFormat, exportPresentation, ansi256ToHex } from '../recorder'
3
- import type { RecordingSession } from '../recorder'
4
- import { readdir, stat, rm } from 'fs/promises'
5
-
6
- describe('VirtualTerminal', () => {
7
- it('creates buffer with correct dimensions', () => {
8
- const vt = new VirtualTerminal(80, 24)
9
- expect(vt.width).toBe(80)
10
- expect(vt.height).toBe(24)
11
- })
12
-
13
- it('sets characters at valid positions', () => {
14
- const vt = new VirtualTerminal(10, 5)
15
- vt.setChar(5, 2, 'X', '#ff0000')
16
-
17
- const str = vt.toString()
18
- const lines = str.split('\n')
19
- expect(lines[2][5]).toBe('X')
20
- })
21
-
22
- it('ignores out-of-bounds positions', () => {
23
- const vt = new VirtualTerminal(10, 5)
24
- vt.setChar(-1, 0, 'X') // Should not crash
25
- vt.setChar(0, -1, 'X') // Should not crash
26
- vt.setChar(100, 0, 'X') // Should not crash
27
- vt.setChar(0, 100, 'X') // Should not crash
28
- })
29
-
30
- it('clears the buffer', () => {
31
- const vt = new VirtualTerminal(10, 5)
32
- vt.setChar(5, 2, 'X', '#ff0000')
33
- vt.clear()
34
-
35
- const str = vt.toString()
36
- expect(str).toBe(' \n \n \n \n ')
37
- })
38
- })
39
-
40
- describe('captureScreen', () => {
41
- it('captures screen content from blessed', () => {
42
- const vt = new VirtualTerminal(10, 5)
43
-
44
- // Mock blessed screen with lines
45
- const mockScreen = {
46
- lines: [
47
- [
48
- ['H', { fg: 15 }], // White 'H'
49
- ['e', { fg: 10 }], // Green 'e'
50
- ['l', { fg: 10 }],
51
- ['l', { fg: 10 }],
52
- ['o', { fg: 10 }],
53
- ],
54
- [
55
- ['W', { fg: 15 }],
56
- ['o', { fg: 9 }], // Red 'o'
57
- ['r', { fg: 9 }],
58
- ['l', { fg: 9 }],
59
- ['d', { fg: 9 }],
60
- ],
61
- ],
62
- }
63
-
64
- captureScreen(mockScreen, vt)
65
-
66
- const str = vt.toString()
67
- const lines = str.split('\n')
68
-
69
- // Check first line contains "Hello"
70
- expect(lines[0].substring(0, 5)).toBe('Hello')
71
-
72
- // Check second line contains "World"
73
- expect(lines[1].substring(0, 5)).toBe('World')
74
- })
75
-
76
- it('handles empty screen', () => {
77
- const vt = new VirtualTerminal(10, 5)
78
- const mockScreen = {
79
- lines: [],
80
- }
81
-
82
- captureScreen(mockScreen, vt)
83
-
84
- const str = vt.toString()
85
- expect(str).toBe(' \n \n \n \n ')
86
- })
87
-
88
- it('handles missing lines', () => {
89
- const vt = new VirtualTerminal(10, 5)
90
- const mockScreen = {} // No lines property
91
-
92
- captureScreen(mockScreen, vt)
93
-
94
- const str = vt.toString()
95
- expect(str).toBe(' \n \n \n \n ')
96
- })
97
-
98
- it('extracts colors from cells with fg attribute', () => {
99
- const vt = new VirtualTerminal(10, 5)
100
-
101
- // Mock screen with different color types
102
- const mockScreen = {
103
- lines: [
104
- [
105
- ['R', { fg: 9 }], // ANSI red
106
- ['G', { fg: '#00ff00' }], // Hex green
107
- ['B', { fg: 12 }], // ANSI blue
108
- ],
109
- ],
110
- }
111
-
112
- captureScreen(mockScreen, vt)
113
-
114
- // Verify it doesn't crash and captures the characters
115
- const str = vt.toString()
116
- expect(str.startsWith('RGB')).toBe(true)
117
- })
118
-
119
- it('handles cells without attributes', () => {
120
- const vt = new VirtualTerminal(10, 5)
121
-
122
- // Mock screen with plain characters (no attributes)
123
- const mockScreen = {
124
- lines: [
125
- [
126
- 'A', // Plain char without attr
127
- 'B',
128
- 'C',
129
- ],
130
- ],
131
- }
132
-
133
- captureScreen(mockScreen, vt)
134
-
135
- const str = vt.toString()
136
- expect(str.startsWith('ABC')).toBe(true)
137
- })
138
-
139
- it('respects virtual terminal dimensions', () => {
140
- const vt = new VirtualTerminal(3, 2) // Small virtual terminal
141
-
142
- // Mock screen with more content than vt can hold
143
- const mockScreen = {
144
- lines: [
145
- [['A', {}], ['B', {}], ['C', {}], ['D', {}], ['E', {}]],
146
- [['F', {}], ['G', {}], ['H', {}], ['I', {}], ['J', {}]],
147
- [['K', {}], ['L', {}], ['M', {}], ['N', {}], ['O', {}]],
148
- ],
149
- }
150
-
151
- captureScreen(mockScreen, vt)
152
-
153
- const str = vt.toString()
154
- const lines = str.split('\n')
155
-
156
- // Should only capture 3x2 area
157
- expect(lines.length).toBe(2)
158
- expect(lines[0]).toBe('ABC')
159
- expect(lines[1]).toBe('FGH')
160
- })
161
- })
162
-
163
- describe('Recording Session', () => {
164
- let session: RecordingSession
165
-
166
- afterEach(async () => {
167
- if (session) {
168
- await cleanupSession(session)
169
- }
170
- })
171
-
172
- it('creates temp directory', async () => {
173
- session = await createRecordingSession({
174
- output: 'test.mp4',
175
- width: 80,
176
- height: 24,
177
- fps: 10,
178
- })
179
-
180
- // Verify temp directory was created
181
- const statResult = await stat(session.tempDir)
182
- expect(statResult.isDirectory()).toBe(true)
183
-
184
- // Verify session has correct defaults
185
- expect(session.frameCount).toBe(0)
186
- expect(session.width).toBe(80)
187
- expect(session.height).toBe(24)
188
- expect(session.fps).toBe(10)
189
- })
190
-
191
- it('uses default dimensions when not provided', async () => {
192
- session = await createRecordingSession({
193
- output: 'test.mp4',
194
- })
195
-
196
- expect(session.width).toBe(120)
197
- expect(session.height).toBe(40)
198
- expect(session.fps).toBe(30)
199
- })
200
-
201
- it('saves frames with padded numbering', async () => {
202
- session = await createRecordingSession({
203
- output: 'test.mp4',
204
- width: 80,
205
- height: 24,
206
- fps: 10,
207
- })
208
-
209
- // Create dummy PNG data (PNG magic number)
210
- const dummyPng = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
211
-
212
- await saveFrame(session, dummyPng)
213
- await saveFrame(session, dummyPng)
214
- await saveFrame(session, dummyPng)
215
-
216
- // Check that files were created with correct names
217
- const files = await readdir(session.tempDir)
218
- expect(files).toContain('frame_000000.png')
219
- expect(files).toContain('frame_000001.png')
220
- expect(files).toContain('frame_000002.png')
221
- expect(session.frameCount).toBe(3)
222
- })
223
-
224
- it('cleanup removes directory', async () => {
225
- session = await createRecordingSession({
226
- output: 'test.mp4',
227
- width: 80,
228
- height: 24,
229
- fps: 10,
230
- })
231
-
232
- const tempDir = session.tempDir
233
-
234
- // Verify directory exists
235
- const statBefore = await stat(tempDir)
236
- expect(statBefore.isDirectory()).toBe(true)
237
-
238
- // Cleanup
239
- await cleanupSession(session)
240
-
241
- // Verify directory no longer exists
242
- try {
243
- await stat(tempDir)
244
- expect(true).toBe(false) // Should not reach here
245
- } catch (error) {
246
- expect((error as any).code).toBe('ENOENT')
247
- }
248
- })
249
-
250
- it('frame count increments correctly', async () => {
251
- session = await createRecordingSession({
252
- output: 'test.mp4',
253
- })
254
-
255
- expect(session.frameCount).toBe(0)
256
-
257
- const dummyPng = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
258
-
259
- await saveFrame(session, dummyPng)
260
- expect(session.frameCount).toBe(1)
261
-
262
- await saveFrame(session, dummyPng)
263
- expect(session.frameCount).toBe(2)
264
-
265
- await saveFrame(session, dummyPng)
266
- expect(session.frameCount).toBe(3)
267
- })
268
- })
269
-
270
- describe('checkFfmpeg', () => {
271
- it('succeeds when ffmpeg is available', async () => {
272
- // This test will pass if ffmpeg is installed on the system
273
- // If ffmpeg is not installed, this test will fail
274
- // The checkFfmpeg function should not throw if ffmpeg exists
275
- try {
276
- await checkFfmpeg()
277
- // If we reach here, ffmpeg is installed
278
- expect(true).toBe(true)
279
- } catch (error) {
280
- // If we catch an error, ffmpeg is not installed
281
- // Check that the error message contains installation instructions
282
- expect((error as Error).message).toContain('ffmpeg not found')
283
- expect((error as Error).message).toContain('brew install ffmpeg')
284
- expect((error as Error).message).toContain('sudo apt install ffmpeg')
285
- }
286
- })
287
-
288
- it('throws error with install instructions when ffmpeg is missing', async () => {
289
- // This test verifies the error message format
290
- // We can't easily mock the `which` command, so we'll test the error format indirectly
291
- // by checking that the error message contains the expected text
292
- try {
293
- await checkFfmpeg()
294
- // If ffmpeg is installed, we can't test the error case
295
- // This is acceptable - the test above covers the success case
296
- } catch (error) {
297
- // Verify error message format
298
- expect(error).toBeInstanceOf(Error)
299
- const errorMessage = (error as Error).message
300
- expect(errorMessage).toContain('ffmpeg not found')
301
- expect(errorMessage).toContain('Install it with:')
302
- expect(errorMessage).toContain('macOS: brew install ffmpeg')
303
- expect(errorMessage).toContain('Ubuntu: sudo apt install ffmpeg')
304
- }
305
- })
306
- })
307
-
308
- describe('detectFormat', () => {
309
- it('detects mp4 format', () => {
310
- expect(detectFormat('output.mp4')).toBe('mp4')
311
- expect(detectFormat('presentation.mp4')).toBe('mp4')
312
- expect(detectFormat('/path/to/video.mp4')).toBe('mp4')
313
- })
314
-
315
- it('detects gif format', () => {
316
- expect(detectFormat('output.gif')).toBe('gif')
317
- expect(detectFormat('presentation.gif')).toBe('gif')
318
- expect(detectFormat('/path/to/animation.gif')).toBe('gif')
319
- })
320
-
321
- it('throws error for unknown format', () => {
322
- expect(() => detectFormat('output.avi')).toThrow('Unknown output format for output.avi. Use .mp4 or .gif extension.')
323
- expect(() => detectFormat('output.mov')).toThrow('Unknown output format for output.mov. Use .mp4 or .gif extension.')
324
- expect(() => detectFormat('output.webm')).toThrow('Unknown output format for output.webm. Use .mp4 or .gif extension.')
325
- })
326
-
327
- it('throws error for missing extension', () => {
328
- expect(() => detectFormat('output')).toThrow('Unknown output format for output. Use .mp4 or .gif extension.')
329
- })
330
-
331
- it('handles case-sensitive extensions', () => {
332
- // Extensions should be case-sensitive (lowercase only)
333
- expect(() => detectFormat('output.MP4')).toThrow()
334
- expect(() => detectFormat('output.GIF')).toThrow()
335
- })
336
- })
337
-
338
- describe('exportPresentation', () => {
339
- it('validates that slides directory exists', async () => {
340
- const options = {
341
- output: 'test.mp4',
342
- width: 80,
343
- height: 24,
344
- fps: 10,
345
- slideTime: 1,
346
- }
347
-
348
- // Test with non-existent directory
349
- try {
350
- await exportPresentation('/non/existent/path', options)
351
- expect(true).toBe(false) // Should not reach here
352
- } catch (error) {
353
- // Should throw an error about missing directory or no slides
354
- expect(error).toBeDefined()
355
- }
356
- })
357
-
358
- it('throws error when no slides found', async () => {
359
- // Create a temp directory with no slides
360
- const { tmpdir } = await import('os')
361
- const { join } = await import('path')
362
- const { mkdir } = await import('fs/promises')
363
-
364
- const testDir = join(tmpdir(), `test-export-${Date.now()}`)
365
- await mkdir(testDir, { recursive: true })
366
-
367
- try {
368
- const options = {
369
- output: 'test.mp4',
370
- width: 80,
371
- height: 24,
372
- fps: 10,
373
- slideTime: 1,
374
- }
375
-
376
- await exportPresentation(testDir, options)
377
- expect(true).toBe(false) // Should not reach here
378
- } catch (error) {
379
- expect((error as Error).message).toContain('No slides found')
380
- } finally {
381
- // Cleanup
382
- await rm(testDir, { recursive: true, force: true })
383
- }
384
- })
385
-
386
- it('detects format from output filename', async () => {
387
- const optionsMp4 = {
388
- output: 'presentation.mp4',
389
- width: 80,
390
- height: 24,
391
- }
392
-
393
- const optionsGif = {
394
- output: 'presentation.gif',
395
- width: 80,
396
- height: 24,
397
- }
398
-
399
- // Verify detectFormat is called correctly (indirectly tested by not throwing on valid extensions)
400
- expect(detectFormat(optionsMp4.output)).toBe('mp4')
401
- expect(detectFormat(optionsGif.output)).toBe('gif')
402
- })
403
-
404
- it('throws error for invalid output format', async () => {
405
- const options = {
406
- output: 'presentation.avi',
407
- width: 80,
408
- height: 24,
409
- }
410
-
411
- try {
412
- await exportPresentation('/any/path', options)
413
- expect(true).toBe(false) // Should not reach here
414
- } catch (error) {
415
- expect((error as Error).message).toContain('Unknown output format')
416
- }
417
- })
418
- })
419
-
420
- describe('ansi256ToHex', () => {
421
- describe('Standard 16 colors', () => {
422
- it('converts black (0)', () => {
423
- expect(ansi256ToHex(0)).toBe('#000000')
424
- })
425
-
426
- it('converts maroon (1)', () => {
427
- expect(ansi256ToHex(1)).toBe('#800000')
428
- })
429
-
430
- it('converts green (2)', () => {
431
- expect(ansi256ToHex(2)).toBe('#008000')
432
- })
433
-
434
- it('converts olive (3)', () => {
435
- expect(ansi256ToHex(3)).toBe('#808000')
436
- })
437
-
438
- it('converts navy (4)', () => {
439
- expect(ansi256ToHex(4)).toBe('#000080')
440
- })
441
-
442
- it('converts purple (5)', () => {
443
- expect(ansi256ToHex(5)).toBe('#800080')
444
- })
445
-
446
- it('converts teal (6)', () => {
447
- expect(ansi256ToHex(6)).toBe('#008080')
448
- })
449
-
450
- it('converts silver (7)', () => {
451
- expect(ansi256ToHex(7)).toBe('#c0c0c0')
452
- })
453
-
454
- it('converts gray (8)', () => {
455
- expect(ansi256ToHex(8)).toBe('#808080')
456
- })
457
-
458
- it('converts red (9)', () => {
459
- expect(ansi256ToHex(9)).toBe('#ff0000')
460
- })
461
-
462
- it('converts lime (10)', () => {
463
- expect(ansi256ToHex(10)).toBe('#00ff00')
464
- })
465
-
466
- it('converts yellow (11)', () => {
467
- expect(ansi256ToHex(11)).toBe('#ffff00')
468
- })
469
-
470
- it('converts blue (12)', () => {
471
- expect(ansi256ToHex(12)).toBe('#0000ff')
472
- })
473
-
474
- it('converts fuchsia (13)', () => {
475
- expect(ansi256ToHex(13)).toBe('#ff00ff')
476
- })
477
-
478
- it('converts aqua (14)', () => {
479
- expect(ansi256ToHex(14)).toBe('#00ffff')
480
- })
481
-
482
- it('converts white (15)', () => {
483
- expect(ansi256ToHex(15)).toBe('#ffffff')
484
- })
485
- })
486
-
487
- describe('216 color cube (16-231)', () => {
488
- it('converts first color in cube (16)', () => {
489
- expect(ansi256ToHex(16)).toBe('#000000')
490
- })
491
-
492
- it('converts a mid-range color in cube (100)', () => {
493
- // Code 100: n = 84
494
- // r = floor(84/36) * 51 = 2 * 51 = 102 = 0x66
495
- // g = floor((84 % 36)/6) * 51 = floor(12/6) * 51 = 2 * 51 = 102 = 0x66
496
- // b = (84 % 6) * 51 = 0 * 51 = 0 = 0x00
497
- expect(ansi256ToHex(100)).toBe('#666600')
498
- })
499
-
500
- it('converts another color in cube (196)', () => {
501
- // Code 196: n = 180
502
- // r = floor(180/36) * 51 = 5 * 51 = 255 = 0xff
503
- // g = floor(0/6) * 51 = 0 * 51 = 0 = 0x00
504
- // b = 0 * 51 = 0 = 0x00
505
- expect(ansi256ToHex(196)).toBe('#ff0000')
506
- })
507
-
508
- it('converts last color in cube (231)', () => {
509
- // Code 231: n = 215
510
- // r = floor(215/36) * 51 = 5 * 51 = 255 = 0xff
511
- // g = floor(35/6) * 51 = 5 * 51 = 255 = 0xff
512
- // b = 5 * 51 = 255 = 0xff
513
- expect(ansi256ToHex(231)).toBe('#ffffff')
514
- })
515
-
516
- it('converts green-ish color (34)', () => {
517
- // Code 34: n = 18
518
- // r = floor(18/36) * 51 = 0 * 51 = 0 = 0x00
519
- // g = floor(18/6) * 51 = 3 * 51 = 153 = 0x99
520
- // b = 0 * 51 = 0 = 0x00
521
- expect(ansi256ToHex(34)).toBe('#009900')
522
- })
523
-
524
- it('converts blue-ish color (21)', () => {
525
- // Code 21: n = 5
526
- // r = floor(5/36) * 51 = 0 * 51 = 0 = 0x00
527
- // g = floor(5/6) * 51 = 0 * 51 = 0 = 0x00
528
- // b = 5 * 51 = 255 = 0xff
529
- expect(ansi256ToHex(21)).toBe('#0000ff')
530
- })
531
- })
532
-
533
- describe('Grayscale (232-255)', () => {
534
- it('converts darkest gray (232)', () => {
535
- // gray = (232 - 232) * 10 + 8 = 8 = 0x08
536
- expect(ansi256ToHex(232)).toBe('#080808')
537
- })
538
-
539
- it('converts mid gray (244)', () => {
540
- // gray = (244 - 232) * 10 + 8 = 120 + 8 = 128 = 0x80
541
- expect(ansi256ToHex(244)).toBe('#808080')
542
- })
543
-
544
- it('converts light gray (250)', () => {
545
- // gray = (250 - 232) * 10 + 8 = 180 + 8 = 188 = 0xbc
546
- expect(ansi256ToHex(250)).toBe('#bcbcbc')
547
- })
548
-
549
- it('converts brightest gray (255)', () => {
550
- // gray = (255 - 232) * 10 + 8 = 230 + 8 = 238 = 0xee
551
- expect(ansi256ToHex(255)).toBe('#eeeeee')
552
- })
553
- })
554
-
555
- describe('Edge cases', () => {
556
- it('handles boundary between standard and cube (15 and 16)', () => {
557
- expect(ansi256ToHex(15)).toBe('#ffffff') // Last standard color
558
- expect(ansi256ToHex(16)).toBe('#000000') // First cube color
559
- })
560
-
561
- it('handles boundary between cube and grayscale (231 and 232)', () => {
562
- expect(ansi256ToHex(231)).toBe('#ffffff') // Last cube color
563
- expect(ansi256ToHex(232)).toBe('#080808') // First grayscale
564
- })
565
- })
566
- })