@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.
- package/package.json +1 -1
- 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
|
+
"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
|
-
//
|
|
159
|
+
// Terminal image rendering (iTerm2 / Kitty / WezTerm inline image protocol)
|
|
160
160
|
// ---------------------------------------------------------------------------
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
const
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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":
|
|
180
|
-
"
|
|
181
|
-
".
|
|
182
|
-
|
|
183
|
-
"
|
|
184
|
-
"
|
|
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]
|
|
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 ?
|
|
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}
|
|
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(`${
|
|
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}
|
|
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}
|
|
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) {
|