@apholdings/jensen-tui 0.0.1 → 0.0.3
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/README.md +11 -11
- package/dist/components/cancellable-loader.d.ts.map +1 -1
- package/dist/components/cancellable-loader.js +1 -1
- package/dist/components/cancellable-loader.js.map +1 -1
- package/dist/components/editor.d.ts +9 -4
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +77 -71
- package/dist/components/editor.js.map +1 -1
- package/dist/components/input.d.ts +1 -0
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +12 -14
- package/dist/components/input.js.map +1 -1
- package/dist/components/loader.d.ts +13 -2
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +105 -9
- package/dist/components/loader.js.map +1 -1
- package/dist/components/text.d.ts +6 -6
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +9 -7
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +8 -0
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -84,6 +84,8 @@ export class Editor {
|
|
|
84
84
|
cursorLine: 0,
|
|
85
85
|
cursorCol: 0,
|
|
86
86
|
};
|
|
87
|
+
promptBg;
|
|
88
|
+
promptChromeBg;
|
|
87
89
|
/** Focusable interface - set by TUI when focus changes */
|
|
88
90
|
focused = false;
|
|
89
91
|
tui;
|
|
@@ -99,7 +101,7 @@ export class Editor {
|
|
|
99
101
|
promptGlyph = DEFAULT_PROMPT_GLYPH;
|
|
100
102
|
placeholderText = "";
|
|
101
103
|
promptGlyphStyle = (str) => str;
|
|
102
|
-
placeholderStyle = (str) => `\x1b[90m${str}\x1b[
|
|
104
|
+
placeholderStyle = (str) => `\x1b[90m${str}\x1b[39m`;
|
|
103
105
|
// Autocomplete support
|
|
104
106
|
autocompleteProvider;
|
|
105
107
|
autocompleteList;
|
|
@@ -131,6 +133,8 @@ export class Editor {
|
|
|
131
133
|
this.tui = tui;
|
|
132
134
|
this.theme = theme;
|
|
133
135
|
this.borderColor = theme.borderColor;
|
|
136
|
+
this.promptBg = theme.promptBg;
|
|
137
|
+
this.promptChromeBg = theme.promptChromeBg ?? theme.promptBg;
|
|
134
138
|
const paddingX = options.paddingX ?? 0;
|
|
135
139
|
this.paddingX = Number.isFinite(paddingX) ? Math.max(0, Math.floor(paddingX)) : 0;
|
|
136
140
|
const maxVisible = options.autocompleteMaxVisible ?? 5;
|
|
@@ -138,7 +142,28 @@ export class Editor {
|
|
|
138
142
|
this.promptGlyph = options.promptGlyph ?? DEFAULT_PROMPT_GLYPH;
|
|
139
143
|
this.placeholderText = options.placeholderText ?? "";
|
|
140
144
|
this.promptGlyphStyle = options.promptGlyphStyle ?? ((str) => `\x1b[1m${this.borderColor(str)}\x1b[22m`);
|
|
141
|
-
this.placeholderStyle = options.placeholderStyle ?? ((str) => `\x1b[90m${str}\x1b[
|
|
145
|
+
this.placeholderStyle = options.placeholderStyle ?? ((str) => `\x1b[90m${str}\x1b[39m`);
|
|
146
|
+
}
|
|
147
|
+
sanitizePromptBgLine(line) {
|
|
148
|
+
// Keep style resets that do NOT clear background.
|
|
149
|
+
// Avoid \x1b[0m because it resets everything, including the prompt background.
|
|
150
|
+
return line.replace(/\x1b\[0m/g, "\x1b[22m\x1b[23m\x1b[24m\x1b[27m\x1b[39m");
|
|
151
|
+
}
|
|
152
|
+
padToVisualWidth(line, width) {
|
|
153
|
+
const padding = Math.max(0, width - visibleWidth(line));
|
|
154
|
+
return line + " ".repeat(padding);
|
|
155
|
+
}
|
|
156
|
+
applyPromptBg(line, width) {
|
|
157
|
+
const safeLine = this.padToVisualWidth(this.sanitizePromptBgLine(line), width);
|
|
158
|
+
if (!this.promptBg)
|
|
159
|
+
return safeLine;
|
|
160
|
+
return this.promptBg(safeLine);
|
|
161
|
+
}
|
|
162
|
+
applyPromptChromeBg(line, width) {
|
|
163
|
+
const safeLine = this.padToVisualWidth(this.sanitizePromptBgLine(line), width);
|
|
164
|
+
if (!this.promptChromeBg)
|
|
165
|
+
return safeLine;
|
|
166
|
+
return this.promptChromeBg(safeLine);
|
|
142
167
|
}
|
|
143
168
|
getPaddingX() {
|
|
144
169
|
return this.paddingX;
|
|
@@ -287,38 +312,11 @@ export class Editor {
|
|
|
287
312
|
};
|
|
288
313
|
});
|
|
289
314
|
}
|
|
290
|
-
stylePromptPrefix(text) {
|
|
291
|
-
if (text.length === 0)
|
|
292
|
-
return text;
|
|
293
|
-
const { first: glyph, rest } = splitFirstGrapheme(text);
|
|
294
|
-
if (glyph.trim().length === 0) {
|
|
295
|
-
return this.borderColor(text);
|
|
296
|
-
}
|
|
297
|
-
const styledGlyph = `\x1b[1m${this.borderColor(glyph)}\x1b[22m`;
|
|
298
|
-
const styledRest = rest.length > 0 ? this.borderColor(rest) : "";
|
|
299
|
-
return styledGlyph + styledRest;
|
|
300
|
-
}
|
|
301
|
-
stylePlaceholderLine(text, promptPrefixLength) {
|
|
302
|
-
if (!promptPrefixLength || promptPrefixLength <= 0) {
|
|
303
|
-
return this.placeholderStyle(text);
|
|
304
|
-
}
|
|
305
|
-
const promptPrefix = text.slice(0, promptPrefixLength);
|
|
306
|
-
const body = text.slice(promptPrefixLength);
|
|
307
|
-
return this.stylePromptPrefix(promptPrefix) + (body ? this.placeholderStyle(body) : "");
|
|
308
|
-
}
|
|
309
315
|
getPromptPrefixWidth() {
|
|
310
316
|
if (!this.promptGlyph)
|
|
311
317
|
return 0;
|
|
312
318
|
return visibleWidth(this.promptGlyph) + 1;
|
|
313
319
|
}
|
|
314
|
-
getRenderedPromptPrefix(isFirstVisualLine, promptPrefixWidth) {
|
|
315
|
-
if (!promptPrefixWidth)
|
|
316
|
-
return "";
|
|
317
|
-
if (isFirstVisualLine) {
|
|
318
|
-
return `${this.promptGlyphStyle(this.promptGlyph)} `;
|
|
319
|
-
}
|
|
320
|
-
return " ".repeat(promptPrefixWidth);
|
|
321
|
-
}
|
|
322
320
|
render(width) {
|
|
323
321
|
const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
|
|
324
322
|
const paddingX = Math.min(this.paddingX, maxPadding);
|
|
@@ -330,6 +328,9 @@ export class Editor {
|
|
|
330
328
|
// Store for cursor navigation (must match wrapping width)
|
|
331
329
|
this.lastWidth = layoutWidth;
|
|
332
330
|
const horizontal = this.borderColor("─");
|
|
331
|
+
const glyphWidth = visibleWidth(this.promptGlyph);
|
|
332
|
+
const paintFullLine = (line) => this.applyPromptBg(line, width);
|
|
333
|
+
const paintChromeLine = (line) => this.applyPromptChromeBg(line, width);
|
|
333
334
|
// Layout the text
|
|
334
335
|
const layoutLines = this.layoutText(layoutWidth);
|
|
335
336
|
// Calculate max visible lines: 30% of terminal height, minimum 5 lines
|
|
@@ -354,66 +355,67 @@ export class Editor {
|
|
|
354
355
|
const result = [];
|
|
355
356
|
const leftPadding = " ".repeat(paddingX);
|
|
356
357
|
const rightPadding = leftPadding;
|
|
357
|
-
//
|
|
358
|
+
// Top border
|
|
358
359
|
if (this.scrollOffset > 0) {
|
|
359
360
|
const indicator = `─── ↑ ${this.scrollOffset} more `;
|
|
360
361
|
const remaining = width - visibleWidth(indicator);
|
|
361
|
-
result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
|
|
362
|
+
result.push(paintChromeLine(this.borderColor(indicator + "─".repeat(Math.max(0, remaining)))));
|
|
362
363
|
}
|
|
363
364
|
else {
|
|
364
|
-
result.push(horizontal.repeat(width));
|
|
365
|
+
result.push(paintChromeLine(horizontal.repeat(width)));
|
|
365
366
|
}
|
|
366
|
-
// Render each visible layout line
|
|
367
367
|
// Emit hardware cursor marker only when focused and not showing autocomplete
|
|
368
368
|
const emitCursorMarker = this.focused && !this.autocompleteState;
|
|
369
369
|
for (let visibleIndex = 0; visibleIndex < visibleLines.length; visibleIndex++) {
|
|
370
370
|
const layoutLine = visibleLines[visibleIndex];
|
|
371
371
|
const absoluteLineIndex = this.scrollOffset + visibleIndex;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
372
|
+
let prefixPart = leftPadding;
|
|
373
|
+
let bodyPart = "";
|
|
374
|
+
if (layoutLine.isPlaceholder && layoutLine.promptPrefixLength !== undefined) {
|
|
375
|
+
const promptPrefixText = layoutLine.text.slice(0, layoutLine.promptPrefixLength);
|
|
376
|
+
const { first: glyph } = splitFirstGrapheme(promptPrefixText);
|
|
377
|
+
const glyphSpace = promptPrefixText.slice(glyph.length);
|
|
378
|
+
prefixPart += glyph.trim() ? this.promptGlyphStyle(glyph) : " ".repeat(visibleWidth(glyph));
|
|
379
|
+
bodyPart = glyphSpace;
|
|
380
|
+
}
|
|
381
|
+
else if (promptPrefixWidth > 0) {
|
|
382
|
+
if (absoluteLineIndex === 0) {
|
|
383
|
+
prefixPart += this.promptGlyphStyle(this.promptGlyph);
|
|
384
|
+
bodyPart = " ";
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
prefixPart += " ".repeat(glyphWidth);
|
|
388
|
+
bodyPart = " ";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
let displayText = "";
|
|
392
|
+
let lineVisibleWidth = 0;
|
|
377
393
|
let cursorInPadding = false;
|
|
378
394
|
// Add cursor if this line has it
|
|
379
395
|
if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
380
396
|
const marker = emitCursorMarker ? CURSOR_MARKER : "";
|
|
381
397
|
if (layoutLine.isPlaceholder) {
|
|
382
|
-
const cursor = "\x1b[7m \x1b[
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
displayText =
|
|
387
|
-
this.stylePromptPrefix(promptPrefixText) +
|
|
388
|
-
marker +
|
|
389
|
-
cursor +
|
|
390
|
-
this.placeholderStyle(placeholderBodyText);
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
displayText = marker + cursor + this.placeholderStyle(layoutLine.text);
|
|
394
|
-
}
|
|
395
|
-
lineVisibleWidth = 1 + visibleWidth(layoutLine.text);
|
|
398
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
399
|
+
const placeholderBodyText = layoutLine.text.slice(layoutLine.promptPrefixLength ?? 0);
|
|
400
|
+
displayText = marker + cursor + this.placeholderStyle(placeholderBodyText);
|
|
401
|
+
lineVisibleWidth = 1 + visibleWidth(placeholderBodyText);
|
|
396
402
|
}
|
|
397
403
|
else {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
404
|
+
const lineText = layoutLine.text;
|
|
405
|
+
const before = lineText.slice(0, layoutLine.cursorPos);
|
|
406
|
+
const after = lineText.slice(layoutLine.cursorPos);
|
|
401
407
|
if (after.length > 0) {
|
|
402
|
-
// Cursor is on a character (grapheme) - replace it with highlighted version
|
|
403
|
-
// Get the first grapheme from 'after'
|
|
404
408
|
const afterGraphemes = [...segmenter.segment(after)];
|
|
405
409
|
const firstGrapheme = afterGraphemes[0]?.segment || "";
|
|
406
410
|
const restAfter = after.slice(firstGrapheme.length);
|
|
407
|
-
const cursor = `\x1b[7m${firstGrapheme}\x1b[
|
|
411
|
+
const cursor = `\x1b[7m${firstGrapheme}\x1b[27m`;
|
|
408
412
|
displayText = before + marker + cursor + restAfter;
|
|
409
|
-
|
|
413
|
+
lineVisibleWidth = visibleWidth(lineText);
|
|
410
414
|
}
|
|
411
415
|
else {
|
|
412
|
-
|
|
413
|
-
const cursor = "\x1b[7m \x1b[0m";
|
|
416
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
414
417
|
displayText = before + marker + cursor;
|
|
415
|
-
lineVisibleWidth =
|
|
416
|
-
// If cursor overflows content width into the padding, flag it
|
|
418
|
+
lineVisibleWidth = visibleWidth(lineText) + 1;
|
|
417
419
|
if (lineVisibleWidth > layoutWidth && paddingX > 0) {
|
|
418
420
|
cursorInPadding = true;
|
|
419
421
|
}
|
|
@@ -421,24 +423,28 @@ export class Editor {
|
|
|
421
423
|
}
|
|
422
424
|
}
|
|
423
425
|
else if (layoutLine.isPlaceholder) {
|
|
424
|
-
|
|
426
|
+
const placeholderBodyText = layoutLine.text.slice(layoutLine.promptPrefixLength ?? 0);
|
|
427
|
+
displayText = this.placeholderStyle(placeholderBodyText);
|
|
428
|
+
lineVisibleWidth = visibleWidth(placeholderBodyText);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
displayText = layoutLine.text;
|
|
425
432
|
lineVisibleWidth = visibleWidth(layoutLine.text);
|
|
426
433
|
}
|
|
427
|
-
|
|
434
|
+
bodyPart += displayText;
|
|
428
435
|
const linePadding = " ".repeat(Math.max(0, layoutWidth - lineVisibleWidth));
|
|
429
436
|
const lineRightPadding = cursorInPadding ? rightPadding.slice(1) : rightPadding;
|
|
430
|
-
|
|
431
|
-
result.push(`${leftPadding}${promptPrefix}${displayText}${linePadding}${lineRightPadding}`);
|
|
437
|
+
result.push(paintFullLine(prefixPart + bodyPart + linePadding + lineRightPadding));
|
|
432
438
|
}
|
|
433
|
-
//
|
|
439
|
+
// Bottom border
|
|
434
440
|
const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
|
|
435
441
|
if (linesBelow > 0) {
|
|
436
442
|
const indicator = `─── ↓ ${linesBelow} more `;
|
|
437
443
|
const remaining = width - visibleWidth(indicator);
|
|
438
|
-
result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
|
|
444
|
+
result.push(paintChromeLine(this.borderColor(indicator + "─".repeat(Math.max(0, remaining)))));
|
|
439
445
|
}
|
|
440
446
|
else {
|
|
441
|
-
result.push(horizontal.repeat(width));
|
|
447
|
+
result.push(paintChromeLine(horizontal.repeat(width)));
|
|
442
448
|
}
|
|
443
449
|
// Add autocomplete list if active
|
|
444
450
|
if (this.autocompleteState && this.autocompleteList) {
|
|
@@ -447,7 +453,7 @@ export class Editor {
|
|
|
447
453
|
for (const line of autocompleteResult) {
|
|
448
454
|
const lineWidth = visibleWidth(line);
|
|
449
455
|
const linePadding = " ".repeat(Math.max(0, layoutWidth - lineWidth));
|
|
450
|
-
result.push(
|
|
456
|
+
result.push(paintFullLine(leftPadding + autocompleteIndent + line + linePadding + rightPadding));
|
|
451
457
|
}
|
|
452
458
|
}
|
|
453
459
|
return result;
|