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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal control: the alternate screen, size, raw input, and frame painting.
|
|
3
|
+
*
|
|
4
|
+
* This is the only module in the viewer that touches the TTY, so every other
|
|
5
|
+
* piece of the dashboard stays unit-testable.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-live-trace/screen
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Sequences the alternate-screen painter emits. */
|
|
11
|
+
export const SEQUENCE = {
|
|
12
|
+
altEnter: '\u001b[?1049h',
|
|
13
|
+
altLeave: '\u001b[?1049l',
|
|
14
|
+
hideCursor: '\u001b[?25l',
|
|
15
|
+
showCursor: '\u001b[?25h',
|
|
16
|
+
home: '\u001b[H',
|
|
17
|
+
clearToEnd: '\u001b[J',
|
|
18
|
+
clearLine: '\u001b[K',
|
|
19
|
+
/** Begin synchronized output (DEC 2026); ignored by terminals without it. */
|
|
20
|
+
syncBegin: '\u001b[?2026h',
|
|
21
|
+
syncEnd: '\u001b[?2026l',
|
|
22
|
+
/** Button-event reporting plus SGR coordinates, so the wheel is unambiguous. */
|
|
23
|
+
mouseEnter: '\u001b[?1000h\u001b[?1006h',
|
|
24
|
+
mouseLeave: '\u001b[?1000l\u001b[?1006l'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decode one SGR mouse report body (`b;x;y`) into a key the dashboard handles.
|
|
29
|
+
*
|
|
30
|
+
* @param {number} button
|
|
31
|
+
* @param {number} x 1-based column
|
|
32
|
+
* @param {number} y 1-based row
|
|
33
|
+
* @returns {string | { type: 'click', x: number, y: number } | null}
|
|
34
|
+
*/
|
|
35
|
+
function mouseEvent(button, x, y, released) {
|
|
36
|
+
if (button >= 64) {
|
|
37
|
+
switch (button) {
|
|
38
|
+
case 64:
|
|
39
|
+
return 'wheel-up'
|
|
40
|
+
case 65:
|
|
41
|
+
return 'wheel-down'
|
|
42
|
+
case 66:
|
|
43
|
+
return 'wheel-left'
|
|
44
|
+
case 67:
|
|
45
|
+
return 'wheel-right'
|
|
46
|
+
default:
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Bit 5 marks pointer motion, which a dashboard has nothing to do with.
|
|
51
|
+
if ((button & 32) !== 0) return null
|
|
52
|
+
// Low two bits give the button. A release carries no new information, and
|
|
53
|
+
// only the left button opens something.
|
|
54
|
+
if (released) return null
|
|
55
|
+
return (button & 3) === 0 ? { type: 'click', x, y } : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse raw terminal input into named keys.
|
|
60
|
+
*
|
|
61
|
+
* Escape sequences arrive split across reads often enough that the parser
|
|
62
|
+
* buffers a partial sequence instead of treating its prefix as keystrokes.
|
|
63
|
+
*
|
|
64
|
+
* @returns {(chunk: string) => string[]}
|
|
65
|
+
*/
|
|
66
|
+
export function createKeyParser() {
|
|
67
|
+
const sequences = [
|
|
68
|
+
['\u001b[1;5A', 'ctrl-up'],
|
|
69
|
+
['\u001b[1;5B', 'ctrl-down'],
|
|
70
|
+
['\u001b[A', 'up'],
|
|
71
|
+
['\u001b[B', 'down'],
|
|
72
|
+
['\u001b[C', 'right'],
|
|
73
|
+
['\u001b[D', 'left'],
|
|
74
|
+
['\u001bOA', 'up'],
|
|
75
|
+
['\u001bOB', 'down'],
|
|
76
|
+
['\u001b[5~', 'page-up'],
|
|
77
|
+
['\u001b[6~', 'page-down'],
|
|
78
|
+
['\u001b[H', 'home'],
|
|
79
|
+
['\u001b[F', 'end'],
|
|
80
|
+
['\u001b[1~', 'home'],
|
|
81
|
+
['\u001b[4~', 'end']
|
|
82
|
+
]
|
|
83
|
+
let pending = ''
|
|
84
|
+
const parser = (chunk) => {
|
|
85
|
+
pending += chunk
|
|
86
|
+
const keys = []
|
|
87
|
+
for (;;) {
|
|
88
|
+
if (pending.length === 0) break
|
|
89
|
+
if (pending[0] !== '\u001b') {
|
|
90
|
+
keys.push(pending[0])
|
|
91
|
+
pending = pending.slice(1)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
// SGR mouse reports are self-delimiting: ESC [ < b;x;y M|m
|
|
95
|
+
if (pending.startsWith('\u001b[<')) {
|
|
96
|
+
const terminator = pending.search(/[Mm]/)
|
|
97
|
+
if (terminator === -1) break
|
|
98
|
+
const fields = pending.slice(3, terminator).split(';')
|
|
99
|
+
const released = pending[terminator] === 'm'
|
|
100
|
+
pending = pending.slice(terminator + 1)
|
|
101
|
+
const [button, x, y] = fields.map((value) => Number.parseInt(value, 10))
|
|
102
|
+
const event = mouseEvent(button, x, y, released)
|
|
103
|
+
if (event !== null) keys.push(event)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
// A lone ESC may still be the prefix of an arrow key.
|
|
107
|
+
if (pending.length === 1) break
|
|
108
|
+
const match = sequences.find(([sequence]) => pending.startsWith(sequence))
|
|
109
|
+
if (match !== undefined) {
|
|
110
|
+
keys.push(match[1])
|
|
111
|
+
pending = pending.slice(match[0].length)
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
const couldExtend = sequences.some(([sequence]) => sequence.startsWith(pending))
|
|
115
|
+
if (couldExtend) break
|
|
116
|
+
// Unknown escape: consume the ESC and re-examine the rest.
|
|
117
|
+
pending = pending.slice(1)
|
|
118
|
+
}
|
|
119
|
+
if (pending.length > 64) pending = ''
|
|
120
|
+
return keys
|
|
121
|
+
}
|
|
122
|
+
parser.pending = () => pending
|
|
123
|
+
parser.takePending = () => {
|
|
124
|
+
const held = pending
|
|
125
|
+
pending = ''
|
|
126
|
+
return held
|
|
127
|
+
}
|
|
128
|
+
return parser
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class Screen {
|
|
132
|
+
/**
|
|
133
|
+
* @param {{ stdout?: NodeJS.WriteStream, stdin?: NodeJS.ReadStream, altScreen?: boolean, color?: boolean, mouse?: boolean }} [options]
|
|
134
|
+
*/
|
|
135
|
+
constructor(options = {}) {
|
|
136
|
+
this.stdout = options.stdout ?? process.stdout
|
|
137
|
+
this.stdin = options.stdin ?? process.stdin
|
|
138
|
+
this.altScreen = options.altScreen !== false
|
|
139
|
+
this.mouse = options.mouse !== false
|
|
140
|
+
this.entered = false
|
|
141
|
+
this.rawWasSet = false
|
|
142
|
+
this.listeners = { key: new Set(), resize: new Set() }
|
|
143
|
+
this.onStdinData = null
|
|
144
|
+
this.onStdoutResize = null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @returns {{ cols: number, rows: number }} */
|
|
148
|
+
size() {
|
|
149
|
+
return {
|
|
150
|
+
cols: Math.max(20, this.stdout.columns ?? 80),
|
|
151
|
+
rows: Math.max(6, this.stdout.rows ?? 24)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Enter the alternate screen, hide the cursor, and start reading raw keys. */
|
|
156
|
+
enter() {
|
|
157
|
+
if (this.entered) return
|
|
158
|
+
this.entered = true
|
|
159
|
+
if (this.altScreen) this.stdout.write(SEQUENCE.altEnter)
|
|
160
|
+
this.stdout.write(SEQUENCE.hideCursor)
|
|
161
|
+
// Wheel events arrive as arrow keys in many terminals, which is reported as
|
|
162
|
+
// a plain keypress and cannot be distinguished from one. Claiming the mouse
|
|
163
|
+
// makes scrolling exact; Shift+drag still selects text.
|
|
164
|
+
if (this.mouse !== false) this.stdout.write(SEQUENCE.mouseEnter)
|
|
165
|
+
|
|
166
|
+
if (this.stdin.isTTY === true) {
|
|
167
|
+
try {
|
|
168
|
+
this.stdin.setRawMode(true)
|
|
169
|
+
this.rawWasSet = true
|
|
170
|
+
} catch {
|
|
171
|
+
/* not a real TTY; key handling stays off */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.stdin.resume?.()
|
|
175
|
+
this.stdin.setEncoding?.('utf8')
|
|
176
|
+
|
|
177
|
+
const parseKeys = createKeyParser()
|
|
178
|
+
let escapeTimer = null
|
|
179
|
+
const flushEscape = () => {
|
|
180
|
+
escapeTimer = null
|
|
181
|
+
// A lone ESC never completes into an arrow key, so it must eventually be
|
|
182
|
+
// delivered as the Escape key rather than held forever.
|
|
183
|
+
if (parseKeys.takePending() === '\u001b') this.emitKey('escape')
|
|
184
|
+
}
|
|
185
|
+
this.onStdinData = (chunk) => {
|
|
186
|
+
for (const key of parseKeys(String(chunk))) {
|
|
187
|
+
if (key === '\u0003') {
|
|
188
|
+
this.emitKey('quit')
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
this.emitKey(key)
|
|
192
|
+
}
|
|
193
|
+
if (escapeTimer !== null) clearTimeout(escapeTimer)
|
|
194
|
+
if (parseKeys.pending() === '\u001b' && !/^\u001b\[<$/.test(parseKeys.pending())) {
|
|
195
|
+
escapeTimer = setTimeout(flushEscape, 40)
|
|
196
|
+
escapeTimer.unref?.()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.escapeTimer = () => escapeTimer
|
|
200
|
+
this.stdin.on('data', this.onStdinData)
|
|
201
|
+
|
|
202
|
+
this.onStdoutResize = () => this.emitResize()
|
|
203
|
+
this.stdout.on('resize', this.onStdoutResize)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Restore the terminal, whatever happened. */
|
|
207
|
+
leave() {
|
|
208
|
+
if (!this.entered) return
|
|
209
|
+
this.entered = false
|
|
210
|
+
if (typeof this.escapeTimer === 'function') {
|
|
211
|
+
const timer = this.escapeTimer()
|
|
212
|
+
if (timer !== null) clearTimeout(timer)
|
|
213
|
+
}
|
|
214
|
+
if (this.onStdinData !== null) this.stdin.off('data', this.onStdinData)
|
|
215
|
+
if (this.onStdoutResize !== null) this.stdout.off('resize', this.onStdoutResize)
|
|
216
|
+
this.onStdinData = null
|
|
217
|
+
this.onStdoutResize = null
|
|
218
|
+
if (this.rawWasSet) {
|
|
219
|
+
try {
|
|
220
|
+
this.stdin.setRawMode(false)
|
|
221
|
+
} catch {
|
|
222
|
+
/* the TTY may already be gone */
|
|
223
|
+
}
|
|
224
|
+
this.rawWasSet = false
|
|
225
|
+
}
|
|
226
|
+
this.stdin.pause?.()
|
|
227
|
+
const mouseOff = this.mouse === false ? '' : SEQUENCE.mouseLeave
|
|
228
|
+
this.stdout.write(`${mouseOff}${SEQUENCE.showCursor}${this.altScreen ? SEQUENCE.altLeave : '\n'}`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Paint exactly `lines`, one per row, without a full-screen clear flicker.
|
|
233
|
+
* @param {string[]} lines
|
|
234
|
+
*/
|
|
235
|
+
paint(lines) {
|
|
236
|
+
const { rows } = this.size()
|
|
237
|
+
let out = `${SEQUENCE.syncBegin}${SEQUENCE.home}`
|
|
238
|
+
for (let index = 0; index < rows; index += 1) {
|
|
239
|
+
out += `${lines[index] ?? ''}${SEQUENCE.clearLine}`
|
|
240
|
+
if (index < rows - 1) out += '\r\n'
|
|
241
|
+
}
|
|
242
|
+
out += SEQUENCE.clearToEnd + SEQUENCE.syncEnd
|
|
243
|
+
this.stdout.write(out)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @param {string} message a message printed after leaving the alternate screen */
|
|
247
|
+
writeLine(message) {
|
|
248
|
+
this.stdout.write(`${message}\n`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @param {(key: string) => void} listener */
|
|
252
|
+
onKey(listener) {
|
|
253
|
+
this.listeners.key.add(listener)
|
|
254
|
+
return () => this.listeners.key.delete(listener)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** @param {() => void} listener */
|
|
258
|
+
onResize(listener) {
|
|
259
|
+
this.listeners.resize.add(listener)
|
|
260
|
+
return () => this.listeners.resize.delete(listener)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
emitKey(key) {
|
|
264
|
+
for (const listener of this.listeners.key) listener(key)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
emitResize() {
|
|
268
|
+
for (const listener of this.listeners.resize) listener()
|
|
269
|
+
}
|
|
270
|
+
}
|
package/src/cli/theme.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's visual vocabulary: one color and one treatment per event tag.
|
|
3
|
+
*
|
|
4
|
+
* Colors are 256-color SGR parameters rather than truecolor so the board looks
|
|
5
|
+
* right in the widest range of terminals, and every value collapses to an empty
|
|
6
|
+
* style when color is disabled (`NO_COLOR`, `--no-color`, or a non-TTY).
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-live-trace/theme
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} Theme
|
|
13
|
+
* @property {boolean} color
|
|
14
|
+
* @property {Record<string, string>} tag style per {@link import('../../lib/normalize.js').TAG}
|
|
15
|
+
* @property {Record<string, string>} level style per entry level
|
|
16
|
+
* @property {Record<string, string>} ui chrome styles
|
|
17
|
+
* @property {Record<string, string>} md markdown palette for markdown.js
|
|
18
|
+
* @property {Record<string, string>} syn syntax palette for highlight.js
|
|
19
|
+
* @property {Record<string, string>} scene palette for the desk scene
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a theme.
|
|
24
|
+
* @param {{ color?: boolean }} [options]
|
|
25
|
+
* @returns {Theme}
|
|
26
|
+
*/
|
|
27
|
+
export function createTheme(options = {}) {
|
|
28
|
+
const color = options.color !== false
|
|
29
|
+
const sgr = (code) => (color ? code : '')
|
|
30
|
+
|
|
31
|
+
const tag = {
|
|
32
|
+
session: sgr('1;38;5;141'),
|
|
33
|
+
turn: sgr('1;38;5;39'),
|
|
34
|
+
step: sgr('38;5;44'),
|
|
35
|
+
user: sgr('1;38;5;255'),
|
|
36
|
+
assistant: sgr('38;5;42'),
|
|
37
|
+
tool: sgr('1;38;5;220'),
|
|
38
|
+
result: sgr('38;5;40'),
|
|
39
|
+
error: sgr('1;38;5;203'),
|
|
40
|
+
approval: sgr('1;38;5;214'),
|
|
41
|
+
meta: sgr('38;5;245')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const level = {
|
|
45
|
+
info: sgr('38;5;252'),
|
|
46
|
+
success: sgr('38;5;42'),
|
|
47
|
+
warn: sgr('38;5;214'),
|
|
48
|
+
error: sgr('1;38;5;203'),
|
|
49
|
+
muted: sgr('38;5;243')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ui = {
|
|
53
|
+
border: sgr('38;5;238'),
|
|
54
|
+
title: sgr('1;38;5;45'),
|
|
55
|
+
label: sgr('38;5;246'),
|
|
56
|
+
value: sgr('1;38;5;253'),
|
|
57
|
+
dim: sgr('38;5;240'),
|
|
58
|
+
headerBg: sgr('48;5;235'),
|
|
59
|
+
footerBg: sgr('48;5;235'),
|
|
60
|
+
statusRunning: sgr('1;38;5;42'),
|
|
61
|
+
statusTool: sgr('1;38;5;220'),
|
|
62
|
+
statusIdle: sgr('38;5;245'),
|
|
63
|
+
statusApproval: sgr('1;38;5;214'),
|
|
64
|
+
statusError: sgr('1;38;5;203'),
|
|
65
|
+
stream: sgr('3;38;5;246'),
|
|
66
|
+
scrollHint: sgr('38;5;245'),
|
|
67
|
+
hint: sgr('38;5;240'),
|
|
68
|
+
ok: sgr('38;5;42'),
|
|
69
|
+
fail: sgr('38;5;203'),
|
|
70
|
+
// View chrome
|
|
71
|
+
cursor: sgr('1;38;5;45'),
|
|
72
|
+
cursorBg: sgr('48;5;237'),
|
|
73
|
+
selected: sgr('1;38;5;255'),
|
|
74
|
+
panelTitle: sgr('1;38;5;45'),
|
|
75
|
+
added: sgr('38;5;42'),
|
|
76
|
+
removed: sgr('38;5;203'),
|
|
77
|
+
addedBg: sgr('48;5;22'),
|
|
78
|
+
removedBg: sgr('48;5;52'),
|
|
79
|
+
hunkHeader: sgr('38;5;141'),
|
|
80
|
+
gutter: sgr('38;5;240'),
|
|
81
|
+
command: sgr('38;5;252'),
|
|
82
|
+
output: sgr('38;5;246'),
|
|
83
|
+
exitOk: sgr('38;5;42'),
|
|
84
|
+
exitFail: sgr('1;38;5;203')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Markdown and syntax palettes are handed to `markdown.js` / `highlight.js`,
|
|
88
|
+
// which never hard-code a colour of their own.
|
|
89
|
+
const md = {
|
|
90
|
+
heading: sgr('1;38;5;45'),
|
|
91
|
+
heading2: sgr('1;38;5;44'),
|
|
92
|
+
bold: sgr('1'),
|
|
93
|
+
italic: sgr('3'),
|
|
94
|
+
strike: sgr('9'),
|
|
95
|
+
link: sgr('4;38;5;39'),
|
|
96
|
+
inlineCode: sgr('38;5;180'),
|
|
97
|
+
bullet: sgr('38;5;39'),
|
|
98
|
+
quote: sgr('38;5;245'),
|
|
99
|
+
rule: sgr('38;5;238'),
|
|
100
|
+
code: sgr('38;5;252'),
|
|
101
|
+
fenceBorder: sgr('38;5;238'),
|
|
102
|
+
fenceLang: sgr('38;5;245'),
|
|
103
|
+
more: sgr('38;5;245')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const syn = {
|
|
107
|
+
keyword: sgr('38;5;170'),
|
|
108
|
+
string: sgr('38;5;114'),
|
|
109
|
+
number: sgr('38;5;180'),
|
|
110
|
+
comment: sgr('3;38;5;243'),
|
|
111
|
+
function: sgr('38;5;39'),
|
|
112
|
+
type: sgr('38;5;80'),
|
|
113
|
+
operator: sgr('38;5;210'),
|
|
114
|
+
punctuation: sgr('38;5;245'),
|
|
115
|
+
variable: sgr('38;5;252'),
|
|
116
|
+
property: sgr('38;5;117'),
|
|
117
|
+
tag: sgr('38;5;170'),
|
|
118
|
+
attribute: sgr('38;5;180'),
|
|
119
|
+
added: sgr('38;5;42'),
|
|
120
|
+
removed: sgr('38;5;203'),
|
|
121
|
+
meta: sgr('38;5;141')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The desk scene owns its own palette: wood, plastic, paper and a bright red
|
|
125
|
+
// telephone against the same blues as the mascot.
|
|
126
|
+
const scene = {
|
|
127
|
+
body: sgr('38;5;69'),
|
|
128
|
+
// A shade off the body: enough to read the limb as a limb, not enough to
|
|
129
|
+
// look like a different animal.
|
|
130
|
+
flipper: sgr('38;5;75'),
|
|
131
|
+
outline: sgr('38;5;25'),
|
|
132
|
+
belly: sgr('38;5;231'),
|
|
133
|
+
spout: sgr('38;5;245'),
|
|
134
|
+
accent: sgr('38;5;222'),
|
|
135
|
+
desk: sgr('38;5;137'),
|
|
136
|
+
deskDark: sgr('38;5;94'),
|
|
137
|
+
keyboard: sgr('38;5;238'),
|
|
138
|
+
key: sgr('38;5;246'),
|
|
139
|
+
phone: sgr('38;5;160'),
|
|
140
|
+
phoneDark: sgr('38;5;88'),
|
|
141
|
+
page: sgr('38;5;255'),
|
|
142
|
+
pageShade: sgr('38;5;250'),
|
|
143
|
+
pageDark: sgr('38;5;243'),
|
|
144
|
+
windowEdge: sgr('38;5;110'),
|
|
145
|
+
// The sky colour is rewritten every frame from the in-game clock; this is
|
|
146
|
+
// only the fallback for a caller that does not provide one.
|
|
147
|
+
sky: sgr('38;5;24'),
|
|
148
|
+
// All four are rewritten every frame from the in-game clock and weather.
|
|
149
|
+
skyLow: sgr('38;5;31'),
|
|
150
|
+
landscape: sgr('38;5;236'),
|
|
151
|
+
glow: sgr('38;5;230'),
|
|
152
|
+
starDim: sgr('38;5;245'),
|
|
153
|
+
windowFrame: sgr('38;5;180'),
|
|
154
|
+
wall: sgr('38;5;145'),
|
|
155
|
+
wallShade: sgr('38;5;102'),
|
|
156
|
+
skirting: sgr('38;5;60'),
|
|
157
|
+
floor: sgr('38;5;236'),
|
|
158
|
+
// The z's sit on the wall, so they cannot be another mid grey.
|
|
159
|
+
sleepZ: sgr('38;5;255'),
|
|
160
|
+
tree: sgr('38;5;22'),
|
|
161
|
+
ground: sgr('38;5;58'),
|
|
162
|
+
cloud: sgr('38;5;252'),
|
|
163
|
+
sun: sgr('38;5;220'),
|
|
164
|
+
moon: sgr('38;5;253'),
|
|
165
|
+
star: sgr('38;5;255'),
|
|
166
|
+
rain: sgr('38;5;111'),
|
|
167
|
+
snow: sgr('38;5;255'),
|
|
168
|
+
fog: sgr('38;5;246'),
|
|
169
|
+
windowFill: sgr('38;5;234'),
|
|
170
|
+
bookGreen: sgr('38;5;65'),
|
|
171
|
+
bookYellow: sgr('38;5;179'),
|
|
172
|
+
bookCyan: sgr('38;5;73'),
|
|
173
|
+
bubble: sgr('38;5;250'),
|
|
174
|
+
bubbleFill: sgr('38;5;236'),
|
|
175
|
+
text: sgr('38;5;253')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { color, tag, level, ui, md, syn, scene }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Wide-character-safe status glyph.
|
|
183
|
+
* @param {'idle' | 'running' | 'tool' | 'waiting-approval' | 'error'} status
|
|
184
|
+
* @param {number} frame rotating frame index
|
|
185
|
+
* @returns {string}
|
|
186
|
+
*/
|
|
187
|
+
export function statusGlyph(status, frame) {
|
|
188
|
+
switch (status) {
|
|
189
|
+
case 'running':
|
|
190
|
+
return ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][frame % 10]
|
|
191
|
+
case 'tool':
|
|
192
|
+
return ['◐', '◓', '◑', '◒'][frame % 4]
|
|
193
|
+
case 'waiting-approval':
|
|
194
|
+
return '▲'
|
|
195
|
+
case 'error':
|
|
196
|
+
return '✖'
|
|
197
|
+
default:
|
|
198
|
+
return '●'
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Style for a status value.
|
|
204
|
+
* @param {Theme} theme
|
|
205
|
+
* @param {string} status
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
export function statusStyle(theme, status) {
|
|
209
|
+
switch (status) {
|
|
210
|
+
case 'running':
|
|
211
|
+
return theme.ui.statusRunning
|
|
212
|
+
case 'tool':
|
|
213
|
+
return theme.ui.statusTool
|
|
214
|
+
case 'waiting-approval':
|
|
215
|
+
return theme.ui.statusApproval
|
|
216
|
+
case 'error':
|
|
217
|
+
return theme.ui.statusError
|
|
218
|
+
default:
|
|
219
|
+
return theme.ui.statusIdle
|
|
220
|
+
}
|
|
221
|
+
}
|