@xp266/dshtui 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 (159) hide show
  1. package/bin/dshtui.js +65 -0
  2. package/cordis.patch.yml +81 -0
  3. package/lib/contract/index.d.mts +2 -0
  4. package/lib/contract/index.mjs +5 -0
  5. package/lib/dialog.d.mts +156 -0
  6. package/lib/dialog.mjs +3 -0
  7. package/lib/index-DqnaSXqP.d.mts +823 -0
  8. package/lib/index.d.mts +17 -0
  9. package/lib/index.mjs +11404 -0
  10. package/lib/prod-react-Dzof4qJx.mjs +4 -0
  11. package/lib/surface-DdxzIgIY.mjs +3126 -0
  12. package/lib/vendor.d.mts +3 -0
  13. package/lib/vendor.mjs +4 -0
  14. package/package.json +128 -0
  15. package/src/apply-theme.ts +8 -0
  16. package/src/boot-log.ts +35 -0
  17. package/src/chat/blocks.ts +41 -0
  18. package/src/chat/bridge.ts +1076 -0
  19. package/src/chat/builtin-tool-views.ts +53 -0
  20. package/src/chat/chat-face.ts +16 -0
  21. package/src/chat/chat-nodes.ts +53 -0
  22. package/src/chat/efforts.ts +4 -0
  23. package/src/chat/interactions.ts +329 -0
  24. package/src/chat/models.ts +263 -0
  25. package/src/chat/partial-json.ts +125 -0
  26. package/src/chat/presets.ts +26 -0
  27. package/src/chat/question-view.ts +106 -0
  28. package/src/chat/retry-status.ts +33 -0
  29. package/src/chat/session-groups.ts +34 -0
  30. package/src/chat/session-list.ts +311 -0
  31. package/src/chat/store.ts +636 -0
  32. package/src/chat/todo-view.ts +66 -0
  33. package/src/chat/tool-view.ts +311 -0
  34. package/src/chat/tool-views.ts +114 -0
  35. package/src/contract/index.ts +704 -0
  36. package/src/contract/upstream.ts +165 -0
  37. package/src/core/caret-nonce.ts +19 -0
  38. package/src/core/composer-layout.ts +65 -0
  39. package/src/core/edit.ts +152 -0
  40. package/src/core/field-view.ts +52 -0
  41. package/src/core/fields.ts +185 -0
  42. package/src/core/metrics.ts +26 -0
  43. package/src/core/paste.ts +149 -0
  44. package/src/core/segments.ts +112 -0
  45. package/src/core/text.ts +290 -0
  46. package/src/dialog.ts +11 -0
  47. package/src/env.ts +38 -0
  48. package/src/harness-home.ts +22 -0
  49. package/src/hot-theme.ts +56 -0
  50. package/src/index.tsx +185 -0
  51. package/src/kernel/registry.ts +102 -0
  52. package/src/kernel/surface.ts +33 -0
  53. package/src/log.ts +249 -0
  54. package/src/model/message.ts +89 -0
  55. package/src/model/selection.ts +63 -0
  56. package/src/performance-guard.ts +28 -0
  57. package/src/runtime/prod-react.ts +10 -0
  58. package/src/terminal/background.ts +66 -0
  59. package/src/terminal/capabilities.ts +54 -0
  60. package/src/terminal/clipboard-backends.ts +69 -0
  61. package/src/terminal/clipboard.ts +161 -0
  62. package/src/terminal/cursor-shape.ts +13 -0
  63. package/src/terminal/glyphs.ts +44 -0
  64. package/src/terminal/mouse.ts +154 -0
  65. package/src/terminal/probe.ts +61 -0
  66. package/src/terminal/screen.ts +349 -0
  67. package/src/theme-settings.ts +31 -0
  68. package/src/theme.ts +319 -0
  69. package/src/ui/app.tsx +454 -0
  70. package/src/ui/chrome/caps.tsx +9 -0
  71. package/src/ui/chrome/hint-service.ts +87 -0
  72. package/src/ui/chrome/input-status.ts +27 -0
  73. package/src/ui/chrome/key-gate.tsx +29 -0
  74. package/src/ui/chrome/status-bar.tsx +147 -0
  75. package/src/ui/contributions.ts +47 -0
  76. package/src/ui/dialog/defaults-dialog.tsx +152 -0
  77. package/src/ui/dialog/dialog-item.tsx +38 -0
  78. package/src/ui/dialog/dialog.tsx +386 -0
  79. package/src/ui/dialog/effort-dialog.tsx +45 -0
  80. package/src/ui/dialog/geometry.ts +131 -0
  81. package/src/ui/dialog/items.ts +124 -0
  82. package/src/ui/dialog/list-dialog.tsx +63 -0
  83. package/src/ui/dialog/models-dialog.tsx +203 -0
  84. package/src/ui/dialog/presets-dialog.tsx +46 -0
  85. package/src/ui/dialog/providers-dialog.tsx +313 -0
  86. package/src/ui/dialog/sessions-dialog.tsx +163 -0
  87. package/src/ui/dialog/sizes.ts +7 -0
  88. package/src/ui/dialog/status-lines.ts +11 -0
  89. package/src/ui/dialog/todo-dialog.tsx +29 -0
  90. package/src/ui/dialog/use-dialog-input.ts +186 -0
  91. package/src/ui/extension-point.ts +188 -0
  92. package/src/ui/home-logo.ts +132 -0
  93. package/src/ui/hooks/use-async-action.ts +25 -0
  94. package/src/ui/hooks/use-async-list.ts +71 -0
  95. package/src/ui/hooks/use-caret.ts +24 -0
  96. package/src/ui/hooks/use-chat-events.ts +153 -0
  97. package/src/ui/hooks/use-mouse-selection.ts +352 -0
  98. package/src/ui/hooks/use-scroll.ts +49 -0
  99. package/src/ui/hooks/use-terminal-size.ts +20 -0
  100. package/src/ui/input/commands.ts +118 -0
  101. package/src/ui/input/composer-bus.ts +14 -0
  102. package/src/ui/input/composer-fields.ts +111 -0
  103. package/src/ui/input/composer-keys.ts +21 -0
  104. package/src/ui/input/composer-paste.ts +20 -0
  105. package/src/ui/input/input-bar.tsx +279 -0
  106. package/src/ui/input/use-composer.ts +495 -0
  107. package/src/ui/key-arbiter.ts +37 -0
  108. package/src/ui/keymap.ts +15 -0
  109. package/src/ui/layout-service.ts +77 -0
  110. package/src/ui/message/layout.ts +763 -0
  111. package/src/ui/message/md/block.ts +247 -0
  112. package/src/ui/message/md/engine.ts +430 -0
  113. package/src/ui/message/md/extensions.ts +58 -0
  114. package/src/ui/message/md/highlight.ts +626 -0
  115. package/src/ui/message/md/index.ts +14 -0
  116. package/src/ui/message/md/inline.ts +97 -0
  117. package/src/ui/message/md/palette.ts +87 -0
  118. package/src/ui/message/message-list.tsx +69 -0
  119. package/src/ui/message/message-row.tsx +137 -0
  120. package/src/ui/message/message-views.ts +16 -0
  121. package/src/ui/message/renderers.ts +39 -0
  122. package/src/ui/message/tool-diff.ts +188 -0
  123. package/src/ui/message/warmup.ts +41 -0
  124. package/src/ui/message/wave.ts +98 -0
  125. package/src/ui/overlay.ts +22 -0
  126. package/src/ui/panels/approval-panel.tsx +193 -0
  127. package/src/ui/panels/exports.ts +3 -0
  128. package/src/ui/panels/question-model.ts +424 -0
  129. package/src/ui/panels/question-panel.tsx +154 -0
  130. package/src/ui/panels/surface.tsx +106 -0
  131. package/src/ui/panels-builtin.tsx +86 -0
  132. package/src/ui/pointer/builtins.ts +281 -0
  133. package/src/ui/pointer/registry.ts +25 -0
  134. package/src/ui/region.tsx +31 -0
  135. package/src/ui/selection/service.ts +92 -0
  136. package/src/ui/selection-registry.ts +88 -0
  137. package/src/ui/selection.tsx +148 -0
  138. package/src/ui/spinner-tick.tsx +24 -0
  139. package/src/ui/use-available-windows.ts +31 -0
  140. package/src/ui/widgets/actions.tsx +84 -0
  141. package/src/ui/widgets/button.tsx +57 -0
  142. package/src/ui/widgets/checkbox.tsx +58 -0
  143. package/src/ui/widgets/header.tsx +36 -0
  144. package/src/ui/widgets/index.ts +8 -0
  145. package/src/ui/widgets/input.tsx +76 -0
  146. package/src/ui/widgets/registry.ts +32 -0
  147. package/src/ui/widgets/search.tsx +59 -0
  148. package/src/ui/widgets/select.tsx +97 -0
  149. package/src/ui/widgets/static.tsx +37 -0
  150. package/src/ui/widgets/types.ts +1 -0
  151. package/src/ui/window-services-bridge.ts +71 -0
  152. package/src/ui/window-services.ts +20 -0
  153. package/src/ui/windows/models-window.tsx +15 -0
  154. package/src/ui/windows/providers-window.tsx +14 -0
  155. package/src/ui/windows/service-window.tsx +18 -0
  156. package/src/ui/windows/sessions-window.tsx +25 -0
  157. package/src/ui/windows-builtin.tsx +94 -0
  158. package/src/ui/windows.ts +18 -0
  159. package/src/vendor.ts +12 -0
@@ -0,0 +1,61 @@
1
+ import { env } from '../env.ts'
2
+ import type { ColorLevel } from './capabilities.ts'
3
+
4
+ const SGR_PROBE_QUERY = '\x1b[0m\x1b[38;2;250;251;252m\x1bP$qm\x1b\\\x1b[c'
5
+ const SGR_PROBE_RESPONSE = /\x1bP(\d)\$r(.*?)(?:\x1b\\|\x07)/s
6
+ const SGR_PROBE_REJECTED = /\x1bP0\$r/
7
+ const PROBE_TIMEOUT_MS = 250
8
+
9
+ export function parseSgrProbe(raw: string): ColorLevel | undefined {
10
+ const response = SGR_PROBE_RESPONSE.exec(raw)
11
+ if (response === null || response[1] !== '1') return undefined
12
+ const tokens = response[2].replace(/[m\s]/g, '').split(/[;:]/)
13
+ for (let i = 0; i < tokens.length - 1; i++) {
14
+ if (tokens[i] !== '38') continue
15
+ if (tokens[i + 1] === '2') return 3
16
+ if (tokens[i + 1] === '5') return 2
17
+ }
18
+ return 1
19
+ }
20
+
21
+ export function probeColorLevel(): Promise<ColorLevel | undefined> {
22
+ return new Promise(resolve => {
23
+ if (env.color !== undefined || process.env.NO_COLOR !== undefined || process.env.FORCE_COLOR !== undefined || (process.env.TERM ?? '') === 'dumb') {
24
+ resolve(undefined)
25
+ return
26
+ }
27
+ const stdin = process.stdin
28
+ if (!stdin.isTTY || !process.stdout.isTTY || stdin.setRawMode === undefined) {
29
+ resolve(undefined)
30
+ return
31
+ }
32
+ let buffer = ''
33
+ let timer: ReturnType<typeof setTimeout> | undefined
34
+ const finish = (result: ColorLevel | undefined): void => {
35
+ if (timer !== undefined) clearTimeout(timer)
36
+ stdin.off('data', onData)
37
+ try {
38
+ stdin.setRawMode(false)
39
+ } catch {
40
+ // The stream may have closed while the probe was in flight; nothing to restore.
41
+ }
42
+ process.stdout.write('\x1b[0m')
43
+ resolve(result)
44
+ }
45
+ const onData = (chunk: Buffer | string): void => {
46
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8')
47
+ const parsed = parseSgrProbe(buffer)
48
+ if (parsed !== undefined || SGR_PROBE_REJECTED.test(buffer)) finish(parsed)
49
+ }
50
+ try {
51
+ stdin.setRawMode(true)
52
+ } catch {
53
+ // Raw mode is unavailable (piped stdin or non-TTY); the probe quietly gives up.
54
+ resolve(undefined)
55
+ return
56
+ }
57
+ stdin.on('data', onData)
58
+ process.stdout.write(SGR_PROBE_QUERY)
59
+ timer = setTimeout(() => finish(undefined), PROBE_TIMEOUT_MS)
60
+ })
61
+ }
@@ -0,0 +1,349 @@
1
+ import { EventEmitter } from 'node:events'
2
+ import { charWidth, segmentGraphemes } from '../core/text.ts'
3
+ import { selectedRange } from '../model/selection.ts'
4
+ import type { LineSelection } from '../model/selection.ts'
5
+
6
+ export interface ScreenRect {
7
+ top: number
8
+ bottom: number
9
+ left: number
10
+ right: number
11
+ }
12
+
13
+ export interface ScreenCapture {
14
+ stream: NodeJS.WriteStream
15
+ feed(chunk: string): void
16
+ extract(rect: ScreenRect): string
17
+ extractSelection(selection: LineSelection): string
18
+ rowHasText(y: number): boolean
19
+ }
20
+
21
+ export function createScreenCapture(): ScreenCapture {
22
+ const real = process.stdout
23
+ let grid: string[][] = []
24
+ let cursorX = 0
25
+ let cursorY = 0
26
+ let pending = ''
27
+ let regionTop = 0
28
+ let regionBottom = -1
29
+ // Every ink frame flows through write(); parsing it into the grid eagerly
30
+ // costs ~1ms per frame even when nothing ever queries the grid. Chunks are
31
+ // buffered and parsed on first query instead — pointer events and copy
32
+ // actions are the only readers, and they are rare relative to frames.
33
+ const buffered: string[] = []
34
+ let bufferedBytes = 0
35
+ const BUFFER_MAX_BYTES = 1024 * 1024
36
+
37
+ function flushBuffer(): void {
38
+ for (const chunk of buffered) feed(chunk)
39
+ buffered.length = 0
40
+ bufferedBytes = 0
41
+ }
42
+
43
+ function bufferChunk(chunk: string): void {
44
+ buffered.push(chunk)
45
+ bufferedBytes += chunk.length
46
+ if (bufferedBytes > BUFFER_MAX_BYTES) {
47
+ // A reader that never queries keeps the grid stale beyond this point;
48
+ // pointer queries still parse the most recent megabyte.
49
+ buffered.splice(0, buffered.length - 1)
50
+ bufferedBytes = buffered[0]?.length ?? 0
51
+ }
52
+ }
53
+
54
+ function ensureRow(y: number): void {
55
+ while (grid.length <= y) grid.push([])
56
+ }
57
+
58
+ function effectiveRegionBottom(): number {
59
+ const fallback = Math.max(real.rows ?? 24, grid.length) - 1
60
+ return regionBottom < 0 ? fallback : Math.min(regionBottom, Math.max(fallback, regionTop))
61
+ }
62
+
63
+ function scrollRegionLines(n: number, down: boolean): void {
64
+ if (n <= 0) return
65
+ const top = Math.max(0, Math.min(regionTop, grid.length))
66
+ const bottom = Math.max(top, effectiveRegionBottom())
67
+ ensureRow(bottom)
68
+ const count = Math.min(n, bottom - top)
69
+ for (let k = 0; k < count; k++) {
70
+ if (down) {
71
+ for (let y = bottom; y > top; y--) grid[y] = grid[y - 1] ?? []
72
+ grid[top] = []
73
+ } else {
74
+ for (let y = top; y < bottom; y++) grid[y] = grid[y + 1] ?? []
75
+ grid[bottom] = []
76
+ }
77
+ }
78
+ }
79
+
80
+ function setCell(y: number, x: number, ch: string): void {
81
+ ensureRow(y)
82
+ const row = grid[y]!
83
+ while (row.length < x) row.push(' ')
84
+ row[x] = ch
85
+ const width = charWidth(ch)
86
+ for (let k = 1; k < width; k++) row[x + k] = ''
87
+ }
88
+
89
+ function eraseLine(y: number, mode: number): void {
90
+ ensureRow(y)
91
+ const row = grid[y]!
92
+ const x = Math.max(0, Math.min(cursorX, row.length))
93
+ if (mode === 0) grid[y] = row.slice(0, x)
94
+ else if (mode === 1) grid[y] = row.slice(x)
95
+ else grid[y] = []
96
+ }
97
+
98
+ function writeText(text: string): void {
99
+ for (const { segment } of segmentGraphemes(text)) {
100
+ if (segment === '\n') {
101
+ cursorY += 1
102
+ cursorX = 0
103
+ continue
104
+ }
105
+ if (segment === '\r') {
106
+ cursorX = 0
107
+ continue
108
+ }
109
+ if (segment === '\t') {
110
+ cursorX += 4
111
+ continue
112
+ }
113
+ if (cursorY < 0) cursorY = 0
114
+ if (cursorX < 0) cursorX = 0
115
+ setCell(cursorY, cursorX, segment)
116
+ cursorX += charWidth(segment)
117
+ }
118
+ }
119
+
120
+ function applyCsi(params: number[], final: string): void {
121
+ switch (final) {
122
+ case 'A':
123
+ cursorY = Math.max(0, cursorY - (params[0] ?? 1))
124
+ break
125
+ case 'B':
126
+ cursorY += params[0] ?? 1
127
+ break
128
+ case 'C':
129
+ cursorX += params[0] ?? 1
130
+ break
131
+ case 'D':
132
+ cursorX = Math.max(0, cursorX - (params[0] ?? 1))
133
+ break
134
+ case 'E':
135
+ cursorY += params[0] ?? 1
136
+ cursorX = 0
137
+ break
138
+ case 'F':
139
+ cursorY = Math.max(0, cursorY - (params[0] ?? 1))
140
+ cursorX = 0
141
+ break
142
+ case 'G':
143
+ cursorX = Math.max(0, (params[0] ?? 1) - 1)
144
+ break
145
+ case 'H':
146
+ case 'f':
147
+ cursorY = Math.max(0, (params[0] ?? 1) - 1)
148
+ cursorX = Math.max(0, (params[1] ?? 1) - 1)
149
+ break
150
+ case 'J': {
151
+ const mode = params[0] ?? 0
152
+ if (mode === 2 || mode === 3) {
153
+ grid = []
154
+ cursorX = 0
155
+ cursorY = 0
156
+ } else if (mode === 0) {
157
+ if (grid.length > cursorY + 1) grid = grid.slice(0, cursorY + 1)
158
+ } else {
159
+ for (let y = 0; y <= cursorY && y < grid.length; y++) grid[y] = []
160
+ }
161
+ break
162
+ }
163
+ case 'K':
164
+ eraseLine(cursorY, params[0] ?? 0)
165
+ break
166
+ case 'r': {
167
+ regionTop = Math.max(0, (params[0] ?? 1) - 1)
168
+ const bottomParam = params[1] ?? 0
169
+ regionBottom = bottomParam > 0 ? Math.max(regionTop, bottomParam - 1) : -1
170
+ cursorX = 0
171
+ cursorY = 0
172
+ break
173
+ }
174
+ case 'S':
175
+ scrollRegionLines(Math.max(1, params[0] || 1), false)
176
+ break
177
+ case 'T':
178
+ scrollRegionLines(Math.max(1, params[0] || 1), true)
179
+ break
180
+ default:
181
+ break
182
+ }
183
+ }
184
+
185
+ function feed(chunk: string): void {
186
+ pending += chunk
187
+ while (pending.length > 0) {
188
+ const esc = pending.indexOf('\x1b')
189
+ if (esc === -1) {
190
+ writeText(pending)
191
+ pending = ''
192
+ return
193
+ }
194
+ if (esc > 0) {
195
+ writeText(pending.slice(0, esc))
196
+ pending = pending.slice(esc)
197
+ continue
198
+ }
199
+ if (pending.length < 2) return
200
+ const kind = pending[1]
201
+ if (kind === '[') {
202
+ let finalIndex = -1
203
+ for (let i = 2; i < pending.length; i++) {
204
+ const code = pending.charCodeAt(i)
205
+ if (code >= 0x40 && code <= 0x7e) {
206
+ finalIndex = i
207
+ break
208
+ }
209
+ }
210
+ if (finalIndex === -1) {
211
+ if (pending.length > 64) pending = ''
212
+ return
213
+ }
214
+ const body = pending.slice(2, finalIndex)
215
+ const final = pending[finalIndex]!
216
+ pending = pending.slice(finalIndex + 1)
217
+ const params: number[] = []
218
+ let current = ''
219
+ for (const ch of body) {
220
+ if (ch >= '0' && ch <= '9') current += ch
221
+ else if (ch === ';') {
222
+ params.push(current === '' ? 0 : Number(current))
223
+ current = ''
224
+ } else {
225
+ current = ''
226
+ }
227
+ }
228
+ if (current !== '') params.push(Number(current))
229
+ applyCsi(params, final)
230
+ continue
231
+ }
232
+ if (kind === ']') {
233
+ const bel = pending.indexOf('\x07')
234
+ const st = pending.indexOf('\x1b\\')
235
+ let done = -1
236
+ if (bel !== -1) done = bel + 1
237
+ if (st !== -1 && (done === -1 || st + 2 < done)) done = st + 2
238
+ if (done === -1) {
239
+ if (pending.length > 4096) pending = ''
240
+ return
241
+ }
242
+ pending = pending.slice(done)
243
+ continue
244
+ }
245
+ pending = pending.slice(2)
246
+ }
247
+ }
248
+
249
+ function extract(rect: ScreenRect): string {
250
+ flushBuffer()
251
+ const top = Math.max(0, Math.min(rect.top, grid.length - 1))
252
+ const bottom = Math.max(0, Math.min(rect.bottom, grid.length - 1))
253
+ const left = Math.max(0, rect.left)
254
+ const right = Math.max(left, rect.right)
255
+ const lines: string[] = []
256
+ for (let y = top; y <= bottom; y++) {
257
+ lines.push((grid[y] ?? []).slice(left, right).join(''))
258
+ }
259
+ return lines.join('\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '')
260
+ }
261
+
262
+ function isBlockOnly(text: string): boolean {
263
+ const stripped = text.replace(/[▀▄█░▒▓\t ]/g, '')
264
+ return stripped === ''
265
+ }
266
+
267
+ function extractSelection(selection: LineSelection): string {
268
+ flushBuffer()
269
+ const top = Math.max(0, Math.min(selection.anchorRow, selection.focusRow))
270
+ const bottom = Math.min(Math.max(selection.anchorRow, selection.focusRow), grid.length - 1)
271
+ const lines: string[] = []
272
+ for (let y = top; y <= bottom; y++) {
273
+ const row = grid[y] ?? []
274
+ if (isBlockOnly(row.join(''))) {
275
+ lines.push('')
276
+ continue
277
+ }
278
+ const range = selectedRange(selection, y)
279
+ if (range === null) continue
280
+ const start = Math.max(0, range.start)
281
+ const end = range.end === Infinity ? undefined : Math.max(start, range.end)
282
+ const text = row.slice(start, end).join('')
283
+ if (text.length > 0) lines.push(text)
284
+ }
285
+ return lines.join('\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '')
286
+ }
287
+
288
+ function rowHasText(y: number): boolean {
289
+ flushBuffer()
290
+ if (y < 0 || y >= grid.length) return false
291
+ const row = (grid[y] ?? []).join('')
292
+ return !isBlockOnly(row) && row.trim() !== ''
293
+ }
294
+
295
+ class CaptureStream extends EventEmitter {
296
+ constructor() {
297
+ super()
298
+ // Re-emit the real terminal's resize events onto the capture stream so
299
+ // subscribers that registered locally (below) observe them too.
300
+ ;(real as unknown as EventEmitter).on('resize', () => {
301
+ this.emit('resize')
302
+ })
303
+ }
304
+ write(chunk: string | Buffer): boolean {
305
+ bufferChunk(typeof chunk === 'string' ? chunk : chunk.toString('utf8'))
306
+ return real.write(chunk)
307
+ }
308
+ get isTTY(): boolean {
309
+ return real.isTTY
310
+ }
311
+ get columns(): number {
312
+ return real.columns ?? 80
313
+ }
314
+ get rows(): number {
315
+ return real.rows ?? 24
316
+ }
317
+ get destroyed(): boolean {
318
+ return real.destroyed
319
+ }
320
+ get writableEnded(): boolean {
321
+ return real.writableEnded
322
+ }
323
+ get writable(): boolean {
324
+ return real.writable
325
+ }
326
+ override on(event: string, listener: (...args: unknown[]) => void): this {
327
+ if (event === 'resize') return super.on(event, listener)
328
+ ;(real as unknown as EventEmitter).on(event, listener)
329
+ return this
330
+ }
331
+ override off(event: string, listener: (...args: unknown[]) => void): this {
332
+ if (event === 'resize') return super.off(event, listener)
333
+ ;(real as unknown as EventEmitter).off(event, listener)
334
+ return this
335
+ }
336
+ override once(event: string, listener: (...args: unknown[]) => void): this {
337
+ if (event === 'resize') return super.once(event, listener)
338
+ ;(real as unknown as EventEmitter).once(event, listener)
339
+ return this
340
+ }
341
+ override removeListener(event: string, listener: (...args: unknown[]) => void): this {
342
+ if (event === 'resize') return super.removeListener(event, listener)
343
+ ;(real as unknown as EventEmitter).removeListener(event, listener)
344
+ return this
345
+ }
346
+ }
347
+
348
+ return { stream: new CaptureStream() as unknown as NodeJS.WriteStream, feed, extract, extractSelection, rowHasText }
349
+ }
@@ -0,0 +1,31 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import z from '@deepseek-ai/schemastery'
3
+ import type { ThemeMode } from './theme.ts'
4
+
5
+ export const THEME_SETTINGS_NAMESPACE = 'dshtui-theme'
6
+
7
+ export interface ThemeSettings {
8
+ mode?: ThemeMode
9
+ }
10
+
11
+ const ThemeSettingsSchema: z<ThemeSettings> = z.object({
12
+ mode: z.union(['dark', 'light']),
13
+ })
14
+
15
+ export interface ThemeSettingsScope {
16
+ get(): ThemeSettings
17
+ }
18
+
19
+ interface SettingsServiceLike {
20
+ register(ns: string, schema: z<ThemeSettings>, options?: object): ThemeSettingsScope
21
+ }
22
+
23
+ export function registerThemeSettings(ctx: Context): ThemeSettingsScope | undefined {
24
+ const settings = ctx.get('settings') as SettingsServiceLike | undefined
25
+ try {
26
+ return settings?.register(THEME_SETTINGS_NAMESPACE, ThemeSettingsSchema)
27
+ } catch {
28
+ // Without a settings service the theme mode is simply not persisted.
29
+ return undefined
30
+ }
31
+ }