@oh-my-pi/pi-tui 13.2.0 → 13.3.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/README.md +11 -11
- package/package.json +3 -3
- package/src/autocomplete.ts +1 -1
- package/src/components/editor.ts +1 -1
- package/src/components/image.ts +4 -1
- package/src/components/tab-bar.ts +45 -12
- package/src/terminal-capabilities.ts +37 -6
- package/src/tui.ts +2 -1
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ Container that applies padding and background color to all children.
|
|
|
94
94
|
const box = new Box(
|
|
95
95
|
1, // paddingX (default: 1)
|
|
96
96
|
1, // paddingY (default: 1)
|
|
97
|
-
(text) => chalk.bgGray(text) // optional background function
|
|
97
|
+
(text) => chalk.bgGray(text), // optional background function
|
|
98
98
|
);
|
|
99
99
|
box.addChild(new Text("Content"));
|
|
100
100
|
box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically
|
|
@@ -109,7 +109,7 @@ const text = new Text(
|
|
|
109
109
|
"Hello World", // text content
|
|
110
110
|
1, // paddingX (default: 1)
|
|
111
111
|
1, // paddingY (default: 1)
|
|
112
|
-
(text) => chalk.bgGray(text) // optional background function
|
|
112
|
+
(text) => chalk.bgGray(text), // optional background function
|
|
113
113
|
);
|
|
114
114
|
text.setText("Updated text");
|
|
115
115
|
text.setCustomBgFn((text) => chalk.bgBlue(text));
|
|
@@ -123,7 +123,7 @@ Single-line text that truncates to fit viewport width. Useful for status lines a
|
|
|
123
123
|
const truncated = new TruncatedText(
|
|
124
124
|
"This is a very long line that will be truncated...",
|
|
125
125
|
0, // paddingX (default: 0)
|
|
126
|
-
0 // paddingY (default: 0)
|
|
126
|
+
0, // paddingY (default: 0)
|
|
127
127
|
);
|
|
128
128
|
```
|
|
129
129
|
|
|
@@ -269,7 +269,7 @@ const md = new Markdown(
|
|
|
269
269
|
1, // paddingY
|
|
270
270
|
theme, // MarkdownTheme
|
|
271
271
|
defaultStyle, // optional DefaultTextStyle
|
|
272
|
-
2 // optional code block indent (spaces)
|
|
272
|
+
2, // optional code block indent (spaces)
|
|
273
273
|
);
|
|
274
274
|
md.setText("Updated markdown");
|
|
275
275
|
```
|
|
@@ -291,7 +291,7 @@ const loader = new Loader(
|
|
|
291
291
|
tui, // TUI instance for render updates
|
|
292
292
|
(s) => chalk.cyan(s), // spinner color function
|
|
293
293
|
(s) => chalk.gray(s), // message color function
|
|
294
|
-
"Loading..." // message (default: "Loading...")
|
|
294
|
+
"Loading...", // message (default: "Loading...")
|
|
295
295
|
);
|
|
296
296
|
loader.start();
|
|
297
297
|
loader.setMessage("Still loading...");
|
|
@@ -307,7 +307,7 @@ const loader = new CancellableLoader(
|
|
|
307
307
|
tui, // TUI instance for render updates
|
|
308
308
|
(s) => chalk.cyan(s), // spinner color function
|
|
309
309
|
(s) => chalk.gray(s), // message color function
|
|
310
|
-
"Working..." // message
|
|
310
|
+
"Working...", // message
|
|
311
311
|
);
|
|
312
312
|
loader.onAbort = () => done(null); // Called when user presses Escape
|
|
313
313
|
doAsyncWork(loader.signal).then(done);
|
|
@@ -345,7 +345,7 @@ const list = new SelectList(
|
|
|
345
345
|
{ value: "opt2", label: "Option 2", description: "Second option" },
|
|
346
346
|
],
|
|
347
347
|
5, // maxVisible
|
|
348
|
-
theme // SelectListTheme
|
|
348
|
+
theme, // SelectListTheme
|
|
349
349
|
);
|
|
350
350
|
|
|
351
351
|
list.onSelect = (item) => console.log("Selected:", item);
|
|
@@ -390,7 +390,7 @@ const settings = new SettingsList(
|
|
|
390
390
|
10, // maxVisible
|
|
391
391
|
theme, // SettingsListTheme
|
|
392
392
|
(id, newValue) => console.log(`${id} changed to ${newValue}`),
|
|
393
|
-
() => console.log("Cancelled")
|
|
393
|
+
() => console.log("Cancelled"),
|
|
394
394
|
);
|
|
395
395
|
settings.updateValue("theme", "light");
|
|
396
396
|
```
|
|
@@ -428,7 +428,7 @@ const image = new Image(
|
|
|
428
428
|
base64Data, // base64-encoded image data
|
|
429
429
|
"image/png", // MIME type
|
|
430
430
|
theme, // ImageTheme
|
|
431
|
-
options // optional ImageOptions
|
|
431
|
+
options, // optional ImageOptions
|
|
432
432
|
);
|
|
433
433
|
tui.addChild(image);
|
|
434
434
|
```
|
|
@@ -443,7 +443,7 @@ Supports both slash commands and file paths.
|
|
|
443
443
|
|
|
444
444
|
```typescript
|
|
445
445
|
import { CombinedAutocompleteProvider } from "@oh-my-pi/pi-tui";
|
|
446
|
-
import { getProjectDir } from "@oh-my-pi/pi-utils
|
|
446
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
447
447
|
|
|
448
448
|
const provider = new CombinedAutocompleteProvider(
|
|
449
449
|
[
|
|
@@ -451,7 +451,7 @@ const provider = new CombinedAutocompleteProvider(
|
|
|
451
451
|
{ name: "clear", description: "Clear screen" },
|
|
452
452
|
{ name: "delete", description: "Delete last message" },
|
|
453
453
|
],
|
|
454
|
-
getProjectDir() // base path for file completion
|
|
454
|
+
getProjectDir(), // base path for file completion
|
|
455
455
|
);
|
|
456
456
|
|
|
457
457
|
editor.setAutocompleteProvider(provider);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.3.0",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"test": "bun test test/*.test.ts"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@oh-my-pi/pi-natives": "13.
|
|
37
|
-
"@oh-my-pi/pi-utils": "13.
|
|
36
|
+
"@oh-my-pi/pi-natives": "13.3.0",
|
|
37
|
+
"@oh-my-pi/pi-utils": "13.3.0",
|
|
38
38
|
"@types/mime-types": "^3.0",
|
|
39
39
|
"chalk": "^5.6",
|
|
40
40
|
"marked": "^17.0",
|
package/src/autocomplete.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fuzzyFind } from "@oh-my-pi/pi-natives";
|
|
5
|
-
import { getProjectDir } from "@oh-my-pi/pi-utils
|
|
5
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
6
6
|
|
|
7
7
|
const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
8
8
|
|
package/src/components/editor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getProjectDir } from "@oh-my-pi/pi-utils
|
|
1
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete";
|
|
3
3
|
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
4
4
|
import { type EditorKeybindingsManager, getEditorKeybindings } from "../keybindings";
|
package/src/components/image.ts
CHANGED
|
@@ -56,7 +56,10 @@ export class Image implements Component {
|
|
|
56
56
|
let lines: string[];
|
|
57
57
|
|
|
58
58
|
if (TERMINAL.imageProtocol) {
|
|
59
|
-
const result = renderImage(this.#base64Data, this.#dimensions, {
|
|
59
|
+
const result = renderImage(this.#base64Data, this.#dimensions, {
|
|
60
|
+
maxWidthCells: maxWidth,
|
|
61
|
+
maxHeightCells: this.#options.maxHeightCells,
|
|
62
|
+
});
|
|
60
63
|
|
|
61
64
|
if (result) {
|
|
62
65
|
// Return `rows` lines so TUI accounts for image height
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { matchesKey } from "../keys";
|
|
12
12
|
import type { Component } from "../tui";
|
|
13
|
-
import {
|
|
13
|
+
import { truncateToWidth, visibleWidth } from "../utils";
|
|
14
14
|
|
|
15
15
|
/** Tab definition */
|
|
16
16
|
export interface Tab {
|
|
@@ -112,31 +112,64 @@ export class TabBar implements Component {
|
|
|
112
112
|
|
|
113
113
|
/** Render the tab bar, wrapping to multiple lines if needed */
|
|
114
114
|
render(width: number): string[] {
|
|
115
|
-
const
|
|
115
|
+
const maxWidth = Math.max(1, width);
|
|
116
|
+
const chunks: string[] = [];
|
|
116
117
|
|
|
117
118
|
// Label prefix
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
chunks.push(this.#theme.label(`${this.#label}:`));
|
|
120
|
+
chunks.push(" ");
|
|
120
121
|
|
|
121
122
|
// Tab buttons
|
|
122
123
|
for (let i = 0; i < this.#tabs.length; i++) {
|
|
123
124
|
const tab = this.#tabs[i];
|
|
124
125
|
if (i === this.#activeIndex) {
|
|
125
|
-
|
|
126
|
+
chunks.push(this.#theme.activeTab(` ${tab.label} `));
|
|
126
127
|
} else {
|
|
127
|
-
|
|
128
|
+
chunks.push(this.#theme.inactiveTab(` ${tab.label} `));
|
|
128
129
|
}
|
|
129
130
|
if (i < this.#tabs.length - 1) {
|
|
130
|
-
|
|
131
|
+
chunks.push(" ");
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
// Navigation hint
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
chunks.push(" ");
|
|
137
|
+
chunks.push(this.#theme.hint("(tab to cycle)"));
|
|
137
138
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
const lines: string[] = [];
|
|
140
|
+
let currentLine = "";
|
|
141
|
+
let currentWidth = 0;
|
|
142
|
+
|
|
143
|
+
for (const chunk of chunks) {
|
|
144
|
+
const chunkWidth = visibleWidth(chunk);
|
|
145
|
+
if (chunkWidth <= 0) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (chunkWidth > maxWidth) {
|
|
150
|
+
if (currentLine) {
|
|
151
|
+
lines.push(currentLine);
|
|
152
|
+
currentLine = "";
|
|
153
|
+
currentWidth = 0;
|
|
154
|
+
}
|
|
155
|
+
lines.push(truncateToWidth(chunk, maxWidth));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (currentWidth > 0 && currentWidth + chunkWidth > maxWidth) {
|
|
160
|
+
lines.push(currentLine);
|
|
161
|
+
currentLine = "";
|
|
162
|
+
currentWidth = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
currentLine += chunk;
|
|
166
|
+
currentWidth += chunkWidth;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (currentLine) {
|
|
170
|
+
lines.push(currentLine);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return lines.length > 0 ? lines : [""];
|
|
141
174
|
}
|
|
142
175
|
}
|
|
@@ -214,6 +214,35 @@ export function calculateImageRows(
|
|
|
214
214
|
return Math.max(1, rows);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
function calculateImageFit(
|
|
218
|
+
imageDimensions: ImageDimensions,
|
|
219
|
+
options: ImageRenderOptions,
|
|
220
|
+
cellDims: CellDimensions,
|
|
221
|
+
): { columns: number; rows: number } {
|
|
222
|
+
const maxColumns = options.maxWidthCells !== undefined ? Math.max(1, Math.floor(options.maxWidthCells)) : undefined;
|
|
223
|
+
const maxRows = options.maxHeightCells !== undefined ? Math.max(1, Math.floor(options.maxHeightCells)) : undefined;
|
|
224
|
+
|
|
225
|
+
if (maxColumns === undefined && maxRows === undefined) {
|
|
226
|
+
const columns = Math.max(1, Math.ceil(imageDimensions.widthPx / cellDims.widthPx));
|
|
227
|
+
const rows = Math.max(1, Math.ceil(imageDimensions.heightPx / cellDims.heightPx));
|
|
228
|
+
return { columns, rows };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const maxWidthPx = maxColumns !== undefined ? maxColumns * cellDims.widthPx : Number.POSITIVE_INFINITY;
|
|
232
|
+
const maxHeightPx = maxRows !== undefined ? maxRows * cellDims.heightPx : Number.POSITIVE_INFINITY;
|
|
233
|
+
const scale = Math.min(maxWidthPx / imageDimensions.widthPx, maxHeightPx / imageDimensions.heightPx);
|
|
234
|
+
const fittedWidthPx = imageDimensions.widthPx * scale;
|
|
235
|
+
const fittedHeightPx = imageDimensions.heightPx * scale;
|
|
236
|
+
|
|
237
|
+
const columns = Math.max(1, Math.floor(fittedWidthPx / cellDims.widthPx));
|
|
238
|
+
const rows = Math.max(1, Math.ceil(fittedHeightPx / cellDims.heightPx));
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
columns: maxColumns !== undefined ? Math.min(columns, maxColumns) : columns,
|
|
242
|
+
rows: maxRows !== undefined ? Math.min(rows, maxRows) : rows,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
217
246
|
export function getPngDimensions(base64Data: string): ImageDimensions | null {
|
|
218
247
|
try {
|
|
219
248
|
const buffer = Buffer.from(base64Data, "base64");
|
|
@@ -364,21 +393,23 @@ export function renderImage(
|
|
|
364
393
|
return null;
|
|
365
394
|
}
|
|
366
395
|
|
|
367
|
-
const
|
|
368
|
-
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
|
|
396
|
+
const fit = calculateImageFit(imageDimensions, options, getCellDimensions());
|
|
369
397
|
|
|
370
398
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
371
|
-
const sequence = encodeKitty(base64Data, {
|
|
372
|
-
|
|
399
|
+
const sequence = encodeKitty(base64Data, {
|
|
400
|
+
columns: fit.columns,
|
|
401
|
+
rows: fit.rows,
|
|
402
|
+
});
|
|
403
|
+
return { sequence, rows: fit.rows };
|
|
373
404
|
}
|
|
374
405
|
|
|
375
406
|
if (TERMINAL.imageProtocol === ImageProtocol.Iterm2) {
|
|
376
407
|
const sequence = encodeITerm2(base64Data, {
|
|
377
|
-
width:
|
|
408
|
+
width: fit.columns,
|
|
378
409
|
height: "auto",
|
|
379
410
|
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
|
380
411
|
});
|
|
381
|
-
return { sequence, rows };
|
|
412
|
+
return { sequence, rows: fit.rows };
|
|
382
413
|
}
|
|
383
414
|
|
|
384
415
|
return null;
|
package/src/tui.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import { getCrashLogPath, getDebugLogPath } from "@oh-my-pi/pi-utils
|
|
6
|
+
import { getCrashLogPath, getDebugLogPath } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { isKeyRelease, matchesKey } from "./keys";
|
|
8
8
|
import type { Terminal } from "./terminal";
|
|
9
9
|
import { setCellDimensions, TERMINAL } from "./terminal-capabilities";
|
|
@@ -188,6 +188,7 @@ export class Container implements Component {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
render(width: number): string[] {
|
|
191
|
+
width = Math.max(1, width);
|
|
191
192
|
const lines: string[] = [];
|
|
192
193
|
for (const child of this.children) {
|
|
193
194
|
lines.push(...child.render(width));
|