@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.
- package/dist/bin/term-deck.d.ts +1 -0
- package/dist/bin/term-deck.js +1720 -0
- package/dist/bin/term-deck.js.map +1 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/package.json +16 -13
- package/bin/term-deck.ts +0 -45
- package/src/cli/__tests__/errors.test.ts +0 -201
- package/src/cli/__tests__/help.test.ts +0 -157
- package/src/cli/__tests__/init.test.ts +0 -110
- package/src/cli/commands/export.ts +0 -33
- package/src/cli/commands/init.ts +0 -125
- package/src/cli/commands/present.ts +0 -29
- package/src/cli/errors.ts +0 -77
- package/src/core/__tests__/slide.test.ts +0 -1759
- package/src/core/__tests__/theme.test.ts +0 -1103
- package/src/core/slide.ts +0 -509
- package/src/core/theme.ts +0 -388
- package/src/export/__tests__/recorder.test.ts +0 -566
- package/src/export/recorder.ts +0 -639
- package/src/index.ts +0 -36
- package/src/presenter/__tests__/main.test.ts +0 -244
- package/src/presenter/main.ts +0 -658
- package/src/renderer/__tests__/screen-extended.test.ts +0 -801
- package/src/renderer/__tests__/screen.test.ts +0 -525
- package/src/renderer/screen.ts +0 -671
- package/src/schemas/__tests__/config.test.ts +0 -429
- package/src/schemas/__tests__/slide.test.ts +0 -349
- package/src/schemas/__tests__/theme.test.ts +0 -970
- package/src/schemas/__tests__/validation.test.ts +0 -256
- package/src/schemas/config.ts +0 -58
- package/src/schemas/slide.ts +0 -56
- package/src/schemas/theme.ts +0 -203
- package/src/schemas/validation.ts +0 -64
- 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
|