@oh-my-pi/pi-tui 3.8.1337 → 3.13.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.8.1337",
3
+ "version": "3.13.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,20 +180,45 @@ 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 6 for borders (3 left + 3 right)
191
+ const contentAreaWidth = width - 6;
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 - 6 (for "│ " prefix and " │" suffix borders)
218
+ const lineContentWidth = width - 6;
176
219
  for (const layoutLine of layoutLines) {
177
220
  let displayText = layoutLine.text;
178
- let lineVisibleWidth = visibleWidth(layoutLine.text);
221
+ let displayWidth = visibleWidth(layoutLine.text);
179
222
 
180
223
  // Add cursor if this line has it
181
224
  if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
@@ -190,16 +233,14 @@ export class Editor implements Component {
190
233
  const restAfter = after.slice(firstGrapheme.length);
191
234
  const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
192
235
  displayText = before + cursor + restAfter;
193
- // lineVisibleWidth stays the same - we're replacing, not adding
236
+ // displayWidth 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";
199
- displayText = before + cursor;
200
- // lineVisibleWidth increases by 1 - we're adding a space
201
- lineVisibleWidth = lineVisibleWidth + 1;
202
- } else {
238
+ // Cursor is at the end - add thin blinking bar cursor
239
+ // The character has width 1
240
+ const cursor = "\x1b[5m▏\x1b[0m";
241
+ displayText = before + cursor;
242
+ displayWidth += 1; // Account for cursor width
243
+ if (displayWidth > lineContentWidth) {
203
244
  // Line is at full width - use reverse video on last grapheme if possible
204
245
  // or just show cursor at the end without adding space
205
246
  const beforeGraphemes = [...segmenter.segment(before)];
@@ -212,22 +253,26 @@ export class Editor implements Component {
212
253
  .map((g) => g.segment)
213
254
  .join("");
214
255
  displayText = beforeWithoutLast + cursor;
256
+ displayWidth -= 1; // Back to original width (reverse video replaces, doesn't add)
215
257
  }
216
- // lineVisibleWidth stays the same
217
258
  }
218
259
  }
219
260
  }
220
261
 
221
- // Calculate padding based on actual visible width
222
- const padding = " ".repeat(Math.max(0, width - lineVisibleWidth));
262
+ // All lines have consistent 6-char borders (3 left + 3 right)
263
+ const isLastLine = layoutLine === layoutLines[layoutLines.length - 1];
264
+ const padding = " ".repeat(Math.max(0, lineContentWidth - displayWidth));
223
265
 
224
- // Render the line (no side borders, just horizontal lines above and below)
225
- result.push(displayText + padding);
266
+ if (isLastLine) {
267
+ // Last line: "╰─ " (3) + content + padding + " ─╯" (3) = 6 chars border
268
+ result.push(`${bottomLeft} ${displayText}${padding} ${bottomRight}`);
269
+ } else {
270
+ const leftBorder = this.borderColor("│ ");
271
+ const rightBorder = this.borderColor(" │");
272
+ result.push(leftBorder + displayText + padding + rightBorder);
273
+ }
226
274
  }
227
275
 
228
- // Render bottom border
229
- result.push(horizontal.repeat(width));
230
-
231
276
  // Add autocomplete list if active
232
277
  if (this.isAutocompleting && this.autocompleteList) {
233
278
  const autocompleteResult = this.autocompleteList.render(width);
@@ -1284,7 +1329,7 @@ export class Editor implements Component {
1284
1329
  https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/559322883
1285
1330
  17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
1286
1331
  536643416/job/55932288317 havea look at .gi
1287
- */
1332
+ */
1288
1333
  private forceFileAutocomplete(): void {
1289
1334
  if (!this.autocompleteProvider) return;
1290
1335
 
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 = [];