mini-coder 0.7.4 → 0.8.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.
Files changed (47) hide show
  1. package/AGENTS.md +114 -0
  2. package/README.md +53 -66
  3. package/bin/mini-coder.ts +2 -0
  4. package/demo.gif +0 -0
  5. package/package.json +17 -20
  6. package/src/agent.ts +181 -274
  7. package/src/cli.ts +101 -0
  8. package/src/config.ts +150 -0
  9. package/src/prompt.ts +54 -207
  10. package/src/session.ts +124 -69
  11. package/src/tools/bash.ts +89 -0
  12. package/src/tools/common.ts +32 -0
  13. package/src/tools/edit.ts +41 -0
  14. package/src/tools/index.ts +47 -0
  15. package/src/tools/read.ts +64 -0
  16. package/src/tui/commands.ts +63 -0
  17. package/src/tui/editor.ts +291 -0
  18. package/src/tui/highlight.ts +189 -0
  19. package/src/tui/stream.ts +142 -0
  20. package/src/tui/styles.ts +42 -0
  21. package/src/tui/term.ts +436 -0
  22. package/src/tui/theme.ts +121 -0
  23. package/src/tui/tui.ts +595 -0
  24. package/src/tui/usage.ts +67 -0
  25. package/tsconfig.json +8 -8
  26. package/bin/mc.ts +0 -11
  27. package/bun.lock +0 -350
  28. package/nono-mini-coder.json +0 -42
  29. package/src/args.ts +0 -252
  30. package/src/error-handling.test.ts +0 -163
  31. package/src/git.ts +0 -23
  32. package/src/headless.ts +0 -66
  33. package/src/index.ts +0 -43
  34. package/src/models.ts +0 -191
  35. package/src/oauth.ts +0 -147
  36. package/src/shared.ts +0 -119
  37. package/src/themes.ts +0 -234
  38. package/src/tool-bash.ts +0 -77
  39. package/src/tool-edit.ts +0 -121
  40. package/src/tool-read.ts +0 -100
  41. package/src/tui-components.ts +0 -127
  42. package/src/tui-conversation.ts +0 -218
  43. package/src/tui-editor.ts +0 -29
  44. package/src/tui-overlay.ts +0 -604
  45. package/src/tui.ts +0 -314
  46. package/src/types.ts +0 -194
  47. package/src/update.ts +0 -171
@@ -0,0 +1,436 @@
1
+ const COMBINING = /\p{M}/u;
2
+
3
+ function charWidth(code: number): number {
4
+ if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
5
+ if (code === 0x200b || code === 0x200c || code === 0x200d || code === 0xfeff) return 0;
6
+ if (code >= 0xfe00 && code <= 0xfe0f) return 0;
7
+ if (code >= 0xe0100 && code <= 0xe01ef) return 0;
8
+ if (COMBINING.test(String.fromCodePoint(code))) return 0;
9
+ if (
10
+ (code >= 0x1100 && code <= 0x115f) ||
11
+ (code >= 0x2e80 && code <= 0x303e) ||
12
+ (code >= 0x3041 && code <= 0x33ff) ||
13
+ (code >= 0x3400 && code <= 0x4dbf) ||
14
+ (code >= 0x4e00 && code <= 0x9fff) ||
15
+ (code >= 0xa000 && code <= 0xa4cf) ||
16
+ (code >= 0xac00 && code <= 0xd7a3) ||
17
+ (code >= 0xf900 && code <= 0xfaff) ||
18
+ (code >= 0xfe10 && code <= 0xfe19) ||
19
+ (code >= 0xfe30 && code <= 0xfe6f) ||
20
+ (code >= 0xff00 && code <= 0xff60) ||
21
+ (code >= 0xffe0 && code <= 0xffe6) ||
22
+ (code >= 0x1f300 && code <= 0x1faff) ||
23
+ (code >= 0x20000 && code <= 0x3fffd)
24
+ ) {
25
+ return 2;
26
+ }
27
+ return 1;
28
+ }
29
+
30
+ export function displayWidth(text: string): number {
31
+ let width = 0;
32
+ for (const ch of text) width += charWidth(ch.codePointAt(0)!);
33
+ return width;
34
+ }
35
+
36
+ /** An SGR sequence: zero cells wide, so it never affects a row break. */
37
+ const SGR = /^\x1b\[[0-9;]*m/;
38
+
39
+ /**
40
+ * Everything a tool, a file or a model can emit that the terminal would act on
41
+ * but the TUI did not write itself: control bytes and every escape sequence but
42
+ * SGR. Dropped before the text is measured, so a row's width is its visible
43
+ * width and a result can only add rows — never move the cursor, erase the
44
+ * screen, or break the row model. Tabs survive to `expandTabs`, which owns them.
45
+ */
46
+ const UNSAFE = /(\x1b\[[0-9;]*m)|\x1b(?:\[[0-9;?]*[ -/]*[@-~]|.)|[\x00-\x08\x0a-\x1f\x7f-\x9f]/g;
47
+
48
+ export function sanitize(text: string): string {
49
+ return text.replace(UNSAFE, (_match, sgr: string | undefined) => sgr ?? "");
50
+ }
51
+
52
+ export function expandTabs(text: string, size = 4): string {
53
+ if (!text.includes("\t")) return text;
54
+ let column = 0;
55
+ let out = "";
56
+ for (let i = 0; i < text.length; ) {
57
+ const escape = SGR.exec(text.slice(i));
58
+ if (escape !== null) {
59
+ out += escape[0];
60
+ i += escape[0].length;
61
+ continue;
62
+ }
63
+ const ch = String.fromCodePoint(text.codePointAt(i)!);
64
+ i += ch.length;
65
+ if (ch === "\t") {
66
+ const spaces = size - (column % size);
67
+ out += " ".repeat(spaces);
68
+ column += spaces;
69
+ } else {
70
+ out += ch;
71
+ column += charWidth(ch.codePointAt(0)!);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /**
78
+ * Splits one logical line into physical rows no wider than `width` cells. SGR
79
+ * sequences count as zero cells and stay attached to the text that follows.
80
+ */
81
+ export function wrapLine(text: string, width: number): string[] {
82
+ if (width <= 0) return [""];
83
+ if (text === "") return [""];
84
+ const rows: string[] = [];
85
+ let current = "";
86
+ let used = 0;
87
+ for (let i = 0; i < text.length; ) {
88
+ if (text.charCodeAt(i) === 0x1b) {
89
+ const escape = SGR.exec(text.slice(i));
90
+ if (escape !== null) {
91
+ current += escape[0];
92
+ i += escape[0].length;
93
+ continue;
94
+ }
95
+ }
96
+ const ch = String.fromCodePoint(text.codePointAt(i)!);
97
+ const w = charWidth(ch.codePointAt(0)!);
98
+ if (used + w > width && current !== "") {
99
+ rows.push(current);
100
+ current = "";
101
+ used = 0;
102
+ }
103
+ current += ch;
104
+ used += w;
105
+ i += ch.length;
106
+ }
107
+ rows.push(current);
108
+ return rows;
109
+ }
110
+
111
+ export type Key =
112
+ | { type: "text"; text: string }
113
+ | { type: "submit" }
114
+ | { type: "newline" }
115
+ | { type: "backspace" }
116
+ | { type: "delete" }
117
+ | { type: "wordBack" }
118
+ | { type: "left" }
119
+ | { type: "right" }
120
+ | { type: "wordLeft" }
121
+ | { type: "wordRight" }
122
+ | { type: "up" }
123
+ | { type: "down" }
124
+ | { type: "home" }
125
+ | { type: "end" }
126
+ | { type: "docStart" }
127
+ | { type: "docEnd" }
128
+ | { type: "tab" }
129
+ | { type: "escape" }
130
+ | { type: "interrupt" }
131
+ | { type: "eof" };
132
+
133
+ const PASTE_START = "\x1b[200~";
134
+ const PASTE_END = "\x1b[201~";
135
+
136
+ const SHIFT = 1;
137
+ const ALT = 2;
138
+ const CTRL = 4;
139
+ /** Only real modifiers are acted on; lock bits and the rest are ignored. */
140
+ const MODS = SHIFT | ALT | CTRL;
141
+
142
+ /** Codes for keys without a printable form, matching kitty's functional range. */
143
+ const CODE = {
144
+ enter: 13,
145
+ escape: 27,
146
+ tab: 9,
147
+ backspace: 127,
148
+ delete: 57349,
149
+ left: 57350,
150
+ right: 57351,
151
+ up: 57352,
152
+ down: 57353,
153
+ home: 57356,
154
+ end: 57357,
155
+ } as const;
156
+
157
+ /** The one table every input path resolves through: code and modifiers to key. */
158
+ const KEYS: Record<string, Key | undefined> = {
159
+ [`${CODE.enter}:0`]: { type: "submit" },
160
+ [`${CODE.enter}:${ALT}`]: { type: "newline" },
161
+ [`${CODE.enter}:${SHIFT}`]: { type: "newline" },
162
+ [`${CODE.escape}:0`]: { type: "escape" },
163
+ [`${CODE.tab}:0`]: { type: "tab" },
164
+ [`${CODE.backspace}:0`]: { type: "backspace" },
165
+ [`${CODE.backspace}:${CTRL}`]: { type: "wordBack" },
166
+ [`${CODE.delete}:0`]: { type: "delete" },
167
+ [`${CODE.left}:0`]: { type: "left" },
168
+ [`${CODE.left}:${CTRL}`]: { type: "wordLeft" },
169
+ [`${CODE.right}:0`]: { type: "right" },
170
+ [`${CODE.right}:${CTRL}`]: { type: "wordRight" },
171
+ [`${CODE.up}:0`]: { type: "up" },
172
+ [`${CODE.down}:0`]: { type: "down" },
173
+ [`${CODE.home}:0`]: { type: "home" },
174
+ [`${CODE.home}:${CTRL}`]: { type: "docStart" },
175
+ [`${CODE.end}:0`]: { type: "end" },
176
+ [`${CODE.end}:${CTRL}`]: { type: "docEnd" },
177
+ ["97:4"]: { type: "home" }, // Ctrl+A
178
+ ["98:2"]: { type: "wordBack" }, // Alt+B
179
+ ["99:4"]: { type: "interrupt" }, // Ctrl+C
180
+ ["100:4"]: { type: "eof" }, // Ctrl+D
181
+ ["101:4"]: { type: "end" }, // Ctrl+E
182
+ ["106:4"]: { type: "newline" }, // Ctrl+J
183
+ ["119:4"]: { type: "wordBack" }, // Ctrl+W
184
+ };
185
+
186
+ const CSI_FINALS: Record<string, number | undefined> = {
187
+ A: CODE.up,
188
+ B: CODE.down,
189
+ C: CODE.right,
190
+ D: CODE.left,
191
+ H: CODE.home,
192
+ F: CODE.end,
193
+ };
194
+
195
+ const CSI_TILDES: Record<number, number | undefined> = {
196
+ 1: CODE.home,
197
+ 3: CODE.delete,
198
+ 4: CODE.end,
199
+ 7: CODE.home,
200
+ 8: CODE.end,
201
+ };
202
+
203
+ interface Chord {
204
+ code: number;
205
+ mods: number;
206
+ }
207
+
208
+ function lookup(code: number, mods: number): Key | null {
209
+ return KEYS[`${code}:${mods}`] ?? null;
210
+ }
211
+
212
+ /** Control bytes are never text: they resolve through the table or vanish. */
213
+ function mapPoint(code: number): Key | null {
214
+ if (code === 0x7f) return lookup(CODE.backspace, 0);
215
+ if (code < 0x20) {
216
+ // A legacy control byte, as the chord the same key arrives as elsewhere.
217
+ if (code === 0x08) return lookup(CODE.backspace, 0);
218
+ if (code === 0x09) return lookup(CODE.tab, 0);
219
+ if (code === 0x0d) return lookup(CODE.enter, 0);
220
+ return lookup(code + 0x60, CTRL);
221
+ }
222
+ return { type: "text", text: String.fromCodePoint(code) };
223
+ }
224
+
225
+ function modsOf(field: string | undefined): number {
226
+ const raw = Number.parseInt(field ?? "", 10);
227
+ return Number.isFinite(raw) && raw > 0 ? (raw - 1) & MODS : 0;
228
+ }
229
+
230
+ /** `code[:shifted[:base]];mods[:event];text` */
231
+ function csiUChord(params: string): Chord | null {
232
+ const fields = params.split(";");
233
+ const code = Number.parseInt(fields[0].split(":")[0], 10);
234
+ if (!Number.isFinite(code)) return null; // probe replies and private forms
235
+ if (code >= 57344) return null; // functional keys never arrive as CSI-u
236
+ return { code, mods: modsOf(fields[1]?.split(":")[0]) };
237
+ }
238
+
239
+ /** `params final`: the legacy `CSI 1;5D` form and SS3's bare `A`. */
240
+ function legacyChord(final: string, params: string): Chord | null {
241
+ const fields = params.split(";");
242
+ const code = final === "~" ? CSI_TILDES[Number.parseInt(fields[0], 10)] : CSI_FINALS[final];
243
+ return code === undefined ? null : { code, mods: modsOf(fields[1]) };
244
+ }
245
+
246
+ function normalizePaste(text: string): string {
247
+ return text.replace(/\r\n?/g, "\n");
248
+ }
249
+
250
+ class KeyParser {
251
+ private buffer = "";
252
+ private pasting = false;
253
+
254
+ /** What the buffer may still complete: a lone ESC, a partial sequence, or nothing. */
255
+ pending(): "escape" | "sequence" | null {
256
+ if (this.pasting) return null;
257
+ if (this.buffer === "\x1b") return "escape";
258
+ return this.buffer.length > 1 ? "sequence" : null;
259
+ }
260
+
261
+ flushEscape(): Key[] {
262
+ if (this.buffer !== "\x1b") return [];
263
+ this.buffer = "";
264
+ return [{ type: "escape" }];
265
+ }
266
+
267
+ flushSequence(): void {
268
+ this.buffer = "";
269
+ }
270
+
271
+ feed(chunk: string): Key[] {
272
+ this.buffer += chunk;
273
+ const keys: Key[] = [];
274
+ for (;;) {
275
+ if (this.pasting) {
276
+ const end = this.buffer.indexOf(PASTE_END);
277
+ if (end >= 0) {
278
+ if (end > 0) keys.push({ type: "text", text: normalizePaste(this.buffer.slice(0, end)) });
279
+ this.buffer = this.buffer.slice(end + PASTE_END.length);
280
+ this.pasting = false;
281
+ continue;
282
+ }
283
+ if (this.buffer.length > PASTE_END.length) {
284
+ const keep = PASTE_END.length - 1;
285
+ const emit = this.buffer.slice(0, this.buffer.length - keep);
286
+ this.buffer = this.buffer.slice(this.buffer.length - keep);
287
+ if (emit) keys.push({ type: "text", text: normalizePaste(emit) });
288
+ }
289
+ break;
290
+ }
291
+
292
+ if (this.buffer === "") break;
293
+ if (this.buffer.startsWith(PASTE_START)) {
294
+ this.buffer = this.buffer.slice(PASTE_START.length);
295
+ this.pasting = true;
296
+ continue;
297
+ }
298
+ if (this.buffer[0] === "\x1b") {
299
+ if (!this.parseEscape(keys)) break;
300
+ continue;
301
+ }
302
+
303
+ const code = this.buffer.codePointAt(0)!;
304
+ const ch = String.fromCodePoint(code);
305
+ this.buffer = this.buffer.slice(ch.length);
306
+ const key = mapPoint(code);
307
+ if (key) keys.push(key);
308
+ }
309
+ return keys;
310
+ }
311
+
312
+ private parseEscape(keys: Key[]): boolean {
313
+ const buffer = this.buffer;
314
+ if (buffer.length < 2) return false;
315
+ const next = buffer[1];
316
+ if (next === "[" || next === "O") return this.parseCsi(keys);
317
+ if (next === "]") return this.skipString(true);
318
+ if (next === "P" || next === "X" || next === "^" || next === "_") return this.skipString(false);
319
+ this.buffer = buffer.slice(2);
320
+ const key = lookup(next.codePointAt(0)!, ALT);
321
+ if (key) keys.push(key);
322
+ return true;
323
+ }
324
+
325
+ private parseCsi(keys: Key[]): boolean {
326
+ const buffer = this.buffer;
327
+ let i = 2;
328
+ while (i < buffer.length) {
329
+ const code = buffer.charCodeAt(i);
330
+ if (code >= 0x40 && code <= 0x7e) break;
331
+ i++;
332
+ }
333
+ if (i >= buffer.length) return false;
334
+ const final = buffer[i];
335
+ const params = buffer.slice(2, i);
336
+ this.buffer = buffer.slice(i + 1);
337
+ const chord = final === "u" ? csiUChord(params) : legacyChord(final, params);
338
+ if (chord !== null) {
339
+ const key = lookup(chord.code, chord.mods);
340
+ if (key) keys.push(key);
341
+ }
342
+ return true;
343
+ }
344
+
345
+ /** OSC ends at BEL or ST; DCS/APC/PM/SOS end at ST. Skipped whole. */
346
+ private skipString(bel: boolean): boolean {
347
+ const st = this.buffer.indexOf("\x1b\\", 2);
348
+ const bell = bel ? this.buffer.indexOf("\x07", 2) : -1;
349
+ let end = -1;
350
+ if (bell >= 0 && (st < 0 || bell < st)) end = bell + 1;
351
+ else if (st >= 0) end = st + 2;
352
+ if (end < 0) return false;
353
+ this.buffer = this.buffer.slice(end);
354
+ return true;
355
+ }
356
+ }
357
+
358
+ interface TerminalHandlers {
359
+ onKey: (key: Key) => void;
360
+ onResize: () => void;
361
+ }
362
+
363
+ export class Terminal {
364
+ private readonly handlers: TerminalHandlers;
365
+ private parser = new KeyParser();
366
+ private escapeTimer: NodeJS.Timeout | undefined;
367
+ private sequenceTimer: NodeJS.Timeout | undefined;
368
+ private started = false;
369
+
370
+ constructor(handlers: TerminalHandlers) {
371
+ this.handlers = handlers;
372
+ }
373
+
374
+ get width(): number {
375
+ return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
376
+ }
377
+
378
+ get height(): number {
379
+ return process.stdout.rows && process.stdout.rows > 0 ? process.stdout.rows : 24;
380
+ }
381
+
382
+ start(): void {
383
+ if (this.started) return;
384
+ this.started = true;
385
+ process.stdin.setEncoding("utf8");
386
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
387
+ process.stdin.resume();
388
+ process.stdin.on("data", this.onData);
389
+ process.stdout.on("resize", this.onResize);
390
+ // Bracketed paste, then kitty flag 1 so `Shift+Enter` is distinguishable.
391
+ if (process.stdout.isTTY) process.stdout.write("\x1b[?2004h\x1b[>1u\x1b[?u");
392
+ }
393
+
394
+ stop(): void {
395
+ if (!this.started) return;
396
+ this.started = false;
397
+ this.clearTimers();
398
+ process.stdin.off("data", this.onData);
399
+ process.stdout.off("resize", this.onResize);
400
+ if (process.stdout.isTTY) process.stdout.write("\x1b[<u\x1b[?2004l");
401
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
402
+ process.stdin.pause();
403
+ }
404
+
405
+ write(text: string): void {
406
+ process.stdout.write(text);
407
+ }
408
+
409
+ private clearTimers(): void {
410
+ if (this.escapeTimer) clearTimeout(this.escapeTimer);
411
+ if (this.sequenceTimer) clearTimeout(this.sequenceTimer);
412
+ this.escapeTimer = undefined;
413
+ this.sequenceTimer = undefined;
414
+ }
415
+
416
+ private onData = (chunk: string): void => {
417
+ // Both timers reset on every chunk, so `Alt`+key stays snappy and an
418
+ // incomplete sequence never wedges the buffer.
419
+ this.clearTimers();
420
+ for (const key of this.parser.feed(chunk)) this.handlers.onKey(key);
421
+ const pending = this.parser.pending();
422
+ if (pending === "escape") {
423
+ this.escapeTimer = setTimeout(() => {
424
+ this.escapeTimer = undefined;
425
+ for (const key of this.parser.flushEscape()) this.handlers.onKey(key);
426
+ }, 30);
427
+ } else if (pending === "sequence") {
428
+ this.sequenceTimer = setTimeout(() => {
429
+ this.sequenceTimer = undefined;
430
+ this.parser.flushSequence();
431
+ }, 150);
432
+ }
433
+ };
434
+
435
+ private onResize = (): void => this.handlers.onResize();
436
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The one palette: the TokyoNight `night` slots the TUI uses, from
3
+ * `folke/tokyonight.nvim` (`lua/tokyonight/colors/night.lua` and its generated
4
+ * highlight groups). Every colour the TUI writes comes from here, foreground and
5
+ * background alike, so no row can fall back to the terminal's own colours.
6
+ * Truecolor only.
7
+ */
8
+ export const PALETTE = {
9
+ bg: "#1a1b26",
10
+ fg: "#c0caf5",
11
+ fg_dark: "#a9b1d6",
12
+ comment: "#565f89",
13
+ terminal_black: "#414868",
14
+ blue: "#7aa2f7",
15
+ blue1: "#2ac3de",
16
+ blue5: "#89ddff",
17
+ green: "#9ece6a",
18
+ green1: "#73daca",
19
+ magenta: "#bb9af7",
20
+ orange: "#ff9e64",
21
+ purple: "#9d7cd8",
22
+ red: "#f7768e",
23
+ teal: "#1abc9c",
24
+ yellow: "#e0af68",
25
+ } as const;
26
+
27
+ /** `Normal`: the pair every row is painted with, and restored to. */
28
+ export const NORMAL_FG = PALETTE.fg;
29
+ export const NORMAL_BG = PALETTE.bg;
30
+
31
+ /** Diff row backgrounds, from the `DiffAdd`/`DiffDelete` groups. */
32
+ export const DIFF_ADD = "#243e4a";
33
+ export const DIFF_DELETE = "#4a272f";
34
+
35
+ /** `r;g;b` for one `#rrggbb`. */
36
+ export function channels(hex: string): string {
37
+ return [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16)).join(";");
38
+ }
39
+
40
+ /**
41
+ * Explicit colour, never a reset: a `39`/`49` or a `0` would hand the rest of
42
+ * the row back to the host terminal, which is the leak this palette closes.
43
+ */
44
+ export function sgrFg(hex: string): string {
45
+ return `\x1b[38;2;${channels(hex)}m`;
46
+ }
47
+
48
+ export function sgrBg(hex: string): string {
49
+ return `\x1b[48;2;${channels(hex)}m`;
50
+ }
51
+
52
+ /**
53
+ * Attributes off, then `Normal`. The state a row starts and ends in: bold, dim
54
+ * and the like must not bleed past the span that set them, and a reset (`0`,
55
+ * or a bare colour off) would drop the row back onto the host terminal.
56
+ */
57
+ export function sgrPlain(): string {
58
+ return `\x1b[22;23;24;38;2;${channels(NORMAL_FG)};48;2;${channels(NORMAL_BG)}m`;
59
+ }
60
+
61
+ export interface Style {
62
+ fg?: string;
63
+ bg?: string;
64
+ bold?: boolean;
65
+ italic?: boolean;
66
+ underline?: boolean;
67
+ }
68
+
69
+ /**
70
+ * Capture name to colour, mapped the way `folke/tokyonight.nvim` maps capture
71
+ * names to highlight groups. A dotted name falls back to its parent.
72
+ */
73
+ export const STYLES: Record<string, Style | undefined> = {
74
+ comment: { fg: PALETTE.comment },
75
+ constant: { fg: PALETTE.orange },
76
+ "constant.builtin": { fg: PALETTE.blue1 },
77
+ constructor: { fg: PALETTE.magenta },
78
+ escape: { fg: PALETTE.magenta },
79
+ function: { fg: PALETTE.blue },
80
+ "function.builtin": { fg: PALETTE.blue1 },
81
+ "function.method": { fg: PALETTE.blue },
82
+ keyword: { fg: PALETTE.purple },
83
+ number: { fg: PALETTE.orange },
84
+ operator: { fg: PALETTE.blue5 },
85
+ property: { fg: PALETTE.green1 },
86
+ "punctuation.bracket": { fg: PALETTE.fg_dark },
87
+ "punctuation.delimiter": { fg: PALETTE.blue5 },
88
+ "punctuation.special": { fg: PALETTE.blue5 },
89
+ string: { fg: PALETTE.green },
90
+ "string.special": { fg: PALETTE.blue1 },
91
+ type: { fg: PALETTE.blue1 },
92
+ "type.builtin": { fg: "#27a1b9" }, // a per-variant literal, not a palette slot
93
+ variable: { fg: PALETTE.fg },
94
+ "variable.builtin": { fg: PALETTE.red },
95
+ "variable.parameter": { fg: PALETTE.yellow },
96
+ // Markdown's queries predate the `@markup` rename; these are the old names.
97
+ "text.emphasis": { italic: true },
98
+ "text.strong": { bold: true },
99
+ "text.literal": { fg: PALETTE.green },
100
+ "text.uri": { underline: true },
101
+ "text.reference": { fg: PALETTE.blue1 },
102
+ };
103
+
104
+ /**
105
+ * Headings take their level's colour from TokyoNight's rainbow, over a 10% tint
106
+ * of it, as `@markup.heading.N.markdown` does. The grammar's own query names one
107
+ * heading colour for all levels, so the level comes from the marker.
108
+ */
109
+ export const HEADINGS: Style[] = [
110
+ { fg: PALETTE.blue, bg: "#24293b" },
111
+ { fg: PALETTE.yellow, bg: "#2e2a2d" },
112
+ { fg: PALETTE.green, bg: "#272d2d" },
113
+ { fg: PALETTE.teal, bg: "#1a2b32" },
114
+ { fg: PALETTE.magenta, bg: "#2a283b" },
115
+ { fg: PALETTE.purple, bg: "#272538" },
116
+ { fg: PALETTE.orange, bg: "#31282c" },
117
+ { fg: PALETTE.red, bg: "#302430" },
118
+ ].map((style) => ({ ...style, bold: true }));
119
+
120
+ /** Inline code, `@markup.raw.markdown_inline`. */
121
+ export const INLINE_CODE: Style = { fg: PALETTE.blue, bg: PALETTE.terminal_black };