@gajae-code/coding-agent 0.16.1 → 0.16.4
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 +70 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/cli/rows.d.ts +7 -0
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -2
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/dist/types/utils/external-editor.d.ts +5 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +94 -6
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +29 -14
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/acp/adapter.ts +1 -1
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/rows.ts +47 -2
- package/src/sdk/cli/session-cli.ts +47 -23
- package/src/sdk/host/events.ts +25 -2
- package/src/sdk/host/host.ts +4 -1
- package/src/sdk/host/session-runtime.test.ts +36 -25
- package/src/sdk/host/session-runtime.ts +279 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +60 -37
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -30
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +50 -21
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -2
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +9 -2
- package/src/utils/external-editor.ts +16 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
|
@@ -3,6 +3,8 @@ import type { Effort } from "@gajae-code/ai/core";
|
|
|
3
3
|
import {
|
|
4
4
|
type Component,
|
|
5
5
|
Container,
|
|
6
|
+
Ellipsis,
|
|
7
|
+
getKeybindings,
|
|
6
8
|
Input,
|
|
7
9
|
matchesKey,
|
|
8
10
|
resolvePetMode,
|
|
@@ -14,6 +16,8 @@ import {
|
|
|
14
16
|
type Tab,
|
|
15
17
|
TabBar,
|
|
16
18
|
Text,
|
|
19
|
+
truncateToWidth,
|
|
20
|
+
visibleWidth,
|
|
17
21
|
} from "@gajae-code/tui";
|
|
18
22
|
import { type SettingPath, settings } from "../../config/settings";
|
|
19
23
|
import type {
|
|
@@ -41,7 +45,7 @@ import { normalizeProviderOrder } from "./provider-order-context";
|
|
|
41
45
|
import { getSettingsForTab, type SettingDef } from "./settings-defs";
|
|
42
46
|
import { getPreset } from "./status-line/presets";
|
|
43
47
|
import { ALL_SEGMENT_IDS } from "./status-line/segments";
|
|
44
|
-
import type { StatusLineSegmentOptions } from "./tool-status-header";
|
|
48
|
+
import type { StatusLinePreviewParts, StatusLineSegmentOptions } from "./tool-status-header";
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* A submenu component for selecting from a list of options.
|
|
@@ -84,6 +88,14 @@ class TextInputSubmenu extends Container {
|
|
|
84
88
|
handleInput(data: string): void {
|
|
85
89
|
handleInputOrEscape(data, this.#input, this.onCancel);
|
|
86
90
|
}
|
|
91
|
+
|
|
92
|
+
submit(): void {
|
|
93
|
+
this.onSubmit(this.#input.getValue());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cancel(): void {
|
|
97
|
+
this.onCancel();
|
|
98
|
+
}
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
class SelectSubmenu extends Container {
|
|
@@ -210,11 +222,15 @@ function mergeSegmentOptions(
|
|
|
210
222
|
overrides: StatusLineSegmentOptions | undefined,
|
|
211
223
|
): StatusLineSegmentOptions {
|
|
212
224
|
return {
|
|
225
|
+
...base,
|
|
226
|
+
...overrides,
|
|
213
227
|
model: base?.model || overrides?.model ? { ...(base?.model ?? {}), ...(overrides?.model ?? {}) } : undefined,
|
|
214
228
|
path: base?.path || overrides?.path ? { ...(base?.path ?? {}), ...(overrides?.path ?? {}) } : undefined,
|
|
215
229
|
git: base?.git || overrides?.git ? { ...(base?.git ?? {}), ...(overrides?.git ?? {}) } : undefined,
|
|
216
230
|
time: base?.time || overrides?.time ? { ...(base?.time ?? {}), ...(overrides?.time ?? {}) } : undefined,
|
|
217
231
|
usage: base?.usage || overrides?.usage ? { ...(base?.usage ?? {}), ...(overrides?.usage ?? {}) } : undefined,
|
|
232
|
+
command:
|
|
233
|
+
base?.command || overrides?.command ? { ...(base?.command ?? {}), ...(overrides?.command ?? {}) } : undefined,
|
|
218
234
|
};
|
|
219
235
|
}
|
|
220
236
|
|
|
@@ -225,13 +241,31 @@ function effectiveSegmentOptions(
|
|
|
225
241
|
return mergeSegmentOptions(getPreset(preset).segmentOptions, options);
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
function normalizeEditorSegments(value: unknown): StatusLineSegmentId[] {
|
|
245
|
+
if (!Array.isArray(value)) return [];
|
|
246
|
+
const seen = new Set<string>();
|
|
247
|
+
const normalized: StatusLineSegmentId[] = [];
|
|
248
|
+
for (const raw of value) {
|
|
249
|
+
const id = typeof raw === "string" ? raw : String(raw);
|
|
250
|
+
if (!id || seen.has(id)) continue;
|
|
251
|
+
seen.add(id);
|
|
252
|
+
normalized.push(id as StatusLineSegmentId);
|
|
253
|
+
}
|
|
254
|
+
return normalized;
|
|
255
|
+
}
|
|
256
|
+
|
|
228
257
|
function effectiveCustomSegments(
|
|
229
258
|
preset: StatusLinePreset,
|
|
230
|
-
leftSegments:
|
|
231
|
-
rightSegments:
|
|
259
|
+
leftSegments: unknown,
|
|
260
|
+
rightSegments: unknown,
|
|
232
261
|
): { leftSegments: StatusLineSegmentId[]; rightSegments: StatusLineSegmentId[] } {
|
|
233
262
|
if (preset === "custom") {
|
|
234
|
-
|
|
263
|
+
const left = normalizeEditorSegments(leftSegments);
|
|
264
|
+
const leftIds = new Set(left);
|
|
265
|
+
return {
|
|
266
|
+
leftSegments: left,
|
|
267
|
+
rightSegments: normalizeEditorSegments(rightSegments).filter(id => !leftIds.has(id)),
|
|
268
|
+
};
|
|
235
269
|
}
|
|
236
270
|
const presetDef = getPreset(preset);
|
|
237
271
|
return {
|
|
@@ -261,20 +295,77 @@ function statusSegmentLabel(id: StatusLineSegmentId): string {
|
|
|
261
295
|
return id.replace(/_/g, " ");
|
|
262
296
|
}
|
|
263
297
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
298
|
+
type StatusLineSide = "left" | "right";
|
|
299
|
+
type StatusLineOptionPath =
|
|
300
|
+
| "model.showThinkingLevel"
|
|
301
|
+
| "path.abbreviate"
|
|
302
|
+
| "path.maxLength"
|
|
303
|
+
| "path.stripWorkPrefix"
|
|
304
|
+
| "git.showBranch"
|
|
305
|
+
| "git.showStaged"
|
|
306
|
+
| "git.showUnstaged"
|
|
307
|
+
| "git.showUntracked"
|
|
308
|
+
| "time.format"
|
|
309
|
+
| "time.showSeconds"
|
|
310
|
+
| "usage.mode"
|
|
311
|
+
| "command.command";
|
|
312
|
+
type StatusLineChoiceTarget = "separator" | StatusLineOptionPath;
|
|
313
|
+
type StatusLineRootFocus =
|
|
314
|
+
| { kind: "statusbar"; side: StatusLineSide; index: number }
|
|
315
|
+
| { kind: "palette"; index: number }
|
|
316
|
+
| { kind: "separator-control" }
|
|
317
|
+
| { kind: "option-control"; path: StatusLineOptionPath }
|
|
318
|
+
| { kind: "confirm" }
|
|
319
|
+
| { kind: "exit" };
|
|
320
|
+
type StatusLineFocus =
|
|
321
|
+
| StatusLineRootFocus
|
|
322
|
+
| { kind: "choice"; target: StatusLineChoiceTarget; focusedIndex: number; returnFocus: StatusLineRootFocus };
|
|
323
|
+
|
|
324
|
+
interface PickedStatusLineSegment {
|
|
325
|
+
id: StatusLineSegmentId;
|
|
326
|
+
origin: { kind: "statusbar"; side: StatusLineSide; index: number } | { kind: "palette" };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface StatusLineChoiceDescriptor {
|
|
330
|
+
target: StatusLineChoiceTarget;
|
|
331
|
+
label: string;
|
|
332
|
+
values: SelectItem[];
|
|
333
|
+
currentValue: () => string;
|
|
334
|
+
apply: (value: string) => void;
|
|
335
|
+
highlightSegment?: StatusLineSegmentId;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const SEPARATOR_OPTIONS: SelectItem[] = [
|
|
339
|
+
{ value: "powerline", label: "Powerline" },
|
|
340
|
+
{ value: "powerline-thin", label: "Thin chevron" },
|
|
341
|
+
{ value: "slash", label: "Slash" },
|
|
342
|
+
{ value: "pipe", label: "Pipe" },
|
|
343
|
+
{ value: "block", label: "Block" },
|
|
344
|
+
{ value: "none", label: "None" },
|
|
345
|
+
{ value: "ascii", label: "ASCII" },
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
function isDeleteKey(data: string): boolean {
|
|
349
|
+
const kb = getKeybindings();
|
|
350
|
+
return (
|
|
351
|
+
kb.matches(data, "tui.editor.deleteCharForward") ||
|
|
352
|
+
matchesKey(data, "delete") ||
|
|
353
|
+
matchesKey(data, "shift+delete") ||
|
|
354
|
+
data === "\x1b[3~"
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function cloneRootFocus(focus: StatusLineRootFocus): StatusLineRootFocus {
|
|
359
|
+
return { ...focus };
|
|
272
360
|
}
|
|
273
361
|
|
|
274
362
|
class StatusLineCustomEditor extends Container {
|
|
275
|
-
#list!: SettingsList;
|
|
276
363
|
#draft: StatusLineDraft;
|
|
277
364
|
#previewHighlightSegment: StatusLineSegmentId | undefined;
|
|
365
|
+
#focus: StatusLineFocus = { kind: "statusbar", side: "left", index: 0 };
|
|
366
|
+
#picked: PickedStatusLineSegment | undefined;
|
|
367
|
+
#pickedStatusbarFocus: Extract<StatusLineRootFocus, { kind: "statusbar" }> | undefined;
|
|
368
|
+
#textInput: TextInputSubmenu | undefined;
|
|
278
369
|
|
|
279
370
|
constructor(
|
|
280
371
|
private readonly callbacks: SettingsCallbacks,
|
|
@@ -297,259 +388,846 @@ class StatusLineCustomEditor extends Container {
|
|
|
297
388
|
settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions,
|
|
298
389
|
),
|
|
299
390
|
};
|
|
300
|
-
this.#
|
|
301
|
-
this.#build();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
#build(): void {
|
|
305
|
-
this.clear();
|
|
306
|
-
this.addChild(new Text(theme.bold(theme.fg("accent", "Status Line Custom Editor")), 0, 0));
|
|
307
|
-
this.addChild(new Spacer(1));
|
|
308
|
-
this.#list = new SettingsList(
|
|
309
|
-
this.#items(),
|
|
310
|
-
14,
|
|
311
|
-
getSettingsListTheme(),
|
|
312
|
-
(id, value) => this.#handleChange(id, value),
|
|
313
|
-
() => this.#cancel(),
|
|
314
|
-
item => this.#setSelectedItem(item),
|
|
315
|
-
2,
|
|
316
|
-
);
|
|
317
|
-
this.addChild(this.#list);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
#refresh(): void {
|
|
321
|
-
this.#list.setItems(this.#items());
|
|
322
|
-
}
|
|
323
|
-
#setSelectedItem(item: SettingItem | undefined): void {
|
|
324
|
-
this.#previewHighlightSegment = this.#highlightSegmentForItem(item);
|
|
391
|
+
this.#normalizeFocus();
|
|
325
392
|
this.#preview();
|
|
326
393
|
}
|
|
327
394
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (item.id.startsWith("segment.")) {
|
|
331
|
-
return item.id.slice("segment.".length) as StatusLineSegmentId;
|
|
332
|
-
}
|
|
333
|
-
if (item.id.startsWith("moveup.")) {
|
|
334
|
-
return item.id.slice("moveup.".length) as StatusLineSegmentId;
|
|
335
|
-
}
|
|
336
|
-
if (item.id.startsWith("movedown.")) {
|
|
337
|
-
return item.id.slice("movedown.".length) as StatusLineSegmentId;
|
|
338
|
-
}
|
|
339
|
-
if (item.id.startsWith("option.")) {
|
|
340
|
-
const [segment] = item.id.slice("option.".length).split(".");
|
|
341
|
-
return segment as StatusLineSegmentId;
|
|
342
|
-
}
|
|
343
|
-
return undefined;
|
|
395
|
+
get navigationLocked(): boolean {
|
|
396
|
+
return true;
|
|
344
397
|
}
|
|
345
398
|
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
{
|
|
349
|
-
id: "action.save",
|
|
350
|
-
label: "Save custom status line",
|
|
351
|
-
description: "Persist the previewed custom status line to user config.",
|
|
352
|
-
currentValue: "approve",
|
|
353
|
-
values: ["approve"],
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
id: "action.cancel",
|
|
357
|
-
label: "Cancel and restore",
|
|
358
|
-
description: "Close without persisting draft settings and restore the previous preview.",
|
|
359
|
-
currentValue: "restore",
|
|
360
|
-
values: ["restore"],
|
|
361
|
-
},
|
|
399
|
+
#choiceDescriptors(): StatusLineChoiceDescriptor[] {
|
|
400
|
+
return [
|
|
362
401
|
{
|
|
363
|
-
|
|
402
|
+
target: "separator",
|
|
364
403
|
label: "Separator",
|
|
365
|
-
|
|
366
|
-
currentValue: this.#draft.separator,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
"Style of separators between segments.",
|
|
371
|
-
[
|
|
372
|
-
{ value: "powerline", label: "Powerline" },
|
|
373
|
-
{ value: "powerline-thin", label: "Thin chevron" },
|
|
374
|
-
{ value: "slash", label: "Slash" },
|
|
375
|
-
{ value: "pipe", label: "Pipe" },
|
|
376
|
-
{ value: "block", label: "Block" },
|
|
377
|
-
{ value: "none", label: "None" },
|
|
378
|
-
{ value: "ascii", label: "ASCII" },
|
|
379
|
-
],
|
|
380
|
-
currentValue,
|
|
381
|
-
done,
|
|
382
|
-
() => done(),
|
|
383
|
-
),
|
|
404
|
+
values: SEPARATOR_OPTIONS,
|
|
405
|
+
currentValue: () => this.#draft.separator,
|
|
406
|
+
apply: value => {
|
|
407
|
+
this.#draft.separator = value as StatusLineSeparatorStyle;
|
|
408
|
+
},
|
|
384
409
|
},
|
|
385
|
-
];
|
|
386
|
-
|
|
387
|
-
for (const id of PUBLIC_STATUS_SEGMENTS) {
|
|
388
|
-
items.push({
|
|
389
|
-
id: `segment.${id}`,
|
|
390
|
-
label: `Segment: ${statusSegmentLabel(id)}`,
|
|
391
|
-
description: "Cycle placement: hidden → left → right. Use move actions below to reorder visible segments.",
|
|
392
|
-
currentValue: segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments),
|
|
393
|
-
values: ["hidden", "left", "right"],
|
|
394
|
-
});
|
|
395
|
-
if (segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments) !== "hidden") {
|
|
396
|
-
items.push(
|
|
397
|
-
{
|
|
398
|
-
id: `moveup.${id}`,
|
|
399
|
-
label: `Move left: ${statusSegmentLabel(id)}`,
|
|
400
|
-
currentValue: "←",
|
|
401
|
-
values: ["←"],
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
id: `movedown.${id}`,
|
|
405
|
-
label: `Move right: ${statusSegmentLabel(id)}`,
|
|
406
|
-
currentValue: "→",
|
|
407
|
-
values: ["→"],
|
|
408
|
-
},
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
if (id === "usage") {
|
|
412
|
-
items.push({
|
|
413
|
-
id: "option.usage.mode",
|
|
414
|
-
label: "Usage: mode",
|
|
415
|
-
currentValue: this.#draft.segmentOptions.usage?.mode ?? "used",
|
|
416
|
-
submenu: (currentValue, done) =>
|
|
417
|
-
new SelectSubmenu(
|
|
418
|
-
"Usage mode",
|
|
419
|
-
"Show used quota or remaining quota in the usage segment.",
|
|
420
|
-
USAGE_MODE_OPTIONS,
|
|
421
|
-
currentValue,
|
|
422
|
-
done,
|
|
423
|
-
() => done(),
|
|
424
|
-
),
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
items.push(
|
|
430
410
|
{
|
|
431
|
-
|
|
411
|
+
target: "model.showThinkingLevel",
|
|
432
412
|
label: "Model: show thinking level",
|
|
433
|
-
|
|
434
|
-
|
|
413
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
414
|
+
currentValue: () => String(this.#draft.segmentOptions.model?.showThinkingLevel !== false),
|
|
415
|
+
apply: value => this.#setOption("model.showThinkingLevel", value),
|
|
416
|
+
highlightSegment: "model",
|
|
435
417
|
},
|
|
436
418
|
{
|
|
437
|
-
|
|
419
|
+
target: "path.abbreviate",
|
|
438
420
|
label: "Path: abbreviate",
|
|
439
|
-
|
|
440
|
-
|
|
421
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
422
|
+
currentValue: () => String(this.#draft.segmentOptions.path?.abbreviate !== false),
|
|
423
|
+
apply: value => this.#setOption("path.abbreviate", value),
|
|
424
|
+
highlightSegment: "path",
|
|
441
425
|
},
|
|
442
426
|
{
|
|
443
|
-
|
|
427
|
+
target: "path.maxLength",
|
|
444
428
|
label: "Path: max length",
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
"Maximum displayed path length.",
|
|
450
|
-
PATH_LENGTH_OPTIONS,
|
|
451
|
-
currentValue,
|
|
452
|
-
done,
|
|
453
|
-
() => done(),
|
|
454
|
-
),
|
|
429
|
+
values: PATH_LENGTH_OPTIONS,
|
|
430
|
+
currentValue: () => String(this.#draft.segmentOptions.path?.maxLength ?? 32),
|
|
431
|
+
apply: value => this.#setOption("path.maxLength", value),
|
|
432
|
+
highlightSegment: "path",
|
|
455
433
|
},
|
|
456
434
|
{
|
|
457
|
-
|
|
435
|
+
target: "path.stripWorkPrefix",
|
|
458
436
|
label: "Path: strip work prefix",
|
|
459
|
-
|
|
460
|
-
|
|
437
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
438
|
+
currentValue: () => String(this.#draft.segmentOptions.path?.stripWorkPrefix === true),
|
|
439
|
+
apply: value => this.#setOption("path.stripWorkPrefix", value),
|
|
440
|
+
highlightSegment: "path",
|
|
461
441
|
},
|
|
462
442
|
{
|
|
463
|
-
|
|
443
|
+
target: "git.showBranch",
|
|
464
444
|
label: "Git: show branch",
|
|
465
|
-
|
|
466
|
-
|
|
445
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
446
|
+
currentValue: () => String(this.#draft.segmentOptions.git?.showBranch !== false),
|
|
447
|
+
apply: value => this.#setOption("git.showBranch", value),
|
|
448
|
+
highlightSegment: "git",
|
|
467
449
|
},
|
|
468
450
|
{
|
|
469
|
-
|
|
451
|
+
target: "git.showStaged",
|
|
470
452
|
label: "Git: show staged",
|
|
471
|
-
|
|
472
|
-
|
|
453
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
454
|
+
currentValue: () => String(this.#draft.segmentOptions.git?.showStaged !== false),
|
|
455
|
+
apply: value => this.#setOption("git.showStaged", value),
|
|
456
|
+
highlightSegment: "git",
|
|
473
457
|
},
|
|
474
458
|
{
|
|
475
|
-
|
|
459
|
+
target: "git.showUnstaged",
|
|
476
460
|
label: "Git: show unstaged",
|
|
477
|
-
|
|
478
|
-
|
|
461
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
462
|
+
currentValue: () => String(this.#draft.segmentOptions.git?.showUnstaged !== false),
|
|
463
|
+
apply: value => this.#setOption("git.showUnstaged", value),
|
|
464
|
+
highlightSegment: "git",
|
|
479
465
|
},
|
|
480
466
|
{
|
|
481
|
-
|
|
467
|
+
target: "git.showUntracked",
|
|
482
468
|
label: "Git: show untracked",
|
|
483
|
-
|
|
484
|
-
|
|
469
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
470
|
+
currentValue: () => String(this.#draft.segmentOptions.git?.showUntracked !== false),
|
|
471
|
+
apply: value => this.#setOption("git.showUntracked", value),
|
|
472
|
+
highlightSegment: "git",
|
|
485
473
|
},
|
|
486
474
|
{
|
|
487
|
-
|
|
475
|
+
target: "time.format",
|
|
488
476
|
label: "Time: format",
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
"Time format",
|
|
493
|
-
"Clock format for the time segment.",
|
|
494
|
-
TIME_FORMAT_OPTIONS,
|
|
495
|
-
currentValue,
|
|
496
|
-
done,
|
|
497
|
-
() => done(),
|
|
498
|
-
),
|
|
477
|
+
values: TIME_FORMAT_OPTIONS,
|
|
478
|
+
currentValue: () => this.#draft.segmentOptions.time?.format ?? "24h",
|
|
479
|
+
apply: value => this.#setOption("time.format", value),
|
|
499
480
|
},
|
|
500
481
|
{
|
|
501
|
-
|
|
482
|
+
target: "time.showSeconds",
|
|
502
483
|
label: "Time: show seconds",
|
|
503
|
-
|
|
504
|
-
|
|
484
|
+
values: BOOL_VALUES.map(value => ({ value, label: value })),
|
|
485
|
+
currentValue: () => String(this.#draft.segmentOptions.time?.showSeconds === true),
|
|
486
|
+
apply: value => this.#setOption("time.showSeconds", value),
|
|
505
487
|
},
|
|
506
|
-
|
|
488
|
+
{
|
|
489
|
+
target: "usage.mode",
|
|
490
|
+
label: "Usage: mode",
|
|
491
|
+
values: USAGE_MODE_OPTIONS,
|
|
492
|
+
currentValue: () => this.#draft.segmentOptions.usage?.mode ?? "used",
|
|
493
|
+
apply: value => this.#setOption("usage.mode", value),
|
|
494
|
+
highlightSegment: "usage",
|
|
495
|
+
},
|
|
496
|
+
];
|
|
497
|
+
}
|
|
507
498
|
|
|
508
|
-
|
|
499
|
+
#descriptor(target: StatusLineChoiceTarget): StatusLineChoiceDescriptor | undefined {
|
|
500
|
+
return this.#choiceDescriptors().find(descriptor => descriptor.target === target);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#controlTargets(): StatusLineChoiceTarget[] {
|
|
504
|
+
return [...this.#choiceDescriptors().map(descriptor => descriptor.target), "command.command"];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#hiddenSegments(): StatusLineSegmentId[] {
|
|
508
|
+
const visible = new Set([...this.#draft.leftSegments, ...this.#draft.rightSegments]);
|
|
509
|
+
return PUBLIC_STATUS_SEGMENTS.filter(id => !visible.has(id));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#paletteSegments(includePicked = this.#focus.kind === "palette"): StatusLineSegmentId[] {
|
|
513
|
+
const visible = new Set([...this.#draft.leftSegments, ...this.#draft.rightSegments]);
|
|
514
|
+
const selected = includePicked ? this.#picked?.id : undefined;
|
|
515
|
+
return PUBLIC_STATUS_SEGMENTS.filter(id => !visible.has(id) || id === selected);
|
|
509
516
|
}
|
|
510
517
|
|
|
511
|
-
#
|
|
512
|
-
if (
|
|
513
|
-
|
|
518
|
+
#focusSegment(): StatusLineSegmentId | undefined {
|
|
519
|
+
if (this.#focus.kind !== "statusbar") return undefined;
|
|
520
|
+
return this.#draft[`${this.#focus.side}Segments`][this.#focus.index];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
#segmentCount(side: StatusLineSide, omitPicked = false): number {
|
|
524
|
+
const segments = this.#draft[`${side}Segments`];
|
|
525
|
+
if (
|
|
526
|
+
!omitPicked ||
|
|
527
|
+
!this.#picked ||
|
|
528
|
+
this.#picked.origin.kind !== "statusbar" ||
|
|
529
|
+
this.#picked.origin.side !== side
|
|
530
|
+
) {
|
|
531
|
+
return segments.length;
|
|
532
|
+
}
|
|
533
|
+
return Math.max(0, segments.length - 1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#normalizeFocus(): void {
|
|
537
|
+
if (this.#focus.kind === "choice") {
|
|
538
|
+
const descriptor = this.#descriptor(this.#focus.target);
|
|
539
|
+
if (!descriptor) {
|
|
540
|
+
this.#focus = { kind: "separator-control" };
|
|
541
|
+
} else {
|
|
542
|
+
this.#focus.focusedIndex = Math.max(0, Math.min(this.#focus.focusedIndex, descriptor.values.length - 1));
|
|
543
|
+
}
|
|
514
544
|
return;
|
|
515
545
|
}
|
|
516
|
-
if (
|
|
517
|
-
this.#
|
|
546
|
+
if (this.#focus.kind === "statusbar") {
|
|
547
|
+
const max = this.#picked
|
|
548
|
+
? this.#segmentCount(this.#focus.side, true)
|
|
549
|
+
: this.#draft[`${this.#focus.side}Segments`].length - 1;
|
|
550
|
+
if (max < 0) {
|
|
551
|
+
const otherSide: StatusLineSide = this.#focus.side === "left" ? "right" : "left";
|
|
552
|
+
if (this.#draft[`${otherSide}Segments`].length > 0) {
|
|
553
|
+
this.#focus = { kind: "statusbar", side: otherSide, index: 0 };
|
|
554
|
+
} else if (this.#paletteSegments().length > 0) {
|
|
555
|
+
this.#focus = { kind: "palette", index: 0 };
|
|
556
|
+
} else {
|
|
557
|
+
this.#focus = { kind: "separator-control" };
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
this.#focus.index = Math.max(0, Math.min(this.#focus.index, max));
|
|
561
|
+
}
|
|
562
|
+
} else if (this.#focus.kind === "palette") {
|
|
563
|
+
const palette = this.#paletteSegments();
|
|
564
|
+
if (palette.length === 0) {
|
|
565
|
+
this.#focus = { kind: "separator-control" };
|
|
566
|
+
} else {
|
|
567
|
+
this.#focus.index = Math.max(0, Math.min(this.#focus.index, palette.length - 1));
|
|
568
|
+
}
|
|
569
|
+
} else if (
|
|
570
|
+
this.#focus.kind === "option-control" &&
|
|
571
|
+
!this.#controlTargets().includes((this.#focus as { path: StatusLineOptionPath }).path)
|
|
572
|
+
) {
|
|
573
|
+
this.#focus = { kind: "separator-control" };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#rootOrder(): StatusLineRootFocus[] {
|
|
578
|
+
const roots: StatusLineRootFocus[] = [];
|
|
579
|
+
roots.push(this.#nearestStatusbarFocus());
|
|
580
|
+
if (this.#paletteSegments().length > 0) roots.push({ kind: "palette", index: 0 });
|
|
581
|
+
roots.push({ kind: "separator-control" });
|
|
582
|
+
roots.push({ kind: "confirm" }, { kind: "exit" });
|
|
583
|
+
return roots;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#nearestStatusbarFocus(): StatusLineRootFocus {
|
|
587
|
+
if (this.#focus.kind === "statusbar") {
|
|
588
|
+
return { kind: "statusbar", side: this.#focus.side, index: this.#focus.index };
|
|
589
|
+
}
|
|
590
|
+
if (this.#draft.leftSegments.length > 0 || this.#picked) {
|
|
591
|
+
return {
|
|
592
|
+
kind: "statusbar",
|
|
593
|
+
side: "left",
|
|
594
|
+
index: this.#picked ? this.#segmentCount("left", true) : 0,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (this.#draft.rightSegments.length > 0) {
|
|
598
|
+
return { kind: "statusbar", side: "right", index: 0 };
|
|
599
|
+
}
|
|
600
|
+
return { kind: "statusbar", side: "left", index: 0 };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#rootFocusKey(focus: StatusLineRootFocus): string {
|
|
604
|
+
switch (focus.kind) {
|
|
605
|
+
case "statusbar":
|
|
606
|
+
return `${focus.kind}:${focus.side}:${focus.index}`;
|
|
607
|
+
case "palette":
|
|
608
|
+
return `${focus.kind}:${focus.index}`;
|
|
609
|
+
case "option-control":
|
|
610
|
+
return `${focus.kind}:${focus.path}`;
|
|
611
|
+
default:
|
|
612
|
+
return focus.kind;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
#moveVertical(delta: -1 | 1): void {
|
|
617
|
+
if (this.#focus.kind === "choice") {
|
|
618
|
+
const descriptor = this.#descriptor(this.#focus.target);
|
|
619
|
+
if (!descriptor || descriptor.values.length === 0) return;
|
|
620
|
+
this.#focus.focusedIndex =
|
|
621
|
+
(this.#focus.focusedIndex + delta + descriptor.values.length) % descriptor.values.length;
|
|
518
622
|
return;
|
|
519
623
|
}
|
|
520
|
-
if (
|
|
521
|
-
this.#
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
} else if (id.startsWith("movedown.")) {
|
|
530
|
-
this.#moveSegment(id.slice("movedown.".length) as StatusLineSegmentId, 1);
|
|
531
|
-
} else if (id.startsWith("option.")) {
|
|
532
|
-
this.#setOption(id.slice("option.".length), value);
|
|
624
|
+
if (this.#picked) {
|
|
625
|
+
if (this.#focus.kind === "palette") {
|
|
626
|
+
if (delta === -1) this.#focus = this.#pickedStatusbarFocus ?? this.#nearestStatusbarFocus();
|
|
627
|
+
} else if (delta === 1) {
|
|
628
|
+
if (this.#focus.kind === "statusbar") this.#pickedStatusbarFocus = { ...this.#focus };
|
|
629
|
+
this.#focus = { kind: "palette", index: this.#focusPaletteIndex() };
|
|
630
|
+
}
|
|
631
|
+
this.#normalizeFocus();
|
|
632
|
+
return;
|
|
533
633
|
}
|
|
634
|
+
const order = this.#rootOrder();
|
|
635
|
+
const currentKey = this.#rootFocusKey(this.#focus);
|
|
636
|
+
const currentIndex = Math.max(
|
|
637
|
+
0,
|
|
638
|
+
order.findIndex(item => this.#rootFocusKey(item) === currentKey),
|
|
639
|
+
);
|
|
640
|
+
this.#focus = cloneRootFocus(order[(currentIndex + delta + order.length) % order.length] ?? order[0]);
|
|
641
|
+
this.#normalizeFocus();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#focusPaletteIndex(): number {
|
|
645
|
+
if (this.#focus.kind === "palette") return this.#focus.index;
|
|
646
|
+
if (this.#picked) return Math.max(0, this.#paletteSegments(true).indexOf(this.#picked.id));
|
|
647
|
+
return Math.max(0, Math.min(this.#paletteSegments(false).length - 1, 0));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
#linearVisibleFocus(): Array<{ side: StatusLineSide; index: number }> {
|
|
651
|
+
return [
|
|
652
|
+
...this.#draft.leftSegments.map((_, index) => ({ side: "left" as const, index })),
|
|
653
|
+
...this.#draft.rightSegments.map((_, index) => ({ side: "right" as const, index })),
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#moveVisibleFocus(delta: -1 | 1): void {
|
|
658
|
+
if (this.#focus.kind !== "statusbar") return;
|
|
659
|
+
const focus = this.#focus;
|
|
660
|
+
const items = this.#linearVisibleFocus();
|
|
661
|
+
if (items.length === 0) return;
|
|
662
|
+
const currentIndex = Math.max(
|
|
663
|
+
0,
|
|
664
|
+
items.findIndex(item => item.side === focus.side && item.index === focus.index),
|
|
665
|
+
);
|
|
666
|
+
this.#focus = { kind: "statusbar", ...items[(currentIndex + delta + items.length) % items.length] };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#slotOrder(): Array<{ side: StatusLineSide; index: number }> {
|
|
670
|
+
const leftCount = this.#segmentCount("left", true);
|
|
671
|
+
const rightCount = this.#segmentCount("right", true);
|
|
672
|
+
return [
|
|
673
|
+
...Array.from({ length: leftCount + 1 }, (_, index) => ({ side: "left" as const, index })),
|
|
674
|
+
...Array.from({ length: rightCount + 1 }, (_, index) => ({ side: "right" as const, index })),
|
|
675
|
+
];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
#movePickedSlot(delta: -1 | 1): void {
|
|
679
|
+
if (!this.#picked || this.#focus.kind !== "statusbar") return;
|
|
680
|
+
const focus = this.#focus;
|
|
681
|
+
const slots = this.#slotOrder();
|
|
682
|
+
const currentIndex = Math.max(
|
|
683
|
+
0,
|
|
684
|
+
slots.findIndex(item => item.side === focus.side && item.index === focus.index),
|
|
685
|
+
);
|
|
686
|
+
this.#focus = { kind: "statusbar", ...slots[Math.max(0, Math.min(slots.length - 1, currentIndex + delta))] };
|
|
687
|
+
this.#pickedStatusbarFocus = { ...this.#focus };
|
|
688
|
+
this.#updatePreviewHighlight();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
#movePaletteFocus(delta: -1 | 1): void {
|
|
692
|
+
if (this.#focus.kind !== "palette") return;
|
|
693
|
+
const palette = this.#paletteSegments();
|
|
694
|
+
if (palette.length === 0) return;
|
|
695
|
+
this.#focus.index = (this.#focus.index + delta + palette.length) % palette.length;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#moveChoiceControl(delta: -1 | 1): void {
|
|
699
|
+
if (this.#focus.kind !== "separator-control" && this.#focus.kind !== "option-control") return;
|
|
700
|
+
const targets = this.#controlTargets();
|
|
701
|
+
const currentTarget = this.#focus.kind === "separator-control" ? "separator" : this.#focus.path;
|
|
702
|
+
const currentIndex = Math.max(0, targets.indexOf(currentTarget));
|
|
703
|
+
const next = targets[(currentIndex + delta + targets.length) % targets.length];
|
|
704
|
+
if (!next) return;
|
|
705
|
+
this.#focus =
|
|
706
|
+
next === "separator"
|
|
707
|
+
? { kind: "separator-control" }
|
|
708
|
+
: { kind: "option-control", path: next as StatusLineOptionPath };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
#moveChoiceTarget(delta: -1 | 1): void {
|
|
712
|
+
if (this.#focus.kind !== "choice") return;
|
|
713
|
+
const focus = this.#focus;
|
|
714
|
+
const descriptors = this.#choiceDescriptors();
|
|
715
|
+
const currentIndex = Math.max(
|
|
716
|
+
0,
|
|
717
|
+
descriptors.findIndex(descriptor => descriptor.target === focus.target),
|
|
718
|
+
);
|
|
719
|
+
const next = descriptors[(currentIndex + delta + descriptors.length) % descriptors.length];
|
|
720
|
+
if (!next) return;
|
|
721
|
+
const currentValue = next.currentValue();
|
|
722
|
+
this.#focus = {
|
|
723
|
+
kind: "choice",
|
|
724
|
+
target: next.target,
|
|
725
|
+
focusedIndex: Math.max(
|
|
726
|
+
0,
|
|
727
|
+
next.values.findIndex(value => value.value === currentValue),
|
|
728
|
+
),
|
|
729
|
+
returnFocus:
|
|
730
|
+
next.target === "separator"
|
|
731
|
+
? { kind: "separator-control" }
|
|
732
|
+
: { kind: "option-control", path: next.target as StatusLineOptionPath },
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
#pickFocusedStatusbar(): void {
|
|
737
|
+
if (this.#focus.kind !== "statusbar") return;
|
|
738
|
+
const id = this.#focusSegment();
|
|
739
|
+
if (!id) return;
|
|
740
|
+
this.#picked = { id, origin: { kind: "statusbar", side: this.#focus.side, index: this.#focus.index } };
|
|
741
|
+
this.#pickedStatusbarFocus = { kind: "statusbar", side: this.#focus.side, index: this.#focus.index };
|
|
742
|
+
this.#updatePreviewHighlight();
|
|
534
743
|
this.#preview();
|
|
535
|
-
this.#refresh();
|
|
536
744
|
}
|
|
537
745
|
|
|
538
|
-
#
|
|
746
|
+
#pickFocusedPalette(): void {
|
|
747
|
+
if (this.#focus.kind !== "palette") return;
|
|
748
|
+
const id = this.#paletteSegments()[this.#focus.index];
|
|
749
|
+
if (!id) return;
|
|
750
|
+
this.#picked = { id, origin: { kind: "palette" } };
|
|
751
|
+
this.#focus = { kind: "statusbar", side: "left", index: this.#segmentCount("left", true) };
|
|
752
|
+
this.#pickedStatusbarFocus = { ...this.#focus };
|
|
753
|
+
this.#updatePreviewHighlight();
|
|
754
|
+
this.#preview();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
#dropPicked(): void {
|
|
758
|
+
if (!this.#picked || this.#focus.kind !== "statusbar") return;
|
|
759
|
+
const id = this.#picked.id;
|
|
760
|
+
const nextLeft = this.#draft.leftSegments.filter(segment => segment !== id);
|
|
761
|
+
const nextRight = this.#draft.rightSegments.filter(segment => segment !== id);
|
|
762
|
+
const target = this.#focus.side === "left" ? nextLeft : nextRight;
|
|
763
|
+
target.splice(Math.max(0, Math.min(target.length, this.#focus.index)), 0, id);
|
|
764
|
+
this.#draft.leftSegments = nextLeft;
|
|
765
|
+
this.#draft.rightSegments = nextRight;
|
|
766
|
+
this.#focus = { kind: "statusbar", side: this.#focus.side, index: target.indexOf(id) };
|
|
767
|
+
this.#picked = undefined;
|
|
768
|
+
this.#pickedStatusbarFocus = undefined;
|
|
769
|
+
this.#updatePreviewHighlight();
|
|
770
|
+
this.#preview();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
#dropPickedToPalette(): void {
|
|
774
|
+
if (!this.#picked || this.#focus.kind !== "palette") return;
|
|
775
|
+
const id = this.#picked.id;
|
|
539
776
|
this.#draft.leftSegments = this.#draft.leftSegments.filter(segment => segment !== id);
|
|
540
777
|
this.#draft.rightSegments = this.#draft.rightSegments.filter(segment => segment !== id);
|
|
541
|
-
|
|
542
|
-
|
|
778
|
+
this.#picked = undefined;
|
|
779
|
+
this.#pickedStatusbarFocus = undefined;
|
|
780
|
+
const hidden = this.#hiddenSegments();
|
|
781
|
+
this.#focus = { kind: "palette", index: Math.max(0, hidden.indexOf(id)) };
|
|
782
|
+
this.#updatePreviewHighlight();
|
|
783
|
+
this.#preview();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
#hideFocusedSegment(): void {
|
|
787
|
+
if (this.#picked || this.#focus.kind !== "statusbar") return;
|
|
788
|
+
const group = this.#draft[`${this.#focus.side}Segments`];
|
|
789
|
+
const removed = group[this.#focus.index];
|
|
790
|
+
if (!removed) return;
|
|
791
|
+
group.splice(this.#focus.index, 1);
|
|
792
|
+
const hidden = this.#hiddenSegments();
|
|
793
|
+
this.#focus = hidden.length
|
|
794
|
+
? { kind: "palette", index: Math.max(0, hidden.indexOf(removed)) }
|
|
795
|
+
: { kind: "separator-control" };
|
|
796
|
+
this.#updatePreviewHighlight();
|
|
797
|
+
this.#preview();
|
|
543
798
|
}
|
|
544
799
|
|
|
545
|
-
#
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
800
|
+
#openChoice(target: StatusLineChoiceTarget, returnFocus: StatusLineRootFocus): void {
|
|
801
|
+
const descriptor = this.#descriptor(target);
|
|
802
|
+
if (!descriptor) return;
|
|
803
|
+
const currentValue = descriptor.currentValue();
|
|
804
|
+
this.#focus = {
|
|
805
|
+
kind: "choice",
|
|
806
|
+
target,
|
|
807
|
+
focusedIndex: Math.max(
|
|
808
|
+
0,
|
|
809
|
+
descriptor.values.findIndex(value => value.value === currentValue),
|
|
810
|
+
),
|
|
811
|
+
returnFocus,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
#openCommandInput(): void {
|
|
816
|
+
this.#textInput = new TextInputSubmenu(
|
|
817
|
+
"Status line command",
|
|
818
|
+
"Runs in the configured shell with a short timeout and cached refreshes.",
|
|
819
|
+
this.#draft.segmentOptions.command?.command ?? "",
|
|
820
|
+
value => {
|
|
821
|
+
this.#setOption("command.command", value);
|
|
822
|
+
this.#textInput = undefined;
|
|
823
|
+
this.#focus = { kind: "option-control", path: "command.command" };
|
|
824
|
+
this.#updatePreviewHighlight();
|
|
825
|
+
this.#preview();
|
|
826
|
+
},
|
|
827
|
+
() => {
|
|
828
|
+
this.#textInput = undefined;
|
|
829
|
+
this.#focus = { kind: "option-control", path: "command.command" };
|
|
830
|
+
this.#updatePreviewHighlight();
|
|
831
|
+
this.#preview();
|
|
832
|
+
},
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
#applyChoice(): void {
|
|
837
|
+
if (this.#focus.kind !== "choice") return;
|
|
838
|
+
const descriptor = this.#descriptor(this.#focus.target);
|
|
839
|
+
if (!descriptor) return;
|
|
840
|
+
const value = descriptor.values[this.#focus.focusedIndex]?.value;
|
|
841
|
+
if (value === undefined) return;
|
|
842
|
+
descriptor.apply(value);
|
|
843
|
+
this.#focus = cloneRootFocus(this.#focus.returnFocus);
|
|
844
|
+
this.#updatePreviewHighlight();
|
|
845
|
+
this.#preview();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
#updatePreviewHighlight(): void {
|
|
849
|
+
if (this.#picked) {
|
|
850
|
+
this.#previewHighlightSegment = this.#picked.id;
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (this.#focus.kind === "statusbar") {
|
|
854
|
+
this.#previewHighlightSegment = this.#focusSegment();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (this.#focus.kind === "palette") {
|
|
858
|
+
this.#previewHighlightSegment = this.#paletteSegments()[this.#focus.index];
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (this.#focus.kind === "option-control") {
|
|
862
|
+
this.#previewHighlightSegment =
|
|
863
|
+
this.#focus.path === "command.command" ? "command" : this.#descriptor(this.#focus.path)?.highlightSegment;
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (this.#focus.kind === "choice") {
|
|
867
|
+
this.#previewHighlightSegment = this.#descriptor(this.#focus.target)?.highlightSegment;
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
this.#previewHighlightSegment = undefined;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
#focusLabel(): string {
|
|
874
|
+
switch (this.#focus.kind) {
|
|
875
|
+
case "statusbar":
|
|
876
|
+
return `statusbar:${this.#focus.side}:${this.#focus.index}`;
|
|
877
|
+
case "palette":
|
|
878
|
+
return `palette:${this.#focus.index}`;
|
|
879
|
+
case "option-control":
|
|
880
|
+
return `option:${this.#focus.path}`;
|
|
881
|
+
case "choice":
|
|
882
|
+
return `choice:${this.#focus.target}`;
|
|
883
|
+
default:
|
|
884
|
+
return this.#focus.kind;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
#separatorGlyph(): string {
|
|
889
|
+
if (theme.getSymbolPreset() === "ascii") {
|
|
890
|
+
return this.#draft.separator === "none" ? " " : this.#draft.separator === "pipe" ? " | " : " / ";
|
|
891
|
+
}
|
|
892
|
+
switch (this.#draft.separator) {
|
|
893
|
+
case "pipe":
|
|
894
|
+
return " | ";
|
|
895
|
+
case "block":
|
|
896
|
+
return " █ ";
|
|
897
|
+
case "none":
|
|
898
|
+
return " ";
|
|
899
|
+
case "ascii":
|
|
900
|
+
return " > ";
|
|
901
|
+
case "powerline":
|
|
902
|
+
return " ";
|
|
903
|
+
case "powerline-thin":
|
|
904
|
+
return " ❯ ";
|
|
905
|
+
case "slash":
|
|
906
|
+
return " / ";
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
#fit(line: string, width: number): string {
|
|
911
|
+
const ascii = theme.getSymbolPreset() === "ascii";
|
|
912
|
+
const fitted = truncateToWidth(
|
|
913
|
+
line,
|
|
914
|
+
ascii ? Math.max(0, width - 2) : width,
|
|
915
|
+
ascii ? Ellipsis.Ascii : Ellipsis.Unicode,
|
|
916
|
+
);
|
|
917
|
+
if (!ascii) return fitted;
|
|
918
|
+
return fitted.replace(/·/g, "-").replace(/│/g, "|").replace(/…/g, "...");
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
#padEndVisible(line: string, width: number): string {
|
|
922
|
+
return line + " ".repeat(Math.max(0, width - visibleWidth(line)));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
#padStartVisible(line: string, width: number): string {
|
|
926
|
+
return " ".repeat(Math.max(0, width - visibleWidth(line))) + line;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
#centerVisible(line: string, width: number): string {
|
|
930
|
+
const left = Math.floor(Math.max(0, width - visibleWidth(line)) / 2);
|
|
931
|
+
return " ".repeat(left) + line;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
#simulationWidth(width: number): number {
|
|
935
|
+
return Math.max(1, width - 2);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
#simulationIndent(width: number): string {
|
|
939
|
+
return " ".repeat(Math.max(0, Math.floor((width - this.#simulationWidth(width)) / 2)));
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
#highlightBox(text: string, style: "focus" | "selected"): string {
|
|
943
|
+
const selectedText = `> ${text} <`;
|
|
944
|
+
const bgColor = style === "selected" ? "warning" : "text";
|
|
945
|
+
const brightBg = theme.getFgAnsi(bgColor).replace("\x1b[38;", "\x1b[48;");
|
|
946
|
+
const darkFg = theme.getBgAnsi("selectedBg").replace("\x1b[48;", "\x1b[38;");
|
|
947
|
+
return `${brightBg}${darkFg}${selectedText}\x1b[0m`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
#focusBox(text: string): string {
|
|
951
|
+
return this.#highlightBox(text, "focus");
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
#selectedBox(text: string): string {
|
|
955
|
+
return this.#highlightBox(text, "selected");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
#dashedBox(text: string): string {
|
|
959
|
+
return theme.fg("dim", theme.getSymbolPreset() === "ascii" ? `[ ${text} ]` : `┆ ${text} ┆`);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
#renderSegment(id: StatusLineSegmentId, selected: boolean, visibleInStatusbar: boolean): string {
|
|
963
|
+
const label = statusSegmentLabel(id);
|
|
964
|
+
if (selected) return this.#focusBox(label);
|
|
965
|
+
if (visibleInStatusbar) return label;
|
|
966
|
+
return this.#dashedBox(label);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
#renderSide(side: StatusLineSide, visibleIds: ReadonlySet<StatusLineSegmentId> | undefined): string {
|
|
970
|
+
const segments = this.#draft[`${side}Segments`];
|
|
971
|
+
const rendered: string[] = [];
|
|
972
|
+
for (let index = 0; index < segments.length; index++) {
|
|
973
|
+
const id = segments[index];
|
|
974
|
+
if (!id) continue;
|
|
975
|
+
const isOrigin =
|
|
976
|
+
this.#picked?.origin.kind === "statusbar" &&
|
|
977
|
+
this.#picked.origin.side === side &&
|
|
978
|
+
this.#picked.origin.index === index;
|
|
979
|
+
if (isOrigin) continue;
|
|
980
|
+
const selected =
|
|
981
|
+
!this.#picked &&
|
|
982
|
+
this.#focus.kind === "statusbar" &&
|
|
983
|
+
this.#focus.side === side &&
|
|
984
|
+
this.#focus.index === index;
|
|
985
|
+
rendered.push(this.#renderSegment(id, selected, visibleIds ? visibleIds.has(id) : true));
|
|
986
|
+
}
|
|
987
|
+
if (this.#picked && this.#focus.kind === "statusbar" && this.#focus.side === side) {
|
|
988
|
+
rendered.splice(
|
|
989
|
+
Math.max(0, Math.min(rendered.length, this.#focus.index)),
|
|
990
|
+
0,
|
|
991
|
+
this.#selectedBox(statusSegmentLabel(this.#picked.id)),
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return rendered.length
|
|
995
|
+
? rendered.join(theme.fg("statusLineSep", this.#separatorGlyph()))
|
|
996
|
+
: theme.fg("muted", "(empty)");
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
#draftPreviewSettings(includePickedPlacement = false): StatusLinePreviewSettings {
|
|
1000
|
+
let leftSegments = [...this.#draft.leftSegments];
|
|
1001
|
+
let rightSegments = [...this.#draft.rightSegments];
|
|
1002
|
+
let previewHighlightSegment = this.#previewHighlightSegment;
|
|
1003
|
+
if (includePickedPlacement && this.#picked) {
|
|
1004
|
+
const id = this.#picked.id;
|
|
1005
|
+
leftSegments = leftSegments.filter(segment => segment !== id);
|
|
1006
|
+
rightSegments = rightSegments.filter(segment => segment !== id);
|
|
1007
|
+
if (this.#focus.kind === "statusbar") {
|
|
1008
|
+
const target = this.#focus.side === "left" ? leftSegments : rightSegments;
|
|
1009
|
+
target.splice(Math.max(0, Math.min(target.length, this.#focus.index)), 0, id);
|
|
1010
|
+
previewHighlightSegment = id;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
preset: "custom",
|
|
1015
|
+
leftSegments,
|
|
1016
|
+
rightSegments,
|
|
1017
|
+
separator: this.#draft.separator,
|
|
1018
|
+
segmentOptions: cloneSegmentOptions(this.#draft.segmentOptions),
|
|
1019
|
+
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
1020
|
+
maxRows: settings.get("statusLine.maxRows"),
|
|
1021
|
+
previewHighlightSegment,
|
|
1022
|
+
previewHighlightStyle: this.#picked ? "selected" : "focus",
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
#actualStatusbarParts(width: number): StatusLinePreviewParts | undefined {
|
|
1027
|
+
return this.callbacks.getStatusLinePreviewPartsForSettings?.(
|
|
1028
|
+
this.#draftPreviewSettings(true),
|
|
1029
|
+
this.#simulationWidth(width),
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
#renderActualStatusbar(width: number, parts: StatusLinePreviewParts | undefined): string[] {
|
|
1034
|
+
const rendered = this.callbacks.getStatusLinePreviewForSettings?.(
|
|
1035
|
+
this.#draftPreviewSettings(true),
|
|
1036
|
+
this.#simulationWidth(width),
|
|
1037
|
+
);
|
|
1038
|
+
if (rendered) {
|
|
1039
|
+
return rendered
|
|
1040
|
+
.split("\n")
|
|
1041
|
+
.map(line => line.trimEnd())
|
|
1042
|
+
.filter(line => line.length > 0)
|
|
1043
|
+
.map(line => this.#centerVisible(this.#fit(line, this.#simulationWidth(width)), width));
|
|
1044
|
+
}
|
|
1045
|
+
return parts ? this.#renderActualStatusbarParts(width, parts) : [];
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
#renderActualStatusbarParts(width: number, parts: StatusLinePreviewParts): string[] {
|
|
1049
|
+
const barWidth = this.#simulationWidth(width);
|
|
1050
|
+
const indent = this.#simulationIndent(width);
|
|
1051
|
+
const leftSeparator = theme.fg("statusLineSep", ` ${parts.separator.left} `);
|
|
1052
|
+
const rightSeparator = theme.fg("statusLineSep", ` ${parts.separator.right} `);
|
|
1053
|
+
const left = parts.left.join(leftSeparator);
|
|
1054
|
+
const right = parts.right.join(rightSeparator);
|
|
1055
|
+
const combinedGap = Math.max(1, barWidth - visibleWidth(left) - visibleWidth(right));
|
|
1056
|
+
if (visibleWidth(left) + visibleWidth(right) + 1 <= barWidth) {
|
|
1057
|
+
return [this.#fit(`${indent}${left}${" ".repeat(combinedGap)}${right}`, width)];
|
|
1058
|
+
}
|
|
1059
|
+
return [
|
|
1060
|
+
this.#fit(
|
|
1061
|
+
`${indent}${this.#padEndVisible(truncateToWidth(left, barWidth, Ellipsis.Unicode), barWidth)}`,
|
|
1062
|
+
width,
|
|
1063
|
+
),
|
|
1064
|
+
this.#fit(
|
|
1065
|
+
`${indent}${this.#padStartVisible(truncateToWidth(right, barWidth, Ellipsis.Unicode), barWidth)}`,
|
|
1066
|
+
width,
|
|
1067
|
+
),
|
|
1068
|
+
];
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
#renderSlotRows(width: number, parts: StatusLinePreviewParts | undefined): string[] {
|
|
1072
|
+
const barWidth = this.#simulationWidth(width);
|
|
1073
|
+
const indent = this.#simulationIndent(width);
|
|
1074
|
+
const leftVisible = parts ? new Set(parts.leftIds) : undefined;
|
|
1075
|
+
const rightVisible = parts
|
|
1076
|
+
? new Set(parts.rightIds.filter((id): id is StatusLineSegmentId => id !== null))
|
|
1077
|
+
: undefined;
|
|
1078
|
+
const left = `${theme.bold("left")} ${this.#renderSide("left", leftVisible)}`;
|
|
1079
|
+
const rightExtras =
|
|
1080
|
+
parts?.right
|
|
1081
|
+
.filter((part, index) => parts.rightIds[index] === null && /^v\d/.test(Bun.stripANSI(part)))
|
|
1082
|
+
.map(part => theme.fg("dim", Bun.stripANSI(part))) ?? [];
|
|
1083
|
+
const rightCore = this.#renderSide("right", rightVisible);
|
|
1084
|
+
const right =
|
|
1085
|
+
rightExtras.length > 0
|
|
1086
|
+
? `${theme.bold("right")} ${rightCore}${theme.fg("statusLineSep", this.#separatorGlyph())}${rightExtras.join(
|
|
1087
|
+
theme.fg("statusLineSep", this.#separatorGlyph()),
|
|
1088
|
+
)}`
|
|
1089
|
+
: `${theme.bold("right")} ${rightCore}`;
|
|
1090
|
+
const combinedGap = Math.max(1, barWidth - visibleWidth(left) - visibleWidth(right));
|
|
1091
|
+
if (visibleWidth(left) + visibleWidth(right) + 1 <= barWidth) {
|
|
1092
|
+
return [this.#fit(`${indent}${left}${" ".repeat(combinedGap)}${right}`, width)];
|
|
1093
|
+
}
|
|
1094
|
+
return [
|
|
1095
|
+
this.#fit(
|
|
1096
|
+
`${indent}${this.#padEndVisible(truncateToWidth(left, barWidth, Ellipsis.Unicode), barWidth)}`,
|
|
1097
|
+
width,
|
|
1098
|
+
),
|
|
1099
|
+
this.#fit(
|
|
1100
|
+
`${indent}${this.#padStartVisible(truncateToWidth(right, barWidth, Ellipsis.Unicode), barWidth)}`,
|
|
1101
|
+
width,
|
|
1102
|
+
),
|
|
1103
|
+
];
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
#renderChoicePanel(width: number): string[] {
|
|
1107
|
+
if (this.#focus.kind !== "choice") return [];
|
|
1108
|
+
const focus = this.#focus;
|
|
1109
|
+
const descriptor = this.#descriptor(focus.target);
|
|
1110
|
+
if (!descriptor) return [];
|
|
1111
|
+
const values = descriptor.values.map((value, index) => {
|
|
1112
|
+
const selected = index === focus.focusedIndex;
|
|
1113
|
+
const active = value.value === descriptor.currentValue();
|
|
1114
|
+
const activeMarker = theme.getSymbolPreset() === "ascii" ? "v " : "✓ ";
|
|
1115
|
+
const label = `${active ? activeMarker : ""}${value.label}`;
|
|
1116
|
+
if (selected) return this.#focusBox(label);
|
|
1117
|
+
if (active) return this.#selectedBox(label);
|
|
1118
|
+
return label;
|
|
1119
|
+
});
|
|
1120
|
+
return [
|
|
1121
|
+
truncateToWidth(theme.bold(`Choices: ${descriptor.label}: ${values.join(" ")}`), width),
|
|
1122
|
+
truncateToWidth(theme.fg("dim", " Left/Right value · Up/Down option · Enter apply · Esc return"), width),
|
|
1123
|
+
];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
override render(width: number): string[] {
|
|
1127
|
+
if (this.#textInput) return this.#textInput.render(width);
|
|
1128
|
+
this.#normalizeFocus();
|
|
1129
|
+
const lines: string[] = [];
|
|
1130
|
+
lines.push(truncateToWidth(theme.bold(theme.fg("accent", "Status Line Custom Editor")), width));
|
|
1131
|
+
const focusLine = `Focus: ${this.#focusLabel()}`;
|
|
1132
|
+
lines.push(truncateToWidth(theme.fg("muted", focusLine), width));
|
|
1133
|
+
if (this.#picked) {
|
|
1134
|
+
lines.push(truncateToWidth(theme.fg("accent", `Selected: ${statusSegmentLabel(this.#picked.id)}`), width));
|
|
1135
|
+
}
|
|
1136
|
+
lines.push("");
|
|
1137
|
+
lines.push(this.#centerVisible(theme.bold("Simulated statusbar"), width));
|
|
1138
|
+
const actualStatusbarParts = this.#actualStatusbarParts(width);
|
|
1139
|
+
const actualStatusbar = this.#renderActualStatusbar(width, actualStatusbarParts);
|
|
1140
|
+
if (actualStatusbar.length > 0) {
|
|
1141
|
+
lines.push(...actualStatusbar);
|
|
1142
|
+
}
|
|
1143
|
+
const slotRows = this.#renderSlotRows(width, actualStatusbarParts);
|
|
1144
|
+
lines.push(...slotRows);
|
|
1145
|
+
if (actualStatusbar.length > 1 || slotRows.length > 1) {
|
|
1146
|
+
lines.push(this.#fit(theme.fg("warning", " Warning: statusbar wrapped to 2 rows"), width));
|
|
1147
|
+
if (this.#focus.kind === "statusbar") {
|
|
1148
|
+
const focused = this.#picked ? this.#picked.id : this.#focusSegment();
|
|
1149
|
+
if (focused) lines.push(this.#fit(` Focused target: ${statusSegmentLabel(focused)}`, width));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
lines.push("");
|
|
1153
|
+
const paletteSegments = this.#paletteSegments();
|
|
1154
|
+
const paletteTokens = paletteSegments.map((id, index) => {
|
|
1155
|
+
const selected = this.#picked?.id === id;
|
|
1156
|
+
const focused = this.#focus.kind === "palette" && this.#focus.index === index;
|
|
1157
|
+
const label = `{${statusSegmentLabel(id)}}`;
|
|
1158
|
+
if (selected) return this.#selectedBox(label);
|
|
1159
|
+
return focused ? this.#focusBox(label) : theme.fg("muted", label);
|
|
1160
|
+
});
|
|
1161
|
+
lines.push(truncateToWidth(theme.bold("Hidden segment palette"), width));
|
|
1162
|
+
lines.push(truncateToWidth(` ${paletteTokens.length ? paletteTokens.join(" ") : "(empty)"}`, width));
|
|
1163
|
+
if (width < 64 && this.#focus.kind === "palette") {
|
|
1164
|
+
const focusedPalette = paletteSegments[this.#focus.index];
|
|
1165
|
+
if (focusedPalette)
|
|
1166
|
+
lines.push(truncateToWidth(` Focused palette target: ${statusSegmentLabel(focusedPalette)}`, width));
|
|
1167
|
+
}
|
|
1168
|
+
lines.push("");
|
|
1169
|
+
lines.push(truncateToWidth(theme.bold("Visible choices"), width));
|
|
1170
|
+
const descriptors = this.#choiceDescriptors();
|
|
1171
|
+
const focusedDescriptor = descriptors.find(
|
|
1172
|
+
descriptor =>
|
|
1173
|
+
(this.#focus.kind === "separator-control" && descriptor.target === "separator") ||
|
|
1174
|
+
(this.#focus.kind === "option-control" && descriptor.target === this.#focus.path) ||
|
|
1175
|
+
(this.#focus.kind === "choice" && descriptor.target === this.#focus.target),
|
|
1176
|
+
);
|
|
1177
|
+
const summaryRows = [descriptors.slice(0, 6), descriptors.slice(6)];
|
|
1178
|
+
for (const row of summaryRows) {
|
|
1179
|
+
lines.push(
|
|
1180
|
+
truncateToWidth(
|
|
1181
|
+
` ${row
|
|
1182
|
+
.map(descriptor => {
|
|
1183
|
+
const focused = focusedDescriptor?.target === descriptor.target;
|
|
1184
|
+
return focused ? this.#focusBox(descriptor.label) : descriptor.label;
|
|
1185
|
+
})
|
|
1186
|
+
.join(" · ")}`,
|
|
1187
|
+
width,
|
|
1188
|
+
),
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const commandFocused = this.#focus.kind === "option-control" && this.#focus.path === "command.command";
|
|
1192
|
+
lines.push(
|
|
1193
|
+
truncateToWidth(` ${commandFocused ? this.#focusBox("Command: command") : "Command: command"}`, width),
|
|
1194
|
+
);
|
|
1195
|
+
if (focusedDescriptor) {
|
|
1196
|
+
const current = focusedDescriptor.currentValue();
|
|
1197
|
+
const choices = focusedDescriptor.values
|
|
1198
|
+
.map(value => (value.value === current ? `[${value.label}]` : value.label))
|
|
1199
|
+
.join(" | ");
|
|
1200
|
+
lines.push(
|
|
1201
|
+
truncateToWidth(`${theme.fg("accent", theme.nav.cursor)} ${focusedDescriptor.label}: ${choices}`, width),
|
|
1202
|
+
);
|
|
1203
|
+
} else if (commandFocused) {
|
|
1204
|
+
const command = this.#draft.segmentOptions.command?.command ?? "";
|
|
1205
|
+
lines.push(truncateToWidth(`${theme.fg("accent", theme.nav.cursor)} Command: ${command || "(empty)"}`, width));
|
|
1206
|
+
} else {
|
|
1207
|
+
const separator = descriptors[0];
|
|
1208
|
+
if (separator) {
|
|
1209
|
+
const current = separator.currentValue();
|
|
1210
|
+
const choices = separator.values
|
|
1211
|
+
.map(value => (value.value === current ? `[${value.label}]` : value.label))
|
|
1212
|
+
.join(" | ");
|
|
1213
|
+
lines.push(truncateToWidth(` ${separator.label}: ${choices}`, width));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
lines.push("");
|
|
1217
|
+
const confirm = this.#focus.kind === "confirm" ? theme.bold(theme.fg("accent", "[Confirm]")) : "[Confirm]";
|
|
1218
|
+
const exit = this.#focus.kind === "exit" ? theme.bold(theme.fg("accent", "[Exit]")) : "[Exit]";
|
|
1219
|
+
lines.push(truncateToWidth(` ${confirm} ${exit}`, width));
|
|
1220
|
+
lines.push(...this.#renderChoicePanel(width));
|
|
1221
|
+
lines.push(
|
|
1222
|
+
truncateToWidth(
|
|
1223
|
+
theme.fg(
|
|
1224
|
+
"dim",
|
|
1225
|
+
" Enter select/drop/apply · Left/Right segment/palette/value · Up/Down slots/palette/options/actions · Delete hide · Esc exit",
|
|
1226
|
+
),
|
|
1227
|
+
width,
|
|
1228
|
+
),
|
|
1229
|
+
);
|
|
1230
|
+
return lines.map(line => this.#fit(line, width));
|
|
553
1231
|
}
|
|
554
1232
|
|
|
555
1233
|
#setOption(path: string, value: string): void {
|
|
@@ -594,20 +1272,21 @@ class StatusLineCustomEditor extends Container {
|
|
|
594
1272
|
mode: value === "remaining" ? "remaining" : "used",
|
|
595
1273
|
};
|
|
596
1274
|
break;
|
|
1275
|
+
case "command.command":
|
|
1276
|
+
this.#draft.segmentOptions.command = {
|
|
1277
|
+
...(this.#draft.segmentOptions.command ?? {}),
|
|
1278
|
+
command: value,
|
|
1279
|
+
};
|
|
1280
|
+
break;
|
|
597
1281
|
}
|
|
598
1282
|
}
|
|
599
1283
|
|
|
600
1284
|
#preview(): void {
|
|
601
|
-
this.callbacks.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
segmentOptions: cloneSegmentOptions(this.#draft.segmentOptions),
|
|
607
|
-
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
608
|
-
maxRows: settings.get("statusLine.maxRows"),
|
|
609
|
-
previewHighlightSegment: this.#previewHighlightSegment,
|
|
610
|
-
});
|
|
1285
|
+
this.callbacks.onRenderRequested?.();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
#emitDraftToParentPreview(): void {
|
|
1289
|
+
this.callbacks.onStatusLinePreview?.(this.#draftPreviewSettings());
|
|
611
1290
|
}
|
|
612
1291
|
|
|
613
1292
|
#restorePreview(): void {
|
|
@@ -641,7 +1320,7 @@ class StatusLineCustomEditor extends Container {
|
|
|
641
1320
|
this.callbacks.onChange("statusLine.separator", this.#draft.separator);
|
|
642
1321
|
this.callbacks.onChange("statusLine.segmentOptions", cloneSegmentOptions(this.#draft.segmentOptions));
|
|
643
1322
|
this.#previewHighlightSegment = undefined;
|
|
644
|
-
this.#
|
|
1323
|
+
this.#emitDraftToParentPreview();
|
|
645
1324
|
this.done("saved");
|
|
646
1325
|
}
|
|
647
1326
|
|
|
@@ -651,7 +1330,124 @@ class StatusLineCustomEditor extends Container {
|
|
|
651
1330
|
}
|
|
652
1331
|
|
|
653
1332
|
handleInput(data: string): void {
|
|
654
|
-
|
|
1333
|
+
const kb = getKeybindings();
|
|
1334
|
+
const selectUp = kb.matches(data, "tui.select.up");
|
|
1335
|
+
const selectDown = kb.matches(data, "tui.select.down");
|
|
1336
|
+
const selectConfirm = kb.matches(data, "tui.select.confirm") || data === "\n";
|
|
1337
|
+
const selectCancel = kb.matches(data, "tui.select.cancel");
|
|
1338
|
+
if (this.#textInput) {
|
|
1339
|
+
if (selectCancel) this.#textInput.cancel();
|
|
1340
|
+
else if (selectConfirm) this.#textInput.submit();
|
|
1341
|
+
else this.#textInput.handleInput(data);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (this.#focus.kind === "choice") {
|
|
1345
|
+
if (matchesKey(data, "left")) {
|
|
1346
|
+
this.#moveVertical(-1);
|
|
1347
|
+
this.#updatePreviewHighlight();
|
|
1348
|
+
this.#preview();
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (matchesKey(data, "right")) {
|
|
1352
|
+
this.#moveVertical(1);
|
|
1353
|
+
this.#updatePreviewHighlight();
|
|
1354
|
+
this.#preview();
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (selectUp) {
|
|
1358
|
+
this.#moveChoiceTarget(-1);
|
|
1359
|
+
this.#updatePreviewHighlight();
|
|
1360
|
+
this.#preview();
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (selectDown) {
|
|
1364
|
+
this.#moveChoiceTarget(1);
|
|
1365
|
+
this.#updatePreviewHighlight();
|
|
1366
|
+
this.#preview();
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (selectConfirm) {
|
|
1370
|
+
this.#applyChoice();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if (selectCancel) {
|
|
1374
|
+
this.#focus = cloneRootFocus(this.#focus.returnFocus);
|
|
1375
|
+
this.#updatePreviewHighlight();
|
|
1376
|
+
this.#preview();
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (selectCancel) {
|
|
1383
|
+
this.#cancel();
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
if (selectUp) {
|
|
1387
|
+
this.#moveVertical(-1);
|
|
1388
|
+
this.#updatePreviewHighlight();
|
|
1389
|
+
this.#preview();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (selectDown) {
|
|
1393
|
+
this.#moveVertical(1);
|
|
1394
|
+
this.#updatePreviewHighlight();
|
|
1395
|
+
this.#preview();
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (matchesKey(data, "left")) {
|
|
1399
|
+
if (this.#focus.kind === "palette") this.#movePaletteFocus(-1);
|
|
1400
|
+
else if (this.#focus.kind === "separator-control" || this.#focus.kind === "option-control")
|
|
1401
|
+
this.#moveChoiceControl(-1);
|
|
1402
|
+
else if (this.#focus.kind === "confirm" || this.#focus.kind === "exit") {
|
|
1403
|
+
this.#focus = this.#focus.kind === "confirm" ? { kind: "exit" } : { kind: "confirm" };
|
|
1404
|
+
} else if (this.#picked) this.#movePickedSlot(-1);
|
|
1405
|
+
else this.#moveVisibleFocus(-1);
|
|
1406
|
+
this.#updatePreviewHighlight();
|
|
1407
|
+
this.#preview();
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (matchesKey(data, "right")) {
|
|
1411
|
+
if (this.#focus.kind === "palette") this.#movePaletteFocus(1);
|
|
1412
|
+
else if (this.#focus.kind === "separator-control" || this.#focus.kind === "option-control")
|
|
1413
|
+
this.#moveChoiceControl(1);
|
|
1414
|
+
else if (this.#focus.kind === "confirm" || this.#focus.kind === "exit") {
|
|
1415
|
+
this.#focus = this.#focus.kind === "confirm" ? { kind: "exit" } : { kind: "confirm" };
|
|
1416
|
+
} else if (this.#picked) this.#movePickedSlot(1);
|
|
1417
|
+
else this.#moveVisibleFocus(1);
|
|
1418
|
+
this.#updatePreviewHighlight();
|
|
1419
|
+
this.#preview();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (isDeleteKey(data)) {
|
|
1423
|
+
this.#hideFocusedSegment();
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (selectConfirm) {
|
|
1427
|
+
switch (this.#focus.kind) {
|
|
1428
|
+
case "statusbar":
|
|
1429
|
+
if (this.#picked) this.#dropPicked();
|
|
1430
|
+
else this.#pickFocusedStatusbar();
|
|
1431
|
+
return;
|
|
1432
|
+
case "palette":
|
|
1433
|
+
if (this.#picked) this.#dropPickedToPalette();
|
|
1434
|
+
else this.#pickFocusedPalette();
|
|
1435
|
+
return;
|
|
1436
|
+
case "separator-control":
|
|
1437
|
+
this.#openChoice("separator", { kind: "separator-control" });
|
|
1438
|
+
return;
|
|
1439
|
+
case "option-control":
|
|
1440
|
+
if (this.#focus.path === "command.command") this.#openCommandInput();
|
|
1441
|
+
else this.#openChoice(this.#focus.path, { kind: "option-control", path: this.#focus.path });
|
|
1442
|
+
return;
|
|
1443
|
+
case "confirm":
|
|
1444
|
+
this.#save();
|
|
1445
|
+
return;
|
|
1446
|
+
case "exit":
|
|
1447
|
+
this.#cancel();
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
655
1451
|
}
|
|
656
1452
|
}
|
|
657
1453
|
|
|
@@ -704,6 +1500,7 @@ export interface StatusLinePreviewSettings {
|
|
|
704
1500
|
separator?: StatusLineSeparatorStyle;
|
|
705
1501
|
segmentOptions?: StatusLineSegmentOptions;
|
|
706
1502
|
previewHighlightSegment?: StatusLineSegmentId;
|
|
1503
|
+
previewHighlightStyle?: "focus" | "selected";
|
|
707
1504
|
sessionAccent?: boolean;
|
|
708
1505
|
maxRows?: number;
|
|
709
1506
|
}
|
|
@@ -733,6 +1530,13 @@ export interface SettingsCallbacks {
|
|
|
733
1530
|
onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
|
|
734
1531
|
/** Get current rendered status line for inline preview */
|
|
735
1532
|
getStatusLinePreview?: (width?: number) => string;
|
|
1533
|
+
/** Render a status-line preview for supplied draft settings without mutating the live status line. */
|
|
1534
|
+
getStatusLinePreviewForSettings?: (settings: StatusLinePreviewSettings, width?: number) => string;
|
|
1535
|
+
/** Render status-line segment groups for supplied draft settings without mutating the live status line. */
|
|
1536
|
+
getStatusLinePreviewPartsForSettings?: (
|
|
1537
|
+
settings: StatusLinePreviewSettings,
|
|
1538
|
+
width?: number,
|
|
1539
|
+
) => StatusLinePreviewParts;
|
|
736
1540
|
/** Called when plugins change */
|
|
737
1541
|
onPluginsChanged?: () => void;
|
|
738
1542
|
/** Called when asynchronously rebuilt settings content needs a repaint. */
|
|
@@ -1229,14 +2033,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1229
2033
|
|
|
1230
2034
|
// Add status line preview for appearance tab
|
|
1231
2035
|
if (tabId === "appearance") {
|
|
1232
|
-
this.#statusPreviewContainer =
|
|
1233
|
-
this.#statusPreviewContainer.addChild(new Spacer(1));
|
|
1234
|
-
this.#statusPreviewContainer.addChild(
|
|
1235
|
-
new DynamicThemeText(() => theme.fg("muted", uiString(settings.get("ui.language"), "settings.preview"))),
|
|
1236
|
-
);
|
|
1237
|
-
this.#statusPreviewText = new Text(this.#getStatusPreviewString(), 0, 0);
|
|
1238
|
-
this.#statusPreviewContainer.addChild(this.#statusPreviewText);
|
|
1239
|
-
this.#statusPreviewContainer.addChild(new Spacer(1));
|
|
2036
|
+
this.#statusPreviewContainer = this.#createStatusPreviewContainer();
|
|
1240
2037
|
this.addChild(this.#statusPreviewContainer);
|
|
1241
2038
|
}
|
|
1242
2039
|
|
|
@@ -1299,6 +2096,39 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1299
2096
|
this.addChild(this.#currentList);
|
|
1300
2097
|
}
|
|
1301
2098
|
|
|
2099
|
+
#createStatusPreviewContainer(): Container {
|
|
2100
|
+
const container = new Container();
|
|
2101
|
+
container.addChild(new Spacer(1));
|
|
2102
|
+
container.addChild(
|
|
2103
|
+
new DynamicThemeText(() => theme.fg("muted", uiString(settings.get("ui.language"), "settings.preview"))),
|
|
2104
|
+
);
|
|
2105
|
+
this.#statusPreviewText = new Text(this.#getStatusPreviewString(), 0, 0);
|
|
2106
|
+
container.addChild(this.#statusPreviewText);
|
|
2107
|
+
container.addChild(new Spacer(1));
|
|
2108
|
+
return container;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
#hideStatusPreview(): void {
|
|
2112
|
+
if (!this.#statusPreviewContainer) return;
|
|
2113
|
+
this.removeChild(this.#statusPreviewContainer);
|
|
2114
|
+
this.#statusPreviewContainer = null;
|
|
2115
|
+
this.#statusPreviewText = null;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
#showStatusPreview(): void {
|
|
2119
|
+
if (this.#statusPreviewContainer || this.#currentTabId !== "appearance") return;
|
|
2120
|
+
const container = this.#createStatusPreviewContainer();
|
|
2121
|
+
this.#statusPreviewContainer = container;
|
|
2122
|
+
const children = [...this.children];
|
|
2123
|
+
const listIndex = this.#currentList ? children.indexOf(this.#currentList) : -1;
|
|
2124
|
+
if (listIndex >= 0) {
|
|
2125
|
+
children.splice(listIndex, 0, container);
|
|
2126
|
+
this.replaceChildren(children);
|
|
2127
|
+
} else {
|
|
2128
|
+
this.addChild(container);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
1302
2132
|
/** Map a definition list to UI items, dropping any whose condition is false. */
|
|
1303
2133
|
#buildItemsForDefs(defs: SettingDef[]): SettingItem[] {
|
|
1304
2134
|
const items: SettingItem[] = [];
|
|
@@ -1341,9 +2171,15 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1341
2171
|
id: STATUS_LINE_CUSTOM_EDITOR_ID,
|
|
1342
2172
|
label: "Status Line Custom Editor",
|
|
1343
2173
|
description:
|
|
1344
|
-
"Edit custom status line segments, placement, separator, and typed segment options
|
|
2174
|
+
"Edit custom status line segments, placement, separator, and typed segment options in a simulated statusbar.",
|
|
1345
2175
|
currentValue: "open",
|
|
1346
|
-
submenu: (_currentValue, done) =>
|
|
2176
|
+
submenu: (_currentValue, done) => {
|
|
2177
|
+
this.#hideStatusPreview();
|
|
2178
|
+
return new StatusLineCustomEditor(customEditorCallbacks, value => {
|
|
2179
|
+
this.#showStatusPreview();
|
|
2180
|
+
done(value);
|
|
2181
|
+
});
|
|
2182
|
+
},
|
|
1347
2183
|
};
|
|
1348
2184
|
const presetIndex = items.findIndex(item => item.id === "statusLine.preset");
|
|
1349
2185
|
if (presetIndex >= 0) {
|
|
@@ -1488,6 +2324,10 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1488
2324
|
// Handle tab switching — but NOT when a text input is active, since
|
|
1489
2325
|
// arrow keys must reach the cursor and Tab must not switch tabs.
|
|
1490
2326
|
if (!this.#textInputActive && tabNavigation) {
|
|
2327
|
+
if (this.#currentList?.navigationLocked) {
|
|
2328
|
+
this.#currentList.handleInput(data);
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
1491
2331
|
this.#tabBar.handleInput(data);
|
|
1492
2332
|
return;
|
|
1493
2333
|
}
|