@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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -0
  3. package/bin/term-deck.ts +45 -0
  4. package/examples/slides/01-welcome.md +9 -0
  5. package/examples/slides/02-features.md +12 -0
  6. package/examples/slides/03-colors.md +17 -0
  7. package/examples/slides/04-ascii-art.md +11 -0
  8. package/examples/slides/05-gradients.md +14 -0
  9. package/examples/slides/06-themes.md +13 -0
  10. package/examples/slides/07-markdown.md +13 -0
  11. package/examples/slides/08-controls.md +13 -0
  12. package/examples/slides/09-thanks.md +11 -0
  13. package/examples/slides/deck.config.ts +13 -0
  14. package/examples/slides-hacker/01-welcome.md +9 -0
  15. package/examples/slides-hacker/02-features.md +12 -0
  16. package/examples/slides-hacker/03-colors.md +17 -0
  17. package/examples/slides-hacker/04-ascii-art.md +11 -0
  18. package/examples/slides-hacker/05-gradients.md +14 -0
  19. package/examples/slides-hacker/06-themes.md +13 -0
  20. package/examples/slides-hacker/07-markdown.md +13 -0
  21. package/examples/slides-hacker/08-controls.md +13 -0
  22. package/examples/slides-hacker/09-thanks.md +11 -0
  23. package/examples/slides-hacker/deck.config.ts +13 -0
  24. package/examples/slides-matrix/01-welcome.md +9 -0
  25. package/examples/slides-matrix/02-features.md +12 -0
  26. package/examples/slides-matrix/03-colors.md +17 -0
  27. package/examples/slides-matrix/04-ascii-art.md +11 -0
  28. package/examples/slides-matrix/05-gradients.md +14 -0
  29. package/examples/slides-matrix/06-themes.md +13 -0
  30. package/examples/slides-matrix/07-markdown.md +13 -0
  31. package/examples/slides-matrix/08-controls.md +13 -0
  32. package/examples/slides-matrix/09-thanks.md +11 -0
  33. package/examples/slides-matrix/deck.config.ts +13 -0
  34. package/examples/slides-minimal/01-welcome.md +9 -0
  35. package/examples/slides-minimal/02-features.md +12 -0
  36. package/examples/slides-minimal/03-colors.md +17 -0
  37. package/examples/slides-minimal/04-ascii-art.md +11 -0
  38. package/examples/slides-minimal/05-gradients.md +14 -0
  39. package/examples/slides-minimal/06-themes.md +13 -0
  40. package/examples/slides-minimal/07-markdown.md +13 -0
  41. package/examples/slides-minimal/08-controls.md +13 -0
  42. package/examples/slides-minimal/09-thanks.md +11 -0
  43. package/examples/slides-minimal/deck.config.ts +13 -0
  44. package/examples/slides-neon/01-welcome.md +9 -0
  45. package/examples/slides-neon/02-features.md +12 -0
  46. package/examples/slides-neon/03-colors.md +17 -0
  47. package/examples/slides-neon/04-ascii-art.md +11 -0
  48. package/examples/slides-neon/05-gradients.md +14 -0
  49. package/examples/slides-neon/06-themes.md +13 -0
  50. package/examples/slides-neon/07-markdown.md +13 -0
  51. package/examples/slides-neon/08-controls.md +13 -0
  52. package/examples/slides-neon/09-thanks.md +11 -0
  53. package/examples/slides-neon/deck.config.ts +13 -0
  54. package/examples/slides-retro/01-welcome.md +9 -0
  55. package/examples/slides-retro/02-features.md +12 -0
  56. package/examples/slides-retro/03-colors.md +17 -0
  57. package/examples/slides-retro/04-ascii-art.md +11 -0
  58. package/examples/slides-retro/05-gradients.md +14 -0
  59. package/examples/slides-retro/06-themes.md +13 -0
  60. package/examples/slides-retro/07-markdown.md +13 -0
  61. package/examples/slides-retro/08-controls.md +13 -0
  62. package/examples/slides-retro/09-thanks.md +11 -0
  63. package/examples/slides-retro/deck.config.ts +13 -0
  64. package/package.json +66 -0
  65. package/src/cli/__tests__/errors.test.ts +201 -0
  66. package/src/cli/__tests__/help.test.ts +157 -0
  67. package/src/cli/__tests__/init.test.ts +110 -0
  68. package/src/cli/commands/export.ts +33 -0
  69. package/src/cli/commands/init.ts +125 -0
  70. package/src/cli/commands/present.ts +29 -0
  71. package/src/cli/errors.ts +77 -0
  72. package/src/core/__tests__/slide.test.ts +1759 -0
  73. package/src/core/__tests__/theme.test.ts +1103 -0
  74. package/src/core/slide.ts +509 -0
  75. package/src/core/theme.ts +388 -0
  76. package/src/export/__tests__/recorder.test.ts +566 -0
  77. package/src/export/recorder.ts +639 -0
  78. package/src/index.ts +36 -0
  79. package/src/presenter/__tests__/main.test.ts +244 -0
  80. package/src/presenter/main.ts +658 -0
  81. package/src/renderer/__tests__/screen-extended.test.ts +801 -0
  82. package/src/renderer/__tests__/screen.test.ts +525 -0
  83. package/src/renderer/screen.ts +671 -0
  84. package/src/schemas/__tests__/config.test.ts +429 -0
  85. package/src/schemas/__tests__/slide.test.ts +349 -0
  86. package/src/schemas/__tests__/theme.test.ts +970 -0
  87. package/src/schemas/__tests__/validation.test.ts +256 -0
  88. package/src/schemas/config.ts +58 -0
  89. package/src/schemas/slide.ts +56 -0
  90. package/src/schemas/theme.ts +203 -0
  91. package/src/schemas/validation.ts +64 -0
  92. package/src/themes/matrix/index.ts +53 -0
  93. package/themes/hacker.ts +53 -0
  94. package/themes/minimal.ts +53 -0
  95. package/themes/neon.ts +53 -0
  96. 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
+ });