@heyhuynhgiabuu/pi-pretty 0.1.5 → 0.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +52 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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
@@ -156,41 +156,70 @@ function lang(fp: string): BundledLanguage | undefined {
156
156
  }
157
157
 
158
158
  // ---------------------------------------------------------------------------
159
- // Terminal image rendering (iTerm2 / Kitty / WezTerm inline image protocol)
159
+ // Terminal image rendering (iTerm2 / Kitty / Ghostty inline image protocols)
160
160
  // ---------------------------------------------------------------------------
161
161
 
162
- function supportsInlineImages(): boolean {
162
+ type ImageProtocol = "iterm2" | "kitty" | "none";
163
+
164
+ function detectImageProtocol(): ImageProtocol {
163
165
  const term = process.env.TERM_PROGRAM ?? "";
166
+ // Ghostty and Kitty use the Kitty graphics protocol
167
+ if (term === "ghostty" || term === "kitty") return "kitty";
164
168
  // 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;
169
+ if (["iTerm.app", "WezTerm", "mintty"].includes(term)) return "iterm2";
170
+ if (process.env.LC_TERMINAL === "iTerm2") return "iterm2";
171
+ return "none";
171
172
  }
172
173
 
173
174
  /**
174
175
  * Render base64 image inline using iTerm2 inline image protocol.
175
176
  * Protocol: ESC ] 1337 ; File=[args] : base64data BEL
176
- * Supported by: iTerm2, WezTerm, Mintty, Kitty (compat mode)
177
177
  */
178
- function renderInlineImage(
178
+ function renderIterm2Image(
179
179
  base64Data: string,
180
- opts: { width?: string; height?: string; preserveAspect?: boolean; name?: string } = {},
180
+ opts: { width?: string; name?: string } = {},
181
181
  ): string {
182
- const args: string[] = ["inline=1"];
182
+ const args: string[] = ["inline=1", "preserveAspectRatio=1"];
183
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
184
  if (opts.name) args.push(`name=${Buffer.from(opts.name).toString("base64")}`);
187
- // Calculate size for the size= param
188
185
  const byteSize = Math.ceil(base64Data.length * 3 / 4);
189
186
  args.push(`size=${byteSize}`);
190
-
191
187
  return `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
192
188
  }
193
189
 
190
+ /**
191
+ * Render base64 image inline using Kitty graphics protocol.
192
+ * Protocol: ESC _G <key>=<value>,...; <base64data> ESC \
193
+ * Chunked in 4096-byte pieces as required by protocol.
194
+ * Supported by: Kitty, Ghostty
195
+ */
196
+ function renderKittyImage(
197
+ base64Data: string,
198
+ opts: { cols?: number } = {},
199
+ ): string {
200
+ const chunks: string[] = [];
201
+ const CHUNK_SIZE = 4096;
202
+
203
+ for (let i = 0; i < base64Data.length; i += CHUNK_SIZE) {
204
+ const chunk = base64Data.slice(i, i + CHUNK_SIZE);
205
+ const isFirst = i === 0;
206
+ const isLast = i + CHUNK_SIZE >= base64Data.length;
207
+ const more = isLast ? 0 : 1;
208
+
209
+ if (isFirst) {
210
+ // First chunk: include all metadata
211
+ // a=T (transmit+display), f=100 (PNG), t=d (direct data)
212
+ const colPart = opts.cols ? `,c=${opts.cols}` : "";
213
+ chunks.push(`\x1b_Ga=T,f=100,t=d,m=${more}${colPart};${chunk}\x1b\\`);
214
+ } else {
215
+ // Continuation chunks
216
+ chunks.push(`\x1b_Gm=${more};${chunk}\x1b\\`);
217
+ }
218
+ }
219
+
220
+ return chunks.join("");
221
+ }
222
+
194
223
  /**
195
224
  * Get human-readable file size
196
225
  */
@@ -690,16 +719,18 @@ export default function piPrettyExtension(pi: any): void {
690
719
  out.push(` ${fileIcon(d.filePath)}${FG_DIM}${mimeStr} · ${sizeStr}${RST}`);
691
720
  out.push(rule(tw));
692
721
 
693
- if (supportsInlineImages()) {
694
- // Max width: use ~80 columns or terminal width, capped for readability
722
+ const protocol = detectImageProtocol();
723
+ if (protocol === "kitty") {
724
+ const imgCols = Math.min(tw - 4, 80);
725
+ out.push(renderKittyImage(d.data, { cols: imgCols }));
726
+ } else if (protocol === "iterm2") {
695
727
  const imgWidth = Math.min(tw - 4, 80);
696
- out.push(renderInlineImage(d.data, {
728
+ out.push(renderIterm2Image(d.data, {
697
729
  width: `${imgWidth}`,
698
730
  name: fname,
699
731
  }));
700
732
  } else {
701
- // Fallback: show metadata only
702
- out.push(` ${FG_DIM}(Inline image preview requires iTerm2, WezTerm, or Kitty)${RST}`);
733
+ out.push(` ${FG_DIM}(Inline image preview requires Ghostty, iTerm2, WezTerm, or Kitty)${RST}`);
703
734
  }
704
735
 
705
736
  out.push(rule(tw));