@pep/term-deck 1.0.10

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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -0
  3. package/bin/term-deck.ts +45 -0
  4. package/examples/slides/01-welcome.md +9 -0
  5. package/examples/slides/02-features.md +12 -0
  6. package/examples/slides/03-colors.md +17 -0
  7. package/examples/slides/04-ascii-art.md +11 -0
  8. package/examples/slides/05-gradients.md +14 -0
  9. package/examples/slides/06-themes.md +13 -0
  10. package/examples/slides/07-markdown.md +13 -0
  11. package/examples/slides/08-controls.md +13 -0
  12. package/examples/slides/09-thanks.md +11 -0
  13. package/examples/slides/deck.config.ts +13 -0
  14. package/examples/slides-hacker/01-welcome.md +9 -0
  15. package/examples/slides-hacker/02-features.md +12 -0
  16. package/examples/slides-hacker/03-colors.md +17 -0
  17. package/examples/slides-hacker/04-ascii-art.md +11 -0
  18. package/examples/slides-hacker/05-gradients.md +14 -0
  19. package/examples/slides-hacker/06-themes.md +13 -0
  20. package/examples/slides-hacker/07-markdown.md +13 -0
  21. package/examples/slides-hacker/08-controls.md +13 -0
  22. package/examples/slides-hacker/09-thanks.md +11 -0
  23. package/examples/slides-hacker/deck.config.ts +13 -0
  24. package/examples/slides-matrix/01-welcome.md +9 -0
  25. package/examples/slides-matrix/02-features.md +12 -0
  26. package/examples/slides-matrix/03-colors.md +17 -0
  27. package/examples/slides-matrix/04-ascii-art.md +11 -0
  28. package/examples/slides-matrix/05-gradients.md +14 -0
  29. package/examples/slides-matrix/06-themes.md +13 -0
  30. package/examples/slides-matrix/07-markdown.md +13 -0
  31. package/examples/slides-matrix/08-controls.md +13 -0
  32. package/examples/slides-matrix/09-thanks.md +11 -0
  33. package/examples/slides-matrix/deck.config.ts +13 -0
  34. package/examples/slides-minimal/01-welcome.md +9 -0
  35. package/examples/slides-minimal/02-features.md +12 -0
  36. package/examples/slides-minimal/03-colors.md +17 -0
  37. package/examples/slides-minimal/04-ascii-art.md +11 -0
  38. package/examples/slides-minimal/05-gradients.md +14 -0
  39. package/examples/slides-minimal/06-themes.md +13 -0
  40. package/examples/slides-minimal/07-markdown.md +13 -0
  41. package/examples/slides-minimal/08-controls.md +13 -0
  42. package/examples/slides-minimal/09-thanks.md +11 -0
  43. package/examples/slides-minimal/deck.config.ts +13 -0
  44. package/examples/slides-neon/01-welcome.md +9 -0
  45. package/examples/slides-neon/02-features.md +12 -0
  46. package/examples/slides-neon/03-colors.md +17 -0
  47. package/examples/slides-neon/04-ascii-art.md +11 -0
  48. package/examples/slides-neon/05-gradients.md +14 -0
  49. package/examples/slides-neon/06-themes.md +13 -0
  50. package/examples/slides-neon/07-markdown.md +13 -0
  51. package/examples/slides-neon/08-controls.md +13 -0
  52. package/examples/slides-neon/09-thanks.md +11 -0
  53. package/examples/slides-neon/deck.config.ts +13 -0
  54. package/examples/slides-retro/01-welcome.md +9 -0
  55. package/examples/slides-retro/02-features.md +12 -0
  56. package/examples/slides-retro/03-colors.md +17 -0
  57. package/examples/slides-retro/04-ascii-art.md +11 -0
  58. package/examples/slides-retro/05-gradients.md +14 -0
  59. package/examples/slides-retro/06-themes.md +13 -0
  60. package/examples/slides-retro/07-markdown.md +13 -0
  61. package/examples/slides-retro/08-controls.md +13 -0
  62. package/examples/slides-retro/09-thanks.md +11 -0
  63. package/examples/slides-retro/deck.config.ts +13 -0
  64. package/package.json +66 -0
  65. package/src/cli/__tests__/errors.test.ts +201 -0
  66. package/src/cli/__tests__/help.test.ts +157 -0
  67. package/src/cli/__tests__/init.test.ts +110 -0
  68. package/src/cli/commands/export.ts +33 -0
  69. package/src/cli/commands/init.ts +125 -0
  70. package/src/cli/commands/present.ts +29 -0
  71. package/src/cli/errors.ts +77 -0
  72. package/src/core/__tests__/slide.test.ts +1759 -0
  73. package/src/core/__tests__/theme.test.ts +1103 -0
  74. package/src/core/slide.ts +509 -0
  75. package/src/core/theme.ts +388 -0
  76. package/src/export/__tests__/recorder.test.ts +566 -0
  77. package/src/export/recorder.ts +639 -0
  78. package/src/index.ts +36 -0
  79. package/src/presenter/__tests__/main.test.ts +244 -0
  80. package/src/presenter/main.ts +658 -0
  81. package/src/renderer/__tests__/screen-extended.test.ts +801 -0
  82. package/src/renderer/__tests__/screen.test.ts +525 -0
  83. package/src/renderer/screen.ts +671 -0
  84. package/src/schemas/__tests__/config.test.ts +429 -0
  85. package/src/schemas/__tests__/slide.test.ts +349 -0
  86. package/src/schemas/__tests__/theme.test.ts +970 -0
  87. package/src/schemas/__tests__/validation.test.ts +256 -0
  88. package/src/schemas/config.ts +58 -0
  89. package/src/schemas/slide.ts +56 -0
  90. package/src/schemas/theme.ts +203 -0
  91. package/src/schemas/validation.ts +64 -0
  92. package/src/themes/matrix/index.ts +53 -0
  93. package/themes/hacker.ts +53 -0
  94. package/themes/minimal.ts +53 -0
  95. package/themes/neon.ts +53 -0
  96. package/themes/retro.ts +53 -0
@@ -0,0 +1,658 @@
1
+ import blessed from 'neo-blessed';
2
+ import type { Deck } from '../core/slide.js';
3
+ import type { Renderer } from '../renderer/screen.js';
4
+ import { loadDeck } from '../core/slide.js';
5
+ import { createRenderer, destroyRenderer, renderSlide, clearWindows } from '../renderer/screen.js';
6
+
7
+ /**
8
+ * Presenter state
9
+ *
10
+ * Manages the state of the active presentation including:
11
+ * - The loaded deck with slides and configuration
12
+ * - The renderer instance for displaying slides
13
+ * - Current slide index
14
+ * - Animation state to prevent concurrent navigation
15
+ * - Optional notes window for presenter mode
16
+ * - Optional auto-advance timer
17
+ * - Optional progress bar
18
+ */
19
+ export interface Presenter {
20
+ deck: Deck;
21
+ renderer: Renderer;
22
+ currentSlide: number;
23
+ isAnimating: boolean;
24
+ notesWindow: NotesWindow | null;
25
+ autoAdvanceTimer: NodeJS.Timer | null;
26
+ progressBar: blessed.Widgets.ProgressBarElement | null;
27
+ }
28
+
29
+ /**
30
+ * Notes window state (separate terminal)
31
+ *
32
+ * Represents a secondary display on a different TTY for presenter notes.
33
+ * Shows current slide notes, slide number, and preview of next slide.
34
+ */
35
+ export interface NotesWindow {
36
+ screen: blessed.Widgets.Screen;
37
+ contentBox: blessed.Widgets.BoxElement;
38
+ tty: string; // TTY device path (e.g., '/dev/tty2')
39
+ }
40
+
41
+ /**
42
+ * Presentation options
43
+ *
44
+ * Configuration options for starting a presentation:
45
+ * - startSlide: Index of slide to start from (defaults to 0)
46
+ * - showNotes: Whether to open a notes window on separate TTY
47
+ * - notesTty: Specific TTY device path for notes (optional, will auto-detect if not provided)
48
+ * - loop: Whether to loop back to first slide after reaching the end
49
+ */
50
+ export interface PresentOptions {
51
+ startSlide?: number;
52
+ showNotes?: boolean;
53
+ notesTty?: string;
54
+ loop?: boolean;
55
+ }
56
+
57
+ /**
58
+ * Start a presentation
59
+ *
60
+ * Loads the deck, creates the renderer, sets up keyboard controls,
61
+ * and enters the main presentation loop.
62
+ *
63
+ * @param slidesDir - Directory containing markdown slides and deck.config.ts
64
+ * @param options - Presentation options (startSlide, showNotes, notesTty, loop)
65
+ * @returns Promise that resolves when the presentation ends (user quits)
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * await present('./slides', { startSlide: 0, showNotes: true });
70
+ * ```
71
+ */
72
+ export async function present(
73
+ slidesDir: string,
74
+ options: PresentOptions = {}
75
+ ): Promise<void> {
76
+ // Load deck
77
+ const deck = await loadDeck(slidesDir);
78
+
79
+ if (deck.slides.length === 0) {
80
+ throw new Error(`No slides found in ${slidesDir}`);
81
+ }
82
+
83
+ // Create renderer
84
+ const renderer = createRenderer(deck.config.theme);
85
+
86
+ // Create presenter state
87
+ const presenter: Presenter = {
88
+ deck,
89
+ renderer,
90
+ currentSlide: options.startSlide ?? deck.config.settings?.startSlide ?? 0,
91
+ isAnimating: false,
92
+ notesWindow: null,
93
+ autoAdvanceTimer: null,
94
+ progressBar: null,
95
+ };
96
+
97
+ // Setup notes window if requested
98
+ if (options.showNotes) {
99
+ presenter.notesWindow = await createNotesWindow(options.notesTty);
100
+ }
101
+
102
+ // Setup progress bar if enabled
103
+ if (deck.config.settings?.showProgress) {
104
+ presenter.progressBar = createProgressBar(presenter);
105
+ }
106
+
107
+ // Setup keyboard controls
108
+ setupControls(presenter);
109
+
110
+ // Show first slide
111
+ await showSlide(presenter, presenter.currentSlide);
112
+
113
+ // Update progress bar
114
+ if (presenter.progressBar) {
115
+ updateProgress(presenter.progressBar, presenter.currentSlide, deck.slides.length);
116
+ }
117
+
118
+ // Start auto-advance if configured
119
+ presenter.autoAdvanceTimer = startAutoAdvance(presenter);
120
+
121
+ // Keep process alive until quit
122
+ await new Promise<void>((resolve) => {
123
+ renderer.screen.key(['q', 'C-c', 'escape'], () => {
124
+ cleanup(presenter);
125
+ resolve();
126
+ });
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Cleanup resources
132
+ *
133
+ * Destroys the notes window (if present), stops auto-advance timer,
134
+ * and destroys the main renderer, freeing all resources and restoring the terminal.
135
+ *
136
+ * @param presenter - The presenter state to clean up
137
+ */
138
+ function cleanup(presenter: Presenter): void {
139
+ stopAutoAdvance(presenter.autoAdvanceTimer);
140
+ if (presenter.notesWindow) {
141
+ presenter.notesWindow.screen.destroy();
142
+ }
143
+ destroyRenderer(presenter.renderer);
144
+ }
145
+
146
+ /**
147
+ * Show a specific slide (placeholder for task 5.4)
148
+ */
149
+ async function showSlide(presenter: Presenter, index: number): Promise<void> {
150
+ if (presenter.isAnimating) return;
151
+ if (index < 0 || index >= presenter.deck.slides.length) return;
152
+
153
+ presenter.isAnimating = true;
154
+ presenter.currentSlide = index;
155
+
156
+ const slide = presenter.deck.slides[index];
157
+
158
+ // Render slide
159
+ await renderSlide(presenter.renderer, slide);
160
+ presenter.renderer.screen.render();
161
+
162
+ // Update notes window
163
+ if (presenter.notesWindow) {
164
+ updateNotesWindow(presenter);
165
+ }
166
+
167
+ // Update progress bar
168
+ if (presenter.progressBar) {
169
+ updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
170
+ }
171
+
172
+ presenter.isAnimating = false;
173
+ }
174
+
175
+ /**
176
+ * Go to next slide
177
+ *
178
+ * Advances to the next slide in the deck. If at the last slide:
179
+ * - If loop is enabled, wraps to first slide
180
+ * - If loop is disabled, stays on last slide
181
+ *
182
+ * @param presenter - The presenter state
183
+ */
184
+ export async function nextSlide(presenter: Presenter): Promise<void> {
185
+ const nextIndex = presenter.currentSlide + 1;
186
+ const { slides } = presenter.deck;
187
+ const loop = presenter.deck.config.settings?.loop ?? false;
188
+
189
+ if (nextIndex >= slides.length) {
190
+ if (loop) {
191
+ await showSlide(presenter, 0);
192
+ }
193
+ return;
194
+ }
195
+
196
+ await showSlide(presenter, nextIndex);
197
+ }
198
+
199
+ /**
200
+ * Go to previous slide
201
+ *
202
+ * Goes back to the previous slide in the deck. If at the first slide:
203
+ * - If loop is enabled, wraps to last slide
204
+ * - If loop is disabled, stays on first slide
205
+ *
206
+ * To maintain the stacked window effect, this function clears all windows
207
+ * and re-renders all slides from 0 up to the target slide.
208
+ *
209
+ * @param presenter - The presenter state
210
+ */
211
+ export async function prevSlide(presenter: Presenter): Promise<void> {
212
+ const prevIndex = presenter.currentSlide - 1;
213
+ const { slides } = presenter.deck;
214
+ const loop = presenter.deck.config.settings?.loop ?? false;
215
+
216
+ if (prevIndex < 0) {
217
+ if (loop) {
218
+ // Clear and re-render all slides up to the last one
219
+ clearWindows(presenter.renderer);
220
+ for (let i = 0; i < slides.length; i++) {
221
+ await renderSlide(presenter.renderer, slides[i]);
222
+ }
223
+ presenter.currentSlide = slides.length - 1;
224
+
225
+ // Update notes window
226
+ if (presenter.notesWindow) {
227
+ updateNotesWindow(presenter);
228
+ }
229
+
230
+ // Update progress bar
231
+ if (presenter.progressBar) {
232
+ updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
233
+ }
234
+
235
+ presenter.renderer.screen.render();
236
+ }
237
+ // If not looping, stay at current slide (index 0)
238
+ return;
239
+ }
240
+
241
+ // Clear windows and re-render from start up to prevIndex
242
+ clearWindows(presenter.renderer);
243
+ for (let i = 0; i <= prevIndex; i++) {
244
+ await renderSlide(presenter.renderer, slides[i]);
245
+ }
246
+
247
+ presenter.currentSlide = prevIndex;
248
+
249
+ // Update notes window
250
+ if (presenter.notesWindow) {
251
+ updateNotesWindow(presenter);
252
+ }
253
+
254
+ // Update progress bar
255
+ if (presenter.progressBar) {
256
+ updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
257
+ }
258
+
259
+ // Render screen to display changes
260
+ presenter.renderer.screen.render();
261
+ }
262
+
263
+ /**
264
+ * Jump to a specific slide by index
265
+ *
266
+ * Jumps directly to the specified slide index. To maintain the stacked window
267
+ * effect, this function clears all windows and re-renders all slides from 0
268
+ * up to the target slide.
269
+ *
270
+ * Invalid indices (negative or beyond deck length) are ignored.
271
+ *
272
+ * @param presenter - The presenter state
273
+ * @param index - The slide index to jump to (0-based)
274
+ */
275
+ export async function jumpToSlide(presenter: Presenter, index: number): Promise<void> {
276
+ // Check bounds - ignore invalid indices
277
+ if (index < 0 || index >= presenter.deck.slides.length) return;
278
+
279
+ // Clear all windows to prepare for re-rendering
280
+ clearWindows(presenter.renderer);
281
+
282
+ // Re-render slides 0 through target index to preserve stacking
283
+ for (let i = 0; i <= index; i++) {
284
+ await renderSlide(presenter.renderer, presenter.deck.slides[i]);
285
+ }
286
+
287
+ // Update current slide
288
+ presenter.currentSlide = index;
289
+
290
+ // Update notes window if present
291
+ if (presenter.notesWindow) {
292
+ updateNotesWindow(presenter);
293
+ }
294
+
295
+ // Update progress bar
296
+ if (presenter.progressBar) {
297
+ updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
298
+ }
299
+
300
+ // Render screen to display changes
301
+ presenter.renderer.screen.render();
302
+ }
303
+
304
+ /**
305
+ * Setup keyboard event handlers
306
+ *
307
+ * Registers all keyboard controls for the presentation:
308
+ * - Next slide: Space, Enter, Right, n
309
+ * - Previous slide: Left, Backspace, p
310
+ * - Jump to slide: 0-9
311
+ * - Show slide list: l
312
+ * - Toggle notes visibility: N
313
+ * - Quit: q, Ctrl+C, Escape (handled in present() function)
314
+ *
315
+ * @param presenter - The presenter state
316
+ */
317
+ function setupControls(presenter: Presenter): void {
318
+ const { screen } = presenter.renderer;
319
+
320
+ // Next slide: Space, Enter, Right, n
321
+ screen.key(['space', 'enter', 'right', 'n'], () => {
322
+ nextSlide(presenter);
323
+ });
324
+
325
+ // Previous slide: Left, Backspace, p
326
+ screen.key(['left', 'backspace', 'p'], () => {
327
+ prevSlide(presenter);
328
+ });
329
+
330
+ // Jump to slide: 0-9
331
+ screen.key(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], (ch) => {
332
+ const index = parseInt(ch, 10);
333
+ jumpToSlide(presenter, index);
334
+ });
335
+
336
+ // Show slide list: l
337
+ screen.key(['l'], () => {
338
+ showSlideList(presenter);
339
+ });
340
+
341
+ // Toggle notes visibility: N (only if notes window exists)
342
+ screen.key(['N'], () => {
343
+ if (presenter.notesWindow) {
344
+ toggleNotesVisibility(presenter.notesWindow);
345
+ }
346
+ });
347
+
348
+ // Note: quit keys (q, Ctrl+C, Escape) are handled in the present() function
349
+ }
350
+
351
+ /**
352
+ * Show slide list overlay
353
+ *
354
+ * Displays an overlay showing all slides in the deck with the current slide marked.
355
+ * User can press Escape, l, or q to close, or press a number key to jump to that slide.
356
+ *
357
+ * @param presenter - The presenter state
358
+ */
359
+ function showSlideList(presenter: Presenter): void {
360
+ const { screen } = presenter.renderer;
361
+ const { slides } = presenter.deck;
362
+
363
+ // Build list content with current slide marker
364
+ const listContent = slides
365
+ .map((slide, i) => {
366
+ const marker = i === presenter.currentSlide ? '▶ ' : ' ';
367
+ return `${marker}${i}: ${slide.frontmatter.title}`;
368
+ })
369
+ .join('\n');
370
+
371
+ // Create overlay box centered on screen
372
+ const listBox = screen.box({
373
+ top: 'center',
374
+ left: 'center',
375
+ width: 50,
376
+ height: Math.min(slides.length + 4, 20),
377
+ border: { type: 'line' },
378
+ label: ' SLIDES (press number or Esc) ',
379
+ style: {
380
+ fg: '#ffffff',
381
+ bg: '#0a0a0a',
382
+ border: { fg: '#ffcc00' },
383
+ },
384
+ padding: 1,
385
+ tags: true,
386
+ content: listContent,
387
+ });
388
+
389
+ screen.append(listBox);
390
+
391
+ screen.render();
392
+
393
+ // Close list helper
394
+ const closeList = () => {
395
+ listBox.destroy();
396
+ screen.render();
397
+ };
398
+
399
+ // Close on Escape, l, or q
400
+ screen.onceKey(['escape', 'l', 'q'], closeList);
401
+
402
+ // Number keys jump to slide and close
403
+ screen.onceKey(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], (ch) => {
404
+ closeList();
405
+ jumpToSlide(presenter, parseInt(ch ?? '0', 10));
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Toggle notes window visibility
411
+ *
412
+ * Toggles the visibility of the notes window between shown and hidden.
413
+ *
414
+ * @param notesWindow - The notes window to toggle
415
+ */
416
+ function toggleNotesVisibility(notesWindow: NotesWindow): void {
417
+ const { contentBox, screen } = notesWindow;
418
+ contentBox.toggle();
419
+ screen.render();
420
+ }
421
+
422
+ /**
423
+ * Find an available TTY for notes window
424
+ *
425
+ * This is a best-effort approach - user should specify with --notes-tty.
426
+ * Searches common TTY paths on macOS and Linux.
427
+ *
428
+ * @returns Promise resolving to an available TTY path
429
+ * @throws Error if no available TTY is found
430
+ */
431
+ async function findAvailableTty(): Promise<string> {
432
+ // On macOS, try common TTY paths
433
+ const candidates = [
434
+ '/dev/ttys001',
435
+ '/dev/ttys002',
436
+ '/dev/ttys003',
437
+ '/dev/pts/1',
438
+ '/dev/pts/2',
439
+ ];
440
+
441
+ for (const tty of candidates) {
442
+ try {
443
+ const file = Bun.file(tty);
444
+ if (await file.exists()) {
445
+ return tty;
446
+ }
447
+ } catch {
448
+ // Continue to next candidate
449
+ }
450
+ }
451
+
452
+ throw new Error(
453
+ 'Could not find available TTY for notes window. ' +
454
+ 'Open a second terminal, run `tty`, and pass the path with --notes-tty'
455
+ );
456
+ }
457
+
458
+ /**
459
+ * Create notes window on a separate TTY
460
+ *
461
+ * Creates a blessed screen on a different TTY device for displaying presenter notes.
462
+ * If no TTY is specified, attempts to find one automatically.
463
+ *
464
+ * Usage: Open a second terminal and run `tty` to get the device path,
465
+ * then pass it with --notes-tty /dev/ttys001
466
+ *
467
+ * @param ttyPath - Optional TTY device path (e.g., '/dev/ttys001')
468
+ * @returns Promise resolving to the created notes window
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * // With explicit TTY path
473
+ * const notesWindow = await createNotesWindow('/dev/ttys001');
474
+ *
475
+ * // Auto-detect TTY
476
+ * const notesWindow = await createNotesWindow();
477
+ * ```
478
+ */
479
+ async function createNotesWindow(ttyPath?: string): Promise<NotesWindow> {
480
+ const blessed = (await import('neo-blessed')).default;
481
+ const { openSync } = await import('node:fs');
482
+
483
+ // If no TTY specified, try to find one
484
+ const tty = ttyPath ?? await findAvailableTty();
485
+
486
+ // Create screen on the specified TTY
487
+ const screen = blessed.screen({
488
+ smartCSR: true,
489
+ title: 'term-deck notes',
490
+ fullUnicode: true,
491
+ input: openSync(tty, 'r'),
492
+ output: openSync(tty, 'w'),
493
+ });
494
+
495
+ // Create content box
496
+ const contentBox = blessed.box({
497
+ top: 0,
498
+ left: 0,
499
+ width: '100%',
500
+ height: '100%',
501
+ tags: true,
502
+ padding: 2,
503
+ style: {
504
+ fg: '#ffffff',
505
+ bg: '#1a1a1a',
506
+ },
507
+ });
508
+
509
+ screen.append(contentBox);
510
+ screen.render();
511
+
512
+ return {
513
+ screen,
514
+ contentBox,
515
+ tty,
516
+ };
517
+ }
518
+
519
+ /**
520
+ * Update notes window content for current slide
521
+ *
522
+ * Updates the notes window to display:
523
+ * - Current slide number and title
524
+ * - Presenter notes (or "No notes" if none exist)
525
+ * - Preview of next slide title (or "Last slide" if at end)
526
+ *
527
+ * @param presenter - The presenter state
528
+ */
529
+ function updateNotesWindow(presenter: Presenter): void {
530
+ if (!presenter.notesWindow) return;
531
+
532
+ const { contentBox, screen } = presenter.notesWindow;
533
+ const { slides } = presenter.deck;
534
+ const currentIndex = presenter.currentSlide;
535
+ const currentSlide = slides[currentIndex];
536
+ const nextSlide = slides[currentIndex + 1];
537
+
538
+ // Build notes content
539
+ let content = '';
540
+
541
+ // Header
542
+ content += `{bold}Slide ${currentIndex + 1} of ${slides.length}{/bold}\n`;
543
+ content += `{gray-fg}${currentSlide.frontmatter.title}{/}\n`;
544
+ content += '\n';
545
+ content += '─'.repeat(50) + '\n';
546
+ content += '\n';
547
+
548
+ // Notes
549
+ if (currentSlide.notes) {
550
+ content += '{bold}PRESENTER NOTES:{/bold}\n\n';
551
+ content += currentSlide.notes + '\n';
552
+ } else {
553
+ content += '{gray-fg}No notes for this slide{/}\n';
554
+ }
555
+
556
+ content += '\n';
557
+ content += '─'.repeat(50) + '\n';
558
+ content += '\n';
559
+
560
+ // Next slide preview
561
+ if (nextSlide) {
562
+ content += `{bold}NEXT:{/bold} "${nextSlide.frontmatter.title}"\n`;
563
+ } else {
564
+ content += '{gray-fg}Last slide{/}\n';
565
+ }
566
+
567
+ contentBox.setContent(content);
568
+ screen.render();
569
+ }
570
+
571
+ /**
572
+ * Start auto-advance timer
573
+ *
574
+ * Automatically advances to the next slide at a specified interval.
575
+ * Respects the isAnimating flag to avoid advancing during animations.
576
+ * Returns null if auto-advance is disabled (interval <= 0).
577
+ *
578
+ * @param presenter - The presenter state
579
+ * @returns Timer object if auto-advance is enabled, null otherwise
580
+ */
581
+ function startAutoAdvance(presenter: Presenter): NodeJS.Timer | null {
582
+ const interval = presenter.deck.config.settings?.autoAdvance;
583
+
584
+ // Auto-advance disabled if interval is undefined, 0, or negative
585
+ if (!interval || interval <= 0) {
586
+ return null;
587
+ }
588
+
589
+ // Start interval timer
590
+ return setInterval(() => {
591
+ // Only advance if not currently animating
592
+ if (!presenter.isAnimating) {
593
+ nextSlide(presenter);
594
+ }
595
+ }, interval);
596
+ }
597
+
598
+ /**
599
+ * Stop auto-advance timer
600
+ *
601
+ * Clears the auto-advance interval timer if it exists.
602
+ *
603
+ * @param timer - The timer to stop (can be null)
604
+ */
605
+ function stopAutoAdvance(timer: NodeJS.Timer | null): void {
606
+ if (timer) {
607
+ clearInterval(timer);
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Create progress bar at bottom of screen
613
+ *
614
+ * Creates a horizontal progress bar that shows presentation progress.
615
+ * The bar is positioned at the bottom of the screen and fills from left
616
+ * to right as the presentation advances.
617
+ *
618
+ * @param presenter - The presenter state
619
+ * @returns Progress bar element
620
+ */
621
+ function createProgressBar(presenter: Presenter): blessed.Widgets.ProgressBarElement {
622
+ const { screen } = presenter.renderer;
623
+
624
+ const progressBar = blessed.progressbar({
625
+ bottom: 0,
626
+ left: 0,
627
+ width: '100%',
628
+ height: 1,
629
+ style: {
630
+ bg: '#333333',
631
+ bar: { bg: '#00cc66' },
632
+ },
633
+ filled: 0,
634
+ });
635
+
636
+ screen.append(progressBar);
637
+
638
+ return progressBar;
639
+ }
640
+
641
+ /**
642
+ * Update progress bar
643
+ *
644
+ * Updates the progress bar to reflect the current slide position.
645
+ * Progress is calculated as (current + 1) / total * 100.
646
+ *
647
+ * @param progressBar - The progress bar element to update
648
+ * @param current - Current slide index (0-based)
649
+ * @param total - Total number of slides
650
+ */
651
+ function updateProgress(
652
+ progressBar: blessed.Widgets.ProgressBarElement,
653
+ current: number,
654
+ total: number
655
+ ): void {
656
+ const progress = ((current + 1) / total) * 100;
657
+ progressBar.setProgress(progress);
658
+ }