@pep/term-deck 1.0.15 → 1.0.17

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,9 @@ async function loadDeckConfig(slidesDir) {
447
333
  theme: DEFAULT_THEME
448
334
  };
449
335
  }
450
- const configModule = await import(configPath + "?t=" + Date.now());
336
+ const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
337
+ const cacheBuster = isTest ? `?t=${Date.now()}-${Math.random()}` : "";
338
+ const configModule = await import(configPath + cacheBuster);
451
339
  if (!configModule.default) {
452
340
  throw new Error("deck.config.ts must export default config");
453
341
  }
@@ -471,50 +359,18 @@ async function loadDeck(slidesDir) {
471
359
  basePath: slidesDir
472
360
  };
473
361
  }
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"() {
362
+ var DeckLoadError;
363
+ var init_deck_loader = __esm({
364
+ "src/core/deck-loader.ts"() {
492
365
  init_esm_shims();
493
- init_slide();
494
366
  init_config();
495
367
  init_validation();
496
368
  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 {
369
+ init_slide2();
370
+ DeckLoadError = class extends Error {
502
371
  /**
503
372
  * @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
373
+ * @param slidesDir - Path to the directory that was being loaded
518
374
  * @param cause - Optional underlying error that caused this failure
519
375
  */
520
376
  constructor(message, slidesDir, cause) {
@@ -526,43 +382,14 @@ var init_slide2 = __esm({
526
382
  };
527
383
  }
528
384
  });
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
385
  function generateTrail(glyphs, length) {
559
386
  return Array.from(
560
387
  { length },
561
388
  () => glyphs[Math.floor(Math.random() * glyphs.length)]
562
389
  );
563
390
  }
564
- function renderMatrixRain(renderer) {
565
- const { screen, matrixBox, matrixDrops, theme } = renderer;
391
+ function renderMatrixRain(screen, state) {
392
+ const { matrixBox, matrixDrops, theme } = state;
566
393
  const width = Math.max(20, screen.width || 80);
567
394
  const height = Math.max(10, screen.height || 24);
568
395
  const grid = Array.from(
@@ -597,27 +424,32 @@ function renderMatrixRain(renderer) {
597
424
  }
598
425
  matrixBox.setContent(output);
599
426
  }
600
- function initMatrixRain(renderer) {
601
- const { screen, theme } = renderer;
427
+ function initMatrixRain(screen, state) {
428
+ const { theme } = state;
602
429
  const width = screen.width || 80;
603
430
  const height = screen.height || 24;
604
431
  const density = theme.animations.matrixDensity;
605
- renderer.matrixDrops = [];
432
+ state.matrixDrops = [];
606
433
  for (let i = 0; i < density; i++) {
607
- renderer.matrixDrops.push({
434
+ state.matrixDrops.push({
608
435
  x: Math.floor(Math.random() * width),
609
436
  y: Math.floor(Math.random() * height),
610
437
  speed: 0.3 + Math.random() * 0.7,
611
438
  trail: generateTrail(theme.glyphs, 5 + Math.floor(Math.random() * 10))
612
439
  });
613
440
  }
614
- renderer.matrixInterval = setInterval(() => {
615
- renderMatrixRain(renderer);
616
- renderer.screen.render();
441
+ state.matrixInterval = setInterval(() => {
442
+ renderMatrixRain(screen, state);
443
+ screen.render();
617
444
  }, theme.animations.matrixInterval);
618
445
  }
619
- function createRenderer(theme) {
620
- const screen = createScreen();
446
+ function stopMatrixRain(state) {
447
+ if (state.matrixInterval) {
448
+ clearInterval(state.matrixInterval);
449
+ state.matrixInterval = null;
450
+ }
451
+ }
452
+ function createMatrixBox(screen) {
621
453
  const matrixBox = blessed.box({
622
454
  top: 0,
623
455
  left: 0,
@@ -626,28 +458,13 @@ function createRenderer(theme) {
626
458
  tags: true
627
459
  });
628
460
  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;
461
+ return matrixBox;
639
462
  }
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();
463
+ var init_matrix_rain = __esm({
464
+ "src/renderer/effects/matrix-rain.ts"() {
465
+ init_esm_shims();
647
466
  }
648
- renderer.windowStack = [];
649
- renderer.screen.destroy();
650
- }
467
+ });
651
468
  function getWindowColor(index, theme) {
652
469
  const colors = [
653
470
  theme.colors.primary,
@@ -662,8 +479,7 @@ function getWindowColor(index, theme) {
662
479
  ];
663
480
  return colors[index % colors.length];
664
481
  }
665
- function createWindow(renderer, options) {
666
- const { screen, windowStack, theme } = renderer;
482
+ function createWindow(screen, windowStack, theme, options) {
667
483
  const windowIndex = windowStack.length;
668
484
  const color = options.color ?? getWindowColor(windowIndex, theme);
669
485
  const screenWidth = screen.width || 120;
@@ -699,144 +515,178 @@ function createWindow(renderer, options) {
699
515
  windowStack.push(box);
700
516
  return box;
701
517
  }
702
- function clearWindows(renderer) {
703
- for (const window of renderer.windowStack) {
518
+ function clearWindows(windowStack) {
519
+ for (const window of windowStack) {
704
520
  window.destroy();
705
521
  }
706
- renderer.windowStack = [];
522
+ windowStack.length = 0;
707
523
  }
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;
524
+ var init_window_manager = __esm({
525
+ "src/renderer/window-manager.ts"() {
526
+ init_esm_shims();
527
+ }
528
+ });
529
+
530
+ // src/core/theme-errors.ts
531
+ var ThemeError;
532
+ var init_theme_errors = __esm({
533
+ "src/core/theme-errors.ts"() {
534
+ init_esm_shims();
535
+ init_validation();
536
+ ThemeError = class extends Error {
537
+ /**
538
+ * @param message - The error message
539
+ * @param themeName - Optional name of the theme that caused the error
540
+ * @param path - Optional path to the theme file or package
541
+ */
542
+ constructor(message, themeName, path2) {
543
+ super(message);
544
+ this.themeName = themeName;
545
+ this.path = path2;
546
+ this.name = "ThemeError";
714
547
  }
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));
548
+ };
549
+ }
550
+ });
551
+ function resolveColorToken(token, theme) {
552
+ switch (token) {
553
+ case "PRIMARY":
554
+ return theme.colors.primary;
555
+ case "SECONDARY":
556
+ return theme.colors.secondary ?? theme.colors.primary;
557
+ case "ACCENT":
558
+ return theme.colors.accent;
559
+ case "MUTED":
560
+ return theme.colors.muted;
561
+ case "TEXT":
562
+ return theme.colors.text;
563
+ case "BACKGROUND":
564
+ return theme.colors.background;
565
+ }
566
+ return BUILTIN_COLORS[token] ?? theme.colors.text;
728
567
  }
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;
568
+ function colorTokensToBlessedTags(content, theme) {
569
+ return content.replace(
570
+ /\{(GREEN|ORANGE|CYAN|PINK|WHITE|GRAY|PRIMARY|SECONDARY|ACCENT|MUTED|TEXT|BACKGROUND|\/)\}/g,
571
+ (_, token) => {
572
+ if (token === "/") {
573
+ return "{/}";
740
574
  }
575
+ const color = resolveColorToken(token, theme);
576
+ return `{${color}-fg}`;
741
577
  }
742
- box.setContent([...currentLines, scrambledLine].join("\n"));
743
- screen.render();
744
- await sleep(20);
745
- }
578
+ );
746
579
  }
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
- }
580
+ var BUILTIN_COLORS;
581
+ var init_theme_colors = __esm({
582
+ "src/core/theme-colors.ts"() {
583
+ init_esm_shims();
584
+ BUILTIN_COLORS = {
585
+ GREEN: "#00cc66",
586
+ ORANGE: "#ff6600",
587
+ CYAN: "#00ccff",
588
+ PINK: "#ff0066",
589
+ WHITE: "#ffffff",
590
+ GRAY: "#666666"
591
+ };
592
+ }
593
+ });
594
+ var init_theme2 = __esm({
595
+ "src/core/theme.ts"() {
596
+ init_esm_shims();
597
+ init_theme();
598
+ init_validation();
599
+ init_theme_errors();
600
+ init_theme_colors();
760
601
  }
602
+ });
603
+ function hasMermaidDiagrams(content) {
604
+ MERMAID_BLOCK_PATTERN.lastIndex = 0;
605
+ return MERMAID_BLOCK_PATTERN.test(content);
761
606
  }
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);
607
+ function extractMermaidBlocks(content) {
608
+ const blocks = [];
609
+ let match;
610
+ MERMAID_BLOCK_PATTERN.lastIndex = 0;
611
+ while ((match = MERMAID_BLOCK_PATTERN.exec(content)) !== null) {
612
+ blocks.push(match[1].trim());
778
613
  }
779
- box.setContent(content);
780
- screen.render();
614
+ return blocks;
781
615
  }
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
- }
616
+ function formatMermaidError(code, _error) {
617
+ const lines = [
618
+ "\u250C\u2500 Diagram (parse error) \u2500\u2510",
619
+ "\u2502 \u2502"
620
+ ];
621
+ const codeLines = code.split("\n").slice(0, 5);
622
+ for (const line of codeLines) {
623
+ const truncated = line.slice(0, 23).padEnd(23);
624
+ lines.push(`\u2502 ${truncated} \u2502`);
792
625
  }
626
+ if (code.split("\n").length > 5) {
627
+ lines.push("\u2502 ... \u2502");
628
+ }
629
+ lines.push("\u2502 \u2502");
630
+ 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");
631
+ return lines.join("\n");
793
632
  }
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();
633
+ function mermaidToAscii(mermaidCode) {
634
+ try {
635
+ return mermaidToAscii$1(mermaidCode);
636
+ } catch (error) {
637
+ return formatMermaidError(mermaidCode);
812
638
  }
813
639
  }
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";
640
+ function escapeRegex(str) {
641
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
642
+ }
643
+ function processMermaidDiagrams(content) {
644
+ if (!hasMermaidDiagrams(content)) {
645
+ return content;
827
646
  }
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;
647
+ let result = content;
648
+ const blocks = extractMermaidBlocks(content);
649
+ for (const block of blocks) {
650
+ const ascii = mermaidToAscii(block);
651
+ result = result.replace(
652
+ new RegExp("```mermaid\\n" + escapeRegex(block) + "\\n?```", "g"),
653
+ ascii
654
+ );
655
+ }
656
+ return result;
833
657
  }
834
- var GLITCH_CHARS, PROTECTED_CHARS;
835
- var init_screen = __esm({
836
- "src/renderer/screen.ts"() {
658
+ var MERMAID_BLOCK_PATTERN;
659
+ var init_mermaid = __esm({
660
+ "src/core/utils/mermaid.ts"() {
837
661
  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";
662
+ MERMAID_BLOCK_PATTERN = /```mermaid\n([\s\S]*?)```/g;
663
+ }
664
+ });
665
+
666
+ // src/core/content-processor.ts
667
+ async function processSlideContent(body, theme) {
668
+ let processed = processMermaidDiagrams(body);
669
+ processed = colorTokensToBlessedTags(processed, theme);
670
+ return processed;
671
+ }
672
+ function normalizeBigText(bigText) {
673
+ if (!bigText) return [];
674
+ return Array.isArray(bigText) ? bigText : [bigText];
675
+ }
676
+ var init_content_processor = __esm({
677
+ "src/core/content-processor.ts"() {
678
+ init_esm_shims();
679
+ init_theme2();
680
+ init_mermaid();
681
+ }
682
+ });
683
+
684
+ // src/renderer/animations/constants.ts
685
+ var GLITCH_CHARS, PROTECTED_CHARS;
686
+ var init_constants = __esm({
687
+ "src/renderer/animations/constants.ts"() {
688
+ init_esm_shims();
689
+ 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
690
  PROTECTED_CHARS = /* @__PURE__ */ new Set([
841
691
  " ",
842
692
  " ",
@@ -915,65 +765,370 @@ var init_screen = __esm({
915
765
  }
916
766
  });
917
767
 
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
768
+ // src/renderer/animations/helpers/animation-utils.ts
769
+ function sleep(ms) {
770
+ return new Promise((resolve) => setTimeout(resolve, ms));
771
+ }
772
+ function renderContent(box, screen, content) {
773
+ box.setContent(content);
774
+ screen.render();
775
+ }
776
+ var init_animation_utils = __esm({
777
+ "src/renderer/animations/helpers/animation-utils.ts"() {
778
+ init_esm_shims();
779
+ }
925
780
  });
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}`);
781
+
782
+ // src/renderer/animations/transitions/glitch-transition.ts
783
+ async function glitchLine(box, screen, currentLines, newLine, iterations = 5) {
784
+ for (let i = iterations; i >= 0; i--) {
785
+ const scrambleRatio = i / iterations;
786
+ let scrambledLine = "";
787
+ for (const char of newLine) {
788
+ if (PROTECTED_CHARS.has(char)) {
789
+ scrambledLine += char;
790
+ } else if (Math.random() < scrambleRatio) {
791
+ scrambledLine += GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
792
+ } else {
793
+ scrambledLine += char;
794
+ }
795
+ }
796
+ renderContent(box, screen, [...currentLines, scrambledLine].join("\n"));
797
+ await sleep(20);
930
798
  }
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);
799
+ }
800
+ async function lineByLineReveal(box, screen, content, theme) {
801
+ const lines = content.split("\n");
802
+ const revealedLines = [];
803
+ const lineDelay = theme.animations.lineDelay;
804
+ const glitchIterations = theme.animations.glitchIterations;
805
+ for (const line of lines) {
806
+ await glitchLine(box, screen, revealedLines, line, glitchIterations);
807
+ revealedLines.push(line);
808
+ renderContent(box, screen, revealedLines.join("\n"));
809
+ if (line.trim()) {
810
+ await sleep(lineDelay);
811
+ }
943
812
  }
944
- if (deck.config.settings?.showProgress) {
945
- presenter.progressBar = createProgressBar(presenter);
813
+ }
814
+ var init_glitch_transition = __esm({
815
+ "src/renderer/animations/transitions/glitch-transition.ts"() {
816
+ init_esm_shims();
817
+ init_constants();
818
+ init_animation_utils();
946
819
  }
947
- setupControls(presenter);
948
- await showSlide(presenter, presenter.currentSlide);
949
- if (presenter.progressBar) {
950
- updateProgress(presenter.progressBar, presenter.currentSlide, deck.slides.length);
820
+ });
821
+
822
+ // src/renderer/animations/transitions/fade-transition.ts
823
+ async function fadeInReveal(box, screen, content, theme) {
824
+ const steps = 10;
825
+ const delay = theme.animations.lineDelay * 2 / steps;
826
+ for (let step = 0; step < steps; step++) {
827
+ const revealRatio = step / steps;
828
+ let revealed = "";
829
+ for (const char of content) {
830
+ if (char === "\n" || PROTECTED_CHARS.has(char) || Math.random() < revealRatio) {
831
+ revealed += char;
832
+ } else {
833
+ revealed += " ";
834
+ }
835
+ }
836
+ renderContent(box, screen, revealed);
837
+ await sleep(delay);
951
838
  }
952
- presenter.autoAdvanceTimer = startAutoAdvance(presenter);
953
- await new Promise((resolve) => {
954
- renderer.screen.key(["q", "C-c", "escape"], () => {
955
- cleanup(presenter);
956
- resolve();
839
+ renderContent(box, screen, content);
840
+ }
841
+ var init_fade_transition = __esm({
842
+ "src/renderer/animations/transitions/fade-transition.ts"() {
843
+ init_esm_shims();
844
+ init_constants();
845
+ init_animation_utils();
846
+ }
847
+ });
848
+
849
+ // src/renderer/animations/transitions/typewriter-transition.ts
850
+ async function typewriterReveal(box, screen, content, theme) {
851
+ const charDelay = theme.animations.lineDelay / 5;
852
+ let revealed = "";
853
+ for (const char of content) {
854
+ revealed += char;
855
+ renderContent(box, screen, revealed);
856
+ if (char !== " " && char !== "\n") {
857
+ await sleep(charDelay);
858
+ }
859
+ }
860
+ }
861
+ var init_typewriter_transition = __esm({
862
+ "src/renderer/animations/transitions/typewriter-transition.ts"() {
863
+ init_esm_shims();
864
+ init_animation_utils();
865
+ }
866
+ });
867
+
868
+ // src/renderer/animations/transitions/instant-transition.ts
869
+ function instantReveal(box, screen, content) {
870
+ renderContent(box, screen, content);
871
+ }
872
+ var init_instant_transition = __esm({
873
+ "src/renderer/animations/transitions/instant-transition.ts"() {
874
+ init_esm_shims();
875
+ init_animation_utils();
876
+ }
877
+ });
878
+
879
+ // src/renderer/animations/transition-orchestrator.ts
880
+ async function applyTransition(box, screen, content, transition, theme) {
881
+ switch (transition) {
882
+ case "glitch":
883
+ await lineByLineReveal(box, screen, content, theme);
884
+ break;
885
+ case "fade":
886
+ await fadeInReveal(box, screen, content, theme);
887
+ break;
888
+ case "instant":
889
+ instantReveal(box, screen, content);
890
+ break;
891
+ case "typewriter":
892
+ await typewriterReveal(box, screen, content, theme);
893
+ break;
894
+ default:
895
+ instantReveal(box, screen, content);
896
+ }
897
+ }
898
+ var init_transition_orchestrator = __esm({
899
+ "src/renderer/animations/transition-orchestrator.ts"() {
900
+ init_esm_shims();
901
+ init_glitch_transition();
902
+ init_fade_transition();
903
+ init_typewriter_transition();
904
+ init_instant_transition();
905
+ }
906
+ });
907
+
908
+ // src/renderer/animations/transitions.ts
909
+ var init_transitions = __esm({
910
+ "src/renderer/animations/transitions.ts"() {
911
+ init_esm_shims();
912
+ init_transition_orchestrator();
913
+ init_glitch_transition();
914
+ init_fade_transition();
915
+ init_typewriter_transition();
916
+ init_instant_transition();
917
+ }
918
+ });
919
+ async function generateBigText(text, gradientColors, font = "Standard") {
920
+ return new Promise((resolve, reject) => {
921
+ figlet.text(text, { font }, (err, result) => {
922
+ if (err || !result) {
923
+ reject(err ?? new Error("Failed to generate figlet text"));
924
+ return;
925
+ }
926
+ const gradientFn = gradient2(gradientColors);
927
+ resolve(gradientFn(result));
957
928
  });
958
929
  });
959
930
  }
960
- function cleanup(presenter) {
961
- stopAutoAdvance(presenter.autoAdvanceTimer);
962
- if (presenter.notesWindow) {
963
- presenter.notesWindow.screen.destroy();
931
+ async function generateMultiLineBigText(lines, gradientColors, font = "Standard") {
932
+ const results = await Promise.all(
933
+ lines.map((line) => generateBigText(line, gradientColors, font))
934
+ );
935
+ return results.join("\n");
936
+ }
937
+ var init_text_generator = __esm({
938
+ "src/renderer/text-generator.ts"() {
939
+ init_esm_shims();
964
940
  }
965
- destroyRenderer(presenter.renderer);
941
+ });
942
+
943
+ // src/renderer/slide-renderer.ts
944
+ async function renderSlide(screen, windowStack, theme, slide) {
945
+ const { frontmatter, body } = slide;
946
+ const window = createWindow(screen, windowStack, theme, {
947
+ title: frontmatter.title
948
+ });
949
+ let content = "";
950
+ const bigTextLines = normalizeBigText(frontmatter.bigText);
951
+ if (bigTextLines.length > 0) {
952
+ const gradientName = frontmatter.gradient ?? "fire";
953
+ const gradientColors = theme.gradients[gradientName] ?? theme.gradients.fire;
954
+ const bigText = await generateMultiLineBigText(bigTextLines, gradientColors);
955
+ content += bigText + "\n\n";
956
+ }
957
+ const processedBody = await processSlideContent(body, theme);
958
+ content += processedBody;
959
+ const transition = frontmatter.transition ?? "glitch";
960
+ await applyTransition(window, screen, content, transition, theme);
961
+ return window;
962
+ }
963
+ var init_slide_renderer = __esm({
964
+ "src/renderer/slide-renderer.ts"() {
965
+ init_esm_shims();
966
+ init_content_processor();
967
+ init_transitions();
968
+ init_text_generator();
969
+ init_window_manager();
970
+ }
971
+ });
972
+ function createScreen(title = "term-deck") {
973
+ const screen = blessed.screen({
974
+ smartCSR: true,
975
+ title,
976
+ fullUnicode: true,
977
+ mouse: false,
978
+ altScreen: true
979
+ });
980
+ return screen;
981
+ }
982
+ function createRenderer(theme) {
983
+ const screen = createScreen();
984
+ const matrixBox = createMatrixBox(screen);
985
+ const matrixRain = {
986
+ matrixBox,
987
+ matrixDrops: [],
988
+ matrixInterval: null,
989
+ theme
990
+ };
991
+ const renderer = {
992
+ screen,
993
+ windowStack: [],
994
+ theme,
995
+ matrixRain
996
+ };
997
+ initMatrixRain(screen, matrixRain);
998
+ return renderer;
999
+ }
1000
+ function destroyRenderer(renderer) {
1001
+ stopMatrixRain(renderer.matrixRain);
1002
+ clearWindows(renderer.windowStack);
1003
+ renderer.screen.destroy();
1004
+ }
1005
+ function clearWindows2(renderer) {
1006
+ clearWindows(renderer.windowStack);
1007
+ }
1008
+ async function renderSlide2(renderer, slide) {
1009
+ return renderSlide(renderer.screen, renderer.windowStack, renderer.theme, slide);
1010
+ }
1011
+ var init_screen = __esm({
1012
+ "src/renderer/screen.ts"() {
1013
+ init_esm_shims();
1014
+ init_matrix_rain();
1015
+ init_window_manager();
1016
+ init_slide_renderer();
1017
+ init_transitions();
1018
+ init_window_manager();
1019
+ init_text_generator();
1020
+ }
1021
+ });
1022
+ async function findAvailableTty() {
1023
+ const candidates = [
1024
+ "/dev/ttys001",
1025
+ "/dev/ttys002",
1026
+ "/dev/ttys003",
1027
+ "/dev/pts/1",
1028
+ "/dev/pts/2"
1029
+ ];
1030
+ for (const tty of candidates) {
1031
+ try {
1032
+ await access(tty);
1033
+ return tty;
1034
+ } catch {
1035
+ }
1036
+ }
1037
+ throw new Error(
1038
+ "Could not find available TTY for notes window. Open a second terminal, run `tty`, and pass the path with --notes-tty"
1039
+ );
966
1040
  }
1041
+ async function createNotesWindow(ttyPath) {
1042
+ const blessed5 = (await import('neo-blessed')).default;
1043
+ const { openSync } = await import('fs');
1044
+ const tty = ttyPath ?? await findAvailableTty();
1045
+ const screen = blessed5.screen({
1046
+ smartCSR: true,
1047
+ title: "term-deck notes",
1048
+ fullUnicode: true,
1049
+ input: openSync(tty, "r"),
1050
+ output: openSync(tty, "w")
1051
+ });
1052
+ const contentBox = blessed5.box({
1053
+ top: 0,
1054
+ left: 0,
1055
+ width: "100%",
1056
+ height: "100%",
1057
+ tags: true,
1058
+ padding: 2,
1059
+ style: {
1060
+ fg: "#ffffff",
1061
+ bg: "#1a1a1a"
1062
+ }
1063
+ });
1064
+ screen.append(contentBox);
1065
+ screen.render();
1066
+ return {
1067
+ screen,
1068
+ contentBox,
1069
+ tty
1070
+ };
1071
+ }
1072
+ function updateNotesWindow(notesWindow, currentSlide, nextSlide2, currentIndex, totalSlides) {
1073
+ const { contentBox, screen } = notesWindow;
1074
+ let content = "";
1075
+ content += `{bold}Slide ${currentIndex + 1} of ${totalSlides}{/bold}
1076
+ `;
1077
+ content += `{gray-fg}${currentSlide.frontmatter.title}{/}
1078
+ `;
1079
+ content += "\n";
1080
+ content += "\u2500".repeat(50) + "\n";
1081
+ content += "\n";
1082
+ if (currentSlide.notes) {
1083
+ content += "{bold}PRESENTER NOTES:{/bold}\n\n";
1084
+ content += currentSlide.notes + "\n";
1085
+ } else {
1086
+ content += "{gray-fg}No notes for this slide{/}\n";
1087
+ }
1088
+ content += "\n";
1089
+ content += "\u2500".repeat(50) + "\n";
1090
+ content += "\n";
1091
+ if (nextSlide2) {
1092
+ content += `{bold}NEXT:{/bold} "${nextSlide2.frontmatter.title}"
1093
+ `;
1094
+ } else {
1095
+ content += "{gray-fg}Last slide{/}\n";
1096
+ }
1097
+ contentBox.setContent(content);
1098
+ screen.render();
1099
+ }
1100
+ function toggleNotesVisibility(notesWindow) {
1101
+ const { contentBox, screen } = notesWindow;
1102
+ contentBox.toggle();
1103
+ screen.render();
1104
+ }
1105
+ function destroyNotesWindow(notesWindow) {
1106
+ notesWindow.screen.destroy();
1107
+ }
1108
+ var init_notes_window = __esm({
1109
+ "src/presenter/notes-window.ts"() {
1110
+ init_esm_shims();
1111
+ }
1112
+ });
1113
+
1114
+ // src/presenter/navigation.ts
967
1115
  async function showSlide(presenter, index) {
968
1116
  if (presenter.isAnimating) return;
969
1117
  if (index < 0 || index >= presenter.deck.slides.length) return;
970
1118
  presenter.isAnimating = true;
971
1119
  presenter.currentSlide = index;
972
1120
  const slide = presenter.deck.slides[index];
973
- await renderSlide(presenter.renderer, slide);
1121
+ await renderSlide2(presenter.renderer, slide);
974
1122
  presenter.renderer.screen.render();
975
1123
  if (presenter.notesWindow) {
976
- updateNotesWindow(presenter);
1124
+ const nextSlide2 = presenter.deck.slides[index + 1];
1125
+ updateNotesWindow(
1126
+ presenter.notesWindow,
1127
+ slide,
1128
+ nextSlide2,
1129
+ index,
1130
+ presenter.deck.slides.length
1131
+ );
977
1132
  }
978
1133
  if (presenter.progressBar) {
979
1134
  updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
@@ -998,49 +1153,64 @@ async function prevSlide(presenter) {
998
1153
  const loop = presenter.deck.config.settings?.loop ?? false;
999
1154
  if (prevIndex < 0) {
1000
1155
  if (loop) {
1001
- clearWindows(presenter.renderer);
1156
+ clearWindows2(presenter.renderer);
1002
1157
  for (let i = 0; i < slides.length; i++) {
1003
- await renderSlide(presenter.renderer, slides[i]);
1158
+ await renderSlide2(presenter.renderer, slides[i]);
1004
1159
  }
1005
1160
  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
- }
1161
+ updateUIComponents(presenter, slides.length - 1);
1012
1162
  presenter.renderer.screen.render();
1013
1163
  }
1014
1164
  return;
1015
1165
  }
1016
- clearWindows(presenter.renderer);
1166
+ clearWindows2(presenter.renderer);
1017
1167
  for (let i = 0; i <= prevIndex; i++) {
1018
- await renderSlide(presenter.renderer, slides[i]);
1168
+ await renderSlide2(presenter.renderer, slides[i]);
1019
1169
  }
1020
1170
  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);
1171
+ updateUIComponents(presenter, prevIndex);
1172
+ presenter.renderer.screen.render();
1173
+ }
1174
+ async function jumpToSlide(presenter, index) {
1175
+ if (index < 0 || index >= presenter.deck.slides.length) return;
1176
+ clearWindows2(presenter.renderer);
1177
+ for (let i = 0; i <= index; i++) {
1178
+ await renderSlide2(presenter.renderer, presenter.deck.slides[i]);
1026
1179
  }
1180
+ presenter.currentSlide = index;
1181
+ updateUIComponents(presenter, index);
1027
1182
  presenter.renderer.screen.render();
1028
1183
  }
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;
1184
+ function updateProgress(progressBar, current, total) {
1185
+ const progress = (current + 1) / total * 100;
1186
+ progressBar.setProgress(progress);
1187
+ }
1188
+ function updateUIComponents(presenter, currentIndex) {
1189
+ const { slides } = presenter.deck;
1190
+ const currentSlide = slides[currentIndex];
1191
+ const nextSlide2 = slides[currentIndex + 1];
1036
1192
  if (presenter.notesWindow) {
1037
- updateNotesWindow(presenter);
1193
+ updateNotesWindow(
1194
+ presenter.notesWindow,
1195
+ currentSlide,
1196
+ nextSlide2,
1197
+ currentIndex,
1198
+ slides.length
1199
+ );
1038
1200
  }
1039
1201
  if (presenter.progressBar) {
1040
- updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
1202
+ updateProgress(presenter.progressBar, currentIndex, slides.length);
1041
1203
  }
1042
- presenter.renderer.screen.render();
1043
1204
  }
1205
+ var init_navigation = __esm({
1206
+ "src/presenter/navigation.ts"() {
1207
+ init_esm_shims();
1208
+ init_screen();
1209
+ init_notes_window();
1210
+ }
1211
+ });
1212
+
1213
+ // src/presenter/keyboard-controls.ts
1044
1214
  function setupControls(presenter) {
1045
1215
  const { screen } = presenter.renderer;
1046
1216
  screen.key(["space", "enter", "right", "n"], () => {
@@ -1097,93 +1267,61 @@ function showSlideList(presenter) {
1097
1267
  jumpToSlide(presenter, parseInt(ch ?? "0", 10));
1098
1268
  });
1099
1269
  }
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
- }
1270
+ var init_keyboard_controls = __esm({
1271
+ "src/presenter/keyboard-controls.ts"() {
1272
+ init_esm_shims();
1273
+ init_navigation();
1274
+ init_notes_window();
1119
1275
  }
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
1276
+ });
1277
+
1278
+ // src/presenter/main.ts
1279
+ var main_exports = {};
1280
+ __export(main_exports, {
1281
+ jumpToSlide: () => jumpToSlide,
1282
+ present: () => present,
1283
+ prevSlide: () => prevSlide
1284
+ });
1285
+ async function present(slidesDir, options = {}) {
1286
+ const deck = await loadDeck(slidesDir);
1287
+ if (deck.slides.length === 0) {
1288
+ throw new Error(`No slides found in ${slidesDir}`);
1289
+ }
1290
+ const renderer = createRenderer(deck.config.theme);
1291
+ const presenter = {
1292
+ deck,
1293
+ renderer,
1294
+ currentSlide: options.startSlide ?? deck.config.settings?.startSlide ?? 0,
1295
+ isAnimating: false,
1296
+ notesWindow: null,
1297
+ autoAdvanceTimer: null,
1298
+ progressBar: null
1153
1299
  };
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";
1300
+ if (options.showNotes) {
1301
+ presenter.notesWindow = await createNotesWindow(options.notesTty);
1175
1302
  }
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";
1303
+ if (deck.config.settings?.showProgress) {
1304
+ presenter.progressBar = createProgressBar(presenter);
1184
1305
  }
1185
- contentBox.setContent(content);
1186
- screen.render();
1306
+ setupControls(presenter);
1307
+ await showSlide(presenter, presenter.currentSlide);
1308
+ if (presenter.progressBar) {
1309
+ updateProgress(presenter.progressBar, presenter.currentSlide, deck.slides.length);
1310
+ }
1311
+ presenter.autoAdvanceTimer = startAutoAdvance(presenter);
1312
+ await new Promise((resolve) => {
1313
+ renderer.screen.key(["q", "C-c", "escape"], () => {
1314
+ cleanup(presenter);
1315
+ resolve();
1316
+ });
1317
+ });
1318
+ }
1319
+ function cleanup(presenter) {
1320
+ stopAutoAdvance(presenter.autoAdvanceTimer);
1321
+ if (presenter.notesWindow) {
1322
+ destroyNotesWindow(presenter.notesWindow);
1323
+ }
1324
+ destroyRenderer(presenter.renderer);
1187
1325
  }
1188
1326
  function startAutoAdvance(presenter) {
1189
1327
  const interval = presenter.deck.config.settings?.autoAdvance;
@@ -1217,15 +1355,15 @@ function createProgressBar(presenter) {
1217
1355
  screen.append(progressBar);
1218
1356
  return progressBar;
1219
1357
  }
1220
- function updateProgress(progressBar, current, total) {
1221
- const progress = (current + 1) / total * 100;
1222
- progressBar.setProgress(progress);
1223
- }
1224
1358
  var init_main = __esm({
1225
1359
  "src/presenter/main.ts"() {
1226
1360
  init_esm_shims();
1227
- init_slide2();
1361
+ init_deck_loader();
1228
1362
  init_screen();
1363
+ init_notes_window();
1364
+ init_keyboard_controls();
1365
+ init_navigation();
1366
+ init_navigation();
1229
1367
  }
1230
1368
  });
1231
1369
 
@@ -1233,7 +1371,7 @@ var init_main = __esm({
1233
1371
  init_esm_shims();
1234
1372
 
1235
1373
  // package.json
1236
- var version = "1.0.15";
1374
+ var version = "1.0.17";
1237
1375
 
1238
1376
  // src/cli/commands/present.ts
1239
1377
  init_esm_shims();
@@ -1243,6 +1381,7 @@ init_main();
1243
1381
  init_esm_shims();
1244
1382
  init_validation();
1245
1383
  init_slide2();
1384
+ init_deck_loader();
1246
1385
  init_theme2();
1247
1386
  function handleError(error) {
1248
1387
  if (error instanceof ValidationError) {
@@ -1316,6 +1455,44 @@ init_esm_shims();
1316
1455
 
1317
1456
  // src/export/recorder.ts
1318
1457
  init_esm_shims();
1458
+
1459
+ // src/export/recording-session.ts
1460
+ init_esm_shims();
1461
+ async function createRecordingSession(options) {
1462
+ const tempDir = join(tmpdir(), `term-deck-export-${Date.now()}`);
1463
+ await mkdir(tempDir, { recursive: true });
1464
+ return {
1465
+ tempDir,
1466
+ frameCount: 0,
1467
+ width: options.width ?? 120,
1468
+ height: options.height ?? 40,
1469
+ fps: options.fps ?? 30
1470
+ };
1471
+ }
1472
+ async function saveFrame(session, png) {
1473
+ const frameNum = session.frameCount.toString().padStart(6, "0");
1474
+ const framePath = join(session.tempDir, `frame_${frameNum}.png`);
1475
+ await writeFile(framePath, png);
1476
+ session.frameCount++;
1477
+ }
1478
+ async function cleanupSession(session) {
1479
+ await rm(session.tempDir, { recursive: true, force: true });
1480
+ }
1481
+
1482
+ // src/export/presentation-exporter.ts
1483
+ init_esm_shims();
1484
+ init_deck_loader();
1485
+ init_screen();
1486
+
1487
+ // src/renderer/types/screen.ts
1488
+ init_esm_shims();
1489
+ function setScreenDimensions(screen, width, height) {
1490
+ screen.width = width;
1491
+ screen.height = height;
1492
+ }
1493
+
1494
+ // src/export/utils/virtual-terminal.ts
1495
+ init_esm_shims();
1319
1496
  var VirtualTerminal = class {
1320
1497
  constructor(width, height) {
1321
1498
  this.width = width;
@@ -1332,7 +1509,7 @@ var VirtualTerminal = class {
1332
1509
  buffer;
1333
1510
  colors;
1334
1511
  /**
1335
- * Set character at position
1512
+ * Set character at position with optional color
1336
1513
  */
1337
1514
  setChar(x, y, char, color = "#ffffff") {
1338
1515
  if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
@@ -1341,7 +1518,7 @@ var VirtualTerminal = class {
1341
1518
  }
1342
1519
  }
1343
1520
  /**
1344
- * Clear the buffer
1521
+ * Clear the buffer to blank spaces
1345
1522
  */
1346
1523
  clear() {
1347
1524
  for (let y = 0; y < this.height; y++) {
@@ -1352,11 +1529,23 @@ var VirtualTerminal = class {
1352
1529
  }
1353
1530
  }
1354
1531
  /**
1355
- * Get buffer as string (for debugging)
1532
+ * Get buffer contents as plain text string
1356
1533
  */
1357
1534
  toString() {
1358
1535
  return this.buffer.map((row) => row.join("")).join("\n");
1359
1536
  }
1537
+ /**
1538
+ * Get the raw character buffer
1539
+ */
1540
+ getBuffer() {
1541
+ return this.buffer;
1542
+ }
1543
+ /**
1544
+ * Get the color buffer
1545
+ */
1546
+ getColors() {
1547
+ return this.colors;
1548
+ }
1360
1549
  /**
1361
1550
  * Convert buffer to PNG image data
1362
1551
  */
@@ -1386,33 +1575,12 @@ async function renderTerminalToPng(buffer, colors, width, height) {
1386
1575
  }
1387
1576
  return canvas.toBuffer("image/png");
1388
1577
  }
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
- }
1578
+
1579
+ // src/export/capture/screen-capture.ts
1580
+ init_esm_shims();
1581
+
1582
+ // src/export/utils/color-conversion.ts
1583
+ init_esm_shims();
1416
1584
  function ansi256ToHex(code) {
1417
1585
  const standard16 = [
1418
1586
  "#000000",
@@ -1446,31 +1614,38 @@ function ansi256ToHex(code) {
1446
1614
  const hex = gray.toString(16).padStart(2, "0");
1447
1615
  return `#${hex}${hex}${hex}`;
1448
1616
  }
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++;
1617
+ function extractColor(attr) {
1618
+ if (!attr) return null;
1619
+ if (typeof attr === "object" && attr.fg !== void 0) {
1620
+ if (typeof attr.fg === "string" && attr.fg.startsWith("#")) {
1621
+ return attr.fg;
1622
+ }
1623
+ if (typeof attr.fg === "number") {
1624
+ return ansi256ToHex(attr.fg);
1625
+ }
1626
+ }
1627
+ return null;
1469
1628
  }
1470
- async function cleanupSession(session) {
1471
- const { rm } = await import('fs/promises');
1472
- await rm(session.tempDir, { recursive: true, force: true });
1629
+
1630
+ // src/export/capture/screen-capture.ts
1631
+ function captureScreen(screen, vt) {
1632
+ const lines = screen.lines || [];
1633
+ for (let y = 0; y < Math.min(lines.length, vt.height); y++) {
1634
+ const line = lines[y];
1635
+ if (!line) continue;
1636
+ for (let x = 0; x < Math.min(line.length, vt.width); x++) {
1637
+ const cell = line[x];
1638
+ if (!cell) continue;
1639
+ const char = Array.isArray(cell) ? cell[0] : cell;
1640
+ const attr = Array.isArray(cell) ? cell[1] : null;
1641
+ const color = extractColor(attr) || "#ffffff";
1642
+ vt.setChar(x, y, char || " ", color);
1643
+ }
1644
+ }
1473
1645
  }
1646
+
1647
+ // src/export/encoding/ffmpeg-encoder.ts
1648
+ init_esm_shims();
1474
1649
  async function checkFfmpeg() {
1475
1650
  try {
1476
1651
  await execa("which", ["ffmpeg"]);
@@ -1487,36 +1662,38 @@ function detectFormat(output) {
1487
1662
  `Unknown output format for ${output}. Use .mp4 or .gif extension.`
1488
1663
  );
1489
1664
  }
1490
- async function encodeVideo(session, output, format, quality) {
1491
- const { join: join3 } = await import('path');
1492
- const inputPattern = join3(session.tempDir, "frame_%06d.png");
1665
+ async function encodeVideo(options) {
1666
+ const { inputPattern, output, format, fps, quality } = options;
1493
1667
  if (format === "mp4") {
1494
- await encodeMp4(inputPattern, output, session.fps, quality ?? 80);
1668
+ await encodeMp4(inputPattern, output, fps, quality ?? 80);
1495
1669
  } else {
1496
- await encodeGif(inputPattern, output, session.fps);
1670
+ await encodeGif(inputPattern, output, fps);
1497
1671
  }
1498
1672
  }
1499
1673
  async function encodeMp4(input, output, fps, quality) {
1500
1674
  const crf = Math.round(51 - quality / 100 * 33);
1501
1675
  await execa("ffmpeg", [
1502
1676
  "-y",
1677
+ // Overwrite output file
1503
1678
  "-framerate",
1504
1679
  fps.toString(),
1505
1680
  "-i",
1506
1681
  input,
1507
1682
  "-c:v",
1508
1683
  "libx264",
1684
+ // H.264 codec
1509
1685
  "-crf",
1510
1686
  crf.toString(),
1511
1687
  "-pix_fmt",
1512
1688
  "yuv420p",
1689
+ // Pixel format for compatibility
1513
1690
  output
1514
1691
  ]);
1515
1692
  }
1516
1693
  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`);
1694
+ const { tmpdir: tmpdir2 } = await import('os');
1695
+ const { join: join5 } = await import('path');
1696
+ const paletteFile = join5(tmpdir2(), `palette-${Date.now()}.png`);
1520
1697
  try {
1521
1698
  await execa("ffmpeg", [
1522
1699
  "-y",
@@ -1547,20 +1724,29 @@ async function encodeGif(input, output, fps) {
1547
1724
  }
1548
1725
  }
1549
1726
  }
1727
+
1728
+ // src/export/presentation-exporter.ts
1729
+ async function encodeFramesToVideo(tempDir, output, format, fps, quality) {
1730
+ const inputPattern = join(tempDir, "frame_%06d.png");
1731
+ await encodeVideo({
1732
+ inputPattern,
1733
+ output,
1734
+ format,
1735
+ fps,
1736
+ quality
1737
+ });
1738
+ }
1550
1739
  async function exportPresentation(slidesDir, options) {
1551
1740
  await checkFfmpeg();
1552
1741
  const format = detectFormat(options.output);
1553
- const { loadDeck: loadDeck2 } = await Promise.resolve().then(() => (init_slide2(), slide_exports));
1554
- const deck = await loadDeck2(slidesDir);
1742
+ const deck = await loadDeck(slidesDir);
1555
1743
  if (deck.slides.length === 0) {
1556
1744
  throw new Error(`No slides found in ${slidesDir}`);
1557
1745
  }
1558
1746
  const session = await createRecordingSession(options);
1559
1747
  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;
1748
+ const renderer = createRenderer(deck.config.theme);
1749
+ setScreenDimensions(renderer.screen, session.width, session.height);
1564
1750
  const slideTime = options.slideTime ?? 3;
1565
1751
  const framesPerSlide = session.fps * slideTime;
1566
1752
  console.log(`Exporting ${deck.slides.length} slides...`);
@@ -1577,14 +1763,25 @@ async function exportPresentation(slidesDir, options) {
1577
1763
  }
1578
1764
  }
1579
1765
  console.log("Encoding video...");
1580
- await encodeVideo(session, options.output, format, options.quality);
1766
+ await encodeFramesToVideo(
1767
+ session.tempDir,
1768
+ options.output,
1769
+ format,
1770
+ session.fps,
1771
+ options.quality
1772
+ );
1581
1773
  console.log(`Exported to ${options.output}`);
1582
1774
  } finally {
1583
- destroyRenderer2(renderer);
1775
+ destroyRenderer(renderer);
1584
1776
  await cleanupSession(session);
1585
1777
  }
1586
1778
  }
1587
1779
 
1780
+ // src/export/ansi-recorder.ts
1781
+ init_esm_shims();
1782
+ init_deck_loader();
1783
+ init_screen();
1784
+
1588
1785
  // src/cli/commands/export.ts
1589
1786
  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
1787
  try {