@pep/term-deck 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/bin/term-deck.d.ts +1 -0
  2. package/dist/bin/term-deck.js +1720 -0
  3. package/dist/bin/term-deck.js.map +1 -0
  4. package/dist/index.d.ts +670 -0
  5. package/dist/index.js +159 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +16 -13
  8. package/bin/term-deck.ts +0 -45
  9. package/src/cli/__tests__/errors.test.ts +0 -201
  10. package/src/cli/__tests__/help.test.ts +0 -157
  11. package/src/cli/__tests__/init.test.ts +0 -110
  12. package/src/cli/commands/export.ts +0 -33
  13. package/src/cli/commands/init.ts +0 -125
  14. package/src/cli/commands/present.ts +0 -29
  15. package/src/cli/errors.ts +0 -77
  16. package/src/core/__tests__/slide.test.ts +0 -1759
  17. package/src/core/__tests__/theme.test.ts +0 -1103
  18. package/src/core/slide.ts +0 -509
  19. package/src/core/theme.ts +0 -388
  20. package/src/export/__tests__/recorder.test.ts +0 -566
  21. package/src/export/recorder.ts +0 -639
  22. package/src/index.ts +0 -36
  23. package/src/presenter/__tests__/main.test.ts +0 -244
  24. package/src/presenter/main.ts +0 -658
  25. package/src/renderer/__tests__/screen-extended.test.ts +0 -801
  26. package/src/renderer/__tests__/screen.test.ts +0 -525
  27. package/src/renderer/screen.ts +0 -671
  28. package/src/schemas/__tests__/config.test.ts +0 -429
  29. package/src/schemas/__tests__/slide.test.ts +0 -349
  30. package/src/schemas/__tests__/theme.test.ts +0 -970
  31. package/src/schemas/__tests__/validation.test.ts +0 -256
  32. package/src/schemas/config.ts +0 -58
  33. package/src/schemas/slide.ts +0 -56
  34. package/src/schemas/theme.ts +0 -203
  35. package/src/schemas/validation.ts +0 -64
  36. package/src/themes/matrix/index.ts +0 -53
@@ -0,0 +1,1720 @@
1
+ #!/usr/bin/env node
2
+ import { join } from 'path';
3
+ import 'url';
4
+ import { z } from 'zod';
5
+ import 'yaml';
6
+ import 'deepmerge';
7
+ import gradient2 from 'gradient-string';
8
+ import matter from 'gray-matter';
9
+ 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
+ import blessed from 'neo-blessed';
13
+ import figlet from 'figlet';
14
+ import { Command } from 'commander';
15
+ import { execa } from 'execa';
16
+
17
+ var __defProp = Object.defineProperty;
18
+ var __getOwnPropNames = Object.getOwnPropertyNames;
19
+ var __esm = (fn, res) => function __init() {
20
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
+ };
22
+ var __export = (target, all) => {
23
+ for (var name in all)
24
+ __defProp(target, name, { get: all[name], enumerable: true });
25
+ };
26
+ var init_esm_shims = __esm({
27
+ "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
+ });
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
+ var HexColorSchema, GradientSchema, ThemeSchema, DEFAULT_THEME;
77
+ var init_theme = __esm({
78
+ "src/schemas/theme.ts"() {
79
+ init_esm_shims();
80
+ HexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, {
81
+ message: "Color must be a valid hex color (e.g., #ff0066)"
82
+ });
83
+ GradientSchema = z.array(HexColorSchema).min(2, {
84
+ message: "Gradient must have at least 2 colors"
85
+ });
86
+ ThemeSchema = z.object({
87
+ // Theme metadata
88
+ name: z.string().min(1, { message: "Theme name is required" }),
89
+ description: z.string().optional(),
90
+ author: z.string().optional(),
91
+ version: z.string().optional(),
92
+ // Color palette
93
+ colors: z.object({
94
+ primary: HexColorSchema,
95
+ secondary: HexColorSchema.optional(),
96
+ accent: HexColorSchema,
97
+ background: HexColorSchema,
98
+ text: HexColorSchema,
99
+ muted: HexColorSchema,
100
+ success: HexColorSchema.optional(),
101
+ warning: HexColorSchema.optional(),
102
+ error: HexColorSchema.optional()
103
+ }),
104
+ // Named gradients for bigText
105
+ gradients: z.record(z.string(), GradientSchema).refine(
106
+ (g) => Object.keys(g).length >= 1,
107
+ { message: "At least one gradient must be defined" }
108
+ ),
109
+ // Glyph set for matrix rain background
110
+ glyphs: z.string().min(10, {
111
+ message: "Glyph set must have at least 10 characters"
112
+ }),
113
+ // Animation settings
114
+ animations: z.object({
115
+ // Speed multiplier (1.0 = normal, 0.5 = half speed, 2.0 = double speed)
116
+ revealSpeed: z.number().min(0.1).max(5).default(1),
117
+ // Matrix rain density (number of drops)
118
+ matrixDensity: z.number().min(10).max(200).default(50),
119
+ // Glitch effect iterations
120
+ glitchIterations: z.number().min(1).max(20).default(5),
121
+ // Delay between lines during reveal (ms)
122
+ lineDelay: z.number().min(0).max(500).default(30),
123
+ // Matrix rain update interval (ms)
124
+ matrixInterval: z.number().min(20).max(200).default(80)
125
+ }),
126
+ // Window appearance
127
+ window: z.object({
128
+ // Border style
129
+ borderStyle: z.enum(["line", "double", "rounded", "none"]).default("line"),
130
+ // Shadow effect
131
+ shadow: z.boolean().default(true),
132
+ // Padding inside windows
133
+ padding: z.object({
134
+ top: z.number().min(0).max(5).default(1),
135
+ bottom: z.number().min(0).max(5).default(1),
136
+ left: z.number().min(0).max(10).default(2),
137
+ right: z.number().min(0).max(10).default(2)
138
+ }).optional()
139
+ }).optional()
140
+ });
141
+ ThemeSchema.deepPartial();
142
+ DEFAULT_THEME = {
143
+ name: "matrix",
144
+ description: "Default cyberpunk/matrix theme",
145
+ colors: {
146
+ primary: "#00cc66",
147
+ accent: "#ff6600",
148
+ background: "#0a0a0a",
149
+ text: "#ffffff",
150
+ muted: "#666666"
151
+ },
152
+ gradients: {
153
+ fire: ["#ff6600", "#ff3300", "#ff0066"],
154
+ cool: ["#00ccff", "#0066ff", "#6600ff"],
155
+ pink: ["#ff0066", "#ff0099", "#cc00ff"],
156
+ hf: ["#99cc00", "#00cc66", "#00cccc"]
157
+ },
158
+ glyphs: "\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\uFF9D0123456789",
159
+ animations: {
160
+ revealSpeed: 1,
161
+ matrixDensity: 50,
162
+ glitchIterations: 5,
163
+ lineDelay: 30,
164
+ matrixInterval: 80
165
+ },
166
+ window: {
167
+ borderStyle: "line",
168
+ shadow: true,
169
+ padding: {
170
+ top: 1,
171
+ bottom: 1,
172
+ left: 2,
173
+ right: 2
174
+ }
175
+ }
176
+ };
177
+ }
178
+ });
179
+ var SettingsSchema, ExportSettingsSchema, DeckConfigSchema;
180
+ var init_config = __esm({
181
+ "src/schemas/config.ts"() {
182
+ init_esm_shims();
183
+ init_theme();
184
+ SettingsSchema = z.object({
185
+ // Start slide (0-indexed)
186
+ startSlide: z.number().min(0).default(0),
187
+ // Loop back to first slide after last
188
+ loop: z.boolean().default(false),
189
+ // Auto-advance slides (ms, 0 = disabled)
190
+ autoAdvance: z.number().min(0).default(0),
191
+ // Show slide numbers
192
+ showSlideNumbers: z.boolean().default(false),
193
+ // Show progress bar
194
+ showProgress: z.boolean().default(false)
195
+ });
196
+ ExportSettingsSchema = z.object({
197
+ // Output width in characters (min 80, max 400)
198
+ width: z.number().min(80).max(400).default(120),
199
+ // Output height in characters (min 24, max 100)
200
+ height: z.number().min(24).max(100).default(40),
201
+ // Frames per second for video (min 10, max 60)
202
+ fps: z.number().min(10).max(60).default(30)
203
+ });
204
+ DeckConfigSchema = z.object({
205
+ // Presentation metadata
206
+ title: z.string().optional(),
207
+ author: z.string().optional(),
208
+ date: z.string().optional(),
209
+ // Theme (already validated Theme object)
210
+ theme: ThemeSchema,
211
+ // Presentation settings
212
+ settings: SettingsSchema.optional(),
213
+ // Export settings
214
+ export: ExportSettingsSchema.optional()
215
+ });
216
+ }
217
+ });
218
+
219
+ // src/schemas/validation.ts
220
+ function formatZodError(error, context) {
221
+ const issues = error.issues.map((issue) => {
222
+ const path2 = issue.path.join(".");
223
+ return ` - ${path2 ? `${path2}: ` : ""}${issue.message}`;
224
+ });
225
+ return `Invalid ${context}:
226
+ ${issues.join("\n")}`;
227
+ }
228
+ function safeParse(schema, data, context) {
229
+ const result = schema.safeParse(data);
230
+ if (!result.success) {
231
+ throw new ValidationError(formatZodError(result.error, context));
232
+ }
233
+ return result.data;
234
+ }
235
+ var ValidationError;
236
+ var init_validation = __esm({
237
+ "src/schemas/validation.ts"() {
238
+ init_esm_shims();
239
+ ValidationError = class extends Error {
240
+ constructor(message) {
241
+ super(message);
242
+ this.name = "ValidationError";
243
+ }
244
+ };
245
+ }
246
+ });
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"() {
279
+ 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
+ };
303
+ }
304
+ });
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
+ function extractNotes(content) {
386
+ const notesStart = content.indexOf(NOTES_MARKER);
387
+ if (notesStart === -1) {
388
+ return { body: content };
389
+ }
390
+ const body = content.slice(0, notesStart).trim();
391
+ const notesEnd = content.indexOf(NOTES_END_MARKER, notesStart);
392
+ let notes;
393
+ if (notesEnd !== -1) {
394
+ notes = content.slice(notesStart + NOTES_MARKER.length, notesEnd).trim();
395
+ } else {
396
+ notes = content.slice(notesStart + NOTES_MARKER.length).trim();
397
+ }
398
+ return { body, notes: notes || void 0 };
399
+ }
400
+ async function parseSlide(filePath, index) {
401
+ const content = await readFile(filePath, "utf-8");
402
+ const { data, content: rawBody } = matter(content);
403
+ const { body, notes } = extractNotes(rawBody);
404
+ const frontmatter = safeParse(
405
+ SlideFrontmatterSchema,
406
+ data,
407
+ `frontmatter in ${filePath}`
408
+ );
409
+ const slide = {
410
+ frontmatter,
411
+ body: body.trim(),
412
+ notes: notes?.trim(),
413
+ sourcePath: filePath,
414
+ index
415
+ };
416
+ return safeParse(SlideSchema, slide, `slide ${filePath}`);
417
+ }
418
+ async function findSlideFiles(dir) {
419
+ const pattern = join(dir, "*.md");
420
+ const foundFiles = await fg(pattern, { onlyFiles: true });
421
+ const files = [];
422
+ for (const filePath of foundFiles) {
423
+ const name = filePath.split("/").pop() || "";
424
+ if (name === "README.md" || name.startsWith("_")) {
425
+ continue;
426
+ }
427
+ files.push({
428
+ path: filePath,
429
+ name,
430
+ index: 0
431
+ // Will be set after sorting
432
+ });
433
+ }
434
+ files.sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
435
+ files.forEach((file, i) => {
436
+ file.index = i;
437
+ });
438
+ return files;
439
+ }
440
+ async function loadDeckConfig(slidesDir) {
441
+ const configPath = join(slidesDir, "deck.config.ts");
442
+ try {
443
+ try {
444
+ await access(configPath);
445
+ } catch {
446
+ return {
447
+ theme: DEFAULT_THEME
448
+ };
449
+ }
450
+ const configModule = await import(configPath + "?t=" + Date.now());
451
+ if (!configModule.default) {
452
+ throw new Error("deck.config.ts must export default config");
453
+ }
454
+ return safeParse(DeckConfigSchema, configModule.default, "deck.config.ts");
455
+ } catch (error) {
456
+ if (error.code === "MODULE_NOT_FOUND") {
457
+ return { theme: DEFAULT_THEME };
458
+ }
459
+ throw error;
460
+ }
461
+ }
462
+ async function loadDeck(slidesDir) {
463
+ const config = await loadDeckConfig(slidesDir);
464
+ const slideFiles = await findSlideFiles(slidesDir);
465
+ const slides = await Promise.all(
466
+ slideFiles.map((file) => parseSlide(file.path, file.index))
467
+ );
468
+ return {
469
+ slides,
470
+ config,
471
+ basePath: slidesDir
472
+ };
473
+ }
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"() {
492
+ init_esm_shims();
493
+ init_slide();
494
+ init_config();
495
+ init_validation();
496
+ 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 {
502
+ /**
503
+ * @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
518
+ * @param cause - Optional underlying error that caused this failure
519
+ */
520
+ constructor(message, slidesDir, cause) {
521
+ super(message);
522
+ this.slidesDir = slidesDir;
523
+ this.cause = cause;
524
+ this.name = "DeckLoadError";
525
+ }
526
+ };
527
+ }
528
+ });
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
+ function generateTrail(glyphs, length) {
559
+ return Array.from(
560
+ { length },
561
+ () => glyphs[Math.floor(Math.random() * glyphs.length)]
562
+ );
563
+ }
564
+ function renderMatrixRain(renderer) {
565
+ const { screen, matrixBox, matrixDrops, theme } = renderer;
566
+ const width = Math.max(20, screen.width || 80);
567
+ const height = Math.max(10, screen.height || 24);
568
+ const grid = Array.from(
569
+ { length: height },
570
+ () => Array(width).fill(" ")
571
+ );
572
+ for (const drop of matrixDrops) {
573
+ drop.y += drop.speed;
574
+ if (drop.y > height + drop.trail.length) {
575
+ drop.y = -drop.trail.length;
576
+ drop.x = Math.floor(Math.random() * width);
577
+ }
578
+ for (let i = 0; i < drop.trail.length; i++) {
579
+ const y = Math.floor(drop.y) - i;
580
+ if (y >= 0 && y < height && drop.x < width) {
581
+ grid[y][drop.x] = drop.trail[i];
582
+ }
583
+ }
584
+ }
585
+ let output = "";
586
+ for (let y = 0; y < height; y++) {
587
+ for (let x = 0; x < width; x++) {
588
+ const char = grid[y][x];
589
+ if (char !== " ") {
590
+ const brightness = Math.random() > 0.7 ? "{bold}" : "";
591
+ output += `${brightness}{${theme.colors.primary}-fg}${char}{/}`;
592
+ } else {
593
+ output += " ";
594
+ }
595
+ }
596
+ if (y < height - 1) output += "\n";
597
+ }
598
+ matrixBox.setContent(output);
599
+ }
600
+ function initMatrixRain(renderer) {
601
+ const { screen, theme } = renderer;
602
+ const width = screen.width || 80;
603
+ const height = screen.height || 24;
604
+ const density = theme.animations.matrixDensity;
605
+ renderer.matrixDrops = [];
606
+ for (let i = 0; i < density; i++) {
607
+ renderer.matrixDrops.push({
608
+ x: Math.floor(Math.random() * width),
609
+ y: Math.floor(Math.random() * height),
610
+ speed: 0.3 + Math.random() * 0.7,
611
+ trail: generateTrail(theme.glyphs, 5 + Math.floor(Math.random() * 10))
612
+ });
613
+ }
614
+ renderer.matrixInterval = setInterval(() => {
615
+ renderMatrixRain(renderer);
616
+ renderer.screen.render();
617
+ }, theme.animations.matrixInterval);
618
+ }
619
+ function createRenderer(theme) {
620
+ const screen = createScreen();
621
+ const matrixBox = blessed.box({
622
+ top: 0,
623
+ left: 0,
624
+ width: "100%",
625
+ height: "100%",
626
+ tags: true
627
+ });
628
+ 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;
639
+ }
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();
647
+ }
648
+ renderer.windowStack = [];
649
+ renderer.screen.destroy();
650
+ }
651
+ function getWindowColor(index, theme) {
652
+ const colors = [
653
+ theme.colors.primary,
654
+ theme.colors.accent,
655
+ theme.colors.secondary ?? theme.colors.primary,
656
+ "#ff0066",
657
+ // pink
658
+ "#9966ff",
659
+ // purple
660
+ "#ffcc00"
661
+ // yellow
662
+ ];
663
+ return colors[index % colors.length];
664
+ }
665
+ function createWindow(renderer, options) {
666
+ const { screen, windowStack, theme } = renderer;
667
+ const windowIndex = windowStack.length;
668
+ const color = options.color ?? getWindowColor(windowIndex, theme);
669
+ const screenWidth = screen.width || 120;
670
+ const screenHeight = screen.height || 40;
671
+ const width = options.width ?? Math.floor(screenWidth * 0.75);
672
+ const height = options.height ?? Math.floor(screenHeight * 0.7);
673
+ const maxTop = Math.max(1, screenHeight - height - 2);
674
+ const maxLeft = Math.max(1, screenWidth - width - 2);
675
+ const top = options.top ?? Math.floor(Math.random() * maxTop);
676
+ const left = options.left ?? Math.floor(Math.random() * maxLeft);
677
+ const window = theme.window ?? { borderStyle: "line", shadow: true };
678
+ const padding = window.padding ?? { top: 1, bottom: 1, left: 2, right: 2 };
679
+ const box = blessed.box({
680
+ top,
681
+ left,
682
+ width,
683
+ height,
684
+ border: {
685
+ type: window.borderStyle === "none" ? void 0 : "line"
686
+ },
687
+ label: ` ${options.title} `,
688
+ style: {
689
+ fg: theme.colors.text,
690
+ bg: theme.colors.background,
691
+ border: { fg: color },
692
+ label: { fg: color, bold: true }
693
+ },
694
+ padding,
695
+ tags: true,
696
+ shadow: window.shadow
697
+ });
698
+ screen.append(box);
699
+ windowStack.push(box);
700
+ return box;
701
+ }
702
+ function clearWindows(renderer) {
703
+ for (const window of renderer.windowStack) {
704
+ window.destroy();
705
+ }
706
+ renderer.windowStack = [];
707
+ }
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;
714
+ }
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));
728
+ }
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;
740
+ }
741
+ }
742
+ box.setContent([...currentLines, scrambledLine].join("\n"));
743
+ screen.render();
744
+ await sleep(20);
745
+ }
746
+ }
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
+ }
760
+ }
761
+ }
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);
778
+ }
779
+ box.setContent(content);
780
+ screen.render();
781
+ }
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
+ }
792
+ }
793
+ }
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();
812
+ }
813
+ }
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";
827
+ }
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;
833
+ }
834
+ var GLITCH_CHARS, PROTECTED_CHARS;
835
+ var init_screen = __esm({
836
+ "src/renderer/screen.ts"() {
837
+ 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";
840
+ PROTECTED_CHARS = /* @__PURE__ */ new Set([
841
+ " ",
842
+ " ",
843
+ "\n",
844
+ "{",
845
+ "}",
846
+ "-",
847
+ "/",
848
+ "#",
849
+ "[",
850
+ "]",
851
+ "(",
852
+ ")",
853
+ ":",
854
+ ";",
855
+ ",",
856
+ ".",
857
+ "!",
858
+ "?",
859
+ "'",
860
+ '"',
861
+ "`",
862
+ "_",
863
+ "|",
864
+ "\\",
865
+ "<",
866
+ ">",
867
+ "=",
868
+ "+",
869
+ "*",
870
+ "&",
871
+ "^",
872
+ "%",
873
+ "$",
874
+ "@",
875
+ "~",
876
+ // Box drawing
877
+ "\u250C",
878
+ "\u2510",
879
+ "\u2514",
880
+ "\u2518",
881
+ "\u2502",
882
+ "\u2500",
883
+ "\u251C",
884
+ "\u2524",
885
+ "\u252C",
886
+ "\u2534",
887
+ "\u253C",
888
+ "\u2550",
889
+ "\u2551",
890
+ "\u2554",
891
+ "\u2557",
892
+ "\u255A",
893
+ "\u255D",
894
+ "\u2560",
895
+ "\u2563",
896
+ "\u2566",
897
+ "\u2569",
898
+ "\u256C",
899
+ "\u256D",
900
+ "\u256E",
901
+ "\u256F",
902
+ "\u2570",
903
+ // Arrows
904
+ "\u2192",
905
+ "\u2190",
906
+ "\u2191",
907
+ "\u2193",
908
+ "\u25B6",
909
+ "\u25C0",
910
+ "\u25B2",
911
+ "\u25BC",
912
+ "\u25BA",
913
+ "\u25C4"
914
+ ]);
915
+ }
916
+ });
917
+
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
925
+ });
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}`);
930
+ }
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);
943
+ }
944
+ if (deck.config.settings?.showProgress) {
945
+ presenter.progressBar = createProgressBar(presenter);
946
+ }
947
+ setupControls(presenter);
948
+ await showSlide(presenter, presenter.currentSlide);
949
+ if (presenter.progressBar) {
950
+ updateProgress(presenter.progressBar, presenter.currentSlide, deck.slides.length);
951
+ }
952
+ presenter.autoAdvanceTimer = startAutoAdvance(presenter);
953
+ await new Promise((resolve) => {
954
+ renderer.screen.key(["q", "C-c", "escape"], () => {
955
+ cleanup(presenter);
956
+ resolve();
957
+ });
958
+ });
959
+ }
960
+ function cleanup(presenter) {
961
+ stopAutoAdvance(presenter.autoAdvanceTimer);
962
+ if (presenter.notesWindow) {
963
+ presenter.notesWindow.screen.destroy();
964
+ }
965
+ destroyRenderer(presenter.renderer);
966
+ }
967
+ async function showSlide(presenter, index) {
968
+ if (presenter.isAnimating) return;
969
+ if (index < 0 || index >= presenter.deck.slides.length) return;
970
+ presenter.isAnimating = true;
971
+ presenter.currentSlide = index;
972
+ const slide = presenter.deck.slides[index];
973
+ await renderSlide(presenter.renderer, slide);
974
+ presenter.renderer.screen.render();
975
+ if (presenter.notesWindow) {
976
+ updateNotesWindow(presenter);
977
+ }
978
+ if (presenter.progressBar) {
979
+ updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
980
+ }
981
+ presenter.isAnimating = false;
982
+ }
983
+ async function nextSlide(presenter) {
984
+ const nextIndex = presenter.currentSlide + 1;
985
+ const { slides } = presenter.deck;
986
+ const loop = presenter.deck.config.settings?.loop ?? false;
987
+ if (nextIndex >= slides.length) {
988
+ if (loop) {
989
+ await showSlide(presenter, 0);
990
+ }
991
+ return;
992
+ }
993
+ await showSlide(presenter, nextIndex);
994
+ }
995
+ async function prevSlide(presenter) {
996
+ const prevIndex = presenter.currentSlide - 1;
997
+ const { slides } = presenter.deck;
998
+ const loop = presenter.deck.config.settings?.loop ?? false;
999
+ if (prevIndex < 0) {
1000
+ if (loop) {
1001
+ clearWindows(presenter.renderer);
1002
+ for (let i = 0; i < slides.length; i++) {
1003
+ await renderSlide(presenter.renderer, slides[i]);
1004
+ }
1005
+ 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
+ }
1012
+ presenter.renderer.screen.render();
1013
+ }
1014
+ return;
1015
+ }
1016
+ clearWindows(presenter.renderer);
1017
+ for (let i = 0; i <= prevIndex; i++) {
1018
+ await renderSlide(presenter.renderer, slides[i]);
1019
+ }
1020
+ 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);
1026
+ }
1027
+ presenter.renderer.screen.render();
1028
+ }
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;
1036
+ if (presenter.notesWindow) {
1037
+ updateNotesWindow(presenter);
1038
+ }
1039
+ if (presenter.progressBar) {
1040
+ updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
1041
+ }
1042
+ presenter.renderer.screen.render();
1043
+ }
1044
+ function setupControls(presenter) {
1045
+ const { screen } = presenter.renderer;
1046
+ screen.key(["space", "enter", "right", "n"], () => {
1047
+ nextSlide(presenter);
1048
+ });
1049
+ screen.key(["left", "backspace", "p"], () => {
1050
+ prevSlide(presenter);
1051
+ });
1052
+ screen.key(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], (ch) => {
1053
+ const index = parseInt(ch, 10);
1054
+ jumpToSlide(presenter, index);
1055
+ });
1056
+ screen.key(["l"], () => {
1057
+ showSlideList(presenter);
1058
+ });
1059
+ screen.key(["N"], () => {
1060
+ if (presenter.notesWindow) {
1061
+ toggleNotesVisibility(presenter.notesWindow);
1062
+ }
1063
+ });
1064
+ }
1065
+ function showSlideList(presenter) {
1066
+ const { screen } = presenter.renderer;
1067
+ const { slides } = presenter.deck;
1068
+ const listContent = slides.map((slide, i) => {
1069
+ const marker = i === presenter.currentSlide ? "\u25B6 " : " ";
1070
+ return `${marker}${i}: ${slide.frontmatter.title}`;
1071
+ }).join("\n");
1072
+ const listBox = screen.box({
1073
+ top: "center",
1074
+ left: "center",
1075
+ width: 50,
1076
+ height: Math.min(slides.length + 4, 20),
1077
+ border: { type: "line" },
1078
+ label: " SLIDES (press number or Esc) ",
1079
+ style: {
1080
+ fg: "#ffffff",
1081
+ bg: "#0a0a0a",
1082
+ border: { fg: "#ffcc00" }
1083
+ },
1084
+ padding: 1,
1085
+ tags: true,
1086
+ content: listContent
1087
+ });
1088
+ screen.append(listBox);
1089
+ screen.render();
1090
+ const closeList = () => {
1091
+ listBox.destroy();
1092
+ screen.render();
1093
+ };
1094
+ screen.onceKey(["escape", "l", "q"], closeList);
1095
+ screen.onceKey(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], (ch) => {
1096
+ closeList();
1097
+ jumpToSlide(presenter, parseInt(ch ?? "0", 10));
1098
+ });
1099
+ }
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
+ }
1119
+ }
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
1153
+ };
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";
1175
+ }
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";
1184
+ }
1185
+ contentBox.setContent(content);
1186
+ screen.render();
1187
+ }
1188
+ function startAutoAdvance(presenter) {
1189
+ const interval = presenter.deck.config.settings?.autoAdvance;
1190
+ if (!interval || interval <= 0) {
1191
+ return null;
1192
+ }
1193
+ return setInterval(() => {
1194
+ if (!presenter.isAnimating) {
1195
+ nextSlide(presenter);
1196
+ }
1197
+ }, interval);
1198
+ }
1199
+ function stopAutoAdvance(timer) {
1200
+ if (timer) {
1201
+ clearInterval(timer);
1202
+ }
1203
+ }
1204
+ function createProgressBar(presenter) {
1205
+ const { screen } = presenter.renderer;
1206
+ const progressBar = blessed.progressbar({
1207
+ bottom: 0,
1208
+ left: 0,
1209
+ width: "100%",
1210
+ height: 1,
1211
+ style: {
1212
+ bg: "#333333",
1213
+ bar: { bg: "#00cc66" }
1214
+ },
1215
+ filled: 0
1216
+ });
1217
+ screen.append(progressBar);
1218
+ return progressBar;
1219
+ }
1220
+ function updateProgress(progressBar, current, total) {
1221
+ const progress = (current + 1) / total * 100;
1222
+ progressBar.setProgress(progress);
1223
+ }
1224
+ var init_main = __esm({
1225
+ "src/presenter/main.ts"() {
1226
+ init_esm_shims();
1227
+ init_slide2();
1228
+ init_screen();
1229
+ }
1230
+ });
1231
+
1232
+ // bin/term-deck.ts
1233
+ init_esm_shims();
1234
+
1235
+ // package.json
1236
+ var version = "1.0.15";
1237
+
1238
+ // src/cli/commands/present.ts
1239
+ init_esm_shims();
1240
+ init_main();
1241
+
1242
+ // src/cli/errors.ts
1243
+ init_esm_shims();
1244
+ init_validation();
1245
+ init_slide2();
1246
+ init_theme2();
1247
+ function handleError(error) {
1248
+ if (error instanceof ValidationError) {
1249
+ console.error(`
1250
+ ${error.message}`);
1251
+ process.exit(1);
1252
+ }
1253
+ if (error instanceof SlideParseError) {
1254
+ console.error(`
1255
+ Slide error in ${error.filePath}:`);
1256
+ console.error(` ${error.message}`);
1257
+ if (error.cause) {
1258
+ const causeMessage = error.cause instanceof Error ? error.cause.message : String(error.cause);
1259
+ console.error(` Caused by: ${causeMessage}`);
1260
+ }
1261
+ process.exit(1);
1262
+ }
1263
+ if (error instanceof DeckLoadError) {
1264
+ console.error(`
1265
+ Failed to load deck from ${error.slidesDir}:`);
1266
+ console.error(` ${error.message}`);
1267
+ process.exit(1);
1268
+ }
1269
+ if (error instanceof ThemeError) {
1270
+ console.error("\nTheme error:");
1271
+ console.error(` ${error.message}`);
1272
+ process.exit(1);
1273
+ }
1274
+ if (error instanceof Error) {
1275
+ if (error.message.includes("ENOENT")) {
1276
+ console.error("\nFile or directory not found.");
1277
+ console.error(` ${error.message}`);
1278
+ process.exit(1);
1279
+ }
1280
+ if (error.message.includes("ffmpeg")) {
1281
+ console.error("\nffmpeg error:");
1282
+ console.error(` ${error.message}`);
1283
+ console.error("\nMake sure ffmpeg is installed:");
1284
+ console.error(" macOS: brew install ffmpeg");
1285
+ console.error(" Ubuntu: sudo apt install ffmpeg");
1286
+ process.exit(1);
1287
+ }
1288
+ console.error(`
1289
+ Error: ${error.message}`);
1290
+ if (process.env.DEBUG) {
1291
+ console.error(error.stack);
1292
+ }
1293
+ process.exit(1);
1294
+ }
1295
+ console.error("\nUnknown error occurred");
1296
+ console.error(error);
1297
+ process.exit(1);
1298
+ }
1299
+
1300
+ // src/cli/commands/present.ts
1301
+ var presentCommand = new Command("present").description("Start a presentation").argument("<dir>", "Slides directory").option("-s, --start <n>", "Start at slide number", "0").option("-n, --notes", "Show presenter notes in separate terminal").option("--notes-tty <path>", "TTY device for notes window (e.g., /dev/ttys001)").option("-l, --loop", "Loop back to first slide after last").action(async (dir, options) => {
1302
+ try {
1303
+ await present(dir, {
1304
+ startSlide: Number.parseInt(options.start, 10),
1305
+ showNotes: options.notes,
1306
+ notesTty: options.notesTty,
1307
+ loop: options.loop
1308
+ });
1309
+ } catch (error) {
1310
+ handleError(error);
1311
+ }
1312
+ });
1313
+
1314
+ // src/cli/commands/export.ts
1315
+ init_esm_shims();
1316
+
1317
+ // src/export/recorder.ts
1318
+ init_esm_shims();
1319
+ var VirtualTerminal = class {
1320
+ constructor(width, height) {
1321
+ this.width = width;
1322
+ this.height = height;
1323
+ this.buffer = Array.from(
1324
+ { length: height },
1325
+ () => Array(width).fill(" ")
1326
+ );
1327
+ this.colors = Array.from(
1328
+ { length: height },
1329
+ () => Array(width).fill("#ffffff")
1330
+ );
1331
+ }
1332
+ buffer;
1333
+ colors;
1334
+ /**
1335
+ * Set character at position
1336
+ */
1337
+ setChar(x, y, char, color = "#ffffff") {
1338
+ if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
1339
+ this.buffer[y][x] = char;
1340
+ this.colors[y][x] = color;
1341
+ }
1342
+ }
1343
+ /**
1344
+ * Clear the buffer
1345
+ */
1346
+ clear() {
1347
+ for (let y = 0; y < this.height; y++) {
1348
+ for (let x = 0; x < this.width; x++) {
1349
+ this.buffer[y][x] = " ";
1350
+ this.colors[y][x] = "#ffffff";
1351
+ }
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Get buffer as string (for debugging)
1356
+ */
1357
+ toString() {
1358
+ return this.buffer.map((row) => row.join("")).join("\n");
1359
+ }
1360
+ /**
1361
+ * Convert buffer to PNG image data
1362
+ */
1363
+ async toPng() {
1364
+ return renderTerminalToPng(this.buffer, this.colors, this.width, this.height);
1365
+ }
1366
+ };
1367
+ var CHAR_WIDTH = 10;
1368
+ var CHAR_HEIGHT = 20;
1369
+ async function renderTerminalToPng(buffer, colors, width, height) {
1370
+ const { createCanvas } = await import('canvas');
1371
+ const canvas = createCanvas(width * CHAR_WIDTH, height * CHAR_HEIGHT);
1372
+ const ctx = canvas.getContext("2d");
1373
+ ctx.fillStyle = "#0a0a0a";
1374
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1375
+ ctx.font = `${CHAR_HEIGHT - 4}px monospace`;
1376
+ ctx.textBaseline = "top";
1377
+ for (let y = 0; y < height; y++) {
1378
+ for (let x = 0; x < width; x++) {
1379
+ const char = buffer[y][x];
1380
+ const color = colors[y][x];
1381
+ if (char !== " ") {
1382
+ ctx.fillStyle = color;
1383
+ ctx.fillText(char, x * CHAR_WIDTH, y * CHAR_HEIGHT + 2);
1384
+ }
1385
+ }
1386
+ }
1387
+ return canvas.toBuffer("image/png");
1388
+ }
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
+ }
1416
+ function ansi256ToHex(code) {
1417
+ const standard16 = [
1418
+ "#000000",
1419
+ "#800000",
1420
+ "#008000",
1421
+ "#808000",
1422
+ "#000080",
1423
+ "#800080",
1424
+ "#008080",
1425
+ "#c0c0c0",
1426
+ "#808080",
1427
+ "#ff0000",
1428
+ "#00ff00",
1429
+ "#ffff00",
1430
+ "#0000ff",
1431
+ "#ff00ff",
1432
+ "#00ffff",
1433
+ "#ffffff"
1434
+ ];
1435
+ if (code < 16) {
1436
+ return standard16[code];
1437
+ }
1438
+ if (code < 232) {
1439
+ const n = code - 16;
1440
+ const r = Math.floor(n / 36) * 51;
1441
+ const g = Math.floor(n % 36 / 6) * 51;
1442
+ const b = n % 6 * 51;
1443
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
1444
+ }
1445
+ const gray = (code - 232) * 10 + 8;
1446
+ const hex = gray.toString(16).padStart(2, "0");
1447
+ return `#${hex}${hex}${hex}`;
1448
+ }
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++;
1469
+ }
1470
+ async function cleanupSession(session) {
1471
+ const { rm } = await import('fs/promises');
1472
+ await rm(session.tempDir, { recursive: true, force: true });
1473
+ }
1474
+ async function checkFfmpeg() {
1475
+ try {
1476
+ await execa("which", ["ffmpeg"]);
1477
+ } catch {
1478
+ throw new Error(
1479
+ "ffmpeg not found. Install it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg"
1480
+ );
1481
+ }
1482
+ }
1483
+ function detectFormat(output) {
1484
+ if (output.endsWith(".gif")) return "gif";
1485
+ if (output.endsWith(".mp4")) return "mp4";
1486
+ throw new Error(
1487
+ `Unknown output format for ${output}. Use .mp4 or .gif extension.`
1488
+ );
1489
+ }
1490
+ async function encodeVideo(session, output, format, quality) {
1491
+ const { join: join3 } = await import('path');
1492
+ const inputPattern = join3(session.tempDir, "frame_%06d.png");
1493
+ if (format === "mp4") {
1494
+ await encodeMp4(inputPattern, output, session.fps, quality ?? 80);
1495
+ } else {
1496
+ await encodeGif(inputPattern, output, session.fps);
1497
+ }
1498
+ }
1499
+ async function encodeMp4(input, output, fps, quality) {
1500
+ const crf = Math.round(51 - quality / 100 * 33);
1501
+ await execa("ffmpeg", [
1502
+ "-y",
1503
+ "-framerate",
1504
+ fps.toString(),
1505
+ "-i",
1506
+ input,
1507
+ "-c:v",
1508
+ "libx264",
1509
+ "-crf",
1510
+ crf.toString(),
1511
+ "-pix_fmt",
1512
+ "yuv420p",
1513
+ output
1514
+ ]);
1515
+ }
1516
+ 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`);
1520
+ try {
1521
+ await execa("ffmpeg", [
1522
+ "-y",
1523
+ "-framerate",
1524
+ fps.toString(),
1525
+ "-i",
1526
+ input,
1527
+ "-vf",
1528
+ `fps=${fps},scale=-1:-1:flags=lanczos,palettegen=stats_mode=diff`,
1529
+ paletteFile
1530
+ ]);
1531
+ await execa("ffmpeg", [
1532
+ "-y",
1533
+ "-framerate",
1534
+ fps.toString(),
1535
+ "-i",
1536
+ input,
1537
+ "-i",
1538
+ paletteFile,
1539
+ "-lavfi",
1540
+ `fps=${fps},scale=-1:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`,
1541
+ output
1542
+ ]);
1543
+ } finally {
1544
+ try {
1545
+ await unlink(paletteFile);
1546
+ } catch {
1547
+ }
1548
+ }
1549
+ }
1550
+ async function exportPresentation(slidesDir, options) {
1551
+ await checkFfmpeg();
1552
+ const format = detectFormat(options.output);
1553
+ const { loadDeck: loadDeck2 } = await Promise.resolve().then(() => (init_slide2(), slide_exports));
1554
+ const deck = await loadDeck2(slidesDir);
1555
+ if (deck.slides.length === 0) {
1556
+ throw new Error(`No slides found in ${slidesDir}`);
1557
+ }
1558
+ const session = await createRecordingSession(options);
1559
+ 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;
1564
+ const slideTime = options.slideTime ?? 3;
1565
+ const framesPerSlide = session.fps * slideTime;
1566
+ console.log(`Exporting ${deck.slides.length} slides...`);
1567
+ try {
1568
+ for (let i = 0; i < deck.slides.length; i++) {
1569
+ const slide = deck.slides[i];
1570
+ console.log(` Slide ${i + 1}/${deck.slides.length}: ${slide.frontmatter.title}`);
1571
+ await renderSlide2(renderer, slide);
1572
+ for (let f = 0; f < framesPerSlide; f++) {
1573
+ renderer.screen.render();
1574
+ captureScreen(renderer.screen, vt);
1575
+ const png = await vt.toPng();
1576
+ await saveFrame(session, png);
1577
+ }
1578
+ }
1579
+ console.log("Encoding video...");
1580
+ await encodeVideo(session, options.output, format, options.quality);
1581
+ console.log(`Exported to ${options.output}`);
1582
+ } finally {
1583
+ destroyRenderer2(renderer);
1584
+ await cleanupSession(session);
1585
+ }
1586
+ }
1587
+
1588
+ // src/cli/commands/export.ts
1589
+ 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
+ try {
1591
+ await exportPresentation(dir, {
1592
+ output: options.output,
1593
+ width: Number.parseInt(options.width, 10),
1594
+ height: Number.parseInt(options.height, 10),
1595
+ fps: Number.parseInt(options.fps, 10),
1596
+ slideTime: Number.parseFloat(options.slideTime),
1597
+ quality: Number.parseInt(options.quality, 10)
1598
+ });
1599
+ } catch (error) {
1600
+ handleError(error);
1601
+ }
1602
+ });
1603
+
1604
+ // src/cli/commands/init.ts
1605
+ init_esm_shims();
1606
+ var initCommand = new Command("init").description("Create a new presentation deck").argument("<name>", "Deck name (will create directory)").option("-t, --theme <name>", "Theme to use", "matrix").action(async (name, options) => {
1607
+ try {
1608
+ await initDeck(name, options.theme);
1609
+ console.log(`Created deck: ${name}/`);
1610
+ console.log("\nNext steps:");
1611
+ console.log(` cd ${name}/slides`);
1612
+ console.log(" term-deck present .");
1613
+ } catch (error) {
1614
+ handleError(error);
1615
+ }
1616
+ });
1617
+ async function initDeck(name, theme) {
1618
+ const deckDir = join(process.cwd(), name);
1619
+ const slidesDir = join(deckDir, "slides");
1620
+ await mkdir(slidesDir, { recursive: true });
1621
+ await writeFile(join(slidesDir, ".gitkeep"), "");
1622
+ const configContent = `import { defineConfig } from 'term-deck'
1623
+ import matrix from '@term-deck/theme-matrix'
1624
+
1625
+ export default defineConfig({
1626
+ title: '${name}',
1627
+ theme: matrix,
1628
+ })
1629
+ `;
1630
+ await writeFile(join(slidesDir, "deck.config.ts"), configContent);
1631
+ const slide1 = `---
1632
+ title: ${name.toUpperCase()}
1633
+ bigText: ${name.toUpperCase()}
1634
+ gradient: fire
1635
+ ---
1636
+
1637
+ {GREEN}Welcome to your presentation{/}
1638
+
1639
+ Press {CYAN}Space{/} or {CYAN}\u2192{/} to advance
1640
+ `;
1641
+ const slide2 = `---
1642
+ title: SLIDE TWO
1643
+ bigText: HELLO
1644
+ gradient: cool
1645
+ ---
1646
+
1647
+ {WHITE}This is the second slide{/}
1648
+
1649
+ - Point one
1650
+ - Point two
1651
+ - Point three
1652
+
1653
+ <!-- notes -->
1654
+ Remember to explain each point clearly.
1655
+ `;
1656
+ const slide3 = `---
1657
+ title: THE END
1658
+ bigText: FIN
1659
+ gradient: pink
1660
+ ---
1661
+
1662
+ {ORANGE}Thank you!{/}
1663
+
1664
+ Press {CYAN}q{/} to exit
1665
+ `;
1666
+ await writeFile(join(slidesDir, "01-intro.md"), slide1);
1667
+ await writeFile(join(slidesDir, "02-content.md"), slide2);
1668
+ await writeFile(join(slidesDir, "03-end.md"), slide3);
1669
+ const readme = `# ${name}
1670
+
1671
+ A term-deck presentation.
1672
+
1673
+ ## Usage
1674
+
1675
+ \`\`\`bash
1676
+ cd slides
1677
+ term-deck present .
1678
+ \`\`\`
1679
+
1680
+ ## Export
1681
+
1682
+ \`\`\`bash
1683
+ term-deck export slides/ -o ${name}.mp4
1684
+ term-deck export slides/ -o ${name}.gif
1685
+ \`\`\`
1686
+
1687
+ ## Hotkeys
1688
+
1689
+ | Key | Action |
1690
+ |-----|--------|
1691
+ | Space / \u2192 | Next slide |
1692
+ | \u2190 | Previous slide |
1693
+ | 0-9 | Jump to slide |
1694
+ | l | Show slide list |
1695
+ | q | Quit |
1696
+ `;
1697
+ await writeFile(join(deckDir, "README.md"), readme);
1698
+ }
1699
+
1700
+ // bin/term-deck.ts
1701
+ var program = new Command();
1702
+ program.name("term-deck").description("Terminal presentation tool with a cyberpunk aesthetic").version(version);
1703
+ program.addCommand(presentCommand);
1704
+ program.addCommand(exportCommand);
1705
+ program.addCommand(initCommand);
1706
+ program.argument("[dir]", "Slides directory to present").action(async (dir) => {
1707
+ if (dir) {
1708
+ try {
1709
+ const { present: present2 } = await Promise.resolve().then(() => (init_main(), main_exports));
1710
+ await present2(dir, {});
1711
+ } catch (error) {
1712
+ handleError(error);
1713
+ }
1714
+ } else {
1715
+ program.help();
1716
+ }
1717
+ });
1718
+ program.parse();
1719
+ //# sourceMappingURL=term-deck.js.map
1720
+ //# sourceMappingURL=term-deck.js.map