@rind-ai/cli 0.4.1 → 0.6.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/rind.js +5 -5
- package/lib/assistant-renderer.js +179 -265
- package/lib/choice-menu-state.js +46 -46
- package/lib/cli-input-actions.js +548 -0
- package/lib/cli-output-controller.js +460 -0
- package/lib/cli-runtime-controller.js +350 -0
- package/lib/cli-state-store.js +32 -0
- package/lib/cli-state.js +41 -0
- package/lib/command-controller.js +159 -126
- package/lib/compact-context-state.js +22 -22
- package/lib/components/assistant-message.js +169 -0
- package/lib/components/composer-area.js +25 -0
- package/lib/components/dynamic-block.js +20 -0
- package/lib/components/monitor-stack.js +35 -0
- package/lib/components/text-block.js +47 -0
- package/lib/components/tool-block.js +122 -0
- package/lib/composer-terminal.js +224 -203
- package/lib/event-controller.js +243 -242
- package/lib/frontend-cli-implementation.js +656 -1111
- package/lib/input-controller.js +75 -94
- package/lib/input-errors.js +3 -3
- package/lib/interrupt-state.js +9 -9
- package/lib/line-editor.js +541 -541
- package/lib/local-slash-commands.js +217 -0
- package/lib/markdown-lines.js +103 -0
- package/lib/model-menu-state.js +50 -50
- package/lib/one-shot-progress.js +145 -0
- package/lib/one-shot.js +228 -0
- package/lib/question-menu-state.js +61 -0
- package/lib/rendering.js +1309 -1060
- package/lib/runtime-client.js +241 -193
- package/lib/runtime-env.js +21 -21
- package/lib/runtime-protocol.js +122 -15
- package/lib/slash-command-mode.js +0 -11
- package/lib/slash-menu-state.js +59 -59
- package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
- package/lib/terminal-key.js +97 -97
- package/lib/text-width.js +335 -151
- package/lib/theme-menu-state.js +31 -0
- package/lib/theme.js +134 -0
- package/lib/tool-display.js +675 -0
- package/lib/tui/component.js +55 -0
- package/lib/tui/cursor.js +29 -0
- package/lib/tui/input-buffer.js +172 -0
- package/lib/tui/tui.js +591 -0
- package/lib/turn-controller.js +68 -78
- package/package.json +28 -28
- package/lib/assistant-stream-buffer.js +0 -25
- package/lib/terminal-ui.js +0 -581
package/lib/tui/tui.js
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { textWidth, truncateToWidth } from "../text-width.js";
|
|
2
|
+
import { createInputBuffer } from "./input-buffer.js";
|
|
3
|
+
|
|
4
|
+
export const CURSOR_MARKER = "\x1b_pi:c\x07";
|
|
5
|
+
|
|
6
|
+
const SYNC_START = "\x1b[?2026h";
|
|
7
|
+
const SYNC_END = "\x1b[?2026l";
|
|
8
|
+
const LINE_RESET = "\x1b[0m";
|
|
9
|
+
const PASTE_ENABLE = "\x1b[?2004h";
|
|
10
|
+
const PASTE_DISABLE = "\x1b[?2004l";
|
|
11
|
+
const DEFAULT_COLUMNS = 80;
|
|
12
|
+
const DEFAULT_ROWS = 24;
|
|
13
|
+
const DEFAULT_RENDER_INTERVAL_MS = 16;
|
|
14
|
+
|
|
15
|
+
export function createTui(options = {}) {
|
|
16
|
+
const input = options.input || process.stdin;
|
|
17
|
+
const output = options.output || process.stdout;
|
|
18
|
+
const schedule = options.setTimeout || setTimeout;
|
|
19
|
+
const cancelSchedule = options.clearTimeout || clearTimeout;
|
|
20
|
+
const now = options.now || Date.now;
|
|
21
|
+
const minRenderIntervalMs = Number.isFinite(options.renderIntervalMs)
|
|
22
|
+
? Math.max(0, options.renderIntervalMs)
|
|
23
|
+
: DEFAULT_RENDER_INTERVAL_MS;
|
|
24
|
+
|
|
25
|
+
let started = false;
|
|
26
|
+
let stopped = false;
|
|
27
|
+
let rawModeBeforeStart = false;
|
|
28
|
+
let inputHandler = null;
|
|
29
|
+
let pasteHandler = null;
|
|
30
|
+
|
|
31
|
+
let children = [];
|
|
32
|
+
let renderRequested = false;
|
|
33
|
+
let renderForceRequested = false;
|
|
34
|
+
let renderMicrotaskQueued = false;
|
|
35
|
+
let renderTimer = null;
|
|
36
|
+
let lastRenderAt = 0;
|
|
37
|
+
|
|
38
|
+
let previousLines = [];
|
|
39
|
+
let previousWidth = 0;
|
|
40
|
+
let previousHeight = 0;
|
|
41
|
+
let cursorRow = 0;
|
|
42
|
+
let hardwareCursorRow = 0;
|
|
43
|
+
let maxLinesRendered = 0;
|
|
44
|
+
let previousViewportTop = 0;
|
|
45
|
+
|
|
46
|
+
let clearOnShrink = false;
|
|
47
|
+
let cursorVisible = false;
|
|
48
|
+
let replayRequested = false;
|
|
49
|
+
const inputBuffer = createInputBuffer({
|
|
50
|
+
onSequence: (sequence) => inputHandler?.(sequence),
|
|
51
|
+
onPaste: (payload) => pasteHandler?.(payload),
|
|
52
|
+
setTimeout: schedule,
|
|
53
|
+
clearTimeout: cancelSchedule,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function columns() {
|
|
57
|
+
return finitePositive(output.columns, DEFAULT_COLUMNS);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rows() {
|
|
61
|
+
return finitePositive(output.rows, DEFAULT_ROWS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function addChild(child) {
|
|
65
|
+
children.push(child);
|
|
66
|
+
requestRender();
|
|
67
|
+
return child;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeChild(child) {
|
|
71
|
+
const index = children.indexOf(child);
|
|
72
|
+
if (index !== -1) {
|
|
73
|
+
children.splice(index, 1);
|
|
74
|
+
requestRender();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clearChildren() {
|
|
79
|
+
if (!children.length) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
children = [];
|
|
83
|
+
requestRender();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderRoot(width) {
|
|
87
|
+
const lines = [];
|
|
88
|
+
for (const child of children) {
|
|
89
|
+
const rendered = child.render(width);
|
|
90
|
+
if (Array.isArray(rendered)) {
|
|
91
|
+
for (const line of rendered) {
|
|
92
|
+
lines.push(typeof line === "string" ? line : String(line ?? ""));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return lines;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function onData(handler) {
|
|
100
|
+
inputHandler = typeof handler === "function" ? handler : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onPaste(handler) {
|
|
104
|
+
pasteHandler = typeof handler === "function" ? handler : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function start() {
|
|
108
|
+
if (started) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
started = true;
|
|
112
|
+
stopped = false;
|
|
113
|
+
rawModeBeforeStart = Boolean(input.isRaw);
|
|
114
|
+
if (typeof input.setRawMode === "function") {
|
|
115
|
+
input.setRawMode(true);
|
|
116
|
+
}
|
|
117
|
+
if (typeof input.setEncoding === "function") {
|
|
118
|
+
input.setEncoding("utf8");
|
|
119
|
+
}
|
|
120
|
+
if (typeof input.resume === "function") {
|
|
121
|
+
input.resume();
|
|
122
|
+
}
|
|
123
|
+
if (typeof input.on === "function") {
|
|
124
|
+
input.on("data", handleInputData);
|
|
125
|
+
}
|
|
126
|
+
if (typeof output.on === "function") {
|
|
127
|
+
output.on("resize", handleResize);
|
|
128
|
+
}
|
|
129
|
+
write(PASTE_ENABLE);
|
|
130
|
+
hideCursor();
|
|
131
|
+
requestRender();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stop() {
|
|
135
|
+
if (!started) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
started = false;
|
|
139
|
+
stopped = true;
|
|
140
|
+
if (renderTimer !== null) {
|
|
141
|
+
cancelSchedule(renderTimer);
|
|
142
|
+
renderTimer = null;
|
|
143
|
+
}
|
|
144
|
+
if (previousLines.length > 0) {
|
|
145
|
+
const targetRow = previousLines.length;
|
|
146
|
+
const delta = targetRow - hardwareCursorRow;
|
|
147
|
+
let tail = "";
|
|
148
|
+
if (delta > 0) {
|
|
149
|
+
tail += `\x1b[${delta}B`;
|
|
150
|
+
} else if (delta < 0) {
|
|
151
|
+
tail += `\x1b[${-delta}A`;
|
|
152
|
+
}
|
|
153
|
+
tail += "\r\n";
|
|
154
|
+
write(tail);
|
|
155
|
+
}
|
|
156
|
+
showCursor();
|
|
157
|
+
if (typeof output.off === "function") {
|
|
158
|
+
output.off("resize", handleResize);
|
|
159
|
+
}
|
|
160
|
+
if (typeof input.off === "function") {
|
|
161
|
+
input.off("data", handleInputData);
|
|
162
|
+
}
|
|
163
|
+
if (typeof input.pause === "function") {
|
|
164
|
+
input.pause();
|
|
165
|
+
}
|
|
166
|
+
if (typeof input.setRawMode === "function") {
|
|
167
|
+
input.setRawMode(rawModeBeforeStart);
|
|
168
|
+
}
|
|
169
|
+
write(PASTE_DISABLE);
|
|
170
|
+
inputBuffer.clear();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleInputData(data) {
|
|
174
|
+
inputBuffer.feed(data);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleResize() {
|
|
178
|
+
requestRender();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Repaint the entire transcript (viewport + scrollback) with current
|
|
182
|
+
// component state — used after appearance changes like theme switches.
|
|
183
|
+
function replayAll() {
|
|
184
|
+
replayRequested = true;
|
|
185
|
+
requestRender(true);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// "force" only bypasses the render throttle so the next frame paints on
|
|
189
|
+
// the following tick. It never resets diff bookkeeping: clearing the
|
|
190
|
+
// screen is decided by doRender's geometry checks alone, so a burst of
|
|
191
|
+
// forced repaints at startup can never wipe existing terminal content.
|
|
192
|
+
function requestRender(force = false) {
|
|
193
|
+
if (stopped) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
renderRequested = true;
|
|
197
|
+
if (force === true) {
|
|
198
|
+
renderForceRequested = true;
|
|
199
|
+
if (renderTimer !== null) {
|
|
200
|
+
cancelSchedule(renderTimer);
|
|
201
|
+
renderTimer = null;
|
|
202
|
+
}
|
|
203
|
+
} else if (renderMicrotaskQueued || renderTimer !== null) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
queueRenderMicrotask();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function queueRenderMicrotask() {
|
|
210
|
+
if (renderMicrotaskQueued) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
renderMicrotaskQueued = true;
|
|
214
|
+
queueMicrotask(() => {
|
|
215
|
+
renderMicrotaskQueued = false;
|
|
216
|
+
scheduleRender();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function scheduleRender() {
|
|
221
|
+
if (stopped || renderTimer !== null || !renderRequested) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const delay = renderForceRequested
|
|
225
|
+
? 0
|
|
226
|
+
: Math.max(0, minRenderIntervalMs - (now() - lastRenderAt));
|
|
227
|
+
renderTimer = schedule(() => {
|
|
228
|
+
renderTimer = null;
|
|
229
|
+
flushRender();
|
|
230
|
+
}, delay);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function flushRender() {
|
|
234
|
+
if (stopped || !renderRequested) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
renderRequested = false;
|
|
238
|
+
renderForceRequested = false;
|
|
239
|
+
lastRenderAt = now();
|
|
240
|
+
doRender();
|
|
241
|
+
if (renderRequested) {
|
|
242
|
+
queueRenderMicrotask();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function doRender() {
|
|
247
|
+
if (!started || stopped) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (replayRequested) {
|
|
251
|
+
// Full repaint (e.g. theme switch): drop caches so components restyle,
|
|
252
|
+
// then reuse the resize machinery — clear screen + scrollback and
|
|
253
|
+
// replay every line under the active palette.
|
|
254
|
+
replayRequested = false;
|
|
255
|
+
for (const child of children) {
|
|
256
|
+
child.invalidate?.();
|
|
257
|
+
}
|
|
258
|
+
previousLines = [];
|
|
259
|
+
previousWidth = -1;
|
|
260
|
+
previousHeight = -1;
|
|
261
|
+
cursorRow = 0;
|
|
262
|
+
hardwareCursorRow = 0;
|
|
263
|
+
maxLinesRendered = 0;
|
|
264
|
+
previousViewportTop = 0;
|
|
265
|
+
}
|
|
266
|
+
const width = columns();
|
|
267
|
+
const height = rows();
|
|
268
|
+
const widthChanged = previousWidth !== 0 && previousWidth !== width;
|
|
269
|
+
const heightChanged = previousHeight !== 0 && previousHeight !== height;
|
|
270
|
+
const previousBufferLength = previousHeight > 0 ? previousViewportTop + previousHeight : height;
|
|
271
|
+
let prevViewportTop = heightChanged ? Math.max(0, previousBufferLength - height) : previousViewportTop;
|
|
272
|
+
let viewportTop = prevViewportTop;
|
|
273
|
+
let localHardwareCursorRow = hardwareCursorRow;
|
|
274
|
+
const computeLineDiff = (targetRow) => {
|
|
275
|
+
const currentScreenRow = localHardwareCursorRow - prevViewportTop;
|
|
276
|
+
const targetScreenRow = targetRow - viewportTop;
|
|
277
|
+
return targetScreenRow - currentScreenRow;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
let newLines = renderRoot(width);
|
|
281
|
+
const cursorPos = extractCursorPosition(newLines, height);
|
|
282
|
+
newLines = applyLineResets(newLines);
|
|
283
|
+
|
|
284
|
+
const fullRender = (clear) => {
|
|
285
|
+
let buffer = SYNC_START;
|
|
286
|
+
if (clear) {
|
|
287
|
+
buffer += "\x1b[2J\x1b[H\x1b[3J";
|
|
288
|
+
}
|
|
289
|
+
for (let index = 0; index < newLines.length; index += 1) {
|
|
290
|
+
if (index > 0) {
|
|
291
|
+
buffer += "\r\n";
|
|
292
|
+
}
|
|
293
|
+
buffer += writableLine(newLines[index], width);
|
|
294
|
+
}
|
|
295
|
+
buffer += SYNC_END;
|
|
296
|
+
write(buffer);
|
|
297
|
+
cursorRow = Math.max(0, newLines.length - 1);
|
|
298
|
+
hardwareCursorRow = cursorRow;
|
|
299
|
+
maxLinesRendered = clear ? newLines.length : Math.max(maxLinesRendered, newLines.length);
|
|
300
|
+
const bufferLength = Math.max(height, newLines.length);
|
|
301
|
+
previousViewportTop = Math.max(0, bufferLength - height);
|
|
302
|
+
positionHardwareCursor(cursorPos, newLines);
|
|
303
|
+
previousLines = newLines;
|
|
304
|
+
previousWidth = width;
|
|
305
|
+
previousHeight = height;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (previousLines.length === 0 && !widthChanged && !heightChanged) {
|
|
309
|
+
logRedraw("first render");
|
|
310
|
+
fullRender(false);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (widthChanged) {
|
|
315
|
+
logRedraw(`terminal width changed (${previousWidth} -> ${width})`);
|
|
316
|
+
fullRender(true);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (heightChanged) {
|
|
321
|
+
logRedraw(`terminal height changed (${previousHeight} -> ${height})`);
|
|
322
|
+
fullRender(true);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (clearOnShrink && newLines.length < maxLinesRendered) {
|
|
327
|
+
logRedraw(`clearOnShrink (maxLinesRendered=${maxLinesRendered})`);
|
|
328
|
+
fullRender(true);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let firstChanged = -1;
|
|
333
|
+
let lastChanged = -1;
|
|
334
|
+
const maxLines = Math.max(newLines.length, previousLines.length);
|
|
335
|
+
for (let index = 0; index < maxLines; index += 1) {
|
|
336
|
+
const oldLine = index < previousLines.length ? previousLines[index] : "";
|
|
337
|
+
const newLine = index < newLines.length ? newLines[index] : "";
|
|
338
|
+
if (oldLine !== newLine) {
|
|
339
|
+
if (firstChanged === -1) {
|
|
340
|
+
firstChanged = index;
|
|
341
|
+
}
|
|
342
|
+
lastChanged = index;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const appendedLines = newLines.length > previousLines.length;
|
|
346
|
+
if (appendedLines) {
|
|
347
|
+
if (firstChanged === -1) {
|
|
348
|
+
firstChanged = previousLines.length;
|
|
349
|
+
}
|
|
350
|
+
lastChanged = newLines.length - 1;
|
|
351
|
+
}
|
|
352
|
+
const appendStart = appendedLines && firstChanged === previousLines.length && firstChanged > 0;
|
|
353
|
+
|
|
354
|
+
if (firstChanged === -1) {
|
|
355
|
+
positionHardwareCursor(cursorPos, newLines);
|
|
356
|
+
previousViewportTop = prevViewportTop;
|
|
357
|
+
previousHeight = height;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (firstChanged >= newLines.length) {
|
|
362
|
+
if (previousLines.length > newLines.length) {
|
|
363
|
+
const targetRow = Math.max(0, newLines.length - 1);
|
|
364
|
+
if (targetRow < prevViewportTop) {
|
|
365
|
+
logRedraw(`deleted lines moved viewport up (${targetRow} < ${prevViewportTop})`);
|
|
366
|
+
fullRender(true);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const extraLines = previousLines.length - newLines.length;
|
|
370
|
+
if (extraLines > height) {
|
|
371
|
+
logRedraw(`extraLines > height (${extraLines} > ${height})`);
|
|
372
|
+
fullRender(true);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
let buffer = SYNC_START;
|
|
376
|
+
const lineDiff = computeLineDiff(targetRow);
|
|
377
|
+
if (lineDiff > 0) {
|
|
378
|
+
buffer += `\x1b[${lineDiff}B`;
|
|
379
|
+
} else if (lineDiff < 0) {
|
|
380
|
+
buffer += `\x1b[${-lineDiff}A`;
|
|
381
|
+
}
|
|
382
|
+
buffer += "\r";
|
|
383
|
+
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
|
384
|
+
if (extraLines > 0 && clearStartOffset > 0) {
|
|
385
|
+
buffer += `\x1b[${clearStartOffset}B`;
|
|
386
|
+
}
|
|
387
|
+
for (let index = 0; index < extraLines; index += 1) {
|
|
388
|
+
buffer += "\r\x1b[2K";
|
|
389
|
+
if (index < extraLines - 1) {
|
|
390
|
+
buffer += "\x1b[1B";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const moveBack = Math.max(0, extraLines - 1 + clearStartOffset);
|
|
394
|
+
if (moveBack > 0) {
|
|
395
|
+
buffer += `\x1b[${moveBack}A`;
|
|
396
|
+
}
|
|
397
|
+
buffer += SYNC_END;
|
|
398
|
+
write(buffer);
|
|
399
|
+
cursorRow = targetRow;
|
|
400
|
+
hardwareCursorRow = targetRow;
|
|
401
|
+
}
|
|
402
|
+
positionHardwareCursor(cursorPos, newLines);
|
|
403
|
+
previousLines = newLines;
|
|
404
|
+
previousWidth = width;
|
|
405
|
+
previousHeight = height;
|
|
406
|
+
previousViewportTop = prevViewportTop;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (firstChanged < prevViewportTop) {
|
|
411
|
+
logRedraw(`firstChanged < viewportTop (${firstChanged} < ${prevViewportTop})`);
|
|
412
|
+
fullRender(true);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let buffer = SYNC_START;
|
|
417
|
+
const prevViewportBottom = prevViewportTop + height - 1;
|
|
418
|
+
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
|
419
|
+
if (moveTargetRow > prevViewportBottom) {
|
|
420
|
+
const currentScreenRow = Math.max(0, Math.min(height - 1, localHardwareCursorRow - prevViewportTop));
|
|
421
|
+
const moveToBottom = height - 1 - currentScreenRow;
|
|
422
|
+
if (moveToBottom > 0) {
|
|
423
|
+
buffer += `\x1b[${moveToBottom}B`;
|
|
424
|
+
}
|
|
425
|
+
const scrollCount = moveTargetRow - prevViewportBottom;
|
|
426
|
+
buffer += "\r\n".repeat(scrollCount);
|
|
427
|
+
prevViewportTop += scrollCount;
|
|
428
|
+
viewportTop += scrollCount;
|
|
429
|
+
localHardwareCursorRow = moveTargetRow;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const lineDiff = computeLineDiff(moveTargetRow);
|
|
433
|
+
if (lineDiff > 0) {
|
|
434
|
+
buffer += `\x1b[${lineDiff}B`;
|
|
435
|
+
} else if (lineDiff < 0) {
|
|
436
|
+
buffer += `\x1b[${-lineDiff}A`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
buffer += appendStart ? "\r\n" : "\r";
|
|
440
|
+
|
|
441
|
+
const renderEnd = Math.min(lastChanged, newLines.length - 1);
|
|
442
|
+
for (let index = firstChanged; index <= renderEnd; index += 1) {
|
|
443
|
+
if (index > firstChanged) {
|
|
444
|
+
buffer += "\r\n";
|
|
445
|
+
}
|
|
446
|
+
buffer += "\x1b[2K";
|
|
447
|
+
buffer += writableLine(newLines[index], width);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let finalCursorRow = renderEnd;
|
|
451
|
+
if (previousLines.length > newLines.length) {
|
|
452
|
+
if (renderEnd < newLines.length - 1) {
|
|
453
|
+
const moveDown = newLines.length - 1 - renderEnd;
|
|
454
|
+
buffer += `\x1b[${moveDown}B`;
|
|
455
|
+
finalCursorRow = newLines.length - 1;
|
|
456
|
+
}
|
|
457
|
+
const extraLines = previousLines.length - newLines.length;
|
|
458
|
+
for (let index = newLines.length; index < previousLines.length; index += 1) {
|
|
459
|
+
buffer += "\r\n\x1b[2K";
|
|
460
|
+
}
|
|
461
|
+
buffer += `\x1b[${extraLines}A`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
buffer += SYNC_END;
|
|
465
|
+
write(buffer);
|
|
466
|
+
|
|
467
|
+
cursorRow = Math.max(0, newLines.length - 1);
|
|
468
|
+
hardwareCursorRow = finalCursorRow;
|
|
469
|
+
maxLinesRendered = Math.max(maxLinesRendered, newLines.length);
|
|
470
|
+
previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
|
|
471
|
+
positionHardwareCursor(cursorPos, newLines);
|
|
472
|
+
previousLines = newLines;
|
|
473
|
+
previousWidth = width;
|
|
474
|
+
previousHeight = height;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function writableLine(line, width) {
|
|
478
|
+
let value = typeof line === "string" ? line : String(line ?? "");
|
|
479
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
480
|
+
value = value.replace(/\r?\n/g, " ");
|
|
481
|
+
}
|
|
482
|
+
const measured = textWidth(value);
|
|
483
|
+
if (measured > width) {
|
|
484
|
+
value = truncateToWidth(value, width);
|
|
485
|
+
}
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function applyLineResets(lines) {
|
|
490
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
491
|
+
const line = lines[index];
|
|
492
|
+
lines[index] = line.includes("\x1b[") ? `${line}${LINE_RESET}` : line;
|
|
493
|
+
}
|
|
494
|
+
return lines;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function extractCursorPosition(lines, height) {
|
|
498
|
+
const viewportTop = Math.max(0, lines.length - height);
|
|
499
|
+
for (let row = lines.length - 1; row >= viewportTop; row -= 1) {
|
|
500
|
+
const line = lines[row];
|
|
501
|
+
const markerIndex = line.indexOf(CURSOR_MARKER);
|
|
502
|
+
if (markerIndex === -1) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const beforeMarker = line.slice(0, markerIndex);
|
|
506
|
+
const col = textWidth(beforeMarker);
|
|
507
|
+
lines[row] = beforeMarker + line.slice(markerIndex + CURSOR_MARKER.length);
|
|
508
|
+
return { row, col };
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function positionHardwareCursor(cursorPos, lines) {
|
|
514
|
+
const totalLines = lines.length;
|
|
515
|
+
if (!cursorPos || totalLines <= 0) {
|
|
516
|
+
if (cursorVisible) {
|
|
517
|
+
hideCursor();
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
let targetRow;
|
|
522
|
+
let targetCol;
|
|
523
|
+
targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
|
|
524
|
+
targetCol = Math.max(0, cursorPos.col);
|
|
525
|
+
const rowDelta = targetRow - hardwareCursorRow;
|
|
526
|
+
let buffer = "";
|
|
527
|
+
if (rowDelta > 0) {
|
|
528
|
+
buffer += `\x1b[${rowDelta}B`;
|
|
529
|
+
} else if (rowDelta < 0) {
|
|
530
|
+
buffer += `\x1b[${-rowDelta}A`;
|
|
531
|
+
}
|
|
532
|
+
buffer += `\x1b[${targetCol + 1}G`;
|
|
533
|
+
write(buffer);
|
|
534
|
+
hardwareCursorRow = targetRow;
|
|
535
|
+
showCursor();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function hideCursor() {
|
|
539
|
+
write("\x1b[?25l");
|
|
540
|
+
cursorVisible = false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function showCursor() {
|
|
544
|
+
write("\x1b[?25h");
|
|
545
|
+
cursorVisible = true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function write(value) {
|
|
549
|
+
if (value) {
|
|
550
|
+
output.write(value);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function logRedraw(reason) {
|
|
555
|
+
if (process.env.RIND_DEBUG_REDRAW !== "1") {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
process.stderr.write(
|
|
559
|
+
`[rind-tui] fullRedraw: ${reason} (prev=${previousLines.length}, height=${rows()})\n`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
start,
|
|
565
|
+
stop,
|
|
566
|
+
requestRender,
|
|
567
|
+
replayAll,
|
|
568
|
+
addChild,
|
|
569
|
+
removeChild,
|
|
570
|
+
clearChildren,
|
|
571
|
+
onData,
|
|
572
|
+
onPaste,
|
|
573
|
+
setClearOnShrink(value) {
|
|
574
|
+
clearOnShrink = Boolean(value);
|
|
575
|
+
},
|
|
576
|
+
get started() {
|
|
577
|
+
return started;
|
|
578
|
+
},
|
|
579
|
+
get columns() {
|
|
580
|
+
return columns();
|
|
581
|
+
},
|
|
582
|
+
get rows() {
|
|
583
|
+
return rows();
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function finitePositive(value, fallback) {
|
|
589
|
+
const number = Number(value);
|
|
590
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
591
|
+
}
|