@heyhuynhgiabuu/pi-pretty 0.1.4 → 0.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +87 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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
@@ -155,6 +155,51 @@ function lang(fp: string): BundledLanguage | undefined {
155
155
  return EXT_LANG[extname(fp).slice(1).toLowerCase()];
156
156
  }
157
157
 
158
+ // ---------------------------------------------------------------------------
159
+ // Terminal image rendering (iTerm2 / Kitty / WezTerm inline image protocol)
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function supportsInlineImages(): boolean {
163
+ const term = process.env.TERM_PROGRAM ?? "";
164
+ // iTerm2, WezTerm, Mintty support the iTerm2 protocol
165
+ if (["iTerm.app", "WezTerm", "mintty"].includes(term)) return true;
166
+ // Kitty has its own protocol but also supports iTerm2 compat in newer versions
167
+ if (term === "kitty") return true;
168
+ // TERM_PROGRAM_VERSION for iTerm2 check
169
+ if (process.env.LC_TERMINAL === "iTerm2") return true;
170
+ return false;
171
+ }
172
+
173
+ /**
174
+ * Render base64 image inline using iTerm2 inline image protocol.
175
+ * Protocol: ESC ] 1337 ; File=[args] : base64data BEL
176
+ * Supported by: iTerm2, WezTerm, Mintty, Kitty (compat mode)
177
+ */
178
+ function renderInlineImage(
179
+ base64Data: string,
180
+ opts: { width?: string; height?: string; preserveAspect?: boolean; name?: string } = {},
181
+ ): string {
182
+ const args: string[] = ["inline=1"];
183
+ if (opts.width) args.push(`width=${opts.width}`);
184
+ if (opts.height) args.push(`height=${opts.height}`);
185
+ if (opts.preserveAspect !== false) args.push("preserveAspectRatio=1");
186
+ if (opts.name) args.push(`name=${Buffer.from(opts.name).toString("base64")}`);
187
+ // Calculate size for the size= param
188
+ const byteSize = Math.ceil(base64Data.length * 3 / 4);
189
+ args.push(`size=${byteSize}`);
190
+
191
+ return `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
192
+ }
193
+
194
+ /**
195
+ * Get human-readable file size
196
+ */
197
+ function humanSize(bytes: number): string {
198
+ if (bytes < 1024) return `${bytes}B`;
199
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
200
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
201
+ }
202
+
158
203
  // ---------------------------------------------------------------------------
159
204
  // File-type icons — Nerd Font glyphs (Seti-UI + Devicons, stable in NF v3+)
160
205
  //
@@ -576,6 +621,18 @@ export default function piPrettyExtension(pi: any): void {
576
621
  const fp = params.path ?? "";
577
622
  const offset = params.offset ?? 1;
578
623
 
624
+ // Check for image content
625
+ const imageBlock = result.content?.find((c: any) => c.type === "image");
626
+ if (imageBlock) {
627
+ (result as any).details = {
628
+ _type: "readImage",
629
+ filePath: fp,
630
+ data: imageBlock.data,
631
+ mimeType: imageBlock.mimeType ?? "image/png",
632
+ };
633
+ return result;
634
+ }
635
+
579
636
  // Extract text content for rendering
580
637
  const textContent = result.content
581
638
  ?.filter((c: any) => c.type === "text")
@@ -620,6 +677,36 @@ export default function piPrettyExtension(pi: any): void {
620
677
  }
621
678
 
622
679
  const d = result.details;
680
+
681
+ // Image rendering
682
+ if (d?._type === "readImage") {
683
+ const tw = termW();
684
+ const out: string[] = [];
685
+ const fname = basename(d.filePath);
686
+ const byteSize = Math.ceil((d.data as string).length * 3 / 4);
687
+ const sizeStr = humanSize(byteSize);
688
+ const mimeStr = d.mimeType ?? "image";
689
+
690
+ out.push(` ${fileIcon(d.filePath)}${FG_DIM}${mimeStr} · ${sizeStr}${RST}`);
691
+ out.push(rule(tw));
692
+
693
+ if (supportsInlineImages()) {
694
+ // Max width: use ~80 columns or terminal width, capped for readability
695
+ const imgWidth = Math.min(tw - 4, 80);
696
+ out.push(renderInlineImage(d.data, {
697
+ width: `${imgWidth}`,
698
+ name: fname,
699
+ }));
700
+ } else {
701
+ // Fallback: show metadata only
702
+ out.push(` ${FG_DIM}(Inline image preview requires iTerm2, WezTerm, or Kitty)${RST}`);
703
+ }
704
+
705
+ out.push(rule(tw));
706
+ text.setText(out.join("\n"));
707
+ return text;
708
+ }
709
+
623
710
  if (d?._type === "readFile" && d.content) {
624
711
  const key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${termW()}`;
625
712
  if (ctx.state._rk !== key) {