jeopi-tui 16.2.13

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 (75) hide show
  1. package/CHANGELOG.md +1861 -0
  2. package/README.md +705 -0
  3. package/dist/types/autocomplete.d.ts +99 -0
  4. package/dist/types/bracketed-paste.d.ts +51 -0
  5. package/dist/types/components/box.d.ts +31 -0
  6. package/dist/types/components/cancellable-loader.d.ts +21 -0
  7. package/dist/types/components/editor.d.ts +155 -0
  8. package/dist/types/components/image.d.ts +112 -0
  9. package/dist/types/components/input.d.ts +23 -0
  10. package/dist/types/components/loader.d.ts +20 -0
  11. package/dist/types/components/markdown.d.ts +64 -0
  12. package/dist/types/components/scroll-view.d.ts +62 -0
  13. package/dist/types/components/select-list.d.ts +68 -0
  14. package/dist/types/components/settings-list.d.ts +123 -0
  15. package/dist/types/components/spacer.d.ts +11 -0
  16. package/dist/types/components/tab-bar.d.ts +89 -0
  17. package/dist/types/components/text.d.ts +14 -0
  18. package/dist/types/components/truncated-text.d.ts +10 -0
  19. package/dist/types/deccara.d.ts +49 -0
  20. package/dist/types/desktop-notify.d.ts +51 -0
  21. package/dist/types/editor-component.d.ts +38 -0
  22. package/dist/types/fuzzy.d.ts +32 -0
  23. package/dist/types/index.d.ts +32 -0
  24. package/dist/types/keybindings.d.ts +191 -0
  25. package/dist/types/keys.d.ts +208 -0
  26. package/dist/types/kill-ring.d.ts +20 -0
  27. package/dist/types/kitty-graphics.d.ts +79 -0
  28. package/dist/types/latex-block.d.ts +7 -0
  29. package/dist/types/latex-to-unicode.d.ts +33 -0
  30. package/dist/types/loop-watchdog.d.ts +39 -0
  31. package/dist/types/mouse.d.ts +67 -0
  32. package/dist/types/stdin-buffer.d.ts +60 -0
  33. package/dist/types/symbols.d.ts +25 -0
  34. package/dist/types/terminal-capabilities.d.ts +284 -0
  35. package/dist/types/terminal.d.ts +107 -0
  36. package/dist/types/ttyid.d.ts +9 -0
  37. package/dist/types/tui.d.ts +423 -0
  38. package/dist/types/utils.d.ts +95 -0
  39. package/package.json +73 -0
  40. package/src/autocomplete.ts +1026 -0
  41. package/src/bracketed-paste.ts +123 -0
  42. package/src/components/box.ts +194 -0
  43. package/src/components/cancellable-loader.ts +40 -0
  44. package/src/components/editor.ts +3092 -0
  45. package/src/components/image.ts +444 -0
  46. package/src/components/input.ts +474 -0
  47. package/src/components/loader.ts +103 -0
  48. package/src/components/markdown.ts +2068 -0
  49. package/src/components/scroll-view.ts +227 -0
  50. package/src/components/select-list.ts +531 -0
  51. package/src/components/settings-list.ts +793 -0
  52. package/src/components/spacer.ts +32 -0
  53. package/src/components/tab-bar.ts +300 -0
  54. package/src/components/text.ts +122 -0
  55. package/src/components/truncated-text.ts +69 -0
  56. package/src/deccara.ts +314 -0
  57. package/src/desktop-notify.ts +186 -0
  58. package/src/editor-component.ts +74 -0
  59. package/src/fuzzy.ts +356 -0
  60. package/src/index.ts +51 -0
  61. package/src/keybindings.ts +337 -0
  62. package/src/keys.ts +561 -0
  63. package/src/kill-ring.ts +51 -0
  64. package/src/kitty-graphics.ts +171 -0
  65. package/src/latex-block.ts +461 -0
  66. package/src/latex-to-unicode.ts +1994 -0
  67. package/src/loop-watchdog.ts +106 -0
  68. package/src/mouse.ts +105 -0
  69. package/src/stdin-buffer.ts +669 -0
  70. package/src/symbols.ts +26 -0
  71. package/src/terminal-capabilities.ts +1152 -0
  72. package/src/terminal.ts +1463 -0
  73. package/src/ttyid.ts +84 -0
  74. package/src/tui.ts +3901 -0
  75. package/src/utils.ts +570 -0
@@ -0,0 +1,123 @@
1
+ const PASTE_START = "\x1b[200~";
2
+ const PASTE_END = "\x1b[201~";
3
+
4
+ export type PasteResult = { handled: false } | { handled: true; pasteContent?: string; remaining: string };
5
+
6
+ // Some terminals re-encode the control bytes inside a bracketed paste as key-event
7
+ // escape sequences (observed with tmux extended-keys passthrough under kitty). tmux
8
+ // emits one of two formats depending on `extended-keys-format`:
9
+ // - csi-u: ESC [ <codepoint> ; 5 u (Ctrl+J → ESC [ 106 ; 5 u)
10
+ // - xterm: ESC [ 27 ; 5 ; <codepoint> ~ (Ctrl+J → ESC [ 27 ; 5 ; 106 ~)
11
+ // Callers must decode these back to the literal control byte (Ctrl+J → "\n") before
12
+ // stripping control chars; otherwise ESC is dropped and the printable tail
13
+ // ("[106;5u" / "[27;5;106~") leaks into the editor.
14
+ //
15
+ // Only Ctrl+<letter> is decoded (codepoint a-z/A-Z → 0x01..0x1A). That is the set tmux
16
+ // actually re-encodes from paste content in practice — TAB (Ctrl+I), LF (Ctrl+J), CR
17
+ // (Ctrl+M), VT (Ctrl+K), FF (Ctrl+L), … Non-letter Ctrl combos (NUL, ESC, FS-US, DEL)
18
+ // never appear as re-encoded paste bytes, so they are left untouched rather than
19
+ // synthesized into raw control bytes. Callers still strip leftover control characters
20
+ // after decoding (the editor keeps "\n"; the single-line input strips all of them).
21
+ const REENCODED_CTRL_CSI_U = /\x1b\[(\d+);5u/g;
22
+ const REENCODED_CTRL_XTERM = /\x1b\[27;5;(\d+)~/g;
23
+
24
+ function decodeReencodedCtrlByte(match: string, code: string): string {
25
+ const cp = Number(code);
26
+ if (cp >= 97 && cp <= 122) return String.fromCharCode(cp - 96); // a-z → Ctrl+A..Ctrl+Z
27
+ if (cp >= 65 && cp <= 90) return String.fromCharCode(cp - 64); // A-Z → Ctrl+A..Ctrl+Z
28
+ return match;
29
+ }
30
+
31
+ /**
32
+ * Decode tmux's re-encoded control bytes (both `extended-keys-format` variants) inside a
33
+ * bracketed-paste payload back to their literal byte (e.g. Ctrl+J → "\n"). Leaves the rest of
34
+ * the text untouched. Call before any control-character stripping so newlines/tabs survive
35
+ * instead of leaking the printable escape tail into the buffer.
36
+ */
37
+ export function decodeReencodedPasteControls(text: string): string {
38
+ return text
39
+ .replace(REENCODED_CTRL_CSI_U, decodeReencodedCtrlByte)
40
+ .replace(REENCODED_CTRL_XTERM, decodeReencodedCtrlByte);
41
+ }
42
+
43
+ /**
44
+ * Options for {@link BracketedPasteHandler}.
45
+ */
46
+ export type BracketedPasteHandlerOptions = {
47
+ /**
48
+ * Byte cap for buffered paste content (default: 64 MiB). When exceeded,
49
+ * paste mode is aborted and the accumulated content is delivered as
50
+ * `pasteContent` on the same `process()` call so a lost/corrupted end
51
+ * marker cannot consume unbounded memory. Mirrors `StdinBuffer#abortPaste`
52
+ * — defense in depth for callers that bypass `StdinBuffer` (issue #4073
53
+ * case B). The normal `ProcessTerminal` path re-wraps `StdinBuffer`'s
54
+ * bounded paste with both markers, so this cap only fires on alternate
55
+ * callers.
56
+ */
57
+ byteLimit?: number;
58
+ };
59
+
60
+ const DEFAULT_BYTE_LIMIT = 64 * 1024 * 1024;
61
+
62
+ /**
63
+ * Handles bracketed paste mode buffering for terminal input components.
64
+ *
65
+ * Bracketed paste mode wraps pasted content between start (\x1b[200~) and
66
+ * end (\x1b[201~) markers, which may arrive split across multiple chunks.
67
+ * This class buffers incoming data and assembles complete paste payloads.
68
+ */
69
+ export class BracketedPasteHandler {
70
+ #buffer = "";
71
+ #active = false;
72
+ readonly #byteLimit: number;
73
+
74
+ constructor(options: BracketedPasteHandlerOptions = {}) {
75
+ this.#byteLimit = options.byteLimit ?? DEFAULT_BYTE_LIMIT;
76
+ }
77
+
78
+ /**
79
+ * Process incoming terminal data for bracketed paste sequences.
80
+ *
81
+ * @returns `{ handled: false }` if the data contains no paste sequence and
82
+ * should be processed normally. `{ handled: true }` if the data was
83
+ * consumed by paste buffering — `pasteContent` is set when a complete
84
+ * paste has been assembled (or the byte cap has aborted a runaway
85
+ * buffer); omitted when still buffering.
86
+ */
87
+ process(data: string): PasteResult {
88
+ if (data.includes(PASTE_START)) {
89
+ this.#active = true;
90
+ this.#buffer = "";
91
+ data = data.replace(PASTE_START, "");
92
+ }
93
+
94
+ if (!this.#active) return { handled: false };
95
+
96
+ this.#buffer += data;
97
+
98
+ const endIndex = this.#buffer.indexOf(PASTE_END);
99
+ if (endIndex !== -1) {
100
+ const pasteContent = this.#buffer.substring(0, endIndex);
101
+ const remaining = this.#buffer.substring(endIndex + PASTE_END.length);
102
+
103
+ this.#buffer = "";
104
+ this.#active = false;
105
+
106
+ return { handled: true, pasteContent, remaining };
107
+ }
108
+
109
+ // Byte cap: a lost/corrupted end marker (ssh/tmux truncation) must not
110
+ // consume unbounded memory. Deliver the accumulated bytes so they are
111
+ // neither lost nor held forever, and reset paste mode so subsequent
112
+ // input recovers. See `StdinBuffer#abortPaste` for the sibling recovery
113
+ // semantics inside `StdinBuffer`.
114
+ if (this.#buffer.length > this.#byteLimit) {
115
+ const pasteContent = this.#buffer;
116
+ this.#buffer = "";
117
+ this.#active = false;
118
+ return { handled: true, pasteContent, remaining: "" };
119
+ }
120
+
121
+ return { handled: true, remaining: "" };
122
+ }
123
+ }
@@ -0,0 +1,194 @@
1
+ import type { Component } from "../tui";
2
+ import { applyBackgroundToLine, getPaddingX, padding, visibleWidth } from "../utils";
3
+
4
+ type Cache = {
5
+ width: number;
6
+ bgSample: string | undefined;
7
+ borderSample: string | undefined;
8
+ childLines: (readonly string[])[];
9
+ result: string[];
10
+ };
11
+
12
+ /** Box-drawing glyphs plus an optional colorizer for an outline drawn around a {@link Box}. */
13
+ export interface BoxBorder {
14
+ chars: {
15
+ topLeft: string;
16
+ topRight: string;
17
+ bottomLeft: string;
18
+ bottomRight: string;
19
+ horizontal: string;
20
+ vertical: string;
21
+ };
22
+ color?: (text: string) => string;
23
+ }
24
+
25
+ /**
26
+ * Box component - a container that applies padding and background to all children
27
+ */
28
+ export class Box implements Component {
29
+ children: Component[] = [];
30
+ #paddingX: number;
31
+ #paddingY: number;
32
+ #bgFn?: (text: string) => string;
33
+ #border?: BoxBorder;
34
+
35
+ #ignoreTight = false;
36
+
37
+ setIgnoreTight(ignore: boolean): this {
38
+ this.#ignoreTight = ignore;
39
+ this.#invalidateCache();
40
+ return this;
41
+ }
42
+
43
+ // Cache for rendered output
44
+ #cached?: Cache;
45
+
46
+ constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string, border?: BoxBorder) {
47
+ this.#paddingX = paddingX;
48
+ this.#paddingY = paddingY;
49
+ this.#bgFn = bgFn;
50
+ this.#border = border;
51
+ }
52
+
53
+ addChild(component: Component): void {
54
+ this.children.push(component);
55
+ if (this.#ignoreTight) {
56
+ component.setIgnoreTight?.(true);
57
+ }
58
+ this.#invalidateCache();
59
+ }
60
+
61
+ removeChild(component: Component): void {
62
+ const index = this.children.indexOf(component);
63
+ if (index !== -1) {
64
+ this.children.splice(index, 1);
65
+ this.#invalidateCache();
66
+ }
67
+ }
68
+
69
+ clear(): void {
70
+ this.children = [];
71
+ this.#invalidateCache();
72
+ }
73
+
74
+ setPaddingX(paddingX: number): void {
75
+ if (this.#paddingX === paddingX) return;
76
+ this.#paddingX = paddingX;
77
+ this.#invalidateCache();
78
+ }
79
+
80
+ setPaddingY(paddingY: number): void {
81
+ if (this.#paddingY === paddingY) return;
82
+ this.#paddingY = paddingY;
83
+ this.#invalidateCache();
84
+ }
85
+
86
+ setBgFn(bgFn?: (text: string) => string): void {
87
+ this.#bgFn = bgFn;
88
+ // Don't invalidate here - we'll detect bgFn changes by sampling output
89
+ }
90
+
91
+ setBorder(border?: BoxBorder): void {
92
+ this.#border = border;
93
+ this.#invalidateCache();
94
+ }
95
+
96
+ #invalidateCache(): void {
97
+ this.#cached = undefined;
98
+ }
99
+
100
+ invalidate(): void {
101
+ this.#invalidateCache();
102
+ for (const child of this.children) {
103
+ child.invalidate?.();
104
+ }
105
+ }
106
+
107
+ render(width: number): readonly string[] {
108
+ const children = this.children;
109
+ const count = children.length;
110
+ const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
111
+ // A border eats one column on each side; skip it unless the interior can still
112
+ // hold the horizontal padding plus at least one content column, so a bordered
113
+ // Box never overflows the width it was given.
114
+ const border = this.#border && width - 2 >= paddingX * 2 + 1 ? this.#border : undefined;
115
+ const innerWidth = border ? width - 2 : width;
116
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
117
+ // bgFn / border output can change without the function reference changing
118
+ // (theme mutation); sample both so a silent palette swap still misses the cache.
119
+ const bgSample = this.#bgFn ? this.#bgFn("test") : undefined;
120
+ const borderSample = border
121
+ ? `${border.color ? border.color("|") : "|"}${border.chars.topLeft}${border.chars.vertical}`
122
+ : undefined;
123
+
124
+ // Render every child every frame (renders may carry side effects); the
125
+ // memo only skips re-deriving the padded/background rows. Per the
126
+ // Component render contract, identical child array references prove the
127
+ // content is unchanged.
128
+ const cached = this.#cached;
129
+ let unchanged =
130
+ cached !== undefined &&
131
+ cached.width === width &&
132
+ cached.bgSample === bgSample &&
133
+ cached.borderSample === borderSample &&
134
+ cached.childLines.length === count;
135
+ const childLines: (readonly string[])[] = new Array(count);
136
+ let contentRows = 0;
137
+ for (let i = 0; i < count; i++) {
138
+ const lines = children[i]!.render(contentWidth);
139
+ childLines[i] = lines;
140
+ contentRows += lines.length;
141
+ if (unchanged && cached!.childLines[i] !== lines) unchanged = false;
142
+ }
143
+ if (unchanged) return cached!.result;
144
+
145
+ const result: string[] = [];
146
+ if (contentRows > 0) {
147
+ const leftPad = padding(paddingX);
148
+ const interior: string[] = [];
149
+ // Top padding
150
+ for (let i = 0; i < this.#paddingY; i++) {
151
+ interior.push(this.#applyBg("", innerWidth));
152
+ }
153
+ // Content
154
+ for (const lines of childLines) {
155
+ for (const line of lines) {
156
+ interior.push(this.#applyBg(leftPad + line, innerWidth));
157
+ }
158
+ }
159
+ // Bottom padding
160
+ for (let i = 0; i < this.#paddingY; i++) {
161
+ interior.push(this.#applyBg("", innerWidth));
162
+ }
163
+
164
+ if (border) {
165
+ const paint = border.color ?? (s => s);
166
+ const rule = border.chars.horizontal.repeat(Math.max(0, innerWidth));
167
+ const side = paint(border.chars.vertical);
168
+ result.push(paint(border.chars.topLeft + rule + border.chars.topRight));
169
+ for (const row of interior) {
170
+ result.push(side + row + side);
171
+ }
172
+ result.push(paint(border.chars.bottomLeft + rule + border.chars.bottomRight));
173
+ } else {
174
+ for (const row of interior) {
175
+ result.push(row);
176
+ }
177
+ }
178
+ }
179
+
180
+ this.#cached = { width, bgSample, borderSample, childLines, result };
181
+ return result;
182
+ }
183
+
184
+ #applyBg(line: string, width: number): string {
185
+ const visLen = visibleWidth(line);
186
+ const padNeeded = Math.max(0, width - visLen);
187
+ const padded = line + padding(padNeeded);
188
+
189
+ if (this.#bgFn) {
190
+ return applyBackgroundToLine(padded, width, this.#bgFn);
191
+ }
192
+ return padded;
193
+ }
194
+ }
@@ -0,0 +1,40 @@
1
+ import { getKeybindings } from "../keybindings";
2
+ import { Loader } from "./loader";
3
+
4
+ /**
5
+ * Loader that can be cancelled with Escape.
6
+ * Extends Loader with an AbortSignal for cancelling async operations.
7
+ *
8
+ * @example
9
+ * const loader = new CancellableLoader(tui, cyan, dim, "Working...");
10
+ * loader.onAbort = () => done(null);
11
+ * doWork(loader.signal).then(done);
12
+ */
13
+ export class CancellableLoader extends Loader {
14
+ #abortController = new AbortController();
15
+
16
+ /** Called when user presses Escape */
17
+ onAbort?: () => void;
18
+
19
+ /** AbortSignal that is aborted when user presses Escape */
20
+ get signal(): AbortSignal {
21
+ return this.#abortController.signal;
22
+ }
23
+
24
+ /** Whether the loader was aborted */
25
+ get aborted(): boolean {
26
+ return this.#abortController.signal.aborted;
27
+ }
28
+
29
+ handleInput(data: string): void {
30
+ const kb = getKeybindings();
31
+ if (kb.matches(data, "tui.select.cancel")) {
32
+ this.#abortController.abort();
33
+ this.onAbort?.();
34
+ }
35
+ }
36
+
37
+ dispose(): void {
38
+ this.stop();
39
+ }
40
+ }