@heyhuynhgiabuu/pi-pretty 0.4.2 → 0.4.3
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/package.json +1 -1
- package/src/index.ts +10 -82
- package/test/fff-integration.test.ts +29 -1
- package/test/image-rendering.test.ts +7 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
|
|
5
5
|
"author": "huynhgiabuu",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -134,13 +134,11 @@ function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/** Read themed tool backgrounds and update BG_BASE / BG_ERROR + RST.
|
|
137
|
-
*
|
|
138
|
-
let _bgBaseResolved = false;
|
|
137
|
+
* Recompute on each render so runtime theme changes are respected. */
|
|
139
138
|
function resolveBaseBackground(theme: BgTheme | null | undefined): void {
|
|
140
|
-
if (
|
|
141
|
-
_bgBaseResolved = true;
|
|
139
|
+
if (!theme?.getBgAnsi) return;
|
|
142
140
|
|
|
143
|
-
BG_BASE = getThemeBgAnsi(theme, "
|
|
141
|
+
BG_BASE = getThemeBgAnsi(theme, "toolBg") ?? getThemeBgAnsi(theme, "background") ?? BG_DEFAULT;
|
|
144
142
|
BG_ERROR = getThemeBgAnsi(theme, "toolErrorBg") ?? BG_BASE;
|
|
145
143
|
RST = `\x1b[0m${BG_BASE}`;
|
|
146
144
|
}
|
|
@@ -440,47 +438,6 @@ export const __imageInternals = {
|
|
|
440
438
|
},
|
|
441
439
|
};
|
|
442
440
|
|
|
443
|
-
/**
|
|
444
|
-
* Render base64 image inline using iTerm2 inline image protocol.
|
|
445
|
-
* Protocol: ESC ] 1337 ; File=[args] : base64data BEL
|
|
446
|
-
*/
|
|
447
|
-
function renderIterm2Image(base64Data: string, opts: { width?: string; name?: string } = {}): string {
|
|
448
|
-
const args: string[] = ["inline=1", "preserveAspectRatio=1"];
|
|
449
|
-
if (opts.width) args.push(`width=${opts.width}`);
|
|
450
|
-
if (opts.name) args.push(`name=${Buffer.from(opts.name).toString("base64")}`);
|
|
451
|
-
const byteSize = Math.ceil((base64Data.length * 3) / 4);
|
|
452
|
-
args.push(`size=${byteSize}`);
|
|
453
|
-
const seq = `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
|
|
454
|
-
return tmuxWrap(seq);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Render base64 image inline using Kitty graphics protocol.
|
|
459
|
-
* Protocol: ESC _G <key>=<value>,...; <base64data> ESC \
|
|
460
|
-
* Chunked in 4096-byte pieces as required by protocol.
|
|
461
|
-
* Supported by: Kitty, Ghostty
|
|
462
|
-
*/
|
|
463
|
-
function renderKittyImage(base64Data: string, opts: { cols?: number } = {}): string {
|
|
464
|
-
const chunks: string[] = [];
|
|
465
|
-
const CHUNK_SIZE = 4096;
|
|
466
|
-
|
|
467
|
-
for (let i = 0; i < base64Data.length; i += CHUNK_SIZE) {
|
|
468
|
-
const chunk = base64Data.slice(i, i + CHUNK_SIZE);
|
|
469
|
-
const isFirst = i === 0;
|
|
470
|
-
const isLast = i + CHUNK_SIZE >= base64Data.length;
|
|
471
|
-
const more = isLast ? 0 : 1;
|
|
472
|
-
|
|
473
|
-
if (isFirst) {
|
|
474
|
-
const colPart = opts.cols ? `,c=${opts.cols}` : "";
|
|
475
|
-
chunks.push(tmuxWrap(`\x1b_Ga=T,f=100,t=d,m=${more}${colPart};${chunk}\x1b\\`));
|
|
476
|
-
} else {
|
|
477
|
-
chunks.push(tmuxWrap(`\x1b_Gm=${more};${chunk}\x1b\\`));
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
return chunks.join("");
|
|
482
|
-
}
|
|
483
|
-
|
|
484
441
|
/**
|
|
485
442
|
* Get human-readable file size
|
|
486
443
|
*/
|
|
@@ -1156,8 +1113,9 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1156
1113
|
if (_fffPartialIndex) {
|
|
1157
1114
|
ctx.ui?.notify?.("FFF: scan timed out — using partial index. Run /fff-rescan when ready.", "warning");
|
|
1158
1115
|
} else {
|
|
1159
|
-
ctx.ui
|
|
1160
|
-
|
|
1116
|
+
const ui = ctx.ui;
|
|
1117
|
+
ui?.setStatus?.("fff", "FFF indexed");
|
|
1118
|
+
setTimeout(() => ui?.setStatus?.("fff", undefined), 3000);
|
|
1161
1119
|
}
|
|
1162
1120
|
} catch (error: unknown) {
|
|
1163
1121
|
ctx.ui?.notify?.(`FFF init failed: ${getErrorMessage(error)}`, "error");
|
|
@@ -1242,45 +1200,15 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1242
1200
|
|
|
1243
1201
|
const d = result.details as RenderDetails | undefined;
|
|
1244
1202
|
|
|
1245
|
-
// Image
|
|
1203
|
+
// Image reads keep the original image content so Pi's native TUI renderer
|
|
1204
|
+
// can display it exactly once. pi-pretty only renders metadata here;
|
|
1205
|
+
// rendering another inline image caused duplicate previews.
|
|
1246
1206
|
if (d?._type === "readImage") {
|
|
1247
|
-
const tw = termW();
|
|
1248
|
-
const out: string[] = [];
|
|
1249
|
-
const fname = basename(d.filePath);
|
|
1250
1207
|
const byteSize = Math.ceil(((d.data as string).length * 3) / 4);
|
|
1251
1208
|
const sizeStr = humanSize(byteSize);
|
|
1252
1209
|
const mimeStr = d.mimeType ?? "image";
|
|
1253
1210
|
|
|
1254
|
-
|
|
1255
|
-
out.push(rule(tw));
|
|
1256
|
-
|
|
1257
|
-
const protocol = detectImageProtocol();
|
|
1258
|
-
const passthroughWarning = getTmuxPassthroughWarning(protocol);
|
|
1259
|
-
if (passthroughWarning) {
|
|
1260
|
-
out.push(` ${FG_YELLOW}${passthroughWarning}${RST}`);
|
|
1261
|
-
} else if (protocol === "kitty") {
|
|
1262
|
-
if (d.mimeType && d.mimeType !== "image/png") {
|
|
1263
|
-
out.push(
|
|
1264
|
-
` ${FG_YELLOW}Kitty/Ghostty inline preview currently supports PNG payloads (got ${d.mimeType})${RST}`,
|
|
1265
|
-
);
|
|
1266
|
-
} else {
|
|
1267
|
-
const imgCols = Math.min(tw - 4, 80);
|
|
1268
|
-
out.push(renderKittyImage(d.data, { cols: imgCols }));
|
|
1269
|
-
}
|
|
1270
|
-
} else if (protocol === "iterm2") {
|
|
1271
|
-
const imgWidth = Math.min(tw - 4, 80);
|
|
1272
|
-
out.push(
|
|
1273
|
-
renderIterm2Image(d.data, {
|
|
1274
|
-
width: `${imgWidth}`,
|
|
1275
|
-
name: fname,
|
|
1276
|
-
}),
|
|
1277
|
-
);
|
|
1278
|
-
} else {
|
|
1279
|
-
out.push(` ${FG_DIM}(Inline image preview requires Ghostty, iTerm2, WezTerm, or Kitty)${RST}`);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
out.push(rule(tw));
|
|
1283
|
-
text.setText(fillToolBackground(out.join("\n")));
|
|
1211
|
+
text.setText(fillToolBackground(` ${fileIcon(d.filePath)}${FG_DIM}${mimeStr} · ${sizeStr}${RST}`));
|
|
1284
1212
|
return text;
|
|
1285
1213
|
}
|
|
1286
1214
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
14
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
15
15
|
import { CursorStore, fffFormatGrepText } from "../src/fff-helpers.js";
|
|
16
16
|
import piPrettyExtension, { type PiPrettyDeps } from "../src/index.js";
|
|
17
17
|
import {
|
|
@@ -314,6 +314,7 @@ describe("piPrettyExtension integration", () => {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
beforeEach(() => {
|
|
317
|
+
vi.useRealTimers();
|
|
317
318
|
tools = new Map();
|
|
318
319
|
events = new Map();
|
|
319
320
|
mockPi = {
|
|
@@ -336,6 +337,10 @@ describe("piPrettyExtension integration", () => {
|
|
|
336
337
|
piPrettyExtension(mockPi, deps);
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
afterEach(() => {
|
|
341
|
+
vi.useRealTimers();
|
|
342
|
+
});
|
|
343
|
+
|
|
339
344
|
async function loadWithFFF(finderOverrides?: Record<string, any>) {
|
|
340
345
|
load(true, finderOverrides);
|
|
341
346
|
const start = events.get("session_start")!;
|
|
@@ -714,6 +719,29 @@ describe("piPrettyExtension integration", () => {
|
|
|
714
719
|
}));
|
|
715
720
|
});
|
|
716
721
|
|
|
722
|
+
it("delayed FFF status clear does not read a stale session ctx", async () => {
|
|
723
|
+
vi.useFakeTimers();
|
|
724
|
+
const setStatus = vi.fn();
|
|
725
|
+
let stale = false;
|
|
726
|
+
const ctx = {
|
|
727
|
+
cwd: "/tmp/test",
|
|
728
|
+
get ui() {
|
|
729
|
+
if (stale) throw new Error("stale ctx");
|
|
730
|
+
return { setStatus };
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
load(true);
|
|
735
|
+
const start = events.get("session_start")!;
|
|
736
|
+
await start({}, ctx);
|
|
737
|
+
stale = true;
|
|
738
|
+
|
|
739
|
+
vi.advanceTimersByTime(3000);
|
|
740
|
+
|
|
741
|
+
expect(setStatus).toHaveBeenNthCalledWith(1, "fff", "FFF indexed");
|
|
742
|
+
expect(setStatus).toHaveBeenNthCalledWith(2, "fff", undefined);
|
|
743
|
+
});
|
|
744
|
+
|
|
717
745
|
it("shutdown → subsequent find falls back to SDK", async () => {
|
|
718
746
|
await loadWithFFF();
|
|
719
747
|
await events.get("session_shutdown")!();
|
|
@@ -128,11 +128,8 @@ describe("image rendering terminal detection", () => {
|
|
|
128
128
|
expect(__imageInternals.getTmuxPassthroughWarning("kitty")).toBeNull();
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
it("renders
|
|
132
|
-
process.env.
|
|
133
|
-
process.env.TERM_PROGRAM = "tmux";
|
|
134
|
-
process.env.KITTY_WINDOW_ID = "1";
|
|
135
|
-
__imageInternals.setTmuxAllowPassthroughOverrideForTests(false);
|
|
131
|
+
it("renders image metadata without a second inline preview", async () => {
|
|
132
|
+
process.env.TERM_PROGRAM = "kitty";
|
|
136
133
|
|
|
137
134
|
const readTool = loadReadTool(async () => ({
|
|
138
135
|
content: [{ type: "image", data: Buffer.from("fake").toString("base64"), mimeType: "image/png" }],
|
|
@@ -147,25 +144,10 @@ describe("image rendering terminal detection", () => {
|
|
|
147
144
|
invalidate: () => {},
|
|
148
145
|
});
|
|
149
146
|
|
|
150
|
-
expect(rendered.getText()).toContain("
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const readTool = loadReadTool(async () => ({
|
|
157
|
-
content: [{ type: "image", data: Buffer.from("jpeg").toString("base64"), mimeType: "image/jpeg" }],
|
|
158
|
-
}));
|
|
159
|
-
|
|
160
|
-
const result = await readTool.execute("t1", { path: "media/photo.jpg" }, null, null, {});
|
|
161
|
-
const rendered = readTool.renderResult(result, {}, {}, {
|
|
162
|
-
lastComponent: new MockText(),
|
|
163
|
-
isError: false,
|
|
164
|
-
state: {},
|
|
165
|
-
expanded: false,
|
|
166
|
-
invalidate: () => {},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
expect(rendered.getText()).toContain("supports PNG payloads");
|
|
147
|
+
expect(rendered.getText()).toContain("image/png");
|
|
148
|
+
expect(rendered.getText()).not.toContain("\x1b_G");
|
|
149
|
+
expect(result.content).toEqual([
|
|
150
|
+
{ type: "image", data: Buffer.from("fake").toString("base64"), mimeType: "image/png" },
|
|
151
|
+
]);
|
|
170
152
|
});
|
|
171
153
|
});
|