dsh-neotui 0.1.7 → 0.1.9
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/term.js +31 -0
- package/src/widgets.js +50 -22
package/package.json
CHANGED
package/src/term.js
CHANGED
|
@@ -35,6 +35,7 @@ export class Term {
|
|
|
35
35
|
this.buf = "";
|
|
36
36
|
this.started = false;
|
|
37
37
|
this.pasteBuf = null;
|
|
38
|
+
this.pasting = false; // inside a bracketed paste (200~ … 201~)
|
|
38
39
|
this.w = (output.columns || 80); this.h = (output.rows || 24);
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -108,8 +109,31 @@ export class Term {
|
|
|
108
109
|
|
|
109
110
|
#parse() {
|
|
110
111
|
const buf = this.buf;
|
|
112
|
+
const PASTE_END = "\x1b[201~";
|
|
111
113
|
let i = 0;
|
|
112
114
|
while (i < buf.length) {
|
|
115
|
+
// Inside a bracketed paste: collect raw content up to the END marker
|
|
116
|
+
// and emit it as ONE text event — otherwise a large paste arrives as
|
|
117
|
+
// many small text chunks and the input's two-stage paste never fires.
|
|
118
|
+
if (this.pasting) {
|
|
119
|
+
const end = buf.indexOf(PASTE_END, i);
|
|
120
|
+
if (end === -1) {
|
|
121
|
+
// keep any partial END-marker suffix in the buffer for reassembly
|
|
122
|
+
let keep = 0;
|
|
123
|
+
for (let k = 1; k < PASTE_END.length; k++) {
|
|
124
|
+
if (buf.length - k >= 0 && buf.endsWith(PASTE_END.slice(0, k))) keep = k;
|
|
125
|
+
}
|
|
126
|
+
this.pasteBuf += buf.slice(i, buf.length - keep);
|
|
127
|
+
i = buf.length - keep;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
this.pasteBuf += buf.slice(i, end);
|
|
131
|
+
this.pasting = false;
|
|
132
|
+
this.#emit({ type: "text", text: this.pasteBuf });
|
|
133
|
+
this.pasteBuf = "";
|
|
134
|
+
i = end + PASTE_END.length;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
113
137
|
const ch = buf[i];
|
|
114
138
|
if (ch === "\x1b") {
|
|
115
139
|
// escape sequence
|
|
@@ -262,6 +286,13 @@ export class Term {
|
|
|
262
286
|
return;
|
|
263
287
|
}
|
|
264
288
|
const [n = 0, mod = 1] = nums;
|
|
289
|
+
if (n === 200) { this.pasting = true; this.pasteBuf = ""; return; } // bracketed paste START
|
|
290
|
+
if (n === 201) { // bracketed paste END (normally consumed by the collector)
|
|
291
|
+
this.pasting = false;
|
|
292
|
+
if (this.pasteBuf !== "") this.#emit({ type: "text", text: this.pasteBuf });
|
|
293
|
+
this.pasteBuf = "";
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
265
296
|
const name = TILDE_NAMES[n];
|
|
266
297
|
if (!name) return;
|
|
267
298
|
const ctrl = !!(mod - 1 & 4), alt = !!(mod - 1 & 2), shift = !!(mod - 1 & 1);
|
package/src/widgets.js
CHANGED
|
@@ -257,7 +257,7 @@ export class Input extends Widget {
|
|
|
257
257
|
this.expanded = false; // Ctrl+L: drop the line cap and fill the window
|
|
258
258
|
this.app = opts.app ?? null;
|
|
259
259
|
this.pendingPaste = null; // large-paste stage 1: held-back clipboard text
|
|
260
|
-
this.
|
|
260
|
+
this.pasteMark = null; // code-point span of the immutable "[已复制…]" token
|
|
261
261
|
this.onChange = opts.onChange ?? null;
|
|
262
262
|
this.allowEmptyEnter = opts.allowEmptyEnter ?? false;
|
|
263
263
|
this.history = [];
|
|
@@ -325,19 +325,45 @@ export class Input extends Widget {
|
|
|
325
325
|
this.onChange?.();
|
|
326
326
|
}
|
|
327
327
|
insert(text) {
|
|
328
|
-
const cps = this.selectAll ? [] : this.#cps();
|
|
329
328
|
const at = this.selectAll ? 0 : this.cursor;
|
|
330
|
-
this.selectAll
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
329
|
+
if (this.selectAll) {
|
|
330
|
+
this.selectAll = false;
|
|
331
|
+
this.value = "";
|
|
332
|
+
this.cursor = 0;
|
|
333
|
+
this.#touch();
|
|
334
|
+
}
|
|
335
|
+
this.#edit(at, at, text);
|
|
334
336
|
this.onChange?.();
|
|
335
337
|
}
|
|
336
|
-
|
|
338
|
+
/** EVERY edit goes through here so the immutable paste token behaves as
|
|
339
|
+
* one unit: deleting any part of it removes it whole, typing inside it
|
|
340
|
+
* replaces it, edits elsewhere just shift it. */
|
|
341
|
+
#edit(from, to, text = "") {
|
|
337
342
|
const cps = this.#cps();
|
|
338
|
-
|
|
343
|
+
const t = Array.from(text);
|
|
344
|
+
const m = this.pasteMark;
|
|
345
|
+
if (m && to > m.start && from < m.end) {
|
|
346
|
+
// the edit touches the token: apply it over the whole token span
|
|
347
|
+
const lo = Math.min(from, m.start);
|
|
348
|
+
const hi = Math.max(to, m.end);
|
|
349
|
+
cps.splice(lo, hi - lo, ...t);
|
|
350
|
+
this.cursor = lo + t.length;
|
|
351
|
+
this.pasteMark = null;
|
|
352
|
+
this.pendingPaste = null;
|
|
353
|
+
this.value = cps.join("");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (m && to <= m.start) {
|
|
357
|
+
const delta = t.length - (to - from);
|
|
358
|
+
m.start += delta; m.end += delta;
|
|
359
|
+
}
|
|
360
|
+
cps.splice(from, to - from, ...t);
|
|
361
|
+
this.cursor = from + t.length;
|
|
339
362
|
this.value = cps.join("");
|
|
340
363
|
}
|
|
364
|
+
#deleteAt(idx) {
|
|
365
|
+
this.#edit(idx, idx + 1);
|
|
366
|
+
}
|
|
341
367
|
/** Scroll offset that keeps the cursor's visual row inside the window. */
|
|
342
368
|
#scrollStart(h) {
|
|
343
369
|
const rows = this.#visualRows();
|
|
@@ -422,29 +448,31 @@ export class Input extends Widget {
|
|
|
422
448
|
}
|
|
423
449
|
return false;
|
|
424
450
|
}
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
#touch() { this.pendingPaste = null; this.pendingPlaceholder = null; }
|
|
451
|
+
/** Cancel the held-back paste AND its token. */
|
|
452
|
+
#touch() { this.pendingPaste = null; this.pasteMark = null; }
|
|
428
453
|
/** Claude-Code-style two-stage paste: the first Ctrl+Shift+V of a large
|
|
429
|
-
* clipboard shows a "[已复制 N 行内容]" placeholder
|
|
430
|
-
*
|
|
454
|
+
* clipboard shows a "[已复制 N 行内容]" placeholder — an IMMUTABLE token:
|
|
455
|
+
* deleting/typing into it consumes it whole, edits around it keep it, and
|
|
456
|
+
* pasting the same content again replaces exactly that token. */
|
|
431
457
|
#paste(text) {
|
|
432
458
|
text = String(text ?? "");
|
|
433
459
|
const large = text.includes("\n") || text.length > 300;
|
|
434
460
|
if (large) {
|
|
435
461
|
if (this.pendingPaste && this.pendingPaste.text === text) {
|
|
436
462
|
const full = this.pendingPaste.text;
|
|
437
|
-
const
|
|
463
|
+
const m = this.pasteMark;
|
|
438
464
|
this.#touch();
|
|
439
|
-
if (this.
|
|
465
|
+
if (m) this.#edit(m.start, m.end, full);
|
|
440
466
|
else this.insert(full);
|
|
441
467
|
this.app?.toast?.("已粘贴完整内容");
|
|
442
468
|
return true;
|
|
443
469
|
}
|
|
444
470
|
const lines = text.split("\n").length;
|
|
471
|
+
const placeholder = `[已复制 ${lines} 行内容]`;
|
|
472
|
+
const at = this.selectAll ? 0 : this.cursor;
|
|
473
|
+
this.insert(placeholder);
|
|
474
|
+
this.pasteMark = { start: at, end: at + Array.from(placeholder).length };
|
|
445
475
|
this.pendingPaste = { text };
|
|
446
|
-
this.pendingPlaceholder = `[已复制 ${lines} 行内容]`;
|
|
447
|
-
this.insert(this.pendingPlaceholder);
|
|
448
476
|
this.app?.toast?.("再次 Ctrl+Shift+V 粘贴完整内容(Ctrl+L 展开输入栏)");
|
|
449
477
|
return true;
|
|
450
478
|
}
|
|
@@ -457,10 +485,10 @@ export class Input extends Widget {
|
|
|
457
485
|
if (ev.type !== "key") return false;
|
|
458
486
|
switch (ev.name) {
|
|
459
487
|
case "backspace":
|
|
460
|
-
if (this.cursor > 0) { this.#
|
|
488
|
+
if (this.cursor > 0) { this.#edit(this.cursor - 1, this.cursor); this.onChange?.(); }
|
|
461
489
|
return true;
|
|
462
490
|
case "delete":
|
|
463
|
-
if (this.cursor < this.#cps().length) { this.#
|
|
491
|
+
if (this.cursor < this.#cps().length) { this.#edit(this.cursor, this.cursor + 1); this.onChange?.(); }
|
|
464
492
|
return true;
|
|
465
493
|
case "left": this.selectAll = false; this.cursor = Math.max(0, this.cursor - 1); return true;
|
|
466
494
|
case "right": this.selectAll = false; this.cursor = Math.min(this.#cps().length, this.cursor + 1); return true;
|
|
@@ -500,8 +528,8 @@ export class Input extends Widget {
|
|
|
500
528
|
switch (ev.key) {
|
|
501
529
|
case "j": if (this.multi) { this.insert("\n"); return true; } return false;
|
|
502
530
|
case "c": this.#touch(); this.value = ""; this.cursor = 0; this.selectAll = false; this.onChange?.(); this.app?.toast?.("已清空输入栏"); return true;
|
|
503
|
-
case "u": this.#
|
|
504
|
-
case "k": this.#
|
|
531
|
+
case "u": this.#edit(0, this.cursor); this.onChange?.(); return true;
|
|
532
|
+
case "k": this.#edit(this.cursor, this.#cps().length); this.onChange?.(); return true;
|
|
505
533
|
case "l": {
|
|
506
534
|
// expand/collapse the input: expanded drops the 6-line cap and
|
|
507
535
|
// lets the editor fill the window above the input
|
|
@@ -527,13 +555,13 @@ export class Input extends Widget {
|
|
|
527
555
|
}
|
|
528
556
|
return false;
|
|
529
557
|
}
|
|
530
|
-
this.#touch();
|
|
531
558
|
this.insert(ev.text);
|
|
532
559
|
return true;
|
|
533
560
|
case "enter":
|
|
534
561
|
if (ev.shift && this.multi) { this.insert("\n"); return true; } // Shift+Enter = newline
|
|
535
562
|
if (this.value.trim() === "" && !this.allowEmptyEnter) return false;
|
|
536
563
|
const v = this.value;
|
|
564
|
+
this.#touch();
|
|
537
565
|
this.history.push(v);
|
|
538
566
|
this.histIdx = -1;
|
|
539
567
|
this.value = "";
|