@oh-my-pi/pi-utils 15.8.3 → 15.9.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/dist/types/color.d.ts +23 -3
- package/dist/types/peek-file.d.ts +20 -0
- package/package.json +2 -2
- package/src/color.ts +101 -3
- package/src/peek-file.ts +74 -0
package/dist/types/color.d.ts
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* ```ts
|
|
6
6
|
* import { hexToHsv, hsvToHex } from "@oh-my-pi/pi-utils";
|
|
7
7
|
*
|
|
8
|
-
* //
|
|
9
|
-
*
|
|
10
|
-
* // Or work with HSV directly
|
|
8
|
+
* // Rotate the hue by 90°
|
|
11
9
|
* const hsv = hexToHsv("#4ade80");
|
|
12
10
|
* hsv.h = (hsv.h + 90) % 360;
|
|
13
11
|
* const newHex = hsvToHex(hsv);
|
|
@@ -80,3 +78,25 @@ export interface HSVAdjustment {
|
|
|
80
78
|
* ```
|
|
81
79
|
*/
|
|
82
80
|
export declare function adjustHsv(hex: string, adj: HSVAdjustment): string;
|
|
81
|
+
/**
|
|
82
|
+
* Convert HSL (h: 0-360, s: 0-1, l: 0-1) to a CSS hex string.
|
|
83
|
+
*/
|
|
84
|
+
export declare function hslToHex(h: number, s: number, l: number): string;
|
|
85
|
+
/**
|
|
86
|
+
* Perceptual luma (gamma-encoded BT.709 weights over raw sRGB), normalized to 0..1.
|
|
87
|
+
*
|
|
88
|
+
* Accepts a hex string (`#rgb` / `#rrggbb`) or a 256-color palette index; returns
|
|
89
|
+
* `undefined` for var refs, empty strings, or anything unparseable.
|
|
90
|
+
*
|
|
91
|
+
* Cheap and good enough for a light/dark *classification* threshold. NOT suitable
|
|
92
|
+
* for contrast ratios — use {@link relativeLuminance} for those.
|
|
93
|
+
*/
|
|
94
|
+
export declare function colorLuma(value: string | number): number | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* WCAG 2.x relative luminance (BT.709 weights over linearized sRGB), normalized to
|
|
97
|
+
* 0..1. This is the value the WCAG contrast-ratio formula expects.
|
|
98
|
+
*
|
|
99
|
+
* Accepts a hex string (`#rgb` / `#rrggbb`) or a 256-color palette index; returns
|
|
100
|
+
* `undefined` for var refs, empty strings, or anything unparseable.
|
|
101
|
+
*/
|
|
102
|
+
export declare function relativeLuminance(value: string | number): number | undefined;
|
|
@@ -7,3 +7,23 @@ export declare function peekFileSync<T>(filePath: string, maxBytes: number, op:
|
|
|
7
7
|
* Like {@link peekFileSync} but uses async I/O.
|
|
8
8
|
*/
|
|
9
9
|
export declare function peekFile<T>(filePath: string, maxBytes: number, op: (header: Uint8Array) => T): Promise<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Read up to the last `maxBytes` of `filePath` and pass that slice to `op`.
|
|
12
|
+
*
|
|
13
|
+
* The tail mirror of {@link peekFile}: same pooled-buffer strategy (no per-call
|
|
14
|
+
* allocation for small reads), but the read is positioned at `size - len` so the
|
|
15
|
+
* window ends at EOF. When the file is shorter than `maxBytes`, the whole file is
|
|
16
|
+
* returned. A multi-byte codepoint straddling the leading cut decodes to a
|
|
17
|
+
* replacement char — callers that parse line-oriented tails drop the partial
|
|
18
|
+
* leading line anyway.
|
|
19
|
+
*/
|
|
20
|
+
export declare function peekFileTail<T>(filePath: string, maxBytes: number, op: (tail: Uint8Array) => T): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* Read up to the first `prefixBytes` and last `suffixBytes` of `filePath`, then
|
|
23
|
+
* pass both slices to `op`.
|
|
24
|
+
*
|
|
25
|
+
* Uses a single open/stat sequence. When the whole file fits in the head window,
|
|
26
|
+
* the tail is sliced from the already-read head bytes instead of issuing a
|
|
27
|
+
* second read.
|
|
28
|
+
*/
|
|
29
|
+
export declare function peekFileEnds<T>(filePath: string, prefixBytes: number, suffixBytes: number, op: (head: Uint8Array, tail: Uint8Array) => T): Promise<T>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.9.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "15.
|
|
34
|
+
"@oh-my-pi/pi-natives": "15.9.0",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/color.ts
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* ```ts
|
|
6
6
|
* import { hexToHsv, hsvToHex } from "@oh-my-pi/pi-utils";
|
|
7
7
|
*
|
|
8
|
-
* //
|
|
9
|
-
*
|
|
10
|
-
* // Or work with HSV directly
|
|
8
|
+
* // Rotate the hue by 90°
|
|
11
9
|
* const hsv = hexToHsv("#4ade80");
|
|
12
10
|
* hsv.h = (hsv.h + 90) % 360;
|
|
13
11
|
* const newHex = hsvToHex(hsv);
|
|
@@ -202,3 +200,103 @@ export function adjustHsv(hex: string, adj: HSVAdjustment): string {
|
|
|
202
200
|
}
|
|
203
201
|
return hsvToHex(hsv);
|
|
204
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Convert HSL (h: 0-360, s: 0-1, l: 0-1) to a CSS hex string.
|
|
206
|
+
*/
|
|
207
|
+
export function hslToHex(h: number, s: number, l: number): string {
|
|
208
|
+
const a = s * Math.min(l, 1 - l);
|
|
209
|
+
const f = (n: number) => {
|
|
210
|
+
const k = (n + h / 30) % 12;
|
|
211
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
212
|
+
return Math.round(255 * color)
|
|
213
|
+
.toString(16)
|
|
214
|
+
.padStart(2, "0");
|
|
215
|
+
};
|
|
216
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Conventional xterm RGB for the 16 base ANSI colors. Terminals may remap these,
|
|
220
|
+
// so they're a best-effort approximation for light/dark classification.
|
|
221
|
+
const ANSI_16: readonly (readonly [number, number, number])[] = [
|
|
222
|
+
[0, 0, 0],
|
|
223
|
+
[128, 0, 0],
|
|
224
|
+
[0, 128, 0],
|
|
225
|
+
[128, 128, 0],
|
|
226
|
+
[0, 0, 128],
|
|
227
|
+
[128, 0, 128],
|
|
228
|
+
[0, 128, 128],
|
|
229
|
+
[192, 192, 192],
|
|
230
|
+
[128, 128, 128],
|
|
231
|
+
[255, 0, 0],
|
|
232
|
+
[0, 255, 0],
|
|
233
|
+
[255, 255, 0],
|
|
234
|
+
[0, 0, 255],
|
|
235
|
+
[255, 0, 255],
|
|
236
|
+
[0, 255, 255],
|
|
237
|
+
[255, 255, 255],
|
|
238
|
+
];
|
|
239
|
+
const CUBE_STEPS = [0, 95, 135, 175, 215, 255] as const;
|
|
240
|
+
|
|
241
|
+
/** Parse a 256-color palette index (0–255) to RGB (0..255). */
|
|
242
|
+
function paletteToRgb(index: number): RGB | undefined {
|
|
243
|
+
if (!Number.isInteger(index) || index < 0 || index > 255) return undefined;
|
|
244
|
+
if (index < 16) {
|
|
245
|
+
const rgb = ANSI_16[index];
|
|
246
|
+
return rgb ? { r: rgb[0], g: rgb[1], b: rgb[2] } : undefined;
|
|
247
|
+
}
|
|
248
|
+
if (index < 232) {
|
|
249
|
+
const n = index - 16;
|
|
250
|
+
return {
|
|
251
|
+
r: CUBE_STEPS[Math.floor(n / 36) % 6] ?? 0,
|
|
252
|
+
g: CUBE_STEPS[Math.floor(n / 6) % 6] ?? 0,
|
|
253
|
+
b: CUBE_STEPS[n % 6] ?? 0,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const gray = 8 + (index - 232) * 10;
|
|
257
|
+
return { r: gray, g: gray, b: gray };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Parse a theme color value — `#rgb`/`#rrggbb` hex or 256-color palette index — to RGB (0..255). */
|
|
261
|
+
function toRgb(value: string | number): RGB | undefined {
|
|
262
|
+
if (typeof value === "number") return paletteToRgb(value);
|
|
263
|
+
if (typeof value !== "string" || value[0] !== "#") return undefined;
|
|
264
|
+
if (value.length !== 4 && value.length !== 7) return undefined;
|
|
265
|
+
const rgb = hexToRgb(value);
|
|
266
|
+
if (Number.isNaN(rgb.r) || Number.isNaN(rgb.g) || Number.isNaN(rgb.b)) return undefined;
|
|
267
|
+
return rgb;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Gamma-decode a single 0..255 sRGB channel to linear 0..1. */
|
|
271
|
+
function linearizeChannel(channel: number): number {
|
|
272
|
+
const c = channel / 255;
|
|
273
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Perceptual luma (gamma-encoded BT.709 weights over raw sRGB), normalized to 0..1.
|
|
278
|
+
*
|
|
279
|
+
* Accepts a hex string (`#rgb` / `#rrggbb`) or a 256-color palette index; returns
|
|
280
|
+
* `undefined` for var refs, empty strings, or anything unparseable.
|
|
281
|
+
*
|
|
282
|
+
* Cheap and good enough for a light/dark *classification* threshold. NOT suitable
|
|
283
|
+
* for contrast ratios — use {@link relativeLuminance} for those.
|
|
284
|
+
*/
|
|
285
|
+
export function colorLuma(value: string | number): number | undefined {
|
|
286
|
+
const rgb = toRgb(value);
|
|
287
|
+
if (!rgb) return undefined;
|
|
288
|
+
return (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* WCAG 2.x relative luminance (BT.709 weights over linearized sRGB), normalized to
|
|
293
|
+
* 0..1. This is the value the WCAG contrast-ratio formula expects.
|
|
294
|
+
*
|
|
295
|
+
* Accepts a hex string (`#rgb` / `#rrggbb`) or a 256-color palette index; returns
|
|
296
|
+
* `undefined` for var refs, empty strings, or anything unparseable.
|
|
297
|
+
*/
|
|
298
|
+
export function relativeLuminance(value: string | number): number | undefined {
|
|
299
|
+
const rgb = toRgb(value);
|
|
300
|
+
if (!rgb) return undefined;
|
|
301
|
+
return 0.2126 * linearizeChannel(rgb.r) + 0.7152 * linearizeChannel(rgb.g) + 0.0722 * linearizeChannel(rgb.b);
|
|
302
|
+
}
|
package/src/peek-file.ts
CHANGED
|
@@ -112,3 +112,77 @@ export async function peekFile<T>(filePath: string, maxBytes: number, op: (heade
|
|
|
112
112
|
await fileHandle.close();
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read up to the last `maxBytes` of `filePath` and pass that slice to `op`.
|
|
118
|
+
*
|
|
119
|
+
* The tail mirror of {@link peekFile}: same pooled-buffer strategy (no per-call
|
|
120
|
+
* allocation for small reads), but the read is positioned at `size - len` so the
|
|
121
|
+
* window ends at EOF. When the file is shorter than `maxBytes`, the whole file is
|
|
122
|
+
* returned. A multi-byte codepoint straddling the leading cut decodes to a
|
|
123
|
+
* replacement char — callers that parse line-oriented tails drop the partial
|
|
124
|
+
* leading line anyway.
|
|
125
|
+
*/
|
|
126
|
+
export async function peekFileTail<T>(filePath: string, maxBytes: number, op: (tail: Uint8Array) => T): Promise<T> {
|
|
127
|
+
if (maxBytes <= 0) {
|
|
128
|
+
return op(EMPTY_BUFFER);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fileHandle = await fs.promises.open(filePath, "r");
|
|
132
|
+
try {
|
|
133
|
+
const { size } = await fileHandle.stat();
|
|
134
|
+
const len = Math.min(maxBytes, size);
|
|
135
|
+
if (len <= 0) {
|
|
136
|
+
return op(EMPTY_BUFFER);
|
|
137
|
+
}
|
|
138
|
+
return await withAsyncPoolBuffer(len, async buffer => {
|
|
139
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, buffer.byteLength, size - len);
|
|
140
|
+
return op(buffer.subarray(0, bytesRead));
|
|
141
|
+
});
|
|
142
|
+
} finally {
|
|
143
|
+
await fileHandle.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read up to the first `prefixBytes` and last `suffixBytes` of `filePath`, then
|
|
149
|
+
* pass both slices to `op`.
|
|
150
|
+
*
|
|
151
|
+
* Uses a single open/stat sequence. When the whole file fits in the head window,
|
|
152
|
+
* the tail is sliced from the already-read head bytes instead of issuing a
|
|
153
|
+
* second read.
|
|
154
|
+
*/
|
|
155
|
+
export async function peekFileEnds<T>(
|
|
156
|
+
filePath: string,
|
|
157
|
+
prefixBytes: number,
|
|
158
|
+
suffixBytes: number,
|
|
159
|
+
op: (head: Uint8Array, tail: Uint8Array) => T,
|
|
160
|
+
): Promise<T> {
|
|
161
|
+
if (prefixBytes <= 0 && suffixBytes <= 0) {
|
|
162
|
+
return op(EMPTY_BUFFER, EMPTY_BUFFER);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const fileHandle = await fs.promises.open(filePath, "r");
|
|
166
|
+
try {
|
|
167
|
+
const { size } = await fileHandle.stat();
|
|
168
|
+
const headLen = prefixBytes > 0 ? Math.min(prefixBytes, size) : 0;
|
|
169
|
+
const tailLen = suffixBytes > 0 ? Math.min(suffixBytes, size) : 0;
|
|
170
|
+
|
|
171
|
+
const head = headLen > 0 ? Buffer.allocUnsafe(headLen) : EMPTY_BUFFER;
|
|
172
|
+
const headBytesRead = headLen > 0 ? (await fileHandle.read(head, 0, head.byteLength, 0)).bytesRead : 0;
|
|
173
|
+
const headSlice = head.subarray(0, headBytesRead);
|
|
174
|
+
|
|
175
|
+
if (tailLen <= 0) {
|
|
176
|
+
return op(headSlice, EMPTY_BUFFER);
|
|
177
|
+
}
|
|
178
|
+
if (size <= headLen) {
|
|
179
|
+
return op(headSlice, headSlice.subarray(Math.max(0, headBytesRead - tailLen)));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const tail = Buffer.allocUnsafe(tailLen);
|
|
183
|
+
const { bytesRead: tailBytesRead } = await fileHandle.read(tail, 0, tail.byteLength, size - tailLen);
|
|
184
|
+
return op(headSlice, tail.subarray(0, tailBytesRead));
|
|
185
|
+
} finally {
|
|
186
|
+
await fileHandle.close();
|
|
187
|
+
}
|
|
188
|
+
}
|