diff --git a/src/tui.ts b/src/tui.ts index 67c2276..4f17eeb 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -120,8 +120,8 @@ export class TuiEngine { process.stdin.resume(); process.stdin.on('keypress', this.handleKeypress.bind(this)); - // Hide standard cursor - process.stdout.write('\x1B[?25l'); + // Enter alternate screen buffer and hide standard cursor + process.stdout.write('\x1B[?1049h\x1B[?25l'); // Bootstrap if config exists if (this.state.config.url && this.state.config.owner && this.state.config.repo) { @@ -137,8 +137,8 @@ export class TuiEngine { */ public stop() { this.stopSpinner(); - // Show standard cursor - process.stdout.write('\x1B[?25h'); + // Exit alternate screen buffer and show standard cursor + process.stdout.write('\x1B[?1049l\x1B[?25h'); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } @@ -670,8 +670,8 @@ export class TuiEngine { const cols = process.stdout.columns || 80; const rows = process.stdout.rows || 24; - // Clear terminal screen and reset cursor position - process.stdout.write('\x1B[2J\x1B[H'); + // Clear terminal screen, clear scrollback buffer, and reset cursor position + process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); if (this.state.screen === 'setup') { this.renderSetupScreen(cols, rows); @@ -794,19 +794,64 @@ export class TuiEngine { // Header Bar const spinnerStr = this.state.loading ? chalk.bold.cyan(SPINNER_FRAMES[spinnerIndex]) + ' ' : ''; const instanceName = normalizeUrl(this.state.config.url).replace(/^https?:\/\//, ''); + const rightHeader = `repo: ${chalk.bold.cyan(`${this.state.config.owner}/${this.state.config.repo}`)} `; + const rightLen = stripAnsi(rightHeader).length; + + // We want the total line length to fit exactly within 'cols', so max left side width is cols - rightLen - 1 + const maxLeftWidth = cols - rightLen - 1; + + // Spinner plain length is 2 if loading, 0 otherwise + const spinnerPlainLen = this.state.loading ? 2 : 0; + const titlePlain = 'Forgejo Issue Explorer'; + const titleColor = chalk.bold.hex('#4A90E2')(titlePlain); - let leftHeader = ` ${spinnerStr}${chalk.bold.hex('#4A90E2')('Forgejo Issue Explorer')} ─ ${chalk.bold.white(instanceName)}`; + const instanceColor = ` ─ ${chalk.bold.white(instanceName)}`; + const instancePlain = ` ─ ${instanceName}`; + + let statsColor = ''; + let statsPlain = ''; + let shortStatsColor = ''; + let shortStatsPlain = ''; + if (this.state.totalIssuesCount > 0) { const totalPages = Math.ceil(this.state.totalIssuesCount / this.state.issuesPerPage); - leftHeader += ` ─ Page ${chalk.bold.white(this.state.currentPage)} of ${chalk.bold.white(totalPages)} (${chalk.yellow(this.state.totalIssuesCount)} matching)`; + statsColor = ` ─ Page ${chalk.bold.white(this.state.currentPage)} of ${chalk.bold.white(totalPages)} (${chalk.yellow(this.state.totalIssuesCount)} matching)`; + statsPlain = ` ─ Page ${this.state.currentPage} of ${totalPages} (${this.state.totalIssuesCount} matching)`; + + shortStatsColor = ` ─ P. ${chalk.bold.white(this.state.currentPage)}/${chalk.bold.white(totalPages)} (${chalk.yellow(this.state.totalIssuesCount)})`; + shortStatsPlain = ` ─ P. ${this.state.currentPage}/${totalPages} (${this.state.totalIssuesCount})`; } else if (!this.state.loading) { - leftHeader += ' ─ (0 issues found)'; + statsColor = ' ─ (0 issues found)'; + statsPlain = ' ─ (0 issues found)'; + + shortStatsColor = ' ─ (0)'; + shortStatsPlain = ' ─ (0)'; } - const rightHeader = `repo: ${chalk.bold.cyan(`${this.state.config.owner}/${this.state.config.repo}`)} `; + let leftHeader = ''; + + // Attempt 1: Full header: spinner + Title + instance + stats + if (1 + spinnerPlainLen + titlePlain.length + instancePlain.length + statsPlain.length <= maxLeftWidth) { + leftHeader = ` ${spinnerStr}${titleColor}${instanceColor}${statsColor}`; + } + // Attempt 2: Drop instance, keep full stats + else if (1 + spinnerPlainLen + titlePlain.length + statsPlain.length <= maxLeftWidth) { + leftHeader = ` ${spinnerStr}${titleColor}${statsColor}`; + } + // Attempt 3: Drop instance, use short stats + else if (1 + spinnerPlainLen + titlePlain.length + shortStatsPlain.length <= maxLeftWidth) { + leftHeader = ` ${spinnerStr}${titleColor}${shortStatsColor}`; + } + // Attempt 4: Drop stats, keep title + shortened instance if it fits + else if (1 + spinnerPlainLen + titlePlain.length + instancePlain.length <= maxLeftWidth) { + leftHeader = ` ${spinnerStr}${titleColor}${instanceColor}`; + } + // Attempt 5: Just Title + else { + leftHeader = ` ${spinnerStr}${titleColor}`; + } const leftLen = stripAnsi(leftHeader).length; - const rightLen = stripAnsi(rightHeader).length; let spacesCount = cols - leftLen - rightLen; if (spacesCount < 1) spacesCount = 1; @@ -839,8 +884,8 @@ export class TuiEngine { ); console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); - // Table Content Height available - const tableHeight = rows - 7; // Header: 1, Box borders: 2, Table headers: 2, Bottom filter line: 1, Keyboard help: 1 + // Table Content Height available (rows - 9 ensures total printed lines is rows - 1, preventing terminal scrolling) + const tableHeight = rows - 9; if (this.state.issues.length === 0) { const msg = this.state.loading ? 'Fetching issues from server...' : (this.state.error ? `Error: ${this.state.error}` : 'No issues found.'); @@ -997,7 +1042,8 @@ export class TuiEngine { } // Paginate/Scroll calculations - const displayHeight = rows - 6; // Header: 1, borders: 2, meta: 2, help: 1 + const metaHeight = labels ? 3 : 2; // Meta 1, optionally Meta 2 (Labels), and Separator + const displayHeight = rows - 5 - metaHeight; // Header: 1, Borders: 2, Meta lines, Help: 1, and -1 to avoid scrolling const maxScroll = Math.max(0, contentLines.length - displayHeight); // Bounds check scroll offset