@cssints/tui 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +205 -2
- package/dist/bomb.d.mts +86 -0
- package/dist/bomb.mjs +323 -0
- package/dist/index.d.mts +49 -0
- package/dist/index.mjs +88 -0
- package/dist/opentui.d.mts +66 -0
- package/dist/opentui.mjs +209 -0
- package/dist/system-DNKgrSBA.d.mts +116 -0
- package/dist/system.d.mts +2 -0
- package/dist/system.mjs +290 -0
- package/package.json +55 -4
package/dist/system.mjs
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { defineTheme } from "./index.mjs";
|
|
2
|
+
import { clampRgb, converter, formatHex, modeLrgb, modeOkhsl, modeOklab, modeOklch, modeRgb, parse, useMode, wcagLuminance } from "culori/fn";
|
|
3
|
+
//#region src/system.ts
|
|
4
|
+
for (const mode of [
|
|
5
|
+
modeRgb,
|
|
6
|
+
modeLrgb,
|
|
7
|
+
modeOklab,
|
|
8
|
+
modeOklch,
|
|
9
|
+
modeOkhsl
|
|
10
|
+
]) useMode(mode);
|
|
11
|
+
const toOkhsl = converter("okhsl");
|
|
12
|
+
const toOklch = converter("oklch");
|
|
13
|
+
const toRgb = converter("rgb");
|
|
14
|
+
/** The 16 ANSI colours, by their index. */
|
|
15
|
+
const SLOTS = [
|
|
16
|
+
"black",
|
|
17
|
+
"red",
|
|
18
|
+
"green",
|
|
19
|
+
"yellow",
|
|
20
|
+
"blue",
|
|
21
|
+
"magenta",
|
|
22
|
+
"cyan",
|
|
23
|
+
"white",
|
|
24
|
+
"brightBlack",
|
|
25
|
+
"brightRed",
|
|
26
|
+
"brightGreen",
|
|
27
|
+
"brightYellow",
|
|
28
|
+
"brightBlue",
|
|
29
|
+
"brightMagenta",
|
|
30
|
+
"brightCyan",
|
|
31
|
+
"brightWhite"
|
|
32
|
+
];
|
|
33
|
+
const HUES = {
|
|
34
|
+
black: "#000000",
|
|
35
|
+
red: "#cd3131",
|
|
36
|
+
green: "#0dbc79",
|
|
37
|
+
yellow: "#e5e510",
|
|
38
|
+
blue: "#2472c8",
|
|
39
|
+
magenta: "#bc3fbc",
|
|
40
|
+
cyan: "#11a8cd",
|
|
41
|
+
white: "#e5e5e5",
|
|
42
|
+
brightBlack: "#666666",
|
|
43
|
+
brightRed: "#f14c4c",
|
|
44
|
+
brightGreen: "#23d18b",
|
|
45
|
+
brightYellow: "#f5f543",
|
|
46
|
+
brightBlue: "#3b8eea",
|
|
47
|
+
brightMagenta: "#d670d6",
|
|
48
|
+
brightCyan: "#29b8db",
|
|
49
|
+
brightWhite: "#e5e5e5"
|
|
50
|
+
};
|
|
51
|
+
const TOKEN = /^var\(--([\w-]+)\)$/;
|
|
52
|
+
const nameOf = (token, where) => {
|
|
53
|
+
const name = typeof token === "string" ? TOKEN.exec(token)?.[1] : void 0;
|
|
54
|
+
if (!name) throw new Error(`cssints: ${where}: ${String(token)} is no token of createTokens`);
|
|
55
|
+
return name;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The relative luminance of a colour (WCAG 2), as check.contrast reads it.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} hex a colour
|
|
61
|
+
* @returns {number} its luminance, 0 to 1
|
|
62
|
+
*/
|
|
63
|
+
const luminance = (hex) => {
|
|
64
|
+
const c = parse(hex);
|
|
65
|
+
if (!c) throw new Error(`cssints: ${hex} is not a colour`);
|
|
66
|
+
return wcagLuminance(clampRgb(c));
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The WCAG 2 contrast ratio of two colours.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} a a colour
|
|
72
|
+
* @param {string} b another
|
|
73
|
+
* @returns {number} the ratio, 1 to 21
|
|
74
|
+
*/
|
|
75
|
+
const contrast = (a, b) => {
|
|
76
|
+
const [x, y] = [luminance(a), luminance(b)];
|
|
77
|
+
return (Math.max(x, y) + .05) / (Math.min(x, y) + .05);
|
|
78
|
+
};
|
|
79
|
+
const sourceOf = (hex) => {
|
|
80
|
+
const c = parse(hex);
|
|
81
|
+
const hsl = toOkhsl(c);
|
|
82
|
+
return {
|
|
83
|
+
h: hsl.h ?? 0,
|
|
84
|
+
s: hsl.s,
|
|
85
|
+
l: hsl.l,
|
|
86
|
+
c: toOklch(c).c,
|
|
87
|
+
hex: formatHex(c)
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const bell = (l) => Math.sqrt(Math.max(0, 4 * l * (1 - l)));
|
|
91
|
+
const at = (src, l) => {
|
|
92
|
+
const s = src.s * Math.min(1, bell(l) / Math.max(bell(src.l), 1e-6));
|
|
93
|
+
const lch = toOklch({
|
|
94
|
+
mode: "okhsl",
|
|
95
|
+
h: src.h,
|
|
96
|
+
s,
|
|
97
|
+
l
|
|
98
|
+
});
|
|
99
|
+
return formatHex(clampRgb(toRgb({
|
|
100
|
+
...lch,
|
|
101
|
+
c: Math.min(lch.c, src.c)
|
|
102
|
+
})));
|
|
103
|
+
};
|
|
104
|
+
const bisect = (src, ok, up, from = src.l) => {
|
|
105
|
+
let [lo, hi] = up ? [from, 1] : [0, from];
|
|
106
|
+
for (let i = 0; i < 40; i++) {
|
|
107
|
+
const mid = (lo + hi) / 2;
|
|
108
|
+
if (ok(at(src, mid)) === up) hi = mid;
|
|
109
|
+
else lo = mid;
|
|
110
|
+
}
|
|
111
|
+
let l = up ? hi : lo;
|
|
112
|
+
let hex = at(src, l);
|
|
113
|
+
for (let i = 0; i < 100 && !ok(hex) && l > 0 && l < 1; i++) {
|
|
114
|
+
l = Math.min(1, Math.max(0, l + (up ? .002 : -.002)));
|
|
115
|
+
hex = at(src, l);
|
|
116
|
+
}
|
|
117
|
+
return hex;
|
|
118
|
+
};
|
|
119
|
+
const text = (src, on, k, dark) => {
|
|
120
|
+
const meets = (hex) => on.every((s) => contrast(hex, s) >= k - 1e-9);
|
|
121
|
+
if (meets(src.hex)) return src.hex;
|
|
122
|
+
const ys = on.map(luminance);
|
|
123
|
+
const y = luminance(src.hex);
|
|
124
|
+
const above = Math.max(...ys.map((v) => (v + .05) * k - .05));
|
|
125
|
+
const below = Math.min(...ys.map((v) => (v + .05) / k - .05));
|
|
126
|
+
const first = y >= Math.max(...ys) ? true : y <= Math.min(...ys) ? false : dark;
|
|
127
|
+
for (const up of [first, !first]) if (up ? above <= 1 : below >= 0) return bisect(src, up ? (h) => luminance(h) >= above && meets(h) : (h) => luminance(h) <= below && meets(h), up);
|
|
128
|
+
const worst = (hex) => Math.min(...on.map((s) => contrast(hex, s)));
|
|
129
|
+
const [white, black] = [at(src, 1), at(src, 0)];
|
|
130
|
+
return worst(white) >= worst(black) ? white : black;
|
|
131
|
+
};
|
|
132
|
+
const surface = (src, under, k, dark) => {
|
|
133
|
+
const y = luminance(under);
|
|
134
|
+
for (const up of [dark, !dark]) {
|
|
135
|
+
const target = up ? (y + .05) * k - .05 : (y + .05) / k - .05;
|
|
136
|
+
if (up ? target <= 1 : target >= 0) return bisect(src, up ? (h) => luminance(h) >= target : (h) => luminance(h) <= target, up, up ? 0 : 1);
|
|
137
|
+
}
|
|
138
|
+
return src.hex;
|
|
139
|
+
};
|
|
140
|
+
const appearanceOf = (report, colorfgbg) => {
|
|
141
|
+
if (report.bg) {
|
|
142
|
+
const y = luminance(report.bg);
|
|
143
|
+
return (report.fg ? y < luminance(report.fg) : y < .18) ? "dark" : "light";
|
|
144
|
+
}
|
|
145
|
+
if (report.scheme) return report.scheme;
|
|
146
|
+
const last = colorfgbg?.split(";").at(-1);
|
|
147
|
+
if (last && /^\d+$/.test(last)) return Number(last) <= 6 || Number(last) === 8 ? "dark" : "light";
|
|
148
|
+
return "dark";
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* A system theme: each token of `roles` takes its hue from a colour of the terminal and its lightness from its contrast
|
|
152
|
+
* rules against the tokens it sits on. Checks the roles (an `on` without a role, a cycle) and orders them.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} tokens the tokens of `createTokens` (cssints/tui)
|
|
155
|
+
* @param {Roles} roles a role per colour token, in the shape of `tokens`
|
|
156
|
+
* @param {SystemOptions} options the name, the app's own hues, the static theme under it
|
|
157
|
+
* @returns {SystemTheme} the theme, for `followSystemTheme` of an adapter, or `solve` and `apply`
|
|
158
|
+
*/
|
|
159
|
+
const createSystemTheme = (tokens, roles, options = {}) => {
|
|
160
|
+
const entries = /* @__PURE__ */ new Map();
|
|
161
|
+
const walk = (t, r, path) => {
|
|
162
|
+
for (const [k, role] of Object.entries(r)) {
|
|
163
|
+
const token = t[k];
|
|
164
|
+
if (typeof token === "string") {
|
|
165
|
+
const name = nameOf(token, `${path}${k}`);
|
|
166
|
+
const on = (role.on ?? []).map((o) => nameOf(o, `${path}${k}.on`));
|
|
167
|
+
entries.set(name, {
|
|
168
|
+
name,
|
|
169
|
+
role,
|
|
170
|
+
on
|
|
171
|
+
});
|
|
172
|
+
} else if (token && typeof token === "object" && role && typeof role === "object") walk(token, role, `${path}${k}.`);
|
|
173
|
+
else throw new Error(`cssints: ${path}${k} is no token of createTokens`);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
walk(tokens, roles, "");
|
|
177
|
+
const order = [];
|
|
178
|
+
const state = /* @__PURE__ */ new Map();
|
|
179
|
+
const visit = (e) => {
|
|
180
|
+
if (state.get(e.name) === "done") return;
|
|
181
|
+
if (state.get(e.name) === "visiting") throw new Error(`cssints: the roles of ${e.name} sit on each other (a cycle of on)`);
|
|
182
|
+
state.set(e.name, "visiting");
|
|
183
|
+
for (const o of e.on) {
|
|
184
|
+
const dep = entries.get(o);
|
|
185
|
+
if (!dep) throw new Error(`cssints: ${e.name} sits on ${o}, which has no role`);
|
|
186
|
+
visit(dep);
|
|
187
|
+
}
|
|
188
|
+
state.set(e.name, "done");
|
|
189
|
+
order.push(e);
|
|
190
|
+
};
|
|
191
|
+
for (const e of entries.values()) visit(e);
|
|
192
|
+
const name = options.name ?? "system";
|
|
193
|
+
const solve = (report, { indexed = false, colorfgbg } = {}) => {
|
|
194
|
+
const appearance = appearanceOf(report, colorfgbg ?? globalThis.process?.env.COLORFGBG);
|
|
195
|
+
const base = options.themes?.[appearance] ?? "";
|
|
196
|
+
const values = {};
|
|
197
|
+
if (!report.bg) {
|
|
198
|
+
if (indexed) for (const { name: n, role } of order) {
|
|
199
|
+
const hue = role.hue ?? (role.surface ? "bg" : "fg");
|
|
200
|
+
const slot = SLOTS.indexOf(hue);
|
|
201
|
+
const ansi = role.ansi ?? (role.surface || slot < 0 ? "default" : slot);
|
|
202
|
+
values[n] = ansi === "default" ? "default" : `ansi:${ansi}`;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
appearance,
|
|
206
|
+
values,
|
|
207
|
+
base
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const dark = appearance === "dark";
|
|
211
|
+
const hex = (hue) => {
|
|
212
|
+
if (hue === "bg") return report.bg;
|
|
213
|
+
if (hue === "fg") return report.fg ?? options.hues?.fg ?? (dark ? "#ffffff" : "#000000");
|
|
214
|
+
const i = SLOTS.indexOf(hue);
|
|
215
|
+
return report.ansi?.[i] ?? options.hues?.[hue] ?? HUES[hue];
|
|
216
|
+
};
|
|
217
|
+
for (const { name: n, role, on } of order) {
|
|
218
|
+
const src = sourceOf(hex(role.hue ?? (role.surface ? "bg" : "fg")));
|
|
219
|
+
const under = on.map((o) => values[o]);
|
|
220
|
+
const k = role.contrast;
|
|
221
|
+
values[n] = k === void 0 || !under.length ? src.hex : role.surface ? surface(src, under[0], k, dark) : text(src, under, k, dark);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
appearance,
|
|
225
|
+
values,
|
|
226
|
+
base
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
name,
|
|
231
|
+
solve,
|
|
232
|
+
apply: (report, o) => {
|
|
233
|
+
const solved = solve(report, o);
|
|
234
|
+
defineTheme(name, solved.values, solved.base);
|
|
235
|
+
return solved;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
/** OSC 10 and 11 (the default colours), OSC 4 for the 16 ANSI colours, and the colour-scheme report (`CSI ? 996 n`). */
|
|
240
|
+
const QUERY = [
|
|
241
|
+
"\x1B]10;?\x1B\\",
|
|
242
|
+
"\x1B]11;?\x1B\\",
|
|
243
|
+
...SLOTS.map((_, i) => `\x1b]4;${i};?\x1b\\`),
|
|
244
|
+
"\x1B[?996n"
|
|
245
|
+
].join("");
|
|
246
|
+
/** Mode 2031 on: the terminal sends `CSI ? 997 ; 1|2 n` when its palette changes. */
|
|
247
|
+
const NOTIFY = "\x1B[?2031h";
|
|
248
|
+
/** Mode 2031 off. */
|
|
249
|
+
const UNNOTIFY = "\x1B[?2031l";
|
|
250
|
+
const REPLY = /\x1b\](10|11|4;(\d+));(?:rgba?:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})(?:\/[0-9a-f]{1,4})?|#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{12}))(?:\x07|\x1b\\)|\x1b\[\?997;([12])n/gi;
|
|
251
|
+
const CUT = /\x1b(?:\][^\x07\x1b]*(?:\x1b)?|\[\?(?:9(?:9(?:7(?:;\d?)?)?)?)?)$/;
|
|
252
|
+
const channel = (h) => Math.round(Number.parseInt(h, 16) / (16 ** h.length - 1) * 255);
|
|
253
|
+
const hexOf = (r, g, b) => `#${[
|
|
254
|
+
r,
|
|
255
|
+
g,
|
|
256
|
+
b
|
|
257
|
+
].map((h) => channel(h).toString(16).padStart(2, "0")).join("")}`;
|
|
258
|
+
/**
|
|
259
|
+
* Takes the terminal's replies (OSC 10, 11 and 4, `CSI ? 997 n`) out of a chunk of input, as text of one char per byte.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} text the chunk, after what `tail` of the last one kept
|
|
262
|
+
* @returns {object} the replies, the rest of the input (keys, mouse), and the tail of a reply cut off at the end
|
|
263
|
+
*/
|
|
264
|
+
const scan = (text) => {
|
|
265
|
+
const replies = { ansi: /* @__PURE__ */ new Map() };
|
|
266
|
+
const cut = CUT.exec(text);
|
|
267
|
+
const tail = cut ? cut[0] : "";
|
|
268
|
+
return {
|
|
269
|
+
replies,
|
|
270
|
+
rest: (cut ? text.slice(0, cut.index) : text).replace(REPLY, (...m) => {
|
|
271
|
+
const [, what, index, r = "", g = "", b = "", hash, scheme] = m;
|
|
272
|
+
if (scheme) {
|
|
273
|
+
replies.scheme = scheme === "1" ? "dark" : "light";
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
let colour;
|
|
277
|
+
if (hash) {
|
|
278
|
+
const n = hash.length / 3;
|
|
279
|
+
colour = hexOf(hash.slice(0, n), hash.slice(n, 2 * n), hash.slice(2 * n));
|
|
280
|
+
} else colour = hexOf(r, g, b);
|
|
281
|
+
if (what === "10") replies.fg = colour;
|
|
282
|
+
else if (what === "11") replies.bg = colour;
|
|
283
|
+
else if (Number(index) < 16) replies.ansi.set(Number(index), colour);
|
|
284
|
+
return "";
|
|
285
|
+
}),
|
|
286
|
+
tail
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
//#endregion
|
|
290
|
+
export { NOTIFY, QUERY, SLOTS, UNNOTIFY, contrast, createSystemTheme, luminance, scan };
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cssints/tui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "cssints in a terminal: the runtime of cssints/tui styles (states, cursors, themes) and its OpenTUI and @bomb.sh/tty adapters",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/alexeiaccio/css-in-ts-compiler.git",
|
|
9
|
+
"directory": "packages/tui"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://cssints.dev",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./opentui": {
|
|
23
|
+
"types": "./dist/opentui.d.mts",
|
|
24
|
+
"default": "./dist/opentui.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./bomb": {
|
|
27
|
+
"types": "./dist/bomb.d.mts",
|
|
28
|
+
"default": "./dist/bomb.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./system": {
|
|
31
|
+
"types": "./dist/system.d.mts",
|
|
32
|
+
"default": "./dist/system.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"culori": "^4.0.2"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@bomb.sh/tty": ">=0.9.0",
|
|
40
|
+
"@opentui/core": ">=0.5.17",
|
|
41
|
+
"cssints": "^0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@bomb.sh/tty": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@opentui/core": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|