@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 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.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-code.dev",
7
- "author": "Can Boluk",
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.1",
41
- "@gajae-code/utils": "0.1.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
  },
@@ -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 layoutWidth = this.#getLayoutWidth(width, paddingX);
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 rounded corners
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
- const hasCursor = layoutLine.hasCursor && layoutLine.cursorPos !== undefined;
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
- if (after.length === 0 && inlineHint) {
804
- const hintText = hintStyle(truncateToWidth(inlineHint, Math.max(0, lineContentWidth - displayWidth)));
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(inlineHint);
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 (inlineHint) {
876
+ } else if (ghostText) {
839
877
  const availWidth = Math.max(0, lineContentWidth - displayWidth - overrideWidth);
840
- const hintText = hintStyle(truncateToWidth(inlineHint, availWidth));
878
+ const hintText = hintStyle(truncateToWidth(ghostText, availWidth));
841
879
  displayText = before + marker + this.cursorOverride + hintText;
842
- displayWidth += overrideWidth + Math.min(visibleWidth(inlineHint), availWidth);
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 (inlineHint) {
895
+ } else if (ghostText) {
857
896
  const availWidth = Math.max(0, lineContentWidth - displayWidth - cursorWidth);
858
- const hintText = hintStyle(truncateToWidth(inlineHint, availWidth));
897
+ const hintText = hintStyle(truncateToWidth(ghostText, availWidth));
859
898
  displayText = before + marker + cursor + hintText;
860
- displayWidth += cursorWidth + Math.min(visibleWidth(inlineHint), availWidth);
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 + displayText + linePad);
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 (isLastLine) {
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}${displayText}${linePad}${bottomRightAdjusted}`);
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 + displayText + linePad + rightBorder);
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);