@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.
- package/README.md +206 -0
- package/index.ts +6 -0
- package/package.json +52 -0
- package/src/commands.ts +204 -0
- package/src/config.ts +177 -0
- package/src/events.ts +256 -0
- package/src/index.ts +208 -0
- package/src/presets.ts +131 -0
- package/src/registry/index.ts +162 -0
- package/src/rendering/icons.ts +318 -0
- package/src/rendering/renderer.ts +310 -0
- package/src/rendering/separators.ts +112 -0
- package/src/rendering/theme.ts +98 -0
- package/src/segments/compactor.ts +135 -0
- package/src/segments/core.ts +283 -0
- package/src/segments/kanboard.ts +75 -0
- package/src/segments/mcp.ts +100 -0
- package/src/segments/memory.ts +140 -0
- package/src/segments/notify.ts +50 -0
- package/src/segments/ralph.ts +109 -0
- package/src/segments/status-ext.ts +119 -0
- package/src/segments/workflow.ts +100 -0
- package/src/tui/settings-tui.ts +252 -0
- package/src/types.ts +183 -0
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# @pi-unipi/footer
|
|
2
|
+
|
|
3
|
+
Persistent status bar for the Unipi extension suite.
|
|
4
|
+
|
|
5
|
+
Subscribes to `UNIPI_EVENTS` and renders key stats from all unipi packages using pi's `setFooter` + `setWidget` APIs with responsive layout, presets, and per-segment toggling.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Persistent status bar** — always-visible footer showing key stats from all unipi packages
|
|
10
|
+
- **Segment groups** — organized by package (core, compactor, memory, MCP, ralph, workflow, kanboard, notify)
|
|
11
|
+
- **Presets** — default, minimal, compact, full, nerd, ascii
|
|
12
|
+
- **Responsive layout** — adjusts to terminal width with secondary row overflow
|
|
13
|
+
- **Per-segment toggling** — enable/disable individual segments or entire groups
|
|
14
|
+
- **Theme integration** — uses pi's theme system with semantic colors
|
|
15
|
+
- **Nerd Font support** — auto-detection with ASCII fallback
|
|
16
|
+
- **Separator styles** — powerline, powerline-thin, slash, pipe, dot, ascii
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────────────────────────────────────────┐
|
|
22
|
+
│ FooterRenderer (setFooter + setWidget) │ ← Renders to screen
|
|
23
|
+
│ - Responsive layout (top + secondary rows) │
|
|
24
|
+
│ - Preset system, separators, theming │
|
|
25
|
+
├─────────────────────────────────────────────────────┤
|
|
26
|
+
│ FooterRegistry (segment groups) │ ← Manages segments
|
|
27
|
+
│ - Subscribes to UNIPI_EVENTS │
|
|
28
|
+
│ - Per-segment enable/disable │
|
|
29
|
+
│ - Reactive data caching │
|
|
30
|
+
├─────────────────────────────────────────────────────┤
|
|
31
|
+
│ Event Sources (existing packages) │ ← Data providers
|
|
32
|
+
│ - compactor, memory, workflow, ralph, mcp, │
|
|
33
|
+
│ kanboard, notify, core │
|
|
34
|
+
└─────────────────────────────────────────────────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
The footer is automatically enabled when unipi loads. Use commands to control it:
|
|
40
|
+
|
|
41
|
+
- `/unipi:footer` — toggle footer on/off
|
|
42
|
+
- `/unipi:footer <preset>` — switch preset (default, minimal, compact, full, nerd, ascii)
|
|
43
|
+
- `/unipi:footer sep:<style>` — change separator style (powerline, powerline-thin, slash, pipe, dot, ascii)
|
|
44
|
+
- `/unipi:footer icon:<style>` — change icon style (nerd, emoji, text)
|
|
45
|
+
- `/unipi:footer on` / `/unipi:footer off` — enable/disable explicitly
|
|
46
|
+
- `/unipi:footer-settings` — open settings TUI for per-group/per-segment toggles
|
|
47
|
+
|
|
48
|
+
## Segment Groups
|
|
49
|
+
|
|
50
|
+
| Group | Segments | Default | Data Source |
|
|
51
|
+
|-------|----------|---------|-------------|
|
|
52
|
+
| **core** | `model`, `thinking`, `path`, `git`, `context_pct`, `cost`, `tokens_total`, `tokens_in`, `tokens_out`, `session`, `hostname`, `time` | ON (except hostname, time, tokens variants) | pi SDK (ctx.sessionManager, footerData) |
|
|
53
|
+
| **compactor** | `session_events`, `compactions`, `tokens_saved`, `compression_ratio`, `indexed_docs`, `sandbox_runs`, `search_queries` | ON (key stats only) | `COMPACTOR_STATS_UPDATED` event |
|
|
54
|
+
| **memory** | `project_count`, `total_count`, `consolidations` | ON | `MEMORY_STORED`/`DELETED`/`CONSOLIDATED` events |
|
|
55
|
+
| **mcp** | `servers_total`, `servers_active`, `tools_total`, `servers_failed` | ON | `MCP_SERVER_STARTED`/`STOPPED`/`ERROR` events |
|
|
56
|
+
| **ralph** | `active_loops`, `total_iterations`, `loop_status` | ON | `RALPH_LOOP_START`/`END`/`ITERATION_DONE` events |
|
|
57
|
+
| **workflow** | `current_command`, `sandbox_level`, `command_duration` | ON | `WORKFLOW_START`/`END` events |
|
|
58
|
+
| **kanboard** | `docs_count`, `tasks_done`, `tasks_total`, `task_pct` | ON | Kanboard registry (direct read) |
|
|
59
|
+
| **notify** | `platforms_enabled`, `last_sent` | OFF | `NOTIFICATION_SENT` event |
|
|
60
|
+
| **status_ext** | `extension_statuses` | ON | `footerData.getExtensionStatuses()` |
|
|
61
|
+
|
|
62
|
+
## Presets
|
|
63
|
+
|
|
64
|
+
| Preset | Description | Key Segments |
|
|
65
|
+
|--------|-------------|-------------|
|
|
66
|
+
| `default` | Balanced view | model, thinking, path, git, context, cost + compactor + memory + ralph |
|
|
67
|
+
| `minimal` | Just the essentials | path, git, context |
|
|
68
|
+
| `compact` | Core + key stats | model, git, cost, context + compactor + memory |
|
|
69
|
+
| `full` | Everything | All segments from all groups |
|
|
70
|
+
| `nerd` | Maximum detail for Nerd Font users | full + hostname + time + session + extensions |
|
|
71
|
+
| `ascii` | Safe for any terminal | Core segments with ASCII icons |
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
Settings are stored in `~/.pi/agent/settings.json` under `unipi.footer`:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"unipi": {
|
|
80
|
+
"footer": {
|
|
81
|
+
"enabled": true,
|
|
82
|
+
"preset": "default",
|
|
83
|
+
"separator": "powerline-thin",
|
|
84
|
+
"iconStyle": "nerd",
|
|
85
|
+
"groups": {
|
|
86
|
+
"compactor": {
|
|
87
|
+
"show": true,
|
|
88
|
+
"segments": {
|
|
89
|
+
"session_events": true,
|
|
90
|
+
"compactions": true,
|
|
91
|
+
"tokens_saved": true,
|
|
92
|
+
"compression_ratio": false,
|
|
93
|
+
"indexed_docs": false,
|
|
94
|
+
"sandbox_runs": false,
|
|
95
|
+
"search_queries": false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"memory": {
|
|
99
|
+
"show": true,
|
|
100
|
+
"segments": {
|
|
101
|
+
"project_count": true,
|
|
102
|
+
"total_count": true,
|
|
103
|
+
"consolidations": false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Responsive Layout
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
Wide terminal (>120 cols):
|
|
116
|
+
┌─ model │ thinking │ path │ git │ context │ cost │ compactions │ tokens_saved │ project_count ─┐
|
|
117
|
+
└──────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
118
|
+
|
|
119
|
+
Narrow terminal (<120 cols):
|
|
120
|
+
┌─ model │ thinking │ path │ git │ context │ cost ───────────────────────────────────────────────┐
|
|
121
|
+
└─ compactions │ tokens_saved │ project_count │ ralph │ workflow ────────────────────────────────┘
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Separator Styles
|
|
125
|
+
|
|
126
|
+
| Style | Look | Description |
|
|
127
|
+
|-------|------|-------------|
|
|
128
|
+
| `powerline` | ◀ ▶ | Thick powerline arrows |
|
|
129
|
+
| `powerline-thin` | | Thin powerline arrows (default) |
|
|
130
|
+
| `slash` | / | Slash separator |
|
|
131
|
+
| `pipe` | \| | Pipe separator |
|
|
132
|
+
| `dot` | · | Middle dot separator |
|
|
133
|
+
| `ascii` | > < | ASCII angle brackets |
|
|
134
|
+
|
|
135
|
+
## Icon Styles
|
|
136
|
+
|
|
137
|
+
Three icon styles are available, controlled by `/unipi:footer icon:<style>` or the `iconStyle` setting:
|
|
138
|
+
|
|
139
|
+
| Style | Description | Example |
|
|
140
|
+
-------|-------------|--------|
|
|
141
|
+
| `nerd` | Nerd Font glyphs (default, requires Nerd Font terminal) | , , |
|
|
142
|
+
| `emoji` | Unicode emoji/symbols (works on most terminals) | ⚡, ◧, $] |
|
|
143
|
+
| `text` | Plain text abbreviations (works everywhere, most compact) | evt, cmp, $] |
|
|
144
|
+
|
|
145
|
+
When `iconStyle` is not explicitly set, the footer auto-detects Nerd Font support and
|
|
146
|
+
defaults to `nerd` if available, `emoji` otherwise.
|
|
147
|
+
|
|
148
|
+
## Error Handling
|
|
149
|
+
|
|
150
|
+
- **Event subscription failures:** Each handler wrapped in try/catch — one failing handler doesn't break others
|
|
151
|
+
- **Data provider failures:** Segments hide when data unavailable (graceful degradation)
|
|
152
|
+
- **Config parse failures:** Fall back to default preset with warning
|
|
153
|
+
- **Module loading order:** Footer works even if packages load after it — late-arriving events update cache
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Run tests
|
|
159
|
+
pnpm test
|
|
160
|
+
|
|
161
|
+
# Type check
|
|
162
|
+
pnpm tsc --noEmit
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Package Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
packages/footer/
|
|
169
|
+
├── index.ts # Re-exports
|
|
170
|
+
├── types.ts # Re-exports from src/types.ts
|
|
171
|
+
├── package.json # Package manifest
|
|
172
|
+
├── tsconfig.json # TypeScript config
|
|
173
|
+
├── README.md # This file
|
|
174
|
+
├── src/
|
|
175
|
+
│ ├── index.ts # Extension entry point
|
|
176
|
+
│ ├── types.ts # Type definitions
|
|
177
|
+
│ ├── config.ts # Settings load/save
|
|
178
|
+
│ ├── events.ts # Event subscription wiring
|
|
179
|
+
│ ├── commands.ts # Command registration
|
|
180
|
+
│ ├── presets.ts # Preset definitions
|
|
181
|
+
│ ├── registry/ # FooterRegistry
|
|
182
|
+
│ │ └── index.ts
|
|
183
|
+
│ ├── rendering/ # Rendering engine
|
|
184
|
+
│ │ ├── renderer.ts # FooterRenderer class
|
|
185
|
+
│ │ ├── separators.ts # Separator system
|
|
186
|
+
│ │ ├── theme.ts # Theme color resolution
|
|
187
|
+
│ │ └── icons.ts # Icon system with Nerd Font detection
|
|
188
|
+
│ ├── segments/ # Segment implementations
|
|
189
|
+
│ │ ├── core.ts # Core segments (model, path, git, etc.)
|
|
190
|
+
│ │ ├── compactor.ts # Compactor segments
|
|
191
|
+
│ │ ├── memory.ts # Memory segments
|
|
192
|
+
│ │ ├── mcp.ts # MCP segments
|
|
193
|
+
│ │ ├── ralph.ts # Ralph segments
|
|
194
|
+
│ │ ├── workflow.ts # Workflow segments
|
|
195
|
+
│ │ ├── kanboard.ts # Kanboard segments
|
|
196
|
+
│ │ ├── notify.ts # Notify segments
|
|
197
|
+
│ │ └── status-ext.ts # Extension statuses segment
|
|
198
|
+
│ └── tui/
|
|
199
|
+
│ └── settings-tui.ts # Settings overlay TUI
|
|
200
|
+
└── tests/ # Unit tests
|
|
201
|
+
├── separators.test.ts
|
|
202
|
+
├── registry.test.ts
|
|
203
|
+
├── config.test.ts
|
|
204
|
+
├── segments.test.ts
|
|
205
|
+
└── events.test.ts
|
|
206
|
+
```
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-unipi/footer",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Neuron Mr White",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Neuron-Mr-White/unipi.git",
|
|
11
|
+
"directory": "packages/footer"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"pi-extension",
|
|
16
|
+
"pi-coding-agent",
|
|
17
|
+
"unipi",
|
|
18
|
+
"footer",
|
|
19
|
+
"status-bar"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"index.ts",
|
|
23
|
+
"src/**/*.ts",
|
|
24
|
+
"skills/**/*",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"pi": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"src/index.ts"
|
|
30
|
+
],
|
|
31
|
+
"skills": [
|
|
32
|
+
"skills"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --experimental-strip-types --test tests/**/*.test.ts"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@pi-unipi/core": "*"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
46
|
+
"@mariozechner/pi-tui": "*"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^25.6.0",
|
|
50
|
+
"typescript": "^6.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Commands
|
|
3
|
+
*
|
|
4
|
+
* Footer commands: /unipi:footer (toggle), /unipi:footer <preset>,
|
|
5
|
+
* /unipi:footer-settings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { UNIPI_PREFIX, FOOTER_COMMANDS } from "@pi-unipi/core";
|
|
10
|
+
import { loadFooterSettings, saveFooterSettings } from "./config.js";
|
|
11
|
+
import { PRESET_NAMES } from "./presets.js";
|
|
12
|
+
import { showFooterSettings } from "./tui/settings-tui.js";
|
|
13
|
+
import type { FooterGroup, SeparatorStyle, IconStyle } from "./types.js";
|
|
14
|
+
import { setIconStyle } from "./rendering/icons.js";
|
|
15
|
+
|
|
16
|
+
/** Minimal autocomplete item (compatible with pi-tui AutocompleteItem) */
|
|
17
|
+
interface ArgSuggestion {
|
|
18
|
+
value: string;
|
|
19
|
+
label: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** All valid separator styles */
|
|
24
|
+
const SEPARATOR_STYLES: SeparatorStyle[] = [
|
|
25
|
+
"powerline",
|
|
26
|
+
"powerline-thin",
|
|
27
|
+
"slash",
|
|
28
|
+
"pipe",
|
|
29
|
+
"dot",
|
|
30
|
+
"ascii",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** All valid icon styles */
|
|
34
|
+
const ICON_STYLES: IconStyle[] = [
|
|
35
|
+
"nerd",
|
|
36
|
+
"emoji",
|
|
37
|
+
"text",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** Extension state interface */
|
|
41
|
+
interface FooterState {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
renderer: {
|
|
44
|
+
setPreset(name: string): void;
|
|
45
|
+
setActive(active: boolean): void;
|
|
46
|
+
getPresetName(): string;
|
|
47
|
+
resetLayoutCache(): void;
|
|
48
|
+
};
|
|
49
|
+
piContext: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register footer commands.
|
|
54
|
+
*/
|
|
55
|
+
export function registerCommands(
|
|
56
|
+
pi: ExtensionAPI,
|
|
57
|
+
state: FooterState,
|
|
58
|
+
groups?: FooterGroup[],
|
|
59
|
+
): void {
|
|
60
|
+
// /unipi:footer — toggle or switch preset
|
|
61
|
+
pi.registerCommand(`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`, {
|
|
62
|
+
description: "Toggle footer or switch preset (default, minimal, compact, full, nerd, ascii)",
|
|
63
|
+
getArgumentCompletions(argumentPrefix: string): ArgSuggestion[] | null {
|
|
64
|
+
const allOptions: ArgSuggestion[] = [
|
|
65
|
+
...PRESET_NAMES.map(p => ({
|
|
66
|
+
value: p,
|
|
67
|
+
label: p,
|
|
68
|
+
description: `Switch to ${p} preset`,
|
|
69
|
+
})),
|
|
70
|
+
...SEPARATOR_STYLES.map(s => ({
|
|
71
|
+
value: `sep:${s}`,
|
|
72
|
+
label: `sep:${s}`,
|
|
73
|
+
description: `Set separator style: ${s}`,
|
|
74
|
+
})),
|
|
75
|
+
...ICON_STYLES.map(s => ({
|
|
76
|
+
value: `icon:${s}`,
|
|
77
|
+
label: `icon:${s}`,
|
|
78
|
+
description: `Set icon style: ${s}`,
|
|
79
|
+
})),
|
|
80
|
+
{
|
|
81
|
+
value: "on",
|
|
82
|
+
label: "on",
|
|
83
|
+
description: "Enable footer",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
value: "off",
|
|
87
|
+
label: "off",
|
|
88
|
+
description: "Disable footer",
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
if (!argumentPrefix) return allOptions;
|
|
93
|
+
|
|
94
|
+
const prefix = argumentPrefix.toLowerCase();
|
|
95
|
+
const filtered = allOptions.filter(o =>
|
|
96
|
+
o.value.toLowerCase().startsWith(prefix),
|
|
97
|
+
);
|
|
98
|
+
return filtered.length > 0 ? filtered : null;
|
|
99
|
+
},
|
|
100
|
+
handler: async (args, ctx) => {
|
|
101
|
+
if (!args?.trim()) {
|
|
102
|
+
// Toggle on/off
|
|
103
|
+
state.enabled = !state.enabled;
|
|
104
|
+
state.renderer.setActive(state.enabled);
|
|
105
|
+
|
|
106
|
+
if (state.enabled) {
|
|
107
|
+
ctx.ui.notify("Footer enabled", "info");
|
|
108
|
+
} else {
|
|
109
|
+
ctx.ui.setFooter(undefined);
|
|
110
|
+
ctx.ui.setWidget("footer-top", undefined);
|
|
111
|
+
ctx.ui.setWidget("footer-secondary", undefined);
|
|
112
|
+
ctx.ui.notify("Footer disabled", "info");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
saveFooterSettings({ enabled: state.enabled });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const arg = args.trim().toLowerCase();
|
|
120
|
+
|
|
121
|
+
// on / off
|
|
122
|
+
if (arg === "on") {
|
|
123
|
+
state.enabled = true;
|
|
124
|
+
state.renderer.setActive(true);
|
|
125
|
+
saveFooterSettings({ enabled: true });
|
|
126
|
+
ctx.ui.notify("Footer enabled", "info");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (arg === "off") {
|
|
130
|
+
state.enabled = false;
|
|
131
|
+
state.renderer.setActive(false);
|
|
132
|
+
ctx.ui.setFooter(undefined);
|
|
133
|
+
ctx.ui.setWidget("footer-top", undefined);
|
|
134
|
+
ctx.ui.setWidget("footer-secondary", undefined);
|
|
135
|
+
saveFooterSettings({ enabled: false });
|
|
136
|
+
ctx.ui.notify("Footer disabled", "info");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// sep:<style> — change separator
|
|
141
|
+
if (arg.startsWith("sep:")) {
|
|
142
|
+
const style = arg.slice(4) as SeparatorStyle;
|
|
143
|
+
if (SEPARATOR_STYLES.includes(style)) {
|
|
144
|
+
saveFooterSettings({ separator: style });
|
|
145
|
+
state.renderer.resetLayoutCache();
|
|
146
|
+
ctx.ui.notify(`Separator: ${style}`, "info");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
ctx.ui.notify(`Unknown separator. Available: ${SEPARATOR_STYLES.join(", ")}`, "warning");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// icon:<style> — change icon style
|
|
154
|
+
if (arg.startsWith("icon:")) {
|
|
155
|
+
const style = arg.slice(5) as IconStyle;
|
|
156
|
+
if (ICON_STYLES.includes(style)) {
|
|
157
|
+
saveFooterSettings({ iconStyle: style });
|
|
158
|
+
setIconStyle(style);
|
|
159
|
+
state.renderer.resetLayoutCache();
|
|
160
|
+
ctx.ui.notify(`Icon style: ${style}`, "info");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
ctx.ui.notify(`Unknown icon style. Available: ${ICON_STYLES.join(", ")}`, "warning");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Preset name
|
|
168
|
+
if (PRESET_NAMES.includes(arg)) {
|
|
169
|
+
state.renderer.setPreset(arg);
|
|
170
|
+
saveFooterSettings({ preset: arg });
|
|
171
|
+
ctx.ui.notify(`Footer preset: ${arg}`, "info");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
ctx.ui.notify(`Unknown argument. Use a preset (${PRESET_NAMES.join(", ")}), sep:<style>, icon:<style>, on, or off`, "info");
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// /unipi:footer-settings — open settings TUI
|
|
180
|
+
pi.registerCommand(`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_SETTINGS}`, {
|
|
181
|
+
description: "Open footer settings (toggle groups and segments)",
|
|
182
|
+
handler: async (_args, ctx) => {
|
|
183
|
+
if (!ctx.hasUI) {
|
|
184
|
+
ctx.ui.notify("Footer settings requires a TUI", "warning");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (groups && groups.length > 0) {
|
|
189
|
+
showFooterSettings(ctx, groups);
|
|
190
|
+
} else {
|
|
191
|
+
// Fallback: show text summary
|
|
192
|
+
const settings = loadFooterSettings();
|
|
193
|
+
const info = [
|
|
194
|
+
`Enabled: ${settings.enabled}`,
|
|
195
|
+
`Preset: ${state.renderer.getPresetName()}`,
|
|
196
|
+
`Separator: ${settings.separator}`,
|
|
197
|
+
`Icon: ${settings.iconStyle}`,
|
|
198
|
+
`Groups: ${Object.entries(settings.groups).filter(([, g]) => g.show).map(([id]) => id).join(", ")}`,
|
|
199
|
+
].join("\n");
|
|
200
|
+
ctx.ui.notify(info, "info");
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Configuration system
|
|
3
|
+
*
|
|
4
|
+
* Loads/saves footer settings from ~/.pi/agent/settings.json
|
|
5
|
+
* under the `unipi.footer` key.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import type { FooterSettings, FooterGroupSettings, SeparatorStyle, IconStyle } from "./types.js";
|
|
12
|
+
import { UNIPI_SETTINGS_KEY } from "@pi-unipi/core";
|
|
13
|
+
|
|
14
|
+
/** Default footer settings */
|
|
15
|
+
export const DEFAULT_FOOTER_SETTINGS: FooterSettings = {
|
|
16
|
+
enabled: true,
|
|
17
|
+
preset: "default",
|
|
18
|
+
separator: "powerline-thin",
|
|
19
|
+
iconStyle: "nerd",
|
|
20
|
+
groups: {
|
|
21
|
+
core: { show: true, segments: {} },
|
|
22
|
+
compactor: { show: true, segments: {} },
|
|
23
|
+
memory: { show: true, segments: {} },
|
|
24
|
+
mcp: { show: true, segments: {} },
|
|
25
|
+
ralph: { show: true, segments: {} },
|
|
26
|
+
workflow: { show: true, segments: {} },
|
|
27
|
+
kanboard: { show: true, segments: {} },
|
|
28
|
+
notify: { show: false, segments: {} },
|
|
29
|
+
status_ext: { show: true, segments: {} },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the path to pi's settings.json
|
|
35
|
+
*/
|
|
36
|
+
function getSettingsPath(): string {
|
|
37
|
+
const agentDir = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
|
|
38
|
+
return path.join(agentDir, "settings.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read the raw settings.json file.
|
|
43
|
+
* Returns null if file doesn't exist or is malformed.
|
|
44
|
+
*/
|
|
45
|
+
function readSettingsFile(): Record<string, unknown> | null {
|
|
46
|
+
try {
|
|
47
|
+
const settingsPath = getSettingsPath();
|
|
48
|
+
if (!fs.existsSync(settingsPath)) return null;
|
|
49
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
50
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.warn("[footer] Failed to read settings.json:", err);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Write settings back to settings.json.
|
|
59
|
+
*/
|
|
60
|
+
function writeSettingsFile(settings: Record<string, unknown>): boolean {
|
|
61
|
+
try {
|
|
62
|
+
const settingsPath = getSettingsPath();
|
|
63
|
+
const dir = path.dirname(settingsPath);
|
|
64
|
+
if (!fs.existsSync(dir)) {
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
68
|
+
return true;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn("[footer] Failed to write settings.json:", err);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load footer settings from settings.json.
|
|
77
|
+
* Falls back to defaults for any missing fields.
|
|
78
|
+
*/
|
|
79
|
+
export function loadFooterSettings(): FooterSettings {
|
|
80
|
+
const raw = readSettingsFile();
|
|
81
|
+
if (!raw) return { ...DEFAULT_FOOTER_SETTINGS };
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const unipi = raw[UNIPI_SETTINGS_KEY] as Record<string, unknown> | undefined;
|
|
85
|
+
if (!unipi) return { ...DEFAULT_FOOTER_SETTINGS };
|
|
86
|
+
|
|
87
|
+
const footer = unipi.footer as Record<string, unknown> | undefined;
|
|
88
|
+
if (!footer) return { ...DEFAULT_FOOTER_SETTINGS };
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
enabled: typeof footer.enabled === "boolean" ? footer.enabled : DEFAULT_FOOTER_SETTINGS.enabled,
|
|
92
|
+
preset: typeof footer.preset === "string" ? footer.preset : DEFAULT_FOOTER_SETTINGS.preset,
|
|
93
|
+
separator: isValidSeparator(footer.separator) ? footer.separator as SeparatorStyle : DEFAULT_FOOTER_SETTINGS.separator,
|
|
94
|
+
iconStyle: isValidIconStyle(footer.iconStyle) ? footer.iconStyle as IconStyle : DEFAULT_FOOTER_SETTINGS.iconStyle,
|
|
95
|
+
groups: mergeGroupSettings(
|
|
96
|
+
DEFAULT_FOOTER_SETTINGS.groups,
|
|
97
|
+
footer.groups as Record<string, FooterGroupSettings> | undefined,
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn("[footer] Failed to parse footer settings, using defaults:", err);
|
|
102
|
+
return { ...DEFAULT_FOOTER_SETTINGS };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Save footer settings to settings.json.
|
|
108
|
+
* Merges with existing settings (preserves other keys).
|
|
109
|
+
*/
|
|
110
|
+
export function saveFooterSettings(partial: Partial<FooterSettings>): boolean {
|
|
111
|
+
const raw = readSettingsFile() ?? {};
|
|
112
|
+
const unipi = (raw[UNIPI_SETTINGS_KEY] as Record<string, unknown>) ?? {};
|
|
113
|
+
const existing = (unipi.footer as Record<string, unknown>) ?? {};
|
|
114
|
+
|
|
115
|
+
unipi.footer = { ...existing, ...partial };
|
|
116
|
+
raw[UNIPI_SETTINGS_KEY] = unipi;
|
|
117
|
+
|
|
118
|
+
return writeSettingsFile(raw);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get settings for a specific group.
|
|
123
|
+
* Falls back to defaults if group not configured.
|
|
124
|
+
*/
|
|
125
|
+
export function getGroupSettings(groupId: string): FooterGroupSettings {
|
|
126
|
+
const settings = loadFooterSettings();
|
|
127
|
+
return settings.groups[groupId] ?? { show: true, segments: {} };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if a specific segment is enabled.
|
|
132
|
+
* Respects both group-level and segment-level settings.
|
|
133
|
+
*/
|
|
134
|
+
export function isSegmentEnabled(groupId: string, segmentId: string): boolean {
|
|
135
|
+
const groupSettings = getGroupSettings(groupId);
|
|
136
|
+
if (!groupSettings.show) return false;
|
|
137
|
+
if (groupSettings.segments && segmentId in groupSettings.segments) {
|
|
138
|
+
return groupSettings.segments[segmentId] ?? true;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function isValidSeparator(value: unknown): boolean {
|
|
146
|
+
if (typeof value !== "string") return false;
|
|
147
|
+
const valid: string[] = ["powerline", "powerline-thin", "slash", "pipe", "dot", "ascii"];
|
|
148
|
+
return valid.includes(value);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isValidIconStyle(value: unknown): boolean {
|
|
152
|
+
if (typeof value !== "string") return false;
|
|
153
|
+
const valid: string[] = ["nerd", "emoji", "text"];
|
|
154
|
+
return valid.includes(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mergeGroupSettings(
|
|
158
|
+
defaults: Record<string, FooterGroupSettings>,
|
|
159
|
+
overrides: Record<string, FooterGroupSettings> | undefined,
|
|
160
|
+
): Record<string, FooterGroupSettings> {
|
|
161
|
+
const result: Record<string, FooterGroupSettings> = { ...defaults };
|
|
162
|
+
|
|
163
|
+
if (!overrides) return result;
|
|
164
|
+
|
|
165
|
+
for (const [groupId, groupOverride] of Object.entries(overrides)) {
|
|
166
|
+
const defaultGroup = result[groupId] ?? { show: true, segments: {} };
|
|
167
|
+
result[groupId] = {
|
|
168
|
+
show: typeof groupOverride.show === "boolean" ? groupOverride.show : defaultGroup.show,
|
|
169
|
+
segments: {
|
|
170
|
+
...defaultGroup.segments,
|
|
171
|
+
...(groupOverride.segments ?? {}),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result;
|
|
177
|
+
}
|