@pep/term-deck 1.0.31 → 1.1.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # term-deck
2
2
 
3
- A terminal-based presentation tool with a cyberpunk aesthetic. Create beautiful slideshows in your terminal with matrix rain backgrounds, glitch effects, and ASCII art.
3
+ A terminal-based presentation tool with a cyberpunk aesthetic. Create beautiful slideshows in your terminal with matrix rain backgrounds, glitch effects, and ASCII art. Share them on the web or play them anywhere.
4
4
 
5
5
  ![npm version](https://img.shields.io/npm/v/@pep/term-deck?color=green)
6
6
  ![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?logo=typescript)
@@ -12,6 +12,8 @@ A terminal-based presentation tool with a cyberpunk aesthetic. Create beautiful
12
12
 
13
13
  *Matrix rain backgrounds, glitch animations, and ASCII art in your terminal*
14
14
 
15
+ **Try it online:** [term-deck-web.vercel.app](https://term-deck-web.vercel.app)
16
+
15
17
  ## Features
16
18
 
17
19
  - 🌊 **Matrix Rain Background** - Animated katakana/symbol rain effects
@@ -26,6 +28,7 @@ A terminal-based presentation tool with a cyberpunk aesthetic. Create beautiful
26
28
  - 🔧 **Fully Themeable** - Create custom themes
27
29
  - ⚡ **Beautiful CLI** - Colorful, styled terminal output
28
30
  - 📦 **Type-Safe** - Full TypeScript with Zod validation
31
+ - 🌐 **Web Platform** - Share decks online and play from URLs
29
32
 
30
33
  ## Installation
31
34
 
@@ -52,6 +55,9 @@ term-deck present .
52
55
 
53
56
  # Export to video
54
57
  term-deck export . -o presentation.mp4
58
+
59
+ # Play a shared deck from the web
60
+ npx @pep/term-deck play https://term-deck-web.vercel.app/d/demo
55
61
  ```
56
62
 
57
63
  Output:
@@ -116,6 +122,11 @@ Run `term-deck --help` to see the styled help:
116
122
  init <name> Create a new presentation deck
117
123
  -t, --theme <name> Theme preset (default: matrix)
118
124
 
125
+ play <url> Play a deck from term-deck web
126
+ -s, --start <n> Start at slide number
127
+ -n, --notes Show presenter notes
128
+ -l, --loop Loop back after last slide
129
+
119
130
  ▶ HOTKEYS:
120
131
 
121
132
  Space / → Next slide
@@ -264,6 +275,35 @@ term-deck record . -o presentation.cast
264
275
  asciinema play presentation.cast
265
276
  ```
266
277
 
278
+ ## Web Platform
279
+
280
+ Share your presentations online at [term-deck-web.vercel.app](https://term-deck-web.vercel.app).
281
+
282
+ ### Upload & Share
283
+
284
+ 1. Go to [term-deck-web.vercel.app/upload](https://term-deck-web.vercel.app/upload)
285
+ 2. Drag and drop your markdown slides
286
+ 3. Get a shareable URL like `term-deck-web.vercel.app/d/abc123`
287
+
288
+ ### Play from URL
289
+
290
+ Anyone can play a shared deck in their terminal:
291
+
292
+ ```bash
293
+ # Play a shared presentation
294
+ npx @pep/term-deck play https://term-deck-web.vercel.app/d/abc123
295
+
296
+ # With options
297
+ npx @pep/term-deck play https://term-deck-web.vercel.app/d/abc123 --notes --loop
298
+ ```
299
+
300
+ The web viewer includes:
301
+ - Matrix rain background
302
+ - Keyboard navigation (arrows, space, numbers)
303
+ - Fullscreen mode (F key)
304
+ - Slide list (L key)
305
+ - All transition effects (glitch, fade, typewriter)
306
+
267
307
  ## Themes
268
308
 
269
309
  term-deck includes built-in themes. Set via `themePreset` in config:
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { join } from 'path';
2
+ import path2, { join } from 'path';
3
3
  import { pathToFileURL } from 'url';
4
4
  import { z } from 'zod';
5
5
  import matter from 'gray-matter';
6
- import { mkdir, writeFile, access, rm, readFile, unlink } from 'fs/promises';
6
+ import fs, { mkdir, writeFile, access, rm, readFile, unlink } from 'fs/promises';
7
7
  import fg from 'fast-glob';
8
8
  import blessed from 'neo-blessed';
9
9
  import gradient2 from 'gradient-string';
@@ -11,10 +11,10 @@ import 'yaml';
11
11
  import 'deepmerge';
12
12
  import { mermaidToAscii as mermaidToAscii$1 } from 'mermaid-ascii';
13
13
  import figlet from 'figlet';
14
- import { createReadStream, createWriteStream } from 'fs';
14
+ import { createWriteStream } from 'fs';
15
15
  import { Command } from 'commander';
16
16
  import { intro, log, outro, spinner } from '@clack/prompts';
17
- import { tmpdir } from 'os';
17
+ import os, { tmpdir } from 'os';
18
18
  import { execa } from 'execa';
19
19
  import pc from 'picocolors';
20
20
 
@@ -181,8 +181,8 @@ var init_config = __esm({
181
181
  // src/schemas/validation.ts
182
182
  function formatZodError(error, context) {
183
183
  const issues = error.issues.map((issue) => {
184
- const path2 = issue.path.join(".");
185
- return ` - ${path2 ? `${path2}: ` : ""}${issue.message}`;
184
+ const path3 = issue.path.join(".");
185
+ return ` - ${path3 ? `${path3}: ` : ""}${issue.message}`;
186
186
  });
187
187
  return `Invalid ${context}:
188
188
  ${issues.join("\n")}`;
@@ -568,10 +568,10 @@ var init_theme_errors = __esm({
568
568
  * @param themeName - Optional name of the theme that caused the error
569
569
  * @param path - Optional path to the theme file or package
570
570
  */
571
- constructor(message, themeName, path2) {
571
+ constructor(message, themeName, path3) {
572
572
  super(message);
573
573
  this.themeName = themeName;
574
- this.path = path2;
574
+ this.path = path3;
575
575
  this.name = "ThemeError";
576
576
  }
577
577
  };
@@ -1055,7 +1055,6 @@ async function createNotesWindow(ttyPath) {
1055
1055
  if (!ttyPath) {
1056
1056
  throw new Error(getMissingTtyError());
1057
1057
  }
1058
- const blessed5 = (await import('neo-blessed')).default;
1059
1058
  try {
1060
1059
  await access(ttyPath);
1061
1060
  } catch {
@@ -1063,92 +1062,85 @@ async function createNotesWindow(ttyPath) {
1063
1062
 
1064
1063
  Make sure the path is correct and the terminal is open.`);
1065
1064
  }
1066
- const input = createReadStream(ttyPath);
1067
1065
  const output = createWriteStream(ttyPath);
1068
1066
  await new Promise((resolve, reject) => {
1069
- let ready = 0;
1070
- const checkReady = () => {
1071
- ready++;
1072
- if (ready === 2) resolve();
1073
- };
1074
- input.once("open", checkReady);
1075
- output.once("open", checkReady);
1076
- input.once("error", reject);
1067
+ output.once("open", resolve);
1077
1068
  output.once("error", reject);
1078
1069
  });
1079
- const screen = blessed5.screen({
1080
- smartCSR: true,
1081
- title: "term-deck notes",
1082
- fullUnicode: true,
1083
- input,
1084
- output,
1085
- terminal: "xterm-256color",
1086
- forceUnicode: true
1087
- });
1088
- const contentBox = blessed5.box({
1089
- top: 0,
1090
- left: 0,
1091
- width: "100%",
1092
- height: "100%",
1093
- tags: true,
1094
- padding: 2,
1095
- style: {
1096
- fg: "#ffffff",
1097
- bg: "#1a1a1a"
1098
- }
1099
- });
1100
- screen.append(contentBox);
1101
- screen.render();
1070
+ output.write(ANSI.CLEAR);
1071
+ output.write(`${ANSI.BG_DARK}${ANSI.GREEN}${ANSI.BOLD}term-deck notes${ANSI.RESET}
1072
+
1073
+ `);
1074
+ output.write(`${ANSI.GRAY}Waiting for presentation to start...${ANSI.RESET}
1075
+ `);
1102
1076
  return {
1103
- screen,
1104
- contentBox,
1077
+ output,
1105
1078
  tty: ttyPath
1106
1079
  };
1107
1080
  }
1108
1081
  function updateNotesWindow(notesWindow, currentSlide, nextSlide2, currentIndex, totalSlides) {
1109
- const { contentBox, screen } = notesWindow;
1110
- let content = "";
1111
- content += `{bold}Slide ${currentIndex + 1} of ${totalSlides}{/bold}
1112
- `;
1113
- content += `{gray-fg}${currentSlide.frontmatter.title}{/}
1114
- `;
1115
- content += "\n";
1116
- content += "\u2500".repeat(50) + "\n";
1117
- content += "\n";
1082
+ const { output } = notesWindow;
1083
+ const divider = "\u2500".repeat(50);
1084
+ output.write(ANSI.CLEAR);
1085
+ output.write(`${ANSI.GREEN}${ANSI.BOLD}term-deck notes${ANSI.RESET}
1086
+
1087
+ `);
1088
+ output.write(`${ANSI.CYAN}${ANSI.BOLD}Slide ${currentIndex + 1} of ${totalSlides}${ANSI.RESET}
1089
+ `);
1090
+ output.write(`${ANSI.GRAY}${currentSlide.frontmatter.title}${ANSI.RESET}
1091
+ `);
1092
+ output.write("\n");
1093
+ output.write(`${ANSI.GRAY}${divider}${ANSI.RESET}
1094
+ `);
1095
+ output.write("\n");
1118
1096
  if (currentSlide.notes) {
1119
- content += "{bold}PRESENTER NOTES:{/bold}\n\n";
1120
- content += currentSlide.notes + "\n";
1097
+ output.write(`${ANSI.YELLOW}${ANSI.BOLD}PRESENTER NOTES:${ANSI.RESET}
1098
+
1099
+ `);
1100
+ output.write(`${ANSI.WHITE}${currentSlide.notes}${ANSI.RESET}
1101
+ `);
1121
1102
  } else {
1122
- content += "{gray-fg}No notes for this slide{/}\n";
1103
+ output.write(`${ANSI.GRAY}No notes for this slide${ANSI.RESET}
1104
+ `);
1123
1105
  }
1124
- content += "\n";
1125
- content += "\u2500".repeat(50) + "\n";
1126
- content += "\n";
1106
+ output.write("\n");
1107
+ output.write(`${ANSI.GRAY}${divider}${ANSI.RESET}
1108
+ `);
1109
+ output.write("\n");
1127
1110
  if (nextSlide2) {
1128
- content += `{bold}NEXT:{/bold} "${nextSlide2.frontmatter.title}"
1129
- `;
1111
+ output.write(`${ANSI.CYAN}${ANSI.BOLD}NEXT:${ANSI.RESET} ${ANSI.WHITE}"${nextSlide2.frontmatter.title}"${ANSI.RESET}
1112
+ `);
1130
1113
  } else {
1131
- content += "{gray-fg}Last slide{/}\n";
1114
+ output.write(`${ANSI.GRAY}Last slide${ANSI.RESET}
1115
+ `);
1132
1116
  }
1133
- contentBox.setContent(content);
1134
- screen.render();
1135
- }
1136
- function toggleNotesVisibility(notesWindow) {
1137
- const { contentBox, screen } = notesWindow;
1138
- contentBox.toggle();
1139
- screen.render();
1140
1117
  }
1141
1118
  function destroyNotesWindow(notesWindow) {
1142
1119
  try {
1143
- if (notesWindow.screen && notesWindow.screen.program) {
1144
- notesWindow.screen.destroy();
1145
- }
1120
+ notesWindow.output.write(ANSI.CLEAR);
1121
+ notesWindow.output.write(`${ANSI.GRAY}Presentation ended.${ANSI.RESET}
1122
+ `);
1123
+ notesWindow.output.end();
1146
1124
  } catch {
1147
1125
  }
1148
1126
  }
1127
+ var ANSI;
1149
1128
  var init_notes_window = __esm({
1150
1129
  "src/presenter/notes-window.ts"() {
1151
1130
  init_esm_shims();
1131
+ ANSI = {
1132
+ CLEAR: "\x1B[2J\x1B[H",
1133
+ // Clear screen and move cursor to top
1134
+ BOLD: "\x1B[1m",
1135
+ DIM: "\x1B[2m",
1136
+ RESET: "\x1B[0m",
1137
+ GREEN: "\x1B[32m",
1138
+ CYAN: "\x1B[36m",
1139
+ YELLOW: "\x1B[33m",
1140
+ WHITE: "\x1B[37m",
1141
+ GRAY: "\x1B[90m",
1142
+ BG_DARK: "\x1B[48;5;234m"
1143
+ };
1152
1144
  }
1153
1145
  });
1154
1146
 
@@ -1267,11 +1259,6 @@ function setupControls(presenter) {
1267
1259
  screen.key(["l"], () => {
1268
1260
  showSlideList(presenter);
1269
1261
  });
1270
- screen.key(["N"], () => {
1271
- if (presenter.notesWindow) {
1272
- toggleNotesVisibility(presenter.notesWindow);
1273
- }
1274
- });
1275
1262
  }
1276
1263
  function showSlideList(presenter) {
1277
1264
  const { screen } = presenter.renderer;
@@ -1312,7 +1299,6 @@ var init_keyboard_controls = __esm({
1312
1299
  "src/presenter/keyboard-controls.ts"() {
1313
1300
  init_esm_shims();
1314
1301
  init_navigation();
1315
- init_notes_window();
1316
1302
  }
1317
1303
  });
1318
1304
 
@@ -1417,7 +1403,7 @@ var init_main = __esm({
1417
1403
  init_esm_shims();
1418
1404
 
1419
1405
  // package.json
1420
- var version = "1.0.31";
1406
+ var version = "1.1.0";
1421
1407
 
1422
1408
  // src/cli/commands/present.ts
1423
1409
  init_esm_shims();
@@ -1979,6 +1965,103 @@ term-deck export . -o ${name}.gif
1979
1965
  await writeFile(join(deckDir, "README.md"), readme);
1980
1966
  }
1981
1967
 
1968
+ // src/cli/commands/play.ts
1969
+ init_esm_shims();
1970
+ init_main();
1971
+ async function fetchDeck(url) {
1972
+ let apiUrl;
1973
+ try {
1974
+ const urlObj = new URL(url);
1975
+ if (urlObj.pathname.startsWith("/d/")) {
1976
+ const id = urlObj.pathname.split("/d/")[1];
1977
+ apiUrl = `${urlObj.origin}/api/deck/${id}/raw`;
1978
+ } else if (urlObj.pathname.startsWith("/api/deck/")) {
1979
+ apiUrl = url;
1980
+ } else {
1981
+ throw new Error("Invalid deck URL format");
1982
+ }
1983
+ } catch {
1984
+ apiUrl = `https://term-deck-web.vercel.app/api/deck/${url}/raw`;
1985
+ }
1986
+ const response = await fetch(apiUrl);
1987
+ if (!response.ok) {
1988
+ if (response.status === 404) {
1989
+ throw new Error("Deck not found");
1990
+ }
1991
+ throw new Error(`Failed to fetch deck: ${response.status} ${response.statusText}`);
1992
+ }
1993
+ return response.json();
1994
+ }
1995
+ async function writeTempSlides(deck) {
1996
+ const tempDir = await fs.mkdtemp(path2.join(os.tmpdir(), "term-deck-"));
1997
+ for (const slide of deck.slides) {
1998
+ const frontmatter = Object.entries(slide.frontmatter).filter(([, value]) => value !== void 0).map(([key, value]) => {
1999
+ if (typeof value === "string") {
2000
+ return `${key}: ${value}`;
2001
+ }
2002
+ if (Array.isArray(value)) {
2003
+ return `${key}:
2004
+ ${value.map((v) => ` - ${v}`).join("\n")}`;
2005
+ }
2006
+ return `${key}: ${JSON.stringify(value)}`;
2007
+ }).join("\n");
2008
+ let content = `---
2009
+ ${frontmatter}
2010
+ ---
2011
+
2012
+ ${slide.body}`;
2013
+ if (slide.notes) {
2014
+ content += `
2015
+
2016
+ <!-- notes -->
2017
+ ${slide.notes}
2018
+ <!-- /notes -->`;
2019
+ }
2020
+ const filename = `${String(slide.index + 1).padStart(2, "0")}-${slide.frontmatter.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.md`;
2021
+ await fs.writeFile(path2.join(tempDir, filename), content, "utf-8");
2022
+ }
2023
+ if (deck.config.theme) {
2024
+ const themeContent = `import { defineTheme } from '@pep/term-deck'
2025
+
2026
+ export default defineTheme(${JSON.stringify(deck.config.theme, null, 2)})
2027
+ `;
2028
+ await fs.writeFile(path2.join(tempDir, "theme.ts"), themeContent, "utf-8");
2029
+ }
2030
+ return tempDir;
2031
+ }
2032
+ async function cleanupTempDir(tempDir) {
2033
+ try {
2034
+ await fs.rm(tempDir, { recursive: true });
2035
+ } catch {
2036
+ }
2037
+ }
2038
+ var playCommand = new Command("play").description("Play a presentation from a term-deck web URL").argument("<url>", "Deck URL (e.g., https://termdeck.vercel.app/d/abc123 or just abc123)").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").option("-l, --loop", "Loop back to first slide after last").action(async (url, options) => {
2039
+ let tempDir = null;
2040
+ try {
2041
+ console.log("Fetching deck...");
2042
+ const deck = await fetchDeck(url);
2043
+ console.log(`Playing: ${deck.config.title || "Untitled Deck"}`);
2044
+ if (deck.config.author) {
2045
+ console.log(`By: ${deck.config.author}`);
2046
+ }
2047
+ console.log(`Slides: ${deck.slides.length}
2048
+ `);
2049
+ tempDir = await writeTempSlides(deck);
2050
+ await present(tempDir, {
2051
+ startSlide: Number.parseInt(options.start, 10),
2052
+ showNotes: options.notes,
2053
+ notesTty: options.notesTty,
2054
+ loop: options.loop
2055
+ });
2056
+ } catch (error) {
2057
+ handleError(error);
2058
+ } finally {
2059
+ if (tempDir) {
2060
+ await cleanupTempDir(tempDir);
2061
+ }
2062
+ }
2063
+ });
2064
+
1982
2065
  // src/cli/help.ts
1983
2066
  init_esm_shims();
1984
2067
  function showHelp() {
@@ -2022,6 +2105,11 @@ function showHelp() {
2022
2105
  console.log(pc.green(" init") + pc.dim(" <name> ") + pc.white("Create a new presentation deck"));
2023
2106
  console.log(pc.dim(" -t, --theme <name> ") + pc.white("Theme preset (default: matrix)"));
2024
2107
  console.log("");
2108
+ console.log(pc.green(" play") + pc.dim(" <url> ") + pc.white("Play a deck from term-deck web"));
2109
+ console.log(pc.dim(" -s, --start <n> ") + pc.white("Start at slide number"));
2110
+ console.log(pc.dim(" -n, --notes ") + pc.white("Show presenter notes"));
2111
+ console.log(pc.dim(" -l, --loop ") + pc.white("Loop back after last slide"));
2112
+ console.log("");
2025
2113
  console.log(pc.bold(pc.cyan("\u25B6 HOTKEYS:")));
2026
2114
  console.log("");
2027
2115
  console.log(pc.dim(" Space / \u2192 ") + pc.white("Next slide"));
@@ -2058,7 +2146,7 @@ function showVersion(version2) {
2058
2146
  // bin/term-deck.ts
2059
2147
  var args = process.argv.slice(2);
2060
2148
  if (args.includes("-h") || args.includes("--help") || args.length === 0) {
2061
- if (!args.some((arg) => ["present", "export", "init"].includes(arg))) {
2149
+ if (!args.some((arg) => ["present", "export", "init", "play"].includes(arg))) {
2062
2150
  showHelp();
2063
2151
  process.exit(0);
2064
2152
  }
@@ -2072,6 +2160,7 @@ program.name("term-deck").description("Terminal presentation tool with a cyberpu
2072
2160
  program.addCommand(presentCommand);
2073
2161
  program.addCommand(exportCommand);
2074
2162
  program.addCommand(initCommand);
2163
+ program.addCommand(playCommand);
2075
2164
  program.argument("[dir]", "Slides directory to present").action(async (dir) => {
2076
2165
  if (dir) {
2077
2166
  try {