@oh-my-pi/pi-tui 13.14.2 → 13.15.2
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/CHANGELOG.md +11 -1
- package/package.json +3 -3
- package/src/components/cancellable-loader.ts +3 -2
- package/src/components/editor.ts +59 -58
- package/src/components/input.ts +19 -19
- package/src/components/markdown.ts +84 -10
- package/src/components/select-list.ts +118 -76
- package/src/components/settings-list.ts +6 -5
- package/src/keybindings.ts +216 -134
- package/src/keys.ts +28 -0
- package/src/tui.ts +23 -4
- package/src/utils.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.15.0] - 2026-03-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `renderInlineMarkdown()` function to render inline markdown (bold, italic, code, links, strikethrough) to styled strings
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed editor consuming user-rebound copy keys, preventing custom keybindings from working in the editor
|
|
14
|
+
|
|
5
15
|
## [13.14.1] - 2026-03-21
|
|
6
16
|
### Added
|
|
7
17
|
|
|
@@ -652,4 +662,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
|
|
|
652
662
|
|
|
653
663
|
### Fixed
|
|
654
664
|
|
|
655
|
-
- **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
|
|
665
|
+
- **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.15.2",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"test": "bun test test/*.test.ts"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@oh-my-pi/pi-natives": "13.
|
|
37
|
-
"@oh-my-pi/pi-utils": "13.
|
|
36
|
+
"@oh-my-pi/pi-natives": "13.15.2",
|
|
37
|
+
"@oh-my-pi/pi-utils": "13.15.2",
|
|
38
38
|
"marked": "^17.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getKeybindings } from "../keybindings";
|
|
2
2
|
import { Loader } from "./loader";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -27,7 +27,8 @@ export class CancellableLoader extends Loader {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
handleInput(data: string): void {
|
|
30
|
-
|
|
30
|
+
const kb = getKeybindings();
|
|
31
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
31
32
|
this.#abortController.abort();
|
|
32
33
|
this.onAbort?.();
|
|
33
34
|
}
|
package/src/components/editor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete";
|
|
3
3
|
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
4
|
-
import { type
|
|
4
|
+
import { getKeybindings, type KeybindingsManager } from "../keybindings";
|
|
5
5
|
import { extractPrintableText, matchesKey } from "../keys";
|
|
6
6
|
import { KillRing } from "../kill-ring";
|
|
7
7
|
import type { SymbolTheme } from "../symbols";
|
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
truncateToWidth,
|
|
16
16
|
visibleWidth,
|
|
17
17
|
} from "../utils";
|
|
18
|
-
import { SelectList, type SelectListTheme } from "./select-list";
|
|
18
|
+
import { SelectList, type SelectListLayoutOptions, type SelectListTheme } from "./select-list";
|
|
19
|
+
|
|
20
|
+
const SLASH_COMMAND_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
|
21
|
+
minPrimaryColumnWidth: 12,
|
|
22
|
+
maxPrimaryColumnWidth: 32,
|
|
23
|
+
};
|
|
19
24
|
|
|
20
25
|
const segmenter = getSegmenter();
|
|
21
26
|
|
|
@@ -691,12 +696,12 @@ export class Editor implements Component, Focusable {
|
|
|
691
696
|
}
|
|
692
697
|
|
|
693
698
|
handleInput(data: string): void {
|
|
694
|
-
const kb =
|
|
699
|
+
const kb = getKeybindings();
|
|
695
700
|
|
|
696
701
|
// Handle character jump mode (awaiting next character to jump to)
|
|
697
702
|
if (this.#jumpMode !== null) {
|
|
698
703
|
// Cancel if the hotkey is pressed again
|
|
699
|
-
if (kb.matches(data, "jumpForward") || kb.matches(data, "jumpBackward")) {
|
|
704
|
+
if (kb.matches(data, "tui.editor.jumpForward") || kb.matches(data, "tui.editor.jumpBackward")) {
|
|
700
705
|
this.#jumpMode = null;
|
|
701
706
|
return;
|
|
702
707
|
}
|
|
@@ -727,13 +732,15 @@ export class Editor implements Component, Focusable {
|
|
|
727
732
|
|
|
728
733
|
// Handle special key combinations first
|
|
729
734
|
|
|
730
|
-
// Ctrl+C
|
|
735
|
+
// Ctrl+C is reserved by parent components for app-level handling.
|
|
736
|
+
// Do not consume arbitrary user-bound "copy" keys here, since the editor
|
|
737
|
+
// has no copy implementation and would make those keys disappear.
|
|
731
738
|
if (matchesKey(data, "ctrl+c")) {
|
|
732
739
|
return;
|
|
733
740
|
}
|
|
734
741
|
|
|
735
742
|
// Undo
|
|
736
|
-
if (kb.matches(data, "undo")) {
|
|
743
|
+
if (kb.matches(data, "tui.editor.undo")) {
|
|
737
744
|
this.#applyUndo();
|
|
738
745
|
return;
|
|
739
746
|
}
|
|
@@ -741,27 +748,26 @@ export class Editor implements Component, Focusable {
|
|
|
741
748
|
// Handle autocomplete special keys first (but don't block other input)
|
|
742
749
|
if (this.#autocompleteState && this.#autocompleteList) {
|
|
743
750
|
// Escape - cancel autocomplete
|
|
744
|
-
if (
|
|
751
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
745
752
|
this.#cancelAutocomplete(true);
|
|
746
753
|
return;
|
|
747
754
|
}
|
|
748
755
|
// Let the autocomplete list handle navigation and selection
|
|
749
756
|
else if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
matchesKey(data, "return") ||
|
|
757
|
+
kb.matches(data, "tui.select.up") ||
|
|
758
|
+
kb.matches(data, "tui.select.down") ||
|
|
759
|
+
kb.matches(data, "tui.select.pageUp") ||
|
|
760
|
+
kb.matches(data, "tui.select.pageDown") ||
|
|
761
|
+
kb.matches(data, "tui.input.submit") ||
|
|
756
762
|
data === "\n" ||
|
|
757
|
-
|
|
763
|
+
kb.matches(data, "tui.input.tab")
|
|
758
764
|
) {
|
|
759
765
|
// Only pass navigation keys to the list, not Enter/Tab (we handle those directly)
|
|
760
766
|
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
767
|
+
kb.matches(data, "tui.select.up") ||
|
|
768
|
+
kb.matches(data, "tui.select.down") ||
|
|
769
|
+
kb.matches(data, "tui.select.pageUp") ||
|
|
770
|
+
kb.matches(data, "tui.select.pageDown")
|
|
765
771
|
) {
|
|
766
772
|
this.#autocompleteList.handleInput(data);
|
|
767
773
|
this.onAutocompleteUpdate?.();
|
|
@@ -769,7 +775,7 @@ export class Editor implements Component, Focusable {
|
|
|
769
775
|
}
|
|
770
776
|
|
|
771
777
|
// If Tab was pressed, always apply the selection
|
|
772
|
-
if (
|
|
778
|
+
if (kb.matches(data, "tui.input.tab")) {
|
|
773
779
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
774
780
|
if (selected && this.#autocompleteProvider) {
|
|
775
781
|
const shouldChainSlashCommandAutocomplete = this.#isSlashCommandNameAutocompleteSelection();
|
|
@@ -801,10 +807,7 @@ export class Editor implements Component, Focusable {
|
|
|
801
807
|
}
|
|
802
808
|
|
|
803
809
|
// If Enter was pressed on a slash command, apply completion and submit
|
|
804
|
-
if (
|
|
805
|
-
(matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") &&
|
|
806
|
-
this.#autocompletePrefix.startsWith("/")
|
|
807
|
-
) {
|
|
810
|
+
if ((kb.matches(data, "tui.input.submit") || data === "\n") && this.#autocompletePrefix.startsWith("/")) {
|
|
808
811
|
// Check for stale autocomplete state due to debounce
|
|
809
812
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
810
813
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
@@ -832,7 +835,7 @@ export class Editor implements Component, Focusable {
|
|
|
832
835
|
// Don't return - fall through to submission logic
|
|
833
836
|
}
|
|
834
837
|
// If Enter was pressed on a file path, apply completion
|
|
835
|
-
else if (
|
|
838
|
+
else if (kb.matches(data, "tui.input.submit") || data === "\n") {
|
|
836
839
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
837
840
|
if (selected && this.#autocompleteProvider) {
|
|
838
841
|
const result = this.#autocompleteProvider.applyCompletion(
|
|
@@ -863,7 +866,7 @@ export class Editor implements Component, Focusable {
|
|
|
863
866
|
}
|
|
864
867
|
|
|
865
868
|
// Tab key - context-aware completion (but not when already autocompleting)
|
|
866
|
-
if (
|
|
869
|
+
if (kb.matches(data, "tui.input.tab") && !this.#autocompleteState) {
|
|
867
870
|
this.#handleTabCompletion();
|
|
868
871
|
return;
|
|
869
872
|
}
|
|
@@ -920,7 +923,7 @@ export class Editor implements Component, Focusable {
|
|
|
920
923
|
data === "\x1b[27;5;13~" || // Ctrl+Enter (legacy format)
|
|
921
924
|
data === "\x1b\r" || // Option+Enter in some terminals (legacy)
|
|
922
925
|
data === "\x1b[13;2~" || // Shift+Enter in some terminals (legacy format)
|
|
923
|
-
|
|
926
|
+
kb.matches(data, "tui.input.newLine") || // Shift+Enter (Kitty protocol, handles lock bits)
|
|
924
927
|
(data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
|
|
925
928
|
(data === "\n" && data.length === 1) // Shift+Enter from iTerm2 mapping
|
|
926
929
|
) {
|
|
@@ -932,7 +935,7 @@ export class Editor implements Component, Focusable {
|
|
|
932
935
|
this.#addNewLine();
|
|
933
936
|
}
|
|
934
937
|
// Plain Enter - submit (handles both legacy \r and Kitty protocol with lock bits)
|
|
935
|
-
else if (
|
|
938
|
+
else if (kb.matches(data, "tui.input.submit") || data === "\n") {
|
|
936
939
|
// If submit is disabled, do nothing
|
|
937
940
|
if (this.disableSubmit) {
|
|
938
941
|
return;
|
|
@@ -941,17 +944,17 @@ export class Editor implements Component, Focusable {
|
|
|
941
944
|
this.#submitValue();
|
|
942
945
|
}
|
|
943
946
|
// Backspace (including Shift+Backspace)
|
|
944
|
-
else if (
|
|
947
|
+
else if (kb.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, "shift+backspace")) {
|
|
945
948
|
this.#handleBackspace();
|
|
946
949
|
}
|
|
947
950
|
// Line navigation shortcuts (Home/End keys)
|
|
948
|
-
else if (
|
|
951
|
+
else if (kb.matches(data, "tui.editor.cursorLineStart")) {
|
|
949
952
|
this.#moveToLineStart();
|
|
950
|
-
} else if (
|
|
953
|
+
} else if (kb.matches(data, "tui.editor.cursorLineEnd")) {
|
|
951
954
|
this.#moveToLineEnd();
|
|
952
955
|
}
|
|
953
956
|
// Page navigation (PageUp/PageDown)
|
|
954
|
-
else if (
|
|
957
|
+
else if (kb.matches(data, "tui.editor.pageUp")) {
|
|
955
958
|
if (this.#isEditorEmpty()) {
|
|
956
959
|
this.#navigateHistory(-1);
|
|
957
960
|
} else if (this.#historyIndex > -1 && this.#isOnFirstVisualLine()) {
|
|
@@ -959,7 +962,7 @@ export class Editor implements Component, Focusable {
|
|
|
959
962
|
} else {
|
|
960
963
|
this.#pageScroll(-1);
|
|
961
964
|
}
|
|
962
|
-
} else if (
|
|
965
|
+
} else if (kb.matches(data, "tui.editor.pageDown")) {
|
|
963
966
|
if (this.#historyIndex > -1 && this.#isOnLastVisualLine()) {
|
|
964
967
|
this.#navigateHistory(1);
|
|
965
968
|
} else {
|
|
@@ -967,21 +970,21 @@ export class Editor implements Component, Focusable {
|
|
|
967
970
|
}
|
|
968
971
|
}
|
|
969
972
|
// Forward delete (Fn+Backspace or Delete key, including Shift+Delete)
|
|
970
|
-
else if (
|
|
973
|
+
else if (kb.matches(data, "tui.editor.deleteCharForward") || matchesKey(data, "shift+delete")) {
|
|
971
974
|
this.#handleForwardDelete();
|
|
972
975
|
}
|
|
973
976
|
// Word navigation (Option/Alt + Arrow or Ctrl + Arrow)
|
|
974
|
-
else if (
|
|
977
|
+
else if (kb.matches(data, "tui.editor.cursorWordLeft")) {
|
|
975
978
|
// Word left
|
|
976
979
|
this.#resetKillSequence();
|
|
977
980
|
this.#moveWordBackwards();
|
|
978
|
-
} else if (
|
|
981
|
+
} else if (kb.matches(data, "tui.editor.cursorWordRight")) {
|
|
979
982
|
// Word right
|
|
980
983
|
this.#resetKillSequence();
|
|
981
984
|
this.#moveWordForwards();
|
|
982
985
|
}
|
|
983
986
|
// Arrow keys
|
|
984
|
-
else if (
|
|
987
|
+
else if (kb.matches(data, "tui.editor.cursorUp")) {
|
|
985
988
|
// Up - history navigation or cursor movement
|
|
986
989
|
if (this.#isEditorEmpty()) {
|
|
987
990
|
this.#navigateHistory(-1); // Start browsing history
|
|
@@ -993,7 +996,7 @@ export class Editor implements Component, Focusable {
|
|
|
993
996
|
} else {
|
|
994
997
|
this.#moveCursor(-1, 0); // Cursor movement (within text or history entry)
|
|
995
998
|
}
|
|
996
|
-
} else if (
|
|
999
|
+
} else if (kb.matches(data, "tui.editor.cursorDown")) {
|
|
997
1000
|
// Down - history navigation or cursor movement
|
|
998
1001
|
if (this.#historyIndex > -1 && this.#isOnLastVisualLine()) {
|
|
999
1002
|
this.#navigateHistory(1); // Navigate to newer history entry or clear
|
|
@@ -1003,10 +1006,10 @@ export class Editor implements Component, Focusable {
|
|
|
1003
1006
|
} else {
|
|
1004
1007
|
this.#moveCursor(1, 0); // Cursor movement (within text or history entry)
|
|
1005
1008
|
}
|
|
1006
|
-
} else if (
|
|
1009
|
+
} else if (kb.matches(data, "tui.editor.cursorRight")) {
|
|
1007
1010
|
// Right
|
|
1008
1011
|
this.#moveCursor(0, 1);
|
|
1009
|
-
} else if (
|
|
1012
|
+
} else if (kb.matches(data, "tui.editor.cursorLeft")) {
|
|
1010
1013
|
// Left
|
|
1011
1014
|
this.#moveCursor(0, -1);
|
|
1012
1015
|
}
|
|
@@ -1015,9 +1018,9 @@ export class Editor implements Component, Focusable {
|
|
|
1015
1018
|
this.#insertCharacter(" ");
|
|
1016
1019
|
}
|
|
1017
1020
|
// Character jump mode triggers
|
|
1018
|
-
else if (kb.matches(data, "jumpForward")) {
|
|
1021
|
+
else if (kb.matches(data, "tui.editor.jumpForward")) {
|
|
1019
1022
|
this.#jumpMode = "forward";
|
|
1020
|
-
} else if (kb.matches(data, "jumpBackward")) {
|
|
1023
|
+
} else if (kb.matches(data, "tui.editor.jumpBackward")) {
|
|
1021
1024
|
this.#jumpMode = "backward";
|
|
1022
1025
|
}
|
|
1023
1026
|
// Printable keystrokes, including Kitty CSI-u text-producing sequences.
|
|
@@ -1393,10 +1396,10 @@ export class Editor implements Component, Focusable {
|
|
|
1393
1396
|
}
|
|
1394
1397
|
}
|
|
1395
1398
|
|
|
1396
|
-
#shouldSubmitOnBackslashEnter(data: string, kb:
|
|
1399
|
+
#shouldSubmitOnBackslashEnter(data: string, kb: KeybindingsManager): boolean {
|
|
1397
1400
|
if (this.disableSubmit) return false;
|
|
1398
1401
|
if (!matchesKey(data, "enter")) return false;
|
|
1399
|
-
const submitKeys = kb.getKeys("submit");
|
|
1402
|
+
const submitKeys = kb.getKeys("tui.input.submit");
|
|
1400
1403
|
const hasShiftEnter = submitKeys.includes("shift+enter") || submitKeys.includes("shift+return");
|
|
1401
1404
|
if (!hasShiftEnter) return false;
|
|
1402
1405
|
|
|
@@ -2184,11 +2187,7 @@ export class Editor implements Component, Focusable {
|
|
|
2184
2187
|
|
|
2185
2188
|
if (suggestions && suggestions.items.length > 0) {
|
|
2186
2189
|
this.#autocompletePrefix = suggestions.prefix;
|
|
2187
|
-
this.#autocompleteList =
|
|
2188
|
-
suggestions.items,
|
|
2189
|
-
this.#autocompleteMaxVisible,
|
|
2190
|
-
this.#theme.selectList,
|
|
2191
|
-
);
|
|
2190
|
+
this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
2192
2191
|
this.#autocompleteState = "regular";
|
|
2193
2192
|
this.onAutocompleteUpdate?.();
|
|
2194
2193
|
} else {
|
|
@@ -2196,6 +2195,16 @@ export class Editor implements Component, Focusable {
|
|
|
2196
2195
|
this.onAutocompleteUpdate?.();
|
|
2197
2196
|
}
|
|
2198
2197
|
}
|
|
2198
|
+
#createAutocompleteList(
|
|
2199
|
+
prefix: string,
|
|
2200
|
+
items: Array<{ value: string; label: string; description?: string }>,
|
|
2201
|
+
): SelectList {
|
|
2202
|
+
// Layout options prepared for future SelectList enhancements (e.g., for slash commands)
|
|
2203
|
+
const layout = prefix.startsWith("/") ? SLASH_COMMAND_SELECT_LIST_LAYOUT : undefined;
|
|
2204
|
+
// TODO: Pass layout to SelectList when constructor is updated to support it
|
|
2205
|
+
void layout; // Use layout variable to avoid lint warnings
|
|
2206
|
+
return new SelectList(items, this.#autocompleteMaxVisible, this.#theme.selectList);
|
|
2207
|
+
}
|
|
2199
2208
|
|
|
2200
2209
|
#handleTabCompletion(): void {
|
|
2201
2210
|
if (!this.#autocompleteProvider) return;
|
|
@@ -2263,11 +2272,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2263
2272
|
}
|
|
2264
2273
|
|
|
2265
2274
|
this.#autocompletePrefix = suggestions.prefix;
|
|
2266
|
-
this.#autocompleteList =
|
|
2267
|
-
suggestions.items,
|
|
2268
|
-
this.#autocompleteMaxVisible,
|
|
2269
|
-
this.#theme.selectList,
|
|
2270
|
-
);
|
|
2275
|
+
this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
2271
2276
|
this.#autocompleteState = "force";
|
|
2272
2277
|
this.onAutocompleteUpdate?.();
|
|
2273
2278
|
} else {
|
|
@@ -2313,11 +2318,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2313
2318
|
if (suggestions && suggestions.items.length > 0) {
|
|
2314
2319
|
this.#autocompletePrefix = suggestions.prefix;
|
|
2315
2320
|
// Always create new SelectList to ensure update
|
|
2316
|
-
this.#autocompleteList =
|
|
2317
|
-
suggestions.items,
|
|
2318
|
-
this.#autocompleteMaxVisible,
|
|
2319
|
-
this.#theme.selectList,
|
|
2320
|
-
);
|
|
2321
|
+
this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
2321
2322
|
this.onAutocompleteUpdate?.();
|
|
2322
2323
|
} else {
|
|
2323
2324
|
this.#cancelAutocomplete();
|
package/src/components/input.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
2
|
-
import {
|
|
2
|
+
import { getKeybindings } from "../keybindings";
|
|
3
3
|
import { extractPrintableText } from "../keys";
|
|
4
4
|
import { KillRing } from "../kill-ring";
|
|
5
5
|
import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
|
|
@@ -65,69 +65,69 @@ export class Input implements Component, Focusable {
|
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const kb =
|
|
68
|
+
const kb = getKeybindings();
|
|
69
69
|
|
|
70
70
|
// Escape/Cancel
|
|
71
|
-
if (kb.matches(data, "
|
|
71
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
72
72
|
if (this.onEscape) this.onEscape();
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// Undo
|
|
77
|
-
if (kb.matches(data, "undo")) {
|
|
77
|
+
if (kb.matches(data, "tui.editor.undo")) {
|
|
78
78
|
this.#undo();
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// Submit
|
|
83
|
-
if (kb.matches(data, "submit") || data === "\n") {
|
|
83
|
+
if (kb.matches(data, "tui.input.submit") || data === "\n") {
|
|
84
84
|
if (this.onSubmit) this.onSubmit(this.#value);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// Deletion
|
|
89
|
-
if (kb.matches(data, "deleteCharBackward")) {
|
|
89
|
+
if (kb.matches(data, "tui.editor.deleteCharBackward")) {
|
|
90
90
|
this.#handleBackspace();
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
if (kb.matches(data, "deleteCharForward")) {
|
|
94
|
+
if (kb.matches(data, "tui.editor.deleteCharForward")) {
|
|
95
95
|
this.#handleForwardDelete();
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
if (kb.matches(data, "deleteWordBackward")) {
|
|
99
|
+
if (kb.matches(data, "tui.editor.deleteWordBackward")) {
|
|
100
100
|
this.#deleteWordBackwards();
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
if (kb.matches(data, "deleteWordForward")) {
|
|
104
|
+
if (kb.matches(data, "tui.editor.deleteWordForward")) {
|
|
105
105
|
this.#deleteWordForward();
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
if (kb.matches(data, "deleteToLineStart")) {
|
|
109
|
+
if (kb.matches(data, "tui.editor.deleteToLineStart")) {
|
|
110
110
|
this.#deleteToLineStart();
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
if (kb.matches(data, "deleteToLineEnd")) {
|
|
114
|
+
if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
|
|
115
115
|
this.#deleteToLineEnd();
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// Kill ring actions
|
|
120
|
-
if (kb.matches(data, "yank")) {
|
|
120
|
+
if (kb.matches(data, "tui.editor.yank")) {
|
|
121
121
|
this.#yank();
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
|
-
if (kb.matches(data, "yankPop")) {
|
|
124
|
+
if (kb.matches(data, "tui.editor.yankPop")) {
|
|
125
125
|
this.#yankPop();
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// Cursor movement
|
|
130
|
-
if (kb.matches(data, "cursorLeft")) {
|
|
130
|
+
if (kb.matches(data, "tui.editor.cursorLeft")) {
|
|
131
131
|
this.#lastAction = null;
|
|
132
132
|
if (this.#cursor > 0) {
|
|
133
133
|
const beforeCursor = this.#value.slice(0, this.#cursor);
|
|
@@ -138,7 +138,7 @@ export class Input implements Component, Focusable {
|
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
if (kb.matches(data, "cursorRight")) {
|
|
141
|
+
if (kb.matches(data, "tui.editor.cursorRight")) {
|
|
142
142
|
this.#lastAction = null;
|
|
143
143
|
if (this.#cursor < this.#value.length) {
|
|
144
144
|
const afterCursor = this.#value.slice(this.#cursor);
|
|
@@ -149,24 +149,24 @@ export class Input implements Component, Focusable {
|
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
if (kb.matches(data, "cursorLineStart")) {
|
|
152
|
+
if (kb.matches(data, "tui.editor.cursorLineStart")) {
|
|
153
153
|
this.#lastAction = null;
|
|
154
154
|
this.#cursor = 0;
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
if (kb.matches(data, "cursorLineEnd")) {
|
|
158
|
+
if (kb.matches(data, "tui.editor.cursorLineEnd")) {
|
|
159
159
|
this.#lastAction = null;
|
|
160
160
|
this.#cursor = this.#value.length;
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
if (kb.matches(data, "cursorWordLeft")) {
|
|
164
|
+
if (kb.matches(data, "tui.editor.cursorWordLeft")) {
|
|
165
165
|
this.#moveWordBackwards();
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
if (kb.matches(data, "cursorWordRight")) {
|
|
169
|
+
if (kb.matches(data, "tui.editor.cursorWordRight")) {
|
|
170
170
|
this.#moveWordForwards();
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { marked, type Token } from "marked";
|
|
1
|
+
import { marked, type Token, type Tokens } from "marked";
|
|
2
2
|
import type { SymbolTheme } from "../symbols";
|
|
3
3
|
import { TERMINAL } from "../terminal-capabilities";
|
|
4
4
|
import type { Component } from "../tui";
|
|
@@ -306,7 +306,7 @@ export class Markdown implements Component {
|
|
|
306
306
|
styledHeading = this.#theme.heading(this.#theme.bold(headingPrefix + headingText));
|
|
307
307
|
}
|
|
308
308
|
lines.push(styledHeading);
|
|
309
|
-
if (nextTokenType !== "space") {
|
|
309
|
+
if (nextTokenType && nextTokenType !== "space") {
|
|
310
310
|
lines.push(""); // Add spacing after headings (unless space token follows)
|
|
311
311
|
}
|
|
312
312
|
break;
|
|
@@ -332,7 +332,7 @@ export class Markdown implements Component {
|
|
|
332
332
|
for (const asciiLine of Bun.stripANSI(ascii).split("\n")) {
|
|
333
333
|
lines.push(asciiLine);
|
|
334
334
|
}
|
|
335
|
-
if (nextTokenType !== "space") {
|
|
335
|
+
if (nextTokenType && nextTokenType !== "space") {
|
|
336
336
|
lines.push("");
|
|
337
337
|
}
|
|
338
338
|
break;
|
|
@@ -354,7 +354,7 @@ export class Markdown implements Component {
|
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
lines.push(this.#theme.codeBlockBorder("```"));
|
|
357
|
-
if (nextTokenType !== "space") {
|
|
357
|
+
if (nextTokenType && nextTokenType !== "space") {
|
|
358
358
|
lines.push(""); // Add spacing after code blocks (unless space token follows)
|
|
359
359
|
}
|
|
360
360
|
break;
|
|
@@ -369,7 +369,7 @@ export class Markdown implements Component {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
case "table": {
|
|
372
|
-
const tableLines = this.#renderTable(token as TableToken, width, styleContext);
|
|
372
|
+
const tableLines = this.#renderTable(token as TableToken, width, nextTokenType, styleContext);
|
|
373
373
|
lines.push(...tableLines);
|
|
374
374
|
break;
|
|
375
375
|
}
|
|
@@ -415,7 +415,7 @@ export class Markdown implements Component {
|
|
|
415
415
|
lines.push(this.#theme.quoteBorder(`${this.#theme.symbols.quoteBorder} `) + wrappedLine);
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
|
-
if (nextTokenType !== "space") {
|
|
418
|
+
if (nextTokenType && nextTokenType !== "space") {
|
|
419
419
|
lines.push(""); // Add spacing after blockquotes (unless space token follows)
|
|
420
420
|
}
|
|
421
421
|
break;
|
|
@@ -423,7 +423,7 @@ export class Markdown implements Component {
|
|
|
423
423
|
|
|
424
424
|
case "hr":
|
|
425
425
|
lines.push(this.#theme.hr(this.#theme.symbols.hrChar.repeat(Math.min(width, 80))));
|
|
426
|
-
if (nextTokenType !== "space") {
|
|
426
|
+
if (nextTokenType && nextTokenType !== "space") {
|
|
427
427
|
lines.push(""); // Add spacing after horizontal rules (unless space token follows)
|
|
428
428
|
}
|
|
429
429
|
break;
|
|
@@ -669,7 +669,12 @@ export class Markdown implements Component {
|
|
|
669
669
|
* Render a table with width-aware cell wrapping.
|
|
670
670
|
* Cells that don't fit are wrapped to multiple lines.
|
|
671
671
|
*/
|
|
672
|
-
#renderTable(
|
|
672
|
+
#renderTable(
|
|
673
|
+
token: TableToken,
|
|
674
|
+
availableWidth: number,
|
|
675
|
+
nextTokenType?: string,
|
|
676
|
+
styleContext?: InlineStyleContext,
|
|
677
|
+
): string[] {
|
|
673
678
|
const lines: string[] = [];
|
|
674
679
|
const numCols = token.header.length;
|
|
675
680
|
|
|
@@ -684,7 +689,9 @@ export class Markdown implements Component {
|
|
|
684
689
|
if (availableForCells < numCols) {
|
|
685
690
|
// Too narrow to render a stable table. Fall back to raw markdown.
|
|
686
691
|
const fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];
|
|
687
|
-
|
|
692
|
+
if (nextTokenType && nextTokenType !== "space") {
|
|
693
|
+
fallbackLines.push("");
|
|
694
|
+
}
|
|
688
695
|
return fallbackLines;
|
|
689
696
|
}
|
|
690
697
|
|
|
@@ -834,7 +841,74 @@ export class Markdown implements Component {
|
|
|
834
841
|
const bottomBorderCells = columnWidths.map(w => h.repeat(w));
|
|
835
842
|
lines.push(`${t.bottomLeft}${h}${bottomBorderCells.join(`${h}${t.teeUp}${h}`)}${h}${t.bottomRight}`);
|
|
836
843
|
|
|
837
|
-
|
|
844
|
+
if (nextTokenType && nextTokenType !== "space") {
|
|
845
|
+
lines.push(""); // Add spacing after table
|
|
846
|
+
}
|
|
838
847
|
return lines;
|
|
839
848
|
}
|
|
840
849
|
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Render inline markdown (bold, italic, code, links, strikethrough) to a styled string.
|
|
853
|
+
* Unlike the full Markdown component, this produces a single line with no block-level elements.
|
|
854
|
+
*/
|
|
855
|
+
export function renderInlineMarkdown(text: string, mdTheme: MarkdownTheme, baseColor?: (t: string) => string): string {
|
|
856
|
+
const tokens = marked.lexer(text);
|
|
857
|
+
const applyText = baseColor ?? ((t: string) => t);
|
|
858
|
+
let result = "";
|
|
859
|
+
for (const token of tokens) {
|
|
860
|
+
if (token.type === "paragraph" && token.tokens) {
|
|
861
|
+
result += renderInlineTokens(token.tokens, mdTheme, applyText);
|
|
862
|
+
} else if (token.type === "list") {
|
|
863
|
+
result += token.items
|
|
864
|
+
.map((item: Tokens.ListItem, index: number) => {
|
|
865
|
+
const prefix = token.ordered ? `${(token.start || 1) + index}. ` : "• ";
|
|
866
|
+
const content = item.tokens ? renderInlineTokens(item.tokens, mdTheme, applyText) : applyText(item.text);
|
|
867
|
+
return `${applyText(prefix)}${content}`;
|
|
868
|
+
})
|
|
869
|
+
.join(applyText(" "));
|
|
870
|
+
} else if ("text" in token && typeof token.text === "string") {
|
|
871
|
+
result += applyText(token.text);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return result;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function renderInlineTokens(tokens: Token[], mdTheme: MarkdownTheme, applyText: (t: string) => string): string {
|
|
878
|
+
let result = "";
|
|
879
|
+
const styleReset = applyText("");
|
|
880
|
+
for (const token of tokens) {
|
|
881
|
+
switch (token.type) {
|
|
882
|
+
case "text":
|
|
883
|
+
if (token.tokens && token.tokens.length > 0) {
|
|
884
|
+
result += renderInlineTokens(token.tokens, mdTheme, applyText);
|
|
885
|
+
} else {
|
|
886
|
+
result += applyText(token.text);
|
|
887
|
+
}
|
|
888
|
+
break;
|
|
889
|
+
case "strong":
|
|
890
|
+
result += mdTheme.bold(renderInlineTokens(token.tokens || [], mdTheme, applyText)) + styleReset;
|
|
891
|
+
break;
|
|
892
|
+
case "em":
|
|
893
|
+
result += mdTheme.italic(renderInlineTokens(token.tokens || [], mdTheme, applyText)) + styleReset;
|
|
894
|
+
break;
|
|
895
|
+
case "codespan":
|
|
896
|
+
result += mdTheme.code(token.text) + styleReset;
|
|
897
|
+
break;
|
|
898
|
+
case "del":
|
|
899
|
+
result += mdTheme.strikethrough(renderInlineTokens(token.tokens || [], mdTheme, applyText)) + styleReset;
|
|
900
|
+
break;
|
|
901
|
+
case "link": {
|
|
902
|
+
const linkText = renderInlineTokens(token.tokens || [], mdTheme, applyText);
|
|
903
|
+
result += mdTheme.link(mdTheme.underline(linkText)) + styleReset;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
default:
|
|
907
|
+
if ("text" in token && typeof token.text === "string") {
|
|
908
|
+
result += applyText(token.text);
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return result;
|
|
914
|
+
}
|