@code-yeongyu/senpi-tui 2026.9.30 → 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.
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";
@@ -25,15 +26,17 @@ export { getKeybindings, type Keybinding, type KeybindingConflict, type Keybindi
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
@@ -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;
package/dist/terminal.js CHANGED
@@ -188,6 +188,8 @@ export class ProcessTerminal {
188
188
  this._kittyProtocolActive = false;
189
189
  this._modifyOtherKeysActive = false;
190
190
  this.keyboardProtocolPushed = false;
191
+ /** DA1 replies owed to keyboard protocol queries. Later DA1 replies answer other queries and are forwarded. */
192
+ this.pendingKeyboardProtocolDeviceAttributes = 0;
191
193
  this.keyboardProtocolNegotiationBuffer = "";
192
194
  this.discardingPrivateResponse = false;
193
195
  this.writeLogPath = (() => {
@@ -399,15 +401,15 @@ export class ProcessTerminal {
399
401
  return;
400
402
  }
401
403
  }
402
- const negotiationSequence = this.readKeyboardProtocolNegotiationSequence(sequence);
403
- if (negotiationSequence === "pending") {
404
+ const negotiation = this.readKeyboardProtocolNegotiationSequence(sequence);
405
+ if (negotiation === "pending") {
404
406
  this.scheduleKeyboardProtocolNegotiationBufferFlush();
405
407
  return; // Wait briefly for the rest of a split Kitty response.
406
408
  }
407
- if (this.handleKeyboardProtocolNegotiationSequence(negotiationSequence)) {
409
+ if (negotiation && this.handleKeyboardProtocolNegotiationSequence(negotiation.parsed)) {
408
410
  return;
409
411
  }
410
- this.forwardInputSequence(sequence);
412
+ this.forwardInputSequence(negotiation?.sequence ?? sequence);
411
413
  });
412
414
  // Re-wrap paste content with bracketed paste markers for existing editor handling
413
415
  this.stdinBuffer.on("paste", (content) => {
@@ -443,12 +445,11 @@ export class ProcessTerminal {
443
445
  this.enableModifyOtherKeys();
444
446
  }
445
447
  this.keyboardProtocolPushed = true;
448
+ this.pendingKeyboardProtocolDeviceAttributes += 1;
446
449
  this.clearKeyboardProtocolNegotiationBuffer();
447
450
  this.rawWrite(KITTY_KEYBOARD_PROTOCOL_QUERY);
448
451
  }
449
452
  handleKeyboardProtocolNegotiationSequence(negotiationSequence) {
450
- if (!negotiationSequence)
451
- return false;
452
453
  this.clearKeyboardProtocolNegotiationBuffer();
453
454
  if (negotiationSequence.type === "cursor-position") {
454
455
  if (this.cursorQuery?.issued && this.cursorQuery.tmuxPane === undefined) {
@@ -457,6 +458,11 @@ export class ProcessTerminal {
457
458
  }
458
459
  return true;
459
460
  }
461
+ if (negotiationSequence.type === "device-attributes") {
462
+ if (this.pendingKeyboardProtocolDeviceAttributes === 0)
463
+ return false;
464
+ this.pendingKeyboardProtocolDeviceAttributes -= 1;
465
+ }
460
466
  this.keyboardNegotiationSettled = true;
461
467
  this.issueCursorQuery();
462
468
  if (negotiationSequence.type === "kitty-flags") {
@@ -477,13 +483,14 @@ export class ProcessTerminal {
477
483
  }
478
484
  return true;
479
485
  }
486
+ /** Returns the parsed negotiation reply with its full (possibly reassembled) sequence. */
480
487
  readKeyboardProtocolNegotiationSequence(sequence) {
481
488
  if (this.keyboardProtocolNegotiationBuffer) {
482
489
  const bufferedSequence = this.keyboardProtocolNegotiationBuffer + sequence;
483
490
  const negotiationSequence = parseKeyboardProtocolNegotiationSequence(bufferedSequence);
484
491
  if (negotiationSequence) {
485
492
  this.clearKeyboardProtocolNegotiationBuffer();
486
- return negotiationSequence;
493
+ return { parsed: negotiationSequence, sequence: bufferedSequence };
487
494
  }
488
495
  if (isKeyboardProtocolNegotiationSequencePrefix(bufferedSequence)) {
489
496
  this.setKeyboardProtocolNegotiationBuffer(bufferedSequence);
@@ -493,7 +500,7 @@ export class ProcessTerminal {
493
500
  }
494
501
  const negotiationSequence = parseKeyboardProtocolNegotiationSequence(sequence);
495
502
  if (negotiationSequence)
496
- return negotiationSequence;
503
+ return { parsed: negotiationSequence, sequence };
497
504
  if (isKeyboardProtocolNegotiationSequencePrefix(sequence)) {
498
505
  this.setKeyboardProtocolNegotiationBuffer(sequence);
499
506
  return "pending";
@@ -1,8 +1,12 @@
1
1
  import type { Terminal } from "./terminal.ts";
2
2
  import { type Component, TuiBase, type TuiStopOptions, VIEWPORT_TUI, type ViewportTUI } from "./tui.ts";
3
+ import { type WheelScrollLines } from "./wheel-scroll.ts";
3
4
  export interface TuiAltScreenOptions {
4
- /** Number of logical lines moved for each mouse-wheel event. */
5
- wheelScrollLines?: number;
5
+ /**
6
+ * Logical lines moved for each mouse-wheel event (default: 1). `"auto"` accelerates fast wheel
7
+ * spins on terminals that send one event per notch. Alt+wheel moves five times as far.
8
+ */
9
+ wheelScrollLines?: WheelScrollLines;
6
10
  /** Capture mouse events for viewport scrolling and application-owned text selection. */
7
11
  mouse?: boolean;
8
12
  /** Style a non-current transcript search match. */
@@ -23,10 +27,11 @@ export interface TuiAltScreenOptions {
23
27
  /** Automatically copy selected text to the clipboard on mouse release (default: true). */
24
28
  copyOnSelect?: boolean;
25
29
  /**
26
- * Copy selected text to the system clipboard. Return `true` on success; the caller flashes
27
- * an error otherwise. When omitted, the selection is copied via an OSC 52 write.
30
+ * Copy selected text to the system clipboard. Return `true` on success, an error message to
31
+ * display on failure, or `false` for a generic error. When omitted, the selection is copied
32
+ * via an OSC 52 write.
28
33
  */
29
- copySelection?: (text: string) => Promise<boolean>;
34
+ copySelection?: (text: string) => Promise<boolean | string>;
30
35
  }
31
36
  /** Alternate-screen TUI with a scrollable, application-owned viewport. */
32
37
  export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
@@ -65,7 +70,7 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
65
70
  private mousePressPoint?;
66
71
  private mousePressMoved;
67
72
  private lastComponentClick?;
68
- private readonly wheelScrollLines;
73
+ private readonly wheelScroll;
69
74
  private readonly mouseEnabled;
70
75
  private readonly searchMatchStyle;
71
76
  private readonly searchCurrentMatchStyle;
@@ -78,6 +83,7 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
78
83
  constructor(terminal: Terminal, showHardwareCursor?: boolean, logDirectory?: string, options?: TuiAltScreenOptions);
79
84
  get viewportTop(): number;
80
85
  get isFollowingOutput(): boolean;
86
+ setWheelScrollLines(lines: WheelScrollLines): void;
81
87
  getCopyOnSelect(): boolean;
82
88
  setCopyOnSelect(enabled: boolean): void;
83
89
  /** Whether the fullscreen viewport has a non-empty active text selection. */
@@ -119,7 +125,6 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
119
125
  private clearTextSelection;
120
126
  private handleMouseEvent;
121
127
  private parseWheelEvent;
122
- private getWheelScrollLines;
123
128
  private routeWheel;
124
129
  private parseSgrMouseEvent;
125
130
  private handleRightClickPaste;