@heyhuynhgiabuu/pi-pretty 0.1.6 → 0.1.8
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/README.md +28 -150
- package/biome.json +20 -15
- package/media/bash-and-read.png +0 -0
- package/media/icons-and-grep.png +0 -0
- package/media/inline-image.png +0 -0
- package/package.json +48 -48
- package/src/index.ts +258 -205
package/README.md
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
# pi-pretty
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@heyhuynhgiabuu/pi-pretty)
|
|
4
|
+
[](https://github.com/heyhuynhgiabuu/pi-pretty/releases/latest)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
A [pi](https://pi.dev) extension that upgrades built-in tool output in the terminal without changing tool behavior.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
It currently enhances:
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- **`read`**: syntax-highlighted text previews with line numbers, plus inline image rendering when the terminal supports it
|
|
11
|
+
- **`bash`**: colored exit summary (`exit 0`/`exit 1`) with a preview body of command output
|
|
12
|
+
- **`ls` / `find` / `grep`**: Nerd Font file icons with tree/grouped layouts and clearer match rendering
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|------|-------------|
|
|
13
|
-
| **read** | Syntax-highlighted file content with line numbers (190+ languages via Shiki) |
|
|
14
|
-
| **bash** | Colored exit status (`✓ exit 0` / `✗ exit 1`), line count |
|
|
15
|
-
| **ls** | Tree-view directory listing with file-type icons (📁🟦🐍🦀…) |
|
|
16
|
-
| **find** | Grouped results by directory with file-type icons |
|
|
17
|
-
| **grep** | Highlighted pattern matches with file headers and line numbers |
|
|
14
|
+
> Companion to [@heyhuynhgiabuu/pi-diff](https://github.com/heyhuynhgiabuu/pi-diff) for `write`/`edit` diff rendering.
|
|
18
15
|
|
|
19
16
|
## Install
|
|
20
17
|
|
|
@@ -22,168 +19,49 @@ A [pi](https://pi.dev) extension that enhances built-in tool output with **synta
|
|
|
22
19
|
pi install npm:@heyhuynhgiabuu/pi-pretty
|
|
23
20
|
```
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
Latest release: https://github.com/heyhuynhgiabuu/pi-pretty/releases/latest
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
pi -e ./src/index.ts
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## How It Works
|
|
32
|
-
|
|
33
|
-
pi-pretty wraps the built-in SDK tools (`createReadTool`, `createBashTool`, `createLsTool`, `createFindTool`, `createGrepTool`). For each tool:
|
|
34
|
-
|
|
35
|
-
1. **Delegates** to the original `execute()` — no behavior changes
|
|
36
|
-
2. **Attaches metadata** to `result.details` for custom rendering
|
|
37
|
-
3. **Renders** enhanced output via `renderCall` / `renderResult`
|
|
38
|
-
|
|
39
|
-
The agent sees the same tool results. Only the TUI display changes.
|
|
40
|
-
|
|
41
|
-
## Configuration
|
|
42
|
-
|
|
43
|
-
All settings via environment variables. Add to your shell profile or `.envrc`:
|
|
44
|
-
|
|
45
|
-
### Theme
|
|
46
|
-
|
|
47
|
-
| Variable | Default | Description |
|
|
48
|
-
|----------|---------|-------------|
|
|
49
|
-
| `PRETTY_THEME` | `github-dark` | Shiki theme for syntax highlighting |
|
|
50
|
-
|
|
51
|
-
### Limits
|
|
52
|
-
|
|
53
|
-
| Variable | Default | Description |
|
|
54
|
-
|----------|---------|-------------|
|
|
55
|
-
| `PRETTY_MAX_HL_CHARS` | `80000` | Skip syntax highlighting above this |
|
|
56
|
-
| `PRETTY_MAX_PREVIEW_LINES` | `80` | Max lines shown in rendered output |
|
|
57
|
-
| `PRETTY_CACHE_LIMIT` | `128` | LRU cache entries for highlighted blocks |
|
|
58
|
-
|
|
59
|
-
### Example `.envrc`
|
|
24
|
+
Or load locally:
|
|
60
25
|
|
|
61
26
|
```bash
|
|
62
|
-
|
|
63
|
-
export PRETTY_MAX_PREVIEW_LINES=120
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Tool Details
|
|
67
|
-
|
|
68
|
-
### `read` — Syntax Highlighting
|
|
69
|
-
|
|
70
|
-
When the agent reads a file, pi-pretty renders it with:
|
|
71
|
-
- Shiki syntax highlighting (190+ languages, auto-detected from extension)
|
|
72
|
-
- Line numbers in a left gutter
|
|
73
|
-
- Long lines truncated with `›` indicator
|
|
74
|
-
- Respects `offset` and `limit` parameters
|
|
75
|
-
|
|
76
|
-
### `bash` — Exit Status
|
|
77
|
-
|
|
78
|
-
Bash command results show:
|
|
79
|
-
- `✓ exit 0` in green for success
|
|
80
|
-
- `✗ exit 1` in red for failure
|
|
81
|
-
- `⚡ killed` in yellow for terminated processes
|
|
82
|
-
- Line count for multi-line output
|
|
83
|
-
|
|
84
|
-
### `ls` — Tree View
|
|
85
|
-
|
|
86
|
-
Directory listings rendered as:
|
|
87
|
-
```
|
|
88
|
-
3 entries
|
|
89
|
-
├── 📁 src
|
|
90
|
-
├── 📦 package.json
|
|
91
|
-
└── 📖 README.md
|
|
27
|
+
pi -e ./src/index.ts
|
|
92
28
|
```
|
|
93
29
|
|
|
94
|
-
|
|
30
|
+
## Screenshots
|
|
95
31
|
|
|
96
|
-
|
|
32
|
+

|
|
33
|
+
*`bash` exit summary + output preview, and syntax-highlighted `read` text output.*
|
|
97
34
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
5 files
|
|
101
|
-
📁 src/
|
|
102
|
-
├── 🟦 index.ts
|
|
103
|
-
└── 🟦 utils.ts
|
|
104
|
-
📁 test/
|
|
105
|
-
├── 🟦 index.test.ts
|
|
106
|
-
└── 🟦 utils.test.ts
|
|
107
|
-
```
|
|
35
|
+

|
|
36
|
+
*`ls`/`find`/`grep` with Nerd Font icons and grouped/tree-oriented rendering.*
|
|
108
37
|
|
|
109
|
-
|
|
38
|
+

|
|
39
|
+
*`read` rendering an image inline in supported terminals.*
|
|
110
40
|
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
3 matches
|
|
114
|
-
🟦 src/index.ts
|
|
115
|
-
12 │ const result = await createReadTool(cwd);
|
|
116
|
-
45 │ export function createReadTool(path: string) {
|
|
117
|
-
|
|
118
|
-
🟦 src/utils.ts
|
|
119
|
-
8 │ import { createReadTool } from "./index";
|
|
120
|
-
```
|
|
41
|
+
## Terminal support for inline images
|
|
121
42
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
| Icon | Extensions |
|
|
125
|
-
|------|-----------|
|
|
126
|
-
| 🟦 | `.ts`, `.tsx`, `tsconfig.json` |
|
|
127
|
-
| 🟨 | `.js`, `.jsx`, `.mjs`, `.cjs` |
|
|
128
|
-
| 🐍 | `.py`, `pyproject.toml` |
|
|
129
|
-
| 🦀 | `.rs`, `Cargo.toml` |
|
|
130
|
-
| 🔵 | `.go`, `go.mod` |
|
|
131
|
-
| ☕ | `.java` |
|
|
132
|
-
| 🍊 | `.swift` |
|
|
133
|
-
| 💎 | `.rb` |
|
|
134
|
-
| 🌐 | `.html` |
|
|
135
|
-
| 🎨 | `.css`, `.scss`, `.less` |
|
|
136
|
-
| 📋 | `.json`, `.yaml`, `.toml` |
|
|
137
|
-
| 📝 | `.md`, `.mdx` |
|
|
138
|
-
| 🐚 | `.sh`, `.bash`, `.zsh` |
|
|
139
|
-
| 🖼️ | `.png`, `.jpg`, `.svg`, `.webp` |
|
|
140
|
-
| 📦 | `package.json` |
|
|
141
|
-
| 🐳 | `Dockerfile` |
|
|
142
|
-
| 🔐 | `.env`, `.envrc` |
|
|
143
|
-
| 📖 | `README.md` |
|
|
144
|
-
| ⚖️ | `LICENSE` |
|
|
145
|
-
|
|
146
|
-
## Architecture
|
|
43
|
+
Inline image previews are supported in **Ghostty**, **Kitty**, **iTerm2**, and **WezTerm**.
|
|
44
|
+
When running in **tmux**, pi-pretty uses passthrough escape sequences so inline image protocols still work.
|
|
147
45
|
|
|
148
|
-
|
|
149
|
-
src/
|
|
150
|
-
└── index.ts # Extension entry — wraps read/bash/ls/find/grep with pretty rendering
|
|
151
|
-
```
|
|
46
|
+
## Configuration
|
|
152
47
|
|
|
153
|
-
|
|
48
|
+
Optional environment variables:
|
|
154
49
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
| `renderTree()` | Tree-view with connectors and file icons |
|
|
161
|
-
| `renderFindResults()` | Directory-grouped file list with icons |
|
|
162
|
-
| `renderGrepResults()` | Pattern-highlighted matches with file headers |
|
|
163
|
-
| `fileIcon()` | Extension → emoji icon mapper |
|
|
164
|
-
| `lang()` | Extension → Shiki language mapper |
|
|
50
|
+
- `PRETTY_THEME` (default: `github-dark`)
|
|
51
|
+
- `PRETTY_MAX_HL_CHARS` (default: `80000`)
|
|
52
|
+
- `PRETTY_MAX_PREVIEW_LINES` (default: `80`)
|
|
53
|
+
- `PRETTY_CACHE_LIMIT` (default: `128`)
|
|
54
|
+
- `PRETTY_ICONS` (`nerd` by default, set to `none` to disable icons)
|
|
165
55
|
|
|
166
56
|
## Development
|
|
167
57
|
|
|
168
58
|
```bash
|
|
169
|
-
git clone https://github.com/heyhuynhgiabuu/pi-pretty.git
|
|
170
|
-
cd pi-pretty
|
|
171
59
|
npm install
|
|
172
60
|
npm run typecheck
|
|
173
61
|
npm run lint
|
|
174
62
|
npm test
|
|
175
63
|
```
|
|
176
64
|
|
|
177
|
-
### Load in pi for testing
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
pi -e ./src/index.ts
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Related
|
|
184
|
-
|
|
185
|
-
- [@heyhuynhgiabuu/pi-diff](https://github.com/heyhuynhgiabuu/pi-diff) — Syntax-highlighted diffs for `write`/`edit` tools
|
|
186
|
-
|
|
187
65
|
## License
|
|
188
66
|
|
|
189
67
|
MIT — [huynhgiabuu](https://github.com/heyhuynhgiabuu)
|
package/biome.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
|
|
3
|
+
"assist": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"actions": {
|
|
6
|
+
"source": {
|
|
7
|
+
"organizeImports": "on"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"linter": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"rules": {
|
|
14
|
+
"recommended": true
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"formatter": {
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"indentStyle": "tab",
|
|
20
|
+
"lineWidth": 120
|
|
21
|
+
}
|
|
17
22
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
2
|
+
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
|
|
5
|
+
"author": "huynhgiabuu",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/heyhuynhgiabuu/pi-pretty.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/heyhuynhgiabuu/pi-pretty#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/heyhuynhgiabuu/pi-pretty/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"syntax-highlighting",
|
|
20
|
+
"shiki",
|
|
21
|
+
"terminal",
|
|
22
|
+
"pretty-print"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@shikijs/cli": "^4.0.2"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
29
|
+
"@mariozechner/pi-tui": "*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"typescript": "^5.0.0",
|
|
34
|
+
"@biomejs/biome": "^2.3.5",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"lint": "biome check src/",
|
|
41
|
+
"lint:fix": "biome check --fix src/",
|
|
42
|
+
"test": "vitest run --passWithNoTests",
|
|
43
|
+
"test:watch": "vitest --passWithNoTests"
|
|
44
|
+
},
|
|
45
|
+
"pi": {
|
|
46
|
+
"extensions": [
|
|
47
|
+
"./src/index.ts"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
50
|
}
|
package/src/index.ts
CHANGED
|
@@ -33,8 +33,7 @@ import type { BundledLanguage, BundledTheme } from "shiki";
|
|
|
33
33
|
// Config
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
|
|
36
|
-
const THEME: BundledTheme =
|
|
37
|
-
(process.env.PRETTY_THEME as BundledTheme | undefined) ?? "github-dark";
|
|
36
|
+
const THEME: BundledTheme = (process.env.PRETTY_THEME as BundledTheme | undefined) ?? "github-dark";
|
|
38
37
|
|
|
39
38
|
function envInt(name: string, fallback: number): number {
|
|
40
39
|
const v = Number.parseInt(process.env[name] ?? "", 10);
|
|
@@ -49,7 +48,7 @@ const CACHE_LIMIT = envInt("PRETTY_CACHE_LIMIT", 128);
|
|
|
49
48
|
// ANSI
|
|
50
49
|
// ---------------------------------------------------------------------------
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
let RST = "\x1b[0m";
|
|
53
52
|
const BOLD = "\x1b[1m";
|
|
54
53
|
const DIM = "\x1b[2m";
|
|
55
54
|
const ITALIC = "\x1b[3m";
|
|
@@ -68,7 +67,34 @@ const FG_PURPLE = "\x1b[38;2;170;120;200m";
|
|
|
68
67
|
|
|
69
68
|
const BG_STDERR = "\x1b[48;2;40;25;25m";
|
|
70
69
|
|
|
71
|
-
const
|
|
70
|
+
const BG_DEFAULT = "\x1b[49m";
|
|
71
|
+
let BG_BASE = BG_DEFAULT; // tool box base bg — updated from theme's toolSuccessBg
|
|
72
|
+
|
|
73
|
+
/** Parse an ANSI 24-bit color escape into { r, g, b }. Handles both fg (38;2) and bg (48;2). */
|
|
74
|
+
function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
|
|
75
|
+
const m = ansi.match(/\x1b\[(?:38|48);2;(\d+);(\d+);(\d+)m/);
|
|
76
|
+
return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Read toolSuccessBg from the pi theme and update BG_BASE + RST.
|
|
80
|
+
* Call once when theme is first available. Idempotent. */
|
|
81
|
+
let _bgBaseResolved = false;
|
|
82
|
+
function resolveBaseBackground(theme: any): void {
|
|
83
|
+
if (_bgBaseResolved || !theme?.getBgAnsi) return;
|
|
84
|
+
_bgBaseResolved = true;
|
|
85
|
+
try {
|
|
86
|
+
const bgAnsi = theme.getBgAnsi("toolSuccessBg");
|
|
87
|
+
const parsed = parseAnsiRgb(bgAnsi);
|
|
88
|
+
if (parsed) {
|
|
89
|
+
BG_BASE = bgAnsi;
|
|
90
|
+
RST = `\x1b[0m${BG_BASE}`;
|
|
91
|
+
}
|
|
92
|
+
} catch { /* ignore — keep defaults */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ESC_RE = "\u001b";
|
|
96
|
+
const ANSI_RE = new RegExp(`${ESC_RE}\\[[0-9;]*m`, "g");
|
|
97
|
+
const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([0-9;]*)m`, "g");
|
|
72
98
|
|
|
73
99
|
// ---------------------------------------------------------------------------
|
|
74
100
|
// Low-contrast fix (same as pi-diff)
|
|
@@ -79,19 +105,14 @@ function isLowContrastShikiFg(params: string): boolean {
|
|
|
79
105
|
if (params === "38;5;0" || params === "38;5;8") return true;
|
|
80
106
|
if (!params.startsWith("38;2;")) return false;
|
|
81
107
|
const parts = params.split(";").map(Number);
|
|
82
|
-
if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n)))
|
|
83
|
-
return false;
|
|
108
|
+
if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) return false;
|
|
84
109
|
const [, , r, g, b] = parts;
|
|
85
110
|
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
86
111
|
return luminance < 72;
|
|
87
112
|
}
|
|
88
113
|
|
|
89
114
|
function normalizeShikiContrast(ansi: string): string {
|
|
90
|
-
return ansi.replace(
|
|
91
|
-
/\x1b\[([0-9;]*)m/g,
|
|
92
|
-
(seq, params: string) =>
|
|
93
|
-
isLowContrastShikiFg(params) ? FG_MUTED : seq,
|
|
94
|
-
);
|
|
115
|
+
return ansi.replace(ANSI_CAPTURE_RE, (seq, params: string) => (isLowContrastShikiFg(params) ? FG_MUTED : seq));
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
// ---------------------------------------------------------------------------
|
|
@@ -104,10 +125,7 @@ function strip(s: string): string {
|
|
|
104
125
|
|
|
105
126
|
function termW(): number {
|
|
106
127
|
const raw =
|
|
107
|
-
process.stdout.columns ||
|
|
108
|
-
(process.stderr as any).columns ||
|
|
109
|
-
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
110
|
-
200;
|
|
128
|
+
process.stdout.columns || (process.stderr as any).columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
|
|
111
129
|
return Math.max(80, Math.min(raw - 4, 210));
|
|
112
130
|
}
|
|
113
131
|
|
|
@@ -132,19 +150,54 @@ function lnum(n: number, w: number): string {
|
|
|
132
150
|
// ---------------------------------------------------------------------------
|
|
133
151
|
|
|
134
152
|
const EXT_LANG: Record<string, BundledLanguage> = {
|
|
135
|
-
ts: "typescript",
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
ts: "typescript",
|
|
154
|
+
tsx: "tsx",
|
|
155
|
+
js: "javascript",
|
|
156
|
+
jsx: "jsx",
|
|
157
|
+
mjs: "javascript",
|
|
158
|
+
cjs: "javascript",
|
|
159
|
+
py: "python",
|
|
160
|
+
rb: "ruby",
|
|
161
|
+
rs: "rust",
|
|
162
|
+
go: "go",
|
|
163
|
+
java: "java",
|
|
164
|
+
c: "c",
|
|
165
|
+
cpp: "cpp",
|
|
166
|
+
h: "c",
|
|
167
|
+
hpp: "cpp",
|
|
168
|
+
cs: "csharp",
|
|
169
|
+
swift: "swift",
|
|
170
|
+
kt: "kotlin",
|
|
171
|
+
html: "html",
|
|
172
|
+
css: "css",
|
|
173
|
+
scss: "scss",
|
|
174
|
+
less: "css",
|
|
175
|
+
json: "json",
|
|
176
|
+
jsonc: "jsonc",
|
|
177
|
+
yaml: "yaml",
|
|
178
|
+
yml: "yaml",
|
|
179
|
+
toml: "toml",
|
|
180
|
+
md: "markdown",
|
|
181
|
+
mdx: "mdx",
|
|
182
|
+
sql: "sql",
|
|
183
|
+
sh: "bash",
|
|
184
|
+
bash: "bash",
|
|
185
|
+
zsh: "bash",
|
|
186
|
+
lua: "lua",
|
|
187
|
+
php: "php",
|
|
188
|
+
dart: "dart",
|
|
189
|
+
xml: "xml",
|
|
190
|
+
graphql: "graphql",
|
|
191
|
+
svelte: "svelte",
|
|
192
|
+
vue: "vue",
|
|
193
|
+
dockerfile: "dockerfile",
|
|
194
|
+
makefile: "make",
|
|
195
|
+
zig: "zig",
|
|
196
|
+
nim: "nim",
|
|
197
|
+
elixir: "elixir",
|
|
198
|
+
ex: "elixir",
|
|
199
|
+
erb: "erb",
|
|
200
|
+
hbs: "handlebars",
|
|
148
201
|
};
|
|
149
202
|
|
|
150
203
|
function lang(fp: string): BundledLanguage | undefined {
|
|
@@ -157,12 +210,41 @@ function lang(fp: string): BundledLanguage | undefined {
|
|
|
157
210
|
|
|
158
211
|
// ---------------------------------------------------------------------------
|
|
159
212
|
// Terminal image rendering (iTerm2 / Kitty / Ghostty inline image protocols)
|
|
213
|
+
// Handles tmux passthrough for image protocols.
|
|
160
214
|
// ---------------------------------------------------------------------------
|
|
161
215
|
|
|
162
216
|
type ImageProtocol = "iterm2" | "kitty" | "none";
|
|
163
217
|
|
|
164
|
-
|
|
218
|
+
const IS_TMUX = !!process.env.TMUX;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Detect the outer terminal when running inside tmux.
|
|
222
|
+
* tmux sets TERM_PROGRAM=tmux, but the real terminal is often in
|
|
223
|
+
* the environment of the tmux server or can be inferred.
|
|
224
|
+
*/
|
|
225
|
+
function getOuterTerminal(): string {
|
|
226
|
+
// Direct terminal (not in tmux)
|
|
165
227
|
const term = process.env.TERM_PROGRAM ?? "";
|
|
228
|
+
if (term !== "tmux" && term !== "screen") return term;
|
|
229
|
+
|
|
230
|
+
// Inside tmux: check common env vars that leak through
|
|
231
|
+
// Ghostty sets this; iTerm2 sets LC_TERMINAL
|
|
232
|
+
if (process.env.LC_TERMINAL === "iTerm2") return "iTerm.app";
|
|
233
|
+
|
|
234
|
+
// TERM_PROGRAM_VERSION sometimes survives into tmux
|
|
235
|
+
// Try to detect via COLORTERM or other hints
|
|
236
|
+
if (process.env.GHOSTTY_RESOURCES_DIR) return "ghostty";
|
|
237
|
+
|
|
238
|
+
// Default: assume modern terminal if truecolor is supported
|
|
239
|
+
if (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit") {
|
|
240
|
+
// Can't determine exact terminal, but likely modern
|
|
241
|
+
return "unknown-modern";
|
|
242
|
+
}
|
|
243
|
+
return term;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function detectImageProtocol(): ImageProtocol {
|
|
247
|
+
const term = getOuterTerminal();
|
|
166
248
|
// Ghostty and Kitty use the Kitty graphics protocol
|
|
167
249
|
if (term === "ghostty" || term === "kitty") return "kitty";
|
|
168
250
|
// iTerm2, WezTerm, Mintty support the iTerm2 protocol
|
|
@@ -171,20 +253,30 @@ function detectImageProtocol(): ImageProtocol {
|
|
|
171
253
|
return "none";
|
|
172
254
|
}
|
|
173
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Wrap escape sequence for tmux passthrough.
|
|
258
|
+
* tmux requires: ESC Ptmux; <escaped-sequence> ESC \
|
|
259
|
+
* Inner ESC chars must be doubled.
|
|
260
|
+
*/
|
|
261
|
+
function tmuxWrap(seq: string): string {
|
|
262
|
+
if (!IS_TMUX) return seq;
|
|
263
|
+
// Double all ESC chars inside the sequence
|
|
264
|
+
const escaped = seq.split("\x1b").join("\x1b\x1b");
|
|
265
|
+
return `\x1bPtmux;${escaped}\x1b\\`;
|
|
266
|
+
}
|
|
267
|
+
|
|
174
268
|
/**
|
|
175
269
|
* Render base64 image inline using iTerm2 inline image protocol.
|
|
176
270
|
* Protocol: ESC ] 1337 ; File=[args] : base64data BEL
|
|
177
271
|
*/
|
|
178
|
-
function renderIterm2Image(
|
|
179
|
-
base64Data: string,
|
|
180
|
-
opts: { width?: string; name?: string } = {},
|
|
181
|
-
): string {
|
|
272
|
+
function renderIterm2Image(base64Data: string, opts: { width?: string; name?: string } = {}): string {
|
|
182
273
|
const args: string[] = ["inline=1", "preserveAspectRatio=1"];
|
|
183
274
|
if (opts.width) args.push(`width=${opts.width}`);
|
|
184
275
|
if (opts.name) args.push(`name=${Buffer.from(opts.name).toString("base64")}`);
|
|
185
|
-
const byteSize = Math.ceil(base64Data.length * 3 / 4);
|
|
276
|
+
const byteSize = Math.ceil((base64Data.length * 3) / 4);
|
|
186
277
|
args.push(`size=${byteSize}`);
|
|
187
|
-
|
|
278
|
+
const seq = `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
|
|
279
|
+
return tmuxWrap(seq);
|
|
188
280
|
}
|
|
189
281
|
|
|
190
282
|
/**
|
|
@@ -193,10 +285,7 @@ function renderIterm2Image(
|
|
|
193
285
|
* Chunked in 4096-byte pieces as required by protocol.
|
|
194
286
|
* Supported by: Kitty, Ghostty
|
|
195
287
|
*/
|
|
196
|
-
function renderKittyImage(
|
|
197
|
-
base64Data: string,
|
|
198
|
-
opts: { cols?: number } = {},
|
|
199
|
-
): string {
|
|
288
|
+
function renderKittyImage(base64Data: string, opts: { cols?: number } = {}): string {
|
|
200
289
|
const chunks: string[] = [];
|
|
201
290
|
const CHUNK_SIZE = 4096;
|
|
202
291
|
|
|
@@ -207,13 +296,10 @@ function renderKittyImage(
|
|
|
207
296
|
const more = isLast ? 0 : 1;
|
|
208
297
|
|
|
209
298
|
if (isFirst) {
|
|
210
|
-
// First chunk: include all metadata
|
|
211
|
-
// a=T (transmit+display), f=100 (PNG), t=d (direct data)
|
|
212
299
|
const colPart = opts.cols ? `,c=${opts.cols}` : "";
|
|
213
|
-
chunks.push(`\x1b_Ga=T,f=100,t=d,m=${more}${colPart};${chunk}\x1b\\`);
|
|
300
|
+
chunks.push(tmuxWrap(`\x1b_Ga=T,f=100,t=d,m=${more}${colPart};${chunk}\x1b\\`));
|
|
214
301
|
} else {
|
|
215
|
-
|
|
216
|
-
chunks.push(`\x1b_Gm=${more};${chunk}\x1b\\`);
|
|
302
|
+
chunks.push(tmuxWrap(`\x1b_Gm=${more};${chunk}\x1b\\`));
|
|
217
303
|
}
|
|
218
304
|
}
|
|
219
305
|
|
|
@@ -240,96 +326,96 @@ const ICONS_MODE = (process.env.PRETTY_ICONS ?? "nerd").toLowerCase();
|
|
|
240
326
|
const USE_ICONS = ICONS_MODE !== "none" && ICONS_MODE !== "off";
|
|
241
327
|
|
|
242
328
|
// Nerd Font codepoints + ANSI color per file type
|
|
243
|
-
const NF_DIR
|
|
244
|
-
const NF_DIR_OPEN = `${FG_BLUE}\ue5fe${RST}`;
|
|
245
|
-
const NF_DEFAULT
|
|
329
|
+
const NF_DIR = `${FG_BLUE}\ue5ff${RST}`; // folder
|
|
330
|
+
const NF_DIR_OPEN = `${FG_BLUE}\ue5fe${RST}`; // folder open
|
|
331
|
+
const NF_DEFAULT = `${FG_DIM}\uf15b${RST}`; // generic file
|
|
246
332
|
|
|
247
333
|
const EXT_ICON: Record<string, string> = {
|
|
248
334
|
// TypeScript / JavaScript
|
|
249
|
-
ts:
|
|
250
|
-
tsx: `\x1b[38;2;49;120;198m\ue7ba${RST}`,
|
|
251
|
-
js:
|
|
252
|
-
jsx: `\x1b[38;2;97;218;251m\ue7ba${RST}`,
|
|
335
|
+
ts: `\x1b[38;2;49;120;198m\ue628${RST}`, // blue
|
|
336
|
+
tsx: `\x1b[38;2;49;120;198m\ue7ba${RST}`, // react blue
|
|
337
|
+
js: `\x1b[38;2;241;224;90m\ue74e${RST}`, // yellow
|
|
338
|
+
jsx: `\x1b[38;2;97;218;251m\ue7ba${RST}`, // react cyan
|
|
253
339
|
mjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
|
|
254
340
|
cjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
|
|
255
341
|
|
|
256
342
|
// Systems / Backend
|
|
257
|
-
py:
|
|
258
|
-
rs:
|
|
259
|
-
go:
|
|
260
|
-
java:
|
|
261
|
-
swift: `\x1b[38;2;255;172;77m\ue755${RST}`,
|
|
262
|
-
rb:
|
|
263
|
-
kt:
|
|
264
|
-
c:
|
|
265
|
-
cpp:
|
|
266
|
-
h:
|
|
267
|
-
hpp:
|
|
268
|
-
cs:
|
|
343
|
+
py: `\x1b[38;2;55;118;171m\ue73c${RST}`, // python blue
|
|
344
|
+
rs: `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust orange
|
|
345
|
+
go: `\x1b[38;2;0;173;216m\ue724${RST}`, // go cyan
|
|
346
|
+
java: `\x1b[38;2;204;62;68m\ue738${RST}`, // java red
|
|
347
|
+
swift: `\x1b[38;2;255;172;77m\ue755${RST}`, // swift orange
|
|
348
|
+
rb: `\x1b[38;2;204;52;45m\ue739${RST}`, // ruby red
|
|
349
|
+
kt: `\x1b[38;2;126;103;200m\ue634${RST}`, // kotlin purple
|
|
350
|
+
c: `\x1b[38;2;85;154;211m\ue61e${RST}`, // c blue
|
|
351
|
+
cpp: `\x1b[38;2;85;154;211m\ue61d${RST}`, // cpp blue
|
|
352
|
+
h: `\x1b[38;2;140;160;185m\ue61e${RST}`, // header muted
|
|
353
|
+
hpp: `\x1b[38;2;140;160;185m\ue61d${RST}`,
|
|
354
|
+
cs: `\x1b[38;2;104;33;122m\ue648${RST}`, // c# purple
|
|
269
355
|
|
|
270
356
|
// Web
|
|
271
|
-
html:
|
|
272
|
-
css:
|
|
273
|
-
scss:
|
|
274
|
-
less:
|
|
275
|
-
vue:
|
|
276
|
-
svelte: `\x1b[38;2;255;62;0m\ue697${RST}`,
|
|
357
|
+
html: `\x1b[38;2;228;77;38m\ue736${RST}`, // html orange
|
|
358
|
+
css: `\x1b[38;2;66;165;245m\ue749${RST}`, // css blue
|
|
359
|
+
scss: `\x1b[38;2;207;100;154m\ue749${RST}`, // scss pink
|
|
360
|
+
less: `\x1b[38;2;66;165;245m\ue749${RST}`,
|
|
361
|
+
vue: `\x1b[38;2;65;184;131m\ue6a0${RST}`, // vue green
|
|
362
|
+
svelte: `\x1b[38;2;255;62;0m\ue697${RST}`, // svelte red-orange
|
|
277
363
|
|
|
278
364
|
// Config / Data
|
|
279
|
-
json:
|
|
365
|
+
json: `\x1b[38;2;241;224;90m\ue60b${RST}`, // json yellow
|
|
280
366
|
jsonc: `\x1b[38;2;241;224;90m\ue60b${RST}`,
|
|
281
|
-
yaml:
|
|
282
|
-
yml:
|
|
283
|
-
toml:
|
|
284
|
-
xml:
|
|
285
|
-
sql:
|
|
367
|
+
yaml: `\x1b[38;2;160;116;196m\ue6a8${RST}`, // yaml purple
|
|
368
|
+
yml: `\x1b[38;2;160;116;196m\ue6a8${RST}`,
|
|
369
|
+
toml: `\x1b[38;2;160;116;196m\ue6b2${RST}`, // toml purple
|
|
370
|
+
xml: `\x1b[38;2;228;77;38m\ue619${RST}`, // xml orange
|
|
371
|
+
sql: `\x1b[38;2;218;218;218m\ue706${RST}`, // sql gray
|
|
286
372
|
|
|
287
373
|
// Markdown / Docs
|
|
288
|
-
md:
|
|
374
|
+
md: `\x1b[38;2;66;165;245m\ue73e${RST}`, // markdown blue
|
|
289
375
|
mdx: `\x1b[38;2;66;165;245m\ue73e${RST}`,
|
|
290
376
|
|
|
291
377
|
// Shell / Scripts
|
|
292
|
-
sh:
|
|
378
|
+
sh: `\x1b[38;2;137;180;130m\ue795${RST}`, // shell green
|
|
293
379
|
bash: `\x1b[38;2;137;180;130m\ue795${RST}`,
|
|
294
|
-
zsh:
|
|
380
|
+
zsh: `\x1b[38;2;137;180;130m\ue795${RST}`,
|
|
295
381
|
fish: `\x1b[38;2;137;180;130m\ue795${RST}`,
|
|
296
|
-
lua:
|
|
297
|
-
php:
|
|
298
|
-
dart: `\x1b[38;2;87;182;240m\ue798${RST}`,
|
|
382
|
+
lua: `\x1b[38;2;81;160;207m\ue620${RST}`, // lua blue
|
|
383
|
+
php: `\x1b[38;2;137;147;186m\ue73d${RST}`, // php purple
|
|
384
|
+
dart: `\x1b[38;2;87;182;240m\ue798${RST}`, // dart blue
|
|
299
385
|
|
|
300
386
|
// Images
|
|
301
|
-
png:
|
|
302
|
-
jpg:
|
|
387
|
+
png: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
|
|
388
|
+
jpg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
|
|
303
389
|
jpeg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
|
|
304
|
-
gif:
|
|
305
|
-
svg:
|
|
390
|
+
gif: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
|
|
391
|
+
svg: `\x1b[38;2;255;180;50m\uf1c5${RST}`,
|
|
306
392
|
webp: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
|
|
307
|
-
ico:
|
|
393
|
+
ico: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
|
|
308
394
|
|
|
309
395
|
// Misc
|
|
310
|
-
lock: `\x1b[38;2;130;130;130m\uf023${RST}`,
|
|
311
|
-
env:
|
|
312
|
-
graphql: `\x1b[38;2;224;51;144m\ue662${RST}`,
|
|
396
|
+
lock: `\x1b[38;2;130;130;130m\uf023${RST}`, // lock gray
|
|
397
|
+
env: `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
|
|
398
|
+
graphql: `\x1b[38;2;224;51;144m\ue662${RST}`, // graphql pink
|
|
313
399
|
dockerfile: `\x1b[38;2;56;152;236m\ue7b0${RST}`,
|
|
314
400
|
};
|
|
315
401
|
|
|
316
402
|
const NAME_ICON: Record<string, string> = {
|
|
317
|
-
"package.json":
|
|
318
|
-
"package-lock.json": `\x1b[38;2;130;130;130m\ue71e${RST}`,
|
|
319
|
-
"tsconfig.json":
|
|
320
|
-
"biome.json":
|
|
321
|
-
".gitignore":
|
|
322
|
-
".git":
|
|
323
|
-
".env":
|
|
324
|
-
".envrc":
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"readme.md":
|
|
329
|
-
|
|
330
|
-
"cargo.toml":
|
|
331
|
-
"go.mod":
|
|
332
|
-
"pyproject.toml":
|
|
403
|
+
"package.json": `\x1b[38;2;137;180;130m\ue71e${RST}`, // npm green
|
|
404
|
+
"package-lock.json": `\x1b[38;2;130;130;130m\ue71e${RST}`, // npm gray
|
|
405
|
+
"tsconfig.json": `\x1b[38;2;49;120;198m\ue628${RST}`, // ts blue
|
|
406
|
+
"biome.json": `\x1b[38;2;96;165;250m\ue615${RST}`, // config blue
|
|
407
|
+
".gitignore": `\x1b[38;2;222;165;132m\ue702${RST}`, // git orange
|
|
408
|
+
".git": `\x1b[38;2;222;165;132m\ue702${RST}`,
|
|
409
|
+
".env": `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
|
|
410
|
+
".envrc": `\x1b[38;2;241;224;90m\ue615${RST}`,
|
|
411
|
+
dockerfile: `\x1b[38;2;56;152;236m\ue7b0${RST}`, // docker blue
|
|
412
|
+
makefile: `\x1b[38;2;130;130;130m\ue615${RST}`, // make gray
|
|
413
|
+
gnumakefile: `\x1b[38;2;130;130;130m\ue615${RST}`,
|
|
414
|
+
"readme.md": `\x1b[38;2;66;165;245m\ue73e${RST}`, // readme blue
|
|
415
|
+
license: `\x1b[38;2;218;218;218m\ue60a${RST}`, // license white
|
|
416
|
+
"cargo.toml": `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust
|
|
417
|
+
"go.mod": `\x1b[38;2;0;173;216m\ue724${RST}`, // go
|
|
418
|
+
"pyproject.toml": `\x1b[38;2;55;118;171m\ue73c${RST}`, // python
|
|
333
419
|
};
|
|
334
420
|
|
|
335
421
|
function fileIcon(fp: string): string {
|
|
@@ -364,10 +450,7 @@ function _touch(k: string, v: string[]): string[] {
|
|
|
364
450
|
return v;
|
|
365
451
|
}
|
|
366
452
|
|
|
367
|
-
async function hlBlock(
|
|
368
|
-
code: string,
|
|
369
|
-
language: BundledLanguage | undefined,
|
|
370
|
-
): Promise<string[]> {
|
|
453
|
+
async function hlBlock(code: string, language: BundledLanguage | undefined): Promise<string[]> {
|
|
371
454
|
if (!code) return [""];
|
|
372
455
|
if (!language || code.length > MAX_HL_CHARS) return code.split("\n");
|
|
373
456
|
|
|
@@ -431,32 +514,24 @@ async function renderFileContent(
|
|
|
431
514
|
vis++;
|
|
432
515
|
j++;
|
|
433
516
|
}
|
|
434
|
-
display = code.slice(0, j)
|
|
517
|
+
display = `${code.slice(0, j)}${RST}${FG_DIM}›${RST}`;
|
|
435
518
|
}
|
|
436
519
|
out.push(`${lnum(ln, nw)} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
437
520
|
}
|
|
438
521
|
|
|
439
522
|
out.push(rule(tw));
|
|
440
523
|
if (total > maxLines) {
|
|
441
|
-
out.push(
|
|
442
|
-
`${FG_DIM} … ${total - maxLines} more lines (${total} total)${RST}`,
|
|
443
|
-
);
|
|
524
|
+
out.push(`${FG_DIM} … ${total - maxLines} more lines (${total} total)${RST}`);
|
|
444
525
|
}
|
|
445
526
|
return out.join("\n");
|
|
446
527
|
}
|
|
447
528
|
|
|
448
529
|
/** Render bash output with colored exit code and stderr highlighting. */
|
|
449
|
-
function renderBashOutput(
|
|
450
|
-
text: string,
|
|
451
|
-
exitCode: number | null,
|
|
452
|
-
): { summary: string; body: string } {
|
|
530
|
+
function renderBashOutput(text: string, exitCode: number | null): { summary: string; body: string } {
|
|
453
531
|
const isOk = exitCode === 0;
|
|
454
532
|
const statusFg = isOk ? FG_GREEN : FG_RED;
|
|
455
533
|
const statusIcon = isOk ? "✓" : "✗";
|
|
456
|
-
const codeStr =
|
|
457
|
-
exitCode !== null
|
|
458
|
-
? `${statusFg}${statusIcon} exit ${exitCode}${RST}`
|
|
459
|
-
: `${FG_YELLOW}⚡ killed${RST}`;
|
|
534
|
+
const codeStr = exitCode !== null ? `${statusFg}${statusIcon} exit ${exitCode}${RST}` : `${FG_YELLOW}⚡ killed${RST}`;
|
|
460
535
|
|
|
461
536
|
const lines = text.split("\n");
|
|
462
537
|
const maxShow = MAX_PREVIEW_LINES;
|
|
@@ -497,9 +572,7 @@ function renderTree(text: string, basePath: string): string {
|
|
|
497
572
|
}
|
|
498
573
|
|
|
499
574
|
if (total > MAX_PREVIEW_LINES) {
|
|
500
|
-
out.push(
|
|
501
|
-
`${FG_RULE}└── ${RST}${FG_DIM}… ${total - MAX_PREVIEW_LINES} more entries${RST}`,
|
|
502
|
-
);
|
|
575
|
+
out.push(`${FG_RULE}└── ${RST}${FG_DIM}… ${total - MAX_PREVIEW_LINES} more entries${RST}`);
|
|
503
576
|
}
|
|
504
577
|
|
|
505
578
|
return out.join("\n");
|
|
@@ -528,9 +601,7 @@ function renderFindResults(text: string): string {
|
|
|
528
601
|
out.push(`${dirIcon()}${FG_BLUE}${BOLD}${dir}/${RST}`);
|
|
529
602
|
for (let i = 0; i < files.length; i++) {
|
|
530
603
|
if (count >= MAX_PREVIEW_LINES) {
|
|
531
|
-
out.push(
|
|
532
|
-
` ${FG_DIM}… ${lines.length - count} more files${RST}`,
|
|
533
|
-
);
|
|
604
|
+
out.push(` ${FG_DIM}… ${lines.length - count} more files${RST}`);
|
|
534
605
|
return out.join("\n");
|
|
535
606
|
}
|
|
536
607
|
const isLast = i === files.length - 1;
|
|
@@ -545,13 +616,9 @@ function renderFindResults(text: string): string {
|
|
|
545
616
|
}
|
|
546
617
|
|
|
547
618
|
/** Render grep results with highlighted matches and line numbers. */
|
|
548
|
-
async function renderGrepResults(
|
|
549
|
-
text: string,
|
|
550
|
-
pattern: string,
|
|
551
|
-
): Promise<string> {
|
|
619
|
+
async function renderGrepResults(text: string, pattern: string): Promise<string> {
|
|
552
620
|
const lines = text.split("\n");
|
|
553
|
-
if (!lines.length || (lines.length === 1 && !lines[0].trim()))
|
|
554
|
-
return `${FG_DIM}(no matches)${RST}`;
|
|
621
|
+
if (!lines.length || (lines.length === 1 && !lines[0].trim())) return `${FG_DIM}(no matches)${RST}`;
|
|
555
622
|
|
|
556
623
|
const tw = termW();
|
|
557
624
|
const out: string[] = [];
|
|
@@ -573,7 +640,7 @@ async function renderGrepResults(
|
|
|
573
640
|
}
|
|
574
641
|
|
|
575
642
|
// ripgrep-style: "file:line:content" or "file-line-content" or just "file"
|
|
576
|
-
const fileMatch = line.match(/^(.+?)[
|
|
643
|
+
const fileMatch = line.match(/^(.+?)[:-](\d+)[:-](.*)$/);
|
|
577
644
|
if (fileMatch) {
|
|
578
645
|
const [, file, lineNo, content] = fileMatch;
|
|
579
646
|
if (file !== currentFile) {
|
|
@@ -586,10 +653,7 @@ async function renderGrepResults(
|
|
|
586
653
|
const nw = Math.max(3, lineNo.length);
|
|
587
654
|
let display = content;
|
|
588
655
|
if (re) {
|
|
589
|
-
display = content.replace(
|
|
590
|
-
re,
|
|
591
|
-
`${RST}${FG_YELLOW}${BOLD}$1${RST}`,
|
|
592
|
-
);
|
|
656
|
+
display = content.replace(re, `${RST}${FG_YELLOW}${BOLD}$1${RST}`);
|
|
593
657
|
}
|
|
594
658
|
out.push(` ${lnum(Number(lineNo), nw)} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
595
659
|
count++;
|
|
@@ -683,24 +747,25 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
683
747
|
},
|
|
684
748
|
|
|
685
749
|
renderCall(args: any, theme: any, ctx: any) {
|
|
750
|
+
resolveBaseBackground(theme);
|
|
686
751
|
const fp = args?.path ?? "";
|
|
687
752
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
688
753
|
const offset = args?.offset ? ` ${theme.fg("muted", `from line ${args.offset}`)}` : "";
|
|
689
754
|
const limit = args?.limit ? ` ${theme.fg("muted", `(${args.limit} lines)`)}` : "";
|
|
690
|
-
text.setText(
|
|
691
|
-
`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`,
|
|
692
|
-
);
|
|
755
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`);
|
|
693
756
|
return text;
|
|
694
757
|
},
|
|
695
758
|
|
|
696
759
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
760
|
+
resolveBaseBackground(theme);
|
|
697
761
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
698
762
|
|
|
699
763
|
if (ctx.isError) {
|
|
700
|
-
const e =
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
764
|
+
const e =
|
|
765
|
+
result.content
|
|
766
|
+
?.filter((c: any) => c.type === "text")
|
|
767
|
+
.map((c: any) => c.text || "")
|
|
768
|
+
.join("\n") ?? "Error";
|
|
704
769
|
text.setText(`\n${theme.fg("error", e)}`);
|
|
705
770
|
return text;
|
|
706
771
|
}
|
|
@@ -712,7 +777,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
712
777
|
const tw = termW();
|
|
713
778
|
const out: string[] = [];
|
|
714
779
|
const fname = basename(d.filePath);
|
|
715
|
-
const byteSize = Math.ceil((d.data as string).length * 3 / 4);
|
|
780
|
+
const byteSize = Math.ceil(((d.data as string).length * 3) / 4);
|
|
716
781
|
const sizeStr = humanSize(byteSize);
|
|
717
782
|
const mimeStr = d.mimeType ?? "image";
|
|
718
783
|
|
|
@@ -725,10 +790,12 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
725
790
|
out.push(renderKittyImage(d.data, { cols: imgCols }));
|
|
726
791
|
} else if (protocol === "iterm2") {
|
|
727
792
|
const imgWidth = Math.min(tw - 4, 80);
|
|
728
|
-
out.push(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
793
|
+
out.push(
|
|
794
|
+
renderIterm2Image(d.data, {
|
|
795
|
+
width: `${imgWidth}`,
|
|
796
|
+
name: fname,
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
732
799
|
} else {
|
|
733
800
|
out.push(` ${FG_DIM}(Inline image preview requires Ghostty, iTerm2, WezTerm, or Kitty)${RST}`);
|
|
734
801
|
}
|
|
@@ -787,15 +854,10 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
787
854
|
// Try to extract exit code from the output
|
|
788
855
|
let exitCode: number | null = 0;
|
|
789
856
|
if (textContent) {
|
|
790
|
-
const exitMatch = textContent.match(
|
|
791
|
-
/(?:exit code|exited with|exit status)[:\s]*(\d+)/i,
|
|
792
|
-
);
|
|
857
|
+
const exitMatch = textContent.match(/(?:exit code|exited with|exit status)[:\s]*(\d+)/i);
|
|
793
858
|
if (exitMatch) exitCode = Number(exitMatch[1]);
|
|
794
859
|
// Check for common error indicators
|
|
795
|
-
if (
|
|
796
|
-
textContent.includes("command not found") ||
|
|
797
|
-
textContent.includes("No such file")
|
|
798
|
-
) {
|
|
860
|
+
if (textContent.includes("command not found") || textContent.includes("No such file")) {
|
|
799
861
|
exitCode = 1;
|
|
800
862
|
}
|
|
801
863
|
}
|
|
@@ -811,11 +873,10 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
811
873
|
},
|
|
812
874
|
|
|
813
875
|
renderCall(args: any, theme: any, ctx: any) {
|
|
876
|
+
resolveBaseBackground(theme);
|
|
814
877
|
const cmd = args?.command ?? "";
|
|
815
878
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
816
|
-
const timeout = args?.timeout
|
|
817
|
-
? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}`
|
|
818
|
-
: "";
|
|
879
|
+
const timeout = args?.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
|
|
819
880
|
text.setText(
|
|
820
881
|
`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", cmd.length > 80 ? cmd.slice(0, 77) + "…" : cmd)}${timeout}`,
|
|
821
882
|
);
|
|
@@ -823,13 +884,15 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
823
884
|
},
|
|
824
885
|
|
|
825
886
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
887
|
+
resolveBaseBackground(theme);
|
|
826
888
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
827
889
|
|
|
828
890
|
if (ctx.isError) {
|
|
829
|
-
const e =
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
891
|
+
const e =
|
|
892
|
+
result.content
|
|
893
|
+
?.filter((c: any) => c.type === "text")
|
|
894
|
+
.map((c: any) => c.text || "")
|
|
895
|
+
.join("\n") ?? "Error";
|
|
833
896
|
text.setText(`\n${theme.fg("error", e)}`);
|
|
834
897
|
return text;
|
|
835
898
|
}
|
|
@@ -863,9 +926,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
863
926
|
}
|
|
864
927
|
|
|
865
928
|
const fallback = result.content?.[0]?.text ?? "done";
|
|
866
|
-
text.setText(
|
|
867
|
-
` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
|
|
868
|
-
);
|
|
929
|
+
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
869
930
|
return text;
|
|
870
931
|
},
|
|
871
932
|
});
|
|
@@ -891,9 +952,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
891
952
|
.join("\n");
|
|
892
953
|
|
|
893
954
|
const fp = params.path ?? cwd;
|
|
894
|
-
const entryCount = textContent
|
|
895
|
-
? textContent.trim().split("\n").filter(Boolean).length
|
|
896
|
-
: 0;
|
|
955
|
+
const entryCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
|
|
897
956
|
|
|
898
957
|
(result as any).details = {
|
|
899
958
|
_type: "lsResult",
|
|
@@ -906,22 +965,23 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
906
965
|
},
|
|
907
966
|
|
|
908
967
|
renderCall(args: any, theme: any, ctx: any) {
|
|
968
|
+
resolveBaseBackground(theme);
|
|
909
969
|
const fp = args?.path ?? ".";
|
|
910
970
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
911
|
-
text.setText(
|
|
912
|
-
`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`,
|
|
913
|
-
);
|
|
971
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`);
|
|
914
972
|
return text;
|
|
915
973
|
},
|
|
916
974
|
|
|
917
975
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
976
|
+
resolveBaseBackground(theme);
|
|
918
977
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
919
978
|
|
|
920
979
|
if (ctx.isError) {
|
|
921
|
-
const e =
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
980
|
+
const e =
|
|
981
|
+
result.content
|
|
982
|
+
?.filter((c: any) => c.type === "text")
|
|
983
|
+
.map((c: any) => c.text || "")
|
|
984
|
+
.join("\n") ?? "Error";
|
|
925
985
|
text.setText(`\n${theme.fg("error", e)}`);
|
|
926
986
|
return text;
|
|
927
987
|
}
|
|
@@ -935,9 +995,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
935
995
|
}
|
|
936
996
|
|
|
937
997
|
const fallback = result.content?.[0]?.text ?? "listed";
|
|
938
|
-
text.setText(
|
|
939
|
-
` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
|
|
940
|
-
);
|
|
998
|
+
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
941
999
|
return text;
|
|
942
1000
|
},
|
|
943
1001
|
});
|
|
@@ -962,9 +1020,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
962
1020
|
.map((c: any) => c.text || "")
|
|
963
1021
|
.join("\n");
|
|
964
1022
|
|
|
965
|
-
const matchCount = textContent
|
|
966
|
-
? textContent.trim().split("\n").filter(Boolean).length
|
|
967
|
-
: 0;
|
|
1023
|
+
const matchCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
|
|
968
1024
|
|
|
969
1025
|
(result as any).details = {
|
|
970
1026
|
_type: "findResult",
|
|
@@ -977,23 +1033,24 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
977
1033
|
},
|
|
978
1034
|
|
|
979
1035
|
renderCall(args: any, theme: any, ctx: any) {
|
|
1036
|
+
resolveBaseBackground(theme);
|
|
980
1037
|
const pattern = args?.pattern ?? "";
|
|
981
1038
|
const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
|
|
982
1039
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
983
|
-
text.setText(
|
|
984
|
-
`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`,
|
|
985
|
-
);
|
|
1040
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`);
|
|
986
1041
|
return text;
|
|
987
1042
|
},
|
|
988
1043
|
|
|
989
1044
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
1045
|
+
resolveBaseBackground(theme);
|
|
990
1046
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
991
1047
|
|
|
992
1048
|
if (ctx.isError) {
|
|
993
|
-
const e =
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1049
|
+
const e =
|
|
1050
|
+
result.content
|
|
1051
|
+
?.filter((c: any) => c.type === "text")
|
|
1052
|
+
.map((c: any) => c.text || "")
|
|
1053
|
+
.join("\n") ?? "Error";
|
|
997
1054
|
text.setText(`\n${theme.fg("error", e)}`);
|
|
998
1055
|
return text;
|
|
999
1056
|
}
|
|
@@ -1007,9 +1064,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
1007
1064
|
}
|
|
1008
1065
|
|
|
1009
1066
|
const fallback = result.content?.[0]?.text ?? "found";
|
|
1010
|
-
text.setText(
|
|
1011
|
-
` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
|
|
1012
|
-
);
|
|
1067
|
+
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1013
1068
|
return text;
|
|
1014
1069
|
},
|
|
1015
1070
|
});
|
|
@@ -1038,8 +1093,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
1038
1093
|
? textContent
|
|
1039
1094
|
.trim()
|
|
1040
1095
|
.split("\n")
|
|
1041
|
-
.filter((l: string) => l.match(/^.+?[:\-]\d+[:\-]/))
|
|
1042
|
-
.length
|
|
1096
|
+
.filter((l: string) => l.match(/^.+?[:\-]\d+[:\-]/)).length
|
|
1043
1097
|
: 0;
|
|
1044
1098
|
|
|
1045
1099
|
(result as any).details = {
|
|
@@ -1053,24 +1107,25 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
1053
1107
|
},
|
|
1054
1108
|
|
|
1055
1109
|
renderCall(args: any, theme: any, ctx: any) {
|
|
1110
|
+
resolveBaseBackground(theme);
|
|
1056
1111
|
const pattern = args?.pattern ?? "";
|
|
1057
1112
|
const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
|
|
1058
1113
|
const glob = args?.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
|
|
1059
1114
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1060
|
-
text.setText(
|
|
1061
|
-
`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`,
|
|
1062
|
-
);
|
|
1115
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`);
|
|
1063
1116
|
return text;
|
|
1064
1117
|
},
|
|
1065
1118
|
|
|
1066
1119
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
1120
|
+
resolveBaseBackground(theme);
|
|
1067
1121
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1068
1122
|
|
|
1069
1123
|
if (ctx.isError) {
|
|
1070
|
-
const e =
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1124
|
+
const e =
|
|
1125
|
+
result.content
|
|
1126
|
+
?.filter((c: any) => c.type === "text")
|
|
1127
|
+
.map((c: any) => c.text || "")
|
|
1128
|
+
.join("\n") ?? "Error";
|
|
1074
1129
|
text.setText(`\n${theme.fg("error", e)}`);
|
|
1075
1130
|
return text;
|
|
1076
1131
|
}
|
|
@@ -1096,9 +1151,7 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
1096
1151
|
}
|
|
1097
1152
|
|
|
1098
1153
|
const fallback = result.content?.[0]?.text ?? "searched";
|
|
1099
|
-
text.setText(
|
|
1100
|
-
` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
|
|
1101
|
-
);
|
|
1154
|
+
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1102
1155
|
return text;
|
|
1103
1156
|
},
|
|
1104
1157
|
});
|