@groeponline/pi-wishcraft 0.20.0 → 0.22.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/CHANGELOG.md +17 -0
- package/README.md +83 -0
- package/ROADMAP.md +18 -20
- package/bash-mode/editor-ghost.ts +63 -0
- package/bash-mode/editor-history.ts +81 -0
- package/bash-mode/editor-input.ts +35 -0
- package/bash-mode/editor.ts +16 -134
- package/bash-mode/shell-session.ts +12 -15
- package/bash-mode/types.ts +2 -0
- package/docs/bash-mode.md +46 -0
- package/docs/commands.md +122 -0
- package/docs/configuration.md +307 -0
- package/docs/index.md +18 -0
- package/docs/segments.md +168 -0
- package/docs/skill-manager.md +7 -0
- package/docs/stash-and-shortcuts.md +64 -0
- package/docs/working-vibes.md +71 -0
- package/package.json +2 -1
- package/queue/store.ts +72 -3
- package/src/config/custom-items.ts +45 -0
- package/src/config/parse.ts +19 -3
- package/src/config/powerline-config.ts +5 -0
- package/src/config/primitives.ts +13 -1
- package/src/config/segment-options.ts +29 -24
- package/src/config/settings-patch.ts +23 -2
- package/src/config/types.ts +32 -17
- package/src/extension/commands/bash-mode-actions.ts +1 -0
- package/src/extension/commands/commands.ts +36 -18
- package/src/extension/commands/powerline-completions.ts +8 -0
- package/src/extension/commands/powerline-doctor.ts +247 -0
- package/src/extension/commands/powerline-export.ts +94 -0
- package/src/extension/commands/queue-commands.ts +29 -1
- package/src/extension/commands/vibe-command.ts +49 -1
- package/src/extension/core/constants.ts +1 -0
- package/src/extension/core/segment-context.ts +38 -2
- package/src/extension/core/state.ts +5 -1
- package/src/extension/core/status-export.ts +68 -0
- package/src/extension/core/types.ts +4 -0
- package/src/extension/hooks/repairs.ts +89 -16
- package/src/extension/session/activate.ts +1 -2
- package/src/extension/session/cost-alert.ts +36 -0
- package/src/extension/session/session-lifecycle.ts +82 -1
- package/src/extension/settings/settings-io.ts +23 -17
- package/src/extension/settings/wishcraft-config.ts +2 -1
- package/src/extension/shortcuts/shortcuts-config.ts +5 -0
- package/src/extension/ui/layout.ts +1 -1
- package/src/extension/ui/menu-items.ts +379 -0
- package/src/extension/ui/menu-views.ts +377 -191
- package/src/extension/ui/overlay-chrome.ts +163 -0
- package/src/extension/ui/powerline-menu-view.ts +2 -4
- package/src/extension/ui/token-overlays.ts +82 -0
- package/src/extension/welcome/welcome-integration.ts +5 -9
- package/src/segments/registry.ts +100 -39
- package/src/segments/system.ts +204 -64
- package/src/segments/usage.ts +10 -4
- package/src/usage/ledger.ts +6 -0
- package/src/usage/token-budget.ts +57 -0
- package/src/usage/tps-ring.ts +151 -0
- package/src/usage/usage-store.ts +351 -0
- package/src/welcome/banner.ts +2 -1
- package/src/welcome/index.ts +3 -1
- package/src/welcome/overlay.ts +2 -0
- package/src/welcome/renderer.ts +52 -10
- package/src/welcome/types.ts +1 -2
- package/src/welcome/whats-new.ts +156 -0
- package/src/welcome/widgets/index.ts +3 -0
- package/src/welcome/widgets/whats-new-widget.ts +19 -0
- package/src/working-vibes/generate.ts +144 -58
- package/src/working-vibes/index.ts +10 -2
- package/src/working-vibes/storage.ts +5 -0
|
@@ -1,107 +1,78 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
+
matchesKey,
|
|
3
4
|
type SelectItem,
|
|
4
5
|
SelectList,
|
|
5
6
|
truncateToWidth,
|
|
6
7
|
} from "@earendil-works/pi-tui";
|
|
7
8
|
import { execSync } from "node:child_process";
|
|
8
9
|
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import type {
|
|
11
|
+
StatusLinePreset,
|
|
12
|
+
StatusLineSegmentId,
|
|
13
|
+
StatusLineSeparatorStyle,
|
|
14
|
+
} from "../../config/types.ts";
|
|
15
|
+
import { getPreset, PRESETS, registerCustomPresets } from "../../config/presets.ts";
|
|
16
|
+
import { SEPARATOR_STYLES } from "../../config/primitives.ts";
|
|
12
17
|
import {
|
|
13
|
-
|
|
18
|
+
countListeningPorts,
|
|
19
|
+
listOpenPortProcesses,
|
|
20
|
+
sanitizeSshHost,
|
|
21
|
+
} from "../../segments/system.ts";
|
|
22
|
+
import {
|
|
23
|
+
writePowerlineCustomPresetSetting,
|
|
24
|
+
writePowerlineOptionSetting,
|
|
14
25
|
writePowerlinePresetSetting,
|
|
15
26
|
} from "../settings/settings-io.ts";
|
|
16
|
-
import { renderSegmentWithWidth } from "./layout.ts";
|
|
17
27
|
import {
|
|
18
28
|
buildSegmentContext,
|
|
19
29
|
requestImmediateStatusRender,
|
|
30
|
+
resetLayoutCache,
|
|
20
31
|
} from "../core/segment-context.ts";
|
|
21
32
|
import { config, setConfig } from "../core/state.ts";
|
|
33
|
+
import {
|
|
34
|
+
formatPortsStatusValue,
|
|
35
|
+
publishPowerlineStatuses,
|
|
36
|
+
} from "../core/status-export.ts";
|
|
22
37
|
import type { RuntimeState } from "../core/types.ts";
|
|
38
|
+
import {
|
|
39
|
+
buildConfigureItems,
|
|
40
|
+
buildCustomPresetDef,
|
|
41
|
+
buildCustomPresetSegmentIds,
|
|
42
|
+
buildPresetEditorAddItems,
|
|
43
|
+
buildSegmentDetailLines,
|
|
44
|
+
buildSegmentItems,
|
|
45
|
+
collectSegmentIds,
|
|
46
|
+
configureItemsToSelectItems,
|
|
47
|
+
segmentItemsToSelectItems,
|
|
48
|
+
selectIndexForValue,
|
|
49
|
+
validatePresetName,
|
|
50
|
+
} from "./menu-items.ts";
|
|
51
|
+
import { overlaySelectListTheme, showSelectOverlay } from "./overlay-chrome.ts";
|
|
52
|
+
import { showTpsOverlay } from "./token-overlays.ts";
|
|
23
53
|
|
|
24
|
-
export
|
|
25
|
-
return {
|
|
26
|
-
selectedPrefix: (text: string) => theme.fg("accent", text),
|
|
27
|
-
selectedText: (text: string) => theme.fg("accent", text),
|
|
28
|
-
description: (text: string) => theme.fg("muted", text),
|
|
29
|
-
scrollInfo: (text: string) => theme.fg("dim", text),
|
|
30
|
-
noMatch: (text: string) => theme.fg("warning", text),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function showSelectOverlay(
|
|
35
|
-
ctx: any,
|
|
36
|
-
title: string,
|
|
37
|
-
hint: string,
|
|
38
|
-
items: SelectItem[],
|
|
39
|
-
maxVisible: number,
|
|
40
|
-
): Promise<SelectItem | null> {
|
|
41
|
-
return ctx.ui.custom(
|
|
42
|
-
(
|
|
43
|
-
tui: any,
|
|
44
|
-
theme: Theme,
|
|
45
|
-
_keybindings: any,
|
|
46
|
-
done: (result: SelectItem | null) => void,
|
|
47
|
-
) => {
|
|
48
|
-
const selectList = new SelectList(
|
|
49
|
-
items,
|
|
50
|
-
maxVisible,
|
|
51
|
-
overlaySelectListTheme(theme),
|
|
52
|
-
);
|
|
53
|
-
const border = (text: string) => theme.fg("dim", text);
|
|
54
|
-
const wrapRow = (text: string, innerWidth: number): string => {
|
|
55
|
-
return `${border("│")}${truncateToWidth(text, innerWidth, "…", true)}${border("│")}`;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
selectList.onSelect = (item) => done(item);
|
|
59
|
-
selectList.onCancel = () => done(null);
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
render: (width: number) => {
|
|
63
|
-
const innerWidth = Math.max(1, width - 2);
|
|
64
|
-
const lines: string[] = [];
|
|
65
|
-
|
|
66
|
-
lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
67
|
-
lines.push(
|
|
68
|
-
wrapRow(theme.fg("accent", theme.bold(title)), innerWidth),
|
|
69
|
-
);
|
|
70
|
-
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
71
|
-
|
|
72
|
-
for (const line of selectList.render(innerWidth)) {
|
|
73
|
-
lines.push(wrapRow(line, innerWidth));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
77
|
-
lines.push(wrapRow(theme.fg("dim", hint), innerWidth));
|
|
78
|
-
lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
54
|
+
export { overlaySelectListTheme, showSelectOverlay } from "./overlay-chrome.ts";
|
|
79
55
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
invalidate: () => selectList.invalidate(),
|
|
83
|
-
handleInput: (data: string) => {
|
|
84
|
-
selectList.handleInput(data);
|
|
85
|
-
tui.requestRender();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
overlay: true,
|
|
91
|
-
overlayOptions: () => ({
|
|
92
|
-
verticalAlign: "center",
|
|
93
|
-
horizontalAlign: "center",
|
|
94
|
-
}),
|
|
95
|
-
},
|
|
96
|
-
);
|
|
97
|
-
}
|
|
56
|
+
/** How often the open segment navigator re-reads live segment state. */
|
|
57
|
+
const SEGMENT_NAVIGATOR_REFRESH_MS = 1000;
|
|
98
58
|
|
|
99
59
|
/** Full open-ports list as a scrollable overlay (the Info view). */
|
|
100
60
|
export async function showOpenPortsList(ctx: any): Promise<void> {
|
|
101
61
|
try {
|
|
102
62
|
const includeUdp = config.segmentOptions?.openPorts?.includeUdp === true;
|
|
103
|
-
const
|
|
104
|
-
|
|
63
|
+
const host = config.segmentOptions?.openPorts?.host;
|
|
64
|
+
if (host && !sanitizeSshHost(host)) {
|
|
65
|
+
ctx.ui.notify(`Invalid open-ports host: ${host}`, "error");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const proto = includeUdp ? "-tulnp" : "-tlnp";
|
|
69
|
+
const command = host
|
|
70
|
+
? `ssh -o ConnectTimeout=3 -o BatchMode=yes ${host} "ss ${proto} 2>/dev/null" 2>/dev/null`
|
|
71
|
+
: `ss ${proto} 2>/dev/null`;
|
|
72
|
+
const stdout = execSync(command, { encoding: "utf8" });
|
|
73
|
+
publishPowerlineStatuses(ctx, {
|
|
74
|
+
ports: formatPortsStatusValue(countListeningPorts(includeUdp, host)),
|
|
75
|
+
});
|
|
105
76
|
const lines = stdout
|
|
106
77
|
.split("\n")
|
|
107
78
|
.map((l) => l.trim())
|
|
@@ -118,7 +89,7 @@ export async function showOpenPortsList(ctx: any): Promise<void> {
|
|
|
118
89
|
}));
|
|
119
90
|
const picked = await showSelectOverlay(
|
|
120
91
|
ctx,
|
|
121
|
-
"Open ports",
|
|
92
|
+
host ? `Open ports · ${host}` : "Open ports",
|
|
122
93
|
"↑↓ navigate · enter copy · esc close",
|
|
123
94
|
items,
|
|
124
95
|
Math.min(items.length, 24),
|
|
@@ -132,47 +103,37 @@ export async function showOpenPortsList(ctx: any): Promise<void> {
|
|
|
132
103
|
}
|
|
133
104
|
}
|
|
134
105
|
|
|
135
|
-
/** Alle segment-ids van de huidige preset + custom items (voor menu's). */
|
|
136
|
-
function mergedIds(): string[] {
|
|
137
|
-
const presetDef = getPreset(config.preset);
|
|
138
|
-
const merged = mergeSegmentsWithCustomItems(presetDef, config.customItems, {
|
|
139
|
-
layout: config.layout,
|
|
140
|
-
disabledSegments: [],
|
|
141
|
-
});
|
|
142
|
-
return [
|
|
143
|
-
...new Set([
|
|
144
|
-
...merged.leftSegments,
|
|
145
|
-
...merged.rightSegments,
|
|
146
|
-
...merged.secondarySegments,
|
|
147
|
-
]),
|
|
148
|
-
] as string[];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
106
|
/** Configure sub-menu: preset, TPS value/label, ports UDP, segment labels. */
|
|
152
107
|
export async function configurePowerline(
|
|
153
108
|
rt: RuntimeState,
|
|
154
109
|
ctx: any,
|
|
155
110
|
): Promise<void> {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
]);
|
|
111
|
+
const picked = await showSelectOverlay(
|
|
112
|
+
ctx,
|
|
113
|
+
"Powerline · configure",
|
|
114
|
+
"↑↓ navigate · enter select · esc back",
|
|
115
|
+
configureItemsToSelectItems(),
|
|
116
|
+
Math.min(buildConfigureItems().length, 12),
|
|
117
|
+
);
|
|
118
|
+
const choice = picked?.value;
|
|
165
119
|
if (!choice) return;
|
|
166
120
|
if (choice === "Change preset") {
|
|
167
121
|
const names = Object.keys(PRESETS) as StatusLinePreset[];
|
|
168
|
-
const picked = await
|
|
122
|
+
const picked = await showSelectOverlay(
|
|
123
|
+
ctx,
|
|
124
|
+
"Preset",
|
|
125
|
+
"↑↓ navigate · enter apply · esc back",
|
|
126
|
+
names.map((name) => ({ label: name, value: name })),
|
|
127
|
+
Math.min(names.length, 12),
|
|
128
|
+
);
|
|
169
129
|
if (picked) {
|
|
170
|
-
setConfig({ ...config, preset: picked as StatusLinePreset });
|
|
130
|
+
setConfig({ ...config, preset: picked.value as StatusLinePreset });
|
|
171
131
|
writePowerlinePresetSetting(
|
|
172
|
-
picked as StatusLinePreset,
|
|
132
|
+
picked.value as StatusLinePreset,
|
|
173
133
|
ctx.cwd ?? process.cwd(),
|
|
174
134
|
);
|
|
175
|
-
ctx
|
|
135
|
+
publishPowerlineStatuses(ctx, { preset: picked.value });
|
|
136
|
+
ctx.ui.notify(`Preset: ${picked.value} (saved)`, "info");
|
|
176
137
|
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
177
138
|
}
|
|
178
139
|
return;
|
|
@@ -184,6 +145,7 @@ export async function configurePowerline(
|
|
|
184
145
|
);
|
|
185
146
|
if (val && val.trim()) {
|
|
186
147
|
process.env.POWERLINE_TPS = val.trim();
|
|
148
|
+
publishPowerlineStatuses(ctx, { tps: val.trim() });
|
|
187
149
|
ctx.ui.notify(`TPS override set: ${val.trim()}`, "info");
|
|
188
150
|
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
189
151
|
}
|
|
@@ -191,23 +153,59 @@ export async function configurePowerline(
|
|
|
191
153
|
}
|
|
192
154
|
if (choice === "Clear TPS override (use live)") {
|
|
193
155
|
delete process.env.POWERLINE_TPS;
|
|
156
|
+
publishPowerlineStatuses(ctx, { tps: undefined });
|
|
194
157
|
ctx.ui.notify("TPS override cleared (live rate)", "info");
|
|
195
158
|
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
196
159
|
return;
|
|
197
160
|
}
|
|
198
161
|
if (choice === "Toggle UDP in open-ports") {
|
|
199
162
|
const now = config.segmentOptions?.openPorts?.includeUdp === true;
|
|
163
|
+
const host = config.segmentOptions?.openPorts?.host;
|
|
200
164
|
setConfig({
|
|
201
165
|
...config,
|
|
202
166
|
segmentOptions: {
|
|
203
167
|
...config.segmentOptions,
|
|
204
|
-
openPorts: { includeUdp: !now },
|
|
168
|
+
openPorts: { ...config.segmentOptions?.openPorts, includeUdp: !now },
|
|
205
169
|
},
|
|
206
170
|
});
|
|
171
|
+
publishPowerlineStatuses(ctx, {
|
|
172
|
+
ports: formatPortsStatusValue(countListeningPorts(!now, host)),
|
|
173
|
+
});
|
|
207
174
|
ctx.ui.notify(`Open-ports UDP: ${!now ? "on" : "off"}`, "info");
|
|
208
175
|
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
209
176
|
return;
|
|
210
177
|
}
|
|
178
|
+
if (choice === "Toggle segment visibility…") {
|
|
179
|
+
const ids = collectSegmentIds(config);
|
|
180
|
+
const items: SelectItem[] = ids.map((id) => ({
|
|
181
|
+
label: `${config.disabledSegments.includes(id) ? "◌" : "●"} ${id}`,
|
|
182
|
+
value: id,
|
|
183
|
+
}));
|
|
184
|
+
const picked = await showSelectOverlay(
|
|
185
|
+
ctx,
|
|
186
|
+
"Toggle segments",
|
|
187
|
+
"↑↓ navigate · enter toggle · esc close",
|
|
188
|
+
items,
|
|
189
|
+
Math.min(items.length, 20),
|
|
190
|
+
);
|
|
191
|
+
if (!picked) return;
|
|
192
|
+
const id = picked.value as StatusLineSegmentId;
|
|
193
|
+
const next = config.disabledSegments.includes(id)
|
|
194
|
+
? config.disabledSegments.filter((x) => x !== id)
|
|
195
|
+
: [...config.disabledSegments, id];
|
|
196
|
+
setConfig({ ...config, disabledSegments: next });
|
|
197
|
+
const saved = writePowerlineOptionSetting(
|
|
198
|
+
ctx.cwd ?? process.cwd(),
|
|
199
|
+
{ disabledSegments: next },
|
|
200
|
+
config.preset,
|
|
201
|
+
);
|
|
202
|
+
ctx.ui.notify(
|
|
203
|
+
`${next.includes(id) ? "Disabled" : "Enabled"}: ${id}${saved ? " (saved)" : " (not persisted; check settings.json)"}`,
|
|
204
|
+
"info",
|
|
205
|
+
);
|
|
206
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
211
209
|
if (choice === "Set segment label…") {
|
|
212
210
|
const id = await ctx.ui.input("Segment id", "tps");
|
|
213
211
|
if (!id) return;
|
|
@@ -224,28 +222,8 @@ export async function configurePowerline(
|
|
|
224
222
|
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
225
223
|
return;
|
|
226
224
|
}
|
|
227
|
-
if (choice === "
|
|
228
|
-
|
|
229
|
-
...mergedIds(),
|
|
230
|
-
];
|
|
231
|
-
const names = ids.map(
|
|
232
|
-
(id) =>
|
|
233
|
-
`${config.disabledSegments.includes(id as any) ? "[ ]" : "[x]"} ${id}`,
|
|
234
|
-
);
|
|
235
|
-
const picked = await ctx.ui.select("Segment aan/uit (enter wisselt)", names);
|
|
236
|
-
if (!picked) return;
|
|
237
|
-
const id = picked.replace(/^\[.\] /, "");
|
|
238
|
-
const disabled = new Set(config.disabledSegments);
|
|
239
|
-
if (disabled.has(id)) disabled.delete(id);
|
|
240
|
-
else disabled.add(id);
|
|
241
|
-
const list = [...disabled];
|
|
242
|
-
setConfig({ ...config, disabledSegments: list });
|
|
243
|
-
writePowerlineDisabledSegmentSetting(list, ctx.cwd ?? process.cwd());
|
|
244
|
-
ctx.ui.notify(
|
|
245
|
-
`${id}: ${disabled.has(id) ? "verborgen" : "zichtbaar"} (opgeslagen)`,
|
|
246
|
-
"info",
|
|
247
|
-
);
|
|
248
|
-
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
225
|
+
if (choice === "Build custom preset…") {
|
|
226
|
+
await runPresetEditor(rt, ctx);
|
|
249
227
|
return;
|
|
250
228
|
}
|
|
251
229
|
if (choice === "Show current config") {
|
|
@@ -262,6 +240,109 @@ export async function configurePowerline(
|
|
|
262
240
|
}
|
|
263
241
|
}
|
|
264
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Interactive loop for one preset group (left/right/secondary): pick segments
|
|
245
|
+
* one at a time from the remaining ids, choosing the done sentinel to finish.
|
|
246
|
+
* Returns null when the user cancels with esc.
|
|
247
|
+
*/
|
|
248
|
+
async function pickPresetGroup(
|
|
249
|
+
ctx: any,
|
|
250
|
+
group: string,
|
|
251
|
+
all: readonly StatusLineSegmentId[],
|
|
252
|
+
initial: readonly StatusLineSegmentId[],
|
|
253
|
+
): Promise<StatusLineSegmentId[] | null> {
|
|
254
|
+
const chosen: StatusLineSegmentId[] = [...initial];
|
|
255
|
+
for (;;) {
|
|
256
|
+
const items = buildPresetEditorAddItems(all, chosen);
|
|
257
|
+
const picked = await showSelectOverlay(
|
|
258
|
+
ctx,
|
|
259
|
+
`Preset · ${group} segments`,
|
|
260
|
+
"↑↓ pick · enter add · done to finish · esc cancel",
|
|
261
|
+
items,
|
|
262
|
+
Math.min(items.length, 20),
|
|
263
|
+
);
|
|
264
|
+
if (!picked || picked.value === "__done__") break;
|
|
265
|
+
chosen.push(picked.value as StatusLineSegmentId);
|
|
266
|
+
}
|
|
267
|
+
return chosen;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Build a custom preset interactively and save it to settings + runtime. */
|
|
271
|
+
export async function runPresetEditor(
|
|
272
|
+
rt: RuntimeState,
|
|
273
|
+
ctx: any,
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
const rawName = await ctx.ui.input("Preset name", "");
|
|
276
|
+
if (!rawName) return;
|
|
277
|
+
const name = validatePresetName(rawName);
|
|
278
|
+
if (!name) {
|
|
279
|
+
ctx.ui.notify("Invalid preset name (a-z, 0-9, -, _)", "error");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const baseNames = Object.keys(PRESETS) as StatusLinePreset[];
|
|
284
|
+
const basePick = await showSelectOverlay(
|
|
285
|
+
ctx,
|
|
286
|
+
"Base preset (colors + options)",
|
|
287
|
+
"↑↓ navigate · enter select · esc cancel",
|
|
288
|
+
baseNames.map((name) => ({ label: name, value: name })),
|
|
289
|
+
Math.min(baseNames.length, 12),
|
|
290
|
+
);
|
|
291
|
+
if (!basePick) return;
|
|
292
|
+
const base = getPreset(basePick.value as StatusLinePreset);
|
|
293
|
+
|
|
294
|
+
const all = buildCustomPresetSegmentIds(config);
|
|
295
|
+
const left = await pickPresetGroup(ctx, "left", all, base.leftSegments);
|
|
296
|
+
if (!left) return;
|
|
297
|
+
const right = await pickPresetGroup(ctx, "right", all, base.rightSegments);
|
|
298
|
+
if (!right) return;
|
|
299
|
+
const secondary = await pickPresetGroup(
|
|
300
|
+
ctx,
|
|
301
|
+
"secondary",
|
|
302
|
+
all,
|
|
303
|
+
base.secondarySegments ?? [],
|
|
304
|
+
);
|
|
305
|
+
if (!secondary) return;
|
|
306
|
+
|
|
307
|
+
const separator = await showSelectOverlay(
|
|
308
|
+
ctx,
|
|
309
|
+
"Separator",
|
|
310
|
+
"↑↓ navigate · enter select · esc cancel",
|
|
311
|
+
(SEPARATOR_STYLES as unknown as string[]).map((name) => ({
|
|
312
|
+
label: name,
|
|
313
|
+
value: name,
|
|
314
|
+
})),
|
|
315
|
+
Math.min(SEPARATOR_STYLES.length, 12),
|
|
316
|
+
);
|
|
317
|
+
if (!separator) return;
|
|
318
|
+
|
|
319
|
+
const def = buildCustomPresetDef(
|
|
320
|
+
base,
|
|
321
|
+
left,
|
|
322
|
+
right,
|
|
323
|
+
secondary,
|
|
324
|
+
separator.value as StatusLineSeparatorStyle,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const saved = writePowerlineCustomPresetSetting(
|
|
328
|
+
ctx.cwd ?? process.cwd(),
|
|
329
|
+
name,
|
|
330
|
+
def,
|
|
331
|
+
);
|
|
332
|
+
const presets = { ...config.presets, [name]: def };
|
|
333
|
+
registerCustomPresets(presets);
|
|
334
|
+
setConfig({ ...config, preset: name as StatusLinePreset, presets });
|
|
335
|
+
resetLayoutCache(rt);
|
|
336
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
337
|
+
publishPowerlineStatuses(ctx, { preset: name });
|
|
338
|
+
ctx.ui.notify(
|
|
339
|
+
saved
|
|
340
|
+
? `Preset "${name}" saved and applied`
|
|
341
|
+
: `Preset "${name}" applied (not persisted; check settings.json)`,
|
|
342
|
+
saved ? "info" : "warning",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
265
346
|
/** Activate a segment picked from the navigator (Enter). */
|
|
266
347
|
export async function activateSegment(
|
|
267
348
|
rt: RuntimeState,
|
|
@@ -271,11 +352,7 @@ export async function activateSegment(
|
|
|
271
352
|
const id = picked.id;
|
|
272
353
|
if (id === "__none__") return;
|
|
273
354
|
if (id === "tps") {
|
|
274
|
-
|
|
275
|
-
ctx.ui.notify(
|
|
276
|
-
`TPS: ${current} — set via Configure or /tps <value>`,
|
|
277
|
-
"info",
|
|
278
|
-
);
|
|
355
|
+
await showTpsOverlay(rt, ctx);
|
|
279
356
|
return;
|
|
280
357
|
}
|
|
281
358
|
if (id === "open_ports") {
|
|
@@ -305,7 +382,11 @@ export async function activateSegment(
|
|
|
305
382
|
ctx.ui.notify(`${id}: ${value}`, "info");
|
|
306
383
|
}
|
|
307
384
|
|
|
308
|
-
/**
|
|
385
|
+
/**
|
|
386
|
+
* Navigable overlay that mirrors the live powerline segments. Arrow keys move,
|
|
387
|
+
* Enter activates, and →/tab opens a stacked per-segment detail view (←/esc
|
|
388
|
+
* returns to the list). Pure item/detail building lives in `menu-items.ts`.
|
|
389
|
+
*/
|
|
309
390
|
export async function showSegmentNavigator(
|
|
310
391
|
rt: RuntimeState,
|
|
311
392
|
ctx: any,
|
|
@@ -317,70 +398,175 @@ export async function showSegmentNavigator(
|
|
|
317
398
|
_keybindings: any,
|
|
318
399
|
done: (result: { id: string; label: string } | null) => void,
|
|
319
400
|
) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
{
|
|
327
|
-
layout: config.layout,
|
|
328
|
-
disabledSegments: config.disabledSegments,
|
|
329
|
-
},
|
|
330
|
-
);
|
|
331
|
-
const ids = [
|
|
332
|
-
...merged.leftSegments,
|
|
333
|
-
...merged.rightSegments,
|
|
334
|
-
...merged.secondarySegments,
|
|
335
|
-
];
|
|
336
|
-
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
337
|
-
const items: SelectItem[] = [];
|
|
338
|
-
for (const id of ids) {
|
|
339
|
-
const rendered = renderSegmentWithWidth(id, segCtx);
|
|
340
|
-
if (!rendered.visible) continue;
|
|
341
|
-
const value = stripAnsi(rendered.content).trim() || "(empty)";
|
|
342
|
-
items.push({ label: `${id} ${value}`, value: id });
|
|
343
|
-
}
|
|
344
|
-
if (items.length === 0) {
|
|
345
|
-
items.push({ label: "(no visible segments)", value: "__none__" });
|
|
346
|
-
}
|
|
347
|
-
const selectList = new SelectList(
|
|
348
|
-
items,
|
|
349
|
-
Math.min(items.length, 20),
|
|
350
|
-
overlaySelectListTheme(theme),
|
|
351
|
-
);
|
|
401
|
+
let segCtx = buildSegmentContext(rt, ctx, theme);
|
|
402
|
+
let items = segmentItemsToSelectItems(buildSegmentItems(segCtx, config));
|
|
403
|
+
let detailId: StatusLineSegmentId | null = null;
|
|
404
|
+
let detailLines: ReturnType<typeof buildSegmentDetailLines> = [];
|
|
405
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
406
|
+
|
|
352
407
|
const border = (text: string) => theme.fg("dim", text);
|
|
353
408
|
const wrapRow = (text: string, innerWidth: number) =>
|
|
354
409
|
`${border("│")}${truncateToWidth(text, innerWidth, "…", true)}${border("│")}`;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
410
|
+
|
|
411
|
+
const buildDetail = (id: StatusLineSegmentId) =>
|
|
412
|
+
id === "open_ports"
|
|
413
|
+
? buildSegmentDetailLines(
|
|
414
|
+
id,
|
|
415
|
+
segCtx,
|
|
416
|
+
listOpenPortProcesses(
|
|
417
|
+
config.segmentOptions?.openPorts?.includeUdp === true,
|
|
418
|
+
config.segmentOptions?.openPorts?.host,
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
: buildSegmentDetailLines(id, segCtx);
|
|
422
|
+
|
|
423
|
+
const openDetail = (id: string) => {
|
|
424
|
+
if (id === "__none__") return;
|
|
425
|
+
detailId = id as StatusLineSegmentId;
|
|
426
|
+
detailLines = buildDetail(detailId);
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const finish = (result: { id: string; label: string } | null) => {
|
|
430
|
+
if (timer !== null) {
|
|
431
|
+
clearInterval(timer);
|
|
432
|
+
timer = null;
|
|
433
|
+
}
|
|
434
|
+
done(result);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const makeSelectList = () => {
|
|
438
|
+
const list = new SelectList(
|
|
439
|
+
items,
|
|
440
|
+
Math.min(items.length, 20),
|
|
441
|
+
overlaySelectListTheme(theme),
|
|
442
|
+
);
|
|
443
|
+
list.onSelect = (item) =>
|
|
444
|
+
finish(
|
|
445
|
+
item.value === "__none__"
|
|
446
|
+
? null
|
|
447
|
+
: { id: item.value, label: item.label },
|
|
368
448
|
);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
449
|
+
list.onCancel = () => finish(null);
|
|
450
|
+
return list;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
let selectList = makeSelectList();
|
|
454
|
+
|
|
455
|
+
// Re-read live segment state (TPS, context %, cost, git, queue, …) while
|
|
456
|
+
// the overlay stays open, preserving the selected segment across rebuilds.
|
|
457
|
+
const refresh = () => {
|
|
458
|
+
const previous = selectList.getSelectedItem()?.value ?? null;
|
|
459
|
+
segCtx = buildSegmentContext(rt, ctx, theme);
|
|
460
|
+
items = segmentItemsToSelectItems(buildSegmentItems(segCtx, config));
|
|
461
|
+
selectList = makeSelectList();
|
|
462
|
+
selectList.setSelectedIndex(selectIndexForValue(items, previous));
|
|
463
|
+
if (detailId !== null) {
|
|
464
|
+
detailLines = buildDetail(detailId);
|
|
465
|
+
}
|
|
466
|
+
tui.requestRender();
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
timer = setInterval(refresh, SEGMENT_NAVIGATOR_REFRESH_MS);
|
|
470
|
+
|
|
471
|
+
const renderList = (innerWidth: number): string[] => {
|
|
472
|
+
const lines: string[] = [];
|
|
473
|
+
lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
474
|
+
lines.push(
|
|
475
|
+
wrapRow(
|
|
476
|
+
theme.fg("accent", theme.bold("Powerline segments")),
|
|
477
|
+
innerWidth,
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
481
|
+
for (const line of selectList.render(innerWidth))
|
|
482
|
+
lines.push(wrapRow(line, innerWidth));
|
|
483
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
484
|
+
lines.push(
|
|
485
|
+
wrapRow(
|
|
486
|
+
theme.fg(
|
|
487
|
+
"dim",
|
|
488
|
+
"↑↓ navigate · enter activate · →/tab detail · esc cancel",
|
|
489
|
+
),
|
|
490
|
+
innerWidth,
|
|
491
|
+
),
|
|
492
|
+
);
|
|
493
|
+
lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
494
|
+
return lines;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const renderDetail = (innerWidth: number): string[] => {
|
|
498
|
+
const lines: string[] = [];
|
|
499
|
+
lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
500
|
+
lines.push(
|
|
501
|
+
wrapRow(
|
|
502
|
+
theme.fg(
|
|
503
|
+
"accent",
|
|
504
|
+
theme.bold(`Segment detail: ${detailId ?? ""}`),
|
|
505
|
+
),
|
|
506
|
+
innerWidth,
|
|
507
|
+
),
|
|
508
|
+
);
|
|
509
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
510
|
+
const labelWidth = detailLines.reduce(
|
|
511
|
+
(max, line) => Math.max(max, line.label.length),
|
|
512
|
+
0,
|
|
513
|
+
);
|
|
514
|
+
for (const line of detailLines) {
|
|
373
515
|
lines.push(
|
|
374
516
|
wrapRow(
|
|
375
|
-
|
|
517
|
+
` ${line.label.padEnd(labelWidth)} ${line.value}`,
|
|
376
518
|
innerWidth,
|
|
377
519
|
),
|
|
378
520
|
);
|
|
379
|
-
|
|
380
|
-
|
|
521
|
+
}
|
|
522
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
523
|
+
lines.push(
|
|
524
|
+
wrapRow(
|
|
525
|
+
theme.fg("dim", "← back · enter copy · esc close"),
|
|
526
|
+
innerWidth,
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
530
|
+
return lines;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
render: (width: number) => {
|
|
535
|
+
const innerWidth = Math.max(1, width - 2);
|
|
536
|
+
return detailId
|
|
537
|
+
? renderDetail(innerWidth)
|
|
538
|
+
: renderList(innerWidth);
|
|
381
539
|
},
|
|
382
540
|
invalidate: () => selectList.invalidate(),
|
|
383
541
|
handleInput: (data: string) => {
|
|
542
|
+
if (detailId !== null) {
|
|
543
|
+
if (
|
|
544
|
+
matchesKey(data, "left") ||
|
|
545
|
+
matchesKey(data, "escape") ||
|
|
546
|
+
matchesKey(data, "backspace")
|
|
547
|
+
) {
|
|
548
|
+
detailId = null;
|
|
549
|
+
} else if (matchesKey(data, "enter")) {
|
|
550
|
+
const summary = detailLines
|
|
551
|
+
.map((line) => `${line.label}: ${line.value}`)
|
|
552
|
+
.join(" · ");
|
|
553
|
+
ctx.ui.notify(`${detailId}: ${summary}`, "info");
|
|
554
|
+
finish(null);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
tui.requestRender();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (matchesKey(data, "right") || matchesKey(data, "tab")) {
|
|
562
|
+
const selected = selectList.getSelectedItem();
|
|
563
|
+
if (selected && selected.value !== "__none__") {
|
|
564
|
+
openDetail(selected.value);
|
|
565
|
+
tui.requestRender();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
384
570
|
selectList.handleInput(data);
|
|
385
571
|
tui.requestRender();
|
|
386
572
|
},
|