pi-open-tui 0.3.4 → 0.3.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/extensions/open-tui/editor.ts +3 -3
- package/extensions/open-tui/git.ts +151 -157
- package/package.json +1 -1
|
@@ -107,15 +107,15 @@ function roundedBorder(
|
|
|
107
107
|
}
|
|
108
108
|
if (canFitOverflow()) {
|
|
109
109
|
const leftBlockWidth = 3 + statusWidth + 1;
|
|
110
|
-
return `${corners[0]}
|
|
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]}
|
|
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]}${
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
s.
|
|
149
|
-
s.
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.6",
|
|
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": {
|