@alchemy.run/sigil 0.0.0-alpha.6 → 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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/{Text-BobFKi74.d.ts → Text-DV9CuzAT.d.ts} +1 -1
  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-DZ88EXJv.d.ts → index-D48vQhhe.d.ts} +2 -2
  14. package/dist/index.d.ts +10 -6
  15. package/dist/index.js +64 -59
  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-BOSLQ8fF.d.ts → screen-CgC2WlVM.d.ts} +1 -1
  22. package/dist/screen.d.ts +3 -3
  23. package/dist/{session-aZr9O8h3.js → session-Cg6STjFV.js} +42 -7
  24. package/dist/{store-CgrG9K4y.d.ts → store-C1P5fOUi.d.ts} +1 -1
  25. package/dist/terminal.d.ts +8 -5
  26. package/dist/terminal.js +1 -1
  27. package/dist/{use-focus-BzqAJi0n.js → use-focus-DSV01Ulb.js} +1 -1
  28. package/package.json +1 -1
  29. package/src/capabilities/detect.ts +15 -1
  30. package/src/capabilities/store.ts +17 -2
  31. package/src/hooks/use-window-size.ts +8 -19
  32. package/src/ink.tsx +93 -23
  33. package/src/terminal/screen-presenter.ts +58 -4
  34. package/src/terminal/session.ts +3 -2
  35. package/src/testing/terminal.ts +15 -3
  36. package/src/utils.ts +0 -40
package/README.md CHANGED
@@ -2254,7 +2254,7 @@ Type: `number`
2254
2254
  Number of rows (vertical character cells).
2255
2255
 
2256
2256
  > [!NOTE]
2257
- > When the terminal is resized narrower, ghost lines may briefly appear depending on the terminal emulator's reflow behavior.
2257
+ > On terminals that send in-band size reports (mode 2048, e.g. Ghostty, kitty, foot) the frame is repainted as soon as the report arrives, since the emulator has already rewrapped by then. Elsewhere the size comes from the PTY, which can run ahead of or behind the emulator, so resize events are coalesced: the frame is erased and repainted about 50 ms after the size stops changing and is briefly absent during a window drag. Painting mid-rewrap is what leaves ghost rows behind.
2258
2258
 
2259
2259
  ### useFocus(options?)
2260
2260
 
@@ -1,4 +1,4 @@
1
- import { s as Paint } from "./paint-C19minOS.js";
1
+ import { s as Paint } from "./paint-Cx-zC_sX.js";
2
2
  import { ReactNode } from "react";
3
3
  //#region src/glyphs.d.ts
4
4
  declare const BOXES: {
package/dist/ansi.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as ColorSupportLevel, n as ColorInfo } from "./detect-Bh4yGP6w.js";
1
+ import { i as ColorSupportLevel, n as ColorInfo } from "./detect-Db6GbKOm.js";
2
2
  import { A as exitAlternativeScreen, C as enableBracketedPaste, D as eraseLines, E as eraseLine, F as popKittyKeyboard, I as pushKittyKeyboard, L as resetCursorColor, M as link, N as pasteEnd, O as eraseScreen, P as pasteStart, S as disableBracketedPaste, T as eraseEndLine, _ as cursorNextLine, a as CursorShape, b as cursorTo, c as OSC, d as bsu, f as clearTerminal, g as cursorLeft, h as cursorHide, i as CSI, j as kittyQuery, k as esu, l as ST, m as cursorDown, n as C1_CSI, o as DEL, p as cursorColor, r as C1_ST, s as ESC, t as BEL, u as ansiEscapes, v as cursorShape, w as enterAlternativeScreen, x as cursorUp, y as cursorShow } from "./escapes-CB_6CWOE.js";
3
3
  import { a as queryClipboard, c as setTerminalProgress, d as tmuxPassthrough, i as notify, l as setWindowTitle, n as TerminalProgressState, o as setClipboard, r as clearClipboard, s as setPointerShape, t as ClipboardSelection, u as setWorkingDirectory } from "./osc-Cn0fw77g.js";
4
4
  import { _ as tokenize, a as StyledChar, c as diffAnsiCodes, d as getLinkStartCode, f as isIntensityCode, g as styledCharsToString, h as styledCharsFromTokens, i as ControlToken, l as endCodesSet, m as reduceAnsiCodesIncremental, n as AnsiToken, o as Token, p as reduceAnsiCodes, r as CharToken, s as ansiCodesToString, t as AnsiCode, u as getEndCode, v as undoAnsiCodes } from "./tokenize-Dx1y_l5H.js";
package/dist/ansi.js CHANGED
@@ -2,8 +2,8 @@ import { A as cursorShape, B as eraseScreen, C as bsu, D as cursorHide, E as cur
2
2
  import { A as isFullwidthCodePoint, C as fullwidthRanges, D as halfwidthRanges, E as halfwidthMinimalCodePoint, F as narrowMinimalCodePoint, I as narrowRanges, L as wideMaximumCodePoint, M as isWide, N as isWideNotCJKTNotEmoji, O as isAmbiguous, P as narrowMaximumCodePoint, R as wideMinimalCodePoint, S as fullwidthMinimalCodePoint, T as halfwidthMaximumCodePoint, _ as ambiguousMinimalCodePoint, a as getLinkStartCode, b as eastAsianWidthType, c as reduceAnsiCodesIncremental, d as tokenize, f as undoAnsiCodes, g as ambiguousMaximumCodePoint, i as getEndCode, j as isFullwidthGrapheme, k as isFullWidth, l as styledCharsFromTokens, n as diffAnsiCodes, o as isIntensityCode, r as endCodesSet, s as reduceAnsiCodes, t as ansiCodesToString, u as styledCharsToString, v as ambiguousRanges, w as getCategory, x as fullwidthMaximumCodePoint, y as eastAsianWidth, z as wideRanges } from "./tokenize-AjqbvtiT.js";
3
3
  import { n as widestLine, t as stringWidth } from "./string-width-CijQwpIk.js";
4
4
  import { n as sliceAnsi, r as wrapAnsi, t as cliTruncate } from "./truncate-D31fhU6i.js";
5
- import { i as detectHyperlinkSupport, t as createSupportsColor } from "./detect-BuTXtY6e.js";
6
- import { a as setPointerShape, c as setWorkingDirectory, i as setClipboard, l as tmuxPassthrough, n as notify, o as setTerminalProgress, r as queryClipboard, s as setWindowTitle, t as clearClipboard, u as cliCursor } from "./osc-CCH7xDoS.js";
5
+ import { i as detectHyperlinkSupport, t as createSupportsColor } from "./detect-B3dL4Q11.js";
6
+ import { a as setPointerShape, c as setWorkingDirectory, i as setClipboard, l as tmuxPassthrough, n as notify, o as setTerminalProgress, r as queryClipboard, s as setWindowTitle, t as clearClipboard, u as cliCursor } from "./osc-BFKKSqpg.js";
7
7
  import { t as stripAnsi } from "./strip-BvU4toXG.js";
8
8
  import { isatty } from "node:tty";
9
9
  //#region src/ansi/chalk.ts
@@ -1,5 +1,5 @@
1
- import { a as Multiplexer, c as TerminalAppearance, d as detectCapabilities, f as detectColorLevel, h as detectUnicodeSupport, i as ColorSupportLevel, l as TerminalIdentity, m as detectTerminal, n as ColorInfo, o as PixelGeometry, p as detectHyperlinkSupport, r as ColorSupport, s as RgbColor, t as Capabilities, u as createSupportsColor } from "./detect-Bh4yGP6w.js";
2
- import { a as getTerminalQuery, i as applyTerminalQuery, n as TerminalQueryOptions, o as queryTerminal, r as TerminalQueryResult, s as refreshTerminalQuery, t as PixelSize } from "./query-vaIeGOkH.js";
3
- import { n as capabilities, r as getCapabilities, t as CapabilitiesStore } from "./store-CgrG9K4y.js";
4
- import { i as resolveColorProfile, n as ColorState, r as colorState, t as ColorPolicy } from "./color-policy-BMzMwV7Q.js";
1
+ import { a as Multiplexer, c as TerminalAppearance, d as detectCapabilities, f as detectColorLevel, h as detectUnicodeSupport, i as ColorSupportLevel, l as TerminalIdentity, m as detectTerminal, n as ColorInfo, o as PixelGeometry, p as detectHyperlinkSupport, r as ColorSupport, s as RgbColor, t as Capabilities, u as createSupportsColor } from "./detect-Db6GbKOm.js";
2
+ import { a as getTerminalQuery, i as applyTerminalQuery, n as TerminalQueryOptions, o as queryTerminal, r as TerminalQueryResult, s as refreshTerminalQuery, t as PixelSize } from "./query-BNc2B8GD.js";
3
+ import { n as capabilities, r as getCapabilities, t as CapabilitiesStore } from "./store-C1P5fOUi.js";
4
+ import { i as resolveColorProfile, n as ColorState, r as colorState, t as ColorPolicy } from "./color-policy-CCxuHIdD.js";
5
5
  export { type Capabilities, type CapabilitiesStore, type ColorInfo, type ColorPolicy, type ColorState, type ColorSupport, type ColorSupportLevel, type Multiplexer, type PixelGeometry, type PixelSize, type RgbColor, type TerminalAppearance, type TerminalIdentity, type TerminalQueryOptions, type TerminalQueryResult, applyTerminalQuery, capabilities, colorState, createSupportsColor, detectCapabilities, detectColorLevel, detectHyperlinkSupport, detectTerminal, detectUnicodeSupport, getCapabilities, getTerminalQuery, queryTerminal, refreshTerminalQuery, resolveColorProfile };
@@ -1,3 +1,3 @@
1
- import { a as detectTerminal, i as detectHyperlinkSupport, n as detectCapabilities, o as detectUnicodeSupport, r as detectColorLevel, t as createSupportsColor } from "./detect-BuTXtY6e.js";
2
- import { d as queryTerminal, f as refreshTerminalQuery, i as getCapabilities, l as getTerminalQuery, n as resolveColorProfile, r as capabilities, s as applyTerminalQuery, t as colorState } from "./color-policy-SVj1pYTA.js";
1
+ import { a as detectTerminal, i as detectHyperlinkSupport, n as detectCapabilities, o as detectUnicodeSupport, r as detectColorLevel, t as createSupportsColor } from "./detect-B3dL4Q11.js";
2
+ import { d as queryTerminal, f as refreshTerminalQuery, i as getCapabilities, l as getTerminalQuery, n as resolveColorProfile, r as capabilities, s as applyTerminalQuery, t as colorState } from "./color-policy-DlrZXC0f.js";
3
3
  export { applyTerminalQuery, capabilities, colorState, createSupportsColor, detectCapabilities, detectColorLevel, detectHyperlinkSupport, detectTerminal, detectUnicodeSupport, getCapabilities, getTerminalQuery, queryTerminal, refreshTerminalQuery, resolveColorProfile };
@@ -1,5 +1,5 @@
1
- import { i as ColorSupportLevel } from "./detect-Bh4yGP6w.js";
2
- import { t as ColorProfile } from "./color-profile-u0Nhe9Nv.js";
1
+ import { i as ColorSupportLevel } from "./detect-Db6GbKOm.js";
2
+ import { t as ColorProfile } from "./color-profile-CyeHnG1T.js";
3
3
  //#region src/capabilities/color-policy.d.ts
4
4
  /** A user-selected output policy. `auto` follows the target stream. */
5
5
  type ColorPolicy = "auto" | ColorProfile;
@@ -1,6 +1,6 @@
1
1
  import { _ as CSI, b as OSC } from "./sgr-BhwaWAJB.js";
2
2
  import { a as isTty } from "./env-YVw64yZS.js";
3
- import { c as signalExit, n as detectCapabilities } from "./detect-BuTXtY6e.js";
3
+ import { n as detectCapabilities, s as signalExit } from "./detect-B3dL4Q11.js";
4
4
  import { t as colorProfileFromLevel } from "./color-profile-DHhQHY55.js";
5
5
  //#region src/capabilities/query.ts
6
6
  /**
@@ -373,25 +373,36 @@ const createStore = (stdin, stdout) => {
373
373
  const listeners = /* @__PURE__ */ new Set();
374
374
  let resizeSubscribers = 0;
375
375
  let focused;
376
+ let reportedSize;
376
377
  let snapshot;
377
378
  let snapshotQuery;
378
379
  let snapshotColumns;
379
380
  let snapshotRows;
380
381
  let snapshotFocused;
382
+ let snapshotReportedSize;
381
383
  const current = () => {
382
384
  const query = getTerminalQuery(stdout);
383
385
  const { columns, rows } = stdout;
384
- if (!snapshot || query !== snapshotQuery || columns !== snapshotColumns || rows !== snapshotRows || focused !== snapshotFocused) {
386
+ if (!snapshot || query !== snapshotQuery || columns !== snapshotColumns || rows !== snapshotRows || focused !== snapshotFocused || reportedSize !== snapshotReportedSize) {
385
387
  const detected = detectCapabilities({ stdout });
386
388
  const applied = query ? applyTerminalQuery(detected, query) : detected;
387
- snapshot = focused === void 0 ? applied : {
389
+ const sized = reportedSize ? {
388
390
  ...applied,
391
+ size: {
392
+ ...applied.size,
393
+ ...reportedSize,
394
+ source: "terminal"
395
+ }
396
+ } : applied;
397
+ snapshot = focused === void 0 ? sized : {
398
+ ...sized,
389
399
  focused
390
400
  };
391
401
  snapshotQuery = query;
392
402
  snapshotColumns = columns;
393
403
  snapshotRows = rows;
394
404
  snapshotFocused = focused;
405
+ snapshotReportedSize = reportedSize;
395
406
  }
396
407
  return snapshot;
397
408
  };
@@ -431,6 +442,7 @@ const createStore = (stdin, stdout) => {
431
442
  integration?.setReportFeed?.(false);
432
443
  removeExitHandler?.();
433
444
  removeExitHandler = void 0;
445
+ reportedSize = void 0;
434
446
  }
435
447
  };
436
448
  reportingUpdaters.set(stdout, updateReporting);
@@ -457,16 +469,22 @@ const createStore = (stdin, stdout) => {
457
469
  const resize = sequence.match(inBandResizeReport);
458
470
  if (resize) {
459
471
  const [, rows, columns, pixelHeight, pixelWidth] = resize.map(Number);
460
- if (columns && rows) patchTerminalQuery(stdout, {
461
- textAreaPixels: pixelWidth && pixelHeight ? {
462
- width: pixelWidth,
463
- height: pixelHeight
464
- } : void 0,
465
- cellPixels: pixelWidth && pixelHeight ? {
466
- width: Math.round(pixelWidth / columns),
467
- height: Math.round(pixelHeight / rows)
468
- } : void 0
469
- });
472
+ if (columns && rows) {
473
+ reportedSize = {
474
+ columns,
475
+ rows
476
+ };
477
+ patchTerminalQuery(stdout, {
478
+ textAreaPixels: pixelWidth && pixelHeight ? {
479
+ width: pixelWidth,
480
+ height: pixelHeight
481
+ } : void 0,
482
+ cellPixels: pixelWidth && pixelHeight ? {
483
+ width: Math.round(pixelWidth / columns),
484
+ height: Math.round(pixelHeight / rows)
485
+ } : void 0
486
+ });
487
+ }
470
488
  notify();
471
489
  return true;
472
490
  }
@@ -1,4 +1,4 @@
1
- import { i as ColorSupportLevel } from "./detect-Bh4yGP6w.js";
1
+ import { i as ColorSupportLevel } from "./detect-Db6GbKOm.js";
2
2
  //#region src/screen/cell.d.ts
3
3
  /**
4
4
  A terminal color before it is converted to an output profile.
package/dist/color.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as Color, d as RgbColor } from "./color-profile-u0Nhe9Nv.js";
2
- import { _ as perimeterGradient, a as LinearGradient, c as PaintContext, d as adaptive, f as ansi, g as perProfile, h as named, i as InterpolationSpace, l as PerimeterGradient, m as linearGradient, n as ColorInput, o as NamedColor, p as ansi256, r as GradientStop, s as Paint, t as AdaptiveColor, u as ProfileColor, v as rgb } from "./paint-C19minOS.js";
1
+ import { c as Color, d as RgbColor } from "./color-profile-CyeHnG1T.js";
2
+ import { _ as perimeterGradient, a as LinearGradient, c as PaintContext, d as adaptive, f as ansi, g as perProfile, h as named, i as InterpolationSpace, l as PerimeterGradient, m as linearGradient, n as ColorInput, o as NamedColor, p as ansi256, r as GradientStop, s as Paint, t as AdaptiveColor, u as ProfileColor, v as rgb } from "./paint-Cx-zC_sX.js";
3
3
  import { n as Rect } from "./geometry-BxXOzJgo.js";
4
4
  //#region src/color/palette.d.ts
5
5
  declare const canonicalAnsiPalette: readonly RgbColor[];
@@ -356,7 +356,8 @@ function detectCapabilities({ stdout = process.stdout } = {}) {
356
356
  columns: stdout.columns,
357
357
  rows: stdout.rows
358
358
  } : terminalSize(),
359
- pixels: void 0
359
+ pixels: void 0,
360
+ source: "pty"
360
361
  },
361
362
  focused: void 0,
362
363
  theme: {
@@ -370,4 +371,4 @@ function detectCapabilities({ stdout = process.stdout } = {}) {
370
371
  };
371
372
  }
372
373
  //#endregion
373
- export { detectTerminal as a, signalExit as c, detectHyperlinkSupport as i, detectCapabilities as n, detectUnicodeSupport as o, detectColorLevel as r, terminalSize as s, createSupportsColor as t };
374
+ export { detectTerminal as a, detectHyperlinkSupport as i, detectCapabilities as n, detectUnicodeSupport as o, detectColorLevel as r, signalExit as s, createSupportsColor as t };
@@ -64,11 +64,20 @@ type Capabilities = {
64
64
  /**
65
65
  Current terminal dimensions in cells, plus pixel geometry once the
66
66
  terminal has answered the query.
67
+
68
+ `source` tells where the cell dimensions come from. `"pty"` is the
69
+ stream's own `columns`/`rows`, updated by the OS on SIGWINCH — which says
70
+ nothing about whether the emulator has finished rewrapping its screen.
71
+ `"terminal"` is the emulator's in-band size report (mode 2048), which
72
+ arrives in the input stream after the rewrap and so describes the screen
73
+ exactly as later output will find it. While the terminal reports, its
74
+ size wins over the stream's.
67
75
  */
68
76
  size: {
69
77
  columns: number;
70
78
  rows: number;
71
79
  pixels: PixelGeometry | undefined;
80
+ source: "pty" | "terminal";
72
81
  };
73
82
  platform: NodeJS.Platform;
74
83
  /**
@@ -1,6 +1,6 @@
1
1
  import { a as StyledChar } from "./tokenize-Dx1y_l5H.js";
2
- import { i as Cell, t as ColorProfile } from "./color-profile-u0Nhe9Nv.js";
3
- import { n as Screen, t as Line } from "./screen-BOSLQ8fF.js";
2
+ import { i as Cell, t as ColorProfile } from "./color-profile-CyeHnG1T.js";
3
+ import { n as Screen, t as Line } from "./screen-CgC2WlVM.js";
4
4
  //#region src/screen/ansi.d.ts
5
5
  /** Converts the renderer's canonical ANSI state into backend-neutral cells. */
6
6
  declare function cellsFromAnsi(text: string): Cell[];
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { a as Multiplexer, c as TerminalAppearance, d as detectCapabilities, f as detectColorLevel, h as detectUnicodeSupport, i as ColorSupportLevel, l as TerminalIdentity, m as detectTerminal, n as ColorInfo, o as PixelGeometry, p as detectHyperlinkSupport, r as ColorSupport, s as RgbColor, t as Capabilities, u as createSupportsColor } from "./detect-Bh4yGP6w.js";
1
+ import { a as Multiplexer, c as TerminalAppearance, d as detectCapabilities, f as detectColorLevel, h as detectUnicodeSupport, i as ColorSupportLevel, l as TerminalIdentity, m as detectTerminal, n as ColorInfo, o as PixelGeometry, p as detectHyperlinkSupport, r as ColorSupport, s as RgbColor, t as Capabilities, u as createSupportsColor } from "./detect-Db6GbKOm.js";
2
2
  import { n as TerminalProgressState, t as ClipboardSelection } from "./osc-Cn0fw77g.js";
3
- import { a as getTerminalQuery, i as applyTerminalQuery, n as TerminalQueryOptions, o as queryTerminal, r as TerminalQueryResult, s as refreshTerminalQuery, t as PixelSize } from "./query-vaIeGOkH.js";
4
- import { i as OutputStream, n as capabilities, r as getCapabilities, t as CapabilitiesStore } from "./store-CgrG9K4y.js";
5
- import { s as CellStyle, t as ColorProfile } from "./color-profile-u0Nhe9Nv.js";
6
- import { s as Paint } from "./paint-C19minOS.js";
7
- import { n as Text, r as Styles, t as Props$8 } from "./Text-BobFKi74.js";
3
+ import { a as getTerminalQuery, i as applyTerminalQuery, n as TerminalQueryOptions, o as queryTerminal, r as TerminalQueryResult, s as refreshTerminalQuery, t as PixelSize } from "./query-BNc2B8GD.js";
4
+ import { i as OutputStream, n as capabilities, r as getCapabilities, t as CapabilitiesStore } from "./store-C1P5fOUi.js";
5
+ import { s as CellStyle, t as ColorProfile } from "./color-profile-CyeHnG1T.js";
6
+ import { s as Paint } from "./paint-Cx-zC_sX.js";
7
+ import { n as Text, r as Styles, t as Props$8 } from "./Text-DV9CuzAT.js";
8
8
  import { i as Node } from "./index-DDVME65c.js";
9
9
  import { t as CursorPosition } from "./cursor-position-D2LAkRG0.js";
10
10
  import { Writable } from "node:stream";
@@ -1046,6 +1046,10 @@ type WindowSize = {
1046
1046
  };
1047
1047
  /**
1048
1048
  A React hook that returns the current terminal window dimensions and re-renders the component whenever the terminal is resized.
1049
+
1050
+ Reads the capabilities store, so on terminals that send in-band size reports
1051
+ (mode 2048) the dimensions are the emulator's own, arriving after it has
1052
+ rewrapped its screen; elsewhere they are the stream's `columns`/`rows`.
1049
1053
  */
1050
1054
  declare const useWindowSize: () => WindowSize;
1051
1055
  //#endregion
package/dist/index.js CHANGED
@@ -2,12 +2,12 @@ import { C as bsu, S as ansiEscapes, V as esu, W as link, _ as CSI } from "./sgr
2
2
  import { h as tokenizeAnsi, p as graphemes } from "./tokenize-AjqbvtiT.js";
3
3
  import { t as stringWidth } from "./string-width-CijQwpIk.js";
4
4
  import { n as sliceAnsi, r as wrapAnsi } from "./truncate-D31fhU6i.js";
5
- import { _ as squashTextNodes, a as reconciler, c as kittyModifiers, d as FocusContext, f as Text, g as transformAnsiLine, h as emitLayoutListeners, i as useStdinContext, l as resolveFlags, m as createNode, n as useInput, o as detectKittySupport, p as addLayoutListener, r as useStdin, s as kittyFlags, t as useFocus, u as StdinContext, v as accessibilityContext } from "./use-focus-BzqAJi0n.js";
5
+ import { _ as squashTextNodes, a as reconciler, c as kittyModifiers, d as FocusContext, f as Text, g as transformAnsiLine, h as emitLayoutListeners, i as useStdinContext, l as resolveFlags, m as createNode, n as useInput, o as detectKittySupport, p as addLayoutListener, r as useStdin, s as kittyFlags, t as useFocus, u as StdinContext, v as accessibilityContext } from "./use-focus-DSV01Ulb.js";
6
6
  import { a as isTty, i as isSigilDev, o as isWindows, r as isScreenReader, t as isInCi } from "./env-YVw64yZS.js";
7
- import { a as detectTerminal, c as signalExit, i as detectHyperlinkSupport, n as detectCapabilities, o as detectUnicodeSupport, r as detectColorLevel, s as terminalSize, t as createSupportsColor } from "./detect-BuTXtY6e.js";
8
- import { u as cliCursor } from "./osc-CCH7xDoS.js";
9
- import { a as registerTerminalIntegration, c as ensureTerminalQuery, d as queryTerminal, f as refreshTerminalQuery, i as getCapabilities, l as getTerminalQuery, o as getRawModeStream, r as capabilities, s as applyTerminalQuery, u as getTerminalQueryPromise } from "./color-policy-SVj1pYTA.js";
10
- import { a as buildCursorSuffix, c as hideCursorEscape, i as buildCursorOnlySequence, o as buildReturnToBottomPrefix, s as cursorPositionChanged, t as TerminalSession } from "./session-aZr9O8h3.js";
7
+ import { a as detectTerminal, i as detectHyperlinkSupport, n as detectCapabilities, o as detectUnicodeSupport, r as detectColorLevel, s as signalExit, t as createSupportsColor } from "./detect-B3dL4Q11.js";
8
+ import { u as cliCursor } from "./osc-BFKKSqpg.js";
9
+ import { a as registerTerminalIntegration, c as ensureTerminalQuery, d as queryTerminal, f as refreshTerminalQuery, i as getCapabilities, l as getTerminalQuery, o as getRawModeStream, r as capabilities, s as applyTerminalQuery, u as getTerminalQueryPromise } from "./color-policy-DlrZXC0f.js";
10
+ import { a as buildCursorSuffix, c as hideCursorEscape, i as buildCursorOnlySequence, o as buildReturnToBottomPrefix, s as cursorPositionChanged, t as TerminalSession } from "./session-Cg6STjFV.js";
11
11
  import { t as Yoga } from "./yoga-5jKhYCJC.js";
12
12
  import { r as createCell, t as cellAttributes } from "./cell-_ZVhbfl0.js";
13
13
  import { i as semanticTextStyleToCellStyle, n as mergeSemanticTextStyles, t as emptySemanticTextStyle } from "./semantic-text-style-DIMzC7xt.js";
@@ -1660,31 +1660,6 @@ function createRenderScheduler(render, options) {
1660
1660
  };
1661
1661
  }
1662
1662
  //#endregion
1663
- //#region src/utils.ts
1664
- const resolveDimension = (value, fallback, defaultValue) => {
1665
- if (value !== void 0 && value > 0) return value;
1666
- if (fallback !== void 0 && fallback > 0) return fallback;
1667
- return defaultValue;
1668
- };
1669
- /**
1670
- Get the effective terminal dimensions from the given stdout stream.
1671
-
1672
- Falls back to `terminal-size` for columns in piped processes where `stdout.columns` is 0, and uses standard defaults (80×24) when dimensions cannot be determined.
1673
- */
1674
- const getWindowSize = (stdout) => {
1675
- const columns = stdout.columns ?? 0;
1676
- const rows = stdout.rows ?? 0;
1677
- if (columns && rows) return {
1678
- columns,
1679
- rows
1680
- };
1681
- const fallbackSize = terminalSize();
1682
- return {
1683
- columns: resolveDimension(columns, fallbackSize.columns, 80),
1684
- rows: resolveDimension(rows, fallbackSize.rows, 24)
1685
- };
1686
- };
1687
- //#endregion
1688
1663
  //#region src/ink.tsx
1689
1664
  /** @jsxImportSource react */
1690
1665
  const noop = () => {};
@@ -1710,6 +1685,7 @@ function bottomRows(screen, height) {
1710
1685
  }
1711
1686
  return cropped;
1712
1687
  }
1688
+ const RESIZE_SETTLE_MS = 50;
1713
1689
  const shouldClearTerminalForFrame = ({ isTTY, viewportRows, previousOutputHeight, nextOutputHeight, isUnmounting }) => {
1714
1690
  if (!isTTY) return false;
1715
1691
  const hadPreviousFrame = previousOutputHeight > 0;
@@ -1801,8 +1777,10 @@ const createInk = (options) => {
1801
1777
  let lastOutputToRender = "";
1802
1778
  let lastOutputHeight = 0;
1803
1779
  let lastScreen;
1804
- let lastTerminalWidth = getWindowSize(options.stdout).columns;
1805
- let lastTerminalHeight = getWindowSize(options.stdout).rows;
1780
+ const windowSize = () => capabilitiesStore.current.size;
1781
+ let lastTerminalWidth = windowSize().columns;
1782
+ let lastTerminalHeight = windowSize().rows;
1783
+ let resizeSettle;
1806
1784
  let fullStaticOutput = "";
1807
1785
  let exitResult;
1808
1786
  let unsubscribeBeforeExit;
@@ -1825,29 +1803,48 @@ const createInk = (options) => {
1825
1803
  if (isSigilDev) reconciler.injectIntoDevTools();
1826
1804
  if (options.patchConsole) installConsolePatch();
1827
1805
  if (interactive) {
1828
- options.stdout.on("resize", resized);
1829
- unsubscribeResize = () => {
1830
- options.stdout.off("resize", resized);
1831
- };
1806
+ let seenColumns = lastTerminalWidth;
1807
+ let seenRows = lastTerminalHeight;
1808
+ unsubscribeResize = capabilitiesStore.subscribe(({ size }) => {
1809
+ if (size.columns === seenColumns && size.rows === seenRows) return;
1810
+ seenColumns = size.columns;
1811
+ seenRows = size.rows;
1812
+ resized(size.source);
1813
+ });
1832
1814
  }
1833
1815
  initKittyKeyboard();
1834
1816
  const { promise: exitPromise, resolve: resolveExitPromise, reject: rejectExitPromise } = Promise.withResolvers();
1835
1817
  exitPromise.catch(noop);
1836
- function resized() {
1837
- const currentWidth = getWindowSize(options.stdout).columns;
1838
- const currentHeight = getWindowSize(options.stdout).rows;
1818
+ function resized(source) {
1819
+ if (resizeSettle !== void 0) {
1820
+ clearTimeout(resizeSettle);
1821
+ resizeSettle = void 0;
1822
+ }
1823
+ if (source === "terminal") {
1824
+ settleResize();
1825
+ return;
1826
+ }
1827
+ resizeSettle = setTimeout(settleResize, RESIZE_SETTLE_MS);
1828
+ }
1829
+ function settleResize() {
1830
+ resizeSettle = void 0;
1831
+ if (isUnmounted || isUnmounting) return;
1832
+ const { columns: currentWidth, rows: currentHeight } = windowSize();
1839
1833
  if (currentWidth < lastTerminalWidth || currentHeight !== lastTerminalHeight) {
1840
- clearLiveOutput();
1834
+ clearLiveOutput(rewrapsOnResize() ? currentWidth : void 0);
1841
1835
  resetLiveOutput();
1842
1836
  lastOutput = "";
1843
1837
  lastOutputToRender = "";
1844
1838
  lastOutputHeight = 0;
1845
1839
  }
1840
+ lastTerminalWidth = currentWidth;
1841
+ lastTerminalHeight = currentHeight;
1846
1842
  calculateLayout();
1847
1843
  emitLayoutListeners(rootNode);
1848
1844
  onRender();
1849
- lastTerminalWidth = currentWidth;
1850
- lastTerminalHeight = currentHeight;
1845
+ }
1846
+ function rewrapsOnResize() {
1847
+ return !isWindows && capabilitiesStore.current.terminal.name !== "apple-terminal";
1851
1848
  }
1852
1849
  function handleAppExit(errorOrResult) {
1853
1850
  if (isUnmounted || isUnmounting) return;
@@ -1872,9 +1869,9 @@ const createInk = (options) => {
1872
1869
  forceRewrite: true
1873
1870
  });
1874
1871
  }
1875
- function clearLiveOutput() {
1872
+ function clearLiveOutput(columns) {
1876
1873
  if (isScreenReaderEnabled) accessiblePresenter.clear();
1877
- else terminal.clearFrame();
1874
+ else terminal.clearFrame({ columns });
1878
1875
  }
1879
1876
  function finishLiveOutput() {
1880
1877
  if (isScreenReaderEnabled) accessiblePresenter.done();
@@ -1885,7 +1882,7 @@ const createInk = (options) => {
1885
1882
  else terminal.resetFrame();
1886
1883
  }
1887
1884
  function calculateLayout() {
1888
- const terminalWidth = getWindowSize(options.stdout).columns;
1885
+ const terminalWidth = windowSize().columns;
1889
1886
  rootNode.yogaNode.setWidth(terminalWidth);
1890
1887
  rootNode.yogaNode.calculateLayout(void 0, void 0, Yoga.DIRECTION_LTR);
1891
1888
  }
@@ -1902,6 +1899,13 @@ const createInk = (options) => {
1902
1899
  }
1903
1900
  return;
1904
1901
  }
1902
+ if (resizeSettle !== void 0 && !isUnmounting) {
1903
+ if (nextRenderCommit) {
1904
+ nextRenderCommit.resolve();
1905
+ nextRenderCommit = void 0;
1906
+ }
1907
+ return;
1908
+ }
1905
1909
  if (nextRenderCommit) {
1906
1910
  nextRenderCommit.resolve();
1907
1911
  nextRenderCommit = void 0;
@@ -1948,7 +1952,7 @@ const createInk = (options) => {
1948
1952
  if (sync) options.stdout.write(esu);
1949
1953
  return;
1950
1954
  }
1951
- const terminalWidth = getWindowSize(options.stdout).columns;
1955
+ const terminalWidth = windowSize().columns;
1952
1956
  const wrappedOutput = wrapAnsi(output, terminalWidth, {
1953
1957
  trim: false,
1954
1958
  hard: true
@@ -2037,6 +2041,10 @@ const createInk = (options) => {
2037
2041
  function unmount(error) {
2038
2042
  if (isUnmounted || isUnmounting) return;
2039
2043
  isUnmounting = true;
2044
+ if (resizeSettle !== void 0) {
2045
+ clearTimeout(resizeSettle);
2046
+ resizeSettle = void 0;
2047
+ }
2040
2048
  unsubscribeBeforeExit?.();
2041
2049
  unsubscribeBeforeExit = void 0;
2042
2050
  const { canWriteToStdout } = getWritableStreamState(options.stdout);
@@ -2221,7 +2229,7 @@ const createInk = (options) => {
2221
2229
  if (!screen) return;
2222
2230
  const hasStaticOutput = staticOutput !== "";
2223
2231
  const isTTY = Boolean(options.stdout.isTTY);
2224
- const viewportRows = isTTY ? getWindowSize(options.stdout).rows : 24;
2232
+ const viewportRows = isTTY ? windowSize().rows : 24;
2225
2233
  if (isTTY && outputHeight > viewportRows) {
2226
2234
  const lines = output.split("\n");
2227
2235
  output = lines.slice(lines.length - viewportRows).join("\n");
@@ -2907,20 +2915,17 @@ const usePointerShape = (shape) => {
2907
2915
  //#region src/hooks/use-window-size.ts
2908
2916
  /**
2909
2917
  A React hook that returns the current terminal window dimensions and re-renders the component whenever the terminal is resized.
2918
+
2919
+ Reads the capabilities store, so on terminals that send in-band size reports
2920
+ (mode 2048) the dimensions are the emulator's own, arriving after it has
2921
+ rewrapped its screen; elsewhere they are the stream's `columns`/`rows`.
2910
2922
  */
2911
2923
  const useWindowSize = () => {
2912
- const { stdout } = useStdout();
2913
- const [size, setSize] = useState(() => getWindowSize(stdout));
2914
- useEffect(() => {
2915
- const onResize = () => {
2916
- setSize(getWindowSize(stdout));
2917
- };
2918
- stdout.on("resize", onResize);
2919
- return () => {
2920
- stdout.off("resize", onResize);
2921
- };
2922
- }, [stdout]);
2923
- return size;
2924
+ const { size } = useCapabilities();
2925
+ return useMemo(() => ({
2926
+ columns: size.columns,
2927
+ rows: size.rows
2928
+ }), [size.columns, size.rows]);
2924
2929
  };
2925
2930
  //#endregion
2926
2931
  //#region src/hooks/use-box-metrics.ts
@@ -1,5 +1,5 @@
1
1
  import { D as cursorHide, b as OSC, j as cursorShow, x as ST } from "./sgr-BhwaWAJB.js";
2
- import { c as signalExit } from "./detect-BuTXtY6e.js";
2
+ import { s as signalExit } from "./detect-B3dL4Q11.js";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/ansi/cursor.ts
5
5
  let restoreRegistered = false;
@@ -1,5 +1,5 @@
1
- import { c as TerminalAppearance } from "./detect-Bh4yGP6w.js";
2
- import { c as Color, d as RgbColor, t as ColorProfile } from "./color-profile-u0Nhe9Nv.js";
1
+ import { c as TerminalAppearance } from "./detect-Db6GbKOm.js";
2
+ import { c as Color, d as RgbColor, t as ColorProfile } from "./color-profile-CyeHnG1T.js";
3
3
  //#region src/color/paint.d.ts
4
4
  type ColorInput = Color | string;
5
5
  type InterpolationSpace = "oklab" | "lab" | "rgb" | "hsv";
@@ -1,4 +1,4 @@
1
- import { c as TerminalAppearance, s as RgbColor, t as Capabilities } from "./detect-Bh4yGP6w.js";
1
+ import { c as TerminalAppearance, s as RgbColor, t as Capabilities } from "./detect-Db6GbKOm.js";
2
2
  //#region src/capabilities/query.d.ts
3
3
  type PixelSize = {
4
4
  width: number;
package/dist/router.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as Props } from "./Text-BobFKi74.js";
1
+ import { t as Props } from "./Text-DV9CuzAT.js";
2
2
  import { ReactElement, ReactNode } from "react";
3
3
  //#region src/router/history.d.ts
4
4
  /**
package/dist/router.js CHANGED
@@ -1,4 +1,4 @@
1
- import { f as Text, n as useInput, t as useFocus } from "./use-focus-BzqAJi0n.js";
1
+ import { f as Text, n as useInput, t as useFocus } from "./use-focus-DSV01Ulb.js";
2
2
  import { Children, Fragment, createContext, isValidElement, startTransition, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
4
4
  //#region src/router/history.ts
@@ -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[];
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-DZ88EXJv.js";
4
- import { n as Screen, t as Line } from "./screen-BOSLQ8fF.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 };
@@ -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,6 +369,24 @@ 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;
@@ -437,8 +471,9 @@ var TerminalSession = class {
437
471
  willPresent(screen, options = {}) {
438
472
  return this.#presenter.willPresent(screen, this.cursor.position, options.fullscreen, options.forceRewrite);
439
473
  }
440
- clearFrame() {
441
- 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);
442
477
  }
443
478
  resetFrame() {
444
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-BOSLQ8fF.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-aZr9O8h3.js";
1
+ import { n as TerminalInput, r as parseMouseEvent, t as TerminalSession } from "./session-Cg6STjFV.js";
2
2
  export { TerminalInput, TerminalSession, parseMouseEvent };
@@ -564,7 +564,7 @@ const detectKittySupport = (stdin, stdout, onSupported) => {
564
564
  //#endregion
565
565
  //#region package.json
566
566
  var name = "@alchemy.run/sigil";
567
- var version = "0.0.0-alpha.6";
567
+ var version = "0.0.0-alpha.7";
568
568
  //#endregion
569
569
  //#region src/reconciler.ts
570
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.6",
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,
@@ -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
  };
package/src/ink.tsx CHANGED
@@ -33,7 +33,6 @@ import { createInlinePresenter } from "#/terminal/inline-presenter.ts";
33
33
  import { createRenderScheduler } from "#/terminal/render-scheduler.ts";
34
34
  import { TerminalSession } from "#/terminal/session.ts";
35
35
  import { type Throttled } from "#/throttle.ts";
36
- import { getWindowSize } from "#/utils.ts";
37
36
  import { Yoga } from "#/yoga/index.ts";
38
37
 
39
38
  const noop = () => {};
@@ -64,6 +63,11 @@ function bottomRows(screen: Screen, height: number): Screen {
64
63
  return cropped;
65
64
  }
66
65
 
66
+ // How long the reported terminal size must hold still before a resize is
67
+ // acted on. Window drags deliver an event per frame (~16 ms), and the
68
+ // emulator needs a frame or so to agree with the PTY size either way.
69
+ const RESIZE_SETTLE_MS = 50;
70
+
67
71
  const shouldClearTerminalForFrame = ({
68
72
  isTTY,
69
73
  viewportRows,
@@ -381,8 +385,20 @@ export const createInk = (options: Options): Ink => {
381
385
  let lastOutputToRender = "";
382
386
  let lastOutputHeight = 0;
383
387
  let lastScreen: Screen | undefined;
384
- let lastTerminalWidth = getWindowSize(options.stdout).columns;
385
- let lastTerminalHeight = getWindowSize(options.stdout).rows;
388
+ // The terminal size comes from the capabilities store: the stream's
389
+ // `columns`/`rows`, or — on terminals that send in-band size reports —
390
+ // the emulator's own figure, which is the one later output will meet.
391
+ const windowSize = () => capabilitiesStore.current.size;
392
+ let lastTerminalWidth = windowSize().columns;
393
+ let lastTerminalHeight = windowSize().rows;
394
+ // Resize events arrive in bursts (one per frame of a window drag), and the
395
+ // emulator's rewrap and the PTY size never update atomically — depending on
396
+ // the terminal app the PTY is resized before or after the screen is
397
+ // rewrapped. Anything written mid-burst lands on a screen whose width is
398
+ // not the reported one, and the rows that leaves behind can never be
399
+ // accounted for by a later erase. Events therefore only arm this settle
400
+ // timer; the frame is erased and repainted once the size has held still.
401
+ let resizeSettle: ReturnType<typeof setTimeout> | undefined;
386
402
 
387
403
  // This variable is used only in debug mode to store full static output
388
404
  // so that it's rerendered every time, not just new static parts, like in non-debug mode
@@ -431,11 +447,17 @@ export const createInk = (options: Options): Ink => {
431
447
  if (options.patchConsole) installConsolePatch();
432
448
 
433
449
  if (interactive) {
434
- options.stdout.on("resize", resized);
435
-
436
- unsubscribeResize = () => {
437
- options.stdout.off("resize", resized);
438
- };
450
+ // Follow the store rather than the stream's `resize` event: the store
451
+ // folds in the terminal's in-band size reports where available, and
452
+ // says which source a size came from.
453
+ let seenColumns = lastTerminalWidth;
454
+ let seenRows = lastTerminalHeight;
455
+ unsubscribeResize = capabilitiesStore.subscribe(({ size }) => {
456
+ if (size.columns === seenColumns && size.rows === seenRows) return;
457
+ seenColumns = size.columns;
458
+ seenRows = size.rows;
459
+ resized(size.source);
460
+ });
439
461
  }
440
462
 
441
463
  initKittyKeyboard();
@@ -450,9 +472,28 @@ export const createInk = (options: Options): Ink => {
450
472
 
451
473
  void exitPromise.catch(noop);
452
474
 
453
- function resized(): void {
454
- const currentWidth = getWindowSize(options.stdout).columns;
455
- const currentHeight = getWindowSize(options.stdout).rows;
475
+ function resized(source: "pty" | "terminal"): void {
476
+ if (resizeSettle !== undefined) {
477
+ clearTimeout(resizeSettle);
478
+ resizeSettle = undefined;
479
+ }
480
+
481
+ // An in-band report is the emulator's own word, sent after it rewrapped
482
+ // and ordered with everything else in the stream, so there is nothing
483
+ // to wait for. A PTY size may run ahead of or behind the emulator.
484
+ if (source === "terminal") {
485
+ settleResize();
486
+ return;
487
+ }
488
+
489
+ resizeSettle = setTimeout(settleResize, RESIZE_SETTLE_MS);
490
+ }
491
+
492
+ function settleResize(): void {
493
+ resizeSettle = undefined;
494
+ if (isUnmounted || isUnmounting) return;
495
+
496
+ const { columns: currentWidth, rows: currentHeight } = windowSize();
456
497
 
457
498
  // A width decrease rewraps lines and any height change moves content
458
499
  // through scrollback, so the incremental render state no longer
@@ -460,11 +501,15 @@ export const createInk = (options: Options): Ink => {
460
501
  // render to be a full rewrite instead of an incremental diff that
461
502
  // would skip "unchanged" lines over stale screen content.
462
503
  if (currentWidth < lastTerminalWidth || currentHeight !== lastTerminalHeight) {
463
- // Clearing erases the full previous frame line count from
464
- // the cursor upward — after a height grow that also covers frame
465
- // lines the emulator pulled back from scrollback, so no extra
466
- // erase is needed for them.
467
- clearLiveOutput();
504
+ // Clearing erases the full previous frame from the cursor upward —
505
+ // after a height grow that also covers frame lines the emulator
506
+ // pulled back from scrollback, so no extra erase is needed for them.
507
+ // The current width lets the presenter count the rows a reflowing
508
+ // emulator has already rewrapped after a width shrink; the logical
509
+ // line count alone under-erases and leaves the frame's top rows
510
+ // behind. Terminals that never rewrap keep the logical count, since
511
+ // the rewrap-aware one would erase rows above the frame there.
512
+ clearLiveOutput(rewrapsOnResize() ? currentWidth : undefined);
468
513
  resetLiveOutput();
469
514
  lastOutput = "";
470
515
  lastOutputToRender = "";
@@ -475,12 +520,20 @@ export const createInk = (options: Options): Ink => {
475
520
  lastOutputHeight = 0;
476
521
  }
477
522
 
523
+ lastTerminalWidth = currentWidth;
524
+ lastTerminalHeight = currentHeight;
525
+
478
526
  calculateLayout();
479
527
  dom.emitLayoutListeners(rootNode);
480
528
  onRender();
529
+ }
481
530
 
482
- lastTerminalWidth = currentWidth;
483
- lastTerminalHeight = currentHeight;
531
+ // Whether the terminal rewraps existing screen rows when its width
532
+ // changes. Nearly every modern emulator does (xterm.js, Ghostty, kitty,
533
+ // iTerm2, WezTerm, Alacritty, Windows Terminal, tmux); Apple's Terminal.app
534
+ // and the classic Windows console keep rows as they were written.
535
+ function rewrapsOnResize(): boolean {
536
+ return !isWindows && capabilitiesStore.current.terminal.name !== "apple-terminal";
484
537
  }
485
538
 
486
539
  function handleAppExit(errorOrResult?: unknown): void {
@@ -519,9 +572,9 @@ export const createInk = (options: Options): Ink => {
519
572
  }
520
573
  }
521
574
 
522
- function clearLiveOutput(): void {
575
+ function clearLiveOutput(columns?: number): void {
523
576
  if (isScreenReaderEnabled) accessiblePresenter!.clear();
524
- else terminal.clearFrame();
577
+ else terminal.clearFrame({ columns });
525
578
  }
526
579
 
527
580
  function finishLiveOutput(): void {
@@ -535,7 +588,7 @@ export const createInk = (options: Options): Ink => {
535
588
  }
536
589
 
537
590
  function calculateLayout(): void {
538
- const terminalWidth = getWindowSize(options.stdout).columns;
591
+ const terminalWidth = windowSize().columns;
539
592
 
540
593
  rootNode.yogaNode!.setWidth(terminalWidth);
541
594
 
@@ -566,6 +619,18 @@ export const createInk = (options: Options): Ink => {
566
619
  return;
567
620
  }
568
621
 
622
+ // A resize burst is still settling: the emulator is rewrapping the screen
623
+ // under us, and the frame will be erased and repainted as a whole once it
624
+ // holds still. Writing now would leave rows no later erase can find.
625
+ if (resizeSettle !== undefined && !isUnmounting) {
626
+ if (nextRenderCommit) {
627
+ nextRenderCommit.resolve();
628
+ nextRenderCommit = undefined;
629
+ }
630
+
631
+ return;
632
+ }
633
+
569
634
  if (nextRenderCommit) {
570
635
  nextRenderCommit.resolve();
571
636
  nextRenderCommit = undefined;
@@ -642,7 +707,7 @@ export const createInk = (options: Options): Ink => {
642
707
  return;
643
708
  }
644
709
 
645
- const terminalWidth = getWindowSize(options.stdout).columns;
710
+ const terminalWidth = windowSize().columns;
646
711
 
647
712
  const wrappedOutput = wrapAnsi(output, terminalWidth, {
648
713
  trim: false,
@@ -796,6 +861,11 @@ export const createInk = (options: Options): Ink => {
796
861
 
797
862
  isUnmounting = true;
798
863
 
864
+ if (resizeSettle !== undefined) {
865
+ clearTimeout(resizeSettle);
866
+ resizeSettle = undefined;
867
+ }
868
+
799
869
  unsubscribeBeforeExit?.();
800
870
  unsubscribeBeforeExit = undefined;
801
871
 
@@ -1163,7 +1233,7 @@ export const createInk = (options: Options): Ink => {
1163
1233
 
1164
1234
  // Detect fullscreen: output fills or exceeds terminal height.
1165
1235
  // Only apply when writing to a real TTY — piped output always gets trailing newlines.
1166
- const viewportRows = isTTY ? getWindowSize(options.stdout).rows : 24;
1236
+ const viewportRows = isTTY ? windowSize().rows : 24;
1167
1237
 
1168
1238
  // Clamp the frame to the viewport, keeping its bottom rows. Rows above
1169
1239
  // the top margin cannot be updated or erased in place, and the
@@ -7,9 +7,9 @@ import {
7
7
  cursorPositionChanged,
8
8
  type CursorPosition,
9
9
  } from "#/cursor-position.ts";
10
- import { cellsEqual } from "#/screen/cell.ts";
10
+ import { cellAttributes, cellsEqual, type Cell } from "#/screen/cell.ts";
11
11
  import type { ColorProfile } from "#/screen/color-profile.ts";
12
- import type { Screen } from "#/screen/screen.ts";
12
+ import type { Line, Screen } from "#/screen/screen.ts";
13
13
  import { serializeLine, serializeScreen } from "#/screen/serialize.ts";
14
14
 
15
15
  type Write = (data: string) => boolean;
@@ -123,10 +123,20 @@ export class ScreenPresenter {
123
123
  );
124
124
  }
125
125
 
126
- clear(): void {
126
+ /**
127
+ Erases the presented frame from the cursor upward.
128
+
129
+ `columns` is the terminal's current width. When it is narrower than the width
130
+ the frame was painted at, a reflowing emulator (xterm.js, Ghostty, kitty,
131
+ iTerm2, WezTerm, tmux, …) has already rewrapped every wider row onto several
132
+ physical rows before the resize event reaches us, so the erase has to cover
133
+ that physical footprint. Erasing only the logical row count leaves the frame's
134
+ top rows behind as ghosts, one more batch per resize event.
135
+ */
136
+ clear(options: { readonly columns?: number } = {}): void {
127
137
  this.#write(
128
138
  buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) +
129
- ansiEscapes.eraseLines(this.#lineCount),
139
+ ansiEscapes.eraseLines(this.#physicalLineCount(options.columns)),
130
140
  );
131
141
  this.#lineCount = 0;
132
142
  this.#cursor = undefined;
@@ -134,6 +144,22 @@ export class ScreenPresenter {
134
144
  this.#fullscreen = false;
135
145
  }
136
146
 
147
+ #physicalLineCount(columns: number | undefined): number {
148
+ if (
149
+ this.#screen === undefined ||
150
+ this.#lineCount === 0 ||
151
+ columns === undefined ||
152
+ columns < 1
153
+ ) {
154
+ return this.#lineCount;
155
+ }
156
+ let rows = 0;
157
+ for (const line of this.#screen.toRows()) {
158
+ rows += Math.max(1, Math.ceil(contentWidth(line) / columns));
159
+ }
160
+ return rows + (this.#fullscreen ? 0 : 1);
161
+ }
162
+
137
163
  reset(): void {
138
164
  this.#screen = undefined;
139
165
  this.#lineCount = 0;
@@ -162,6 +188,34 @@ export class ScreenPresenter {
162
188
  }
163
189
  }
164
190
 
191
+ /**
192
+ The columns a row occupies once written: everything up to its last visible
193
+ cell. Trailing unstyled blanks are trimmed by the serializer and never reach
194
+ the terminal, while styled blanks (a highlighted tab's padding) do.
195
+ */
196
+ function contentWidth(line: Line): number {
197
+ let width = 0;
198
+ let end = 0;
199
+ for (const cell of line) {
200
+ width += cell.width;
201
+ if (!isBlank(cell)) end = width;
202
+ }
203
+ return end;
204
+ }
205
+
206
+ function isBlank(cell: Cell): boolean {
207
+ const { style } = cell;
208
+ return (
209
+ (cell.grapheme === " " || cell.width === 0) &&
210
+ style.foreground === undefined &&
211
+ style.background === undefined &&
212
+ style.underlineColor === undefined &&
213
+ style.underline === "none" &&
214
+ style.attributes === cellAttributes.none &&
215
+ cell.hyperlink === undefined
216
+ );
217
+ }
218
+
165
219
  function findFirstChangedRow(previous: Screen, next: Screen): number | undefined {
166
220
  const height = Math.max(previous.height, next.height);
167
221
  for (let y = 0; y < height; y++) {
@@ -140,8 +140,9 @@ export class TerminalSession {
140
140
  );
141
141
  }
142
142
 
143
- clearFrame(): void {
144
- this.#presenter.clear();
143
+ /** Erases the presented frame; pass the current `columns` after a resize so the erase covers rows the emulator rewrapped. */
144
+ clearFrame(options: { readonly columns?: number } = {}): void {
145
+ this.#presenter.clear(options);
145
146
  }
146
147
 
147
148
  resetFrame(): void {
@@ -100,7 +100,13 @@ export type TerminalApp = {
100
100
  */
101
101
  type: (text: string) => void;
102
102
 
103
- resize: (columns: number, rows: number) => void;
103
+ /**
104
+ Resizes the PTY and the emulator. Real terminal apps never do both at
105
+ once: `emulatorLag` (ms) delays the emulator's rewrap behind the PTY
106
+ resize, simulating an app that reports the new size to the process
107
+ before its screen has been rewrapped.
108
+ */
109
+ resize: (columns: number, rows: number, options?: { readonly emulatorLag?: number }) => void;
104
110
 
105
111
  /**
106
112
  Flips the emulated OS color scheme (Ghostty engine only) — the app
@@ -306,9 +312,15 @@ export const launchTerminal = async (
306
312
  type: (text) => {
307
313
  child.write(text);
308
314
  },
309
- resize: (nextColumns, nextRows) => {
315
+ resize: (nextColumns, nextRows, options = {}) => {
310
316
  child.resize(nextColumns, nextRows);
311
- emulator.resize(nextColumns, nextRows);
317
+ if (options.emulatorLag === undefined) {
318
+ emulator.resize(nextColumns, nextRows);
319
+ return;
320
+ }
321
+ setTimeout(() => {
322
+ if (!closed) emulator.resize(nextColumns, nextRows);
323
+ }, options.emulatorLag);
312
324
  },
313
325
  setColorScheme: (scheme) => {
314
326
  if (!emulator.setColorScheme) {
package/src/utils.ts DELETED
@@ -1,40 +0,0 @@
1
- import type { OutputStream } from "#/stream.ts";
2
- import { terminalSize } from "#/terminal-size.ts";
3
-
4
- const resolveDimension = (
5
- value: number | undefined,
6
- fallback: number | undefined,
7
- defaultValue: number,
8
- ): number => {
9
- if (value !== undefined && value > 0) {
10
- return value;
11
- }
12
-
13
- if (fallback !== undefined && fallback > 0) {
14
- return fallback;
15
- }
16
-
17
- return defaultValue;
18
- };
19
-
20
- /**
21
- Get the effective terminal dimensions from the given stdout stream.
22
-
23
- Falls back to `terminal-size` for columns in piped processes where `stdout.columns` is 0, and uses standard defaults (80×24) when dimensions cannot be determined.
24
- */
25
- export const getWindowSize = (stdout: OutputStream): { columns: number; rows: number } => {
26
- // `stdout.columns`/`rows` can be 0 or undefined in non-TTY environments.
27
- const columns = stdout.columns ?? 0;
28
- const rows = stdout.rows ?? 0;
29
-
30
- if (columns && rows) {
31
- return { columns, rows };
32
- }
33
-
34
- const fallbackSize = terminalSize();
35
-
36
- return {
37
- columns: resolveDimension(columns, fallbackSize.columns, 80),
38
- rows: resolveDimension(rows, fallbackSize.rows, 24),
39
- };
40
- };