@pep/term-deck 1.0.14 → 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.js +0 -14
- 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
package/src/presenter/main.ts
DELETED
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
import blessed from 'neo-blessed';
|
|
2
|
-
import type { Deck } from '../core/slide.js';
|
|
3
|
-
import type { Renderer } from '../renderer/screen.js';
|
|
4
|
-
import { loadDeck } from '../core/slide.js';
|
|
5
|
-
import { createRenderer, destroyRenderer, renderSlide, clearWindows } from '../renderer/screen.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Presenter state
|
|
9
|
-
*
|
|
10
|
-
* Manages the state of the active presentation including:
|
|
11
|
-
* - The loaded deck with slides and configuration
|
|
12
|
-
* - The renderer instance for displaying slides
|
|
13
|
-
* - Current slide index
|
|
14
|
-
* - Animation state to prevent concurrent navigation
|
|
15
|
-
* - Optional notes window for presenter mode
|
|
16
|
-
* - Optional auto-advance timer
|
|
17
|
-
* - Optional progress bar
|
|
18
|
-
*/
|
|
19
|
-
export interface Presenter {
|
|
20
|
-
deck: Deck;
|
|
21
|
-
renderer: Renderer;
|
|
22
|
-
currentSlide: number;
|
|
23
|
-
isAnimating: boolean;
|
|
24
|
-
notesWindow: NotesWindow | null;
|
|
25
|
-
autoAdvanceTimer: NodeJS.Timer | null;
|
|
26
|
-
progressBar: blessed.Widgets.ProgressBarElement | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Notes window state (separate terminal)
|
|
31
|
-
*
|
|
32
|
-
* Represents a secondary display on a different TTY for presenter notes.
|
|
33
|
-
* Shows current slide notes, slide number, and preview of next slide.
|
|
34
|
-
*/
|
|
35
|
-
export interface NotesWindow {
|
|
36
|
-
screen: blessed.Widgets.Screen;
|
|
37
|
-
contentBox: blessed.Widgets.BoxElement;
|
|
38
|
-
tty: string; // TTY device path (e.g., '/dev/tty2')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Presentation options
|
|
43
|
-
*
|
|
44
|
-
* Configuration options for starting a presentation:
|
|
45
|
-
* - startSlide: Index of slide to start from (defaults to 0)
|
|
46
|
-
* - showNotes: Whether to open a notes window on separate TTY
|
|
47
|
-
* - notesTty: Specific TTY device path for notes (optional, will auto-detect if not provided)
|
|
48
|
-
* - loop: Whether to loop back to first slide after reaching the end
|
|
49
|
-
*/
|
|
50
|
-
export interface PresentOptions {
|
|
51
|
-
startSlide?: number;
|
|
52
|
-
showNotes?: boolean;
|
|
53
|
-
notesTty?: string;
|
|
54
|
-
loop?: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Start a presentation
|
|
59
|
-
*
|
|
60
|
-
* Loads the deck, creates the renderer, sets up keyboard controls,
|
|
61
|
-
* and enters the main presentation loop.
|
|
62
|
-
*
|
|
63
|
-
* @param slidesDir - Directory containing markdown slides and deck.config.ts
|
|
64
|
-
* @param options - Presentation options (startSlide, showNotes, notesTty, loop)
|
|
65
|
-
* @returns Promise that resolves when the presentation ends (user quits)
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```typescript
|
|
69
|
-
* await present('./slides', { startSlide: 0, showNotes: true });
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
export async function present(
|
|
73
|
-
slidesDir: string,
|
|
74
|
-
options: PresentOptions = {}
|
|
75
|
-
): Promise<void> {
|
|
76
|
-
// Load deck
|
|
77
|
-
const deck = await loadDeck(slidesDir);
|
|
78
|
-
|
|
79
|
-
if (deck.slides.length === 0) {
|
|
80
|
-
throw new Error(`No slides found in ${slidesDir}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Create renderer
|
|
84
|
-
const renderer = createRenderer(deck.config.theme);
|
|
85
|
-
|
|
86
|
-
// Create presenter state
|
|
87
|
-
const presenter: Presenter = {
|
|
88
|
-
deck,
|
|
89
|
-
renderer,
|
|
90
|
-
currentSlide: options.startSlide ?? deck.config.settings?.startSlide ?? 0,
|
|
91
|
-
isAnimating: false,
|
|
92
|
-
notesWindow: null,
|
|
93
|
-
autoAdvanceTimer: null,
|
|
94
|
-
progressBar: null,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Setup notes window if requested
|
|
98
|
-
if (options.showNotes) {
|
|
99
|
-
presenter.notesWindow = await createNotesWindow(options.notesTty);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Setup progress bar if enabled
|
|
103
|
-
if (deck.config.settings?.showProgress) {
|
|
104
|
-
presenter.progressBar = createProgressBar(presenter);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Setup keyboard controls
|
|
108
|
-
setupControls(presenter);
|
|
109
|
-
|
|
110
|
-
// Show first slide
|
|
111
|
-
await showSlide(presenter, presenter.currentSlide);
|
|
112
|
-
|
|
113
|
-
// Update progress bar
|
|
114
|
-
if (presenter.progressBar) {
|
|
115
|
-
updateProgress(presenter.progressBar, presenter.currentSlide, deck.slides.length);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Start auto-advance if configured
|
|
119
|
-
presenter.autoAdvanceTimer = startAutoAdvance(presenter);
|
|
120
|
-
|
|
121
|
-
// Keep process alive until quit
|
|
122
|
-
await new Promise<void>((resolve) => {
|
|
123
|
-
renderer.screen.key(['q', 'C-c', 'escape'], () => {
|
|
124
|
-
cleanup(presenter);
|
|
125
|
-
resolve();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Cleanup resources
|
|
132
|
-
*
|
|
133
|
-
* Destroys the notes window (if present), stops auto-advance timer,
|
|
134
|
-
* and destroys the main renderer, freeing all resources and restoring the terminal.
|
|
135
|
-
*
|
|
136
|
-
* @param presenter - The presenter state to clean up
|
|
137
|
-
*/
|
|
138
|
-
function cleanup(presenter: Presenter): void {
|
|
139
|
-
stopAutoAdvance(presenter.autoAdvanceTimer);
|
|
140
|
-
if (presenter.notesWindow) {
|
|
141
|
-
presenter.notesWindow.screen.destroy();
|
|
142
|
-
}
|
|
143
|
-
destroyRenderer(presenter.renderer);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Show a specific slide (placeholder for task 5.4)
|
|
148
|
-
*/
|
|
149
|
-
async function showSlide(presenter: Presenter, index: number): Promise<void> {
|
|
150
|
-
if (presenter.isAnimating) return;
|
|
151
|
-
if (index < 0 || index >= presenter.deck.slides.length) return;
|
|
152
|
-
|
|
153
|
-
presenter.isAnimating = true;
|
|
154
|
-
presenter.currentSlide = index;
|
|
155
|
-
|
|
156
|
-
const slide = presenter.deck.slides[index];
|
|
157
|
-
|
|
158
|
-
// Render slide
|
|
159
|
-
await renderSlide(presenter.renderer, slide);
|
|
160
|
-
presenter.renderer.screen.render();
|
|
161
|
-
|
|
162
|
-
// Update notes window
|
|
163
|
-
if (presenter.notesWindow) {
|
|
164
|
-
updateNotesWindow(presenter);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Update progress bar
|
|
168
|
-
if (presenter.progressBar) {
|
|
169
|
-
updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
presenter.isAnimating = false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Go to next slide
|
|
177
|
-
*
|
|
178
|
-
* Advances to the next slide in the deck. If at the last slide:
|
|
179
|
-
* - If loop is enabled, wraps to first slide
|
|
180
|
-
* - If loop is disabled, stays on last slide
|
|
181
|
-
*
|
|
182
|
-
* @param presenter - The presenter state
|
|
183
|
-
*/
|
|
184
|
-
export async function nextSlide(presenter: Presenter): Promise<void> {
|
|
185
|
-
const nextIndex = presenter.currentSlide + 1;
|
|
186
|
-
const { slides } = presenter.deck;
|
|
187
|
-
const loop = presenter.deck.config.settings?.loop ?? false;
|
|
188
|
-
|
|
189
|
-
if (nextIndex >= slides.length) {
|
|
190
|
-
if (loop) {
|
|
191
|
-
await showSlide(presenter, 0);
|
|
192
|
-
}
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
await showSlide(presenter, nextIndex);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Go to previous slide
|
|
201
|
-
*
|
|
202
|
-
* Goes back to the previous slide in the deck. If at the first slide:
|
|
203
|
-
* - If loop is enabled, wraps to last slide
|
|
204
|
-
* - If loop is disabled, stays on first slide
|
|
205
|
-
*
|
|
206
|
-
* To maintain the stacked window effect, this function clears all windows
|
|
207
|
-
* and re-renders all slides from 0 up to the target slide.
|
|
208
|
-
*
|
|
209
|
-
* @param presenter - The presenter state
|
|
210
|
-
*/
|
|
211
|
-
export async function prevSlide(presenter: Presenter): Promise<void> {
|
|
212
|
-
const prevIndex = presenter.currentSlide - 1;
|
|
213
|
-
const { slides } = presenter.deck;
|
|
214
|
-
const loop = presenter.deck.config.settings?.loop ?? false;
|
|
215
|
-
|
|
216
|
-
if (prevIndex < 0) {
|
|
217
|
-
if (loop) {
|
|
218
|
-
// Clear and re-render all slides up to the last one
|
|
219
|
-
clearWindows(presenter.renderer);
|
|
220
|
-
for (let i = 0; i < slides.length; i++) {
|
|
221
|
-
await renderSlide(presenter.renderer, slides[i]);
|
|
222
|
-
}
|
|
223
|
-
presenter.currentSlide = slides.length - 1;
|
|
224
|
-
|
|
225
|
-
// Update notes window
|
|
226
|
-
if (presenter.notesWindow) {
|
|
227
|
-
updateNotesWindow(presenter);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Update progress bar
|
|
231
|
-
if (presenter.progressBar) {
|
|
232
|
-
updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
presenter.renderer.screen.render();
|
|
236
|
-
}
|
|
237
|
-
// If not looping, stay at current slide (index 0)
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Clear windows and re-render from start up to prevIndex
|
|
242
|
-
clearWindows(presenter.renderer);
|
|
243
|
-
for (let i = 0; i <= prevIndex; i++) {
|
|
244
|
-
await renderSlide(presenter.renderer, slides[i]);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
presenter.currentSlide = prevIndex;
|
|
248
|
-
|
|
249
|
-
// Update notes window
|
|
250
|
-
if (presenter.notesWindow) {
|
|
251
|
-
updateNotesWindow(presenter);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Update progress bar
|
|
255
|
-
if (presenter.progressBar) {
|
|
256
|
-
updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Render screen to display changes
|
|
260
|
-
presenter.renderer.screen.render();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Jump to a specific slide by index
|
|
265
|
-
*
|
|
266
|
-
* Jumps directly to the specified slide index. To maintain the stacked window
|
|
267
|
-
* effect, this function clears all windows and re-renders all slides from 0
|
|
268
|
-
* up to the target slide.
|
|
269
|
-
*
|
|
270
|
-
* Invalid indices (negative or beyond deck length) are ignored.
|
|
271
|
-
*
|
|
272
|
-
* @param presenter - The presenter state
|
|
273
|
-
* @param index - The slide index to jump to (0-based)
|
|
274
|
-
*/
|
|
275
|
-
export async function jumpToSlide(presenter: Presenter, index: number): Promise<void> {
|
|
276
|
-
// Check bounds - ignore invalid indices
|
|
277
|
-
if (index < 0 || index >= presenter.deck.slides.length) return;
|
|
278
|
-
|
|
279
|
-
// Clear all windows to prepare for re-rendering
|
|
280
|
-
clearWindows(presenter.renderer);
|
|
281
|
-
|
|
282
|
-
// Re-render slides 0 through target index to preserve stacking
|
|
283
|
-
for (let i = 0; i <= index; i++) {
|
|
284
|
-
await renderSlide(presenter.renderer, presenter.deck.slides[i]);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Update current slide
|
|
288
|
-
presenter.currentSlide = index;
|
|
289
|
-
|
|
290
|
-
// Update notes window if present
|
|
291
|
-
if (presenter.notesWindow) {
|
|
292
|
-
updateNotesWindow(presenter);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Update progress bar
|
|
296
|
-
if (presenter.progressBar) {
|
|
297
|
-
updateProgress(presenter.progressBar, presenter.currentSlide, presenter.deck.slides.length);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Render screen to display changes
|
|
301
|
-
presenter.renderer.screen.render();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Setup keyboard event handlers
|
|
306
|
-
*
|
|
307
|
-
* Registers all keyboard controls for the presentation:
|
|
308
|
-
* - Next slide: Space, Enter, Right, n
|
|
309
|
-
* - Previous slide: Left, Backspace, p
|
|
310
|
-
* - Jump to slide: 0-9
|
|
311
|
-
* - Show slide list: l
|
|
312
|
-
* - Toggle notes visibility: N
|
|
313
|
-
* - Quit: q, Ctrl+C, Escape (handled in present() function)
|
|
314
|
-
*
|
|
315
|
-
* @param presenter - The presenter state
|
|
316
|
-
*/
|
|
317
|
-
function setupControls(presenter: Presenter): void {
|
|
318
|
-
const { screen } = presenter.renderer;
|
|
319
|
-
|
|
320
|
-
// Next slide: Space, Enter, Right, n
|
|
321
|
-
screen.key(['space', 'enter', 'right', 'n'], () => {
|
|
322
|
-
nextSlide(presenter);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
// Previous slide: Left, Backspace, p
|
|
326
|
-
screen.key(['left', 'backspace', 'p'], () => {
|
|
327
|
-
prevSlide(presenter);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// Jump to slide: 0-9
|
|
331
|
-
screen.key(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], (ch) => {
|
|
332
|
-
const index = parseInt(ch, 10);
|
|
333
|
-
jumpToSlide(presenter, index);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// Show slide list: l
|
|
337
|
-
screen.key(['l'], () => {
|
|
338
|
-
showSlideList(presenter);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// Toggle notes visibility: N (only if notes window exists)
|
|
342
|
-
screen.key(['N'], () => {
|
|
343
|
-
if (presenter.notesWindow) {
|
|
344
|
-
toggleNotesVisibility(presenter.notesWindow);
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Note: quit keys (q, Ctrl+C, Escape) are handled in the present() function
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Show slide list overlay
|
|
353
|
-
*
|
|
354
|
-
* Displays an overlay showing all slides in the deck with the current slide marked.
|
|
355
|
-
* User can press Escape, l, or q to close, or press a number key to jump to that slide.
|
|
356
|
-
*
|
|
357
|
-
* @param presenter - The presenter state
|
|
358
|
-
*/
|
|
359
|
-
function showSlideList(presenter: Presenter): void {
|
|
360
|
-
const { screen } = presenter.renderer;
|
|
361
|
-
const { slides } = presenter.deck;
|
|
362
|
-
|
|
363
|
-
// Build list content with current slide marker
|
|
364
|
-
const listContent = slides
|
|
365
|
-
.map((slide, i) => {
|
|
366
|
-
const marker = i === presenter.currentSlide ? '▶ ' : ' ';
|
|
367
|
-
return `${marker}${i}: ${slide.frontmatter.title}`;
|
|
368
|
-
})
|
|
369
|
-
.join('\n');
|
|
370
|
-
|
|
371
|
-
// Create overlay box centered on screen
|
|
372
|
-
const listBox = screen.box({
|
|
373
|
-
top: 'center',
|
|
374
|
-
left: 'center',
|
|
375
|
-
width: 50,
|
|
376
|
-
height: Math.min(slides.length + 4, 20),
|
|
377
|
-
border: { type: 'line' },
|
|
378
|
-
label: ' SLIDES (press number or Esc) ',
|
|
379
|
-
style: {
|
|
380
|
-
fg: '#ffffff',
|
|
381
|
-
bg: '#0a0a0a',
|
|
382
|
-
border: { fg: '#ffcc00' },
|
|
383
|
-
},
|
|
384
|
-
padding: 1,
|
|
385
|
-
tags: true,
|
|
386
|
-
content: listContent,
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
screen.append(listBox);
|
|
390
|
-
|
|
391
|
-
screen.render();
|
|
392
|
-
|
|
393
|
-
// Close list helper
|
|
394
|
-
const closeList = () => {
|
|
395
|
-
listBox.destroy();
|
|
396
|
-
screen.render();
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
// Close on Escape, l, or q
|
|
400
|
-
screen.onceKey(['escape', 'l', 'q'], closeList);
|
|
401
|
-
|
|
402
|
-
// Number keys jump to slide and close
|
|
403
|
-
screen.onceKey(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], (ch) => {
|
|
404
|
-
closeList();
|
|
405
|
-
jumpToSlide(presenter, parseInt(ch ?? '0', 10));
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Toggle notes window visibility
|
|
411
|
-
*
|
|
412
|
-
* Toggles the visibility of the notes window between shown and hidden.
|
|
413
|
-
*
|
|
414
|
-
* @param notesWindow - The notes window to toggle
|
|
415
|
-
*/
|
|
416
|
-
function toggleNotesVisibility(notesWindow: NotesWindow): void {
|
|
417
|
-
const { contentBox, screen } = notesWindow;
|
|
418
|
-
contentBox.toggle();
|
|
419
|
-
screen.render();
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Find an available TTY for notes window
|
|
424
|
-
*
|
|
425
|
-
* This is a best-effort approach - user should specify with --notes-tty.
|
|
426
|
-
* Searches common TTY paths on macOS and Linux.
|
|
427
|
-
*
|
|
428
|
-
* @returns Promise resolving to an available TTY path
|
|
429
|
-
* @throws Error if no available TTY is found
|
|
430
|
-
*/
|
|
431
|
-
async function findAvailableTty(): Promise<string> {
|
|
432
|
-
// On macOS, try common TTY paths
|
|
433
|
-
const candidates = [
|
|
434
|
-
'/dev/ttys001',
|
|
435
|
-
'/dev/ttys002',
|
|
436
|
-
'/dev/ttys003',
|
|
437
|
-
'/dev/pts/1',
|
|
438
|
-
'/dev/pts/2',
|
|
439
|
-
];
|
|
440
|
-
|
|
441
|
-
for (const tty of candidates) {
|
|
442
|
-
try {
|
|
443
|
-
const file = Bun.file(tty);
|
|
444
|
-
if (await file.exists()) {
|
|
445
|
-
return tty;
|
|
446
|
-
}
|
|
447
|
-
} catch {
|
|
448
|
-
// Continue to next candidate
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
throw new Error(
|
|
453
|
-
'Could not find available TTY for notes window. ' +
|
|
454
|
-
'Open a second terminal, run `tty`, and pass the path with --notes-tty'
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Create notes window on a separate TTY
|
|
460
|
-
*
|
|
461
|
-
* Creates a blessed screen on a different TTY device for displaying presenter notes.
|
|
462
|
-
* If no TTY is specified, attempts to find one automatically.
|
|
463
|
-
*
|
|
464
|
-
* Usage: Open a second terminal and run `tty` to get the device path,
|
|
465
|
-
* then pass it with --notes-tty /dev/ttys001
|
|
466
|
-
*
|
|
467
|
-
* @param ttyPath - Optional TTY device path (e.g., '/dev/ttys001')
|
|
468
|
-
* @returns Promise resolving to the created notes window
|
|
469
|
-
*
|
|
470
|
-
* @example
|
|
471
|
-
* ```typescript
|
|
472
|
-
* // With explicit TTY path
|
|
473
|
-
* const notesWindow = await createNotesWindow('/dev/ttys001');
|
|
474
|
-
*
|
|
475
|
-
* // Auto-detect TTY
|
|
476
|
-
* const notesWindow = await createNotesWindow();
|
|
477
|
-
* ```
|
|
478
|
-
*/
|
|
479
|
-
async function createNotesWindow(ttyPath?: string): Promise<NotesWindow> {
|
|
480
|
-
const blessed = (await import('neo-blessed')).default;
|
|
481
|
-
const { openSync } = await import('node:fs');
|
|
482
|
-
|
|
483
|
-
// If no TTY specified, try to find one
|
|
484
|
-
const tty = ttyPath ?? await findAvailableTty();
|
|
485
|
-
|
|
486
|
-
// Create screen on the specified TTY
|
|
487
|
-
const screen = blessed.screen({
|
|
488
|
-
smartCSR: true,
|
|
489
|
-
title: 'term-deck notes',
|
|
490
|
-
fullUnicode: true,
|
|
491
|
-
input: openSync(tty, 'r'),
|
|
492
|
-
output: openSync(tty, 'w'),
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// Create content box
|
|
496
|
-
const contentBox = blessed.box({
|
|
497
|
-
top: 0,
|
|
498
|
-
left: 0,
|
|
499
|
-
width: '100%',
|
|
500
|
-
height: '100%',
|
|
501
|
-
tags: true,
|
|
502
|
-
padding: 2,
|
|
503
|
-
style: {
|
|
504
|
-
fg: '#ffffff',
|
|
505
|
-
bg: '#1a1a1a',
|
|
506
|
-
},
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
screen.append(contentBox);
|
|
510
|
-
screen.render();
|
|
511
|
-
|
|
512
|
-
return {
|
|
513
|
-
screen,
|
|
514
|
-
contentBox,
|
|
515
|
-
tty,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Update notes window content for current slide
|
|
521
|
-
*
|
|
522
|
-
* Updates the notes window to display:
|
|
523
|
-
* - Current slide number and title
|
|
524
|
-
* - Presenter notes (or "No notes" if none exist)
|
|
525
|
-
* - Preview of next slide title (or "Last slide" if at end)
|
|
526
|
-
*
|
|
527
|
-
* @param presenter - The presenter state
|
|
528
|
-
*/
|
|
529
|
-
function updateNotesWindow(presenter: Presenter): void {
|
|
530
|
-
if (!presenter.notesWindow) return;
|
|
531
|
-
|
|
532
|
-
const { contentBox, screen } = presenter.notesWindow;
|
|
533
|
-
const { slides } = presenter.deck;
|
|
534
|
-
const currentIndex = presenter.currentSlide;
|
|
535
|
-
const currentSlide = slides[currentIndex];
|
|
536
|
-
const nextSlide = slides[currentIndex + 1];
|
|
537
|
-
|
|
538
|
-
// Build notes content
|
|
539
|
-
let content = '';
|
|
540
|
-
|
|
541
|
-
// Header
|
|
542
|
-
content += `{bold}Slide ${currentIndex + 1} of ${slides.length}{/bold}\n`;
|
|
543
|
-
content += `{gray-fg}${currentSlide.frontmatter.title}{/}\n`;
|
|
544
|
-
content += '\n';
|
|
545
|
-
content += '─'.repeat(50) + '\n';
|
|
546
|
-
content += '\n';
|
|
547
|
-
|
|
548
|
-
// Notes
|
|
549
|
-
if (currentSlide.notes) {
|
|
550
|
-
content += '{bold}PRESENTER NOTES:{/bold}\n\n';
|
|
551
|
-
content += currentSlide.notes + '\n';
|
|
552
|
-
} else {
|
|
553
|
-
content += '{gray-fg}No notes for this slide{/}\n';
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
content += '\n';
|
|
557
|
-
content += '─'.repeat(50) + '\n';
|
|
558
|
-
content += '\n';
|
|
559
|
-
|
|
560
|
-
// Next slide preview
|
|
561
|
-
if (nextSlide) {
|
|
562
|
-
content += `{bold}NEXT:{/bold} "${nextSlide.frontmatter.title}"\n`;
|
|
563
|
-
} else {
|
|
564
|
-
content += '{gray-fg}Last slide{/}\n';
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
contentBox.setContent(content);
|
|
568
|
-
screen.render();
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Start auto-advance timer
|
|
573
|
-
*
|
|
574
|
-
* Automatically advances to the next slide at a specified interval.
|
|
575
|
-
* Respects the isAnimating flag to avoid advancing during animations.
|
|
576
|
-
* Returns null if auto-advance is disabled (interval <= 0).
|
|
577
|
-
*
|
|
578
|
-
* @param presenter - The presenter state
|
|
579
|
-
* @returns Timer object if auto-advance is enabled, null otherwise
|
|
580
|
-
*/
|
|
581
|
-
function startAutoAdvance(presenter: Presenter): NodeJS.Timer | null {
|
|
582
|
-
const interval = presenter.deck.config.settings?.autoAdvance;
|
|
583
|
-
|
|
584
|
-
// Auto-advance disabled if interval is undefined, 0, or negative
|
|
585
|
-
if (!interval || interval <= 0) {
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Start interval timer
|
|
590
|
-
return setInterval(() => {
|
|
591
|
-
// Only advance if not currently animating
|
|
592
|
-
if (!presenter.isAnimating) {
|
|
593
|
-
nextSlide(presenter);
|
|
594
|
-
}
|
|
595
|
-
}, interval);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Stop auto-advance timer
|
|
600
|
-
*
|
|
601
|
-
* Clears the auto-advance interval timer if it exists.
|
|
602
|
-
*
|
|
603
|
-
* @param timer - The timer to stop (can be null)
|
|
604
|
-
*/
|
|
605
|
-
function stopAutoAdvance(timer: NodeJS.Timer | null): void {
|
|
606
|
-
if (timer) {
|
|
607
|
-
clearInterval(timer);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Create progress bar at bottom of screen
|
|
613
|
-
*
|
|
614
|
-
* Creates a horizontal progress bar that shows presentation progress.
|
|
615
|
-
* The bar is positioned at the bottom of the screen and fills from left
|
|
616
|
-
* to right as the presentation advances.
|
|
617
|
-
*
|
|
618
|
-
* @param presenter - The presenter state
|
|
619
|
-
* @returns Progress bar element
|
|
620
|
-
*/
|
|
621
|
-
function createProgressBar(presenter: Presenter): blessed.Widgets.ProgressBarElement {
|
|
622
|
-
const { screen } = presenter.renderer;
|
|
623
|
-
|
|
624
|
-
const progressBar = blessed.progressbar({
|
|
625
|
-
bottom: 0,
|
|
626
|
-
left: 0,
|
|
627
|
-
width: '100%',
|
|
628
|
-
height: 1,
|
|
629
|
-
style: {
|
|
630
|
-
bg: '#333333',
|
|
631
|
-
bar: { bg: '#00cc66' },
|
|
632
|
-
},
|
|
633
|
-
filled: 0,
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
screen.append(progressBar);
|
|
637
|
-
|
|
638
|
-
return progressBar;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Update progress bar
|
|
643
|
-
*
|
|
644
|
-
* Updates the progress bar to reflect the current slide position.
|
|
645
|
-
* Progress is calculated as (current + 1) / total * 100.
|
|
646
|
-
*
|
|
647
|
-
* @param progressBar - The progress bar element to update
|
|
648
|
-
* @param current - Current slide index (0-based)
|
|
649
|
-
* @param total - Total number of slides
|
|
650
|
-
*/
|
|
651
|
-
function updateProgress(
|
|
652
|
-
progressBar: blessed.Widgets.ProgressBarElement,
|
|
653
|
-
current: number,
|
|
654
|
-
total: number
|
|
655
|
-
): void {
|
|
656
|
-
const progress = ((current + 1) / total) * 100;
|
|
657
|
-
progressBar.setProgress(progress);
|
|
658
|
-
}
|