@mariozechner/pi-tui 0.32.2 → 0.33.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/README.md +24 -22
- package/dist/components/cancellable-loader.d.ts.map +1 -1
- package/dist/components/cancellable-loader.js +3 -2
- package/dist/components/cancellable-loader.js.map +1 -1
- package/dist/components/editor.d.ts +5 -0
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +113 -156
- package/dist/components/editor.js.map +1 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +36 -49
- package/dist/components/input.js.map +1 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +6 -5
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +6 -5
- package/dist/components/settings-list.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +39 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +94 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/keys.d.ts +66 -242
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +325 -432
- package/dist/keys.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +2 -2
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getEditorKeybindings } from "../keybindings.js";
|
|
2
|
+
import { matchesKey } from "../keys.js";
|
|
2
3
|
import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
|
|
3
4
|
import { SelectList } from "./select-list.js";
|
|
4
5
|
const segmenter = getSegmenter();
|
|
@@ -318,241 +319,188 @@ export class Editor {
|
|
|
318
319
|
return result;
|
|
319
320
|
}
|
|
320
321
|
handleInput(data) {
|
|
322
|
+
const kb = getEditorKeybindings();
|
|
321
323
|
// Handle bracketed paste mode
|
|
322
|
-
// Start of paste: \x1b[200~
|
|
323
|
-
// End of paste: \x1b[201~
|
|
324
|
-
// Check if we're starting a bracketed paste
|
|
325
324
|
if (data.includes("\x1b[200~")) {
|
|
326
325
|
this.isInPaste = true;
|
|
327
326
|
this.pasteBuffer = "";
|
|
328
|
-
// Remove the start marker and keep the rest
|
|
329
327
|
data = data.replace("\x1b[200~", "");
|
|
330
328
|
}
|
|
331
|
-
// If we're in a paste, buffer the data
|
|
332
329
|
if (this.isInPaste) {
|
|
333
|
-
// Append data to buffer first (end marker could be split across chunks)
|
|
334
330
|
this.pasteBuffer += data;
|
|
335
|
-
// Check if the accumulated buffer contains the end marker
|
|
336
331
|
const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
|
|
337
332
|
if (endIndex !== -1) {
|
|
338
|
-
// Extract content before the end marker
|
|
339
333
|
const pasteContent = this.pasteBuffer.substring(0, endIndex);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
334
|
+
if (pasteContent.length > 0) {
|
|
335
|
+
this.handlePaste(pasteContent);
|
|
336
|
+
}
|
|
343
337
|
this.isInPaste = false;
|
|
344
|
-
|
|
345
|
-
const remaining = this.pasteBuffer.substring(endIndex + 6); // 6 = length of \x1b[201~
|
|
338
|
+
const remaining = this.pasteBuffer.substring(endIndex + 6);
|
|
346
339
|
this.pasteBuffer = "";
|
|
347
340
|
if (remaining.length > 0) {
|
|
348
341
|
this.handleInput(remaining);
|
|
349
342
|
}
|
|
350
343
|
return;
|
|
351
344
|
}
|
|
352
|
-
|
|
353
|
-
// Still accumulating, wait for more data
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
345
|
+
return;
|
|
356
346
|
}
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
if (isCtrlC(data)) {
|
|
347
|
+
// Ctrl+C - let parent handle (exit/clear)
|
|
348
|
+
if (kb.matches(data, "copy")) {
|
|
360
349
|
return;
|
|
361
350
|
}
|
|
362
|
-
// Handle autocomplete
|
|
351
|
+
// Handle autocomplete mode
|
|
363
352
|
if (this.isAutocompleting && this.autocompleteList) {
|
|
364
|
-
|
|
365
|
-
if (isEscape(data)) {
|
|
353
|
+
if (kb.matches(data, "selectCancel")) {
|
|
366
354
|
this.cancelAutocomplete();
|
|
367
355
|
return;
|
|
368
356
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
357
|
+
if (kb.matches(data, "selectUp") || kb.matches(data, "selectDown")) {
|
|
358
|
+
this.autocompleteList.handleInput(data);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (kb.matches(data, "tab")) {
|
|
362
|
+
const selected = this.autocompleteList.getSelectedItem();
|
|
363
|
+
if (selected && this.autocompleteProvider) {
|
|
364
|
+
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
365
|
+
this.state.lines = result.lines;
|
|
366
|
+
this.state.cursorLine = result.cursorLine;
|
|
367
|
+
this.state.cursorCol = result.cursorCol;
|
|
368
|
+
this.cancelAutocomplete();
|
|
369
|
+
if (this.onChange)
|
|
370
|
+
this.onChange(this.getText());
|
|
375
371
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (kb.matches(data, "selectConfirm")) {
|
|
375
|
+
const selected = this.autocompleteList.getSelectedItem();
|
|
376
|
+
if (selected && this.autocompleteProvider) {
|
|
377
|
+
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
378
|
+
this.state.lines = result.lines;
|
|
379
|
+
this.state.cursorLine = result.cursorLine;
|
|
380
|
+
this.state.cursorCol = result.cursorCol;
|
|
381
|
+
if (this.autocompletePrefix.startsWith("/")) {
|
|
384
382
|
this.cancelAutocomplete();
|
|
385
|
-
|
|
386
|
-
this.onChange(this.getText());
|
|
387
|
-
}
|
|
383
|
+
// Fall through to submit
|
|
388
384
|
}
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
// If Enter was pressed on a slash command, apply completion and submit
|
|
392
|
-
if (isEnter(data) && this.autocompletePrefix.startsWith("/")) {
|
|
393
|
-
const selected = this.autocompleteList.getSelectedItem();
|
|
394
|
-
if (selected && this.autocompleteProvider) {
|
|
395
|
-
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
396
|
-
this.state.lines = result.lines;
|
|
397
|
-
this.state.cursorLine = result.cursorLine;
|
|
398
|
-
this.state.cursorCol = result.cursorCol;
|
|
399
|
-
}
|
|
400
|
-
this.cancelAutocomplete();
|
|
401
|
-
// Don't return - fall through to submission logic
|
|
402
|
-
}
|
|
403
|
-
// If Enter was pressed on a file path, apply completion
|
|
404
|
-
else if (isEnter(data)) {
|
|
405
|
-
const selected = this.autocompleteList.getSelectedItem();
|
|
406
|
-
if (selected && this.autocompleteProvider) {
|
|
407
|
-
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
408
|
-
this.state.lines = result.lines;
|
|
409
|
-
this.state.cursorLine = result.cursorLine;
|
|
410
|
-
this.state.cursorCol = result.cursorCol;
|
|
385
|
+
else {
|
|
411
386
|
this.cancelAutocomplete();
|
|
412
|
-
if (this.onChange)
|
|
387
|
+
if (this.onChange)
|
|
413
388
|
this.onChange(this.getText());
|
|
414
|
-
|
|
389
|
+
return;
|
|
415
390
|
}
|
|
416
|
-
return;
|
|
417
391
|
}
|
|
418
392
|
}
|
|
419
|
-
// For other keys (like regular typing), DON'T return here
|
|
420
|
-
// Let them fall through to normal character handling
|
|
421
393
|
}
|
|
422
|
-
// Tab
|
|
423
|
-
if (
|
|
394
|
+
// Tab - trigger completion
|
|
395
|
+
if (kb.matches(data, "tab") && !this.isAutocompleting) {
|
|
424
396
|
this.handleTabCompletion();
|
|
425
397
|
return;
|
|
426
398
|
}
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
if (isCtrlK(data)) {
|
|
399
|
+
// Deletion actions
|
|
400
|
+
if (kb.matches(data, "deleteToLineEnd")) {
|
|
430
401
|
this.deleteToEndOfLine();
|
|
402
|
+
return;
|
|
431
403
|
}
|
|
432
|
-
|
|
433
|
-
else if (isCtrlU(data)) {
|
|
404
|
+
if (kb.matches(data, "deleteToLineStart")) {
|
|
434
405
|
this.deleteToStartOfLine();
|
|
406
|
+
return;
|
|
435
407
|
}
|
|
436
|
-
|
|
437
|
-
else if (isCtrlW(data)) {
|
|
408
|
+
if (kb.matches(data, "deleteWordBackward")) {
|
|
438
409
|
this.deleteWordBackwards();
|
|
410
|
+
return;
|
|
439
411
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
412
|
+
if (kb.matches(data, "deleteCharBackward") || matchesKey(data, "shift+backspace")) {
|
|
413
|
+
this.handleBackspace();
|
|
414
|
+
return;
|
|
443
415
|
}
|
|
444
|
-
|
|
445
|
-
|
|
416
|
+
if (kb.matches(data, "deleteCharForward") || matchesKey(data, "shift+delete")) {
|
|
417
|
+
this.handleForwardDelete();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// Cursor movement actions
|
|
421
|
+
if (kb.matches(data, "cursorLineStart")) {
|
|
446
422
|
this.moveToLineStart();
|
|
423
|
+
return;
|
|
447
424
|
}
|
|
448
|
-
|
|
449
|
-
else if (isCtrlE(data)) {
|
|
425
|
+
if (kb.matches(data, "cursorLineEnd")) {
|
|
450
426
|
this.moveToLineEnd();
|
|
427
|
+
return;
|
|
451
428
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
429
|
+
if (kb.matches(data, "cursorWordLeft")) {
|
|
430
|
+
this.moveWordBackwards();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (kb.matches(data, "cursorWordRight")) {
|
|
434
|
+
this.moveWordForwards();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// New line (Shift+Enter, Alt+Enter, etc.)
|
|
438
|
+
if (kb.matches(data, "newLine") ||
|
|
439
|
+
(data.charCodeAt(0) === 10 && data.length > 1) ||
|
|
440
|
+
data === "\x1b\r" ||
|
|
441
|
+
data === "\x1b[13;2~" ||
|
|
458
442
|
(data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
|
|
459
|
-
(data === "\n" && data.length === 1) ||
|
|
460
|
-
data === "\\\r"
|
|
461
|
-
) {
|
|
462
|
-
// Modifier + Enter = new line
|
|
443
|
+
(data === "\n" && data.length === 1) ||
|
|
444
|
+
data === "\\\r") {
|
|
463
445
|
this.addNewLine();
|
|
446
|
+
return;
|
|
464
447
|
}
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (this.disableSubmit) {
|
|
448
|
+
// Submit (Enter)
|
|
449
|
+
if (kb.matches(data, "submit")) {
|
|
450
|
+
if (this.disableSubmit)
|
|
469
451
|
return;
|
|
470
|
-
}
|
|
471
|
-
// Get text and substitute paste markers with actual content
|
|
472
452
|
let result = this.state.lines.join("\n").trim();
|
|
473
|
-
// Replace all [paste #N +xxx lines] or [paste #N xxx chars] markers with actual paste content
|
|
474
453
|
for (const [pasteId, pasteContent] of this.pastes) {
|
|
475
|
-
// Match formats: [paste #N], [paste #N +xxx lines], or [paste #N xxx chars]
|
|
476
454
|
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
477
455
|
result = result.replace(markerRegex, pasteContent);
|
|
478
456
|
}
|
|
479
|
-
|
|
480
|
-
this.state = {
|
|
481
|
-
lines: [""],
|
|
482
|
-
cursorLine: 0,
|
|
483
|
-
cursorCol: 0,
|
|
484
|
-
};
|
|
457
|
+
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
485
458
|
this.pastes.clear();
|
|
486
459
|
this.pasteCounter = 0;
|
|
487
|
-
this.historyIndex = -1;
|
|
488
|
-
|
|
489
|
-
if (this.onChange) {
|
|
460
|
+
this.historyIndex = -1;
|
|
461
|
+
if (this.onChange)
|
|
490
462
|
this.onChange("");
|
|
491
|
-
|
|
492
|
-
if (this.onSubmit) {
|
|
463
|
+
if (this.onSubmit)
|
|
493
464
|
this.onSubmit(result);
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
// Backspace (including Shift+Backspace)
|
|
497
|
-
else if (isBackspace(data) || isShiftBackspace(data)) {
|
|
498
|
-
this.handleBackspace();
|
|
499
|
-
}
|
|
500
|
-
// Line navigation shortcuts (Home/End keys)
|
|
501
|
-
else if (isHome(data)) {
|
|
502
|
-
this.moveToLineStart();
|
|
503
|
-
}
|
|
504
|
-
else if (isEnd(data)) {
|
|
505
|
-
this.moveToLineEnd();
|
|
506
|
-
}
|
|
507
|
-
// Forward delete (Fn+Backspace or Delete key, including Shift+Delete)
|
|
508
|
-
else if (isDelete(data) || isShiftDelete(data)) {
|
|
509
|
-
this.handleForwardDelete();
|
|
510
|
-
}
|
|
511
|
-
// Word navigation (Option/Alt + Arrow or Ctrl + Arrow)
|
|
512
|
-
else if (isAltLeft(data) || isCtrlLeft(data)) {
|
|
513
|
-
// Word left
|
|
514
|
-
this.moveWordBackwards();
|
|
515
|
-
}
|
|
516
|
-
else if (isAltRight(data) || isCtrlRight(data)) {
|
|
517
|
-
// Word right
|
|
518
|
-
this.moveWordForwards();
|
|
465
|
+
return;
|
|
519
466
|
}
|
|
520
|
-
// Arrow
|
|
521
|
-
|
|
522
|
-
// Up - history navigation or cursor movement
|
|
467
|
+
// Arrow key navigation (with history support)
|
|
468
|
+
if (kb.matches(data, "cursorUp")) {
|
|
523
469
|
if (this.isEditorEmpty()) {
|
|
524
|
-
this.navigateHistory(-1);
|
|
470
|
+
this.navigateHistory(-1);
|
|
525
471
|
}
|
|
526
472
|
else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
|
|
527
|
-
this.navigateHistory(-1);
|
|
473
|
+
this.navigateHistory(-1);
|
|
528
474
|
}
|
|
529
475
|
else {
|
|
530
|
-
this.moveCursor(-1, 0);
|
|
476
|
+
this.moveCursor(-1, 0);
|
|
531
477
|
}
|
|
478
|
+
return;
|
|
532
479
|
}
|
|
533
|
-
|
|
534
|
-
// Down - history navigation or cursor movement
|
|
480
|
+
if (kb.matches(data, "cursorDown")) {
|
|
535
481
|
if (this.historyIndex > -1 && this.isOnLastVisualLine()) {
|
|
536
|
-
this.navigateHistory(1);
|
|
482
|
+
this.navigateHistory(1);
|
|
537
483
|
}
|
|
538
484
|
else {
|
|
539
|
-
this.moveCursor(1, 0);
|
|
485
|
+
this.moveCursor(1, 0);
|
|
540
486
|
}
|
|
487
|
+
return;
|
|
541
488
|
}
|
|
542
|
-
|
|
543
|
-
// Right
|
|
489
|
+
if (kb.matches(data, "cursorRight")) {
|
|
544
490
|
this.moveCursor(0, 1);
|
|
491
|
+
return;
|
|
545
492
|
}
|
|
546
|
-
|
|
547
|
-
// Left
|
|
493
|
+
if (kb.matches(data, "cursorLeft")) {
|
|
548
494
|
this.moveCursor(0, -1);
|
|
495
|
+
return;
|
|
549
496
|
}
|
|
550
|
-
// Shift+Space - insert regular space
|
|
551
|
-
|
|
497
|
+
// Shift+Space - insert regular space
|
|
498
|
+
if (matchesKey(data, "shift+space")) {
|
|
552
499
|
this.insertCharacter(" ");
|
|
500
|
+
return;
|
|
553
501
|
}
|
|
554
|
-
// Regular characters
|
|
555
|
-
|
|
502
|
+
// Regular characters
|
|
503
|
+
if (data.charCodeAt(0) >= 32) {
|
|
556
504
|
this.insertCharacter(data);
|
|
557
505
|
}
|
|
558
506
|
}
|
|
@@ -652,6 +600,15 @@ export class Editor {
|
|
|
652
600
|
this.historyIndex = -1; // Exit history browsing mode
|
|
653
601
|
this.setTextInternal(text);
|
|
654
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Insert text at the current cursor position.
|
|
605
|
+
* Used for programmatic insertion (e.g., clipboard image markers).
|
|
606
|
+
*/
|
|
607
|
+
insertTextAtCursor(text) {
|
|
608
|
+
for (const char of text) {
|
|
609
|
+
this.insertCharacter(char);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
655
612
|
// All the editor methods from before...
|
|
656
613
|
insertCharacter(char) {
|
|
657
614
|
this.historyIndex = -1; // Exit history browsing mode
|