@mariozechner/pi-tui 0.32.3 → 0.34.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.
@@ -1,4 +1,5 @@
1
- import { isAltBackspace, isAltEnter, isAltLeft, isAltRight, isArrowDown, isArrowLeft, isArrowRight, isArrowUp, isBackspace, isCtrlA, isCtrlC, isCtrlE, isCtrlK, isCtrlLeft, isCtrlRight, isCtrlU, isCtrlW, isDelete, isEnd, isEnter, isEscape, isHome, isShiftBackspace, isShiftDelete, isShiftEnter, isShiftSpace, isTab, } from "../keys.js";
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
- // Process the complete paste
341
- this.handlePaste(pasteContent);
342
- // Reset paste state
334
+ if (pasteContent.length > 0) {
335
+ this.handlePaste(pasteContent);
336
+ }
343
337
  this.isInPaste = false;
344
- // Process any remaining data after the end marker
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
- else {
353
- // Still accumulating, wait for more data
354
- return;
355
- }
345
+ return;
356
346
  }
357
- // Handle special key combinations first
358
- // Ctrl+C - Exit (let parent handle this)
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 special keys first (but don't block other input)
351
+ // Handle autocomplete mode
363
352
  if (this.isAutocompleting && this.autocompleteList) {
364
- // Escape - cancel autocomplete
365
- if (isEscape(data)) {
353
+ if (kb.matches(data, "selectCancel")) {
366
354
  this.cancelAutocomplete();
367
355
  return;
368
356
  }
369
- // Let the autocomplete list handle navigation and selection
370
- else if (isArrowUp(data) || isArrowDown(data) || isEnter(data) || isTab(data)) {
371
- // Only pass arrow keys to the list, not Enter/Tab (we handle those directly)
372
- if (isArrowUp(data) || isArrowDown(data)) {
373
- this.autocompleteList.handleInput(data);
374
- return;
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
- // If Tab was pressed, always apply the selection
377
- if (isTab(data)) {
378
- const selected = this.autocompleteList.getSelectedItem();
379
- if (selected && this.autocompleteProvider) {
380
- const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
381
- this.state.lines = result.lines;
382
- this.state.cursorLine = result.cursorLine;
383
- this.state.cursorCol = result.cursorCol;
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
- if (this.onChange) {
386
- this.onChange(this.getText());
387
- }
388
- }
389
- return;
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;
383
+ // Fall through to submit
399
384
  }
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 key - context-aware completion (but not when already autocompleting)
423
- if (isTab(data) && !this.isAutocompleting) {
394
+ // Tab - trigger completion
395
+ if (kb.matches(data, "tab") && !this.isAutocompleting) {
424
396
  this.handleTabCompletion();
425
397
  return;
426
398
  }
427
- // Continue with rest of input handling
428
- // Ctrl+K - Delete to end of line
429
- if (isCtrlK(data)) {
399
+ // Deletion actions
400
+ if (kb.matches(data, "deleteToLineEnd")) {
430
401
  this.deleteToEndOfLine();
402
+ return;
431
403
  }
432
- // Ctrl+U - Delete to start of line
433
- else if (isCtrlU(data)) {
404
+ if (kb.matches(data, "deleteToLineStart")) {
434
405
  this.deleteToStartOfLine();
406
+ return;
435
407
  }
436
- // Ctrl+W - Delete word backwards
437
- else if (isCtrlW(data)) {
408
+ if (kb.matches(data, "deleteWordBackward")) {
438
409
  this.deleteWordBackwards();
410
+ return;
439
411
  }
440
- // Option/Alt+Backspace - Delete word backwards
441
- else if (isAltBackspace(data)) {
442
- this.deleteWordBackwards();
412
+ if (kb.matches(data, "deleteCharBackward") || matchesKey(data, "shift+backspace")) {
413
+ this.handleBackspace();
414
+ return;
415
+ }
416
+ if (kb.matches(data, "deleteCharForward") || matchesKey(data, "shift+delete")) {
417
+ this.handleForwardDelete();
418
+ return;
443
419
  }
444
- // Ctrl+A - Move to start of line
445
- else if (isCtrlA(data)) {
420
+ // Cursor movement actions
421
+ if (kb.matches(data, "cursorLineStart")) {
446
422
  this.moveToLineStart();
423
+ return;
447
424
  }
448
- // Ctrl+E - Move to end of line
449
- else if (isCtrlE(data)) {
425
+ if (kb.matches(data, "cursorLineEnd")) {
450
426
  this.moveToLineEnd();
427
+ return;
428
+ }
429
+ if (kb.matches(data, "cursorWordLeft")) {
430
+ this.moveWordBackwards();
431
+ return;
432
+ }
433
+ if (kb.matches(data, "cursorWordRight")) {
434
+ this.moveWordForwards();
435
+ return;
451
436
  }
452
- // New line shortcuts (but not plain LF/CR which should be submit)
453
- else if ((data.charCodeAt(0) === 10 && data.length > 1) || // Ctrl+Enter with modifiers
454
- data === "\x1b\r" || // Option+Enter in some terminals (legacy)
455
- data === "\x1b[13;2~" || // Shift+Enter in some terminals (legacy format)
456
- isShiftEnter(data) || // Shift+Enter (Kitty protocol, handles lock bits)
457
- isAltEnter(data) || // Alt+Enter (Kitty protocol, handles lock bits)
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) || // Shift+Enter from iTerm2 mapping
460
- data === "\\\r" // Shift+Enter in VS Code terminal
461
- ) {
462
- // Modifier + Enter = new line
443
+ (data === "\n" && data.length === 1) ||
444
+ data === "\\\r") {
463
445
  this.addNewLine();
446
+ return;
464
447
  }
465
- // Plain Enter - submit (handles both legacy \r and Kitty protocol with lock bits)
466
- else if (isEnter(data)) {
467
- // If submit is disabled, do nothing
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
- // Reset editor and clear pastes
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; // Exit history browsing mode
488
- // Notify that editor is now empty
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 keys
521
- else if (isArrowUp(data)) {
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); // Start browsing history
470
+ this.navigateHistory(-1);
525
471
  }
526
472
  else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
527
- this.navigateHistory(-1); // Navigate to older history entry
473
+ this.navigateHistory(-1);
528
474
  }
529
475
  else {
530
- this.moveCursor(-1, 0); // Cursor movement (within text or history entry)
476
+ this.moveCursor(-1, 0);
531
477
  }
478
+ return;
532
479
  }
533
- else if (isArrowDown(data)) {
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); // Navigate to newer history entry or clear
482
+ this.navigateHistory(1);
537
483
  }
538
484
  else {
539
- this.moveCursor(1, 0); // Cursor movement (within text or history entry)
485
+ this.moveCursor(1, 0);
540
486
  }
487
+ return;
541
488
  }
542
- else if (isArrowRight(data)) {
543
- // Right
489
+ if (kb.matches(data, "cursorRight")) {
544
490
  this.moveCursor(0, 1);
491
+ return;
545
492
  }
546
- else if (isArrowLeft(data)) {
547
- // Left
493
+ if (kb.matches(data, "cursorLeft")) {
548
494
  this.moveCursor(0, -1);
495
+ return;
549
496
  }
550
- // Shift+Space - insert regular space (Kitty protocol sends escape sequence)
551
- else if (isShiftSpace(data)) {
497
+ // Shift+Space - insert regular space
498
+ if (matchesKey(data, "shift+space")) {
552
499
  this.insertCharacter(" ");
500
+ return;
553
501
  }
554
- // Regular characters (printable characters and unicode, but not control characters)
555
- else if (data.charCodeAt(0) >= 32) {
502
+ // Regular characters
503
+ if (data.charCodeAt(0) >= 32) {
556
504
  this.insertCharacter(data);
557
505
  }
558
506
  }
@@ -642,6 +590,18 @@ export class Editor {
642
590
  getText() {
643
591
  return this.state.lines.join("\n");
644
592
  }
593
+ /**
594
+ * Get text with paste markers expanded to their actual content.
595
+ * Use this when you need the full content (e.g., for external editor).
596
+ */
597
+ getExpandedText() {
598
+ let result = this.state.lines.join("\n");
599
+ for (const [pasteId, pasteContent] of this.pastes) {
600
+ const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
601
+ result = result.replace(markerRegex, pasteContent);
602
+ }
603
+ return result;
604
+ }
645
605
  getLines() {
646
606
  return [...this.state.lines];
647
607
  }
@@ -652,6 +612,15 @@ export class Editor {
652
612
  this.historyIndex = -1; // Exit history browsing mode
653
613
  this.setTextInternal(text);
654
614
  }
615
+ /**
616
+ * Insert text at the current cursor position.
617
+ * Used for programmatic insertion (e.g., clipboard image markers).
618
+ */
619
+ insertTextAtCursor(text) {
620
+ for (const char of text) {
621
+ this.insertCharacter(char);
622
+ }
623
+ }
655
624
  // All the editor methods from before...
656
625
  insertCharacter(char) {
657
626
  this.historyIndex = -1; // Exit history browsing mode