@xynogen/pix-pretty 1.0.0

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/src/icons.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { basename, extname } from "node:path";
2
+
3
+ import { FG_BLUE, FG_DIM, RST } from "./ansi.js";
4
+
5
+ const ICONS_MODE = (process.env.PRETTY_ICONS ?? "nerd").toLowerCase();
6
+
7
+ const USE_ICONS = ICONS_MODE !== "none" && ICONS_MODE !== "off";
8
+
9
+ // Nerd Font codepoints + ANSI color per file type
10
+ const NF_DIR = `${FG_BLUE}\ue5ff${RST}`; // folder
11
+
12
+ const NF_DEFAULT = `${FG_DIM}\uf15b${RST}`; // generic file
13
+
14
+ const EXT_ICON: Record<string, string> = {
15
+ // TypeScript / JavaScript
16
+ ts: `\x1b[38;2;49;120;198m\ue628${RST}`, // blue
17
+ tsx: `\x1b[38;2;49;120;198m\ue7ba${RST}`, // react blue
18
+ js: `\x1b[38;2;241;224;90m\ue74e${RST}`, // yellow
19
+ jsx: `\x1b[38;2;97;218;251m\ue7ba${RST}`, // react cyan
20
+ mjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
21
+ cjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
22
+
23
+ // Systems / Backend
24
+ py: `\x1b[38;2;55;118;171m\ue73c${RST}`, // python blue
25
+ rs: `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust orange
26
+ go: `\x1b[38;2;0;173;216m\ue724${RST}`, // go cyan
27
+ java: `\x1b[38;2;204;62;68m\ue738${RST}`, // java red
28
+ swift: `\x1b[38;2;255;172;77m\ue755${RST}`, // swift orange
29
+ rb: `\x1b[38;2;204;52;45m\ue739${RST}`, // ruby red
30
+ kt: `\x1b[38;2;126;103;200m\ue634${RST}`, // kotlin purple
31
+ c: `\x1b[38;2;85;154;211m\ue61e${RST}`, // c blue
32
+ cpp: `\x1b[38;2;85;154;211m\ue61d${RST}`, // cpp blue
33
+ h: `\x1b[38;2;140;160;185m\ue61e${RST}`, // header muted
34
+ hpp: `\x1b[38;2;140;160;185m\ue61d${RST}`,
35
+ cs: `\x1b[38;2;104;33;122m\ue648${RST}`, // c# purple
36
+
37
+ // Web
38
+ html: `\x1b[38;2;228;77;38m\ue736${RST}`, // html orange
39
+ css: `\x1b[38;2;66;165;245m\ue749${RST}`, // css blue
40
+ scss: `\x1b[38;2;207;100;154m\ue749${RST}`, // scss pink
41
+ less: `\x1b[38;2;66;165;245m\ue749${RST}`,
42
+ vue: `\x1b[38;2;65;184;131m\ue6a0${RST}`, // vue green
43
+ svelte: `\x1b[38;2;255;62;0m\ue697${RST}`, // svelte red-orange
44
+
45
+ // Config / Data
46
+ json: `\x1b[38;2;241;224;90m\ue60b${RST}`, // json yellow
47
+ jsonc: `\x1b[38;2;241;224;90m\ue60b${RST}`,
48
+ yaml: `\x1b[38;2;160;116;196m\ue6a8${RST}`, // yaml purple
49
+ yml: `\x1b[38;2;160;116;196m\ue6a8${RST}`,
50
+ toml: `\x1b[38;2;160;116;196m\ue6b2${RST}`, // toml purple
51
+ xml: `\x1b[38;2;228;77;38m\ue619${RST}`, // xml orange
52
+ sql: `\x1b[38;2;218;218;218m\ue706${RST}`, // sql gray
53
+
54
+ // Markdown / Docs
55
+ md: `\x1b[38;2;66;165;245m\ue73e${RST}`, // markdown blue
56
+ mdx: `\x1b[38;2;66;165;245m\ue73e${RST}`,
57
+
58
+ // Shell / Scripts
59
+ sh: `\x1b[38;2;137;180;130m\ue795${RST}`, // shell green
60
+ bash: `\x1b[38;2;137;180;130m\ue795${RST}`,
61
+ zsh: `\x1b[38;2;137;180;130m\ue795${RST}`,
62
+ fish: `\x1b[38;2;137;180;130m\ue795${RST}`,
63
+ lua: `\x1b[38;2;81;160;207m\ue620${RST}`, // lua blue
64
+ php: `\x1b[38;2;137;147;186m\ue73d${RST}`, // php purple
65
+ dart: `\x1b[38;2;87;182;240m\ue798${RST}`, // dart blue
66
+
67
+ // Images
68
+ png: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
69
+ jpg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
70
+ jpeg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
71
+ gif: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
72
+ svg: `\x1b[38;2;255;180;50m\uf1c5${RST}`,
73
+ webp: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
74
+ ico: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
75
+
76
+ // Misc
77
+ lock: `\x1b[38;2;130;130;130m\uf023${RST}`, // lock gray
78
+ env: `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
79
+ graphql: `\x1b[38;2;224;51;144m\ue662${RST}`, // graphql pink
80
+ dockerfile: `\x1b[38;2;56;152;236m\ue7b0${RST}`,
81
+ };
82
+
83
+ const NAME_ICON: Record<string, string> = {
84
+ "package.json": `\x1b[38;2;137;180;130m\ue71e${RST}`, // npm green
85
+ "package-lock.json": `\x1b[38;2;130;130;130m\ue71e${RST}`, // npm gray
86
+ "tsconfig.json": `\x1b[38;2;49;120;198m\ue628${RST}`, // ts blue
87
+ "biome.json": `\x1b[38;2;96;165;250m\ue615${RST}`, // config blue
88
+ ".gitignore": `\x1b[38;2;222;165;132m\ue702${RST}`, // git orange
89
+ ".git": `\x1b[38;2;222;165;132m\ue702${RST}`,
90
+ ".env": `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
91
+ ".envrc": `\x1b[38;2;241;224;90m\ue615${RST}`,
92
+ dockerfile: `\x1b[38;2;56;152;236m\ue7b0${RST}`, // docker blue
93
+ makefile: `\x1b[38;2;130;130;130m\ue615${RST}`, // make gray
94
+ gnumakefile: `\x1b[38;2;130;130;130m\ue615${RST}`,
95
+ "readme.md": `\x1b[38;2;66;165;245m\ue73e${RST}`, // readme blue
96
+ license: `\x1b[38;2;218;218;218m\ue60a${RST}`, // license white
97
+ "cargo.toml": `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust
98
+ "go.mod": `\x1b[38;2;0;173;216m\ue724${RST}`, // go
99
+ "pyproject.toml": `\x1b[38;2;55;118;171m\ue73c${RST}`, // python
100
+ };
101
+
102
+ export function fileIcon(fp: string): string {
103
+ if (!USE_ICONS) return "";
104
+ const base = basename(fp).toLowerCase();
105
+ if (NAME_ICON[base]) return `${NAME_ICON[base]} `;
106
+ const ext = extname(fp).slice(1).toLowerCase();
107
+ return EXT_ICON[ext] ? `${EXT_ICON[ext]} ` : `${NF_DEFAULT} `;
108
+ }
109
+
110
+ export function dirIcon(): string {
111
+ return USE_ICONS ? `${NF_DIR} ` : "";
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // cli-highlight ANSI cache
116
+ //
117
+ // highlight.js uses different language ids than shiki for a few entries
118
+ // (no tsx/jsx grammar, jsonc, mdx, make, etc.). Map the shiki-style ids the
119
+ // EXT_LANG table produces onto highlight.js-supported ids.
120
+ // ---------------------------------------------------------------------------
package/src/image.ts ADDED
@@ -0,0 +1,166 @@
1
+ import * as childProcess from "node:child_process";
2
+
3
+ import type { ImageProtocol } from "./types.js";
4
+
5
+ let _tmuxClientTermCache: string | null | undefined;
6
+
7
+ let _tmuxAllowPassthroughCache: boolean | null | undefined;
8
+
9
+ let _tmuxClientTermOverrideForTests: string | null | undefined;
10
+
11
+ let _tmuxAllowPassthroughOverrideForTests: boolean | null | undefined;
12
+
13
+ function isTmuxSession(): boolean {
14
+ return !!process.env.TMUX || /^(tmux|screen)/.test(process.env.TERM ?? "");
15
+ }
16
+
17
+ function normalizeTerminalName(term: string): string {
18
+ const t = term.toLowerCase();
19
+ if (t.includes("kitty")) return "kitty";
20
+ if (t.includes("ghostty")) return "ghostty";
21
+ if (t.includes("wezterm")) return "WezTerm";
22
+ if (t.includes("iterm")) return "iTerm.app";
23
+ if (t.includes("mintty")) return "mintty";
24
+ return term;
25
+ }
26
+
27
+ function readTmuxClientTerm(): string | null {
28
+ if (_tmuxClientTermOverrideForTests !== undefined) {
29
+ return _tmuxClientTermOverrideForTests
30
+ ? normalizeTerminalName(_tmuxClientTermOverrideForTests)
31
+ : null;
32
+ }
33
+ if (!isTmuxSession()) return null;
34
+ if (_tmuxClientTermCache !== undefined) return _tmuxClientTermCache;
35
+ try {
36
+ const term = childProcess
37
+ .execFileSync("tmux", ["display-message", "-p", "#{client_termname}"], {
38
+ encoding: "utf8",
39
+ stdio: ["ignore", "pipe", "ignore"],
40
+ timeout: 200,
41
+ })
42
+ .trim();
43
+ _tmuxClientTermCache = term ? normalizeTerminalName(term) : null;
44
+ } catch {
45
+ _tmuxClientTermCache = null;
46
+ }
47
+ return _tmuxClientTermCache;
48
+ }
49
+
50
+ /**
51
+ * Detect the outer terminal when running inside tmux.
52
+ * tmux sets TERM_PROGRAM=tmux, but the real terminal is often in
53
+ * the environment of the tmux server or can be inferred.
54
+
55
+ */
56
+ function getOuterTerminal(): string {
57
+ // Environment hints that often survive inside tmux
58
+ if (process.env.LC_TERMINAL === "iTerm2") return "iTerm.app";
59
+ if (process.env.GHOSTTY_RESOURCES_DIR) return "ghostty";
60
+ if (process.env.KITTY_WINDOW_ID || process.env.KITTY_PID) return "kitty";
61
+ if (
62
+ process.env.WEZTERM_EXECUTABLE ||
63
+ process.env.WEZTERM_CONFIG_DIR ||
64
+ process.env.WEZTERM_CONFIG_FILE
65
+ ) {
66
+ return "WezTerm";
67
+ }
68
+
69
+ const termProgram = process.env.TERM_PROGRAM ?? "";
70
+ if (termProgram && termProgram !== "tmux" && termProgram !== "screen") {
71
+ return normalizeTerminalName(termProgram);
72
+ }
73
+
74
+ const tmuxClientTerm = readTmuxClientTerm();
75
+ if (tmuxClientTerm) return tmuxClientTerm;
76
+
77
+ const term = process.env.TERM ?? "";
78
+ if (term) return normalizeTerminalName(term);
79
+ if (
80
+ process.env.COLORTERM === "truecolor" ||
81
+ process.env.COLORTERM === "24bit"
82
+ )
83
+ return "unknown-modern";
84
+ return termProgram;
85
+ }
86
+
87
+ function detectImageProtocol(): ImageProtocol {
88
+ const forced = (process.env.PRETTY_IMAGE_PROTOCOL ?? "").toLowerCase();
89
+ if (forced === "kitty" || forced === "iterm2" || forced === "none") {
90
+ return forced;
91
+ }
92
+
93
+ const term = getOuterTerminal();
94
+ // Ghostty and Kitty use the Kitty graphics protocol
95
+ if (term === "ghostty" || term === "kitty") return "kitty";
96
+ // iTerm2, WezTerm, Mintty support the iTerm2 protocol
97
+ if (["iTerm.app", "WezTerm", "mintty"].includes(term)) return "iterm2";
98
+ if (process.env.LC_TERMINAL === "iTerm2") return "iterm2";
99
+ return "none";
100
+ }
101
+
102
+ function tmuxAllowsPassthrough(): boolean | null {
103
+ if (_tmuxAllowPassthroughOverrideForTests !== undefined)
104
+ return _tmuxAllowPassthroughOverrideForTests;
105
+ if (!isTmuxSession()) return null;
106
+ if (_tmuxAllowPassthroughCache !== undefined)
107
+ return _tmuxAllowPassthroughCache;
108
+ try {
109
+ const value = childProcess
110
+ .execFileSync("tmux", ["show-options", "-gv", "allow-passthrough"], {
111
+ encoding: "utf8",
112
+ stdio: ["ignore", "pipe", "ignore"],
113
+ timeout: 200,
114
+ })
115
+ .trim()
116
+ .toLowerCase();
117
+ _tmuxAllowPassthroughCache = value === "on" || value === "all";
118
+ } catch {
119
+ _tmuxAllowPassthroughCache = null;
120
+ }
121
+ return _tmuxAllowPassthroughCache;
122
+ }
123
+
124
+ function getTmuxPassthroughWarning(protocol: ImageProtocol): string | null {
125
+ if (!isTmuxSession() || protocol === "none") return null;
126
+ if (tmuxAllowsPassthrough() === false) {
127
+ return "tmux allow-passthrough is off. Run: tmux set -g allow-passthrough on";
128
+ }
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Wrap escape sequence for tmux passthrough.
134
+ * tmux requires: ESC Ptmux; <escaped-sequence> ESC \
135
+ * Inner ESC chars must be doubled.
136
+
137
+ */
138
+ function tmuxWrap(seq: string): string {
139
+ if (!isTmuxSession()) return seq;
140
+ // Double all ESC chars inside the sequence
141
+ const escaped = seq.split("\x1b").join("\x1b\x1b");
142
+ return `\x1bPtmux;${escaped}\x1b\\`;
143
+ }
144
+
145
+ export const __imageInternals = {
146
+ isTmuxSession,
147
+ getOuterTerminal,
148
+ detectImageProtocol,
149
+ tmuxWrap,
150
+ tmuxAllowsPassthrough,
151
+ getTmuxPassthroughWarning,
152
+ setTmuxClientTermOverrideForTests: (value: string | null | undefined) => {
153
+ _tmuxClientTermOverrideForTests = value;
154
+ },
155
+ setTmuxAllowPassthroughOverrideForTests: (
156
+ value: boolean | null | undefined,
157
+ ) => {
158
+ _tmuxAllowPassthroughOverrideForTests = value;
159
+ },
160
+ resetCachesForTests: () => {
161
+ _tmuxClientTermCache = undefined;
162
+ _tmuxAllowPassthroughCache = undefined;
163
+ _tmuxClientTermOverrideForTests = undefined;
164
+ _tmuxAllowPassthroughOverrideForTests = undefined;
165
+ },
166
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Basic smoke tests for pix-pretty extensions
3
+ */
4
+
5
+ import { describe, it, expect } from "bun:test";
6
+
7
+ describe("pix-pretty", () => {
8
+ it("exports are valid TypeScript modules", () => {
9
+ // Smoke test - just verify the files can be imported
10
+ expect(true).toBe(true);
11
+ });
12
+
13
+ describe("tool rendering extension", () => {
14
+ it("main extension exports a function", async () => {
15
+ const mainModule = await import("./index");
16
+ expect(mainModule.default).toBeFunction();
17
+ });
18
+ });
19
+
20
+ describe("paste-chips extension", () => {
21
+ it("paste-chips extension exports a function", async () => {
22
+ const pasteChipsModule = await import("./paste-chips");
23
+ expect(pasteChipsModule.default).toBeFunction();
24
+ });
25
+ });
26
+
27
+ describe("thinking extension", () => {
28
+ it("thinking extension exports a function", async () => {
29
+ const thinkingModule = await import("./thinking");
30
+ expect(thinkingModule.default).toBeFunction();
31
+ });
32
+ });
33
+ });