@heyhuynhgiabuu/pi-pretty 0.1.4 → 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.
- package/package.json +1 -1
- package/src/index.ts +118 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.1.
|
|
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
|
@@ -155,6 +155,80 @@ 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 / Ghostty inline image protocols)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
type ImageProtocol = "iterm2" | "kitty" | "none";
|
|
163
|
+
|
|
164
|
+
function detectImageProtocol(): ImageProtocol {
|
|
165
|
+
const term = process.env.TERM_PROGRAM ?? "";
|
|
166
|
+
// Ghostty and Kitty use the Kitty graphics protocol
|
|
167
|
+
if (term === "ghostty" || term === "kitty") return "kitty";
|
|
168
|
+
// iTerm2, WezTerm, Mintty support the iTerm2 protocol
|
|
169
|
+
if (["iTerm.app", "WezTerm", "mintty"].includes(term)) return "iterm2";
|
|
170
|
+
if (process.env.LC_TERMINAL === "iTerm2") return "iterm2";
|
|
171
|
+
return "none";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Render base64 image inline using iTerm2 inline image protocol.
|
|
176
|
+
* Protocol: ESC ] 1337 ; File=[args] : base64data BEL
|
|
177
|
+
*/
|
|
178
|
+
function renderIterm2Image(
|
|
179
|
+
base64Data: string,
|
|
180
|
+
opts: { width?: string; name?: string } = {},
|
|
181
|
+
): string {
|
|
182
|
+
const args: string[] = ["inline=1", "preserveAspectRatio=1"];
|
|
183
|
+
if (opts.width) args.push(`width=${opts.width}`);
|
|
184
|
+
if (opts.name) args.push(`name=${Buffer.from(opts.name).toString("base64")}`);
|
|
185
|
+
const byteSize = Math.ceil(base64Data.length * 3 / 4);
|
|
186
|
+
args.push(`size=${byteSize}`);
|
|
187
|
+
return `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
|
|
188
|
+
}
|
|
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
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get human-readable file size
|
|
225
|
+
*/
|
|
226
|
+
function humanSize(bytes: number): string {
|
|
227
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
228
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
229
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
230
|
+
}
|
|
231
|
+
|
|
158
232
|
// ---------------------------------------------------------------------------
|
|
159
233
|
// File-type icons — Nerd Font glyphs (Seti-UI + Devicons, stable in NF v3+)
|
|
160
234
|
//
|
|
@@ -576,6 +650,18 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
576
650
|
const fp = params.path ?? "";
|
|
577
651
|
const offset = params.offset ?? 1;
|
|
578
652
|
|
|
653
|
+
// Check for image content
|
|
654
|
+
const imageBlock = result.content?.find((c: any) => c.type === "image");
|
|
655
|
+
if (imageBlock) {
|
|
656
|
+
(result as any).details = {
|
|
657
|
+
_type: "readImage",
|
|
658
|
+
filePath: fp,
|
|
659
|
+
data: imageBlock.data,
|
|
660
|
+
mimeType: imageBlock.mimeType ?? "image/png",
|
|
661
|
+
};
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
579
665
|
// Extract text content for rendering
|
|
580
666
|
const textContent = result.content
|
|
581
667
|
?.filter((c: any) => c.type === "text")
|
|
@@ -620,6 +706,38 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
620
706
|
}
|
|
621
707
|
|
|
622
708
|
const d = result.details;
|
|
709
|
+
|
|
710
|
+
// Image rendering
|
|
711
|
+
if (d?._type === "readImage") {
|
|
712
|
+
const tw = termW();
|
|
713
|
+
const out: string[] = [];
|
|
714
|
+
const fname = basename(d.filePath);
|
|
715
|
+
const byteSize = Math.ceil((d.data as string).length * 3 / 4);
|
|
716
|
+
const sizeStr = humanSize(byteSize);
|
|
717
|
+
const mimeStr = d.mimeType ?? "image";
|
|
718
|
+
|
|
719
|
+
out.push(` ${fileIcon(d.filePath)}${FG_DIM}${mimeStr} · ${sizeStr}${RST}`);
|
|
720
|
+
out.push(rule(tw));
|
|
721
|
+
|
|
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") {
|
|
727
|
+
const imgWidth = Math.min(tw - 4, 80);
|
|
728
|
+
out.push(renderIterm2Image(d.data, {
|
|
729
|
+
width: `${imgWidth}`,
|
|
730
|
+
name: fname,
|
|
731
|
+
}));
|
|
732
|
+
} else {
|
|
733
|
+
out.push(` ${FG_DIM}(Inline image preview requires Ghostty, iTerm2, WezTerm, or Kitty)${RST}`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
out.push(rule(tw));
|
|
737
|
+
text.setText(out.join("\n"));
|
|
738
|
+
return text;
|
|
739
|
+
}
|
|
740
|
+
|
|
623
741
|
if (d?._type === "readFile" && d.content) {
|
|
624
742
|
const key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${termW()}`;
|
|
625
743
|
if (ctx.state._rk !== key) {
|