pi-powerline-footer 0.2.2

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 ADDED
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ ## [0.2.2] - 2026-01-15
4
+
5
+ ### Changed
6
+ - **Path segment defaults to basename** — Shows just the directory name (e.g., `powerline-footer`) instead of full path to save space
7
+ - **New path modes** — `basename` (default), `abbreviated` (truncated full path), `full` (complete path)
8
+ - Simplified path options: replaced `abbreviate`, `stripWorkPrefix` with cleaner `mode` option
9
+ - Full/nerd presets use `abbreviated` mode, default/minimal/compact use `basename`
10
+ - Thinking segment now uses dedicated gradient colors (thinkingOff → thinkingMedium)
11
+
12
+ ### Fixed
13
+ - Path basename extraction now uses `path.basename()` for Windows compatibility
14
+ - Git branch cache now stores `null` results, preventing repeated git calls in non-git directories
15
+ - Git status cache now stores empty results for non-git directories (was also spawning repeatedly)
16
+ - Removed dead `footerDispose` variable (cleanup handled by pi internally)
17
+
18
+ ## [0.2.1] - 2026-01-10
19
+
20
+ ### Added
21
+ - **Live git branch updates** — Branch now updates in real-time when switching via `git checkout`, `git switch`, etc.
22
+ - **Own branch fetching** — Extension fetches branch directly via `git branch --show-current` instead of relying solely on FooterDataProvider
23
+ - **Branch cache with 500ms TTL** — Faster refresh cycle for branch changes
24
+ - **Staggered re-renders for escape commands** — Multiple re-renders at 100/300/500ms to catch updates from `!` commands
25
+
26
+ ### Fixed
27
+ - Git branch not updating after `git checkout` to existing branches
28
+ - Race condition where FooterDataProvider's branch cache wasn't updating in time
29
+
30
+ ## [0.2.0] - 2026-01-10
31
+
32
+ ### Added
33
+ - **Extension statuses segment** — Displays status text from other extensions (e.g., rewind checkpoint count)
34
+ - **Thinking level segment** — Live-updating display of current thinking level (`thinking:off`, `thinking:med`, etc.)
35
+ - **Rainbow effect** — High and xhigh thinking levels display with rainbow gradient inspired by Claude Code's ultrathink
36
+ - **Color gradient** — Thinking levels use progressive colors: gray → purple-gray → blue → teal → rainbow
37
+ - **Streaming visibility** — Status bar now renders in footer during streaming so it's always visible
38
+
39
+ ### Changed
40
+ - Extension statuses appear at end of status bar (last item in default/full/nerd presets)
41
+ - Default preset now includes `thinking` segment after model
42
+ - Thinking level reads from session branch entries for live updates
43
+ - Footer invalidate() now triggers re-render for settings changes
44
+ - Responsive truncation — progressively removes segments on narrow windows instead of hiding status
45
+
46
+ ### Fixed
47
+ - ANSI color reset after status content to prevent color bleeding
48
+ - ANSI color reset after rainbow text
49
+
50
+ ### Removed
51
+ - Unused brain icon definitions
52
+
53
+ ## [0.1.0] - 2026-01-10
54
+
55
+ ### Added
56
+ - Initial release
57
+ - Rounded box design rendering in editor top border
58
+ - 18 segment types: pi, model, thinking, path, git, subagents, token_in, token_out, token_total, cost, context_pct, context_total, time_spent, time, session, hostname, cache_read, cache_write
59
+ - 6 presets: default, minimal, compact, full, nerd, ascii
60
+ - 10 separator styles: powerline, powerline-thin, slash, pipe, dot, chevron, star, block, none, ascii
61
+ - Git integration with async status fetching and 1s cache TTL
62
+ - Nerd Font auto-detection for common terminals
63
+ - oh-my-pi dark theme color matching
64
+ - Context percentage warnings at 70%/90%
65
+ - Auto-compact indicator
66
+ - Subscription detection
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # pi-powerline-footer
2
+
3
+ A powerline-style status bar extension for [pi](https://github.com/badlogic/pi-mono), the coding agent. Inspired by [oh-my-pi](https://github.com/can1357/oh-my-pi).
4
+
5
+ <img width="1555" height="171" alt="image" src="https://github.com/user-attachments/assets/7bfc8a5a-29b5-478e-b9c8-c462c032a840" />
6
+
7
+ ## Features
8
+
9
+ **Rounded box design** — Status renders directly in the editor's top border, not as a separate footer.
10
+
11
+ **Live thinking level indicator** — Shows current thinking level (`thinking:off`, `thinking:med`, etc.) with color-coded gradient. High and xhigh levels get a rainbow shimmer effect inspired by Claude Code's ultrathink.
12
+
13
+ **Smart defaults** — Nerd Font auto-detection for iTerm, WezTerm, Kitty, Ghostty, and Alacritty with ASCII fallbacks. Colors matched to oh-my-pi's dark theme.
14
+
15
+ **Git integration** — Async status fetching with 1s cache TTL. Automatically invalidates on file writes/edits. Shows branch, staged (+), unstaged (*), and untracked (?) counts.
16
+
17
+ **Context awareness** — Color-coded warnings at 70% (yellow) and 90% (red) context usage. Auto-compact indicator when enabled.
18
+
19
+ **Token intelligence** — Smart formatting (1.2k, 45M), subscription detection (shows "(sub)" vs dollar cost).
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npx pi-powerline-footer
25
+ ```
26
+
27
+ This copies the extension to `~/.pi/agent/extensions/powerline-footer/`. Restart pi to activate.
28
+
29
+ ## Usage
30
+
31
+ Activates automatically. Toggle with `/powerline`, switch presets with `/powerline <name>`.
32
+
33
+ | Preset | Description |
34
+ |--------|-------------|
35
+ | `default` | Model, thinking, path (basename), git, context, tokens, cost |
36
+ | `minimal` | Just path (basename), git, context |
37
+ | `compact` | Model, git, cost, context |
38
+ | `full` | Everything including hostname, time, abbreviated path |
39
+ | `nerd` | Maximum detail for Nerd Font users |
40
+ | `ascii` | Safe for any terminal |
41
+
42
+ **Environment:** `POWERLINE_NERD_FONTS=1` to force Nerd Fonts, `=0` for ASCII.
43
+
44
+ ## Thinking Level Display
45
+
46
+ The thinking segment shows live updates when you change thinking level:
47
+
48
+ | Level | Display | Color |
49
+ |-------|---------|-------|
50
+ | off | `thinking:off` | gray |
51
+ | minimal | `thinking:min` | purple-gray |
52
+ | low | `thinking:low` | blue |
53
+ | medium | `thinking:med` | teal |
54
+ | high | `thinking:high` | 🌈 rainbow |
55
+ | xhigh | `thinking:xhigh` | 🌈 rainbow |
56
+
57
+ ## Path Display
58
+
59
+ The path segment supports three modes:
60
+
61
+ | Mode | Example | Description |
62
+ |------|---------|-------------|
63
+ | `basename` | `powerline-footer` | Just the directory name (default) |
64
+ | `abbreviated` | `…/extensions/powerline-footer` | Full path with home abbreviated and length limit |
65
+ | `full` | `~/.pi/agent/extensions/powerline-footer` | Complete path with home abbreviated |
66
+
67
+ Configure via preset options: `path: { mode: "full" }`
68
+
69
+ ## Segments
70
+
71
+ `pi` · `model` · `thinking` · `path` · `git` · `subagents` · `token_in` · `token_out` · `token_total` · `cost` · `context_pct` · `context_total` · `time_spent` · `time` · `session` · `hostname` · `cache_read` · `cache_write`
72
+
73
+ ## Separators
74
+
75
+ `powerline` · `powerline-thin` · `slash` · `pipe` · `dot` · `chevron` · `star` · `block` · `none` · `ascii`
package/colors.ts ADDED
@@ -0,0 +1,126 @@
1
+ // ANSI escape codes for colors
2
+ // Matching oh-my-pi dark theme colors exactly
3
+
4
+ export interface AnsiColors {
5
+ getBgAnsi(r: number, g: number, b: number): string;
6
+ getFgAnsi(r: number, g: number, b: number): string;
7
+ getFgAnsi256(code: number): string;
8
+ reset: string;
9
+ }
10
+
11
+ export const ansi: AnsiColors = {
12
+ getBgAnsi: (r, g, b) => `\x1b[48;2;${r};${g};${b}m`,
13
+ getFgAnsi: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
14
+ getFgAnsi256: (code) => `\x1b[38;5;${code}m`,
15
+ reset: "\x1b[0m",
16
+ };
17
+
18
+ // Convert hex to RGB tuple
19
+ function hexToRgb(hex: string): [number, number, number] {
20
+ const h = hex.replace("#", "");
21
+ return [
22
+ parseInt(h.slice(0, 2), 16),
23
+ parseInt(h.slice(2, 4), 16),
24
+ parseInt(h.slice(4, 6), 16),
25
+ ];
26
+ }
27
+
28
+ // oh-my-pi dark theme colors (exact match)
29
+ const THEME = {
30
+ // Status line colors
31
+ sep: 244, // ANSI 256 gray
32
+ model: "#d787af", // Pink/mauve
33
+ path: "#00afaf", // Teal/cyan
34
+ gitClean: "#5faf5f", // Green
35
+ gitDirty: "#d7af5f", // Gold/orange
36
+ context: "#8787af", // Purple-gray
37
+ spend: "#5fafaf", // Teal
38
+ staged: 70, // ANSI 256 green
39
+ unstaged: 178, // ANSI 256 gold
40
+ untracked: 39, // ANSI 256 blue
41
+ output: 205, // ANSI 256 pink
42
+ cost: 205, // ANSI 256 pink
43
+ subagents: "#febc38", // Accent orange
44
+
45
+ // UI colors
46
+ accent: "#febc38", // Orange (for pi icon)
47
+ border: "#178fb9", // Blue (for box border)
48
+ warning: "#e4c00f", // Yellow
49
+ error: "#fc3a4b", // Red
50
+ text: "", // Default terminal color
51
+
52
+ // Thinking level colors (gradient from dim to bright)
53
+ thinkingOff: "#3d424a", // Dark gray
54
+ thinkingMinimal: "#5f6673", // Dim gray
55
+ thinkingLow: "#178fb9", // Blue
56
+ thinkingMedium: "#0088fa", // Bright blue
57
+ thinkingHigh: "#b281d6", // Purple
58
+ thinkingXhigh: "#e5c1ff", // Bright lavender
59
+ };
60
+
61
+ // Color name to ANSI code mapping
62
+ type ColorName =
63
+ | "sep" | "model" | "path" | "gitClean" | "gitDirty"
64
+ | "context" | "spend" | "staged" | "unstaged" | "untracked"
65
+ | "output" | "cost" | "subagents" | "accent" | "border"
66
+ | "warning" | "error" | "text"
67
+ | "thinkingOff" | "thinkingMinimal" | "thinkingLow"
68
+ | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh";
69
+
70
+ function getAnsiCode(color: ColorName): string {
71
+ const value = THEME[color as keyof typeof THEME];
72
+
73
+ if (value === undefined || value === "") {
74
+ return ""; // No color, use terminal default
75
+ }
76
+
77
+ if (typeof value === "number") {
78
+ return ansi.getFgAnsi256(value);
79
+ }
80
+
81
+ if (typeof value === "string" && value.startsWith("#")) {
82
+ const [r, g, b] = hexToRgb(value);
83
+ return ansi.getFgAnsi(r, g, b);
84
+ }
85
+
86
+ return "";
87
+ }
88
+
89
+ // Helper to apply foreground color only (no reset - caller manages reset)
90
+ export function fgOnly(color: ColorName, text: string): string {
91
+ const code = getAnsiCode(color);
92
+ return code ? `${code}${text}` : text;
93
+ }
94
+
95
+ // Get raw ANSI code for a color
96
+ export function getFgAnsiCode(color: ColorName): string {
97
+ return getAnsiCode(color);
98
+ }
99
+
100
+ // Rainbow colors for ultra/xhigh thinking (matches Claude Code ultrathink)
101
+ const RAINBOW_COLORS = [
102
+ "#b281d6", // purple
103
+ "#d787af", // pink
104
+ "#febc38", // orange
105
+ "#e4c00f", // yellow
106
+ "#89d281", // green
107
+ "#00afaf", // cyan
108
+ "#178fb9", // blue
109
+ "#b281d6", // purple (loop)
110
+ ];
111
+
112
+ // Apply rainbow gradient to text (each character gets next color)
113
+ export function rainbow(text: string): string {
114
+ let result = "";
115
+ let colorIndex = 0;
116
+ for (const char of text) {
117
+ if (char === " " || char === ":") {
118
+ result += char;
119
+ } else {
120
+ const [r, g, b] = hexToRgb(RAINBOW_COLORS[colorIndex % RAINBOW_COLORS.length]);
121
+ result += `${ansi.getFgAnsi(r, g, b)}${char}`;
122
+ colorIndex++;
123
+ }
124
+ }
125
+ return result + ansi.reset;
126
+ }
package/git-status.ts ADDED
@@ -0,0 +1,242 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { GitStatus } from "./types.js";
3
+
4
+ interface CachedGitStatus {
5
+ staged: number;
6
+ unstaged: number;
7
+ untracked: number;
8
+ timestamp: number;
9
+ }
10
+
11
+ interface CachedBranch {
12
+ branch: string | null;
13
+ timestamp: number;
14
+ }
15
+
16
+ const CACHE_TTL_MS = 1000; // 1 second for file status
17
+ const BRANCH_TTL_MS = 500; // Shorter TTL so branch updates quickly after invalidation
18
+ let cachedStatus: CachedGitStatus | null = null;
19
+ let cachedBranch: CachedBranch | null = null;
20
+ let pendingFetch: Promise<void> | null = null;
21
+ let pendingBranchFetch: Promise<void> | null = null;
22
+ let invalidationCounter = 0; // Track invalidations to prevent stale updates
23
+ let branchInvalidationCounter = 0;
24
+
25
+ /**
26
+ * Parse git status --porcelain output
27
+ *
28
+ * Format: XY filename
29
+ * X = index status, Y = working tree status
30
+ * ?? = untracked
31
+ * Other X values = staged
32
+ * Other Y values = unstaged
33
+ */
34
+ function parseGitStatusOutput(output: string): { staged: number; unstaged: number; untracked: number } {
35
+ let staged = 0;
36
+ let unstaged = 0;
37
+ let untracked = 0;
38
+
39
+ for (const line of output.split("\n")) {
40
+ if (!line) continue;
41
+ const x = line[0];
42
+ const y = line[1];
43
+
44
+ if (x === "?" && y === "?") {
45
+ untracked++;
46
+ continue;
47
+ }
48
+
49
+ // X position (index/staged)
50
+ if (x && x !== " " && x !== "?") {
51
+ staged++;
52
+ }
53
+
54
+ // Y position (working tree/unstaged)
55
+ if (y && y !== " ") {
56
+ unstaged++;
57
+ }
58
+ }
59
+
60
+ return { staged, unstaged, untracked };
61
+ }
62
+
63
+ /**
64
+ * Fetch current git branch asynchronously
65
+ */
66
+ async function fetchGitBranch(): Promise<string | null> {
67
+ return new Promise((resolve) => {
68
+ const proc = spawn("git", ["branch", "--show-current"], {
69
+ stdio: ["ignore", "pipe", "pipe"],
70
+ });
71
+
72
+ let stdout = "";
73
+ let resolved = false;
74
+
75
+ const finish = (result: string | null) => {
76
+ if (resolved) return;
77
+ resolved = true;
78
+ clearTimeout(timeoutId);
79
+ resolve(result);
80
+ };
81
+
82
+ proc.stdout.on("data", (data) => {
83
+ stdout += data.toString();
84
+ });
85
+
86
+ proc.on("close", (code) => {
87
+ if (code !== 0) {
88
+ finish(null);
89
+ return;
90
+ }
91
+ const branch = stdout.trim();
92
+ finish(branch || null); // Empty string means detached HEAD
93
+ });
94
+
95
+ proc.on("error", () => {
96
+ finish(null);
97
+ });
98
+
99
+ // Timeout after 200ms
100
+ const timeoutId = setTimeout(() => {
101
+ proc.kill();
102
+ finish(null);
103
+ }, 200);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Fetch git status asynchronously
109
+ */
110
+ async function fetchGitStatus(): Promise<{ staged: number; unstaged: number; untracked: number } | null> {
111
+ return new Promise((resolve) => {
112
+ const proc = spawn("git", ["status", "--porcelain"], {
113
+ stdio: ["ignore", "pipe", "pipe"],
114
+ });
115
+
116
+ let stdout = "";
117
+ let resolved = false;
118
+
119
+ const finish = (result: { staged: number; unstaged: number; untracked: number } | null) => {
120
+ if (resolved) return;
121
+ resolved = true;
122
+ clearTimeout(timeoutId);
123
+ resolve(result);
124
+ };
125
+
126
+ proc.stdout.on("data", (data) => {
127
+ stdout += data.toString();
128
+ });
129
+
130
+ proc.on("close", (code) => {
131
+ if (code !== 0) {
132
+ finish(null);
133
+ return;
134
+ }
135
+ finish(parseGitStatusOutput(stdout));
136
+ });
137
+
138
+ proc.on("error", () => {
139
+ finish(null);
140
+ });
141
+
142
+ // Timeout after 500ms
143
+ const timeoutId = setTimeout(() => {
144
+ proc.kill();
145
+ finish(null);
146
+ }, 500);
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Get the current git branch with caching.
152
+ * Falls back to provider branch if our cache is empty.
153
+ */
154
+ export function getCurrentBranch(providerBranch: string | null): string | null {
155
+ const now = Date.now();
156
+
157
+ // Return cached if fresh
158
+ if (cachedBranch && now - cachedBranch.timestamp < BRANCH_TTL_MS) {
159
+ return cachedBranch.branch;
160
+ }
161
+
162
+ // Trigger background fetch if not already pending
163
+ if (!pendingBranchFetch) {
164
+ const fetchId = branchInvalidationCounter;
165
+ pendingBranchFetch = fetchGitBranch().then((result) => {
166
+ // Cache result if no invalidation happened (including null for non-git dirs)
167
+ if (fetchId === branchInvalidationCounter) {
168
+ cachedBranch = {
169
+ branch: result,
170
+ timestamp: Date.now(),
171
+ };
172
+ }
173
+ pendingBranchFetch = null;
174
+ });
175
+ }
176
+
177
+ // Return cached branch, or fall back to provider
178
+ return cachedBranch?.branch ?? providerBranch;
179
+ }
180
+
181
+ /**
182
+ * Get git status with caching.
183
+ * Returns cached value if within TTL, otherwise triggers async fetch.
184
+ * This is designed for synchronous render() calls - returns last known value
185
+ * while refreshing in background.
186
+ */
187
+ export function getGitStatus(providerBranch: string | null): GitStatus {
188
+ const now = Date.now();
189
+ const branch = getCurrentBranch(providerBranch);
190
+
191
+ // Return cached if fresh
192
+ if (cachedStatus && now - cachedStatus.timestamp < CACHE_TTL_MS) {
193
+ return {
194
+ branch,
195
+ staged: cachedStatus.staged,
196
+ unstaged: cachedStatus.unstaged,
197
+ untracked: cachedStatus.untracked,
198
+ };
199
+ }
200
+
201
+ // Trigger background fetch if not already pending
202
+ if (!pendingFetch) {
203
+ const fetchId = invalidationCounter; // Capture current counter
204
+ pendingFetch = fetchGitStatus().then((result) => {
205
+ // Cache result if no invalidation happened (including null for non-git dirs)
206
+ if (fetchId === invalidationCounter) {
207
+ cachedStatus = result
208
+ ? { staged: result.staged, unstaged: result.unstaged, untracked: result.untracked, timestamp: Date.now() }
209
+ : { staged: 0, unstaged: 0, untracked: 0, timestamp: Date.now() };
210
+ }
211
+ pendingFetch = null;
212
+ });
213
+ }
214
+
215
+ // Return last cached or empty
216
+ if (cachedStatus) {
217
+ return {
218
+ branch,
219
+ staged: cachedStatus.staged,
220
+ unstaged: cachedStatus.unstaged,
221
+ untracked: cachedStatus.untracked,
222
+ };
223
+ }
224
+
225
+ return { branch, staged: 0, unstaged: 0, untracked: 0 };
226
+ }
227
+
228
+ /**
229
+ * Force refresh git status (call when you know files changed)
230
+ */
231
+ export function invalidateGitStatus(): void {
232
+ cachedStatus = null;
233
+ invalidationCounter++; // Increment to invalidate any pending fetches
234
+ }
235
+
236
+ /**
237
+ * Force refresh git branch (call when you know branch might have changed)
238
+ */
239
+ export function invalidateGitBranch(): void {
240
+ cachedBranch = null;
241
+ branchInvalidationCounter++;
242
+ }
package/icons.ts ADDED
@@ -0,0 +1,156 @@
1
+ export interface IconSet {
2
+ pi: string;
3
+ model: string;
4
+ folder: string;
5
+ branch: string;
6
+ git: string;
7
+ tokens: string;
8
+ context: string;
9
+ cost: string;
10
+ time: string;
11
+ agents: string;
12
+ cache: string;
13
+ input: string;
14
+ output: string;
15
+ host: string;
16
+ session: string;
17
+ auto: string;
18
+ warning: string;
19
+ }
20
+
21
+ // Separator characters
22
+ export const SEP_DOT = " · ";
23
+
24
+ // Thinking level display text (Unicode/ASCII)
25
+ export const THINKING_TEXT_UNICODE: Record<string, string> = {
26
+ minimal: "[min]",
27
+ low: "[low]",
28
+ medium: "[med]",
29
+ high: "[high]",
30
+ xhigh: "[xhi]",
31
+ };
32
+
33
+ // Thinking level display text (Nerd Fonts - with icons)
34
+ export const THINKING_TEXT_NERD: Record<string, string> = {
35
+ minimal: "\u{F0E7} min", // lightning bolt
36
+ low: "\u{F10C} low", // circle outline
37
+ medium: "\u{F192} med", // dot circle
38
+ high: "\u{F111} high", // circle
39
+ xhigh: "\u{F06D} xhi", // fire
40
+ };
41
+
42
+ // Get thinking text based on font support
43
+ export function getThinkingText(level: string): string | undefined {
44
+ if (hasNerdFonts()) {
45
+ return THINKING_TEXT_NERD[level];
46
+ }
47
+ return THINKING_TEXT_UNICODE[level];
48
+ }
49
+
50
+ // Nerd Font icons (matching oh-my-pi exactly)
51
+ export const NERD_ICONS: IconSet = {
52
+ pi: "\uE22C", // nf-oct-pi (stylized pi icon)
53
+ model: "\uEC19", // nf-md-chip (model/AI chip)
54
+ folder: "\uF115", // nf-fa-folder_open
55
+ branch: "\uF126", // nf-fa-code_fork (git branch)
56
+ git: "\uF1D3", // nf-fa-git (git logo)
57
+ tokens: "\uE26B", // nf-seti-html (tokens symbol)
58
+ context: "\uE70F", // nf-dev-database (database)
59
+ cost: "\uF155", // nf-fa-dollar
60
+ time: "\uF017", // nf-fa-clock_o
61
+ agents: "\uF0C0", // nf-fa-users
62
+ cache: "\uF1C0", // nf-fa-database (cache)
63
+ input: "\uF090", // nf-fa-sign_in (input arrow)
64
+ output: "\uF08B", // nf-fa-sign_out (output arrow)
65
+ host: "\uF109", // nf-fa-laptop (host)
66
+ session: "\uF550", // nf-md-identifier (session id)
67
+ auto: "\u{F0068}", // nf-md-lightning_bolt (auto-compact)
68
+ warning: "\uF071", // nf-fa-warning
69
+ };
70
+
71
+ // ASCII/Unicode fallback icons (matching oh-my-pi)
72
+ export const ASCII_ICONS: IconSet = {
73
+ pi: "π",
74
+ model: "◈",
75
+ folder: "📁",
76
+ branch: "⎇",
77
+ git: "⎇",
78
+ tokens: "⊛",
79
+ context: "◫",
80
+ cost: "$",
81
+ time: "◷",
82
+ agents: "AG",
83
+ cache: "cache",
84
+ input: "in:",
85
+ output: "out:",
86
+ host: "host",
87
+ session: "id",
88
+ auto: "⚡",
89
+ warning: "⚠",
90
+ };
91
+
92
+ // Separator characters
93
+ export interface SeparatorChars {
94
+ powerlineLeft: string;
95
+ powerlineRight: string;
96
+ powerlineThinLeft: string;
97
+ powerlineThinRight: string;
98
+ slash: string;
99
+ pipe: string;
100
+ block: string;
101
+ space: string;
102
+ asciiLeft: string;
103
+ asciiRight: string;
104
+ dot: string;
105
+ }
106
+
107
+ export const NERD_SEPARATORS: SeparatorChars = {
108
+ powerlineLeft: "\uE0B0", //
109
+ powerlineRight: "\uE0B2", //
110
+ powerlineThinLeft: "\uE0B1", //
111
+ powerlineThinRight: "\uE0B3", //
112
+ slash: "/",
113
+ pipe: "|",
114
+ block: "█",
115
+ space: " ",
116
+ asciiLeft: ">",
117
+ asciiRight: "<",
118
+ dot: "·",
119
+ };
120
+
121
+ export const ASCII_SEPARATORS: SeparatorChars = {
122
+ powerlineLeft: ">",
123
+ powerlineRight: "<",
124
+ powerlineThinLeft: "|",
125
+ powerlineThinRight: "|",
126
+ slash: "/",
127
+ pipe: "|",
128
+ block: "#",
129
+ space: " ",
130
+ asciiLeft: ">",
131
+ asciiRight: "<",
132
+ dot: ".",
133
+ };
134
+
135
+ // Detect Nerd Font support (check TERM or specific env var)
136
+ export function hasNerdFonts(): boolean {
137
+ // User can set this env var to force Nerd Fonts
138
+ if (process.env.POWERLINE_NERD_FONTS === "1") return true;
139
+ if (process.env.POWERLINE_NERD_FONTS === "0") return false;
140
+
141
+ // Check for Ghostty (survives into tmux via GHOSTTY_RESOURCES_DIR)
142
+ if (process.env.GHOSTTY_RESOURCES_DIR) return true;
143
+
144
+ // Check common terminals known to support Nerd Fonts (case-insensitive)
145
+ const term = (process.env.TERM_PROGRAM || "").toLowerCase();
146
+ const nerdTerms = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
147
+ return nerdTerms.some(t => term.includes(t));
148
+ }
149
+
150
+ export function getIcons(): IconSet {
151
+ return hasNerdFonts() ? NERD_ICONS : ASCII_ICONS;
152
+ }
153
+
154
+ export function getSeparatorChars(): SeparatorChars {
155
+ return hasNerdFonts() ? NERD_SEPARATORS : ASCII_SEPARATORS;
156
+ }