@pi-unipi/footer 0.1.1

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.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @pi-unipi/footer — FooterRegistry
3
+ *
4
+ * Central registry for segment groups with event subscription,
5
+ * data caching, and reactive updates.
6
+ */
7
+
8
+ import type { FooterGroup } from "../types.js";
9
+
10
+ /** Type for the reactive update callback */
11
+ type UpdateCallback = () => void;
12
+
13
+ /**
14
+ * FooterRegistry manages segment groups and their cached data.
15
+ * It subscribes to UNIPI_EVENTS, caches data per group, and
16
+ * notifies subscribers when data changes.
17
+ */
18
+ export class FooterRegistry {
19
+ /** Registered segment groups */
20
+ private groups = new Map<string, FooterGroup>();
21
+
22
+ /** Cached event data per group */
23
+ private dataCache = new Map<string, unknown>();
24
+
25
+ /** Reactive update subscribers */
26
+ private subscribers = new Set<UpdateCallback>();
27
+
28
+ /** Whether to log debug info */
29
+ private debug: boolean;
30
+
31
+ constructor(options?: { debug?: boolean }) {
32
+ this.debug = options?.debug ?? false;
33
+ }
34
+
35
+ // ─── Group Management ─────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Register a segment group.
39
+ */
40
+ registerGroup(group: FooterGroup): void {
41
+ this.groups.set(group.id, group);
42
+ this.log("registerGroup", group.id);
43
+ }
44
+
45
+ /**
46
+ * Get a registered group by ID.
47
+ */
48
+ getGroup(groupId: string): FooterGroup | undefined {
49
+ return this.groups.get(groupId);
50
+ }
51
+
52
+ /**
53
+ * Get all registered groups.
54
+ */
55
+ getAllGroups(): FooterGroup[] {
56
+ return Array.from(this.groups.values());
57
+ }
58
+
59
+ // ─── Data Cache ───────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Update cached data for a group and notify subscribers.
63
+ */
64
+ updateData(groupId: string, data: unknown): void {
65
+ const previous = this.dataCache.get(groupId);
66
+ // Only notify if data actually changed (shallow compare)
67
+ if (previous === data) return;
68
+
69
+ this.dataCache.set(groupId, data);
70
+ this.log("updateData", groupId, data);
71
+ this.notifySubscribers();
72
+ }
73
+
74
+ /**
75
+ * Get cached data for a group.
76
+ */
77
+ getGroupData(groupId: string): unknown {
78
+ return this.dataCache.get(groupId);
79
+ }
80
+
81
+ /**
82
+ * Clear all cached data.
83
+ */
84
+ invalidateAll(): void {
85
+ this.dataCache.clear();
86
+ this.log("invalidateAll");
87
+ this.notifySubscribers();
88
+ }
89
+
90
+ /**
91
+ * Clear cached data for a specific group.
92
+ */
93
+ invalidateGroup(groupId: string): void {
94
+ this.dataCache.delete(groupId);
95
+ this.log("invalidateGroup", groupId);
96
+ this.notifySubscribers();
97
+ }
98
+
99
+ // ─── Reactive Subscriptions ───────────────────────────────────────────────
100
+
101
+ /**
102
+ * Subscribe to data updates. Returns an unsubscribe function.
103
+ */
104
+ subscribe(callback: UpdateCallback): () => void {
105
+ this.subscribers.add(callback);
106
+ return () => {
107
+ this.subscribers.delete(callback);
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Notify all subscribers of a data change.
113
+ */
114
+ private notifySubscribers(): void {
115
+ for (const callback of this.subscribers) {
116
+ try {
117
+ callback();
118
+ } catch (err) {
119
+ console.error("[footer] Subscriber error:", err);
120
+ }
121
+ }
122
+ }
123
+
124
+ // ─── Debug ────────────────────────────────────────────────────────────────
125
+
126
+ private log(event: string, ...args: unknown[]): void {
127
+ if (!this.debug) return;
128
+ const ts = new Date().toISOString().slice(11, 23);
129
+ const details = args.length > 0 ? " " + JSON.stringify(args) : "";
130
+ console.error(`[footer-registry:${ts}] ${event}${details}`);
131
+ }
132
+ }
133
+
134
+ // ─── Singleton ──────────────────────────────────────────────────────────────
135
+
136
+ /** Global registry instance */
137
+ let registryInstance: FooterRegistry | null = null;
138
+
139
+ /**
140
+ * Get the global FooterRegistry instance.
141
+ * Creates one on first call.
142
+ */
143
+ export function getFooterRegistry(): FooterRegistry {
144
+ if (!registryInstance) {
145
+ registryInstance = new FooterRegistry();
146
+ }
147
+ return registryInstance;
148
+ }
149
+
150
+ /**
151
+ * Reset the global registry (for testing).
152
+ */
153
+ export function resetFooterRegistry(): void {
154
+ registryInstance = null;
155
+ }
156
+
157
+ // ─── Global reference ───────────────────────────────────────────────────────
158
+
159
+ // Expose on globalThis for cross-package access
160
+ if (typeof globalThis !== "undefined") {
161
+ (globalThis as Record<string, unknown>).__unipi_footer_registry = getFooterRegistry();
162
+ }
@@ -0,0 +1,318 @@
1
+ /**
2
+ * @pi-unipi/footer — Icon system with 3 styles: Nerd Font, Emoji, Text
3
+ *
4
+ * Each icon set maps segment IDs to glyph strings.
5
+ * The active set is determined by the `iconStyle` setting:
6
+ * - "nerd" → Nerd Font glyphs (requires Nerd Font installed)
7
+ * - "emoji" → Unicode emoji / symbols (works on most terminals)
8
+ * - "text" → Plain text labels (works everywhere, most compact)
9
+ */
10
+
11
+ import { detectNerdFontSupport } from "./separators.js";
12
+
13
+ // ─── Icon definitions ───────────────────────────────────────────────────────
14
+
15
+ /** Icon set mapping segment IDs to glyph strings */
16
+ export interface IconSet {
17
+ // Core segments
18
+ model: string;
19
+ thinking: string;
20
+ path: string;
21
+ git: string;
22
+ context: string;
23
+ cost: string;
24
+ tokens: string;
25
+ tokensIn: string;
26
+ tokensOut: string;
27
+ session: string;
28
+ hostname: string;
29
+ time: string;
30
+
31
+ // Compactor segments
32
+ sessionEvents: string;
33
+ compactions: string;
34
+ tokensSaved: string;
35
+ compressionRatio: string;
36
+ indexedDocs: string;
37
+ sandboxRuns: string;
38
+ searchQueries: string;
39
+
40
+ // Memory segments
41
+ projectCount: string;
42
+ totalCount: string;
43
+ consolidations: string;
44
+
45
+ // MCP segments
46
+ serversTotal: string;
47
+ serversActive: string;
48
+ toolsTotal: string;
49
+ serversFailed: string;
50
+
51
+ // Ralph segments
52
+ activeLoops: string;
53
+ totalIterations: string;
54
+ loopStatus: string;
55
+
56
+ // Workflow segments
57
+ currentCommand: string;
58
+ sandboxLevel: string;
59
+ commandDuration: string;
60
+
61
+ // Kanboard segments
62
+ docsCount: string;
63
+ tasksDone: string;
64
+ tasksTotal: string;
65
+ taskPct: string;
66
+
67
+ // Notify segments
68
+ platformsEnabled: string;
69
+ lastSent: string;
70
+
71
+ // Status extension
72
+ extensionStatuses: string;
73
+
74
+ // Separator/group markers
75
+ separator: string;
76
+ }
77
+
78
+ // ─── Nerd Font icons ────────────────────────────────────────────────────────
79
+
80
+ /** Nerd Font glyphs — requires a Nerd Font installed in the terminal */
81
+ export const NERD_ICONS: IconSet = {
82
+ // Core
83
+ model: "\uEC19", // nf-md-chip
84
+ thinking: "\uE22C", // nf-oct-pi
85
+ path: "\uF115", // nf-fa-folder_open
86
+ git: "\uF126", // nf-fa-code_fork
87
+ context: "\uE70F", // nf-dev-database
88
+ cost: "\uF155", // nf-fa-dollar
89
+ tokens: "\uE26B", // nf-seti-html
90
+ tokensIn: "\uF090", // nf-fa-sign_in
91
+ tokensOut: "\uF08B", // nf-fa-sign_out
92
+ session: "\uF550", // nf-md-identifier
93
+ hostname: "\uF109", // nf-fa-laptop
94
+ time: "\uF017", // nf-fa-clock_o
95
+
96
+ // Compactor
97
+ sessionEvents: "\uF0C0", // nf-fa-users
98
+ compactions: "\uF1C0", // nf-fa-database
99
+ tokensSaved: "\uF155", // nf-fa-dollar
100
+ compressionRatio:"\uE70F", // nf-dev-database
101
+ indexedDocs: "\uF02D", // nf-fa-book
102
+ sandboxRuns: "\uF121", // nf-fa-terminal
103
+ searchQueries: "\uF002", // nf-fa-search
104
+
105
+ // Memory
106
+ projectCount: "\uee9c", // memory icon
107
+ totalCount: "\uee9c", // memory icon
108
+ consolidations: "\uee9c", // memory icon
109
+
110
+ // MCP
111
+ serversTotal: "\uF233", // nf-fa-server
112
+ serversActive: "\uF058", // nf-fa-check_circle
113
+ toolsTotal: "\uF0AD", // nf-fa-wrench
114
+ serversFailed: "\uF071", // nf-fa-warning
115
+
116
+ // Ralph
117
+ activeLoops: "\udb81\udf09", // 󰼉 ralph loop icon
118
+ totalIterations: "\udb81\udf09", // 󰼉 ralph loop icon
119
+ loopStatus: "\udb81\udf09", // 󰼉 ralph loop icon
120
+
121
+ // Workflow
122
+ currentCommand: "\uf52e", // workflow icon
123
+ sandboxLevel: "\uf023", // nf-fa-lock
124
+ commandDuration: "\uf017", // nf-fa-clock_o
125
+
126
+ // Kanboard
127
+ docsCount: "\uF15C", // nf-fa-file_text
128
+ tasksDone: "\uF058", // nf-fa-check_circle
129
+ tasksTotal: "\uF0AE", // nf-fa-tasks
130
+ taskPct: "\uF200", // nf-fa-pie_chart
131
+
132
+ // Notify
133
+ platformsEnabled:"\uF0E0", // nf-fa-envelope
134
+ lastSent: "\uF017", // nf-fa-clock_o
135
+
136
+ // Extension status
137
+ extensionStatuses:"\uF1E6", // nf-fa-plug
138
+
139
+ separator: "\uE0B1", // nf-pl-left_soft_divider
140
+ };
141
+
142
+ // ─── Emoji icons ─────────────────────────────────────────────────────────────
143
+
144
+ /** Unicode emoji / symbol icons — works on most modern terminals */
145
+ export const EMOJI_ICONS: IconSet = {
146
+ // Core
147
+ model: "",
148
+ thinking: "π",
149
+ path: "",
150
+ git: "⎇",
151
+ context: "",
152
+ cost: "$",
153
+ tokens: "⊛",
154
+ tokensIn: "→",
155
+ tokensOut: "←",
156
+ session: "#",
157
+ hostname: "⌂",
158
+ time: "⏱",
159
+
160
+ // Compactor
161
+ sessionEvents: "⚡",
162
+ compactions: "◧",
163
+ tokensSaved: "$",
164
+ compressionRatio:"⇄",
165
+ indexedDocs: "☰",
166
+ sandboxRuns: "▶",
167
+ searchQueries: "⊗",
168
+
169
+ // Memory
170
+ projectCount: "\uee9c",
171
+ totalCount: "\uee9c",
172
+ consolidations: "\uee9c",
173
+
174
+ // MCP
175
+ serversTotal: "srv",
176
+ serversActive: "●",
177
+ toolsTotal: "🔧",
178
+ serversFailed: "⚠",
179
+
180
+ // Ralph
181
+ activeLoops: "",
182
+ totalIterations: "",
183
+ loopStatus: "",
184
+
185
+ // Workflow
186
+ currentCommand: "",
187
+ sandboxLevel: "◧",
188
+ commandDuration: "⏱",
189
+
190
+ // Kanboard
191
+ docsCount: "☰",
192
+ tasksDone: "✓",
193
+ tasksTotal: "☐",
194
+ taskPct: "%",
195
+
196
+ // Notify
197
+ platformsEnabled:"♮",
198
+ lastSent: "⏱",
199
+
200
+ // Extension status
201
+ extensionStatuses:"▦",
202
+
203
+ separator: "|",
204
+ };
205
+
206
+ // ─── Text icons ──────────────────────────────────────────────────────────────
207
+
208
+ /** Plain text labels — works everywhere, most compact */
209
+ export const TEXT_ICONS: IconSet = {
210
+ // Core
211
+ model: "",
212
+ thinking: "",
213
+ path: "",
214
+ git: "",
215
+ context: "",
216
+ cost: "",
217
+ tokens: "",
218
+ tokensIn: "",
219
+ tokensOut: "",
220
+ session: "",
221
+ hostname: "",
222
+ time: "",
223
+
224
+ // Compactor
225
+ sessionEvents: "evt",
226
+ compactions: "cmp",
227
+ tokensSaved: "svd",
228
+ compressionRatio:"rat",
229
+ indexedDocs: "idx",
230
+ sandboxRuns: "sbx",
231
+ searchQueries: "qry",
232
+
233
+ // Memory
234
+ projectCount: "mem",
235
+ totalCount: "mem",
236
+ consolidations: "cns",
237
+
238
+ // MCP
239
+ serversTotal: "srv",
240
+ serversActive: "act",
241
+ toolsTotal: "tls",
242
+ serversFailed: "err",
243
+
244
+ // Ralph
245
+ activeLoops: "",
246
+ totalIterations: "",
247
+ loopStatus: "",
248
+
249
+ // Workflow
250
+ currentCommand: "",
251
+ sandboxLevel: "sbx",
252
+ commandDuration: "dur",
253
+
254
+ // Kanboard
255
+ docsCount: "doc",
256
+ tasksDone: "✓",
257
+ tasksTotal: "tsk",
258
+ taskPct: "pct",
259
+
260
+ // Notify
261
+ platformsEnabled:"ntf",
262
+ lastSent: "lst",
263
+
264
+ // Extension status
265
+ extensionStatuses:"ext",
266
+
267
+ separator: "|",
268
+ };
269
+
270
+ // ─── Icon lookup ─────────────────────────────────────────────────────────────
271
+
272
+ /** Current icon style — updated by the renderer when settings change */
273
+ let currentIconStyle: "nerd" | "emoji" | "text" | undefined;
274
+
275
+ /** Set the active icon style (called by renderer when settings change) */
276
+ export function setIconStyle(style: "nerd" | "emoji" | "text" | undefined): void {
277
+ currentIconStyle = style;
278
+ }
279
+
280
+ /** Resolve the effective icon style from settings + terminal detection */
281
+ export function resolveIconStyle(configured?: string): "nerd" | "emoji" | "text" {
282
+ // Explicit setting wins
283
+ if (configured === "nerd" || configured === "emoji" || configured === "text") {
284
+ return configured;
285
+ }
286
+
287
+ // Auto-detect: use Nerd Font if terminal supports it, emoji otherwise
288
+ return detectNerdFontSupport() ? "nerd" : "emoji";
289
+ }
290
+
291
+ /**
292
+ * Get the icon for a segment by ID.
293
+ * Uses the configured icon style, falling back to auto-detection.
294
+ */
295
+ export function getIcon(segmentId: string, overrideStyle?: "nerd" | "emoji" | "text"): string {
296
+ const style = overrideStyle ?? currentIconStyle ?? resolveIconStyle();
297
+ const sets: Record<string, IconSet> = {
298
+ nerd: NERD_ICONS,
299
+ emoji: EMOJI_ICONS,
300
+ text: TEXT_ICONS,
301
+ };
302
+ const icons = sets[style];
303
+ const key = segmentId as keyof IconSet;
304
+ return icons[key] ?? "";
305
+ }
306
+
307
+ /**
308
+ * Get the full icon set based on the configured style.
309
+ */
310
+ export function getIcons(iconStyle?: "nerd" | "emoji" | "text"): IconSet {
311
+ const style = iconStyle ?? currentIconStyle ?? resolveIconStyle();
312
+ const sets: Record<string, IconSet> = {
313
+ nerd: NERD_ICONS,
314
+ emoji: EMOJI_ICONS,
315
+ text: TEXT_ICONS,
316
+ };
317
+ return sets[style];
318
+ }