pi-open-tui 0.2.12 → 0.2.14

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,41 +1,65 @@
1
1
  # pi-open-tui
2
2
 
3
- A polished TUI for [Pi](https://pi.dev) coding agent. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui into one cohesive package.
3
+ **English** | [简体中文](./README.zh-CN.md)
4
4
 
5
- ![Preview](https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png)
5
+ A polished terminal interface for the [Pi](https://pi.dev) coding agent. It brings the strongest ideas from pi-haiku, pi-claude-code-tui, and pi-zentui into one configurable extension.
6
6
 
7
- ## What's in it
7
+ ![pi-open-tui preview](https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png)
8
8
 
9
- - **Animated Pi logo header** — 16-frame color-changing logo animation + "Let's build something great" tagline
10
- - **Starship-style footer** — 2 lines showing cwd, git branch/status, runtime version, context bar, model, token counts, and cost
11
- - **Rounded editor** accent rail + borderMuted rounded corners, clean visual frame
12
- - **60+ runtime detection** Node, Rust, Go, Python, Ruby, Java, Swift, Kotlin, C/C++, Deno, Bun, and many more
13
- - **Git status** branch, ahead/behind, modified/untracked/staged/stashed, detached HEAD commit hash + tag
14
- - **Working timer** live elapsed time while the agent is working, done duration when finished
15
- - **Turn telemetry** generation speed, TTFT, stalls, tokens, and list-price rate after each complete agent run
16
- - **Zero prototype patches** uses public Pi APIs (setHeader/setFooter/setEditorComponent), safe across Pi updates
17
- - **Interactive settings UI** `/open-tui` opens a tabbed settings dialog (General / Icons / Footer / Telemetry)
9
+ ## Highlights
10
+
11
+ - **Pi header** with model, thinking level, working directory, and useful slash-command hints
12
+ - **Responsive footer** with Git state, detected runtime, context usage, token counts, cost, and extension status
13
+ - **Framed editor** with block, bar, and underline cursor styles
14
+ - **Project awareness** for 50+ runtimes and detailed Git states, including ahead/behind, staged, modified, untracked, stashed, and detached HEAD
15
+ - **Turn telemetry** for TPS, time to first token (TTFT), duration, stalls, tokens, and list-price rate
16
+ - **Interactive settings** through `/open-tui`, available in English and Simplified Chinese
17
+ - **Version-guarded Pi compatibility shim**: fullscreen wheel speed falls back to Pi's default if its runtime support changes
18
+
19
+ ## Requirements
20
+
21
+ - Pi 0.80 or later
22
+ - A terminal with UTF-8 and color support
23
+ - A [Nerd Font](https://www.nerdfonts.com/font-downloads) for the full icon set (optional; ASCII icons are built in)
18
24
 
19
25
  ## Install
20
26
 
27
+ Install the extension:
28
+
21
29
  ```bash
22
30
  pi install npm:pi-open-tui
23
31
  ```
24
32
 
25
- Or try it for one run:
33
+ Or try it for one session:
26
34
 
27
35
  ```bash
28
36
  pi -e npm:pi-open-tui
29
37
  ```
30
38
 
39
+ ## Font and icons
40
+
41
+ Download any patched font from the official [Nerd Fonts downloads page](https://www.nerdfonts.com/font-downloads) or [latest GitHub release](https://github.com/ryanoasis/nerd-fonts/releases/latest). Install it, select that font in your terminal profile, and restart the terminal.
42
+
43
+ The default `auto` mode detects the terminal environment, not the installed font file. If icons appear as boxes or incorrect symbols, open `/open-tui` and choose one of these modes under **Appearance**:
44
+
45
+ - `nerd`: force Nerd Font icons after configuring a Nerd Font in the terminal
46
+ - `ascii`: use plain-text icons with no patched font required
47
+ - `auto`: use Nerd Font icons in recognized terminals and ASCII elsewhere
48
+
49
+ If the font is installed but `auto` still selects ASCII, choose `nerd` explicitly. In VS Code, Windows Terminal, and similar apps, configure the font in the terminal profile rather than only installing it in the operating system.
50
+
31
51
  ## Configuration
32
52
 
33
- Run `/open-tui` to open the interactive settings UI. Configuration is stored at `~/.pi/agent/open-tui.json`:
53
+ Run `/open-tui` to open the settings dialog. It provides **General**, **Appearance**, **Footer**, and **Telemetry** tabs. Settings are stored in `~/.pi/agent/open-tui.json`:
34
54
 
35
55
  ```json
36
56
  {
37
57
  "enabled": true,
38
58
  "settingsLanguage": "en",
59
+ "cursorStyle": "block",
60
+ "fullscreen": {
61
+ "wheelScrollLines": 4
62
+ },
39
63
  "icons": {
40
64
  "mode": "auto"
41
65
  },
@@ -63,43 +87,55 @@ Run `/open-tui` to open the interactive settings UI. Configuration is stored at
63
87
  }
64
88
  ```
65
89
 
66
- - `settingsLanguage`: language for the `/open-tui` settings UI only; `en` or `zh`
67
- - `icons.mode`: `auto` (detect Nerd Font), `nerd` (force Nerd Font glyphs), or `ascii` (plain fallbacks)
68
- - `footerSegments.sessionName`: shows the current session name next to cwd (off by default); hidden when the session has no name
69
- - `footerSegments.gitCommit`: shows short hash + tag on detached HEAD (off by default)
70
- - `footerSegments.extensionStatuses`: shows statuses published by extensions through Pi's `setStatus()` API (on by default); turn it off to hide the whole status line, including MCP
90
+ Key options:
91
+
92
+ | Option | Values | Notes |
93
+ | --- | --- | --- |
94
+ | `settingsLanguage` | `en`, `zh` | Changes the `/open-tui` interface language |
95
+ | `cursorStyle` | `block`, `bar`, `underline` | `bar` and `underline` require terminal cursor-shape support |
96
+ | `fullscreen.wheelScrollLines` | `1`-`10` | Lines scrolled per mouse-wheel notch in fullscreen mode; defaults to `4`. `/open-tui` cycles through `1`, `4`, `7`, and `10` for quick switching |
97
+ | `icons.mode` | `auto`, `nerd`, `ascii` | Controls footer and telemetry icons |
98
+ | `footerSegments` | Boolean flags | Shows or hides individual footer data |
99
+ | `telemetry` | Boolean flags | Enables telemetry and its individual measurements |
100
+
101
+ `sessionName` appears only when the session has a name. `gitCommit` shows the short hash and tag in detached HEAD state. Disabling `extensionStatuses` hides the entire extension status line, including MCP status.
102
+
103
+ Fullscreen wheel speed uses an isolated compatibility shim for Pi 0.84.2's runtime field because Pi does not yet expose a public setter. On Pi versions without a compatible field, the setting is ignored and Pi's default scrolling remains active.
71
104
 
72
105
  ## Turn telemetry
73
106
 
74
- After each complete agent run, open-tui shows one transient notification. Tool-call turns are aggregated into that single result:
107
+ After each complete agent run, pi-open-tui shows one transient result. Tool-call turns are combined into that result:
75
108
 
76
109
  ```text
77
110
  > TPS 42.5 tok/s | ~ TTFT 1.2s | + 29.7s | ↑ 567 | ↓ 1.2k | ! stall 1x / 4.3s | $ $3.60/M
78
111
  ```
79
112
 
80
- The notification uses the footer's icon mode and semantic theme colors. Configure its master switch and individual TPS, TTFT, duration, token, stall, and cost segments from the **Telemetry** tab in `/open-tui`.
113
+ TPS is calculated from all provider-reported assistant output tokens divided by the total generation time across the run. Timing starts at `turn_start` and ends at the assistant `message_end`, so it includes TTFT, hidden reasoning, buffering, and stalls; tool execution between turns is excluded. Runs without output tokens or measurable generation time show `TPS —`.
81
114
 
82
- TPS is the complete generation throughput for the agent run: all provider-reported assistant output tokens divided by the summed generation time of every LLM turn, measured from `turn_start` through the assistant `message_end`. This includes time-to-first-token, hidden reasoning, buffering, and stalls so the token count and timing cover the same interval. Tool execution between turns is excluded. A run with no output tokens or no measurable generation time is shown as `TPS —`. The `stall` segment shows occurrence count followed by accumulated duration. The optional `$ / M` value uses the model's list-price `usage.cost.total`; it is not the session's cumulative cost shown in the footer.
115
+ The `$ / M` value is the model's list-price rate from `usage.cost.total`, not the cumulative session cost shown in the footer. Every telemetry field can be toggled from the **Telemetry** tab.
83
116
 
84
117
  ## Local development
85
118
 
86
119
  ```bash
120
+ npm install
121
+ npm test
122
+ npm run typecheck
87
123
  pi -e .
88
124
  ```
89
125
 
90
- ## License
126
+ ## Acknowledgements
91
127
 
92
- MIT
128
+ This project builds on several Pi community packages:
93
129
 
94
- ## Acknowledgements
130
+ - **[pi-haiku](https://github.com/nnocte/pi-haiku)** — two-line footer structure and working timer
131
+ - **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — Pi logo frames and rounded editor border technique
132
+ - **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — Starship-style footer segments, runtime detection, session lifecycle, and settings UI pattern
133
+ - **[pi-tps](https://github.com/monotykamary/pi-tps)** — turn timing, stall detection, and conservative TPS measurement
95
134
 
96
- This project builds on the work of several Pi community packages:
135
+ The logo frames are derived from Pi's official install script (`pi.dev/install.sh`). Runtime detection and Git porcelain parsing borrow structure from `pi-zentui`.
97
136
 
98
- - **[pi-haiku](https://github.com/nnocte/pi-haiku)** the 2-line footer structure (location+model · timer+context) and working-timer pattern
99
- - **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — the 16-frame animated Pi logo and rounded editor border technique
100
- - **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — the Starship-style footer segments (git status icons, runtime detection, context gauge), generation-based session lifecycle, and interactive settings UI pattern
101
- - **[pi-tps](https://github.com/monotykamary/pi-tps)** — the turn timing, stall detection, and conservative TPS measurement approach
137
+ Special thanks to the **[LINUX DO](https://linux.do)** community for its support.
102
138
 
103
- The animated logo frames are derived from `pi-claude-code-tui`, which in turn derive from Pi's official install script (`pi.dev/install.sh`). The runtime detection list and git porcelain parsing borrow structure from `pi-zentui`.
139
+ ## License
104
140
 
105
- Special thanks to the **[LINUX DO](https://linux.do)** community for their support.
141
+ [MIT](./LICENSE)
@@ -0,0 +1,141 @@
1
+ # pi-open-tui
2
+
3
+ [English](./README.md) | **简体中文**
4
+
5
+ 一个为 [Pi](https://pi.dev) 编程代理打造的终端界面扩展,将 pi-haiku、pi-claude-code-tui 与 pi-zentui 的优秀设计整合为统一、可配置的使用体验。
6
+
7
+ ![pi-open-tui 预览](https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png)
8
+
9
+ ## 功能亮点
10
+
11
+ - **Pi 顶栏**:显示模型、思考等级、当前目录和常用斜杠命令提示
12
+ - **自适应底栏**:集中展示 Git 状态、运行环境、上下文用量、Token、费用和扩展状态
13
+ - **带边框的编辑器**:支持块状、竖线和下划线三种光标样式
14
+ - **项目环境感知**:识别 50 多种运行环境,并展示 ahead/behind、已暂存、已修改、未跟踪、stash 和 detached HEAD 等 Git 状态
15
+ - **单轮遥测**:展示 TPS、首 Token 延迟(TTFT)、耗时、停顿、Token 数量和模型标价速率
16
+ - **交互式设置**:通过 `/open-tui` 配置,并支持英文和简体中文界面
17
+ - **带版本保护的 Pi 兼容层**:全屏滚轮速度所依赖的运行时支持发生变化时,会回退为 Pi 默认行为
18
+
19
+ ## 环境要求
20
+
21
+ - Pi 0.80 或更高版本
22
+ - 支持 UTF-8 和彩色输出的终端
23
+ - 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)(可选;内置 ASCII 图标)
24
+
25
+ ## 安装
26
+
27
+ 安装扩展:
28
+
29
+ ```bash
30
+ pi install npm:pi-open-tui
31
+ ```
32
+
33
+ 也可以只在当前会话中试用:
34
+
35
+ ```bash
36
+ pi -e npm:pi-open-tui
37
+ ```
38
+
39
+ ## 字体与图标
40
+
41
+ 可从 [Nerd Fonts 官方下载页](https://www.nerdfonts.com/font-downloads)或 [GitHub 最新版本](https://github.com/ryanoasis/nerd-fonts/releases/latest)下载任意已修补字体。安装后,请在终端配置中选择该字体,并重启终端。
42
+
43
+ 默认的 `auto` 模式检测的是终端环境,无法确认终端当前实际使用的字体。如果图标显示为方框、乱码或错误符号,请打开 `/open-tui`,在**外观**页选择合适的模式:
44
+
45
+ - `nerd`:终端已配置 Nerd Font 时,强制使用 Nerd Font 图标
46
+ - `ascii`:使用纯文本图标,无需安装修补字体
47
+ - `auto`:在已识别的终端中使用 Nerd Font 图标,其他环境回退到 ASCII
48
+
49
+ 如果已经安装字体,但 `auto` 仍选择 ASCII,请手动切换为 `nerd`。使用 VS Code、Windows Terminal 等应用时,只在操作系统中安装字体还不够,还需要在对应的终端配置中选中该字体。
50
+
51
+ ## 配置
52
+
53
+ 运行 `/open-tui` 打开设置窗口,其中包含**常规**、**外观**、**底栏**和**遥测**四个页面。设置保存在 `~/.pi/agent/open-tui.json`:
54
+
55
+ ```json
56
+ {
57
+ "enabled": true,
58
+ "settingsLanguage": "zh",
59
+ "cursorStyle": "block",
60
+ "fullscreen": {
61
+ "wheelScrollLines": 4
62
+ },
63
+ "icons": {
64
+ "mode": "auto"
65
+ },
66
+ "footerSegments": {
67
+ "cwd": true,
68
+ "sessionName": false,
69
+ "gitBranch": true,
70
+ "gitStatus": true,
71
+ "gitCommit": false,
72
+ "runtime": true,
73
+ "context": true,
74
+ "tokens": true,
75
+ "cost": true,
76
+ "extensionStatuses": true
77
+ },
78
+ "telemetry": {
79
+ "enabled": true,
80
+ "tps": true,
81
+ "ttft": true,
82
+ "duration": true,
83
+ "tokens": true,
84
+ "stalls": true,
85
+ "cost": true
86
+ }
87
+ }
88
+ ```
89
+
90
+ 主要选项:
91
+
92
+ | 选项 | 可选值 | 说明 |
93
+ | --- | --- | --- |
94
+ | `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
95
+ | `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
96
+ | `fullscreen.wheelScrollLines` | `1`-`10` | 全屏模式下滚轮每格滚动的行数,默认值为 `4`;`/open-tui` 使用 `1`、`4`、`7`、`10` 四档快速切换 |
97
+ | `icons.mode` | `auto`、`nerd`、`ascii` | 控制底栏和遥测通知使用的图标 |
98
+ | `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
99
+ | `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
100
+
101
+ `sessionName` 仅在会话有名称时显示;`gitCommit` 会在 detached HEAD 状态下显示短哈希和标签;关闭 `extensionStatuses` 会隐藏整行扩展状态,其中也包括 MCP 状态。
102
+
103
+ 全屏滚轮速度通过隔离的兼容层写入 Pi 0.84.2 的运行时字段,因为 Pi 尚未提供公开 setter。若后续 Pi 版本不再包含兼容字段,该设置会被忽略并继续使用 Pi 的默认滚动行为。
104
+
105
+ ## 单轮遥测
106
+
107
+ 每次 Agent 完整运行结束后,pi-open-tui 会显示一条临时结果,并将其中的多个工具调用轮次合并统计:
108
+
109
+ ```text
110
+ > TPS 42.5 tok/s | ~ TTFT 1.2s | + 29.7s | ↑ 567 | ↓ 1.2k | ! stall 1x / 4.3s | $ $3.60/M
111
+ ```
112
+
113
+ TPS 的计算方式是:将本次运行中服务商报告的全部 Assistant 输出 Token,除以各个生成轮次的总耗时。计时范围从 `turn_start` 到 Assistant 的 `message_end`,包含 TTFT、隐藏推理、缓冲和停顿,但不包含轮次之间的工具执行时间。没有输出 Token 或无法测得生成时间时,会显示 `TPS —`。
114
+
115
+ `$ / M` 表示根据 `usage.cost.total` 得到的模型标价速率,不是底栏中的会话累计费用。所有遥测字段都可以在**遥测**页单独开关。
116
+
117
+ ## 本地开发
118
+
119
+ ```bash
120
+ npm install
121
+ npm test
122
+ npm run typecheck
123
+ pi -e .
124
+ ```
125
+
126
+ ## 致谢
127
+
128
+ 本项目基于多个 Pi 社区包的工作:
129
+
130
+ - **[pi-haiku](https://github.com/nnocte/pi-haiku)** — 双行底栏结构和工作计时器
131
+ - **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — Pi Logo 帧与圆角编辑器边框技术
132
+ - **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — Starship 风格底栏、运行环境检测、会话生命周期和设置界面模式
133
+ - **[pi-tps](https://github.com/monotykamary/pi-tps)** — 单轮计时、停顿检测和保守的 TPS 计算方式
134
+
135
+ Logo 帧源自 Pi 官方安装脚本(`pi.dev/install.sh`)。运行环境检测和 Git porcelain 解析借鉴了 `pi-zentui` 的结构。
136
+
137
+ 特别感谢 **[LINUX DO](https://linux.do)** 社区的支持。
138
+
139
+ ## 许可证
140
+
141
+ [MIT](./LICENSE)
@@ -1,9 +1,14 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import {
5
+ DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
6
+ normalizeFullscreenWheelScrollLines,
7
+ } from "./fullscreen-scroll.ts";
4
8
  import type { IconMode } from "./icons.ts";
5
9
 
6
10
  export type SettingsLanguage = "en" | "zh";
11
+ export type CursorStyle = "block" | "bar" | "underline";
7
12
 
8
13
  export type { IconMode } from "./icons.ts";
9
14
 
@@ -30,9 +35,15 @@ export interface TelemetryConfig {
30
35
  cost: boolean;
31
36
  }
32
37
 
38
+ export interface FullscreenConfig {
39
+ wheelScrollLines: number;
40
+ }
41
+
33
42
  export interface OpenTuiConfig {
34
43
  enabled: boolean;
35
44
  settingsLanguage: SettingsLanguage;
45
+ cursorStyle: CursorStyle;
46
+ fullscreen: FullscreenConfig;
36
47
  icons: {
37
48
  mode: IconMode;
38
49
  };
@@ -43,6 +54,10 @@ export interface OpenTuiConfig {
43
54
  export const DEFAULT_CONFIG: OpenTuiConfig = {
44
55
  enabled: true,
45
56
  settingsLanguage: "en",
57
+ cursorStyle: "block",
58
+ fullscreen: {
59
+ wheelScrollLines: DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
60
+ },
46
61
  icons: {
47
62
  mode: "auto",
48
63
  },
@@ -122,6 +137,13 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
122
137
  if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
123
138
  config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
124
139
  }
140
+ if (config.cursorStyle !== "block" && config.cursorStyle !== "bar" && config.cursorStyle !== "underline") {
141
+ config.cursorStyle = DEFAULT_CONFIG.cursorStyle;
142
+ }
143
+ config.fullscreen.wheelScrollLines = normalizeFullscreenWheelScrollLines(
144
+ config.fullscreen.wheelScrollLines,
145
+ DEFAULT_CONFIG.fullscreen.wheelScrollLines,
146
+ );
125
147
  return config;
126
148
  } catch (err) {
127
149
  notify?.(`open-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
@@ -5,7 +5,12 @@ import {
5
5
  type KeybindingsManager,
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
8
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
+ import { CURSOR_MARKER, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import type { CursorStyle } from "./config.ts";
10
+ import {
11
+ applyFullscreenWheelScrollLines,
12
+ DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
13
+ } from "./fullscreen-scroll.ts";
9
14
  import { findBottomBorderIndex, isEditorBorderLine, stripAnsi } from "./utils.ts";
10
15
 
11
16
  function fillLine(content: string, width: number): string {
@@ -14,6 +19,27 @@ function fillLine(content: string, width: number): string {
14
19
  return `${truncated}${pad}`;
15
20
  }
16
21
 
22
+ const CURSOR_STYLE_SEQUENCES: Partial<Record<CursorStyle, string>> = {
23
+ bar: "\x1b[6 q",
24
+ underline: "\x1b[4 q",
25
+ };
26
+ const DEFAULT_CURSOR_STYLE_SEQUENCE = "\x1b[0 q";
27
+
28
+ function removeSoftwareCursor(line: string, cursorMarker = ""): string {
29
+ return line.replace(/\x1b\[7m([\s\S]*?)\x1b\[0m/g, (_match, cursor: string) => {
30
+ const replacement = `${cursorMarker}${cursor}`;
31
+ cursorMarker = "";
32
+ return replacement;
33
+ });
34
+ }
35
+
36
+ function configureCursor(tui: TUI, cursorStyle: CursorStyle): void {
37
+ if (cursorStyle === "block") return;
38
+ tui.setShowHardwareCursor(true);
39
+ const sequence = CURSOR_STYLE_SEQUENCES[cursorStyle];
40
+ if (sequence) tui.terminal.write(sequence);
41
+ }
42
+
17
43
  function roundedBorder(
18
44
  width: number,
19
45
  kind: "top" | "bottom",
@@ -39,13 +65,18 @@ function roundedBorder(
39
65
  export class OpenTuiEditor extends CustomEditor {
40
66
  private readonly getRail: () => string;
41
67
  private readonly getBorder: (s: string) => string;
68
+ private cursorStyle: CursorStyle;
69
+ private previewHardwareCursor = false;
42
70
 
43
71
  constructor(
44
72
  tui: TUI,
45
73
  editorTheme: EditorTheme,
46
74
  keybindings: KeybindingsManager,
75
+ cursorStyle: CursorStyle = "block",
47
76
  ) {
48
77
  super(tui, editorTheme, keybindings, { paddingX: 0 });
78
+ this.cursorStyle = cursorStyle;
79
+ configureCursor(tui, cursorStyle);
49
80
  // ponytail: route the frame through this.borderColor so Pi can recolor it
50
81
  // via updateEditorBorderColor() — bash mode ("! " prefix → green) and
51
82
  // thinking-level borders both flow through this one property.
@@ -58,14 +89,44 @@ export class OpenTuiEditor extends CustomEditor {
58
89
  super.setPaddingX(0);
59
90
  }
60
91
 
92
+ setCursorStyle(cursorStyle: CursorStyle, blockHardwareCursor = false): void {
93
+ const styleChanged = cursorStyle !== this.cursorStyle;
94
+ this.previewHardwareCursor = cursorStyle !== "block";
95
+ this.cursorStyle = cursorStyle;
96
+ if (styleChanged) {
97
+ if (cursorStyle === "block") {
98
+ this.tui.terminal.write(DEFAULT_CURSOR_STYLE_SEQUENCE);
99
+ this.tui.setShowHardwareCursor(blockHardwareCursor);
100
+ } else {
101
+ configureCursor(this.tui, cursorStyle);
102
+ }
103
+ }
104
+ this.tui.requestRender();
105
+ }
106
+
107
+ private renderBase(width: number): string[] {
108
+ const renderedLines = super.render(width);
109
+ if (this.cursorStyle === "block") return renderedLines;
110
+
111
+ // A focused overlay suppresses the editor's cursor marker. Preserve its
112
+ // position only for the live settings preview, then clear it on refocus.
113
+ let cursorMarker = this.previewHardwareCursor && !this.focused ? CURSOR_MARKER : "";
114
+ if (this.focused) this.previewHardwareCursor = false;
115
+ return renderedLines.map((line) => {
116
+ const rendered = removeSoftwareCursor(line, cursorMarker);
117
+ if (rendered !== line) cursorMarker = "";
118
+ return rendered;
119
+ });
120
+ }
121
+
61
122
  render(width: number): string[] {
62
- if (width < 4) return super.render(width);
123
+ if (width < 4) return this.renderBase(width);
63
124
 
64
125
  const rail = this.getRail();
65
126
  const borderPaint = this.getBorder;
66
127
  // ponytail: 1-char rail + 1-char gap on each side = 4 chars of chrome.
67
128
  const innerWidth = Math.max(0, width - 4);
68
- const baseLines = super.render(innerWidth);
129
+ const baseLines = this.renderBase(innerWidth);
69
130
  const bottomIdx = findBottomBorderIndex(baseLines);
70
131
 
71
132
  const result: string[] = [];
@@ -90,11 +151,40 @@ export class OpenTuiEditor extends CustomEditor {
90
151
  }
91
152
  }
92
153
 
93
- export function installEditor(_pi: ExtensionAPI, ctx: ExtensionContext): () => void {
94
- ctx.ui.setEditorComponent((tui, editorTheme, keybindings) =>
95
- new OpenTuiEditor(tui, editorTheme, keybindings),
96
- );
97
- return () => {
98
- ctx.ui.setEditorComponent(undefined);
154
+ export function installEditor(
155
+ _pi: ExtensionAPI,
156
+ ctx: ExtensionContext,
157
+ cursorStyle: CursorStyle = "block",
158
+ wheelScrollLines = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
159
+ ) {
160
+ let activeTui: TUI | undefined;
161
+ let activeEditor: OpenTuiEditor | undefined;
162
+ let previousHardwareCursor: boolean | undefined;
163
+ let currentCursorStyle = cursorStyle;
164
+ let currentWheelScrollLines = wheelScrollLines;
165
+
166
+ ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
167
+ activeTui = tui;
168
+ applyFullscreenWheelScrollLines(tui, currentWheelScrollLines);
169
+ previousHardwareCursor = tui.getShowHardwareCursor();
170
+ activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle);
171
+ return activeEditor;
172
+ });
173
+ return {
174
+ setCursorStyle(nextCursorStyle: CursorStyle): void {
175
+ currentCursorStyle = nextCursorStyle;
176
+ activeEditor?.setCursorStyle(nextCursorStyle, previousHardwareCursor);
177
+ },
178
+ setWheelScrollLines(nextWheelScrollLines: number): void {
179
+ currentWheelScrollLines = nextWheelScrollLines;
180
+ if (activeTui) applyFullscreenWheelScrollLines(activeTui, currentWheelScrollLines);
181
+ },
182
+ cleanup(): void {
183
+ ctx.ui.setEditorComponent(undefined);
184
+ if (activeTui) {
185
+ if (currentCursorStyle !== "block") activeTui.terminal.write(DEFAULT_CURSOR_STYLE_SEQUENCE);
186
+ if (previousHardwareCursor !== undefined) activeTui.setShowHardwareCursor(previousHardwareCursor);
187
+ }
188
+ },
99
189
  };
100
190
  }
@@ -0,0 +1,34 @@
1
+ import type { TUI } from "@earendil-works/pi-tui";
2
+
3
+ export const MIN_FULLSCREEN_WHEEL_SCROLL_LINES = 1;
4
+ export const MAX_FULLSCREEN_WHEEL_SCROLL_LINES = 10;
5
+ export const DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES = 4;
6
+
7
+ export function normalizeFullscreenWheelScrollLines(
8
+ value: unknown,
9
+ fallback = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
10
+ ): number {
11
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
12
+ return Math.min(
13
+ MAX_FULLSCREEN_WHEEL_SCROLL_LINES,
14
+ Math.max(MIN_FULLSCREEN_WHEEL_SCROLL_LINES, Math.floor(value)),
15
+ );
16
+ }
17
+
18
+ type FullscreenWheelTui = TUI & { wheelScrollLines?: unknown };
19
+
20
+ /**
21
+ * Pi 0.84.2 keeps this constructor option in a private runtime field. Remove
22
+ * this shim once Pi exposes a public setter; incompatible versions are no-ops.
23
+ */
24
+ export function applyFullscreenWheelScrollLines(tui: TUI, value: number): boolean {
25
+ try {
26
+ const fullscreenTui = tui as FullscreenWheelTui;
27
+ if (fullscreenTui.mode !== "fullscreen" || typeof fullscreenTui.wheelScrollLines !== "number") {
28
+ return false;
29
+ }
30
+ return Reflect.set(fullscreenTui, "wheelScrollLines", normalizeFullscreenWheelScrollLines(value));
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
@@ -26,6 +26,13 @@ function isInteractiveLaunch(): boolean {
26
26
  return true;
27
27
  }
28
28
 
29
+ type PendingUiChange = "install" | "uninstall";
30
+
31
+ export function getPendingUiChange(enabled: boolean, active: boolean): PendingUiChange | undefined {
32
+ if (enabled === active) return undefined;
33
+ return enabled ? "install" : "uninstall";
34
+ }
35
+
29
36
  function clearVisibleScreen(): void {
30
37
  if (process.stdout.isTTY) {
31
38
  process.stdout.write("\x1b[2J\x1b[H");
@@ -53,8 +60,8 @@ export default function (pi: ExtensionAPI) {
53
60
  let workingTimer: ReturnType<typeof setInterval> | undefined;
54
61
  let cleanupHeader: (() => void) | undefined;
55
62
  let cleanupFooter: (() => void) | undefined;
56
- let cleanupEditor: (() => void) | undefined;
57
- let pendingUiChange: "install" | "uninstall" | undefined;
63
+ let editor: ReturnType<typeof installEditor> | undefined;
64
+ let pendingUiChange: PendingUiChange | undefined;
58
65
 
59
66
  const getThinkingLevel = () => (sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : "off");
60
67
 
@@ -80,7 +87,7 @@ export default function (pi: ExtensionAPI) {
80
87
  },
81
88
  },
82
89
  );
83
- cleanupEditor = installEditor(pi, ctx);
90
+ editor = installEditor(pi, ctx, config.cursorStyle, config.fullscreen.wheelScrollLines);
84
91
  active = true;
85
92
  }
86
93
  };
@@ -90,10 +97,10 @@ export default function (pi: ExtensionAPI) {
90
97
  if (active) {
91
98
  cleanupHeader?.();
92
99
  cleanupFooter?.();
93
- cleanupEditor?.();
100
+ editor?.cleanup();
94
101
  cleanupHeader = undefined;
95
102
  cleanupFooter = undefined;
96
- cleanupEditor = undefined;
103
+ editor = undefined;
97
104
  requestFooterRender = undefined;
98
105
  active = false;
99
106
  }
@@ -265,14 +272,18 @@ export default function (pi: ExtensionAPI) {
265
272
  registerSettingsCommand(pi, {
266
273
  getConfig: () => config,
267
274
  onConfigChanged: (newConfig) => {
268
- const wasEnabled = config.enabled;
275
+ const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
276
+ const wheelScrollLinesChanged = config.fullscreen.wheelScrollLines !== newConfig.fullscreen.wheelScrollLines;
269
277
  saveConfig(newConfig);
270
278
  config = newConfig;
271
- if (lastCtx && wasEnabled !== newConfig.enabled) {
272
- // Both directions defer to onOverlayClosed: while the settings overlay
273
- // is open, pi core's setEditorComponent() steals focus from the overlay
274
- // and strands it without keyboard input.
275
- pendingUiChange = newConfig.enabled ? "install" : "uninstall";
279
+ if (cursorStyleChanged && active && editor) {
280
+ editor.setCursorStyle(newConfig.cursorStyle);
281
+ }
282
+ if (wheelScrollLinesChanged && active && editor) {
283
+ editor.setWheelScrollLines(newConfig.fullscreen.wheelScrollLines);
284
+ }
285
+ if (lastCtx) {
286
+ pendingUiChange = getPendingUiChange(newConfig.enabled, active);
276
287
  }
277
288
  const gitNeeded = newConfig.footerSegments.gitBranch || newConfig.footerSegments.gitStatus || newConfig.footerSegments.gitCommit;
278
289
  if (lastCtx && gitNeeded) {
@@ -286,7 +297,7 @@ export default function (pi: ExtensionAPI) {
286
297
  if (!lastCtx || pendingUiChange === undefined) return;
287
298
  const change = pendingUiChange;
288
299
  pendingUiChange = undefined;
289
- if (change === "uninstall") {
300
+ if (!config.enabled || change === "uninstall") {
290
301
  uninstallUi(lastCtx);
291
302
  } else {
292
303
  applyUi(lastCtx);
@@ -8,7 +8,9 @@ import {
8
8
  type TUI,
9
9
  Text,
10
10
  } from "@earendil-works/pi-tui";
11
- import type { IconMode, OpenTuiConfig, SettingsLanguage } from "./config.ts";
11
+ import type { CursorStyle, IconMode, OpenTuiConfig, SettingsLanguage } from "./config.ts";
12
+
13
+ const WHEEL_SCROLL_PRESETS = [1, 4, 7, 10] as const;
12
14
 
13
15
  interface SettingItem {
14
16
  id: string;
@@ -23,11 +25,13 @@ const TABS: Tab[] = ["features", "icons", "segments", "telemetry"];
23
25
  const COPY = {
24
26
  en: {
25
27
  title: "Open TUI Settings",
26
- tabs: { features: "General", icons: "Icons", segments: "Footer", telemetry: "Telemetry" },
28
+ tabs: { features: "General", icons: "Appearance", segments: "Footer", telemetry: "Telemetry" },
27
29
  hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
28
30
  labels: {
29
31
  enabled: "Enabled",
30
32
  language: "Language",
33
+ wheelScrollLines: "Mouse wheel speed",
34
+ cursorStyle: "Cursor style",
31
35
  iconMode: "Icon mode",
32
36
  cwd: "CWD",
33
37
  sessionName: "Session name",
@@ -48,16 +52,20 @@ const COPY = {
48
52
  on: "On",
49
53
  off: "Off",
50
54
  languages: { en: "English", zh: "简体中文" },
55
+ wheelLines: (count: number) => `${count} ${count === 1 ? "line" : "lines"} / notch`,
56
+ cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
51
57
  icons: { auto: "Auto", nerd: "Nerd", ascii: "ASCII" },
52
58
  },
53
59
  },
54
60
  zh: {
55
61
  title: "Open TUI 设置",
56
- tabs: { features: "常规", icons: "图标", segments: "Footer", telemetry: "遥测" },
62
+ tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
57
63
  hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
58
64
  labels: {
59
65
  enabled: "启用",
60
66
  language: "语言",
67
+ wheelScrollLines: "鼠标滚轮速度",
68
+ cursorStyle: "光标样式",
61
69
  iconMode: "图标模式",
62
70
  cwd: "当前目录",
63
71
  sessionName: "会话名",
@@ -78,6 +86,8 @@ const COPY = {
78
86
  on: "开启",
79
87
  off: "关闭",
80
88
  languages: { en: "English", zh: "简体中文" },
89
+ wheelLines: (count: number) => `每格 ${count} 行`,
90
+ cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
81
91
  icons: { auto: "自动", nerd: "Nerd", ascii: "ASCII" },
82
92
  },
83
93
  },
@@ -110,6 +120,19 @@ function toggleLanguage(config: OpenTuiConfig): OpenTuiConfig {
110
120
  return { ...config, settingsLanguage: config.settingsLanguage === "en" ? "zh" : "en" };
111
121
  }
112
122
 
123
+ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
124
+ const order: CursorStyle[] = ["block", "bar", "underline"];
125
+ const currentIdx = order.indexOf(config.cursorStyle);
126
+ const next = order[(currentIdx + 1) % order.length]!;
127
+ return { ...config, cursorStyle: next };
128
+ }
129
+
130
+ function cycleWheelScrollLines(config: OpenTuiConfig): OpenTuiConfig {
131
+ const current = config.fullscreen.wheelScrollLines;
132
+ const wheelScrollLines = WHEEL_SCROLL_PRESETS.find((value) => value > current) ?? WHEEL_SCROLL_PRESETS[0];
133
+ return { ...config, fullscreen: { ...config.fullscreen, wheelScrollLines } };
134
+ }
135
+
113
136
  function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
114
137
  return {
115
138
  ...config,
@@ -121,11 +144,19 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
121
144
  return [
122
145
  { id: "enabled", label: copy.labels.enabled, currentValue: config.enabled ? copy.values.on : copy.values.off },
123
146
  { id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
147
+ {
148
+ id: "wheelScrollLines",
149
+ label: copy.labels.wheelScrollLines,
150
+ currentValue: copy.values.wheelLines(config.fullscreen.wheelScrollLines),
151
+ },
124
152
  ];
125
153
  }
126
154
 
127
155
  function buildIconsItems(config: OpenTuiConfig, copy: SettingsCopy): SettingItem[] {
128
- return [{ id: "mode", label: copy.labels.iconMode, currentValue: copy.values.icons[config.icons.mode] }];
156
+ return [
157
+ { id: "mode", label: copy.labels.iconMode, currentValue: copy.values.icons[config.icons.mode] },
158
+ { id: "cursorStyle", label: copy.labels.cursorStyle, currentValue: copy.values.cursorStyles[config.cursorStyle] },
159
+ ];
129
160
  }
130
161
 
131
162
  function buildSegmentsItems(config: OpenTuiConfig, copy: SettingsCopy): SettingItem[] {
@@ -177,8 +208,12 @@ function handleSettingChange(
177
208
  if (tab === "features") {
178
209
  if (itemId === "enabled") return toggleEnabled(config);
179
210
  if (itemId === "settingsLanguage") return toggleLanguage(config);
211
+ if (itemId === "wheelScrollLines") return cycleWheelScrollLines(config);
212
+ }
213
+ if (tab === "icons") {
214
+ if (itemId === "mode") return cycleIconMode(config);
215
+ if (itemId === "cursorStyle") return cycleCursorStyle(config);
180
216
  }
181
- if (tab === "icons" && itemId === "mode") return cycleIconMode(config);
182
217
  if (tab === "segments") {
183
218
  return toggleSetting(config, itemId as keyof OpenTuiConfig["footerSegments"]);
184
219
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-open-tui",
3
- "version": "0.2.12",
4
- "description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui.",
3
+ "version": "0.2.14",
4
+ "description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -21,10 +21,11 @@
21
21
  "extensions/",
22
22
  "assets/",
23
23
  "README.md",
24
+ "README.zh-CN.md",
24
25
  "LICENSE"
25
26
  ],
26
27
  "scripts": {
27
- "test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts",
28
+ "test": "node --test tests/settings-command.test.ts tests/fullscreen-scroll.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts",
28
29
  "typecheck": "tsc --noEmit"
29
30
  },
30
31
  "pi": {