@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.4.2",
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
- * Call once when theme is first available. Idempotent. */
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 (_bgBaseResolved || !theme?.getBgAnsi) return;
141
- _bgBaseResolved = true;
139
+ if (!theme?.getBgAnsi) return;
142
140
 
143
- BG_BASE = getThemeBgAnsi(theme, "toolSuccessBg") ?? BG_DEFAULT;
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?.setStatus?.("fff", "FFF indexed");
1160
- setTimeout(() => ctx.ui?.setStatus?.("fff", undefined), 3000);
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 rendering
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
- out.push(` ${fileIcon(d.filePath)}${FG_DIM}${mimeStr} · ${sizeStr}${RST}`);
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 explicit warning for read image when tmux passthrough is off", async () => {
132
- process.env.TMUX = "/tmp/tmux-1000/default,123,0";
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("allow-passthrough is off");
151
- });
152
-
153
- it("warns on non-PNG payloads for kitty protocol", async () => {
154
- process.env.TERM_PROGRAM = "kitty";
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
  });