@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 +45 -0
- package/package.json +1 -1
- package/src/components/editor.ts +16 -13
- package/src/components/loader.ts +4 -0
- package/src/components/markdown.ts +19 -10
- package/src/components/select-list.ts +9 -6
- package/src/index.ts +1 -0
- package/src/symbols.ts +25 -0
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
package/src/components/editor.ts
CHANGED
|
@@ -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
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
|
|
240
|
-
const cursor =
|
|
242
|
+
const cursorChar = this.theme.symbols.inputCursor;
|
|
243
|
+
const cursor = `\x1b[5m${cursorChar}\x1b[0m`;
|
|
241
244
|
displayText = before + cursor;
|
|
242
|
-
displayWidth +=
|
|
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
|
}
|
package/src/components/loader.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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) =>
|
|
600
|
-
lines.push(
|
|
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(
|
|
624
|
+
lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
|
|
616
625
|
}
|
|
617
626
|
|
|
618
627
|
// Render separator
|
|
619
|
-
const separatorCells = columnWidths.map((w) =>
|
|
620
|
-
lines.push(
|
|
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(
|
|
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) =>
|
|
641
|
-
lines.push(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|