@quandev104/pi-style 0.2.0 → 0.2.2
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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/dist/extensions/pi-style.js +1256 -491
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/index.ts +3 -2
- package/extension-src/pi-style/app/runtime.ts +99 -86
- package/extension-src/pi-style/app/snapshot.ts +41 -2
- package/extension-src/pi-style/domain/status-renderer.ts +40 -8
- package/extension-src/pi-style/domain/status.ts +15 -5
- package/extension-src/pi-style/domain/theme.ts +32 -1
- package/extension-src/pi-style/features/editor/index.ts +97 -66
- package/extension-src/pi-style/features/messages/index.ts +469 -90
- package/extension-src/pi-style/features/status-line/index.ts +41 -11
- package/extension-src/pi-style/features/tools/bash-execution.ts +12 -1
- package/extension-src/pi-style/features/tools/boxed/bash.ts +195 -66
- package/extension-src/pi-style/features/tools/boxed/batch.ts +60 -10
- package/extension-src/pi-style/features/tools/boxed/edit.ts +45 -25
- package/extension-src/pi-style/features/tools/boxed/find.ts +9 -4
- package/extension-src/pi-style/features/tools/boxed/git.ts +46 -2
- package/extension-src/pi-style/features/tools/boxed/grep.ts +9 -2
- package/extension-src/pi-style/features/tools/boxed/ls.ts +9 -4
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +45 -22
- package/extension-src/pi-style/features/tools/boxed/read.ts +4 -2
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -14
- package/extension-src/pi-style/features/tools/boxed/shared.ts +51 -0
- package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +12 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +1 -1
- package/extension-src/pi-style/pi/index.ts +28 -13
- package/extension-src/pi-style/pi/session-usage.ts +204 -21
- package/extension-src/pi-style/shared/ansi.ts +17 -5
- package/extension-src/pi-style/shared/box.ts +83 -6
- package/extension-src/pi-style/shared/split-diff.ts +8 -5
- package/package.json +1 -1
|
@@ -39,13 +39,30 @@ interface EditorOptions {
|
|
|
39
39
|
onSnapshot: (snapshot: StatusSnapshot) => void;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
interface RenderPlan {
|
|
43
|
+
readonly style: "compact" | "boxed" | "dock" | "native";
|
|
44
|
+
readonly kind: "compact" | "boxed" | "outline" | "rounded" | "native";
|
|
45
|
+
readonly prompt: string;
|
|
46
|
+
readonly promptWidth: number;
|
|
47
|
+
readonly padding: number;
|
|
48
|
+
readonly sideReserve: number;
|
|
49
|
+
readonly renderWidth: number;
|
|
50
|
+
readonly innerWidth: number;
|
|
51
|
+
readonly prefix: string;
|
|
52
|
+
readonly continuation: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
const widthOf = visibleWidth;
|
|
43
56
|
|
|
44
57
|
function widthSafe(value: string, width: number): string {
|
|
45
58
|
if (width <= 0) return "";
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
const measured = widthOf(value);
|
|
60
|
+
if (measured > width) {
|
|
61
|
+
const fitted = truncateAnsi(value, width, "");
|
|
62
|
+
const current = widthOf(fitted);
|
|
63
|
+
return current < width ? fitted + " ".repeat(width - current) : fitted;
|
|
64
|
+
}
|
|
65
|
+
return measured < width ? value + " ".repeat(width - measured) : value;
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
function isNativeBorderLine(line: string): boolean {
|
|
@@ -129,7 +146,10 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
|
|
|
129
146
|
private readonly fullTheme: EditorOptions["fullTheme"];
|
|
130
147
|
private readonly onSnapshot: (snapshot: StatusSnapshot) => void;
|
|
131
148
|
private semantic: ResolvedTheme;
|
|
149
|
+
/** Set when config changes; invalidate() then rebuilds the (otherwise stable) semantic theme. */
|
|
150
|
+
private semanticDirty = false;
|
|
132
151
|
private disposed = false;
|
|
152
|
+
private renderPlanCache: { key: string; plan: RenderPlan } | undefined;
|
|
133
153
|
|
|
134
154
|
constructor(tui: Tui, theme: PiEditorTheme, keybindings: Keybindings, options: EditorOptions) {
|
|
135
155
|
super(tui, theme, keybindings);
|
|
@@ -151,7 +171,7 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
|
|
|
151
171
|
configure(config: NormalizedPiStyleConfig): void {
|
|
152
172
|
if (this.disposed) return;
|
|
153
173
|
this.config = config;
|
|
154
|
-
this.
|
|
174
|
+
this.semanticDirty = true;
|
|
155
175
|
this.invalidate();
|
|
156
176
|
}
|
|
157
177
|
|
|
@@ -163,89 +183,44 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
|
|
|
163
183
|
|
|
164
184
|
override invalidate(): void {
|
|
165
185
|
super.invalidate();
|
|
166
|
-
|
|
186
|
+
// The semantic theme is derived from (piTheme, config); piTheme is fixed per
|
|
187
|
+
// instance and config changes go through configure(), so keystroke-driven
|
|
188
|
+
// invalidations reuse the cached instance instead of rebuilding it.
|
|
189
|
+
if (this.semanticDirty) {
|
|
190
|
+
this.semantic = semanticTheme(this.piTheme, this.config);
|
|
191
|
+
this.semanticDirty = false;
|
|
192
|
+
}
|
|
193
|
+
this.renderPlanCache = undefined;
|
|
167
194
|
this.tui.requestRender();
|
|
168
195
|
}
|
|
169
196
|
|
|
170
197
|
override render(width: number): string[] {
|
|
171
198
|
if (width <= 0) return [];
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// widgets (status line) down. Re-frame the native output so the dropdown
|
|
177
|
-
// lives INSIDE the input box, keeping the footer directly below the input.
|
|
178
|
-
// Native layout: [top border, text lines, bottom border, dropdown lines...].
|
|
179
|
-
if ((this as unknown as { autocompleteState?: unknown }).autocompleteState) {
|
|
180
|
-
const prompt = this.prompt();
|
|
181
|
-
const padding = this.paddingFor(width, style);
|
|
182
|
-
const promptWidth = widthOf(prompt) + 1;
|
|
183
|
-
const prefix = `${" ".repeat(padding)}${prompt} `;
|
|
184
|
-
const continuation = " ".repeat(padding + promptWidth);
|
|
185
|
-
const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
|
|
186
|
-
const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
|
|
187
|
-
const body = nativeLines.slice(1, split);
|
|
188
|
-
const dropdown = nativeLines.slice(split);
|
|
189
|
-
const border = this.borderFor();
|
|
190
|
-
const kind = this.frameKind(style);
|
|
191
|
-
const renderWidth = width - (kind === "rounded" ? 2 : 0);
|
|
192
|
-
const sideColor = kind === "rounded" ? this.borderColorFor() : undefined;
|
|
193
|
-
const wrap = (line: string) =>
|
|
194
|
-
kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
|
|
195
|
-
const bashHidden = this.bashHiddenCount();
|
|
196
|
-
const renderedBody = body.map((line, index) => {
|
|
197
|
-
const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
|
|
198
|
-
return wrap(widthSafe(`${index === 0 ? prefix : continuation}${source}`, renderWidth));
|
|
199
|
-
});
|
|
200
|
-
const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, renderWidth)));
|
|
201
|
-
if (kind === "rounded") {
|
|
202
|
-
return [
|
|
203
|
-
border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
204
|
-
...renderedBody,
|
|
205
|
-
...dropdownLines,
|
|
206
|
-
border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
|
|
207
|
-
];
|
|
208
|
-
}
|
|
209
|
-
return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
|
|
210
|
-
}
|
|
211
|
-
if (style === "native") return nativeLines.map((line) => widthSafe(line, width));
|
|
199
|
+
const plan = this.renderPlan(width);
|
|
200
|
+
const autocompleteState = (this as unknown as { autocompleteState?: unknown }).autocompleteState;
|
|
201
|
+
if (plan.style === "native") return super.render(width).map((line) => widthSafe(line, width));
|
|
202
|
+
if (autocompleteState) return this.renderAutocompleteFrame(width, plan);
|
|
212
203
|
|
|
213
|
-
const
|
|
214
|
-
const promptWidth = widthOf(prompt) + 1;
|
|
215
|
-
const padding = this.paddingFor(width, style);
|
|
216
|
-
const kind = this.frameKind(style);
|
|
217
|
-
const sideReserve = kind === "rounded" ? 2 : 0;
|
|
218
|
-
const renderWidth = Math.max(1, width - sideReserve);
|
|
219
|
-
const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
|
|
220
|
-
const innerLines = super.render(innerWidth);
|
|
204
|
+
const innerLines = super.render(plan.innerWidth);
|
|
221
205
|
if (innerLines.length === 0) return [];
|
|
222
|
-
|
|
223
206
|
const body = innerLines.slice(1, -1);
|
|
224
|
-
const prefix = `${" ".repeat(padding)}${prompt} `;
|
|
225
|
-
const continuation = " ".repeat(padding + promptWidth);
|
|
226
207
|
const hint = this.config.editor.hint;
|
|
227
208
|
const showHint = hint !== "" && this.getText() === "";
|
|
228
209
|
const bashHidden = this.bashHiddenCount();
|
|
229
210
|
const renderedBody = body.map((line, index) => {
|
|
230
|
-
const lead = index === 0 ? prefix : continuation;
|
|
211
|
+
const lead = index === 0 ? plan.prefix : plan.continuation;
|
|
231
212
|
const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
|
|
232
213
|
let content = `${lead}${source}`;
|
|
233
|
-
// Empty-input hint: the cursor block (first cell of the native empty
|
|
234
|
-
// line) stays at the input position, the dim hint trails it. The native
|
|
235
|
-
// line is pre-padded to renderWidth with literal spaces; drop them from
|
|
236
|
-
// the raw end (safe: no ANSI follows the padding) before appending the
|
|
237
|
-
// hint, or the hint is truncated away by widthSafe. Typing any character
|
|
238
|
-
// makes the text non-empty and the hint disappears.
|
|
239
214
|
if (showHint && index === 0 && line) {
|
|
240
215
|
let end = content.length;
|
|
241
216
|
while (end > 0 && content[end - 1] === " ") end--;
|
|
242
217
|
if (end < content.length) content = content.slice(0, end);
|
|
243
218
|
content += this.semantic.apply("hint", hint);
|
|
244
219
|
}
|
|
245
|
-
return widthSafe(content, renderWidth);
|
|
220
|
+
return widthSafe(content, plan.renderWidth);
|
|
246
221
|
});
|
|
247
|
-
const metadata = this.metadata(width, style);
|
|
248
|
-
const framed = this.frame(width, style, renderedBody, metadata);
|
|
222
|
+
const metadata = this.metadata(width, plan.style);
|
|
223
|
+
const framed = this.frame(width, plan.style, renderedBody, metadata);
|
|
249
224
|
return framed.map((line) => widthSafe(line, width));
|
|
250
225
|
}
|
|
251
226
|
|
|
@@ -255,6 +230,62 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
|
|
|
255
230
|
this.invalidate();
|
|
256
231
|
}
|
|
257
232
|
|
|
233
|
+
private renderPlan(width: number): RenderPlan {
|
|
234
|
+
const style = this.styleFor(width);
|
|
235
|
+
const prompt = this.prompt();
|
|
236
|
+
const kind = this.frameKind(style);
|
|
237
|
+
const key = `${width}:${style}:${kind}:${prompt}`;
|
|
238
|
+
if (this.renderPlanCache?.key === key) return this.renderPlanCache.plan;
|
|
239
|
+
const promptWidth = widthOf(prompt) + 1;
|
|
240
|
+
const padding = this.paddingFor(width, style);
|
|
241
|
+
const sideReserve = kind === "rounded" ? 2 : 0;
|
|
242
|
+
const renderWidth = Math.max(1, width - sideReserve);
|
|
243
|
+
const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
|
|
244
|
+
const prefix = `${" ".repeat(padding)}${prompt} `;
|
|
245
|
+
const continuation = " ".repeat(padding + promptWidth);
|
|
246
|
+
const plan = {
|
|
247
|
+
style,
|
|
248
|
+
kind,
|
|
249
|
+
prompt,
|
|
250
|
+
promptWidth,
|
|
251
|
+
padding,
|
|
252
|
+
sideReserve,
|
|
253
|
+
renderWidth,
|
|
254
|
+
innerWidth,
|
|
255
|
+
prefix,
|
|
256
|
+
continuation,
|
|
257
|
+
};
|
|
258
|
+
this.renderPlanCache = { key, plan };
|
|
259
|
+
return plan;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private renderAutocompleteFrame(width: number, plan: RenderPlan): string[] {
|
|
263
|
+
const nativeLines = super.render(width);
|
|
264
|
+
const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
|
|
265
|
+
const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
|
|
266
|
+
const body = nativeLines.slice(1, split);
|
|
267
|
+
const dropdown = nativeLines.slice(split);
|
|
268
|
+
const border = this.borderFor();
|
|
269
|
+
const sideColor = plan.kind === "rounded" ? this.borderColorFor() : undefined;
|
|
270
|
+
const wrap = (line: string) =>
|
|
271
|
+
plan.kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
|
|
272
|
+
const bashHidden = this.bashHiddenCount();
|
|
273
|
+
const renderedBody = body.map((line, index) => {
|
|
274
|
+
const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
|
|
275
|
+
return wrap(widthSafe(`${index === 0 ? plan.prefix : plan.continuation}${source}`, plan.renderWidth));
|
|
276
|
+
});
|
|
277
|
+
const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, plan.renderWidth)));
|
|
278
|
+
if (plan.kind === "rounded") {
|
|
279
|
+
return [
|
|
280
|
+
border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
281
|
+
...renderedBody,
|
|
282
|
+
...dropdownLines,
|
|
283
|
+
border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
|
|
287
|
+
}
|
|
288
|
+
|
|
258
289
|
private prompt(): string {
|
|
259
290
|
if (this.isBashMode()) {
|
|
260
291
|
// Bash mode (`!` prefix): the prompt glyph becomes the bash icon and the
|