dsh-neotui 0.1.10 → 0.1.12
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 +1 -0
- package/src/views.js +21 -3
- package/src/widgets.js +80 -9
package/package.json
CHANGED
package/src/panels.js
CHANGED
|
@@ -1219,6 +1219,7 @@ export class ControlPanel extends Widget {
|
|
|
1219
1219
|
["[", "上一提问的终点", () => { this.app.closeOverlay(); this.app.focus(this.app.chat); this.app.chat.onKey({ type: "key", name: "char", key: "[", text: "[", ctrl: false, alt: false, shift: false }); }],
|
|
1220
1220
|
["]", "下一提问的终点", () => { this.app.closeOverlay(); this.app.focus(this.app.chat); this.app.chat.onKey({ type: "key", name: "char", key: "]", text: "]", ctrl: false, alt: false, shift: false }); }],
|
|
1221
1221
|
["Ctrl+L", "输入栏 展开/折叠", () => { this.app.closeOverlay(); this.app.focus(this.app.chat.input); this.app.chat.input.onKey({ type: "key", name: "char", key: "l", text: "l", ctrl: true, alt: false, shift: false }); }],
|
|
1222
|
+
["Ctrl+Shift+C", "复制输入栏选区", () => { this.app.closeOverlay(); this.app.chat.input.onKey({ type: "key", name: "char", key: "c", text: "c", ctrl: true, alt: false, shift: true }); }],
|
|
1222
1223
|
["Ctrl+P", "控制面板", () => { this.page = 1; this.sel = 0; this.app.redraw(); }],
|
|
1223
1224
|
["Ctrl+M", "切换模型", () => { this.app.overlay = buildModelPicker(this.app); }],
|
|
1224
1225
|
["Ctrl+T", "轨迹视图", () => { this.app.closeOverlay(); this.app.setMode("trajectory"); }],
|
package/src/views.js
CHANGED
|
@@ -1824,6 +1824,7 @@ export class App {
|
|
|
1824
1824
|
this.sidebarVisible = true; // Ctrl+B hides the whole session pane (nvim-style)
|
|
1825
1825
|
this.sidebarWidth = 30; // draggable divider: the session pane's column width
|
|
1826
1826
|
this.draggingDivider = false;
|
|
1827
|
+
this.inputDrag = false; // mouse drag-selection inside the input is active
|
|
1827
1828
|
this.feedbackMap = new Map(); // messageId → {rating, version}
|
|
1828
1829
|
this.searchQuery = null; // active find-in-conversation term (highlight)
|
|
1829
1830
|
this.findQuery = null; // term being typed in the find picker
|
|
@@ -2768,6 +2769,13 @@ export class App {
|
|
|
2768
2769
|
// unhandled keys fall through to global shortcuts
|
|
2769
2770
|
}
|
|
2770
2771
|
if (ev.type === "mouse") {
|
|
2772
|
+
// input drag-selection: the gesture continues across motion events
|
|
2773
|
+
// even when the pointer leaves the input area
|
|
2774
|
+
if (ev.kind === "release" && ev.button === 0 && this.inputDrag) {
|
|
2775
|
+
this.inputDrag = false;
|
|
2776
|
+
if (this.chat.input.onMouse(ev)) this.redraw();
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2771
2779
|
// Draggable sidebar divider: press on the boundary column (±1) starts a
|
|
2772
2780
|
// resize; drag events (motion flag) update the width live.
|
|
2773
2781
|
const divX = this.sidebarVisible ? this.sidebarWidth : -1;
|
|
@@ -2788,6 +2796,10 @@ export class App {
|
|
|
2788
2796
|
// Pure mouse motion must never change focus/mode (vim-style: INSERT is
|
|
2789
2797
|
// keyboard-only). It reaches just the already-focused widget (drag select).
|
|
2790
2798
|
if (ev.motion) {
|
|
2799
|
+
if (this.inputDrag) {
|
|
2800
|
+
if (this.chat.input.onMouse(ev)) this.redraw();
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2791
2803
|
if (this.focused?.onMouse?.(ev)) this.redraw();
|
|
2792
2804
|
return;
|
|
2793
2805
|
}
|
|
@@ -2795,9 +2807,15 @@ export class App {
|
|
|
2795
2807
|
this.focus(this.sidebar);
|
|
2796
2808
|
if (this.sidebar.onMouse(ev)) this.redraw();
|
|
2797
2809
|
} else if (this.chat.input.inside(ev.x, ev.y)) {
|
|
2798
|
-
//
|
|
2799
|
-
//
|
|
2800
|
-
if (
|
|
2810
|
+
// mouse SELECTION in the input works regardless of mode; typing stays
|
|
2811
|
+
// gated behind i / Esc (vim-style)
|
|
2812
|
+
if (ev.kind === "press" && ev.button === 0) {
|
|
2813
|
+
this.inputDrag = true;
|
|
2814
|
+
if (this.chat.input.onMouse(ev)) this.redraw();
|
|
2815
|
+
} else if (ev.kind === "release" && ev.button === 0) {
|
|
2816
|
+
this.inputDrag = false;
|
|
2817
|
+
if (this.chat.input.onMouse(ev)) this.redraw();
|
|
2818
|
+
} else if (this.focused === this.chat.input) {
|
|
2801
2819
|
if (this.chat.input.onMouse(ev)) this.redraw();
|
|
2802
2820
|
} else if (ev.kind === "press") {
|
|
2803
2821
|
this.toast("按 i 进入输入(vim 式)");
|
package/src/widgets.js
CHANGED
|
@@ -258,6 +258,8 @@ export class Input extends Widget {
|
|
|
258
258
|
this.app = opts.app ?? null;
|
|
259
259
|
this.pendingPaste = null; // large-paste stage 1: held-back clipboard text
|
|
260
260
|
this.pasteMark = null; // code-point span of the immutable "[已复制…]" token
|
|
261
|
+
this.selStart = null; // drag-selection [start, end) code-point span
|
|
262
|
+
this.selEnd = null;
|
|
261
263
|
this.onChange = opts.onChange ?? null;
|
|
262
264
|
this.allowEmptyEnter = opts.allowEmptyEnter ?? false;
|
|
263
265
|
this.history = [];
|
|
@@ -333,12 +335,13 @@ export class Input extends Widget {
|
|
|
333
335
|
this.#touch();
|
|
334
336
|
}
|
|
335
337
|
this.#edit(at, at, text);
|
|
336
|
-
this.onChange?.();
|
|
337
338
|
}
|
|
338
339
|
/** EVERY edit goes through here so the immutable paste token behaves as
|
|
339
340
|
* one unit: deleting any part of it removes it whole, typing inside it
|
|
340
|
-
* replaces it, edits elsewhere just shift it.
|
|
341
|
+
* replaces it, edits elsewhere just shift it. Always notifies onChange —
|
|
342
|
+
* the second-paste swap must reflow the layout just like typing. */
|
|
341
343
|
#edit(from, to, text = "") {
|
|
344
|
+
this.selStart = this.selEnd = null; // edits consume the selection
|
|
342
345
|
const cps = this.#cps();
|
|
343
346
|
const t = Array.from(text);
|
|
344
347
|
const m = this.pasteMark;
|
|
@@ -351,6 +354,7 @@ export class Input extends Widget {
|
|
|
351
354
|
this.pasteMark = null;
|
|
352
355
|
this.pendingPaste = null;
|
|
353
356
|
this.value = cps.join("");
|
|
357
|
+
this.onChange?.();
|
|
354
358
|
return;
|
|
355
359
|
}
|
|
356
360
|
if (m && to <= m.start) {
|
|
@@ -360,6 +364,7 @@ export class Input extends Widget {
|
|
|
360
364
|
cps.splice(from, to - from, ...t);
|
|
361
365
|
this.cursor = from + t.length;
|
|
362
366
|
this.value = cps.join("");
|
|
367
|
+
this.onChange?.();
|
|
363
368
|
}
|
|
364
369
|
#deleteAt(idx) {
|
|
365
370
|
this.#edit(idx, idx + 1);
|
|
@@ -419,6 +424,21 @@ export class Input extends Widget {
|
|
|
419
424
|
screen.text(this.x + 1, y, r.text, { fg: this.fg, bg: this.bg });
|
|
420
425
|
}
|
|
421
426
|
}
|
|
427
|
+
// drag-selection highlight: invert the selected columns per wrapped row
|
|
428
|
+
if (this.selStart !== null && this.selEnd !== null && this.selEnd > this.selStart) {
|
|
429
|
+
for (let ri = start; ri < Math.min(rows.length, start + h); ri++) {
|
|
430
|
+
const r = rows[ri];
|
|
431
|
+
const lo = Math.max(r.start, this.selStart);
|
|
432
|
+
const hi = Math.min(r.end, this.selEnd);
|
|
433
|
+
if (lo >= hi) continue;
|
|
434
|
+
const rowY = this.y + (ri - start);
|
|
435
|
+
const text = Array.from(r.text);
|
|
436
|
+
const preW = strWidth(text.slice(0, lo - r.start).join(""));
|
|
437
|
+
const selW = strWidth(text.slice(lo - r.start, hi - r.start).join(""));
|
|
438
|
+
const x0 = this.x + (ri === 0 ? strWidth(this.prompt) : 1) + preW;
|
|
439
|
+
screen.invertRect(x0, rowY, Math.min(this.x + this.w - 1, x0 + Math.max(0, selW - 1)), rowY);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
422
442
|
// Native terminal caret (blinking bar) is positioned by the app after the
|
|
423
443
|
// frame; store the cell it should occupy (the char at the cursor index).
|
|
424
444
|
const curY = this.y + (curRow - start);
|
|
@@ -446,6 +466,33 @@ export class Input extends Widget {
|
|
|
446
466
|
this.cursor = idx;
|
|
447
467
|
this.#snapCursor();
|
|
448
468
|
}
|
|
469
|
+
// press anchors a drag-selection at the cursor
|
|
470
|
+
this.selStart = this.cursor;
|
|
471
|
+
this.selEnd = this.cursor;
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
if ((ev.kind === "drag" || ev.kind === "release") && ev.button === 0 && this.selStart !== null) {
|
|
475
|
+
// extend the selection with the pointer (clamped to the text)
|
|
476
|
+
if (this.multi) {
|
|
477
|
+
const h = this.height();
|
|
478
|
+
const start = this.scrollY ?? 0;
|
|
479
|
+
const row = start + Math.max(0, Math.min(h - 1, ev.y - this.y));
|
|
480
|
+
const rx = ev.x - this.x - (row === 0 ? strWidth(this.prompt) : 1);
|
|
481
|
+
this.cursor = this.#indexAtVisual(row, Math.max(0, rx));
|
|
482
|
+
} else {
|
|
483
|
+
const rx = ev.x - this.x - strWidth(this.prompt);
|
|
484
|
+
let w = 0, idx = 0;
|
|
485
|
+
for (const ch of Array.from(this.value)) {
|
|
486
|
+
const cw = strWidth(ch);
|
|
487
|
+
if (rx < w + cw / 2) break;
|
|
488
|
+
w += cw;
|
|
489
|
+
idx++;
|
|
490
|
+
}
|
|
491
|
+
this.cursor = idx;
|
|
492
|
+
}
|
|
493
|
+
this.#snapCursor();
|
|
494
|
+
this.selEnd = this.cursor;
|
|
495
|
+
if (this.selEnd < this.selStart) { const t = this.selStart; this.selStart = this.selEnd; this.selEnd = t; }
|
|
449
496
|
return true;
|
|
450
497
|
}
|
|
451
498
|
return false;
|
|
@@ -500,26 +547,29 @@ export class Input extends Widget {
|
|
|
500
547
|
if (ev.type !== "key") return false;
|
|
501
548
|
switch (ev.name) {
|
|
502
549
|
case "backspace":
|
|
503
|
-
if (this.cursor > 0)
|
|
550
|
+
if (this.cursor > 0) this.#edit(this.cursor - 1, this.cursor);
|
|
504
551
|
return true;
|
|
505
552
|
case "delete":
|
|
506
|
-
if (this.cursor < this.#cps().length)
|
|
553
|
+
if (this.cursor < this.#cps().length) this.#edit(this.cursor, this.cursor + 1);
|
|
507
554
|
return true;
|
|
508
|
-
case "left": this.selectAll = false; this.cursor = Math.max(0, this.cursor - 1); this.#snapCursor(-1); return true;
|
|
509
|
-
case "right": this.selectAll = false; this.cursor = Math.min(this.#cps().length, this.cursor + 1); this.#snapCursor(1); return true;
|
|
555
|
+
case "left": this.selectAll = false; this.selStart = this.selEnd = null; this.cursor = Math.max(0, this.cursor - 1); this.#snapCursor(-1); return true;
|
|
556
|
+
case "right": this.selectAll = false; this.selStart = this.selEnd = null; this.cursor = Math.min(this.#cps().length, this.cursor + 1); this.#snapCursor(1); return true;
|
|
510
557
|
case "home": {
|
|
558
|
+
this.selStart = this.selEnd = null;
|
|
511
559
|
if (this.multi) { const rows = this.#visualRows(); const { row } = this.#cursorVisual(); this.cursor = rows[row].start; }
|
|
512
560
|
else this.cursor = 0;
|
|
513
561
|
this.#snapCursor();
|
|
514
562
|
return true;
|
|
515
563
|
}
|
|
516
564
|
case "end": {
|
|
565
|
+
this.selStart = this.selEnd = null;
|
|
517
566
|
if (this.multi) { const rows = this.#visualRows(); const { row } = this.#cursorVisual(); this.cursor = rows[row].end; }
|
|
518
567
|
else this.cursor = this.#cps().length;
|
|
519
568
|
this.#snapCursor();
|
|
520
569
|
return true;
|
|
521
570
|
}
|
|
522
571
|
case "up":
|
|
572
|
+
this.selStart = this.selEnd = null;
|
|
523
573
|
if (this.multi) {
|
|
524
574
|
const rows = this.#visualRows();
|
|
525
575
|
const { row, col } = this.#cursorVisual();
|
|
@@ -530,6 +580,7 @@ export class Input extends Widget {
|
|
|
530
580
|
}
|
|
531
581
|
return true;
|
|
532
582
|
case "down":
|
|
583
|
+
this.selStart = this.selEnd = null;
|
|
533
584
|
if (this.multi) {
|
|
534
585
|
const rows = this.#visualRows();
|
|
535
586
|
const { row, col } = this.#cursorVisual();
|
|
@@ -544,9 +595,29 @@ export class Input extends Widget {
|
|
|
544
595
|
if (ev.ctrl) {
|
|
545
596
|
switch (ev.key) {
|
|
546
597
|
case "j": if (this.multi) { this.insert("\n"); return true; } return false;
|
|
547
|
-
case "c":
|
|
548
|
-
|
|
549
|
-
|
|
598
|
+
case "c": {
|
|
599
|
+
if (ev.shift) {
|
|
600
|
+
// Ctrl+Shift+C: copy the drag-selection (if any)
|
|
601
|
+
if (this.selStart !== null && this.selEnd !== null && this.selEnd > this.selStart) {
|
|
602
|
+
const text = Array.from(this.value).slice(this.selStart, this.selEnd).join("");
|
|
603
|
+
this.selStart = this.selEnd = null;
|
|
604
|
+
this.app?.copyText?.(text);
|
|
605
|
+
} else {
|
|
606
|
+
this.app?.toast?.("先用鼠标拖动选中要复制的内容");
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
// plain Ctrl+C keeps ONE meaning here: clear the input
|
|
611
|
+
this.#touch();
|
|
612
|
+
this.value = "";
|
|
613
|
+
this.cursor = 0;
|
|
614
|
+
this.selectAll = false;
|
|
615
|
+
this.onChange?.();
|
|
616
|
+
this.app?.toast?.("已清空输入栏");
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
case "u": this.#edit(0, this.cursor); return true;
|
|
620
|
+
case "k": this.#edit(this.cursor, this.#cps().length); return true;
|
|
550
621
|
case "l": {
|
|
551
622
|
// expand/collapse the input: expanded drops the 6-line cap and
|
|
552
623
|
// lets the editor fill the window above the input
|