@heyhuynhgiabuu/pi-pretty 0.1.3 โ†’ 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 +192 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.1.3",
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
@@ -156,39 +156,163 @@ function lang(fp: string): BundledLanguage | undefined {
156
156
  }
157
157
 
158
158
  // ---------------------------------------------------------------------------
159
- // File-type icons
159
+ // Terminal image rendering (iTerm2 / Kitty / WezTerm inline image protocol)
160
160
  // ---------------------------------------------------------------------------
161
161
 
162
- const ICON_DIR = "๐Ÿ“";
163
- const ICON_DEFAULT = " ";
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
+
203
+ // ---------------------------------------------------------------------------
204
+ // File-type icons โ€” Nerd Font glyphs (Seti-UI + Devicons, stable in NF v3+)
205
+ //
206
+ // Requires a Nerd Font installed (e.g., JetBrainsMono Nerd Font, FiraCode NF).
207
+ // Fallback: set PRETTY_ICONS=none to disable icons.
208
+ // ---------------------------------------------------------------------------
209
+
210
+ const ICONS_MODE = (process.env.PRETTY_ICONS ?? "nerd").toLowerCase();
211
+ const USE_ICONS = ICONS_MODE !== "none" && ICONS_MODE !== "off";
212
+
213
+ // Nerd Font codepoints + ANSI color per file type
214
+ const NF_DIR = `${FG_BLUE}\ue5ff${RST}`; // folder
215
+ const NF_DIR_OPEN = `${FG_BLUE}\ue5fe${RST}`; // folder open
216
+ const NF_DEFAULT = `${FG_DIM}\uf15b${RST}`; // generic file
164
217
 
165
218
  const EXT_ICON: Record<string, string> = {
166
- ts: "๐ŸŸฆ", tsx: "๐ŸŸฆ", js: "๐ŸŸจ", jsx: "๐ŸŸจ", mjs: "๐ŸŸจ", cjs: "๐ŸŸจ",
167
- py: "๐Ÿ", rb: "๐Ÿ’Ž", rs: "๐Ÿฆ€", go: "๐Ÿ”ต", java: "โ˜•",
168
- c: "๐Ÿ”ง", cpp: "๐Ÿ”ง", h: "๐Ÿ”ง", hpp: "๐Ÿ”ง", cs: "๐ŸŸช",
169
- swift: "๐ŸŠ", kt: "๐ŸŸฃ",
170
- html: "๐ŸŒ", css: "๐ŸŽจ", scss: "๐ŸŽจ", less: "๐ŸŽจ",
171
- json: "๐Ÿ“‹", yaml: "๐Ÿ“‹", yml: "๐Ÿ“‹", toml: "๐Ÿ“‹",
172
- md: "๐Ÿ“", mdx: "๐Ÿ“",
173
- sql: "๐Ÿ—ƒ๏ธ", sh: "๐Ÿš", bash: "๐Ÿš", zsh: "๐Ÿš",
174
- png: "๐Ÿ–ผ๏ธ", jpg: "๐Ÿ–ผ๏ธ", jpeg: "๐Ÿ–ผ๏ธ", gif: "๐Ÿ–ผ๏ธ", svg: "๐Ÿ–ผ๏ธ", webp: "๐Ÿ–ผ๏ธ",
175
- lock: "๐Ÿ”’", env: "๐Ÿ”",
219
+ // TypeScript / JavaScript
220
+ ts: `\x1b[38;2;49;120;198m\ue628${RST}`, // blue
221
+ tsx: `\x1b[38;2;49;120;198m\ue7ba${RST}`, // react blue
222
+ js: `\x1b[38;2;241;224;90m\ue74e${RST}`, // yellow
223
+ jsx: `\x1b[38;2;97;218;251m\ue7ba${RST}`, // react cyan
224
+ mjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
225
+ cjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
226
+
227
+ // Systems / Backend
228
+ py: `\x1b[38;2;55;118;171m\ue73c${RST}`, // python blue
229
+ rs: `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust orange
230
+ go: `\x1b[38;2;0;173;216m\ue724${RST}`, // go cyan
231
+ java: `\x1b[38;2;204;62;68m\ue738${RST}`, // java red
232
+ swift: `\x1b[38;2;255;172;77m\ue755${RST}`, // swift orange
233
+ rb: `\x1b[38;2;204;52;45m\ue739${RST}`, // ruby red
234
+ kt: `\x1b[38;2;126;103;200m\ue634${RST}`, // kotlin purple
235
+ c: `\x1b[38;2;85;154;211m\ue61e${RST}`, // c blue
236
+ cpp: `\x1b[38;2;85;154;211m\ue61d${RST}`, // cpp blue
237
+ h: `\x1b[38;2;140;160;185m\ue61e${RST}`, // header muted
238
+ hpp: `\x1b[38;2;140;160;185m\ue61d${RST}`,
239
+ cs: `\x1b[38;2;104;33;122m\ue648${RST}`, // c# purple
240
+
241
+ // Web
242
+ html: `\x1b[38;2;228;77;38m\ue736${RST}`, // html orange
243
+ css: `\x1b[38;2;66;165;245m\ue749${RST}`, // css blue
244
+ scss: `\x1b[38;2;207;100;154m\ue749${RST}`, // scss pink
245
+ less: `\x1b[38;2;66;165;245m\ue749${RST}`,
246
+ vue: `\x1b[38;2;65;184;131m\ue6a0${RST}`, // vue green
247
+ svelte: `\x1b[38;2;255;62;0m\ue697${RST}`, // svelte red-orange
248
+
249
+ // Config / Data
250
+ json: `\x1b[38;2;241;224;90m\ue60b${RST}`, // json yellow
251
+ jsonc: `\x1b[38;2;241;224;90m\ue60b${RST}`,
252
+ yaml: `\x1b[38;2;160;116;196m\ue6a8${RST}`, // yaml purple
253
+ yml: `\x1b[38;2;160;116;196m\ue6a8${RST}`,
254
+ toml: `\x1b[38;2;160;116;196m\ue6b2${RST}`, // toml purple
255
+ xml: `\x1b[38;2;228;77;38m\ue619${RST}`, // xml orange
256
+ sql: `\x1b[38;2;218;218;218m\ue706${RST}`, // sql gray
257
+
258
+ // Markdown / Docs
259
+ md: `\x1b[38;2;66;165;245m\ue73e${RST}`, // markdown blue
260
+ mdx: `\x1b[38;2;66;165;245m\ue73e${RST}`,
261
+
262
+ // Shell / Scripts
263
+ sh: `\x1b[38;2;137;180;130m\ue795${RST}`, // shell green
264
+ bash: `\x1b[38;2;137;180;130m\ue795${RST}`,
265
+ zsh: `\x1b[38;2;137;180;130m\ue795${RST}`,
266
+ fish: `\x1b[38;2;137;180;130m\ue795${RST}`,
267
+ lua: `\x1b[38;2;81;160;207m\ue620${RST}`, // lua blue
268
+ php: `\x1b[38;2;137;147;186m\ue73d${RST}`, // php purple
269
+ dart: `\x1b[38;2;87;182;240m\ue798${RST}`, // dart blue
270
+
271
+ // Images
272
+ png: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
273
+ jpg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
274
+ jpeg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
275
+ gif: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
276
+ svg: `\x1b[38;2;255;180;50m\uf1c5${RST}`,
277
+ webp: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
278
+ ico: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
279
+
280
+ // Misc
281
+ lock: `\x1b[38;2;130;130;130m\uf023${RST}`, // lock gray
282
+ env: `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
283
+ graphql: `\x1b[38;2;224;51;144m\ue662${RST}`, // graphql pink
284
+ dockerfile: `\x1b[38;2;56;152;236m\ue7b0${RST}`,
176
285
  };
177
286
 
178
287
  const NAME_ICON: Record<string, string> = {
179
- "package.json": "๐Ÿ“ฆ", "package-lock.json": "๐Ÿ“ฆ",
180
- "tsconfig.json": "๐ŸŸฆ", "biome.json": "๐Ÿงน",
181
- ".gitignore": "๐Ÿ™ˆ", ".env": "๐Ÿ”", ".envrc": "๐Ÿ”",
182
- dockerfile: "๐Ÿณ", makefile: "๐Ÿ”ง", gnumakefile: "๐Ÿ”ง",
183
- "readme.md": "๐Ÿ“–", "license": "โš–๏ธ",
184
- "cargo.toml": "๐Ÿฆ€", "go.mod": "๐Ÿ”ต", "pyproject.toml": "๐Ÿ",
288
+ "package.json": `\x1b[38;2;137;180;130m\ue71e${RST}`, // npm green
289
+ "package-lock.json": `\x1b[38;2;130;130;130m\ue71e${RST}`, // npm gray
290
+ "tsconfig.json": `\x1b[38;2;49;120;198m\ue628${RST}`, // ts blue
291
+ "biome.json": `\x1b[38;2;96;165;250m\ue615${RST}`, // config blue
292
+ ".gitignore": `\x1b[38;2;222;165;132m\ue702${RST}`, // git orange
293
+ ".git": `\x1b[38;2;222;165;132m\ue702${RST}`,
294
+ ".env": `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
295
+ ".envrc": `\x1b[38;2;241;224;90m\ue615${RST}`,
296
+ "dockerfile": `\x1b[38;2;56;152;236m\ue7b0${RST}`, // docker blue
297
+ "makefile": `\x1b[38;2;130;130;130m\ue615${RST}`, // make gray
298
+ "gnumakefile": `\x1b[38;2;130;130;130m\ue615${RST}`,
299
+ "readme.md": `\x1b[38;2;66;165;245m\ue73e${RST}`, // readme blue
300
+ "license": `\x1b[38;2;218;218;218m\ue60a${RST}`, // license white
301
+ "cargo.toml": `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust
302
+ "go.mod": `\x1b[38;2;0;173;216m\ue724${RST}`, // go
303
+ "pyproject.toml": `\x1b[38;2;55;118;171m\ue73c${RST}`, // python
185
304
  };
186
305
 
187
306
  function fileIcon(fp: string): string {
307
+ if (!USE_ICONS) return "";
188
308
  const base = basename(fp).toLowerCase();
189
- if (NAME_ICON[base]) return NAME_ICON[base];
309
+ if (NAME_ICON[base]) return `${NAME_ICON[base]} `;
190
310
  const ext = extname(fp).slice(1).toLowerCase();
191
- return EXT_ICON[ext] ?? ICON_DEFAULT;
311
+ return EXT_ICON[ext] ? `${EXT_ICON[ext]} ` : `${NF_DEFAULT} `;
312
+ }
313
+
314
+ function dirIcon(): string {
315
+ return USE_ICONS ? `${NF_DIR} ` : "";
192
316
  }
193
317
 
194
318
  // ---------------------------------------------------------------------------
@@ -336,11 +460,11 @@ function renderTree(text: string, basePath: string): string {
336
460
  // Detect directories (entries ending with /)
337
461
  const isDir = entry.endsWith("/");
338
462
  const name = isDir ? entry.slice(0, -1) : entry;
339
- const icon = isDir ? ICON_DIR : fileIcon(name);
463
+ const icon = isDir ? dirIcon() : fileIcon(name);
340
464
  const fg = isDir ? FG_BLUE + BOLD : "";
341
465
  const reset = isDir ? RST : "";
342
466
 
343
- out.push(`${connector}${icon} ${fg}${name}${reset}`);
467
+ out.push(`${connector}${icon}${fg}${name}${reset}`);
344
468
  }
345
469
 
346
470
  if (total > MAX_PREVIEW_LINES) {
@@ -372,7 +496,7 @@ function renderFindResults(text: string): string {
372
496
 
373
497
  for (const [dir, files] of groups) {
374
498
  if (count > 0) out.push(""); // blank line between groups
375
- out.push(`${ICON_DIR} ${FG_BLUE}${BOLD}${dir}/${RST}`);
499
+ out.push(`${dirIcon()}${FG_BLUE}${BOLD}${dir}/${RST}`);
376
500
  for (let i = 0; i < files.length; i++) {
377
501
  if (count >= MAX_PREVIEW_LINES) {
378
502
  out.push(
@@ -383,7 +507,7 @@ function renderFindResults(text: string): string {
383
507
  const isLast = i === files.length - 1;
384
508
  const prefix = isLast ? "โ””โ”€โ”€ " : "โ”œโ”€โ”€ ";
385
509
  const icon = fileIcon(files[i]);
386
- out.push(` ${FG_RULE}${prefix}${RST}${icon} ${files[i]}`);
510
+ out.push(` ${FG_RULE}${prefix}${RST}${icon}${files[i]}`);
387
511
  count++;
388
512
  }
389
513
  }
@@ -426,7 +550,7 @@ async function renderGrepResults(
426
550
  if (file !== currentFile) {
427
551
  if (currentFile) out.push(""); // blank line between files
428
552
  const icon = fileIcon(file);
429
- out.push(`${icon} ${FG_BLUE}${BOLD}${file}${RST}`);
553
+ out.push(`${icon}${FG_BLUE}${BOLD}${file}${RST}`);
430
554
  currentFile = file;
431
555
  }
432
556
 
@@ -497,6 +621,18 @@ export default function piPrettyExtension(pi: any): void {
497
621
  const fp = params.path ?? "";
498
622
  const offset = params.offset ?? 1;
499
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
+
500
636
  // Extract text content for rendering
501
637
  const textContent = result.content
502
638
  ?.filter((c: any) => c.type === "text")
@@ -541,6 +677,36 @@ export default function piPrettyExtension(pi: any): void {
541
677
  }
542
678
 
543
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
+
544
710
  if (d?._type === "readFile" && d.content) {
545
711
  const key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${termW()}`;
546
712
  if (ctx.state._rk !== key) {