@oh-my-pi/pi-tui 18.0.8 → 18.0.10
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/CHANGELOG.md +26 -0
- package/dist/types/components/box.d.ts +2 -0
- package/dist/types/components/cancellable-loader.d.ts +2 -0
- package/dist/types/components/composer/band.d.ts +2 -0
- package/dist/types/components/composer/index.d.ts +1 -0
- package/dist/types/components/composer/types.d.ts +3 -3
- package/dist/types/components/editor.d.ts +4 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/input.d.ts +2 -0
- package/dist/types/components/loader.d.ts +8 -0
- package/dist/types/components/markdown.d.ts +2 -0
- package/dist/types/components/scroll-view.d.ts +2 -0
- package/dist/types/components/select-list.d.ts +2 -0
- package/dist/types/components/settings-list.d.ts +4 -0
- package/dist/types/components/spacer.d.ts +2 -0
- package/dist/types/components/tab-bar.d.ts +2 -0
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/components/truncated-text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/latex-to-unicode.d.ts +4 -4
- package/dist/types/tui.d.ts +23 -0
- package/package.json +3 -3
- package/src/components/box.ts +10 -0
- package/src/components/cancellable-loader.ts +7 -0
- package/src/components/composer/band.ts +42 -0
- package/src/components/composer/index.ts +1 -0
- package/src/components/composer/registry.ts +2 -0
- package/src/components/composer/types.ts +12 -3
- package/src/components/editor.ts +56 -10
- package/src/components/image.ts +11 -0
- package/src/components/input.ts +13 -0
- package/src/components/loader.ts +29 -2
- package/src/components/markdown.ts +39 -10
- package/src/components/scroll-view.ts +12 -0
- package/src/components/select-list.ts +13 -0
- package/src/components/settings-list.ts +19 -0
- package/src/components/spacer.ts +4 -0
- package/src/components/tab-bar.ts +11 -0
- package/src/components/text.ts +11 -0
- package/src/components/truncated-text.ts +15 -1
- package/src/debug-server.ts +422 -0
- package/src/index.ts +2 -0
- package/src/latex-to-unicode.ts +401 -172
- package/src/tui.ts +105 -12
- package/src/utils.ts +10 -1
package/src/components/loader.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { TUI } from "../tui";
|
|
2
|
-
import { getPaddingX, sliceByColumn, visibleWidth } from "../utils";
|
|
2
|
+
import { getPaddingX, padding, sliceByColumn, visibleWidth } from "../utils";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
|
|
5
5
|
const RENDER_INTERVAL_MS = 1000 / 30;
|
|
6
|
-
|
|
6
|
+
/** Milliseconds between spinner-frame advances; exported so time-derived spinners elsewhere tick at the Loader cadence. */
|
|
7
|
+
export const SPINNER_ADVANCE_MS = 80;
|
|
7
8
|
const RENDER_BACKPRESSURE_MULTIPLIER = 9;
|
|
8
9
|
const MAX_BACKPRESSURE_FRAME_COST_MS = 200;
|
|
9
10
|
|
|
@@ -28,6 +29,7 @@ export class Loader extends Text {
|
|
|
28
29
|
#layout?: readonly { leading: string; content: string; trailing: string }[];
|
|
29
30
|
#layoutFrames: readonly string[];
|
|
30
31
|
#layoutFrame: string;
|
|
32
|
+
#trailer?: () => string | undefined;
|
|
31
33
|
|
|
32
34
|
constructor(
|
|
33
35
|
ui: TUI,
|
|
@@ -54,6 +56,16 @@ export class Loader extends Text {
|
|
|
54
56
|
this.#layoutFrame = this.#layoutFrames[0];
|
|
55
57
|
this.start();
|
|
56
58
|
}
|
|
59
|
+
/** Return the current message and animation state for debug inspection. */
|
|
60
|
+
override debugState(): Record<string, unknown> {
|
|
61
|
+
const message = this.#resolveMessage();
|
|
62
|
+
return {
|
|
63
|
+
message: message.slice(0, 120),
|
|
64
|
+
messageLength: message.length,
|
|
65
|
+
running: this.#intervalId !== undefined,
|
|
66
|
+
frame: this.#currentFrame,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
57
69
|
|
|
58
70
|
override render(width: number): readonly string[] {
|
|
59
71
|
const source = super.render(width);
|
|
@@ -92,6 +104,15 @@ export class Loader extends Text {
|
|
|
92
104
|
lines.push(`${leading}${content ? this.messageColorFn(content) : ""}${trailing}`);
|
|
93
105
|
}
|
|
94
106
|
}
|
|
107
|
+
if (this.#trailer && lines.length > 1) {
|
|
108
|
+
const trailer = this.#trailer();
|
|
109
|
+
if (trailer) {
|
|
110
|
+
// Text pads rows to full width; drop that pad before docking right.
|
|
111
|
+
const body = lines[1].trimEnd();
|
|
112
|
+
const gap = width - visibleWidth(body) - visibleWidth(trailer);
|
|
113
|
+
if (gap >= 2) lines[1] = body + padding(gap) + trailer;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
95
116
|
return lines;
|
|
96
117
|
}
|
|
97
118
|
|
|
@@ -114,6 +135,12 @@ export class Loader extends Text {
|
|
|
114
135
|
dispose() {
|
|
115
136
|
this.stop();
|
|
116
137
|
}
|
|
138
|
+
/** Install a lazy right-docked suffix for the spinner row (e.g. a styled
|
|
139
|
+
* session title). Re-evaluated every paint; dropped when the row leaves
|
|
140
|
+
* less than a two-cell gap. */
|
|
141
|
+
setTrailer(trailer: (() => string | undefined) | undefined): void {
|
|
142
|
+
this.#trailer = trailer;
|
|
143
|
+
}
|
|
117
144
|
|
|
118
145
|
setMessage(message: string) {
|
|
119
146
|
if (message === this.message) {
|
|
@@ -1539,12 +1539,28 @@ function classifyHexColor(hex: string, strict: boolean): boolean {
|
|
|
1539
1539
|
return true;
|
|
1540
1540
|
}
|
|
1541
1541
|
|
|
1542
|
-
/**
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1542
|
+
/** Black-or-white foreground legible on `#${hex}` used as a fill. VS Code's
|
|
1543
|
+
* rule (Color.isLighter): YIQ brightness (r·299 + g·587 + b·114)/1000 ≥ 128
|
|
1544
|
+
* → dark text, else light. */
|
|
1545
|
+
function swatchContrastFg(hex: string): string {
|
|
1546
|
+
const rgba = Bun.color(`#${hex}`, "{rgba}");
|
|
1547
|
+
if (!rgba) return "";
|
|
1548
|
+
const light = (rgba.r * 299 + rgba.g * 587 + rgba.b * 114) / 1000 >= 128;
|
|
1549
|
+
if (TERMINAL.trueColor) return light ? "\x1b[38;2;0;0;0m" : "\x1b[38;2;255;255;255m";
|
|
1550
|
+
return light ? "\x1b[38;5;16m" : "\x1b[38;5;231m";
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/** Chip glyph + `text` (the `#hex` mention) painted onto the color itself:
|
|
1554
|
+
* a fg-painted chip, then the token with the color as background and a
|
|
1555
|
+
* YIQ-contrast foreground (VS Code's color-picker rule). Returns "" when the
|
|
1556
|
+
* color can't be encoded. Foreground closes with \x1b[39m and background with
|
|
1557
|
+
* a standalone \x1b[49m so line-level backgrounds re-open themselves via
|
|
1558
|
+
* applyBackgroundToLine / Theme.bgFill. */
|
|
1559
|
+
function colorSwatch(hex: string, glyph: string, text: string): string {
|
|
1560
|
+
const fg = Bun.color(`#${hex}`, TERMINAL.trueColor ? "ansi-16m" : "ansi-256");
|
|
1561
|
+
if (!fg) return "";
|
|
1562
|
+
const bg = fg.replace("[38;", "[48;");
|
|
1563
|
+
return `${fg}${glyph}\x1b[39m ${bg}${swatchContrastFg(hex)}${text}\x1b[39m\x1b[49m`;
|
|
1548
1564
|
}
|
|
1549
1565
|
|
|
1550
1566
|
/**
|
|
@@ -1560,10 +1576,10 @@ function renderTextWithSwatches(text: string, applySegment: (t: string) => strin
|
|
|
1560
1576
|
const match = HEX_COLOR_REGEX.exec(text);
|
|
1561
1577
|
if (match === null) break;
|
|
1562
1578
|
if (!classifyHexColor(match[1], true)) continue;
|
|
1563
|
-
const swatch = colorSwatch(match[1], glyph);
|
|
1579
|
+
const swatch = colorSwatch(match[1], glyph, match[0]);
|
|
1564
1580
|
if (!swatch) continue;
|
|
1565
1581
|
if (match.index > last) result += applySegment(text.slice(last, match.index));
|
|
1566
|
-
result += swatch
|
|
1582
|
+
result += swatch;
|
|
1567
1583
|
last = match.index + match[0].length;
|
|
1568
1584
|
}
|
|
1569
1585
|
if (last === 0) return applySegment(text);
|
|
@@ -1575,7 +1591,7 @@ function renderTextWithSwatches(text: string, applySegment: (t: string) => strin
|
|
|
1575
1591
|
function codespanSwatch(code: string, glyph: string): string {
|
|
1576
1592
|
const match = HEX_COLOR_EXACT_REGEX.exec(code.trim());
|
|
1577
1593
|
if (!match || !classifyHexColor(match[1], false)) return "";
|
|
1578
|
-
return colorSwatch(match[1], glyph);
|
|
1594
|
+
return colorSwatch(match[1], glyph, match[0]);
|
|
1579
1595
|
}
|
|
1580
1596
|
|
|
1581
1597
|
interface RenderSignature {
|
|
@@ -1757,6 +1773,18 @@ export class Markdown implements Component {
|
|
|
1757
1773
|
this.#defaultTextStyle = defaultTextStyle;
|
|
1758
1774
|
this.#codeBlockIndent = Math.max(0, Math.floor(codeBlockIndent));
|
|
1759
1775
|
}
|
|
1776
|
+
/** Return bounded source text and layout state for debug inspection. */
|
|
1777
|
+
debugState(): Record<string, unknown> {
|
|
1778
|
+
return {
|
|
1779
|
+
textPreview: this.#text.slice(0, 120),
|
|
1780
|
+
textLength: this.#text.length,
|
|
1781
|
+
previewTruncated: this.#text.length > 120,
|
|
1782
|
+
paddingX: this.#paddingX,
|
|
1783
|
+
paddingY: this.#paddingY,
|
|
1784
|
+
codeBlockIndent: this.#codeBlockIndent,
|
|
1785
|
+
ignoreTight: this.#ignoreTight,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1760
1788
|
|
|
1761
1789
|
setText(text: string): boolean {
|
|
1762
1790
|
// Identical re-emit (throttled tick): fully normalized already.
|
|
@@ -3118,7 +3146,8 @@ export class Markdown implements Component {
|
|
|
3118
3146
|
|
|
3119
3147
|
case "codespan": {
|
|
3120
3148
|
markHtmlItemWhenContent(token.text);
|
|
3121
|
-
|
|
3149
|
+
const painted = codespanSwatch(token.text, swatchGlyph);
|
|
3150
|
+
result += (painted || this.#theme.code(token.text)) + stylePrefix;
|
|
3122
3151
|
break;
|
|
3123
3152
|
}
|
|
3124
3153
|
|
|
@@ -79,6 +79,18 @@ export class ScrollView implements Component {
|
|
|
79
79
|
this.#fastScrollLines = Math.max(1, Math.trunc(options.fastScrollLines ?? 5));
|
|
80
80
|
this.#clampScrollOffset();
|
|
81
81
|
}
|
|
82
|
+
/** Return viewport, content, and scroll position state for debug inspection. */
|
|
83
|
+
debugState(): Record<string, unknown> {
|
|
84
|
+
const rowCount = this.#totalRows ?? this.#lines.length;
|
|
85
|
+
return {
|
|
86
|
+
scrollOffset: this.#scrollOffset,
|
|
87
|
+
maxScrollOffset: this.getMaxScrollOffset(),
|
|
88
|
+
height: this.#height,
|
|
89
|
+
rowCount,
|
|
90
|
+
bufferedLineCount: this.#lines.length,
|
|
91
|
+
scrollbar: this.#scrollbar,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
82
94
|
|
|
83
95
|
setLines(lines: readonly string[]): void {
|
|
84
96
|
this.#lines = [...lines];
|
|
@@ -116,6 +116,19 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
116
116
|
this.#maxVisible = Math.max(1, Math.trunc(maxVisible));
|
|
117
117
|
this.#filteredItems = items;
|
|
118
118
|
}
|
|
119
|
+
/** Return item, selection, and filter state for debug inspection. */
|
|
120
|
+
debugState(): Record<string, unknown> {
|
|
121
|
+
const selected = this.#filteredItems[this.#selectedIndex];
|
|
122
|
+
return {
|
|
123
|
+
itemCount: this.items.length,
|
|
124
|
+
filteredItemCount: this.#filteredItems.length,
|
|
125
|
+
selectedIndex: this.#filteredItems.length > 0 ? this.#selectedIndex : -1,
|
|
126
|
+
selectedItemId: selected?.value ?? null,
|
|
127
|
+
selectedItemLabel: selected?.label ?? null,
|
|
128
|
+
filterText: this.#filterQuery,
|
|
129
|
+
maxVisible: this.#maxVisible,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
119
132
|
|
|
120
133
|
/** Refit the visible row budget (hosts clamp the list to available height). */
|
|
121
134
|
setMaxVisible(rows: number): void {
|
|
@@ -142,6 +142,25 @@ export class SettingsList implements Component {
|
|
|
142
142
|
this.#selectedIndex = this.#firstSelectableIndex();
|
|
143
143
|
this.#lastNotifiedSelectionId = this.getSelectedItem()?.id;
|
|
144
144
|
}
|
|
145
|
+
/** Return item, selection, filter, and submenu state for debug inspection. */
|
|
146
|
+
debugState(): Record<string, unknown> {
|
|
147
|
+
const selected = this.getSelectedItem();
|
|
148
|
+
return {
|
|
149
|
+
itemCount: this.#items.length,
|
|
150
|
+
filteredItemCount: this.#filteredItems.length,
|
|
151
|
+
selectedIndex: this.#filteredItems.length > 0 ? this.#selectedIndex : -1,
|
|
152
|
+
selectedItemId: selected?.id ?? null,
|
|
153
|
+
selectedItemLabel: selected?.label ?? null,
|
|
154
|
+
filterText: this.#filterQuery,
|
|
155
|
+
sectionFocused: this.#sectionFocus,
|
|
156
|
+
submenuOpen: this.#submenuComponent !== null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Expose the active submenu to the debug tree walker. */
|
|
161
|
+
get debugChildren(): readonly Component[] {
|
|
162
|
+
return this.#submenuComponent ? [this.#submenuComponent] : [];
|
|
163
|
+
}
|
|
145
164
|
|
|
146
165
|
/** The currently selected item, or undefined when empty or on a heading. */
|
|
147
166
|
getSelectedItem(): SettingItem | undefined {
|
package/src/components/spacer.ts
CHANGED
|
@@ -10,6 +10,10 @@ export class Spacer implements Component {
|
|
|
10
10
|
constructor(lines: number = 1) {
|
|
11
11
|
this.#lines = lines;
|
|
12
12
|
}
|
|
13
|
+
/** Return the spacer height for debug inspection. */
|
|
14
|
+
debugState(): Record<string, unknown> {
|
|
15
|
+
return { height: this.#lines };
|
|
16
|
+
}
|
|
13
17
|
|
|
14
18
|
setLines(lines: number): void {
|
|
15
19
|
if (lines === this.#lines) return;
|
|
@@ -74,6 +74,17 @@ export class TabBar implements Component {
|
|
|
74
74
|
this.#theme = theme;
|
|
75
75
|
this.#activeIndex = initialIndex;
|
|
76
76
|
}
|
|
77
|
+
/** Return tab identities and active-tab state for debug inspection. */
|
|
78
|
+
debugState(): Record<string, unknown> {
|
|
79
|
+
const active = this.#tabs[this.#activeIndex];
|
|
80
|
+
return {
|
|
81
|
+
label: this.#label,
|
|
82
|
+
tabs: this.#tabs.map(tab => ({ id: tab.id, label: tab.label })),
|
|
83
|
+
activeIndex: this.#tabs.length > 0 ? this.#activeIndex : -1,
|
|
84
|
+
activeTabId: active?.id ?? null,
|
|
85
|
+
activeTabLabel: active?.label ?? null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
77
88
|
|
|
78
89
|
/** Get the currently active tab */
|
|
79
90
|
getActiveTab(): Tab {
|
package/src/components/text.ts
CHANGED
|
@@ -46,6 +46,17 @@ export class Text implements Component {
|
|
|
46
46
|
this.#paddingY = paddingY;
|
|
47
47
|
this.#customBgFn = customBgFn;
|
|
48
48
|
}
|
|
49
|
+
/** Return bounded text and layout state for debug inspection. */
|
|
50
|
+
debugState(): Record<string, unknown> {
|
|
51
|
+
return {
|
|
52
|
+
textPreview: this.#text.slice(0, 120),
|
|
53
|
+
textLength: this.#text.length,
|
|
54
|
+
previewTruncated: this.#text.length > 120,
|
|
55
|
+
paddingX: this.#paddingX,
|
|
56
|
+
paddingY: this.#paddingY,
|
|
57
|
+
ignoreTight: this.#ignoreTight,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
49
60
|
|
|
50
61
|
getText(): string {
|
|
51
62
|
return this.#text;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "../tui";
|
|
2
|
-
import { padding, truncateToWidth } from "../utils";
|
|
2
|
+
import { padding, truncateToWidth, visibleWidth } from "../utils";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Text component that truncates to fit viewport width
|
|
@@ -16,6 +16,20 @@ export class TruncatedText implements Component {
|
|
|
16
16
|
this.#paddingX = paddingX;
|
|
17
17
|
this.#paddingY = paddingY;
|
|
18
18
|
}
|
|
19
|
+
/** Return bounded source text and the last-known truncation state. */
|
|
20
|
+
debugState(): Record<string, unknown> {
|
|
21
|
+
const newlineIndex = this.#text.indexOf("\n");
|
|
22
|
+
const firstLine = newlineIndex === -1 ? this.#text : this.#text.slice(0, newlineIndex);
|
|
23
|
+
const availableWidth = Math.max(1, this.#cachedWidth - this.#paddingX * 2);
|
|
24
|
+
return {
|
|
25
|
+
textPreview: this.#text.slice(0, 120),
|
|
26
|
+
textLength: this.#text.length,
|
|
27
|
+
previewTruncated: this.#text.length > 120,
|
|
28
|
+
truncated: newlineIndex !== -1 || (this.#cachedWidth >= 0 && visibleWidth(firstLine) > availableWidth),
|
|
29
|
+
paddingX: this.#paddingX,
|
|
30
|
+
paddingY: this.#paddingY,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
19
33
|
|
|
20
34
|
invalidate(): void {
|
|
21
35
|
this.#cachedWidth = -1;
|