@cdoing/opentuicli 0.1.21 → 0.1.26

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.
@@ -1,640 +0,0 @@
1
- /**
2
- * Theme Context — Multi-theme system for the TUI
3
- *
4
- * Supports 15+ built-in themes inspired by popular editor themes.
5
- * Each theme has dark and light variants with full color palettes.
6
- *
7
- * Detection:
8
- * 1. OSC 11 escape sequence (queries terminal directly)
9
- * 2. COLORFGBG environment variable (fallback)
10
- * 3. Defaults to dark mode
11
- */
12
-
13
- import { RGBA, rgbToHex, SyntaxStyle } from "@opentui/core";
14
- import { createContext, useContext, useState, useEffect, useRef, useMemo } from "react";
15
- import type { ReactNode } from "react";
16
- import { useSettingsStore } from "../store/settings";
17
-
18
- export interface Theme {
19
- text: RGBA;
20
- textMuted: RGBA;
21
- textDim: RGBA;
22
- primary: RGBA;
23
- secondary: RGBA;
24
- success: RGBA;
25
- error: RGBA;
26
- warning: RGBA;
27
- info: RGBA;
28
- border: RGBA;
29
- borderFocused: RGBA;
30
- bg: RGBA;
31
- bgSubtle: RGBA;
32
- // Roles
33
- userText: RGBA;
34
- assistantText: RGBA;
35
- systemText: RGBA;
36
- toolText: RGBA;
37
- // Tool status
38
- toolRunning: RGBA;
39
- toolDone: RGBA;
40
- toolError: RGBA;
41
- // Diff
42
- diffAdd: RGBA;
43
- diffRemove: RGBA;
44
- diffHunk: RGBA;
45
- }
46
-
47
- // ── Theme Definitions ─────────────────────────────────────
48
-
49
- export interface ThemeDef {
50
- name: string;
51
- dark: Theme;
52
- light: Theme;
53
- }
54
-
55
- function t(hex: string): RGBA {
56
- return RGBA.fromHex(hex);
57
- }
58
-
59
- /** Helper to build a full theme from a minimal palette */
60
- function buildTheme(p: {
61
- bg: string; bgSubtle: string; text: string; textMuted: string; textDim: string;
62
- primary: string; secondary: string; success: string; error: string;
63
- warning: string; info: string; border: string;
64
- diffAdd?: string; diffRemove?: string;
65
- }): Theme {
66
- return {
67
- text: t(p.text), textMuted: t(p.textMuted), textDim: t(p.textDim),
68
- primary: t(p.primary), secondary: t(p.secondary),
69
- success: t(p.success), error: t(p.error), warning: t(p.warning), info: t(p.info),
70
- border: t(p.border), borderFocused: t(p.primary),
71
- bg: t(p.bg), bgSubtle: t(p.bgSubtle),
72
- userText: t(p.success), assistantText: t(p.text),
73
- systemText: t(p.warning), toolText: t(p.textMuted),
74
- toolRunning: t(p.warning), toolDone: t(p.success), toolError: t(p.error),
75
- diffAdd: t(p.diffAdd || p.success), diffRemove: t(p.diffRemove || p.error),
76
- diffHunk: t(p.info),
77
- };
78
- }
79
-
80
- // ── Built-in Themes ───────────────────────────────────────
81
-
82
- export const THEMES: Record<string, ThemeDef> = {
83
- default: {
84
- name: "Default",
85
- dark: buildTheme({
86
- bg: "#000000", bgSubtle: "#111111", text: "#e5e7eb", textMuted: "#9ca3af", textDim: "#6b7280",
87
- primary: "#06b6d4", secondary: "#8b5cf6", success: "#22c55e", error: "#ef4444",
88
- warning: "#eab308", info: "#3b82f6", border: "#374151",
89
- }),
90
- light: buildTheme({
91
- bg: "#ffffff", bgSubtle: "#f3f4f6", text: "#1f2937", textMuted: "#6b7280", textDim: "#9ca3af",
92
- primary: "#0891b2", secondary: "#7c3aed", success: "#16a34a", error: "#dc2626",
93
- warning: "#ca8a04", info: "#2563eb", border: "#d1d5db",
94
- }),
95
- },
96
- catppuccin: {
97
- name: "Catppuccin",
98
- dark: buildTheme({
99
- bg: "#1e1e2e", bgSubtle: "#313244", text: "#cdd6f4", textMuted: "#a6adc8", textDim: "#6c7086",
100
- primary: "#89b4fa", secondary: "#cba6f7", success: "#a6e3a1", error: "#f38ba8",
101
- warning: "#f9e2af", info: "#74c7ec", border: "#45475a",
102
- }),
103
- light: buildTheme({
104
- bg: "#eff1f5", bgSubtle: "#e6e9ef", text: "#4c4f69", textMuted: "#6c6f85", textDim: "#9ca0b0",
105
- primary: "#1e66f5", secondary: "#8839ef", success: "#40a02b", error: "#d20f39",
106
- warning: "#df8e1d", info: "#04a5e5", border: "#ccd0da",
107
- }),
108
- },
109
- dracula: {
110
- name: "Dracula",
111
- dark: buildTheme({
112
- bg: "#282a36", bgSubtle: "#44475a", text: "#f8f8f2", textMuted: "#6272a4", textDim: "#44475a",
113
- primary: "#bd93f9", secondary: "#ff79c6", success: "#50fa7b", error: "#ff5555",
114
- warning: "#f1fa8c", info: "#8be9fd", border: "#44475a",
115
- }),
116
- light: buildTheme({
117
- bg: "#f8f8f2", bgSubtle: "#e6e6e6", text: "#282a36", textMuted: "#6272a4", textDim: "#999999",
118
- primary: "#7c3aed", secondary: "#d946ef", success: "#16a34a", error: "#dc2626",
119
- warning: "#ca8a04", info: "#0891b2", border: "#d1d5db",
120
- }),
121
- },
122
- nord: {
123
- name: "Nord",
124
- dark: buildTheme({
125
- bg: "#2e3440", bgSubtle: "#3b4252", text: "#eceff4", textMuted: "#d8dee9", textDim: "#4c566a",
126
- primary: "#88c0d0", secondary: "#b48ead", success: "#a3be8c", error: "#bf616a",
127
- warning: "#ebcb8b", info: "#81a1c1", border: "#434c5e",
128
- }),
129
- light: buildTheme({
130
- bg: "#eceff4", bgSubtle: "#e5e9f0", text: "#2e3440", textMuted: "#4c566a", textDim: "#7b88a1",
131
- primary: "#5e81ac", secondary: "#b48ead", success: "#a3be8c", error: "#bf616a",
132
- warning: "#ebcb8b", info: "#81a1c1", border: "#d8dee9",
133
- }),
134
- },
135
- tokyonight: {
136
- name: "Tokyo Night",
137
- dark: buildTheme({
138
- bg: "#1a1b26", bgSubtle: "#24283b", text: "#c0caf5", textMuted: "#a9b1d6", textDim: "#565f89",
139
- primary: "#7aa2f7", secondary: "#bb9af7", success: "#9ece6a", error: "#f7768e",
140
- warning: "#e0af68", info: "#7dcfff", border: "#3b4261",
141
- }),
142
- light: buildTheme({
143
- bg: "#d5d6db", bgSubtle: "#cbccd1", text: "#343b58", textMuted: "#565a6e", textDim: "#9699a3",
144
- primary: "#34548a", secondary: "#5a4a78", success: "#485e30", error: "#8c4351",
145
- warning: "#8f5e15", info: "#0f4b6e", border: "#b4b5b9",
146
- }),
147
- },
148
- gruvbox: {
149
- name: "Gruvbox",
150
- dark: buildTheme({
151
- bg: "#282828", bgSubtle: "#3c3836", text: "#ebdbb2", textMuted: "#a89984", textDim: "#665c54",
152
- primary: "#fabd2f", secondary: "#d3869b", success: "#b8bb26", error: "#fb4934",
153
- warning: "#fe8019", info: "#83a598", border: "#504945",
154
- }),
155
- light: buildTheme({
156
- bg: "#fbf1c7", bgSubtle: "#f2e5bc", text: "#3c3836", textMuted: "#7c6f64", textDim: "#a89984",
157
- primary: "#b57614", secondary: "#8f3f71", success: "#79740e", error: "#9d0006",
158
- warning: "#af3a03", info: "#427b58", border: "#d5c4a1",
159
- }),
160
- },
161
- rosepine: {
162
- name: "Rosé Pine",
163
- dark: buildTheme({
164
- bg: "#191724", bgSubtle: "#1f1d2e", text: "#e0def4", textMuted: "#908caa", textDim: "#6e6a86",
165
- primary: "#c4a7e7", secondary: "#ebbcba", success: "#31748f", error: "#eb6f92",
166
- warning: "#f6c177", info: "#9ccfd8", border: "#26233a",
167
- }),
168
- light: buildTheme({
169
- bg: "#faf4ed", bgSubtle: "#f2e9e1", text: "#575279", textMuted: "#797593", textDim: "#9893a5",
170
- primary: "#907aa9", secondary: "#d7827e", success: "#286983", error: "#b4637a",
171
- warning: "#ea9d34", info: "#56949f", border: "#dfdad9",
172
- }),
173
- },
174
- monokai: {
175
- name: "Monokai",
176
- dark: buildTheme({
177
- bg: "#272822", bgSubtle: "#3e3d32", text: "#f8f8f2", textMuted: "#75715e", textDim: "#49483e",
178
- primary: "#66d9ef", secondary: "#ae81ff", success: "#a6e22e", error: "#f92672",
179
- warning: "#e6db74", info: "#66d9ef", border: "#49483e",
180
- }),
181
- light: buildTheme({
182
- bg: "#fafafa", bgSubtle: "#f0f0f0", text: "#272822", textMuted: "#75715e", textDim: "#b0b0b0",
183
- primary: "#0096d1", secondary: "#7c3aed", success: "#629e25", error: "#c4265e",
184
- warning: "#b5a21d", info: "#0096d1", border: "#d0d0d0",
185
- }),
186
- },
187
- solarized: {
188
- name: "Solarized",
189
- dark: buildTheme({
190
- bg: "#002b36", bgSubtle: "#073642", text: "#839496", textMuted: "#657b83", textDim: "#586e75",
191
- primary: "#268bd2", secondary: "#6c71c4", success: "#859900", error: "#dc322f",
192
- warning: "#b58900", info: "#2aa198", border: "#073642",
193
- }),
194
- light: buildTheme({
195
- bg: "#fdf6e3", bgSubtle: "#eee8d5", text: "#657b83", textMuted: "#839496", textDim: "#93a1a1",
196
- primary: "#268bd2", secondary: "#6c71c4", success: "#859900", error: "#dc322f",
197
- warning: "#b58900", info: "#2aa198", border: "#eee8d5",
198
- }),
199
- },
200
- amoled: {
201
- name: "AMOLED",
202
- dark: buildTheme({
203
- bg: "#000000", bgSubtle: "#0a0a0a", text: "#ffffff", textMuted: "#888888", textDim: "#555555",
204
- primary: "#00e5ff", secondary: "#e040fb", success: "#00e676", error: "#ff1744",
205
- warning: "#ffea00", info: "#40c4ff", border: "#222222",
206
- }),
207
- light: buildTheme({
208
- bg: "#ffffff", bgSubtle: "#f5f5f5", text: "#000000", textMuted: "#666666", textDim: "#aaaaaa",
209
- primary: "#0097a7", secondary: "#7b1fa2", success: "#2e7d32", error: "#c62828",
210
- warning: "#f9a825", info: "#0277bd", border: "#e0e0e0",
211
- }),
212
- },
213
- github: {
214
- name: "GitHub",
215
- dark: buildTheme({
216
- bg: "#0d1117", bgSubtle: "#161b22", text: "#e6edf3", textMuted: "#8b949e", textDim: "#484f58",
217
- primary: "#58a6ff", secondary: "#bc8cff", success: "#3fb950", error: "#f85149",
218
- warning: "#d29922", info: "#58a6ff", border: "#30363d",
219
- }),
220
- light: buildTheme({
221
- bg: "#ffffff", bgSubtle: "#f6f8fa", text: "#1f2328", textMuted: "#656d76", textDim: "#8c959f",
222
- primary: "#0969da", secondary: "#8250df", success: "#1a7f37", error: "#cf222e",
223
- warning: "#9a6700", info: "#0969da", border: "#d0d7de",
224
- }),
225
- },
226
- material: {
227
- name: "Material",
228
- dark: buildTheme({
229
- bg: "#263238", bgSubtle: "#37474f", text: "#eeffff", textMuted: "#b0bec5", textDim: "#546e7a",
230
- primary: "#82aaff", secondary: "#c792ea", success: "#c3e88d", error: "#ff5370",
231
- warning: "#ffcb6b", info: "#89ddff", border: "#37474f",
232
- }),
233
- light: buildTheme({
234
- bg: "#fafafa", bgSubtle: "#eeeeee", text: "#90a4ae", textMuted: "#546e7a", textDim: "#b0bec5",
235
- primary: "#6182b8", secondary: "#7c4dff", success: "#91b859", error: "#e53935",
236
- warning: "#f76d47", info: "#39adb5", border: "#d0d0d0",
237
- }),
238
- },
239
- synthwave: {
240
- name: "Synthwave '84",
241
- dark: buildTheme({
242
- bg: "#262335", bgSubtle: "#34294f", text: "#ffffff", textMuted: "#bbbbbb", textDim: "#6d5da0",
243
- primary: "#ff7edb", secondary: "#36f9f6", success: "#72f1b8", error: "#fe4450",
244
- warning: "#fede5d", info: "#36f9f6", border: "#495495",
245
- }),
246
- light: buildTheme({
247
- bg: "#f5f0ff", bgSubtle: "#e8e0f0", text: "#262335", textMuted: "#6d5da0", textDim: "#a090c0",
248
- primary: "#b03090", secondary: "#108888", success: "#308050", error: "#c03030",
249
- warning: "#a08000", info: "#207080", border: "#d0c8e0",
250
- }),
251
- },
252
- everforest: {
253
- name: "Everforest",
254
- dark: buildTheme({
255
- bg: "#2d353b", bgSubtle: "#343f44", text: "#d3c6aa", textMuted: "#859289", textDim: "#5c6a72",
256
- primary: "#a7c080", secondary: "#d699b6", success: "#a7c080", error: "#e67e80",
257
- warning: "#dbbc7f", info: "#7fbbb3", border: "#475258",
258
- }),
259
- light: buildTheme({
260
- bg: "#fdf6e3", bgSubtle: "#f3efda", text: "#5c6a72", textMuted: "#829181", textDim: "#a6b0a0",
261
- primary: "#8da101", secondary: "#df69ba", success: "#8da101", error: "#f85552",
262
- warning: "#dfa000", info: "#35a77c", border: "#e0dcc7",
263
- }),
264
- },
265
- cobalt2: {
266
- name: "Cobalt2",
267
- dark: buildTheme({
268
- bg: "#193549", bgSubtle: "#1f4662", text: "#ffffff", textMuted: "#8db0cc", textDim: "#4d7ea0",
269
- primary: "#ffc600", secondary: "#ff9d00", success: "#3ad900", error: "#ff628c",
270
- warning: "#ffc600", info: "#80fcff", border: "#2a5a7a",
271
- }),
272
- light: buildTheme({
273
- bg: "#ffffff", bgSubtle: "#f0f0f0", text: "#193549", textMuted: "#4d7ea0", textDim: "#8db0cc",
274
- primary: "#b88d00", secondary: "#cc7a00", success: "#2ba600", error: "#cc4e70",
275
- warning: "#b88d00", info: "#0090a0", border: "#d0d0d0",
276
- }),
277
- },
278
- kanagawa: {
279
- name: "Kanagawa",
280
- dark: buildTheme({
281
- bg: "#1F1F28", bgSubtle: "#2A2A37", text: "#DCD7BA", textMuted: "#727169", textDim: "#54546D",
282
- primary: "#7E9CD8", secondary: "#957FB8", success: "#98BB6C", error: "#E82424",
283
- warning: "#D7A657", info: "#76946A", border: "#54546D",
284
- }),
285
- light: buildTheme({
286
- bg: "#F2E9DE", bgSubtle: "#EAE4D7", text: "#54433A", textMuted: "#9E9389", textDim: "#B8AFA6",
287
- primary: "#2D4F67", secondary: "#957FB8", success: "#98BB6C", error: "#E82424",
288
- warning: "#D7A657", info: "#76946A", border: "#D4CBBF",
289
- }),
290
- },
291
- nightowl: {
292
- name: "Night Owl",
293
- dark: buildTheme({
294
- bg: "#011627", bgSubtle: "#0b2942", text: "#d6deeb", textMuted: "#637777", textDim: "#44596b",
295
- primary: "#82aaff", secondary: "#c792ea", success: "#addb67", error: "#ef5350",
296
- warning: "#ffcb8b", info: "#7fdbca", border: "#1d3b53",
297
- }),
298
- light: buildTheme({
299
- bg: "#fbfbfb", bgSubtle: "#f0f0f0", text: "#403f53", textMuted: "#989fb1", textDim: "#c0c0c0",
300
- primary: "#4876d6", secondary: "#994cc3", success: "#4d804e", error: "#de3d3b",
301
- warning: "#e0af68", info: "#0c969b", border: "#d9d9d9",
302
- }),
303
- },
304
- onedark: {
305
- name: "One Dark",
306
- dark: buildTheme({
307
- bg: "#282c34", bgSubtle: "#2c313c", text: "#abb2bf", textMuted: "#636d83", textDim: "#4b5263",
308
- primary: "#61afef", secondary: "#c678dd", success: "#98c379", error: "#e06c75",
309
- warning: "#e5c07b", info: "#56b6c2", border: "#3e4451",
310
- }),
311
- light: buildTheme({
312
- bg: "#fafafa", bgSubtle: "#f0f0f0", text: "#383a42", textMuted: "#a0a1a7", textDim: "#c0c0c0",
313
- primary: "#4078f2", secondary: "#a626a4", success: "#50a14f", error: "#e45649",
314
- warning: "#c18401", info: "#0184bc", border: "#d3d3d3",
315
- }),
316
- },
317
- matrix: {
318
- name: "Matrix",
319
- dark: buildTheme({
320
- bg: "#0a0e0a", bgSubtle: "#0e130d", text: "#62ff94", textMuted: "#8ca391", textDim: "#3d4a44",
321
- primary: "#2eff6a", secondary: "#00efff", success: "#62ff94", error: "#ff4b4b",
322
- warning: "#e6ff57", info: "#30b3ff", border: "#1e2a1b",
323
- }),
324
- light: buildTheme({
325
- bg: "#eef3ea", bgSubtle: "#e4ebe1", text: "#203022", textMuted: "#748476", textDim: "#a0b0a5",
326
- primary: "#1cc24b", secondary: "#24f6d9", success: "#1cc24b", error: "#ff4b4b",
327
- warning: "#e6ff57", info: "#30b3ff", border: "#748476",
328
- }),
329
- },
330
- flexoki: {
331
- name: "Flexoki",
332
- dark: buildTheme({
333
- bg: "#100f0f", bgSubtle: "#1c1b1a", text: "#cecdc3", textMuted: "#878580", textDim: "#575653",
334
- primary: "#4385be", secondary: "#8b7ec8", success: "#879a39", error: "#d14d41",
335
- warning: "#da702c", info: "#3aa99f", border: "#343331",
336
- }),
337
- light: buildTheme({
338
- bg: "#fffcf0", bgSubtle: "#f2f0e5", text: "#100f0f", textMuted: "#878580", textDim: "#b7b5ac",
339
- primary: "#205ea6", secondary: "#5e409d", success: "#66800b", error: "#af3029",
340
- warning: "#bc5215", info: "#24837b", border: "#e6e4d9",
341
- }),
342
- },
343
- cursor: {
344
- name: "Cursor",
345
- dark: buildTheme({
346
- bg: "#1e1e1e", bgSubtle: "#252526", text: "#d4d4d4", textMuted: "#808080", textDim: "#5a5a5a",
347
- primary: "#007acc", secondary: "#c586c0", success: "#6a9955", error: "#f44747",
348
- warning: "#cca700", info: "#4ec9b0", border: "#333333",
349
- }),
350
- light: buildTheme({
351
- bg: "#ffffff", bgSubtle: "#f3f3f3", text: "#1e1e1e", textMuted: "#808080", textDim: "#b0b0b0",
352
- primary: "#007acc", secondary: "#af00db", success: "#388a34", error: "#cd3131",
353
- warning: "#bf8803", info: "#16825d", border: "#e5e5e5",
354
- }),
355
- },
356
- vercel: {
357
- name: "Vercel",
358
- dark: buildTheme({
359
- bg: "#000000", bgSubtle: "#111111", text: "#ededed", textMuted: "#888888", textDim: "#444444",
360
- primary: "#ffffff", secondary: "#888888", success: "#0070f3", error: "#ee0000",
361
- warning: "#f5a623", info: "#0070f3", border: "#333333",
362
- }),
363
- light: buildTheme({
364
- bg: "#ffffff", bgSubtle: "#fafafa", text: "#000000", textMuted: "#666666", textDim: "#999999",
365
- primary: "#000000", secondary: "#666666", success: "#0070f3", error: "#ee0000",
366
- warning: "#f5a623", info: "#0070f3", border: "#eaeaea",
367
- }),
368
- },
369
- "osaka-jade": {
370
- name: "Osaka Jade",
371
- dark: buildTheme({
372
- bg: "#111c18", bgSubtle: "#1a2520", text: "#C1C497", textMuted: "#53685B", textDim: "#3d4a44",
373
- primary: "#2DD5B7", secondary: "#D2689C", success: "#549e6a", error: "#FF5345",
374
- warning: "#E5C736", info: "#2DD5B7", border: "#3d4a44",
375
- }),
376
- light: buildTheme({
377
- bg: "#F6F5DD", bgSubtle: "#E8E7CC", text: "#111c18", textMuted: "#53685B", textDim: "#A8A78C",
378
- primary: "#1faa90", secondary: "#a8527a", success: "#3d7a52", error: "#c7392d",
379
- warning: "#b5a020", info: "#1faa90", border: "#A8A78C",
380
- }),
381
- },
382
- };
383
-
384
- /** Get sorted list of theme IDs */
385
- export function getThemeIds(): string[] {
386
- return Object.keys(THEMES).sort((a, b) => {
387
- if (a === "default") return -1;
388
- if (b === "default") return 1;
389
- return a.localeCompare(b);
390
- });
391
- }
392
-
393
- /** Get theme by ID and mode */
394
- export function getThemeColors(themeId: string, mode: "dark" | "light"): Theme {
395
- const def = THEMES[themeId] || THEMES["default"];
396
- return mode === "light" ? def.light : def.dark;
397
- }
398
-
399
- // ── Terminal detection ────────────────────────────────────
400
-
401
- function luminance(r: number, g: number, b: number): number {
402
- const [rs, gs, bs] = [r, g, b].map((c) => {
403
- const s = c / 255;
404
- return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
405
- });
406
- return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
407
- }
408
-
409
- function queryTerminalBackground(): Promise<{ r: number; g: number; b: number } | null> {
410
- return new Promise((resolve) => {
411
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
412
- resolve(null);
413
- return;
414
- }
415
- const timeout = setTimeout(() => { cleanup(); resolve(null); }, 500);
416
- let buf = "";
417
- const onData = (data: Buffer) => {
418
- buf += data.toString();
419
- const match = buf.match(
420
- /\x1b\]11;rgb:([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})/
421
- );
422
- if (match) {
423
- cleanup();
424
- const parse = (hex: string) =>
425
- hex.length <= 2 ? parseInt(hex, 16) : Math.round((parseInt(hex, 16) / 65535) * 255);
426
- resolve({ r: parse(match[1]), g: parse(match[2]), b: parse(match[3]) });
427
- }
428
- };
429
- const cleanup = () => {
430
- clearTimeout(timeout);
431
- process.stdin.removeListener("data", onData);
432
- if (wasRaw === false) process.stdin.setRawMode(false);
433
- };
434
- const wasRaw = process.stdin.isRaw;
435
- try { process.stdin.setRawMode(true); } catch { resolve(null); return; }
436
- process.stdin.on("data", onData);
437
- process.stdout.write("\x1b]11;?\x07");
438
- });
439
- }
440
-
441
- function detectThemeFromEnv(): "dark" | "light" {
442
- const bg = process.env.COLORFGBG;
443
- if (bg) {
444
- const parts = bg.split(";");
445
- const bgVal = parseInt(parts[parts.length - 1] || "0", 10);
446
- if (bgVal > 8) return "light";
447
- }
448
- return "dark";
449
- }
450
-
451
- let _detectedMode: "dark" | "light" | null = null;
452
-
453
- export async function detectTerminalTheme(): Promise<"dark" | "light"> {
454
- if (_detectedMode) return _detectedMode;
455
- const rgb = await queryTerminalBackground();
456
- if (rgb) {
457
- const lum = luminance(rgb.r, rgb.g, rgb.b);
458
- _detectedMode = lum > 0.5 ? "light" : "dark";
459
- } else {
460
- _detectedMode = detectThemeFromEnv();
461
- }
462
- return _detectedMode;
463
- }
464
-
465
- export function setTerminalBackground(color: RGBA): void {
466
- if (!process.stdout.isTTY) return;
467
- const hex = rgbToHex(color);
468
- process.stdout.write(`\x1b]11;${hex}\x07`);
469
- }
470
-
471
- export function restoreTerminalBackground(): void {
472
- if (!process.stdout.isTTY) return;
473
- process.stdout.write("\x1b]111;\x07");
474
- }
475
-
476
- // ── Syntax Style Generation ───────────────────────────────
477
-
478
- function generateSyntaxStyle(theme: Theme): SyntaxStyle {
479
- return SyntaxStyle.fromStyles({
480
- // Default text
481
- "default": { fg: theme.text },
482
- // Markdown-specific scopes (used by <markdown> component)
483
- "markup.heading.1": { fg: theme.primary, bold: true },
484
- "markup.heading.2": { fg: theme.primary, bold: true },
485
- "markup.heading.3": { fg: theme.info, bold: true },
486
- "markup.heading.4": { fg: theme.info, bold: true },
487
- "markup.heading.5": { fg: theme.info },
488
- "markup.heading.6": { fg: theme.info },
489
- "markup.bold": { bold: true },
490
- "markup.italic": { italic: true },
491
- "markup.list": { fg: theme.warning },
492
- "markup.link": { fg: theme.info, underline: true },
493
- "markup.raw": { fg: theme.warning },
494
- "markup.quote": { fg: theme.textMuted, italic: true },
495
- // Code block syntax highlighting
496
- "keyword": { fg: theme.secondary, bold: true },
497
- "keyword.control": { fg: theme.secondary, bold: true },
498
- "keyword.operator": { fg: theme.secondary },
499
- "string": { fg: theme.success },
500
- "string.quoted": { fg: theme.success },
501
- "comment": { fg: theme.textDim, italic: true },
502
- "comment.line": { fg: theme.textDim, italic: true },
503
- "comment.block": { fg: theme.textDim, italic: true },
504
- "constant": { fg: theme.warning },
505
- "constant.numeric": { fg: theme.warning },
506
- "number": { fg: theme.warning },
507
- "variable": { fg: theme.text },
508
- "variable.parameter": { fg: theme.text },
509
- "function": { fg: theme.info },
510
- "entity.name.function": { fg: theme.info },
511
- "support.function": { fg: theme.info },
512
- "type": { fg: theme.primary },
513
- "entity.name.type": { fg: theme.primary },
514
- "support.type": { fg: theme.primary },
515
- "operator": { fg: theme.textMuted },
516
- "punctuation": { fg: theme.textMuted },
517
- });
518
- }
519
-
520
- // ── Theme Context ─────────────────────────────────────────
521
-
522
- const ThemeContext = createContext<{
523
- theme: Theme;
524
- themeId: string;
525
- mode: "dark" | "light";
526
- syntaxStyle: SyntaxStyle;
527
- customBg: string | null;
528
- setThemeId: (id: string) => void;
529
- setMode: (m: "dark" | "light") => void;
530
- setCustomBg: (hex: string | null) => void;
531
- syncTerminalBg: boolean;
532
- setSyncTerminalBg: (sync: boolean) => void;
533
- } | undefined>(undefined);
534
-
535
- export function ThemeProvider(props: {
536
- mode?: string;
537
- themeId?: string;
538
- syncTerminalBg?: boolean;
539
- detectedMode?: "dark" | "light";
540
- children: ReactNode;
541
- }) {
542
- const initialMode: "dark" | "light" =
543
- props.mode === "light" ? "light"
544
- : props.mode === "auto" ? (props.detectedMode || "dark")
545
- : "dark";
546
-
547
- const initialThemeId = props.themeId || "vercel";
548
-
549
- const [themeId, setThemeIdState] = useState(initialThemeId);
550
- const [currentMode, setCurrentMode] = useState<"dark" | "light">(initialMode);
551
- const [theme, setTheme] = useState<Theme>(getThemeColors(initialThemeId, initialMode));
552
- const [syncBg, setSyncBg] = useState(props.syncTerminalBg ?? false);
553
- const [customBg, setCustomBgState] = useState<string | null>(null);
554
-
555
- // Refs to avoid stale closures — always read current values
556
- const syncBgRef = useRef(syncBg);
557
- syncBgRef.current = syncBg;
558
- const customBgRef = useRef(customBg);
559
- customBgRef.current = customBg;
560
- const modeRef = useRef(currentMode);
561
- modeRef.current = currentMode;
562
- const themeIdRef = useRef(themeId);
563
- themeIdRef.current = themeId;
564
-
565
- /** Apply terminal background to match theme — always call after setTheme */
566
- const applyTerminalBg = (colors: Theme) => {
567
- if (!syncBgRef.current) return;
568
- if (customBgRef.current) {
569
- setTerminalBackground(RGBA.fromHex(customBgRef.current));
570
- } else {
571
- setTerminalBackground(colors.bg);
572
- }
573
- };
574
-
575
- const setThemeId = (id: string) => {
576
- setThemeIdState(id);
577
- themeIdRef.current = id;
578
- const colors = getThemeColors(id, modeRef.current);
579
- setTheme(colors);
580
- applyTerminalBg(colors);
581
- useSettingsStore.getState().setThemeId(id);
582
- };
583
-
584
- const setMode = (m: "dark" | "light") => {
585
- setCurrentMode(m);
586
- modeRef.current = m;
587
- const colors = getThemeColors(themeIdRef.current, m);
588
- setTheme(colors);
589
- applyTerminalBg(colors);
590
- useSettingsStore.getState().setMode(m);
591
- };
592
-
593
- const setCustomBg = (hex: string | null) => {
594
- setCustomBgState(hex);
595
- customBgRef.current = hex;
596
- if (!syncBgRef.current) return;
597
- if (hex) {
598
- setTerminalBackground(RGBA.fromHex(hex));
599
- } else {
600
- setTerminalBackground(getThemeColors(themeIdRef.current, modeRef.current).bg);
601
- }
602
- };
603
-
604
- const setSyncTerminalBg = (sync: boolean) => {
605
- setSyncBg(sync);
606
- syncBgRef.current = sync;
607
- if (sync) {
608
- const colors = getThemeColors(themeIdRef.current, modeRef.current);
609
- applyTerminalBg(colors);
610
- } else {
611
- restoreTerminalBackground();
612
- }
613
- useSettingsStore.getState().setSyncTerminalBg(sync);
614
- };
615
-
616
- // Sync terminal bg on mount — force it immediately
617
- useEffect(() => {
618
- if (syncBgRef.current) {
619
- const colors = getThemeColors(initialThemeId, initialMode);
620
- applyTerminalBg(colors);
621
- }
622
- return () => {
623
- if (syncBgRef.current) restoreTerminalBackground();
624
- };
625
- }, []);
626
-
627
- const syntaxStyle = useMemo(() => generateSyntaxStyle(theme), [theme]);
628
-
629
- return (
630
- <ThemeContext.Provider value={{ theme, themeId, mode: currentMode, syntaxStyle, customBg, setThemeId, setMode, setCustomBg, syncTerminalBg: syncBg, setSyncTerminalBg }}>
631
- {props.children}
632
- </ThemeContext.Provider>
633
- );
634
- }
635
-
636
- export function useTheme() {
637
- const ctx = useContext(ThemeContext);
638
- if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
639
- return ctx;
640
- }