@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.
- package/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- 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
|
+
}
|