@oh-my-pi/pi-tui 3.6.1337 → 3.9.1337

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "3.6.1337",
3
+ "version": "3.9.1337",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -26,7 +26,7 @@ import {
26
26
  isTab,
27
27
  } from "../keys";
28
28
  import type { Component } from "../tui";
29
- import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils";
29
+ import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils";
30
30
  import { SelectList, type SelectListTheme } from "./select-list";
31
31
 
32
32
  const segmenter = getSegmenter();
@@ -48,6 +48,13 @@ export interface EditorTheme {
48
48
  selectList: SelectListTheme;
49
49
  }
50
50
 
51
+ export interface EditorTopBorder {
52
+ /** The status content (already styled) */
53
+ content: string;
54
+ /** Visible width of the content */
55
+ width: number;
56
+ }
57
+
51
58
  export class Editor implements Component {
52
59
  private state: EditorState = {
53
60
  lines: [""],
@@ -85,6 +92,9 @@ export class Editor implements Component {
85
92
  public onChange?: (text: string) => void;
86
93
  public disableSubmit: boolean = false;
87
94
 
95
+ // Custom top border (for status line integration)
96
+ private topBorderContent?: EditorTopBorder;
97
+
88
98
  constructor(theme: EditorTheme) {
89
99
  this.theme = theme;
90
100
  this.borderColor = theme.borderColor;
@@ -94,6 +104,14 @@ export class Editor implements Component {
94
104
  this.autocompleteProvider = provider;
95
105
  }
96
106
 
107
+ /**
108
+ * Set custom content for the top border (e.g., status line).
109
+ * Pass undefined to use the default plain border.
110
+ */
111
+ setTopBorder(content: EditorTopBorder | undefined): void {
112
+ this.topBorderContent = content;
113
+ }
114
+
97
115
  /**
98
116
  * Add a prompt to history for up/down arrow navigation.
99
117
  * Called after successful submission.
@@ -162,17 +180,42 @@ export class Editor implements Component {
162
180
  // Store width for cursor navigation
163
181
  this.lastWidth = width;
164
182
 
183
+ // Box-drawing characters for rounded corners
184
+ const topLeft = this.borderColor("╭─");
185
+ const topRight = this.borderColor("─╮");
186
+ const bottomLeft = this.borderColor("╰─");
187
+ const bottomRight = this.borderColor("─╯");
165
188
  const horizontal = this.borderColor("─");
166
189
 
167
- // Layout the text - use full width
168
- const layoutLines = this.layoutText(width);
190
+ // Layout the text - content area is width minus 4 for borders (2 left + 2 right)
191
+ const contentAreaWidth = width - 4;
192
+ const layoutLines = this.layoutText(contentAreaWidth);
169
193
 
170
194
  const result: string[] = [];
171
195
 
172
- // Render top border
173
- result.push(horizontal.repeat(width));
196
+ // Render top border: ╭─ [status content] ────────────────╮
197
+ // Reserve: 2 for "╭─", 2 for "─╮" = 4 total for corners
198
+ const topFillWidth = width - 4;
199
+ if (this.topBorderContent) {
200
+ const { content, width: statusWidth } = this.topBorderContent;
201
+ if (statusWidth <= topFillWidth) {
202
+ // Status fits - add fill after it
203
+ const fillWidth = topFillWidth - statusWidth;
204
+ result.push(topLeft + content + this.borderColor("─".repeat(fillWidth)) + topRight);
205
+ } else {
206
+ // Status too long - truncate it
207
+ const truncated = truncateToWidth(content, topFillWidth - 1, this.borderColor("…"));
208
+ const truncatedWidth = visibleWidth(truncated);
209
+ const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
210
+ result.push(topLeft + truncated + this.borderColor("─".repeat(fillWidth)) + topRight);
211
+ }
212
+ } else {
213
+ result.push(topLeft + horizontal.repeat(topFillWidth) + topRight);
214
+ }
174
215
 
175
216
  // Render each layout line
217
+ // Content area is width - 4 (for prefix and suffix borders)
218
+ const lineContentWidth = width - 4;
176
219
  for (const layoutLine of layoutLines) {
177
220
  let displayText = layoutLine.text;
178
221
  let lineVisibleWidth = visibleWidth(layoutLine.text);
@@ -192,10 +235,11 @@ export class Editor implements Component {
192
235
  displayText = before + cursor + restAfter;
193
236
  // lineVisibleWidth stays the same - we're replacing, not adding
194
237
  } else {
195
- // Cursor is at the end - check if we have room for the space
196
- if (lineVisibleWidth < width) {
197
- // We have room - add highlighted space
198
- const cursor = "\x1b[7m \x1b[0m";
238
+ // Cursor is at the end - check if we have room for the cursor
239
+ if (lineVisibleWidth < lineContentWidth) {
240
+ // We have room - add thin bar cursor (▏) with blink
241
+ // \x1b[5m = slow blink, no reverse video so it's a thin line
242
+ const cursor = "\x1b[5m▏\x1b[0m";
199
243
  displayText = before + cursor;
200
244
  // lineVisibleWidth increases by 1 - we're adding a space
201
245
  lineVisibleWidth = lineVisibleWidth + 1;
@@ -218,16 +262,19 @@ export class Editor implements Component {
218
262
  }
219
263
  }
220
264
 
221
- // Calculate padding based on actual visible width
222
- const padding = " ".repeat(Math.max(0, width - lineVisibleWidth));
265
+ // All lines get 1 char padding on each side for consistent alignment
266
+ const isLastLine = layoutLine === layoutLines[layoutLines.length - 1];
267
+ const padding = " ".repeat(Math.max(0, lineContentWidth - lineVisibleWidth - 2));
223
268
 
224
- // Render the line (no side borders, just horizontal lines above and below)
225
- result.push(displayText + padding);
269
+ if (isLastLine) {
270
+ result.push(`${bottomLeft} ${displayText}${padding} ${bottomRight}`);
271
+ } else {
272
+ const leftBorder = this.borderColor("│ ");
273
+ const rightBorder = this.borderColor(" │");
274
+ result.push(leftBorder + displayText + padding + rightBorder);
275
+ }
226
276
  }
227
277
 
228
- // Render bottom border
229
- result.push(horizontal.repeat(width));
230
-
231
278
  // Add autocomplete list if active
232
279
  if (this.isAutocompleting && this.autocompleteList) {
233
280
  const autocompleteResult = this.autocompleteList.render(width);
@@ -1284,7 +1331,7 @@ export class Editor implements Component {
1284
1331
  https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/559322883
1285
1332
  17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
1286
1333
  536643416/job/55932288317 havea look at .gi
1287
- */
1334
+ */
1288
1335
  private forceFileAutocomplete(): void {
1289
1336
  if (!this.autocompleteProvider) return;
1290
1337
 
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ export {
10
10
  // Components
11
11
  export { Box } from "./components/box";
12
12
  export { CancellableLoader } from "./components/cancellable-loader";
13
- export { Editor, type EditorTheme } from "./components/editor";
13
+ export { Editor, type EditorTheme, type EditorTopBorder } from "./components/editor";
14
14
  export { Image, type ImageOptions, type ImageTheme } from "./components/image";
15
15
  export { Input } from "./components/input";
16
16
  export { Loader } from "./components/loader";
package/src/tui.ts CHANGED
@@ -123,6 +123,10 @@ export class TUI extends Container {
123
123
  this.terminal.stop();
124
124
  }
125
125
 
126
+ getWidth(): number {
127
+ return this.terminal.columns;
128
+ }
129
+
126
130
  requestRender(force = false): void {
127
131
  if (force) {
128
132
  this.previousLines = [];