@pep/term-deck 1.0.15 → 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.
@@ -2,16 +2,17 @@
2
2
  import { join } from 'path';
3
3
  import 'url';
4
4
  import { z } from 'zod';
5
- import 'yaml';
6
- import 'deepmerge';
7
- import gradient2 from 'gradient-string';
8
5
  import matter from 'gray-matter';
6
+ import { mkdir, writeFile, rm, access, readFile, unlink } from 'fs/promises';
9
7
  import fg from 'fast-glob';
10
- import { mkdir, writeFile, access, readFile, unlink } from 'fs/promises';
11
- import { mermaidToAscii as mermaidToAscii$1 } from 'mermaid-ascii';
12
8
  import blessed from 'neo-blessed';
9
+ import gradient2 from 'gradient-string';
10
+ import 'yaml';
11
+ import 'deepmerge';
12
+ import { mermaidToAscii as mermaidToAscii$1 } from 'mermaid-ascii';
13
13
  import figlet from 'figlet';
14
14
  import { Command } from 'commander';
15
+ import { tmpdir } from 'os';
15
16
  import { execa } from 'execa';
16
17
 
17
18
  var __defProp = Object.defineProperty;
@@ -27,52 +28,6 @@ var init_esm_shims = __esm({
27
28
  "node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js"() {
28
29
  }
29
30
  });
30
- var SlideFrontmatterSchema, SlideSchema;
31
- var init_slide = __esm({
32
- "src/schemas/slide.ts"() {
33
- init_esm_shims();
34
- SlideFrontmatterSchema = z.object({
35
- // Required: window title
36
- title: z.string().min(1, {
37
- message: "Slide must have a title"
38
- }),
39
- // ASCII art text (figlet) - can be a single line or multiple lines
40
- bigText: z.union([
41
- z.string(),
42
- z.array(z.string())
43
- ]).optional(),
44
- // Which gradient to use for bigText
45
- gradient: z.string().optional(),
46
- // Override theme for this slide
47
- theme: z.string().optional(),
48
- // Transition effect
49
- transition: z.enum([
50
- "glitch",
51
- // Default: glitch reveal line by line
52
- "fade",
53
- // Fade in
54
- "instant",
55
- // No animation
56
- "typewriter"
57
- // Character by character
58
- ]).default("glitch"),
59
- // Custom metadata (ignored by renderer, useful for tooling)
60
- meta: z.record(z.string(), z.unknown()).optional()
61
- });
62
- SlideSchema = z.object({
63
- // Parsed frontmatter
64
- frontmatter: SlideFrontmatterSchema,
65
- // Markdown body content
66
- body: z.string(),
67
- // Presenter notes (extracted from <!-- notes --> block)
68
- notes: z.string().optional(),
69
- // Source file path
70
- sourcePath: z.string(),
71
- // Slide index in deck (0-indexed)
72
- index: z.number()
73
- });
74
- }
75
- });
76
31
  var HexColorSchema, GradientSchema, ThemeSchema, DEFAULT_THEME;
77
32
  var init_theme = __esm({
78
33
  "src/schemas/theme.ts"() {
@@ -244,144 +199,52 @@ var init_validation = __esm({
244
199
  };
245
200
  }
246
201
  });
247
- function resolveColorToken(token, theme) {
248
- switch (token) {
249
- case "PRIMARY":
250
- return theme.colors.primary;
251
- case "SECONDARY":
252
- return theme.colors.secondary ?? theme.colors.primary;
253
- case "ACCENT":
254
- return theme.colors.accent;
255
- case "MUTED":
256
- return theme.colors.muted;
257
- case "TEXT":
258
- return theme.colors.text;
259
- case "BACKGROUND":
260
- return theme.colors.background;
261
- }
262
- return BUILTIN_COLORS[token] ?? theme.colors.text;
263
- }
264
- function colorTokensToBlessedTags(content, theme) {
265
- return content.replace(
266
- /\{(GREEN|ORANGE|CYAN|PINK|WHITE|GRAY|PRIMARY|SECONDARY|ACCENT|MUTED|TEXT|BACKGROUND|\/)\}/g,
267
- (_, token) => {
268
- if (token === "/") {
269
- return "{/}";
270
- }
271
- const color = resolveColorToken(token, theme);
272
- return `{${color}-fg}`;
273
- }
274
- );
275
- }
276
- var ThemeError, BUILTIN_COLORS;
277
- var init_theme2 = __esm({
278
- "src/core/theme.ts"() {
202
+ var SlideFrontmatterSchema, SlideSchema;
203
+ var init_slide = __esm({
204
+ "src/schemas/slide.ts"() {
279
205
  init_esm_shims();
280
- init_theme();
281
- init_validation();
282
- ThemeError = class extends Error {
283
- /**
284
- * @param message - The error message
285
- * @param themeName - Optional name of the theme that caused the error
286
- * @param path - Optional path to the theme file or package
287
- */
288
- constructor(message, themeName, path2) {
289
- super(message);
290
- this.themeName = themeName;
291
- this.path = path2;
292
- this.name = "ThemeError";
293
- }
294
- };
295
- BUILTIN_COLORS = {
296
- GREEN: "#00cc66",
297
- ORANGE: "#ff6600",
298
- CYAN: "#00ccff",
299
- PINK: "#ff0066",
300
- WHITE: "#ffffff",
301
- GRAY: "#666666"
302
- };
206
+ SlideFrontmatterSchema = z.object({
207
+ // Required: window title
208
+ title: z.string().min(1, {
209
+ message: "Slide must have a title"
210
+ }),
211
+ // ASCII art text (figlet) - can be a single line or multiple lines
212
+ bigText: z.union([
213
+ z.string(),
214
+ z.array(z.string())
215
+ ]).optional(),
216
+ // Which gradient to use for bigText
217
+ gradient: z.string().optional(),
218
+ // Override theme for this slide
219
+ theme: z.string().optional(),
220
+ // Transition effect
221
+ transition: z.enum([
222
+ "glitch",
223
+ // Default: glitch reveal line by line
224
+ "fade",
225
+ // Fade in
226
+ "instant",
227
+ // No animation
228
+ "typewriter"
229
+ // Character by character
230
+ ]).default("glitch"),
231
+ // Custom metadata (ignored by renderer, useful for tooling)
232
+ meta: z.record(z.string(), z.unknown()).optional()
233
+ });
234
+ SlideSchema = z.object({
235
+ // Parsed frontmatter
236
+ frontmatter: SlideFrontmatterSchema,
237
+ // Markdown body content
238
+ body: z.string(),
239
+ // Presenter notes (extracted from <!-- notes --> block)
240
+ notes: z.string().optional(),
241
+ // Source file path
242
+ sourcePath: z.string(),
243
+ // Slide index in deck (0-indexed)
244
+ index: z.number()
245
+ });
303
246
  }
304
247
  });
305
-
306
- // src/core/slide.ts
307
- var slide_exports = {};
308
- __export(slide_exports, {
309
- DeckLoadError: () => DeckLoadError,
310
- SlideParseError: () => SlideParseError,
311
- extractMermaidBlocks: () => extractMermaidBlocks,
312
- extractNotes: () => extractNotes,
313
- findSlideFiles: () => findSlideFiles,
314
- formatMermaidError: () => formatMermaidError,
315
- formatSlideError: () => formatSlideError,
316
- hasMermaidDiagrams: () => hasMermaidDiagrams,
317
- loadDeck: () => loadDeck,
318
- loadDeckConfig: () => loadDeckConfig,
319
- mermaidToAscii: () => mermaidToAscii,
320
- normalizeBigText: () => normalizeBigText,
321
- parseSlide: () => parseSlide,
322
- processMermaidDiagrams: () => processMermaidDiagrams,
323
- processSlideContent: () => processSlideContent
324
- });
325
- function hasMermaidDiagrams(content) {
326
- MERMAID_BLOCK_PATTERN.lastIndex = 0;
327
- return MERMAID_BLOCK_PATTERN.test(content);
328
- }
329
- function extractMermaidBlocks(content) {
330
- const blocks = [];
331
- let match;
332
- MERMAID_BLOCK_PATTERN.lastIndex = 0;
333
- while ((match = MERMAID_BLOCK_PATTERN.exec(content)) !== null) {
334
- blocks.push(match[1].trim());
335
- }
336
- return blocks;
337
- }
338
- function formatMermaidError(code, _error) {
339
- const lines = [
340
- "\u250C\u2500 Diagram (parse error) \u2500\u2510",
341
- "\u2502 \u2502"
342
- ];
343
- const codeLines = code.split("\n").slice(0, 5);
344
- for (const line of codeLines) {
345
- const truncated = line.slice(0, 23).padEnd(23);
346
- lines.push(`\u2502 ${truncated} \u2502`);
347
- }
348
- if (code.split("\n").length > 5) {
349
- lines.push("\u2502 ... \u2502");
350
- }
351
- lines.push("\u2502 \u2502");
352
- lines.push("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
353
- return lines.join("\n");
354
- }
355
- function mermaidToAscii(mermaidCode) {
356
- try {
357
- return mermaidToAscii$1(mermaidCode);
358
- } catch (error) {
359
- return formatMermaidError(mermaidCode);
360
- }
361
- }
362
- function escapeRegex(str) {
363
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
364
- }
365
- function processMermaidDiagrams(content) {
366
- if (!hasMermaidDiagrams(content)) {
367
- return content;
368
- }
369
- let result = content;
370
- const blocks = extractMermaidBlocks(content);
371
- for (const block of blocks) {
372
- const ascii = mermaidToAscii(block);
373
- result = result.replace(
374
- new RegExp("```mermaid\\n" + escapeRegex(block) + "\\n?```", "g"),
375
- ascii
376
- );
377
- }
378
- return result;
379
- }
380
- async function processSlideContent(body, theme) {
381
- let processed = processMermaidDiagrams(body);
382
- processed = colorTokensToBlessedTags(processed, theme);
383
- return processed;
384
- }
385
248
  function extractNotes(content) {
386
249
  const notesStart = content.indexOf(NOTES_MARKER);
387
250
  if (notesStart === -1) {
@@ -415,6 +278,29 @@ async function parseSlide(filePath, index) {
415
278
  };
416
279
  return safeParse(SlideSchema, slide, `slide ${filePath}`);
417
280
  }
281
+ var NOTES_MARKER, NOTES_END_MARKER, SlideParseError;
282
+ var init_slide2 = __esm({
283
+ "src/core/slide.ts"() {
284
+ init_esm_shims();
285
+ init_slide();
286
+ init_validation();
287
+ NOTES_MARKER = "<!-- notes -->";
288
+ NOTES_END_MARKER = "<!-- /notes -->";
289
+ SlideParseError = class extends Error {
290
+ /**
291
+ * @param message - The error message describing what went wrong
292
+ * @param filePath - Path to the slide file that failed to parse
293
+ * @param cause - Optional underlying error that caused this failure
294
+ */
295
+ constructor(message, filePath, cause) {
296
+ super(message);
297
+ this.filePath = filePath;
298
+ this.cause = cause;
299
+ this.name = "SlideParseError";
300
+ }
301
+ };
302
+ }
303
+ });
418
304
  async function findSlideFiles(dir) {
419
305
  const pattern = join(dir, "*.md");
420
306
  const foundFiles = await fg(pattern, { onlyFiles: true });
@@ -447,7 +333,8 @@ async function loadDeckConfig(slidesDir) {
447
333
  theme: DEFAULT_THEME
448
334
  };
449
335
  }
450
- const configModule = await import(configPath + "?t=" + Date.now());
336
+ const cacheBuster = `${Date.now()}-${Math.random()}`;
337
+ const configModule = await import(configPath + "?t=" + cacheBuster);
451
338
  if (!configModule.default) {
452
339
  throw new Error("deck.config.ts must export default config");
453
340
  }
@@ -471,50 +358,18 @@ async function loadDeck(slidesDir) {
471
358
  basePath: slidesDir
472
359
  };
473
360
  }
474
- function normalizeBigText(bigText) {
475
- if (!bigText) return [];
476
- return Array.isArray(bigText) ? bigText : [bigText];
477
- }
478
- function formatSlideError(error) {
479
- let msg = `Error parsing slide: ${error.filePath}
480
- `;
481
- msg += ` ${error.message}
482
- `;
483
- if (error.cause) {
484
- msg += ` Caused by: ${error.cause.message}
485
- `;
486
- }
487
- return msg;
488
- }
489
- var NOTES_MARKER, NOTES_END_MARKER, MERMAID_BLOCK_PATTERN, SlideParseError, DeckLoadError;
490
- var init_slide2 = __esm({
491
- "src/core/slide.ts"() {
361
+ var DeckLoadError;
362
+ var init_deck_loader = __esm({
363
+ "src/core/deck-loader.ts"() {
492
364
  init_esm_shims();
493
- init_slide();
494
365
  init_config();
495
366
  init_validation();
496
367
  init_theme();
497
- init_theme2();
498
- NOTES_MARKER = "<!-- notes -->";
499
- NOTES_END_MARKER = "<!-- /notes -->";
500
- MERMAID_BLOCK_PATTERN = /```mermaid\n([\s\S]*?)```/g;
501
- SlideParseError = class extends Error {
368
+ init_slide2();
369
+ DeckLoadError = class extends Error {
502
370
  /**
503
371
  * @param message - The error message describing what went wrong
504
- * @param filePath - Path to the slide file that failed to parse
505
- * @param cause - Optional underlying error that caused this failure
506
- */
507
- constructor(message, filePath, cause) {
508
- super(message);
509
- this.filePath = filePath;
510
- this.cause = cause;
511
- this.name = "SlideParseError";
512
- }
513
- };
514
- DeckLoadError = class extends Error {
515
- /**
516
- * @param message - The error message describing what went wrong
517
- * @param slidesDir - Path to the directory that was being loaded
372
+ * @param slidesDir - Path to the directory that was being loaded
518
373
  * @param cause - Optional underlying error that caused this failure
519
374
  */
520
375
  constructor(message, slidesDir, cause) {
@@ -526,43 +381,14 @@ var init_slide2 = __esm({
526
381
  };
527
382
  }
528
383
  });
529
-
530
- // src/renderer/screen.ts
531
- var screen_exports = {};
532
- __export(screen_exports, {
533
- applyTransition: () => applyTransition,
534
- clearWindows: () => clearWindows,
535
- createRenderer: () => createRenderer,
536
- createScreen: () => createScreen,
537
- createWindow: () => createWindow,
538
- destroyRenderer: () => destroyRenderer,
539
- generateBigText: () => generateBigText,
540
- generateMultiLineBigText: () => generateMultiLineBigText,
541
- getWindowColor: () => getWindowColor,
542
- glitchLine: () => glitchLine,
543
- initMatrixRain: () => initMatrixRain,
544
- lineByLineReveal: () => lineByLineReveal,
545
- renderMatrixRain: () => renderMatrixRain,
546
- renderSlide: () => renderSlide
547
- });
548
- function createScreen(title = "term-deck") {
549
- const screen = blessed.screen({
550
- smartCSR: true,
551
- title,
552
- fullUnicode: true,
553
- mouse: false,
554
- altScreen: true
555
- });
556
- return screen;
557
- }
558
384
  function generateTrail(glyphs, length) {
559
385
  return Array.from(
560
386
  { length },
561
387
  () => glyphs[Math.floor(Math.random() * glyphs.length)]
562
388
  );
563
389
  }
564
- function renderMatrixRain(renderer) {
565
- const { screen, matrixBox, matrixDrops, theme } = renderer;
390
+ function renderMatrixRain(screen, state) {
391
+ const { matrixBox, matrixDrops, theme } = state;
566
392
  const width = Math.max(20, screen.width || 80);
567
393
  const height = Math.max(10, screen.height || 24);
568
394
  const grid = Array.from(
@@ -597,27 +423,32 @@ function renderMatrixRain(renderer) {
597
423
  }
598
424
  matrixBox.setContent(output);
599
425
  }
600
- function initMatrixRain(renderer) {
601
- const { screen, theme } = renderer;
426
+ function initMatrixRain(screen, state) {
427
+ const { theme } = state;
602
428
  const width = screen.width || 80;
603
429
  const height = screen.height || 24;
604
430
  const density = theme.animations.matrixDensity;
605
- renderer.matrixDrops = [];
431
+ state.matrixDrops = [];
606
432
  for (let i = 0; i < density; i++) {
607
- renderer.matrixDrops.push({
433
+ state.matrixDrops.push({
608
434
  x: Math.floor(Math.random() * width),
609
435
  y: Math.floor(Math.random() * height),
610
436
  speed: 0.3 + Math.random() * 0.7,
611
437
  trail: generateTrail(theme.glyphs, 5 + Math.floor(Math.random() * 10))
612
438
  });
613
439
  }
614
- renderer.matrixInterval = setInterval(() => {
615
- renderMatrixRain(renderer);
616
- renderer.screen.render();
440
+ state.matrixInterval = setInterval(() => {
441
+ renderMatrixRain(screen, state);
442
+ screen.render();
617
443
  }, theme.animations.matrixInterval);
618
444
  }
619
- function createRenderer(theme) {
620
- const screen = createScreen();
445
+ function stopMatrixRain(state) {
446
+ if (state.matrixInterval) {
447
+ clearInterval(state.matrixInterval);
448
+ state.matrixInterval = null;
449
+ }
450
+ }
451
+ function createMatrixBox(screen) {
621
452
  const matrixBox = blessed.box({
622
453
  top: 0,
623
454
  left: 0,
@@ -626,28 +457,13 @@ function createRenderer(theme) {
626
457
  tags: true
627
458
  });
628
459
  screen.append(matrixBox);
629
- const renderer = {
630
- screen,
631
- matrixBox,
632
- windowStack: [],
633
- theme,
634
- matrixDrops: [],
635
- matrixInterval: null
636
- };
637
- initMatrixRain(renderer);
638
- return renderer;
460
+ return matrixBox;
639
461
  }
640
- function destroyRenderer(renderer) {
641
- if (renderer.matrixInterval) {
642
- clearInterval(renderer.matrixInterval);
643
- renderer.matrixInterval = null;
644
- }
645
- for (const win of renderer.windowStack) {
646
- win.destroy();
462
+ var init_matrix_rain = __esm({
463
+ "src/renderer/effects/matrix-rain.ts"() {
464
+ init_esm_shims();
647
465
  }
648
- renderer.windowStack = [];
649
- renderer.screen.destroy();
650
- }
466
+ });
651
467
  function getWindowColor(index, theme) {
652
468
  const colors = [
653
469
  theme.colors.primary,
@@ -662,8 +478,7 @@ function getWindowColor(index, theme) {
662
478
  ];
663
479
  return colors[index % colors.length];
664
480
  }
665
- function createWindow(renderer, options) {
666
- const { screen, windowStack, theme } = renderer;
481
+ function createWindow(screen, windowStack, theme, options) {
667
482
  const windowIndex = windowStack.length;
668
483
  const color = options.color ?? getWindowColor(windowIndex, theme);
669
484
  const screenWidth = screen.width || 120;
@@ -699,144 +514,178 @@ function createWindow(renderer, options) {
699
514
  windowStack.push(box);
700
515
  return box;
701
516
  }
702
- function clearWindows(renderer) {
703
- for (const window of renderer.windowStack) {
517
+ function clearWindows(windowStack) {
518
+ for (const window of windowStack) {
704
519
  window.destroy();
705
520
  }
706
- renderer.windowStack = [];
521
+ windowStack.length = 0;
707
522
  }
708
- async function generateBigText(text, gradientColors, font = "Standard") {
709
- return new Promise((resolve, reject) => {
710
- figlet.text(text, { font }, (err, result) => {
711
- if (err || !result) {
712
- reject(err ?? new Error("Failed to generate figlet text"));
713
- return;
523
+ var init_window_manager = __esm({
524
+ "src/renderer/window-manager.ts"() {
525
+ init_esm_shims();
526
+ }
527
+ });
528
+
529
+ // src/core/theme-errors.ts
530
+ var ThemeError;
531
+ var init_theme_errors = __esm({
532
+ "src/core/theme-errors.ts"() {
533
+ init_esm_shims();
534
+ init_validation();
535
+ ThemeError = class extends Error {
536
+ /**
537
+ * @param message - The error message
538
+ * @param themeName - Optional name of the theme that caused the error
539
+ * @param path - Optional path to the theme file or package
540
+ */
541
+ constructor(message, themeName, path2) {
542
+ super(message);
543
+ this.themeName = themeName;
544
+ this.path = path2;
545
+ this.name = "ThemeError";
714
546
  }
715
- const gradientFn = gradient2(gradientColors);
716
- resolve(gradientFn(result));
717
- });
718
- });
719
- }
720
- async function generateMultiLineBigText(lines, gradientColors, font = "Standard") {
721
- const results = await Promise.all(
722
- lines.map((line) => generateBigText(line, gradientColors, font))
723
- );
724
- return results.join("\n");
725
- }
726
- function sleep(ms) {
727
- return new Promise((resolve) => setTimeout(resolve, ms));
547
+ };
548
+ }
549
+ });
550
+ function resolveColorToken(token, theme) {
551
+ switch (token) {
552
+ case "PRIMARY":
553
+ return theme.colors.primary;
554
+ case "SECONDARY":
555
+ return theme.colors.secondary ?? theme.colors.primary;
556
+ case "ACCENT":
557
+ return theme.colors.accent;
558
+ case "MUTED":
559
+ return theme.colors.muted;
560
+ case "TEXT":
561
+ return theme.colors.text;
562
+ case "BACKGROUND":
563
+ return theme.colors.background;
564
+ }
565
+ return BUILTIN_COLORS[token] ?? theme.colors.text;
728
566
  }
729
- async function glitchLine(box, screen, currentLines, newLine, iterations = 5) {
730
- for (let i = iterations; i >= 0; i--) {
731
- const scrambleRatio = i / iterations;
732
- let scrambledLine = "";
733
- for (const char of newLine) {
734
- if (PROTECTED_CHARS.has(char)) {
735
- scrambledLine += char;
736
- } else if (Math.random() < scrambleRatio) {
737
- scrambledLine += GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
738
- } else {
739
- scrambledLine += char;
567
+ function colorTokensToBlessedTags(content, theme) {
568
+ return content.replace(
569
+ /\{(GREEN|ORANGE|CYAN|PINK|WHITE|GRAY|PRIMARY|SECONDARY|ACCENT|MUTED|TEXT|BACKGROUND|\/)\}/g,
570
+ (_, token) => {
571
+ if (token === "/") {
572
+ return "{/}";
740
573
  }
574
+ const color = resolveColorToken(token, theme);
575
+ return `{${color}-fg}`;
741
576
  }
742
- box.setContent([...currentLines, scrambledLine].join("\n"));
743
- screen.render();
744
- await sleep(20);
745
- }
577
+ );
746
578
  }
747
- async function lineByLineReveal(box, screen, content, theme) {
748
- const lines = content.split("\n");
749
- const revealedLines = [];
750
- const lineDelay = theme.animations.lineDelay;
751
- const glitchIterations = theme.animations.glitchIterations;
752
- for (const line of lines) {
753
- await glitchLine(box, screen, revealedLines, line, glitchIterations);
754
- revealedLines.push(line);
755
- box.setContent(revealedLines.join("\n"));
756
- screen.render();
757
- if (line.trim()) {
758
- await sleep(lineDelay);
759
- }
579
+ var BUILTIN_COLORS;
580
+ var init_theme_colors = __esm({
581
+ "src/core/theme-colors.ts"() {
582
+ init_esm_shims();
583
+ BUILTIN_COLORS = {
584
+ GREEN: "#00cc66",
585
+ ORANGE: "#ff6600",
586
+ CYAN: "#00ccff",
587
+ PINK: "#ff0066",
588
+ WHITE: "#ffffff",
589
+ GRAY: "#666666"
590
+ };
591
+ }
592
+ });
593
+ var init_theme2 = __esm({
594
+ "src/core/theme.ts"() {
595
+ init_esm_shims();
596
+ init_theme();
597
+ init_validation();
598
+ init_theme_errors();
599
+ init_theme_colors();
760
600
  }
601
+ });
602
+ function hasMermaidDiagrams(content) {
603
+ MERMAID_BLOCK_PATTERN.lastIndex = 0;
604
+ return MERMAID_BLOCK_PATTERN.test(content);
761
605
  }
762
- async function fadeInReveal(box, screen, content, theme) {
763
- const steps = 10;
764
- const delay = theme.animations.lineDelay * 2 / steps;
765
- for (let step = 0; step < steps; step++) {
766
- const revealRatio = step / steps;
767
- let revealed = "";
768
- for (const char of content) {
769
- if (char === "\n" || PROTECTED_CHARS.has(char) || Math.random() < revealRatio) {
770
- revealed += char;
771
- } else {
772
- revealed += " ";
773
- }
774
- }
775
- box.setContent(revealed);
776
- screen.render();
777
- await sleep(delay);
606
+ function extractMermaidBlocks(content) {
607
+ const blocks = [];
608
+ let match;
609
+ MERMAID_BLOCK_PATTERN.lastIndex = 0;
610
+ while ((match = MERMAID_BLOCK_PATTERN.exec(content)) !== null) {
611
+ blocks.push(match[1].trim());
778
612
  }
779
- box.setContent(content);
780
- screen.render();
613
+ return blocks;
781
614
  }
782
- async function typewriterReveal(box, screen, content, theme) {
783
- const charDelay = theme.animations.lineDelay / 5;
784
- let revealed = "";
785
- for (const char of content) {
786
- revealed += char;
787
- box.setContent(revealed);
788
- screen.render();
789
- if (char !== " " && char !== "\n") {
790
- await sleep(charDelay);
791
- }
615
+ function formatMermaidError(code, _error) {
616
+ const lines = [
617
+ "\u250C\u2500 Diagram (parse error) \u2500\u2510",
618
+ "\u2502 \u2502"
619
+ ];
620
+ const codeLines = code.split("\n").slice(0, 5);
621
+ for (const line of codeLines) {
622
+ const truncated = line.slice(0, 23).padEnd(23);
623
+ lines.push(`\u2502 ${truncated} \u2502`);
792
624
  }
625
+ if (code.split("\n").length > 5) {
626
+ lines.push("\u2502 ... \u2502");
627
+ }
628
+ lines.push("\u2502 \u2502");
629
+ lines.push("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
630
+ return lines.join("\n");
793
631
  }
794
- async function applyTransition(box, screen, content, transition, theme) {
795
- switch (transition) {
796
- case "glitch":
797
- await lineByLineReveal(box, screen, content, theme);
798
- break;
799
- case "fade":
800
- await fadeInReveal(box, screen, content, theme);
801
- break;
802
- case "instant":
803
- box.setContent(content);
804
- screen.render();
805
- break;
806
- case "typewriter":
807
- await typewriterReveal(box, screen, content, theme);
808
- break;
809
- default:
810
- box.setContent(content);
811
- screen.render();
632
+ function mermaidToAscii(mermaidCode) {
633
+ try {
634
+ return mermaidToAscii$1(mermaidCode);
635
+ } catch (error) {
636
+ return formatMermaidError(mermaidCode);
812
637
  }
813
638
  }
814
- async function renderSlide(renderer, slide) {
815
- const { theme } = renderer;
816
- const { frontmatter, body } = slide;
817
- const window = createWindow(renderer, {
818
- title: frontmatter.title
819
- });
820
- let content = "";
821
- const bigTextLines = normalizeBigText(frontmatter.bigText);
822
- if (bigTextLines.length > 0) {
823
- const gradientName = frontmatter.gradient ?? "fire";
824
- const gradientColors = theme.gradients[gradientName] ?? theme.gradients.fire;
825
- const bigText = await generateMultiLineBigText(bigTextLines, gradientColors);
826
- content += bigText + "\n\n";
639
+ function escapeRegex(str) {
640
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
641
+ }
642
+ function processMermaidDiagrams(content) {
643
+ if (!hasMermaidDiagrams(content)) {
644
+ return content;
827
645
  }
828
- const processedBody = await processSlideContent(body, theme);
829
- content += processedBody;
830
- const transition = frontmatter.transition ?? "glitch";
831
- await applyTransition(window, renderer.screen, content, transition, theme);
832
- return window;
646
+ let result = content;
647
+ const blocks = extractMermaidBlocks(content);
648
+ for (const block of blocks) {
649
+ const ascii = mermaidToAscii(block);
650
+ result = result.replace(
651
+ new RegExp("```mermaid\\n" + escapeRegex(block) + "\\n?```", "g"),
652
+ ascii
653
+ );
654
+ }
655
+ return result;
833
656
  }
834
- var GLITCH_CHARS, PROTECTED_CHARS;
835
- var init_screen = __esm({
836
- "src/renderer/screen.ts"() {
657
+ var MERMAID_BLOCK_PATTERN;
658
+ var init_mermaid = __esm({
659
+ "src/core/utils/mermaid.ts"() {
837
660
  init_esm_shims();
838
- init_slide2();
839
- GLITCH_CHARS = "\u2588\u2593\u2592\u2591\u2580\u2584\u258C\u2590\u25A0\u25A1\u25AA\u25AB\u25CF\u25CB\u25CA\u25D8\u25D9\u2666\u2663\u2660\u2665\u2605\u2606\u2302\u207F\xB2\xB3\xC6\xD8\u221E\u2248\u2260\xB1\xD7\xF7\u03B1\u03B2\u03B3\u03B4\u03B5\u03B6\u03B7\u03B8\u03BB\u03BC\u03C0\u03C3\u03C6\u03C9\u0394\u03A3\u03A9\uFF71\uFF72\uFF73\uFF74\uFF75\uFF76\uFF77\uFF78\uFF79\uFF7A\uFF7B\uFF7C\uFF7D\uFF7E\uFF7F\uFF80\uFF81\uFF82\uFF83\uFF84\uFF85\uFF86\uFF87\uFF88\uFF89\uFF8A\uFF8B\uFF8C\uFF8D\uFF8E\uFF8F\uFF90\uFF91\uFF92\uFF93\uFF94\uFF95\uFF96\uFF97\uFF98\uFF99\uFF9A\uFF9B\uFF9C\uFF9D";
661
+ MERMAID_BLOCK_PATTERN = /```mermaid\n([\s\S]*?)```/g;
662
+ }
663
+ });
664
+
665
+ // src/core/content-processor.ts
666
+ async function processSlideContent(body, theme) {
667
+ let processed = processMermaidDiagrams(body);
668
+ processed = colorTokensToBlessedTags(processed, theme);
669
+ return processed;
670
+ }
671
+ function normalizeBigText(bigText) {
672
+ if (!bigText) return [];
673
+ return Array.isArray(bigText) ? bigText : [bigText];
674
+ }
675
+ var init_content_processor = __esm({
676
+ "src/core/content-processor.ts"() {
677
+ init_esm_shims();
678
+ init_theme2();
679
+ init_mermaid();
680
+ }
681
+ });
682
+
683
+ // src/renderer/animations/constants.ts
684
+ var GLITCH_CHARS, PROTECTED_CHARS;
685
+ var init_constants = __esm({
686
+ "src/renderer/animations/constants.ts"() {
687
+ init_esm_shims();
688
+ GLITCH_CHARS = "\u2588\u2593\u2592\u2591\u2580\u2584\u258C\u2590\u25A0\u25A1\u25AA\u25AB\u25CF\u25CB\u25CA\u25D8\u25D9\u2666\u2663\u2660\u2665\u2605\u2606\u2302\u207F\xB2\xB3\xC6\xD8\u221E\u2248\u2260\xB1\xD7\xF7\u03B1\u03B2\u03B3\u03B4\u03B5\u03B6\u03B7\u03B8\u03BB\u03BC\u03C0\u03C3\u03C6\u03C9\u0394\u03A3\u03A9\uFF71\uFF72\uFF73\uFF74\uFF75\uFF76\uFF77\uFF78\uFF79\uFF7A\uFF7B\uFF7C\uFF7D\uFF7E\uFF7F\uFF80\uFF81\uFF82\uFF83\uFF84\uFF85\uFF86\uFF87\uFF88\uFF89\uFF8A\uFF8B\uFF8C\uFF8D\uFF8E\uFF8F\uFF90\uFF91\uFF92\uFF93\uFF94\uFF95\uFF96\uFF97\uFF98\uFF99\uFF9A\uFF9B\uFF9C\uFF9D";
840
689
  PROTECTED_CHARS = /* @__PURE__ */ new Set([
841
690
  " ",
842
691
  " ",
@@ -915,65 +764,370 @@ var init_screen = __esm({
915
764
  }
916
765
  });
917
766
 
918
- // src/presenter/main.ts
919
- var main_exports = {};
920
- __export(main_exports, {
921
- jumpToSlide: () => jumpToSlide,
922
- nextSlide: () => nextSlide,
923
- present: () => present,
924
- prevSlide: () => prevSlide
767
+ // src/renderer/animations/helpers/animation-utils.ts
768
+ function sleep(ms) {
769
+ return new Promise((resolve) => setTimeout(resolve, ms));
770
+ }
771
+ function renderContent(box, screen, content) {
772
+ box.setContent(content);
773
+ screen.render();
774
+ }
775
+ var init_animation_utils = __esm({
776
+ "src/renderer/animations/helpers/animation-utils.ts"() {
777
+ init_esm_shims();
778
+ }
925
779
  });
926
- async function present(slidesDir, options = {}) {
927
- const deck = await loadDeck(slidesDir);
928
- if (deck.slides.length === 0) {
929
- throw new Error(`No slides found in ${slidesDir}`);
780
+
781
+ // src/renderer/animations/transitions/glitch-transition.ts
782
+ async function glitchLine(box, screen, currentLines, newLine, iterations = 5) {
783
+ for (let i = iterations; i >= 0; i--) {
784
+ const scrambleRatio = i / iterations;
785
+ let scrambledLine = "";
786
+ for (const char of newLine) {
787
+ if (PROTECTED_CHARS.has(char)) {
788
+ scrambledLine += char;
789
+ } else if (Math.random() < scrambleRatio) {
790
+ scrambledLine += GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
791
+ } else {
792
+ scrambledLine += char;
793
+ }
794
+ }
795
+ renderContent(box, screen, [...currentLines, scrambledLine].join("\n"));
796
+ await sleep(20);
930
797
  }
931
- const renderer = createRenderer(deck.config.theme);
932
- const presenter = {
933
- deck,
934
- renderer,
935
- currentSlide: options.startSlide ?? deck.config.settings?.startSlide ?? 0,
936
- isAnimating: false,
937
- notesWindow: null,
938
- autoAdvanceTimer: null,
939
- progressBar: null
940
- };
941
- if (options.showNotes) {
942
- presenter.notesWindow = await createNotesWindow(options.notesTty);
798
+ }
799
+ async function lineByLineReveal(box, screen, content, theme) {
800
+ const lines = content.split("\n");
801
+ const revealedLines = [];
802
+ const lineDelay = theme.animations.lineDelay;
803
+ const glitchIterations = theme.animations.glitchIterations;
804
+ for (const line of lines) {
805
+ await glitchLine(box, screen, revealedLines, line, glitchIterations);
806
+ revealedLines.push(line);
807
+ renderContent(box, screen, revealedLines.join("\n"));
808
+ if (line.trim()) {
809
+ await sleep(lineDelay);
810
+ }
943
811
  }
944
- if (deck.config.settings?.showProgress) {
945
- presenter.progressBar = createProgressBar(presenter);
812
+ }
813
+ var init_glitch_transition = __esm({
814
+ "src/renderer/animations/transitions/glitch-transition.ts"() {
815
+ init_esm_shims();
816
+ init_constants();
817
+ init_animation_utils();
946
818
  }
947
- setupControls(presenter);
948
- await showSlide(presenter, presenter.currentSlide);
949
- if (presenter.progressBar) {
950
- updateProgress(presenter.progressBar, presenter.currentSlide, deck.slides.length);
819
+ });
820
+
821
+ // src/renderer/animations/transitions/fade-transition.ts
822
+ async function fadeInReveal(box, screen, content, theme) {
823
+ const steps = 10;
824
+ const delay = theme.animations.lineDelay * 2 / steps;
825
+ for (let step = 0; step < steps; step++) {
826
+ const revealRatio = step / steps;
827
+ let revealed = "";
828
+ for (const char of content) {
829
+ if (char === "\n" || PROTECTED_CHARS.has(char) || Math.random() < revealRatio) {
830
+ revealed += char;
831
+ } else {
832
+ revealed += " ";
833
+ }
834
+ }
835
+ renderContent(box, screen, revealed);
836
+ await sleep(delay);
951
837
  }
952
- presenter.autoAdvanceTimer = startAutoAdvance(presenter);
953
- await new Promise((resolve) => {
954
- renderer.screen.key(["q", "C-c", "escape"], () => {
955
- cleanup(presenter);
956
- resolve();
838
+ renderContent(box, screen, content);
839
+ }
840
+ var init_fade_transition = __esm({
841
+ "src/renderer/animations/transitions/fade-transition.ts"() {
842
+ init_esm_shims();
843
+ init_constants();
844
+ init_animation_utils();
845
+ }
846
+ });
847
+
848
+ // src/renderer/animations/transitions/typewriter-transition.ts
849
+ async function typewriterReveal(box, screen, content, theme) {
850
+ const charDelay = theme.animations.lineDelay / 5;
851
+ let revealed = "";
852
+ for (const char of content) {
853
+ revealed += char;
854
+ renderContent(box, screen, revealed);
855
+ if (char !== " " && char !== "\n") {
856
+ await sleep(charDelay);
857
+ }
858
+ }
859
+ }
860
+ var init_typewriter_transition = __esm({
861
+ "src/renderer/animations/transitions/typewriter-transition.ts"() {
862
+ init_esm_shims();
863
+ init_animation_utils();
864
+ }
865
+ });
866
+
867
+ // src/renderer/animations/transitions/instant-transition.ts
868
+ function instantReveal(box, screen, content) {
869
+ renderContent(box, screen, content);
870
+ }
871
+ var init_instant_transition = __esm({
872
+ "src/renderer/animations/transitions/instant-transition.ts"() {
873
+ init_esm_shims();
874
+ init_animation_utils();
875
+ }
876
+ });
877
+
878
+ // src/renderer/animations/transition-orchestrator.ts
879
+ async function applyTransition(box, screen, content, transition, theme) {
880
+ switch (transition) {
881
+ case "glitch":
882
+ await lineByLineReveal(box, screen, content, theme);
883
+ break;
884
+ case "fade":
885
+ await fadeInReveal(box, screen, content, theme);
886
+ break;
887
+ case "instant":
888
+ instantReveal(box, screen, content);
889
+ break;
890
+ case "typewriter":
891
+ await typewriterReveal(box, screen, content, theme);
892
+ break;
893
+ default:
894
+ instantReveal(box, screen, content);
895
+ }
896
+ }
897
+ var init_transition_orchestrator = __esm({
898
+ "src/renderer/animations/transition-orchestrator.ts"() {
899
+ init_esm_shims();
900
+ init_glitch_transition();
901
+ init_fade_transition();
902
+ init_typewriter_transition();
903
+ init_instant_transition();
904
+ }
905
+ });
906
+
907
+ // src/renderer/animations/transitions.ts
908
+ var init_transitions = __esm({
909
+ "src/renderer/animations/transitions.ts"() {
910
+ init_esm_shims();
911
+ init_transition_orchestrator();
912
+ init_glitch_transition();
913
+ init_fade_transition();
914
+ init_typewriter_transition();
915
+ init_instant_transition();
916
+ }
917
+ });
918
+ async function generateBigText(text, gradientColors, font = "Standard") {
919
+ return new Promise((resolve, reject) => {
920
+ figlet.text(text, { font }, (err, result) => {
921
+ if (err || !result) {
922
+ reject(err ?? new Error("Failed to generate figlet text"));
923
+ return;
924
+ }
925
+ const gradientFn = gradient2(gradientColors);
926
+ resolve(gradientFn(result));
957
927
  });
958
928
  });
959
929
  }
960
- function cleanup(presenter) {
961
- stopAutoAdvance(presenter.autoAdvanceTimer);
962
- if (presenter.notesWindow) {
963
- presenter.notesWindow.screen.destroy();
930
+ async function generateMultiLineBigText(lines, gradientColors, font = "Standard") {
931
+ const results = await Promise.all(
932
+ lines.map((line) => generateBigText(line, gradientColors, font))
933
+ );
934
+ return results.join("\n");
935
+ }
936
+ var init_text_generator = __esm({
937
+ "src/renderer/text-generator.ts"() {
938
+ init_esm_shims();
964
939
  }
965
- destroyRenderer(presenter.renderer);
940
+ });
941
+
942
+ // src/renderer/slide-renderer.ts
943
+ async function renderSlide(screen, windowStack, theme, slide) {
944
+ const { frontmatter, body } = slide;
945
+ const window = createWindow(screen, windowStack, theme, {
946
+ title: frontmatter.title
947
+ });
948
+ let content = "";
949
+ const bigTextLines = normalizeBigText(frontmatter.bigText);
950
+ if (bigTextLines.length > 0) {
951
+ const gradientName = frontmatter.gradient ?? "fire";
952
+ const gradientColors = theme.gradients[gradientName] ?? theme.gradients.fire;
953
+ const bigText = await generateMultiLineBigText(bigTextLines, gradientColors);
954
+ content += bigText + "\n\n";
955
+ }
956
+ const processedBody = await processSlideContent(body, theme);
957
+ content += processedBody;
958
+ const transition = frontmatter.transition ?? "glitch";
959
+ await applyTransition(window, screen, content, transition, theme);
960
+ return window;
961
+ }
962
+ var init_slide_renderer = __esm({
963
+ "src/renderer/slide-renderer.ts"() {
964
+ init_esm_shims();
965
+ init_content_processor();
966
+ init_transitions();
967
+ init_text_generator();
968
+ init_window_manager();
969
+ }
970
+ });
971
+ function createScreen(title = "term-deck") {
972
+ const screen = blessed.screen({
973
+ smartCSR: true,
974
+ title,
975
+ fullUnicode: true,
976
+ mouse: false,
977
+ altScreen: true
978
+ });
979
+ return screen;
980
+ }
981
+ function createRenderer(theme) {
982
+ const screen = createScreen();
983
+ const matrixBox = createMatrixBox(screen);
984
+ const matrixRain = {
985
+ matrixBox,
986
+ matrixDrops: [],
987
+ matrixInterval: null,
988
+ theme
989
+ };
990
+ const renderer = {
991
+ screen,
992
+ windowStack: [],
993
+ theme,
994
+ matrixRain
995
+ };
996
+ initMatrixRain(screen, matrixRain);
997
+ return renderer;
998
+ }
999
+ function destroyRenderer(renderer) {
1000
+ stopMatrixRain(renderer.matrixRain);
1001
+ clearWindows(renderer.windowStack);
1002
+ renderer.screen.destroy();
1003
+ }
1004
+ function clearWindows2(renderer) {
1005
+ clearWindows(renderer.windowStack);
1006
+ }
1007
+ async function renderSlide2(renderer, slide) {
1008
+ return renderSlide(renderer.screen, renderer.windowStack, renderer.theme, slide);
1009
+ }
1010
+ var init_screen = __esm({
1011
+ "src/renderer/screen.ts"() {
1012
+ init_esm_shims();
1013
+ init_matrix_rain();
1014
+ init_window_manager();
1015
+ init_slide_renderer();
1016
+ init_transitions();
1017
+ init_window_manager();
1018
+ init_text_generator();
1019
+ }
1020
+ });
1021
+ async function findAvailableTty() {
1022
+ const candidates = [
1023
+ "/dev/ttys001",
1024
+ "/dev/ttys002",
1025
+ "/dev/ttys003",
1026
+ "/dev/pts/1",
1027
+ "/dev/pts/2"
1028
+ ];
1029
+ for (const tty of candidates) {
1030
+ try {
1031
+ await access(tty);
1032
+ return tty;
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ throw new Error(
1037
+ "Could not find available TTY for notes window. Open a second terminal, run `tty`, and pass the path with --notes-tty"
1038
+ );
966
1039
  }
1040
+ async function createNotesWindow(ttyPath) {
1041
+ const blessed5 = (await import('neo-blessed')).default;
1042
+ const { openSync } = await import('fs');
1043
+ const tty = ttyPath ?? await findAvailableTty();
1044
+ const screen = blessed5.screen({
1045
+ smartCSR: true,
1046
+ title: "term-deck notes",
1047
+ fullUnicode: true,
1048
+ input: openSync(tty, "r"),
1049
+ output: openSync(tty, "w")
1050
+ });
1051
+ const contentBox = blessed5.box({
1052
+ top: 0,
1053
+ left: 0,
1054
+ width: "100%",
1055
+ height: "100%",
1056
+ tags: true,
1057
+ padding: 2,
1058
+ style: {
1059
+ fg: "#ffffff",
1060
+ bg: "#1a1a1a"
1061
+ }
1062
+ });
1063
+ screen.append(contentBox);
1064
+ screen.render();
1065
+ return {
1066
+ screen,
1067
+ contentBox,
1068
+ tty
1069
+ };
1070
+ }
1071
+ function updateNotesWindow(notesWindow, currentSlide, nextSlide2, currentIndex, totalSlides) {
1072
+ const { contentBox, screen } = notesWindow;
1073
+ let content = "";
1074
+ content += `{bold}Slide ${currentIndex + 1} of ${totalSlides}{/bold}
1075
+ `;
1076
+ content += `{gray-fg}${currentSlide.frontmatter.title}{/}
1077
+ `;
1078
+ content += "\n";
1079
+ content += "\u2500".repeat(50) + "\n";
1080
+ content += "\n";
1081
+ if (currentSlide.notes) {
1082
+ content += "{bold}PRESENTER NOTES:{/bold}\n\n";
1083
+ content += currentSlide.notes + "\n";
1084
+ } else {
1085
+ content += "{gray-fg}No notes for this slide{/}\n";
1086
+ }
1087
+ content += "\n";
1088
+ content += "\u2500".repeat(50) + "\n";
1089
+ content += "\n";
1090
+ if (nextSlide2) {
1091
+ content += `{bold}NEXT:{/bold} "${nextSlide2.frontmatter.title}"
1092
+ `;
1093
+ } else {
1094
+ content += "{gray-fg}Last slide{/}\n";
1095
+ }
1096
+ contentBox.setContent(content);
1097
+ screen.render();
1098
+ }
1099
+ function toggleNotesVisibility(notesWindow) {
1100
+ const { contentBox, screen } = notesWindow;
1101
+ contentBox.toggle();
1102
+ screen.render();
1103
+ }
1104
+ function destroyNotesWindow(notesWindow) {
1105
+ notesWindow.screen.destroy();
1106
+ }
1107
+ var init_notes_window = __esm({
1108
+ "src/presenter/notes-window.ts"() {
1109
+ init_esm_shims();
1110
+ }
1111
+ });
1112
+
1113
+ // src/presenter/navigation.ts
967
1114
  async function showSlide(presenter, index) {
968
1115
  if (presenter.isAnimating) return;
969
1116
  if (index < 0 || index >= presenter.deck.slides.length) return;
970
1117
  presenter.isAnimating = true;
971
1118
  presenter.currentSlide = index;
972
1119
  const slide = presenter.deck.slides[index];
973
- await renderSlide(presenter.renderer, slide);
1120
+ await renderSlide2(presenter.renderer, slide);
974
1121
  presenter.renderer.screen.render();
975
1122
  if (presenter.notesWindow) {
976
- updateNotesWindow(presenter);
1123
+ const nextSlide2 = presenter.deck.slides[index + 1];
1124
+ updateNotesWindow(
1125
+ presenter.notesWindow,
1126
+ slide,
1127
+ nextSlide2,
1128
+ index,
1129
+ presenter.deck.slides.length
1130
+ );
977
1131
  }
978
1132
  if (presenter.progressBar) {
979
1133
  updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
@@ -998,49 +1152,64 @@ async function prevSlide(presenter) {
998
1152
  const loop = presenter.deck.config.settings?.loop ?? false;
999
1153
  if (prevIndex < 0) {
1000
1154
  if (loop) {
1001
- clearWindows(presenter.renderer);
1155
+ clearWindows2(presenter.renderer);
1002
1156
  for (let i = 0; i < slides.length; i++) {
1003
- await renderSlide(presenter.renderer, slides[i]);
1157
+ await renderSlide2(presenter.renderer, slides[i]);
1004
1158
  }
1005
1159
  presenter.currentSlide = slides.length - 1;
1006
- if (presenter.notesWindow) {
1007
- updateNotesWindow(presenter);
1008
- }
1009
- if (presenter.progressBar) {
1010
- updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
1011
- }
1160
+ updateUIComponents(presenter, slides.length - 1);
1012
1161
  presenter.renderer.screen.render();
1013
1162
  }
1014
1163
  return;
1015
1164
  }
1016
- clearWindows(presenter.renderer);
1165
+ clearWindows2(presenter.renderer);
1017
1166
  for (let i = 0; i <= prevIndex; i++) {
1018
- await renderSlide(presenter.renderer, slides[i]);
1167
+ await renderSlide2(presenter.renderer, slides[i]);
1019
1168
  }
1020
1169
  presenter.currentSlide = prevIndex;
1021
- if (presenter.notesWindow) {
1022
- updateNotesWindow(presenter);
1023
- }
1024
- if (presenter.progressBar) {
1025
- updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
1170
+ updateUIComponents(presenter, prevIndex);
1171
+ presenter.renderer.screen.render();
1172
+ }
1173
+ async function jumpToSlide(presenter, index) {
1174
+ if (index < 0 || index >= presenter.deck.slides.length) return;
1175
+ clearWindows2(presenter.renderer);
1176
+ for (let i = 0; i <= index; i++) {
1177
+ await renderSlide2(presenter.renderer, presenter.deck.slides[i]);
1026
1178
  }
1179
+ presenter.currentSlide = index;
1180
+ updateUIComponents(presenter, index);
1027
1181
  presenter.renderer.screen.render();
1028
1182
  }
1029
- async function jumpToSlide(presenter, index) {
1030
- if (index < 0 || index >= presenter.deck.slides.length) return;
1031
- clearWindows(presenter.renderer);
1032
- for (let i = 0; i <= index; i++) {
1033
- await renderSlide(presenter.renderer, presenter.deck.slides[i]);
1034
- }
1035
- presenter.currentSlide = index;
1183
+ function updateProgress(progressBar, current, total) {
1184
+ const progress = (current + 1) / total * 100;
1185
+ progressBar.setProgress(progress);
1186
+ }
1187
+ function updateUIComponents(presenter, currentIndex) {
1188
+ const { slides } = presenter.deck;
1189
+ const currentSlide = slides[currentIndex];
1190
+ const nextSlide2 = slides[currentIndex + 1];
1036
1191
  if (presenter.notesWindow) {
1037
- updateNotesWindow(presenter);
1192
+ updateNotesWindow(
1193
+ presenter.notesWindow,
1194
+ currentSlide,
1195
+ nextSlide2,
1196
+ currentIndex,
1197
+ slides.length
1198
+ );
1038
1199
  }
1039
1200
  if (presenter.progressBar) {
1040
- updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
1201
+ updateProgress(presenter.progressBar, currentIndex, slides.length);
1041
1202
  }
1042
- presenter.renderer.screen.render();
1043
1203
  }
1204
+ var init_navigation = __esm({
1205
+ "src/presenter/navigation.ts"() {
1206
+ init_esm_shims();
1207
+ init_screen();
1208
+ init_notes_window();
1209
+ }
1210
+ });
1211
+
1212
+ // src/presenter/keyboard-controls.ts
1044
1213
  function setupControls(presenter) {
1045
1214
  const { screen } = presenter.renderer;
1046
1215
  screen.key(["space", "enter", "right", "n"], () => {
@@ -1097,93 +1266,61 @@ function showSlideList(presenter) {
1097
1266
  jumpToSlide(presenter, parseInt(ch ?? "0", 10));
1098
1267
  });
1099
1268
  }
1100
- function toggleNotesVisibility(notesWindow) {
1101
- const { contentBox, screen } = notesWindow;
1102
- contentBox.toggle();
1103
- screen.render();
1104
- }
1105
- async function findAvailableTty() {
1106
- const candidates = [
1107
- "/dev/ttys001",
1108
- "/dev/ttys002",
1109
- "/dev/ttys003",
1110
- "/dev/pts/1",
1111
- "/dev/pts/2"
1112
- ];
1113
- for (const tty of candidates) {
1114
- try {
1115
- await access(tty);
1116
- return tty;
1117
- } catch {
1118
- }
1269
+ var init_keyboard_controls = __esm({
1270
+ "src/presenter/keyboard-controls.ts"() {
1271
+ init_esm_shims();
1272
+ init_navigation();
1273
+ init_notes_window();
1119
1274
  }
1120
- throw new Error(
1121
- "Could not find available TTY for notes window. Open a second terminal, run `tty`, and pass the path with --notes-tty"
1122
- );
1123
- }
1124
- async function createNotesWindow(ttyPath) {
1125
- const blessed3 = (await import('neo-blessed')).default;
1126
- const { openSync } = await import('fs');
1127
- const tty = ttyPath ?? await findAvailableTty();
1128
- const screen = blessed3.screen({
1129
- smartCSR: true,
1130
- title: "term-deck notes",
1131
- fullUnicode: true,
1132
- input: openSync(tty, "r"),
1133
- output: openSync(tty, "w")
1134
- });
1135
- const contentBox = blessed3.box({
1136
- top: 0,
1137
- left: 0,
1138
- width: "100%",
1139
- height: "100%",
1140
- tags: true,
1141
- padding: 2,
1142
- style: {
1143
- fg: "#ffffff",
1144
- bg: "#1a1a1a"
1145
- }
1146
- });
1147
- screen.append(contentBox);
1148
- screen.render();
1149
- return {
1150
- screen,
1151
- contentBox,
1152
- tty
1275
+ });
1276
+
1277
+ // src/presenter/main.ts
1278
+ var main_exports = {};
1279
+ __export(main_exports, {
1280
+ jumpToSlide: () => jumpToSlide,
1281
+ present: () => present,
1282
+ prevSlide: () => prevSlide
1283
+ });
1284
+ async function present(slidesDir, options = {}) {
1285
+ const deck = await loadDeck(slidesDir);
1286
+ if (deck.slides.length === 0) {
1287
+ throw new Error(`No slides found in ${slidesDir}`);
1288
+ }
1289
+ const renderer = createRenderer(deck.config.theme);
1290
+ const presenter = {
1291
+ deck,
1292
+ renderer,
1293
+ currentSlide: options.startSlide ?? deck.config.settings?.startSlide ?? 0,
1294
+ isAnimating: false,
1295
+ notesWindow: null,
1296
+ autoAdvanceTimer: null,
1297
+ progressBar: null
1153
1298
  };
1154
- }
1155
- function updateNotesWindow(presenter) {
1156
- if (!presenter.notesWindow) return;
1157
- const { contentBox, screen } = presenter.notesWindow;
1158
- const { slides } = presenter.deck;
1159
- const currentIndex = presenter.currentSlide;
1160
- const currentSlide = slides[currentIndex];
1161
- const nextSlide2 = slides[currentIndex + 1];
1162
- let content = "";
1163
- content += `{bold}Slide ${currentIndex + 1} of ${slides.length}{/bold}
1164
- `;
1165
- content += `{gray-fg}${currentSlide.frontmatter.title}{/}
1166
- `;
1167
- content += "\n";
1168
- content += "\u2500".repeat(50) + "\n";
1169
- content += "\n";
1170
- if (currentSlide.notes) {
1171
- content += "{bold}PRESENTER NOTES:{/bold}\n\n";
1172
- content += currentSlide.notes + "\n";
1173
- } else {
1174
- content += "{gray-fg}No notes for this slide{/}\n";
1299
+ if (options.showNotes) {
1300
+ presenter.notesWindow = await createNotesWindow(options.notesTty);
1175
1301
  }
1176
- content += "\n";
1177
- content += "\u2500".repeat(50) + "\n";
1178
- content += "\n";
1179
- if (nextSlide2) {
1180
- content += `{bold}NEXT:{/bold} "${nextSlide2.frontmatter.title}"
1181
- `;
1182
- } else {
1183
- content += "{gray-fg}Last slide{/}\n";
1302
+ if (deck.config.settings?.showProgress) {
1303
+ presenter.progressBar = createProgressBar(presenter);
1184
1304
  }
1185
- contentBox.setContent(content);
1186
- screen.render();
1305
+ setupControls(presenter);
1306
+ await showSlide(presenter, presenter.currentSlide);
1307
+ if (presenter.progressBar) {
1308
+ updateProgress(presenter.progressBar, presenter.currentSlide, deck.slides.length);
1309
+ }
1310
+ presenter.autoAdvanceTimer = startAutoAdvance(presenter);
1311
+ await new Promise((resolve) => {
1312
+ renderer.screen.key(["q", "C-c", "escape"], () => {
1313
+ cleanup(presenter);
1314
+ resolve();
1315
+ });
1316
+ });
1317
+ }
1318
+ function cleanup(presenter) {
1319
+ stopAutoAdvance(presenter.autoAdvanceTimer);
1320
+ if (presenter.notesWindow) {
1321
+ destroyNotesWindow(presenter.notesWindow);
1322
+ }
1323
+ destroyRenderer(presenter.renderer);
1187
1324
  }
1188
1325
  function startAutoAdvance(presenter) {
1189
1326
  const interval = presenter.deck.config.settings?.autoAdvance;
@@ -1217,15 +1354,15 @@ function createProgressBar(presenter) {
1217
1354
  screen.append(progressBar);
1218
1355
  return progressBar;
1219
1356
  }
1220
- function updateProgress(progressBar, current, total) {
1221
- const progress = (current + 1) / total * 100;
1222
- progressBar.setProgress(progress);
1223
- }
1224
1357
  var init_main = __esm({
1225
1358
  "src/presenter/main.ts"() {
1226
1359
  init_esm_shims();
1227
- init_slide2();
1360
+ init_deck_loader();
1228
1361
  init_screen();
1362
+ init_notes_window();
1363
+ init_keyboard_controls();
1364
+ init_navigation();
1365
+ init_navigation();
1229
1366
  }
1230
1367
  });
1231
1368
 
@@ -1233,7 +1370,7 @@ var init_main = __esm({
1233
1370
  init_esm_shims();
1234
1371
 
1235
1372
  // package.json
1236
- var version = "1.0.15";
1373
+ var version = "1.0.16";
1237
1374
 
1238
1375
  // src/cli/commands/present.ts
1239
1376
  init_esm_shims();
@@ -1243,6 +1380,7 @@ init_main();
1243
1380
  init_esm_shims();
1244
1381
  init_validation();
1245
1382
  init_slide2();
1383
+ init_deck_loader();
1246
1384
  init_theme2();
1247
1385
  function handleError(error) {
1248
1386
  if (error instanceof ValidationError) {
@@ -1316,6 +1454,44 @@ init_esm_shims();
1316
1454
 
1317
1455
  // src/export/recorder.ts
1318
1456
  init_esm_shims();
1457
+
1458
+ // src/export/recording-session.ts
1459
+ init_esm_shims();
1460
+ async function createRecordingSession(options) {
1461
+ const tempDir = join(tmpdir(), `term-deck-export-${Date.now()}`);
1462
+ await mkdir(tempDir, { recursive: true });
1463
+ return {
1464
+ tempDir,
1465
+ frameCount: 0,
1466
+ width: options.width ?? 120,
1467
+ height: options.height ?? 40,
1468
+ fps: options.fps ?? 30
1469
+ };
1470
+ }
1471
+ async function saveFrame(session, png) {
1472
+ const frameNum = session.frameCount.toString().padStart(6, "0");
1473
+ const framePath = join(session.tempDir, `frame_${frameNum}.png`);
1474
+ await writeFile(framePath, png);
1475
+ session.frameCount++;
1476
+ }
1477
+ async function cleanupSession(session) {
1478
+ await rm(session.tempDir, { recursive: true, force: true });
1479
+ }
1480
+
1481
+ // src/export/presentation-exporter.ts
1482
+ init_esm_shims();
1483
+ init_deck_loader();
1484
+ init_screen();
1485
+
1486
+ // src/renderer/types/screen.ts
1487
+ init_esm_shims();
1488
+ function setScreenDimensions(screen, width, height) {
1489
+ screen.width = width;
1490
+ screen.height = height;
1491
+ }
1492
+
1493
+ // src/export/utils/virtual-terminal.ts
1494
+ init_esm_shims();
1319
1495
  var VirtualTerminal = class {
1320
1496
  constructor(width, height) {
1321
1497
  this.width = width;
@@ -1332,7 +1508,7 @@ var VirtualTerminal = class {
1332
1508
  buffer;
1333
1509
  colors;
1334
1510
  /**
1335
- * Set character at position
1511
+ * Set character at position with optional color
1336
1512
  */
1337
1513
  setChar(x, y, char, color = "#ffffff") {
1338
1514
  if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
@@ -1341,7 +1517,7 @@ var VirtualTerminal = class {
1341
1517
  }
1342
1518
  }
1343
1519
  /**
1344
- * Clear the buffer
1520
+ * Clear the buffer to blank spaces
1345
1521
  */
1346
1522
  clear() {
1347
1523
  for (let y = 0; y < this.height; y++) {
@@ -1352,11 +1528,23 @@ var VirtualTerminal = class {
1352
1528
  }
1353
1529
  }
1354
1530
  /**
1355
- * Get buffer as string (for debugging)
1531
+ * Get buffer contents as plain text string
1356
1532
  */
1357
1533
  toString() {
1358
1534
  return this.buffer.map((row) => row.join("")).join("\n");
1359
1535
  }
1536
+ /**
1537
+ * Get the raw character buffer
1538
+ */
1539
+ getBuffer() {
1540
+ return this.buffer;
1541
+ }
1542
+ /**
1543
+ * Get the color buffer
1544
+ */
1545
+ getColors() {
1546
+ return this.colors;
1547
+ }
1360
1548
  /**
1361
1549
  * Convert buffer to PNG image data
1362
1550
  */
@@ -1386,33 +1574,12 @@ async function renderTerminalToPng(buffer, colors, width, height) {
1386
1574
  }
1387
1575
  return canvas.toBuffer("image/png");
1388
1576
  }
1389
- function captureScreen(screen, vt) {
1390
- const lines = screen.lines || [];
1391
- for (let y = 0; y < Math.min(lines.length, vt.height); y++) {
1392
- const line = lines[y];
1393
- if (!line) continue;
1394
- for (let x = 0; x < Math.min(line.length, vt.width); x++) {
1395
- const cell = line[x];
1396
- if (!cell) continue;
1397
- const char = Array.isArray(cell) ? cell[0] : cell;
1398
- const attr = Array.isArray(cell) ? cell[1] : null;
1399
- const color = extractColor(attr) || "#ffffff";
1400
- vt.setChar(x, y, char || " ", color);
1401
- }
1402
- }
1403
- }
1404
- function extractColor(attr) {
1405
- if (!attr) return null;
1406
- if (typeof attr === "object" && attr.fg !== void 0) {
1407
- if (typeof attr.fg === "string" && attr.fg.startsWith("#")) {
1408
- return attr.fg;
1409
- }
1410
- if (typeof attr.fg === "number") {
1411
- return ansi256ToHex(attr.fg);
1412
- }
1413
- }
1414
- return null;
1415
- }
1577
+
1578
+ // src/export/capture/screen-capture.ts
1579
+ init_esm_shims();
1580
+
1581
+ // src/export/utils/color-conversion.ts
1582
+ init_esm_shims();
1416
1583
  function ansi256ToHex(code) {
1417
1584
  const standard16 = [
1418
1585
  "#000000",
@@ -1446,31 +1613,38 @@ function ansi256ToHex(code) {
1446
1613
  const hex = gray.toString(16).padStart(2, "0");
1447
1614
  return `#${hex}${hex}${hex}`;
1448
1615
  }
1449
- async function createRecordingSession(options) {
1450
- const { tmpdir } = await import('os');
1451
- const { join: join3 } = await import('path');
1452
- const { mkdir: mkdir2 } = await import('fs/promises');
1453
- const tempDir = join3(tmpdir(), `term-deck-export-${Date.now()}`);
1454
- await mkdir2(tempDir, { recursive: true });
1455
- return {
1456
- tempDir,
1457
- frameCount: 0,
1458
- width: options.width ?? 120,
1459
- height: options.height ?? 40,
1460
- fps: options.fps ?? 30
1461
- };
1462
- }
1463
- async function saveFrame(session, png) {
1464
- const { join: join3 } = await import('path');
1465
- const frameNum = session.frameCount.toString().padStart(6, "0");
1466
- const framePath = join3(session.tempDir, `frame_${frameNum}.png`);
1467
- await writeFile(framePath, png);
1468
- session.frameCount++;
1616
+ function extractColor(attr) {
1617
+ if (!attr) return null;
1618
+ if (typeof attr === "object" && attr.fg !== void 0) {
1619
+ if (typeof attr.fg === "string" && attr.fg.startsWith("#")) {
1620
+ return attr.fg;
1621
+ }
1622
+ if (typeof attr.fg === "number") {
1623
+ return ansi256ToHex(attr.fg);
1624
+ }
1625
+ }
1626
+ return null;
1469
1627
  }
1470
- async function cleanupSession(session) {
1471
- const { rm } = await import('fs/promises');
1472
- await rm(session.tempDir, { recursive: true, force: true });
1628
+
1629
+ // src/export/capture/screen-capture.ts
1630
+ function captureScreen(screen, vt) {
1631
+ const lines = screen.lines || [];
1632
+ for (let y = 0; y < Math.min(lines.length, vt.height); y++) {
1633
+ const line = lines[y];
1634
+ if (!line) continue;
1635
+ for (let x = 0; x < Math.min(line.length, vt.width); x++) {
1636
+ const cell = line[x];
1637
+ if (!cell) continue;
1638
+ const char = Array.isArray(cell) ? cell[0] : cell;
1639
+ const attr = Array.isArray(cell) ? cell[1] : null;
1640
+ const color = extractColor(attr) || "#ffffff";
1641
+ vt.setChar(x, y, char || " ", color);
1642
+ }
1643
+ }
1473
1644
  }
1645
+
1646
+ // src/export/encoding/ffmpeg-encoder.ts
1647
+ init_esm_shims();
1474
1648
  async function checkFfmpeg() {
1475
1649
  try {
1476
1650
  await execa("which", ["ffmpeg"]);
@@ -1487,36 +1661,38 @@ function detectFormat(output) {
1487
1661
  `Unknown output format for ${output}. Use .mp4 or .gif extension.`
1488
1662
  );
1489
1663
  }
1490
- async function encodeVideo(session, output, format, quality) {
1491
- const { join: join3 } = await import('path');
1492
- const inputPattern = join3(session.tempDir, "frame_%06d.png");
1664
+ async function encodeVideo(options) {
1665
+ const { inputPattern, output, format, fps, quality } = options;
1493
1666
  if (format === "mp4") {
1494
- await encodeMp4(inputPattern, output, session.fps, quality ?? 80);
1667
+ await encodeMp4(inputPattern, output, fps, quality ?? 80);
1495
1668
  } else {
1496
- await encodeGif(inputPattern, output, session.fps);
1669
+ await encodeGif(inputPattern, output, fps);
1497
1670
  }
1498
1671
  }
1499
1672
  async function encodeMp4(input, output, fps, quality) {
1500
1673
  const crf = Math.round(51 - quality / 100 * 33);
1501
1674
  await execa("ffmpeg", [
1502
1675
  "-y",
1676
+ // Overwrite output file
1503
1677
  "-framerate",
1504
1678
  fps.toString(),
1505
1679
  "-i",
1506
1680
  input,
1507
1681
  "-c:v",
1508
1682
  "libx264",
1683
+ // H.264 codec
1509
1684
  "-crf",
1510
1685
  crf.toString(),
1511
1686
  "-pix_fmt",
1512
1687
  "yuv420p",
1688
+ // Pixel format for compatibility
1513
1689
  output
1514
1690
  ]);
1515
1691
  }
1516
1692
  async function encodeGif(input, output, fps) {
1517
- const { tmpdir } = await import('os');
1518
- const { join: join3 } = await import('path');
1519
- const paletteFile = join3(tmpdir(), `palette-${Date.now()}.png`);
1693
+ const { tmpdir: tmpdir2 } = await import('os');
1694
+ const { join: join5 } = await import('path');
1695
+ const paletteFile = join5(tmpdir2(), `palette-${Date.now()}.png`);
1520
1696
  try {
1521
1697
  await execa("ffmpeg", [
1522
1698
  "-y",
@@ -1547,20 +1723,29 @@ async function encodeGif(input, output, fps) {
1547
1723
  }
1548
1724
  }
1549
1725
  }
1726
+
1727
+ // src/export/presentation-exporter.ts
1728
+ async function encodeFramesToVideo(tempDir, output, format, fps, quality) {
1729
+ const inputPattern = join(tempDir, "frame_%06d.png");
1730
+ await encodeVideo({
1731
+ inputPattern,
1732
+ output,
1733
+ format,
1734
+ fps,
1735
+ quality
1736
+ });
1737
+ }
1550
1738
  async function exportPresentation(slidesDir, options) {
1551
1739
  await checkFfmpeg();
1552
1740
  const format = detectFormat(options.output);
1553
- const { loadDeck: loadDeck2 } = await Promise.resolve().then(() => (init_slide2(), slide_exports));
1554
- const deck = await loadDeck2(slidesDir);
1741
+ const deck = await loadDeck(slidesDir);
1555
1742
  if (deck.slides.length === 0) {
1556
1743
  throw new Error(`No slides found in ${slidesDir}`);
1557
1744
  }
1558
1745
  const session = await createRecordingSession(options);
1559
1746
  const vt = new VirtualTerminal(session.width, session.height);
1560
- const { createRenderer: createRenderer2, destroyRenderer: destroyRenderer2, renderSlide: renderSlide2 } = await Promise.resolve().then(() => (init_screen(), screen_exports));
1561
- const renderer = createRenderer2(deck.config.theme);
1562
- renderer.screen.width = session.width;
1563
- renderer.screen.height = session.height;
1747
+ const renderer = createRenderer(deck.config.theme);
1748
+ setScreenDimensions(renderer.screen, session.width, session.height);
1564
1749
  const slideTime = options.slideTime ?? 3;
1565
1750
  const framesPerSlide = session.fps * slideTime;
1566
1751
  console.log(`Exporting ${deck.slides.length} slides...`);
@@ -1577,14 +1762,25 @@ async function exportPresentation(slidesDir, options) {
1577
1762
  }
1578
1763
  }
1579
1764
  console.log("Encoding video...");
1580
- await encodeVideo(session, options.output, format, options.quality);
1765
+ await encodeFramesToVideo(
1766
+ session.tempDir,
1767
+ options.output,
1768
+ format,
1769
+ session.fps,
1770
+ options.quality
1771
+ );
1581
1772
  console.log(`Exported to ${options.output}`);
1582
1773
  } finally {
1583
- destroyRenderer2(renderer);
1774
+ destroyRenderer(renderer);
1584
1775
  await cleanupSession(session);
1585
1776
  }
1586
1777
  }
1587
1778
 
1779
+ // src/export/ansi-recorder.ts
1780
+ init_esm_shims();
1781
+ init_deck_loader();
1782
+ init_screen();
1783
+
1588
1784
  // src/cli/commands/export.ts
1589
1785
  var exportCommand = new Command("export").description("Export presentation to GIF or MP4").argument("<dir>", "Slides directory").requiredOption("-o, --output <file>", "Output file (.mp4 or .gif)").option("-w, --width <n>", "Terminal width in characters", "120").option("-h, --height <n>", "Terminal height in characters", "40").option("--fps <n>", "Frames per second", "30").option("-t, --slide-time <n>", "Seconds per slide", "3").option("-q, --quality <n>", "Quality 1-100 (video only)", "80").action(async (dir, options) => {
1590
1786
  try {