@code-yeongyu/senpi-tui 2026.9.29 → 2026.10.1-2

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 (151) hide show
  1. package/README.md +38 -0
  2. package/dist/autocomplete.d.ts +5 -0
  3. package/dist/autocomplete.js +38 -14
  4. package/dist/colors.d.ts +73 -0
  5. package/dist/colors.js +273 -0
  6. package/dist/components/box.js +5 -3
  7. package/dist/components/editor.d.ts +4 -1
  8. package/dist/components/editor.js +23 -8
  9. package/dist/components/latex.js +5 -0
  10. package/dist/editor-component.d.ts +7 -2
  11. package/dist/fuzzy.js +23 -22
  12. package/dist/index.d.ts +6 -3
  13. package/dist/index.js +5 -2
  14. package/dist/native-platform.d.ts +2 -0
  15. package/dist/oklab.d.ts +43 -0
  16. package/dist/oklab.js +210 -0
  17. package/dist/slash-command-autocomplete.js +3 -0
  18. package/dist/terminal-capabilities.js +1 -1
  19. package/dist/terminal-colors.d.ts +19 -2
  20. package/dist/terminal-colors.js +12 -7
  21. package/dist/terminal-image.d.ts +3 -1
  22. package/dist/terminal-image.js +33 -9
  23. package/dist/terminal.d.ts +3 -0
  24. package/dist/terminal.js +15 -8
  25. package/dist/tui-alt-screen.d.ts +12 -7
  26. package/dist/tui-alt-screen.js +37 -17
  27. package/dist/tui.d.ts +18 -17
  28. package/dist/tui.js +79 -62
  29. package/dist/utils.d.ts +3 -0
  30. package/dist/utils.js +108 -60
  31. package/dist/wheel-scroll.d.ts +23 -0
  32. package/dist/wheel-scroll.js +70 -0
  33. package/native/clipboard.h +40 -2
  34. package/native/darwin/README.md +1 -1
  35. package/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  36. package/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  37. package/native/darwin/src/darwin-platform.m +39 -0
  38. package/native/napi.h +2 -0
  39. package/package.json +3 -2
  40. package/dist/alt-screen-search.d.ts.map +0 -1
  41. package/dist/alt-screen-search.js.map +0 -1
  42. package/dist/autocomplete.d.ts.map +0 -1
  43. package/dist/autocomplete.js.map +0 -1
  44. package/dist/components/alt-screen-flash.d.ts.map +0 -1
  45. package/dist/components/alt-screen-flash.js.map +0 -1
  46. package/dist/components/box.d.ts.map +0 -1
  47. package/dist/components/box.js.map +0 -1
  48. package/dist/components/cancellable-loader.d.ts.map +0 -1
  49. package/dist/components/cancellable-loader.js.map +0 -1
  50. package/dist/components/editor-line-render.d.ts.map +0 -1
  51. package/dist/components/editor-line-render.js.map +0 -1
  52. package/dist/components/editor.d.ts.map +0 -1
  53. package/dist/components/editor.js.map +0 -1
  54. package/dist/components/h-stack.d.ts.map +0 -1
  55. package/dist/components/h-stack.js.map +0 -1
  56. package/dist/components/image.d.ts.map +0 -1
  57. package/dist/components/image.js.map +0 -1
  58. package/dist/components/input.d.ts.map +0 -1
  59. package/dist/components/input.js.map +0 -1
  60. package/dist/components/latex.d.ts.map +0 -1
  61. package/dist/components/latex.js.map +0 -1
  62. package/dist/components/loader.d.ts.map +0 -1
  63. package/dist/components/loader.js.map +0 -1
  64. package/dist/components/markdown.d.ts.map +0 -1
  65. package/dist/components/markdown.js.map +0 -1
  66. package/dist/components/mouse-region.d.ts.map +0 -1
  67. package/dist/components/mouse-region.js.map +0 -1
  68. package/dist/components/scroll-view.d.ts.map +0 -1
  69. package/dist/components/scroll-view.js.map +0 -1
  70. package/dist/components/select-list.d.ts.map +0 -1
  71. package/dist/components/select-list.js.map +0 -1
  72. package/dist/components/settings-list.d.ts.map +0 -1
  73. package/dist/components/settings-list.js.map +0 -1
  74. package/dist/components/spacer.d.ts.map +0 -1
  75. package/dist/components/spacer.js.map +0 -1
  76. package/dist/components/stack.d.ts.map +0 -1
  77. package/dist/components/stack.js.map +0 -1
  78. package/dist/components/text.d.ts.map +0 -1
  79. package/dist/components/text.js.map +0 -1
  80. package/dist/components/truncated-text.d.ts.map +0 -1
  81. package/dist/components/truncated-text.js.map +0 -1
  82. package/dist/components/v-stack.d.ts.map +0 -1
  83. package/dist/components/v-stack.js.map +0 -1
  84. package/dist/dollar-invocation-autocomplete.d.ts.map +0 -1
  85. package/dist/dollar-invocation-autocomplete.js.map +0 -1
  86. package/dist/editor-component.d.ts.map +0 -1
  87. package/dist/editor-component.js.map +0 -1
  88. package/dist/fuzzy.d.ts.map +0 -1
  89. package/dist/fuzzy.js.map +0 -1
  90. package/dist/image-markers.d.ts.map +0 -1
  91. package/dist/image-markers.js.map +0 -1
  92. package/dist/index.d.ts.map +0 -1
  93. package/dist/index.js.map +0 -1
  94. package/dist/keybindings.d.ts.map +0 -1
  95. package/dist/keybindings.js.map +0 -1
  96. package/dist/keys.d.ts.map +0 -1
  97. package/dist/keys.js.map +0 -1
  98. package/dist/kill-ring.d.ts.map +0 -1
  99. package/dist/kill-ring.js.map +0 -1
  100. package/dist/layout-node.d.ts.map +0 -1
  101. package/dist/layout-node.js.map +0 -1
  102. package/dist/layout.d.ts.map +0 -1
  103. package/dist/layout.js.map +0 -1
  104. package/dist/mouse-input.d.ts.map +0 -1
  105. package/dist/mouse-input.js.map +0 -1
  106. package/dist/mux.d.ts.map +0 -1
  107. package/dist/mux.js.map +0 -1
  108. package/dist/native-modifiers.d.ts.map +0 -1
  109. package/dist/native-modifiers.js.map +0 -1
  110. package/dist/native-module-path.d.ts.map +0 -1
  111. package/dist/native-module-path.js.map +0 -1
  112. package/dist/native-platform.d.ts.map +0 -1
  113. package/dist/native-platform.js.map +0 -1
  114. package/dist/paste-markers.d.ts.map +0 -1
  115. package/dist/paste-markers.js.map +0 -1
  116. package/dist/slash-command-autocomplete.d.ts.map +0 -1
  117. package/dist/slash-command-autocomplete.js.map +0 -1
  118. package/dist/stderr-observer.d.ts.map +0 -1
  119. package/dist/stderr-observer.js.map +0 -1
  120. package/dist/stdin-buffer.d.ts.map +0 -1
  121. package/dist/stdin-buffer.js.map +0 -1
  122. package/dist/terminal-capabilities.d.ts.map +0 -1
  123. package/dist/terminal-capabilities.js.map +0 -1
  124. package/dist/terminal-colors.d.ts.map +0 -1
  125. package/dist/terminal-colors.js.map +0 -1
  126. package/dist/terminal-image.d.ts.map +0 -1
  127. package/dist/terminal-image.js.map +0 -1
  128. package/dist/terminal-text.d.ts.map +0 -1
  129. package/dist/terminal-text.js.map +0 -1
  130. package/dist/terminal.d.ts.map +0 -1
  131. package/dist/terminal.js.map +0 -1
  132. package/dist/tmux-cursor-query.d.ts.map +0 -1
  133. package/dist/tmux-cursor-query.js.map +0 -1
  134. package/dist/tmux-focus.d.ts.map +0 -1
  135. package/dist/tmux-focus.js.map +0 -1
  136. package/dist/tmux-image-capability.d.ts.map +0 -1
  137. package/dist/tmux-image-capability.js.map +0 -1
  138. package/dist/tmux-image-probe.d.ts.map +0 -1
  139. package/dist/tmux-image-probe.js.map +0 -1
  140. package/dist/tui-alt-screen.d.ts.map +0 -1
  141. package/dist/tui-alt-screen.js.map +0 -1
  142. package/dist/tui-main-screen.d.ts.map +0 -1
  143. package/dist/tui-main-screen.js.map +0 -1
  144. package/dist/tui.d.ts.map +0 -1
  145. package/dist/tui.js.map +0 -1
  146. package/dist/undo-stack.d.ts.map +0 -1
  147. package/dist/undo-stack.js.map +0 -1
  148. package/dist/utils.d.ts.map +0 -1
  149. package/dist/utils.js.map +0 -1
  150. package/dist/word-navigation.d.ts.map +0 -1
  151. package/dist/word-navigation.js.map +0 -1
@@ -134,6 +134,7 @@ const MAX_FORMULA_LENGTH = 4096;
134
134
  const MAX_NESTING_DEPTH = 64;
135
135
  const LEADING_COMBINING_MARK_REGEX = /^\p{Mark}/u;
136
136
  const STYLE_COMMANDS = new Set(["\\mathrm", "\\mathbf", "\\mathit", "\\text", "\\operatorname"]);
137
+ const FONT_SWITCH_COMMANDS = new Set(["\\bf", "\\cal", "\\it", "\\rm", "\\sf", "\\sl", "\\tt"]);
137
138
  const scriptText = (text, alphabet) => {
138
139
  let output = "";
139
140
  for (const character of text) {
@@ -219,6 +220,10 @@ class LatexParser {
219
220
  const symbol = SYMBOLS[command];
220
221
  if (symbol !== undefined)
221
222
  return symbol;
223
+ if (FONT_SWITCH_COMMANDS.has(command)) {
224
+ this.skipSpaces();
225
+ return "";
226
+ }
222
227
  if (STYLE_COMMANDS.has(command)) {
223
228
  this.skipSpaces();
224
229
  return this.input[this.index] === "{" ? this.parseGroup(depth) : command;
@@ -2,6 +2,11 @@ import type { AutocompleteProvider } from "./autocomplete.ts";
2
2
  import type { EditorImageState } from "./image-markers.ts";
3
3
  import type { EditorPasteState } from "./paste-markers.ts";
4
4
  import type { Component } from "./tui.ts";
5
+ /** What a submission carried before the editor trimmed it for `onSubmit`. */
6
+ export interface EditorSubmitDetails {
7
+ /** The submitted text with paste markers expanded, before trimming. */
8
+ readonly rawText: string;
9
+ }
5
10
  /**
6
11
  * Interface for custom editor components.
7
12
  *
@@ -16,8 +21,8 @@ export interface EditorComponent extends Component {
16
21
  setText(text: string): void;
17
22
  /** Handle raw terminal input (key presses, paste sequences, etc.) */
18
23
  handleInput(data: string): void;
19
- /** Called when user submits (e.g., Enter key) */
20
- onSubmit?: (text: string) => void;
24
+ /** Called when user submits (e.g., Enter key); `details` is absent when an editor does not report it. */
25
+ onSubmit?: (text: string, details?: EditorSubmitDetails) => void;
21
26
  /** Called when text changes */
22
27
  onChange?: (text: string) => void;
23
28
  /** Add text to history for up/down navigation */
package/dist/fuzzy.js CHANGED
@@ -45,30 +45,31 @@ function scoreMatch(queryLower, textLower) {
45
45
  let score = 0;
46
46
  let lastMatchIndex = -1;
47
47
  let consecutiveMatches = 0;
48
- for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
49
- if (textLower[i] === queryLower[queryIndex]) {
50
- const isWordBoundary = i === 0 || isWordBoundaryPrefix(textLower[i - 1]);
51
- // Reward consecutive matches
52
- if (lastMatchIndex === i - 1) {
53
- consecutiveMatches++;
54
- score -= consecutiveMatches * 5;
55
- }
56
- else {
57
- consecutiveMatches = 0;
58
- // Penalize gaps
59
- if (lastMatchIndex >= 0) {
60
- score += (i - lastMatchIndex - 1) * 2;
61
- }
62
- }
63
- // Reward word boundary matches
64
- if (isWordBoundary) {
65
- score -= 10;
48
+ while (queryIndex < queryLower.length) {
49
+ const i = textLower.indexOf(queryLower[queryIndex], lastMatchIndex + 1);
50
+ if (i === -1)
51
+ break;
52
+ const isWordBoundary = i === 0 || isWordBoundaryPrefix(textLower[i - 1]);
53
+ // Reward consecutive matches
54
+ if (lastMatchIndex === i - 1) {
55
+ consecutiveMatches++;
56
+ score -= consecutiveMatches * 5;
57
+ }
58
+ else {
59
+ consecutiveMatches = 0;
60
+ // Penalize gaps
61
+ if (lastMatchIndex >= 0) {
62
+ score += (i - lastMatchIndex - 1) * 2;
66
63
  }
67
- // Slight penalty for later matches
68
- score += i * 0.1;
69
- lastMatchIndex = i;
70
- queryIndex++;
71
64
  }
65
+ // Reward word boundary matches
66
+ if (isWordBoundary) {
67
+ score -= 10;
68
+ }
69
+ // Slight penalty for later matches
70
+ score += i * 0.1;
71
+ lastMatchIndex = i;
72
+ queryIndex++;
72
73
  }
73
74
  if (queryIndex < queryLower.length) {
74
75
  return { matches: false, score: 0 };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { CellDimensions, ImageDimensions } from "./terminal-image.ts";
2
2
  export { Marked, type Token, type Tokens } from "marked";
3
3
  export { AltScreenSearchComponent, type AltScreenSearchMatch, type AltScreenSearchSegment, findAltScreenSearchMatches, getAltScreenSearchMatchKey, } from "./alt-screen-search.ts";
4
4
  export { type AutocompleteItem, type AutocompleteProvider, type AutocompleteSuggestions, CombinedAutocompleteProvider, type SlashCommand, } from "./autocomplete.ts";
5
+ export { backgroundAnsi, type Color, type ColorMixSpace, colorToHex, colorToOkhsl, colorToOklch, colorToRgb, foregroundAnsi, type IndexedColor, indexedColor, mixColors, type OkhslChannels, type OklchChannels, type OklchColorValue, okhslColor, oklchColor, parseColor, type RgbColorValue, rgbColor, styleText, styleTextWithAnsi, type TerminalColorMode, type TextAttributes, type TextStyle, } from "./colors.ts";
5
6
  export { Box } from "./components/box.ts";
6
7
  export { CancellableLoader } from "./components/cancellable-loader.ts";
7
8
  export { Editor, type EditorOptions, type EditorTheme, } from "./components/editor.ts";
@@ -18,22 +19,24 @@ export { Spacer } from "./components/spacer.ts";
18
19
  export { Text } from "./components/text.ts";
19
20
  export { TruncatedText } from "./components/truncated-text.ts";
20
21
  export { type StackChild, type StackEntry, type StackEntryOptions, type StackOptions, VStack, } from "./components/v-stack.ts";
21
- export type { EditorComponent } from "./editor-component.ts";
22
+ export type { EditorComponent, EditorSubmitDetails } from "./editor-component.ts";
22
23
  export { type FuzzyMatch, fuzzyFilter, fuzzyMatch, fuzzyMatchLower } from "./fuzzy.ts";
23
24
  export { type EditorImageState, formatImageMarker, IMAGE_MARKER_REGEX, IMAGE_MARKER_SINGLE, type ImageMarkerCanonicalization, ImageMarkerRegistry, type ImageMarkerRemoval, imageMarkerId, isImageMarker, } from "./image-markers.ts";
24
25
  export { getKeybindings, type Keybinding, type KeybindingConflict, type KeybindingDefinition, type KeybindingDefinitions, type Keybindings, type KeybindingsConfig, KeybindingsManager, setKeybindings, TUI_KEYBINDINGS, } from "./keybindings.ts";
25
26
  export { decodeKittyPrintable, isKeyRelease, isKeyRepeat, isKittyProtocolActive, Key, type KeyEventType, type KeyId, matchesKey, parseKey, setKittyProtocolActive, } from "./keys.ts";
26
27
  export { decodeMouseButton, isMouseSequence, MOUSE_TRACKING, MouseClickSynthesizer, parseSgrMouseEvent, parseWheelEvent, type SgrMouseEvent, toTuiMouseEvent, type WheelEvent, } from "./mouse-input.ts";
27
28
  export { getNativeClipboard, type NativeClipboard } from "./native-platform.ts";
29
+ export { oklabToOkhslLightness } from "./oklab.ts";
28
30
  export { type EditorPasteState, expandPasteMarkers } from "./paste-markers.ts";
29
31
  export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.ts";
30
32
  export { type CursorPosition, ProcessTerminal, type ProcessTerminalOptions, type Terminal } from "./terminal.ts";
31
- export { parseOsc11BackgroundColor, parseTerminalColorSchemeReport, type RgbColor, type TerminalColorScheme, } from "./terminal-colors.ts";
33
+ export { parseTerminalColorSchemeReport, type RgbColor, type TerminalColorScheme, type TerminalColors, } from "./terminal-colors.ts";
32
34
  export declare function calculateImageRows(imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions?: CellDimensions): number;
33
- export { allocateImageId, buildKittyPlaceholderRow, type CellDimensions, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getWebpDimensions, hyperlink, type ImageDimensions, type ImageProtocol, type ImageRenderOptions, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, type TerminalCapabilities, type TmuxPassthroughState, wrapTmuxPassthrough, } from "./terminal-image.ts";
35
+ export { allocateImageId, buildKittyPlaceholderRow, type CellDimensions, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, type ImageDimensions, type ImageProtocol, type ImageRenderOptions, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, type TerminalCapabilities, type TmuxPassthroughState, wrapTmuxPassthrough, } from "./terminal-image.ts";
34
36
  export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.ts";
35
37
  export { type Component, Container, CURSOR_MARKER, compositeTuiLine, type Focusable, isFocusable, isViewportTUI, type OverlayAnchor, type OverlayBounds, type OverlayHandle, type OverlayMargin, type OverlayOptions, type OverlayUnfocusOptions, type SizeValue, TUI, type TuiInputListener, type TuiInputListenerResult, type TuiMode, type TuiMouseButton, type TuiMouseEvent, type TuiMouseEventResult, type TuiMouseEventType, type TuiStopOptions, type ViewportTUI, } from "./tui.ts";
36
38
  export { TuiAltScreen, type TuiAltScreenOptions } from "./tui-alt-screen.ts";
37
39
  export { TuiMainScreen, type TuiMainScreenRenderState } from "./tui-main-screen.ts";
38
40
  export { getGraphemeSegmenter, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "./utils.ts";
41
+ export type { WheelScrollLines } from "./wheel-scroll.ts";
39
42
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ export { Marked } from "marked";
5
5
  export { AltScreenSearchComponent, findAltScreenSearchMatches, getAltScreenSearchMatchKey, } from "./alt-screen-search.js";
6
6
  // Autocomplete support
7
7
  export { CombinedAutocompleteProvider, } from "./autocomplete.js";
8
+ // Colors and styling
9
+ export { backgroundAnsi, colorToHex, colorToOkhsl, colorToOklch, colorToRgb, foregroundAnsi, indexedColor, mixColors, okhslColor, oklchColor, parseColor, rgbColor, styleText, styleTextWithAnsi, } from "./colors.js";
8
10
  // Components
9
11
  export { Box } from "./components/box.js";
10
12
  export { CancellableLoader } from "./components/cancellable-loader.js";
@@ -33,18 +35,19 @@ export { decodeKittyPrintable, isKeyRelease, isKeyRepeat, isKittyProtocolActive,
33
35
  export { decodeMouseButton, isMouseSequence, MOUSE_TRACKING, MouseClickSynthesizer, parseSgrMouseEvent, parseWheelEvent, toTuiMouseEvent, } from "./mouse-input.js";
34
36
  // Native platform integration
35
37
  export { getNativeClipboard } from "./native-platform.js";
38
+ export { oklabToOkhslLightness } from "./oklab.js";
36
39
  export { expandPasteMarkers } from "./paste-markers.js";
37
40
  // Input buffering for batch splitting
38
41
  export { StdinBuffer } from "./stdin-buffer.js";
39
42
  // Terminal interface and implementations
40
43
  export { ProcessTerminal } from "./terminal.js";
41
44
  // Terminal colors
42
- export { parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
45
+ export { parseTerminalColorSchemeReport, } from "./terminal-colors.js";
43
46
  // Terminal image support
44
47
  export function calculateImageRows(imageDimensions, targetWidthCells, cellDimensions = { widthPx: 9, heightPx: 18 }) {
45
48
  return calculateImageCellSize(imageDimensions, targetWidthCells, undefined, cellDimensions).rows;
46
49
  }
47
- export { allocateImageId, buildKittyPlaceholderRow, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getWebpDimensions, hyperlink, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, wrapTmuxPassthrough, } from "./terminal-image.js";
50
+ export { allocateImageId, buildKittyPlaceholderRow, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, wrapTmuxPassthrough, } from "./terminal-image.js";
48
51
  export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.js";
49
52
  export { Container, CURSOR_MARKER, compositeTuiLine, isFocusable, isViewportTUI, TUI, } from "./tui.js";
50
53
  export { TuiAltScreen } from "./tui-alt-screen.js";
@@ -4,6 +4,8 @@ export interface NativeClipboard {
4
4
  getText(): Promise<string | null | undefined>;
5
5
  /** Undefined means unavailable, null means no image; transfer failures reject. */
6
6
  getImage(): Promise<Uint8Array | null | undefined>;
7
+ /** POSIX paths of file URLs on the clipboard. Undefined means unsupported, null means no files. */
8
+ getFilePaths?(): Promise<string[] | null | undefined>;
7
9
  /** Linux uses command-line tools to retain clipboard ownership instead. */
8
10
  setText?(text: string): Promise<void>;
9
11
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Oklab and OKHSL <-> sRGB conversion. `colors.ts` builds its OKLCH, OKHSL, and color mixing on it.
3
+ *
4
+ * Oklab and OKHSL are Björn Ottosson's color spaces; OKHSL's saturation is relative to the sRGB gamut at
5
+ * each hue and lightness. This is a port of his reference implementation (https://bottosson.github.io/posts/colorpicker/),
6
+ * Copyright (c) 2021 Björn Ottosson, used under the MIT license:
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
9
+ * associated documentation files (the "Software"), to deal in the Software without restriction, including
10
+ * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
12
+ * following conditions: The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
14
+ * KIND, EXPRESS OR IMPLIED.
15
+ */
16
+ import type { RgbColor } from "./terminal-colors.ts";
17
+ type Vector = [number, number, number];
18
+ /** Oklab lightness to OKHSL lightness. */
19
+ export declare const oklabToOkhslLightness: (x: number) => number;
20
+ /** Oklab [L, a, b] to linear sRGB [r, g, b] (0-1, may leave the gamut). */
21
+ export declare function oklabToLinearSrgb(lab: Vector): Vector;
22
+ /** sRGB channels (0-255) to Oklab [L, a, b]. */
23
+ export declare function rgbToOklab({ r, g, b }: RgbColor): Vector;
24
+ /** Linear sRGB [r, g, b] to sRGB channels (0-255, rounded), clipping out-of-gamut channels. */
25
+ export declare function linearSrgbToRgb(linear: Vector): RgbColor;
26
+ /**
27
+ * Convert OKHSL to sRGB channels (0-255, rounded), clipping out-of-gamut channels.
28
+ * @param hue Hue in degrees.
29
+ * @param saturation Saturation, 0-1.
30
+ * @param lightness Lightness, 0-1.
31
+ */
32
+ export declare function okhslToRgb(hue: number, saturation: number, lightness: number): RgbColor;
33
+ /**
34
+ * Convert sRGB channels (0-255) to OKHSL.
35
+ * @returns Hue `h` in degrees (0 for grays), saturation `s` and lightness `l` 0-1.
36
+ */
37
+ export declare function rgbToOkhsl(rgb: RgbColor): {
38
+ h: number;
39
+ s: number;
40
+ l: number;
41
+ };
42
+ export {};
43
+ //# sourceMappingURL=oklab.d.ts.map
package/dist/oklab.js ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Oklab and OKHSL <-> sRGB conversion. `colors.ts` builds its OKLCH, OKHSL, and color mixing on it.
3
+ *
4
+ * Oklab and OKHSL are Björn Ottosson's color spaces; OKHSL's saturation is relative to the sRGB gamut at
5
+ * each hue and lightness. This is a port of his reference implementation (https://bottosson.github.io/posts/colorpicker/),
6
+ * Copyright (c) 2021 Björn Ottosson, used under the MIT license:
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
9
+ * associated documentation files (the "Software"), to deal in the Software without restriction, including
10
+ * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
12
+ * following conditions: The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
14
+ * KIND, EXPRESS OR IMPLIED.
15
+ */
16
+ const multiply = (m, [x, y, z]) => m.map((row) => row[0] * x + row[1] * y + row[2] * z);
17
+ // ============================================================================
18
+ // OKHSL <-> sRGB
19
+ // ============================================================================
20
+ const LINEAR_SRGB_TO_LMS = [
21
+ [0.4122214694707629, 0.5363325372617349, 0.0514459932675022],
22
+ [0.2119034958178251, 0.6806995506452344, 0.1073969535369405],
23
+ [0.0883024591900564, 0.2817188391361215, 0.6299787016738222],
24
+ ];
25
+ const LMS_TO_LAB = [
26
+ [0.210454268309314, 0.793617774702305, -0.0040720430116193],
27
+ [1.9779985324311684, -2.42859224204858, 0.450593709617411],
28
+ [0.0259040424655478, 0.7827717124575296, -0.8086757549230774],
29
+ ];
30
+ const LAB_TO_LMS = [
31
+ [1, 0.3963377773761749, 0.2158037573099136],
32
+ [1, -0.1055613458156586, -0.0638541728258133],
33
+ [1, -0.0894841775298119, -1.2914855480194092],
34
+ ];
35
+ const LMS_TO_LINEAR_SRGB = [
36
+ [4.0767416360759583, -3.3077115392580629, 0.2309699031821043],
37
+ [-1.2684379732850315, 2.6097573492876882, -0.341319376002657],
38
+ [-0.0041960761386756, -0.7034186179359362, 1.7076146940746117],
39
+ ];
40
+ /**
41
+ * Per sRGB channel (red, green, blue): the (a, b) half-plane where that channel clips
42
+ * first, and the polynomial approximating the maximum saturation there.
43
+ */
44
+ const SATURATION_FIT = [
45
+ [
46
+ [-1.8817031, -0.80936501],
47
+ [1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245],
48
+ ],
49
+ [
50
+ [1.8144408, -1.19445267],
51
+ [0.73956515, -0.45954404, 0.08285427, 0.12541073, -0.14503204],
52
+ ],
53
+ [
54
+ [0.13110758, 1.81333971],
55
+ [1.35733652, -0.00915799, -1.1513021, -0.50559606, 0.00692167],
56
+ ],
57
+ ];
58
+ const K1 = 0.206;
59
+ const K2 = 0.03;
60
+ const K3 = (1 + K1) / (1 + K2);
61
+ /** Oklab lightness to OKHSL lightness. */
62
+ export const oklabToOkhslLightness = (x) => 0.5 * (K3 * x - K1 + Math.sqrt((K3 * x - K1) ** 2 + 4 * K2 * K3 * x));
63
+ /** OKHSL lightness to Oklab lightness. */
64
+ const okhslToOklabLightness = (x) => (x * x + K1 * x) / (K3 * (x + K2));
65
+ /** sRGB transfer function: linear to encoded channel, both 0-1. */
66
+ const linearToSrgb = (value) => value > 0.0031308 ? 1.055 * value ** (1 / 2.4) - 0.055 : 12.92 * value;
67
+ /** Inverse sRGB transfer function: encoded to linear channel, both 0-1. */
68
+ const srgbToLinear = (value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
69
+ /** Oklab [L, a, b] to linear sRGB [r, g, b] (0-1, may leave the gamut). */
70
+ export function oklabToLinearSrgb(lab) {
71
+ return multiply(LMS_TO_LINEAR_SRGB, multiply(LAB_TO_LMS, lab).map((value) => value ** 3));
72
+ }
73
+ /** Linear sRGB [r, g, b] (0-1) to Oklab [L, a, b]. */
74
+ function linearSrgbToOklab(rgb) {
75
+ return multiply(LMS_TO_LAB, multiply(LINEAR_SRGB_TO_LMS, rgb).map(Math.cbrt));
76
+ }
77
+ /** sRGB channels (0-255) to Oklab [L, a, b]. */
78
+ export function rgbToOklab({ r, g, b }) {
79
+ return linearSrgbToOklab([r / 255, g / 255, b / 255].map(srgbToLinear));
80
+ }
81
+ /** Linear sRGB [r, g, b] to sRGB channels (0-255, rounded), clipping out-of-gamut channels. */
82
+ export function linearSrgbToRgb(linear) {
83
+ const [r, g, b] = linear.map((value) => Math.round(Math.min(1, Math.max(0, linearToSrgb(value))) * 255));
84
+ return { r, g, b };
85
+ }
86
+ /** Rate of change of each cube-root LMS component along a chroma direction (a, b). */
87
+ function lmsSlopes(a, b) {
88
+ return [LAB_TO_LMS[0], LAB_TO_LMS[1], LAB_TO_LMS[2]].map((row) => row[1] * a + row[2] * b);
89
+ }
90
+ /** Largest saturation (C/L) inside sRGB for hue (a, b): polynomial fit plus one Halley step. */
91
+ function maxSaturation(a, b) {
92
+ const channel = SATURATION_FIT.findIndex(([[x, y]], index) => index === 2 || x * a + y * b > 1);
93
+ const [k0, k1, k2, k3, k4] = SATURATION_FIT[channel][1];
94
+ const weights = LMS_TO_LINEAR_SRGB[channel];
95
+ const saturation = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b;
96
+ const slopes = lmsSlopes(a, b);
97
+ const base = slopes.map((k) => 1 + saturation * k);
98
+ const dot = (values) => values.reduce((sum, value, index) => sum + weights[index] * value, 0);
99
+ const f = dot(base.map((value) => value ** 3));
100
+ const f1 = dot(base.map((value, index) => 3 * slopes[index] * value ** 2));
101
+ const f2 = dot(base.map((value, index) => 6 * slopes[index] ** 2 * value));
102
+ return saturation - (f * f1) / (f1 * f1 - 0.5 * f * f2);
103
+ }
104
+ /** Oklab lightness and chroma of the most saturated sRGB color of hue (a, b). */
105
+ function cusp(a, b) {
106
+ const saturation = maxSaturation(a, b);
107
+ const lightness = Math.cbrt(1 / Math.max(...oklabToLinearSrgb([1, saturation * a, saturation * b])));
108
+ return [lightness, lightness * saturation];
109
+ }
110
+ /** Chroma where the constant-lightness line at `lightness` leaves the sRGB gamut. */
111
+ function maxChroma(a, b, lightness, [cuspL, cuspC]) {
112
+ if (lightness <= cuspL)
113
+ return (cuspC * lightness) / cuspL;
114
+ // Upper half: triangle edge, then one Halley step against each channel reaching 1.
115
+ const t = (cuspC * (lightness - 1)) / (cuspL - 1);
116
+ const slopes = lmsSlopes(a, b);
117
+ const lms = slopes.map((k) => lightness + t * k);
118
+ const cubes = lms.map((value) => value ** 3);
119
+ const first = lms.map((value, index) => 3 * slopes[index] * value ** 2);
120
+ const second = lms.map((value, index) => 6 * slopes[index] ** 2 * value);
121
+ const dot = (row, values) => row[0] * values[0] + row[1] * values[1] + row[2] * values[2];
122
+ const steps = LMS_TO_LINEAR_SRGB.map((row) => {
123
+ const f = dot(row, cubes) - 1;
124
+ const f1 = dot(row, first);
125
+ const f2 = dot(row, second);
126
+ const u = f1 / (f1 * f1 - 0.5 * f * f2);
127
+ return u >= 0 ? -f * u : Number.MAX_VALUE;
128
+ });
129
+ return t + Math.min(...steps);
130
+ }
131
+ /** OKHSL's chroma reference points at lightness L and hue (a, b): [c0, cMid, cMax]. */
132
+ function chromaStops(L, a, b) {
133
+ const peak = cusp(a, b);
134
+ const cMax = maxChroma(a, b, L, peak);
135
+ const k = cMax / Math.min(L * (peak[1] / peak[0]), (1 - L) * (peak[1] / (1 - peak[0])));
136
+ const midS = 0.11516993 +
137
+ 1 /
138
+ (7.4477897 +
139
+ 4.1590124 * b +
140
+ a *
141
+ (-2.19557347 +
142
+ 1.75198401 * b +
143
+ a * (-2.13704948 - 10.02301043 * b + a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a))));
144
+ const midT = 0.11239642 +
145
+ 1 /
146
+ (1.6132032 -
147
+ 0.68124379 * b +
148
+ a *
149
+ (0.40370612 +
150
+ 0.90148123 * b +
151
+ a * (-0.27087943 + 0.6122399 * b + a * (0.00299215 - 0.45399568 * b - 0.14661872 * a))));
152
+ const cMid = 0.9 * k * Math.sqrt(Math.sqrt(1 / (1 / (L * midS) ** 4 + 1 / ((1 - L) * midT) ** 4)));
153
+ const c0 = Math.sqrt(1 / (1 / (L * 0.4) ** 2 + 1 / ((1 - L) * 0.8) ** 2));
154
+ return [c0, cMid, cMax];
155
+ }
156
+ /**
157
+ * Convert OKHSL to sRGB channels (0-255, rounded), clipping out-of-gamut channels.
158
+ * @param hue Hue in degrees.
159
+ * @param saturation Saturation, 0-1.
160
+ * @param lightness Lightness, 0-1.
161
+ */
162
+ export function okhslToRgb(hue, saturation, lightness) {
163
+ const L = okhslToOklabLightness(lightness);
164
+ let lab = [L, 0, 0];
165
+ if (L > 0 && L < 1 && saturation > 0) {
166
+ const angle = (2 * Math.PI * (((hue % 360) + 360) % 360)) / 360;
167
+ const a = Math.cos(angle);
168
+ const b = Math.sin(angle);
169
+ const [c0, cMid, cMax] = chromaStops(L, a, b);
170
+ // Chroma rises from 0 through cMid at s = 0.8 to cMax at s = 1.
171
+ let chroma;
172
+ if (saturation < 0.8) {
173
+ const t = 1.25 * saturation;
174
+ const k1 = 0.8 * c0;
175
+ chroma = (t * k1) / (1 - (1 - k1 / cMid) * t);
176
+ }
177
+ else {
178
+ const t = 5 * (saturation - 0.8);
179
+ const k1 = (0.2 * cMid ** 2 * 1.25 ** 2) / c0;
180
+ chroma = cMid + (t * k1) / (1 - (1 - k1 / (cMax - cMid)) * t);
181
+ }
182
+ lab = [L, chroma * a, chroma * b];
183
+ }
184
+ return linearSrgbToRgb(oklabToLinearSrgb(lab));
185
+ }
186
+ /**
187
+ * Convert sRGB channels (0-255) to OKHSL.
188
+ * @returns Hue `h` in degrees (0 for grays), saturation `s` and lightness `l` 0-1.
189
+ */
190
+ export function rgbToOkhsl(rgb) {
191
+ const [L, labA, labB] = rgbToOklab(rgb);
192
+ const chroma = Math.hypot(labA, labB);
193
+ const lightness = oklabToOkhslLightness(L);
194
+ if (chroma < 1e-9 || lightness <= 0 || lightness >= 1)
195
+ return { h: 0, s: 0, l: lightness };
196
+ const hue = ((Math.atan2(labB, labA) * 180) / Math.PI + 360) % 360;
197
+ const [c0, cMid, cMax] = chromaStops(L, labA / chroma, labB / chroma);
198
+ let saturation;
199
+ if (chroma < cMid) {
200
+ const k1 = 0.8 * c0;
201
+ saturation = 0.8 * (chroma / (k1 + (1 - k1 / cMid) * chroma));
202
+ }
203
+ else {
204
+ const k1 = (0.2 * cMid ** 2 * 1.25 ** 2) / c0;
205
+ const offset = chroma - cMid;
206
+ saturation = 0.8 + 0.2 * (offset / (k1 + (1 - k1 / (cMax - cMid)) * offset));
207
+ }
208
+ return { h: hue, s: Math.min(1, Math.max(0, saturation)), l: lightness };
209
+ }
210
+ //# sourceMappingURL=oklab.js.map
@@ -46,6 +46,7 @@ export function getSlashCommandSuggestions(commands, prefix) {
46
46
  label: name,
47
47
  description: fullDesc || undefined,
48
48
  searchText: isSkill && !explicitSkillNamespace ? skillName : name,
49
+ awaitsArguments: hint !== undefined,
49
50
  },
50
51
  ];
51
52
  });
@@ -58,6 +59,7 @@ export function getSlashCommandSuggestions(commands, prefix) {
58
59
  label: SKILL_COMMAND_PREFIX,
59
60
  description: "Browse available skills",
60
61
  searchText: SKILL_COMMAND_PREFIX,
62
+ awaitsArguments: false,
61
63
  });
62
64
  }
63
65
  return fuzzyFilter(commandItems, prefix, (item) => item.searchText)
@@ -65,6 +67,7 @@ export function getSlashCommandSuggestions(commands, prefix) {
65
67
  value: item.name,
66
68
  label: item.label,
67
69
  ...(item.description && { description: item.description }),
70
+ ...(item.awaitsArguments && { awaitsArguments: true }),
68
71
  index,
69
72
  }))
70
73
  .sort((left, right) => compareSlashCommandSuggestion(normalizedPrefix, left, right))
@@ -9,7 +9,7 @@ export function detectTerminalCapabilities(env = process.env, platform = process
9
9
  const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
10
10
  const terminalEmulator = env.TERMINAL_EMULATOR ?? "";
11
11
  const colorTerm = env.COLORTERM ?? "";
12
- const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
12
+ const trueColor = colorTerm === "truecolor" || colorTerm === "24bit" || term.toLowerCase().endsWith("-direct");
13
13
  if (env.TMUX || term.startsWith("tmux")) {
14
14
  const state = probeTmuxImageState(env, execTmux);
15
15
  const override = parseTmuxKittyTerminalOverride(env.PI_TUI_TMUX_KITTY_TERMINAL);
@@ -4,7 +4,24 @@ export interface RgbColor {
4
4
  b: number;
5
5
  }
6
6
  export type TerminalColorScheme = "dark" | "light";
7
- export declare function isOsc11BackgroundColorResponse(data: string): boolean;
8
- export declare function parseOsc11BackgroundColor(data: string): RgbColor | undefined;
7
+ /** Colors the terminal reports for its current theme. */
8
+ export interface TerminalColors {
9
+ /** Default foreground (OSC 10). */
10
+ foreground?: RgbColor;
11
+ /** Default background (OSC 11). */
12
+ background?: RgbColor;
13
+ /** ANSI colors 0-15 (OSC 4). Only set when the terminal reported all 16. */
14
+ palette?: RgbColor[];
15
+ }
16
+ /** What an OSC color reply reports: the default foreground (OSC 10), background (OSC 11), or a palette index (OSC 4). */
17
+ export type OscColorTarget = "foreground" | "background" | number;
18
+ /**
19
+ * Parse an OSC 10, 11, or 4 color reply. Returns undefined when `data` is not such a reply;
20
+ * `rgb` is undefined when it is a reply with an unparseable color.
21
+ */
22
+ export declare function parseOscColorResponse(data: string): {
23
+ target: OscColorTarget;
24
+ rgb: RgbColor | undefined;
25
+ } | undefined;
9
26
  export declare function parseTerminalColorSchemeReport(data: string): TerminalColorScheme | undefined;
10
27
  //# sourceMappingURL=terminal-colors.d.ts.map
@@ -15,17 +15,22 @@ function parseOscHexChannel(channel) {
15
15
  }
16
16
  return Math.round((parseInt(channel, 16) / max) * 255);
17
17
  }
18
- const OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN = /^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i;
18
+ const OSC_COLOR_RESPONSE_PATTERN = /^\x1b\](?:(1[01])|4;(\d{1,3}));([^\x07\x1b]*)(?:\x07|\x1b\\)$/i;
19
19
  const COLOR_SCHEME_REPORT_PATTERN = /^(?:\x1b\[\?997;(1|2)n)+$/;
20
- export function isOsc11BackgroundColorResponse(data) {
21
- return OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN.test(data);
22
- }
23
- export function parseOsc11BackgroundColor(data) {
24
- const match = data.match(OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN);
20
+ /**
21
+ * Parse an OSC 10, 11, or 4 color reply. Returns undefined when `data` is not such a reply;
22
+ * `rgb` is undefined when it is a reply with an unparseable color.
23
+ */
24
+ export function parseOscColorResponse(data) {
25
+ const match = data.match(OSC_COLOR_RESPONSE_PATTERN);
25
26
  if (!match) {
26
27
  return undefined;
27
28
  }
28
- const value = match[1].trim();
29
+ const target = match[1] === "10" ? "foreground" : match[1] === "11" ? "background" : Number.parseInt(match[2], 10);
30
+ return { target, rgb: parseOscColorValue(match[3]) };
31
+ }
32
+ function parseOscColorValue(rawValue) {
33
+ const value = rawValue.trim();
29
34
  if (value.startsWith("#")) {
30
35
  const hex = value.slice(1);
31
36
  if (/^[0-9a-f]{6}$/i.test(hex)) {
@@ -1,3 +1,4 @@
1
+ import type { TerminalColorMode } from "./colors.ts";
1
2
  import { type DetectedTerminalCapabilities, type TerminalCapabilities } from "./terminal-capabilities.ts";
2
3
  export type { ImageProtocol, TerminalCapabilities } from "./terminal-capabilities.ts";
3
4
  export { outerKittyGraphicsMode } from "./terminal-capabilities.ts";
@@ -28,6 +29,7 @@ export declare function getCellDimensions(): CellDimensions;
28
29
  export declare function setCellDimensions(dims: CellDimensions): void;
29
30
  export declare function detectCapabilities(tmuxForwardsHyperlink?: () => boolean, tmuxPassthroughState?: () => TmuxPassthroughState): DetectedTerminalCapabilities;
30
31
  export declare function getCapabilities(): TerminalCapabilities;
32
+ export declare function getTerminalColorMode(capabilities?: TerminalCapabilities): TerminalColorMode;
31
33
  export declare function resetCapabilitiesCache(): void;
32
34
  /** Override selected auto-detected capabilities. */
33
35
  export declare function setCapabilityOverrides(overrides: Partial<TerminalCapabilities>): void;
@@ -109,7 +111,7 @@ export declare function getKittyImageMetadata(line: string): KittyImageMetadata
109
111
  /** Build a placement-only command for an image line emitted by {@link renderImage}. */
110
112
  export declare function getKittyImagePlacement(line: string): KittyImagePlacement | undefined;
111
113
  export declare function cropKittyImageLine(line: string, hiddenRows: number, visibleRows: number): string;
112
- export declare function calculateImageCellSize(imageDimensions: ImageDimensions, maxWidthCells: number, maxHeightCells?: number, cellDimensions?: CellDimensions): ImageCellSize;
114
+ export declare function calculateImageCellSize(imageDimensions: ImageDimensions, maxWidthCells: number, maxHeightCells?: number, cellDimensions?: CellDimensions, optimizeAspectRatio?: boolean): ImageCellSize;
113
115
  export declare function getPngDimensions(base64Data: string): ImageDimensions | null;
114
116
  export declare function getJpegDimensions(base64Data: string): ImageDimensions | null;
115
117
  export declare function getGifDimensions(base64Data: string): ImageDimensions | null;
@@ -35,7 +35,7 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
35
35
  const terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || "";
36
36
  const term = process.env.TERM?.toLowerCase() || "";
37
37
  const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
38
- const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit";
38
+ const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit" || term.endsWith("-direct");
39
39
  const isWindowsConsole = process.platform === "win32";
40
40
  // Emit OSC 8 hyperlinks only when tmux confirms it forwards.
41
41
  // Image protocols are unreliable under tmux, so leave `images: null`.
@@ -132,6 +132,9 @@ export function getCapabilities() {
132
132
  }
133
133
  return cachedCapabilities;
134
134
  }
135
+ export function getTerminalColorMode(capabilities = getCapabilities()) {
136
+ return capabilities.trueColor ? "truecolor" : "256color";
137
+ }
135
138
  export function resetCapabilitiesCache() {
136
139
  cachedCapabilities = null;
137
140
  }
@@ -415,7 +418,15 @@ export function cropKittyImageLine(line, hiddenRows, visibleRows) {
415
418
  controls.push(`y=${sourceY}`, `h=${sourceHeight}`, `r=${croppedRows}`);
416
419
  return `${line.slice(0, match.index)}\x1b_G${controls.join(",")};${line.slice(match.index + match[0].length)}`;
417
420
  }
418
- export function calculateImageCellSize(imageDimensions, maxWidthCells, maxHeightCells, cellDimensions = { widthPx: 9, heightPx: 18 }) {
421
+ function chooseLessDistortedCellCount(upperCount, idealCount) {
422
+ if (upperCount <= 1)
423
+ return upperCount;
424
+ const lowerCount = upperCount - 1;
425
+ const upperDistortion = Math.max(upperCount / idealCount, idealCount / upperCount);
426
+ const lowerDistortion = Math.max(lowerCount / idealCount, idealCount / lowerCount);
427
+ return lowerDistortion < upperDistortion ? lowerCount : upperCount;
428
+ }
429
+ export function calculateImageCellSize(imageDimensions, maxWidthCells, maxHeightCells, cellDimensions = { widthPx: 9, heightPx: 18 }, optimizeAspectRatio = false) {
419
430
  const maxWidth = Math.max(1, Math.floor(maxWidthCells));
420
431
  const maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));
421
432
  const imageWidth = Math.max(1, imageDimensions.widthPx);
@@ -425,12 +436,24 @@ export function calculateImageCellSize(imageDimensions, maxWidthCells, maxHeight
425
436
  const scale = Math.min(widthScale, heightScale);
426
437
  const scaledWidthPx = imageWidth * scale;
427
438
  const scaledHeightPx = imageHeight * scale;
428
- const columns = Math.ceil(scaledWidthPx / cellDimensions.widthPx);
429
- const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
430
- return {
431
- columns: Math.max(1, Math.min(maxWidth, columns)),
432
- rows: Math.max(1, maxHeight === undefined ? rows : Math.min(maxHeight, rows)),
433
- };
439
+ let columns = Math.max(1, Math.min(maxWidth, Math.ceil(scaledWidthPx / cellDimensions.widthPx)));
440
+ const heightRows = scaledHeightPx / cellDimensions.heightPx;
441
+ let rows = Math.max(1, Math.ceil(heightRows));
442
+ if (maxHeight !== undefined) {
443
+ rows = Math.min(maxHeight, rows);
444
+ }
445
+ if (!optimizeAspectRatio) {
446
+ return { columns, rows };
447
+ }
448
+ if (widthScale <= heightScale) {
449
+ const idealRows = (columns * cellDimensions.widthPx * imageHeight) / (imageWidth * cellDimensions.heightPx);
450
+ rows = chooseLessDistortedCellCount(rows, idealRows);
451
+ }
452
+ else {
453
+ const idealColumns = (rows * cellDimensions.heightPx * imageWidth) / (imageHeight * cellDimensions.widthPx);
454
+ columns = chooseLessDistortedCellCount(columns, idealColumns);
455
+ }
456
+ return { columns, rows };
434
457
  }
435
458
  export function getPngDimensions(base64Data) {
436
459
  try {
@@ -564,7 +587,8 @@ export function renderImage(base64Data, imageDimensions, options = {}) {
564
587
  return null;
565
588
  }
566
589
  const maxWidth = options.maxWidthCells ?? 80;
567
- const size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions());
590
+ // Reduce Kitty's cell-aligned distortion without shrinking iTerm2 reservations.
591
+ const size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions(), caps.images === "kitty");
568
592
  if (caps.images === "kitty" && caps.kittyUnicodePlaceholders) {
569
593
  // Virtual placement: transmit once, then show the image with plain-text
570
594
  // placeholder cells. Under tmux the placeholder cells move/clip with the
@@ -97,6 +97,8 @@ export declare class ProcessTerminal implements Terminal {
97
97
  private _kittyProtocolActive;
98
98
  private _modifyOtherKeysActive;
99
99
  private keyboardProtocolPushed;
100
+ /** DA1 replies owed to keyboard protocol queries. Later DA1 replies answer other queries and are forwarded. */
101
+ private pendingKeyboardProtocolDeviceAttributes;
100
102
  private keyboardProtocolNegotiationBuffer;
101
103
  private discardingPrivateResponse;
102
104
  private keyboardProtocolBufferFlushTimer?;
@@ -143,6 +145,7 @@ export declare class ProcessTerminal implements Terminal {
143
145
  */
144
146
  private queryAndEnableKittyProtocol;
145
147
  private handleKeyboardProtocolNegotiationSequence;
148
+ /** Returns the parsed negotiation reply with its full (possibly reassembled) sequence. */
146
149
  private readKeyboardProtocolNegotiationSequence;
147
150
  private setKeyboardProtocolNegotiationBuffer;
148
151
  private clearKeyboardProtocolNegotiationBuffer;