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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Zentui
2
2
 
3
- A Starship-inspired statusline and Opencode-style TUI for [Pi](https://github.com/badlogic/pi-mono).
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 and messages** — clean bordered input box with accent rail, transparent previous messages, and model/provider display inside the editor frame
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 (Bun, Deno, Node, Python, Go, Rust, Lua, Java, Ruby, PHP)
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 project type and shows runtime version:
51
-
52
- | Runtime | Detection |
53
- | ------- | ----------------------------------------------------------- |
54
- | Bun | `bun.lock`, `bun.lockb` |
55
- | Deno | `deno.json`, `deno.jsonc`, `deno.lock` |
56
- | Node.js | `package.json`, `.nvmrc`, `.node-version` |
57
- | Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile` |
58
- | Go | `go.mod` |
59
- | Rust | `Cargo.toml` |
60
- | Lua | `stylua.toml`, `.luarc.json`, `init.lua`, `lua/` dir |
61
- | Java | `pom.xml`, `build.gradle` |
62
- | Ruby | `Gemfile`, `.ruby-version` |
63
- | PHP | `composer.json` |
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://github.com/badlogic/pi-mono) coding agent
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 "@mariozechner/pi-coding-agent";
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 "@mariozechner/pi-ai";
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 "@mariozechner/pi-coding-agent";
8
- import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
- import { type PolishedTuiConfig, colorize, ensureConfigExists, loadConfig } from "./config";
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, patchUserMessageComponent, restoreUserMessageComponent } from "./ui";
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")} ${colorize(theme, getRuntimeColorToken(runtime), label)}`;
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
- return [` ${content} `];
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
- stopProjectRefreshInterval();
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
- syncState(ctx);
314
- refresh();
314
+ refreshInteractiveState(ctx);
315
315
  });
316
316
 
317
317
  pi.on("agent_end", async (_event, ctx) => {
318
- syncState(ctx);
319
- scheduleProjectRefresh(ctx);
320
- refresh();
318
+ refreshInteractiveState(ctx, true);
321
319
  });
322
320
 
323
321
  pi.on("model_select", async (_event, ctx) => {
324
- syncState(ctx);
325
- refresh();
322
+ refreshInteractiveState(ctx);
326
323
  });
327
324
 
328
325
  pi.on("message_end", async (_event, ctx) => {
329
- syncState(ctx);
330
- scheduleProjectRefresh(ctx);
331
- refresh();
326
+ refreshInteractiveState(ctx, true);
332
327
  });
333
328
 
334
329
  pi.on("tool_execution_end", async (_event, ctx) => {
335
- syncState(ctx);
336
- scheduleProjectRefresh(ctx);
337
- refresh();
330
+ refreshInteractiveState(ctx, true);
338
331
  });
339
332
 
340
333
  pi.on("session_compact", async (_event, ctx) => {
341
- syncState(ctx);
342
- scheduleProjectRefresh(ctx);
343
- refresh();
334
+ refreshInteractiveState(ctx, true);
344
335
  });
345
336
  }