@alchemy.run/sigil 0.0.0-alpha.4 → 0.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/THIRD_PARTY_NOTICES.md +39 -1
- package/dist/Text-BobFKi74.d.ts +452 -0
- package/dist/ansi.d.ts +122 -131
- package/dist/ansi.js +86 -6
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-DZ88EXJv.d.ts +21 -0
- package/dist/index.d.ts +143 -498
- package/dist/index.js +1026 -2244
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOSLQ8fF.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-aZr9O8h3.js +665 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-BzqAJi0n.js +1337 -0
- package/package.json +41 -9
- package/src/ansi/chalk.ts +5 -3
- package/src/ansi/escapes.ts +14 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +3 -1
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/tokenize.ts +3 -4
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +98 -10
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +0 -8
- package/src/components/CursorContext.ts +1 -1
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +22 -45
- package/src/components/Transform.tsx +1 -1
- package/src/dom.ts +11 -2
- package/src/global.d.ts +3 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +2 -2
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/index.ts +45 -1
- package/src/ink.tsx +213 -153
- package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
- package/src/reconciler.ts +24 -3
- package/src/render-background.ts +36 -15
- package/src/render-border.ts +94 -61
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +21 -6
- package/src/render.ts +15 -6
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/squash-text-nodes.ts +2 -5
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +19 -14
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/wrap-text.ts +4 -0
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-Cr6xVFMa.js +0 -2330
- package/src/ansi/supports-color.ts +0 -207
- package/src/colorize.ts +0 -60
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rgb,
|
|
3
|
+
type GradientStop,
|
|
4
|
+
type InterpolationSpace,
|
|
5
|
+
type LinearGradient,
|
|
6
|
+
type Paint,
|
|
7
|
+
type PaintContext,
|
|
8
|
+
type PerimeterGradient,
|
|
9
|
+
} from "#/color/paint.ts";
|
|
10
|
+
import { colorToRgb } from "#/color/palette.ts";
|
|
11
|
+
import type { Color, RgbColor } from "#/screen/cell.ts";
|
|
12
|
+
import type { Rect } from "#/screen/geometry.ts";
|
|
13
|
+
import { parseSemanticColor } from "#/semantic-text-style.ts";
|
|
14
|
+
|
|
15
|
+
export function samplePaint(
|
|
16
|
+
paint: Paint,
|
|
17
|
+
x: number,
|
|
18
|
+
y: number,
|
|
19
|
+
bounds: Rect,
|
|
20
|
+
context: PaintContext = {},
|
|
21
|
+
): Color | undefined {
|
|
22
|
+
const resolved = resolveConditional(paint, context);
|
|
23
|
+
if (resolved && typeof resolved === "object" && "type" in resolved) {
|
|
24
|
+
if (resolved.type === "perimeter-gradient") {
|
|
25
|
+
return samplePerimeterGradient(resolved, x, y, bounds, context);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (isLinearGradient(resolved)) {
|
|
29
|
+
return sampleGradient(resolved, x, y, bounds, context);
|
|
30
|
+
}
|
|
31
|
+
return resolveColor(resolved);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function blend(source: Color, destination: Color, palette?: PaintContext["palette"]): Color {
|
|
35
|
+
const foreground = colorToRgb(source, palette);
|
|
36
|
+
if (foreground.alpha >= 255) return foreground;
|
|
37
|
+
const background = colorToRgb(destination, palette);
|
|
38
|
+
const alpha = foreground.alpha / 255;
|
|
39
|
+
return rgb(
|
|
40
|
+
foreground.red * alpha + background.red * (1 - alpha),
|
|
41
|
+
foreground.green * alpha + background.green * (1 - alpha),
|
|
42
|
+
foreground.blue * alpha + background.blue * (1 - alpha),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Progressively moves a color toward white in perceptual OKLab space. */
|
|
47
|
+
export function lighten(color: Color, amount: number, palette?: PaintContext["palette"]): RgbColor {
|
|
48
|
+
return interpolateColor(color, rgb(255, 255, 255), amount, "oklab", palette);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Progressively moves a color toward black in perceptual OKLab space. */
|
|
52
|
+
export function darken(color: Color, amount: number, palette?: PaintContext["palette"]): RgbColor {
|
|
53
|
+
return interpolateColor(color, rgb(0, 0, 0), amount, "oklab", palette);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sampleGradient(
|
|
57
|
+
gradient: LinearGradient,
|
|
58
|
+
x: number,
|
|
59
|
+
y: number,
|
|
60
|
+
bounds: Rect,
|
|
61
|
+
context: PaintContext,
|
|
62
|
+
): Color | undefined {
|
|
63
|
+
const radians = (gradient.angle * Math.PI) / 180;
|
|
64
|
+
const dx = Math.cos(radians);
|
|
65
|
+
const dy = Math.sin(radians);
|
|
66
|
+
const width = Math.max(1, bounds.width - 1);
|
|
67
|
+
const height = Math.max(1, bounds.height - 1);
|
|
68
|
+
const projections = [0, width * dx, height * dy, width * dx + height * dy];
|
|
69
|
+
const minimum = Math.min(...projections);
|
|
70
|
+
const maximum = Math.max(...projections);
|
|
71
|
+
const projection = (x - bounds.x) * dx + (y - bounds.y) * dy;
|
|
72
|
+
const position = maximum === minimum ? 0 : (projection - minimum) / (maximum - minimum);
|
|
73
|
+
return sampleStops(gradient.stops, position, gradient.space, x, y, bounds, context);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function samplePerimeterGradient(
|
|
77
|
+
gradient: PerimeterGradient,
|
|
78
|
+
x: number,
|
|
79
|
+
y: number,
|
|
80
|
+
bounds: Rect,
|
|
81
|
+
context: PaintContext,
|
|
82
|
+
): Color | undefined {
|
|
83
|
+
const width = Math.max(1, bounds.width);
|
|
84
|
+
const height = Math.max(1, bounds.height);
|
|
85
|
+
const steps = width === 1 || height === 1 ? Math.max(width, height) : 2 * width + 2 * height - 4;
|
|
86
|
+
if (steps <= 1) return samplePaint(gradient.stops[0]!.color, x, y, bounds, context);
|
|
87
|
+
|
|
88
|
+
const left = bounds.x;
|
|
89
|
+
const top = bounds.y;
|
|
90
|
+
const right = left + width - 1;
|
|
91
|
+
const bottom = top + height - 1;
|
|
92
|
+
let index: number;
|
|
93
|
+
|
|
94
|
+
if (height === 1) index = x - left;
|
|
95
|
+
else if (width === 1) index = y - top;
|
|
96
|
+
else if (y <= top) index = Math.max(0, Math.min(width - 1, x - left));
|
|
97
|
+
else if (x >= right) index = width + Math.max(0, Math.min(height - 2, y - top - 1));
|
|
98
|
+
else if (y >= bottom) index = width + height - 2 + Math.max(0, Math.min(width - 1, right - x));
|
|
99
|
+
else index = 2 * width + height - 2 + Math.max(0, Math.min(height - 2, bottom - y - 1));
|
|
100
|
+
|
|
101
|
+
index = modulo(index - gradient.offset, steps);
|
|
102
|
+
const position = blendPosition(index, steps, gradient.stops.length);
|
|
103
|
+
return sampleStops(gradient.stops, position, gradient.space, x, y, bounds, context);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Matches Lip Gloss Blend1D's even, discrete distribution of stops. */
|
|
107
|
+
function blendPosition(index: number, steps: number, stopCount: number): number {
|
|
108
|
+
if (steps <= stopCount) return index / Math.max(1, stopCount - 1);
|
|
109
|
+
const segments = stopCount - 1;
|
|
110
|
+
const baseSize = Math.floor(steps / segments);
|
|
111
|
+
const remainder = steps % segments;
|
|
112
|
+
let start = 0;
|
|
113
|
+
for (let segment = 0; segment < segments; segment++) {
|
|
114
|
+
const size = baseSize + (segment < remainder ? 1 : 0);
|
|
115
|
+
if (index < start + size) {
|
|
116
|
+
const local = size <= 1 ? 0 : (index - start) / (size - 1);
|
|
117
|
+
return (segment + local) / segments;
|
|
118
|
+
}
|
|
119
|
+
start += size;
|
|
120
|
+
}
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sampleStops(
|
|
125
|
+
stops: readonly GradientStop[],
|
|
126
|
+
position: number,
|
|
127
|
+
space: InterpolationSpace,
|
|
128
|
+
x: number,
|
|
129
|
+
y: number,
|
|
130
|
+
bounds: Rect,
|
|
131
|
+
context: PaintContext,
|
|
132
|
+
): Color | undefined {
|
|
133
|
+
let right = 1;
|
|
134
|
+
while (right < stops.length - 1 && position > stops[right]!.offset!) right++;
|
|
135
|
+
const left = right - 1;
|
|
136
|
+
const start = stops[left]!;
|
|
137
|
+
const end = stops[right]!;
|
|
138
|
+
const span = end.offset! - start.offset!;
|
|
139
|
+
const amount = span === 0 ? 0 : Math.max(0, Math.min(1, (position - start.offset!) / span));
|
|
140
|
+
const startColor = samplePaint(start.color, x, y, bounds, context);
|
|
141
|
+
const endColor = samplePaint(end.color, x, y, bounds, context);
|
|
142
|
+
if (!startColor || !endColor) return startColor ?? endColor;
|
|
143
|
+
return interpolateColor(startColor, endColor, amount, space, context.palette);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function interpolateColor(
|
|
147
|
+
from: Color,
|
|
148
|
+
to: Color,
|
|
149
|
+
amount: number,
|
|
150
|
+
space: InterpolationSpace = "oklab",
|
|
151
|
+
palette?: PaintContext["palette"],
|
|
152
|
+
): RgbColor {
|
|
153
|
+
const a = colorToRgb(from, palette);
|
|
154
|
+
const b = colorToRgb(to, palette);
|
|
155
|
+
const t = Math.max(0, Math.min(1, amount));
|
|
156
|
+
if (space === "rgb")
|
|
157
|
+
return rgb(
|
|
158
|
+
mix(a.red, b.red, t),
|
|
159
|
+
mix(a.green, b.green, t),
|
|
160
|
+
mix(a.blue, b.blue, t),
|
|
161
|
+
mix(a.alpha, b.alpha, t),
|
|
162
|
+
);
|
|
163
|
+
if (space === "hsv") {
|
|
164
|
+
const ah = toHsv(a),
|
|
165
|
+
bh = toHsv(b);
|
|
166
|
+
let delta = bh[0] - ah[0];
|
|
167
|
+
if (delta > 180) delta -= 360;
|
|
168
|
+
if (delta < -180) delta += 360;
|
|
169
|
+
return fromHsv(
|
|
170
|
+
ah[0] + delta * t,
|
|
171
|
+
mix(ah[1], bh[1], t),
|
|
172
|
+
mix(ah[2], bh[2], t),
|
|
173
|
+
mix(a.alpha, b.alpha, t),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (space === "lab") {
|
|
177
|
+
const al = toLab(a),
|
|
178
|
+
bl = toLab(b);
|
|
179
|
+
return fromLab(
|
|
180
|
+
mix(al[0], bl[0], t),
|
|
181
|
+
mix(al[1], bl[1], t),
|
|
182
|
+
mix(al[2], bl[2], t),
|
|
183
|
+
mix(a.alpha, b.alpha, t),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const al = toOklab(a),
|
|
187
|
+
bl = toOklab(b);
|
|
188
|
+
return fromOklab(
|
|
189
|
+
mix(al[0], bl[0], t),
|
|
190
|
+
mix(al[1], bl[1], t),
|
|
191
|
+
mix(al[2], bl[2], t),
|
|
192
|
+
mix(a.alpha, b.alpha, t),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const D65 = [0.95047, 1, 1.08883] as const;
|
|
197
|
+
function labCurve(value: number): number {
|
|
198
|
+
return value > (6 / 29) ** 3 ? Math.cbrt(value) : (value / 3) * (29 / 6) ** 2 + 4 / 29;
|
|
199
|
+
}
|
|
200
|
+
function inverseLabCurve(value: number): number {
|
|
201
|
+
return value > 6 / 29 ? value ** 3 : 3 * (6 / 29) ** 2 * (value - 4 / 29);
|
|
202
|
+
}
|
|
203
|
+
function toLab(color: RgbColor): [number, number, number] {
|
|
204
|
+
const r = linear(color.red),
|
|
205
|
+
g = linear(color.green),
|
|
206
|
+
b = linear(color.blue);
|
|
207
|
+
const x = 0.4123907992659595 * r + 0.357584339383878 * g + 0.1804807884018343 * b;
|
|
208
|
+
const y = 0.2126390058715104 * r + 0.7151686787677559 * g + 0.0721923153607337 * b;
|
|
209
|
+
const z = 0.0193308187155919 * r + 0.119194779794626 * g + 0.9505321522496606 * b;
|
|
210
|
+
const fy = labCurve(y / D65[1]);
|
|
211
|
+
return [1.16 * fy - 0.16, 5 * (labCurve(x / D65[0]) - fy), 2 * (fy - labCurve(z / D65[2]))];
|
|
212
|
+
}
|
|
213
|
+
function fromLab(l: number, a: number, b: number, alpha: number): RgbColor {
|
|
214
|
+
const center = (l + 0.16) / 1.16;
|
|
215
|
+
const x = D65[0] * inverseLabCurve(center + a / 5);
|
|
216
|
+
const y = D65[1] * inverseLabCurve(center);
|
|
217
|
+
const z = D65[2] * inverseLabCurve(center - b / 2);
|
|
218
|
+
return rgb(
|
|
219
|
+
colorfulChannel(3.240969941904521 * x - 1.5373831775700935 * y - 0.4986107602930033 * z),
|
|
220
|
+
colorfulChannel(-0.9692436362808798 * x + 1.8759675015077206 * y + 0.0415550574071756 * z),
|
|
221
|
+
colorfulChannel(0.0556300796969936 * x - 0.2039769588889766 * y + 1.0569715142428786 * z),
|
|
222
|
+
alpha,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const mix = (a: number, b: number, t: number) => a + (b - a) * t;
|
|
227
|
+
const linear = (value: number) => {
|
|
228
|
+
const x = value / 255;
|
|
229
|
+
return x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
|
230
|
+
};
|
|
231
|
+
const gamma = (value: number) =>
|
|
232
|
+
255 * (value <= 0.0031308 ? 12.92 * value : 1.055 * value ** (1 / 2.4) - 0.055);
|
|
233
|
+
const colorfulChannel = (value: number) => {
|
|
234
|
+
const encoded = value <= 0.0031308 ? 12.92 * value : 1.055 * value ** (1 / 2.4) - 0.055;
|
|
235
|
+
return Math.floor(Math.round(Math.max(0, Math.min(1, encoded)) * 65_535) / 256);
|
|
236
|
+
};
|
|
237
|
+
function toOklab(c: RgbColor): [number, number, number] {
|
|
238
|
+
const r = linear(c.red),
|
|
239
|
+
g = linear(c.green),
|
|
240
|
+
b = linear(c.blue);
|
|
241
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b),
|
|
242
|
+
m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b),
|
|
243
|
+
s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
244
|
+
return [
|
|
245
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
246
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
247
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
function fromOklab(L: number, a: number, b: number, alpha: number): RgbColor {
|
|
251
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3,
|
|
252
|
+
m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3,
|
|
253
|
+
s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
254
|
+
return rgb(
|
|
255
|
+
gamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
256
|
+
gamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
257
|
+
gamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
|
258
|
+
alpha,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
function toHsv(c: RgbColor): [number, number, number] {
|
|
262
|
+
const r = c.red / 255,
|
|
263
|
+
g = c.green / 255,
|
|
264
|
+
b = c.blue / 255,
|
|
265
|
+
max = Math.max(r, g, b),
|
|
266
|
+
min = Math.min(r, g, b),
|
|
267
|
+
d = max - min;
|
|
268
|
+
let h = 0;
|
|
269
|
+
if (d)
|
|
270
|
+
h =
|
|
271
|
+
max === r
|
|
272
|
+
? 60 * (((g - b) / d) % 6)
|
|
273
|
+
: max === g
|
|
274
|
+
? 60 * ((b - r) / d + 2)
|
|
275
|
+
: 60 * ((r - g) / d + 4);
|
|
276
|
+
return [(h + 360) % 360, max === 0 ? 0 : d / max, max];
|
|
277
|
+
}
|
|
278
|
+
function fromHsv(h: number, s: number, v: number, alpha: number): RgbColor {
|
|
279
|
+
const c = v * s,
|
|
280
|
+
x = c * (1 - Math.abs(((h / 60) % 2) - 1)),
|
|
281
|
+
m = v - c;
|
|
282
|
+
const [r, g, b] =
|
|
283
|
+
h < 60
|
|
284
|
+
? [c, x, 0]
|
|
285
|
+
: h < 120
|
|
286
|
+
? [x, c, 0]
|
|
287
|
+
: h < 180
|
|
288
|
+
? [0, c, x]
|
|
289
|
+
: h < 240
|
|
290
|
+
? [0, x, c]
|
|
291
|
+
: h < 300
|
|
292
|
+
? [x, 0, c]
|
|
293
|
+
: [c, 0, x];
|
|
294
|
+
return rgb((r + m) * 255, (g + m) * 255, (b + m) * 255, alpha);
|
|
295
|
+
}
|
|
296
|
+
function resolveConditional(paint: Paint, context: PaintContext): Paint {
|
|
297
|
+
if (typeof paint !== "object" || paint === null || "model" in paint) return paint;
|
|
298
|
+
if (paint.type === "adaptive-color")
|
|
299
|
+
return resolveConditional(context.appearance === "light" ? paint.light : paint.dark, context);
|
|
300
|
+
if (paint.type === "profile-color")
|
|
301
|
+
return resolveConditional(
|
|
302
|
+
(context.profile && paint.colors[context.profile]) ?? paint.fallback,
|
|
303
|
+
context,
|
|
304
|
+
);
|
|
305
|
+
return paint;
|
|
306
|
+
}
|
|
307
|
+
function resolveColor(value: Paint): Color | undefined {
|
|
308
|
+
return typeof value === "string"
|
|
309
|
+
? parseSemanticColor(value)
|
|
310
|
+
: typeof value === "object" && value !== null && "model" in value
|
|
311
|
+
? value
|
|
312
|
+
: undefined;
|
|
313
|
+
}
|
|
314
|
+
function isLinearGradient(value: Paint): value is LinearGradient {
|
|
315
|
+
return (
|
|
316
|
+
typeof value === "object" &&
|
|
317
|
+
value !== null &&
|
|
318
|
+
"type" in value &&
|
|
319
|
+
value.type === "linear-gradient"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const modulo = (value: number, divisor: number) => ((value % divisor) + divisor) % divisor;
|
package/src/color.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "#/color/index.ts";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
|
|
4
|
+
import { stripAnsi } from "#/ansi/strip.ts";
|
|
5
|
+
import { accessibilityContext } from "#/components/AccessibilityContext.ts";
|
|
6
|
+
import { Text } from "#/components/Text.tsx";
|
|
7
|
+
import type { Styles } from "#/styles.ts";
|
|
8
|
+
|
|
9
|
+
export type Props = {
|
|
10
|
+
/** External text containing ANSI SGR styling or OSC 8 hyperlinks. */
|
|
11
|
+
readonly children: string;
|
|
12
|
+
readonly wrap?: Styles["textWrap"];
|
|
13
|
+
readonly "aria-label"?: string;
|
|
14
|
+
readonly "aria-hidden"?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Renders explicitly trusted ANSI-styled output as structured terminal cells.
|
|
19
|
+
* Ordinary `Text` continues to strip terminal control sequences.
|
|
20
|
+
*/
|
|
21
|
+
export function AnsiText({
|
|
22
|
+
children,
|
|
23
|
+
wrap = "wrap",
|
|
24
|
+
"aria-label": ariaLabel,
|
|
25
|
+
"aria-hidden": ariaHidden = false,
|
|
26
|
+
}: Props) {
|
|
27
|
+
const { isScreenReaderEnabled } = useContext(accessibilityContext);
|
|
28
|
+
|
|
29
|
+
if (isScreenReaderEnabled) {
|
|
30
|
+
if (ariaHidden) return null;
|
|
31
|
+
return <Text>{ariaLabel ?? stripAnsi(children)}</Text>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ink-text
|
|
36
|
+
style={{ flexGrow: 0, flexShrink: 1, flexDirection: "row", textWrap: wrap }}
|
|
37
|
+
internal_ansi
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</ink-text>
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/components/App.tsx
CHANGED
|
@@ -13,6 +13,13 @@ import React, {
|
|
|
13
13
|
|
|
14
14
|
import { cliCursor } from "#/ansi/cursor.ts";
|
|
15
15
|
import { ansiEscapes, CSI, ESC } from "#/ansi/escapes.ts";
|
|
16
|
+
import {
|
|
17
|
+
ensureTerminalQuery,
|
|
18
|
+
getTerminalQueryPromise,
|
|
19
|
+
refreshTerminalQuery,
|
|
20
|
+
type TerminalQueryResult,
|
|
21
|
+
} from "#/capabilities/query.ts";
|
|
22
|
+
import { registerTerminalIntegration } from "#/capabilities/store.ts";
|
|
16
23
|
import { animationContext as AnimationContext } from "#/components/AnimationContext.ts";
|
|
17
24
|
import { AppContext, type SuspendTerminal } from "#/components/AppContext.ts";
|
|
18
25
|
import { CursorContext } from "#/components/CursorContext.ts";
|
|
@@ -21,9 +28,9 @@ import { FocusContext } from "#/components/FocusContext.ts";
|
|
|
21
28
|
import { StderrContext } from "#/components/StderrContext.ts";
|
|
22
29
|
import { StdinContext } from "#/components/StdinContext.ts";
|
|
23
30
|
import { StdoutContext } from "#/components/StdoutContext.ts";
|
|
24
|
-
import {
|
|
25
|
-
import { type CursorPosition } from "#/log-update.ts";
|
|
31
|
+
import { type CursorPosition } from "#/cursor-position.ts";
|
|
26
32
|
import { getRawModeStream, type OutputStream } from "#/stream.ts";
|
|
33
|
+
import type { TerminalInput } from "#/terminal/input.ts";
|
|
27
34
|
|
|
28
35
|
const tab = "\t";
|
|
29
36
|
const shiftTab = `${CSI}Z`;
|
|
@@ -51,6 +58,7 @@ type Props = {
|
|
|
51
58
|
readonly setCursorPosition: (position: CursorPosition | undefined) => void;
|
|
52
59
|
readonly interactive: boolean;
|
|
53
60
|
readonly renderThrottleMs: number;
|
|
61
|
+
readonly terminalInput: TerminalInput;
|
|
54
62
|
};
|
|
55
63
|
|
|
56
64
|
type Focusable = {
|
|
@@ -76,6 +84,7 @@ export function App({
|
|
|
76
84
|
setCursorPosition,
|
|
77
85
|
interactive,
|
|
78
86
|
renderThrottleMs,
|
|
87
|
+
terminalInput,
|
|
79
88
|
}: Props): React.ReactNode {
|
|
80
89
|
const [isFocusEnabled, setIsFocusEnabled] = useState(true);
|
|
81
90
|
const [activeFocusId, setActiveFocusId] = useState<string | undefined>(undefined);
|
|
@@ -100,7 +109,6 @@ export function App({
|
|
|
100
109
|
internal_eventEmitter.current.setMaxListeners(Infinity);
|
|
101
110
|
// Store the currently attached readable listener to avoid stale closure issues
|
|
102
111
|
const readableListenerRef = useRef<(() => void) | undefined>(undefined);
|
|
103
|
-
const inputParserRef = useRef(createInputParser());
|
|
104
112
|
const pendingInputFlushRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
105
113
|
// Small delay to let chunked escape sequences complete before flushing as literal input.
|
|
106
114
|
const pendingInputFlushDelayMilliseconds = 20;
|
|
@@ -214,10 +222,10 @@ export function App({
|
|
|
214
222
|
}, [stdin]);
|
|
215
223
|
|
|
216
224
|
const clearInputState = useCallback((): void => {
|
|
217
|
-
|
|
225
|
+
terminalInput.reset();
|
|
218
226
|
clearPendingInputFlush();
|
|
219
227
|
detachReadableListener();
|
|
220
|
-
}, [clearPendingInputFlush, detachReadableListener]);
|
|
228
|
+
}, [clearPendingInputFlush, detachReadableListener, terminalInput]);
|
|
221
229
|
|
|
222
230
|
const disableRawMode = useCallback((): void => {
|
|
223
231
|
if (!rawModeStdin) {
|
|
@@ -274,21 +282,21 @@ export function App({
|
|
|
274
282
|
clearPendingInputFlush();
|
|
275
283
|
pendingInputFlushRef.current = setTimeout(() => {
|
|
276
284
|
pendingInputFlushRef.current = undefined;
|
|
277
|
-
const pendingEscape =
|
|
285
|
+
const pendingEscape = terminalInput.flushPendingEscape();
|
|
278
286
|
if (!pendingEscape) {
|
|
279
287
|
return;
|
|
280
288
|
}
|
|
281
289
|
|
|
282
290
|
emitInput(pendingEscape);
|
|
283
291
|
}, pendingInputFlushDelayMilliseconds);
|
|
284
|
-
}, [clearPendingInputFlush, emitInput]);
|
|
292
|
+
}, [clearPendingInputFlush, emitInput, terminalInput]);
|
|
285
293
|
|
|
286
294
|
const handleReadable = useCallback((): void => {
|
|
287
295
|
clearPendingInputFlush();
|
|
288
296
|
let chunk;
|
|
289
297
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
290
298
|
while ((chunk = stdin.read() as string | null) !== null) {
|
|
291
|
-
const inputEvents =
|
|
299
|
+
const inputEvents = terminalInput.push(chunk);
|
|
292
300
|
for (const event of inputEvents) {
|
|
293
301
|
if (typeof event === "string") {
|
|
294
302
|
emitInput(event);
|
|
@@ -305,10 +313,10 @@ export function App({
|
|
|
305
313
|
}
|
|
306
314
|
}
|
|
307
315
|
|
|
308
|
-
if (
|
|
316
|
+
if (terminalInput.hasPendingEscape()) {
|
|
309
317
|
schedulePendingInputFlush();
|
|
310
318
|
}
|
|
311
|
-
}, [stdin, emitInput, clearPendingInputFlush, schedulePendingInputFlush]);
|
|
319
|
+
}, [stdin, emitInput, clearPendingInputFlush, schedulePendingInputFlush, terminalInput]);
|
|
312
320
|
|
|
313
321
|
const attachReadableListener = useCallback((): void => {
|
|
314
322
|
if (readableListenerRef.current) {
|
|
@@ -379,6 +387,86 @@ export function App({
|
|
|
379
387
|
[rawModeStdin, stdin, attachReadableListener, clearInputState, disableRawMode],
|
|
380
388
|
);
|
|
381
389
|
|
|
390
|
+
const handleQueryTerminal = useCallback(
|
|
391
|
+
async ({ refresh = false } = {}): Promise<TerminalQueryResult | undefined> => {
|
|
392
|
+
if (!interactive || !isRawModeSupported || !stdout.isTTY) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Without a refresh, an existing (or in-flight) query answers directly.
|
|
397
|
+
if (!refresh) {
|
|
398
|
+
const existing = getTerminalQueryPromise(stdout);
|
|
399
|
+
if (existing) {
|
|
400
|
+
return existing;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Own the input stream for the duration of the query: raw mode on so
|
|
405
|
+
// responses arrive unbuffered, and the readable listener detached so they
|
|
406
|
+
// flow to the query's own data listener instead of being parsed as key
|
|
407
|
+
// presses. queryTerminal buffers real user input typed during the query
|
|
408
|
+
// and unshifts it back; reattaching the readable listener delivers it.
|
|
409
|
+
//
|
|
410
|
+
// Note: if kitty keyboard auto-detection is in flight at the same time,
|
|
411
|
+
// both data listeners see the same bytes; that detector answers the same
|
|
412
|
+
// CSI ? u probe this query sends, so it converges to the same result.
|
|
413
|
+
handleSetRawMode(true);
|
|
414
|
+
detachReadableListener();
|
|
415
|
+
try {
|
|
416
|
+
return await (refresh
|
|
417
|
+
? refreshTerminalQuery(stdin, stdout)
|
|
418
|
+
: ensureTerminalQuery(stdin, stdout));
|
|
419
|
+
} finally {
|
|
420
|
+
attachReadableListener();
|
|
421
|
+
handleSetRawMode(false);
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
[
|
|
425
|
+
interactive,
|
|
426
|
+
isRawModeSupported,
|
|
427
|
+
stdin,
|
|
428
|
+
stdout,
|
|
429
|
+
handleSetRawMode,
|
|
430
|
+
detachReadableListener,
|
|
431
|
+
attachReadableListener,
|
|
432
|
+
],
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// While the store has push reporting enabled, someone must actually read
|
|
436
|
+
// stdin so the reports reach `ingest` — even in apps without a single
|
|
437
|
+
// `useInput`. Holding raw mode (ref-counted, like `useInput` does) keeps
|
|
438
|
+
// the readable pipeline attached for the duration.
|
|
439
|
+
const reportFeedActiveRef = useRef(false);
|
|
440
|
+
const handleSetReportFeed = useCallback(
|
|
441
|
+
(active: boolean): void => {
|
|
442
|
+
if (active === reportFeedActiveRef.current || !isRawModeSupported) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
reportFeedActiveRef.current = active;
|
|
447
|
+
handleSetRawMode(active);
|
|
448
|
+
},
|
|
449
|
+
[isRawModeSupported, handleSetRawMode],
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// Hand the capabilities store this instance's integration: queries go
|
|
453
|
+
// through the input pipeline above instead of competing with it, and the
|
|
454
|
+
// report feed keeps that pipeline running while reports are enabled. An
|
|
455
|
+
// insertion effect runs before every passive effect — parent and child —
|
|
456
|
+
// so a child calling store.query() from its own mount effect always finds
|
|
457
|
+
// the integration already registered.
|
|
458
|
+
useInsertionEffect(() => {
|
|
459
|
+
const unregister = registerTerminalIntegration(stdout, {
|
|
460
|
+
runQuery: handleQueryTerminal,
|
|
461
|
+
setReportFeed: handleSetReportFeed,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return () => {
|
|
465
|
+
unregister();
|
|
466
|
+
handleSetReportFeed(false);
|
|
467
|
+
};
|
|
468
|
+
}, [stdout, handleQueryTerminal, handleSetReportFeed]);
|
|
469
|
+
|
|
382
470
|
const handleSetBracketedPasteMode = useCallback(
|
|
383
471
|
(isEnabled: boolean): void => {
|
|
384
472
|
if (!stdout.isTTY) {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { type LiteralUnion } from "#/types.ts";
|
|
3
|
+
import type { Paint } from "#/color/paint.ts";
|
|
5
4
|
|
|
6
|
-
export type BackgroundColor =
|
|
5
|
+
export type BackgroundColor = Paint;
|
|
7
6
|
|
|
8
7
|
export const backgroundContext = createContext<BackgroundColor | undefined>(undefined);
|
package/src/components/Box.tsx
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { useContext, type PropsWithChildren, type Ref } from "react";
|
|
3
3
|
|
|
4
4
|
import { accessibilityContext } from "#/components/AccessibilityContext.ts";
|
|
5
|
-
import { backgroundContext } from "#/components/BackgroundContext.ts";
|
|
6
5
|
import { type DOMElement } from "#/dom.ts";
|
|
7
6
|
import { type Styles } from "#/styles.ts";
|
|
8
7
|
|
|
@@ -97,12 +96,5 @@ export function Box({
|
|
|
97
96
|
</ink-box>
|
|
98
97
|
);
|
|
99
98
|
|
|
100
|
-
// If this Box has a background color, provide it to children via context
|
|
101
|
-
if (backgroundColor) {
|
|
102
|
-
return (
|
|
103
|
-
<backgroundContext.Provider value={backgroundColor}>{boxElement}</backgroundContext.Provider>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
99
|
return boxElement;
|
|
108
100
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { link } from "#/ansi/escapes.ts";
|
|
5
|
+
import { detectHyperlinkSupport } from "#/capabilities/detect.ts";
|
|
6
|
+
import { Text, type Props as TextProps } from "#/components/Text.tsx";
|
|
7
|
+
import { Transform } from "#/components/Transform.tsx";
|
|
8
|
+
import { useStdout } from "#/hooks/use-stdout.ts";
|
|
9
|
+
|
|
10
|
+
export type Props = Omit<TextProps, "children"> & {
|
|
11
|
+
/**
|
|
12
|
+
The URL the hyperlink points to.
|
|
13
|
+
*/
|
|
14
|
+
readonly url: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
When the terminal does not support OSC 8 hyperlinks, append the URL in
|
|
18
|
+
parentheses after the text so it stays reachable. Set to `false` to render
|
|
19
|
+
the text alone.
|
|
20
|
+
|
|
21
|
+
@default true
|
|
22
|
+
*/
|
|
23
|
+
readonly fallback?: boolean;
|
|
24
|
+
|
|
25
|
+
readonly children?: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
A clickable OSC 8 hyperlink — the counterpart to the router's `<Link>`, which
|
|
30
|
+
navigates between screens. On terminals without hyperlink support it falls
|
|
31
|
+
back to `text (url)`.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<Hyperlink url="https://example.com">Documentation</Hyperlink>
|
|
35
|
+
```
|
|
36
|
+
*/
|
|
37
|
+
export function Hyperlink({ url, fallback = true, children, ...textProps }: Props) {
|
|
38
|
+
const { stdout } = useStdout();
|
|
39
|
+
|
|
40
|
+
if (!detectHyperlinkSupport(stdout)) {
|
|
41
|
+
return (
|
|
42
|
+
<Text {...textProps}>
|
|
43
|
+
{children}
|
|
44
|
+
{fallback ? <Text dimColor> ({url})</Text> : null}
|
|
45
|
+
</Text>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// OSC 8 sequences are zero-width, so wrapping after layout is safe. The
|
|
50
|
+
// transform runs per output line, giving every line its own complete pair.
|
|
51
|
+
return (
|
|
52
|
+
<Transform transform={(text) => link(text, url)}>
|
|
53
|
+
<Text {...textProps}>{children}</Text>
|
|
54
|
+
</Transform>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ClipboardSelection, TerminalProgressState } from "#/ansi/osc.ts";
|
|
4
|
+
|
|
5
|
+
export type TerminalOsc = {
|
|
6
|
+
readonly publishProgress: (owner: symbol, state: TerminalProgressState, value?: number) => void;
|
|
7
|
+
readonly copyToClipboard: (text: string, selection?: ClipboardSelection) => void;
|
|
8
|
+
readonly publishTitle: (owner: symbol, title?: string) => void;
|
|
9
|
+
readonly setWorkingDirectory: (directory: URL | string) => void;
|
|
10
|
+
readonly notify: (title: string) => void;
|
|
11
|
+
readonly setPointerShape: (shape: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const noop = () => {};
|
|
15
|
+
|
|
16
|
+
export const TerminalOscContext = createContext<TerminalOsc>({
|
|
17
|
+
publishProgress: noop,
|
|
18
|
+
copyToClipboard: noop,
|
|
19
|
+
publishTitle: noop,
|
|
20
|
+
setWorkingDirectory: noop,
|
|
21
|
+
notify: noop,
|
|
22
|
+
setPointerShape: noop,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
TerminalOscContext.displayName = "InternalTerminalOscContext";
|