compressed-context 0.1.0 → 0.1.1

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 (55) hide show
  1. package/package.json +64 -64
  2. package/src/compression/active-summary-cache.ts +50 -50
  3. package/src/compression/adopt.ts +57 -57
  4. package/src/compression/auto.ts +372 -372
  5. package/src/compression/babel.ts +85 -85
  6. package/src/compression/block.ts +25 -25
  7. package/src/compression/cache-history.ts +75 -75
  8. package/src/compression/compaction.ts +189 -189
  9. package/src/compression/decompress-tool.ts +74 -74
  10. package/src/compression/decompress.ts +292 -292
  11. package/src/compression/deleted-sessions.ts +24 -24
  12. package/src/compression/distill.ts +220 -220
  13. package/src/compression/events.ts +275 -275
  14. package/src/compression/markdown.ts +53 -53
  15. package/src/compression/normalize.ts +114 -114
  16. package/src/compression/render.ts +70 -70
  17. package/src/compression/schema.ts +84 -84
  18. package/src/compression/select.ts +123 -123
  19. package/src/compression/session-message.ts +112 -112
  20. package/src/compression/settings-rpc.ts +111 -111
  21. package/src/compression/settings.ts +175 -175
  22. package/src/compression/state.ts +248 -248
  23. package/src/compression/summarize.ts +595 -595
  24. package/src/compression/summary.ts +73 -73
  25. package/src/compression/tool.ts +142 -142
  26. package/src/compression/transcript-store.ts +258 -258
  27. package/src/compression/transcript.ts +63 -63
  28. package/src/compression/validate.ts +81 -81
  29. package/src/i18n/en.ts +93 -93
  30. package/src/i18n/index.ts +28 -28
  31. package/src/i18n/types.ts +89 -89
  32. package/src/i18n/zh.ts +93 -93
  33. package/src/index.ts +108 -108
  34. package/src/shared/blocks.ts +17 -17
  35. package/src/shared/cache.ts +199 -199
  36. package/src/shared/format.ts +23 -23
  37. package/src/shared/media.ts +184 -184
  38. package/src/shared/parse.ts +25 -25
  39. package/src/shared/parts.ts +72 -72
  40. package/src/shared/rpc.ts +145 -145
  41. package/src/shared/scan.ts +32 -32
  42. package/src/shared/settings.ts +471 -471
  43. package/src/shared/stats.ts +142 -142
  44. package/src/shared/tokens.ts +259 -259
  45. package/src/tui/chart/cache-chart.ts +237 -237
  46. package/src/tui/chart/color.ts +58 -58
  47. package/src/tui/index.tsx +111 -111
  48. package/src/tui/panel/cache.tsx +203 -203
  49. package/src/tui/panel/data.ts +105 -105
  50. package/src/tui/panel/index.tsx +176 -176
  51. package/src/tui/panel/summaries.tsx +192 -192
  52. package/src/tui/settings/dialogs.tsx +215 -215
  53. package/src/tui/settings/options.ts +238 -238
  54. package/src/tui/settings.tsx +418 -418
  55. 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
+ }