@dunkinfrunkin/mdcat 0.1.10 → 0.1.12

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
@@ -32,6 +32,8 @@ npx @dunkinfrunkin/mdcat README.md
32
32
  ```sh
33
33
  mdcat README.md # open a file
34
34
  mdcat --web README.md # render and open in browser
35
+ mdcat --light README.md # force light theme
36
+ mdcat --dark README.md # force dark theme
35
37
  cat CHANGELOG.md | mdcat # pipe from stdin
36
38
  curl -s https://… | mdcat # pipe from curl
37
39
  mdcat --help # show help
@@ -54,6 +56,23 @@ mdcat --version # show version
54
56
  | `Esc` | Clear search |
55
57
  | Mouse wheel | Scroll three lines |
56
58
 
59
+ ## Theme
60
+
61
+ mdcat auto-detects your terminal's light or dark background and adjusts colors accordingly.
62
+
63
+ **Auto-detection** checks (in order):
64
+ 1. `MDCAT_THEME` env var (`light` or `dark`)
65
+ 2. `COLORFGBG` env var (set by many terminals)
66
+ 3. macOS system appearance (light/dark mode)
67
+ 4. Falls back to dark
68
+
69
+ **Override manually:**
70
+ ```sh
71
+ mdcat --light README.md # force light theme
72
+ mdcat --dark README.md # force dark theme
73
+ MDCAT_THEME=light mdcat file.md # env var override
74
+ ```
75
+
57
76
  ## What it renders
58
77
 
59
78
  | Element | Rendering |
@@ -62,7 +81,7 @@ mdcat --version # show version
62
81
  | H2 | Bold blue with underline |
63
82
  | H3–H6 | Green → yellow → cyan → dim |
64
83
  | **Bold** / _italic_ / ~~strike~~ | Standard ANSI |
65
- | `inline code` | Amber on dark background |
84
+ | `inline code` | Amber on dark bg / brown on light bg |
66
85
  | Fenced code | Bordered box with syntax highlighting |
67
86
  | Blockquotes | Amber `▌` bar, dim italic |
68
87
  | Unordered lists | `●` / `○` / `‣` bullets |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunkinfrunkin/mdcat",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "View markdown files beautifully in your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3,10 +3,11 @@ import { readFileSync, writeFileSync } from "fs";
3
3
  import { resolve, basename } from "path";
4
4
  import { tmpdir } from "os";
5
5
  import { execFileSync } from "child_process";
6
- import { marked } from "marked";
7
- import { renderTokens } from "./render.js";
6
+ import { marked, Marked } from "marked";
7
+ import { renderTokens, setTheme } from "./render.js";
8
8
  import { launch } from "./tui.js";
9
9
  import { toDocx } from "./docx.js";
10
+ import { detectTheme, themeFromArgs, stripThemeArgs } from "./theme.js";
10
11
 
11
12
  marked.use({ gfm: true });
12
13
 
@@ -20,7 +21,7 @@ const bold = s => `${E}[1m${s}${E}[0m`;
20
21
 
21
22
  const CAT = ` ${gray("/\\")}${blue("(o.o)")}${gray("/\\")} `;
22
23
 
23
- const args = process.argv.slice(2);
24
+ let args = process.argv.slice(2);
24
25
 
25
26
  if (args[0] === "--help" || args[0] === "-h") {
26
27
  console.log(`\n${CAT} ${bold("mdcat")} ${dim(`v${pkg.version}`)}`);
@@ -29,7 +30,12 @@ if (args[0] === "--help" || args[0] === "-h") {
29
30
  console.log(` mdcat ${dim("<file.md>")}`);
30
31
  console.log(` mdcat ${dim("--web <file.md>")} ${dim("# open in browser")}`);
31
32
  console.log(` mdcat ${dim("--doc <file.md>")} ${dim("# export to .docx")}`);
33
+ console.log(` mdcat ${dim("--light")} ${dim("# force light theme")}`);
34
+ console.log(` mdcat ${dim("--dark")} ${dim("# force dark theme")}`);
32
35
  console.log(` cat file.md ${dim("|")} mdcat\n`);
36
+ console.log(`${bold("Theme:")}`);
37
+ console.log(` Auto-detects terminal theme. Override with ${blue("--light")} / ${blue("--dark")}`);
38
+ console.log(` or set ${blue("MDCAT_THEME")}=light|dark\n`);
33
39
  console.log(`${bold("Keys:")}`);
34
40
  console.log(` ${blue("/")} search ${blue("n/N")} next/prev match`);
35
41
  console.log(` ${blue("j/k")} ${dim("↑↓")} scroll line ${blue("space/b")} page down/up`);
@@ -38,6 +44,11 @@ if (args[0] === "--help" || args[0] === "-h") {
38
44
  process.exit(0);
39
45
  }
40
46
 
47
+ // Resolve theme: CLI flag > env var > auto-detect
48
+ const activeTheme = themeFromArgs(args) ?? detectTheme();
49
+ args = stripThemeArgs(args);
50
+ setTheme(activeTheme);
51
+
41
52
  if (args[0] === "--version" || args[0] === "-v") {
42
53
  console.log(`${CAT} ${bold("mdcat")} ${dim(`v${pkg.version}`)}`);
43
54
  process.exit(0);
@@ -52,7 +63,7 @@ function escapeHtml(s) {
52
63
  function openInBrowser(title, content) {
53
64
  // Use a custom renderer that escapes raw HTML tokens so bare <tag> in
54
65
  // markdown text isn't swallowed by the browser.
55
- const webMarked = new marked.Marked({ gfm: true });
66
+ const webMarked = new Marked({ gfm: true });
56
67
  webMarked.use({
57
68
  renderer: {
58
69
  html(token) {
@@ -61,6 +72,7 @@ function openInBrowser(title, content) {
61
72
  },
62
73
  });
63
74
  const html = webMarked.parse(content);
75
+ const isLight = activeTheme === "light";
64
76
  const page = `<!DOCTYPE html>
65
77
  <html lang="en">
66
78
  <head>
@@ -70,31 +82,31 @@ function openInBrowser(title, content) {
70
82
  <style>
71
83
  * { box-sizing: border-box; margin: 0; padding: 0; }
72
84
  body {
73
- background: #282c34;
74
- color: #abb2bf;
85
+ background: ${isLight ? "#fafafa" : "#282c34"};
86
+ color: ${isLight ? "#383a42" : "#abb2bf"};
75
87
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
76
88
  font-size: 16px;
77
89
  line-height: 1.7;
78
90
  padding: 2rem 1rem;
79
91
  }
80
92
  .wrap { max-width: 760px; margin: 0 auto; }
81
- h1, h2, h3, h4, h5, h6 { color: #e5c07b; margin: 1.5rem 0 0.5rem; font-weight: 700; }
82
- h1 { font-size: 2rem; color: #c678dd; border-bottom: 2px solid #3e4451; padding-bottom: 0.4rem; }
83
- h2 { font-size: 1.4rem; color: #61afef; border-bottom: 1px solid #3e4451; padding-bottom: 0.3rem; }
84
- h3 { font-size: 1.15rem; color: #98c379; }
93
+ h1, h2, h3, h4, h5, h6 { color: ${isLight ? "#c18401" : "#e5c07b"}; margin: 1.5rem 0 0.5rem; font-weight: 700; }
94
+ h1 { font-size: 2rem; color: ${isLight ? "#a626a4" : "#c678dd"}; border-bottom: 2px solid ${isLight ? "#d3d3d8" : "#3e4451"}; padding-bottom: 0.4rem; }
95
+ h2 { font-size: 1.4rem; color: ${isLight ? "#4078f2" : "#61afef"}; border-bottom: 1px solid ${isLight ? "#d3d3d8" : "#3e4451"}; padding-bottom: 0.3rem; }
96
+ h3 { font-size: 1.15rem; color: ${isLight ? "#50a14f" : "#98c379"}; }
85
97
  p { margin: 0.75rem 0; }
86
- a { color: #61afef; text-decoration: underline; }
87
- code { background: #2c313a; color: #e5c07b; padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.9em; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
88
- pre { background: #21252b; border: 1px solid #3e4451; border-radius: 8px; padding: 1rem 1.25rem; overflow-x: auto; margin: 1rem 0; }
89
- pre code { background: none; color: #abb2bf; padding: 0; font-size: 0.875rem; }
90
- blockquote { border-left: 3px solid #e5c07b; padding: 0.5rem 1rem; margin: 1rem 0; color: #5c6370; font-style: italic; }
98
+ a { color: ${isLight ? "#4078f2" : "#61afef"}; text-decoration: underline; }
99
+ code { background: ${isLight ? "#e8e8e8" : "#2c313a"}; color: ${isLight ? "#986801" : "#e5c07b"}; padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.9em; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
100
+ pre { background: ${isLight ? "#f0f0f0" : "#21252b"}; border: 1px solid ${isLight ? "#d3d3d8" : "#3e4451"}; border-radius: 8px; padding: 1rem 1.25rem; overflow-x: auto; margin: 1rem 0; }
101
+ pre code { background: none; color: ${isLight ? "#383a42" : "#abb2bf"}; padding: 0; font-size: 0.875rem; }
102
+ blockquote { border-left: 3px solid ${isLight ? "#c18401" : "#e5c07b"}; padding: 0.5rem 1rem; margin: 1rem 0; color: ${isLight ? "#696c77" : "#5c6370"}; font-style: italic; }
91
103
  ul, ol { padding-left: 1.5rem; margin: 0.75rem 0; }
92
104
  li { margin: 0.25rem 0; }
93
105
  table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
94
- th { background: #21252b; color: #61afef; padding: 0.5rem 0.75rem; border: 1px solid #3e4451; text-align: left; }
95
- td { padding: 0.5rem 0.75rem; border: 1px solid #3e4451; }
96
- tr:nth-child(even) { background: #2c313a; }
97
- hr { border: none; border-top: 1px solid #3e4451; margin: 1.5rem 0; }
106
+ th { background: ${isLight ? "#e8e8e8" : "#21252b"}; color: ${isLight ? "#4078f2" : "#61afef"}; padding: 0.5rem 0.75rem; border: 1px solid ${isLight ? "#d3d3d8" : "#3e4451"}; text-align: left; }
107
+ td { padding: 0.5rem 0.75rem; border: 1px solid ${isLight ? "#d3d3d8" : "#3e4451"}; }
108
+ tr:nth-child(even) { background: ${isLight ? "#f0f0f0" : "#2c313a"}; }
109
+ hr { border: none; border-top: 1px solid ${isLight ? "#d3d3d8" : "#3e4451"}; margin: 1.5rem 0; }
98
110
  img { max-width: 100%; border-radius: 6px; }
99
111
  </style>
100
112
  </head>
@@ -112,10 +124,11 @@ function openInBrowser(title, content) {
112
124
  }
113
125
 
114
126
  function runTUI(title, content) {
115
- const cols = Math.min(process.stdout.columns || 80, MAX_COLS);
127
+ const termCols = process.stdout.columns || 80;
128
+ const cols = Math.min(termCols, MAX_COLS);
116
129
  const tokens = marked.lexer(content);
117
- const lines = renderTokens(tokens, cols);
118
- launch(title, lines);
130
+ const lines = renderTokens(tokens, cols, termCols > MAX_COLS ? termCols : undefined);
131
+ launch(title, lines, activeTheme);
119
132
  }
120
133
 
121
134
  // --web / --doc flags
package/src/render.js CHANGED
@@ -4,44 +4,96 @@ import { highlight as cliHighlight } from "cli-highlight";
4
4
 
5
5
  const chalk = new Chalk({ level: 3 });
6
6
 
7
- // ─── One Dark palette ──────────────────────────────────────────────────────────
8
- const c = {
9
- // Headings
10
- h1: chalk.hex("#c678dd").bold, // purple
11
- h2: chalk.hex("#61afef").bold, // blue
12
- h3: chalk.hex("#98c379").bold, // green
13
- h4: chalk.hex("#e5c07b").bold, // yellow
14
- h5: chalk.hex("#56b6c2"), // cyan
15
- h6: chalk.dim,
16
- // Inline
17
- strong: chalk.bold.white,
18
- em: chalk.italic,
19
- del: chalk.strikethrough.dim,
20
- code: chalk.hex("#e5c07b").bgHex("#2a2a2a"), // amber on dark bg
21
- link: chalk.hex("#61afef").underline,
22
- image: chalk.dim,
23
- // Code block
24
- border: chalk.dim,
25
- codeLang: chalk.hex("#e5c07b").dim,
26
- // Blockquote
27
- bqBar: chalk.hex("#e5c07b"),
28
- bqText: chalk.dim.italic,
29
- // Lists
30
- bullet0: chalk.hex("#61afef"), // blue ● depth 0
31
- bullet1: chalk.dim, // dim ○ depth 1
32
- bullet2: chalk.dim, // very dim ‣ depth 2+
33
- ordered: chalk.hex("#56b6c2"), // cyan number
34
- taskDone: chalk.hex("#98c379"), // green
35
- taskTodo: chalk.dim, // dim
36
- // Table
37
- tableBorder: chalk.dim,
38
- tableHead: chalk.hex("#61afef").bold,
39
- tableCell: chalk.reset,
40
- // HR
41
- hr: chalk.dim,
42
- // Paragraph text
43
- fg: chalk.reset,
44
- };
7
+ // ─── Color palettes ─────────────────────────────────────────────────────────────
8
+
9
+ function darkPalette() {
10
+ return {
11
+ // Headings
12
+ h1: chalk.hex("#c678dd").bold, // purple
13
+ h2: chalk.hex("#61afef").bold, // blue
14
+ h3: chalk.hex("#98c379").bold, // green
15
+ h4: chalk.hex("#e5c07b").bold, // yellow
16
+ h5: chalk.hex("#56b6c2"), // cyan
17
+ h6: chalk.dim,
18
+ // Inline
19
+ strong: chalk.bold.white,
20
+ em: chalk.italic,
21
+ del: chalk.strikethrough.dim,
22
+ code: chalk.hex("#e5c07b").bgHex("#2a2a2a"), // amber on dark bg
23
+ link: chalk.hex("#61afef").underline,
24
+ image: chalk.dim,
25
+ badge: chalk.hex("#abb2bf"),
26
+ // Code block
27
+ border: chalk.dim,
28
+ codeLang: chalk.hex("#e5c07b").dim,
29
+ // Blockquote
30
+ bqBar: chalk.hex("#e5c07b"),
31
+ bqText: chalk.dim.italic,
32
+ // Lists
33
+ bullet0: chalk.hex("#61afef"), // blue ● depth 0
34
+ bullet1: chalk.dim, // dim ○ depth 1
35
+ bullet2: chalk.dim, // very dim ‣ depth 2+
36
+ ordered: chalk.hex("#56b6c2"), // cyan number
37
+ taskDone: chalk.hex("#98c379"), // green ☑
38
+ taskTodo: chalk.dim, // dim ☐
39
+ // Table
40
+ tableBorder: chalk.dim,
41
+ tableHead: chalk.hex("#61afef").bold,
42
+ tableCell: chalk.reset,
43
+ // HR
44
+ hr: chalk.dim,
45
+ // Paragraph text
46
+ fg: chalk.reset,
47
+ };
48
+ }
49
+
50
+ function lightPalette() {
51
+ return {
52
+ // Headings
53
+ h1: chalk.hex("#a626a4").bold, // magenta
54
+ h2: chalk.hex("#4078f2").bold, // blue
55
+ h3: chalk.hex("#50a14f").bold, // green
56
+ h4: chalk.hex("#c18401").bold, // dark yellow
57
+ h5: chalk.hex("#0184bc"), // teal
58
+ h6: chalk.hex("#696c77"), // gray
59
+ // Inline
60
+ strong: chalk.bold.black,
61
+ em: chalk.italic,
62
+ del: chalk.strikethrough.hex("#696c77"),
63
+ code: chalk.hex("#986801").bgHex("#e8e8e8"), // brown on light gray
64
+ link: chalk.hex("#4078f2").underline,
65
+ image: chalk.hex("#696c77"),
66
+ badge: chalk.hex("#383a42"),
67
+ // Code block
68
+ border: chalk.hex("#a0a1a7"),
69
+ codeLang: chalk.hex("#986801"),
70
+ // Blockquote
71
+ bqBar: chalk.hex("#c18401"),
72
+ bqText: chalk.hex("#696c77").italic,
73
+ // Lists
74
+ bullet0: chalk.hex("#4078f2"), // blue ● depth 0
75
+ bullet1: chalk.hex("#a0a1a7"), // gray ○ depth 1
76
+ bullet2: chalk.hex("#a0a1a7"), // gray ‣ depth 2+
77
+ ordered: chalk.hex("#0184bc"), // teal number
78
+ taskDone: chalk.hex("#50a14f"), // green ☑
79
+ taskTodo: chalk.hex("#a0a1a7"), // gray ☐
80
+ // Table
81
+ tableBorder: chalk.hex("#a0a1a7"),
82
+ tableHead: chalk.hex("#4078f2").bold,
83
+ tableCell: chalk.reset,
84
+ // HR
85
+ hr: chalk.hex("#a0a1a7"),
86
+ // Paragraph text
87
+ fg: chalk.reset,
88
+ };
89
+ }
90
+
91
+ let c = darkPalette();
92
+
93
+ /** Set the active color palette. Call before renderTokens(). */
94
+ export function setTheme(theme) {
95
+ c = theme === "light" ? lightPalette() : darkPalette();
96
+ }
45
97
 
46
98
  const MARGIN = " "; // 2-space left margin
47
99
 
@@ -124,7 +176,7 @@ function inline(tokens) {
124
176
  if (tok.tokens?.length === 1 && tok.tokens[0].type === "image") {
125
177
  const alt = tok.tokens[0].text || tok.tokens[0].alt || "";
126
178
  if (alt) {
127
- const badge = chalk.dim("[") + chalk.hex("#abb2bf")(alt) + chalk.dim("]");
179
+ const badge = chalk.dim("[") + c.badge(alt) + chalk.dim("]");
128
180
  return `\x1B]8;;${href}\x1B\\${badge}\x1B]8;;\x1B\\`;
129
181
  }
130
182
  }
@@ -468,7 +520,7 @@ function hr(w) {
468
520
 
469
521
  // ─── Token dispatcher ──────────────────────────────────────────────────────────
470
522
 
471
- function token(tok, w) {
523
+ function token(tok, w, tableW) {
472
524
  try {
473
525
  switch (tok.type) {
474
526
  case "heading":
@@ -482,7 +534,7 @@ function token(tok, w) {
482
534
  case "list":
483
535
  return ["", ...list(tok, w, 0), ""];
484
536
  case "table":
485
- return table(tok, w);
537
+ return table(tok, tableW ?? w);
486
538
  case "hr":
487
539
  return hr(w);
488
540
  case "space":
@@ -504,9 +556,11 @@ function token(tok, w) {
504
556
  *
505
557
  * @param {import("marked").Token[]} tokens - Output of `marked.lexer()`
506
558
  * @param {number} cols - Terminal width (default 80)
559
+ * @param {number} [fullCols] - Uncapped terminal width for tables
507
560
  * @returns {string[]}
508
561
  */
509
- export function renderTokens(tokens, cols = 80) {
562
+ export function renderTokens(tokens, cols = 80, fullCols) {
510
563
  const w = Math.max(20, cols - MARGIN.length * 2);
511
- return tokens.flatMap((tok) => token(tok, w));
564
+ const tableW = fullCols ? Math.max(20, fullCols - MARGIN.length * 2) : undefined;
565
+ return tokens.flatMap((tok) => token(tok, w, tableW));
512
566
  }
package/src/theme.js ADDED
@@ -0,0 +1,54 @@
1
+ import { execSync } from "child_process";
2
+
3
+ /**
4
+ * Detect whether the terminal is using a light or dark background.
5
+ *
6
+ * Checks (in order):
7
+ * 1. MDCAT_THEME env var ("light" or "dark")
8
+ * 2. COLORFGBG env var (e.g. "15;0" → dark, "0;15" → light)
9
+ * 3. macOS Appearance via `defaults read`
10
+ * 4. Falls back to "dark"
11
+ */
12
+ export function detectTheme() {
13
+ // 1. Explicit override
14
+ const env = (process.env.MDCAT_THEME || "").toLowerCase();
15
+ if (env === "light" || env === "dark") return env;
16
+
17
+ // 2. COLORFGBG — "fg;bg" where bg >= 8 usually means light
18
+ const colorfgbg = process.env.COLORFGBG;
19
+ if (colorfgbg) {
20
+ const parts = colorfgbg.split(";");
21
+ const bg = parseInt(parts[parts.length - 1], 10);
22
+ if (!isNaN(bg) && bg >= 8) return "light";
23
+ if (!isNaN(bg) && bg < 8) return "dark";
24
+ }
25
+
26
+ // 3. macOS: check system appearance
27
+ if (process.platform === "darwin") {
28
+ try {
29
+ const result = execSync(
30
+ "defaults read -g AppleInterfaceStyle 2>/dev/null",
31
+ { encoding: "utf8", timeout: 500 }
32
+ ).trim();
33
+ // "Dark" means dark mode; absence or error means light
34
+ return result === "Dark" ? "dark" : "light";
35
+ } catch {
36
+ // Key missing → light mode on macOS
37
+ return "light";
38
+ }
39
+ }
40
+
41
+ return "dark";
42
+ }
43
+
44
+ /** Check CLI args for --light or --dark flags, returns the flag value or null. */
45
+ export function themeFromArgs(args) {
46
+ if (args.includes("--light")) return "light";
47
+ if (args.includes("--dark")) return "dark";
48
+ return null;
49
+ }
50
+
51
+ /** Remove --light / --dark from an args array. */
52
+ export function stripThemeArgs(args) {
53
+ return args.filter(a => a !== "--light" && a !== "--dark");
54
+ }
package/src/tui.js CHANGED
@@ -12,17 +12,36 @@ const ERASE_L = `${ESC}[2K`;
12
12
  const RESET = `${ESC}[0m`;
13
13
  const move = (r, c) => `${ESC}[${r};${c}H`;
14
14
 
15
- // One Dark-aligned color palette (24-bit ANSI)
16
- const C = {
17
- chromeBg: `${ESC}[48;2;33;37;43m`, // #21252b — top/bottom chrome bg
15
+ // Color palettes for TUI chrome (24-bit ANSI)
16
+ const DARK = {
17
+ chromeBg: `${ESC}[48;2;33;37;43m`, // #21252b
18
18
  badge: `${ESC}[38;2;198;120;221m`, // #c678dd — purple badge
19
19
  titleFg: `${ESC}[97m`, // bright white — filename
20
- dimFg: `${ESC}[38;2;92;99;112m`, // #5c6370 — dim hints / app name
21
- accentFg: `${ESC}[38;2;97;175;239m`, // #61afef — blue search prompt
22
- matchFg: `${ESC}[38;2;229;192;123m`, // #e5c07b — gold current match
23
- greenFg: `${ESC}[38;2;152;195;121m`, // #98c379 — match count
24
- redFg: `${ESC}[38;2;224;108;117m`, // #e06c75 — no matches
25
- otherMatchFg:`${ESC}[38;2;92;99;112m`, // #5c6370 — other match gutter
20
+ dimFg: `${ESC}[38;2;92;99;112m`, // #5c6370
21
+ accentFg: `${ESC}[38;2;97;175;239m`, // #61afef
22
+ matchFg: `${ESC}[38;2;229;192;123m`, // #e5c07b
23
+ greenFg: `${ESC}[38;2;152;195;121m`, // #98c379
24
+ redFg: `${ESC}[38;2;224;108;117m`, // #e06c75
25
+ otherMatchFg:`${ESC}[38;2;92;99;112m`, // #5c6370
26
+ hlMatch: `${ESC}[48;2;62;68;82m${ESC}[38;2;229;192;123m`,
27
+ hlCurrent: `${ESC}[48;2;229;192;123m${ESC}[38;2;0;0;0m`,
28
+ };
29
+
30
+ const LIGHT = {
31
+ chromeBg: `${ESC}[48;2;228;228;228m`, // #e4e4e4
32
+ badge: `${ESC}[38;2;166;38;164m`, // #a626a4 — magenta badge
33
+ titleFg: `${ESC}[30m`, // black — filename
34
+ dimFg: `${ESC}[38;2;105;108;119m`, // #696c77
35
+ accentFg: `${ESC}[38;2;64;120;242m`, // #4078f2
36
+ matchFg: `${ESC}[38;2;152;104;1m`, // #986801
37
+ greenFg: `${ESC}[38;2;80;161;79m`, // #50a14f
38
+ redFg: `${ESC}[38;2;228;86;73m`, // #e45649
39
+ otherMatchFg:`${ESC}[38;2;105;108;119m`, // #696c77
40
+ hlMatch: `${ESC}[48;2;209;226;255m${ESC}[38;2;64;120;242m`,
41
+ hlCurrent: `${ESC}[48;2;64;120;242m${ESC}[38;2;255;255;255m`,
42
+ };
43
+
44
+ let C = { ...DARK,
26
45
  bold: `${ESC}[1m`,
27
46
  dim: `${ESC}[2m`,
28
47
  italic: `${ESC}[3m`,
@@ -42,9 +61,9 @@ function plen(s) { return s.replace(/\x1B\[[0-9;]*m/g, "").length; }
42
61
 
43
62
  // Highlight all occurrences of `query` within an ANSI-coloured line.
44
63
  // Walks the string char-by-char so ANSI escape sequences don't shift offsets.
45
- const HL_MATCH = `${ESC}[48;2;62;68;82m${ESC}[38;2;229;192;123m`; // other matches
46
- const HL_CURRENT = `${ESC}[48;2;229;192;123m${ESC}[38;2;0;0;0m`; // current match
47
- const HL_OFF = `${ESC}[49m${ESC}[39m`;
64
+ let HL_MATCH = C.hlMatch;
65
+ let HL_CURRENT = C.hlCurrent;
66
+ const HL_OFF = `${ESC}[49m${ESC}[39m`;
48
67
 
49
68
  function highlightInLine(line, query, isCurrent) {
50
69
  if (!query) return line;
@@ -110,7 +129,14 @@ function copyText(text) {
110
129
  catch { /* not on macOS or pbcopy unavailable */ }
111
130
  }
112
131
 
113
- export function launch(title, lines) {
132
+ export function launch(title, lines, theme) {
133
+ // Apply theme to TUI chrome
134
+ if (theme === "light") {
135
+ const pal = LIGHT;
136
+ Object.assign(C, pal);
137
+ HL_MATCH = pal.hlMatch;
138
+ HL_CURRENT = pal.hlCurrent;
139
+ }
114
140
  // Viewport state
115
141
  let offset = 0;
116
142