@oh-my-pi/pi-tui 13.15.3 → 13.16.1

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,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.16.1] - 2026-03-27
6
+
7
+ ### Added
8
+
9
+ - Support for optional SearchDb parameter in CombinedAutocompleteProvider constructor for improved fuzzy search performance
10
+ - Fuzzy matching filter for autocomplete suggestions to improve relevance of results
11
+
12
+ ### Changed
13
+
14
+ - Fuzzy discovery now applies fuzzy matching filter to results for improved relevance of autocomplete suggestions
15
+ - Autocomplete fuzzy discovery now accepts optional SearchDb instance for faster searches
16
+
17
+ ## [13.16.0] - 2026-03-27
18
+ ### Changed
19
+
20
+ - Updated tab replacement in editor text sanitization to respect configured tab width setting
21
+
5
22
  ## [13.15.0] - 2026-03-23
6
23
 
7
24
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "13.15.3",
4
+ "version": "13.16.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -33,8 +33,8 @@
33
33
  "test": "bun test test/*.test.ts"
34
34
  },
35
35
  "dependencies": {
36
- "@oh-my-pi/pi-natives": "13.15.3",
37
- "@oh-my-pi/pi-utils": "13.15.3",
36
+ "@oh-my-pi/pi-natives": "13.16.1",
37
+ "@oh-my-pi/pi-utils": "13.16.1",
38
38
  "marked": "^17.0"
39
39
  },
40
40
  "devDependencies": {
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import type { SearchDb } from "@oh-my-pi/pi-natives";
4
5
  import { fuzzyFind } from "@oh-my-pi/pi-natives";
5
6
  import { getProjectDir } from "@oh-my-pi/pi-utils";
6
7
 
@@ -203,11 +204,17 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
203
204
  // per-directory readdir fast-path for prefix completions. Global fuzzy
204
205
  // discovery continues to use native fuzzyFind + shared scan cache.
205
206
  #dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
207
+ #searchDb?: SearchDb;
206
208
  readonly #DIR_CACHE_TTL = 2000; // 2 seconds
207
209
 
208
- constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = getProjectDir()) {
210
+ constructor(
211
+ commands: (SlashCommand | AutocompleteItem)[] = [],
212
+ basePath: string = getProjectDir(),
213
+ searchDb?: SearchDb,
214
+ ) {
209
215
  this.#commands = commands;
210
216
  this.#basePath = basePath;
217
+ this.#searchDb = searchDb;
211
218
  }
212
219
 
213
220
  async getSuggestions(
@@ -675,11 +682,15 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
675
682
  const scopedQuery = await this.#resolveScopedFuzzyQuery(query);
676
683
  const searchPath = scopedQuery?.baseDir ?? this.#basePath;
677
684
  const fuzzyQuery = scopedQuery?.query ?? query;
678
- const result = await fuzzyFind(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath));
685
+ const result = await fuzzyFind(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath), this.#searchDb);
686
+ const lowerQuery = fuzzyQuery.toLowerCase();
679
687
  const filteredMatches = result.matches.filter(entry => {
680
688
  const p = entry.path.endsWith("/") ? entry.path.slice(0, -1) : entry.path;
681
689
  const normalized = p.replaceAll("\\", "/");
682
- return !/(^|\/)\.git(\/|$)/.test(normalized);
690
+ if (/(^|\/)\.git(\/|$)/.test(normalized)) {
691
+ return false;
692
+ }
693
+ return lowerQuery.length === 0 || fuzzyMatch(lowerQuery, normalized.toLowerCase());
683
694
  });
684
695
  const topEntries = filteredMatches.slice(0, 20);
685
696
  const suggestions: AutocompleteItem[] = [];
@@ -12,6 +12,8 @@ import {
12
12
  moveWordLeft,
13
13
  moveWordRight,
14
14
  padding,
15
+ replaceTabs,
16
+ sliceByColumn,
15
17
  truncateToWidth,
16
18
  visibleWidth,
17
19
  } from "../utils";
@@ -22,6 +24,13 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
22
24
  maxPrimaryColumnWidth: 32,
23
25
  };
24
26
 
27
+ function sanitizeLoadedText(text: string): string {
28
+ return replaceTabs(text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"))
29
+ .split("")
30
+ .filter(char => char === "\n" || char.charCodeAt(0) >= 32)
31
+ .join("");
32
+ }
33
+
25
34
  const segmenter = getSegmenter();
26
35
 
27
36
  /**
@@ -318,6 +327,7 @@ export class Editor implements Component, Focusable {
318
327
  cursorOverride: string | undefined;
319
328
  /** Display width of the cursorOverride glyph (needed because override may contain ANSI escapes). */
320
329
  cursorOverrideWidth: number | undefined;
330
+ #promptGutter: string | undefined;
321
331
 
322
332
  // Store last layout width for cursor navigation
323
333
  #lastLayoutWidth: number = 80;
@@ -374,6 +384,7 @@ export class Editor implements Component, Focusable {
374
384
 
375
385
  // Custom top border (for status line integration)
376
386
  #topBorderContent?: EditorTopBorder;
387
+ #borderVisible = true;
377
388
 
378
389
  constructor(theme: EditorTheme) {
379
390
  this.#theme = theme;
@@ -392,13 +403,24 @@ export class Editor implements Component, Focusable {
392
403
  this.#topBorderContent = content;
393
404
  }
394
405
 
406
+ /**
407
+ * Show or hide the editor border chrome.
408
+ */
409
+ setBorderVisible(borderVisible: boolean): void {
410
+ this.#borderVisible = borderVisible;
411
+ }
412
+
413
+ setPromptGutter(promptGutter: string | undefined): void {
414
+ this.#promptGutter = promptGutter;
415
+ }
416
+
395
417
  /**
396
418
  * Get the available width for top border content given a total terminal width.
397
- * Accounts for the border characters and horizontal padding.
419
+ * Accounts for the border characters and horizontal padding when visible.
398
420
  */
399
421
  getTopBorderAvailableWidth(terminalWidth: number): number {
400
422
  const paddingX = this.#getEditorPaddingX();
401
- const borderWidth = paddingX + 1;
423
+ const borderWidth = this.#getHorizontalChromeWidth(paddingX);
402
424
  return Math.max(0, terminalWidth - borderWidth * 2);
403
425
  }
404
426
 
@@ -492,7 +514,7 @@ export class Editor implements Component, Focusable {
492
514
  /** Internal setText that doesn't reset history state - used by navigateHistory */
493
515
  #setTextInternal(text: string, cursorAnchor: HistoryCursorAnchor = "end"): void {
494
516
  this.#undoStack.length = 0;
495
- const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
517
+ const lines = sanitizeLoadedText(text).split("\n");
496
518
  this.#state.lines = lines.length === 0 ? [""] : lines;
497
519
  if (cursorAnchor === "start") {
498
520
  this.#state.cursorLine = 0;
@@ -515,18 +537,113 @@ export class Editor implements Component, Focusable {
515
537
  return Math.max(0, padding);
516
538
  }
517
539
 
540
+ #getHorizontalChromeWidth(paddingX: number): number {
541
+ return this.#borderVisible ? paddingX + 1 : 0;
542
+ }
543
+
544
+ #getPromptGutterWidth(width: number, paddingX: number): number {
545
+ if (this.#borderVisible || !this.#promptGutter) return 0;
546
+ const chromeWidth = 2 * this.#getHorizontalChromeWidth(paddingX);
547
+ const availableWidth = Math.max(0, width - chromeWidth);
548
+ return Math.min(visibleWidth(this.#promptGutter), availableWidth);
549
+ }
550
+
551
+ #getPromptGutter(
552
+ width: number,
553
+ paddingX: number,
554
+ ): { firstLine: string; continuation: string; width: number } | undefined {
555
+ if (this.#borderVisible || !this.#promptGutter) return undefined;
556
+ const gutterWidth = this.#getPromptGutterWidth(width, paddingX);
557
+ if (gutterWidth === 0) return undefined;
558
+ return {
559
+ firstLine: sliceByColumn(this.#promptGutter, 0, gutterWidth, true),
560
+ continuation: padding(gutterWidth),
561
+ width: gutterWidth,
562
+ };
563
+ }
564
+
518
565
  #getContentWidth(width: number, paddingX: number): number {
519
- return Math.max(0, width - 2 * (paddingX + 1));
566
+ const chromeWidth = 2 * this.#getHorizontalChromeWidth(paddingX);
567
+ return Math.max(0, width - chromeWidth - this.#getPromptGutterWidth(width, paddingX));
520
568
  }
521
569
 
522
570
  #getLayoutWidth(width: number, paddingX: number): number {
523
571
  const contentWidth = this.#getContentWidth(width, paddingX);
524
- return Math.max(1, contentWidth - (paddingX === 0 ? 1 : 0));
572
+ const cursorReserve = this.#borderVisible && paddingX === 0 ? 1 : 0;
573
+ // Keep cursor/scroll layout addressable even when a borderless prompt gutter consumes every visible column.
574
+ return Math.max(1, contentWidth - cursorReserve);
525
575
  }
526
576
 
527
577
  #getVisibleContentHeight(contentLines: number): number {
528
578
  if (this.#maxHeight === undefined) return contentLines;
529
- return Math.max(1, this.#maxHeight - 2);
579
+ const verticalChrome = this.#borderVisible ? 2 : 0;
580
+ return Math.max(1, this.#maxHeight - verticalChrome);
581
+ }
582
+
583
+ #getStyledInputCursor(): { text: string; width: number } {
584
+ const cursorChar = this.#theme.symbols.inputCursor;
585
+ return { text: `\x1b[5m${cursorChar}\x1b[0m`, width: visibleWidth(cursorChar) };
586
+ }
587
+
588
+ #renderEndOfLineCursorAtWidthLimit(
589
+ before: string,
590
+ marker: string,
591
+ maxWidth: number,
592
+ replacement?: { text: string; width: number },
593
+ ): { text: string; width: number } {
594
+ const beforeGraphemes = [...segmenter.segment(before)];
595
+ const lastGrapheme = beforeGraphemes[beforeGraphemes.length - 1]?.segment;
596
+ const lastGraphemeWidth = lastGrapheme ? visibleWidth(lastGrapheme) : 0;
597
+ const builtInCursor = this.#getStyledInputCursor();
598
+ const fallbackReplacement = lastGrapheme
599
+ ? { text: `\x1b[7m${lastGrapheme}\x1b[0m`, width: lastGraphemeWidth }
600
+ : builtInCursor;
601
+ const clampReplacement = (candidate: { text: string; width: number }): { text: string; width: number } => {
602
+ let text = sliceByColumn(candidate.text, 0, maxWidth, true);
603
+ let width = visibleWidth(text);
604
+ if (width > maxWidth) {
605
+ text = "";
606
+ width = 0;
607
+ }
608
+ return { text, width };
609
+ };
610
+
611
+ let clampedReplacement = clampReplacement(replacement ?? fallbackReplacement);
612
+ if (replacement && clampedReplacement.width === 0) {
613
+ // A custom override that cannot fit at all should first fall back to the highlighted tail.
614
+ clampedReplacement = clampReplacement(fallbackReplacement);
615
+ }
616
+ if (lastGrapheme && clampedReplacement.width === 0) {
617
+ // If even the highlighted trailing grapheme cannot fit, show the built-in single-column cursor.
618
+ clampedReplacement = clampReplacement(builtInCursor);
619
+ }
620
+
621
+ const replacedSpanWidth = Math.min(maxWidth, Math.max(lastGraphemeWidth, clampedReplacement.width));
622
+ const prefixWidth = Math.max(0, maxWidth - replacedSpanWidth);
623
+ const beforePrefix = sliceByColumn(before, 0, prefixWidth, true);
624
+ const replacementPad = padding(Math.max(0, replacedSpanWidth - clampedReplacement.width));
625
+ return {
626
+ text: `${beforePrefix}${replacementPad}${clampedReplacement.text}${marker}`,
627
+ width: visibleWidth(beforePrefix) + replacedSpanWidth,
628
+ };
629
+ }
630
+
631
+ #renderTerminalCursorMarker(text: string, marker: string, maxWidth: number): string {
632
+ if (!marker) return text;
633
+ if (visibleWidth(text) < maxWidth) {
634
+ return text + marker;
635
+ }
636
+
637
+ let insertAt = text.length;
638
+ let offset = 0;
639
+ for (const seg of segmenter.segment(text)) {
640
+ if (visibleWidth(seg.segment) > 0) {
641
+ insertAt = offset;
642
+ }
643
+ offset += seg.segment.length;
644
+ }
645
+
646
+ return `${text.slice(0, insertAt)}${marker}${text.slice(insertAt)}`;
530
647
  }
531
648
 
532
649
  #getPageScrollStep(totalVisualLines: number): number {
@@ -555,13 +672,15 @@ export class Editor implements Component, Focusable {
555
672
 
556
673
  render(width: number): string[] {
557
674
  const paddingX = this.#getEditorPaddingX();
675
+ const borderVisible = this.#borderVisible;
676
+ const promptGutter = this.#getPromptGutter(width, paddingX);
558
677
  const contentAreaWidth = this.#getContentWidth(width, paddingX);
559
678
  const layoutWidth = this.#getLayoutWidth(width, paddingX);
560
679
  this.#lastLayoutWidth = layoutWidth;
561
680
 
562
681
  // Box-drawing characters for rounded corners
563
682
  const box = this.#theme.symbols.boxRound;
564
- const borderWidth = paddingX + 1;
683
+ const borderWidth = this.#getHorizontalChromeWidth(paddingX);
565
684
  const topLeft = this.borderColor(`${box.topLeft}${box.horizontal.repeat(paddingX)}`);
566
685
  const topRight = this.borderColor(`${box.horizontal.repeat(paddingX)}${box.topRight}`);
567
686
  const bottomLeft = this.borderColor(`${box.bottomLeft}${box.horizontal}${padding(Math.max(0, paddingX - 1))}`);
@@ -575,23 +694,25 @@ export class Editor implements Component, Focusable {
575
694
 
576
695
  const result: string[] = [];
577
696
 
578
- // Render top border: ╭─ [status content] ────────────────╮
579
- const topFillWidth = width - borderWidth * 2;
580
- if (this.#topBorderContent) {
581
- const { content, width: statusWidth } = this.#topBorderContent;
582
- if (statusWidth <= topFillWidth) {
583
- // Status fits - add fill after it
584
- const fillWidth = topFillWidth - statusWidth;
585
- result.push(topLeft + content + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
697
+ if (borderVisible) {
698
+ // Render top border: ╭─ [status content] ────────────────╮
699
+ const topFillWidth = Math.max(0, width - borderWidth * 2);
700
+ if (this.#topBorderContent) {
701
+ const { content, width: statusWidth } = this.#topBorderContent;
702
+ if (statusWidth <= topFillWidth) {
703
+ // Status fits - add fill after it
704
+ const fillWidth = topFillWidth - statusWidth;
705
+ result.push(topLeft + content + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
706
+ } else {
707
+ // Status too long - truncate it
708
+ const truncated = truncateToWidth(content, Math.max(0, topFillWidth - 1));
709
+ const truncatedWidth = visibleWidth(truncated);
710
+ const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
711
+ result.push(topLeft + truncated + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
712
+ }
586
713
  } else {
587
- // Status too long - truncate it
588
- const truncated = truncateToWidth(content, topFillWidth - 1);
589
- const truncatedWidth = visibleWidth(truncated);
590
- const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
591
- result.push(topLeft + truncated + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
714
+ result.push(topLeft + horizontal.repeat(topFillWidth) + topRight);
592
715
  }
593
- } else {
594
- result.push(topLeft + horizontal.repeat(topFillWidth) + topRight);
595
716
  }
596
717
 
597
718
  // Render each layout line
@@ -603,15 +724,63 @@ export class Editor implements Component, Focusable {
603
724
  const inlineHint = this.#getInlineHint();
604
725
  const hintStyle = this.#theme.hintStyle ?? ((t: string) => `\x1b[2m${t}\x1b[0m`);
605
726
 
606
- for (const layoutLine of visibleLayoutLines) {
727
+ for (let visibleIndex = 0; visibleIndex < visibleLayoutLines.length; visibleIndex++) {
728
+ const layoutLine = visibleLayoutLines[visibleIndex]!;
607
729
  let displayText = layoutLine.text;
608
730
  let displayWidth = visibleWidth(layoutLine.text);
609
731
  let cursorInPadding = false;
732
+ const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
733
+ const gutterText =
734
+ promptGutter === undefined ? "" : showPromptGutter ? promptGutter.firstLine : promptGutter.continuation;
610
735
 
611
736
  // Add cursor if this line has it
612
737
  const hasCursor = layoutLine.hasCursor && layoutLine.cursorPos !== undefined;
613
738
  const marker = emitCursorMarker ? CURSOR_MARKER : "";
614
739
 
740
+ if (!borderVisible && displayWidth > lineContentWidth) {
741
+ displayText = sliceByColumn(displayText, 0, lineContentWidth, true);
742
+ displayWidth = visibleWidth(displayText);
743
+ }
744
+
745
+ if (!borderVisible && lineContentWidth === 0) {
746
+ if (hasCursor && !this.#useTerminalCursor) {
747
+ const zeroWidthCursorBudget = visibleWidth(gutterText);
748
+ const zeroWidthCursorReplacement = this.cursorOverride
749
+ ? { text: this.cursorOverride, width: this.cursorOverrideWidth ?? 1 }
750
+ : this.#getStyledInputCursor();
751
+ if (showPromptGutter && zeroWidthCursorBudget > 0) {
752
+ // Keep the leading prompt glyph visible when the gutter consumes the whole row.
753
+ const promptGlyph = [...segmenter.segment(gutterText)][0]?.segment ?? "";
754
+ const promptGlyphWidth = visibleWidth(promptGlyph);
755
+ const remainingCursorWidth = Math.max(0, zeroWidthCursorBudget - promptGlyphWidth);
756
+ if (remainingCursorWidth === 0) {
757
+ result.push(`\x1b[7m${promptGlyph}\x1b[0m${marker}`);
758
+ } else {
759
+ const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(
760
+ "",
761
+ marker,
762
+ remainingCursorWidth,
763
+ zeroWidthCursorReplacement,
764
+ );
765
+ result.push(`${promptGlyph}${widthLimitedCursor.text}`);
766
+ }
767
+ } else {
768
+ const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(
769
+ gutterText,
770
+ marker,
771
+ zeroWidthCursorBudget,
772
+ zeroWidthCursorReplacement,
773
+ );
774
+ result.push(widthLimitedCursor.text);
775
+ }
776
+ } else if (hasCursor && this.#useTerminalCursor) {
777
+ result.push(this.#renderTerminalCursorMarker(gutterText, marker, visibleWidth(gutterText)));
778
+ } else {
779
+ result.push(gutterText + (hasCursor ? marker : ""));
780
+ }
781
+ continue;
782
+ }
783
+
615
784
  if (hasCursor && this.#useTerminalCursor) {
616
785
  if (marker) {
617
786
  const before = displayText.slice(0, layoutLine.cursorPos);
@@ -620,6 +789,8 @@ export class Editor implements Component, Focusable {
620
789
  const hintText = hintStyle(truncateToWidth(inlineHint, Math.max(0, lineContentWidth - displayWidth)));
621
790
  displayText = before + marker + hintText;
622
791
  displayWidth += visibleWidth(inlineHint);
792
+ } else if (after.length === 0 && !borderVisible && displayWidth >= lineContentWidth) {
793
+ displayText = this.#renderTerminalCursorMarker(before, marker, lineContentWidth);
623
794
  } else {
624
795
  displayText = before + marker + after;
625
796
  }
@@ -640,7 +811,16 @@ export class Editor implements Component, Focusable {
640
811
  } else if (this.cursorOverride) {
641
812
  // Cursor override replaces the normal end-of-text cursor glyph
642
813
  const overrideWidth = this.cursorOverrideWidth ?? 1;
643
- if (inlineHint) {
814
+ if (!borderVisible && displayWidth + overrideWidth > lineContentWidth) {
815
+ // Borderless editors have no spare padding cell for an end-of-line cursor glyph.
816
+ // Preserve cursorOverride by replacing the tail of the line with it.
817
+ const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(before, marker, lineContentWidth, {
818
+ text: this.cursorOverride,
819
+ width: overrideWidth,
820
+ });
821
+ displayText = widthLimitedCursor.text;
822
+ displayWidth = widthLimitedCursor.width;
823
+ } else if (inlineHint) {
644
824
  const availWidth = Math.max(0, lineContentWidth - displayWidth - overrideWidth);
645
825
  const hintText = hintStyle(truncateToWidth(inlineHint, availWidth));
646
826
  displayText = before + marker + this.cursorOverride + hintText;
@@ -651,16 +831,21 @@ export class Editor implements Component, Focusable {
651
831
  }
652
832
  } else {
653
833
  // Cursor is at the end - add thin cursor glyph
654
- const cursorChar = this.#theme.symbols.inputCursor;
655
- const cursor = `\x1b[5m${cursorChar}\x1b[0m`;
656
- if (inlineHint) {
657
- const availWidth = Math.max(0, lineContentWidth - displayWidth - visibleWidth(cursorChar));
834
+ const { text: cursor, width: cursorWidth } = this.#getStyledInputCursor();
835
+ if (!borderVisible && displayWidth + cursorWidth > lineContentWidth) {
836
+ // Borderless editors have no spare padding cell for an end-of-line cursor glyph.
837
+ // Highlight the last grapheme so the cursor stays visible without consuming width.
838
+ const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(before, marker, lineContentWidth);
839
+ displayText = widthLimitedCursor.text;
840
+ displayWidth = widthLimitedCursor.width;
841
+ } else if (inlineHint) {
842
+ const availWidth = Math.max(0, lineContentWidth - displayWidth - cursorWidth);
658
843
  const hintText = hintStyle(truncateToWidth(inlineHint, availWidth));
659
844
  displayText = before + marker + cursor + hintText;
660
- displayWidth += visibleWidth(cursorChar) + Math.min(visibleWidth(inlineHint), availWidth);
845
+ displayWidth += cursorWidth + Math.min(visibleWidth(inlineHint), availWidth);
661
846
  } else {
662
847
  displayText = before + marker + cursor;
663
- displayWidth += visibleWidth(cursorChar);
848
+ displayWidth += cursorWidth;
664
849
  }
665
850
  if (displayWidth > lineContentWidth && paddingX > 0) {
666
851
  cursorInPadding = true;
@@ -668,10 +853,15 @@ export class Editor implements Component, Focusable {
668
853
  }
669
854
  }
670
855
 
671
- // All lines have consistent borders based on padding
672
- const isLastLine = layoutLine === visibleLayoutLines[visibleLayoutLines.length - 1];
673
856
  const linePad = padding(Math.max(0, lineContentWidth - displayWidth));
674
857
 
858
+ if (!borderVisible) {
859
+ result.push(gutterText + displayText + linePad);
860
+ continue;
861
+ }
862
+
863
+ // All lines have consistent borders based on padding
864
+ const isLastLine = visibleIndex === visibleLayoutLines.length - 1;
675
865
  const rightPaddingWidth = Math.max(0, paddingX - (cursorInPadding ? 1 : 0));
676
866
  if (isLastLine) {
677
867
  const bottomRightPadding = Math.max(0, paddingX - 1 - (cursorInPadding ? 1 : 0));