@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/LICENSE +21 -0
- package/README.md +68 -0
- package/package.json +54 -0
- package/src/README.md +66 -0
- package/src/ansi.ts +89 -0
- package/src/config.ts +66 -0
- package/src/diff-render.ts +892 -0
- package/src/diff.ts +68 -0
- package/src/fff.ts +416 -0
- package/src/highlight.ts +118 -0
- package/src/icons.ts +120 -0
- package/src/image.ts +166 -0
- package/src/index.test.ts +33 -0
- package/src/index.ts +1623 -0
- package/src/lang.ts +67 -0
- package/src/paste-chips.test.ts +138 -0
- package/src/paste-chips.ts +160 -0
- package/src/renderers.ts +222 -0
- package/src/thinking.test.ts +223 -0
- package/src/thinking.ts +100 -0
- package/src/tsconfig.json +14 -0
- package/src/types-diff.d.ts +41 -0
- package/src/types-fff.d.ts +80 -0
- package/src/types.ts +275 -0
- package/src/utils.ts +180 -0
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
|
+
});
|