@oh-my-pi/pi-tui 5.5.0 → 5.6.7

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,5 +1,5 @@
1
1
  import { getEditorKeybindings } from "../keybindings";
2
- import type { Component } from "../tui";
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 { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys";
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 (isArrowUp(keyData)) {
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 (isArrowDown(keyData)) {
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 (isEnter(keyData)) {
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 (isEscape(keyData) || isCtrlC(keyData)) {
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 { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys";
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 (isArrowUp(data)) {
148
+ if (matchesKey(data, "up")) {
149
149
  this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
150
- } else if (isArrowDown(data)) {
150
+ } else if (matchesKey(data, "down")) {
151
151
  this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
152
- } else if (isEnter(data) || data === " ") {
152
+ } else if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n" || data === " ") {
153
153
  this.activateItem();
154
- } else if (isEscape(data) || isCtrlC(data)) {
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 { isArrowLeft, isArrowRight, isShiftTab, isTab } from "../keys";
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 (isTab(data) || isArrowRight(data)) {
103
+ if (matchesKey(data, "tab") || matchesKey(data, "right")) {
103
104
  this.nextTab();
104
105
  return true;
105
106
  }
106
- if (isShiftTab(data) || isArrowLeft(data)) {
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 as a single line */
114
- render(_width: number): string[] {
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
- return [parts.join("")];
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
@@ -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
- return false;
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
  /**