@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 +20 -1
- package/package.json +1 -1
- package/src/cli.js +35 -22
- package/src/render.js +97 -43
- package/src/theme.js +54 -0
- package/src/tui.js +39 -13
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
|
|
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
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
|
-
|
|
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
|
|
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
|
|
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
|
-
// ───
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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("[") +
|
|
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
|
-
|
|
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
|
-
//
|
|
16
|
-
const
|
|
17
|
-
chromeBg: `${ESC}[48;2;33;37;43m`, // #21252b
|
|
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
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
const HL_OFF
|
|
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
|
|