@groeponline/pi-wishcraft 0.17.3

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.
Files changed (106) hide show
  1. package/AGENTS.md +68 -0
  2. package/CHANGELOG.md +724 -0
  3. package/CONTRIBUTING.md +37 -0
  4. package/README.md +648 -0
  5. package/RELEASE.md +117 -0
  6. package/ROADMAP.md +52 -0
  7. package/bash-mode/completion-providers.ts +269 -0
  8. package/bash-mode/completion.ts +416 -0
  9. package/bash-mode/editor-ghost.ts +40 -0
  10. package/bash-mode/editor-input.ts +80 -0
  11. package/bash-mode/editor.ts +437 -0
  12. package/bash-mode/history.ts +263 -0
  13. package/bash-mode/shell-session.ts +286 -0
  14. package/bash-mode/transcript.ts +108 -0
  15. package/bash-mode/types.ts +80 -0
  16. package/index.ts +6 -0
  17. package/package.json +55 -0
  18. package/queue/store.ts +443 -0
  19. package/queue/types.ts +54 -0
  20. package/src/config/custom-items.ts +182 -0
  21. package/src/config/extension-statuses.ts +51 -0
  22. package/src/config/layout.ts +60 -0
  23. package/src/config/parse.ts +127 -0
  24. package/src/config/powerline-config.ts +18 -0
  25. package/src/config/presets.ts +245 -0
  26. package/src/config/primitives.ts +117 -0
  27. package/src/config/segment-ids.ts +114 -0
  28. package/src/config/segment-options.ts +128 -0
  29. package/src/config/settings-patch.ts +26 -0
  30. package/src/config/types.ts +277 -0
  31. package/src/core/frontmatter.ts +40 -0
  32. package/src/editor/autocomplete-chain.ts +41 -0
  33. package/src/extension/activate.ts +28 -0
  34. package/src/extension/bash-mode-actions.ts +104 -0
  35. package/src/extension/commands.ts +268 -0
  36. package/src/extension/constants.ts +46 -0
  37. package/src/extension/custom-editor.ts +406 -0
  38. package/src/extension/git-invalidation.ts +40 -0
  39. package/src/extension/layout.ts +160 -0
  40. package/src/extension/menu-views.ts +393 -0
  41. package/src/extension/powerline-widgets.ts +95 -0
  42. package/src/extension/prompt-history.ts +219 -0
  43. package/src/extension/queue-commands.ts +245 -0
  44. package/src/extension/queue-context.ts +12 -0
  45. package/src/extension/queue-integration.ts +434 -0
  46. package/src/extension/segment-context.ts +212 -0
  47. package/src/extension/session-lifecycle.ts +373 -0
  48. package/src/extension/settings-io.ts +202 -0
  49. package/src/extension/shortcuts-config.ts +357 -0
  50. package/src/extension/shortcuts-router.ts +383 -0
  51. package/src/extension/skills/inline-invocation.ts +174 -0
  52. package/src/extension/skills/ook.md +6 -0
  53. package/src/extension/skills/test.md +6 -0
  54. package/src/extension/stale-context.ts +10 -0
  55. package/src/extension/stash-history.ts +103 -0
  56. package/src/extension/state.ts +159 -0
  57. package/src/extension/status-line-renderers.ts +222 -0
  58. package/src/extension/types.ts +97 -0
  59. package/src/extension/vibe-command.ts +160 -0
  60. package/src/extension/welcome-control.ts +27 -0
  61. package/src/extension/welcome-integration.ts +153 -0
  62. package/src/git/status.ts +332 -0
  63. package/src/paths/agent-dirs.ts +67 -0
  64. package/src/render/timer.ts +46 -0
  65. package/src/segments/core.ts +256 -0
  66. package/src/segments/custom.ts +114 -0
  67. package/src/segments/index.ts +3 -0
  68. package/src/segments/registry.ts +87 -0
  69. package/src/segments/shared.ts +36 -0
  70. package/src/segments/system.ts +235 -0
  71. package/src/segments/usage.ts +178 -0
  72. package/src/shell/cd-command.ts +190 -0
  73. package/src/shortcuts/matching.ts +61 -0
  74. package/src/theme/colors.ts +60 -0
  75. package/src/theme/icons.ts +175 -0
  76. package/src/theme/separators.ts +41 -0
  77. package/src/theme/theme.ts +211 -0
  78. package/src/tools/graph.ts +75 -0
  79. package/src/tools/patch.ts +179 -0
  80. package/src/tools/ripgrep.ts +104 -0
  81. package/src/usage/context.ts +97 -0
  82. package/src/usage/ledger.ts +293 -0
  83. package/src/usage/rates.ts +155 -0
  84. package/src/welcome/auto-dismiss.ts +43 -0
  85. package/src/welcome/banner.ts +68 -0
  86. package/src/welcome/discover.ts +234 -0
  87. package/src/welcome/format.ts +18 -0
  88. package/src/welcome/index.ts +5 -0
  89. package/src/welcome/layout.ts +36 -0
  90. package/src/welcome/overlay.ts +80 -0
  91. package/src/welcome/renderer.ts +157 -0
  92. package/src/welcome/sessions.ts +107 -0
  93. package/src/welcome/types.ts +41 -0
  94. package/src/welcome/widgets/graph-widget.ts +25 -0
  95. package/src/welcome/widgets/index.ts +20 -0
  96. package/src/welcome/widgets/queue-widget.ts +26 -0
  97. package/src/welcome/widgets/sessions-widget.ts +23 -0
  98. package/src/welcome/widgets/shortcuts-widget.ts +17 -0
  99. package/src/welcome/widgets/system-widget.ts +29 -0
  100. package/src/working-vibes/generate.ts +144 -0
  101. package/src/working-vibes/index.ts +24 -0
  102. package/src/working-vibes/manager.ts +198 -0
  103. package/src/working-vibes/provider.ts +163 -0
  104. package/src/working-vibes/storage.ts +357 -0
  105. package/theme.example.json +24 -0
  106. package/tsconfig.json +13 -0
@@ -0,0 +1,51 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { CustomStatusItem } from "./types.ts";
3
+
4
+ export function collectHiddenExtensionStatusKeys(
5
+ customItems: readonly CustomStatusItem[],
6
+ ): Set<string> {
7
+ const hidden = new Set<string>();
8
+ for (const item of customItems) {
9
+ if (item.excludeFromExtensionStatuses) hidden.add(item.statusKey);
10
+ }
11
+ return hidden;
12
+ }
13
+
14
+ export function isNotificationExtensionStatus(value: string): boolean {
15
+ return value.trimStart().startsWith("[");
16
+ }
17
+
18
+ export function getNotificationExtensionStatuses(
19
+ statuses: ReadonlyMap<string, string>,
20
+ hiddenKeys: ReadonlySet<string>,
21
+ ): string[] {
22
+ const notifications: string[] = [];
23
+ for (const [statusKey, value] of statuses.entries()) {
24
+ if (
25
+ hiddenKeys.has(statusKey) ||
26
+ !value ||
27
+ !isNotificationExtensionStatus(value)
28
+ ) {
29
+ continue;
30
+ }
31
+ notifications.push(value);
32
+ }
33
+ return notifications;
34
+ }
35
+
36
+ export function normalizeExtensionStatusValue(value: string): string | null {
37
+ if (!value || visibleWidth(value) <= 0) {
38
+ return null;
39
+ }
40
+
41
+ const stripped = value.replace(/(\x1b\[[0-9;]*m|\s|·|[|])+$/, "");
42
+ return visibleWidth(stripped) > 0 ? stripped : null;
43
+ }
44
+
45
+ export function normalizeCompactExtensionStatus(value: string): string | null {
46
+ if (isNotificationExtensionStatus(value)) {
47
+ return null;
48
+ }
49
+
50
+ return normalizeExtensionStatusValue(value);
51
+ }
@@ -0,0 +1,60 @@
1
+ import type {
2
+ CustomItemPosition,
3
+ CustomStatusItem,
4
+ PresetDef,
5
+ StatusLineLayout,
6
+ StatusLineSegmentId,
7
+ } from "./types.ts";
8
+
9
+ export function mergeSegmentsWithCustomItems(
10
+ presetDef: PresetDef,
11
+ customItems: readonly CustomStatusItem[],
12
+ options: {
13
+ layout?: StatusLineLayout | null;
14
+ disabledSegments?: readonly StatusLineSegmentId[];
15
+ } = {},
16
+ ): {
17
+ leftSegments: StatusLineSegmentId[];
18
+ rightSegments: StatusLineSegmentId[];
19
+ secondarySegments: StatusLineSegmentId[];
20
+ } {
21
+ const layout = options.layout ?? null;
22
+ const explicitlyPlaced = new Set([
23
+ ...(layout?.left ?? []),
24
+ ...(layout?.right ?? []),
25
+ ...(layout?.secondary ?? []),
26
+ ]);
27
+ const disabled = new Set(options.disabledSegments ?? []);
28
+
29
+ const buildRow = (
30
+ position: CustomItemPosition,
31
+ configured: StatusLineSegmentId[] | undefined,
32
+ presetSegments: readonly StatusLineSegmentId[],
33
+ ): StatusLineSegmentId[] => {
34
+ const segments =
35
+ configured !== undefined
36
+ ? [...configured]
37
+ : presetSegments.filter((id) => !explicitlyPlaced.has(id));
38
+
39
+ if (configured === undefined) {
40
+ for (const item of customItems) {
41
+ const segmentId: StatusLineSegmentId = `custom:${item.id}`;
42
+ if (item.position === position && !explicitlyPlaced.has(segmentId)) {
43
+ segments.push(segmentId);
44
+ }
45
+ }
46
+ }
47
+
48
+ return segments.filter((id) => !disabled.has(id));
49
+ };
50
+
51
+ return {
52
+ leftSegments: buildRow("left", layout?.left, presetDef.leftSegments),
53
+ rightSegments: buildRow("right", layout?.right, presetDef.rightSegments),
54
+ secondarySegments: buildRow(
55
+ "secondary",
56
+ layout?.secondary,
57
+ presetDef.secondarySegments ?? [],
58
+ ),
59
+ };
60
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ normalizeCustomItems,
3
+ normalizeCustomPresets,
4
+ normalizeCustomSegments,
5
+ } from "./custom-items.ts";
6
+ import {
7
+ isRecord,
8
+ normalizeCaptureSigil,
9
+ normalizePlacement,
10
+ normalizePreset,
11
+ normalizeSegmentLabels,
12
+ normalizeSeparator,
13
+ } from "./primitives.ts";
14
+ import { normalizeDisabledSegments, normalizeLayout } from "./segment-ids.ts";
15
+ import { normalizeSegmentOptions } from "./segment-options.ts";
16
+ import type {
17
+ CustomSegmentConfig,
18
+ CustomStatusItem,
19
+ PowerlinePlacement,
20
+ StatusLineLayout,
21
+ StatusLinePreset,
22
+ StatusLineSegmentId,
23
+ StatusLineSegmentOptions,
24
+ StatusLineSeparatorStyle,
25
+ } from "./types.ts";
26
+
27
+ export interface PowerlineConfig {
28
+ preset: StatusLinePreset;
29
+ customItems: CustomStatusItem[];
30
+ disabledSegments: StatusLineSegmentId[];
31
+ invalidDisabledSegments: string[];
32
+ layout: StatusLineLayout | null;
33
+ invalidLayoutSegments: string[];
34
+ separator: StatusLineSeparatorStyle | null;
35
+ segmentOptions: StatusLineSegmentOptions;
36
+ placement: PowerlinePlacement;
37
+ invalidPlacement: string | null;
38
+ welcome: boolean;
39
+ stashSharpSShortcut: boolean;
40
+ queue: { captureSigil: string | false };
41
+ /** User-defined computed segments (command/env/static), keyed by id */
42
+ segments: Record<string, CustomSegmentConfig>;
43
+ /** User-defined presets, keyed by name */
44
+ presets: Record<string, import("./types.ts").CustomPresetConfig>;
45
+ /** Per-segment custom text label shown before the value (e.g. tps -> "speed"). */
46
+ segmentLabels: Record<string, string>;
47
+ }
48
+
49
+ export function parsePowerlineConfig(
50
+ value: unknown,
51
+ presets: readonly StatusLinePreset[],
52
+ ): PowerlineConfig {
53
+ const defaultConfig: PowerlineConfig = {
54
+ preset: "default",
55
+ customItems: [],
56
+ disabledSegments: [],
57
+ invalidDisabledSegments: [],
58
+ layout: null,
59
+ invalidLayoutSegments: [],
60
+ separator: null,
61
+ segmentOptions: {},
62
+ placement: "above",
63
+ invalidPlacement: null,
64
+ welcome: true,
65
+ stashSharpSShortcut: false,
66
+ queue: { captureSigil: "#" },
67
+ segments: {},
68
+ presets: {},
69
+ segmentLabels: {},
70
+ };
71
+
72
+ const directPreset = normalizePreset(value, presets);
73
+ if (directPreset) return { ...defaultConfig, preset: directPreset };
74
+
75
+ if (!isRecord(value)) return defaultConfig;
76
+
77
+ const customItems = normalizeCustomItems(value.customItems);
78
+ const customSegments = normalizeCustomSegments(value.segments);
79
+ const customSegmentIds = new Set(Object.keys(customSegments));
80
+ const { disabledSegments, invalidDisabledSegments } =
81
+ normalizeDisabledSegments(
82
+ value.disabledSegments,
83
+ customItems,
84
+ customSegmentIds,
85
+ );
86
+ const { layout, invalidLayoutSegments } = normalizeLayout(
87
+ value.layout,
88
+ customItems,
89
+ customSegmentIds,
90
+ );
91
+ const { placement, invalidPlacement } = normalizePlacement(value.placement);
92
+ const queue = isRecord(value.queue)
93
+ ? { captureSigil: normalizeCaptureSigil(value.queue.captureSigil) }
94
+ : defaultConfig.queue;
95
+ const customItemIds = new Set(customItems.map((item) => item.id));
96
+ const customPresetDefs = normalizeCustomPresets(
97
+ value.presets,
98
+ customItemIds,
99
+ customSegmentIds,
100
+ );
101
+ const requestedPreset =
102
+ typeof value.preset === "string" ? value.preset.trim().toLowerCase() : "";
103
+ const preset =
104
+ normalizePreset(value.preset, presets) ??
105
+ (Object.prototype.hasOwnProperty.call(customPresetDefs, requestedPreset)
106
+ ? (requestedPreset as StatusLinePreset)
107
+ : defaultConfig.preset);
108
+
109
+ return {
110
+ preset,
111
+ customItems,
112
+ disabledSegments,
113
+ invalidDisabledSegments,
114
+ layout,
115
+ invalidLayoutSegments,
116
+ separator: normalizeSeparator(value.separator),
117
+ segmentOptions: normalizeSegmentOptions(value),
118
+ placement,
119
+ invalidPlacement,
120
+ welcome: value.welcome !== false,
121
+ stashSharpSShortcut: value.stashSharpSShortcut === true,
122
+ queue,
123
+ segments: customSegments,
124
+ presets: customPresetDefs,
125
+ segmentLabels: normalizeSegmentLabels(value.segmentLabels),
126
+ };
127
+ }
@@ -0,0 +1,18 @@
1
+ // Barrel file: this module's implementation has been split into cohesive
2
+ // internal files below. Re-exported here to keep a stable public import
3
+ // path (`./config/powerline-config.ts`) for the rest of the codebase.
4
+ export type { PowerlineConfig } from "./parse.ts";
5
+ export { parsePowerlineConfig } from "./parse.ts";
6
+ export { mergeSegmentOptions } from "./segment-options.ts";
7
+ export { mergeSegmentsWithCustomItems } from "./layout.ts";
8
+ export {
9
+ nextPowerlineSettingWithPreset,
10
+ nextPowerlineSettingWithOptions,
11
+ } from "./settings-patch.ts";
12
+ export {
13
+ collectHiddenExtensionStatusKeys,
14
+ isNotificationExtensionStatus,
15
+ getNotificationExtensionStatuses,
16
+ normalizeExtensionStatusValue,
17
+ normalizeCompactExtensionStatus,
18
+ } from "./extension-statuses.ts";
@@ -0,0 +1,245 @@
1
+ import type { ColorScheme, PresetDef, StatusLinePreset } from "./types.ts";
2
+ import { getDefaultColors } from "../theme/theme.ts";
3
+ import type { CustomPresetConfig } from "./types.ts";
4
+
5
+ // Get base colors from theme.ts (single source of truth)
6
+ const DEFAULT_COLORS: ColorScheme = getDefaultColors();
7
+
8
+ // Minimal - more muted, less colorful
9
+ const MINIMAL_COLORS: ColorScheme = {
10
+ ...DEFAULT_COLORS,
11
+ model: "text",
12
+ path: "text",
13
+ gitClean: "dim",
14
+ };
15
+
16
+ // Nerd - vibrant colors
17
+ const NERD_COLORS: ColorScheme = {
18
+ ...DEFAULT_COLORS,
19
+ model: "accent",
20
+ path: "success",
21
+ tokens: "muted",
22
+ cost: "warning",
23
+ };
24
+
25
+ const CHEF_COLORS: ColorScheme = {
26
+ ...DEFAULT_COLORS,
27
+ model: "text",
28
+ path: "text",
29
+ gitClean: "dim",
30
+ queue: "dim",
31
+ };
32
+
33
+ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
34
+ default: {
35
+ leftSegments: [
36
+ "model",
37
+ "thinking",
38
+ "shell_mode",
39
+ "path",
40
+ "git",
41
+ "queue",
42
+ "context_pct",
43
+ "cache_read",
44
+ "cost",
45
+ ],
46
+ rightSegments: [],
47
+ secondarySegments: ["extension_statuses"],
48
+ separator: "powerline-thin",
49
+ colors: DEFAULT_COLORS,
50
+ segmentOptions: {
51
+ model: { showThinkingLevel: false },
52
+ path: { mode: "basename" },
53
+ git: {
54
+ showBranch: true,
55
+ showStaged: true,
56
+ showUnstaged: true,
57
+ showUntracked: true,
58
+ },
59
+ },
60
+ },
61
+
62
+ minimal: {
63
+ leftSegments: ["shell_mode", "path", "git"],
64
+ rightSegments: ["context_pct"],
65
+ separator: "slash",
66
+ colors: MINIMAL_COLORS,
67
+ segmentOptions: {
68
+ path: { mode: "basename" },
69
+ git: {
70
+ showBranch: true,
71
+ showStaged: false,
72
+ showUnstaged: false,
73
+ showUntracked: false,
74
+ },
75
+ },
76
+ },
77
+
78
+ compact: {
79
+ leftSegments: ["model", "shell_mode", "git"],
80
+ rightSegments: ["queue", "cost", "context_pct"],
81
+ separator: "powerline-thin",
82
+ colors: DEFAULT_COLORS,
83
+ segmentOptions: {
84
+ model: { showThinkingLevel: false },
85
+ git: {
86
+ showBranch: true,
87
+ showStaged: true,
88
+ showUnstaged: true,
89
+ showUntracked: false,
90
+ },
91
+ },
92
+ },
93
+
94
+ full: {
95
+ leftSegments: [
96
+ "hostname",
97
+ "model",
98
+ "thinking",
99
+ "shell_mode",
100
+ "path",
101
+ "git",
102
+ "queue",
103
+ "subagents",
104
+ ],
105
+ rightSegments: [
106
+ "token_in",
107
+ "token_out",
108
+ "cache_read",
109
+ "cost",
110
+ "context_pct",
111
+ "time_spent",
112
+ "time",
113
+ "extension_statuses",
114
+ ],
115
+ separator: "powerline",
116
+ colors: DEFAULT_COLORS,
117
+ segmentOptions: {
118
+ model: { showThinkingLevel: false },
119
+ path: { mode: "abbreviated", maxLength: 50 },
120
+ git: {
121
+ showBranch: true,
122
+ showStaged: true,
123
+ showUnstaged: true,
124
+ showUntracked: true,
125
+ },
126
+ time: { format: "24h", showSeconds: false },
127
+ },
128
+ },
129
+
130
+ nerd: {
131
+ leftSegments: [
132
+ "hostname",
133
+ "model",
134
+ "thinking",
135
+ "shell_mode",
136
+ "path",
137
+ "git",
138
+ "queue",
139
+ "session",
140
+ "subagents",
141
+ ],
142
+ rightSegments: [
143
+ "token_in",
144
+ "token_out",
145
+ "cache_read",
146
+ "cache_write",
147
+ "cost",
148
+ "context_pct",
149
+ "context_total",
150
+ "time_spent",
151
+ "time",
152
+ "extension_statuses",
153
+ ],
154
+ separator: "powerline",
155
+ colors: NERD_COLORS,
156
+ segmentOptions: {
157
+ model: { showThinkingLevel: false },
158
+ path: { mode: "abbreviated", maxLength: 60 },
159
+ git: {
160
+ showBranch: true,
161
+ showStaged: true,
162
+ showUnstaged: true,
163
+ showUntracked: true,
164
+ },
165
+ time: { format: "24h", showSeconds: true },
166
+ },
167
+ },
168
+
169
+ ascii: {
170
+ leftSegments: ["model", "shell_mode", "path", "git"],
171
+ rightSegments: ["queue", "token_total", "cost", "context_pct"],
172
+ separator: "ascii",
173
+ colors: MINIMAL_COLORS,
174
+ segmentOptions: {
175
+ model: { showThinkingLevel: true },
176
+ path: { mode: "abbreviated", maxLength: 40 },
177
+ git: {
178
+ showBranch: true,
179
+ showStaged: true,
180
+ showUnstaged: true,
181
+ showUntracked: true,
182
+ },
183
+ },
184
+ },
185
+
186
+ chef: {
187
+ leftSegments: [
188
+ "hostname",
189
+ "model",
190
+ "thinking",
191
+ "shell_mode",
192
+ "path",
193
+ "git",
194
+ "queue",
195
+ ],
196
+ rightSegments: ["tps", "open_ports", "cost", "context_pct", "time"],
197
+ separator: "slash",
198
+ colors: CHEF_COLORS,
199
+ segmentOptions: {
200
+ model: { showThinkingLevel: false },
201
+ path: { mode: "basename" },
202
+ git: {
203
+ showBranch: true,
204
+ showStaged: true,
205
+ showUnstaged: true,
206
+ showUntracked: true,
207
+ },
208
+ time: { format: "24h", showSeconds: false },
209
+ },
210
+ },
211
+ };
212
+
213
+ export function getPreset(name: string): PresetDef {
214
+ return resolvePreset(name);
215
+ }
216
+
217
+ // User-defined presets (settings), merged over built-ins at config time.
218
+ const customPresets = new Map<string, PresetDef>();
219
+
220
+ /** Register user-defined presets from settings. Replaces any previously registered. */
221
+ export function registerCustomPresets(
222
+ defs: Record<string, CustomPresetConfig>,
223
+ ): void {
224
+ customPresets.clear();
225
+ for (const [name, def] of Object.entries(defs)) {
226
+ const presetDef: PresetDef = {
227
+ leftSegments: def.left ?? [],
228
+ rightSegments: def.right ?? [],
229
+ secondarySegments: def.secondary,
230
+ separator: def.separator ?? "slash",
231
+ segmentOptions: def.segmentOptions,
232
+ colors: def.colors,
233
+ };
234
+ customPresets.set(name, presetDef);
235
+ }
236
+ }
237
+
238
+ /** Resolve a preset by name, checking user-defined presets first. */
239
+ export function resolvePreset(name: string): PresetDef {
240
+ return (
241
+ customPresets.get(name) ??
242
+ PRESETS[name as StatusLinePreset] ??
243
+ PRESETS.default
244
+ );
245
+ }
@@ -0,0 +1,117 @@
1
+ import type {
2
+ ColorValue,
3
+ CustomItemPosition,
4
+ PowerlinePlacement,
5
+ StatusLinePreset,
6
+ StatusLineSeparatorStyle,
7
+ } from "./types.ts";
8
+
9
+ export function isRecord(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ export function normalizePreset(
14
+ value: unknown,
15
+ presets: readonly StatusLinePreset[],
16
+ ): StatusLinePreset | null {
17
+ if (typeof value !== "string") return null;
18
+ const normalized = value.trim().toLowerCase();
19
+ return (presets as readonly string[]).includes(normalized)
20
+ ? (normalized as StatusLinePreset)
21
+ : null;
22
+ }
23
+
24
+ export function normalizePlacement(value: unknown): {
25
+ placement: PowerlinePlacement;
26
+ invalidPlacement: string | null;
27
+ } {
28
+ if (value === undefined)
29
+ return { placement: "above", invalidPlacement: null };
30
+
31
+ const normalized =
32
+ typeof value === "string" ? value.trim().toLowerCase() : "";
33
+ if (normalized === "above" || normalized === "below") {
34
+ return { placement: normalized, invalidPlacement: null };
35
+ }
36
+
37
+ return {
38
+ placement: "above",
39
+ invalidPlacement: typeof value === "string" ? value.trim() : String(value),
40
+ };
41
+ }
42
+
43
+ const SEPARATOR_STYLES = [
44
+ "powerline",
45
+ "powerline-thin",
46
+ "slash",
47
+ "pipe",
48
+ "block",
49
+ "none",
50
+ "ascii",
51
+ "dot",
52
+ "chevron",
53
+ "star",
54
+ ] as const satisfies readonly StatusLineSeparatorStyle[];
55
+
56
+ export function normalizeSeparator(
57
+ value: unknown,
58
+ ): StatusLineSeparatorStyle | null {
59
+ if (typeof value !== "string") return null;
60
+ const normalized = value.trim().toLowerCase();
61
+ return (SEPARATOR_STYLES as readonly string[]).includes(normalized)
62
+ ? (normalized as StatusLineSeparatorStyle)
63
+ : null;
64
+ }
65
+
66
+ export function normalizeCustomItemId(value: unknown): string | null {
67
+ if (typeof value !== "string") return null;
68
+ const normalized = value.trim();
69
+ if (!normalized) return null;
70
+ return /^[a-zA-Z0-9_-]+$/.test(normalized) ? normalized : null;
71
+ }
72
+
73
+ export function normalizeCustomItemPosition(
74
+ value: unknown,
75
+ ): CustomItemPosition {
76
+ if (value === "left" || value === "right" || value === "secondary")
77
+ return value;
78
+ return "right";
79
+ }
80
+
81
+ export function normalizeCustomColor(value: unknown): ColorValue | undefined {
82
+ if (typeof value !== "string") return undefined;
83
+ const normalized = value.trim();
84
+ return normalized ? (normalized as ColorValue) : undefined;
85
+ }
86
+
87
+ export function normalizeCustomPrefix(value: unknown): string | undefined {
88
+ if (typeof value !== "string") return undefined;
89
+ const normalized = value.trim();
90
+ return normalized ? normalized : undefined;
91
+ }
92
+
93
+ export function normalizeCaptureSigil(value: unknown): string | false {
94
+ if (value === false) return false;
95
+ if (typeof value !== "string") return "#";
96
+ const normalized = value.trim();
97
+ return normalized && !/\s/.test(normalized) ? normalized : "#";
98
+ }
99
+
100
+ export function normalizeCustomSegmentType(
101
+ value: unknown,
102
+ ): "command" | "env" | "static" | null {
103
+ if (value === "command" || value === "env" || value === "static")
104
+ return value;
105
+ return null;
106
+ }
107
+
108
+ export function normalizeSegmentLabels(raw: unknown): Record<string, string> {
109
+ const result: Record<string, string> = {};
110
+ if (!isRecord(raw)) return result;
111
+ for (const [id, label] of Object.entries(raw)) {
112
+ if (typeof label === "string" && label.trim()) {
113
+ result[id] = label.trim();
114
+ }
115
+ }
116
+ return result;
117
+ }