@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 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/dirs";
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.2.0",
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.2.0",
37
- "@oh-my-pi/pi-utils": "13.2.0",
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",
@@ -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/dirs";
5
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
6
6
 
7
7
  const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
8
8
 
@@ -1,4 +1,4 @@
1
- import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
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";
@@ -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, { maxWidthCells: maxWidth });
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 { wrapTextWithAnsi } from "../utils";
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 parts: string[] = [];
115
+ const maxWidth = Math.max(1, width);
116
+ const chunks: string[] = [];
116
117
 
117
118
  // Label prefix
118
- parts.push(this.#theme.label(`${this.#label}:`));
119
- parts.push(" ");
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
- parts.push(this.#theme.activeTab(` ${tab.label} `));
126
+ chunks.push(this.#theme.activeTab(` ${tab.label} `));
126
127
  } else {
127
- parts.push(this.#theme.inactiveTab(` ${tab.label} `));
128
+ chunks.push(this.#theme.inactiveTab(` ${tab.label} `));
128
129
  }
129
130
  if (i < this.#tabs.length - 1) {
130
- parts.push(" ");
131
+ chunks.push(" ");
131
132
  }
132
133
  }
133
134
 
134
135
  // Navigation hint
135
- parts.push(" ");
136
- parts.push(this.#theme.hint("(tab to cycle)"));
136
+ chunks.push(" ");
137
+ chunks.push(this.#theme.hint("(tab to cycle)"));
137
138
 
138
- const line = parts.join("");
139
- const maxWidth = Math.max(1, width);
140
- return wrapTextWithAnsi(line, maxWidth);
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 maxWidth = options.maxWidthCells ?? 80;
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, { columns: maxWidth, rows });
372
- return { sequence, rows };
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: maxWidth,
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/dirs";
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));