@f5xc-salesdemos/pi-tui 14.0.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.
@@ -0,0 +1,47 @@
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
+ /**
7
+ * Handles bracketed paste mode buffering for terminal input components.
8
+ *
9
+ * Bracketed paste mode wraps pasted content between start (\x1b[200~) and
10
+ * end (\x1b[201~) markers, which may arrive split across multiple chunks.
11
+ * This class buffers incoming data and assembles complete paste payloads.
12
+ */
13
+ export class BracketedPasteHandler {
14
+ #buffer = "";
15
+ #active = false;
16
+
17
+ /**
18
+ * Process incoming terminal data for bracketed paste sequences.
19
+ *
20
+ * @returns `{ handled: false }` if the data contains no paste sequence and
21
+ * should be processed normally. `{ handled: true }` if the data was
22
+ * consumed by paste buffering — `pasteContent` is set when a complete
23
+ * paste has been assembled; omitted when still buffering.
24
+ */
25
+ process(data: string): PasteResult {
26
+ if (data.includes(PASTE_START)) {
27
+ this.#active = true;
28
+ this.#buffer = "";
29
+ data = data.replace(PASTE_START, "");
30
+ }
31
+
32
+ if (!this.#active) return { handled: false };
33
+
34
+ this.#buffer += data;
35
+
36
+ const endIndex = this.#buffer.indexOf(PASTE_END);
37
+ if (endIndex === -1) return { handled: true, remaining: "" };
38
+
39
+ const pasteContent = this.#buffer.substring(0, endIndex);
40
+ const remaining = this.#buffer.substring(endIndex + PASTE_END.length);
41
+
42
+ this.#buffer = "";
43
+ this.#active = false;
44
+
45
+ return { handled: true, pasteContent, remaining };
46
+ }
47
+ }
@@ -0,0 +1,144 @@
1
+ import type { Component } from "../tui";
2
+ import { applyBackgroundToLine, padding, visibleWidth } from "../utils";
3
+
4
+ type Cache = {
5
+ key: bigint;
6
+ result: string[];
7
+ };
8
+
9
+ /**
10
+ * Box component - a container that applies padding and background to all children
11
+ */
12
+ export class Box implements Component {
13
+ children: Component[] = [];
14
+ #paddingX: number;
15
+ #paddingY: number;
16
+ #bgFn?: (text: string) => string;
17
+
18
+ // Cache for rendered output
19
+ #cached?: Cache;
20
+
21
+ constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {
22
+ this.#paddingX = paddingX;
23
+ this.#paddingY = paddingY;
24
+ this.#bgFn = bgFn;
25
+ }
26
+
27
+ addChild(component: Component): void {
28
+ this.children.push(component);
29
+ this.#invalidateCache();
30
+ }
31
+
32
+ removeChild(component: Component): void {
33
+ const index = this.children.indexOf(component);
34
+ if (index !== -1) {
35
+ this.children.splice(index, 1);
36
+ this.#invalidateCache();
37
+ }
38
+ }
39
+
40
+ clear(): void {
41
+ this.children = [];
42
+ this.#invalidateCache();
43
+ }
44
+
45
+ setBgFn(bgFn?: (text: string) => string): void {
46
+ this.#bgFn = bgFn;
47
+ // Don't invalidate here - we'll detect bgFn changes by sampling output
48
+ }
49
+
50
+ #invalidateCache(): void {
51
+ this.#cached = undefined;
52
+ }
53
+
54
+ static #tmp = new Uint32Array(2);
55
+ #computeCacheKey(width: number, childLines: string[], bgSample: string | undefined): bigint {
56
+ Box.#tmp[0] = width;
57
+ Box.#tmp[1] = childLines.length;
58
+ let h = Bun.hash.xxHash64(Box.#tmp);
59
+ for (const line of childLines) {
60
+ Box.#tmp[0] = line.length;
61
+ h = Bun.hash.xxHash64(Box.#tmp, h);
62
+ h = Bun.hash.xxHash64(line, h);
63
+ }
64
+ h = Bun.hash.xxHash64(bgSample ?? "", h);
65
+ return h;
66
+ }
67
+
68
+ #matchCache(cacheKey: bigint): boolean {
69
+ return this.#cached?.key === cacheKey;
70
+ }
71
+
72
+ invalidate(): void {
73
+ this.#invalidateCache();
74
+ for (const child of this.children) {
75
+ child.invalidate?.();
76
+ }
77
+ }
78
+
79
+ render(width: number): string[] {
80
+ if (this.children.length === 0) {
81
+ return [];
82
+ }
83
+
84
+ const contentWidth = Math.max(1, width - this.#paddingX * 2);
85
+ const leftPad = padding(this.#paddingX);
86
+
87
+ // Render all children
88
+ const childLines: string[] = [];
89
+ for (const child of this.children) {
90
+ const lines = child.render(contentWidth);
91
+ for (const line of lines) {
92
+ childLines.push(leftPad + line);
93
+ }
94
+ }
95
+
96
+ if (childLines.length === 0) {
97
+ return [];
98
+ }
99
+
100
+ // Check if bgFn output changed by sampling
101
+ const bgSample = this.#bgFn ? this.#bgFn("test") : undefined;
102
+
103
+ const cacheKey = this.#computeCacheKey(width, childLines, bgSample);
104
+
105
+ // Check cache validity
106
+ if (this.#matchCache(cacheKey)) {
107
+ return this.#cached!.result;
108
+ }
109
+
110
+ // Apply background and padding
111
+ const result: string[] = [];
112
+
113
+ // Top padding
114
+ for (let i = 0; i < this.#paddingY; i++) {
115
+ result.push(this.#applyBg("", width));
116
+ }
117
+
118
+ // Content
119
+ for (const line of childLines) {
120
+ result.push(this.#applyBg(line, width));
121
+ }
122
+
123
+ // Bottom padding
124
+ for (let i = 0; i < this.#paddingY; i++) {
125
+ result.push(this.#applyBg("", width));
126
+ }
127
+
128
+ // Update cache
129
+ this.#cached = { key: cacheKey, result };
130
+
131
+ return result;
132
+ }
133
+
134
+ #applyBg(line: string, width: number): string {
135
+ const visLen = visibleWidth(line);
136
+ const padNeeded = Math.max(0, width - visLen);
137
+ const padded = line + padding(padNeeded);
138
+
139
+ if (this.#bgFn) {
140
+ return applyBackgroundToLine(padded, width, this.#bgFn);
141
+ }
142
+ return padded;
143
+ }
144
+ }
@@ -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
+ }