(width: number)
| 548 | } |
| 549 | |
| 550 | render(width: number): string[] { |
| 551 | const maxPadding = Math.max(0, Math.floor((width - 1) / 2)); |
| 552 | const paddingX = Math.min(this.paddingX, maxPadding); |
| 553 | const contentWidth = Math.max(1, width - paddingX * 2); |
| 554 | |
| 555 | // Layout width: with padding the cursor can overflow into it, |
| 556 | // without padding we reserve 1 column for the cursor. |
| 557 | const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1)); |
| 558 | |
| 559 | // Store for cursor navigation (must match wrapping width) |
| 560 | this.lastWidth = layoutWidth; |
| 561 | |
| 562 | const horizontal = this.borderColor("─"); |
| 563 | |
| 564 | // Layout the text |
| 565 | const layoutLines = this.layoutText(layoutWidth); |
| 566 | |
| 567 | // Calculate max visible lines: 30% of terminal height, minimum 5 lines |
| 568 | const terminalRows = this.tui.terminal.rows; |
| 569 | const maxVisibleLines = Math.max(5, Math.floor(terminalRows * 0.3)); |
| 570 | |
| 571 | // Find the cursor line index in layoutLines |
| 572 | let cursorLineIndex = layoutLines.findIndex((line) => line.hasCursor); |
| 573 | if (cursorLineIndex === -1) cursorLineIndex = 0; |
| 574 | |
| 575 | // Adjust scroll offset to keep cursor visible |
| 576 | if (cursorLineIndex < this.scrollOffset) { |
| 577 | this.scrollOffset = cursorLineIndex; |
| 578 | } else if (cursorLineIndex >= this.scrollOffset + maxVisibleLines) { |
| 579 | this.scrollOffset = cursorLineIndex - maxVisibleLines + 1; |
| 580 | } |
| 581 | |
| 582 | // Clamp scroll offset to valid range |
| 583 | const maxScrollOffset = Math.max(0, layoutLines.length - maxVisibleLines); |
| 584 | this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScrollOffset)); |
| 585 | |
| 586 | // Get visible lines slice |
| 587 | const visibleLines = layoutLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines); |
| 588 | |
| 589 | const result: string[] = []; |
| 590 | const leftPadding = " ".repeat(paddingX); |
| 591 | const rightPadding = leftPadding; |
| 592 | |
| 593 | // Render top border (with scroll indicator if scrolled down) |
| 594 | if (this.scrollOffset > 0) { |
| 595 | const indicator = `─── ↑ ${this.scrollOffset} more `; |
| 596 | const remaining = width - visibleWidth(indicator); |
| 597 | if (remaining >= 0) { |
| 598 | result.push(this.borderColor(indicator + "─".repeat(remaining))); |
| 599 | } else { |
| 600 | result.push(this.borderColor(truncateToWidth(indicator, width))); |
| 601 | } |
| 602 | } else { |
| 603 | result.push(horizontal.repeat(Math.max(0, width))); |
| 604 | } |
| 605 | |
| 606 | // Render each visible layout line |
| 607 | // Emit hardware cursor marker when focused so TUI can position the |
nothing calls this directly
no test coverage detected