@gajae-code/tui 0.1.1 → 0.1.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/CHANGELOG.md +12 -0
- package/dist/types/components/editor.d.ts +5 -0
- package/package.json +5 -5
- package/src/components/editor.ts +70 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.3] - 2026-05-28
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Released the current dev branch fixes with refreshed 0.1.3 package metadata.
|
|
10
|
+
|
|
11
|
+
## [0.1.2] - 2026-05-28
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Updated package metadata for the Gajae Code npm publication.
|
|
16
|
+
|
|
5
17
|
## [15.3.2] - 2026-05-25
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -16,6 +16,7 @@ export interface EditorTopBorder {
|
|
|
16
16
|
/** Visible width of the content */
|
|
17
17
|
width: number;
|
|
18
18
|
}
|
|
19
|
+
export type EditorBorderStyle = "round" | "sharp";
|
|
19
20
|
interface HistoryEntry {
|
|
20
21
|
prompt: string;
|
|
21
22
|
}
|
|
@@ -49,7 +50,11 @@ export declare class Editor implements Component, Focusable {
|
|
|
49
50
|
* Show or hide the editor border chrome.
|
|
50
51
|
*/
|
|
51
52
|
setBorderVisible(borderVisible: boolean): void;
|
|
53
|
+
setBorderStyle(borderStyle: EditorBorderStyle): void;
|
|
54
|
+
setClosedBorderBox(closedBorderBox: boolean): void;
|
|
52
55
|
setPromptGutter(promptGutter: string | undefined): void;
|
|
56
|
+
setInputPrefix(inputPrefix: string | undefined): void;
|
|
57
|
+
setPlaceholder(placeholder: string | undefined): void;
|
|
53
58
|
/**
|
|
54
59
|
* Get the available width for top border content given a total terminal width.
|
|
55
60
|
* Accounts for the border characters and horizontal padding when visible.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/tui",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
|
-
"homepage": "https://gajae
|
|
7
|
-
"author": "
|
|
6
|
+
"homepage": "https://gaebal-gajae.dev",
|
|
7
|
+
"author": "Yeachan-Heo",
|
|
8
8
|
"contributors": [
|
|
9
9
|
"Mario Zechner"
|
|
10
10
|
],
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@gajae-code/natives": "0.1.
|
|
41
|
-
"@gajae-code/utils": "0.1.
|
|
40
|
+
"@gajae-code/natives": "0.1.3",
|
|
41
|
+
"@gajae-code/utils": "0.1.3",
|
|
42
42
|
"lru-cache": "11.3.6",
|
|
43
43
|
"marked": "^18.0.3"
|
|
44
44
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -309,6 +309,8 @@ export interface EditorTopBorder {
|
|
|
309
309
|
width: number;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
export type EditorBorderStyle = "round" | "sharp";
|
|
313
|
+
|
|
312
314
|
interface HistoryEntry {
|
|
313
315
|
prompt: string;
|
|
314
316
|
}
|
|
@@ -338,6 +340,9 @@ export class Editor implements Component, Focusable {
|
|
|
338
340
|
/** Display width of the cursorOverride glyph (needed because override may contain ANSI escapes). */
|
|
339
341
|
cursorOverrideWidth: number | undefined;
|
|
340
342
|
#promptGutter: string | undefined;
|
|
343
|
+
#inputPrefix: string | undefined;
|
|
344
|
+
#inputPrefixWidth = 0;
|
|
345
|
+
#placeholder: string | undefined;
|
|
341
346
|
|
|
342
347
|
// Store last layout width for cursor navigation
|
|
343
348
|
#lastLayoutWidth: number = 80;
|
|
@@ -395,6 +400,8 @@ export class Editor implements Component, Focusable {
|
|
|
395
400
|
// Custom top border (for status line integration)
|
|
396
401
|
#topBorderContent?: EditorTopBorder;
|
|
397
402
|
#borderVisible = true;
|
|
403
|
+
#borderStyle: EditorBorderStyle = "round";
|
|
404
|
+
#closedBorderBox = false;
|
|
398
405
|
|
|
399
406
|
constructor(theme: EditorTheme) {
|
|
400
407
|
this.#theme = theme;
|
|
@@ -420,10 +427,28 @@ export class Editor implements Component, Focusable {
|
|
|
420
427
|
this.#borderVisible = borderVisible;
|
|
421
428
|
}
|
|
422
429
|
|
|
430
|
+
setBorderStyle(borderStyle: EditorBorderStyle): void {
|
|
431
|
+
this.#borderStyle = borderStyle;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
setClosedBorderBox(closedBorderBox: boolean): void {
|
|
435
|
+
this.#closedBorderBox = closedBorderBox;
|
|
436
|
+
}
|
|
437
|
+
|
|
423
438
|
setPromptGutter(promptGutter: string | undefined): void {
|
|
424
439
|
this.#promptGutter = promptGutter;
|
|
425
440
|
}
|
|
426
441
|
|
|
442
|
+
setInputPrefix(inputPrefix: string | undefined): void {
|
|
443
|
+
this.#inputPrefix = inputPrefix;
|
|
444
|
+
this.#inputPrefixWidth = inputPrefix ? visibleWidth(inputPrefix) : 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
setPlaceholder(placeholder: string | undefined): void {
|
|
448
|
+
const trimmed = placeholder?.trim();
|
|
449
|
+
this.#placeholder = trimmed ? trimmed : undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
427
452
|
/**
|
|
428
453
|
* Get the available width for top border content given a total terminal width.
|
|
429
454
|
* Accounts for the border characters and horizontal padding when visible.
|
|
@@ -690,11 +715,12 @@ export class Editor implements Component, Focusable {
|
|
|
690
715
|
const borderVisible = this.#borderVisible;
|
|
691
716
|
const promptGutter = this.#getPromptGutter(width, paddingX);
|
|
692
717
|
const contentAreaWidth = this.#getContentWidth(width, paddingX);
|
|
693
|
-
const
|
|
718
|
+
const inputPrefixWidth = this.#inputPrefixWidth;
|
|
719
|
+
const layoutWidth = Math.max(1, this.#getLayoutWidth(width, paddingX) - inputPrefixWidth);
|
|
694
720
|
this.#lastLayoutWidth = layoutWidth;
|
|
695
721
|
|
|
696
|
-
// Box-drawing characters for
|
|
697
|
-
const box = this.#theme.symbols.boxRound;
|
|
722
|
+
// Box-drawing characters for the configured input box shape.
|
|
723
|
+
const box = this.#borderStyle === "sharp" ? this.#theme.symbols.boxSharp : this.#theme.symbols.boxRound;
|
|
698
724
|
const borderWidth = this.#getHorizontalChromeWidth(paddingX);
|
|
699
725
|
const topLeft = this.borderColor(`${box.topLeft}${box.horizontal.repeat(paddingX)}`);
|
|
700
726
|
const topRight = this.borderColor(`${box.horizontal.repeat(paddingX)}${box.topRight}`);
|
|
@@ -733,25 +759,35 @@ export class Editor implements Component, Focusable {
|
|
|
733
759
|
// Render each layout line
|
|
734
760
|
// Emit hardware cursor marker only when focused and not showing autocomplete
|
|
735
761
|
const emitCursorMarker = this.focused && !this.#autocompleteState;
|
|
736
|
-
const lineContentWidth = contentAreaWidth;
|
|
762
|
+
const lineContentWidth = Math.max(0, contentAreaWidth - inputPrefixWidth);
|
|
737
763
|
|
|
738
764
|
// Compute inline hint text (dim ghost text after cursor)
|
|
739
765
|
const inlineHint = this.#getInlineHint();
|
|
740
766
|
const hintStyle = this.#theme.hintStyle ?? ((t: string) => `\x1b[2m${t}\x1b[0m`);
|
|
767
|
+
const showPlaceholder = this.#isEditorEmpty() && !inlineHint && !!this.#placeholder;
|
|
741
768
|
|
|
742
769
|
for (let visibleIndex = 0; visibleIndex < visibleLayoutLines.length; visibleIndex++) {
|
|
743
770
|
const layoutLine = visibleLayoutLines[visibleIndex]!;
|
|
744
771
|
let displayText = layoutLine.text;
|
|
745
772
|
let displayWidth = visibleWidth(layoutLine.text);
|
|
746
773
|
let cursorInPadding = false;
|
|
774
|
+
const absoluteVisibleIndex = this.#scrollOffset + visibleIndex;
|
|
747
775
|
const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
|
|
748
776
|
const gutterText =
|
|
749
777
|
promptGutter === undefined ? "" : showPromptGutter ? promptGutter.firstLine : promptGutter.continuation;
|
|
778
|
+
const inputPrefix = absoluteVisibleIndex === 0 ? (this.#inputPrefix ?? "") : padding(inputPrefixWidth);
|
|
750
779
|
|
|
751
780
|
// Add cursor if this line has it
|
|
752
|
-
|
|
781
|
+
let hasCursor = layoutLine.hasCursor && layoutLine.cursorPos !== undefined;
|
|
753
782
|
const marker = emitCursorMarker ? CURSOR_MARKER : "";
|
|
754
783
|
|
|
784
|
+
if (showPlaceholder) {
|
|
785
|
+
const hintText = hintStyle(truncateToWidth(this.#placeholder ?? "", lineContentWidth));
|
|
786
|
+
displayText = hintText;
|
|
787
|
+
displayWidth = Math.min(visibleWidth(this.#placeholder ?? ""), lineContentWidth);
|
|
788
|
+
hasCursor = false;
|
|
789
|
+
}
|
|
790
|
+
|
|
755
791
|
if (!borderVisible && displayWidth > lineContentWidth) {
|
|
756
792
|
displayText = sliceByColumn(displayText, 0, lineContentWidth, true);
|
|
757
793
|
displayWidth = visibleWidth(displayText);
|
|
@@ -800,10 +836,11 @@ export class Editor implements Component, Focusable {
|
|
|
800
836
|
if (marker) {
|
|
801
837
|
const before = displayText.slice(0, layoutLine.cursorPos);
|
|
802
838
|
const after = displayText.slice(layoutLine.cursorPos);
|
|
803
|
-
|
|
804
|
-
|
|
839
|
+
const ghostText = showPlaceholder ? this.#placeholder : inlineHint;
|
|
840
|
+
if (after.length === 0 && ghostText) {
|
|
841
|
+
const hintText = hintStyle(truncateToWidth(ghostText, Math.max(0, lineContentWidth - displayWidth)));
|
|
805
842
|
displayText = before + marker + hintText;
|
|
806
|
-
displayWidth += visibleWidth(
|
|
843
|
+
displayWidth += Math.min(visibleWidth(ghostText), Math.max(0, lineContentWidth - displayWidth));
|
|
807
844
|
} else if (after.length === 0 && !borderVisible && displayWidth >= lineContentWidth) {
|
|
808
845
|
displayText = this.#renderTerminalCursorMarker(before, marker, lineContentWidth);
|
|
809
846
|
} else {
|
|
@@ -826,6 +863,7 @@ export class Editor implements Component, Focusable {
|
|
|
826
863
|
} else if (this.cursorOverride) {
|
|
827
864
|
// Cursor override replaces the normal end-of-text cursor glyph
|
|
828
865
|
const overrideWidth = this.cursorOverrideWidth ?? 1;
|
|
866
|
+
const ghostText = showPlaceholder ? this.#placeholder : inlineHint;
|
|
829
867
|
if (!borderVisible && displayWidth + overrideWidth > lineContentWidth) {
|
|
830
868
|
// Borderless editors have no spare padding cell for an end-of-line cursor glyph.
|
|
831
869
|
// Preserve cursorOverride by replacing the tail of the line with it.
|
|
@@ -835,11 +873,11 @@ export class Editor implements Component, Focusable {
|
|
|
835
873
|
});
|
|
836
874
|
displayText = widthLimitedCursor.text;
|
|
837
875
|
displayWidth = widthLimitedCursor.width;
|
|
838
|
-
} else if (
|
|
876
|
+
} else if (ghostText) {
|
|
839
877
|
const availWidth = Math.max(0, lineContentWidth - displayWidth - overrideWidth);
|
|
840
|
-
const hintText = hintStyle(truncateToWidth(
|
|
878
|
+
const hintText = hintStyle(truncateToWidth(ghostText, availWidth));
|
|
841
879
|
displayText = before + marker + this.cursorOverride + hintText;
|
|
842
|
-
displayWidth += overrideWidth + Math.min(visibleWidth(
|
|
880
|
+
displayWidth += overrideWidth + Math.min(visibleWidth(ghostText), availWidth);
|
|
843
881
|
} else {
|
|
844
882
|
displayText = before + marker + this.cursorOverride;
|
|
845
883
|
displayWidth += overrideWidth;
|
|
@@ -847,17 +885,18 @@ export class Editor implements Component, Focusable {
|
|
|
847
885
|
} else {
|
|
848
886
|
// Cursor is at the end - add thin cursor glyph
|
|
849
887
|
const { text: cursor, width: cursorWidth } = this.#getStyledInputCursor();
|
|
888
|
+
const ghostText = showPlaceholder ? this.#placeholder : inlineHint;
|
|
850
889
|
if (!borderVisible && displayWidth + cursorWidth > lineContentWidth) {
|
|
851
890
|
// Borderless editors have no spare padding cell for an end-of-line cursor glyph.
|
|
852
891
|
// Highlight the last grapheme so the cursor stays visible without consuming width.
|
|
853
892
|
const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(before, marker, lineContentWidth);
|
|
854
893
|
displayText = widthLimitedCursor.text;
|
|
855
894
|
displayWidth = widthLimitedCursor.width;
|
|
856
|
-
} else if (
|
|
895
|
+
} else if (ghostText) {
|
|
857
896
|
const availWidth = Math.max(0, lineContentWidth - displayWidth - cursorWidth);
|
|
858
|
-
const hintText = hintStyle(truncateToWidth(
|
|
897
|
+
const hintText = hintStyle(truncateToWidth(ghostText, availWidth));
|
|
859
898
|
displayText = before + marker + cursor + hintText;
|
|
860
|
-
displayWidth += cursorWidth + Math.min(visibleWidth(
|
|
899
|
+
displayWidth += cursorWidth + Math.min(visibleWidth(ghostText), availWidth);
|
|
861
900
|
} else {
|
|
862
901
|
displayText = before + marker + cursor;
|
|
863
902
|
displayWidth += cursorWidth;
|
|
@@ -868,29 +907,41 @@ export class Editor implements Component, Focusable {
|
|
|
868
907
|
}
|
|
869
908
|
}
|
|
870
909
|
|
|
910
|
+
const displayWithPrefix = inputPrefix + displayText;
|
|
871
911
|
const linePad = padding(Math.max(0, lineContentWidth - displayWidth));
|
|
872
912
|
|
|
873
913
|
if (!borderVisible) {
|
|
874
|
-
result.push(gutterText +
|
|
914
|
+
result.push(gutterText + displayWithPrefix + linePad);
|
|
875
915
|
continue;
|
|
876
916
|
}
|
|
877
917
|
|
|
878
|
-
// All lines have consistent borders based on padding
|
|
918
|
+
// All lines have consistent borders based on padding.
|
|
879
919
|
const isLastLine = visibleIndex === visibleLayoutLines.length - 1;
|
|
880
920
|
const rightPaddingWidth = Math.max(0, paddingX - (cursorInPadding ? 1 : 0));
|
|
881
|
-
if (
|
|
921
|
+
if (this.#closedBorderBox) {
|
|
922
|
+
const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`);
|
|
923
|
+
const rightBorder = this.borderColor(`${padding(rightPaddingWidth)}${box.vertical}`);
|
|
924
|
+
result.push(leftBorder + displayWithPrefix + linePad + rightBorder);
|
|
925
|
+
} else if (isLastLine) {
|
|
882
926
|
const bottomRightPadding = Math.max(0, paddingX - 1 - (cursorInPadding ? 1 : 0));
|
|
883
927
|
const bottomRightAdjusted = this.borderColor(
|
|
884
928
|
`${padding(bottomRightPadding)}${box.horizontal}${box.bottomRight}`,
|
|
885
929
|
);
|
|
886
|
-
result.push(`${bottomLeft}${
|
|
930
|
+
result.push(`${bottomLeft}${displayWithPrefix}${linePad}${bottomRightAdjusted}`);
|
|
887
931
|
} else {
|
|
888
932
|
const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`);
|
|
889
933
|
const rightBorder = this.borderColor(`${padding(rightPaddingWidth)}${box.vertical}`);
|
|
890
|
-
result.push(leftBorder +
|
|
934
|
+
result.push(leftBorder + displayWithPrefix + linePad + rightBorder);
|
|
891
935
|
}
|
|
892
936
|
}
|
|
893
937
|
|
|
938
|
+
if (borderVisible && this.#closedBorderBox) {
|
|
939
|
+
const bottomFillWidth = Math.max(0, width - borderWidth * 2);
|
|
940
|
+
const bottomLeftClosed = this.borderColor(`${box.bottomLeft}${box.horizontal.repeat(paddingX)}`);
|
|
941
|
+
const bottomRightClosed = this.borderColor(`${box.horizontal.repeat(paddingX)}${box.bottomRight}`);
|
|
942
|
+
result.push(bottomLeftClosed + horizontal.repeat(bottomFillWidth) + bottomRightClosed);
|
|
943
|
+
}
|
|
944
|
+
|
|
894
945
|
// Add autocomplete list if active
|
|
895
946
|
if (this.#autocompleteState && this.#autocompleteList) {
|
|
896
947
|
const autocompleteResult = this.#autocompleteList.render(width);
|