@hraness/dawg 0.0.0-stage → 0.3.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.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +327 -0
- package/LICENSE +21 -0
- package/README.md +213 -2
- package/core/diff.ts +249 -0
- package/core/drums.ts +102 -0
- package/core/key.ts +43 -0
- package/core/loop.ts +78 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +1388 -0
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +45 -4
- package/src/agent/agent.ts +853 -0
- package/src/agent/brief.ts +160 -0
- package/src/agent/gateway.ts +441 -0
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +157 -0
- package/src/agent/planner.ts +259 -0
- package/src/agent/provider.ts +454 -0
- package/src/agent/sse.ts +114 -0
- package/src/agent/tools.ts +1373 -0
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +262 -0
- package/src/agent/xcb.ts +579 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/clock.ts +68 -0
- package/src/audio/engine.ts +841 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/lock.ts +57 -0
- package/src/audio/player.ts +134 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +861 -0
- package/src/auth/cli.ts +231 -0
- package/src/auth/credentials.ts +411 -0
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +1191 -0
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +207 -0
- package/src/auth/tui.ts +107 -0
- package/src/commands/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +69 -0
- package/src/commands/music.ts +461 -0
- package/src/commands/sample.ts +302 -0
- package/src/daemon.ts +31 -0
- package/src/main.ts +2209 -0
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +292 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +121 -0
- package/src/session/attach.ts +181 -0
- package/src/session/client.ts +498 -0
- package/src/session/daemon.ts +740 -0
- package/src/session/delta.ts +249 -0
- package/src/session/list.ts +180 -0
- package/src/session/lock.ts +92 -0
- package/src/session/meta.ts +253 -0
- package/src/session/naming.ts +430 -0
- package/src/session/port.ts +481 -0
- package/src/session/presence.ts +159 -0
- package/src/session/protocol.ts +618 -0
- package/src/session/rebase.ts +168 -0
- package/src/session/store.ts +581 -0
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +364 -0
- package/tui/app.ts +1372 -0
- package/tui/drums.ts +65 -0
- package/tui/highway.ts +921 -0
- package/tui/input.ts +63 -0
- package/tui/keys.ts +102 -0
- package/tui/layers.ts +80 -0
- package/tui/play-strip.ts +143 -0
- package/tui/prompt.ts +609 -0
- package/tui/render.ts +124 -0
- package/tui/screen.ts +247 -0
- package/tui/text.ts +72 -0
- package/tui/theme.ts +451 -0
package/tui/theme.ts
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic theme tokens for the dawg TUI.
|
|
3
|
+
*
|
|
4
|
+
* Components ask for a role ("hit", "promptBg", "error") and never write raw
|
|
5
|
+
* escape colors. A theme maps every role to a `Style`; the capability layer
|
|
6
|
+
* then projects that style onto truecolor, 256-color, 16-color, or attribute-
|
|
7
|
+
* only output. Every musical meaning also has a glyph or attribute cue, so the
|
|
8
|
+
* monochrome projection keeps the same information at the same positions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type ColorDepth = "truecolor" | "ansi256" | "ansi16" | "none";
|
|
12
|
+
|
|
13
|
+
export interface TerminalCapabilities {
|
|
14
|
+
colorDepth: ColorDepth;
|
|
15
|
+
unicode: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether SGR attributes (bold, dim, reverse, underline) may be emitted.
|
|
18
|
+
* `TERM=dumb` disables them; `NO_COLOR` keeps them, as the spec allows.
|
|
19
|
+
*/
|
|
20
|
+
attributes?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ThemeName = "default" | "high-contrast" | "mono";
|
|
24
|
+
export const THEME_NAMES: readonly ThemeName[] = [
|
|
25
|
+
"default",
|
|
26
|
+
"high-contrast",
|
|
27
|
+
"mono",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export type SemanticRole =
|
|
31
|
+
| "canvas"
|
|
32
|
+
| "panel"
|
|
33
|
+
| "border"
|
|
34
|
+
| "borderFocus"
|
|
35
|
+
| "text"
|
|
36
|
+
| "muted"
|
|
37
|
+
| "faint"
|
|
38
|
+
| "cursor"
|
|
39
|
+
| "transport"
|
|
40
|
+
| "paused"
|
|
41
|
+
| "track"
|
|
42
|
+
| "pending"
|
|
43
|
+
| "selected"
|
|
44
|
+
| "hit"
|
|
45
|
+
| "sustain"
|
|
46
|
+
| "ghost"
|
|
47
|
+
| "mutedNote"
|
|
48
|
+
| "beatRule"
|
|
49
|
+
| "barRule"
|
|
50
|
+
| "loopRule"
|
|
51
|
+
| "hitLine"
|
|
52
|
+
| "agent"
|
|
53
|
+
| "success"
|
|
54
|
+
| "warning"
|
|
55
|
+
| "error"
|
|
56
|
+
| "promptBg"
|
|
57
|
+
| "promptText"
|
|
58
|
+
| "pillSteer"
|
|
59
|
+
| "pillQueue";
|
|
60
|
+
|
|
61
|
+
export interface Rgb {
|
|
62
|
+
readonly r: number;
|
|
63
|
+
readonly g: number;
|
|
64
|
+
readonly b: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Style {
|
|
68
|
+
readonly fg?: Rgb | undefined;
|
|
69
|
+
readonly bg?: Rgb | undefined;
|
|
70
|
+
readonly bold?: boolean | undefined;
|
|
71
|
+
readonly dim?: boolean | undefined;
|
|
72
|
+
readonly italic?: boolean | undefined;
|
|
73
|
+
readonly underline?: boolean | undefined;
|
|
74
|
+
readonly reverse?: boolean | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface Theme {
|
|
78
|
+
readonly name: ThemeName;
|
|
79
|
+
readonly roles: Readonly<Record<SemanticRole, Style>>;
|
|
80
|
+
/** Stable per-track accents; empty for attribute-only themes. */
|
|
81
|
+
readonly accents: readonly Rgb[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rgb = (r: number, g: number, b: number): Rgb => ({ r, g, b });
|
|
85
|
+
|
|
86
|
+
const defaultTheme: Theme = {
|
|
87
|
+
name: "default",
|
|
88
|
+
roles: {
|
|
89
|
+
canvas: {},
|
|
90
|
+
panel: { fg: rgb(225, 231, 239), bg: rgb(24, 28, 37) },
|
|
91
|
+
border: { fg: rgb(72, 82, 100) },
|
|
92
|
+
borderFocus: { fg: rgb(122, 140, 178) },
|
|
93
|
+
text: { fg: rgb(225, 231, 239) },
|
|
94
|
+
muted: { fg: rgb(130, 143, 164) },
|
|
95
|
+
faint: { fg: rgb(78, 87, 104) },
|
|
96
|
+
cursor: { reverse: true },
|
|
97
|
+
transport: { fg: rgb(91, 211, 145), bold: true },
|
|
98
|
+
paused: { fg: rgb(255, 193, 87), bold: true },
|
|
99
|
+
track: { fg: rgb(95, 181, 255) },
|
|
100
|
+
pending: { fg: rgb(203, 168, 255), italic: true },
|
|
101
|
+
selected: { fg: rgb(255, 212, 94), reverse: true },
|
|
102
|
+
hit: { fg: rgb(255, 246, 222), bold: true },
|
|
103
|
+
sustain: { fg: rgb(113, 202, 255) },
|
|
104
|
+
ghost: { fg: rgb(86, 96, 116) },
|
|
105
|
+
mutedNote: { fg: rgb(102, 111, 125), dim: true },
|
|
106
|
+
beatRule: { fg: rgb(48, 54, 66) },
|
|
107
|
+
barRule: { fg: rgb(76, 86, 106) },
|
|
108
|
+
loopRule: { fg: rgb(132, 118, 190), bold: true },
|
|
109
|
+
hitLine: { fg: rgb(150, 165, 192) },
|
|
110
|
+
agent: { fg: rgb(255, 150, 219) },
|
|
111
|
+
success: { fg: rgb(113, 220, 148) },
|
|
112
|
+
warning: { fg: rgb(255, 193, 87) },
|
|
113
|
+
error: { fg: rgb(255, 108, 112), bold: true },
|
|
114
|
+
promptBg: { bg: rgb(30, 35, 48) },
|
|
115
|
+
promptText: { fg: rgb(232, 237, 246) },
|
|
116
|
+
pillSteer: { fg: rgb(17, 20, 26), bg: rgb(91, 211, 145), bold: true },
|
|
117
|
+
pillQueue: { fg: rgb(17, 20, 26), bg: rgb(255, 193, 87), bold: true },
|
|
118
|
+
},
|
|
119
|
+
accents: [
|
|
120
|
+
rgb(95, 181, 255),
|
|
121
|
+
rgb(255, 138, 101),
|
|
122
|
+
rgb(126, 224, 129),
|
|
123
|
+
rgb(229, 141, 255),
|
|
124
|
+
rgb(255, 213, 79),
|
|
125
|
+
rgb(77, 222, 222),
|
|
126
|
+
rgb(255, 121, 176),
|
|
127
|
+
rgb(170, 176, 255),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const highContrastTheme: Theme = {
|
|
132
|
+
name: "high-contrast",
|
|
133
|
+
roles: {
|
|
134
|
+
canvas: {},
|
|
135
|
+
panel: { fg: rgb(255, 255, 255), bg: rgb(0, 0, 0) },
|
|
136
|
+
border: { fg: rgb(255, 255, 255) },
|
|
137
|
+
borderFocus: { fg: rgb(255, 255, 0), bold: true },
|
|
138
|
+
text: { fg: rgb(255, 255, 255) },
|
|
139
|
+
muted: { fg: rgb(210, 210, 210) },
|
|
140
|
+
faint: { fg: rgb(170, 170, 170) },
|
|
141
|
+
cursor: { reverse: true },
|
|
142
|
+
transport: { fg: rgb(0, 255, 0), bold: true },
|
|
143
|
+
paused: { fg: rgb(255, 255, 0), bold: true },
|
|
144
|
+
track: { fg: rgb(0, 200, 255), bold: true },
|
|
145
|
+
pending: { fg: rgb(255, 0, 255), underline: true },
|
|
146
|
+
selected: { fg: rgb(255, 255, 0), reverse: true, bold: true },
|
|
147
|
+
hit: { fg: rgb(255, 255, 255), bold: true, reverse: true },
|
|
148
|
+
sustain: { fg: rgb(0, 255, 255), bold: true },
|
|
149
|
+
ghost: { fg: rgb(170, 170, 170) },
|
|
150
|
+
mutedNote: { fg: rgb(150, 150, 150) },
|
|
151
|
+
beatRule: { fg: rgb(120, 120, 120) },
|
|
152
|
+
barRule: { fg: rgb(200, 200, 200) },
|
|
153
|
+
loopRule: { fg: rgb(255, 255, 255), bold: true },
|
|
154
|
+
hitLine: { fg: rgb(255, 255, 255), bold: true },
|
|
155
|
+
agent: { fg: rgb(255, 0, 255), bold: true },
|
|
156
|
+
success: { fg: rgb(0, 255, 0), bold: true },
|
|
157
|
+
warning: { fg: rgb(255, 255, 0), bold: true },
|
|
158
|
+
error: { fg: rgb(255, 60, 60), bold: true, underline: true },
|
|
159
|
+
promptBg: { bg: rgb(0, 0, 0) },
|
|
160
|
+
promptText: { fg: rgb(255, 255, 255), bold: true },
|
|
161
|
+
pillSteer: { fg: rgb(0, 0, 0), bg: rgb(0, 255, 0), bold: true },
|
|
162
|
+
pillQueue: { fg: rgb(0, 0, 0), bg: rgb(255, 255, 0), bold: true },
|
|
163
|
+
},
|
|
164
|
+
accents: [
|
|
165
|
+
rgb(0, 200, 255),
|
|
166
|
+
rgb(255, 140, 0),
|
|
167
|
+
rgb(0, 255, 120),
|
|
168
|
+
rgb(255, 0, 255),
|
|
169
|
+
rgb(255, 255, 0),
|
|
170
|
+
rgb(0, 255, 255),
|
|
171
|
+
rgb(255, 80, 160),
|
|
172
|
+
rgb(160, 160, 255),
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** Attribute-only projection: no hue carries meaning. */
|
|
177
|
+
const monoTheme: Theme = {
|
|
178
|
+
name: "mono",
|
|
179
|
+
roles: {
|
|
180
|
+
canvas: {},
|
|
181
|
+
panel: {},
|
|
182
|
+
border: {},
|
|
183
|
+
borderFocus: { bold: true },
|
|
184
|
+
text: {},
|
|
185
|
+
muted: { dim: true },
|
|
186
|
+
faint: { dim: true },
|
|
187
|
+
cursor: { reverse: true },
|
|
188
|
+
transport: { bold: true },
|
|
189
|
+
paused: { bold: true },
|
|
190
|
+
track: {},
|
|
191
|
+
pending: { underline: true },
|
|
192
|
+
selected: { reverse: true },
|
|
193
|
+
hit: { bold: true, reverse: true },
|
|
194
|
+
sustain: { bold: true },
|
|
195
|
+
ghost: { dim: true },
|
|
196
|
+
mutedNote: { dim: true },
|
|
197
|
+
beatRule: { dim: true },
|
|
198
|
+
barRule: {},
|
|
199
|
+
loopRule: { bold: true },
|
|
200
|
+
hitLine: { bold: true },
|
|
201
|
+
agent: { italic: true },
|
|
202
|
+
success: { bold: true },
|
|
203
|
+
warning: { bold: true },
|
|
204
|
+
error: { bold: true, underline: true },
|
|
205
|
+
promptBg: {},
|
|
206
|
+
promptText: {},
|
|
207
|
+
pillSteer: { reverse: true, bold: true },
|
|
208
|
+
pillQueue: { reverse: true, bold: true, underline: true },
|
|
209
|
+
},
|
|
210
|
+
accents: [],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const THEMES: Record<ThemeName, Theme> = {
|
|
214
|
+
default: defaultTheme,
|
|
215
|
+
"high-contrast": highContrastTheme,
|
|
216
|
+
mono: monoTheme,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export function getTheme(name: ThemeName): Theme {
|
|
220
|
+
return THEMES[name];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function parseThemeName(value: unknown): ThemeName | undefined {
|
|
224
|
+
if (typeof value !== "string") return undefined;
|
|
225
|
+
const normalized = value.trim().toLowerCase();
|
|
226
|
+
if (normalized === "monochrome") return "mono";
|
|
227
|
+
if (normalized === "contrast" || normalized === "hc") return "high-contrast";
|
|
228
|
+
return (THEME_NAMES as readonly string[]).includes(normalized)
|
|
229
|
+
? (normalized as ThemeName)
|
|
230
|
+
: undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
type Env = Record<string, string | undefined>;
|
|
234
|
+
|
|
235
|
+
function processEnv(): Env {
|
|
236
|
+
const runtime = globalThis as typeof globalThis & {
|
|
237
|
+
process?: { env?: Env };
|
|
238
|
+
};
|
|
239
|
+
return runtime.process?.env ?? {};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function detectTerminalCapabilities(
|
|
243
|
+
env: Env = processEnv(),
|
|
244
|
+
): TerminalCapabilities {
|
|
245
|
+
const dumb = env.TERM === "dumb";
|
|
246
|
+
const noColor = (env.NO_COLOR !== undefined && env.NO_COLOR !== "") || dumb;
|
|
247
|
+
const colorTerm = (env.COLORTERM ?? "").toLowerCase();
|
|
248
|
+
let colorDepth: ColorDepth = "ansi16";
|
|
249
|
+
if (noColor) colorDepth = "none";
|
|
250
|
+
else if (colorTerm.includes("truecolor") || colorTerm.includes("24bit"))
|
|
251
|
+
colorDepth = "truecolor";
|
|
252
|
+
else if ((env.TERM ?? "").includes("256color")) colorDepth = "ansi256";
|
|
253
|
+
const capabilities: TerminalCapabilities = { colorDepth, unicode: !dumb };
|
|
254
|
+
if (dumb) capabilities.attributes = false;
|
|
255
|
+
return capabilities;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function mix(from: Rgb, to: Rgb, amount: number): Rgb {
|
|
259
|
+
const t = Math.max(0, Math.min(1, amount));
|
|
260
|
+
return {
|
|
261
|
+
r: Math.round(from.r + (to.r - from.r) * t),
|
|
262
|
+
g: Math.round(from.g + (to.g - from.g) * t),
|
|
263
|
+
b: Math.round(from.b + (to.b - from.b) * t),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const WHITE = rgb(255, 255, 255);
|
|
268
|
+
const DARK = rgb(34, 38, 48);
|
|
269
|
+
|
|
270
|
+
/** Brighten (amount > 0) or dim (amount < 0) a style's foreground. */
|
|
271
|
+
export function shade(style: Style, amount: number): Style {
|
|
272
|
+
if (!style.fg || amount === 0) return style;
|
|
273
|
+
return {
|
|
274
|
+
...style,
|
|
275
|
+
fg:
|
|
276
|
+
amount > 0 ? mix(style.fg, WHITE, amount) : mix(style.fg, DARK, -amount),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function hashId(value: string): number {
|
|
281
|
+
let hash = 0x811c9dc5;
|
|
282
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
283
|
+
hash ^= value.charCodeAt(index);
|
|
284
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
285
|
+
}
|
|
286
|
+
return hash;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Stable accent for a track ID; falls back to the theme's track role. */
|
|
290
|
+
export function accentStyle(theme: Theme, trackId: string | undefined): Style {
|
|
291
|
+
const base = theme.roles.track;
|
|
292
|
+
if (theme.accents.length === 0 || !trackId) return base;
|
|
293
|
+
const accent = theme.accents[hashId(trackId) % theme.accents.length]!;
|
|
294
|
+
return { ...base, fg: accent };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Velocity maps to saturation in color themes (and glyph density elsewhere). */
|
|
298
|
+
export function velocityStyle(accent: Style, velocity: number): Style {
|
|
299
|
+
if (!accent.fg) return velocity < 0.35 ? { ...accent, dim: true } : accent;
|
|
300
|
+
const v = Math.max(0, Math.min(1, velocity));
|
|
301
|
+
return { ...accent, fg: mix(DARK, accent.fg, 0.4 + 0.6 * v) };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Overlay a background (for panels) without losing the role's attributes. */
|
|
305
|
+
export function onBackground(style: Style, background: Style): Style {
|
|
306
|
+
return background.bg && !style.bg ? { ...style, bg: background.bg } : style;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Capability projection
|
|
311
|
+
|
|
312
|
+
const ANSI16: ReadonlyArray<Rgb> = [
|
|
313
|
+
rgb(0, 0, 0),
|
|
314
|
+
rgb(205, 49, 49),
|
|
315
|
+
rgb(13, 188, 121),
|
|
316
|
+
rgb(229, 229, 16),
|
|
317
|
+
rgb(36, 114, 200),
|
|
318
|
+
rgb(188, 63, 188),
|
|
319
|
+
rgb(17, 168, 205),
|
|
320
|
+
rgb(229, 229, 229),
|
|
321
|
+
rgb(102, 102, 102),
|
|
322
|
+
rgb(241, 76, 76),
|
|
323
|
+
rgb(35, 209, 139),
|
|
324
|
+
rgb(245, 245, 67),
|
|
325
|
+
rgb(59, 142, 234),
|
|
326
|
+
rgb(214, 112, 214),
|
|
327
|
+
rgb(41, 184, 219),
|
|
328
|
+
rgb(255, 255, 255),
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
function nearest16(color: Rgb): number {
|
|
332
|
+
let best = 0;
|
|
333
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
334
|
+
for (let index = 0; index < ANSI16.length; index += 1) {
|
|
335
|
+
const candidate = ANSI16[index]!;
|
|
336
|
+
const distance =
|
|
337
|
+
(candidate.r - color.r) ** 2 * 0.3 +
|
|
338
|
+
(candidate.g - color.g) ** 2 * 0.59 +
|
|
339
|
+
(candidate.b - color.b) ** 2 * 0.11;
|
|
340
|
+
if (distance < bestDistance) {
|
|
341
|
+
bestDistance = distance;
|
|
342
|
+
best = index;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return best;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const CUBE = [0, 95, 135, 175, 215, 255];
|
|
349
|
+
function cubeIndex(value: number): number {
|
|
350
|
+
let best = 0;
|
|
351
|
+
for (let index = 1; index < CUBE.length; index += 1)
|
|
352
|
+
if (Math.abs(CUBE[index]! - value) < Math.abs(CUBE[best]! - value))
|
|
353
|
+
best = index;
|
|
354
|
+
return best;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function toAnsi256(color: Rgb): number {
|
|
358
|
+
const gray = Math.round((color.r + color.g + color.b) / 3);
|
|
359
|
+
const r = cubeIndex(color.r);
|
|
360
|
+
const g = cubeIndex(color.g);
|
|
361
|
+
const b = cubeIndex(color.b);
|
|
362
|
+
const cube = 16 + 36 * r + 6 * g + b;
|
|
363
|
+
const cubeColor = rgb(CUBE[r]!, CUBE[g]!, CUBE[b]!);
|
|
364
|
+
const grayIndex = Math.max(0, Math.min(23, Math.round((gray - 8) / 10)));
|
|
365
|
+
const grayValue = 8 + grayIndex * 10;
|
|
366
|
+
const cubeError =
|
|
367
|
+
(cubeColor.r - color.r) ** 2 +
|
|
368
|
+
(cubeColor.g - color.g) ** 2 +
|
|
369
|
+
(cubeColor.b - color.b) ** 2;
|
|
370
|
+
const grayError =
|
|
371
|
+
(grayValue - color.r) ** 2 +
|
|
372
|
+
(grayValue - color.g) ** 2 +
|
|
373
|
+
(grayValue - color.b) ** 2;
|
|
374
|
+
return grayError < cubeError ? 232 + grayIndex : cube;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function colorCode(
|
|
378
|
+
color: Rgb,
|
|
379
|
+
depth: ColorDepth,
|
|
380
|
+
background: boolean,
|
|
381
|
+
): string | undefined {
|
|
382
|
+
if (depth === "none") return undefined;
|
|
383
|
+
if (depth === "truecolor")
|
|
384
|
+
return `${background ? 48 : 38};2;${color.r};${color.g};${color.b}`;
|
|
385
|
+
if (depth === "ansi256")
|
|
386
|
+
return `${background ? 48 : 38};5;${toAnsi256(color)}`;
|
|
387
|
+
const index = nearest16(color);
|
|
388
|
+
const base = index < 8 ? (background ? 40 : 30) : background ? 100 : 90;
|
|
389
|
+
return String(base + (index % 8));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const sgrCache = new WeakMap<Style, Map<string, string>>();
|
|
393
|
+
|
|
394
|
+
/** Full SGR sequence (starting from a reset) for a style at a capability. */
|
|
395
|
+
export function styleSgr(
|
|
396
|
+
style: Style | undefined,
|
|
397
|
+
capabilities: TerminalCapabilities,
|
|
398
|
+
): string {
|
|
399
|
+
if (capabilities.attributes === false) return "";
|
|
400
|
+
if (!style) return "\u001b[0m";
|
|
401
|
+
const key = `${capabilities.colorDepth}`;
|
|
402
|
+
let byDepth = sgrCache.get(style);
|
|
403
|
+
const cached = byDepth?.get(key);
|
|
404
|
+
if (cached !== undefined) return cached;
|
|
405
|
+
const codes = ["0"];
|
|
406
|
+
if (style.bold) codes.push("1");
|
|
407
|
+
if (style.dim) codes.push("2");
|
|
408
|
+
if (style.italic) codes.push("3");
|
|
409
|
+
if (style.underline) codes.push("4");
|
|
410
|
+
if (style.reverse) codes.push("7");
|
|
411
|
+
if (style.fg) {
|
|
412
|
+
const code = colorCode(style.fg, capabilities.colorDepth, false);
|
|
413
|
+
if (code) codes.push(code);
|
|
414
|
+
}
|
|
415
|
+
if (style.bg) {
|
|
416
|
+
const code = colorCode(style.bg, capabilities.colorDepth, true);
|
|
417
|
+
if (code) codes.push(code);
|
|
418
|
+
}
|
|
419
|
+
const sequence = `\u001b[${codes.join(";")}m`;
|
|
420
|
+
if (!byDepth) {
|
|
421
|
+
byDepth = new Map();
|
|
422
|
+
sgrCache.set(style, byDepth);
|
|
423
|
+
}
|
|
424
|
+
byDepth.set(key, sequence);
|
|
425
|
+
return sequence;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Resolve the effective theme: no-color terminals always use attributes only. */
|
|
429
|
+
export function effectiveTheme(
|
|
430
|
+
name: ThemeName,
|
|
431
|
+
capabilities: TerminalCapabilities,
|
|
432
|
+
): Theme {
|
|
433
|
+
return capabilities.colorDepth === "none" ? monoTheme : getTheme(name);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Apply a named role to a string (foreground only), retaining a clean,
|
|
438
|
+
* testable no-color mode. Used by simple, non-buffered output paths.
|
|
439
|
+
*/
|
|
440
|
+
export function semanticColor(
|
|
441
|
+
role: SemanticRole,
|
|
442
|
+
value: string,
|
|
443
|
+
capabilities: TerminalCapabilities = detectTerminalCapabilities(),
|
|
444
|
+
theme: Theme = defaultTheme,
|
|
445
|
+
): string {
|
|
446
|
+
if (capabilities.colorDepth === "none" || value.length === 0) return value;
|
|
447
|
+
const fg = theme.roles[role].fg ?? theme.roles[role].bg;
|
|
448
|
+
if (!fg) return value;
|
|
449
|
+
const code = colorCode(fg, capabilities.colorDepth, false);
|
|
450
|
+
return code ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
451
|
+
}
|