@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.
- package/CHANGELOG.md +682 -0
- package/README.md +704 -0
- package/package.json +71 -0
- package/src/autocomplete.ts +787 -0
- package/src/bracketed-paste.ts +47 -0
- package/src/components/box.ts +144 -0
- package/src/components/cancellable-loader.ts +40 -0
- package/src/components/editor.ts +2563 -0
- package/src/components/image.ts +90 -0
- package/src/components/input.ts +439 -0
- package/src/components/loader.ts +67 -0
- package/src/components/markdown.ts +914 -0
- package/src/components/select-list.ts +249 -0
- package/src/components/settings-list.ts +195 -0
- package/src/components/spacer.ts +28 -0
- package/src/components/tab-bar.ts +175 -0
- package/src/components/text.ts +110 -0
- package/src/components/truncated-text.ts +61 -0
- package/src/editor-component.ts +71 -0
- package/src/fuzzy.ts +143 -0
- package/src/index.ts +39 -0
- package/src/keybindings.ts +279 -0
- package/src/keys.ts +404 -0
- package/src/kill-ring.ts +46 -0
- package/src/stdin-buffer.ts +385 -0
- package/src/symbols.ts +24 -0
- package/src/terminal-capabilities.ts +525 -0
- package/src/terminal.ts +630 -0
- package/src/ttyid.ts +66 -0
- package/src/tui.ts +1328 -0
- package/src/utils.ts +301 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { getDefaultTabWidth, getIndentation, sliceWithWidth } from "@f5xc-salesdemos/pi-natives";
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
Ellipsis,
|
|
5
|
+
extractSegments,
|
|
6
|
+
sliceWithWidth,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
wrapTextWithAnsi,
|
|
9
|
+
} from "@f5xc-salesdemos/pi-natives";
|
|
10
|
+
|
|
11
|
+
// Pre-allocated space buffer for padding
|
|
12
|
+
const SPACE_BUFFER = " ".repeat(512);
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Replace tabs with configured spacing for consistent rendering.
|
|
16
|
+
*/
|
|
17
|
+
export function replaceTabs(text: string, file?: string): string {
|
|
18
|
+
return text.replaceAll("\t", " ".repeat(getIndentation(file)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns a string of n spaces. Uses a pre-allocated buffer for efficiency.
|
|
23
|
+
*/
|
|
24
|
+
export function padding(n: number): string {
|
|
25
|
+
if (n <= 0) return "";
|
|
26
|
+
if (n <= 512) return SPACE_BUFFER.slice(0, n);
|
|
27
|
+
return " ".repeat(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Grapheme segmenter (shared instance)
|
|
31
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the shared grapheme segmenter instance.
|
|
35
|
+
*/
|
|
36
|
+
export function getSegmenter(): Intl.Segmenter {
|
|
37
|
+
return segmenter;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Calculate the visible width of a string in terminal columns.
|
|
42
|
+
*/
|
|
43
|
+
function _isPrintableAscii(str: string): boolean {
|
|
44
|
+
for (let i = 0; i < str.length; i++) {
|
|
45
|
+
const code = str.charCodeAt(i);
|
|
46
|
+
if (code < 0x20 || code > 0x7e) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function visibleWidthRaw(str: string): number {
|
|
54
|
+
if (!str) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fast path: pure ASCII printable
|
|
59
|
+
let tabLength = 0;
|
|
60
|
+
const tabWidth = getDefaultTabWidth();
|
|
61
|
+
let isPureAscii = true;
|
|
62
|
+
for (let i = 0; i < str.length; i++) {
|
|
63
|
+
const code = str.charCodeAt(i);
|
|
64
|
+
if (code === 9) {
|
|
65
|
+
tabLength += tabWidth;
|
|
66
|
+
} else if (code < 0x20 || code > 0x7e) {
|
|
67
|
+
isPureAscii = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (isPureAscii) {
|
|
71
|
+
return str.length + tabLength;
|
|
72
|
+
}
|
|
73
|
+
return Bun.stringWidth(str) + tabLength;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculate the visible width of a string in terminal columns.
|
|
78
|
+
*/
|
|
79
|
+
export function visibleWidth(str: string): number {
|
|
80
|
+
if (!str) return 0;
|
|
81
|
+
return visibleWidthRaw(str);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const makeBoolArray = (chars: string): ReadonlyArray<boolean> => {
|
|
85
|
+
const table = Array.from({ length: 128 }, () => false);
|
|
86
|
+
for (let i = 0; i < chars.length; i++) {
|
|
87
|
+
const code = chars.charCodeAt(i);
|
|
88
|
+
if (code < table.length) {
|
|
89
|
+
table[code] = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return table;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const ASCII_WHITESPACE = makeBoolArray("\x09\x0a\x0b\x0c\x0d\x20");
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a character is whitespace.
|
|
99
|
+
*/
|
|
100
|
+
export function isWhitespaceChar(char: string): boolean {
|
|
101
|
+
const code = char.codePointAt(0) || 0;
|
|
102
|
+
return ASCII_WHITESPACE[code] ?? false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ASCII_PUNCTUATION = makeBoolArray("(){}[]<>.,;:'\"!?+-=*/\\|&%^$#@~`");
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a character is punctuation.
|
|
109
|
+
*/
|
|
110
|
+
export function isPunctuationChar(char: string): boolean {
|
|
111
|
+
const code = char.codePointAt(0) || 0;
|
|
112
|
+
return ASCII_PUNCTUATION[code] ?? false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type WordNavKind = "whitespace" | "delimiter" | "cjk" | "word" | "other";
|
|
116
|
+
|
|
117
|
+
const WORD_NAV_RE_WHITESPACE = /^\p{White_Space}$/u;
|
|
118
|
+
const WORD_NAV_RE_PUNCT = /^\p{P}$/u;
|
|
119
|
+
const WORD_NAV_RE_SYMBOL = /^\p{S}$/u;
|
|
120
|
+
const WORD_NAV_RE_LETTER = /^\p{L}$/u;
|
|
121
|
+
const WORD_NAV_RE_NUMBER = /^\p{N}$/u;
|
|
122
|
+
const WORD_NAV_RE_HAN = /^\p{Script=Han}$/u;
|
|
123
|
+
const WORD_NAV_RE_HIRAGANA = /^\p{Script=Hiragana}$/u;
|
|
124
|
+
const WORD_NAV_RE_KATAKANA = /^\p{Script=Katakana}$/u;
|
|
125
|
+
const WORD_NAV_RE_HANGUL = /^\p{Script=Hangul}$/u;
|
|
126
|
+
|
|
127
|
+
function firstCodePointChar(str: string): string {
|
|
128
|
+
const cp = str.codePointAt(0);
|
|
129
|
+
if (cp === undefined) return "";
|
|
130
|
+
return String.fromCodePoint(cp);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Coarse Unicode-aware character classification for word navigation (Option/Alt + Left/Right).
|
|
135
|
+
* This intentionally avoids language-specific word segmentation for predictability across scripts.
|
|
136
|
+
*/
|
|
137
|
+
export function getWordNavKind(grapheme: string): WordNavKind {
|
|
138
|
+
if (!grapheme) return "other";
|
|
139
|
+
const ch = firstCodePointChar(grapheme);
|
|
140
|
+
if (!ch) return "other";
|
|
141
|
+
if (WORD_NAV_RE_WHITESPACE.test(ch)) return "whitespace";
|
|
142
|
+
if (WORD_NAV_RE_PUNCT.test(ch) || WORD_NAV_RE_SYMBOL.test(ch)) return "delimiter";
|
|
143
|
+
if (
|
|
144
|
+
WORD_NAV_RE_HAN.test(ch) ||
|
|
145
|
+
WORD_NAV_RE_HIRAGANA.test(ch) ||
|
|
146
|
+
WORD_NAV_RE_KATAKANA.test(ch) ||
|
|
147
|
+
WORD_NAV_RE_HANGUL.test(ch)
|
|
148
|
+
) {
|
|
149
|
+
return "cjk";
|
|
150
|
+
}
|
|
151
|
+
if (ch === "_" || WORD_NAV_RE_LETTER.test(ch) || WORD_NAV_RE_NUMBER.test(ch)) return "word";
|
|
152
|
+
return "other";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const WORD_NAV_JOINERS = new Set(["'", "’", "-", "‐", "‑"]);
|
|
156
|
+
|
|
157
|
+
export function isWordNavJoiner(grapheme: string): boolean {
|
|
158
|
+
const ch = firstCodePointChar(grapheme);
|
|
159
|
+
return WORD_NAV_JOINERS.has(ch);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Move the cursor one "word" to the left using Unicode-aware coarse navigation.
|
|
164
|
+
*
|
|
165
|
+
* Returns a new cursor index in the range [0, text.length].
|
|
166
|
+
*/
|
|
167
|
+
export function moveWordLeft(text: string, cursor: number): number {
|
|
168
|
+
const len = text.length;
|
|
169
|
+
if (len === 0) return 0;
|
|
170
|
+
let i = Math.min(Math.max(cursor, 0), len);
|
|
171
|
+
if (i === 0) return 0;
|
|
172
|
+
|
|
173
|
+
const graphemes = [...segmenter.segment(text.slice(0, i))];
|
|
174
|
+
if (graphemes.length === 0) return 0;
|
|
175
|
+
|
|
176
|
+
// Skip trailing whitespace.
|
|
177
|
+
while (graphemes.length > 0 && getWordNavKind(graphemes[graphemes.length - 1]?.segment || "") === "whitespace") {
|
|
178
|
+
i -= graphemes.pop()?.segment.length || 0;
|
|
179
|
+
}
|
|
180
|
+
if (i === 0 || graphemes.length === 0) return i;
|
|
181
|
+
|
|
182
|
+
const kind = getWordNavKind(graphemes[graphemes.length - 1]?.segment || "");
|
|
183
|
+
if (kind === "delimiter" || kind === "cjk") {
|
|
184
|
+
while (graphemes.length > 0 && getWordNavKind(graphemes[graphemes.length - 1]?.segment || "") === kind) {
|
|
185
|
+
i -= graphemes.pop()?.segment.length || 0;
|
|
186
|
+
}
|
|
187
|
+
return i;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (kind === "word") {
|
|
191
|
+
// Skip word run (letters/numbers/underscore), keeping common joiners inside words.
|
|
192
|
+
let hasRightWord = false;
|
|
193
|
+
while (graphemes.length > 0) {
|
|
194
|
+
const g = graphemes[graphemes.length - 1]?.segment || "";
|
|
195
|
+
const k = getWordNavKind(g);
|
|
196
|
+
if (k === "word") {
|
|
197
|
+
hasRightWord = true;
|
|
198
|
+
i -= graphemes.pop()?.segment.length || 0;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (hasRightWord && k === "delimiter" && isWordNavJoiner(g)) {
|
|
202
|
+
const left = graphemes[graphemes.length - 2]?.segment || "";
|
|
203
|
+
if (getWordNavKind(left) === "word") {
|
|
204
|
+
i -= graphemes.pop()?.segment.length || 0;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
return i;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fallback: move by one grapheme.
|
|
214
|
+
i -= graphemes.pop()?.segment.length || 0;
|
|
215
|
+
return Math.max(0, i);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Move the cursor one "word" to the right using Unicode-aware coarse navigation.
|
|
220
|
+
*
|
|
221
|
+
* Returns a new cursor index in the range [0, text.length].
|
|
222
|
+
*/
|
|
223
|
+
export function moveWordRight(text: string, cursor: number): number {
|
|
224
|
+
const len = text.length;
|
|
225
|
+
if (len === 0) return 0;
|
|
226
|
+
let i = Math.min(Math.max(cursor, 0), len);
|
|
227
|
+
if (i === len) return len;
|
|
228
|
+
|
|
229
|
+
const iterator = segmenter.segment(text.slice(i))[Symbol.iterator]();
|
|
230
|
+
let next = iterator.next();
|
|
231
|
+
|
|
232
|
+
// Skip leading whitespace.
|
|
233
|
+
while (!next.done && getWordNavKind(next.value.segment) === "whitespace") {
|
|
234
|
+
i += next.value.segment.length;
|
|
235
|
+
next = iterator.next();
|
|
236
|
+
}
|
|
237
|
+
if (next.done) return i;
|
|
238
|
+
|
|
239
|
+
const firstKind = getWordNavKind(next.value.segment);
|
|
240
|
+
if (firstKind === "delimiter" || firstKind === "cjk") {
|
|
241
|
+
while (!next.done && getWordNavKind(next.value.segment) === firstKind) {
|
|
242
|
+
i += next.value.segment.length;
|
|
243
|
+
next = iterator.next();
|
|
244
|
+
}
|
|
245
|
+
return i;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (firstKind === "word") {
|
|
249
|
+
let hasLeftWord = false;
|
|
250
|
+
while (!next.done) {
|
|
251
|
+
const segment = next.value.segment;
|
|
252
|
+
const k = getWordNavKind(segment);
|
|
253
|
+
if (k === "word") {
|
|
254
|
+
hasLeftWord = true;
|
|
255
|
+
i += segment.length;
|
|
256
|
+
next = iterator.next();
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (hasLeftWord && k === "delimiter" && isWordNavJoiner(segment)) {
|
|
260
|
+
const lookahead = iterator.next();
|
|
261
|
+
if (!lookahead.done && getWordNavKind(lookahead.value.segment) === "word") {
|
|
262
|
+
i += segment.length;
|
|
263
|
+
next = lookahead;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
return i;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fallback: move by one grapheme.
|
|
273
|
+
return i + next.value.segment.length;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Apply background color to a line, padding to full width.
|
|
278
|
+
*
|
|
279
|
+
* @param line - Line of text (may contain ANSI codes)
|
|
280
|
+
* @param width - Total width to pad to
|
|
281
|
+
* @param bgFn - Background color function
|
|
282
|
+
* @returns Line with background applied and padded to width
|
|
283
|
+
*/
|
|
284
|
+
export function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string {
|
|
285
|
+
// Calculate padding needed
|
|
286
|
+
const visibleLen = visibleWidth(line);
|
|
287
|
+
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
288
|
+
|
|
289
|
+
// Apply background to content + padding
|
|
290
|
+
const withPadding = line + padding(paddingNeeded);
|
|
291
|
+
return bgFn(withPadding);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Extract a range of visible columns from a line. Handles ANSI codes and wide chars.
|
|
296
|
+
*
|
|
297
|
+
* @param strict - If true, exclude wide chars at boundary that would extend past the range
|
|
298
|
+
*/
|
|
299
|
+
export function sliceByColumn(line: string, startCol: number, length: number, strict = false): string {
|
|
300
|
+
return sliceWithWidth(line, startCol, length, strict).text;
|
|
301
|
+
}
|