dsh-neotui 0.0.2 → 0.0.3
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/package.json +1 -1
- package/src/panels.js +21 -8
- package/src/term.js +2 -1
- package/src/views.js +53 -8
- package/src/widgets.js +137 -64
package/package.json
CHANGED
package/src/panels.js
CHANGED
|
@@ -393,10 +393,16 @@ export class WorkspacePanel extends Widget {
|
|
|
393
393
|
};
|
|
394
394
|
walk(this.tree);
|
|
395
395
|
this.searchResults = results;
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
const visH = Math.max(1, this.h - 2);
|
|
397
|
+
if (this.searchSel < (this.searchScroll ?? 0)) this.searchScroll = this.searchSel;
|
|
398
|
+
else if (this.searchSel >= (this.searchScroll ?? 0) + visH) this.searchScroll = this.searchSel - visH + 1;
|
|
399
|
+
this.searchScroll = Math.max(0, Math.min(Math.max(0, results.length - visH), this.searchScroll ?? 0));
|
|
400
|
+
for (let i = 0; i < visH; i++) {
|
|
401
|
+
const idx = this.searchScroll + i;
|
|
402
|
+
if (idx >= results.length) break;
|
|
403
|
+
const sel = idx === this.searchSel;
|
|
398
404
|
screen.fillRect(this.x + 1, this.y + 2 + i, mid - 2, this.y + 2 + i, " ", { bg: sel ? K.MENUSEL : -1 });
|
|
399
|
-
screen.text(this.x + 2, this.y + 2 + i, truncate("⚲ " + basename(results[
|
|
405
|
+
screen.text(this.x + 2, this.y + 2 + i, truncate("⚲ " + basename(results[idx]), mid - 6), { fg: sel ? K.BOLD : K.TXT, bg: sel ? K.MENUSEL : -1 });
|
|
400
406
|
}
|
|
401
407
|
screen.text(this.x + 1, this.y + this.h - 1, ` 匹配 ${results.length} 个文件 · Esc 退出搜索`, { fg: K.FAINT });
|
|
402
408
|
return;
|
|
@@ -781,6 +787,7 @@ export class ControlPanel extends Widget {
|
|
|
781
787
|
this.subPages = ["常规", "插件"];
|
|
782
788
|
this.subPage = 0;
|
|
783
789
|
this.sel = 0;
|
|
790
|
+
this.scroll = 0;
|
|
784
791
|
this.commands = DEFAULT_COMMANDS;
|
|
785
792
|
this.plugins = null;
|
|
786
793
|
this.pluginError = null;
|
|
@@ -884,9 +891,15 @@ export class ControlPanel extends Widget {
|
|
|
884
891
|
}
|
|
885
892
|
const items = this.items();
|
|
886
893
|
if (this.sel >= items.length) this.sel = Math.max(0, items.length - 1);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
894
|
+
const visible = Math.max(1, this.h - 3);
|
|
895
|
+
if (this.sel < this.scroll) this.scroll = this.sel;
|
|
896
|
+
else if (this.sel >= this.scroll + visible) this.scroll = this.sel - visible + 1;
|
|
897
|
+
this.scroll = Math.max(0, Math.min(Math.max(0, items.length - visible), this.scroll));
|
|
898
|
+
for (let i = 0; i < visible; i++) {
|
|
899
|
+
const idx = this.scroll + i;
|
|
900
|
+
const it = items[idx];
|
|
901
|
+
if (!it) { s.hline(this.x + 1, this.x + this.w - 2, this.y + 2 + i, " ", { bg: T.PANEL }); continue; }
|
|
902
|
+
const sel = idx === this.sel;
|
|
890
903
|
s.fillRect(this.x + 1, this.y + 2 + i, this.x + this.w - 2, this.y + 2 + i, " ", { bg: sel ? T.MENUSEL : T.PANEL });
|
|
891
904
|
const label = it[0];
|
|
892
905
|
s.text(this.x + 2, this.y + 2 + i, truncate(label, this.w - 34), { fg: sel ? T.BOLD : (this.page === 0 ? T.ACCENT : T.TXT), bg: sel ? T.MENUSEL : T.PANEL, attrs: sel ? 1 : 0 });
|
|
@@ -947,9 +960,9 @@ export class ControlPanel extends Widget {
|
|
|
947
960
|
}
|
|
948
961
|
return true;
|
|
949
962
|
}
|
|
950
|
-
const idx = ev.y - this.y - 2;
|
|
963
|
+
const idx = this.scroll + (ev.y - this.y - 2);
|
|
951
964
|
const items = this.items();
|
|
952
|
-
if (idx >= 0 && idx < items.length &&
|
|
965
|
+
if (idx >= 0 && idx < items.length && (ev.y - this.y - 2) < this.h - 3) {
|
|
953
966
|
this.sel = idx;
|
|
954
967
|
const it = items[idx];
|
|
955
968
|
if (it && it[2]) { it[2](); this.app.redraw(); }
|
package/src/term.js
CHANGED
|
@@ -44,7 +44,8 @@ export class Term {
|
|
|
44
44
|
this.input.on("data", (chunk) => this.#feed(this.decoder.write(chunk)));
|
|
45
45
|
const o = this.output;
|
|
46
46
|
o.write("\x1b[?1049h"); // alt screen
|
|
47
|
-
o.write("\x1b[?25l"); // hide cursor
|
|
47
|
+
o.write("\x1b[?25l"); // hide cursor (shown only over the focused input)
|
|
48
|
+
o.write("\x1b[5 q"); // DECSCUSR: blinking vertical bar caret
|
|
48
49
|
o.write("\x1b[?1000h"); // mouse: clicks
|
|
49
50
|
o.write("\x1b[?1002h"); // mouse: drag
|
|
50
51
|
o.write("\x1b[?1003h"); // mouse: all motion
|
package/src/views.js
CHANGED
|
@@ -528,6 +528,7 @@ export class ChatView extends Widget {
|
|
|
528
528
|
this.collapsedBlocks = new Set(); // per-block COLLAPSE (default expanded): `${realIdx}:${bi}`
|
|
529
529
|
this.thinkMode = "expanded"; // think blocks: expanded by default (t toggles)
|
|
530
530
|
this.bashMode = "expanded"; // tool blocks: expanded | collapsed (b toggles)
|
|
531
|
+
this.todosVisible = true; // todo block above the input (Shift+T toggles)
|
|
531
532
|
this.running = false;
|
|
532
533
|
this.hasMore = false;
|
|
533
534
|
this.loadingOlder = false;
|
|
@@ -679,12 +680,20 @@ export class ChatView extends Widget {
|
|
|
679
680
|
}
|
|
680
681
|
}
|
|
681
682
|
|
|
683
|
+
/** Height of the collapsible todo block (0 when empty or collapsed). */
|
|
684
|
+
#todoHeight() {
|
|
685
|
+
const todos = this.app.todos;
|
|
686
|
+
if (!this.todosVisible || !todos || todos.length === 0) return 0;
|
|
687
|
+
return Math.min(todos.length, 6) + 1; // 1 header row + up to 6 todos
|
|
688
|
+
}
|
|
689
|
+
|
|
682
690
|
#inputChanged() {
|
|
683
691
|
// Multi-line input grew/shrunk → reflow view vs input, keep the tail visible.
|
|
684
692
|
const ih = this.input.height();
|
|
685
693
|
const prevIh = this.input.h;
|
|
686
694
|
this.input.h = ih;
|
|
687
|
-
|
|
695
|
+
const th = this.#todoHeight();
|
|
696
|
+
this.view.h = this.h - ih - th - 1;
|
|
688
697
|
this.input.y = this.y + this.h - ih;
|
|
689
698
|
if (ih !== prevIh) this.app.layout();
|
|
690
699
|
this.app.redraw();
|
|
@@ -694,7 +703,8 @@ export class ChatView extends Widget {
|
|
|
694
703
|
this.x = x; this.y = y; this.w = w; this.h = h;
|
|
695
704
|
const ih = this.input.height();
|
|
696
705
|
this.input.h = ih;
|
|
697
|
-
|
|
706
|
+
const th = this.#todoHeight();
|
|
707
|
+
this.view.x = x; this.view.y = y; this.view.w = w; this.view.h = h - ih - th - 1;
|
|
698
708
|
this.input.x = x; this.input.y = y + h - ih; this.input.w = w;
|
|
699
709
|
this.cache.clear();
|
|
700
710
|
this.#rebuild();
|
|
@@ -1107,10 +1117,27 @@ export class ChatView extends Widget {
|
|
|
1107
1117
|
screen.invertRect(this.view.x, this.view.y + (y0 - this.view.scrollY), this.view.x + this.view.w - 2, this.view.y + (y1 - this.view.scrollY));
|
|
1108
1118
|
}
|
|
1109
1119
|
}
|
|
1120
|
+
this.#renderTodos(screen);
|
|
1110
1121
|
screen.hline(this.x, this.x + this.w - 1, this.input.y - 1, "─", { fg: T.BORDER2 });
|
|
1111
1122
|
this.input.render(screen);
|
|
1112
1123
|
}
|
|
1113
1124
|
|
|
1125
|
+
/** Collapsible todo block between the view and the input (Shift+T toggles). */
|
|
1126
|
+
#renderTodos(screen) {
|
|
1127
|
+
const th = this.#todoHeight();
|
|
1128
|
+
if (th === 0) return;
|
|
1129
|
+
const todos = this.app.todos ?? [];
|
|
1130
|
+
const y = this.input.y - th - 1;
|
|
1131
|
+
screen.fillRect(this.x, y, this.x + this.w - 1, y + th - 1, " ", { bg: T.THINKBG });
|
|
1132
|
+
screen.text(this.x, y, " ☐ 任务清单(Shift+T 折叠)", { fg: K.FAINT });
|
|
1133
|
+
for (let i = 0; i < Math.min(todos.length, th - 1); i++) {
|
|
1134
|
+
const t = todos[i];
|
|
1135
|
+
const icon = t.status === "completed" ? "✓" : t.status === "in_progress" ? "◉" : "○";
|
|
1136
|
+
const color = t.status === "completed" ? T.FAINT : t.status === "in_progress" ? T.WARN : T.DIM;
|
|
1137
|
+
screen.text(this.x + 2, y + 1 + i, `${icon} ${truncate(t.content ?? String(t), this.w - 6)}`, { fg: color });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1114
1141
|
onMouse(ev) {
|
|
1115
1142
|
if (this.input.inside(ev.x, ev.y)) {
|
|
1116
1143
|
// INSERT mode is keyboard-only; only position the cursor if already there.
|
|
@@ -1213,6 +1240,12 @@ export class ChatView extends Widget {
|
|
|
1213
1240
|
return true;
|
|
1214
1241
|
}
|
|
1215
1242
|
if (ev.name === "char" && ev.key === "t" && !ev.ctrl) {
|
|
1243
|
+
if (ev.shift) {
|
|
1244
|
+
this.todosVisible = !this.todosVisible;
|
|
1245
|
+
this.app.toast(this.todosVisible ? "任务块:显示(Shift+T 折叠)" : "任务块:折叠(Shift+T 展开)");
|
|
1246
|
+
this.#inputChanged();
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1216
1249
|
this.thinkMode = this.thinkMode === "collapsed" ? "expanded" : "collapsed";
|
|
1217
1250
|
this.expanded.clear();
|
|
1218
1251
|
this.collapsedBlocks.clear();
|
|
@@ -1227,12 +1260,16 @@ export class ChatView extends Widget {
|
|
|
1227
1260
|
function toolSummary(b) {
|
|
1228
1261
|
// Prefer the human description of what the tool did (the agent's `description`
|
|
1229
1262
|
// argument), then the host card title, then the raw command — mirrors the web.
|
|
1263
|
+
// File tools get a path-based summary so read/edit collapse as concisely as bash.
|
|
1230
1264
|
if (b.args) {
|
|
1231
1265
|
try {
|
|
1232
1266
|
const a = JSON.parse(b.args);
|
|
1233
1267
|
if (typeof a === "object" && a !== null) {
|
|
1234
|
-
const desc = a.description ?? a.summary ?? a.title ?? a.
|
|
1268
|
+
const desc = a.description ?? a.summary ?? a.title ?? a.query ?? a.content ?? a.name;
|
|
1235
1269
|
if (desc) return String(desc).slice(0, 120);
|
|
1270
|
+
if (a.file_path) return `read ${String(a.file_path)}`.slice(0, 120);
|
|
1271
|
+
if (a.path && a.command) return `${a.command} ${String(a.path)}`.slice(0, 120);
|
|
1272
|
+
if (a.path) return String(a.path).slice(0, 120);
|
|
1236
1273
|
return a.command ?? null;
|
|
1237
1274
|
}
|
|
1238
1275
|
return String(a).slice(0, 120);
|
|
@@ -2396,11 +2433,14 @@ export class App {
|
|
|
2396
2433
|
if (this.menu) this.menu.render(s);
|
|
2397
2434
|
if (this.overlay) this.overlay.render(s);
|
|
2398
2435
|
if (this.toastMsg) {
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2436
|
+
// Toasts land in the LOWER half (just above the input/footer) where the
|
|
2437
|
+
// user's attention is while pressing shortcuts — a solid color block,
|
|
2438
|
+
// not a top-of-screen whisper.
|
|
2439
|
+
const w = Math.min(s.w - 4, strWidth(this.toastMsg) + 6);
|
|
2440
|
+
const x0 = Math.max(2, Math.floor((s.w - w) / 2));
|
|
2441
|
+
const y = Math.max(1, s.h - this.footerHeight() - 2);
|
|
2442
|
+
s.fillRect(x0 - 1, y - 1, x0 + w, y + 1, " ", { bg: T.ACCENT });
|
|
2443
|
+
s.text(x0 + 1, y, truncate(this.toastMsg, w - 2), { fg: T.SELFG, bg: T.ACCENT, attrs: 1 });
|
|
2404
2444
|
}
|
|
2405
2445
|
if (this.renameInput && this.popup) this.renameInput.render(s);
|
|
2406
2446
|
|
|
@@ -2409,6 +2449,11 @@ export class App {
|
|
|
2409
2449
|
if (this.overlay && typeof this.overlay.kittyTransmit === "function" && kittyCapable()) {
|
|
2410
2450
|
tail = this.overlay.kittyTransmit();
|
|
2411
2451
|
}
|
|
2452
|
+
// Native terminal caret: a blinking vertical bar (text-editor style) at
|
|
2453
|
+
// the focused input's cursor cell; hidden everywhere else.
|
|
2454
|
+
const cell = this.focused?.cursorCell;
|
|
2455
|
+
if (cell) tail = `\x1b[?25h\x1b[${cell.y + 1};${cell.x + 1}H` + tail;
|
|
2456
|
+
else tail = "\x1b[?25l" + tail;
|
|
2412
2457
|
this.term.output.write(out + tail);
|
|
2413
2458
|
}
|
|
2414
2459
|
|
package/src/widgets.js
CHANGED
|
@@ -247,9 +247,59 @@ export class Input extends Widget {
|
|
|
247
247
|
this.histIdx = -1;
|
|
248
248
|
}
|
|
249
249
|
#cps() { return Array.from(this.value); } // code points
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
/** Visual rows for multi-line input: logical lines wrapped at the width. */
|
|
251
|
+
#visualRows() {
|
|
252
|
+
const inner0 = Math.max(1, this.w - strWidth(this.prompt) - 2);
|
|
253
|
+
const innerN = Math.max(1, this.w - 2);
|
|
254
|
+
const rows = [];
|
|
255
|
+
const cps = Array.from(this.value);
|
|
256
|
+
let text = "", width = 0, limit = inner0, start = 0;
|
|
257
|
+
for (let i = 0; i < cps.length; i++) {
|
|
258
|
+
const ch = cps[i];
|
|
259
|
+
if (ch === "\n") {
|
|
260
|
+
rows.push({ text, start, end: i, limit });
|
|
261
|
+
text = ""; width = 0; limit = innerN; start = i + 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const cw = strWidth(ch);
|
|
265
|
+
if (width + cw > limit && width > 0) {
|
|
266
|
+
rows.push({ text, start, end: i, limit });
|
|
267
|
+
text = ""; width = 0; limit = innerN; start = i;
|
|
268
|
+
}
|
|
269
|
+
text += ch; width += cw;
|
|
270
|
+
}
|
|
271
|
+
rows.push({ text, start, end: cps.length, limit });
|
|
272
|
+
return rows;
|
|
273
|
+
}
|
|
274
|
+
/** [visualRow, display-col] of the cursor in wrapped coordinates. */
|
|
275
|
+
#cursorVisual() {
|
|
276
|
+
const rows = this.#visualRows();
|
|
277
|
+
const cursor = Math.max(0, Math.min(this.cursor, Array.from(this.value).length));
|
|
278
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
279
|
+
const r = rows[ri];
|
|
280
|
+
if (cursor >= r.start && cursor <= r.end) {
|
|
281
|
+
const before = Array.from(r.text).slice(0, cursor - r.start);
|
|
282
|
+
return { row: ri, col: before.reduce((w, ch) => w + strWidth(ch), 0) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const last = rows[rows.length - 1];
|
|
286
|
+
return { row: rows.length - 1, col: strWidth(last.text) };
|
|
287
|
+
}
|
|
288
|
+
/** Code-point index of the nearest position at a visual [row, col]. */
|
|
289
|
+
#indexAtVisual(row, col) {
|
|
290
|
+
const rows = this.#visualRows();
|
|
291
|
+
const r = rows[Math.max(0, Math.min(row, rows.length - 1))];
|
|
292
|
+
const cps = Array.from(r.text);
|
|
293
|
+
let w = 0, j = 0;
|
|
294
|
+
for (; j < cps.length; j++) {
|
|
295
|
+
const cw = strWidth(cps[j]);
|
|
296
|
+
if (col < w + cw / 2) break;
|
|
297
|
+
w += cw;
|
|
298
|
+
}
|
|
299
|
+
return r.start + j;
|
|
300
|
+
}
|
|
301
|
+
/** Rendered height: 1, or wrapped rows capped at maxLines when multi. */
|
|
302
|
+
height() { return this.multi ? Math.max(1, Math.min(this.maxLines, this.#visualRows().length)) : 1; }
|
|
253
303
|
setValue(v, opts = {}) {
|
|
254
304
|
this.value = String(v);
|
|
255
305
|
this.cursor = this.#cps().length;
|
|
@@ -270,75 +320,88 @@ export class Input extends Widget {
|
|
|
270
320
|
cps.splice(idx, 1);
|
|
271
321
|
this.value = cps.join("");
|
|
272
322
|
}
|
|
273
|
-
/**
|
|
274
|
-
#
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
#indexAtLineCol(line, col) {
|
|
285
|
-
const lines = this.#lines();
|
|
286
|
-
let idx = 0;
|
|
287
|
-
for (let i = 0; i < Math.min(line, lines.length); i++) idx += Array.from(lines[i]).length + 1;
|
|
288
|
-
const target = lines[Math.min(line, lines.length - 1)] ?? "";
|
|
289
|
-
const cps = Array.from(target);
|
|
290
|
-
let w = 0;
|
|
291
|
-
for (let j = 0; j < cps.length; j++) {
|
|
292
|
-
const cw = strWidth(cps[j]);
|
|
293
|
-
if (col < w + cw / 2) break;
|
|
294
|
-
w += cw;
|
|
295
|
-
idx++;
|
|
296
|
-
}
|
|
297
|
-
return idx;
|
|
323
|
+
/** Scroll offset that keeps the cursor's visual row inside the window. */
|
|
324
|
+
#scrollStart(h) {
|
|
325
|
+
const rows = this.#visualRows();
|
|
326
|
+
const { row } = this.#cursorVisual();
|
|
327
|
+
const maxStart = Math.max(0, rows.length - this.maxLines);
|
|
328
|
+
let start = this.scrollY ?? 0;
|
|
329
|
+
if (row < start) start = row;
|
|
330
|
+
else if (row >= start + h) start = row - h + 1;
|
|
331
|
+
start = Math.max(0, Math.min(maxStart, start));
|
|
332
|
+
this.scrollY = start;
|
|
333
|
+
return start;
|
|
298
334
|
}
|
|
299
335
|
render(screen) {
|
|
300
|
-
|
|
301
|
-
|
|
336
|
+
if (!this.multi) {
|
|
337
|
+
// single-line: horizontal scroll (search/rename/picker inputs)
|
|
338
|
+
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y, " ", { bg: this.bg });
|
|
339
|
+
screen.hline(this.x, this.x + this.w - 1, this.y, "─", { fg: this.border, bg: this.bg });
|
|
340
|
+
const promptW = strWidth(this.prompt);
|
|
341
|
+
const inner = Math.max(0, this.w - promptW - 2);
|
|
342
|
+
screen.text(this.x, this.y, this.prompt, { fg: T.ACCENT, bg: this.bg });
|
|
343
|
+
if (this.value === "" && this.placeholder) {
|
|
344
|
+
screen.text(this.x + promptW, this.y, truncate(this.placeholder, inner), { fg: T.FAINT, bg: this.bg });
|
|
345
|
+
this.cursorCell = { x: this.x + promptW, y: this.y };
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const before = Array.from(this.value).slice(0, this.cursor).join("");
|
|
349
|
+
const cx = strWidth(before);
|
|
350
|
+
let start = 0;
|
|
351
|
+
while (cx - start >= inner) start += Math.max(1, Math.floor(inner / 2));
|
|
352
|
+
const visible = truncate(Array.from(this.value).slice(start).join(""), inner);
|
|
353
|
+
screen.text(this.x + promptW, this.y, visible, { fg: this.fg, bg: this.bg });
|
|
354
|
+
this.cursorCell = { x: this.x + promptW + Math.min(inner, Math.max(0, cx - start)), y: this.y };
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// multi-line: auto-wrap + scroll window that follows the cursor
|
|
358
|
+
const rows = this.#visualRows();
|
|
359
|
+
const h = Math.min(this.maxLines, rows.length);
|
|
360
|
+
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + h - 1, " ", { bg: this.bg });
|
|
302
361
|
screen.hline(this.x, this.x + this.w - 1, this.y, "─", { fg: this.border, bg: this.bg });
|
|
303
|
-
const lines = this.#lines();
|
|
304
|
-
const [curLine, curCol] = this.#cursorPos();
|
|
305
|
-
const promptW = strWidth(this.prompt);
|
|
306
|
-
const inner0 = Math.max(0, this.w - promptW - 2);
|
|
307
|
-
const innerN = Math.max(0, this.w - 2);
|
|
308
362
|
if (this.value === "" && this.placeholder) {
|
|
309
363
|
screen.text(this.x, this.y, this.prompt, { fg: T.ACCENT, bg: this.bg });
|
|
310
|
-
screen.text(this.x +
|
|
364
|
+
screen.text(this.x + strWidth(this.prompt), this.y, truncate(this.placeholder, this.w - strWidth(this.prompt) - 2), { fg: T.FAINT, bg: this.bg });
|
|
365
|
+
this.cursorCell = { x: this.x + strWidth(this.prompt), y: this.y };
|
|
311
366
|
return;
|
|
312
367
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
368
|
+
const { row: curRow, col: curCol } = this.#cursorVisual();
|
|
369
|
+
const start = this.#scrollStart(h);
|
|
370
|
+
for (let ri = start; ri < Math.min(rows.length, start + h); ri++) {
|
|
371
|
+
const r = rows[ri];
|
|
372
|
+
const y = this.y + (ri - start);
|
|
373
|
+
if (ri === 0) {
|
|
317
374
|
screen.text(this.x, y, this.prompt, { fg: T.ACCENT, bg: this.bg });
|
|
318
|
-
|
|
319
|
-
if (curLine === 0) {
|
|
320
|
-
while (curCol - start >= inner0) start += Math.max(1, Math.floor(inner0 / 2));
|
|
321
|
-
}
|
|
322
|
-
const visible = truncate(Array.from(line).slice(start).join(""), inner0);
|
|
323
|
-
screen.text(this.x + promptW, y, visible, { fg: this.fg, bg: this.bg });
|
|
324
|
-
if (curLine === 0) {
|
|
325
|
-
const at = Math.min(inner0, Math.max(0, curCol - start));
|
|
326
|
-
screen.put(this.x + promptW + at, y, "█", { fg: T.FAINT, bg: this.bg });
|
|
327
|
-
}
|
|
375
|
+
screen.text(this.x + strWidth(this.prompt), y, r.text, { fg: this.fg, bg: this.bg });
|
|
328
376
|
} else {
|
|
329
|
-
screen.text(this.x + 1, y,
|
|
330
|
-
if (curLine === li) {
|
|
331
|
-
const at = Math.min(innerN, Math.max(0, curCol));
|
|
332
|
-
screen.put(this.x + 1 + at, y, "█", { fg: T.FAINT, bg: this.bg });
|
|
333
|
-
}
|
|
377
|
+
screen.text(this.x + 1, y, r.text, { fg: this.fg, bg: this.bg });
|
|
334
378
|
}
|
|
335
379
|
}
|
|
380
|
+
// Native terminal caret (blinking bar) is positioned by the app after the
|
|
381
|
+
// frame; store the cell it should occupy (the char at the cursor index).
|
|
382
|
+
const curY = this.y + (curRow - start);
|
|
383
|
+
const curX = curRow === 0 ? this.x + strWidth(this.prompt) + curCol : this.x + 1 + curCol;
|
|
384
|
+
this.cursorCell = { x: Math.min(this.x + this.w - 1, curX), y: Math.min(this.y + h - 1, curY) };
|
|
336
385
|
}
|
|
337
386
|
onMouse(ev) {
|
|
338
387
|
if (ev.kind === "press" && ev.button === 0) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
388
|
+
if (this.multi) {
|
|
389
|
+
const h = this.height();
|
|
390
|
+
const start = this.scrollY ?? 0;
|
|
391
|
+
const row = start + Math.max(0, Math.min(h - 1, ev.y - this.y));
|
|
392
|
+
const rx = ev.x - this.x - (row === 0 ? strWidth(this.prompt) : 1);
|
|
393
|
+
this.cursor = this.#indexAtVisual(row, Math.max(0, rx));
|
|
394
|
+
} else {
|
|
395
|
+
const rx = ev.x - this.x - strWidth(this.prompt);
|
|
396
|
+
let w = 0, idx = 0;
|
|
397
|
+
for (const ch of Array.from(this.value)) {
|
|
398
|
+
const cw = strWidth(ch);
|
|
399
|
+
if (rx < w + cw / 2) break;
|
|
400
|
+
w += cw;
|
|
401
|
+
idx++;
|
|
402
|
+
}
|
|
403
|
+
this.cursor = idx;
|
|
404
|
+
}
|
|
342
405
|
return true;
|
|
343
406
|
}
|
|
344
407
|
return false;
|
|
@@ -355,12 +418,21 @@ export class Input extends Widget {
|
|
|
355
418
|
return true;
|
|
356
419
|
case "left": this.selectAll = false; this.cursor = Math.max(0, this.cursor - 1); return true;
|
|
357
420
|
case "right": this.selectAll = false; this.cursor = Math.min(this.#cps().length, this.cursor + 1); return true;
|
|
358
|
-
case "home": {
|
|
359
|
-
|
|
421
|
+
case "home": {
|
|
422
|
+
if (this.multi) { const rows = this.#visualRows(); const { row } = this.#cursorVisual(); this.cursor = rows[row].start; }
|
|
423
|
+
else this.cursor = 0;
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
case "end": {
|
|
427
|
+
if (this.multi) { const rows = this.#visualRows(); const { row } = this.#cursorVisual(); this.cursor = rows[row].end; }
|
|
428
|
+
else this.cursor = this.#cps().length;
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
360
431
|
case "up":
|
|
361
432
|
if (this.multi) {
|
|
362
|
-
const
|
|
363
|
-
|
|
433
|
+
const rows = this.#visualRows();
|
|
434
|
+
const { row, col } = this.#cursorVisual();
|
|
435
|
+
if (row > 0) this.cursor = this.#indexAtVisual(row - 1, col);
|
|
364
436
|
} else if (this.history.length) {
|
|
365
437
|
this.histIdx = this.histIdx < 0 ? this.history.length - 1 : Math.max(0, this.histIdx - 1);
|
|
366
438
|
this.setValue(this.history[this.histIdx] ?? "");
|
|
@@ -368,8 +440,9 @@ export class Input extends Widget {
|
|
|
368
440
|
return true;
|
|
369
441
|
case "down":
|
|
370
442
|
if (this.multi) {
|
|
371
|
-
const
|
|
372
|
-
|
|
443
|
+
const rows = this.#visualRows();
|
|
444
|
+
const { row, col } = this.#cursorVisual();
|
|
445
|
+
if (row < rows.length - 1) this.cursor = this.#indexAtVisual(row + 1, col);
|
|
373
446
|
} else if (this.histIdx >= 0) {
|
|
374
447
|
this.histIdx++;
|
|
375
448
|
if (this.histIdx >= this.history.length) { this.histIdx = -1; this.setValue(""); }
|