pi-tui-footer 1.2.4 → 1.2.6
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/extensions/config.ts +7 -29
- package/extensions/editor.ts +0 -11
- package/extensions/footer.ts +16 -3
- package/extensions/icons.ts +54 -4
- package/extensions/index.ts +5 -10
- package/extensions/rounded-tools.ts +1 -1
- package/extensions/settings-command.ts +14 -111
- package/extensions/utils.ts +22 -0
- package/package.json +1 -1
- package/extensions/fullscreen-scroll.ts +0 -34
package/extensions/config.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
6
|
-
normalizeFullscreenWheelScrollLines,
|
|
7
|
-
} from "./fullscreen-scroll.ts";
|
|
8
4
|
import type { IconMode } from "./icons.ts";
|
|
9
5
|
|
|
10
6
|
export type SettingsLanguage = "en" | "zh";
|
|
@@ -42,17 +38,12 @@ export interface ThinkingPeekConfig {
|
|
|
42
38
|
lines: ThinkingPeekLines;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
export interface FullscreenConfig {
|
|
46
|
-
wheelScrollLines: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
export interface OpenTuiConfig {
|
|
50
42
|
enabled: boolean;
|
|
51
43
|
roundedTools: boolean;
|
|
52
44
|
inlineFooter: boolean;
|
|
53
45
|
settingsLanguage: SettingsLanguage;
|
|
54
46
|
cursorStyle: CursorStyle;
|
|
55
|
-
fullscreen: FullscreenConfig;
|
|
56
47
|
icons: {
|
|
57
48
|
mode: IconMode;
|
|
58
49
|
};
|
|
@@ -61,17 +52,12 @@ export interface OpenTuiConfig {
|
|
|
61
52
|
thinkingPeek: ThinkingPeekConfig;
|
|
62
53
|
}
|
|
63
54
|
|
|
64
|
-
export type PiTuiConfig = OpenTuiConfig;
|
|
65
|
-
|
|
66
55
|
export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
67
56
|
enabled: true,
|
|
68
57
|
roundedTools: true,
|
|
69
58
|
inlineFooter: false,
|
|
70
59
|
settingsLanguage: "en",
|
|
71
60
|
cursorStyle: "block",
|
|
72
|
-
fullscreen: {
|
|
73
|
-
wheelScrollLines: DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
74
|
-
},
|
|
75
61
|
icons: {
|
|
76
62
|
mode: "auto",
|
|
77
63
|
},
|
|
@@ -105,11 +91,7 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
|
105
91
|
|
|
106
92
|
export function getConfigPath(): string {
|
|
107
93
|
const agentDir = getAgentDir();
|
|
108
|
-
|
|
109
|
-
const openTuiPath = join(agentDir, "open-tui.json");
|
|
110
|
-
if (existsSync(piTuiPath)) return piTuiPath;
|
|
111
|
-
if (existsSync(openTuiPath)) return openTuiPath;
|
|
112
|
-
return piTuiPath;
|
|
94
|
+
return join(agentDir, "pi-tui.json");
|
|
113
95
|
}
|
|
114
96
|
|
|
115
97
|
function normalizeThinkingPeekLines(value: unknown): ThinkingPeekLines {
|
|
@@ -161,27 +143,23 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
161
143
|
const raw = readFileSync(path, "utf8");
|
|
162
144
|
const parsed: unknown = JSON.parse(raw);
|
|
163
145
|
const config = deepMerge(DEFAULT_CONFIG, parsed);
|
|
146
|
+
if (typeof config.inlineFooter !== "boolean") {
|
|
147
|
+
config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
|
|
148
|
+
}
|
|
149
|
+
if (typeof config.roundedTools !== "boolean") {
|
|
150
|
+
config.roundedTools = DEFAULT_CONFIG.roundedTools;
|
|
151
|
+
}
|
|
164
152
|
if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
|
|
165
153
|
config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
|
|
166
154
|
}
|
|
167
155
|
if (config.cursorStyle !== "block" && config.cursorStyle !== "bar" && config.cursorStyle !== "underline") {
|
|
168
156
|
config.cursorStyle = DEFAULT_CONFIG.cursorStyle;
|
|
169
157
|
}
|
|
170
|
-
config.fullscreen.wheelScrollLines = normalizeFullscreenWheelScrollLines(
|
|
171
|
-
config.fullscreen.wheelScrollLines,
|
|
172
|
-
DEFAULT_CONFIG.fullscreen.wheelScrollLines,
|
|
173
|
-
);
|
|
174
158
|
if (typeof config.thinkingPeek !== "object" || config.thinkingPeek === null || Array.isArray(config.thinkingPeek)) {
|
|
175
159
|
config.thinkingPeek = structuredClone(DEFAULT_CONFIG.thinkingPeek);
|
|
176
160
|
} else {
|
|
177
161
|
config.thinkingPeek.lines = normalizeThinkingPeekLines(config.thinkingPeek.lines);
|
|
178
162
|
}
|
|
179
|
-
if (typeof config.inlineFooter !== "boolean") {
|
|
180
|
-
config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
|
|
181
|
-
}
|
|
182
|
-
if (typeof config.roundedTools !== "boolean") {
|
|
183
|
-
config.roundedTools = DEFAULT_CONFIG.roundedTools;
|
|
184
|
-
}
|
|
185
163
|
return config;
|
|
186
164
|
} catch (err) {
|
|
187
165
|
notify?.(`pi-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
package/extensions/editor.ts
CHANGED
|
@@ -7,10 +7,6 @@ import {
|
|
|
7
7
|
import type { EditorTheme, TuiMouseEvent, TuiMouseEventResult, TUI } from "@earendil-works/pi-tui";
|
|
8
8
|
import { CURSOR_MARKER, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { CursorStyle } from "./config.ts";
|
|
10
|
-
import {
|
|
11
|
-
applyFullscreenWheelScrollLines,
|
|
12
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
13
|
-
} from "./fullscreen-scroll.ts";
|
|
14
10
|
import { findBottomBorderIndex, isEditorBorderLine, stripAnsi } from "./utils.ts";
|
|
15
11
|
|
|
16
12
|
function fillLine(content: string, width: number): string {
|
|
@@ -316,14 +312,12 @@ export function installEditor(
|
|
|
316
312
|
_pi: ExtensionAPI,
|
|
317
313
|
ctx: ExtensionContext,
|
|
318
314
|
cursorStyle: CursorStyle = "block",
|
|
319
|
-
wheelScrollLines = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
320
315
|
inlineFooter?: InlineFooterRenderer,
|
|
321
316
|
) {
|
|
322
317
|
let activeTui: TUI | undefined;
|
|
323
318
|
let activeEditor: OpenTuiEditor | undefined;
|
|
324
319
|
let previousHardwareCursor: boolean | undefined;
|
|
325
320
|
let currentCursorStyle = cursorStyle;
|
|
326
|
-
let currentWheelScrollLines = wheelScrollLines;
|
|
327
321
|
let hiddenThinkingTarget: HiddenThinkingLabelComponent | undefined;
|
|
328
322
|
const getActiveTui = (): TUI => {
|
|
329
323
|
if (!activeTui) throw new Error("Open TUI editor is not mounted");
|
|
@@ -333,7 +327,6 @@ export function installEditor(
|
|
|
333
327
|
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
|
|
334
328
|
activeTui = tui;
|
|
335
329
|
hiddenThinkingTarget = undefined;
|
|
336
|
-
applyFullscreenWheelScrollLines(tui, currentWheelScrollLines);
|
|
337
330
|
previousHardwareCursor = tui.getShowHardwareCursor();
|
|
338
331
|
activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle, inlineFooter);
|
|
339
332
|
return activeEditor;
|
|
@@ -359,10 +352,6 @@ export function installEditor(
|
|
|
359
352
|
currentCursorStyle = nextCursorStyle;
|
|
360
353
|
activeEditor?.setCursorStyle(nextCursorStyle, previousHardwareCursor);
|
|
361
354
|
},
|
|
362
|
-
setWheelScrollLines(nextWheelScrollLines: number): void {
|
|
363
|
-
currentWheelScrollLines = nextWheelScrollLines;
|
|
364
|
-
if (activeTui) applyFullscreenWheelScrollLines(activeTui, currentWheelScrollLines);
|
|
365
|
-
},
|
|
366
355
|
cleanup(): void {
|
|
367
356
|
hiddenThinkingTarget = undefined;
|
|
368
357
|
ctx.ui.setEditorComponent(undefined);
|
package/extensions/footer.ts
CHANGED
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
formatCwd,
|
|
17
17
|
formatDuration,
|
|
18
18
|
formatInputBreakdown,
|
|
19
|
+
hasAnsiStyles,
|
|
19
20
|
providerColor,
|
|
20
|
-
|
|
21
|
+
sanitizeStatusPreservingStyles,
|
|
21
22
|
stressColor,
|
|
22
23
|
truncateBranch,
|
|
23
24
|
truncatePath,
|
|
@@ -181,6 +182,18 @@ function renderStatsBlock(
|
|
|
181
182
|
return stats.join(` ${theme.fg("dim", "|")} `);
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
const ANSI_RESET = "\x1b[0m";
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Keep the colours an extension applied to its own status text. Plain text stays
|
|
189
|
+
* muted so unstyled statuses keep the quiet footer look. The trailing reset
|
|
190
|
+
* stops an unclosed style from tinting the separator and the next status.
|
|
191
|
+
*/
|
|
192
|
+
function renderStatusText(theme: Theme, status: string): string {
|
|
193
|
+
if (!hasAnsiStyles(status)) return theme.fg("muted", status);
|
|
194
|
+
return `${status}${ANSI_RESET}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
184
197
|
function renderExtensionStatusLines(
|
|
185
198
|
theme: Theme,
|
|
186
199
|
extensionStatuses: ReadonlyMap<string, string>,
|
|
@@ -189,12 +202,12 @@ function renderExtensionStatusLines(
|
|
|
189
202
|
): string[] {
|
|
190
203
|
const statuses = Array.from(extensionStatuses.entries())
|
|
191
204
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
192
|
-
.map(([, text]) =>
|
|
205
|
+
.map(([, text]) => sanitizeStatusPreservingStyles(text))
|
|
193
206
|
.filter((text) => text.length > 0);
|
|
194
207
|
if (statuses.length === 0) return [];
|
|
195
208
|
|
|
196
209
|
const separator = ` ${theme.fg("dim", "|")} `;
|
|
197
|
-
const statusText = statuses.map((status) => theme
|
|
210
|
+
const statusText = statuses.map((status) => renderStatusText(theme, status)).join(separator);
|
|
198
211
|
const line = `${theme.fg("mdLink", glyphs.extensions)} ${statusText}`;
|
|
199
212
|
return wrapTextWithAnsi(line, width);
|
|
200
213
|
}
|
package/extensions/icons.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IconMode = "auto" | "nerd" | "ascii";
|
|
1
|
+
export type IconMode = "auto" | "nerd" | "unicode" | "ascii";
|
|
2
2
|
|
|
3
3
|
export interface IconGlyphs {
|
|
4
4
|
cwd: string;
|
|
@@ -94,6 +94,47 @@ const ASCII_GLYPHS: IconGlyphs = {
|
|
|
94
94
|
deleted: "x",
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
// Portable Unicode icon set for terminals without a Nerd Font. Keeps the
|
|
98
|
+
// footer icon-like instead of dropping to letters: emoji glyphs (folder,
|
|
99
|
+
// branch, laptop, bulb, floppy, plug, hourglass, bolt) render through the
|
|
100
|
+
// client terminal's emoji fallback at 2 columns, which matches
|
|
101
|
+
// pi-tui's visibleWidth for RGI emoji; the remaining symbols are
|
|
102
|
+
// single-width and covered by DejaVu Sans Mono, JetBrains Mono, Noto Sans
|
|
103
|
+
// Mono and Liberation Mono. Glyph choices are easy to adjust per slot.
|
|
104
|
+
const UNICODE_GLYPHS: IconGlyphs = {
|
|
105
|
+
cwd: "📁",
|
|
106
|
+
host: "⌂",
|
|
107
|
+
session: "🔖",
|
|
108
|
+
git: "🌿",
|
|
109
|
+
working: "◷",
|
|
110
|
+
done: "✓",
|
|
111
|
+
context: "≡",
|
|
112
|
+
model: "💻",
|
|
113
|
+
thinking: "💡",
|
|
114
|
+
input: "↑",
|
|
115
|
+
output: "↓",
|
|
116
|
+
cacheHit: "💾",
|
|
117
|
+
cost: "$",
|
|
118
|
+
speed: "⚡",
|
|
119
|
+
latency: "⌛",
|
|
120
|
+
stall: "⚠",
|
|
121
|
+
extensions: "🔌",
|
|
122
|
+
ahead: "↑",
|
|
123
|
+
behind: "↓",
|
|
124
|
+
diverged: "↕",
|
|
125
|
+
conflicted: "=",
|
|
126
|
+
stashed: "$",
|
|
127
|
+
modified: "!",
|
|
128
|
+
staged: "+",
|
|
129
|
+
untracked: "?",
|
|
130
|
+
renamed: "→",
|
|
131
|
+
deleted: "✗",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function isSshSession(): boolean {
|
|
135
|
+
return Boolean(process.env.SSH_TTY || process.env.SSH_CONNECTION);
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
export function detectNerdFont(): boolean {
|
|
98
139
|
// TTY and locale checks are the only reliable signals available here. The
|
|
99
140
|
// terminal emulator owns font selection, so auto mode is optimistic once
|
|
@@ -103,14 +144,23 @@ export function detectNerdFont(): boolean {
|
|
|
103
144
|
return locale === undefined || /utf-?8/i.test(locale);
|
|
104
145
|
}
|
|
105
146
|
|
|
106
|
-
export function resolveIconMode(mode: IconMode): "nerd" | "ascii" {
|
|
147
|
+
export function resolveIconMode(mode: IconMode): "nerd" | "unicode" | "ascii" {
|
|
107
148
|
if (mode === "nerd") return "nerd";
|
|
108
149
|
if (mode === "ascii") return "ascii";
|
|
109
|
-
|
|
150
|
+
if (mode === "unicode") return "unicode";
|
|
151
|
+
if (!detectNerdFont()) return "ascii";
|
|
152
|
+
// Auto cannot know whether the terminal that renders this session ships a
|
|
153
|
+
// Nerd Font (see #38). Local terminals usually do; SSH clients usually do
|
|
154
|
+
// not, because the font lives on the client device, so portable Unicode
|
|
155
|
+
// symbols are the safer default there. Terminals that do have a Nerd Font
|
|
156
|
+
// can opt back in with an explicit icons.mode of "nerd".
|
|
157
|
+
if (isSshSession()) return "unicode";
|
|
158
|
+
return "nerd";
|
|
110
159
|
}
|
|
111
160
|
|
|
112
161
|
export function resolveGlyphs(mode: IconMode): IconGlyphs {
|
|
113
162
|
const resolved = resolveIconMode(mode);
|
|
163
|
+
if (resolved === "unicode") return UNICODE_GLYPHS;
|
|
114
164
|
return resolved === "nerd" ? NERD_GLYPHS : ASCII_GLYPHS;
|
|
115
165
|
}
|
|
116
166
|
|
|
@@ -185,6 +235,6 @@ const RUNTIME_ASCII_SYMBOLS: Record<string, string> = {
|
|
|
185
235
|
};
|
|
186
236
|
|
|
187
237
|
export function runtimeSymbol(name: string, mode: IconMode): string {
|
|
188
|
-
if (resolveIconMode(mode)
|
|
238
|
+
if (resolveIconMode(mode) !== "nerd") return RUNTIME_ASCII_SYMBOLS[name] ?? name;
|
|
189
239
|
return RUNTIME_SYMBOLS[name] ?? "";
|
|
190
240
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* thinking peek preview, rounded tool frames, and turn telemetry.
|
|
5
5
|
*
|
|
6
6
|
* Integrated from:
|
|
7
|
-
* - pi-open-tui (v0.3.
|
|
7
|
+
* - pi-open-tui (v0.3.11, MIT, by OldSun, https://github.com/OldSuns/pi-open-tui)
|
|
8
8
|
* - pi-rounded-tools (v0.1.3, MIT, by OrionPax, https://github.com/orionpax1997/pi-rounded-tools)
|
|
9
9
|
*
|
|
10
10
|
* Security audit notes:
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { type OpenTuiConfig, DEFAULT_CONFIG, ensureConfigExists, loadConfig, saveConfig } from "./config.ts";
|
|
19
19
|
import { installEditor } from "./editor.ts";
|
|
20
|
-
import { installFooter
|
|
20
|
+
import { installFooter } from "./footer.ts";
|
|
21
21
|
import { installHeader } from "./header.ts";
|
|
22
22
|
import { emptyGitStatus, readGitStatus } from "./git.ts";
|
|
23
23
|
import { readRuntimeInfo } from "./runtime.ts";
|
|
@@ -71,7 +71,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
71
71
|
let requestFooterRender: (() => void) | undefined;
|
|
72
72
|
let workingTimer: ReturnType<typeof setInterval> | undefined;
|
|
73
73
|
let cleanupHeader: (() => void) | undefined;
|
|
74
|
-
let cleanupFooter:
|
|
74
|
+
let cleanupFooter: (() => void) | undefined;
|
|
75
75
|
let editor: ReturnType<typeof installEditor> | undefined;
|
|
76
76
|
let pendingUiChange: PendingUiChange | undefined;
|
|
77
77
|
|
|
@@ -164,12 +164,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
164
164
|
},
|
|
165
165
|
},
|
|
166
166
|
);
|
|
167
|
-
cleanupFooter = footer;
|
|
167
|
+
cleanupFooter = footer.cleanup;
|
|
168
168
|
editor = installEditor(
|
|
169
169
|
pi,
|
|
170
170
|
ctx,
|
|
171
171
|
config.cursorStyle,
|
|
172
|
-
config.fullscreen.wheelScrollLines,
|
|
173
172
|
{
|
|
174
173
|
enabled: () => config.inlineFooter,
|
|
175
174
|
render: footer.renderInline,
|
|
@@ -183,7 +182,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
182
|
if (!isTuiContext(ctx)) return;
|
|
184
183
|
if (active) {
|
|
185
184
|
cleanupHeader?.();
|
|
186
|
-
cleanupFooter?.
|
|
185
|
+
cleanupFooter?.();
|
|
187
186
|
editor?.cleanup();
|
|
188
187
|
cleanupHeader = undefined;
|
|
189
188
|
cleanupFooter = undefined;
|
|
@@ -423,7 +422,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
423
422
|
const wasEnabled = config.enabled;
|
|
424
423
|
const wasRoundedTools = config.roundedTools;
|
|
425
424
|
const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
|
|
426
|
-
const wheelScrollLinesChanged = config.fullscreen.wheelScrollLines !== newConfig.fullscreen.wheelScrollLines;
|
|
427
425
|
const thinkingPeekLinesChanged = config.thinkingPeek.lines !== newConfig.thinkingPeek.lines;
|
|
428
426
|
saveConfig(newConfig);
|
|
429
427
|
config = newConfig;
|
|
@@ -436,9 +434,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
436
434
|
if (cursorStyleChanged && active && editor) {
|
|
437
435
|
editor.setCursorStyle(newConfig.cursorStyle);
|
|
438
436
|
}
|
|
439
|
-
if (wheelScrollLinesChanged && active && editor) {
|
|
440
|
-
editor.setWheelScrollLines(newConfig.fullscreen.wheelScrollLines);
|
|
441
|
-
}
|
|
442
437
|
if (lastCtx) {
|
|
443
438
|
pendingUiChange = getPendingUiChange(newConfig.enabled, active);
|
|
444
439
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Merged 2026-08 from the standalone local extension rounded-tools.ts (a
|
|
5
5
|
* localized copy of npm:pi-rounded-tools@0.1.3, MIT, by OrionPax,
|
|
6
6
|
* https://github.com/orionpax1997/pi-rounded-tools), integrated into pi-tui
|
|
7
|
-
* (based on https://github.com/OldSuns/pi-open-tui v0.3.
|
|
7
|
+
* (based on https://github.com/OldSuns/pi-open-tui v0.3.11, MIT, by
|
|
8
8
|
* OldSun) and gated behind `roundedTools` in pi-tui config
|
|
9
9
|
* (~/.pi/agent/pi-tui.json, default on).
|
|
10
10
|
*
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
|
-
Input,
|
|
5
4
|
Key,
|
|
6
5
|
matchesKey,
|
|
7
6
|
SelectList,
|
|
@@ -11,10 +10,6 @@ import {
|
|
|
11
10
|
Text,
|
|
12
11
|
} from "@earendil-works/pi-tui";
|
|
13
12
|
import type { CursorStyle, IconMode, OpenTuiConfig, SettingsLanguage, ThinkingPeekLines } from "./config.ts";
|
|
14
|
-
import {
|
|
15
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
16
|
-
normalizeFullscreenWheelScrollLines,
|
|
17
|
-
} from "./fullscreen-scroll.ts";
|
|
18
13
|
|
|
19
14
|
interface SettingItem {
|
|
20
15
|
id: string;
|
|
@@ -30,14 +25,13 @@ const COPY = {
|
|
|
30
25
|
en: {
|
|
31
26
|
title: "Pi TUI Settings",
|
|
32
27
|
tabs: { features: "General", icons: "Appearance", segments: "Footer", telemetry: "Telemetry" },
|
|
33
|
-
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change ·
|
|
28
|
+
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
|
|
34
29
|
labels: {
|
|
35
30
|
enabled: "Enabled",
|
|
36
31
|
roundedTools: "Rounded tool frames",
|
|
37
32
|
inlineFooter: "Inline footer",
|
|
38
33
|
thinkingPeek: "Thinking peek",
|
|
39
34
|
language: "Language",
|
|
40
|
-
wheelScrollLines: "Mouse wheel speed",
|
|
41
35
|
cursorStyle: "Cursor style",
|
|
42
36
|
iconMode: "Icon mode",
|
|
43
37
|
cwd: "CWD",
|
|
@@ -62,23 +56,20 @@ const COPY = {
|
|
|
62
56
|
off: "Off",
|
|
63
57
|
thinkingPeek: { off: "Off", one: "1 line", two: "2 lines" },
|
|
64
58
|
languages: { en: "English", zh: "简体中文" },
|
|
65
|
-
wheelLines: (count: number) => `${count} ${count === 1 ? "line" : "lines"} / notch`,
|
|
66
|
-
wheelPrompt: (count: number) => `Wheel scroll lines per notch, 1-10 (current: ${count}). Enter: apply · Esc: cancel`,
|
|
67
59
|
cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
|
|
68
|
-
icons: { auto: "Auto", nerd: "Nerd", ascii: "ASCII" },
|
|
60
|
+
icons: { auto: "Auto", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
69
61
|
},
|
|
70
62
|
},
|
|
71
63
|
zh: {
|
|
72
64
|
title: "Pi TUI 设置",
|
|
73
65
|
tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
|
|
74
|
-
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 ·
|
|
66
|
+
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
|
|
75
67
|
labels: {
|
|
76
68
|
enabled: "启用",
|
|
77
69
|
roundedTools: "圆角工具边框",
|
|
78
70
|
inlineFooter: "内联底栏",
|
|
79
71
|
thinkingPeek: "思考预览",
|
|
80
72
|
language: "语言",
|
|
81
|
-
wheelScrollLines: "鼠标滚轮速度",
|
|
82
73
|
cursorStyle: "光标样式",
|
|
83
74
|
iconMode: "图标模式",
|
|
84
75
|
cwd: "当前目录",
|
|
@@ -103,10 +94,8 @@ const COPY = {
|
|
|
103
94
|
off: "关闭",
|
|
104
95
|
thinkingPeek: { off: "关闭", one: "单行", two: "双行" },
|
|
105
96
|
languages: { en: "English", zh: "简体中文" },
|
|
106
|
-
wheelLines: (count: number) => `每格 ${count} 行`,
|
|
107
|
-
wheelPrompt: (count: number) => `滚轮每格滚动行数(当前 ${count},范围 1-10),输入后 Enter 应用 · Esc 取消`,
|
|
108
97
|
cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
|
|
109
|
-
icons: { auto: "自动", nerd: "Nerd", ascii: "ASCII" },
|
|
98
|
+
icons: { auto: "自动", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
110
99
|
},
|
|
111
100
|
},
|
|
112
101
|
} as const;
|
|
@@ -129,7 +118,7 @@ function toggleSetting(config: OpenTuiConfig, key: keyof OpenTuiConfig["footerSe
|
|
|
129
118
|
}
|
|
130
119
|
|
|
131
120
|
function cycleIconMode(config: OpenTuiConfig): OpenTuiConfig {
|
|
132
|
-
const order: IconMode[] = ["auto", "nerd", "ascii"];
|
|
121
|
+
const order: IconMode[] = ["auto", "nerd", "unicode", "ascii"];
|
|
133
122
|
const currentIdx = order.indexOf(config.icons.mode);
|
|
134
123
|
const next = order[(currentIdx + 1) % order.length]!;
|
|
135
124
|
return { ...config, icons: { mode: next } };
|
|
@@ -150,19 +139,6 @@ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
|
|
|
150
139
|
return { ...config, cursorStyle: next };
|
|
151
140
|
}
|
|
152
141
|
|
|
153
|
-
function setWheelScrollLines(config: OpenTuiConfig, raw: string): OpenTuiConfig | undefined {
|
|
154
|
-
if (!/^\d+$/.test(raw)) return undefined;
|
|
155
|
-
const parsed = Number(raw);
|
|
156
|
-
const bounded = Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
|
|
157
|
-
return {
|
|
158
|
-
...config,
|
|
159
|
-
fullscreen: {
|
|
160
|
-
...config.fullscreen,
|
|
161
|
-
wheelScrollLines: normalizeFullscreenWheelScrollLines(bounded, DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES),
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
142
|
function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
|
|
167
143
|
return {
|
|
168
144
|
...config,
|
|
@@ -183,14 +159,9 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
|
|
|
183
159
|
return [
|
|
184
160
|
{ id: "enabled", label: copy.labels.enabled, currentValue: flag(config.enabled) },
|
|
185
161
|
{ id: "roundedTools", label: copy.labels.roundedTools, currentValue: flag(config.roundedTools) },
|
|
186
|
-
{ id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
|
|
187
162
|
{ id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
|
|
188
|
-
{
|
|
189
|
-
id: "wheelScrollLines",
|
|
190
|
-
label: copy.labels.wheelScrollLines,
|
|
191
|
-
currentValue: copy.values.wheelLines(config.fullscreen.wheelScrollLines),
|
|
192
|
-
},
|
|
193
163
|
{ id: "thinkingPeek", label: copy.labels.thinkingPeek, currentValue: formatThinkingPeekLines(config.thinkingPeek.lines, copy) },
|
|
164
|
+
{ id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
|
|
194
165
|
];
|
|
195
166
|
}
|
|
196
167
|
|
|
@@ -275,23 +246,6 @@ interface SettingsUiHandle {
|
|
|
275
246
|
handleInput: (data: string) => void;
|
|
276
247
|
}
|
|
277
248
|
|
|
278
|
-
function insertComponentAfter(list: Component, child: Component, index: () => number): Component {
|
|
279
|
-
// SelectList renders one line per item; splice the editor into that output.
|
|
280
|
-
return {
|
|
281
|
-
render(width: number): string[] {
|
|
282
|
-
const lines = list.render(width);
|
|
283
|
-
const insertAt = index();
|
|
284
|
-
if (insertAt < 0 || insertAt >= lines.length) return lines;
|
|
285
|
-
const childLines = child.render(width);
|
|
286
|
-
return [...lines.slice(0, insertAt + 1), ...childLines, ...lines.slice(insertAt + 1)];
|
|
287
|
-
},
|
|
288
|
-
invalidate(): void {
|
|
289
|
-
list.invalidate();
|
|
290
|
-
child.invalidate();
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
249
|
class SettingsUi implements SettingsUiHandle {
|
|
296
250
|
private tab: Tab = "features";
|
|
297
251
|
private config: OpenTuiConfig;
|
|
@@ -304,7 +258,6 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
304
258
|
private cachedWidth: number | undefined;
|
|
305
259
|
private cachedLines: string[] | undefined;
|
|
306
260
|
private compact = false;
|
|
307
|
-
private wheelInput: Input | undefined;
|
|
308
261
|
|
|
309
262
|
constructor(
|
|
310
263
|
theme: Theme,
|
|
@@ -329,35 +282,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
329
282
|
|
|
330
283
|
private applySetting(itemId: string): void {
|
|
331
284
|
this.selectedItemByTab[this.tab] = itemId;
|
|
332
|
-
if (this.tab === "features" && itemId === "wheelScrollLines") {
|
|
333
|
-
this.openWheelInput();
|
|
334
|
-
this.invalidate();
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
285
|
this.config = handleSettingChange(this.tab, itemId, this.config);
|
|
338
286
|
this.onChange(this.config);
|
|
339
287
|
this.rebuild(itemId);
|
|
340
288
|
}
|
|
341
289
|
|
|
342
|
-
private openWheelInput(): void {
|
|
343
|
-
const input = new Input();
|
|
344
|
-
input.onSubmit = (value) => {
|
|
345
|
-
const next = setWheelScrollLines(this.config, value);
|
|
346
|
-
this.wheelInput = undefined;
|
|
347
|
-
if (next) {
|
|
348
|
-
this.config = next;
|
|
349
|
-
this.onChange(this.config);
|
|
350
|
-
}
|
|
351
|
-
this.rebuild("wheelScrollLines");
|
|
352
|
-
};
|
|
353
|
-
input.onEscape = () => {
|
|
354
|
-
this.wheelInput = undefined;
|
|
355
|
-
this.rebuild("wheelScrollLines");
|
|
356
|
-
};
|
|
357
|
-
this.wheelInput = input;
|
|
358
|
-
this.rebuild("wheelScrollLines");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
290
|
private switchTab(offset: number): void {
|
|
362
291
|
const idx = TABS.indexOf(this.tab);
|
|
363
292
|
this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
|
|
@@ -377,17 +306,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
377
306
|
this.container.addChild(new Text(tabBar, 1, 0));
|
|
378
307
|
this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
|
|
379
308
|
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
label: editing
|
|
386
|
-
? (this.compact ? `${item.label}:` : item.label)
|
|
387
|
-
: (this.compact ? `${item.label}: ${item.currentValue}` : item.label),
|
|
388
|
-
description: editing || this.compact ? undefined : item.currentValue,
|
|
389
|
-
} as SelectItem;
|
|
390
|
-
});
|
|
309
|
+
const items = buildItems(this.tab, this.config).map((item) => ({
|
|
310
|
+
value: item.id,
|
|
311
|
+
label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
|
|
312
|
+
description: this.compact ? undefined : item.currentValue,
|
|
313
|
+
} as SelectItem));
|
|
391
314
|
this.selectList = new SelectList(items, Math.min(items.length, 12), {
|
|
392
315
|
selectedPrefix: (t) => this.theme.fg("accent", t),
|
|
393
316
|
selectedText: (t) => this.theme.fg("accent", t),
|
|
@@ -409,31 +332,13 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
409
332
|
this.selectList.onCancel = () => {
|
|
410
333
|
this.onClose();
|
|
411
334
|
};
|
|
412
|
-
|
|
413
|
-
this.wheelInput!.focused = true;
|
|
414
|
-
const wheelInputGroup = new Box(4, 0);
|
|
415
|
-
wheelInputGroup.addChild(new Text(
|
|
416
|
-
this.theme.fg("muted", copy.values.wheelPrompt(this.config.fullscreen.wheelScrollLines)),
|
|
417
|
-
0,
|
|
418
|
-
0,
|
|
419
|
-
));
|
|
420
|
-
wheelInputGroup.addChild(this.wheelInput!);
|
|
421
|
-
const selectedIndex = () => items.findIndex((item) => item.value === this.selectList.getSelectedItem()?.value);
|
|
422
|
-
this.container.addChild(insertComponentAfter(this.selectList, wheelInputGroup, selectedIndex));
|
|
423
|
-
} else {
|
|
424
|
-
this.container.addChild(this.selectList);
|
|
425
|
-
}
|
|
335
|
+
this.container.addChild(this.selectList);
|
|
426
336
|
|
|
427
337
|
this.cachedWidth = undefined;
|
|
428
338
|
this.cachedLines = undefined;
|
|
429
339
|
}
|
|
430
340
|
|
|
431
341
|
handleInput(data: string): void {
|
|
432
|
-
if (this.wheelInput && this.tab === "features") {
|
|
433
|
-
this.wheelInput.handleInput(data);
|
|
434
|
-
this.invalidate();
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
342
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
|
438
343
|
this.switchTab(1);
|
|
439
344
|
this.invalidate();
|
|
@@ -484,7 +389,7 @@ export function registerSettingsCommand(
|
|
|
484
389
|
onOverlayClosed?: () => void;
|
|
485
390
|
},
|
|
486
391
|
): void {
|
|
487
|
-
|
|
392
|
+
pi.registerCommand("pi-tui", {
|
|
488
393
|
description: "Open the Pi TUI settings UI",
|
|
489
394
|
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
490
395
|
if (!ctx.hasUI) return;
|
|
@@ -509,7 +414,5 @@ export function registerSettingsCommand(
|
|
|
509
414
|
// cannot strand the overlay without keyboard input.
|
|
510
415
|
hooks.onOverlayClosed?.();
|
|
511
416
|
},
|
|
512
|
-
};
|
|
513
|
-
pi.registerCommand("pi-tui", commandDef);
|
|
514
|
-
pi.registerCommand("open-tui", commandDef);
|
|
417
|
+
});
|
|
515
418
|
}
|
package/extensions/utils.ts
CHANGED
|
@@ -296,6 +296,28 @@ export function sanitizeStatus(text: string): string {
|
|
|
296
296
|
.trim();
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// One status token is an SGR sequence, another escape sequence, or one control
|
|
300
|
+
// character. SGR carries colours. Everything else moves the cursor or rewrites
|
|
301
|
+
// the terminal, so it must never reach the footer row.
|
|
302
|
+
const STATUS_TOKEN = /(?:\x1b\[|\x9b)[0-?]*[ -/]*m|(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]|(?:\x1bP|\x90|\x1bX|\x98|\x1b\^|\x9e|\x1b_|\x9f)[\s\S]*?(?:\x1b\\|\x9c|$)|(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[@-Z\\_]|[\u0000-\u001f\u007f-\u009f]/g;
|
|
303
|
+
const SGR_TOKEN = /^(?:\x1b\[|\x9b)[0-9;:]*m$/;
|
|
304
|
+
const SGR_SEQUENCE = /(?:\x1b\[|\x9b)[0-9;:]*m/;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Sanitize a status text the same way {@link sanitizeStatus} does, but keep the
|
|
308
|
+
* SGR sequences the extension set. Extensions colour their own status text with
|
|
309
|
+
* `ctx.ui.theme.fg()`. Stripping those codes renders every status grey.
|
|
310
|
+
*/
|
|
311
|
+
export function sanitizeStatusPreservingStyles(text: string): string {
|
|
312
|
+
const cleaned = text.replace(STATUS_TOKEN, (token) => (SGR_TOKEN.test(token) ? token : " "));
|
|
313
|
+
return cleaned.replace(/ {2,}/g, " ").trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** True when the text carries SGR colour or style sequences. */
|
|
317
|
+
export function hasAnsiStyles(text: string): boolean {
|
|
318
|
+
return SGR_SEQUENCE.test(text);
|
|
319
|
+
}
|
|
320
|
+
|
|
299
321
|
export function formatThinkingLabel(level: string): string {
|
|
300
322
|
if (level === "off") return "thinking off";
|
|
301
323
|
return `${level} effort`;
|
package/package.json
CHANGED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { TUI } from "@earendil-works/pi-tui";
|
|
2
|
-
|
|
3
|
-
export const MIN_FULLSCREEN_WHEEL_SCROLL_LINES = 1;
|
|
4
|
-
export const MAX_FULLSCREEN_WHEEL_SCROLL_LINES = 10;
|
|
5
|
-
export const DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES = 4;
|
|
6
|
-
|
|
7
|
-
export function normalizeFullscreenWheelScrollLines(
|
|
8
|
-
value: unknown,
|
|
9
|
-
fallback = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
10
|
-
): number {
|
|
11
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
12
|
-
return Math.min(
|
|
13
|
-
MAX_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
14
|
-
Math.max(MIN_FULLSCREEN_WHEEL_SCROLL_LINES, Math.floor(value)),
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type FullscreenWheelTui = TUI & { wheelScrollLines?: unknown };
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Pi 0.84.2 keeps this constructor option in a private runtime field. Remove
|
|
22
|
-
* this shim once Pi exposes a public setter; incompatible versions are no-ops.
|
|
23
|
-
*/
|
|
24
|
-
export function applyFullscreenWheelScrollLines(tui: TUI, value: number): boolean {
|
|
25
|
-
try {
|
|
26
|
-
const fullscreenTui = tui as FullscreenWheelTui;
|
|
27
|
-
if (fullscreenTui.mode !== "fullscreen" || typeof fullscreenTui.wheelScrollLines !== "number") {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return Reflect.set(fullscreenTui, "wheelScrollLines", normalizeFullscreenWheelScrollLines(value));
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|