@alchemy.run/sigil 0.0.0-alpha.5 → 0.0.0-alpha.7

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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/dist/{Text-D5HUf3Fj.d.ts → Text-DV9CuzAT.d.ts} +3 -3
  3. package/dist/ansi.d.ts +1 -1
  4. package/dist/ansi.js +2 -2
  5. package/dist/capabilities.d.ts +4 -4
  6. package/dist/capabilities.js +2 -2
  7. package/dist/{color-policy-BMzMwV7Q.d.ts → color-policy-CCxuHIdD.d.ts} +2 -2
  8. package/dist/{color-policy-SVj1pYTA.js → color-policy-DlrZXC0f.js} +31 -13
  9. package/dist/{color-profile-u0Nhe9Nv.d.ts → color-profile-CyeHnG1T.d.ts} +1 -1
  10. package/dist/color.d.ts +2 -2
  11. package/dist/{detect-BuTXtY6e.js → detect-B3dL4Q11.js} +3 -2
  12. package/dist/{detect-Bh4yGP6w.d.ts → detect-Db6GbKOm.d.ts} +9 -0
  13. package/dist/{index-Bmc2tRPk.d.ts → index-D48vQhhe.d.ts} +2 -2
  14. package/dist/index.d.ts +10 -6
  15. package/dist/index.js +85 -72
  16. package/dist/{osc-CCH7xDoS.js → osc-BFKKSqpg.js} +1 -1
  17. package/dist/{paint-C19minOS.d.ts → paint-Cx-zC_sX.d.ts} +2 -2
  18. package/dist/{query-vaIeGOkH.d.ts → query-BNc2B8GD.d.ts} +1 -1
  19. package/dist/router.d.ts +1 -1
  20. package/dist/router.js +1 -1
  21. package/dist/{screen-BReKIheE.d.ts → screen-CgC2WlVM.d.ts} +12 -3
  22. package/dist/{screen-BOh__-65.js → screen-CiPytswf.js} +30 -12
  23. package/dist/screen.d.ts +3 -3
  24. package/dist/screen.js +1 -1
  25. package/dist/{session-BJmzX2NJ.js → session-Cg6STjFV.js} +45 -9
  26. package/dist/{store-CgrG9K4y.d.ts → store-C1P5fOUi.d.ts} +1 -1
  27. package/dist/terminal.d.ts +8 -5
  28. package/dist/terminal.js +1 -1
  29. package/dist/{use-focus-C2sciZOo.js → use-focus-DSV01Ulb.js} +3 -1
  30. package/package.json +1 -1
  31. package/src/capabilities/detect.ts +15 -1
  32. package/src/capabilities/store.ts +17 -2
  33. package/src/components/Text.tsx +1 -1
  34. package/src/dom.ts +6 -0
  35. package/src/hooks/use-window-size.ts +8 -19
  36. package/src/ink.tsx +93 -23
  37. package/src/paint-tree.ts +7 -3
  38. package/src/screen/canvas.ts +49 -26
  39. package/src/screen/screen.ts +45 -12
  40. package/src/structured-text.ts +4 -0
  41. package/src/styles.ts +7 -1
  42. package/src/terminal/screen-presenter.ts +62 -8
  43. package/src/terminal/session.ts +3 -2
  44. package/src/testing/terminal.ts +15 -3
  45. package/src/wrap-text.ts +4 -0
  46. package/src/utils.ts +0 -40
@@ -1,4 +1,4 @@
1
- import { i as Cell, o as CellPatch } from "./color-profile-u0Nhe9Nv.js";
1
+ import { i as Cell, o as CellPatch } from "./color-profile-CyeHnG1T.js";
2
2
  import { n as Rect } from "./geometry-BxXOzJgo.js";
3
3
  //#region src/screen/screen.d.ts
4
4
  type Line = readonly Cell[];
@@ -16,17 +16,26 @@ declare class Screen {
16
16
  #private;
17
17
  constructor(width: number, height: number);
18
18
  get width(): number;
19
+ /**
20
+ The populated length of one row: the nominal width, or more when overflow
21
+ writes (`textWrap: "none"` content) extended it past the screen's width.
22
+ */
23
+ rowLength(y: number): number;
19
24
  get height(): number;
20
25
  bounds(): Rect;
21
26
  cellAt(x: number, y: number): Cell | undefined;
22
27
  /** Whether a drawable explicitly touched this position. */
23
28
  isPainted(x: number, y: number): boolean;
24
- setCell(x: number, y: number, cell: Cell): void;
29
+ setCell(x: number, y: number, cell: Cell, options?: {
30
+ overflow?: boolean;
31
+ }): void;
25
32
  /**
26
33
  Composes independent cell channels over the existing cell. An undefined
27
34
  patch is transparent; an explicit space in `content` paints a blank.
28
35
  */
29
- composeCell(x: number, y: number, patch: CellPatch | undefined): void;
36
+ composeCell(x: number, y: number, patch: CellPatch | undefined, options?: {
37
+ overflow?: boolean;
38
+ }): void;
30
39
  /** Composes a patch throughout a rectangle, clipped to this screen. */
31
40
  fill(bounds: Rect, patch: CellPatch | undefined): void;
32
41
  /** Resizes while preserving complete graphemes in the shared top-left area. */
@@ -155,6 +155,13 @@ var Screen = class {
155
155
  get width() {
156
156
  return this.#width;
157
157
  }
158
+ /**
159
+ The populated length of one row: the nominal width, or more when overflow
160
+ writes (`textWrap: "none"` content) extended it past the screen's width.
161
+ */
162
+ rowLength(y) {
163
+ return Math.max(this.#width, this.#rows[y]?.length ?? 0);
164
+ }
158
165
  get height() {
159
166
  return this.#height;
160
167
  }
@@ -173,17 +180,20 @@ var Screen = class {
173
180
  isPainted(x, y) {
174
181
  return this.#painted[y]?.[x] ?? false;
175
182
  }
176
- setCell(x, y, cell) {
183
+ setCell(x, y, cell, options) {
177
184
  const row = this.#rows[y];
178
- if (!row || x < 0 || x >= this.width) return;
185
+ const overflow = options?.overflow === true;
186
+ if (!row || x < 0 || !overflow && x >= this.width) return;
179
187
  if (!Number.isInteger(cell.width) || cell.width < 1) throw new Error("Only leading cells with a positive width may be written");
180
188
  const finalColumn = x + cell.width;
181
- const clipped = finalColumn > this.width;
182
- const overwriteEnd = Math.min(finalColumn, this.width);
189
+ if (overflow && finalColumn > row.length) this.#growRow(row, y, finalColumn);
190
+ const capacity = overflow ? row.length : this.width;
191
+ const clipped = finalColumn > capacity;
192
+ const overwriteEnd = Math.min(finalColumn, capacity);
183
193
  for (let column = x; column < overwriteEnd; column++) this.#clearGraphemeAt(row, y, column);
184
194
  if (clipped) {
185
- for (let column = x; column < this.width; column++) row[column] = emptyCell;
186
- this.#markDirty(y, x, this.width);
195
+ for (let column = x; column < capacity; column++) row[column] = emptyCell;
196
+ this.#markDirty(y, x, capacity);
187
197
  return;
188
198
  }
189
199
  row[x] = cell;
@@ -203,10 +213,10 @@ var Screen = class {
203
213
  Composes independent cell channels over the existing cell. An undefined
204
214
  patch is transparent; an explicit space in `content` paints a blank.
205
215
  */
206
- composeCell(x, y, patch) {
216
+ composeCell(x, y, patch, options) {
207
217
  if (!patch) return;
208
218
  const row = this.#rows[y];
209
- if (!row || x < 0 || x >= this.width) return;
219
+ if (!row || x < 0 || options?.overflow !== true && x >= this.width) return;
210
220
  let leadingColumn = x;
211
221
  while (leadingColumn > 0 && row[leadingColumn]?.width === 0) leadingColumn--;
212
222
  const destination = row[leadingColumn] ?? emptyCell;
@@ -220,7 +230,7 @@ var Screen = class {
220
230
  };
221
231
  const content = patch.content ?? destination;
222
232
  const hyperlink = patch.hyperlink === void 0 ? destination.hyperlink : patch.hyperlink ?? void 0;
223
- this.setCell(writeColumn, y, createCell(content.grapheme, content.width, style, hyperlink));
233
+ this.setCell(writeColumn, y, createCell(content.grapheme, content.width, style, hyperlink), options);
224
234
  }
225
235
  /** Composes a patch throughout a rectangle, clipped to this screen. */
226
236
  fill(bounds, patch) {
@@ -265,7 +275,7 @@ var Screen = class {
265
275
  for (const [y, row] of this.#rows.entries()) {
266
276
  row.fill(emptyCell);
267
277
  this.#painted[y].fill(true);
268
- this.#markDirty(y, 0, this.width);
278
+ this.#markDirty(y, 0, this.rowLength(y));
269
279
  }
270
280
  }
271
281
  toRows() {
@@ -282,6 +292,14 @@ var Screen = class {
282
292
  this.#dirty.fill(void 0);
283
293
  return spans;
284
294
  }
295
+ /** Extends a row (and its paint mask) so overflow writes land past `width`. */
296
+ #growRow(row, y, length) {
297
+ const painted = this.#painted[y];
298
+ while (row.length < length) {
299
+ row.push(emptyCell);
300
+ painted.push(false);
301
+ }
302
+ }
285
303
  #clearGraphemeAt(row, y, x) {
286
304
  const existing = row[x];
287
305
  if (!existing || existing === emptyCell) return;
@@ -289,7 +307,7 @@ var Screen = class {
289
307
  while (leadingColumn > 0 && row[leadingColumn]?.width === 0) leadingColumn--;
290
308
  const leadingCell = row[leadingColumn];
291
309
  const width = Math.max(1, leadingCell?.width ?? 1);
292
- for (let column = leadingColumn; column < Math.min(leadingColumn + width, this.width); column++) {
310
+ for (let column = leadingColumn; column < Math.min(leadingColumn + width, row.length); column++) {
293
311
  row[column] = emptyCell;
294
312
  this.#painted[y][column] = true;
295
313
  }
@@ -306,7 +324,7 @@ var Screen = class {
306
324
  #markDirty(y, start, end) {
307
325
  if (y < 0 || y >= this.height || start >= end) return;
308
326
  const clippedStart = Math.max(0, start);
309
- const clippedEnd = Math.min(this.width, end);
327
+ const clippedEnd = Math.min(this.rowLength(y), end);
310
328
  const current = this.#dirty[y];
311
329
  this.#dirty[y] = current ? {
312
330
  start: Math.min(current.start, clippedStart),
package/dist/screen.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as emptyCellStyle, a as CellContent, c as Color, d as RgbColor, f as UnderlineStyle, g as emptyCell, h as createCell, i as Cell, l as Hyperlink, m as cellsEqual, n as colorProfileFromLevel, o as CellPatch, p as cellAttributes, r as quantizeColor, s as CellStyle, t as ColorProfile, u as IndexedColor } from "./color-profile-u0Nhe9Nv.js";
1
+ import { _ as emptyCellStyle, a as CellContent, c as Color, d as RgbColor, f as UnderlineStyle, g as emptyCell, h as createCell, i as Cell, l as Hyperlink, m as cellsEqual, n as colorProfileFromLevel, o as CellPatch, p as cellAttributes, r as quantizeColor, s as CellStyle, t as ColorProfile, u as IndexedColor } from "./color-profile-CyeHnG1T.js";
2
2
  import { n as Rect, t as Point } from "./geometry-BxXOzJgo.js";
3
- import { a as cellFromStyledChar, i as styledCharFromCell, n as serializeLine, o as cellsFromAnsi, r as serializeScreen, t as SerializeOptions } from "./index-Bmc2tRPk.js";
4
- import { n as Screen, t as Line } from "./screen-BReKIheE.js";
3
+ import { a as cellFromStyledChar, i as styledCharFromCell, n as serializeLine, o as cellsFromAnsi, r as serializeScreen, t as SerializeOptions } from "./index-D48vQhhe.js";
4
+ import { n as Screen, t as Line } from "./screen-CgC2WlVM.js";
5
5
  export { Cell, CellContent, CellPatch, CellStyle, Color, ColorProfile, Hyperlink, IndexedColor, type Line, Point, Rect, RgbColor, Screen, SerializeOptions, UnderlineStyle, cellAttributes, cellFromStyledChar, cellsEqual, cellsFromAnsi, colorProfileFromLevel, createCell, emptyCell, emptyCellStyle, quantizeColor, serializeLine, serializeScreen, styledCharFromCell };
package/dist/screen.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as emptyCellStyle, i as emptyCell, n as cellsEqual, r as createCell, t as cellAttributes } from "./cell-_ZVhbfl0.js";
2
- import { n as cellFromStyledChar, r as cellsFromAnsi, t as Screen } from "./screen-BOh__-65.js";
2
+ import { n as cellFromStyledChar, r as cellsFromAnsi, t as Screen } from "./screen-CiPytswf.js";
3
3
  import { n as quantizeColor, t as colorProfileFromLevel } from "./color-profile-DHhQHY55.js";
4
4
  import { n as serializeScreen, r as styledCharFromCell, t as serializeLine } from "./serialize-BTkAZgw1.js";
5
5
  export { Screen, cellAttributes, cellFromStyledChar, cellsEqual, cellsFromAnsi, colorProfileFromLevel, createCell, emptyCell, emptyCellStyle, quantizeColor, serializeLine, serializeScreen, styledCharFromCell };
@@ -1,7 +1,7 @@
1
1
  import { G as pasteEnd, K as pasteStart, S as ansiEscapes } from "./sgr-BhwaWAJB.js";
2
- import { a as setPointerShape, c as setWorkingDirectory, i as setClipboard, l as tmuxPassthrough, n as notify, o as setTerminalProgress, s as setWindowTitle, u as cliCursor } from "./osc-CCH7xDoS.js";
3
- import { i as getCapabilities, t as colorState } from "./color-policy-SVj1pYTA.js";
4
- import { n as cellsEqual } from "./cell-_ZVhbfl0.js";
2
+ import { a as setPointerShape, c as setWorkingDirectory, i as setClipboard, l as tmuxPassthrough, n as notify, o as setTerminalProgress, s as setWindowTitle, u as cliCursor } from "./osc-BFKKSqpg.js";
3
+ import { i as getCapabilities, t as colorState } from "./color-policy-DlrZXC0f.js";
4
+ import { n as cellsEqual, t as cellAttributes } from "./cell-_ZVhbfl0.js";
5
5
  import { n as serializeScreen, t as serializeLine } from "./serialize-BTkAZgw1.js";
6
6
  //#region src/cursor-position.ts
7
7
  const showCursorEscape = ansiEscapes.cursorShow;
@@ -322,13 +322,29 @@ var ScreenPresenter = class {
322
322
  willPresent(screen, cursor, fullscreen = false, forceRewrite = false) {
323
323
  return forceRewrite || this.#screen === void 0 || !screensEqual(this.#screen, screen) || cursorPositionChanged(cursor, this.#cursor) || fullscreen !== this.#fullscreen;
324
324
  }
325
- clear() {
326
- this.#write(buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) + ansiEscapes.eraseLines(this.#lineCount));
325
+ /**
326
+ Erases the presented frame from the cursor upward.
327
+
328
+ `columns` is the terminal's current width. When it is narrower than the width
329
+ the frame was painted at, a reflowing emulator (xterm.js, Ghostty, kitty,
330
+ iTerm2, WezTerm, tmux, …) has already rewrapped every wider row onto several
331
+ physical rows before the resize event reaches us, so the erase has to cover
332
+ that physical footprint. Erasing only the logical row count leaves the frame's
333
+ top rows behind as ghosts, one more batch per resize event.
334
+ */
335
+ clear(options = {}) {
336
+ this.#write(buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) + ansiEscapes.eraseLines(this.#physicalLineCount(options.columns)));
327
337
  this.#lineCount = 0;
328
338
  this.#cursor = void 0;
329
339
  this.#cursorWasShown = false;
330
340
  this.#fullscreen = false;
331
341
  }
342
+ #physicalLineCount(columns) {
343
+ if (this.#screen === void 0 || this.#lineCount === 0 || columns === void 0 || columns < 1) return this.#lineCount;
344
+ let rows = 0;
345
+ for (const line of this.#screen.toRows()) rows += Math.max(1, Math.ceil(contentWidth(line) / columns));
346
+ return rows + (this.#fullscreen ? 0 : 1);
347
+ }
332
348
  reset() {
333
349
  this.#screen = void 0;
334
350
  this.#lineCount = 0;
@@ -353,17 +369,36 @@ var ScreenPresenter = class {
353
369
  this.#fullscreen = fullscreen;
354
370
  }
355
371
  };
372
+ /**
373
+ The columns a row occupies once written: everything up to its last visible
374
+ cell. Trailing unstyled blanks are trimmed by the serializer and never reach
375
+ the terminal, while styled blanks (a highlighted tab's padding) do.
376
+ */
377
+ function contentWidth(line) {
378
+ let width = 0;
379
+ let end = 0;
380
+ for (const cell of line) {
381
+ width += cell.width;
382
+ if (!isBlank(cell)) end = width;
383
+ }
384
+ return end;
385
+ }
386
+ function isBlank(cell) {
387
+ const { style } = cell;
388
+ return (cell.grapheme === " " || cell.width === 0) && style.foreground === void 0 && style.background === void 0 && style.underlineColor === void 0 && style.underline === "none" && style.attributes === cellAttributes.none && cell.hyperlink === void 0;
389
+ }
356
390
  function findFirstChangedRow(previous, next) {
357
391
  const height = Math.max(previous.height, next.height);
358
392
  for (let y = 0; y < height; y++) if (!rowsEqual(previous, next, y)) return y;
359
393
  }
360
394
  function rowsEqual(left, right, y) {
361
- for (let x = 0; x < left.width; x++) if (!cellsEqual(left.cellAt(x, y), right.cellAt(x, y))) return false;
395
+ const length = Math.max(left.rowLength(y), right.rowLength(y));
396
+ for (let x = 0; x < length; x++) if (!cellsEqual(left.cellAt(x, y), right.cellAt(x, y))) return false;
362
397
  return true;
363
398
  }
364
399
  function screensEqual(left, right) {
365
400
  if (left.width !== right.width || left.height !== right.height) return false;
366
- for (let y = 0; y < left.height; y++) for (let x = 0; x < left.width; x++) if (!cellsEqual(left.cellAt(x, y), right.cellAt(x, y))) return false;
401
+ for (let y = 0; y < left.height; y++) if (!rowsEqual(left, right, y)) return false;
367
402
  return true;
368
403
  }
369
404
  //#endregion
@@ -436,8 +471,9 @@ var TerminalSession = class {
436
471
  willPresent(screen, options = {}) {
437
472
  return this.#presenter.willPresent(screen, this.cursor.position, options.fullscreen, options.forceRewrite);
438
473
  }
439
- clearFrame() {
440
- this.#presenter.clear();
474
+ /** Erases the presented frame; pass the current `columns` after a resize so the erase covers rows the emulator rewrapped. */
475
+ clearFrame(options = {}) {
476
+ this.#presenter.clear(options);
441
477
  }
442
478
  resetFrame() {
443
479
  this.#presenter.reset();
@@ -1,4 +1,4 @@
1
- import { t as Capabilities } from "./detect-Bh4yGP6w.js";
1
+ import { t as Capabilities } from "./detect-Db6GbKOm.js";
2
2
  //#region src/stream.d.ts
3
3
  type OutputStream = NodeJS.WritableStream & {
4
4
  isTTY?: boolean;
@@ -1,9 +1,9 @@
1
1
  import { a as CursorShape } from "./escapes-CB_6CWOE.js";
2
2
  import { n as TerminalProgressState, t as ClipboardSelection } from "./osc-Cn0fw77g.js";
3
- import { i as OutputStream, t as CapabilitiesStore } from "./store-CgrG9K4y.js";
4
- import { t as ColorProfile } from "./color-profile-u0Nhe9Nv.js";
5
- import { t as ColorPolicy } from "./color-policy-BMzMwV7Q.js";
6
- import { n as Screen } from "./screen-BReKIheE.js";
3
+ import { i as OutputStream, t as CapabilitiesStore } from "./store-C1P5fOUi.js";
4
+ import { t as ColorProfile } from "./color-profile-CyeHnG1T.js";
5
+ import { t as ColorPolicy } from "./color-policy-CCxuHIdD.js";
6
+ import { n as Screen } from "./screen-CgC2WlVM.js";
7
7
  import { t as CursorPosition } from "./cursor-position-D2LAkRG0.js";
8
8
  //#region src/input-parser.d.ts
9
9
  type InputEvent = string | {
@@ -77,7 +77,10 @@ declare class TerminalSession {
77
77
  readonly fullscreen?: boolean;
78
78
  readonly forceRewrite?: boolean;
79
79
  }): boolean;
80
- clearFrame(): void;
80
+ /** Erases the presented frame; pass the current `columns` after a resize so the erase covers rows the emulator rewrapped. */
81
+ clearFrame(options?: {
82
+ readonly columns?: number;
83
+ }): void;
81
84
  resetFrame(): void;
82
85
  finishFrame(): void;
83
86
  setColorPolicy(policy: ColorPolicy): void;
package/dist/terminal.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as TerminalInput, r as parseMouseEvent, t as TerminalSession } from "./session-BJmzX2NJ.js";
1
+ import { n as TerminalInput, r as parseMouseEvent, t as TerminalSession } from "./session-Cg6STjFV.js";
2
2
  export { TerminalInput, TerminalSession, parseMouseEvent };
@@ -278,6 +278,7 @@ function transformAnsiLine(serializedSubtree, lineIndex, transformers) {
278
278
  //#region src/wrap-text.ts
279
279
  const wrapTextCache = new QuickLru({ maxSize: 4096 });
280
280
  const wrapText = (text, maxWidth, wrapType) => {
281
+ if (wrapType === "none") return text;
281
282
  const cacheKey = text + String(maxWidth) + String(wrapType);
282
283
  const cachedText = wrapTextCache.get(cacheKey);
283
284
  if (cachedText !== void 0) return cachedText;
@@ -386,6 +387,7 @@ const measureTextNode = function(node, width) {
386
387
  if (dimensions.width <= width) return dimensions;
387
388
  if (dimensions.width >= 1 && width > 0 && width < 1) return dimensions;
388
389
  const textWrap = node.style?.textWrap ?? "wrap";
390
+ if (textWrap === "none") return dimensions;
389
391
  const wrappedText = wrapText(text, width, textWrap);
390
392
  return measureText(wrappedText);
391
393
  };
@@ -562,7 +564,7 @@ const detectKittySupport = (stdin, stdout, onSupported) => {
562
564
  //#endregion
563
565
  //#region package.json
564
566
  var name = "@alchemy.run/sigil";
565
- var version = "0.0.0-alpha.5";
567
+ var version = "0.0.0-alpha.7";
566
568
  //#endregion
567
569
  //#region src/reconciler.ts
568
570
  if (isSigilDev) await import("./devtools-DbthxoD1.js").catch(() => {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alchemy.run/sigil",
3
- "version": "0.0.0-alpha.5",
3
+ "version": "0.0.0-alpha.7",
4
4
  "description": "React for CLIs. A self-contained fork of Ink with an integrated TypeScript Yoga layout engine.",
5
5
  "keywords": [
6
6
  "cli",
@@ -85,8 +85,21 @@ export type Capabilities = {
85
85
  /**
86
86
  Current terminal dimensions in cells, plus pixel geometry once the
87
87
  terminal has answered the query.
88
+
89
+ `source` tells where the cell dimensions come from. `"pty"` is the
90
+ stream's own `columns`/`rows`, updated by the OS on SIGWINCH — which says
91
+ nothing about whether the emulator has finished rewrapping its screen.
92
+ `"terminal"` is the emulator's in-band size report (mode 2048), which
93
+ arrives in the input stream after the rewrap and so describes the screen
94
+ exactly as later output will find it. While the terminal reports, its
95
+ size wins over the stream's.
88
96
  */
89
- size: { columns: number; rows: number; pixels: PixelGeometry | undefined };
97
+ size: {
98
+ columns: number;
99
+ rows: number;
100
+ pixels: PixelGeometry | undefined;
101
+ source: "pty" | "terminal";
102
+ };
90
103
 
91
104
  platform: NodeJS.Platform;
92
105
 
@@ -580,6 +593,7 @@ export function detectCapabilities({ stdout = process.stdout }: DetectOptions =
580
593
  ? { columns: stdout.columns, rows: stdout.rows }
581
594
  : terminalSize()),
582
595
  pixels: undefined,
596
+ source: "pty",
583
597
  },
584
598
  focused: undefined,
585
599
  theme: {
@@ -128,11 +128,17 @@ const createStore = (stdin: StoreStdin, stdout: StoreStdout): CapabilitiesStore
128
128
  let resizeSubscribers = 0;
129
129
 
130
130
  let focused: boolean | undefined;
131
+ // The size from the terminal's latest in-band report (mode 2048). While
132
+ // set it is authoritative: the report is ordered in the input stream after
133
+ // the emulator rewrapped, whereas the stream's `columns`/`rows` only say
134
+ // what the OS told the PTY, before or after the emulator caught up.
135
+ let reportedSize: { columns: number; rows: number } | undefined;
131
136
  let snapshot: Capabilities | undefined;
132
137
  let snapshotQuery: TerminalQueryResult | undefined;
133
138
  let snapshotColumns: number | undefined;
134
139
  let snapshotRows: number | undefined;
135
140
  let snapshotFocused: boolean | undefined;
141
+ let snapshotReportedSize: typeof reportedSize;
136
142
 
137
143
  const current = (): Capabilities => {
138
144
  const query = getTerminalQuery(stdout);
@@ -142,15 +148,20 @@ const createStore = (stdin: StoreStdin, stdout: StoreStdout): CapabilitiesStore
142
148
  query !== snapshotQuery ||
143
149
  columns !== snapshotColumns ||
144
150
  rows !== snapshotRows ||
145
- focused !== snapshotFocused
151
+ focused !== snapshotFocused ||
152
+ reportedSize !== snapshotReportedSize
146
153
  ) {
147
154
  const detected = detectCapabilities({ stdout });
148
155
  const applied = query ? applyTerminalQuery(detected, query) : detected;
149
- snapshot = focused === undefined ? applied : { ...applied, focused };
156
+ const sized = reportedSize
157
+ ? { ...applied, size: { ...applied.size, ...reportedSize, source: "terminal" as const } }
158
+ : applied;
159
+ snapshot = focused === undefined ? sized : { ...sized, focused };
150
160
  snapshotQuery = query;
151
161
  snapshotColumns = columns;
152
162
  snapshotRows = rows;
153
163
  snapshotFocused = focused;
164
+ snapshotReportedSize = reportedSize;
154
165
  }
155
166
 
156
167
  return snapshot;
@@ -217,6 +228,9 @@ const createStore = (stdin: StoreStdin, stdout: StoreStdout): CapabilitiesStore
217
228
  integration?.setReportFeed?.(false);
218
229
  removeExitHandler?.();
219
230
  removeExitHandler = undefined;
231
+ // No further reports will arrive; the stream is the only size source
232
+ // again and the last report would go stale on the next resize.
233
+ reportedSize = undefined;
220
234
  }
221
235
  };
222
236
 
@@ -255,6 +269,7 @@ const createStore = (stdin: StoreStdin, stdout: StoreStdout): CapabilitiesStore
255
269
  if (resize) {
256
270
  const [, rows, columns, pixelHeight, pixelWidth] = resize.map(Number);
257
271
  if (columns && rows) {
272
+ reportedSize = { columns, rows };
258
273
  patchTerminalQuery(stdout, {
259
274
  textAreaPixels:
260
275
  pixelWidth && pixelHeight ? { width: pixelWidth, height: pixelHeight } : undefined,
@@ -59,7 +59,7 @@ export type Props = {
59
59
  readonly inverse?: boolean;
60
60
 
61
61
  /**
62
- This property tells Ink to wrap or truncate text if its width is larger than the container. If `wrap` is passed (the default), Ink will wrap text and split it into multiple lines. If `hard` is passed, Ink will fill each line to the full column width, breaking words as necessary. If `truncate-*` is passed, Ink will truncate text instead, resulting in one line of text with the rest cut off.
62
+ This property tells Ink to wrap or truncate text if its width is larger than the container. If `wrap` is passed (the default), Ink will wrap text and split it into multiple lines. If `hard` is passed, Ink will fill each line to the full column width, breaking words as necessary. If `truncate-*` is passed, Ink will truncate text instead, resulting in one line of text with the rest cut off. If `none` is passed, the text is neither wrapped nor truncated — it overflows the container (and the screen's nominal width), which is intended for log lines committed to scrollback (e.g. inside `<Static>`), not for live regions.
63
63
  */
64
64
  readonly wrap?: Styles["textWrap"];
65
65
 
package/src/dom.ts CHANGED
@@ -243,6 +243,12 @@ const measureTextNode = function (node: DOMNode, width: number): { width: number
243
243
  }
244
244
 
245
245
  const textWrap = node.style?.textWrap ?? "wrap";
246
+
247
+ // `none` keeps the intrinsic dimensions and overflows the container.
248
+ if (textWrap === "none") {
249
+ return dimensions;
250
+ }
251
+
246
252
  const wrappedText = wrapText(text, width, textWrap);
247
253
 
248
254
  return measureText(wrappedText);
@@ -1,7 +1,6 @@
1
- import { useState, useEffect } from "react";
1
+ import { useMemo } from "react";
2
2
 
3
- import { useStdout } from "#/hooks/use-stdout.ts";
4
- import { getWindowSize } from "#/utils.ts";
3
+ import { useCapabilities } from "#/hooks/use-capabilities.ts";
5
4
 
6
5
  /**
7
6
  Dimensions of the terminal window.
@@ -20,22 +19,12 @@ export type WindowSize = {
20
19
 
21
20
  /**
22
21
  A React hook that returns the current terminal window dimensions and re-renders the component whenever the terminal is resized.
22
+
23
+ Reads the capabilities store, so on terminals that send in-band size reports
24
+ (mode 2048) the dimensions are the emulator's own, arriving after it has
25
+ rewrapped its screen; elsewhere they are the stream's `columns`/`rows`.
23
26
  */
24
27
  export const useWindowSize = (): WindowSize => {
25
- const { stdout } = useStdout();
26
- const [size, setSize] = useState<WindowSize>(() => getWindowSize(stdout));
27
-
28
- useEffect(() => {
29
- const onResize = () => {
30
- setSize(getWindowSize(stdout));
31
- };
32
-
33
- stdout.on("resize", onResize);
34
-
35
- return () => {
36
- stdout.off("resize", onResize);
37
- };
38
- }, [stdout]);
39
-
40
- return size;
28
+ const { size } = useCapabilities();
29
+ return useMemo(() => ({ columns: size.columns, rows: size.rows }), [size.columns, size.rows]);
41
30
  };