dsh-live-trace 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 +920 -0
- package/README.zh.md +790 -0
- package/assets/rain.ogg +0 -0
- package/bin/dsh-glyph-probe.js +51 -0
- package/bin/dsh-live-trace.js +29 -0
- package/bin/dsh-live-working.js +14 -0
- package/cordis.patch.yml +31 -0
- package/icon.svg +12 -0
- package/index.js +328 -0
- package/lib/client.js +178 -0
- package/lib/instance.js +68 -0
- package/lib/normalize.js +850 -0
- package/lib/paths.js +66 -0
- package/lib/protocol.js +115 -0
- package/lib/registry.js +232 -0
- package/lib/tools.js +257 -0
- package/lib/tracker.js +648 -0
- package/lib/transport.js +231 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -0
- package/picture/call1.png +0 -0
- package/picture/call2.png +0 -0
- package/picture/sleep1.png +0 -0
- package/picture/sleep2.png +0 -0
- package/picture/tui1.png +0 -0
- package/picture/tui2.png +0 -0
- package/picture/type1.png +0 -0
- package/picture/type2.png +0 -0
- package/scripts/bench-render.mjs +69 -0
- package/scripts/demo-working.mjs +130 -0
- package/scripts/demo.mjs +284 -0
- package/scripts/install-profile.mjs +174 -0
- package/scripts/mock-provider.mjs +211 -0
- package/src/cli/cellsize.js +120 -0
- package/src/cli/format.js +73 -0
- package/src/cli/highlight.js +932 -0
- package/src/cli/i18n.js +457 -0
- package/src/cli/main.js +630 -0
- package/src/cli/markdown.js +753 -0
- package/src/cli/renderer.js +1044 -0
- package/src/cli/screen.js +270 -0
- package/src/cli/theme.js +221 -0
- package/src/cli/view-state.js +396 -0
- package/src/cli/views.js +406 -0
- package/src/cli/width.js +337 -0
- package/src/cli/working/art.js +413 -0
- package/src/cli/working/main.js +569 -0
- package/src/cli/working/packing.js +159 -0
- package/src/cli/working/picker.js +75 -0
- package/src/cli/working/props.js +385 -0
- package/src/cli/working/scene.js +837 -0
- package/src/cli/working/sky.js +641 -0
- package/src/cli/working/sound.js +400 -0
- package/src/cli/working/state.js +528 -0
package/src/cli/width.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal text measurement and layout primitives.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard must align in a terminal that may be 80 columns wide and may
|
|
5
|
+
* carry CJK text, so measuring a string with `.length` is wrong twice over:
|
|
6
|
+
* Han characters occupy two cells, and a model's output can contain escape
|
|
7
|
+
* sequences that would otherwise corrupt the screen. Everything here works on
|
|
8
|
+
* plain code points and on styled *segment* lists, so colors never have to be
|
|
9
|
+
* parsed back out of a string.
|
|
10
|
+
*
|
|
11
|
+
* A row is `Segment[]`; a {@link Segment} is `{ text, sgr? }` where `sgr` is a
|
|
12
|
+
* raw SGR parameter list such as `"1;38;5;39"`.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-live-trace/width
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{ text: string, sgr?: string }} Segment
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Ranges whose code points occupy two terminal cells. */
|
|
22
|
+
const WIDE_RANGES = [
|
|
23
|
+
[0x1100, 0x115f],
|
|
24
|
+
[0x2e80, 0x303e],
|
|
25
|
+
[0x3041, 0x33ff],
|
|
26
|
+
[0x3400, 0x4dbf],
|
|
27
|
+
[0x4e00, 0x9fff],
|
|
28
|
+
[0xa000, 0xa4cf],
|
|
29
|
+
[0xa960, 0xa97f],
|
|
30
|
+
[0xac00, 0xd7a3],
|
|
31
|
+
[0xf900, 0xfaff],
|
|
32
|
+
[0xfe10, 0xfe19],
|
|
33
|
+
[0xfe30, 0xfe6f],
|
|
34
|
+
[0xff00, 0xff60],
|
|
35
|
+
[0xffe0, 0xffe6],
|
|
36
|
+
// Emoji_Presentation code points that terminals render double width.
|
|
37
|
+
[0x231a, 0x231b],
|
|
38
|
+
[0x23e9, 0x23ec],
|
|
39
|
+
[0x23f0, 0x23f0],
|
|
40
|
+
[0x23f3, 0x23f3],
|
|
41
|
+
[0x25fd, 0x25fe],
|
|
42
|
+
[0x2614, 0x2615],
|
|
43
|
+
[0x2648, 0x2653],
|
|
44
|
+
[0x267f, 0x267f],
|
|
45
|
+
[0x2693, 0x2693],
|
|
46
|
+
[0x26aa, 0x26ab],
|
|
47
|
+
[0x26bd, 0x26be],
|
|
48
|
+
[0x26c4, 0x26c5],
|
|
49
|
+
[0x26ce, 0x26ce],
|
|
50
|
+
[0x26d4, 0x26d4],
|
|
51
|
+
[0x26ea, 0x26ea],
|
|
52
|
+
[0x26f2, 0x26f3],
|
|
53
|
+
[0x26f5, 0x26f5],
|
|
54
|
+
[0x26fa, 0x26fa],
|
|
55
|
+
[0x26fd, 0x26fd],
|
|
56
|
+
[0x2705, 0x2705],
|
|
57
|
+
[0x270a, 0x270b],
|
|
58
|
+
[0x2728, 0x2728],
|
|
59
|
+
[0x274c, 0x274c],
|
|
60
|
+
[0x274e, 0x274e],
|
|
61
|
+
[0x2753, 0x2755],
|
|
62
|
+
[0x2757, 0x2757],
|
|
63
|
+
[0x2795, 0x2797],
|
|
64
|
+
[0x27b0, 0x27b0],
|
|
65
|
+
[0x27bf, 0x27bf],
|
|
66
|
+
[0x2b1b, 0x2b1c],
|
|
67
|
+
[0x2b50, 0x2b50],
|
|
68
|
+
[0x2b55, 0x2b55],
|
|
69
|
+
[0x1f300, 0x1f64f],
|
|
70
|
+
[0x1f680, 0x1f6ff],
|
|
71
|
+
[0x1f7e0, 0x1f7eb],
|
|
72
|
+
[0x1f900, 0x1f9ff],
|
|
73
|
+
[0x1fa70, 0x1faff],
|
|
74
|
+
[0x20000, 0x2fffd],
|
|
75
|
+
[0x30000, 0x3fffd]
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/** Ranges of zero-width marks that attach to the preceding cell. */
|
|
79
|
+
const ZERO_RANGES = [
|
|
80
|
+
[0x0300, 0x036f],
|
|
81
|
+
[0x0483, 0x0489],
|
|
82
|
+
[0x0591, 0x05bd],
|
|
83
|
+
[0x0610, 0x061a],
|
|
84
|
+
[0x064b, 0x065f],
|
|
85
|
+
[0x0e31, 0x0e31],
|
|
86
|
+
[0x0e34, 0x0e3a],
|
|
87
|
+
[0x0eb1, 0x0eb1],
|
|
88
|
+
[0x1ab0, 0x1aff],
|
|
89
|
+
[0x1dc0, 0x1dff],
|
|
90
|
+
[0x200b, 0x200f],
|
|
91
|
+
[0x20d0, 0x20ff],
|
|
92
|
+
[0xfe00, 0xfe0f],
|
|
93
|
+
[0xfe20, 0xfe2f],
|
|
94
|
+
[0xfeff, 0xfeff],
|
|
95
|
+
[0xe0100, 0xe01ef]
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
function inRanges(codePoint, ranges) {
|
|
99
|
+
for (const [start, end] of ranges) {
|
|
100
|
+
if (codePoint >= start && codePoint <= end) return true
|
|
101
|
+
}
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Terminal cells one code point occupies: 0 for combining marks and zero-width
|
|
107
|
+
* joiners, 2 for East Asian Wide/Fullwidth and emoji, 1 otherwise.
|
|
108
|
+
* @param {number} codePoint
|
|
109
|
+
* @returns {number}
|
|
110
|
+
*/
|
|
111
|
+
export function codePointWidth(codePoint) {
|
|
112
|
+
if (!Number.isFinite(codePoint) || codePoint < 0) return 0
|
|
113
|
+
if (codePoint === 0) return 0
|
|
114
|
+
if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0)) return 0
|
|
115
|
+
if (inRanges(codePoint, ZERO_RANGES)) return 0
|
|
116
|
+
if (inRanges(codePoint, WIDE_RANGES)) return 2
|
|
117
|
+
return 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Display width of plain text.
|
|
122
|
+
* @param {string} text
|
|
123
|
+
* @returns {number}
|
|
124
|
+
*/
|
|
125
|
+
export function displayWidth(text) {
|
|
126
|
+
if (typeof text !== 'string' || text.length === 0) return 0
|
|
127
|
+
let width = 0
|
|
128
|
+
for (const character of text) width += codePointWidth(character.codePointAt(0) ?? 0)
|
|
129
|
+
return width
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove anything that would let untrusted model output drive the terminal.
|
|
134
|
+
*
|
|
135
|
+
* ANSI escape sequences are stripped outright, then remaining C0/C1 controls
|
|
136
|
+
* are replaced with a space so they cannot move the cursor or beep.
|
|
137
|
+
*
|
|
138
|
+
* @param {unknown} input
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function sanitize(input) {
|
|
142
|
+
if (input === null || input === undefined) return ''
|
|
143
|
+
let text = String(input)
|
|
144
|
+
// CSI / OSC / DCS / APC / PM / SOS and the simple two-byte escapes.
|
|
145
|
+
text = text.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '')
|
|
146
|
+
text = text.replace(/\u001b[P^_X][^\u001b]*(?:\u001b\\)?/g, '')
|
|
147
|
+
text = text.replace(/\u001b\[[0-9;?<>=]*[ -/]*[@-~]/g, '')
|
|
148
|
+
text = text.replace(/\u001b[@-Z\\-_]/g, '')
|
|
149
|
+
// eslint-disable-next-line no-control-regex
|
|
150
|
+
text = text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, ' ')
|
|
151
|
+
return text
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Split styled segments into one entry per code point, preserving style.
|
|
156
|
+
* @param {Segment[]} segments
|
|
157
|
+
* @returns {Array<{ ch: string, sgr?: string }>}
|
|
158
|
+
*/
|
|
159
|
+
export function explode(segments) {
|
|
160
|
+
const out = []
|
|
161
|
+
for (const segment of segments) {
|
|
162
|
+
if (segment === null || segment === undefined) continue
|
|
163
|
+
const text = sanitize(segment.text)
|
|
164
|
+
if (text.length === 0) continue
|
|
165
|
+
for (const character of text) out.push({ ch: character, sgr: segment.sgr })
|
|
166
|
+
}
|
|
167
|
+
return out
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Re-join per-code-point entries into the fewest segments.
|
|
172
|
+
* @param {Array<{ ch: string, sgr?: string }>} chars
|
|
173
|
+
* @returns {Segment[]}
|
|
174
|
+
*/
|
|
175
|
+
export function collapse(chars) {
|
|
176
|
+
const out = []
|
|
177
|
+
for (const item of chars) {
|
|
178
|
+
const last = out[out.length - 1]
|
|
179
|
+
if (last !== undefined && last.sgr === item.sgr) last.text += item.ch
|
|
180
|
+
else out.push({ text: item.ch, sgr: item.sgr })
|
|
181
|
+
}
|
|
182
|
+
return out
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Display width of a row.
|
|
187
|
+
* @param {Segment[]} segments
|
|
188
|
+
* @returns {number}
|
|
189
|
+
*/
|
|
190
|
+
export function rowWidth(segments) {
|
|
191
|
+
let width = 0
|
|
192
|
+
for (const segment of segments ?? []) {
|
|
193
|
+
if (segment === null || segment === undefined) continue
|
|
194
|
+
width += displayWidth(sanitize(segment.text))
|
|
195
|
+
}
|
|
196
|
+
return width
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Truncate a plain string to a cell budget.
|
|
201
|
+
* @param {string} text
|
|
202
|
+
* @param {number} width
|
|
203
|
+
* @param {string} [ellipsis]
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
export function truncate(text, width, ellipsis = '…') {
|
|
207
|
+
return truncateSegments([{ text }], width, ellipsis)
|
|
208
|
+
.map((segment) => segment.text)
|
|
209
|
+
.join('')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Truncate a row to a cell budget, appending an ellipsis when it was cut.
|
|
214
|
+
* @param {Segment[]} segments
|
|
215
|
+
* @param {number} width
|
|
216
|
+
* @param {string} [ellipsis]
|
|
217
|
+
* @returns {Segment[]}
|
|
218
|
+
*/
|
|
219
|
+
export function truncateSegments(segments, width, ellipsis = '…') {
|
|
220
|
+
if (width <= 0) return []
|
|
221
|
+
const chars = explode(segments)
|
|
222
|
+
let total = 0
|
|
223
|
+
for (const item of chars) total += codePointWidth(item.ch.codePointAt(0) ?? 0)
|
|
224
|
+
if (total <= width) return collapse(chars)
|
|
225
|
+
const budget = Math.max(0, width - displayWidth(ellipsis))
|
|
226
|
+
const kept = []
|
|
227
|
+
let used = 0
|
|
228
|
+
for (const item of chars) {
|
|
229
|
+
const itemWidth = codePointWidth(item.ch.codePointAt(0) ?? 0)
|
|
230
|
+
if (used + itemWidth > budget) break
|
|
231
|
+
kept.push(item)
|
|
232
|
+
used += itemWidth
|
|
233
|
+
}
|
|
234
|
+
const result = collapse(kept)
|
|
235
|
+
if (displayWidth(ellipsis) > 0) result.push({ text: ellipsis, sgr: kept[kept.length - 1]?.sgr })
|
|
236
|
+
return result
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Wrap a row into lines no wider than `width`.
|
|
241
|
+
*
|
|
242
|
+
* Breaks at a space when one is available on the line; otherwise hard-breaks,
|
|
243
|
+
* which is the correct behavior for CJK text that carries no spaces.
|
|
244
|
+
*
|
|
245
|
+
* @param {Segment[]} segments
|
|
246
|
+
* @param {number} width
|
|
247
|
+
* @returns {Segment[][]}
|
|
248
|
+
*/
|
|
249
|
+
export function wrapSegments(segments, width) {
|
|
250
|
+
if (width <= 0) return [segments]
|
|
251
|
+
const chars = explode(segments)
|
|
252
|
+
/** @type {Segment[][]} */
|
|
253
|
+
const lines = []
|
|
254
|
+
/** @type {Array<{ ch: string, sgr?: string }>} */
|
|
255
|
+
let line = []
|
|
256
|
+
let lineWidth = 0
|
|
257
|
+
let lastSpace = -1
|
|
258
|
+
|
|
259
|
+
const widthUpTo = (list) => {
|
|
260
|
+
let width = 0
|
|
261
|
+
for (const item of list) width += codePointWidth(item.ch.codePointAt(0) ?? 0)
|
|
262
|
+
return width
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const breakLine = () => {
|
|
266
|
+
if (lastSpace > 0) {
|
|
267
|
+
lines.push(collapse(line.slice(0, lastSpace)))
|
|
268
|
+
line = line.slice(lastSpace + 1)
|
|
269
|
+
} else {
|
|
270
|
+
lines.push(collapse(line))
|
|
271
|
+
line = []
|
|
272
|
+
}
|
|
273
|
+
lineWidth = widthUpTo(line)
|
|
274
|
+
lastSpace = -1
|
|
275
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
276
|
+
if (line[index].ch === ' ') lastSpace = index
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const item of chars) {
|
|
281
|
+
const itemWidth = codePointWidth(item.ch.codePointAt(0) ?? 0)
|
|
282
|
+
// A character wider than the whole budget cannot be displayed at all; the
|
|
283
|
+
// only alternatives are overflowing the line or dropping it, and the
|
|
284
|
+
// renderer's contract is that every produced line fits.
|
|
285
|
+
if (itemWidth > width) continue
|
|
286
|
+
if (line.length > 0 && lineWidth + itemWidth > width) breakLine()
|
|
287
|
+
line.push(item)
|
|
288
|
+
lineWidth += itemWidth
|
|
289
|
+
if (item.ch === ' ') lastSpace = line.length - 1
|
|
290
|
+
}
|
|
291
|
+
if (line.length > 0 || lines.length === 0) lines.push(collapse(line))
|
|
292
|
+
return lines
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Serialize a row into an ANSI string.
|
|
297
|
+
* @param {Segment[]} segments
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
export function renderRow(segments) {
|
|
301
|
+
let out = ''
|
|
302
|
+
for (const segment of segments ?? []) {
|
|
303
|
+
if (segment === null || segment === undefined || segment.text.length === 0) continue
|
|
304
|
+
if (segment.sgr === undefined || segment.sgr === '') out += segment.text
|
|
305
|
+
else out += `\u001b[${segment.sgr}m${segment.text}\u001b[0m`
|
|
306
|
+
}
|
|
307
|
+
return out
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Right-pad a row to a full cell width with a background style.
|
|
312
|
+
* @param {Segment[]} segments
|
|
313
|
+
* @param {number} width
|
|
314
|
+
* @param {string} [fillSgr]
|
|
315
|
+
* @returns {Segment[]}
|
|
316
|
+
*/
|
|
317
|
+
export function padRow(segments, width, fillSgr) {
|
|
318
|
+
const used = rowWidth(segments)
|
|
319
|
+
if (used >= width) return segments
|
|
320
|
+
return [...segments, { text: ' '.repeat(width - used), sgr: fillSgr }]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Pad plain text to a cell budget (not a code-unit count).
|
|
325
|
+
*
|
|
326
|
+
* `String.prototype.padEnd` counts UTF-16 units, so a translated label made of
|
|
327
|
+
* Han characters would come out short and break column alignment.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} text
|
|
330
|
+
* @param {number} width
|
|
331
|
+
* @returns {string}
|
|
332
|
+
*/
|
|
333
|
+
export function padTo(text, width) {
|
|
334
|
+
const value = typeof text === 'string' ? text : String(text ?? '')
|
|
335
|
+
const used = displayWidth(value)
|
|
336
|
+
return used >= width ? value : value + ' '.repeat(width - used)
|
|
337
|
+
}
|