@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.
- package/AGENTS.md +68 -0
- package/CHANGELOG.md +724 -0
- package/CONTRIBUTING.md +37 -0
- package/README.md +648 -0
- package/RELEASE.md +117 -0
- package/ROADMAP.md +52 -0
- package/bash-mode/completion-providers.ts +269 -0
- package/bash-mode/completion.ts +416 -0
- package/bash-mode/editor-ghost.ts +40 -0
- package/bash-mode/editor-input.ts +80 -0
- package/bash-mode/editor.ts +437 -0
- package/bash-mode/history.ts +263 -0
- package/bash-mode/shell-session.ts +286 -0
- package/bash-mode/transcript.ts +108 -0
- package/bash-mode/types.ts +80 -0
- package/index.ts +6 -0
- package/package.json +55 -0
- package/queue/store.ts +443 -0
- package/queue/types.ts +54 -0
- package/src/config/custom-items.ts +182 -0
- package/src/config/extension-statuses.ts +51 -0
- package/src/config/layout.ts +60 -0
- package/src/config/parse.ts +127 -0
- package/src/config/powerline-config.ts +18 -0
- package/src/config/presets.ts +245 -0
- package/src/config/primitives.ts +117 -0
- package/src/config/segment-ids.ts +114 -0
- package/src/config/segment-options.ts +128 -0
- package/src/config/settings-patch.ts +26 -0
- package/src/config/types.ts +277 -0
- package/src/core/frontmatter.ts +40 -0
- package/src/editor/autocomplete-chain.ts +41 -0
- package/src/extension/activate.ts +28 -0
- package/src/extension/bash-mode-actions.ts +104 -0
- package/src/extension/commands.ts +268 -0
- package/src/extension/constants.ts +46 -0
- package/src/extension/custom-editor.ts +406 -0
- package/src/extension/git-invalidation.ts +40 -0
- package/src/extension/layout.ts +160 -0
- package/src/extension/menu-views.ts +393 -0
- package/src/extension/powerline-widgets.ts +95 -0
- package/src/extension/prompt-history.ts +219 -0
- package/src/extension/queue-commands.ts +245 -0
- package/src/extension/queue-context.ts +12 -0
- package/src/extension/queue-integration.ts +434 -0
- package/src/extension/segment-context.ts +212 -0
- package/src/extension/session-lifecycle.ts +373 -0
- package/src/extension/settings-io.ts +202 -0
- package/src/extension/shortcuts-config.ts +357 -0
- package/src/extension/shortcuts-router.ts +383 -0
- package/src/extension/skills/inline-invocation.ts +174 -0
- package/src/extension/skills/ook.md +6 -0
- package/src/extension/skills/test.md +6 -0
- package/src/extension/stale-context.ts +10 -0
- package/src/extension/stash-history.ts +103 -0
- package/src/extension/state.ts +159 -0
- package/src/extension/status-line-renderers.ts +222 -0
- package/src/extension/types.ts +97 -0
- package/src/extension/vibe-command.ts +160 -0
- package/src/extension/welcome-control.ts +27 -0
- package/src/extension/welcome-integration.ts +153 -0
- package/src/git/status.ts +332 -0
- package/src/paths/agent-dirs.ts +67 -0
- package/src/render/timer.ts +46 -0
- package/src/segments/core.ts +256 -0
- package/src/segments/custom.ts +114 -0
- package/src/segments/index.ts +3 -0
- package/src/segments/registry.ts +87 -0
- package/src/segments/shared.ts +36 -0
- package/src/segments/system.ts +235 -0
- package/src/segments/usage.ts +178 -0
- package/src/shell/cd-command.ts +190 -0
- package/src/shortcuts/matching.ts +61 -0
- package/src/theme/colors.ts +60 -0
- package/src/theme/icons.ts +175 -0
- package/src/theme/separators.ts +41 -0
- package/src/theme/theme.ts +211 -0
- package/src/tools/graph.ts +75 -0
- package/src/tools/patch.ts +179 -0
- package/src/tools/ripgrep.ts +104 -0
- package/src/usage/context.ts +97 -0
- package/src/usage/ledger.ts +293 -0
- package/src/usage/rates.ts +155 -0
- package/src/welcome/auto-dismiss.ts +43 -0
- package/src/welcome/banner.ts +68 -0
- package/src/welcome/discover.ts +234 -0
- package/src/welcome/format.ts +18 -0
- package/src/welcome/index.ts +5 -0
- package/src/welcome/layout.ts +36 -0
- package/src/welcome/overlay.ts +80 -0
- package/src/welcome/renderer.ts +157 -0
- package/src/welcome/sessions.ts +107 -0
- package/src/welcome/types.ts +41 -0
- package/src/welcome/widgets/graph-widget.ts +25 -0
- package/src/welcome/widgets/index.ts +20 -0
- package/src/welcome/widgets/queue-widget.ts +26 -0
- package/src/welcome/widgets/sessions-widget.ts +23 -0
- package/src/welcome/widgets/shortcuts-widget.ts +17 -0
- package/src/welcome/widgets/system-widget.ts +29 -0
- package/src/working-vibes/generate.ts +144 -0
- package/src/working-vibes/index.ts +24 -0
- package/src/working-vibes/manager.ts +198 -0
- package/src/working-vibes/provider.ts +163 -0
- package/src/working-vibes/storage.ts +357 -0
- package/theme.example.json +24 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface AnsiColors {
|
|
2
|
+
getBgAnsi(r: number, g: number, b: number): string;
|
|
3
|
+
getFgAnsi(r: number, g: number, b: number): string;
|
|
4
|
+
getFgAnsi256(code: number): string;
|
|
5
|
+
reset: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ansi: AnsiColors = {
|
|
9
|
+
getBgAnsi: (r, g, b) => `\x1b[48;2;${r};${g};${b}m`,
|
|
10
|
+
getFgAnsi: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
|
|
11
|
+
getFgAnsi256: (code) => `\x1b[38;5;${code}m`,
|
|
12
|
+
reset: "\x1b[0m",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
16
|
+
const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
17
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
|
|
18
|
+
throw new Error(`Invalid hex color: ${hex}`);
|
|
19
|
+
}
|
|
20
|
+
const r = parseInt(cleanHex.substring(0, 2), 16);
|
|
21
|
+
const g = parseInt(cleanHex.substring(2, 4), 16);
|
|
22
|
+
const b = parseInt(cleanHex.substring(4, 6), 16);
|
|
23
|
+
return [r, g, b];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const PALETTE: Record<string, string | number> = {
|
|
27
|
+
sep: 244,
|
|
28
|
+
model: "#d787af",
|
|
29
|
+
path: "#00afaf",
|
|
30
|
+
gitClean: "#5faf5f",
|
|
31
|
+
accent: "#febc38",
|
|
32
|
+
queue: "#febc38",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ColorName = keyof typeof PALETTE;
|
|
36
|
+
|
|
37
|
+
const ansiCodeCache = new Map<string, string>();
|
|
38
|
+
|
|
39
|
+
function buildAnsiCode(val: string | number | undefined): string {
|
|
40
|
+
if (val === undefined || val === "") return "";
|
|
41
|
+
if (typeof val === "number") return ansi.getFgAnsi256(val);
|
|
42
|
+
if (typeof val === "string" && val.startsWith("#")) {
|
|
43
|
+
const [r, g, b] = hexToRgb(val);
|
|
44
|
+
return ansi.getFgAnsi(r, g, b);
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const key of Object.keys(PALETTE)) {
|
|
50
|
+
ansiCodeCache.set(key, buildAnsiCode(PALETTE[key]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function fgOnly(color: ColorName, text: string): string {
|
|
54
|
+
const code = ansiCodeCache.get(color as string) ?? "";
|
|
55
|
+
return code ? `${code}${text}` : text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getFgAnsiCode(color: ColorName): string {
|
|
59
|
+
return ansiCodeCache.get(color as string) ?? "";
|
|
60
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { loadThemeConfig } from "./theme.ts";
|
|
2
|
+
|
|
3
|
+
export interface IconSet {
|
|
4
|
+
pi: string;
|
|
5
|
+
model: string;
|
|
6
|
+
folder: string;
|
|
7
|
+
branch: string;
|
|
8
|
+
git: string;
|
|
9
|
+
github: string;
|
|
10
|
+
gitlab: string;
|
|
11
|
+
bitbucket: string;
|
|
12
|
+
tokens: string;
|
|
13
|
+
context: string;
|
|
14
|
+
cost: string;
|
|
15
|
+
time: string;
|
|
16
|
+
agents: string;
|
|
17
|
+
cache: string;
|
|
18
|
+
input: string;
|
|
19
|
+
output: string;
|
|
20
|
+
host: string;
|
|
21
|
+
session: string;
|
|
22
|
+
auto: string;
|
|
23
|
+
warning: string;
|
|
24
|
+
tps: string;
|
|
25
|
+
ports: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SEP_DOT: string = " · ";
|
|
29
|
+
|
|
30
|
+
export const THINKING_TEXT_UNICODE: Record<string, string> = {
|
|
31
|
+
minimal: "[min]",
|
|
32
|
+
low: "[low]",
|
|
33
|
+
medium: "[med]",
|
|
34
|
+
high: "[high]",
|
|
35
|
+
xhigh: "[xhi]",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const THINKING_TEXT_NERD: Record<string, string> = {
|
|
39
|
+
minimal: "\u{F0E7} min",
|
|
40
|
+
low: "\u{F10C} low",
|
|
41
|
+
medium: "\u{F192} med",
|
|
42
|
+
high: "\u{F111} high",
|
|
43
|
+
xhigh: "\u{F06D} xhi",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function getThinkingText(level: string): string | undefined {
|
|
47
|
+
const map = hasNerdFonts() ? THINKING_TEXT_NERD : THINKING_TEXT_UNICODE;
|
|
48
|
+
return map[level];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const NERD_ICONS: IconSet = {
|
|
52
|
+
pi: "\uE22C",
|
|
53
|
+
model: "\uEC19",
|
|
54
|
+
folder: "\uF115",
|
|
55
|
+
branch: "\uF126",
|
|
56
|
+
git: "\uF1D3",
|
|
57
|
+
github: "\uF09B",
|
|
58
|
+
gitlab: "\uF296",
|
|
59
|
+
bitbucket: "\uF171",
|
|
60
|
+
tokens: "\uE26B",
|
|
61
|
+
context: "\uF1C0",
|
|
62
|
+
cost: "\uF155",
|
|
63
|
+
time: "\uF017",
|
|
64
|
+
agents: "\uF0C0",
|
|
65
|
+
cache: "\uF1C0",
|
|
66
|
+
input: "\uF090",
|
|
67
|
+
output: "\uF08B",
|
|
68
|
+
host: "\uF109",
|
|
69
|
+
session: "\uF550",
|
|
70
|
+
auto: "\u{F0068}",
|
|
71
|
+
warning: "\uF071",
|
|
72
|
+
tps: "\uF135",
|
|
73
|
+
ports: "\uF1E6",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const ASCII_ICONS: IconSet = {
|
|
77
|
+
pi: "π",
|
|
78
|
+
model: "",
|
|
79
|
+
folder: "dir",
|
|
80
|
+
branch: "⎇",
|
|
81
|
+
git: "⎇",
|
|
82
|
+
github: "⎇",
|
|
83
|
+
gitlab: "⎇",
|
|
84
|
+
bitbucket: "⎇",
|
|
85
|
+
tokens: "⊛",
|
|
86
|
+
context: "◫",
|
|
87
|
+
cost: "$",
|
|
88
|
+
time: "◷",
|
|
89
|
+
agents: "AG",
|
|
90
|
+
cache: "cache",
|
|
91
|
+
input: "in:",
|
|
92
|
+
output: "out:",
|
|
93
|
+
host: "host",
|
|
94
|
+
session: "id",
|
|
95
|
+
auto: "AC",
|
|
96
|
+
warning: "!",
|
|
97
|
+
tps: "⚡",
|
|
98
|
+
ports: "▣",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function sanitizeUserIconOverrides(value: unknown): Partial<IconSet> {
|
|
102
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
const result: Partial<IconSet> = {};
|
|
106
|
+
const validKeys = new Set<string>(Object.keys(NERD_ICONS));
|
|
107
|
+
for (const [key, val] of Object.entries(value)) {
|
|
108
|
+
if (validKeys.has(key) && typeof val === "string") {
|
|
109
|
+
result[key as keyof IconSet] = val;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SeparatorChars {
|
|
116
|
+
powerlineLeft: string;
|
|
117
|
+
powerlineRight: string;
|
|
118
|
+
powerlineThinLeft: string;
|
|
119
|
+
powerlineThinRight: string;
|
|
120
|
+
slash: string;
|
|
121
|
+
pipe: string;
|
|
122
|
+
block: string;
|
|
123
|
+
space: string;
|
|
124
|
+
asciiLeft: string;
|
|
125
|
+
asciiRight: string;
|
|
126
|
+
dot: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const NERD_SEPARATORS: SeparatorChars = {
|
|
130
|
+
powerlineLeft: "\uE0B0",
|
|
131
|
+
powerlineRight: "\uE0B2",
|
|
132
|
+
powerlineThinLeft: "\uE0B1",
|
|
133
|
+
powerlineThinRight: "\uE0B3",
|
|
134
|
+
slash: "/",
|
|
135
|
+
pipe: "|",
|
|
136
|
+
block: "█",
|
|
137
|
+
space: " ",
|
|
138
|
+
asciiLeft: ">",
|
|
139
|
+
asciiRight: "<",
|
|
140
|
+
dot: "·",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const ASCII_SEPARATORS: SeparatorChars = {
|
|
144
|
+
powerlineLeft: ">",
|
|
145
|
+
powerlineRight: "<",
|
|
146
|
+
powerlineThinLeft: "|",
|
|
147
|
+
powerlineThinRight: "|",
|
|
148
|
+
slash: "/",
|
|
149
|
+
pipe: "|",
|
|
150
|
+
block: "#",
|
|
151
|
+
space: " ",
|
|
152
|
+
asciiLeft: ">",
|
|
153
|
+
asciiRight: "<",
|
|
154
|
+
dot: ".",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export function hasNerdFonts(): boolean {
|
|
158
|
+
if (process.env.POWERLINE_NERD_FONTS === "1") return true;
|
|
159
|
+
if (process.env.POWERLINE_NERD_FONTS === "0") return false;
|
|
160
|
+
if (process.env.GHOSTTY_RESOURCES_DIR) return true;
|
|
161
|
+
|
|
162
|
+
const currentTerm = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
163
|
+
const supported = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
|
|
164
|
+
return supported.some((t) => currentTerm.includes(t));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getIcons(): IconSet {
|
|
168
|
+
const defaults = hasNerdFonts() ? NERD_ICONS : ASCII_ICONS;
|
|
169
|
+
const config = loadThemeConfig();
|
|
170
|
+
return { ...defaults, ...sanitizeUserIconOverrides(config.icons) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getSeparatorChars(): SeparatorChars {
|
|
174
|
+
return hasNerdFonts() ? NERD_SEPARATORS : ASCII_SEPARATORS;
|
|
175
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeparatorDef,
|
|
3
|
+
StatusLineSeparatorStyle,
|
|
4
|
+
} from "../config/types.ts";
|
|
5
|
+
import { getSeparatorChars } from "./icons.ts";
|
|
6
|
+
|
|
7
|
+
export function getSeparator(style: StatusLineSeparatorStyle): SeparatorDef {
|
|
8
|
+
const chars = getSeparatorChars();
|
|
9
|
+
|
|
10
|
+
const registry = new Map<string, SeparatorDef>([
|
|
11
|
+
["powerline", {
|
|
12
|
+
left: chars.powerlineLeft,
|
|
13
|
+
right: chars.powerlineRight,
|
|
14
|
+
endCaps: {
|
|
15
|
+
left: chars.powerlineRight,
|
|
16
|
+
right: chars.powerlineLeft,
|
|
17
|
+
useBgAsFg: true,
|
|
18
|
+
},
|
|
19
|
+
}],
|
|
20
|
+
["powerline-thin", {
|
|
21
|
+
left: chars.powerlineThinLeft,
|
|
22
|
+
right: chars.powerlineThinRight,
|
|
23
|
+
endCaps: {
|
|
24
|
+
left: chars.powerlineRight,
|
|
25
|
+
right: chars.powerlineLeft,
|
|
26
|
+
useBgAsFg: true,
|
|
27
|
+
},
|
|
28
|
+
}],
|
|
29
|
+
["slash", { left: ` ${chars.slash} `, right: ` ${chars.slash} ` }],
|
|
30
|
+
["pipe", { left: ` ${chars.pipe} `, right: ` ${chars.pipe} ` }],
|
|
31
|
+
["block", { left: chars.block, right: chars.block }],
|
|
32
|
+
["none", { left: chars.space, right: chars.space }],
|
|
33
|
+
["ascii", { left: chars.asciiLeft, right: chars.asciiRight }],
|
|
34
|
+
["dot", { left: chars.dot, right: chars.dot }],
|
|
35
|
+
["chevron", { left: "›", right: "‹" }],
|
|
36
|
+
["star", { left: "✦", right: "✦" }],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const definition = registry.get(style) ?? registry.get("powerline-thin")!;
|
|
40
|
+
return Object.freeze(definition);
|
|
41
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getAgentPath } from "../paths/agent-dirs.ts";
|
|
6
|
+
import type {
|
|
7
|
+
ColorScheme,
|
|
8
|
+
ColorValue,
|
|
9
|
+
SemanticColor,
|
|
10
|
+
ThemeLike,
|
|
11
|
+
} from "../config/types.ts";
|
|
12
|
+
|
|
13
|
+
export interface PowerlineThemeConfig {
|
|
14
|
+
colors?: unknown;
|
|
15
|
+
icons?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_COLORS: Required<ColorScheme> = {
|
|
19
|
+
model: "#d787af",
|
|
20
|
+
shellMode: "accent",
|
|
21
|
+
path: "#00afaf",
|
|
22
|
+
gitDirty: "warning",
|
|
23
|
+
gitClean: "success",
|
|
24
|
+
thinking: "thinkingOff",
|
|
25
|
+
thinkingMinimal: "thinkingMinimal",
|
|
26
|
+
thinkingLow: "thinkingLow",
|
|
27
|
+
thinkingMedium: "thinkingMedium",
|
|
28
|
+
context: "dim",
|
|
29
|
+
contextWarn: "warning",
|
|
30
|
+
contextError: "error",
|
|
31
|
+
cost: "text",
|
|
32
|
+
tokens: "muted",
|
|
33
|
+
queue: "accent",
|
|
34
|
+
separator: "dim",
|
|
35
|
+
border: "borderMuted",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const RAINBOW_COLORS = [
|
|
39
|
+
"#b281d6",
|
|
40
|
+
"#d787af",
|
|
41
|
+
"#febc38",
|
|
42
|
+
"#e4c00f",
|
|
43
|
+
"#89d281",
|
|
44
|
+
"#00afaf",
|
|
45
|
+
"#178fb9",
|
|
46
|
+
"#b281d6",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const CACHE_LIFETIME_MS = 5000;
|
|
50
|
+
let cachedConfig: PowerlineThemeConfig | null = null;
|
|
51
|
+
let lastCacheUpdate = 0;
|
|
52
|
+
let cacheIdentifier = "";
|
|
53
|
+
|
|
54
|
+
const loggedErrors = new Set<string>();
|
|
55
|
+
|
|
56
|
+
function resolvePackageDir(startDir: string): string {
|
|
57
|
+
let currentDir = startDir;
|
|
58
|
+
for (let i = 0; i < 6; i++) {
|
|
59
|
+
if (existsSync(join(currentDir, "package.json"))) return currentDir;
|
|
60
|
+
const parentDir = dirname(currentDir);
|
|
61
|
+
if (parentDir === currentDir) break;
|
|
62
|
+
currentDir = parentDir;
|
|
63
|
+
}
|
|
64
|
+
return startDir;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getConfigurationPaths(): string[] {
|
|
68
|
+
const currentFileDir = dirname(fileURLToPath(import.meta.url));
|
|
69
|
+
const pkgDir = resolvePackageDir(currentFileDir);
|
|
70
|
+
return [
|
|
71
|
+
getAgentPath("extensions", "powerline-footer", "theme.json"),
|
|
72
|
+
join(pkgDir, "theme.json"),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mergeThemeConfig(base: PowerlineThemeConfig, overrides: unknown): PowerlineThemeConfig {
|
|
77
|
+
if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
|
|
78
|
+
return base;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const record = overrides as Record<string, unknown>;
|
|
82
|
+
return {
|
|
83
|
+
colors: record.colors !== undefined ? record.colors : base.colors,
|
|
84
|
+
icons: record.icons !== undefined ? record.icons : base.icons,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function loadThemeConfig(): PowerlineThemeConfig {
|
|
89
|
+
const paths = getConfigurationPaths();
|
|
90
|
+
const identity = paths.join("|");
|
|
91
|
+
const time = Date.now();
|
|
92
|
+
|
|
93
|
+
if (cachedConfig && cacheIdentifier === identity && time - lastCacheUpdate < CACHE_LIFETIME_MS) {
|
|
94
|
+
return cachedConfig;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let finalConfig: PowerlineThemeConfig = {};
|
|
98
|
+
|
|
99
|
+
for (const path of paths) {
|
|
100
|
+
try {
|
|
101
|
+
if (existsSync(path)) {
|
|
102
|
+
const rawContent = readFileSync(path, "utf8");
|
|
103
|
+
const parsedData = JSON.parse(rawContent);
|
|
104
|
+
finalConfig = mergeThemeConfig(finalConfig, parsedData);
|
|
105
|
+
break; // Stop after first successful load
|
|
106
|
+
}
|
|
107
|
+
} catch (e: unknown) {
|
|
108
|
+
if (e instanceof SyntaxError) {
|
|
109
|
+
console.debug(`[theme] SyntaxError in ${path}:`, e.message);
|
|
110
|
+
} else if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
111
|
+
// File doesn't exist despite existsSync, ignore
|
|
112
|
+
} else {
|
|
113
|
+
console.debug(`[theme] Unexpected error reading ${path}:`, e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
cachedConfig = finalConfig;
|
|
119
|
+
lastCacheUpdate = time;
|
|
120
|
+
cacheIdentifier = identity;
|
|
121
|
+
|
|
122
|
+
return finalConfig;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sanitizeUserColors(rawColors: unknown): ColorScheme {
|
|
126
|
+
if (typeof rawColors !== "object" || rawColors === null || Array.isArray(rawColors)) return {};
|
|
127
|
+
|
|
128
|
+
const result: ColorScheme = {};
|
|
129
|
+
for (const key of Object.keys(DEFAULT_COLORS)) {
|
|
130
|
+
const val = (rawColors as Record<string, unknown>)[key];
|
|
131
|
+
if (typeof val === "string" && val.trim().length > 0) {
|
|
132
|
+
result[key as SemanticColor] = val.trim() as ColorValue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resolveColor(
|
|
139
|
+
semantic: SemanticColor,
|
|
140
|
+
presetColors?: ColorScheme,
|
|
141
|
+
): ColorValue {
|
|
142
|
+
const config = loadThemeConfig();
|
|
143
|
+
const userColors = sanitizeUserColors(config.colors);
|
|
144
|
+
|
|
145
|
+
if (userColors[semantic] !== undefined) {
|
|
146
|
+
return userColors[semantic] as ColorValue;
|
|
147
|
+
}
|
|
148
|
+
if (presetColors && presetColors[semantic] !== undefined) {
|
|
149
|
+
return presetColors[semantic] as ColorValue;
|
|
150
|
+
}
|
|
151
|
+
return DEFAULT_COLORS[semantic];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hexToRgbAnsi(hex: string): string {
|
|
155
|
+
const cleanHex = hex.replace("#", "");
|
|
156
|
+
const r = parseInt(cleanHex.substring(0, 2), 16);
|
|
157
|
+
const g = parseInt(cleanHex.substring(2, 4), 16);
|
|
158
|
+
const b = parseInt(cleanHex.substring(4, 6), 16);
|
|
159
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function applyColor(
|
|
163
|
+
theme: ThemeLike,
|
|
164
|
+
color: ColorValue,
|
|
165
|
+
text: string,
|
|
166
|
+
): string {
|
|
167
|
+
if (typeof color === "string" && /^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
168
|
+
return `${hexToRgbAnsi(color)}${text}\x1b[0m`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
return theme.fg(color as ThemeColor, text);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
const errorKey = String(color);
|
|
175
|
+
if (!loggedErrors.has(errorKey)) {
|
|
176
|
+
loggedErrors.add(errorKey);
|
|
177
|
+
if (loggedErrors.size > 100) loggedErrors.clear();
|
|
178
|
+
console.debug(`[theme] Invalid color "${errorKey}". Fallback to text.`, e);
|
|
179
|
+
}
|
|
180
|
+
return theme.fg("text", text);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function fg(
|
|
185
|
+
theme: ThemeLike,
|
|
186
|
+
semantic: SemanticColor,
|
|
187
|
+
text: string,
|
|
188
|
+
presetColors?: ColorScheme,
|
|
189
|
+
): string {
|
|
190
|
+
const c = resolveColor(semantic, presetColors);
|
|
191
|
+
return applyColor(theme, c, text);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function rainbow(text: string): string {
|
|
195
|
+
let output = "";
|
|
196
|
+
let i = 0;
|
|
197
|
+
for (const char of text) {
|
|
198
|
+
if (char === " " || char === ":") {
|
|
199
|
+
output += char;
|
|
200
|
+
} else {
|
|
201
|
+
const hex = RAINBOW_COLORS[i % RAINBOW_COLORS.length];
|
|
202
|
+
output += hexToRgbAnsi(hex) + char;
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return output + "\x1b[0m";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getDefaultColors(): Required<ColorScheme> {
|
|
210
|
+
return { ...DEFAULT_COLORS };
|
|
211
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const BRAILLE_CHAR_BASE = 0x2800;
|
|
2
|
+
|
|
3
|
+
// Braille dot heights (0-7 for 4 vertical levels in 2 columns)
|
|
4
|
+
const BRAILLE_MAP = [0x40, 0x4, 0x20, 0x2, 0x10, 0x1, 0x80, 0x8];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Render a sequence of numeric data points as a Braille sparkline string.
|
|
8
|
+
*/
|
|
9
|
+
export function renderSparkline(values: number[], width: number = 20): string {
|
|
10
|
+
if (values.length === 0) return " ".repeat(width);
|
|
11
|
+
|
|
12
|
+
const min = Math.min(...values);
|
|
13
|
+
const max = Math.max(...values);
|
|
14
|
+
const range = max - min || 1;
|
|
15
|
+
|
|
16
|
+
// Normalize values to 0..3 range (4 height levels)
|
|
17
|
+
const normalized = values.map((v) => Math.min(3, Math.floor(((v - min) / range) * 3.99)));
|
|
18
|
+
|
|
19
|
+
// Group into pairs for Braille columns (2 values per Braille character)
|
|
20
|
+
let sparkline = "";
|
|
21
|
+
const numChars = Math.min(width, Math.ceil(normalized.length / 2));
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < numChars * 2; i += 2) {
|
|
24
|
+
const leftVal = normalized[i] ?? 0;
|
|
25
|
+
const rightVal = normalized[i + 1] ?? leftVal;
|
|
26
|
+
|
|
27
|
+
// Map 0..3 height to Braille dots
|
|
28
|
+
let charCode = BRAILLE_CHAR_BASE;
|
|
29
|
+
if (leftVal >= 1) charCode |= 0x40; // dot 7
|
|
30
|
+
if (leftVal >= 2) charCode |= 0x04; // dot 3
|
|
31
|
+
if (leftVal >= 3) charCode |= 0x01; // dot 1
|
|
32
|
+
|
|
33
|
+
if (rightVal >= 1) charCode |= 0x80; // dot 8
|
|
34
|
+
if (rightVal >= 2) charCode |= 0x20; // dot 6
|
|
35
|
+
if (rightVal >= 3) charCode |= 0x02; // dot 2
|
|
36
|
+
|
|
37
|
+
sparkline += String.fromCharCode(charCode);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return sparkline;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GraphNode {
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GraphEdge {
|
|
49
|
+
from: string;
|
|
50
|
+
to: string;
|
|
51
|
+
label?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render a simple ASCII graph representation of dependency nodes and edges.
|
|
56
|
+
*/
|
|
57
|
+
export function renderAsciiGraph(nodes: GraphNode[], edges: GraphEdge[]): string[] {
|
|
58
|
+
const lines: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (const node of nodes) {
|
|
61
|
+
lines.push(` ┌──────────────┐`);
|
|
62
|
+
lines.push(` │ ${node.label.padEnd(12)} │`);
|
|
63
|
+
lines.push(` └──────────────┘`);
|
|
64
|
+
|
|
65
|
+
const outEdges = edges.filter((e) => e.from === node.id);
|
|
66
|
+
for (const edge of outEdges) {
|
|
67
|
+
const target = nodes.find((n) => n.id === edge.to);
|
|
68
|
+
if (target) {
|
|
69
|
+
lines.push(` │ ${edge.label ? `[${edge.label}]` : "-->"} ${target.label}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return lines;
|
|
75
|
+
}
|