Fix layout to avoid terminal scrolling and show repo on list screen

This commit is contained in:
Isaac Johnson 2026-05-31 21:40:52 -05:00
parent 64f147d10c
commit 524ad77ea6
1 changed files with 60 additions and 14 deletions

View File

@ -120,8 +120,8 @@ export class TuiEngine {
process.stdin.resume(); process.stdin.resume();
process.stdin.on('keypress', this.handleKeypress.bind(this)); process.stdin.on('keypress', this.handleKeypress.bind(this));
// Hide standard cursor // Enter alternate screen buffer and hide standard cursor
process.stdout.write('\x1B[?25l'); process.stdout.write('\x1B[?1049h\x1B[?25l');
// Bootstrap if config exists // Bootstrap if config exists
if (this.state.config.url && this.state.config.owner && this.state.config.repo) { if (this.state.config.url && this.state.config.owner && this.state.config.repo) {
@ -137,8 +137,8 @@ export class TuiEngine {
*/ */
public stop() { public stop() {
this.stopSpinner(); this.stopSpinner();
// Show standard cursor // Exit alternate screen buffer and show standard cursor
process.stdout.write('\x1B[?25h'); process.stdout.write('\x1B[?1049l\x1B[?25h');
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
process.stdin.setRawMode(false); process.stdin.setRawMode(false);
} }
@ -670,8 +670,8 @@ export class TuiEngine {
const cols = process.stdout.columns || 80; const cols = process.stdout.columns || 80;
const rows = process.stdout.rows || 24; const rows = process.stdout.rows || 24;
// Clear terminal screen and reset cursor position // Clear terminal screen, clear scrollback buffer, and reset cursor position
process.stdout.write('\x1B[2J\x1B[H'); process.stdout.write('\x1B[2J\x1B[3J\x1B[H');
if (this.state.screen === 'setup') { if (this.state.screen === 'setup') {
this.renderSetupScreen(cols, rows); this.renderSetupScreen(cols, rows);
@ -794,19 +794,64 @@ export class TuiEngine {
// Header Bar // Header Bar
const spinnerStr = this.state.loading ? chalk.bold.cyan(SPINNER_FRAMES[spinnerIndex]) + ' ' : ''; const spinnerStr = this.state.loading ? chalk.bold.cyan(SPINNER_FRAMES[spinnerIndex]) + ' ' : '';
const instanceName = normalizeUrl(this.state.config.url).replace(/^https?:\/\//, ''); 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) { if (this.state.totalIssuesCount > 0) {
const totalPages = Math.ceil(this.state.totalIssuesCount / this.state.issuesPerPage); 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) { } 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 leftLen = stripAnsi(leftHeader).length;
const rightLen = stripAnsi(rightHeader).length;
let spacesCount = cols - leftLen - rightLen; let spacesCount = cols - leftLen - rightLen;
if (spacesCount < 1) spacesCount = 1; if (spacesCount < 1) spacesCount = 1;
@ -839,8 +884,8 @@ export class TuiEngine {
); );
console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤'));
// Table Content Height available // Table Content Height available (rows - 9 ensures total printed lines is rows - 1, preventing terminal scrolling)
const tableHeight = rows - 7; // Header: 1, Box borders: 2, Table headers: 2, Bottom filter line: 1, Keyboard help: 1 const tableHeight = rows - 9;
if (this.state.issues.length === 0) { 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.'); 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 // 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); const maxScroll = Math.max(0, contentLines.length - displayHeight);
// Bounds check scroll offset // Bounds check scroll offset