pi-open-tui 0.3.4 → 0.3.5

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.
@@ -107,15 +107,15 @@ function roundedBorder(
107
107
  }
108
108
  if (canFitOverflow()) {
109
109
  const leftBlockWidth = 3 + statusWidth + 1;
110
- return `${corners[0]}${paint("── ")}${status}${paint(` ${"─".repeat(overflowStart - leftBlockWidth)}${overflowLabel}${"─".repeat(contentWidth - overflowStart - overflowLabelWidth)}`)}${corners[1]}`;
110
+ return `${paint(`${corners[0]}── `)}${status}${paint(` ${"─".repeat(overflowStart - leftBlockWidth)}${overflowLabel}${"─".repeat(contentWidth - overflowStart - overflowLabelWidth)}${corners[1]}`)}`;
111
111
  }
112
112
  if (contentWidth >= statusWidth + 5) {
113
- return `${corners[0]}${paint("── ")}${status}${paint(` ${"─".repeat(contentWidth - statusWidth - 4)}`)}${corners[1]}`;
113
+ return `${paint(`${corners[0]}── `)}${status}${paint(` ${"─".repeat(contentWidth - statusWidth - 4)}${corners[1]}`)}`;
114
114
  }
115
115
  status = indicator.renderSpinnerInBorder(contentWidth);
116
116
  statusWidth = visibleWidth(status);
117
117
  const prefixWidth = Math.min(3, Math.max(0, contentWidth - statusWidth));
118
- return `${corners[0]}${paint("─".repeat(prefixWidth))}${status}${paint("─".repeat(Math.max(0, contentWidth - prefixWidth - statusWidth)))}${corners[1]}`;
118
+ return `${paint(`${corners[0]}${"─".repeat(prefixWidth)}`)}${status}${paint(`${"─".repeat(Math.max(0, contentWidth - prefixWidth - statusWidth))}${corners[1]}`)}`;
119
119
  }
120
120
  }
121
121
 
@@ -1,157 +1,151 @@
1
- import { execFile } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { promisify } from "node:util";
5
-
6
- const execFileAsync = promisify(execFile);
7
- const GIT_TIMEOUT_MS = 2000;
8
-
9
- export interface GitCommitInfo {
10
- oid: string | null;
11
- detached: boolean;
12
- tag: string | null;
13
- }
14
-
15
- export interface GitStatus {
16
- branch: string | undefined;
17
- ahead: number;
18
- behind: number;
19
- modified: number;
20
- untracked: number;
21
- staged: number;
22
- stashed: number;
23
- conflicted: number;
24
- renamed: number;
25
- deleted: number;
26
- commit: GitCommitInfo | null;
27
- }
28
-
29
- export function emptyGitStatus(): GitStatus {
30
- return {
31
- branch: undefined,
32
- ahead: 0,
33
- behind: 0,
34
- modified: 0,
35
- untracked: 0,
36
- staged: 0,
37
- stashed: 0,
38
- conflicted: 0,
39
- renamed: 0,
40
- deleted: 0,
41
- commit: null,
42
- };
43
- }
44
-
45
- async function gitExec(args: string[], cwd: string): Promise<string | null> {
46
- try {
47
- const { stdout } = await execFileAsync("git", args, {
48
- cwd,
49
- timeout: GIT_TIMEOUT_MS,
50
- maxBuffer: 1024 * 1024,
51
- });
52
- return stdout;
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- export async function readGitStatus(
59
- cwd: string,
60
- options: { readCommit?: boolean; readTag?: boolean; readCounts?: boolean } = {},
61
- ): Promise<GitStatus> {
62
- if (!existsSync(join(cwd, ".git"))) {
63
- return emptyGitStatus();
64
- }
65
-
66
- const stdout = await gitExec(
67
- ["status", "--porcelain=v1", "--branch", "--show-stash"],
68
- cwd,
69
- );
70
- if (stdout === null) {
71
- return emptyGitStatus();
72
- }
73
-
74
- const status = emptyGitStatus();
75
- const lines = stdout.split("\n");
76
- let stashSupported = true;
77
-
78
- for (const line of lines) {
79
- if (line.startsWith("## ")) {
80
- const branchPart = line.slice(3);
81
- const detached = branchPart.startsWith("HEAD (no branch)");
82
- if (detached) {
83
- status.branch = undefined;
84
- status.commit = { oid: null, detached: true, tag: null };
85
- } else {
86
- const branchMatch = branchPart.match(/^(\S+?)(?:\.\.\.(\S+))?(?:\s+\[(ahead|behind) (\d+)\])?$/);
87
- if (branchMatch) {
88
- status.branch = branchMatch[1];
89
- if (branchMatch[3] === "ahead") status.ahead = parseInt(branchMatch[4]!, 10);
90
- if (branchMatch[3] === "behind") status.behind = parseInt(branchMatch[4]!, 10);
91
- }
92
- }
93
- continue;
94
- }
95
-
96
- if (line.startsWith("# stash ")) {
97
- const stashCount = parseInt(line.slice(8).trim(), 10);
98
- if (!Number.isNaN(stashCount)) {
99
- status.stashed = stashCount;
100
- stashSupported = true;
101
- }
102
- continue;
103
- }
104
-
105
- // ponytail: skip file-entry counting when only branch/commit display is on;
106
- // the first `git status` call itself can't be split, but the stash fallback below can.
107
- if (options.readCounts === false) continue;
108
- if (line.length < 3) continue;
109
- const x = line[0]!;
110
- const y = line[1]!;
111
-
112
- if (x === "U" || y === "U" || (x === "C" && y === "C")) status.conflicted++;
113
- else if (x === "?" && y === "?") status.untracked++;
114
- else if (x === "R") status.renamed++;
115
- else if (x === "D" || y === "D") status.deleted++;
116
- else {
117
- if (x !== " " && x !== "?") status.staged++;
118
- if (y === "M" || y === "D") status.modified++;
119
- }
120
- }
121
-
122
- if (options.readCounts !== false && stashSupported && status.stashed === 0 && !stdout.includes("# stash")) {
123
- const stashOut = await gitExec(["stash", "list", "--count"], cwd);
124
- if (stashOut !== null) {
125
- const count = parseInt(stashOut.trim(), 10);
126
- if (!Number.isNaN(count)) status.stashed = count;
127
- }
128
- }
129
-
130
- if (options.readCommit && status.commit?.detached) {
131
- const oid = await gitExec(["rev-parse", "HEAD"], cwd);
132
- if (oid) {
133
- status.commit.oid = oid.trim();
134
- }
135
- if (options.readTag) {
136
- const tag = await gitExec(["describe", "--tags", "--exact-match", "HEAD"], cwd);
137
- if (tag) {
138
- status.commit.tag = tag.trim();
139
- }
140
- }
141
- }
142
-
143
- return status;
144
- }
145
-
146
- export function hasGitChanges(s: GitStatus): boolean {
147
- return (
148
- s.modified > 0 ||
149
- s.untracked > 0 ||
150
- s.staged > 0 ||
151
- s.conflicted > 0 ||
152
- s.renamed > 0 ||
153
- s.deleted > 0 ||
154
- s.ahead > 0 ||
155
- s.behind > 0
156
- );
157
- }
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+ const GIT_TIMEOUT_MS = 2000;
6
+
7
+ export interface GitCommitInfo {
8
+ oid: string | null;
9
+ detached: boolean;
10
+ tag: string | null;
11
+ }
12
+
13
+ export interface GitStatus {
14
+ branch: string | undefined;
15
+ ahead: number;
16
+ behind: number;
17
+ modified: number;
18
+ untracked: number;
19
+ staged: number;
20
+ stashed: number;
21
+ conflicted: number;
22
+ renamed: number;
23
+ deleted: number;
24
+ commit: GitCommitInfo | null;
25
+ }
26
+
27
+ export function emptyGitStatus(): GitStatus {
28
+ return {
29
+ branch: undefined,
30
+ ahead: 0,
31
+ behind: 0,
32
+ modified: 0,
33
+ untracked: 0,
34
+ staged: 0,
35
+ stashed: 0,
36
+ conflicted: 0,
37
+ renamed: 0,
38
+ deleted: 0,
39
+ commit: null,
40
+ };
41
+ }
42
+
43
+ async function gitExec(args: string[], cwd: string): Promise<string | null> {
44
+ try {
45
+ const { stdout } = await execFileAsync("git", args, {
46
+ cwd,
47
+ timeout: GIT_TIMEOUT_MS,
48
+ maxBuffer: 1024 * 1024,
49
+ });
50
+ return stdout;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ export async function readGitStatus(
57
+ cwd: string,
58
+ options: { readCommit?: boolean; readTag?: boolean; readCounts?: boolean } = {},
59
+ ): Promise<GitStatus> {
60
+ const stdout = await gitExec(
61
+ ["status", "--porcelain=v1", "--branch", "--show-stash"],
62
+ cwd,
63
+ );
64
+ if (stdout === null) {
65
+ return emptyGitStatus();
66
+ }
67
+
68
+ const status = emptyGitStatus();
69
+ const lines = stdout.split("\n");
70
+ let stashSupported = true;
71
+
72
+ for (const line of lines) {
73
+ if (line.startsWith("## ")) {
74
+ const branchPart = line.slice(3);
75
+ const detached = branchPart.startsWith("HEAD (no branch)");
76
+ if (detached) {
77
+ status.branch = undefined;
78
+ status.commit = { oid: null, detached: true, tag: null };
79
+ } else {
80
+ const branchMatch = branchPart.match(/^(\S+?)(?:\.\.\.(\S+))?(?:\s+\[(ahead|behind) (\d+)\])?$/);
81
+ if (branchMatch) {
82
+ status.branch = branchMatch[1];
83
+ if (branchMatch[3] === "ahead") status.ahead = parseInt(branchMatch[4]!, 10);
84
+ if (branchMatch[3] === "behind") status.behind = parseInt(branchMatch[4]!, 10);
85
+ }
86
+ }
87
+ continue;
88
+ }
89
+
90
+ if (line.startsWith("# stash ")) {
91
+ const stashCount = parseInt(line.slice(8).trim(), 10);
92
+ if (!Number.isNaN(stashCount)) {
93
+ status.stashed = stashCount;
94
+ stashSupported = true;
95
+ }
96
+ continue;
97
+ }
98
+
99
+ // ponytail: skip file-entry counting when only branch/commit display is on;
100
+ // the first `git status` call itself can't be split, but the stash fallback below can.
101
+ if (options.readCounts === false) continue;
102
+ if (line.length < 3) continue;
103
+ const x = line[0]!;
104
+ const y = line[1]!;
105
+
106
+ if (x === "U" || y === "U" || (x === "C" && y === "C")) status.conflicted++;
107
+ else if (x === "?" && y === "?") status.untracked++;
108
+ else if (x === "R") status.renamed++;
109
+ else if (x === "D" || y === "D") status.deleted++;
110
+ else {
111
+ if (x !== " " && x !== "?") status.staged++;
112
+ if (y === "M" || y === "D") status.modified++;
113
+ }
114
+ }
115
+
116
+ if (options.readCounts !== false && stashSupported && status.stashed === 0 && !stdout.includes("# stash")) {
117
+ const stashOut = await gitExec(["stash", "list", "--count"], cwd);
118
+ if (stashOut !== null) {
119
+ const count = parseInt(stashOut.trim(), 10);
120
+ if (!Number.isNaN(count)) status.stashed = count;
121
+ }
122
+ }
123
+
124
+ if (options.readCommit && status.commit?.detached) {
125
+ const oid = await gitExec(["rev-parse", "HEAD"], cwd);
126
+ if (oid) {
127
+ status.commit.oid = oid.trim();
128
+ }
129
+ if (options.readTag) {
130
+ const tag = await gitExec(["describe", "--tags", "--exact-match", "HEAD"], cwd);
131
+ if (tag) {
132
+ status.commit.tag = tag.trim();
133
+ }
134
+ }
135
+ }
136
+
137
+ return status;
138
+ }
139
+
140
+ export function hasGitChanges(s: GitStatus): boolean {
141
+ return (
142
+ s.modified > 0 ||
143
+ s.untracked > 0 ||
144
+ s.staged > 0 ||
145
+ s.conflicted > 0 ||
146
+ s.renamed > 0 ||
147
+ s.deleted > 0 ||
148
+ s.ahead > 0 ||
149
+ s.behind > 0
150
+ );
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-tui",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
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": {