compressed-context 0.1.0 → 0.2.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.
Files changed (57) hide show
  1. package/README.md +59 -51
  2. package/package.json +64 -64
  3. package/src/compression/active-summary-cache.ts +50 -50
  4. package/src/compression/adopt.ts +57 -57
  5. package/src/compression/auto.ts +386 -372
  6. package/src/compression/babel.ts +85 -85
  7. package/src/compression/block.ts +25 -25
  8. package/src/compression/cache-history.ts +75 -75
  9. package/src/compression/compaction.ts +189 -189
  10. package/src/compression/decompress-tool.ts +74 -74
  11. package/src/compression/decompress.ts +292 -292
  12. package/src/compression/deleted-sessions.ts +24 -24
  13. package/src/compression/distill.ts +220 -220
  14. package/src/compression/events.ts +275 -275
  15. package/src/compression/markdown.ts +53 -53
  16. package/src/compression/normalize.ts +114 -114
  17. package/src/compression/render.ts +70 -70
  18. package/src/compression/schema.ts +131 -84
  19. package/src/compression/select.ts +123 -123
  20. package/src/compression/session-message.ts +112 -112
  21. package/src/compression/settings-rpc.ts +123 -111
  22. package/src/compression/settings.ts +175 -175
  23. package/src/compression/shape.ts +75 -0
  24. package/src/compression/state.ts +248 -248
  25. package/src/compression/summarize.ts +534 -595
  26. package/src/compression/summary.ts +73 -73
  27. package/src/compression/tool.ts +184 -142
  28. package/src/compression/transcript-store.ts +258 -258
  29. package/src/compression/transcript.ts +63 -63
  30. package/src/compression/validate.ts +81 -81
  31. package/src/i18n/en.ts +93 -93
  32. package/src/i18n/index.ts +28 -28
  33. package/src/i18n/types.ts +89 -89
  34. package/src/i18n/zh.ts +93 -93
  35. package/src/index.ts +116 -108
  36. package/src/shared/blocks.ts +17 -17
  37. package/src/shared/cache.ts +199 -199
  38. package/src/shared/format.ts +23 -23
  39. package/src/shared/media.ts +184 -184
  40. package/src/shared/parse.ts +25 -25
  41. package/src/shared/parts.ts +72 -72
  42. package/src/shared/rpc.ts +145 -145
  43. package/src/shared/scan.ts +32 -32
  44. package/src/shared/settings.ts +466 -471
  45. package/src/shared/stats.ts +142 -142
  46. package/src/shared/tokens.ts +259 -259
  47. package/src/tui/chart/cache-chart.ts +237 -237
  48. package/src/tui/chart/color.ts +58 -58
  49. package/src/tui/index.tsx +109 -111
  50. package/src/tui/panel/cache.tsx +203 -203
  51. package/src/tui/panel/data.ts +105 -105
  52. package/src/tui/panel/index.tsx +176 -176
  53. package/src/tui/panel/summaries.tsx +203 -192
  54. package/src/tui/settings/dialogs.tsx +215 -215
  55. package/src/tui/settings/options.ts +238 -238
  56. package/src/tui/settings.tsx +418 -418
  57. package/src/tui/types.ts +4 -4
@@ -1,237 +1,237 @@
1
- import { mix, toHex, toRgb, type Rgb } from "./color.ts"
2
-
3
- /** 侧栏里折线图的默认尺寸:28 列 × 10 行(点阵 56 × 40)。 */
4
- export const CHART_WIDTH = 28
5
- export const CHART_HEIGHT = 10
6
-
7
- /** 一个字符格:Braille 字符或空占位;空格的 color 为空串。 */
8
- export interface ChartCell {
9
- readonly char: string
10
- readonly color: string
11
- }
12
-
13
- /** 命中率渐变色需要的三个主题色。 */
14
- export interface ChartColors {
15
- readonly success: string
16
- readonly warning: string
17
- readonly error: string
18
- }
19
-
20
- /**
21
- * Braille 点阵位映射:每个字符 2 列 × 4 行。
22
- * 数组下标是 [dy][dx],值是 U+2800 的位。
23
- */
24
- const BRAILLE_BITS = [
25
- [0x01, 0x08],
26
- [0x02, 0x10],
27
- [0x04, 0x20],
28
- [0x40, 0x80],
29
- ] as const
30
-
31
- /** 空 cell 用不换行空格,防止 TUI 把连续空格吞掉。 */
32
- const EMPTY_CELL = "\u00A0"
33
-
34
- /** 网格线字符;只在没有折线点的 cell 上显示。 */
35
- const GRID_CELL = "·"
36
-
37
- /** 一条 y 轴刻度:行号 + 文本(`100%` / ` 75%` …)。 */
38
- export interface ChartAxisLabel {
39
- readonly row: number
40
- readonly text: string
41
- }
42
-
43
- /**
44
- * 输入:图表高度(行)+ 标签步长(默认 10%)+ 网格步长(默认 20%)。
45
- * 输出:100/90/…/10 的刻度行号与文本,以及 80/60/40/20 的网格行。
46
- * 预期:10 行时 10% 正好一行;行不够时同行的低刻度会被丢弃,避免标签重叠。
47
- */
48
- export function chartAxis(
49
- height: number,
50
- labelStep = 10,
51
- gridStep = 20,
52
- ): {
53
- readonly labels: readonly ChartAxisLabel[]
54
- readonly gridRows: readonly number[]
55
- } {
56
- const rows = Math.max(1, Math.floor(height))
57
- const safeLabel = Math.max(1, Math.floor(labelStep))
58
- const safeGrid = Math.max(1, Math.floor(gridStep))
59
- const seen = new Set<number>()
60
- const labels: ChartAxisLabel[] = []
61
- for (let value = 100; value > 0; value -= safeLabel) {
62
- const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
63
- if (seen.has(row)) continue
64
- seen.add(row)
65
- labels.push({ row, text: `${value.toString().padStart(3, " ")}%` })
66
- }
67
- const gridRows: number[] = []
68
- for (let value = 100 - safeGrid; value > 0; value -= safeGrid) {
69
- const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
70
- if (!gridRows.includes(row)) gridRows.push(row)
71
- }
72
- return { labels, gridRows }
73
- }
74
-
75
- /**
76
- * 输入:样本数 + 绘图区宽度。
77
- * 输出:长度等于宽度的 x 轴标签行;每个点标 `1`…`n`。
78
- * 预期:标签居中在点的列上;宽度不够时后面的标签会覆盖前面的,属于可接受降级。
79
- */
80
- export function xAxisLabels(count: number, width: number): string {
81
- const columns = Math.max(1, Math.floor(width))
82
- const chars = Array.from({ length: columns }, () => " ")
83
- if (count <= 0) return chars.join("")
84
- for (let index = 0; index < count; index += 1) {
85
- const label = (index + 1).toString()
86
- const position =
87
- count === 1 ? Math.floor((columns - 1) / 2) : Math.round((index * (columns - 1)) / (count - 1))
88
- const start = Math.min(columns - label.length, Math.max(0, position - Math.floor(label.length / 2)))
89
- for (let offset = 0; offset < label.length; offset += 1) {
90
- const target = start + offset
91
- if (target >= 0 && target < columns) chars[target] = label.charAt(offset)
92
- }
93
- }
94
- return chars.join("")
95
- }
96
-
97
- /** 输入:任意百分比;输出:夹在 0..100 的有限值。 */
98
- function clampPercent(value: number): number {
99
- if (!Number.isFinite(value)) return 0
100
- return Math.min(100, Math.max(0, value))
101
- }
102
-
103
- /**
104
- * 输入:命中率 + 主题色。
105
- * 输出:hex 颜色;0% 红、50% 黄、100% 绿,中间线性插值。
106
- */
107
- export function hitRateColor(pct: number, colors: ChartColors): string {
108
- const red = toRgb(colors.error) ?? ([239, 68, 68] as Rgb)
109
- const yellow = toRgb(colors.warning) ?? ([234, 179, 8] as Rgb)
110
- const green = toRgb(colors.success) ?? ([34, 197, 94] as Rgb)
111
- const ratio = clampPercent(pct) / 100
112
- const rgb: Rgb =
113
- ratio >= 0.5 ? mix(yellow, green, (ratio - 0.5) * 2) : mix(red, yellow, ratio * 2)
114
- return toHex(rgb)
115
- }
116
-
117
- /** 输入:点阵坐标;输出:无;预期:越界点忽略。 */
118
- function plot(points: Set<number>, width: number, height: number, x: number, y: number): void {
119
- if (x < 0 || x >= width || y < 0 || y >= height) return
120
- points.add(y * width + x)
121
- }
122
-
123
- /** 输入:两个点阵坐标;输出:无;预期:Bresenham 直线写入 points。 */
124
- function line(
125
- points: Set<number>,
126
- width: number,
127
- height: number,
128
- x0: number,
129
- y0: number,
130
- x1: number,
131
- y1: number,
132
- ): void {
133
- const dx = Math.abs(x1 - x0)
134
- const sx = x0 < x1 ? 1 : -1
135
- const dy = -Math.abs(y1 - y0)
136
- const sy = y0 < y1 ? 1 : -1
137
- let error = dx + dy
138
- let x = x0
139
- let y = y0
140
- for (;;) {
141
- plot(points, width, height, x, y)
142
- if (x === x1 && y === y1) return
143
- const doubled = 2 * error
144
- if (doubled >= dy) {
145
- error += dy
146
- x += sx
147
- }
148
- if (doubled <= dx) {
149
- error += dx
150
- y += sy
151
- }
152
- }
153
- }
154
-
155
- /** 输入:列号 + 宽度 + 原始值;输出:该列中心插值出的百分比。 */
156
- function valueAtColumn(column: number, width: number, values: readonly number[]): number {
157
- if (values.length === 1) return clampPercent(values[0] ?? 0)
158
- const ratio = (column + 0.5) / width
159
- const position = ratio * (values.length - 1)
160
- const left = Math.floor(position)
161
- const right = Math.min(values.length - 1, left + 1)
162
- const fraction = position - left
163
- const a = clampPercent(values[left] ?? 0)
164
- const b = clampPercent(values[right] ?? a)
165
- return a + (b - a) * fraction
166
- }
167
-
168
- /**
169
- * 输入:命中率序列(旧 → 新)、宽、高、逐列颜色函数。
170
- * 输出:按行排列的 Braille 折线;空 cell 是 NBSP。
171
- * 预期:0% 在底行,100% 在顶行;单点画水平线;空数组返回空。
172
- */
173
- export function renderBrailleChart(
174
- values: readonly number[],
175
- width: number = CHART_WIDTH,
176
- height: number = CHART_HEIGHT,
177
- color: (pct: number) => string = () => "",
178
- gridRows: readonly number[] = [],
179
- gridColor = "",
180
- ): readonly (readonly ChartCell[])[] {
181
- if (values.length === 0) return []
182
- const columns = Math.max(1, Math.floor(width))
183
- const rows = Math.max(1, Math.floor(height))
184
- const dotWidth = columns * 2
185
- const dotHeight = rows * 4
186
- const points = new Set<number>()
187
- // 上下各留 1 个点阵行,避免 0%/100% 贴边看起来越过刻度。
188
- const top = 1
189
- const bottom = Math.max(top, dotHeight - 2)
190
- const yOf = (value: number): number =>
191
- Math.min(
192
- bottom,
193
- Math.max(top, Math.round(top + (1 - clampPercent(value) / 100) * (bottom - top))),
194
- )
195
-
196
- if (values.length === 1) {
197
- const value = values[0] ?? 0
198
- line(points, dotWidth, dotHeight, 0, yOf(value), dotWidth - 1, yOf(value))
199
- } else {
200
- for (let index = 0; index < values.length - 1; index += 1) {
201
- const x0 = Math.round((index * (dotWidth - 1)) / (values.length - 1))
202
- const x1 = Math.round(((index + 1) * (dotWidth - 1)) / (values.length - 1))
203
- line(
204
- points,
205
- dotWidth,
206
- dotHeight,
207
- x0,
208
- yOf(values[index] ?? 0),
209
- x1,
210
- yOf(values[index + 1] ?? 0),
211
- )
212
- }
213
- }
214
-
215
- const result: ChartCell[][] = []
216
- for (let row = 0; row < rows; row += 1) {
217
- const cells: ChartCell[] = []
218
- for (let column = 0; column < columns; column += 1) {
219
- let bits = 0
220
- for (let dy = 0; dy < 4; dy += 1) {
221
- for (let dx = 0; dx < 2; dx += 1) {
222
- if (points.has((row * 4 + dy) * dotWidth + (column * 2 + dx))) {
223
- bits |= BRAILLE_BITS[dy]?.[dx] ?? 0
224
- }
225
- }
226
- }
227
- const pct = valueAtColumn(column, columns, values)
228
- const grid = bits === 0 && gridRows.includes(row)
229
- cells.push({
230
- char: bits === 0 ? (grid ? GRID_CELL : EMPTY_CELL) : String.fromCharCode(0x2800 + bits),
231
- color: bits === 0 ? (grid ? gridColor : "") : color(pct),
232
- })
233
- }
234
- result.push(cells)
235
- }
236
- return result
237
- }
1
+ import { mix, toHex, toRgb, type Rgb } from "./color.ts"
2
+
3
+ /** 侧栏里折线图的默认尺寸:28 列 × 10 行(点阵 56 × 40)。 */
4
+ export const CHART_WIDTH = 28
5
+ export const CHART_HEIGHT = 10
6
+
7
+ /** 一个字符格:Braille 字符或空占位;空格的 color 为空串。 */
8
+ export interface ChartCell {
9
+ readonly char: string
10
+ readonly color: string
11
+ }
12
+
13
+ /** 命中率渐变色需要的三个主题色。 */
14
+ export interface ChartColors {
15
+ readonly success: string
16
+ readonly warning: string
17
+ readonly error: string
18
+ }
19
+
20
+ /**
21
+ * Braille 点阵位映射:每个字符 2 列 × 4 行。
22
+ * 数组下标是 [dy][dx],值是 U+2800 的位。
23
+ */
24
+ const BRAILLE_BITS = [
25
+ [0x01, 0x08],
26
+ [0x02, 0x10],
27
+ [0x04, 0x20],
28
+ [0x40, 0x80],
29
+ ] as const
30
+
31
+ /** 空 cell 用不换行空格,防止 TUI 把连续空格吞掉。 */
32
+ const EMPTY_CELL = "\u00A0"
33
+
34
+ /** 网格线字符;只在没有折线点的 cell 上显示。 */
35
+ const GRID_CELL = "·"
36
+
37
+ /** 一条 y 轴刻度:行号 + 文本(`100%` / ` 75%` …)。 */
38
+ export interface ChartAxisLabel {
39
+ readonly row: number
40
+ readonly text: string
41
+ }
42
+
43
+ /**
44
+ * 输入:图表高度(行)+ 标签步长(默认 10%)+ 网格步长(默认 20%)。
45
+ * 输出:100/90/…/10 的刻度行号与文本,以及 80/60/40/20 的网格行。
46
+ * 预期:10 行时 10% 正好一行;行不够时同行的低刻度会被丢弃,避免标签重叠。
47
+ */
48
+ export function chartAxis(
49
+ height: number,
50
+ labelStep = 10,
51
+ gridStep = 20,
52
+ ): {
53
+ readonly labels: readonly ChartAxisLabel[]
54
+ readonly gridRows: readonly number[]
55
+ } {
56
+ const rows = Math.max(1, Math.floor(height))
57
+ const safeLabel = Math.max(1, Math.floor(labelStep))
58
+ const safeGrid = Math.max(1, Math.floor(gridStep))
59
+ const seen = new Set<number>()
60
+ const labels: ChartAxisLabel[] = []
61
+ for (let value = 100; value > 0; value -= safeLabel) {
62
+ const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
63
+ if (seen.has(row)) continue
64
+ seen.add(row)
65
+ labels.push({ row, text: `${value.toString().padStart(3, " ")}%` })
66
+ }
67
+ const gridRows: number[] = []
68
+ for (let value = 100 - safeGrid; value > 0; value -= safeGrid) {
69
+ const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
70
+ if (!gridRows.includes(row)) gridRows.push(row)
71
+ }
72
+ return { labels, gridRows }
73
+ }
74
+
75
+ /**
76
+ * 输入:样本数 + 绘图区宽度。
77
+ * 输出:长度等于宽度的 x 轴标签行;每个点标 `1`…`n`。
78
+ * 预期:标签居中在点的列上;宽度不够时后面的标签会覆盖前面的,属于可接受降级。
79
+ */
80
+ export function xAxisLabels(count: number, width: number): string {
81
+ const columns = Math.max(1, Math.floor(width))
82
+ const chars = Array.from({ length: columns }, () => " ")
83
+ if (count <= 0) return chars.join("")
84
+ for (let index = 0; index < count; index += 1) {
85
+ const label = (index + 1).toString()
86
+ const position =
87
+ count === 1 ? Math.floor((columns - 1) / 2) : Math.round((index * (columns - 1)) / (count - 1))
88
+ const start = Math.min(columns - label.length, Math.max(0, position - Math.floor(label.length / 2)))
89
+ for (let offset = 0; offset < label.length; offset += 1) {
90
+ const target = start + offset
91
+ if (target >= 0 && target < columns) chars[target] = label.charAt(offset)
92
+ }
93
+ }
94
+ return chars.join("")
95
+ }
96
+
97
+ /** 输入:任意百分比;输出:夹在 0..100 的有限值。 */
98
+ function clampPercent(value: number): number {
99
+ if (!Number.isFinite(value)) return 0
100
+ return Math.min(100, Math.max(0, value))
101
+ }
102
+
103
+ /**
104
+ * 输入:命中率 + 主题色。
105
+ * 输出:hex 颜色;0% 红、50% 黄、100% 绿,中间线性插值。
106
+ */
107
+ export function hitRateColor(pct: number, colors: ChartColors): string {
108
+ const red = toRgb(colors.error) ?? ([239, 68, 68] as Rgb)
109
+ const yellow = toRgb(colors.warning) ?? ([234, 179, 8] as Rgb)
110
+ const green = toRgb(colors.success) ?? ([34, 197, 94] as Rgb)
111
+ const ratio = clampPercent(pct) / 100
112
+ const rgb: Rgb =
113
+ ratio >= 0.5 ? mix(yellow, green, (ratio - 0.5) * 2) : mix(red, yellow, ratio * 2)
114
+ return toHex(rgb)
115
+ }
116
+
117
+ /** 输入:点阵坐标;输出:无;预期:越界点忽略。 */
118
+ function plot(points: Set<number>, width: number, height: number, x: number, y: number): void {
119
+ if (x < 0 || x >= width || y < 0 || y >= height) return
120
+ points.add(y * width + x)
121
+ }
122
+
123
+ /** 输入:两个点阵坐标;输出:无;预期:Bresenham 直线写入 points。 */
124
+ function line(
125
+ points: Set<number>,
126
+ width: number,
127
+ height: number,
128
+ x0: number,
129
+ y0: number,
130
+ x1: number,
131
+ y1: number,
132
+ ): void {
133
+ const dx = Math.abs(x1 - x0)
134
+ const sx = x0 < x1 ? 1 : -1
135
+ const dy = -Math.abs(y1 - y0)
136
+ const sy = y0 < y1 ? 1 : -1
137
+ let error = dx + dy
138
+ let x = x0
139
+ let y = y0
140
+ for (;;) {
141
+ plot(points, width, height, x, y)
142
+ if (x === x1 && y === y1) return
143
+ const doubled = 2 * error
144
+ if (doubled >= dy) {
145
+ error += dy
146
+ x += sx
147
+ }
148
+ if (doubled <= dx) {
149
+ error += dx
150
+ y += sy
151
+ }
152
+ }
153
+ }
154
+
155
+ /** 输入:列号 + 宽度 + 原始值;输出:该列中心插值出的百分比。 */
156
+ function valueAtColumn(column: number, width: number, values: readonly number[]): number {
157
+ if (values.length === 1) return clampPercent(values[0] ?? 0)
158
+ const ratio = (column + 0.5) / width
159
+ const position = ratio * (values.length - 1)
160
+ const left = Math.floor(position)
161
+ const right = Math.min(values.length - 1, left + 1)
162
+ const fraction = position - left
163
+ const a = clampPercent(values[left] ?? 0)
164
+ const b = clampPercent(values[right] ?? a)
165
+ return a + (b - a) * fraction
166
+ }
167
+
168
+ /**
169
+ * 输入:命中率序列(旧 → 新)、宽、高、逐列颜色函数。
170
+ * 输出:按行排列的 Braille 折线;空 cell 是 NBSP。
171
+ * 预期:0% 在底行,100% 在顶行;单点画水平线;空数组返回空。
172
+ */
173
+ export function renderBrailleChart(
174
+ values: readonly number[],
175
+ width: number = CHART_WIDTH,
176
+ height: number = CHART_HEIGHT,
177
+ color: (pct: number) => string = () => "",
178
+ gridRows: readonly number[] = [],
179
+ gridColor = "",
180
+ ): readonly (readonly ChartCell[])[] {
181
+ if (values.length === 0) return []
182
+ const columns = Math.max(1, Math.floor(width))
183
+ const rows = Math.max(1, Math.floor(height))
184
+ const dotWidth = columns * 2
185
+ const dotHeight = rows * 4
186
+ const points = new Set<number>()
187
+ // 上下各留 1 个点阵行,避免 0%/100% 贴边看起来越过刻度。
188
+ const top = 1
189
+ const bottom = Math.max(top, dotHeight - 2)
190
+ const yOf = (value: number): number =>
191
+ Math.min(
192
+ bottom,
193
+ Math.max(top, Math.round(top + (1 - clampPercent(value) / 100) * (bottom - top))),
194
+ )
195
+
196
+ if (values.length === 1) {
197
+ const value = values[0] ?? 0
198
+ line(points, dotWidth, dotHeight, 0, yOf(value), dotWidth - 1, yOf(value))
199
+ } else {
200
+ for (let index = 0; index < values.length - 1; index += 1) {
201
+ const x0 = Math.round((index * (dotWidth - 1)) / (values.length - 1))
202
+ const x1 = Math.round(((index + 1) * (dotWidth - 1)) / (values.length - 1))
203
+ line(
204
+ points,
205
+ dotWidth,
206
+ dotHeight,
207
+ x0,
208
+ yOf(values[index] ?? 0),
209
+ x1,
210
+ yOf(values[index + 1] ?? 0),
211
+ )
212
+ }
213
+ }
214
+
215
+ const result: ChartCell[][] = []
216
+ for (let row = 0; row < rows; row += 1) {
217
+ const cells: ChartCell[] = []
218
+ for (let column = 0; column < columns; column += 1) {
219
+ let bits = 0
220
+ for (let dy = 0; dy < 4; dy += 1) {
221
+ for (let dx = 0; dx < 2; dx += 1) {
222
+ if (points.has((row * 4 + dy) * dotWidth + (column * 2 + dx))) {
223
+ bits |= BRAILLE_BITS[dy]?.[dx] ?? 0
224
+ }
225
+ }
226
+ }
227
+ const pct = valueAtColumn(column, columns, values)
228
+ const grid = bits === 0 && gridRows.includes(row)
229
+ cells.push({
230
+ char: bits === 0 ? (grid ? GRID_CELL : EMPTY_CELL) : String.fromCharCode(0x2800 + bits),
231
+ color: bits === 0 ? (grid ? gridColor : "") : color(pct),
232
+ })
233
+ }
234
+ result.push(cells)
235
+ }
236
+ return result
237
+ }
@@ -1,58 +1,58 @@
1
- /** 颜色三元组,分量 0-255。 */
2
- export type Rgb = readonly [number, number, number]
3
-
4
- /** 输入:0-255 分量;输出:两位十六进制。 */
5
- function channel(value: number): string {
6
- return Math.round(value).toString(16).padStart(2, "0")
7
- }
8
-
9
- /** 输入:颜色值;输出:RGB;不是 hex 返回 undefined。 */
10
- export function parseHex(value: string): Rgb | undefined {
11
- const match = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(value.trim())
12
- const raw = match?.[1]
13
- if (raw === undefined) return undefined
14
- if (raw.length === 3 || raw.length === 4) {
15
- return [
16
- parseInt(raw.charAt(0).repeat(2), 16),
17
- parseInt(raw.charAt(1).repeat(2), 16),
18
- parseInt(raw.charAt(2).repeat(2), 16),
19
- ]
20
- }
21
- return [
22
- parseInt(raw.slice(0, 2), 16),
23
- parseInt(raw.slice(2, 4), 16),
24
- parseInt(raw.slice(4, 6), 16),
25
- ]
26
- }
27
-
28
- /** 输入:两个颜色 + 0-1 比例;输出:线性插值后的颜色。 */
29
- export function mix(from: Rgb, to: Rgb, ratio: number): Rgb {
30
- return [
31
- from[0] + (to[0] - from[0]) * ratio,
32
- from[1] + (to[1] - from[1]) * ratio,
33
- from[2] + (to[2] - from[2]) * ratio,
34
- ]
35
- }
36
-
37
- /** 输入:RGB;输出:hex 字符串。 */
38
- export function toHex(rgb: Rgb): string {
39
- return `#${channel(rgb[0])}${channel(rgb[1])}${channel(rgb[2])}`
40
- }
41
-
42
- /** 输入:颜色值(hex 字符串或 OpenTUI RGBA);输出:RGB。 */
43
- export function toRgb(value: unknown): Rgb | undefined {
44
- if (typeof value === "string") return parseHex(value)
45
- if (typeof value === "object" && value !== null) {
46
- const color = value as { readonly r?: unknown; readonly g?: unknown; readonly b?: unknown }
47
- if (typeof color.r === "number" && typeof color.g === "number" && typeof color.b === "number") {
48
- // OpenTUI 的 RGBA.r/g/b 是 0-1 归一化值,这里转成 0-255。
49
- return [color.r * 255, color.g * 255, color.b * 255]
50
- }
51
- }
52
- return undefined
53
- }
54
-
55
- /** 输入:主题色 + 回退 RGB;输出:hex 字符串。 */
56
- export function themeHex(value: unknown, fallback: Rgb): string {
57
- return toHex(toRgb(value) ?? fallback)
58
- }
1
+ /** 颜色三元组,分量 0-255。 */
2
+ export type Rgb = readonly [number, number, number]
3
+
4
+ /** 输入:0-255 分量;输出:两位十六进制。 */
5
+ function channel(value: number): string {
6
+ return Math.round(value).toString(16).padStart(2, "0")
7
+ }
8
+
9
+ /** 输入:颜色值;输出:RGB;不是 hex 返回 undefined。 */
10
+ export function parseHex(value: string): Rgb | undefined {
11
+ const match = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(value.trim())
12
+ const raw = match?.[1]
13
+ if (raw === undefined) return undefined
14
+ if (raw.length === 3 || raw.length === 4) {
15
+ return [
16
+ parseInt(raw.charAt(0).repeat(2), 16),
17
+ parseInt(raw.charAt(1).repeat(2), 16),
18
+ parseInt(raw.charAt(2).repeat(2), 16),
19
+ ]
20
+ }
21
+ return [
22
+ parseInt(raw.slice(0, 2), 16),
23
+ parseInt(raw.slice(2, 4), 16),
24
+ parseInt(raw.slice(4, 6), 16),
25
+ ]
26
+ }
27
+
28
+ /** 输入:两个颜色 + 0-1 比例;输出:线性插值后的颜色。 */
29
+ export function mix(from: Rgb, to: Rgb, ratio: number): Rgb {
30
+ return [
31
+ from[0] + (to[0] - from[0]) * ratio,
32
+ from[1] + (to[1] - from[1]) * ratio,
33
+ from[2] + (to[2] - from[2]) * ratio,
34
+ ]
35
+ }
36
+
37
+ /** 输入:RGB;输出:hex 字符串。 */
38
+ export function toHex(rgb: Rgb): string {
39
+ return `#${channel(rgb[0])}${channel(rgb[1])}${channel(rgb[2])}`
40
+ }
41
+
42
+ /** 输入:颜色值(hex 字符串或 OpenTUI RGBA);输出:RGB。 */
43
+ export function toRgb(value: unknown): Rgb | undefined {
44
+ if (typeof value === "string") return parseHex(value)
45
+ if (typeof value === "object" && value !== null) {
46
+ const color = value as { readonly r?: unknown; readonly g?: unknown; readonly b?: unknown }
47
+ if (typeof color.r === "number" && typeof color.g === "number" && typeof color.b === "number") {
48
+ // OpenTUI 的 RGBA.r/g/b 是 0-1 归一化值,这里转成 0-255。
49
+ return [color.r * 255, color.g * 255, color.b * 255]
50
+ }
51
+ }
52
+ return undefined
53
+ }
54
+
55
+ /** 输入:主题色 + 回退 RGB;输出:hex 字符串。 */
56
+ export function themeHex(value: unknown, fallback: Rgb): string {
57
+ return toHex(toRgb(value) ?? fallback)
58
+ }