tokenmaw 0.3.0 → 0.4.1

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,26 +1,31 @@
1
1
  import blessed from 'blessed';
2
2
  import { diffKind, renderMarkdown } from '../markdown.js';
3
3
  import { highlightCode } from './syntax.js';
4
+ import { activeTuiTheme } from './theme.js';
4
5
  // Historical messages re-render on every frame; memoize the (expensive) result.
5
- // Entries are keyed by content+width and evicted least-recently-used.
6
+ // Entries are keyed by theme+width+content and evicted least-recently-used.
6
7
  const renderCache = new Map();
7
8
  const RENDER_CACHE_LIMIT = 600;
9
+ export function resetTuiMarkdownCache() {
10
+ renderCache.clear();
11
+ }
8
12
  /** Blessed tags keep patch colors independent of Chalk's stdout/NO_COLOR detection. */
9
13
  export function renderTuiMarkdown(content, columns) {
10
- const cacheKey = `${columns}\u0000${content}`;
14
+ const cacheKey = `${activeTuiTheme().name}\u0000${columns}\u0000${content}`;
11
15
  const cached = renderCache.get(cacheKey);
12
16
  if (cached !== undefined) {
13
17
  renderCache.delete(cacheKey);
14
18
  renderCache.set(cacheKey, cached);
15
19
  return cached;
16
20
  }
21
+ const markdown = activeTuiTheme().markdown;
17
22
  const out = [];
18
23
  let prose = [];
19
24
  let diff = false;
20
25
  let otherCode = false;
21
26
  const flush = () => {
22
27
  if (prose.length)
23
- out.push(blessed.escape(renderMarkdown(prose.join('\n'), columns)));
28
+ out.push(renderMarkdown(prose.join('\n'), columns));
24
29
  prose = [];
25
30
  };
26
31
  for (const line of content.split('\n')) {
@@ -36,14 +41,19 @@ export function renderTuiMarkdown(content, columns) {
36
41
  if (kind === 'add' || kind === 'del') {
37
42
  // Deep, near-black tinted backgrounds keep the code readable while
38
43
  // still signaling added/removed lines.
39
- const background = kind === 'add' ? '#10281a' : '#2b1215';
40
- const base = kind === 'add' ? '#9fd0a6' : '#d99f9f';
44
+ const background = kind === 'add' ? markdown.diffAddBg : markdown.diffDelBg;
45
+ const base = kind === 'add' ? markdown.diffAddText : markdown.diffDelText;
41
46
  const width = blessed.unicode.strWidth(line);
42
- out.push(`{${background}-bg}{${base}-fg}${highlightCode(line)}${' '.repeat(Math.max(0, columns - width))}{/${base}-fg}{/${background}-bg}`);
47
+ out.push(`{${background}-bg}{${base}-fg}${highlightCode(line, activeTuiTheme().syntax)}${' '.repeat(Math.max(0, columns - width))}{/${base}-fg}{/${background}-bg}`);
48
+ }
49
+ else if (kind === 'hunk') {
50
+ out.push(`{${markdown.accent}-fg}${blessed.escape(line)}{/${markdown.accent}-fg}`);
51
+ }
52
+ else if (kind === 'file') {
53
+ out.push(`{${markdown.text}-fg}${blessed.escape(line)}{/${markdown.text}-fg}`);
43
54
  }
44
55
  else {
45
- const color = kind === 'hunk' ? 'cyan' : 'white';
46
- out.push(`{${color}-fg}${kind === 'context' ? highlightCode(line) : blessed.escape(line)}{/${color}-fg}`);
56
+ out.push(highlightCode(line, activeTuiTheme().syntax));
47
57
  }
48
58
  }
49
59
  else {
@@ -52,7 +62,7 @@ export function renderTuiMarkdown(content, columns) {
52
62
  otherCode = !otherCode;
53
63
  }
54
64
  else if (otherCode)
55
- out.push(highlightCode(line));
65
+ out.push(highlightCode(line, activeTuiTheme().syntax));
56
66
  else
57
67
  prose.push(line);
58
68
  }
@@ -0,0 +1,370 @@
1
+ import blessed from 'blessed';
2
+ const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
3
+ /** One-cell glyph set used to build the capsule at half-row resolution. */
4
+ const PILL_BODY = '█';
5
+ const HALF_TOP = '▀';
6
+ const HALF_BOTTOM = '▄';
7
+ /** Number of content rows advanced by one wheel notch on the scrollbar. */
8
+ const WHEEL_SCROLL_LINES = 1;
9
+ /** On hover the feather is pulled in so the pill reads fuller and brighter. */
10
+ const HOVER_FEATHER_SCALE = 0.7;
11
+ const HOVER_BRIGHTEN = 0.16;
12
+ const TRAIL_DECAY = 0.86;
13
+ /** Spring constants (per 16ms frame): stiffness pulls, damping settles. */
14
+ const FRAME_MS = 16;
15
+ const SPRING_STIFFNESS = 0.16;
16
+ const SPRING_DAMPING = 0.72;
17
+ const SPRING_SETTLE = 0.02;
18
+ const RGB = (hex) => [
19
+ parseInt(hex.slice(1, 3), 16),
20
+ parseInt(hex.slice(3, 5), 16),
21
+ parseInt(hex.slice(5, 7), 16),
22
+ ];
23
+ const toHex = (value) => Math.round(clamp(value, 0, 255)).toString(16).padStart(2, '0');
24
+ /** ANSI theme names need RGB equivalents for smooth per-row interpolation. */
25
+ const ANSI_RGB = {
26
+ black: '#000000', gray: '#808080', white: '#c0c0c0',
27
+ 'light-black': '#808080', 'light-white': '#ffffff',
28
+ red: '#800000', 'light-red': '#ff5555', green: '#008000', 'light-green': '#55ff55',
29
+ yellow: '#808000', 'light-yellow': '#ffff55', blue: '#000080', 'light-blue': '#5555ff',
30
+ magenta: '#800080', 'light-magenta': '#ff55ff', cyan: '#008080', 'light-cyan': '#55ffff',
31
+ };
32
+ const rgbColor = (color) => color.startsWith('#') ? color : ANSI_RGB[color.toLowerCase()] ?? color;
33
+ /** Blend two hex colors; when either is a named color, fall back to the base. */
34
+ export const blend = (base, toward, ratio) => {
35
+ if (!base.startsWith('#') || !toward.startsWith('#'))
36
+ return base;
37
+ const [r1, g1, b1] = RGB(base);
38
+ const [r2, g2, b2] = RGB(toward);
39
+ return `#${toHex(r1 + (r2 - r1) * ratio)}${toHex(g1 + (g2 - g1) * ratio)}${toHex(b1 + (b2 - b1) * ratio)}`;
40
+ };
41
+ const luminance = (hex) => {
42
+ if (!hex.startsWith('#'))
43
+ return 0.5;
44
+ const [r, g, b] = RGB(hex);
45
+ return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
46
+ };
47
+ /** Nudge a color brighter on dark themes and darker on light ones. */
48
+ const boost = (hex, ratio) => blend(hex, luminance(hex) >= 0.5 ? '#000000' : '#ffffff', ratio);
49
+ /**
50
+ * Derive the pill palette from a TUI theme. The capsule follows the theme's
51
+ * accent color, while the track remains a quiet panel/background surface. A
52
+ * sparse dot pattern gives the otherwise empty track a little definition.
53
+ */
54
+ export function pillScrollbarColors(ui) {
55
+ const thumb = rgbColor(ui.accent ?? ui.elevated);
56
+ const fadedBase = rgbColor(ui.subtle ?? ui.panel);
57
+ const patternBase = rgbColor(ui.subtle ?? ui.line ?? ui.panel);
58
+ const panel = rgbColor(ui.panel);
59
+ const background = rgbColor(ui.background);
60
+ const fadedBlend = blend(thumb, fadedBase, 0.5);
61
+ return {
62
+ thumb,
63
+ // Named terminal colors cannot be blended numerically; fall back to the
64
+ // theme's quieter surface so the top-of-scroll state still reads faded.
65
+ thumbFaded: fadedBlend === thumb && fadedBase !== thumb ? fadedBase : fadedBlend,
66
+ track: blend(panel, background, 0.3),
67
+ trackPattern: '·',
68
+ trackPatternColor: blend(patternBase, ui.background, 0.45),
69
+ };
70
+ }
71
+ /**
72
+ * Compute pill geometry for a rendered scrollable element. Returns null when
73
+ * the element is off-screen or its content fits the viewport.
74
+ */
75
+ export function computePillScrollbarState(element, lpos) {
76
+ const viewportHeight = Math.max(1, lpos.yl - lpos.yi - Number(element.iheight));
77
+ const scrollHeight = element.getScrollHeight();
78
+ const maxOffset = scrollHeight - viewportHeight;
79
+ if (maxOffset <= 0)
80
+ return null;
81
+ const offset = clamp(element.childBase ?? 0, 0, maxOffset);
82
+ const thumbHeight = clamp(Math.round(viewportHeight * (viewportHeight / scrollHeight)), 3, viewportHeight);
83
+ const travel = viewportHeight - thumbHeight;
84
+ const thumbStart = clamp(Math.round((offset / maxOffset) * travel), 0, travel);
85
+ return { offset, maxOffset, viewportHeight, thumbHeight, thumbStart, faded: offset === 0 };
86
+ }
87
+ /**
88
+ * Attach a pill overlay to a scrollable element. blessed's own scrollbar is
89
+ * not used — do NOT pass a `scrollbar` option to the element. The returned
90
+ * handle must be `sync()`ed after layout/content changes (before
91
+ * `screen.render()`) and `destroy()`ed with its owner.
92
+ */
93
+ export function attachPillScrollbar(element, colors) {
94
+ const overlay = blessed.box({
95
+ parent: element.screen, tags: true, width: 1, height: 1, hidden: true, mouse: true,
96
+ style: {},
97
+ });
98
+ let hover = false;
99
+ /** Spring position of the pill's top edge in rows; null until first shown. */
100
+ let visual = null;
101
+ let velocity = 0;
102
+ let timer = null;
103
+ let lastState = null;
104
+ let trail = [];
105
+ /** Brightest leftover trail ghost from the last paint; 0 when settled. */
106
+ let residue = 0;
107
+ let dragging = false;
108
+ let dragOffset = 0;
109
+ let movedDuringDrag = false;
110
+ const stopTimer = () => {
111
+ if (timer === null)
112
+ return;
113
+ clearInterval(timer);
114
+ timer = null;
115
+ };
116
+ const sleep = () => {
117
+ stopTimer();
118
+ visual = null;
119
+ velocity = 0;
120
+ lastState = null;
121
+ trail = [];
122
+ residue = 0;
123
+ };
124
+ const targetOf = (state) => {
125
+ const travel = state.viewportHeight - state.thumbHeight;
126
+ return state.maxOffset > 0 ? clamp((state.offset / state.maxOffset) * travel, 0, travel) : 0;
127
+ };
128
+ const smoothstep = (value) => {
129
+ const t = clamp(value, 0, 1);
130
+ return t * t * (3 - 2 * t);
131
+ };
132
+ const paint = (state, top) => {
133
+ lastState = state;
134
+ const c = colors();
135
+ const fill = hover ? boost(state.faded ? c.thumbFaded : c.thumb, HOVER_BRIGHTEN) : (state.faded ? c.thumbFaded : c.thumb);
136
+ const featherScale = hover ? HOVER_FEATHER_SCALE : 1;
137
+ // Motion (spring velocity plus remaining distance) drives the feather and
138
+ // the trail; at rest both collapse so the capsule is fully opaque.
139
+ const motion = clamp(Math.abs(velocity) * 10 + Math.abs(targetOf(state) - top) * 1.5, 0, 1);
140
+ overlay.style.bg = c.track;
141
+ const maxTop = Math.max(0, state.viewportHeight - state.thumbHeight);
142
+ const start = clamp(top, 0, maxTop);
143
+ const center = start + state.thumbHeight / 2;
144
+ const halfLength = state.thumbHeight / 2;
145
+ const feather = Math.max(0.06, motion * 1.15 * featherScale);
146
+ // Store two samples per terminal row. A half-block cell can display an
147
+ // independent foreground (top half) and background (bottom half), which
148
+ // doubles the scrollbar's vertical resolution without changing its width.
149
+ if (trail.length !== state.viewportHeight * 2)
150
+ trail = Array.from({ length: state.viewportHeight * 2 }, () => 0);
151
+ // Residue is how much painted brightness exceeds the live intensity —
152
+ // i.e. leftover trail ghosting. The animation keeps running until it
153
+ // drains, so a pill that just stopped never rests with a faded tail.
154
+ residue = 0;
155
+ const sample = (position, index) => {
156
+ const distance = Math.abs(position - center);
157
+ const edge = (distance - halfLength) / feather;
158
+ const intensity = edge <= 0 ? 1 : 1 - smoothstep(edge);
159
+ const next = Math.max(intensity, trail[index] * TRAIL_DECAY);
160
+ residue = Math.max(residue, next - intensity);
161
+ trail[index] = next;
162
+ return next;
163
+ };
164
+ const colorAt = (level) => blend(fill, c.track, 1 - level);
165
+ const lines = [];
166
+ for (let row = 0; row < state.viewportHeight; row += 1) {
167
+ const topLevel = sample(row + 0.25, row * 2);
168
+ const bottomLevel = sample(row + 0.75, row * 2 + 1);
169
+ if (Math.max(topLevel, bottomLevel) < 0.08) {
170
+ const pattern = c.trackPattern;
171
+ lines.push(pattern && row % 2 === 0
172
+ ? `{${c.trackPatternColor ?? c.track}-fg}${pattern}{/}`
173
+ : ' ');
174
+ continue;
175
+ }
176
+ // Continuous intensity and a soft glyph edge make the capsule feel like
177
+ // a moving object. A row is a full block only when both of its halves
178
+ // are lit; otherwise the brighter half's half-block glyph carries the
179
+ // edge, so a growing/shrinking capsule tapers ▀/▄ → █ and never shows a
180
+ // stray dim block ahead of its cap.
181
+ const topColor = colorAt(topLevel);
182
+ const bottomColor = colorAt(bottomLevel);
183
+ const topLit = topLevel > 0.52;
184
+ const bottomLit = bottomLevel > 0.52;
185
+ if (topLit && bottomLit) {
186
+ lines.push(`{${colorAt(Math.max(topLevel, bottomLevel))}-fg}${PILL_BODY}{/}`);
187
+ }
188
+ else if (topLevel >= bottomLevel) {
189
+ lines.push(`{${topColor}-fg}${HALF_TOP}{/}`);
190
+ }
191
+ else {
192
+ lines.push(`{${bottomColor}-fg}${HALF_BOTTOM}{/}`);
193
+ }
194
+ }
195
+ overlay.setContent(lines.join('\n'));
196
+ };
197
+ /** Advance the spring one frame; true when the pill has come to rest. */
198
+ const stepSpring = (target) => {
199
+ if (visual === null) {
200
+ visual = target;
201
+ return true;
202
+ }
203
+ velocity = (velocity + (target - visual) * SPRING_STIFFNESS) * SPRING_DAMPING;
204
+ visual += velocity;
205
+ if (Math.abs(target - visual) < SPRING_SETTLE && Math.abs(velocity) < SPRING_SETTLE) {
206
+ visual = target;
207
+ velocity = 0;
208
+ return true;
209
+ }
210
+ return false;
211
+ };
212
+ const frame = () => {
213
+ if (visual === null || overlay.hidden) {
214
+ sleep();
215
+ return;
216
+ }
217
+ // Native selection mode releases mouse capture; freezing the spring keeps
218
+ // the idle screen bit-stable while the user selects text.
219
+ const program = element.screen.program;
220
+ if (program?.mouseEnabled === false) {
221
+ sleep();
222
+ return;
223
+ }
224
+ const lpos = element.lpos;
225
+ const state = lpos && !element.hidden && !element.detached ? computePillScrollbarState(element, lpos) : null;
226
+ if (!state || !lpos) {
227
+ overlay.hide();
228
+ sleep();
229
+ return;
230
+ }
231
+ overlay.left = lpos.xl - 1;
232
+ overlay.top = lpos.yi + Number(element.itop);
233
+ overlay.height = state.viewportHeight;
234
+ const settled = stepSpring(targetOf(state));
235
+ paint(state, visual);
236
+ // Keep painting until both the spring and the trail have drained, so the
237
+ // pill never comes to rest showing motion-only transparency.
238
+ if (settled && residue < 0.03)
239
+ stopTimer();
240
+ else
241
+ element.screen.render();
242
+ };
243
+ const wake = () => {
244
+ if (timer === null)
245
+ timer = setInterval(frame, FRAME_MS);
246
+ };
247
+ const sync = () => {
248
+ const lpos = element.lpos;
249
+ if (element.hidden || element.detached || !lpos) {
250
+ overlay.hide();
251
+ sleep();
252
+ return;
253
+ }
254
+ const state = computePillScrollbarState(element, lpos);
255
+ if (!state) {
256
+ overlay.hide();
257
+ sleep();
258
+ return;
259
+ }
260
+ overlay.show();
261
+ overlay.left = lpos.xl - 1;
262
+ overlay.top = lpos.yi + Number(element.itop);
263
+ overlay.width = 1;
264
+ overlay.height = state.viewportHeight;
265
+ if (visual === null) {
266
+ // First paint after a show snaps to the scroll position; only later
267
+ // scroll deltas glide on the spring.
268
+ visual = targetOf(state);
269
+ velocity = 0;
270
+ }
271
+ paint(state, visual);
272
+ // Only animate when the spring has distance to cover or a trail ghost is
273
+ // still draining; a settled sync (the common case) paints once and leaves
274
+ // the timer dead.
275
+ if (Math.abs(targetOf(state) - visual) > SPRING_SETTLE || residue > 0.03)
276
+ wake();
277
+ };
278
+ // Hover boost repaints immediately so the affordance does not wait for the
279
+ // next data-driven refresh.
280
+ const repaintHover = () => {
281
+ if (lastState === null || visual === null || overlay.hidden)
282
+ return;
283
+ paint(lastState, visual);
284
+ element.screen.render();
285
+ };
286
+ element.on('scroll', sync);
287
+ element.on('resize', sync);
288
+ element.on('hide', () => {
289
+ overlay.hide();
290
+ sleep();
291
+ });
292
+ element.on('detach', () => {
293
+ overlay.hide();
294
+ sleep();
295
+ });
296
+ // The overlay column stays interactive: wheel scrolls the tracked element,
297
+ // click jumps proportionally to the click position, and drag follows the
298
+ // pointer continuously while retaining the point where the pill was grabbed.
299
+ const pointerState = () => {
300
+ const lpos = element.lpos;
301
+ const state = lpos ? computePillScrollbarState(element, lpos) : null;
302
+ return lpos && state ? { lpos, state } : null;
303
+ };
304
+ const setFromPointer = (y) => {
305
+ const current = pointerState();
306
+ if (!current)
307
+ return;
308
+ const { lpos, state } = current;
309
+ const travel = Math.max(1, state.viewportHeight - state.thumbHeight);
310
+ const top = clamp(y - (lpos.yi + Number(element.itop)) - dragOffset, 0, travel);
311
+ element.scrollTo(Math.round((top / travel) * state.maxOffset));
312
+ };
313
+ overlay.on('wheelup', () => element.scroll(-WHEEL_SCROLL_LINES));
314
+ overlay.on('wheeldown', () => element.scroll(WHEEL_SCROLL_LINES));
315
+ overlay.on('mousedown', (data) => {
316
+ const current = pointerState();
317
+ if (!current || data.y === undefined)
318
+ return;
319
+ const { lpos, state } = current;
320
+ const top = visual ?? targetOf(state);
321
+ const localY = data.y - (lpos.yi + Number(element.itop));
322
+ dragOffset = localY >= top && localY <= top + state.thumbHeight ? localY - top : state.thumbHeight / 2;
323
+ dragging = true;
324
+ movedDuringDrag = false;
325
+ setFromPointer(data.y);
326
+ });
327
+ const onScreenMouseMove = (data) => {
328
+ if (!dragging || data.y === undefined)
329
+ return;
330
+ movedDuringDrag = true;
331
+ setFromPointer(data.y);
332
+ };
333
+ const onScreenMouseUp = () => {
334
+ dragging = false;
335
+ };
336
+ overlay.onScreenEvent('mousemove', onScreenMouseMove);
337
+ overlay.onScreenEvent('mouseup', onScreenMouseUp);
338
+ overlay.on('mouseover', () => {
339
+ hover = true;
340
+ repaintHover();
341
+ });
342
+ overlay.on('mouseout', () => {
343
+ hover = false;
344
+ repaintHover();
345
+ });
346
+ overlay.on('click', (data) => {
347
+ if (movedDuringDrag) {
348
+ movedDuringDrag = false;
349
+ return;
350
+ }
351
+ const lpos = element.lpos;
352
+ const state = lpos ? computePillScrollbarState(element, lpos) : null;
353
+ if (!lpos || !state)
354
+ return;
355
+ const trackY = lpos.yi + Number(element.itop);
356
+ const travel = Math.max(1, state.viewportHeight - state.thumbHeight);
357
+ const ratio = clamp(((data.y ?? trackY) - trackY - state.thumbHeight / 2) / travel, 0, 1);
358
+ element.scrollTo(Math.round(ratio * state.maxOffset));
359
+ });
360
+ return {
361
+ sync,
362
+ destroy: () => {
363
+ stopTimer();
364
+ dragging = false;
365
+ overlay.removeScreenEvent('mousemove', onScreenMouseMove);
366
+ overlay.removeScreenEvent('mouseup', onScreenMouseUp);
367
+ overlay.destroy();
368
+ },
369
+ };
370
+ }
package/dist/ui/syntax.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import blessed from 'blessed';
2
- /** Small lexical highlighter. Unknown languages remain readable plain text. */
3
- export function highlightCode(source, light = false) {
4
- const colors = light
5
- ? ['#58665c', '#98502c', '#6141a0', '#175d93', '#9a3570']
6
- : ['#84918b', '#cead83', '#ba9ce0', '#87b9db', '#d493b5'];
2
+ /** Small lexical highlighter. Unknown languages remain readable plain text.
3
+ * Colors come from the active theme: comment, string, keyword, attr, number. */
4
+ export function highlightCode(source, colors = ['#84918b', '#cead83', '#ba9ce0', '#87b9db', '#d493b5']) {
7
5
  const pattern = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/|<!--.*?-->)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b(?:const|let|var|function|return|if|else|for|while|class|import|from|export|async|await|new|true|false|null|undefined|def|print|in|None|True|False|public|private|interface|type)\b)|(<\/?[\w-]+|\b[\w-]+(?=\s*=))|(\b\d+(?:\.\d+)?(?:px|em|rem|%)?\b)/g;
8
6
  let result = '';
9
7
  let offset = 0;
@@ -0,0 +1,198 @@
1
+ import { setMarkdownTheme } from '../markdown.js';
2
+ const aurora = {
3
+ name: 'aurora',
4
+ label: 'Aurora · polar night',
5
+ ui: {
6
+ background: '#0b1020', panel: '#0d1425', composer: '#121a30', activity: '#0e1526',
7
+ elevated: '#1b2540', modal: '#101830', modalRule: '#263354', line: '#263354',
8
+ text: '#dde7f5', muted: '#9fb0cc', subtle: '#5d6f92', accent: '#5eead4', success: '#7ee2a8',
9
+ warning: '#f2c66d', error: '#f07a8a',
10
+ },
11
+ markdown: {
12
+ text: '#dde7f5', muted: '#8fa1c0', accent: '#5eead4',
13
+ heading: '#a78bfa', headingStrong: '#f0f6ff',
14
+ codeBg: '#101a30', codeText: '#d6e2f2', codeFence: '#44548a',
15
+ diffAddBg: '#12291e', diffAddText: '#8fe3b0',
16
+ diffDelBg: '#2e1620', diffDelText: '#f09aa8',
17
+ },
18
+ syntax: ['#5d6f92', '#7ee2a8', '#a78bfa', '#5eead4', '#f2a2c0'],
19
+ };
20
+ const midnight = {
21
+ name: 'midnight',
22
+ label: 'Midnight · deep blue-black',
23
+ ui: {
24
+ background: 'black', panel: 'black', composer: '#171c23', activity: '#11161c',
25
+ elevated: '#242c36', modal: '#171a1f', modalRule: '#2b3139', line: 'gray',
26
+ text: 'light-white', muted: 'white', subtle: 'gray', accent: 'light-cyan', success: 'light-green',
27
+ warning: 'light-yellow', error: 'light-red',
28
+ },
29
+ markdown: {
30
+ text: '#d7e0ea', muted: '#7f92a6', accent: '#6fb1d6',
31
+ heading: '#8ac3e6', headingStrong: '#d7e0ea',
32
+ codeBg: '#16212d', codeText: '#c7d7e6', codeFence: '#5f7388',
33
+ diffAddBg: '#10281a', diffAddText: '#9fd0a6',
34
+ diffDelBg: '#2b1215', diffDelText: '#d99f9f',
35
+ },
36
+ syntax: ['#84918b', '#cead83', '#ba9ce0', '#87b9db', '#d493b5'],
37
+ };
38
+ const nord = {
39
+ name: 'nord',
40
+ label: 'Nord · arctic blue',
41
+ ui: {
42
+ background: '#2e3440', panel: '#2e3440', composer: '#3b4252', activity: '#333b47',
43
+ elevated: '#434c5e', modal: '#353c4a', modalRule: '#4c566a', line: '#4c566a',
44
+ text: '#eceff4', muted: '#d8dee9', subtle: '#7b88a1', accent: '#88c0d0', success: '#a3be8c',
45
+ warning: '#ebcb8b', error: '#bf616a',
46
+ },
47
+ markdown: {
48
+ text: '#eceff4', muted: '#7b88a1', accent: '#81a1c1',
49
+ heading: '#88c0d0', headingStrong: '#eceff4',
50
+ codeBg: '#353c4a', codeText: '#d8dee9', codeFence: '#4c566a',
51
+ diffAddBg: '#2f4032', diffAddText: '#a3be8c',
52
+ diffDelBg: '#46302c', diffDelText: '#d08770',
53
+ },
54
+ syntax: ['#7b88a1', '#ebcb8b', '#b48ead', '#81a1c1', '#d08770'],
55
+ };
56
+ const dracula = {
57
+ name: 'dracula',
58
+ label: 'Dracula · purple night',
59
+ ui: {
60
+ background: '#282a36', panel: '#282a36', composer: '#343746', activity: '#2f3141',
61
+ elevated: '#44475a', modal: '#2b2d3a', modalRule: '#44475a', line: '#44475a',
62
+ text: '#f8f8f2', muted: '#b6bcc9', subtle: '#6272a4', accent: '#bd93f9', success: '#50fa7b',
63
+ warning: '#f1fa8c', error: '#ff5555',
64
+ },
65
+ markdown: {
66
+ text: '#f8f8f2', muted: '#6272a4', accent: '#8be9fd',
67
+ heading: '#bd93f9', headingStrong: '#f8f8f2',
68
+ codeBg: '#21222c', codeText: '#e2e2dc', codeFence: '#6272a4',
69
+ diffAddBg: '#1f3a2b', diffAddText: '#7ff0a5',
70
+ diffDelBg: '#3a2430', diffDelText: '#ff8b8b',
71
+ },
72
+ syntax: ['#6272a4', '#f1fa8c', '#bd93f9', '#8be9fd', '#ff79c6'],
73
+ };
74
+ const dawn = {
75
+ name: 'dawn',
76
+ label: 'Dawn · warm light',
77
+ ui: {
78
+ background: '#f7f4ee', panel: '#efece4', composer: '#f0ece2', activity: '#eceade',
79
+ elevated: '#dcd7c8', modal: '#f2efe8', modalRule: '#c9c2b2', line: '#b9b2a2',
80
+ text: '#3b3a36', muted: '#5c584f', subtle: '#8f897a', accent: '#1f6f8b', success: '#4a7c3f',
81
+ warning: '#a8760a', error: '#b3413a',
82
+ },
83
+ markdown: {
84
+ text: '#3b3a36', muted: '#8f897a', accent: '#1f6f8b',
85
+ heading: '#1f6f8b', headingStrong: '#2f2e2a',
86
+ codeBg: '#e8e3d6', codeText: '#3b3a36', codeFence: '#a49c88',
87
+ diffAddBg: '#dcead2', diffAddText: '#2e5c28',
88
+ diffDelBg: '#f2dcd8', diffDelText: '#96372f',
89
+ },
90
+ syntax: ['#6f6a5c', '#8a5a20', '#5b4392', '#1f5c8b', '#8f3a63'],
91
+ };
92
+ const solarized = {
93
+ name: 'solarized', label: 'Solarized · balanced contrast',
94
+ ui: { background: '#002b36', panel: '#073642', composer: '#0b3b46', activity: '#06323d', elevated: '#14505b', modal: '#0b3b46', modalRule: '#1b5963', line: '#496b70', text: '#eee8d5', muted: '#c7c0a8', subtle: '#839496', accent: '#2aa198', success: '#859900', warning: '#b58900', error: '#dc322f' },
95
+ markdown: { text: '#eee8d5', muted: '#839496', accent: '#2aa198', heading: '#268bd2', headingStrong: '#fdf6e3', codeBg: '#073642', codeText: '#eee8d5', codeFence: '#586e75', diffAddBg: '#193b31', diffAddText: '#b7d68a', diffDelBg: '#4a2528', diffDelText: '#f28b82' },
96
+ syntax: ['#839496', '#2aa198', '#859900', '#268bd2', '#d33682'],
97
+ };
98
+ const rosePine = {
99
+ name: 'rose-pine', label: 'Rosé Pine · soft dusk',
100
+ ui: { background: '#191724', panel: '#1f1d2e', composer: '#26233a', activity: '#211f32', elevated: '#393552', modal: '#26233a', modalRule: '#403d52', line: '#524f67', text: '#e0def4', muted: '#c4a7e7', subtle: '#908caa', accent: '#ebbcba', success: '#9ccfd8', warning: '#f6c177', error: '#eb6f92' },
101
+ markdown: { text: '#e0def4', muted: '#908caa', accent: '#ebbcba', heading: '#c4a7e7', headingStrong: '#fffaf3', codeBg: '#1f1d2e', codeText: '#e0def4', codeFence: '#6e6a86', diffAddBg: '#20373b', diffAddText: '#9ccfd8', diffDelBg: '#422638', diffDelText: '#eb6f92' },
102
+ syntax: ['#908caa', '#f6c177', '#c4a7e7', '#9ccfd8', '#ebbcba'],
103
+ };
104
+ const tokyoNight = {
105
+ name: 'tokyo-night', label: 'Tokyo Night · neon dusk',
106
+ ui: { background: '#1a1b26', panel: '#16161e', composer: '#1f2335', activity: '#1c1d2b', elevated: '#292e42', modal: '#1f2335', modalRule: '#2f334d', line: '#3b4261', text: '#c0caf5', muted: '#a9b1d6', subtle: '#565f89', accent: '#7aa2f7', success: '#9ece6a', warning: '#e0af68', error: '#f7768e' },
107
+ markdown: { text: '#c0caf5', muted: '#565f89', accent: '#7dcfff', heading: '#7aa2f7', headingStrong: '#c0caf5', codeBg: '#16161e', codeText: '#a9b1d6', codeFence: '#3b4261', diffAddBg: '#1e2a24', diffAddText: '#9ece6a', diffDelBg: '#2d202a', diffDelText: '#f7768e' },
108
+ syntax: ['#565f89', '#9ece6a', '#bb9af7', '#7aa2f7', '#ff9e64'],
109
+ };
110
+ const catppuccinMocha = {
111
+ name: 'catppuccin-mocha', label: 'Catppuccin Mocha · pastel night',
112
+ ui: { background: '#1e1e2e', panel: '#181825', composer: '#242438', activity: '#181825', elevated: '#313244', modal: '#1b1b2a', modalRule: '#45475a', line: '#313244', text: '#cdd6f4', muted: '#a6adc8', subtle: '#6c7086', accent: '#89b4fa', success: '#a6e3a1', warning: '#f9e2af', error: '#f38ba8' },
113
+ markdown: { text: '#cdd6f4', muted: '#6c7086', accent: '#89b4fa', heading: '#cba6f7', headingStrong: '#cdd6f4', codeBg: '#181825', codeText: '#cdd6f4', codeFence: '#45475a', diffAddBg: '#20312a', diffAddText: '#a6e3a1', diffDelBg: '#3b2530', diffDelText: '#f38ba8' },
114
+ syntax: ['#6c7086', '#a6e3a1', '#cba6f7', '#89b4fa', '#fab387'],
115
+ };
116
+ const catppuccinLatte = {
117
+ name: 'catppuccin-latte', label: 'Catppuccin Latte · pastel light',
118
+ ui: { background: '#eff1f5', panel: '#e6e9ef', composer: '#e6e9ef', activity: '#dce0e8', elevated: '#ccd0da', modal: '#f4f6fb', modalRule: '#bcc0cc', line: '#bcc0cc', text: '#4c4f69', muted: '#6c6f85', subtle: '#9ca0b0', accent: '#1e66f5', success: '#40a02b', warning: '#df8e1d', error: '#d20f39' },
119
+ markdown: { text: '#4c4f69', muted: '#9ca0b0', accent: '#1e66f5', heading: '#8839ef', headingStrong: '#4c4f69', codeBg: '#e6e9ef', codeText: '#4c4f69', codeFence: '#acb0be', diffAddBg: '#dcefdd', diffAddText: '#28731c', diffDelBg: '#f4dbdc', diffDelText: '#d20f39' },
120
+ syntax: ['#9ca0b0', '#40a02b', '#8839ef', '#1e66f5', '#fe640b'],
121
+ };
122
+ const gruvboxDark = {
123
+ name: 'gruvbox-dark', label: 'Gruvbox Dark · retro groove',
124
+ ui: { background: '#282828', panel: '#1d2021', composer: '#32302f', activity: '#2c2c28', elevated: '#3c3836', modal: '#2d2c29', modalRule: '#504945', line: '#504945', text: '#ebdbb2', muted: '#d5c4a1', subtle: '#928374', accent: '#fe8019', success: '#b8bb26', warning: '#fabd2f', error: '#fb4934' },
125
+ markdown: { text: '#ebdbb2', muted: '#928374', accent: '#83a598', heading: '#fabd2f', headingStrong: '#ebdbb2', codeBg: '#1d2021', codeText: '#ebdbb2', codeFence: '#504945', diffAddBg: '#2f331f', diffAddText: '#b8bb26', diffDelBg: '#3c2321', diffDelText: '#fb4934' },
126
+ syntax: ['#928374', '#b8bb26', '#fb4934', '#83a598', '#d3869b'],
127
+ };
128
+ const oneDark = {
129
+ name: 'one-dark', label: 'One Dark · atom classic',
130
+ ui: { background: '#282c34', panel: '#21252b', composer: '#2f343d', activity: '#23272e', elevated: '#3a3f4b', modal: '#2a2e37', modalRule: '#3e4451', line: '#3e4451', text: '#abb2bf', muted: '#828997', subtle: '#5c6370', accent: '#61afef', success: '#98c379', warning: '#e5c07b', error: '#e06c75' },
131
+ markdown: { text: '#abb2bf', muted: '#5c6370', accent: '#56b6c2', heading: '#61afef', headingStrong: '#d7dae0', codeBg: '#21252b', codeText: '#abb2bf', codeFence: '#3e4451', diffAddBg: '#243129', diffAddText: '#98c379', diffDelBg: '#38262a', diffDelText: '#e06c75' },
132
+ syntax: ['#5c6370', '#98c379', '#c678dd', '#61afef', '#d19a66'],
133
+ };
134
+ const monokai = {
135
+ name: 'monokai', label: 'Monokai · classic pop',
136
+ ui: { background: '#272822', panel: '#1e1f1c', composer: '#2d2e27', activity: '#262721', elevated: '#3e3d32', modal: '#2b2c25', modalRule: '#49483e', line: '#49483e', text: '#f8f8f2', muted: '#a8a89d', subtle: '#75715e', accent: '#66d9ef', success: '#a6e22e', warning: '#e6db74', error: '#f92672' },
137
+ markdown: { text: '#f8f8f2', muted: '#75715e', accent: '#a6e22e', heading: '#fd971f', headingStrong: '#f8f8f2', codeBg: '#1e1f1c', codeText: '#f8f8f2', codeFence: '#49483e', diffAddBg: '#2a331d', diffAddText: '#a6e22e', diffDelBg: '#3a1f26', diffDelText: '#f92672' },
138
+ syntax: ['#75715e', '#e6db74', '#f92672', '#66d9ef', '#ae81ff'],
139
+ };
140
+ const kanagawa = {
141
+ name: 'kanagawa', label: 'Kanagawa · ink wash',
142
+ ui: { background: '#1f1f28', panel: '#16161d', composer: '#2a2a37', activity: '#1a1a22', elevated: '#363649', modal: '#262635', modalRule: '#363649', line: '#54546d', text: '#dcd7ba', muted: '#c8c093', subtle: '#727169', accent: '#7e9cd8', success: '#98bb6c', warning: '#ff9e3b', error: '#e82424' },
143
+ markdown: { text: '#dcd7ba', muted: '#727169', accent: '#7fb4ca', heading: '#957fb8', headingStrong: '#dcd7ba', codeBg: '#16161d', codeText: '#dcd7ba', codeFence: '#54546d', diffAddBg: '#2a332e', diffAddText: '#98bb6c', diffDelBg: '#43242b', diffDelText: '#e82424' },
144
+ syntax: ['#727169', '#98bb6c', '#957fb8', '#7fb4ca', '#ff9e3b'],
145
+ };
146
+ const everforest = {
147
+ name: 'everforest', label: 'Everforest · moss green',
148
+ ui: { background: '#2d353b', panel: '#272e33', composer: '#343f44', activity: '#2e373d', elevated: '#3d484d', modal: '#333e44', modalRule: '#475258', line: '#475258', text: '#d3c6aa', muted: '#9da9a0', subtle: '#7a8478', accent: '#a7c080', success: '#83c092', warning: '#dbbc7f', error: '#e67e80' },
149
+ markdown: { text: '#d3c6aa', muted: '#7a8478', accent: '#7fbbb3', heading: '#dbbc7f', headingStrong: '#d3c6aa', codeBg: '#272e33', codeText: '#d3c6aa', codeFence: '#475258', diffAddBg: '#2f3a2d', diffAddText: '#a7c080', diffDelBg: '#452f2c', diffDelText: '#e67e80' },
150
+ syntax: ['#7a8478', '#a7c080', '#d699b6', '#7fbbb3', '#dbbc7f'],
151
+ };
152
+ const synthwave = {
153
+ name: 'synthwave', label: "SynthWave '84 · neon grid",
154
+ ui: { background: '#262335', panel: '#211d2e', composer: '#2c2840', activity: '#252138', elevated: '#3a3454', modal: '#2a2540', modalRule: '#443d63', line: '#443d63', text: '#f8f8f4', muted: '#a5a1bd', subtle: '#6f6a8a', accent: '#ff7edb', success: '#72f1b8', warning: '#fede5d', error: '#fe4450' },
155
+ markdown: { text: '#f8f8f4', muted: '#6f6a8a', accent: '#ff7edb', heading: '#36f9f6', headingStrong: '#f8f8f4', codeBg: '#1e1c2c', codeText: '#d9d6e8', codeFence: '#443d63', diffAddBg: '#1e332c', diffAddText: '#72f1b8', diffDelBg: '#3a2233', diffDelText: '#fe4450' },
156
+ syntax: ['#6f6a8a', '#fede5d', '#ff7edb', '#36f9f6', '#fe8b48'],
157
+ };
158
+ const matrix = {
159
+ name: 'matrix', label: 'Matrix · phosphor green',
160
+ ui: { background: '#000d05', panel: '#000a03', composer: '#00180a', activity: '#001206', elevated: '#022412', modal: '#001708', modalRule: '#0b4a22', line: '#0b4a22', text: '#8dffb4', muted: '#3f9e5f', subtle: '#1e5f38', accent: '#00ff41', success: '#39ff14', warning: '#aaff00', error: '#ff4141' },
161
+ markdown: { text: '#8dffb4', muted: '#2e7a4a', accent: '#00ff41', heading: '#7dff8e', headingStrong: '#c8ffd9', codeBg: '#01180c', codeText: '#8dffb4', codeFence: '#1e5f38', diffAddBg: '#032d16', diffAddText: '#4dff88', diffDelBg: '#3a0d12', diffDelText: '#ff6b6b' },
162
+ syntax: ['#2e7a4a', '#7dffa0', '#00ff41', '#aaff00', '#39ff14'],
163
+ };
164
+ const solarizedLight = {
165
+ name: 'solarized-light', label: 'Solarized Light · paper',
166
+ ui: { background: '#fdf6e3', panel: '#eee8d5', composer: '#f4eedd', activity: '#ece5d3', elevated: '#ddd5c1', modal: '#f2ecdb', modalRule: '#ccc4ae', line: '#afa897', text: '#657b83', muted: '#93a1a1', subtle: '#a4aeab', accent: '#268bd2', success: '#859900', warning: '#b58900', error: '#dc322f' },
167
+ markdown: { text: '#657b83', muted: '#93a1a1', accent: '#268bd2', heading: '#268bd2', headingStrong: '#586e75', codeBg: '#eee8d5', codeText: '#657b83', codeFence: '#93a1a1', diffAddBg: '#e3eedd', diffAddText: '#859900', diffDelBg: '#f3dfdb', diffDelText: '#dc322f' },
168
+ syntax: ['#93a1a1', '#859900', '#6c71c4', '#268bd2', '#cb4b16'],
169
+ };
170
+ const githubLight = {
171
+ name: 'github-light', label: 'GitHub Light · clean day',
172
+ ui: { background: '#ffffff', panel: '#f6f8fa', composer: '#f6f8fa', activity: '#eef1f4', elevated: '#e7ebef', modal: '#fafbfc', modalRule: '#d0d7de', line: '#d0d7de', text: '#1f2328', muted: '#656d76', subtle: '#8b949e', accent: '#0969da', success: '#1a7f37', warning: '#9a6700', error: '#cf222e' },
173
+ markdown: { text: '#1f2328', muted: '#656d76', accent: '#0969da', heading: '#0969da', headingStrong: '#1f2328', codeBg: '#f6f8fa', codeText: '#1f2328', codeFence: '#d0d7de', diffAddBg: '#dafbe1', diffAddText: '#116329', diffDelBg: '#ffebe9', diffDelText: '#a40e26' },
174
+ syntax: ['#6e7781', '#0a3069', '#cf222e', '#8250df', '#0550ae'],
175
+ };
176
+ export const THEMES = {
177
+ aurora, midnight, nord, dracula, dawn, solarized, 'rose-pine': rosePine,
178
+ 'tokyo-night': tokyoNight, 'catppuccin-mocha': catppuccinMocha, 'catppuccin-latte': catppuccinLatte,
179
+ 'gruvbox-dark': gruvboxDark, 'one-dark': oneDark, monokai, kanagawa, everforest, synthwave,
180
+ matrix, 'solarized-light': solarizedLight, 'github-light': githubLight,
181
+ };
182
+ export const DEFAULT_THEME = 'aurora';
183
+ export function themeNames() {
184
+ return Object.keys(THEMES);
185
+ }
186
+ export function resolveTheme(name) {
187
+ const key = name?.toLowerCase() ?? '';
188
+ return (key && Object.hasOwn(THEMES, key) ? THEMES[key] : undefined) ?? THEMES[DEFAULT_THEME];
189
+ }
190
+ let activeTheme = THEMES[DEFAULT_THEME];
191
+ export function activeTuiTheme() {
192
+ return activeTheme;
193
+ }
194
+ export function setActiveTheme(name) {
195
+ activeTheme = resolveTheme(name);
196
+ setMarkdownTheme(activeTheme.markdown);
197
+ return activeTheme;
198
+ }