@oh-my-pi/pi-tui 5.5.0 → 5.6.70
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/components/cancellable-loader.ts +2 -2
- package/src/components/editor.ts +314 -91
- package/src/components/input.ts +9 -3
- package/src/components/select-list.ts +5 -5
- package/src/components/settings-list.ts +5 -5
- package/src/components/tab-bar.ts +9 -6
- package/src/index.ts +1 -43
- package/src/keybindings.ts +30 -2
- package/src/keys.ts +416 -237
- package/src/terminal.ts +2 -1
- package/src/tui.ts +159 -69
package/src/components/input.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getEditorKeybindings } from "../keybindings";
|
|
2
|
-
import type
|
|
2
|
+
import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
|
|
3
3
|
import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils";
|
|
4
4
|
|
|
5
5
|
const segmenter = getSegmenter();
|
|
@@ -7,12 +7,15 @@ const segmenter = getSegmenter();
|
|
|
7
7
|
/**
|
|
8
8
|
* Input component - single-line text input with horizontal scrolling
|
|
9
9
|
*/
|
|
10
|
-
export class Input implements Component {
|
|
10
|
+
export class Input implements Component, Focusable {
|
|
11
11
|
private value: string = "";
|
|
12
12
|
private cursor: number = 0; // Cursor position in the value
|
|
13
13
|
public onSubmit?: (value: string) => void;
|
|
14
14
|
public onEscape?: () => void;
|
|
15
15
|
|
|
16
|
+
/** Focusable interface - set by TUI when focus changes */
|
|
17
|
+
focused: boolean = false;
|
|
18
|
+
|
|
16
19
|
// Bracketed paste mode buffering
|
|
17
20
|
private pasteBuffer: string = "";
|
|
18
21
|
private isInPaste: boolean = false;
|
|
@@ -325,9 +328,12 @@ export class Input implements Component {
|
|
|
325
328
|
const atCursor = visibleText[cursorDisplay] || " "; // Character at cursor, or space if at end
|
|
326
329
|
const afterCursor = visibleText.slice(cursorDisplay + 1);
|
|
327
330
|
|
|
331
|
+
// Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)
|
|
332
|
+
const marker = this.focused ? CURSOR_MARKER : "";
|
|
333
|
+
|
|
328
334
|
// Use inverse video to show cursor
|
|
329
335
|
const cursorChar = `\x1b[7m${atCursor}\x1b[27m`; // ESC[7m = reverse video, ESC[27m = normal
|
|
330
|
-
const textWithCursor = beforeCursor + cursorChar + afterCursor;
|
|
336
|
+
const textWithCursor = beforeCursor + marker + cursorChar + afterCursor;
|
|
331
337
|
|
|
332
338
|
// Calculate visual width
|
|
333
339
|
const visualLength = visibleWidth(textWithCursor);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { matchesKey } from "../keys";
|
|
2
2
|
import type { SymbolTheme } from "../symbols";
|
|
3
3
|
import type { Component } from "../tui";
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "../utils";
|
|
@@ -149,24 +149,24 @@ export class SelectList implements Component {
|
|
|
149
149
|
|
|
150
150
|
handleInput(keyData: string): void {
|
|
151
151
|
// Up arrow - wrap to bottom when at top
|
|
152
|
-
if (
|
|
152
|
+
if (matchesKey(keyData, "up")) {
|
|
153
153
|
this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;
|
|
154
154
|
this.notifySelectionChange();
|
|
155
155
|
}
|
|
156
156
|
// Down arrow - wrap to top when at bottom
|
|
157
|
-
else if (
|
|
157
|
+
else if (matchesKey(keyData, "down")) {
|
|
158
158
|
this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;
|
|
159
159
|
this.notifySelectionChange();
|
|
160
160
|
}
|
|
161
161
|
// Enter
|
|
162
|
-
else if (
|
|
162
|
+
else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
163
163
|
const selectedItem = this.filteredItems[this.selectedIndex];
|
|
164
164
|
if (selectedItem && this.onSelect) {
|
|
165
165
|
this.onSelect(selectedItem);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
// Escape or Ctrl+C
|
|
169
|
-
else if (
|
|
169
|
+
else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
|
|
170
170
|
if (this.onCancel) {
|
|
171
171
|
this.onCancel();
|
|
172
172
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { matchesKey } from "../keys";
|
|
2
2
|
import type { Component } from "../tui";
|
|
3
3
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
4
4
|
|
|
@@ -145,13 +145,13 @@ export class SettingsList implements Component {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// Main list input handling
|
|
148
|
-
if (
|
|
148
|
+
if (matchesKey(data, "up")) {
|
|
149
149
|
this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
|
|
150
|
-
} else if (
|
|
150
|
+
} else if (matchesKey(data, "down")) {
|
|
151
151
|
this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
|
|
152
|
-
} else if (
|
|
152
|
+
} else if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n" || data === " ") {
|
|
153
153
|
this.activateItem();
|
|
154
|
-
} else if (
|
|
154
|
+
} else if (matchesKey(data, "escape") || matchesKey(data, "esc") || matchesKey(data, "ctrl+c")) {
|
|
155
155
|
this.onCancel();
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* - Shift+Tab / Arrow Left: Previous tab (wraps around)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { matchesKey } from "../keys";
|
|
13
13
|
import type { Component } from "../tui";
|
|
14
|
+
import { wrapTextWithAnsi } from "../utils";
|
|
14
15
|
|
|
15
16
|
/** Tab definition */
|
|
16
17
|
export interface Tab {
|
|
@@ -99,19 +100,19 @@ export class TabBar implements Component {
|
|
|
99
100
|
* @returns true if the input was handled, false otherwise
|
|
100
101
|
*/
|
|
101
102
|
handleInput(data: string): boolean {
|
|
102
|
-
if (
|
|
103
|
+
if (matchesKey(data, "tab") || matchesKey(data, "right")) {
|
|
103
104
|
this.nextTab();
|
|
104
105
|
return true;
|
|
105
106
|
}
|
|
106
|
-
if (
|
|
107
|
+
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
107
108
|
this.prevTab();
|
|
108
109
|
return true;
|
|
109
110
|
}
|
|
110
111
|
return false;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
/** Render the tab bar
|
|
114
|
-
render(
|
|
114
|
+
/** Render the tab bar, wrapping to multiple lines if needed */
|
|
115
|
+
render(width: number): string[] {
|
|
115
116
|
const parts: string[] = [];
|
|
116
117
|
|
|
117
118
|
// Label prefix
|
|
@@ -135,6 +136,8 @@ export class TabBar implements Component {
|
|
|
135
136
|
parts.push(" ");
|
|
136
137
|
parts.push(this.theme.hint("(tab to cycle)"));
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
const line = parts.join("");
|
|
140
|
+
const maxWidth = Math.max(1, width);
|
|
141
|
+
return wrapTextWithAnsi(line, maxWidth);
|
|
139
142
|
}
|
|
140
143
|
}
|
package/src/index.ts
CHANGED
|
@@ -36,57 +36,15 @@ export {
|
|
|
36
36
|
} from "./keybindings";
|
|
37
37
|
// Kitty keyboard protocol helpers
|
|
38
38
|
export {
|
|
39
|
-
isAltBackspace,
|
|
40
|
-
isAltEnter,
|
|
41
|
-
isAltLeft,
|
|
42
|
-
isAltRight,
|
|
43
|
-
isArrowDown,
|
|
44
|
-
isArrowLeft,
|
|
45
|
-
isArrowRight,
|
|
46
|
-
isArrowUp,
|
|
47
|
-
isBackspace,
|
|
48
|
-
isCapsLock,
|
|
49
|
-
isCtrlA,
|
|
50
|
-
isCtrlC,
|
|
51
|
-
isCtrlD,
|
|
52
|
-
isCtrlE,
|
|
53
|
-
isCtrlG,
|
|
54
|
-
isCtrlK,
|
|
55
|
-
isCtrlL,
|
|
56
|
-
isCtrlLeft,
|
|
57
|
-
isCtrlO,
|
|
58
|
-
isCtrlP,
|
|
59
|
-
isCtrlRight,
|
|
60
|
-
isCtrlT,
|
|
61
|
-
isCtrlU,
|
|
62
|
-
isCtrlV,
|
|
63
|
-
isCtrlW,
|
|
64
|
-
isCtrlY,
|
|
65
|
-
isCtrlZ,
|
|
66
|
-
isDelete,
|
|
67
|
-
isEnd,
|
|
68
|
-
isEnter,
|
|
69
|
-
isEscape,
|
|
70
|
-
isHome,
|
|
71
39
|
isKeyRelease,
|
|
72
40
|
isKeyRepeat,
|
|
73
41
|
isKittyProtocolActive,
|
|
74
|
-
isPageDown,
|
|
75
|
-
isPageUp,
|
|
76
|
-
isShiftBackspace,
|
|
77
|
-
isShiftCtrlD,
|
|
78
|
-
isShiftCtrlO,
|
|
79
|
-
isShiftCtrlP,
|
|
80
|
-
isShiftDelete,
|
|
81
|
-
isShiftEnter,
|
|
82
|
-
isShiftSpace,
|
|
83
|
-
isShiftTab,
|
|
84
|
-
isTab,
|
|
85
42
|
Key,
|
|
86
43
|
type KeyEventType,
|
|
87
44
|
type KeyId,
|
|
88
45
|
matchesKey,
|
|
89
46
|
parseKey,
|
|
47
|
+
parseKittySequence,
|
|
90
48
|
setKittyProtocolActive,
|
|
91
49
|
} from "./keys";
|
|
92
50
|
// Input buffering for batch splitting
|
package/src/keybindings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type KeyId, matchesKey } from "./keys";
|
|
1
|
+
import { type KeyId, matchesKey, parseKey } from "./keys";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Editor actions that can be bound to keys.
|
|
@@ -77,6 +77,29 @@ export const DEFAULT_EDITOR_KEYBINDINGS: Required<EditorKeybindingsConfig> = {
|
|
|
77
77
|
copy: "ctrl+c",
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
const SHIFTED_SYMBOL_KEYS = new Set<string>([
|
|
81
|
+
"!",
|
|
82
|
+
"@",
|
|
83
|
+
"#",
|
|
84
|
+
"$",
|
|
85
|
+
"%",
|
|
86
|
+
"^",
|
|
87
|
+
"&",
|
|
88
|
+
"*",
|
|
89
|
+
"(",
|
|
90
|
+
")",
|
|
91
|
+
"_",
|
|
92
|
+
"+",
|
|
93
|
+
"{",
|
|
94
|
+
"}",
|
|
95
|
+
"|",
|
|
96
|
+
":",
|
|
97
|
+
"<",
|
|
98
|
+
">",
|
|
99
|
+
"?",
|
|
100
|
+
"~",
|
|
101
|
+
]);
|
|
102
|
+
|
|
80
103
|
/**
|
|
81
104
|
* Manages keybindings for the editor.
|
|
82
105
|
*/
|
|
@@ -114,7 +137,12 @@ export class EditorKeybindingsManager {
|
|
|
114
137
|
for (const key of keys) {
|
|
115
138
|
if (matchesKey(data, key)) return true;
|
|
116
139
|
}
|
|
117
|
-
|
|
140
|
+
|
|
141
|
+
const parsed = parseKey(data);
|
|
142
|
+
if (!parsed || !parsed.startsWith("shift+")) return false;
|
|
143
|
+
const keyName = parsed.slice("shift+".length);
|
|
144
|
+
if (!SHIFTED_SYMBOL_KEYS.has(keyName)) return false;
|
|
145
|
+
return keys.includes(keyName as KeyId);
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
/**
|