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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big a terminal cell actually is, in pixels.
|
|
3
|
+
*
|
|
4
|
+
* A terminal will not let an application change its font — there is no escape
|
|
5
|
+
* sequence for it, and `ESC[?3h` only switches between 80 and 132 columns. What
|
|
6
|
+
* it *will* do is report the geometry, which is the half of the idea that
|
|
7
|
+
* works: ask for the cell size, and you know the real resolution.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-live-trace/cellsize
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** `CSI 4 ; <height> ; <width> t` — the text area, in pixels. */
|
|
13
|
+
const TEXT_AREA = /\u001b\[4;(\d+);(\d+)t/
|
|
14
|
+
/** `CSI 6 ; <height> ; <width> t` — one character cell, in pixels. */
|
|
15
|
+
const CELL_SIZE = /\u001b\[6;(\d+);(\d+)t/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read a geometry report out of whatever the terminal has written back.
|
|
19
|
+
*
|
|
20
|
+
* Terminals answer asynchronously and may interleave other replies, so this
|
|
21
|
+
* scans rather than parsing positionally.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} data
|
|
24
|
+
* @returns {{ cell?: { width: number, height: number }, textArea?: { width: number, height: number } }}
|
|
25
|
+
*/
|
|
26
|
+
export function parseSizeReport(data) {
|
|
27
|
+
const out = {}
|
|
28
|
+
if (typeof data !== 'string') return out
|
|
29
|
+
const cell = CELL_SIZE.exec(data)
|
|
30
|
+
if (cell !== null) {
|
|
31
|
+
const height = Number.parseInt(cell[1], 10)
|
|
32
|
+
const width = Number.parseInt(cell[2], 10)
|
|
33
|
+
if (width > 0 && height > 0) out.cell = { width, height }
|
|
34
|
+
}
|
|
35
|
+
const area = TEXT_AREA.exec(data)
|
|
36
|
+
if (area !== null) {
|
|
37
|
+
const height = Number.parseInt(area[1], 10)
|
|
38
|
+
const width = Number.parseInt(area[2], 10)
|
|
39
|
+
if (width > 0 && height > 0) out.textArea = { width, height }
|
|
40
|
+
}
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The sequences that ask for the two reports. */
|
|
45
|
+
export const QUERY_CELL_SIZE = '\u001b[16t'
|
|
46
|
+
export const QUERY_TEXT_AREA = '\u001b[14t'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Ask the terminal for its geometry.
|
|
50
|
+
*
|
|
51
|
+
* Returns as soon as both reports arrive, or when the timeout expires — a
|
|
52
|
+
* terminal that does not answer must not stall the caller, so every outcome
|
|
53
|
+
* resolves rather than rejecting.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ stdin: any, stdout: any, timeoutMs?: number }} io
|
|
56
|
+
* @returns {Promise<{ cell?: object, textArea?: object, answered: boolean }>}
|
|
57
|
+
*/
|
|
58
|
+
export function queryTerminalSize(io, timeoutMs = 250) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const { stdin, stdout } = io
|
|
61
|
+
if (stdin?.isTTY !== true || typeof stdin.setRawMode !== 'function') {
|
|
62
|
+
resolve({ answered: false })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
let buffer = ''
|
|
66
|
+
let timer = null
|
|
67
|
+
const finish = () => {
|
|
68
|
+
if (timer !== null) clearTimeout(timer)
|
|
69
|
+
timer = null
|
|
70
|
+
stdin.removeListener('data', onData)
|
|
71
|
+
if (wasRaw === false) stdin.setRawMode(false)
|
|
72
|
+
stdin.pause?.()
|
|
73
|
+
resolve({ ...parseSizeReport(buffer), answered: buffer.length > 0 })
|
|
74
|
+
}
|
|
75
|
+
const onData = (chunk) => {
|
|
76
|
+
buffer += chunk.toString('utf8')
|
|
77
|
+
const parsed = parseSizeReport(buffer)
|
|
78
|
+
if (parsed.cell !== undefined && parsed.textArea !== undefined) finish()
|
|
79
|
+
}
|
|
80
|
+
const wasRaw = stdin.isRaw
|
|
81
|
+
stdin.setRawMode(true)
|
|
82
|
+
stdin.resume?.()
|
|
83
|
+
stdin.on('data', onData)
|
|
84
|
+
stdout.write(QUERY_CELL_SIZE + QUERY_TEXT_AREA)
|
|
85
|
+
timer = setTimeout(finish, timeoutMs)
|
|
86
|
+
timer.unref?.()
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The window's size in pixels, from the cells it is showing and how big they are.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ width: number, height: number }} cell
|
|
94
|
+
* @param {{ cols: number, rows: number }} cells
|
|
95
|
+
*/
|
|
96
|
+
export function pixelResolution(cell, cells) {
|
|
97
|
+
return {
|
|
98
|
+
width: cell.width * cells.cols,
|
|
99
|
+
height: cell.height * cells.rows,
|
|
100
|
+
aspect: cell.height === 0 ? 0 : cell.width / cell.height
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* How much a given font buys, for a window of a fixed pixel size.
|
|
106
|
+
*
|
|
107
|
+
* This is the number that matters: shrinking the font does **not** change the
|
|
108
|
+
* window's pixels — it changes how many characters fit in them. Every extra
|
|
109
|
+
* cell is two more pixels of drawing, because the renderer puts two vertical
|
|
110
|
+
* pixels in each one.
|
|
111
|
+
*
|
|
112
|
+
* @param {{ width: number, height: number }} cell the font's cell, in pixels
|
|
113
|
+
* @param {{ width: number, height: number }} window the text area, in pixels
|
|
114
|
+
*/
|
|
115
|
+
export function cellsForWindow(cell, window) {
|
|
116
|
+
if (cell.width <= 0 || cell.height <= 0) return { cols: 0, rows: 0, artPixels: 0 }
|
|
117
|
+
const cols = Math.floor(window.width / cell.width)
|
|
118
|
+
const rows = Math.floor(window.height / cell.height)
|
|
119
|
+
return { cols, rows, artPixels: cols * rows * 2 }
|
|
120
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small formatting helpers shared by the dashboard's chrome.
|
|
3
|
+
* @module dsh-live-trace/format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compact elapsed time: `0.8s`, `12.3s`, `1m05s`, `1h04m`.
|
|
8
|
+
* @param {number} ms
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function formatDuration(ms) {
|
|
12
|
+
if (!Number.isFinite(ms) || ms < 0) return '0.0s'
|
|
13
|
+
if (ms < 1000) return `${(ms / 1000).toFixed(1)}s`
|
|
14
|
+
const seconds = ms / 1000
|
|
15
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`
|
|
16
|
+
if (seconds < 3600) {
|
|
17
|
+
const minutes = Math.floor(seconds / 60)
|
|
18
|
+
return `${minutes}m${String(Math.floor(seconds % 60)).padStart(2, '0')}s`
|
|
19
|
+
}
|
|
20
|
+
const hours = Math.floor(seconds / 3600)
|
|
21
|
+
return `${hours}h${String(Math.floor((seconds % 3600) / 60)).padStart(2, '0')}m`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wall-clock `HH:MM:SS` in local time.
|
|
26
|
+
* @param {number} epochMs
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export function formatClock(epochMs) {
|
|
30
|
+
const date = new Date(epochMs)
|
|
31
|
+
if (Number.isNaN(date.getTime())) return '--:--:--'
|
|
32
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
33
|
+
.map((part) => String(part).padStart(2, '0'))
|
|
34
|
+
.join(':')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compact token count: `947`, `4.2K`, `1.3M`.
|
|
39
|
+
* @param {number} value
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function formatTokens(value) {
|
|
43
|
+
if (!Number.isFinite(value) || value <= 0) return '0'
|
|
44
|
+
if (value < 1000) return String(Math.round(value))
|
|
45
|
+
if (value < 1_000_000) return `${(value / 1000).toFixed(value < 10_000 ? 1 : 0)}K`
|
|
46
|
+
return `${(value / 1_000_000).toFixed(1)}M`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Shorten a session id while keeping its distinguishing tail.
|
|
51
|
+
* @param {string} id
|
|
52
|
+
* @param {number} [width]
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function shortSessionId(id, width = 20) {
|
|
56
|
+
if (typeof id !== 'string' || id.length <= width) return id ?? ''
|
|
57
|
+
const tail = Math.max(4, width - 4)
|
|
58
|
+
return `…${id.slice(id.length - tail)}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Middle-truncate a filesystem path so both its root and leaf survive.
|
|
63
|
+
* @param {string} path
|
|
64
|
+
* @param {number} width
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function shortPath(path, width) {
|
|
68
|
+
if (typeof path !== 'string' || path.length <= width) return path ?? ''
|
|
69
|
+
if (width <= 8) return `…${path.slice(path.length - (width - 1))}`
|
|
70
|
+
const head = Math.ceil((width - 1) / 3)
|
|
71
|
+
const tailPart = width - 1 - head
|
|
72
|
+
return `${path.slice(0, head)}…${path.slice(path.length - tailPart)}`
|
|
73
|
+
}
|