@gajae-code/coding-agent 0.16.3 → 0.16.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- 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/file-lock.d.ts +31 -3
- 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/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +35 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- 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/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- 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 +3 -0
- package/dist/types/modes/types.d.ts +7 -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/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- 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/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +26 -1
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +112 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +10 -0
- package/dist/types/task/executor.d.ts +5 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +10 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +9 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +11 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli/root-flags.ts +5 -3
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +379 -31
- package/src/config/model-preset-registry.ts +120 -19
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +73 -23
- 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/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- 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/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +52 -55
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +307 -34
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +228 -60
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- 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/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +149 -190
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +73 -25
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +143 -10
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +12 -12
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +78 -35
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/session-observer-overlay.ts +1 -1
- 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 +396 -22
- package/src/modes/controllers/command-controller.ts +3 -0
- package/src/modes/controllers/event-controller.ts +151 -7
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +53 -6
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +17 -3
- package/src/modes/utils/ui-helpers.ts +2 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +189 -14
- package/src/runtime-mcp/pool.test.ts +218 -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/broker/broker.ts +124 -32
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +478 -49
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +11 -8
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +52 -10
- package/src/sdk/host/session-runtime.ts +383 -44
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +77 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +228 -55
- package/src/sdk/transport/relay.ts +35 -13
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +240 -90
- package/src/session/contribution-prep.ts +214 -21
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +456 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +29 -3
- package/src/task/executor.ts +7 -1
- package/src/task/index.ts +21 -4
- package/src/tools/bash.ts +63 -19
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +10 -0
- package/src/tools/job.ts +21 -4
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/search.ts +116 -58
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +37 -2
- package/src/tools/subagent.ts +6 -3
- package/src/tools/tool-catalog.generated.ts +10 -3
- package/src/tools/ultragoal-ask-guard.ts +2 -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
|
}
|