@oh-my-pi/pi-tui 3.13.1337 → 3.15.0

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/README.md CHANGED
@@ -154,9 +154,52 @@ input.getValue();
154
154
  Multi-line text editor with autocomplete, file completion, and paste handling.
155
155
 
156
156
  ```typescript
157
+ interface SymbolTheme {
158
+ cursor: string;
159
+ ellipsis: string;
160
+ boxRound: {
161
+ topLeft: string;
162
+ topRight: string;
163
+ bottomLeft: string;
164
+ bottomRight: string;
165
+ horizontal: string;
166
+ vertical: string;
167
+ };
168
+ boxSharp: {
169
+ topLeft: string;
170
+ topRight: string;
171
+ bottomLeft: string;
172
+ bottomRight: string;
173
+ horizontal: string;
174
+ vertical: string;
175
+ teeDown: string;
176
+ teeUp: string;
177
+ teeLeft: string;
178
+ teeRight: string;
179
+ cross: string;
180
+ };
181
+ table: {
182
+ topLeft: string;
183
+ topRight: string;
184
+ bottomLeft: string;
185
+ bottomRight: string;
186
+ horizontal: string;
187
+ vertical: string;
188
+ teeDown: string;
189
+ teeUp: string;
190
+ teeLeft: string;
191
+ teeRight: string;
192
+ cross: string;
193
+ };
194
+ quoteBorder: string;
195
+ hrChar: string;
196
+ spinnerFrames: string[];
197
+ }
198
+
157
199
  interface EditorTheme {
158
200
  borderColor: (str: string) => string;
159
201
  selectList: SelectListTheme;
202
+ symbols: SymbolTheme;
160
203
  }
161
204
 
162
205
  const editor = new Editor(theme);
@@ -206,6 +249,7 @@ interface MarkdownTheme {
206
249
  strikethrough: (text: string) => string;
207
250
  underline: (text: string) => string;
208
251
  highlightCode?: (code: string, lang?: string) => string[];
252
+ symbols: SymbolTheme;
209
253
  }
210
254
 
211
255
  interface DefaultTextStyle {
@@ -289,6 +333,7 @@ interface SelectListTheme {
289
333
  description: (text: string) => string;
290
334
  scrollInfo: (text: string) => string;
291
335
  noMatch: (text: string) => string;
336
+ symbols: SymbolTheme;
292
337
  }
293
338
 
294
339
  const list = new SelectList(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "3.13.1337",
3
+ "version": "3.15.0",
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",
@@ -25,6 +25,7 @@ import {
25
25
  isShiftEnter,
26
26
  isTab,
27
27
  } from "../keys";
28
+ import type { SymbolTheme } from "../symbols";
28
29
  import type { Component } from "../tui";
29
30
  import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils";
30
31
  import { SelectList, type SelectListTheme } from "./select-list";
@@ -46,6 +47,7 @@ interface LayoutLine {
46
47
  export interface EditorTheme {
47
48
  borderColor: (str: string) => string;
48
49
  selectList: SelectListTheme;
50
+ symbols: SymbolTheme;
49
51
  }
50
52
 
51
53
  export interface EditorTopBorder {
@@ -181,11 +183,12 @@ export class Editor implements Component {
181
183
  this.lastWidth = width;
182
184
 
183
185
  // 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("─╯");
188
- const horizontal = this.borderColor("─");
186
+ const box = this.theme.symbols.boxRound;
187
+ const topLeft = this.borderColor(`${box.topLeft}${box.horizontal}`);
188
+ const topRight = this.borderColor(`${box.horizontal}${box.topRight}`);
189
+ const bottomLeft = this.borderColor(`${box.bottomLeft}${box.horizontal}`);
190
+ const bottomRight = this.borderColor(`${box.horizontal}${box.bottomRight}`);
191
+ const horizontal = this.borderColor(box.horizontal);
189
192
 
190
193
  // Layout the text - content area is width minus 6 for borders (3 left + 3 right)
191
194
  const contentAreaWidth = width - 6;
@@ -201,13 +204,13 @@ export class Editor implements Component {
201
204
  if (statusWidth <= topFillWidth) {
202
205
  // Status fits - add fill after it
203
206
  const fillWidth = topFillWidth - statusWidth;
204
- result.push(topLeft + content + this.borderColor("─".repeat(fillWidth)) + topRight);
207
+ result.push(topLeft + content + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
205
208
  } else {
206
209
  // Status too long - truncate it
207
- const truncated = truncateToWidth(content, topFillWidth - 1, this.borderColor("…"));
210
+ const truncated = truncateToWidth(content, topFillWidth - 1, this.borderColor(this.theme.symbols.ellipsis));
208
211
  const truncatedWidth = visibleWidth(truncated);
209
212
  const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
210
- result.push(topLeft + truncated + this.borderColor("─".repeat(fillWidth)) + topRight);
213
+ result.push(topLeft + truncated + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
211
214
  }
212
215
  } else {
213
216
  result.push(topLeft + horizontal.repeat(topFillWidth) + topRight);
@@ -236,10 +239,10 @@ export class Editor implements Component {
236
239
  // displayWidth stays the same - we're replacing, not adding
237
240
  } else {
238
241
  // Cursor is at the end - add thin blinking bar cursor
239
- // The character has width 1
240
- const cursor = "\x1b[5m▏\x1b[0m";
242
+ const cursorChar = this.theme.symbols.inputCursor;
243
+ const cursor = `\x1b[5m${cursorChar}\x1b[0m`;
241
244
  displayText = before + cursor;
242
- displayWidth += 1; // Account for cursor width
245
+ displayWidth += visibleWidth(cursorChar);
243
246
  if (displayWidth > lineContentWidth) {
244
247
  // Line is at full width - use reverse video on last grapheme if possible
245
248
  // or just show cursor at the end without adding space
@@ -267,8 +270,8 @@ export class Editor implements Component {
267
270
  // Last line: "╰─ " (3) + content + padding + " ─╯" (3) = 6 chars border
268
271
  result.push(`${bottomLeft} ${displayText}${padding} ${bottomRight}`);
269
272
  } else {
270
- const leftBorder = this.borderColor("│ ");
271
- const rightBorder = this.borderColor(" │");
273
+ const leftBorder = this.borderColor(`${box.vertical} `);
274
+ const rightBorder = this.borderColor(` ${box.vertical}`);
272
275
  result.push(leftBorder + displayText + padding + rightBorder);
273
276
  }
274
277
  }
@@ -15,9 +15,13 @@ export class Loader extends Text {
15
15
  private spinnerColorFn: (str: string) => string,
16
16
  private messageColorFn: (str: string) => string,
17
17
  private message: string = "Loading...",
18
+ spinnerFrames?: string[],
18
19
  ) {
19
20
  super("", 1, 0);
20
21
  this.ui = ui;
22
+ if (spinnerFrames && spinnerFrames.length > 0) {
23
+ this.frames = spinnerFrames;
24
+ }
21
25
  this.start();
22
26
  }
23
27
 
@@ -1,4 +1,5 @@
1
1
  import { marked, type Token } from "marked";
2
+ import type { SymbolTheme } from "../symbols";
2
3
  import type { Component } from "../tui";
3
4
  import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils";
4
5
 
@@ -41,6 +42,7 @@ export interface MarkdownTheme {
41
42
  strikethrough: (text: string) => string;
42
43
  underline: (text: string) => string;
43
44
  highlightCode?: (code: string, lang?: string) => string[];
45
+ symbols: SymbolTheme;
44
46
  }
45
47
 
46
48
  export class Markdown implements Component {
@@ -301,7 +303,10 @@ export class Markdown implements Component {
301
303
  const quoteText = this.renderInlineTokens(token.tokens || []);
302
304
  const quoteLines = quoteText.split("\n");
303
305
  for (const quoteLine of quoteLines) {
304
- lines.push(this.theme.quoteBorder("│ ") + this.theme.quote(this.theme.italic(quoteLine)));
306
+ lines.push(
307
+ this.theme.quoteBorder(`${this.theme.symbols.quoteBorder} `) +
308
+ this.theme.quote(this.theme.italic(quoteLine)),
309
+ );
305
310
  }
306
311
  if (nextTokenType !== "space") {
307
312
  lines.push(""); // Add spacing after blockquotes (unless space token follows)
@@ -310,7 +315,7 @@ export class Markdown implements Component {
310
315
  }
311
316
 
312
317
  case "hr":
313
- lines.push(this.theme.hr("─".repeat(Math.min(width, 80))));
318
+ lines.push(this.theme.hr(this.theme.symbols.hrChar.repeat(Math.min(width, 80))));
314
319
  if (nextTokenType !== "space") {
315
320
  lines.push(""); // Add spacing after horizontal rules (unless space token follows)
316
321
  }
@@ -595,9 +600,13 @@ export class Markdown implements Component {
595
600
  }
596
601
  }
597
602
 
603
+ const t = this.theme.symbols.table;
604
+ const h = t.horizontal;
605
+ const v = t.vertical;
606
+
598
607
  // Render top border
599
- const topBorderCells = columnWidths.map((w) => "─".repeat(w));
600
- lines.push(`┌─${topBorderCells.join("─┬─")}─┐`);
608
+ const topBorderCells = columnWidths.map((w) => h.repeat(w));
609
+ lines.push(`${t.topLeft}${h}${topBorderCells.join(`${h}${t.teeDown}${h}`)}${h}${t.topRight}`);
601
610
 
602
611
  // Render header with wrapping
603
612
  const headerCellLines: string[][] = token.header.map((cell, i) => {
@@ -612,12 +621,12 @@ export class Markdown implements Component {
612
621
  const padded = text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
613
622
  return this.theme.bold(padded);
614
623
  });
615
- lines.push(`│ ${rowParts.join(" ")} │`);
624
+ lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
616
625
  }
617
626
 
618
627
  // Render separator
619
- const separatorCells = columnWidths.map((w) => "─".repeat(w));
620
- lines.push(`├─${separatorCells.join("─┼─")}─┤`);
628
+ const separatorCells = columnWidths.map((w) => h.repeat(w));
629
+ lines.push(`${t.teeRight}${h}${separatorCells.join(`${h}${t.cross}${h}`)}${h}${t.teeLeft}`);
621
630
 
622
631
  // Render rows with wrapping
623
632
  for (const row of token.rows) {
@@ -632,13 +641,13 @@ export class Markdown implements Component {
632
641
  const text = cellLines[lineIdx] || "";
633
642
  return text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
634
643
  });
635
- lines.push(`│ ${rowParts.join(" ")} │`);
644
+ lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
636
645
  }
637
646
  }
638
647
 
639
648
  // Render bottom border
640
- const bottomBorderCells = columnWidths.map((w) => "─".repeat(w));
641
- lines.push(`└─${bottomBorderCells.join("─┴─")}─┘`);
649
+ const bottomBorderCells = columnWidths.map((w) => h.repeat(w));
650
+ lines.push(`${t.bottomLeft}${h}${bottomBorderCells.join(`${h}${t.teeUp}${h}`)}${h}${t.bottomRight}`);
642
651
 
643
652
  lines.push(""); // Add spacing after table
644
653
  return lines;
@@ -1,6 +1,7 @@
1
1
  import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys";
2
+ import type { SymbolTheme } from "../symbols";
2
3
  import type { Component } from "../tui";
3
- import { truncateToWidth } from "../utils";
4
+ import { truncateToWidth, visibleWidth } from "../utils";
4
5
 
5
6
  export interface SelectItem {
6
7
  value: string;
@@ -14,6 +15,7 @@ export interface SelectListTheme {
14
15
  description: (text: string) => string;
15
16
  scrollInfo: (text: string) => string;
16
17
  noMatch: (text: string) => string;
18
+ symbols: SymbolTheme;
17
19
  }
18
20
 
19
21
  export class SelectList implements Component {
@@ -74,7 +76,8 @@ export class SelectList implements Component {
74
76
  let line = "";
75
77
  if (isSelected) {
76
78
  // Use arrow indicator for selection - entire line uses selectedText color
77
- const prefixWidth = 2; // "→ " is 2 characters visually
79
+ const prefix = `${this.theme.symbols.cursor} `;
80
+ const prefixWidth = visibleWidth(prefix);
78
81
  const displayValue = item.label || item.value;
79
82
 
80
83
  if (item.description && width > 40) {
@@ -90,20 +93,20 @@ export class SelectList implements Component {
90
93
  if (remainingWidth > 10) {
91
94
  const truncatedDesc = truncateToWidth(item.description, remainingWidth, "");
92
95
  // Apply selectedText to entire line content
93
- line = this.theme.selectedText(`→ ${truncatedValue}${spacing}${truncatedDesc}`);
96
+ line = this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`);
94
97
  } else {
95
98
  // Not enough space for description
96
99
  const maxWidth = width - prefixWidth - 2;
97
- line = this.theme.selectedText(`→ ${truncateToWidth(displayValue, maxWidth, "")}`);
100
+ line = this.theme.selectedText(`${prefix}${truncateToWidth(displayValue, maxWidth, "")}`);
98
101
  }
99
102
  } else {
100
103
  // No description or not enough width
101
104
  const maxWidth = width - prefixWidth - 2;
102
- line = this.theme.selectedText(`→ ${truncateToWidth(displayValue, maxWidth, "")}`);
105
+ line = this.theme.selectedText(`${prefix}${truncateToWidth(displayValue, maxWidth, "")}`);
103
106
  }
104
107
  } else {
105
108
  const displayValue = item.label || item.value;
106
- const prefix = " ";
109
+ const prefix = " ".repeat(visibleWidth(`${this.theme.symbols.cursor} `));
107
110
 
108
111
  if (item.description && width > 40) {
109
112
  // Calculate how much space we have for value + description
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export {
61
61
  isTab,
62
62
  Keys,
63
63
  } from "./keys";
64
+ export type { BoxSymbols, SymbolTheme } from "./symbols";
64
65
  // Terminal interface and implementations
65
66
  export { emergencyTerminalRestore, ProcessTerminal, type Terminal } from "./terminal";
66
67
  // Terminal image support
package/src/symbols.ts ADDED
@@ -0,0 +1,25 @@
1
+ export interface BoxSymbols {
2
+ topLeft: string;
3
+ topRight: string;
4
+ bottomLeft: string;
5
+ bottomRight: string;
6
+ horizontal: string;
7
+ vertical: string;
8
+ teeDown: string;
9
+ teeUp: string;
10
+ teeLeft: string;
11
+ teeRight: string;
12
+ cross: string;
13
+ }
14
+
15
+ export interface SymbolTheme {
16
+ cursor: string;
17
+ inputCursor: string;
18
+ ellipsis: string;
19
+ boxRound: Omit<BoxSymbols, "teeDown" | "teeUp" | "teeLeft" | "teeRight" | "cross">;
20
+ boxSharp: BoxSymbols;
21
+ table: BoxSymbols;
22
+ quoteBorder: string;
23
+ hrChar: string;
24
+ spinnerFrames: string[];
25
+ }