pi-zentui 0.1.4 → 0.1.6
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 +65 -19
- package/extensions/zentui/config.ts +61 -1
- package/extensions/zentui/index.ts +45 -54
- package/extensions/zentui/runtime.ts +776 -109
- package/extensions/zentui/ui.ts +16 -109
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Zentui
|
|
2
2
|
|
|
3
|
-
A Starship-inspired statusline and Opencode-style TUI for [Pi](https://
|
|
3
|
+
A Starship-inspired statusline and Opencode-style TUI for [Pi](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
## Screenshots
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ A Starship-inspired statusline and Opencode-style TUI for [Pi](https://github.co
|
|
|
11
11
|
Zentui brings two popular aesthetics to Pi:
|
|
12
12
|
|
|
13
13
|
- **[Starship](https://starship.rs/) footer** — shows your current directory, git branch, git status indicators, and runtime/version detection in a compact, icon-rich format
|
|
14
|
-
- **[Opencode](https://github.com/opencode-ai/opencode) editor
|
|
14
|
+
- **[Opencode](https://github.com/opencode-ai/opencode) editor** — clean bordered input box with accent rail and model/provider display inside the editor frame
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
@@ -20,7 +20,7 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
20
20
|
- ` dirname` — current directory with icon
|
|
21
21
|
- `on branch` — git branch with icon
|
|
22
22
|
- `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
|
|
23
|
-
- `via v5.5.0` — runtime detection with version
|
|
23
|
+
- `via v5.5.0` — runtime detection with version and Starship terminal styles for Nerd Font runtime/language modules
|
|
24
24
|
- Right side shows context usage, token counts, and cost
|
|
25
25
|
|
|
26
26
|
### Editor (Opencode-inspired)
|
|
@@ -28,7 +28,6 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
28
28
|
- Bordered input box with accent-colored left rail
|
|
29
29
|
- Model name and provider displayed inside the editor frame
|
|
30
30
|
- Thinking level indicator when enabled
|
|
31
|
-
- Matching style for previous user messages
|
|
32
31
|
|
|
33
32
|
### Git Status Icons
|
|
34
33
|
|
|
@@ -47,20 +46,67 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
47
46
|
|
|
48
47
|
### Runtime Detection
|
|
49
48
|
|
|
50
|
-
Detects
|
|
51
|
-
|
|
52
|
-
| Runtime | Detection
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
49
|
+
Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and styles each runtime with Starship's terminal style strings (for example, Node.js uses `bold green`, so your terminal colorscheme supplies the actual green):
|
|
50
|
+
|
|
51
|
+
| Runtime/language | Detection examples |
|
|
52
|
+
| ---------------- | ------------------------------------------------------------ |
|
|
53
|
+
| Buf | `buf.yaml`, `buf.gen.yaml`, `buf.work.yaml` |
|
|
54
|
+
| Bun | `bun.lock`, `bun.lockb` |
|
|
55
|
+
| C | `.c`, `.h` files |
|
|
56
|
+
| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp` files |
|
|
57
|
+
| CMake | `CMakeLists.txt`, `CMakeCache.txt` |
|
|
58
|
+
| COBOL | `.cbl`, `.cob` files |
|
|
59
|
+
| Conda | `CONDA_DEFAULT_ENV` environment |
|
|
60
|
+
| Crystal | `.cr` files, `shard.yml` |
|
|
61
|
+
| Dart | `.dart` files, `pubspec.yaml`, `.dart_tool/` |
|
|
62
|
+
| Deno | `deno.json`, `deno.jsonc`, `deno.lock` |
|
|
63
|
+
| .NET | `.csproj`, `.fsproj`, `global.json`, `Directory.Build.*` |
|
|
64
|
+
| Elixir | `mix.exs` |
|
|
65
|
+
| Elm | `.elm` files, `elm.json`, `elm-stuff/` |
|
|
66
|
+
| Erlang | `rebar.config`, `erlang.mk` |
|
|
67
|
+
| Fennel | `.fnl` files |
|
|
68
|
+
| Fortran | `.f`, `.f90`, `.f95`, `.f03`, `.f08`, `.f18`, `fpm.toml` |
|
|
69
|
+
| Gleam | `.gleam` files, `gleam.toml` |
|
|
70
|
+
| Go | `go.mod` |
|
|
71
|
+
| Gradle | `build.gradle`, `build.gradle.kts`, `gradle/` |
|
|
72
|
+
| Guix shell | `GUIX_ENVIRONMENT` environment |
|
|
73
|
+
| Haskell | `.hs`, `.cabal`, `stack.yaml`, `cabal.project` |
|
|
74
|
+
| Haxe | `.hx`, `.hxml`, `haxelib.json`, `.haxerc` |
|
|
75
|
+
| Helm | `helmfile.yaml`, `Chart.yaml` |
|
|
76
|
+
| Java | `.java-version` |
|
|
77
|
+
| Julia | `.jl` files, `Project.toml`, `Manifest.toml` |
|
|
78
|
+
| Kotlin | `.kt`, `.kts` files |
|
|
79
|
+
| Lua | `.lua` files, `stylua.toml`, `.luarc.json`, `lua/` dir |
|
|
80
|
+
| Maven | `pom.xml` |
|
|
81
|
+
| Meson | `MESON_DEVENV=1` and `MESON_PROJECT_NAME` environment |
|
|
82
|
+
| Mojo | `.mojo` files |
|
|
83
|
+
| Nim | `.nim`, `.nims`, `.nimble`, `nim.cfg` |
|
|
84
|
+
| Nix shell | `IN_NIX_SHELL=pure` or `IN_NIX_SHELL=impure` environment |
|
|
85
|
+
| Node.js | `package.json`, `.nvmrc`, `.node-version` |
|
|
86
|
+
| OCaml | `.opam`, `.ml`, `.mli`, `dune`, `_opam/`, `esy.lock/` |
|
|
87
|
+
| Odin | `.odin` files |
|
|
88
|
+
| OPA/Rego | `.rego` files |
|
|
89
|
+
| Perl | `.pl`, `.pm`, `Makefile.PL`, `cpanfile`, `META.*` |
|
|
90
|
+
| PHP | `composer.json` |
|
|
91
|
+
| Pixi | `pixi.toml`, `pixi.lock`, `PIXI_ENVIRONMENT_NAME` environment |
|
|
92
|
+
| Pulumi | `Pulumi.yaml`, `Pulumi.yml` |
|
|
93
|
+
| PureScript | `.purs` files, `spago.dhall`, `spago.yaml`, `spago.lock` |
|
|
94
|
+
| Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile` |
|
|
95
|
+
| R | `.R`, `.Rmd`, `.Rproj`, `DESCRIPTION`, `.Rproj.user/` |
|
|
96
|
+
| Raku | `.raku`, `.rakumod`, `.p6`, `.pm6`, `META6.json` |
|
|
97
|
+
| Red | `.red`, `.reds` files |
|
|
98
|
+
| Ruby | `Gemfile`, `.ruby-version` |
|
|
99
|
+
| Rust | `Cargo.toml` |
|
|
100
|
+
| Scala | `.scala`, `.sbt`, `build.sbt`, `.metals/` |
|
|
101
|
+
| Solidity | `.sol` files |
|
|
102
|
+
| Spack | `SPACK_ENV` environment |
|
|
103
|
+
| Swift | `.swift` files, `Package.swift` |
|
|
104
|
+
| Terraform | `.tf`, `.tfplan`, `.tfstate`, `.terraform/` |
|
|
105
|
+
| Typst | `.typ` files, `template.typ` |
|
|
106
|
+
| Vagrant | `Vagrantfile` |
|
|
107
|
+
| V | `.v` files, `v.mod`, `vpkg.json` |
|
|
108
|
+
| Xmake | `xmake.lua` |
|
|
109
|
+
| Zig | `.zig` files, `build.zig` |
|
|
64
110
|
|
|
65
111
|
## Install
|
|
66
112
|
|
|
@@ -127,7 +173,7 @@ This means Zentui works with any Pi theme — it uses your theme's colors by def
|
|
|
127
173
|
|
|
128
174
|
## Requirements
|
|
129
175
|
|
|
130
|
-
- [Pi](https://
|
|
176
|
+
- [Pi](https://pi.dev) coding agent 0.74 or newer
|
|
131
177
|
- A [Nerd Font](https://www.nerdfonts.com/) for icons
|
|
132
178
|
|
|
133
179
|
## Development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getAgentDir } from "@
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
export type ColorSpec = string;
|
|
6
6
|
|
|
@@ -129,6 +129,46 @@ function hexToAnsi(hex: string, isBackground = false): string {
|
|
|
129
129
|
return `\x1b[${isBackground ? 48 : 38};2;${r};${g};${b}m`;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
const terminalColorCodes = new Map([
|
|
133
|
+
["black", 30],
|
|
134
|
+
["red", 31],
|
|
135
|
+
["green", 32],
|
|
136
|
+
["yellow", 33],
|
|
137
|
+
["blue", 34],
|
|
138
|
+
["purple", 35],
|
|
139
|
+
["cyan", 36],
|
|
140
|
+
["white", 37],
|
|
141
|
+
["bright-black", 90],
|
|
142
|
+
["bright-red", 91],
|
|
143
|
+
["bright-green", 92],
|
|
144
|
+
["bright-yellow", 93],
|
|
145
|
+
["bright-blue", 94],
|
|
146
|
+
["bright-purple", 95],
|
|
147
|
+
["bright-cyan", 96],
|
|
148
|
+
["bright-white", 97],
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const terminalStyleModifiers = new Map([
|
|
152
|
+
["bold", 1],
|
|
153
|
+
["dim", 2],
|
|
154
|
+
["dimmed", 2],
|
|
155
|
+
["italic", 3],
|
|
156
|
+
["underline", 4],
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
function terminalColorToAnsi(color: string): string | undefined {
|
|
160
|
+
const normalized = color.toLowerCase();
|
|
161
|
+
const colorCode = terminalColorCodes.get(normalized);
|
|
162
|
+
if (colorCode !== undefined) return `${colorCode}`;
|
|
163
|
+
|
|
164
|
+
if (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized)) {
|
|
165
|
+
return `38;5;${normalized}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isHexColor(normalized)) return hexToAnsi(normalized).slice(2, -1);
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
132
172
|
type ThemeLike = {
|
|
133
173
|
fg(color: string, text: string): string;
|
|
134
174
|
};
|
|
@@ -161,6 +201,26 @@ export function colorize(theme: ThemeLike, color: ColorSpec, text: string): stri
|
|
|
161
201
|
return theme.fg("text", text);
|
|
162
202
|
}
|
|
163
203
|
|
|
204
|
+
export function renderTerminalStyle(style: string, text: string): string {
|
|
205
|
+
const codes: string[] = [];
|
|
206
|
+
for (const token of style.trim().split(/\s+/)) {
|
|
207
|
+
if (!token) continue;
|
|
208
|
+
|
|
209
|
+
const normalized = token.toLowerCase();
|
|
210
|
+
const modifier = terminalStyleModifiers.get(normalized);
|
|
211
|
+
if (modifier !== undefined) {
|
|
212
|
+
codes.push(`${modifier}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const foreground = normalized.startsWith("fg:") ? normalized.slice(3) : normalized;
|
|
217
|
+
const color = terminalColorToAnsi(foreground);
|
|
218
|
+
if (color) codes.push(color);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return codes.length ? `\x1b[${codes.join(";")}m${text}\x1b[0m` : text;
|
|
222
|
+
}
|
|
223
|
+
|
|
164
224
|
export function ensureConfigExists(): void {
|
|
165
225
|
try {
|
|
166
226
|
if (!existsSync(configPath)) {
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import type { AssistantMessage } from "@
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type {
|
|
3
3
|
ExtensionAPI,
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
KeybindingsManager,
|
|
6
6
|
Theme,
|
|
7
|
-
} from "@
|
|
8
|
-
import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@
|
|
9
|
-
import {
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
import {
|
|
10
|
+
type PolishedTuiConfig,
|
|
11
|
+
colorize,
|
|
12
|
+
ensureConfigExists,
|
|
13
|
+
loadConfig,
|
|
14
|
+
renderTerminalStyle,
|
|
15
|
+
} from "./config";
|
|
10
16
|
import { type GitStatusSummary, emptyGitStatus, readGitStatus } from "./git";
|
|
11
17
|
import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
|
|
12
18
|
import { type RuntimeInfo, readRuntimeInfo } from "./runtime";
|
|
13
|
-
import { PolishedEditor
|
|
19
|
+
import { PolishedEditor } from "./ui";
|
|
14
20
|
|
|
15
21
|
type FooterState = GitStatusSummary & {
|
|
16
22
|
modelLabel: string;
|
|
@@ -85,30 +91,6 @@ function buildContextLabel(ctx: ExtensionContext): string {
|
|
|
85
91
|
return `${percent}/${formatCount(contextWindow)}`;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
function getRuntimeColorToken(runtime: RuntimeInfo | undefined): string {
|
|
89
|
-
switch (runtime?.name) {
|
|
90
|
-
case "nodejs":
|
|
91
|
-
return "success";
|
|
92
|
-
case "deno":
|
|
93
|
-
return "syntaxType";
|
|
94
|
-
case "bun":
|
|
95
|
-
return "warning";
|
|
96
|
-
case "python":
|
|
97
|
-
case "java":
|
|
98
|
-
return "warning";
|
|
99
|
-
case "rust":
|
|
100
|
-
case "ruby":
|
|
101
|
-
return "error";
|
|
102
|
-
case "golang":
|
|
103
|
-
return "syntaxType";
|
|
104
|
-
case "lua":
|
|
105
|
-
case "php":
|
|
106
|
-
return "accent";
|
|
107
|
-
default:
|
|
108
|
-
return "text";
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
94
|
function formatRuntimeSegment(
|
|
113
95
|
theme: Pick<Theme, "fg">,
|
|
114
96
|
runtime: RuntimeInfo | undefined,
|
|
@@ -116,7 +98,7 @@ function formatRuntimeSegment(
|
|
|
116
98
|
): string {
|
|
117
99
|
if (!runtime) return "";
|
|
118
100
|
const label = runtime.version ? `${runtime.symbol} ${runtime.version}` : runtime.symbol;
|
|
119
|
-
return `${colorize(theme, mutedColor, "via")} ${
|
|
101
|
+
return `${colorize(theme, mutedColor, "via")} ${renderTerminalStyle(runtime.style, label)}`;
|
|
120
102
|
}
|
|
121
103
|
|
|
122
104
|
function formatCwdLabel(cwd: string, cwdIcon: string): string {
|
|
@@ -145,6 +127,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
145
127
|
|
|
146
128
|
const refresh = () => requestFooterRender?.();
|
|
147
129
|
|
|
130
|
+
const cleanupUi = (ctx?: ExtensionContext) => {
|
|
131
|
+
stopProjectRefreshInterval();
|
|
132
|
+
stopProjectRefreshInterval = () => {};
|
|
133
|
+
projectRefreshInFlight = false;
|
|
134
|
+
projectRefreshPending = false;
|
|
135
|
+
requestFooterRender = undefined;
|
|
136
|
+
if (ctx?.hasUI) {
|
|
137
|
+
ctx.ui.setFooter(undefined);
|
|
138
|
+
ctx.ui.setEditorComponent(undefined);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
|
|
143
|
+
if (!ctx.hasUI) return;
|
|
144
|
+
syncState(ctx);
|
|
145
|
+
if (project) scheduleProjectRefresh(ctx);
|
|
146
|
+
refresh();
|
|
147
|
+
};
|
|
148
|
+
|
|
148
149
|
const syncState = (ctx: ExtensionContext) => {
|
|
149
150
|
const totals = getUsageTotals(ctx);
|
|
150
151
|
state.modelLabel = ctx.model?.id ?? "no-model";
|
|
@@ -198,6 +199,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
198
199
|
},
|
|
199
200
|
invalidate() {},
|
|
200
201
|
render(width: number): string[] {
|
|
202
|
+
if (width <= 0) return [""];
|
|
201
203
|
const innerWidth = Math.max(1, width - 2);
|
|
202
204
|
const cwdLabel = colorize(
|
|
203
205
|
theme,
|
|
@@ -254,11 +256,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
254
256
|
const rightWidth = visibleWidth(right);
|
|
255
257
|
const content =
|
|
256
258
|
leftWidth >= innerWidth
|
|
257
|
-
? truncateToWidth(left, innerWidth)
|
|
259
|
+
? truncateToWidth(left, innerWidth, "")
|
|
258
260
|
: leftWidth + 1 + rightWidth <= innerWidth
|
|
259
261
|
? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
|
|
260
|
-
: left;
|
|
261
|
-
|
|
262
|
+
: truncateToWidth(left, innerWidth, "");
|
|
263
|
+
const framed = width > 2 ? ` ${truncateToWidth(content, width - 2, "")} ` : content;
|
|
264
|
+
return [truncateToWidth(framed, width, "")];
|
|
262
265
|
},
|
|
263
266
|
};
|
|
264
267
|
});
|
|
@@ -285,9 +288,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
285
288
|
};
|
|
286
289
|
|
|
287
290
|
const installUi = (ctx: ExtensionContext) => {
|
|
291
|
+
if (!ctx.hasUI) return;
|
|
288
292
|
ensureConfigExists();
|
|
289
293
|
currentConfig = loadConfig();
|
|
290
|
-
patchUserMessageComponent(ctx.ui.theme);
|
|
291
294
|
installFooter(ctx);
|
|
292
295
|
installEditor(ctx);
|
|
293
296
|
stopProjectRefreshInterval();
|
|
@@ -303,43 +306,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
303
306
|
installUi(ctx);
|
|
304
307
|
});
|
|
305
308
|
|
|
306
|
-
pi.on("session_shutdown", async () => {
|
|
307
|
-
|
|
308
|
-
stopProjectRefreshInterval = () => {};
|
|
309
|
-
restoreUserMessageComponent();
|
|
309
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
310
|
+
cleanupUi(ctx);
|
|
310
311
|
});
|
|
311
312
|
|
|
312
313
|
pi.on("agent_start", async (_event, ctx) => {
|
|
313
|
-
|
|
314
|
-
refresh();
|
|
314
|
+
refreshInteractiveState(ctx);
|
|
315
315
|
});
|
|
316
316
|
|
|
317
317
|
pi.on("agent_end", async (_event, ctx) => {
|
|
318
|
-
|
|
319
|
-
scheduleProjectRefresh(ctx);
|
|
320
|
-
refresh();
|
|
318
|
+
refreshInteractiveState(ctx, true);
|
|
321
319
|
});
|
|
322
320
|
|
|
323
321
|
pi.on("model_select", async (_event, ctx) => {
|
|
324
|
-
|
|
325
|
-
refresh();
|
|
322
|
+
refreshInteractiveState(ctx);
|
|
326
323
|
});
|
|
327
324
|
|
|
328
325
|
pi.on("message_end", async (_event, ctx) => {
|
|
329
|
-
|
|
330
|
-
scheduleProjectRefresh(ctx);
|
|
331
|
-
refresh();
|
|
326
|
+
refreshInteractiveState(ctx, true);
|
|
332
327
|
});
|
|
333
328
|
|
|
334
329
|
pi.on("tool_execution_end", async (_event, ctx) => {
|
|
335
|
-
|
|
336
|
-
scheduleProjectRefresh(ctx);
|
|
337
|
-
refresh();
|
|
330
|
+
refreshInteractiveState(ctx, true);
|
|
338
331
|
});
|
|
339
332
|
|
|
340
333
|
pi.on("session_compact", async (_event, ctx) => {
|
|
341
|
-
|
|
342
|
-
scheduleProjectRefresh(ctx);
|
|
343
|
-
refresh();
|
|
334
|
+
refreshInteractiveState(ctx, true);
|
|
344
335
|
});
|
|
345
336
|
}
|