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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +920 -0
  3. package/README.zh.md +790 -0
  4. package/assets/rain.ogg +0 -0
  5. package/bin/dsh-glyph-probe.js +51 -0
  6. package/bin/dsh-live-trace.js +29 -0
  7. package/bin/dsh-live-working.js +14 -0
  8. package/cordis.patch.yml +31 -0
  9. package/icon.svg +12 -0
  10. package/index.js +328 -0
  11. package/lib/client.js +178 -0
  12. package/lib/instance.js +68 -0
  13. package/lib/normalize.js +850 -0
  14. package/lib/paths.js +66 -0
  15. package/lib/protocol.js +115 -0
  16. package/lib/registry.js +232 -0
  17. package/lib/tools.js +257 -0
  18. package/lib/tracker.js +648 -0
  19. package/lib/transport.js +231 -0
  20. package/locale/en.json +6 -0
  21. package/locale/zh.json +6 -0
  22. package/package.json +94 -0
  23. package/picture/call1.png +0 -0
  24. package/picture/call2.png +0 -0
  25. package/picture/sleep1.png +0 -0
  26. package/picture/sleep2.png +0 -0
  27. package/picture/tui1.png +0 -0
  28. package/picture/tui2.png +0 -0
  29. package/picture/type1.png +0 -0
  30. package/picture/type2.png +0 -0
  31. package/scripts/bench-render.mjs +69 -0
  32. package/scripts/demo-working.mjs +130 -0
  33. package/scripts/demo.mjs +284 -0
  34. package/scripts/install-profile.mjs +174 -0
  35. package/scripts/mock-provider.mjs +211 -0
  36. package/src/cli/cellsize.js +120 -0
  37. package/src/cli/format.js +73 -0
  38. package/src/cli/highlight.js +932 -0
  39. package/src/cli/i18n.js +457 -0
  40. package/src/cli/main.js +630 -0
  41. package/src/cli/markdown.js +753 -0
  42. package/src/cli/renderer.js +1044 -0
  43. package/src/cli/screen.js +270 -0
  44. package/src/cli/theme.js +221 -0
  45. package/src/cli/view-state.js +396 -0
  46. package/src/cli/views.js +406 -0
  47. package/src/cli/width.js +337 -0
  48. package/src/cli/working/art.js +413 -0
  49. package/src/cli/working/main.js +569 -0
  50. package/src/cli/working/packing.js +159 -0
  51. package/src/cli/working/picker.js +75 -0
  52. package/src/cli/working/props.js +385 -0
  53. package/src/cli/working/scene.js +837 -0
  54. package/src/cli/working/sky.js +641 -0
  55. package/src/cli/working/sound.js +400 -0
  56. 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
+ }