pi-image-tools 1.0.10 → 1.0.11

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/CHANGELOG.md CHANGED
@@ -1,25 +1,11 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [1.0.10] - 2026-04-22
11
-
12
- ### Added
13
- - Added preview width resolution from Pi terminal settings so inline and recent-image previews can honor `terminal.imageWidthCells`.
3
+ ## [1.0.11] - 2026-04-25
14
4
 
15
5
  ### Changed
16
- - Updated recent image selection previews to resolve terminal settings from the active session working directory before rendering.
17
- - Windows Sixel fallback now activates only when native terminal image support is unavailable, with environment toggles to force or disable Sixel rendering.
18
- - Clarified README global install and config paths when `PI_CODING_AGENT_DIR` is set.
19
- - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.68.1.
20
-
21
- ### Fixed
22
- - Ensured generated Sixel output is emitted as a complete DCS sequence and preserved inline image protocol rows during preview width fitting.
6
+ - Avoid Pi's built-in image paste shortcut by default while preserving the previous primary shortcut when users disable or rebind `app.clipboard.pasteImage` (thanks to @danielcherubini for reporting this in PR #3).
7
+ - Replaced the placeholder `enabled` config with validated `debug`, explicit `shortcuts.pasteImage`, configurable built-in conflict avoidance, and built-in warning suppression settings.
8
+ - Removed obsolete packaged README image assets now that the README uses a GitHub-hosted image.
23
9
 
24
10
  ## [1.0.9] - 2026-04-01
25
11
 
@@ -74,12 +60,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
74
60
  - Rewrote README.md with professional documentation standards
75
61
  - Added comprehensive feature documentation, configuration reference, and usage examples
76
62
 
77
- ## [1.0.1] - 2026-03-04
78
-
79
- - Included `asset/` in the npm package whitelist so README image assets ship in the tarball.
80
-
81
- ## [1.0.0] - 2026-03-02
63
+ ## 1.0.0
82
64
 
83
65
  - Standardized repository layout to `src/` + root shim entrypoint.
84
66
  - Added TypeScript/Bundler project config, package metadata, and publish whitelist.
85
- - Added standard docs, license, and config template/runtime placeholder files.
67
+ - Added standard docs, license, and initial runtime config files.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # 🖼️ pi-image-tools
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pi-image-tools?style=flat-square)](https://www.npmjs.com/package/pi-image-tools) [![License](https://img.shields.io/github/license/MasuRii/pi-image-tools?style=flat-square)](LICENSE)
4
+
3
5
  Image attachment and preview extension for the **Pi coding agent**.
4
6
 
5
7
  `pi-image-tools` lets you attach clipboard images or recent screenshots to your next user message, then preview them inline in the TUI before the message is sent.
@@ -93,8 +95,16 @@ Selecting an entry queues that image for your next message.
93
95
 
94
96
  | Platform | Shortcuts |
95
97
  |----------|-----------|
96
- | Windows | `Alt+V`, `Ctrl+Alt+V` |
97
- | Linux / macOS | `Ctrl+V`, `Alt+V`, `Ctrl+Alt+V` |
98
+ | Windows | `Ctrl+Alt+V`; `Alt+V` when Pi's built-in `app.clipboard.pasteImage` shortcut is disabled or rebound |
99
+ | Linux / macOS | `Alt+V`, `Ctrl+Alt+V`; `Ctrl+V` when Pi's built-in `app.clipboard.pasteImage` shortcut is disabled or rebound |
100
+
101
+ Pi's built-in image paste shortcut is not overridden by default. To keep the previous primary shortcut behavior without startup conflict warnings, disable the built-in binding manually in `~/.pi/agent/keybindings.json`:
102
+
103
+ ```json
104
+ {
105
+ "app.clipboard.pasteImage": []
106
+ }
107
+ ```
98
108
 
99
109
  ## Configuration
100
110
 
@@ -124,12 +134,85 @@ Starter template:
124
134
 
125
135
  ```json
126
136
  {
127
- "enabled": true
137
+ "debug": false,
138
+ "shortcuts": {
139
+ "pasteImage": ["ctrl+alt+v"],
140
+ "avoidBuiltinConflicts": true,
141
+ "suppressBuiltinConflictWarnings": true
142
+ }
128
143
  }
129
144
  ```
130
145
 
131
146
  See `config/config.example.json` for the same template.
132
147
 
148
+ #### Debug logging
149
+
150
+ Debug logging is disabled by default. Set `debug` to `true` to append debug events to `debug/debug.log` inside the extension directory:
151
+
152
+ ```json
153
+ {
154
+ "debug": true,
155
+ "shortcuts": {
156
+ "pasteImage": ["ctrl+alt+v"],
157
+ "avoidBuiltinConflicts": true,
158
+ "suppressBuiltinConflictWarnings": true
159
+ }
160
+ }
161
+ ```
162
+
163
+ When `debug` is `false` or omitted, no debug log file is opened or written.
164
+
165
+ #### Custom shortcuts
166
+
167
+ Set `shortcuts.pasteImage` to choose the exact shortcuts registered by `pi-image-tools`. The value can be a single shortcut string or an array of shortcut strings. Add multiple entries when you want several key combinations to run the same paste-image action:
168
+
169
+ ```json
170
+ {
171
+ "debug": false,
172
+ "shortcuts": {
173
+ "pasteImage": ["ctrl+alt+v", "alt+v", "ctrl+shift+v"],
174
+ "avoidBuiltinConflicts": true,
175
+ "suppressBuiltinConflictWarnings": true
176
+ }
177
+ }
178
+ ```
179
+
180
+ A single shortcut is also valid:
181
+
182
+ ```json
183
+ {
184
+ "debug": false,
185
+ "shortcuts": {
186
+ "pasteImage": "ctrl+alt+v",
187
+ "avoidBuiltinConflicts": true,
188
+ "suppressBuiltinConflictWarnings": true
189
+ }
190
+ }
191
+ ```
192
+
193
+ Config changes are applied the next time the extension loads, so restart Pi or reload extensions after editing `config.json`.
194
+
195
+ Not every terminal can transmit every key combination to Pi. Triple-modifier shortcuts such as `ctrl+shift+alt+v` may be intercepted by the OS, terminal, shell, SSH, or tmux before Pi can see them. If a configured shortcut does not work, try a simpler shortcut such as `ctrl+alt+v`, `ctrl+shift+v`, `alt+p`, `f8`, or another function key.
196
+
197
+ Keep `shortcuts.avoidBuiltinConflicts` set to `true` to skip configured paste-image shortcuts that overlap any effective Pi built-in shortcut. Keep `shortcuts.suppressBuiltinConflictWarnings` set to `true` when your goal is specifically to remove Pi's startup conflict warning noise. Both options use the same safe mechanism: `pi-image-tools` does not register the overlapping shortcut, so Pi has nothing to warn about. For example, `ctrl+p` is skipped because Pi uses it for built-in model/session actions.
198
+
199
+ If both options are `false`, Pi handles conflicts itself. Non-reserved built-in conflicts may be taken over by `pi-image-tools`, but Pi can still print a startup warning. Reserved built-in shortcuts cannot be stolen through the extension shortcut API; Pi skips those registrations before `pi-image-tools` can handle them. To use a reserved Pi shortcut, rebind or disable the relevant built-in action in `~/.pi/agent/keybindings.json`, then restart Pi or reload extensions.
200
+
201
+ Use an empty array to disable the extension's paste-image shortcuts while keeping `/paste-image` available:
202
+
203
+ ```json
204
+ {
205
+ "debug": false,
206
+ "shortcuts": {
207
+ "pasteImage": [],
208
+ "avoidBuiltinConflicts": true,
209
+ "suppressBuiltinConflictWarnings": true
210
+ }
211
+ }
212
+ ```
213
+
214
+ If `shortcuts.pasteImage` is omitted, `pi-image-tools` uses non-conflicting defaults and automatically restores the previous primary shortcut when Pi's built-in `app.clipboard.pasteImage` binding is disabled or rebound.
215
+
133
216
  ## Default recent-image search locations
134
217
 
135
218
  ### Windows
@@ -200,16 +283,16 @@ pi-image-tools/
200
283
  │ ├── index.ts # Extension bootstrap and message flow
201
284
  │ ├── commands.ts # /paste-image command registration
202
285
  │ ├── clipboard.ts # Cross-platform clipboard image reading
286
+ │ ├── config.ts # Runtime config loading and validation
287
+ │ ├── debug-logger.ts # File-based debug logging
203
288
  │ ├── recent-images.ts # Recent image discovery and cache management
204
289
  │ ├── image-preview.ts # Preview building and Sixel/native rendering
205
290
  │ ├── inline-user-preview.ts # Inline preview patching for user messages
206
291
  │ ├── keybindings.ts # Keyboard shortcut registration
207
292
  │ ├── temp-file.ts # Temporary file management and cleanup
208
293
  │ └── types.ts # Shared TypeScript types
209
- ├── config/
210
- └── config.example.json # Starter runtime config
211
- └── asset/
212
- └── pi-image-tools.png # README preview image
294
+ └── config/
295
+ └── config.example.json # Starter runtime config
213
296
  ```
214
297
 
215
298
  ## Troubleshooting
@@ -1,3 +1,8 @@
1
- {
2
- "enabled": true
3
- }
1
+ {
2
+ "debug": false,
3
+ "shortcuts": {
4
+ "pasteImage": ["ctrl+alt+v"],
5
+ "avoidBuiltinConflicts": true,
6
+ "suppressBuiltinConflictWarnings": true
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,67 +1,66 @@
1
- {
2
- "name": "pi-image-tools",
3
- "version": "1.0.10",
4
- "description": "Image attachment and rendering extension for Pi TUI",
5
- "type": "module",
6
- "main": "./index.ts",
7
- "exports": {
8
- ".": "./index.ts"
9
- },
10
- "files": [
11
- "index.ts",
12
- "src",
13
- "config/config.example.json",
14
- "asset",
15
- "README.md",
16
- "CHANGELOG.md",
17
- "LICENSE"
18
- ],
19
- "scripts": {
20
- "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
21
- "lint": "npm run build",
22
- "test": "node --test",
23
- "check": "npm run lint && npm run test"
24
- },
25
- "keywords": [
26
- "pi-package",
27
- "pi",
28
- "pi-extension",
29
- "pi-coding-agent",
30
- "pi-tui",
31
- "coding-agent",
32
- "image",
33
- "image-attachment",
34
- "image-preview",
35
- "clipboard",
36
- "preview",
37
- "windows"
38
- ],
39
- "author": "MasuRii",
40
- "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/MasuRii/pi-image-tools.git"
44
- },
45
- "homepage": "https://github.com/MasuRii/pi-image-tools#readme",
46
- "bugs": {
47
- "url": "https://github.com/MasuRii/pi-image-tools/issues"
48
- },
49
- "engines": {
50
- "node": ">=20"
51
- },
52
- "publishConfig": {
53
- "access": "public"
54
- },
55
- "pi": {
56
- "extensions": [
57
- "./index.ts"
58
- ]
59
- },
60
- "peerDependencies": {
61
- "@mariozechner/pi-coding-agent": "^0.68.1",
62
- "@mariozechner/pi-tui": "^0.68.1"
63
- },
64
- "optionalDependencies": {
65
- "@mariozechner/clipboard": "^0.3.2"
66
- }
67
- }
1
+ {
2
+ "name": "pi-image-tools",
3
+ "version": "1.0.11",
4
+ "description": "Image attachment and rendering extension for Pi TUI",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "config/config.example.json",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
20
+ "lint": "npm run build",
21
+ "test": "node --test",
22
+ "check": "npm run lint && npm run test"
23
+ },
24
+ "keywords": [
25
+ "pi-package",
26
+ "pi",
27
+ "pi-extension",
28
+ "pi-coding-agent",
29
+ "pi-tui",
30
+ "coding-agent",
31
+ "image",
32
+ "image-attachment",
33
+ "image-preview",
34
+ "clipboard",
35
+ "preview",
36
+ "windows"
37
+ ],
38
+ "author": "MasuRii",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/MasuRii/pi-image-tools.git"
43
+ },
44
+ "homepage": "https://github.com/MasuRii/pi-image-tools#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/MasuRii/pi-image-tools/issues"
47
+ },
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "pi": {
55
+ "extensions": [
56
+ "./index.ts"
57
+ ]
58
+ },
59
+ "peerDependencies": {
60
+ "@mariozechner/pi-coding-agent": "^0.70.2",
61
+ "@mariozechner/pi-tui": "^0.70.2"
62
+ },
63
+ "optionalDependencies": {
64
+ "@mariozechner/clipboard": "^0.3.3"
65
+ }
66
+ }
package/src/config.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import type { KeyId } from "@mariozechner/pi-tui";
6
+
7
+ const CONFIG_FILE_NAME = "config.json";
8
+ const EXTENSION_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
+
10
+ export interface ImageToolsShortcutConfig {
11
+ avoidBuiltinConflicts: boolean;
12
+ suppressBuiltinConflictWarnings: boolean;
13
+ pasteImage?: KeyId[];
14
+ }
15
+
16
+ export interface ImageToolsConfig {
17
+ debug: boolean;
18
+ shortcuts: ImageToolsShortcutConfig;
19
+ }
20
+
21
+ export function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ export function getExtensionRoot(): string {
26
+ return EXTENSION_ROOT;
27
+ }
28
+
29
+ export function getConfigPath(): string {
30
+ return join(getExtensionRoot(), CONFIG_FILE_NAME);
31
+ }
32
+
33
+ function formatConfigPath(path: string, property: string): string {
34
+ return `${path}${property.length > 0 ? ` property \"${property}\"` : ""}`;
35
+ }
36
+
37
+ function parseBoolean(value: unknown, property: string, path: string): boolean {
38
+ if (value === undefined) {
39
+ return false;
40
+ }
41
+
42
+ if (typeof value !== "boolean") {
43
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, property)}: expected a boolean.`);
44
+ }
45
+
46
+ return value;
47
+ }
48
+
49
+ function normalizeShortcut(value: unknown, property: string, path: string): KeyId {
50
+ if (typeof value !== "string") {
51
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, property)}: expected a string shortcut.`);
52
+ }
53
+
54
+ const shortcut = value.trim();
55
+ if (shortcut.length === 0) {
56
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, property)}: shortcut cannot be empty.`);
57
+ }
58
+
59
+ return shortcut as KeyId;
60
+ }
61
+
62
+ function parseShortcutList(value: unknown, property: string, path: string): KeyId[] | undefined {
63
+ if (value === undefined) {
64
+ return undefined;
65
+ }
66
+
67
+ const rawShortcuts = Array.isArray(value) ? value : [value];
68
+ const shortcuts: KeyId[] = [];
69
+ const seen = new Set<string>();
70
+
71
+ for (const [index, rawShortcut] of rawShortcuts.entries()) {
72
+ const shortcut = normalizeShortcut(rawShortcut, `${property}[${index}]`, path);
73
+ const normalized = shortcut.toLowerCase();
74
+ if (seen.has(normalized)) {
75
+ continue;
76
+ }
77
+
78
+ seen.add(normalized);
79
+ shortcuts.push(shortcut);
80
+ }
81
+
82
+ return shortcuts;
83
+ }
84
+
85
+ function parseShortcutConfig(value: unknown, path: string): ImageToolsShortcutConfig {
86
+ if (value === undefined) {
87
+ return {
88
+ avoidBuiltinConflicts: false,
89
+ suppressBuiltinConflictWarnings: false,
90
+ };
91
+ }
92
+
93
+ if (!isRecord(value)) {
94
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, "shortcuts")}: expected an object.`);
95
+ }
96
+
97
+ const avoidBuiltinConflicts = parseBoolean(
98
+ value.avoidBuiltinConflicts,
99
+ "shortcuts.avoidBuiltinConflicts",
100
+ path,
101
+ );
102
+ const suppressBuiltinConflictWarnings = parseBoolean(
103
+ value.suppressBuiltinConflictWarnings,
104
+ "shortcuts.suppressBuiltinConflictWarnings",
105
+ path,
106
+ );
107
+ const pasteImage = parseShortcutList(value.pasteImage, "shortcuts.pasteImage", path);
108
+
109
+ return pasteImage === undefined
110
+ ? { avoidBuiltinConflicts, suppressBuiltinConflictWarnings }
111
+ : { avoidBuiltinConflicts, suppressBuiltinConflictWarnings, pasteImage };
112
+ }
113
+
114
+ function parseConfig(rawConfig: unknown, path: string): ImageToolsConfig {
115
+ if (!isRecord(rawConfig)) {
116
+ throw new Error(`Invalid pi-image-tools config at ${path}: expected a JSON object.`);
117
+ }
118
+
119
+ return {
120
+ debug: parseBoolean(rawConfig.debug, "debug", path),
121
+ shortcuts: parseShortcutConfig(rawConfig.shortcuts, path),
122
+ };
123
+ }
124
+
125
+ export function loadImageToolsConfig(path = getConfigPath()): ImageToolsConfig {
126
+ if (!existsSync(path)) {
127
+ return {
128
+ debug: false,
129
+ shortcuts: {
130
+ avoidBuiltinConflicts: false,
131
+ suppressBuiltinConflictWarnings: false,
132
+ },
133
+ };
134
+ }
135
+
136
+ try {
137
+ const rawConfig = JSON.parse(readFileSync(path, "utf-8")) as unknown;
138
+ return parseConfig(rawConfig, path);
139
+ } catch (error) {
140
+ if (error instanceof SyntaxError) {
141
+ throw new Error(`Invalid pi-image-tools config at ${path}: ${error.message}`);
142
+ }
143
+
144
+ throw error;
145
+ }
146
+ }
@@ -0,0 +1,48 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getExtensionRoot, type ImageToolsConfig } from "./config.js";
5
+
6
+ const DEBUG_DIRECTORY_NAME = "debug";
7
+ const DEBUG_LOG_FILE_NAME = "debug.log";
8
+
9
+ type DebugFields = Record<string, unknown>;
10
+
11
+ function getErrorMessage(error: unknown): string {
12
+ if (error instanceof Error && error.message.trim().length > 0) {
13
+ return error.message;
14
+ }
15
+
16
+ return "Unknown error";
17
+ }
18
+
19
+ export class DebugLogger {
20
+ private readonly logPath: string | undefined;
21
+
22
+ private constructor(private readonly enabled: boolean) {
23
+ this.logPath = enabled ? join(getExtensionRoot(), DEBUG_DIRECTORY_NAME, DEBUG_LOG_FILE_NAME) : undefined;
24
+ }
25
+
26
+ static create(config: ImageToolsConfig): DebugLogger {
27
+ return new DebugLogger(config.debug);
28
+ }
29
+
30
+ log(event: string, fields: DebugFields = {}): void {
31
+ if (!this.enabled || !this.logPath) {
32
+ return;
33
+ }
34
+
35
+ const debugDirectory = join(getExtensionRoot(), DEBUG_DIRECTORY_NAME);
36
+
37
+ try {
38
+ mkdirSync(debugDirectory, { recursive: true });
39
+ appendFileSync(
40
+ this.logPath,
41
+ `${JSON.stringify({ timestamp: new Date().toISOString(), event, ...fields })}\n`,
42
+ "utf-8",
43
+ );
44
+ } catch (error) {
45
+ throw new Error(`pi-image-tools debug logging failed at ${this.logPath}: ${getErrorMessage(error)}`);
46
+ }
47
+ }
48
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
 
3
3
  import { readClipboardImage } from "./clipboard.js";
4
4
  import { registerPasteImageCommand } from "./commands.js";
5
+ import { loadImageToolsConfig } from "./config.js";
6
+ import { DebugLogger } from "./debug-logger.js";
5
7
  import {
6
8
  IMAGE_PREVIEW_CUSTOM_TYPE,
7
9
  buildPreviewItems,
@@ -144,8 +146,15 @@ function showRecentSelectionPreview(
144
146
  }
145
147
 
146
148
  export default function imageToolsExtension(pi: ExtensionAPI): void {
149
+ const config = loadImageToolsConfig();
150
+ const logger = DebugLogger.create(config);
147
151
  const pendingImages: PendingImage[] = [];
148
152
 
153
+ logger.log("extension.initialize", {
154
+ debug: config.debug,
155
+ pasteImageShortcutsConfigured: config.shortcuts.pasteImage !== undefined,
156
+ });
157
+
149
158
  registerInlineUserImagePreview(pi);
150
159
  registerImagePreviewDisplay(pi);
151
160
 
@@ -249,7 +258,7 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
249
258
  };
250
259
  });
251
260
 
252
- registerImagePasteKeybindings(pi, pasteImageFromClipboard);
261
+ registerImagePasteKeybindings(pi, pasteImageFromClipboard, { config, logger });
253
262
  registerPasteImageCommand(pi, {
254
263
  fromClipboard: pasteImageFromClipboard,
255
264
  fromRecent: pasteImageFromRecent,
@@ -1,9 +1,222 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import type { KeyId } from "@mariozechner/pi-tui";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
3
 
4
+ import { getAgentDir, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import { TUI_KEYBINDINGS, type KeyId, type KeybindingsConfig } from "@mariozechner/pi-tui";
6
+
7
+ import { isRecord, type ImageToolsConfig } from "./config.js";
8
+ import type { DebugLogger } from "./debug-logger.js";
4
9
  import type { PasteImageHandler } from "./types.js";
5
10
 
6
- function getImagePasteShortcuts(platform: NodeJS.Platform = process.platform): KeyId[] {
11
+ type KeybindingDefaults = Record<string, KeyId | KeyId[]>;
12
+
13
+ const LEGACY_KEYBINDING_NAME_MIGRATIONS: Record<string, string> = {
14
+ cursorUp: "tui.editor.cursorUp",
15
+ cursorDown: "tui.editor.cursorDown",
16
+ cursorLeft: "tui.editor.cursorLeft",
17
+ cursorRight: "tui.editor.cursorRight",
18
+ cursorWordLeft: "tui.editor.cursorWordLeft",
19
+ cursorWordRight: "tui.editor.cursorWordRight",
20
+ cursorLineStart: "tui.editor.cursorLineStart",
21
+ cursorLineEnd: "tui.editor.cursorLineEnd",
22
+ jumpForward: "tui.editor.jumpForward",
23
+ jumpBackward: "tui.editor.jumpBackward",
24
+ pageUp: "tui.editor.pageUp",
25
+ pageDown: "tui.editor.pageDown",
26
+ deleteCharBackward: "tui.editor.deleteCharBackward",
27
+ deleteCharForward: "tui.editor.deleteCharForward",
28
+ deleteWordBackward: "tui.editor.deleteWordBackward",
29
+ deleteWordForward: "tui.editor.deleteWordForward",
30
+ deleteToLineStart: "tui.editor.deleteToLineStart",
31
+ deleteToLineEnd: "tui.editor.deleteToLineEnd",
32
+ yank: "tui.editor.yank",
33
+ yankPop: "tui.editor.yankPop",
34
+ undo: "tui.editor.undo",
35
+ newLine: "tui.input.newLine",
36
+ submit: "tui.input.submit",
37
+ tab: "tui.input.tab",
38
+ copy: "tui.input.copy",
39
+ selectUp: "tui.select.up",
40
+ selectDown: "tui.select.down",
41
+ selectPageUp: "tui.select.pageUp",
42
+ selectPageDown: "tui.select.pageDown",
43
+ selectConfirm: "tui.select.confirm",
44
+ selectCancel: "tui.select.cancel",
45
+ interrupt: "app.interrupt",
46
+ clear: "app.clear",
47
+ exit: "app.exit",
48
+ suspend: "app.suspend",
49
+ cycleThinkingLevel: "app.thinking.cycle",
50
+ cycleModelForward: "app.model.cycleForward",
51
+ cycleModelBackward: "app.model.cycleBackward",
52
+ selectModel: "app.model.select",
53
+ expandTools: "app.tools.expand",
54
+ toggleThinking: "app.thinking.toggle",
55
+ toggleSessionNamedFilter: "app.session.toggleNamedFilter",
56
+ externalEditor: "app.editor.external",
57
+ followUp: "app.message.followUp",
58
+ dequeue: "app.message.dequeue",
59
+ pasteImage: "app.clipboard.pasteImage",
60
+ newSession: "app.session.new",
61
+ tree: "app.session.tree",
62
+ fork: "app.session.fork",
63
+ resume: "app.session.resume",
64
+ treeFoldOrUp: "app.tree.foldOrUp",
65
+ treeUnfoldOrDown: "app.tree.unfoldOrDown",
66
+ treeEditLabel: "app.tree.editLabel",
67
+ treeToggleLabelTimestamp: "app.tree.toggleLabelTimestamp",
68
+ toggleSessionPath: "app.session.togglePath",
69
+ toggleSessionSort: "app.session.toggleSort",
70
+ renameSession: "app.session.rename",
71
+ deleteSession: "app.session.delete",
72
+ deleteSessionNoninvasive: "app.session.deleteNoninvasive",
73
+ };
74
+
75
+ const APP_KEYBINDING_DEFAULTS: KeybindingDefaults = {
76
+ "app.interrupt": "escape",
77
+ "app.clear": "ctrl+c",
78
+ "app.exit": "ctrl+d",
79
+ "app.suspend": process.platform === "win32" ? [] : "ctrl+z",
80
+ "app.thinking.cycle": "shift+tab",
81
+ "app.model.cycleForward": "ctrl+p",
82
+ "app.model.cycleBackward": "shift+ctrl+p",
83
+ "app.model.select": "ctrl+l",
84
+ "app.tools.expand": "ctrl+o",
85
+ "app.thinking.toggle": "ctrl+t",
86
+ "app.session.toggleNamedFilter": "ctrl+n",
87
+ "app.editor.external": "ctrl+g",
88
+ "app.message.followUp": "alt+enter",
89
+ "app.message.dequeue": "alt+up",
90
+ "app.clipboard.pasteImage": process.platform === "win32" ? "alt+v" : "ctrl+v",
91
+ "app.session.new": [],
92
+ "app.session.tree": [],
93
+ "app.session.fork": [],
94
+ "app.session.resume": [],
95
+ "app.tree.foldOrUp": ["ctrl+left", "alt+left"],
96
+ "app.tree.unfoldOrDown": ["ctrl+right", "alt+right"],
97
+ "app.tree.editLabel": "shift+l",
98
+ "app.tree.toggleLabelTimestamp": "shift+t",
99
+ "app.session.togglePath": "ctrl+p",
100
+ "app.session.toggleSort": "ctrl+s",
101
+ "app.session.rename": "ctrl+r",
102
+ "app.session.delete": "ctrl+d",
103
+ "app.session.deleteNoninvasive": "ctrl+backspace",
104
+ "app.models.save": "ctrl+s",
105
+ "app.models.enableAll": "ctrl+a",
106
+ "app.models.clearAll": "ctrl+x",
107
+ "app.models.toggleProvider": "ctrl+p",
108
+ "app.models.reorderUp": "alt+up",
109
+ "app.models.reorderDown": "alt+down",
110
+ "app.tree.filter.default": "ctrl+d",
111
+ "app.tree.filter.noTools": "ctrl+t",
112
+ "app.tree.filter.userOnly": "ctrl+u",
113
+ "app.tree.filter.labeledOnly": "ctrl+l",
114
+ "app.tree.filter.all": "ctrl+a",
115
+ "app.tree.filter.cycleForward": "ctrl+o",
116
+ "app.tree.filter.cycleBackward": "shift+ctrl+o",
117
+ };
118
+
119
+ export interface RegisterImagePasteKeybindingsOptions {
120
+ config: ImageToolsConfig;
121
+ logger: DebugLogger;
122
+ }
123
+
124
+ function toShortcutList(shortcuts: KeybindingsConfig[string]): KeyId[] {
125
+ if (shortcuts === undefined) {
126
+ return [];
127
+ }
128
+
129
+ return Array.isArray(shortcuts) ? shortcuts : [shortcuts];
130
+ }
131
+
132
+ function normalizeKeybinding(value: unknown): KeybindingsConfig[string] | undefined {
133
+ if (typeof value === "string") {
134
+ return value as KeyId;
135
+ }
136
+
137
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
138
+ return value as KeyId[];
139
+ }
140
+
141
+ return undefined;
142
+ }
143
+
144
+ function normalizeShortcutKey(shortcut: KeyId): string {
145
+ return shortcut.toLowerCase();
146
+ }
147
+
148
+ function readKeybindingsConfig(): Record<string, unknown> | undefined {
149
+ const keybindingsPath = join(getAgentDir(), "keybindings.json");
150
+ if (!existsSync(keybindingsPath)) {
151
+ return undefined;
152
+ }
153
+
154
+ try {
155
+ const parsed = JSON.parse(readFileSync(keybindingsPath, "utf-8")) as unknown;
156
+ return isRecord(parsed) ? parsed : undefined;
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ function migrateKeybindingName(key: string): string {
163
+ return LEGACY_KEYBINDING_NAME_MIGRATIONS[key] ?? key;
164
+ }
165
+
166
+ function normalizeUserKeybindings(rawConfig: Record<string, unknown> | undefined): KeybindingsConfig {
167
+ if (!rawConfig) {
168
+ return {};
169
+ }
170
+
171
+ const userKeybindings: KeybindingsConfig = {};
172
+ for (const [key, value] of Object.entries(rawConfig)) {
173
+ const normalizedValue = normalizeKeybinding(value);
174
+ if (normalizedValue === undefined) {
175
+ continue;
176
+ }
177
+
178
+ const migratedKey = migrateKeybindingName(key);
179
+ if (migratedKey !== key && Object.hasOwn(rawConfig, migratedKey)) {
180
+ continue;
181
+ }
182
+
183
+ userKeybindings[migratedKey] = normalizedValue;
184
+ }
185
+
186
+ return userKeybindings;
187
+ }
188
+
189
+ function getBuiltinKeybindingDefaults(): KeybindingDefaults {
190
+ const tuiDefaults: KeybindingDefaults = {};
191
+ for (const [keybinding, definition] of Object.entries(TUI_KEYBINDINGS)) {
192
+ tuiDefaults[keybinding] = definition.defaultKeys;
193
+ }
194
+
195
+ return {
196
+ ...tuiDefaults,
197
+ ...APP_KEYBINDING_DEFAULTS,
198
+ };
199
+ }
200
+
201
+ function getConfiguredBuiltinShortcuts(): Set<string> {
202
+ const defaults = getBuiltinKeybindingDefaults();
203
+ const userKeybindings = normalizeUserKeybindings(readKeybindingsConfig());
204
+ const builtinShortcuts = new Set<string>();
205
+
206
+ for (const [keybinding, defaultShortcuts] of Object.entries(defaults)) {
207
+ const configuredShortcuts = Object.hasOwn(userKeybindings, keybinding)
208
+ ? userKeybindings[keybinding]
209
+ : defaultShortcuts;
210
+
211
+ for (const shortcut of toShortcutList(configuredShortcuts)) {
212
+ builtinShortcuts.add(normalizeShortcutKey(shortcut));
213
+ }
214
+ }
215
+
216
+ return builtinShortcuts;
217
+ }
218
+
219
+ function getImagePasteShortcutCandidates(platform: NodeJS.Platform): KeyId[] {
7
220
  if (platform === "win32") {
8
221
  return ["alt+v", "ctrl+alt+v"];
9
222
  }
@@ -11,8 +224,48 @@ function getImagePasteShortcuts(platform: NodeJS.Platform = process.platform): K
11
224
  return ["ctrl+v", "alt+v", "ctrl+alt+v"];
12
225
  }
13
226
 
14
- export function registerImagePasteKeybindings(pi: ExtensionAPI, handler: PasteImageHandler): void {
15
- for (const shortcut of getImagePasteShortcuts()) {
227
+ function removeBuiltinConflicts(shortcuts: readonly KeyId[]): KeyId[] {
228
+ const builtinShortcuts = getConfiguredBuiltinShortcuts();
229
+
230
+ return shortcuts.filter((shortcut) => !builtinShortcuts.has(normalizeShortcutKey(shortcut)));
231
+ }
232
+
233
+ function getImagePasteShortcuts(
234
+ config: ImageToolsConfig,
235
+ platform: NodeJS.Platform = process.platform,
236
+ ): KeyId[] {
237
+ const shouldAvoidBuiltinConflicts =
238
+ config.shortcuts.avoidBuiltinConflicts || config.shortcuts.suppressBuiltinConflictWarnings;
239
+
240
+ if (config.shortcuts.pasteImage !== undefined) {
241
+ return shouldAvoidBuiltinConflicts
242
+ ? removeBuiltinConflicts(config.shortcuts.pasteImage)
243
+ : config.shortcuts.pasteImage;
244
+ }
245
+
246
+ return removeBuiltinConflicts(getImagePasteShortcutCandidates(platform));
247
+ }
248
+
249
+ export function registerImagePasteKeybindings(
250
+ pi: ExtensionAPI,
251
+ handler: PasteImageHandler,
252
+ options: RegisterImagePasteKeybindingsOptions,
253
+ ): void {
254
+ const configuredShortcuts = options.config.shortcuts.pasteImage;
255
+ const shortcuts = getImagePasteShortcuts(options.config);
256
+ const skippedShortcuts = configuredShortcuts?.filter(
257
+ (shortcut) => !shortcuts.some((registeredShortcut) => normalizeShortcutKey(registeredShortcut) === normalizeShortcutKey(shortcut)),
258
+ ) ?? [];
259
+
260
+ options.logger.log("keybindings.register", {
261
+ avoidBuiltinConflicts: options.config.shortcuts.avoidBuiltinConflicts,
262
+ suppressBuiltinConflictWarnings: options.config.shortcuts.suppressBuiltinConflictWarnings,
263
+ configured: configuredShortcuts !== undefined,
264
+ registeredShortcuts: shortcuts,
265
+ skippedShortcuts,
266
+ });
267
+
268
+ for (const shortcut of shortcuts) {
16
269
  pi.registerShortcut(shortcut, {
17
270
  description: "Attach clipboard image to draft (send when ready)",
18
271
  handler,
@@ -1,57 +1,57 @@
1
- const SIXEL_IMAGE_LINE_MARKER = "\x1b_Gm=0;\x1b\\";
2
- const KITTY_IMAGE_LINE_MARKER = "\x1b_G";
3
- const ITERM_IMAGE_LINE_MARKER = "\x1b]1337;File=";
4
- const SIXEL_DCS_PREFIX = "\x1bP";
5
- const STRING_TERMINATOR = "\x1b\\";
6
- const MAX_IMAGE_ROWS = 80;
7
-
8
- function sanitizeRows(rows: number): number {
9
- return Math.max(1, Math.min(Math.trunc(rows), MAX_IMAGE_ROWS));
10
- }
11
-
12
- function normalizeSixelOutput(value: string): string {
13
- return value.replace(/\r?\n/g, "").replace(/\s+$/g, "");
14
- }
15
-
16
- /**
17
- * Ensure the PowerShell Sixel output is emitted as a complete DCS sequence.
18
- * Some converters return only the sixel payload body; terminals need the
19
- * enclosing ESC P ... ESC \\ wrapper to render it as an image.
20
- */
21
- export function ensureCompleteSixelSequence(sequence: string): string {
22
- let normalized = normalizeSixelOutput(sequence);
23
- if (normalized.length === 0) {
24
- return "";
25
- }
26
-
27
- if (!normalized.startsWith(SIXEL_DCS_PREFIX)) {
28
- normalized = `${SIXEL_DCS_PREFIX}${normalized.startsWith("q") ? normalized : `q${normalized}`}`;
29
- }
30
-
31
- if (!normalized.endsWith(STRING_TERMINATOR)) {
32
- normalized = `${normalized}${STRING_TERMINATOR}`;
33
- }
34
-
35
- return normalized;
36
- }
37
-
38
- export function buildSixelRenderLines(sequence: string, rows: number): string[] {
39
- const safeRows = sanitizeRows(rows);
40
- const completeSequence = ensureCompleteSixelSequence(sequence);
41
- if (completeSequence.length === 0) {
42
- return [];
43
- }
44
-
45
- const lines = Array.from({ length: Math.max(0, safeRows - 1) }, () => "");
46
- const moveUp = safeRows > 1 ? `\x1b[${safeRows - 1}A` : "";
47
- return [...lines, `${SIXEL_IMAGE_LINE_MARKER}${moveUp}${completeSequence}`];
48
- }
49
-
50
- export function isInlineImageProtocolLine(line: string): boolean {
51
- return (
52
- line.includes(SIXEL_IMAGE_LINE_MARKER) ||
53
- line.includes(KITTY_IMAGE_LINE_MARKER) ||
54
- line.includes(ITERM_IMAGE_LINE_MARKER) ||
55
- line.includes(SIXEL_DCS_PREFIX)
56
- );
57
- }
1
+ const SIXEL_IMAGE_LINE_MARKER = "\x1b_Gm=0;\x1b\\";
2
+ const KITTY_IMAGE_LINE_MARKER = "\x1b_G";
3
+ const ITERM_IMAGE_LINE_MARKER = "\x1b]1337;File=";
4
+ const SIXEL_DCS_PREFIX = "\x1bP";
5
+ const STRING_TERMINATOR = "\x1b\\";
6
+ const MAX_IMAGE_ROWS = 80;
7
+
8
+ function sanitizeRows(rows: number): number {
9
+ return Math.max(1, Math.min(Math.trunc(rows), MAX_IMAGE_ROWS));
10
+ }
11
+
12
+ function normalizeSixelOutput(value: string): string {
13
+ return value.replace(/\r?\n/g, "").replace(/\s+$/g, "");
14
+ }
15
+
16
+ /**
17
+ * Ensure the PowerShell Sixel output is emitted as a complete DCS sequence.
18
+ * Some converters return only the sixel payload body; terminals need the
19
+ * enclosing ESC P ... ESC \\ wrapper to render it as an image.
20
+ */
21
+ export function ensureCompleteSixelSequence(sequence: string): string {
22
+ let normalized = normalizeSixelOutput(sequence);
23
+ if (normalized.length === 0) {
24
+ return "";
25
+ }
26
+
27
+ if (!normalized.startsWith(SIXEL_DCS_PREFIX)) {
28
+ normalized = `${SIXEL_DCS_PREFIX}${normalized.startsWith("q") ? normalized : `q${normalized}`}`;
29
+ }
30
+
31
+ if (!normalized.endsWith(STRING_TERMINATOR)) {
32
+ normalized = `${normalized}${STRING_TERMINATOR}`;
33
+ }
34
+
35
+ return normalized;
36
+ }
37
+
38
+ export function buildSixelRenderLines(sequence: string, rows: number): string[] {
39
+ const safeRows = sanitizeRows(rows);
40
+ const completeSequence = ensureCompleteSixelSequence(sequence);
41
+ if (completeSequence.length === 0) {
42
+ return [];
43
+ }
44
+
45
+ const lines = Array.from({ length: Math.max(0, safeRows - 1) }, () => "");
46
+ const moveUp = safeRows > 1 ? `\x1b[${safeRows - 1}A` : "";
47
+ return [...lines, `${SIXEL_IMAGE_LINE_MARKER}${moveUp}${completeSequence}`];
48
+ }
49
+
50
+ export function isInlineImageProtocolLine(line: string): boolean {
51
+ return (
52
+ line.includes(SIXEL_IMAGE_LINE_MARKER) ||
53
+ line.includes(KITTY_IMAGE_LINE_MARKER) ||
54
+ line.includes(ITERM_IMAGE_LINE_MARKER) ||
55
+ line.includes(SIXEL_DCS_PREFIX)
56
+ );
57
+ }
@@ -1,90 +1,90 @@
1
- import { SettingsManager, getAgentDir } from "@mariozechner/pi-coding-agent";
2
- import { resolve } from "node:path";
3
-
4
- export const DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS = 60;
5
-
6
- export interface TerminalImageWidthOptions {
7
- cwd?: string;
8
- agentDir?: string;
9
- }
10
-
11
- interface SettingsWithOptionalTerminalWidth {
12
- terminal?: {
13
- imageWidthCells?: unknown;
14
- };
15
- }
16
-
17
- type SettingsManagerWithOptionalWidthGetter = SettingsManager & {
18
- getImageWidthCells?: () => number;
19
- };
20
-
21
- let activeTerminalSettingsCwd = process.cwd();
22
-
23
- function normalizeDirectoryPath(value: string | undefined): string | undefined {
24
- if (typeof value !== "string") {
25
- return undefined;
26
- }
27
-
28
- const trimmed = value.trim();
29
- if (!trimmed) {
30
- return undefined;
31
- }
32
-
33
- return resolve(trimmed);
34
- }
35
-
36
- function normalizeImageWidthCells(value: unknown): number {
37
- if (typeof value !== "number" || !Number.isFinite(value)) {
38
- return DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS;
39
- }
40
-
41
- return Math.max(1, Math.floor(value));
42
- }
43
-
44
- function readRawImageWidthCells(settings: unknown): unknown {
45
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
46
- return undefined;
47
- }
48
-
49
- return (settings as SettingsWithOptionalTerminalWidth).terminal?.imageWidthCells;
50
- }
51
-
52
- function resolveSettingsManagerImageWidthCells(settingsManager: SettingsManager): unknown {
53
- const settingsManagerWithOptionalWidthGetter =
54
- settingsManager as SettingsManagerWithOptionalWidthGetter;
55
-
56
- if (typeof settingsManagerWithOptionalWidthGetter.getImageWidthCells === "function") {
57
- return settingsManagerWithOptionalWidthGetter.getImageWidthCells();
58
- }
59
-
60
- const projectWidth = readRawImageWidthCells(settingsManager.getProjectSettings());
61
- if (projectWidth !== undefined) {
62
- return projectWidth;
63
- }
64
-
65
- return readRawImageWidthCells(settingsManager.getGlobalSettings());
66
- }
67
-
68
- export function setActiveTerminalImageSettingsCwd(cwd: string | undefined): void {
69
- const normalized = normalizeDirectoryPath(cwd);
70
- if (normalized) {
71
- activeTerminalSettingsCwd = normalized;
72
- }
73
- }
74
-
75
- export function resolveTerminalImageWidthCells(
76
- options: TerminalImageWidthOptions = {},
77
- ): number {
78
- const cwd = normalizeDirectoryPath(options.cwd) ?? activeTerminalSettingsCwd;
79
-
80
- try {
81
- const settingsManager = SettingsManager.create(
82
- cwd,
83
- normalizeDirectoryPath(options.agentDir) ?? getAgentDir(),
84
- );
85
-
86
- return normalizeImageWidthCells(resolveSettingsManagerImageWidthCells(settingsManager));
87
- } catch {
88
- return DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS;
89
- }
90
- }
1
+ import { SettingsManager, getAgentDir } from "@mariozechner/pi-coding-agent";
2
+ import { resolve } from "node:path";
3
+
4
+ export const DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS = 60;
5
+
6
+ export interface TerminalImageWidthOptions {
7
+ cwd?: string;
8
+ agentDir?: string;
9
+ }
10
+
11
+ interface SettingsWithOptionalTerminalWidth {
12
+ terminal?: {
13
+ imageWidthCells?: unknown;
14
+ };
15
+ }
16
+
17
+ type SettingsManagerWithOptionalWidthGetter = SettingsManager & {
18
+ getImageWidthCells?: () => number;
19
+ };
20
+
21
+ let activeTerminalSettingsCwd = process.cwd();
22
+
23
+ function normalizeDirectoryPath(value: string | undefined): string | undefined {
24
+ if (typeof value !== "string") {
25
+ return undefined;
26
+ }
27
+
28
+ const trimmed = value.trim();
29
+ if (!trimmed) {
30
+ return undefined;
31
+ }
32
+
33
+ return resolve(trimmed);
34
+ }
35
+
36
+ function normalizeImageWidthCells(value: unknown): number {
37
+ if (typeof value !== "number" || !Number.isFinite(value)) {
38
+ return DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS;
39
+ }
40
+
41
+ return Math.max(1, Math.floor(value));
42
+ }
43
+
44
+ function readRawImageWidthCells(settings: unknown): unknown {
45
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
46
+ return undefined;
47
+ }
48
+
49
+ return (settings as SettingsWithOptionalTerminalWidth).terminal?.imageWidthCells;
50
+ }
51
+
52
+ function resolveSettingsManagerImageWidthCells(settingsManager: SettingsManager): unknown {
53
+ const settingsManagerWithOptionalWidthGetter =
54
+ settingsManager as SettingsManagerWithOptionalWidthGetter;
55
+
56
+ if (typeof settingsManagerWithOptionalWidthGetter.getImageWidthCells === "function") {
57
+ return settingsManagerWithOptionalWidthGetter.getImageWidthCells();
58
+ }
59
+
60
+ const projectWidth = readRawImageWidthCells(settingsManager.getProjectSettings());
61
+ if (projectWidth !== undefined) {
62
+ return projectWidth;
63
+ }
64
+
65
+ return readRawImageWidthCells(settingsManager.getGlobalSettings());
66
+ }
67
+
68
+ export function setActiveTerminalImageSettingsCwd(cwd: string | undefined): void {
69
+ const normalized = normalizeDirectoryPath(cwd);
70
+ if (normalized) {
71
+ activeTerminalSettingsCwd = normalized;
72
+ }
73
+ }
74
+
75
+ export function resolveTerminalImageWidthCells(
76
+ options: TerminalImageWidthOptions = {},
77
+ ): number {
78
+ const cwd = normalizeDirectoryPath(options.cwd) ?? activeTerminalSettingsCwd;
79
+
80
+ try {
81
+ const settingsManager = SettingsManager.create(
82
+ cwd,
83
+ normalizeDirectoryPath(options.agentDir) ?? getAgentDir(),
84
+ );
85
+
86
+ return normalizeImageWidthCells(resolveSettingsManagerImageWidthCells(settingsManager));
87
+ } catch {
88
+ return DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS;
89
+ }
90
+ }
Binary file