@pep/term-deck 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +356 -0
- package/bin/term-deck.ts +45 -0
- package/examples/slides/01-welcome.md +9 -0
- package/examples/slides/02-features.md +12 -0
- package/examples/slides/03-colors.md +17 -0
- package/examples/slides/04-ascii-art.md +11 -0
- package/examples/slides/05-gradients.md +14 -0
- package/examples/slides/06-themes.md +13 -0
- package/examples/slides/07-markdown.md +13 -0
- package/examples/slides/08-controls.md +13 -0
- package/examples/slides/09-thanks.md +11 -0
- package/examples/slides/deck.config.ts +13 -0
- package/examples/slides-hacker/01-welcome.md +9 -0
- package/examples/slides-hacker/02-features.md +12 -0
- package/examples/slides-hacker/03-colors.md +17 -0
- package/examples/slides-hacker/04-ascii-art.md +11 -0
- package/examples/slides-hacker/05-gradients.md +14 -0
- package/examples/slides-hacker/06-themes.md +13 -0
- package/examples/slides-hacker/07-markdown.md +13 -0
- package/examples/slides-hacker/08-controls.md +13 -0
- package/examples/slides-hacker/09-thanks.md +11 -0
- package/examples/slides-hacker/deck.config.ts +13 -0
- package/examples/slides-matrix/01-welcome.md +9 -0
- package/examples/slides-matrix/02-features.md +12 -0
- package/examples/slides-matrix/03-colors.md +17 -0
- package/examples/slides-matrix/04-ascii-art.md +11 -0
- package/examples/slides-matrix/05-gradients.md +14 -0
- package/examples/slides-matrix/06-themes.md +13 -0
- package/examples/slides-matrix/07-markdown.md +13 -0
- package/examples/slides-matrix/08-controls.md +13 -0
- package/examples/slides-matrix/09-thanks.md +11 -0
- package/examples/slides-matrix/deck.config.ts +13 -0
- package/examples/slides-minimal/01-welcome.md +9 -0
- package/examples/slides-minimal/02-features.md +12 -0
- package/examples/slides-minimal/03-colors.md +17 -0
- package/examples/slides-minimal/04-ascii-art.md +11 -0
- package/examples/slides-minimal/05-gradients.md +14 -0
- package/examples/slides-minimal/06-themes.md +13 -0
- package/examples/slides-minimal/07-markdown.md +13 -0
- package/examples/slides-minimal/08-controls.md +13 -0
- package/examples/slides-minimal/09-thanks.md +11 -0
- package/examples/slides-minimal/deck.config.ts +13 -0
- package/examples/slides-neon/01-welcome.md +9 -0
- package/examples/slides-neon/02-features.md +12 -0
- package/examples/slides-neon/03-colors.md +17 -0
- package/examples/slides-neon/04-ascii-art.md +11 -0
- package/examples/slides-neon/05-gradients.md +14 -0
- package/examples/slides-neon/06-themes.md +13 -0
- package/examples/slides-neon/07-markdown.md +13 -0
- package/examples/slides-neon/08-controls.md +13 -0
- package/examples/slides-neon/09-thanks.md +11 -0
- package/examples/slides-neon/deck.config.ts +13 -0
- package/examples/slides-retro/01-welcome.md +9 -0
- package/examples/slides-retro/02-features.md +12 -0
- package/examples/slides-retro/03-colors.md +17 -0
- package/examples/slides-retro/04-ascii-art.md +11 -0
- package/examples/slides-retro/05-gradients.md +14 -0
- package/examples/slides-retro/06-themes.md +13 -0
- package/examples/slides-retro/07-markdown.md +13 -0
- package/examples/slides-retro/08-controls.md +13 -0
- package/examples/slides-retro/09-thanks.md +11 -0
- package/examples/slides-retro/deck.config.ts +13 -0
- package/package.json +66 -0
- package/src/cli/__tests__/errors.test.ts +201 -0
- package/src/cli/__tests__/help.test.ts +157 -0
- package/src/cli/__tests__/init.test.ts +110 -0
- package/src/cli/commands/export.ts +33 -0
- package/src/cli/commands/init.ts +125 -0
- package/src/cli/commands/present.ts +29 -0
- package/src/cli/errors.ts +77 -0
- package/src/core/__tests__/slide.test.ts +1759 -0
- package/src/core/__tests__/theme.test.ts +1103 -0
- package/src/core/slide.ts +509 -0
- package/src/core/theme.ts +388 -0
- package/src/export/__tests__/recorder.test.ts +566 -0
- package/src/export/recorder.ts +639 -0
- package/src/index.ts +36 -0
- package/src/presenter/__tests__/main.test.ts +244 -0
- package/src/presenter/main.ts +658 -0
- package/src/renderer/__tests__/screen-extended.test.ts +801 -0
- package/src/renderer/__tests__/screen.test.ts +525 -0
- package/src/renderer/screen.ts +671 -0
- package/src/schemas/__tests__/config.test.ts +429 -0
- package/src/schemas/__tests__/slide.test.ts +349 -0
- package/src/schemas/__tests__/theme.test.ts +970 -0
- package/src/schemas/__tests__/validation.test.ts +256 -0
- package/src/schemas/config.ts +58 -0
- package/src/schemas/slide.ts +56 -0
- package/src/schemas/theme.ts +203 -0
- package/src/schemas/validation.ts +64 -0
- package/src/themes/matrix/index.ts +53 -0
- package/themes/hacker.ts +53 -0
- package/themes/minimal.ts +53 -0
- package/themes/neon.ts +53 -0
- package/themes/retro.ts +53 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI error handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, mock, spyOn, beforeEach, afterEach } from 'bun:test';
|
|
6
|
+
import { handleError } from '../errors.js';
|
|
7
|
+
import { ValidationError } from '../../schemas/validation.js';
|
|
8
|
+
import { SlideParseError, DeckLoadError } from '../../core/slide.js';
|
|
9
|
+
import { ThemeError } from '../../core/theme.js';
|
|
10
|
+
|
|
11
|
+
describe('handleError', () => {
|
|
12
|
+
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
13
|
+
let processExitSpy: ReturnType<typeof spyOn>;
|
|
14
|
+
let originalDebug: string | undefined;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
+
processExitSpy = spyOn(process, 'exit').mockImplementation(
|
|
19
|
+
(() => {}) as never,
|
|
20
|
+
);
|
|
21
|
+
originalDebug = process.env.DEBUG;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
consoleErrorSpy.mockRestore();
|
|
26
|
+
processExitSpy.mockRestore();
|
|
27
|
+
process.env.DEBUG = originalDebug;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('handles ValidationError', () => {
|
|
31
|
+
const error = new ValidationError('Invalid theme configuration');
|
|
32
|
+
|
|
33
|
+
handleError(error);
|
|
34
|
+
|
|
35
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
36
|
+
expect.stringContaining('Invalid theme configuration'),
|
|
37
|
+
);
|
|
38
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('handles SlideParseError', () => {
|
|
42
|
+
const cause = new Error('Missing title field');
|
|
43
|
+
const error = new SlideParseError('Invalid frontmatter', 'slides/01.md', cause);
|
|
44
|
+
|
|
45
|
+
handleError(error);
|
|
46
|
+
|
|
47
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
48
|
+
expect.stringContaining('slides/01.md'),
|
|
49
|
+
);
|
|
50
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
51
|
+
expect.stringContaining('Invalid frontmatter'),
|
|
52
|
+
);
|
|
53
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
54
|
+
expect.stringContaining('Missing title field'),
|
|
55
|
+
);
|
|
56
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('handles SlideParseError without cause', () => {
|
|
60
|
+
const error = new SlideParseError('Invalid frontmatter', 'slides/02.md');
|
|
61
|
+
|
|
62
|
+
handleError(error);
|
|
63
|
+
|
|
64
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
65
|
+
expect.stringContaining('slides/02.md'),
|
|
66
|
+
);
|
|
67
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('handles DeckLoadError', () => {
|
|
71
|
+
const error = new DeckLoadError('No slides found', './slides');
|
|
72
|
+
|
|
73
|
+
handleError(error);
|
|
74
|
+
|
|
75
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
76
|
+
expect.stringContaining('./slides'),
|
|
77
|
+
);
|
|
78
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
79
|
+
expect.stringContaining('No slides found'),
|
|
80
|
+
);
|
|
81
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('handles ThemeError', () => {
|
|
85
|
+
const error = new ThemeError('Invalid color format', 'custom-theme');
|
|
86
|
+
|
|
87
|
+
handleError(error);
|
|
88
|
+
|
|
89
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining('Theme error'),
|
|
91
|
+
);
|
|
92
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
93
|
+
expect.stringContaining('Invalid color format'),
|
|
94
|
+
);
|
|
95
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('handles ENOENT errors', () => {
|
|
99
|
+
const error = new Error('ENOENT: no such file or directory');
|
|
100
|
+
|
|
101
|
+
handleError(error);
|
|
102
|
+
|
|
103
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
104
|
+
expect.stringContaining('File or directory not found'),
|
|
105
|
+
);
|
|
106
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('handles ffmpeg errors', () => {
|
|
110
|
+
const error = new Error('ffmpeg not found in PATH');
|
|
111
|
+
|
|
112
|
+
handleError(error);
|
|
113
|
+
|
|
114
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
115
|
+
expect.stringContaining('ffmpeg error'),
|
|
116
|
+
);
|
|
117
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
118
|
+
expect.stringContaining('Make sure ffmpeg is installed'),
|
|
119
|
+
);
|
|
120
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
121
|
+
expect.stringContaining('brew install ffmpeg'),
|
|
122
|
+
);
|
|
123
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('handles generic errors', () => {
|
|
127
|
+
const error = new Error('Something went wrong');
|
|
128
|
+
|
|
129
|
+
handleError(error);
|
|
130
|
+
|
|
131
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
132
|
+
expect.stringContaining('Something went wrong'),
|
|
133
|
+
);
|
|
134
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('shows stack trace when DEBUG is set', () => {
|
|
138
|
+
process.env.DEBUG = '1';
|
|
139
|
+
const error = new Error('Debug error');
|
|
140
|
+
error.stack = 'Error: Debug error\n at test.ts:10:20';
|
|
141
|
+
|
|
142
|
+
handleError(error);
|
|
143
|
+
|
|
144
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
145
|
+
expect.stringContaining('at test.ts'),
|
|
146
|
+
);
|
|
147
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('hides stack trace when DEBUG is not set', () => {
|
|
151
|
+
delete process.env.DEBUG;
|
|
152
|
+
const error = new Error('Regular error');
|
|
153
|
+
error.stack = 'Error: Regular error\n at test.ts:10:20';
|
|
154
|
+
|
|
155
|
+
handleError(error);
|
|
156
|
+
|
|
157
|
+
// Should show error message
|
|
158
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
159
|
+
expect.stringContaining('Regular error'),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Should NOT show stack trace
|
|
163
|
+
const calls = consoleErrorSpy.mock.calls;
|
|
164
|
+
const hasStackTrace = calls.some((call) =>
|
|
165
|
+
call.some((arg) => String(arg).includes('at test.ts')),
|
|
166
|
+
);
|
|
167
|
+
expect(hasStackTrace).toBe(false);
|
|
168
|
+
|
|
169
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('handles unknown errors', () => {
|
|
173
|
+
const error = 'string error';
|
|
174
|
+
|
|
175
|
+
handleError(error);
|
|
176
|
+
|
|
177
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
178
|
+
expect.stringContaining('Unknown error'),
|
|
179
|
+
);
|
|
180
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('exits with code 1 for all errors', () => {
|
|
184
|
+
const errors = [
|
|
185
|
+
new ValidationError('test'),
|
|
186
|
+
new SlideParseError('test', 'test.md'),
|
|
187
|
+
new DeckLoadError('test', './slides'),
|
|
188
|
+
new ThemeError('test', 'theme'),
|
|
189
|
+
new Error('ENOENT'),
|
|
190
|
+
new Error('ffmpeg'),
|
|
191
|
+
new Error('generic'),
|
|
192
|
+
'unknown',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
for (const error of errors) {
|
|
196
|
+
processExitSpy.mockClear();
|
|
197
|
+
handleError(error);
|
|
198
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI help text
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from 'bun:test';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { presentCommand } from '../commands/present.js';
|
|
8
|
+
import { exportCommand } from '../commands/export.js';
|
|
9
|
+
import { initCommand } from '../commands/init.js';
|
|
10
|
+
|
|
11
|
+
describe('CLI help text', () => {
|
|
12
|
+
test('present command has description', () => {
|
|
13
|
+
expect(presentCommand.description()).toBeTruthy();
|
|
14
|
+
expect(presentCommand.description()).toContain('present');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('present command has all options', () => {
|
|
18
|
+
const options = presentCommand.options;
|
|
19
|
+
|
|
20
|
+
const optionNames = options.map((opt) => opt.long);
|
|
21
|
+
expect(optionNames).toContain('--start');
|
|
22
|
+
expect(optionNames).toContain('--notes');
|
|
23
|
+
expect(optionNames).toContain('--notes-tty');
|
|
24
|
+
expect(optionNames).toContain('--loop');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('present command has short options', () => {
|
|
28
|
+
const options = presentCommand.options;
|
|
29
|
+
|
|
30
|
+
const shortNames = options.map((opt) => opt.short).filter(Boolean);
|
|
31
|
+
expect(shortNames).toContain('-s');
|
|
32
|
+
expect(shortNames).toContain('-n');
|
|
33
|
+
expect(shortNames).toContain('-l');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('present command requires dir argument', () => {
|
|
37
|
+
const args = presentCommand.registeredArguments;
|
|
38
|
+
expect(args.length).toBeGreaterThan(0);
|
|
39
|
+
expect(args[0].name()).toBe('dir');
|
|
40
|
+
expect(args[0].required).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('export command has description', () => {
|
|
44
|
+
expect(exportCommand.description()).toBeTruthy();
|
|
45
|
+
expect(exportCommand.description().toLowerCase()).toContain('export');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('export command has all options', () => {
|
|
49
|
+
const options = exportCommand.options;
|
|
50
|
+
|
|
51
|
+
const optionNames = options.map((opt) => opt.long);
|
|
52
|
+
expect(optionNames).toContain('--output');
|
|
53
|
+
expect(optionNames).toContain('--width');
|
|
54
|
+
expect(optionNames).toContain('--height');
|
|
55
|
+
expect(optionNames).toContain('--fps');
|
|
56
|
+
expect(optionNames).toContain('--slide-time');
|
|
57
|
+
expect(optionNames).toContain('--quality');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('export command has short options', () => {
|
|
61
|
+
const options = exportCommand.options;
|
|
62
|
+
|
|
63
|
+
const shortNames = options.map((opt) => opt.short).filter(Boolean);
|
|
64
|
+
expect(shortNames).toContain('-o');
|
|
65
|
+
expect(shortNames).toContain('-w');
|
|
66
|
+
expect(shortNames).toContain('-h');
|
|
67
|
+
expect(shortNames).toContain('-t');
|
|
68
|
+
expect(shortNames).toContain('-q');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('export command requires dir argument', () => {
|
|
72
|
+
const args = exportCommand.registeredArguments;
|
|
73
|
+
expect(args.length).toBeGreaterThan(0);
|
|
74
|
+
expect(args[0].name()).toBe('dir');
|
|
75
|
+
expect(args[0].required).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('export command requires output option', () => {
|
|
79
|
+
const outputOption = exportCommand.options.find((opt) => opt.long === '--output');
|
|
80
|
+
expect(outputOption).toBeTruthy();
|
|
81
|
+
expect(outputOption?.required).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('init command has description', () => {
|
|
85
|
+
expect(initCommand.description()).toBeTruthy();
|
|
86
|
+
expect(initCommand.description()).toContain('presentation deck');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('init command has theme option', () => {
|
|
90
|
+
const options = initCommand.options;
|
|
91
|
+
|
|
92
|
+
const optionNames = options.map((opt) => opt.long);
|
|
93
|
+
expect(optionNames).toContain('--theme');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('init command has short theme option', () => {
|
|
97
|
+
const options = initCommand.options;
|
|
98
|
+
|
|
99
|
+
const shortNames = options.map((opt) => opt.short).filter(Boolean);
|
|
100
|
+
expect(shortNames).toContain('-t');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('init command requires name argument', () => {
|
|
104
|
+
const args = initCommand.registeredArguments;
|
|
105
|
+
expect(args.length).toBeGreaterThan(0);
|
|
106
|
+
expect(args[0].name()).toBe('name');
|
|
107
|
+
expect(args[0].required).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('main program would have version', () => {
|
|
111
|
+
// Test that version can be imported from package.json
|
|
112
|
+
const pkg = require('../../../package.json');
|
|
113
|
+
expect(pkg.version).toBeTruthy();
|
|
114
|
+
expect(typeof pkg.version).toBe('string');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('main program would have name', () => {
|
|
118
|
+
const pkg = require('../../../package.json');
|
|
119
|
+
expect(pkg.name).toBeTruthy();
|
|
120
|
+
expect(pkg.name).toBe('term-deck');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('all commands have help text', () => {
|
|
124
|
+
const commands = [presentCommand, exportCommand, initCommand];
|
|
125
|
+
|
|
126
|
+
for (const cmd of commands) {
|
|
127
|
+
// Commander automatically generates help
|
|
128
|
+
const helpInfo = cmd.helpInformation();
|
|
129
|
+
expect(helpInfo).toBeTruthy();
|
|
130
|
+
expect(helpInfo.length).toBeGreaterThan(0);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('present command help includes options descriptions', () => {
|
|
135
|
+
const helpInfo = presentCommand.helpInformation();
|
|
136
|
+
|
|
137
|
+
expect(helpInfo).toContain('start');
|
|
138
|
+
expect(helpInfo).toContain('notes');
|
|
139
|
+
expect(helpInfo).toContain('loop');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('export command help includes options descriptions', () => {
|
|
143
|
+
const helpInfo = exportCommand.helpInformation();
|
|
144
|
+
|
|
145
|
+
expect(helpInfo).toContain('output');
|
|
146
|
+
expect(helpInfo).toContain('width');
|
|
147
|
+
expect(helpInfo).toContain('height');
|
|
148
|
+
expect(helpInfo).toContain('fps');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('init command help includes options descriptions', () => {
|
|
152
|
+
const helpInfo = initCommand.helpInformation();
|
|
153
|
+
|
|
154
|
+
expect(helpInfo).toContain('theme');
|
|
155
|
+
expect(helpInfo).toContain('name');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for init command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
6
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { initDeck } from '../commands/init.js';
|
|
9
|
+
|
|
10
|
+
const TEST_DECK_NAME = 'test-deck-init';
|
|
11
|
+
const TEST_DECK_PATH = join(process.cwd(), TEST_DECK_NAME);
|
|
12
|
+
|
|
13
|
+
describe('initDeck', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Clean up if test directory exists
|
|
16
|
+
if (existsSync(TEST_DECK_PATH)) {
|
|
17
|
+
rmSync(TEST_DECK_PATH, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
// Clean up after test
|
|
23
|
+
if (existsSync(TEST_DECK_PATH)) {
|
|
24
|
+
rmSync(TEST_DECK_PATH, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('creates deck directory structure', async () => {
|
|
29
|
+
await initDeck(TEST_DECK_NAME, 'matrix');
|
|
30
|
+
|
|
31
|
+
// Check directories
|
|
32
|
+
expect(existsSync(TEST_DECK_PATH)).toBe(true);
|
|
33
|
+
expect(existsSync(join(TEST_DECK_PATH, 'slides'))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('creates deck.config.ts', async () => {
|
|
37
|
+
await initDeck(TEST_DECK_NAME, 'matrix');
|
|
38
|
+
|
|
39
|
+
const configPath = join(TEST_DECK_PATH, 'slides', 'deck.config.ts');
|
|
40
|
+
expect(existsSync(configPath)).toBe(true);
|
|
41
|
+
|
|
42
|
+
const content = await Bun.file(configPath).text();
|
|
43
|
+
expect(content).toContain('import { defineConfig }');
|
|
44
|
+
expect(content).toContain('import matrix from');
|
|
45
|
+
expect(content).toContain(`title: '${TEST_DECK_NAME}'`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('creates sample slides', async () => {
|
|
49
|
+
await initDeck(TEST_DECK_NAME, 'matrix');
|
|
50
|
+
|
|
51
|
+
const slidesDir = join(TEST_DECK_PATH, 'slides');
|
|
52
|
+
expect(existsSync(join(slidesDir, '01-intro.md'))).toBe(true);
|
|
53
|
+
expect(existsSync(join(slidesDir, '02-content.md'))).toBe(true);
|
|
54
|
+
expect(existsSync(join(slidesDir, '03-end.md'))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('slides have valid frontmatter', async () => {
|
|
58
|
+
await initDeck(TEST_DECK_NAME, 'matrix');
|
|
59
|
+
|
|
60
|
+
const slidesDir = join(TEST_DECK_PATH, 'slides');
|
|
61
|
+
const slide1 = await Bun.file(join(slidesDir, '01-intro.md')).text();
|
|
62
|
+
const slide2 = await Bun.file(join(slidesDir, '02-content.md')).text();
|
|
63
|
+
const slide3 = await Bun.file(join(slidesDir, '03-end.md')).text();
|
|
64
|
+
|
|
65
|
+
// Check frontmatter structure
|
|
66
|
+
expect(slide1).toMatch(/^---\s*\ntitle:/);
|
|
67
|
+
expect(slide1).toContain('bigText:');
|
|
68
|
+
expect(slide1).toContain('gradient:');
|
|
69
|
+
|
|
70
|
+
expect(slide2).toMatch(/^---\s*\ntitle:/);
|
|
71
|
+
expect(slide2).toContain('bigText:');
|
|
72
|
+
expect(slide2).toContain('gradient:');
|
|
73
|
+
|
|
74
|
+
expect(slide3).toMatch(/^---\s*\ntitle:/);
|
|
75
|
+
expect(slide3).toContain('bigText:');
|
|
76
|
+
expect(slide3).toContain('gradient:');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('slide 2 includes presenter notes', async () => {
|
|
80
|
+
await initDeck(TEST_DECK_NAME, 'matrix');
|
|
81
|
+
|
|
82
|
+
const slidesDir = join(TEST_DECK_PATH, 'slides');
|
|
83
|
+
const slide2 = await Bun.file(join(slidesDir, '02-content.md')).text();
|
|
84
|
+
|
|
85
|
+
expect(slide2).toContain('<!-- notes -->');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('creates README.md', async () => {
|
|
89
|
+
await initDeck(TEST_DECK_NAME, 'matrix');
|
|
90
|
+
|
|
91
|
+
const readmePath = join(TEST_DECK_PATH, 'README.md');
|
|
92
|
+
expect(existsSync(readmePath)).toBe(true);
|
|
93
|
+
|
|
94
|
+
const content = await Bun.file(readmePath).text();
|
|
95
|
+
expect(content).toContain(`# ${TEST_DECK_NAME}`);
|
|
96
|
+
expect(content).toContain('term-deck present');
|
|
97
|
+
expect(content).toContain('term-deck export');
|
|
98
|
+
expect(content).toContain('Hotkeys');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('uses deck name in slide titles', async () => {
|
|
102
|
+
await initDeck(TEST_DECK_NAME, 'matrix');
|
|
103
|
+
|
|
104
|
+
const slide1 = await Bun.file(
|
|
105
|
+
join(TEST_DECK_PATH, 'slides', '01-intro.md'),
|
|
106
|
+
).text();
|
|
107
|
+
|
|
108
|
+
expect(slide1).toContain(TEST_DECK_NAME.toUpperCase());
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export Command
|
|
3
|
+
*
|
|
4
|
+
* Exports a presentation to GIF or MP4 format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { exportPresentation } from '../../export/recorder.js';
|
|
9
|
+
import { handleError } from '../errors.js';
|
|
10
|
+
|
|
11
|
+
export const exportCommand = new Command('export')
|
|
12
|
+
.description('Export presentation to GIF or MP4')
|
|
13
|
+
.argument('<dir>', 'Slides directory')
|
|
14
|
+
.requiredOption('-o, --output <file>', 'Output file (.mp4 or .gif)')
|
|
15
|
+
.option('-w, --width <n>', 'Terminal width in characters', '120')
|
|
16
|
+
.option('-h, --height <n>', 'Terminal height in characters', '40')
|
|
17
|
+
.option('--fps <n>', 'Frames per second', '30')
|
|
18
|
+
.option('-t, --slide-time <n>', 'Seconds per slide', '3')
|
|
19
|
+
.option('-q, --quality <n>', 'Quality 1-100 (video only)', '80')
|
|
20
|
+
.action(async (dir, options) => {
|
|
21
|
+
try {
|
|
22
|
+
await exportPresentation(dir, {
|
|
23
|
+
output: options.output,
|
|
24
|
+
width: Number.parseInt(options.width, 10),
|
|
25
|
+
height: Number.parseInt(options.height, 10),
|
|
26
|
+
fps: Number.parseInt(options.fps, 10),
|
|
27
|
+
slideTime: Number.parseFloat(options.slideTime),
|
|
28
|
+
quality: Number.parseInt(options.quality, 10),
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
handleError(error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Command
|
|
3
|
+
*
|
|
4
|
+
* Creates a new presentation deck with sample slides and configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { handleError } from '../errors.js';
|
|
10
|
+
|
|
11
|
+
export const initCommand = new Command('init')
|
|
12
|
+
.description('Create a new presentation deck')
|
|
13
|
+
.argument('<name>', 'Deck name (will create directory)')
|
|
14
|
+
.option('-t, --theme <name>', 'Theme to use', 'matrix')
|
|
15
|
+
.action(async (name, options) => {
|
|
16
|
+
try {
|
|
17
|
+
await initDeck(name, options.theme);
|
|
18
|
+
console.log(`Created deck: ${name}/`);
|
|
19
|
+
console.log('\nNext steps:');
|
|
20
|
+
console.log(` cd ${name}/slides`);
|
|
21
|
+
console.log(' term-deck present .');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
handleError(error);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize a new deck directory
|
|
29
|
+
*
|
|
30
|
+
* Creates the directory structure, configuration file, sample slides,
|
|
31
|
+
* and README for a new presentation deck.
|
|
32
|
+
*/
|
|
33
|
+
export async function initDeck(name: string, theme: string): Promise<void> {
|
|
34
|
+
const deckDir = join(process.cwd(), name);
|
|
35
|
+
const slidesDir = join(deckDir, 'slides');
|
|
36
|
+
|
|
37
|
+
// Create directories
|
|
38
|
+
await Bun.write(join(slidesDir, '.gitkeep'), '');
|
|
39
|
+
|
|
40
|
+
// Create deck.config.ts
|
|
41
|
+
const configContent = `import { defineConfig } from 'term-deck'
|
|
42
|
+
import matrix from '@term-deck/theme-matrix'
|
|
43
|
+
|
|
44
|
+
export default defineConfig({
|
|
45
|
+
title: '${name}',
|
|
46
|
+
theme: matrix,
|
|
47
|
+
})
|
|
48
|
+
`;
|
|
49
|
+
await Bun.write(join(slidesDir, 'deck.config.ts'), configContent);
|
|
50
|
+
|
|
51
|
+
// Create sample slides
|
|
52
|
+
const slide1 = `---
|
|
53
|
+
title: ${name.toUpperCase()}
|
|
54
|
+
bigText: ${name.toUpperCase()}
|
|
55
|
+
gradient: fire
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
{GREEN}Welcome to your presentation{/}
|
|
59
|
+
|
|
60
|
+
Press {CYAN}Space{/} or {CYAN}→{/} to advance
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const slide2 = `---
|
|
64
|
+
title: SLIDE TWO
|
|
65
|
+
bigText: HELLO
|
|
66
|
+
gradient: cool
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
{WHITE}This is the second slide{/}
|
|
70
|
+
|
|
71
|
+
- Point one
|
|
72
|
+
- Point two
|
|
73
|
+
- Point three
|
|
74
|
+
|
|
75
|
+
<!-- notes -->
|
|
76
|
+
Remember to explain each point clearly.
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const slide3 = `---
|
|
80
|
+
title: THE END
|
|
81
|
+
bigText: FIN
|
|
82
|
+
gradient: pink
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
{ORANGE}Thank you!{/}
|
|
86
|
+
|
|
87
|
+
Press {CYAN}q{/} to exit
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
await Bun.write(join(slidesDir, '01-intro.md'), slide1);
|
|
91
|
+
await Bun.write(join(slidesDir, '02-content.md'), slide2);
|
|
92
|
+
await Bun.write(join(slidesDir, '03-end.md'), slide3);
|
|
93
|
+
|
|
94
|
+
// Create README
|
|
95
|
+
const readme = `# ${name}
|
|
96
|
+
|
|
97
|
+
A term-deck presentation.
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
|
|
101
|
+
\`\`\`bash
|
|
102
|
+
cd slides
|
|
103
|
+
term-deck present .
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
## Export
|
|
107
|
+
|
|
108
|
+
\`\`\`bash
|
|
109
|
+
term-deck export slides/ -o ${name}.mp4
|
|
110
|
+
term-deck export slides/ -o ${name}.gif
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
## Hotkeys
|
|
114
|
+
|
|
115
|
+
| Key | Action |
|
|
116
|
+
|-----|--------|
|
|
117
|
+
| Space / → | Next slide |
|
|
118
|
+
| ← | Previous slide |
|
|
119
|
+
| 0-9 | Jump to slide |
|
|
120
|
+
| l | Show slide list |
|
|
121
|
+
| q | Quit |
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
await Bun.write(join(deckDir, 'README.md'), readme);
|
|
125
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Present Command
|
|
3
|
+
*
|
|
4
|
+
* Starts a presentation with the given options.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { present } from '../../presenter/main.js';
|
|
9
|
+
import { handleError } from '../errors.js';
|
|
10
|
+
|
|
11
|
+
export const presentCommand = new Command('present')
|
|
12
|
+
.description('Start a presentation')
|
|
13
|
+
.argument('<dir>', 'Slides directory')
|
|
14
|
+
.option('-s, --start <n>', 'Start at slide number', '0')
|
|
15
|
+
.option('-n, --notes', 'Show presenter notes in separate terminal')
|
|
16
|
+
.option('--notes-tty <path>', 'TTY device for notes window (e.g., /dev/ttys001)')
|
|
17
|
+
.option('-l, --loop', 'Loop back to first slide after last')
|
|
18
|
+
.action(async (dir, options) => {
|
|
19
|
+
try {
|
|
20
|
+
await present(dir, {
|
|
21
|
+
startSlide: Number.parseInt(options.start, 10),
|
|
22
|
+
showNotes: options.notes,
|
|
23
|
+
notesTty: options.notesTty,
|
|
24
|
+
loop: options.loop,
|
|
25
|
+
});
|
|
26
|
+
} catch (error) {
|
|
27
|
+
handleError(error);
|
|
28
|
+
}
|
|
29
|
+
});
|