From f9e6329bc92c8516a91f4405c6b11ec4f3b8768a Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Fri, 3 Jul 2026 11:34:49 -0500 Subject: [PATCH] Update issue and repo picker to accept VIM style naviations - j and k for up and down, respectively. --- src/tui.ts | 3032 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 1862 insertions(+), 1170 deletions(-) diff --git a/src/tui.ts b/src/tui.ts index c925764..dfa62a1 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,100 +1,117 @@ -import readline from 'readline'; -import chalk from 'chalk'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { AppState, Issue, Comment } from './types.js'; -import { fetchIssues, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime, changeIssueState, setIssueAssignees, fetchLabels, createLabel, updateLabel, deleteLabel } from './api.js'; -import { saveGlobalConfig } from './config.js'; +import readline from "readline"; +import chalk from "chalk"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { AppState, Issue, Comment } from "./types.js"; +import { + fetchIssues, + fetchIssue, + fetchIssueComments, + validateConnection, + normalizeUrl, + authenticateAndFetchRepos, + createIssue, + createIssueComment, + editIssue, + addIssueTime, + changeIssueState, + setIssueAssignees, + fetchLabels, + createLabel, + updateLabel, + deleteLabel, +} from "./api.js"; +import { saveGlobalConfig } from "./config.js"; // Setup readline for stdin keypress events readline.emitKeypressEvents(process.stdin); // Spinner chars for loading animations -const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; +const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; let spinnerIndex = 0; const GRAVESTONE_FRAMES = [ [ - chalk.gray(' '), - chalk.gray(' '), - chalk.gray(' '), - chalk.gray(' '), - chalk.gray(' ___________ '), + chalk.gray(" "), + chalk.gray(" "), + chalk.gray(" "), + chalk.gray(" "), + chalk.gray(" ___________ "), ], [ - chalk.gray(' '), - chalk.gray(' '), - chalk.gray(' '), - chalk.gray(' ___ '), - chalk.gray(' ___/___\\___ '), + chalk.gray(" "), + chalk.gray(" "), + chalk.gray(" "), + chalk.gray(" ___ "), + chalk.gray(" ___/___\\___ "), ], [ - chalk.gray(' '), - chalk.gray(' '), - chalk.gray(' ___ '), - chalk.gray(' / \\ '), - chalk.gray(' __|_____|__ '), + chalk.gray(" "), + chalk.gray(" "), + chalk.gray(" ___ "), + chalk.gray(" / \\ "), + chalk.gray(" __|_____|__ "), ], [ - chalk.gray(' '), - chalk.gray(' ___ '), - chalk.gray(' / \\ '), - chalk.gray(' | RIP | '), - chalk.gray(' __|_____|__ '), + chalk.gray(" "), + chalk.gray(" ___ "), + chalk.gray(" / \\ "), + chalk.gray(" | RIP | "), + chalk.gray(" __|_____|__ "), ], [ - chalk.gray(' ___ '), - chalk.gray(' / \\ '), - chalk.gray(' | RIP | '), - chalk.gray(' | | '), - chalk.gray(' __|_____|__ '), - ] + chalk.gray(" ___ "), + chalk.gray(" / \\ "), + chalk.gray(" | RIP | "), + chalk.gray(" | | "), + chalk.gray(" __|_____|__ "), + ], ]; const ZOMBIE_FRAMES = [ [ - chalk.gray(' ___ '), - chalk.gray(' / \\ '), - chalk.gray(' | RIP | '), - chalk.gray(' | | '), - chalk.gray(' __|_____|__ '), + chalk.gray(" ___ "), + chalk.gray(" / \\ "), + chalk.gray(" | RIP | "), + chalk.gray(" | | "), + chalk.gray(" __|_____|__ "), ], [ - chalk.gray(' ___ '), - chalk.gray(' / \\ '), - chalk.gray(' | RIP | '), - chalk.gray(' | | '), - chalk.gray(' __|_ ') + chalk.green('.') + chalk.gray(' _|__ '), + chalk.gray(" ___ "), + chalk.gray(" / \\ "), + chalk.gray(" | RIP | "), + chalk.gray(" | | "), + chalk.gray(" __|_ ") + chalk.green(".") + chalk.gray(" _|__ "), ], [ - chalk.gray(' ___ '), - chalk.gray(' / \\ '), - chalk.gray(' | RIP | '), - chalk.gray(' | ') + chalk.green('^') + chalk.gray(' | '), - chalk.gray(' __|_') + chalk.green('/ \\') + chalk.gray('_|__ '), + chalk.gray(" ___ "), + chalk.gray(" / \\ "), + chalk.gray(" | RIP | "), + chalk.gray(" | ") + chalk.green("^") + chalk.gray(" | "), + chalk.gray(" __|_") + chalk.green("/ \\") + chalk.gray("_|__ "), ], [ - chalk.gray(' ___ '), - chalk.gray(' / \\ '), - chalk.gray(' | ') + chalk.green('o_o') + chalk.gray(' | '), - chalk.gray(' | ') + chalk.green('/|\\') + chalk.gray(' | '), - chalk.gray(' __|_ ') + chalk.green('|') + chalk.gray(' _|__ '), + chalk.gray(" ___ "), + chalk.gray(" / \\ "), + chalk.gray(" | ") + chalk.green("o_o") + chalk.gray(" | "), + chalk.gray(" | ") + chalk.green("/|\\") + chalk.gray(" | "), + chalk.gray(" __|_ ") + chalk.green("|") + chalk.gray(" _|__ "), ], [ - chalk.gray(' ___ '), - chalk.gray(' /') + chalk.green('o_o') + chalk.gray('\\ '), - chalk.gray(' | ') + chalk.green('/|\\') + chalk.gray(' | '), - chalk.gray(' | ') + chalk.green('|') + chalk.gray(' | '), - chalk.gray(' __|_') + chalk.green('/ \\') + chalk.gray('_|__ '), + chalk.gray(" ___ "), + chalk.gray(" /") + chalk.green("o_o") + chalk.gray("\\ "), + chalk.gray(" | ") + chalk.green("/|\\") + chalk.gray(" | "), + chalk.gray(" | ") + chalk.green("|") + chalk.gray(" | "), + chalk.gray(" __|_") + chalk.green("/ \\") + chalk.gray("_|__ "), ], [ - chalk.gray(' \\') + chalk.green('o_o') + chalk.gray('/ '), - chalk.gray(' / ') + chalk.green('|') + chalk.gray(' \\ '), - chalk.gray(' | ') + chalk.green('/ \\') + chalk.gray(' | '), - chalk.gray(' | | '), - chalk.gray(' __|_____|__ '), - ] + chalk.gray(" \\") + chalk.green("o_o") + chalk.gray("/ "), + chalk.gray(" / ") + chalk.green("|") + chalk.gray(" \\ "), + chalk.gray(" | ") + chalk.green("/ \\") + chalk.gray(" | "), + chalk.gray(" | | "), + chalk.gray(" __|_____|__ "), + ], ]; let spinnerInterval: NodeJS.Timeout | null = null; @@ -105,22 +122,22 @@ let spinnerInterval: NodeJS.Timeout | null = null; export function wordWrap(text: string, maxWidth: number): string[] { if (!text) return []; const lines: string[] = []; - const rawLines = text.split('\n'); + const rawLines = text.split("\n"); for (const rawLine of rawLines) { - if (rawLine.trim() === '') { - lines.push(''); + if (rawLine.trim() === "") { + lines.push(""); continue; } - let currentLine = ''; + let currentLine = ""; const words = rawLine.split(/\s+/); for (const word of words) { if (!word) continue; - + if (currentLine.length + word.length + 1 <= maxWidth) { - currentLine += (currentLine ? ' ' : '') + word; + currentLine += (currentLine ? " " : "") + word; } else { if (currentLine) { lines.push(currentLine); @@ -148,27 +165,27 @@ export function wordWrap(text: string, maxWidth: number): string[] { export function formatDate(dateStr: string): string { try { const d = new Date(dateStr); - if (isNaN(d.getTime())) return 'n/a'; + if (isNaN(d.getTime())) return "n/a"; const yr = d.getFullYear(); - const mo = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - const hr = String(d.getHours()).padStart(2, '0'); - const min = String(d.getMinutes()).padStart(2, '0'); + const mo = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + const hr = String(d.getHours()).padStart(2, "0"); + const min = String(d.getMinutes()).padStart(2, "0"); return `${yr}-${mo}-${day} ${hr}:${min}`; } catch { - return 'n/a'; + return "n/a"; } } // Utility to safely truncate strings function truncate(str: string, length: number): string { if (str.length <= length) return str; - return str.substring(0, length - 1) + '…'; + return str.substring(0, length - 1) + "…"; } // Utility to format seconds into h m format function formatTime(seconds?: number): string { - if (!seconds || seconds <= 0) return '0m'; + if (!seconds || seconds <= 0) return "0m"; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); if (h > 0 && m > 0) return `${h}h ${m}m`; @@ -180,10 +197,12 @@ function formatTime(seconds?: number): string { * Helper to strip ANSI escape codes from a string for accurate length calculations. */ function stripAnsi(str: string): string { - return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); + return str.replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "", + ); } - /** * TUI State Controller and Render Engine */ @@ -191,10 +210,10 @@ export class TuiEngine { private state: AppState; private onQuit: () => void = () => {}; private activeSearchInput: boolean = false; - private searchInputBuffer: string = ''; + private searchInputBuffer: string = ""; private animationFrame: number = 0; private animationInterval: NodeJS.Timeout | null = null; - private launchDestScreen: 'list' | 'setup' = 'setup'; + private launchDestScreen: "list" | "setup" = "setup"; private launchFrame: string[] = []; private launchFrameIndex = 0; private launchInterval: NodeJS.Timeout | null = null; @@ -206,30 +225,34 @@ export class TuiEngine { clearInterval(this.autoRefreshTimer); this.autoRefreshTimer = null; } - + if (this.state.autoRefreshInterval && this.state.autoRefreshInterval > 0) { this.autoRefreshTimer = setInterval(() => { // We need a way to check if input is active, but isTextInputActive isn't accessible here if we didn't add it. // Wait, isTextInputActive is a method of TuiEngine, or I reverted it? // Ah! I reverted isTextInputActive earlier! - const isTextInputActive = - this.state.screen === 'setup' || - (this.state.screen === 'repo-picker' && this.state.repoPickerActiveSearch) || - (this.state.screen === 'list' && this.activeSearchInput) || - this.state.screen === 'create-issue' || - this.state.screen === 'add-comment' || - this.state.screen === 'edit-issue' || - this.state.screen === 'add-time' || - this.state.screen === 'set-assignees' || - this.state.screen === 'create-label' || - this.state.screen === 'edit-label'; + const isTextInputActive = + this.state.screen === "setup" || + (this.state.screen === "repo-picker" && + this.state.repoPickerActiveSearch) || + (this.state.screen === "list" && this.activeSearchInput) || + this.state.screen === "create-issue" || + this.state.screen === "add-comment" || + this.state.screen === "edit-issue" || + this.state.screen === "add-time" || + this.state.screen === "set-assignees" || + this.state.screen === "create-label" || + this.state.screen === "edit-label"; if (isTextInputActive) return; - + // Background refresh - if (this.state.screen === 'list') { + if (this.state.screen === "list") { this.loadIssues(); - } else if (this.state.screen === 'details' && this.state.selectedIssue) { + } else if ( + this.state.screen === "details" && + this.state.selectedIssue + ) { this.reloadSingleIssue(); } }, this.state.autoRefreshInterval * 1000); @@ -238,9 +261,9 @@ export class TuiEngine { constructor(initialState: AppState) { this.state = initialState; - + // Set up resize handler - process.stdout.on('resize', () => { + process.stdout.on("resize", () => { this.render(); }); } @@ -250,26 +273,30 @@ export class TuiEngine { */ public start(onQuit: () => void) { this.onQuit = onQuit; - + // Put terminal in raw mode if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); - process.stdin.on('keypress', this.handleKeypress.bind(this)); - + process.stdin.on("keypress", this.handleKeypress.bind(this)); + // Enter alternate screen buffer and hide standard cursor - process.stdout.write('\x1B[?1049h\x1B[?25l'); + process.stdout.write("\x1B[?1049h\x1B[?25l"); // Bootstrap destination - if (this.state.config.url && this.state.config.owner && this.state.config.repo) { - this.launchDestScreen = 'list'; + if ( + this.state.config.url && + this.state.config.owner && + this.state.config.repo + ) { + this.launchDestScreen = "list"; } else { - this.launchDestScreen = 'setup'; + this.launchDestScreen = "setup"; } this.restartAutoRefresh(); - this.state.screen = 'launch'; + this.state.screen = "launch"; this.startLaunchAnimation(); } @@ -291,7 +318,7 @@ export class TuiEngine { this.autoRefreshTimer = null; } // Exit alternate screen buffer and show standard cursor - process.stdout.write('\x1B[?1049l\x1B[?25h'); + process.stdout.write("\x1B[?1049l\x1B[?25h"); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } @@ -335,7 +362,7 @@ export class TuiEngine { this.state.issues = issues; this.state.totalIssuesCount = totalCount; - + // Keep cursor bounds-checked if (this.state.selectedIssueIndex >= issues.length) { this.state.selectedIssueIndex = Math.max(0, issues.length - 1); @@ -361,15 +388,20 @@ export class TuiEngine { this.startSpinner(); try { - const issue = await fetchIssue(this.state.config, this.state.selectedIssue.number); + const issue = await fetchIssue( + this.state.config, + this.state.selectedIssue.number, + ); this.state.selectedIssue = issue; - + // Update in the main list as well - const listIndex = this.state.issues.findIndex(i => i.number === issue.number); + const listIndex = this.state.issues.findIndex( + (i) => i.number === issue.number, + ); if (listIndex !== -1) { this.state.issues[listIndex] = issue; } - + await this.loadComments(issue); } catch (err: any) { this.state.error = `Failed to reload issue: ${err.message}`; @@ -389,17 +421,22 @@ export class TuiEngine { this.render(); try { - const comments = await fetchIssueComments(this.state.config, issue.number); + const comments = await fetchIssueComments( + this.state.config, + issue.number, + ); this.state.selectedIssueComments = comments; } catch (err: any) { // Gracefully show comment loading error - this.state.selectedIssueComments = [{ - id: -1, - user: { id: 0, login: 'system', full_name: 'System Error' }, - body: `Failed to load comments: ${err.message}`, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }]; + this.state.selectedIssueComments = [ + { + id: -1, + user: { id: 0, login: "system", full_name: "System Error" }, + body: `Failed to load comments: ${err.message}`, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + ]; } finally { this.state.commentsLoading = false; this.render(); @@ -411,47 +448,50 @@ export class TuiEngine { */ private async handleKeypress(str: string, key: any) { // Standard Ctrl+C quit - if (key && key.ctrl && key.name === 'c') { + if (key && key.ctrl && key.name === "c") { this.stop(); return; } - if (this.state.screen === 'launch') { + if (this.state.screen === "launch") { this.skipLaunchAnimation(); return; } - if (this.state.screen === 'setup') { + if (this.state.screen === "setup") { this.handleSetupKeypress(str, key); - } else if (this.state.screen === 'repo-picker') { + } else if (this.state.screen === "repo-picker") { this.handleRepoPickerKeypress(str, key); - } else if (this.state.screen === 'list') { + } else if (this.state.screen === "list") { if (this.activeSearchInput) { this.handleSearchKeypress(str, key); } else { this.handleListKeypress(str, key); } - } else if (this.state.screen === 'details') { + } else if (this.state.screen === "details") { this.handleDetailsKeypress(str, key); - } else if (this.state.screen === 'create-issue') { + } else if (this.state.screen === "create-issue") { this.handleCreateIssueKeypress(str, key); - } else if (this.state.screen === 'confirm-cancel-create') { + } else if (this.state.screen === "confirm-cancel-create") { this.handleConfirmCancelCreateKeypress(str, key); - } else if (this.state.screen === 'confirm-cancel-edit') { + } else if (this.state.screen === "confirm-cancel-edit") { this.handleConfirmCancelEditKeypress(str, key); - } else if (this.state.screen === 'add-comment') { + } else if (this.state.screen === "add-comment") { this.handleAddCommentKeypress(str, key); - } else if (this.state.screen === 'edit-issue') { + } else if (this.state.screen === "edit-issue") { this.handleEditIssueKeypress(str, key); - } else if (this.state.screen === 'add-time') { + } else if (this.state.screen === "add-time") { this.handleAddTimeKeypress(str, key); - } else if (this.state.screen === 'set-assignees') { + } else if (this.state.screen === "set-assignees") { this.handleSetAssigneesKeypress(str, key); - } else if (this.state.screen === 'labels-list') { + } else if (this.state.screen === "labels-list") { this.handleLabelsListKeypress(str, key); - } else if (this.state.screen === 'create-label' || this.state.screen === 'edit-label') { + } else if ( + this.state.screen === "create-label" || + this.state.screen === "edit-label" + ) { this.handleLabelFormKeypress(str, key); - } else if (this.state.screen === 'help') { + } else if (this.state.screen === "help") { this.handleHelpKeypress(str, key); } } @@ -460,8 +500,13 @@ export class TuiEngine { * Key handling for Help screen */ private handleHelpKeypress(str: string, key: any) { - if ((key && (key.name === 'escape' || key.name === 'return')) || str === 'q' || str === 'Q' || str === '?') { - this.state.screen = this.state.previousScreen || 'list'; + if ( + (key && (key.name === "escape" || key.name === "return")) || + str === "q" || + str === "Q" || + str === "?" + ) { + this.state.screen = this.state.previousScreen || "list"; this.render(); } } @@ -471,37 +516,43 @@ export class TuiEngine { */ private async handleSetupKeypress(str: string, key: any) { const form = this.state.setupForm; - const fields: Array<'url' | 'userid' | 'token' | 'saveConfig'> = ['url', 'userid', 'token', 'saveConfig']; + const fields: Array<"url" | "userid" | "token" | "saveConfig"> = [ + "url", + "userid", + "token", + "saveConfig", + ]; const currentIdx = fields.indexOf(form.activeField); - if (key && key.name === 'down') { - form.activeField = fields[(currentIdx + 1) % fields.length]; - this.render(); - return; - } - - if (key && key.name === 'up') { - form.activeField = fields[(currentIdx - 1 + fields.length) % fields.length]; - this.render(); - return; - } - - if (key && key.name === 'tab') { + if (key && key.name === "down") { form.activeField = fields[(currentIdx + 1) % fields.length]; this.render(); return; } - if (str === ' ') { - if (form.activeField === 'saveConfig') { + if (key && key.name === "up") { + form.activeField = + fields[(currentIdx - 1 + fields.length) % fields.length]; + this.render(); + return; + } + + if (key && key.name === "tab") { + form.activeField = fields[(currentIdx + 1) % fields.length]; + this.render(); + return; + } + + if (str === " ") { + if (form.activeField === "saveConfig") { form.saveConfig = !form.saveConfig; this.render(); return; } } - if ((key && key.name === 'return') || str === '\r' || str === '\n') { - if (form.activeField === 'saveConfig') { + if ((key && key.name === "return") || str === "\r" || str === "\n") { + if (form.activeField === "saveConfig") { form.saveConfig = !form.saveConfig; this.render(); return; @@ -509,12 +560,12 @@ export class TuiEngine { // Submit form if (!form.url.trim()) { - this.state.error = 'Instance URL is required.'; + this.state.error = "Instance URL is required."; this.render(); return; } if (!form.userid.trim()) { - this.state.error = 'User ID / Username is required.'; + this.state.error = "User ID / Username is required."; this.render(); return; } @@ -524,19 +575,25 @@ export class TuiEngine { this.render(); try { - const repos = await authenticateAndFetchRepos(form.url, form.userid, form.token); - + const repos = await authenticateAndFetchRepos( + form.url, + form.userid, + form.token, + ); + this.state.repos = repos; this.state.selectedRepoIndex = 0; - this.state.repoSearchQuery = ''; + this.state.repoSearchQuery = ""; this.state.repoPickerActiveSearch = false; - + if (repos.length === 0) { - throw new Error('No repositories found for the provided credentials.'); + throw new Error( + "No repositories found for the provided credentials.", + ); } // Go to repo picker screen! - this.state.screen = 'repo-picker'; + this.state.screen = "repo-picker"; this.state.loading = false; this.render(); } catch (err: any) { @@ -547,24 +604,30 @@ export class TuiEngine { return; } - if ((key && key.name === 'escape') || str === '\u001b') { + if ((key && key.name === "escape") || str === "\u001b") { this.stop(); return; } - if (key && key.name === 'backspace') { - if (form.activeField === 'url') form.url = form.url.slice(0, -1); - if (form.activeField === 'userid') form.userid = form.userid.slice(0, -1); - if (form.activeField === 'token') form.token = form.token.slice(0, -1); + if (key && key.name === "backspace") { + if (form.activeField === "url") form.url = form.url.slice(0, -1); + if (form.activeField === "userid") form.userid = form.userid.slice(0, -1); + if (form.activeField === "token") form.token = form.token.slice(0, -1); this.render(); return; } // Type character - if (str && !key.ctrl && !key.meta && str.length === 1 && str.charCodeAt(0) >= 32) { - if (form.activeField === 'url') form.url += str; - if (form.activeField === 'userid') form.userid += str; - if (form.activeField === 'token') form.token += str; + if ( + str && + !key.ctrl && + !key.meta && + str.length === 1 && + str.charCodeAt(0) >= 32 + ) { + if (form.activeField === "url") form.url += str; + if (form.activeField === "userid") form.userid += str; + if (form.activeField === "token") form.token += str; this.render(); } } @@ -575,36 +638,48 @@ export class TuiEngine { private handleRepoPickerKeypress(str: string, key: any) { if (this.state.loading) return; - const filteredRepos = this.state.repos.filter(r => - r.full_name.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase()) || - (r.description && r.description.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase())) + const filteredRepos = this.state.repos.filter( + (r) => + r.full_name + .toLowerCase() + .includes(this.state.repoSearchQuery.toLowerCase()) || + (r.description && + r.description + .toLowerCase() + .includes(this.state.repoSearchQuery.toLowerCase())), ); if (this.state.repoPickerActiveSearch) { - if ((key && key.name === 'escape') || str === '\u001b') { + if ((key && key.name === "escape") || str === "\u001b") { this.state.repoPickerActiveSearch = false; - this.state.repoSearchQuery = ''; + this.state.repoSearchQuery = ""; this.state.selectedRepoIndex = 0; - process.stdout.write('\x1B[?25l'); // Hide cursor + process.stdout.write("\x1B[?25l"); // Hide cursor this.render(); return; } - if ((key && key.name === 'return') || str === '\r' || str === '\n') { + if ((key && key.name === "return") || str === "\r" || str === "\n") { this.state.repoPickerActiveSearch = false; - process.stdout.write('\x1B[?25l'); // Hide cursor + process.stdout.write("\x1B[?25l"); // Hide cursor this.render(); return; } - if (key && key.name === 'backspace') { + if (key && key.name === "backspace") { this.state.repoSearchQuery = this.state.repoSearchQuery.slice(0, -1); this.state.selectedRepoIndex = 0; this.render(); return; } - if (str && !key.ctrl && !key.meta && str.length === 1 && str.charCodeAt(0) >= 32) { + if ( + str && + !key.ctrl && + !key.meta && + str.length === 1 && + str.charCodeAt(0) >= 32 + ) { this.state.repoSearchQuery += str; this.state.selectedRepoIndex = 0; this.render(); @@ -613,13 +688,13 @@ export class TuiEngine { } // Search inactive keypresses - if ((key && key.name === 'escape') || str === '\u001b') { - this.state.screen = 'setup'; + if ((key && key.name === "escape") || str === "\u001b") { + this.state.screen = "setup"; this.render(); return; } - if (key && key.name === 'up') { + if ((key && key.name === "up") || str === "k") { if (this.state.selectedRepoIndex > 0) { this.state.selectedRepoIndex--; this.render(); @@ -627,7 +702,7 @@ export class TuiEngine { return; } - if (key && key.name === 'down') { + if ((key && key.name === "down") || str === "j") { if (this.state.selectedRepoIndex < filteredRepos.length - 1) { this.state.selectedRepoIndex++; this.render(); @@ -635,17 +710,20 @@ export class TuiEngine { return; } - if (str === '/') { + if (str === "/") { this.state.repoPickerActiveSearch = true; this.render(); return; } - if ((key && key.name === 'return') || str === '\r' || str === '\n') { - if (filteredRepos.length > 0 && this.state.selectedRepoIndex < filteredRepos.length) { + if ((key && key.name === "return") || str === "\r" || str === "\n") { + if ( + filteredRepos.length > 0 && + this.state.selectedRepoIndex < filteredRepos.length + ) { const selectedRepo = filteredRepos[this.state.selectedRepoIndex]; - const parts = selectedRepo.full_name.split('/'); - + const parts = selectedRepo.full_name.split("/"); + this.state.config = { url: normalizeUrl(this.state.setupForm.url), token: this.state.setupForm.token.trim() || null, @@ -662,46 +740,51 @@ export class TuiEngine { repo: `${parts[0]}/${parts[1]}`, }); } - - this.state.screen = 'list'; + + this.state.screen = "list"; this.state.currentPage = 1; this.state.selectedIssueIndex = 0; - this.state.searchQuery = ''; + this.state.searchQuery = ""; this.loadIssues(); } } } - /** * Key handling for searching overlay */ private handleSearchKeypress(str: string, key: any) { - if ((key && key.name === 'escape') || str === '\u001b') { + if ((key && key.name === "escape") || str === "\u001b") { this.activeSearchInput = false; - this.searchInputBuffer = ''; - process.stdout.write('\x1B[?25l'); // Hide cursor + this.searchInputBuffer = ""; + process.stdout.write("\x1B[?25l"); // Hide cursor this.render(); return; } - if ((key && key.name === 'return') || str === '\r' || str === '\n') { + if ((key && key.name === "return") || str === "\r" || str === "\n") { this.activeSearchInput = false; this.state.searchQuery = this.searchInputBuffer; this.state.currentPage = 1; this.state.selectedIssueIndex = 0; - process.stdout.write('\x1B[?25l'); // Hide cursor + process.stdout.write("\x1B[?25l"); // Hide cursor this.loadIssues(); return; } - if (key && key.name === 'backspace') { + if (key && key.name === "backspace") { this.searchInputBuffer = this.searchInputBuffer.slice(0, -1); this.render(); return; } - if (str && !key.ctrl && !key.meta && str.length === 1 && str.charCodeAt(0) >= 32) { + if ( + str && + !key.ctrl && + !key.meta && + str.length === 1 && + str.charCodeAt(0) >= 32 + ) { this.searchInputBuffer += str; this.render(); } @@ -713,30 +796,33 @@ export class TuiEngine { private handleListKeypress(str: string, key: any) { if (this.state.loading) return; - if (key && key.name === 'tab') { - this.state.focusedPane = this.state.focusedPane === 'settings' ? 'list' : 'settings'; + if (key && key.name === "tab") { + this.state.focusedPane = + this.state.focusedPane === "settings" ? "list" : "settings"; this.render(); return; } - if (this.state.focusedPane === 'settings') { - if (key && key.name === 'left') { - this.state.selectedSettingIndex = (this.state.selectedSettingIndex - 1 + 5) % 5; + if (this.state.focusedPane === "settings") { + if (key && key.name === "left") { + this.state.selectedSettingIndex = + (this.state.selectedSettingIndex - 1 + 5) % 5; this.render(); return; } - if (key && key.name === 'right') { - this.state.selectedSettingIndex = (this.state.selectedSettingIndex + 1) % 5; + if (key && key.name === "right") { + this.state.selectedSettingIndex = + (this.state.selectedSettingIndex + 1) % 5; this.render(); return; } - if (key && key.name === 'up') { + if (key && key.name === "up") { return; } - if (key && key.name === 'down') { + if (key && key.name === "down") { return; } - if ((key && key.name === 'return') || str === '\r' || str === '\n') { + if ((key && key.name === "return") || str === "\r" || str === "\n") { const selIdx = this.state.selectedSettingIndex; if (selIdx === 0) { // Search @@ -745,7 +831,11 @@ export class TuiEngine { this.render(); } else if (selIdx === 1) { // State - const states: Array<'open' | 'closed' | 'all'> = ['open', 'closed', 'all']; + const states: Array<"open" | "closed" | "all"> = [ + "open", + "closed", + "all", + ]; const currentIdx = states.indexOf(this.state.stateFilter); this.state.stateFilter = states[(currentIdx + 1) % states.length]; this.state.currentPage = 1; @@ -753,7 +843,11 @@ export class TuiEngine { this.loadIssues(); } else if (selIdx === 2) { // Type - const types: Array<'issues' | 'pulls' | 'all'> = ['issues', 'pulls', 'all']; + const types: Array<"issues" | "pulls" | "all"> = [ + "issues", + "pulls", + "all", + ]; const currentIdx = types.indexOf(this.state.typeFilter); this.state.typeFilter = types[(currentIdx + 1) % types.length]; this.state.currentPage = 1; @@ -761,12 +855,14 @@ export class TuiEngine { this.loadIssues(); } else if (selIdx === 3) { // Sort - const fields: Array<'created' | 'updated' | 'comments' | 'assignees'> = ['created', 'updated', 'comments', 'assignees']; + const fields: Array< + "created" | "updated" | "comments" | "assignees" + > = ["created", "updated", "comments", "assignees"]; const currentSortIdx = fields.indexOf(this.state.sortField); - if (this.state.sortOrder === 'desc') { - this.state.sortOrder = 'asc'; + if (this.state.sortOrder === "desc") { + this.state.sortOrder = "asc"; } else { - this.state.sortOrder = 'desc'; + this.state.sortOrder = "desc"; this.state.sortField = fields[(currentSortIdx + 1) % fields.length]; } this.state.currentPage = 1; @@ -775,9 +871,13 @@ export class TuiEngine { } else if (selIdx === 4) { // Auto-Refresh const intervals = [0, 15, 30, 60, 300]; - const currentIdx = intervals.indexOf(this.state.autoRefreshInterval || 0); - this.state.autoRefreshInterval = intervals[(currentIdx + 1) % intervals.length]; - this.state.config.autoRefreshInterval = this.state.autoRefreshInterval; + const currentIdx = intervals.indexOf( + this.state.autoRefreshInterval || 0, + ); + this.state.autoRefreshInterval = + intervals[(currentIdx + 1) % intervals.length]; + this.state.config.autoRefreshInterval = + this.state.autoRefreshInterval; // Restart timer this.restartAutoRefresh(); // Save to config globally @@ -788,26 +888,26 @@ export class TuiEngine { } } - if (str === '?') { - this.state.previousScreen = 'list'; - this.state.screen = 'help'; + if (str === "?") { + this.state.previousScreen = "list"; + this.state.screen = "help"; this.render(); return; } - if ((key && key.name === 'escape') || str === '\u001b') { + if ((key && key.name === "escape") || str === "\u001b") { this.stop(); return; } - if (str === 'o' || str === 'O') { - this.state.screen = 'setup'; + if (str === "o" || str === "O") { + this.state.screen = "setup"; this.state.error = null; this.render(); return; } - if (key && key.name === 'up') { + if ((key && key.name === "up") || str === "k") { if (this.state.selectedIssueIndex > 0) { this.state.selectedIssueIndex--; this.render(); @@ -815,7 +915,7 @@ export class TuiEngine { return; } - if (key && key.name === 'down') { + if ((key && key.name === "down") || str === "j") { if (this.state.selectedIssueIndex < this.state.issues.length - 1) { this.state.selectedIssueIndex++; this.render(); @@ -824,8 +924,14 @@ export class TuiEngine { } // Pagination: Right/PageDown -> Next, Left/PageUp -> Prev - if ((key && key.name === 'right') || (key && key.name === 'pagedown') || str === 'n') { - const maxPages = Math.ceil(this.state.totalIssuesCount / this.state.issuesPerPage); + if ( + (key && key.name === "right") || + (key && key.name === "pagedown") || + str === "n" + ) { + const maxPages = Math.ceil( + this.state.totalIssuesCount / this.state.issuesPerPage, + ); if (this.state.currentPage < maxPages) { this.state.currentPage++; this.state.selectedIssueIndex = 0; @@ -834,7 +940,11 @@ export class TuiEngine { return; } - if ((key && key.name === 'left') || (key && key.name === 'pageup') || str === 'p') { + if ( + (key && key.name === "left") || + (key && key.name === "pageup") || + str === "p" + ) { if (this.state.currentPage > 1) { this.state.currentPage--; this.state.selectedIssueIndex = 0; @@ -843,19 +953,19 @@ export class TuiEngine { return; } - if ((key && key.name === 'return') || str === '\r' || str === '\n') { + if ((key && key.name === "return") || str === "\r" || str === "\n") { if (this.state.issues.length > 0) { const issue = this.state.issues[this.state.selectedIssueIndex]; this.state.selectedIssue = issue; this.state.detailScrollOffset = 0; - this.state.screen = 'details'; + this.state.screen = "details"; this.loadComments(issue); } return; } // Toggle Filters & Sorting - if (str === '/') { + if (str === "/") { // Focus Search input this.activeSearchInput = true; this.searchInputBuffer = this.state.searchQuery; @@ -863,16 +973,21 @@ export class TuiEngine { return; } - if (str === 's' || str === 'S') { + if (str === "s" || str === "S") { // Cycle sorts: created -> updated -> comments -> assignees - const fields: Array<'created' | 'updated' | 'comments' | 'assignees'> = ['created', 'updated', 'comments', 'assignees']; + const fields: Array<"created" | "updated" | "comments" | "assignees"> = [ + "created", + "updated", + "comments", + "assignees", + ]; const currentSortIdx = fields.indexOf(this.state.sortField); - + // If desc, switch to asc. If asc, switch to next field desc! - if (this.state.sortOrder === 'desc') { - this.state.sortOrder = 'asc'; + if (this.state.sortOrder === "desc") { + this.state.sortOrder = "asc"; } else { - this.state.sortOrder = 'desc'; + this.state.sortOrder = "desc"; this.state.sortField = fields[(currentSortIdx + 1) % fields.length]; } this.state.currentPage = 1; @@ -880,21 +995,26 @@ export class TuiEngine { this.loadIssues(); return; } - - if (str === 'c' || str === 'C') { + + if (str === "c" || str === "C") { // Navigate to Create Issue Screen - this.state.screen = 'create-issue'; - this.state.createIssueForm.title = ''; - this.state.createIssueForm.body = ''; - this.state.createIssueForm.activeField = 'title'; - this.state.createIssueForm.cursor = this.state.createIssueForm.title.length; + this.state.screen = "create-issue"; + this.state.createIssueForm.title = ""; + this.state.createIssueForm.body = ""; + this.state.createIssueForm.activeField = "title"; + this.state.createIssueForm.cursor = + this.state.createIssueForm.title.length; this.render(); return; } - if (str === 'f' || str === 'F') { + if (str === "f" || str === "F") { // Cycle states: open -> closed -> all - const states: Array<'open' | 'closed' | 'all'> = ['open', 'closed', 'all']; + const states: Array<"open" | "closed" | "all"> = [ + "open", + "closed", + "all", + ]; const currentIdx = states.indexOf(this.state.stateFilter); this.state.stateFilter = states[(currentIdx + 1) % states.length]; this.state.currentPage = 1; @@ -903,9 +1023,13 @@ export class TuiEngine { return; } - if (str === 't' || str === 'T') { + if (str === "t" || str === "T") { // Cycle type: issues -> pulls -> all - const types: Array<'issues' | 'pulls' | 'all'> = ['issues', 'pulls', 'all']; + const types: Array<"issues" | "pulls" | "all"> = [ + "issues", + "pulls", + "all", + ]; const currentIdx = types.indexOf(this.state.typeFilter); this.state.typeFilter = types[(currentIdx + 1) % types.length]; this.state.currentPage = 1; @@ -914,20 +1038,20 @@ export class TuiEngine { return; } - if (str === 'r' || str === 'R') { + if (str === "r" || str === "R") { // Refresh this.loadIssues(); return; } - if (str === 'l' || str === 'L') { - this.state.screen = 'labels-list'; + if (str === "l" || str === "L") { + this.state.screen = "labels-list"; this.state.selectedLabelIndex = 0; this.loadLabels(); return; } - if (str === 'q' || str === 'Q') { + if (str === "q" || str === "Q") { this.stop(); } } @@ -936,25 +1060,29 @@ export class TuiEngine { * Key handling for detail screen scrolling */ private handleDetailsKeypress(str: string, key: any) { - if (key && key.name === 'tab') { - this.state.focusedPane = this.state.focusedPane === 'settings' ? 'list' : 'settings'; + if (key && key.name === "tab") { + this.state.focusedPane = + this.state.focusedPane === "settings" ? "list" : "settings"; this.render(); return; } - if (this.state.focusedPane === 'settings') { - if ((key && key.name === 'return') || str === '\r' || str === '\n') { + if (this.state.focusedPane === "settings") { + if ((key && key.name === "return") || str === "\r" || str === "\n") { const intervals = [0, 15, 30, 60, 300]; - const currentIdx = intervals.indexOf(this.state.autoRefreshInterval || 0); - this.state.autoRefreshInterval = intervals[(currentIdx + 1) % intervals.length]; + const currentIdx = intervals.indexOf( + this.state.autoRefreshInterval || 0, + ); + this.state.autoRefreshInterval = + intervals[(currentIdx + 1) % intervals.length]; this.state.config.autoRefreshInterval = this.state.autoRefreshInterval; this.restartAutoRefresh(); saveGlobalConfig(this.state.config); this.render(); return; } - if ((key && key.name === 'escape') || str === '\u001b') { - this.state.focusedPane = 'list'; + if ((key && key.name === "escape") || str === "\u001b") { + this.state.focusedPane = "list"; this.render(); return; } @@ -962,7 +1090,7 @@ export class TuiEngine { return; } - if (key && key.name === 'up') { + if ((key && key.name === "up") || str === "k") { if (this.state.detailScrollOffset > 0) { this.state.detailScrollOffset--; this.render(); @@ -970,100 +1098,101 @@ export class TuiEngine { return; } - if (key && key.name === 'down') { - // We will bounds check this dynamically based on content length during render + // Navigate with j/k as well for vim-style navigation in details view content scroll + else if ((key && key.name === "down") || str === "j") { this.state.detailScrollOffset++; this.render(); return; } - if (key && key.name === 'pageup') { - this.state.detailScrollOffset = Math.max(0, this.state.detailScrollOffset - 10); - this.render(); - return; - } - - if (key && key.name === 'pagedown') { + if (key && key.name === "pagedown") { this.state.detailScrollOffset += 10; this.render(); return; } - if (str === 'o' || str === 'O') { - this.state.screen = 'setup'; + if (str === "o" || str === "O") { + this.state.screen = "setup"; this.state.selectedIssue = null; this.state.selectedIssueComments = []; this.state.error = null; this.render(); return; } - - if (str === 'c' || str === 'C') { - this.state.screen = 'add-comment'; - this.state.addCommentForm.body = ''; + + if (str === "c" || str === "C") { + this.state.screen = "add-comment"; + this.state.addCommentForm.body = ""; this.state.error = null; this.render(); return; } - if (str === 'e' || str === 'E') { + if (str === "e" || str === "E") { if (this.state.selectedIssue) { - this.state.screen = 'edit-issue'; + this.state.screen = "edit-issue"; this.state.editIssueForm.title = this.state.selectedIssue.title; this.state.editIssueForm.body = this.state.selectedIssue.body; - this.state.editIssueForm.activeField = 'title'; + this.state.editIssueForm.activeField = "title"; this.state.editIssueForm.cursor = this.state.editIssueForm.title.length; this.state.error = null; this.render(); } return; } - - if (str === 'r' || str === 'R') { + + if (str === "r" || str === "R") { if (this.state.selectedIssue) { this.reloadSingleIssue(); } return; } - if (str === 't' || str === 'T') { + if (str === "t" || str === "T") { if (this.state.selectedIssue) { - this.state.screen = 'add-time'; - this.state.addTimeForm.timeInput = ''; + this.state.screen = "add-time"; + this.state.addTimeForm.timeInput = ""; this.state.error = null; this.render(); } return; } - if (str === 'x' || str === 'X') { + if (str === "x" || str === "X") { if (this.state.selectedIssue) { - this.state.screen = 'confirm-state-change'; + this.state.screen = "confirm-state-change"; this.state.error = null; this.render(); } return; } - if (str === 'a' || str === 'A') { + if (str === "a" || str === "A") { if (this.state.selectedIssue) { - this.state.screen = 'set-assignees'; - this.state.assigneesInput = (this.state.selectedIssue.assignees || []).map(u => u.login).join(', '); + this.state.screen = "set-assignees"; + this.state.assigneesInput = (this.state.selectedIssue.assignees || []) + .map((u) => u.login) + .join(", "); this.state.error = null; this.render(); } return; } - if (str === '?') { - this.state.previousScreen = 'details'; - this.state.screen = 'help'; + if (str === "?") { + this.state.previousScreen = "details"; + this.state.screen = "help"; this.render(); return; } - if ((key && (key.name === 'escape' || key.name === 'backspace')) || str === '\u001b' || str === 'q' || str === 'Q') { - this.state.screen = 'list'; + if ( + (key && (key.name === "escape" || key.name === "backspace")) || + str === "\u001b" || + str === "q" || + str === "Q" + ) { + this.state.screen = "list"; this.state.selectedIssue = null; this.state.selectedIssueComments = []; this.render(); @@ -1078,61 +1207,68 @@ export class TuiEngine { const rows = process.stdout.rows || 24; // Clear terminal screen, clear scrollback buffer, and reset cursor position - process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); + process.stdout.write("\x1B[2J\x1B[3J\x1B[H"); - if (this.state.screen === 'launch') { + if (this.state.screen === "launch") { this.renderLaunchScreen(cols, rows); - } else if (this.state.screen === 'setup') { + } else if (this.state.screen === "setup") { this.renderSetupScreen(cols, rows); - } else if (this.state.screen === 'repo-picker') { + } else if (this.state.screen === "repo-picker") { this.renderRepoPickerScreen(cols, rows); - } else if (this.state.screen === 'list') { + } else if (this.state.screen === "list") { this.renderListScreen(cols, rows); - } else if (this.state.screen === 'details') { + } else if (this.state.screen === "details") { this.renderDetailsScreen(cols, rows); - } else if (this.state.screen === 'create-issue') { + } else if (this.state.screen === "create-issue") { this.renderCreateIssueScreen(cols, rows); - } else if (this.state.screen === 'add-comment') { + } else if (this.state.screen === "add-comment") { this.renderAddCommentScreen(cols, rows); - } else if (this.state.screen === 'edit-issue') { + } else if (this.state.screen === "edit-issue") { this.renderEditIssueScreen(cols, rows); - } else if (this.state.screen === 'add-time') { + } else if (this.state.screen === "add-time") { this.renderAddTimeScreen(cols, rows); - } else if (this.state.screen === 'confirm-state-change') { + } else if (this.state.screen === "confirm-state-change") { this.renderConfirmStateChangeScreen(cols, rows); - } else if (this.state.screen === 'confirm-cancel-create') { + } else if (this.state.screen === "confirm-cancel-create") { this.renderConfirmCancelCreateScreen(cols, rows); - } else if (this.state.screen === 'confirm-cancel-edit') { + } else if (this.state.screen === "confirm-cancel-edit") { this.renderConfirmCancelEditScreen(cols, rows); - } else if (this.state.screen === 'animating-close') { + } else if (this.state.screen === "animating-close") { this.renderAnimationScreen(cols, rows, GRAVESTONE_FRAMES); - } else if (this.state.screen === 'animating-reopen') { + } else if (this.state.screen === "animating-reopen") { this.renderAnimationScreen(cols, rows, ZOMBIE_FRAMES); - } else if (this.state.screen === 'set-assignees') { + } else if (this.state.screen === "set-assignees") { this.renderSetAssigneesScreen(cols, rows); - } else if (this.state.screen === 'labels-list') { + } else if (this.state.screen === "labels-list") { this.renderLabelsList(cols, rows); - } else if (this.state.screen === 'create-label' || this.state.screen === 'edit-label') { + } else if ( + this.state.screen === "create-label" || + this.state.screen === "edit-label" + ) { this.renderLabelForm(cols, rows); - } else if (this.state.screen === 'help') { + } else if (this.state.screen === "help") { this.renderHelpScreen(cols, rows); } } - private renderAnimationScreen(cols: number, rows: number, frames: string[][]) { + private renderAnimationScreen( + cols: number, + rows: number, + frames: string[][], + ) { const frame = frames[Math.min(this.animationFrame, frames.length - 1)]; const paddingRows = Math.max(0, Math.floor((rows - frame.length) / 2)); - for (let i = 0; i < paddingRows; i++) console.log(' '.repeat(cols)); - + for (let i = 0; i < paddingRows; i++) console.log(" ".repeat(cols)); + for (const line of frame) { const plainLen = stripAnsi(line).length; const padLeft = Math.max(0, Math.floor((cols - plainLen) / 2)); - console.log(' '.repeat(padLeft) + line); + console.log(" ".repeat(padLeft) + line); } const remainingRows = Math.max(0, rows - paddingRows - frame.length - 1); - for (let i = 0; i < remainingRows; i++) console.log(' '.repeat(cols)); + for (let i = 0; i < remainingRows; i++) console.log(" ".repeat(cols)); } /** @@ -1141,114 +1277,216 @@ export class TuiEngine { private renderSetupScreen(cols: number, rows: number) { const form = this.state.setupForm; const width = Math.min(68, cols - 4); - const border = '│'; + const border = "│"; const lines: string[] = []; - lines.push(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(width - 2) + '┐')); - - const title = 'FORGEJO & GITEA TUI ISSUE EXPLORER'; - const titlePadding = ' '.repeat(Math.max(0, Math.floor((width - 2 - title.length) / 2))); - const titleLine = border + titlePadding + chalk.bold.white(title) + ' '.repeat(width - 2 - titlePadding.length - title.length) + border; + lines.push(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(width - 2) + "┐")); + + const title = "FORGEJO & GITEA TUI ISSUE EXPLORER"; + const titlePadding = " ".repeat( + Math.max(0, Math.floor((width - 2 - title.length) / 2)), + ); + const titleLine = + border + + titlePadding + + chalk.bold.white(title) + + " ".repeat(width - 2 - titlePadding.length - title.length) + + border; lines.push(titleLine); - lines.push(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(width - 2) + '┤')); + lines.push(chalk.bold.hex("#4A90E2")("├" + "─".repeat(width - 2) + "┤")); // Instructions - lines.push(border + ' '.repeat(width - 2) + border); - const desc = 'Please connect to a Forgejo/Gitea server to explore repository issues.'; - lines.push(border + ' ' + chalk.gray(desc.padEnd(width - 4)) + ' ' + border); - lines.push(border + ' '.repeat(width - 2) + border); + lines.push(border + " ".repeat(width - 2) + border); + const desc = + "Please connect to a Forgejo/Gitea server to explore repository issues."; + lines.push( + border + " " + chalk.gray(desc.padEnd(width - 4)) + " " + border, + ); + lines.push(border + " ".repeat(width - 2) + border); // Form inputs - const renderField = (label: string, value: string, active: boolean, secret: boolean = false) => { + const renderField = ( + label: string, + value: string, + active: boolean, + secret: boolean = false, + ) => { const fieldWidth = width - 8; - const displayVal = secret ? '*'.repeat(value.length) : value; + const displayVal = secret ? "*".repeat(value.length) : value; const valStrLength = displayVal.length + (active ? 1 : 0); const paddingInsideBracket = Math.max(0, fieldWidth - 19 - valStrLength); - + let coloredValStr = displayVal; if (active) { - coloredValStr = chalk.bold.cyan(displayVal) + chalk.inverse(' ') + ' '.repeat(paddingInsideBracket); + coloredValStr = + chalk.bold.cyan(displayVal) + + chalk.inverse(" ") + + " ".repeat(paddingInsideBracket); } else { - coloredValStr = chalk.white(displayVal) + ' '.repeat(paddingInsideBracket); + coloredValStr = + chalk.white(displayVal) + " ".repeat(paddingInsideBracket); } - - const plainContent = label.padEnd(16) + ': [ ' + displayVal + (active ? ' ' : '') + ' '.repeat(paddingInsideBracket) + ' ]'; - const coloredContent = (active ? chalk.bold.white(label.padEnd(16) + ': [ ') : chalk.gray(label.padEnd(16) + ': [ ')) + - coloredValStr + - (active ? chalk.bold.white(' ]') : chalk.gray(' ]')); - - const activeMarker = active ? chalk.cyan('▶ ') : ' '; + + const plainContent = + label.padEnd(16) + + ": [ " + + displayVal + + (active ? " " : "") + + " ".repeat(paddingInsideBracket) + + " ]"; + const coloredContent = + (active + ? chalk.bold.white(label.padEnd(16) + ": [ ") + : chalk.gray(label.padEnd(16) + ": [ ")) + + coloredValStr + + (active ? chalk.bold.white(" ]") : chalk.gray(" ]")); + + const activeMarker = active ? chalk.cyan("▶ ") : " "; const remainingPadding = Math.max(0, width - 6 - plainContent.length); - - return border + ' ' + activeMarker + coloredContent + ' '.repeat(remainingPadding) + ' ' + border; + + return ( + border + + " " + + activeMarker + + coloredContent + + " ".repeat(remainingPadding) + + " " + + border + ); }; - const renderCheckbox = (label: string, checked: boolean, active: boolean) => { - const activeMarker = active ? chalk.cyan('▶ ') : ' '; - const box = checked ? '[X]' : '[ ]'; - const plainContent = label.padEnd(16) + ': ' + box; - const coloredContent = active - ? chalk.bold.white(label.padEnd(16) + ': ') + chalk.bold.cyan(box) - : chalk.gray(label.padEnd(16) + ': ') + chalk.gray(box); - + const renderCheckbox = ( + label: string, + checked: boolean, + active: boolean, + ) => { + const activeMarker = active ? chalk.cyan("▶ ") : " "; + const box = checked ? "[X]" : "[ ]"; + const plainContent = label.padEnd(16) + ": " + box; + const coloredContent = active + ? chalk.bold.white(label.padEnd(16) + ": ") + chalk.bold.cyan(box) + : chalk.gray(label.padEnd(16) + ": ") + chalk.gray(box); + const remainingPadding = Math.max(0, width - 6 - plainContent.length); - return border + ' ' + activeMarker + coloredContent + ' '.repeat(remainingPadding) + ' ' + border; + return ( + border + + " " + + activeMarker + + coloredContent + + " ".repeat(remainingPadding) + + " " + + border + ); }; - lines.push(renderField('Gitea URL', form.url, form.activeField === 'url')); - lines.push(border + ' '.repeat(width - 2) + border); - lines.push(renderField('User ID', form.userid, form.activeField === 'userid')); - lines.push(border + ' ' + chalk.gray('Forgejo/Gitea username (e.g. gitea_admin)').padEnd(width - 5) + border); - lines.push(border + ' '.repeat(width - 2) + border); - lines.push(renderField('Access Token', form.token, form.activeField === 'token', true)); - lines.push(border + ' ' + chalk.gray('Optional. Required for private repos.').padEnd(width - 5) + border); - lines.push(border + ' '.repeat(width - 2) + border); - lines.push(renderCheckbox('Save to config', form.saveConfig, form.activeField === 'saveConfig')); - lines.push(border + ' ' + chalk.gray('Saves settings to ~/.config/fjtui/fjtui.json').padEnd(width - 5) + border); - lines.push(border + ' '.repeat(width - 2) + border); + lines.push(renderField("Gitea URL", form.url, form.activeField === "url")); + lines.push(border + " ".repeat(width - 2) + border); + lines.push( + renderField("User ID", form.userid, form.activeField === "userid"), + ); + lines.push( + border + + " " + + chalk + .gray("Forgejo/Gitea username (e.g. gitea_admin)") + .padEnd(width - 5) + + border, + ); + lines.push(border + " ".repeat(width - 2) + border); + lines.push( + renderField( + "Access Token", + form.token, + form.activeField === "token", + true, + ), + ); + lines.push( + border + + " " + + chalk.gray("Optional. Required for private repos.").padEnd(width - 5) + + border, + ); + lines.push(border + " ".repeat(width - 2) + border); + lines.push( + renderCheckbox( + "Save to config", + form.saveConfig, + form.activeField === "saveConfig", + ), + ); + lines.push( + border + + " " + + chalk + .gray("Saves settings to ~/.config/fjtui/fjtui.json") + .padEnd(width - 5) + + border, + ); + lines.push(border + " ".repeat(width - 2) + border); // Error message if (this.state.error) { - lines.push(chalk.bold.red('├' + '─'.repeat(width - 2) + '┤')); + lines.push(chalk.bold.red("├" + "─".repeat(width - 2) + "┤")); const errorWrapped = wordWrap(this.state.error, width - 4); for (const errLine of errorWrapped) { - lines.push(border + ' ' + chalk.bold.red(errLine.padEnd(width - 4)) + ' ' + border); + lines.push( + border + + " " + + chalk.bold.red(errLine.padEnd(width - 4)) + + " " + + border, + ); } } - lines.push(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(width - 2) + '┘')); + lines.push(chalk.bold.hex("#4A90E2")("└" + "─".repeat(width - 2) + "┘")); // Loading / Footer indicator - const spinner = this.state.loading ? chalk.cyan(SPINNER_FRAMES[spinnerIndex]) : ' '; - const prompt = this.state.loading ? ' Connecting to instance...' : ' [Tab/Arrows] Navigate [Space/Enter] Toggle Checkbox [Enter] Connect [Esc] Quit'; - + const spinner = this.state.loading + ? chalk.cyan(SPINNER_FRAMES[spinnerIndex]) + : " "; + const prompt = this.state.loading + ? " Connecting to instance..." + : " [Tab/Arrows] Navigate [Space/Enter] Toggle Checkbox [Enter] Connect [Esc] Quit"; + // Draw centered on terminal const vertPadding = Math.max(0, Math.floor((rows - lines.length - 2) / 2)); - process.stdout.write('\n'.repeat(vertPadding)); + process.stdout.write("\n".repeat(vertPadding)); for (const line of lines) { - console.log(' '.repeat(Math.max(0, Math.floor((cols - width) / 2))) + line); + console.log( + " ".repeat(Math.max(0, Math.floor((cols - width) / 2))) + line, + ); } - - const footerText = ' '.repeat(Math.max(0, Math.floor((cols - width) / 2))) + spinner + chalk.gray(prompt); - console.log('\n' + footerText); + + const footerText = + " ".repeat(Math.max(0, Math.floor((cols - width) / 2))) + + spinner + + chalk.gray(prompt); + console.log("\n" + footerText); // Cursor trick: if typing, place cursor at appropriate spot if (!this.state.loading) { // Find current active field offset row let activeRowOffset = vertPadding + 6; // URL row - if (form.activeField === 'userid') activeRowOffset = vertPadding + 8; - if (form.activeField === 'token') activeRowOffset = vertPadding + 11; - if (form.activeField === 'saveConfig') activeRowOffset = vertPadding + 14; - + if (form.activeField === "userid") activeRowOffset = vertPadding + 8; + if (form.activeField === "token") activeRowOffset = vertPadding + 11; + if (form.activeField === "saveConfig") activeRowOffset = vertPadding + 14; + let cursorCol = Math.max(0, Math.floor((cols - width) / 2)); - if (form.activeField === 'saveConfig') { + if (form.activeField === "saveConfig") { cursorCol += 24; // positions cursor inside the checkbox brackets [ ] } else { - const activeValue = form.activeField === 'token' ? '*'.repeat(form.token.length) : form[form.activeField]; + const activeValue = + form.activeField === "token" + ? "*".repeat(form.token.length) + : form[form.activeField]; cursorCol += 25 + activeValue.length; } - - process.stdout.write(`\x1B[${activeRowOffset + 1};${cursorCol}H\x1B[?25h`); // Show cursor + + process.stdout.write( + `\x1B[${activeRowOffset + 1};${cursorCol}H\x1B[?25h`, + ); // Show cursor } } @@ -1257,9 +1495,14 @@ export class TuiEngine { */ private renderListScreen(cols: number, rows: number) { // Header Bar - const spinnerStr = this.state.loading ? chalk.bold.cyan(SPINNER_FRAMES[spinnerIndex]) + ' ' : ''; - const instanceName = normalizeUrl(this.state.config.url).replace(/^https?:\/\//, ''); - const rightHeader = ''; + const spinnerStr = this.state.loading + ? chalk.bold.cyan(SPINNER_FRAMES[spinnerIndex]) + " " + : ""; + const instanceName = normalizeUrl(this.state.config.url).replace( + /^https?:\/\//, + "", + ); + const rightHeader = ""; const rightLen = 0; // We want the total line length to fit exactly within 'cols' @@ -1267,48 +1510,66 @@ export class TuiEngine { // 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); - + const titlePlain = "Forgejo Issue Explorer"; + const titleColor = chalk.bold.hex("#4A90E2")(titlePlain); + const instanceColor = ` ─ ${chalk.bold.white(instanceName)}`; const instancePlain = ` ─ ${instanceName}`; - let statsColor = ''; - let statsPlain = ''; - let shortStatsColor = ''; - let shortStatsPlain = ''; + let statsColor = ""; + let statsPlain = ""; + let shortStatsColor = ""; + let shortStatsPlain = ""; 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, + ); 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) { - statsColor = ' ─ (0 issues found)'; - statsPlain = ' ─ (0 issues found)'; - - shortStatsColor = ' ─ (0)'; - shortStatsPlain = ' ─ (0)'; + statsColor = " ─ (0 issues found)"; + statsPlain = " ─ (0 issues found)"; + + shortStatsColor = " ─ (0)"; + shortStatsPlain = " ─ (0)"; } - let leftHeader = ''; - + let leftHeader = ""; + // Attempt 1: Full header: spinner + Title + instance + stats - if (1 + spinnerPlainLen + titlePlain.length + instancePlain.length + statsPlain.length <= maxLeftWidth) { + 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) { + 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) { + 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) { + else if ( + 1 + spinnerPlainLen + titlePlain.length + instancePlain.length <= + maxLeftWidth + ) { leftHeader = ` ${spinnerStr}${titleColor}${instanceColor}`; } // Attempt 5: Just Title @@ -1320,8 +1581,8 @@ export class TuiEngine { let spacesCount = cols - leftLen - rightLen; if (spacesCount < 1) spacesCount = 1; - console.log(leftHeader + ' '.repeat(spacesCount - 1) + rightHeader); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); + console.log(leftHeader + " ".repeat(spacesCount - 1) + rightHeader); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); // Column widths const idWidth = 6; @@ -1334,39 +1595,87 @@ export class TuiEngine { const commentsWidth = 6; const timeWidth = 7; // Title takes all remaining space. There are 10 columns + 11 borders + 10 spaces = 21 extra chars - const titleWidth = Math.max(10, cols - idWidth - typeWidth - stateWidth - authorWidth - assigneesWidth - labelsWidth - createdWidth - commentsWidth - timeWidth - 21); + const titleWidth = Math.max( + 10, + cols - + idWidth - + typeWidth - + stateWidth - + authorWidth - + assigneesWidth - + labelsWidth - + createdWidth - + commentsWidth - + timeWidth - + 21, + ); // Render Table Header - const padHeader = (title: string, w: number) => chalk.bold.white(title.padEnd(w)); - const borderCh = chalk.bold.hex('#4A90E2')('│'); - + const padHeader = (title: string, w: number) => + chalk.bold.white(title.padEnd(w)); + const borderCh = chalk.bold.hex("#4A90E2")("│"); + console.log( - borderCh + ' ' + - padHeader('ID', idWidth) + borderCh + ' ' + - padHeader('Type', typeWidth) + borderCh + ' ' + - padHeader('State', stateWidth) + borderCh + ' ' + - padHeader('Title', titleWidth) + borderCh + ' ' + - padHeader('Author', authorWidth) + borderCh + ' ' + - padHeader('Assignees', assigneesWidth) + borderCh + ' ' + - padHeader('Labels', labelsWidth) + borderCh + ' ' + - padHeader('Created', createdWidth) + borderCh + ' ' + - padHeader('Coms', commentsWidth) + borderCh + ' ' + - padHeader('Time', timeWidth) + borderCh + borderCh + + " " + + padHeader("ID", idWidth) + + borderCh + + " " + + padHeader("Type", typeWidth) + + borderCh + + " " + + padHeader("State", stateWidth) + + borderCh + + " " + + padHeader("Title", titleWidth) + + borderCh + + " " + + padHeader("Author", authorWidth) + + borderCh + + " " + + padHeader("Assignees", assigneesWidth) + + borderCh + + " " + + padHeader("Labels", labelsWidth) + + borderCh + + " " + + padHeader("Created", createdWidth) + + borderCh + + " " + + padHeader("Coms", commentsWidth) + + borderCh + + " " + + padHeader("Time", timeWidth) + + borderCh, ); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); // Table Content Height available (rows - 10 ensures total printed lines is rows - 2, preventing terminal scrolling) const tableHeight = rows - 10; 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."; const paddingRows = Math.floor((tableHeight - 1) / 2); - for (let i = 0; i < paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - - const contentLine = (' '.repeat(Math.max(0, Math.floor((cols - 2 - msg.length) / 2))) + msg).padEnd(cols - 2); - console.log(borderCh + (this.state.error ? chalk.bold.red(contentLine) : chalk.gray(contentLine)) + borderCh); - - for (let i = 0; i < tableHeight - paddingRows - 1; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + for (let i = 0; i < paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + + const contentLine = ( + " ".repeat(Math.max(0, Math.floor((cols - 2 - msg.length) / 2))) + msg + ).padEnd(cols - 2); + console.log( + borderCh + + (this.state.error + ? chalk.bold.red(contentLine) + : chalk.gray(contentLine)) + + borderCh, + ); + + for (let i = 0; i < tableHeight - paddingRows - 1; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); } else { // Loop through issues on current page for (let i = 0; i < tableHeight; i++) { @@ -1375,89 +1684,132 @@ export class TuiEngine { const isSelected = i === this.state.selectedIssueIndex; const idStr = `#${issue.number}`.padEnd(idWidth); - const typeStr = (issue.pull_request ? 'PR' : 'Issue').padEnd(typeWidth); - + const typeStr = (issue.pull_request ? "PR" : "Issue").padEnd( + typeWidth, + ); + let stateStr = issue.state.toUpperCase().padEnd(stateWidth); - if (issue.state === 'open') { - stateStr = issue.pull_request ? chalk.bold.magenta(stateStr) : chalk.bold.green(stateStr); + if (issue.state === "open") { + stateStr = issue.pull_request + ? chalk.bold.magenta(stateStr) + : chalk.bold.green(stateStr); } else { stateStr = chalk.bold.red(stateStr); } const titleStr = truncate(issue.title, titleWidth).padEnd(titleWidth); - const authorStr = truncate(issue.user.login, authorWidth).padEnd(authorWidth); - const assigneesNames = (issue.assignees || []).map(u => u.login).join(','); - const assigneesStr = truncate(assigneesNames, assigneesWidth).padEnd(assigneesWidth); + const authorStr = truncate(issue.user.login, authorWidth).padEnd( + authorWidth, + ); + const assigneesNames = (issue.assignees || []) + .map((u) => u.login) + .join(","); + const assigneesStr = truncate(assigneesNames, assigneesWidth).padEnd( + assigneesWidth, + ); let plainLen = 0; - let issueLabelsStr = ''; + let issueLabelsStr = ""; for (let idx = 0; idx < issue.labels.length; idx++) { const l = issue.labels[idx]; let name = l.name; if (name.length + 2 > labelsWidth) { - name = name.substring(0, labelsWidth - 3) + '…'; + name = name.substring(0, labelsWidth - 3) + "…"; } const lText = ` ${name} `; if (plainLen + lText.length > labelsWidth) break; - issueLabelsStr += chalk.bgHex('#' + l.color).black(lText); + issueLabelsStr += chalk.bgHex("#" + l.color).black(lText); plainLen += lText.length; if (idx < issue.labels.length - 1 && plainLen + 1 <= labelsWidth) { - issueLabelsStr += ' '; - plainLen += 1; + issueLabelsStr += " "; + plainLen += 1; } else if (idx < issue.labels.length - 1) { - break; + break; } } - issueLabelsStr += ' '.repeat(Math.max(0, labelsWidth - plainLen)); - const createdStr = formatDate(issue.created_at).substring(0, 10).padEnd(createdWidth); + issueLabelsStr += " ".repeat(Math.max(0, labelsWidth - plainLen)); + const createdStr = formatDate(issue.created_at) + .substring(0, 10) + .padEnd(createdWidth); const commentsStr = String(issue.comments).padEnd(commentsWidth); - const timeStr = formatTime(issue.total_tracked_time).padEnd(timeWidth); + const timeStr = formatTime(issue.total_tracked_time).padEnd( + timeWidth, + ); - let rowText = - ' ' + idStr + borderCh + - ' ' + typeStr + borderCh + - ' ' + stateStr + borderCh + - ' ' + (issue.pull_request ? chalk.magenta(titleStr) : titleStr) + borderCh + - ' ' + authorStr + borderCh + - ' ' + assigneesStr + borderCh + - ' ' + issueLabelsStr + borderCh + - ' ' + createdStr + borderCh + - ' ' + commentsStr + borderCh + - ' ' + timeStr + borderCh; + let rowText = + " " + + idStr + + borderCh + + " " + + typeStr + + borderCh + + " " + + stateStr + + borderCh + + " " + + (issue.pull_request ? chalk.magenta(titleStr) : titleStr) + + borderCh + + " " + + authorStr + + borderCh + + " " + + assigneesStr + + borderCh + + " " + + issueLabelsStr + + borderCh + + " " + + createdStr + + borderCh + + " " + + commentsStr + + borderCh + + " " + + timeStr + + borderCh; if (isSelected) { - if (this.state.focusedPane === 'settings') { - rowText = chalk.bgHex('#1C2F4D').white(rowText); + if (this.state.focusedPane === "settings") { + rowText = chalk.bgHex("#1C2F4D").white(rowText); } else { - rowText = chalk.bgHex('#2E4E7E').white.bold(rowText); + rowText = chalk.bgHex("#2E4E7E").white.bold(rowText); } } console.log(borderCh + rowText); } else { // Fill empty space - console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + console.log(borderCh + " ".repeat(cols - 2) + borderCh); } } } - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); // Render Filters line - const isSettings = this.state.focusedPane === 'settings'; + const isSettings = this.state.focusedPane === "settings"; const selIdx = this.state.selectedSettingIndex ?? 0; - const buildSegment = (idx: number, label: string, val: string, formattedVal: string) => { + const buildSegment = ( + idx: number, + label: string, + val: string, + formattedVal: string, + ) => { const text = ` ${label}: ${val} `; if (isSettings && selIdx === idx) { - return chalk.bgHex('#2E4E7E').white.bold(text); + return chalk.bgHex("#2E4E7E").white.bold(text); } return chalk.gray(` ${label}: `) + formattedVal; }; - const searchPlain = this.activeSearchInput ? this.searchInputBuffer : (this.state.searchQuery || 'none'); + const searchPlain = this.activeSearchInput + ? this.searchInputBuffer + : this.state.searchQuery || "none"; const searchLabel = this.activeSearchInput - ? chalk.inverse(this.searchInputBuffer + ' ') - : (this.state.searchQuery ? chalk.yellow(`"${this.state.searchQuery}"`) : chalk.gray('none')); + ? chalk.inverse(this.searchInputBuffer + " ") + : this.state.searchQuery + ? chalk.yellow(`"${this.state.searchQuery}"`) + : chalk.gray("none"); const statePlain = this.state.stateFilter.toUpperCase(); const stateLabel = chalk.bold(statePlain); @@ -1468,12 +1820,12 @@ export class TuiEngine { const sortPlain = `${this.state.sortField} (${this.state.sortOrder.toUpperCase()})`; const sortLabel = chalk.bold(sortPlain); - const seg0 = buildSegment(0, 'Search', searchPlain, searchLabel); - const seg1 = buildSegment(1, 'State', statePlain, stateLabel); - const seg2 = buildSegment(2, 'Type', typePlain, typeLabel); - const seg3 = buildSegment(3, 'Sort', sortPlain, sortLabel); + const seg0 = buildSegment(0, "Search", searchPlain, searchLabel); + const seg1 = buildSegment(1, "State", statePlain, stateLabel); + const seg2 = buildSegment(2, "Type", typePlain, typeLabel); + const seg3 = buildSegment(3, "Sort", sortPlain, sortLabel); - let refreshStr = 'Off'; + let refreshStr = "Off"; if (this.state.autoRefreshInterval) { if (this.state.autoRefreshInterval >= 60) { refreshStr = `${this.state.autoRefreshInterval / 60}m`; @@ -1481,33 +1833,39 @@ export class TuiEngine { refreshStr = `${this.state.autoRefreshInterval}s`; } } - const seg4 = buildSegment(4, 'Refresh', refreshStr, chalk.bold(refreshStr)); + const seg4 = buildSegment(4, "Refresh", refreshStr, chalk.bold(refreshStr)); - const separator = chalk.bold.hex('#4A90E2')(' ─ '); + const separator = chalk.bold.hex("#4A90E2")(" ─ "); const filtersText = ` ${seg0}${separator}${seg1}${separator}${seg2}${separator}${seg3}${separator}${seg4}`; const filtersPlainLen = stripAnsi(filtersText).length; const padding = Math.max(0, cols - 2 - filtersPlainLen); - console.log(borderCh + filtersText + ' '.repeat(padding) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); + console.log(borderCh + filtersText + " ".repeat(padding) + borderCh); + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); // Keyboard controls help line - let fullHelpLine = ' [?] Help [↑/↓] Navigate [Enter] View [C] Create [/] Search [S] Sort [F] State [T] Type [L] Labels [N/P] Page [R] Reload [O] Settings [Esc] Quit'; + let fullHelpLine = + " [?] Help [↑/↓] Navigate [Enter] View [C] Create [/] Search [S] Sort [F] State [T] Type [L] Labels [N/P] Page [R] Reload [O] Settings [Esc] Quit"; if (isSettings) { - fullHelpLine = ' [?] Help [Tab] Back to List [←/→] Select Setting [Enter] Select/Toggle [C] Create [R] Reload [Esc] Quit'; + fullHelpLine = + " [?] Help [Tab] Back to List [←/→] Select Setting [Enter] Select/Toggle [C] Create [R] Reload [Esc] Quit"; } - const repoStr = chalk.bold.cyan(` repo: ${this.state.config.owner}/${this.state.config.repo} `); + const repoStr = chalk.bold.cyan( + ` repo: ${this.state.config.owner}/${this.state.config.repo} `, + ); const repoLen = stripAnsi(repoStr).length; - + // We want total length to be cols - 1 to prevent any wrapping const maxHelpLen = cols - repoLen - 1; let helpLinePlain = fullHelpLine; if (helpLinePlain.length > maxHelpLen) { - helpLinePlain = helpLinePlain.substring(0, maxHelpLen - 3) + '...'; + helpLinePlain = helpLinePlain.substring(0, maxHelpLen - 3) + "..."; } - + const spaces = cols - helpLinePlain.length - repoLen - 1; - process.stdout.write(chalk.gray(helpLinePlain) + ' '.repeat(Math.max(0, spaces)) + repoStr); + process.stdout.write( + chalk.gray(helpLinePlain) + " ".repeat(Math.max(0, spaces)) + repoStr, + ); // If search active, position terminal cursor in search box if (this.activeSearchInput) { @@ -1525,7 +1883,9 @@ export class TuiEngine { // Header info line const isPR = !!issue.pull_request; - const typeTag = isPR ? chalk.bold.magenta('[PR] ') : chalk.bold.green('[Issue] '); + const typeTag = isPR + ? chalk.bold.magenta("[PR] ") + : chalk.bold.green("[Issue] "); const rightHeader = `repo: ${chalk.bold.cyan(`${this.state.config.owner}/${this.state.config.repo}`)} `; const rightLen = stripAnsi(rightHeader).length; @@ -1534,71 +1894,83 @@ export class TuiEngine { const prefixLen = stripAnsi(prefix).length; const titleWidth = Math.max(10, maxLeftWidth - prefixLen); - const leftHeader = prefix + chalk.bold.white(truncate(issue.title, titleWidth)); + const leftHeader = + prefix + chalk.bold.white(truncate(issue.title, titleWidth)); const leftLen = stripAnsi(leftHeader).length; let spacesCount = cols - leftLen - rightLen; if (spacesCount < 1) spacesCount = 1; - console.log(leftHeader + ' '.repeat(spacesCount) + rightHeader); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); + console.log(leftHeader + " ".repeat(spacesCount) + rightHeader); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); // Metadata lines - const borderCh = chalk.bold.hex('#4A90E2')('│'); - + const borderCh = chalk.bold.hex("#4A90E2")("│"); + let stateLabel = issue.state.toUpperCase(); - if (issue.state === 'open') { - stateLabel = isPR ? chalk.bold.magenta(stateLabel) : chalk.bold.green(stateLabel); + if (issue.state === "open") { + stateLabel = isPR + ? chalk.bold.magenta(stateLabel) + : chalk.bold.green(stateLabel); } else { stateLabel = chalk.bold.red(stateLabel); } - const labels = issue.labels.map(l => chalk.bgHex('#' + l.color).black(` ${l.name} `)).join(' '); - const assigneesStr = (issue.assignees || []).map(u => u.login).join(', '); - const assigneesDisp = assigneesStr ? chalk.magenta(assigneesStr) : chalk.gray('none'); + const labels = issue.labels + .map((l) => chalk.bgHex("#" + l.color).black(` ${l.name} `)) + .join(" "); + const assigneesStr = (issue.assignees || []).map((u) => u.login).join(", "); + const assigneesDisp = assigneesStr + ? chalk.magenta(assigneesStr) + : chalk.gray("none"); const timeStr = formatTime(issue.total_tracked_time); const metaLine = ` State: ${stateLabel} Author: ${chalk.cyan(issue.user.login)} Assignees: ${assigneesDisp} Created: ${formatDate(issue.created_at)} Updated: ${formatDate(issue.updated_at)} Time: ${chalk.yellow(timeStr)}`; const metaPlainLen = stripAnsi(metaLine).length; const padding = Math.max(0, cols - 2 - metaPlainLen); - console.log(borderCh + metaLine + ' '.repeat(padding) + borderCh); - + console.log(borderCh + metaLine + " ".repeat(padding) + borderCh); + if (labels) { const labelsLine = ` Labels: ${labels}`; const labelsPlainLen = stripAnsi(labelsLine).length; const labelsPadding = Math.max(0, cols - 2 - labelsPlainLen); - console.log(borderCh + labelsLine + ' '.repeat(labelsPadding) + borderCh); + console.log(borderCh + labelsLine + " ".repeat(labelsPadding) + borderCh); } - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); // Gather and format all detail text (Body + Comments) const contentLines: string[] = []; - + // 1. Render Body - contentLines.push(chalk.bold.yellow('--- DESCRIPTION ---')); + contentLines.push(chalk.bold.yellow("--- DESCRIPTION ---")); if (issue.body.trim()) { const wrappedBody = wordWrap(issue.body, cols - 6); contentLines.push(...wrappedBody); } else { - contentLines.push(chalk.italic.gray('No description provided.')); + contentLines.push(chalk.italic.gray("No description provided.")); } - contentLines.push(''); + contentLines.push(""); // 2. Render Comments Section - contentLines.push(chalk.bold.yellow(`--- COMMENTS (${issue.comments}) ---`)); - + contentLines.push( + chalk.bold.yellow(`--- COMMENTS (${issue.comments}) ---`), + ); + if (this.state.commentsLoading) { - contentLines.push(chalk.cyan(' Loading comments...')); + contentLines.push(chalk.cyan(" Loading comments...")); } else if (this.state.selectedIssueComments.length === 0) { - contentLines.push(chalk.italic.gray(' No comments.')); + contentLines.push(chalk.italic.gray(" No comments.")); } else { for (const comment of this.state.selectedIssueComments) { - contentLines.push(chalk.bold.cyan(`▶ ${comment.user.login}`) + chalk.gray(` at ${formatDate(comment.created_at)}`)); + contentLines.push( + chalk.bold.cyan(`▶ ${comment.user.login}`) + + chalk.gray(` at ${formatDate(comment.created_at)}`), + ); const wrappedComment = wordWrap(comment.body, cols - 8); for (const cLine of wrappedComment) { - contentLines.push(' ' + cLine); + contentLines.push(" " + cLine); } - contentLines.push(''); // blank line between comments + contentLines.push(""); // blank line between comments } } @@ -1606,7 +1978,7 @@ export class TuiEngine { const metaHeight = labels ? 3 : 2; // Meta 1, optionally Meta 2 (Labels), and Separator const displayHeight = rows - 7 - metaHeight; // Header: 1, Borders: 3, Meta, Settings: 1, Help: 1, and -1 const maxScroll = Math.max(0, contentLines.length - displayHeight); - + // Bounds check scroll offset if (this.state.detailScrollOffset > maxScroll) { this.state.detailScrollOffset = maxScroll; @@ -1619,16 +1991,18 @@ export class TuiEngine { const line = contentLines[lineIndex]; const linePlainLen = stripAnsi(line).length; const padding = Math.max(0, cols - 4 - linePlainLen); - console.log(borderCh + ' ' + line + ' '.repeat(padding) + ' ' + borderCh); + console.log( + borderCh + " " + line + " ".repeat(padding) + " " + borderCh, + ); } else { - console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + console.log(borderCh + " ".repeat(cols - 2) + borderCh); } } - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); - const isSettings = this.state.focusedPane === 'settings'; - let refreshStr = 'Off'; + const isSettings = this.state.focusedPane === "settings"; + let refreshStr = "Off"; if (this.state.autoRefreshInterval) { if (this.state.autoRefreshInterval >= 60) { refreshStr = `${this.state.autoRefreshInterval / 60}m`; @@ -1637,23 +2011,31 @@ export class TuiEngine { } } const text = ` Auto-Refresh: ${refreshStr} `; - const coloredText = isSettings ? chalk.bgHex('#2E4E7E').white.bold(text) : chalk.gray(` Auto-Refresh: `) + chalk.bold(refreshStr); + const coloredText = isSettings + ? chalk.bgHex("#2E4E7E").white.bold(text) + : chalk.gray(` Auto-Refresh: `) + chalk.bold(refreshStr); const paddingLength = Math.max(0, cols - 2 - stripAnsi(text).length - 1); - console.log(borderCh + ' ' + coloredText + ' '.repeat(paddingLength) + borderCh); + console.log( + borderCh + " " + coloredText + " ".repeat(paddingLength) + borderCh, + ); - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); // Footer Scroll percentage - let scrollHelp = ' Scroll: [↑/↓] / [PgUp/PgDn]'; + let scrollHelp = " Scroll: [↑/↓] / [PgUp/PgDn]"; if (maxScroll > 0) { const pct = Math.round((this.state.detailScrollOffset / maxScroll) * 100); scrollHelp += chalk.yellow(` (${pct}%)`); } - - const actionKey = issue.state === 'open' ? 'Close' : 'Reopen'; - let helpLine = chalk.gray(` [?] Help [Esc/Backspace] Back to List [C] Add Comment [A] Assign [E] Edit [T] Add Time [X] ${actionKey} [R] Reload [Tab] Menu ${scrollHelp}`); + + const actionKey = issue.state === "open" ? "Close" : "Reopen"; + let helpLine = chalk.gray( + ` [?] Help [Esc/Backspace] Back to List [C] Add Comment [A] Assign [E] Edit [T] Add Time [X] ${actionKey} [R] Reload [Tab] Menu ${scrollHelp}`, + ); if (isSettings) { - helpLine = chalk.gray(` [?] Help [Tab] Back to Details [Enter] Toggle Auto-Refresh [Esc] Quit menu`); + helpLine = chalk.gray( + ` [?] Help [Tab] Back to Details [Enter] Toggle Auto-Refresh [Esc] Quit menu`, + ); } process.stdout.write(helpLine); } @@ -1662,22 +2044,30 @@ export class TuiEngine { * Draw Repository Picker Screen */ private renderRepoPickerScreen(cols: number, rows: number) { - const title = 'SELECT REPOSITORY'; - const subtitle = `Connected to ${normalizeUrl(this.state.setupForm.url).replace(/^https?:\/\//, '')} as ${this.state.setupForm.userid}`; - - console.log(` ${chalk.bold.hex('#4A90E2')(title)} ─ ${chalk.gray(subtitle)}`); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); + const title = "SELECT REPOSITORY"; + const subtitle = `Connected to ${normalizeUrl(this.state.setupForm.url).replace(/^https?:\/\//, "")} as ${this.state.setupForm.userid}`; + + console.log( + ` ${chalk.bold.hex("#4A90E2")(title)} ─ ${chalk.gray(subtitle)}`, + ); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + + const borderCh = chalk.bold.hex("#4A90E2")("│"); - const borderCh = chalk.bold.hex('#4A90E2')('│'); - // Filter repositories based on query - const filteredRepos = this.state.repos.filter(r => - r.full_name.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase()) || - (r.description && r.description.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase())) + const filteredRepos = this.state.repos.filter( + (r) => + r.full_name + .toLowerCase() + .includes(this.state.repoSearchQuery.toLowerCase()) || + (r.description && + r.description + .toLowerCase() + .includes(this.state.repoSearchQuery.toLowerCase())), ); const listHeight = rows - 7; // Header, borders, search, footer - + // Scroll window calculation let startIndex = 0; if (this.state.selectedRepoIndex >= listHeight) { @@ -1685,52 +2075,73 @@ export class TuiEngine { } if (filteredRepos.length === 0) { - const msg = this.state.repoSearchQuery ? 'No repositories match your filter.' : 'No repositories found.'; + const msg = this.state.repoSearchQuery + ? "No repositories match your filter." + : "No repositories found."; const paddingRows = Math.floor((listHeight - 1) / 2); - for (let i = 0; i < paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - console.log(borderCh + (' '.repeat(Math.max(0, Math.floor((cols - 2 - msg.length) / 2))) + chalk.gray(msg)).padEnd(cols - 2) + borderCh); - for (let i = 0; i < listHeight - paddingRows - 1; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + for (let i = 0; i < paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + console.log( + borderCh + + ( + " ".repeat(Math.max(0, Math.floor((cols - 2 - msg.length) / 2))) + + chalk.gray(msg) + ).padEnd(cols - 2) + + borderCh, + ); + for (let i = 0; i < listHeight - paddingRows - 1; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); } else { for (let i = 0; i < listHeight; i++) { const repoIdx = i + startIndex; if (repoIdx < filteredRepos.length) { const repo = filteredRepos[repoIdx]; const isSelected = repoIdx === this.state.selectedRepoIndex; - - const typeStr = repo.private ? chalk.bold.yellow('[Private]') : chalk.bold.green('[Public] '); + + const typeStr = repo.private + ? chalk.bold.yellow("[Private]") + : chalk.bold.green("[Public] "); const nameStr = chalk.bold(repo.full_name); - const descStr = repo.description ? chalk.gray(` ─ ${truncate(repo.description, cols - repo.full_name.length - 20)}`) : ''; - + const descStr = repo.description + ? chalk.gray( + ` ─ ${truncate(repo.description, cols - repo.full_name.length - 20)}`, + ) + : ""; + let lineContent = ` ${typeStr} ${nameStr}${descStr}`; - + // Pad the line content properly to match terminal width const visibleLen = stripAnsi(lineContent).length; - const padding = ' '.repeat(Math.max(0, cols - 4 - visibleLen)); + const padding = " ".repeat(Math.max(0, cols - 4 - visibleLen)); lineContent = lineContent + padding; - + if (isSelected) { - lineContent = chalk.bgHex('#2E4E7E').white.bold(lineContent); + lineContent = chalk.bgHex("#2E4E7E").white.bold(lineContent); } - - console.log(borderCh + ' ' + lineContent + ' ' + borderCh); + + console.log(borderCh + " " + lineContent + " " + borderCh); } else { - console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + console.log(borderCh + " ".repeat(cols - 2) + borderCh); } } } - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); // Filter status line - let filterLabel = this.state.repoSearchQuery ? chalk.yellow(`"${this.state.repoSearchQuery}"`) : chalk.gray('none'); + let filterLabel = this.state.repoSearchQuery + ? chalk.yellow(`"${this.state.repoSearchQuery}"`) + : chalk.gray("none"); if (this.state.repoPickerActiveSearch) { - filterLabel = chalk.inverse(this.state.repoSearchQuery + ' '); + filterLabel = chalk.inverse(this.state.repoSearchQuery + " "); } const filterText = ` Filter: ${filterLabel}`; console.log(borderCh + filterText.padEnd(cols - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); - const helpLine = chalk.gray(' [↑/↓] Navigate [Enter] Select [/] Filter [Esc] Back to Setup'); + const helpLine = chalk.gray( + " [↑/↓] Navigate [Enter] Select [/] Filter [Esc] Back to Setup", + ); process.stdout.write(helpLine); if (this.state.repoPickerActiveSearch) { @@ -1743,70 +2154,85 @@ export class TuiEngine { * Draw Create Issue Screen */ private renderCreateIssueScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - + const borderCh = chalk.bold.hex("#4A90E2")("│"); + // Header - const title = ' Create New Issue '; + const title = " Create New Issue "; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + if (this.state.error) { - console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log( + borderCh + + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); } const form = this.state.createIssueForm; - + // Title Field - let titleLabel = ' Title: '; + let titleLabel = " Title: "; let titleVisible = form.title; - let titleContent = ''; - - if (form.activeField === 'title') { - titleLabel = chalk.yellow(' Title: '); + let titleContent = ""; + + if (form.activeField === "title") { + titleLabel = chalk.yellow(" Title: "); const c = Math.min(form.cursor || 0, form.title.length); - + let startIdx = 0; if (c > cols - 13) { - startIdx = c - (cols - 13) + 1; + startIdx = c - (cols - 13) + 1; } - + const visTitle = form.title.substring(startIdx); const visCursor = c - startIdx; - + const visBefore = visTitle.substring(0, visCursor); - const visChar = visCursor < visTitle.length ? visTitle[visCursor] : ' '; - const visAfter = visTitle.substring(visCursor + 1).padEnd(Math.max(0, cols - 13 - visBefore.length - 1), ' ').substring(0, Math.max(0, cols - 13 - visBefore.length - 1)); - + const visChar = visCursor < visTitle.length ? visTitle[visCursor] : " "; + const visAfter = visTitle + .substring(visCursor + 1) + .padEnd(Math.max(0, cols - 13 - visBefore.length - 1), " ") + .substring(0, Math.max(0, cols - 13 - visBefore.length - 1)); + titleContent = visBefore + chalk.inverse(visChar) + visAfter; } else { - titleVisible = titleVisible.length > cols - 13 ? titleVisible.substring(titleVisible.length - (cols - 13)) : titleVisible; + titleVisible = + titleVisible.length > cols - 13 + ? titleVisible.substring(titleVisible.length - (cols - 13)) + : titleVisible; titleContent = titleVisible.padEnd(cols - 12); } - + console.log(borderCh + titleLabel + titleContent + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); - + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); + // Body Field - let bodyLabel = chalk.bold(' Body (markdown supported): '); - if (form.activeField === 'body') { - bodyLabel = chalk.yellow.bold(' Body (markdown supported): '); + let bodyLabel = chalk.bold(" Body (markdown supported): "); + if (form.activeField === "body") { + bodyLabel = chalk.yellow.bold(" Body (markdown supported): "); } - console.log(borderCh + bodyLabel + ' '.repeat(cols - stripAnsi(bodyLabel).length - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); - + console.log( + borderCh + + bodyLabel + + " ".repeat(cols - stripAnsi(bodyLabel).length - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); + // Body Content (multiline) const errorOffset = this.state.error ? 2 : 0; const bodyRows = rows - 10 - errorOffset; // available space for body - - const lines = form.body.split('\n'); + + const lines = form.body.split("\n"); let cursorLine = 0; let cursorCol = 0; let remaining = Math.min(form.cursor || 0, form.body.length); - + for (let i = 0; i < lines.length; i++) { if (remaining <= lines[i].length) { cursorLine = i; @@ -1815,95 +2241,108 @@ export class TuiEngine { } remaining -= lines[i].length + 1; } - + let startLine = Math.max(0, lines.length - bodyRows); - if (form.activeField === 'body') { - if (cursorLine < startLine) { - startLine = cursorLine; - } else if (cursorLine >= startLine + bodyRows) { - startLine = cursorLine - bodyRows + 1; - } + if (form.activeField === "body") { + if (cursorLine < startLine) { + startLine = cursorLine; + } else if (cursorLine >= startLine + bodyRows) { + startLine = cursorLine - bodyRows + 1; + } } - + for (let i = 0; i < bodyRows; i++) { const lineIdx = startLine + i; - let lineContent = ''; + let lineContent = ""; if (lineIdx < lines.length) { lineContent = lines[lineIdx]; } - - if (form.activeField === 'body' && lineIdx === cursorLine) { + + if (form.activeField === "body" && lineIdx === cursorLine) { let startCol = 0; if (cursorCol > cols - 6) { - startCol = cursorCol - (cols - 6) + 1; + startCol = cursorCol - (cols - 6) + 1; } - + const visLineStr = lineContent.substring(startCol); const visCursorCol = cursorCol - startCol; - + const before = visLineStr.substring(0, visCursorCol); - const char = visCursorCol < visLineStr.length ? visLineStr[visCursorCol] : ' '; + const char = + visCursorCol < visLineStr.length ? visLineStr[visCursorCol] : " "; const after = visLineStr.substring(visCursorCol + 1); - + let visLine = before + chalk.inverse(char) + after; const plainLen = stripAnsi(visLine).length; if (plainLen < cols - 4) { - visLine += ' '.repeat(cols - 4 - plainLen); + visLine += " ".repeat(cols - 4 - plainLen); } - - console.log(borderCh + ' ' + visLine + ' ' + borderCh); + + console.log(borderCh + " " + visLine + " " + borderCh); } else { let visLineStr = lineContent; if (visLineStr.length > cols - 5) { visLineStr = visLineStr.substring(visLineStr.length - (cols - 5)); } visLineStr = visLineStr.padEnd(cols - 4); - console.log(borderCh + ' ' + visLineStr + ' ' + borderCh); + console.log(borderCh + " " + visLineStr + " " + borderCh); } } - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - + + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + // Help line - const helpLine = chalk.gray(' [Tab] Switch Fields [Ctrl+S] Submit [Esc] Cancel'); - const repoStr = chalk.bold.cyan(` repo: ${this.state.config.owner}/${this.state.config.repo} `); + const helpLine = chalk.gray( + " [Tab] Switch Fields [Ctrl+S] Submit [Esc] Cancel", + ); + const repoStr = chalk.bold.cyan( + ` repo: ${this.state.config.owner}/${this.state.config.repo} `, + ); const repoLen = stripAnsi(repoStr).length; - + const maxHelpLen = cols - repoLen - 1; let helpLinePlain = helpLine; if (stripAnsi(helpLinePlain).length > maxHelpLen) { - helpLinePlain = chalk.gray(stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + '...'); + helpLinePlain = chalk.gray( + stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + "...", + ); } - + const spaces = cols - stripAnsi(helpLinePlain).length - repoLen - 1; - process.stdout.write(helpLinePlain + ' '.repeat(Math.max(0, spaces)) + repoStr); + process.stdout.write( + helpLinePlain + " ".repeat(Math.max(0, spaces)) + repoStr, + ); } /** * Handle Keypress for Create Issue Screen */ private async handleCreateIssueKeypress(str: string, key: any) { - if (key && key.name === 'escape') { - this.state.screen = 'confirm-cancel-create'; + if (key && key.name === "escape") { + this.state.screen = "confirm-cancel-create"; this.state.error = null; this.render(); return; } - - if (key && key.ctrl && key.name === 's') { + + if (key && key.ctrl && key.name === "s") { // Submit issue if (!this.state.createIssueForm.title.trim()) { - this.state.error = 'Title is required to create an issue.'; + this.state.error = "Title is required to create an issue."; this.render(); return; } - + this.state.error = null; this.state.loading = true; this.render(); try { - await createIssue(this.state.config, this.state.createIssueForm.title, this.state.createIssueForm.body); - this.state.screen = 'list'; + await createIssue( + this.state.config, + this.state.createIssueForm.title, + this.state.createIssueForm.body, + ); + this.state.screen = "list"; this.state.loading = false; // Refresh issues this.state.currentPage = 1; @@ -1916,35 +2355,45 @@ export class TuiEngine { } return; } - - if (key && key.name === 'tab') { - this.state.createIssueForm.activeField = this.state.createIssueForm.activeField === 'title' ? 'body' : 'title'; - this.state.createIssueForm.cursor = this.state.createIssueForm[this.state.createIssueForm.activeField].length; + + if (key && key.name === "tab") { + this.state.createIssueForm.activeField = + this.state.createIssueForm.activeField === "title" ? "body" : "title"; + this.state.createIssueForm.cursor = + this.state.createIssueForm[ + this.state.createIssueForm.activeField + ].length; this.render(); return; } - + // Text input handling const activeField = this.state.createIssueForm.activeField; let val = this.state.createIssueForm[activeField]; let cursor = this.state.createIssueForm.cursor; if (cursor === undefined || cursor > val.length) cursor = val.length; - - if (key && key.name === 'left') { + + if (key && key.name === "left") { this.state.createIssueForm.cursor = Math.max(0, cursor - 1); this.render(); return; } - - if (key && key.name === 'right') { + + if (key && key.name === "right") { this.state.createIssueForm.cursor = Math.min(val.length, cursor + 1); this.render(); return; } - if (key && (key.name === 'up' || key.name === 'down') && activeField === 'body') { - const lines = val.split('\n'); - let lineIdx = 0, col = 0, count = 0; + if ( + key && + (key.name === "up" || key.name === "down") && + activeField === "body" + ) { + const lines = val.split("\n"); + let lineIdx = 0, + col = 0, + count = 0; for (let i = 0; i < lines.length; i++) { if (count + lines[i].length >= cursor) { lineIdx = i; @@ -1953,15 +2402,15 @@ export class TuiEngine { } count += lines[i].length + 1; } - - if (key.name === 'up' && lineIdx > 0) { + + if (key.name === "up" && lineIdx > 0) { const prevLine = lines[lineIdx - 1]; const newCol = Math.min(col, prevLine.length); let newCursor = 0; for (let i = 0; i < lineIdx - 1; i++) newCursor += lines[i].length + 1; this.state.createIssueForm.cursor = newCursor + newCol; this.render(); - } else if (key.name === 'down' && lineIdx < lines.length - 1) { + } else if (key.name === "down" && lineIdx < lines.length - 1) { const nextLine = lines[lineIdx + 1]; const newCol = Math.min(col, nextLine.length); let newCursor = 0; @@ -1971,137 +2420,164 @@ export class TuiEngine { } return; } - - if (key && (key.name === 'backspace' || key.name === 'delete')) { - if (key.name === 'backspace' && cursor > 0) { - this.state.createIssueForm[activeField] = val.slice(0, cursor - 1) + val.slice(cursor); + + if (key && (key.name === "backspace" || key.name === "delete")) { + if (key.name === "backspace" && cursor > 0) { + this.state.createIssueForm[activeField] = + val.slice(0, cursor - 1) + val.slice(cursor); this.state.createIssueForm.cursor = cursor - 1; this.render(); - } else if (key.name === 'delete' && cursor < val.length) { - this.state.createIssueForm[activeField] = val.slice(0, cursor) + val.slice(cursor + 1); + } else if (key.name === "delete" && cursor < val.length) { + this.state.createIssueForm[activeField] = + val.slice(0, cursor) + val.slice(cursor + 1); this.render(); } return; } - - if (key && key.name === 'return') { - if (activeField === 'body') { - this.state.createIssueForm.body = val.slice(0, cursor) + '\n' + val.slice(cursor); + + if (key && key.name === "return") { + if (activeField === "body") { + this.state.createIssueForm.body = + val.slice(0, cursor) + "\n" + val.slice(cursor); this.state.createIssueForm.cursor = cursor + 1; this.render(); } else { - this.state.createIssueForm.activeField = 'body'; - this.state.createIssueForm.cursor = this.state.createIssueForm.body.length; + this.state.createIssueForm.activeField = "body"; + this.state.createIssueForm.cursor = + this.state.createIssueForm.body.length; this.render(); } return; } - + if (str && str.length === 1 && !key.ctrl && !key.meta) { this.state.error = null; - this.state.createIssueForm[activeField] = val.slice(0, cursor) + str + val.slice(cursor); + this.state.createIssueForm[activeField] = + val.slice(0, cursor) + str + val.slice(cursor); this.state.createIssueForm.cursor = cursor + 1; this.render(); } } private renderAddCommentScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - + const borderCh = chalk.bold.hex("#4A90E2")("│"); + // Header - const title = ' Add Comment '; + const title = " Add Comment "; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + if (this.state.error) { - console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log( + borderCh + + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); } const form = this.state.addCommentForm; - + // Body Field - const bodyLabel = chalk.yellow.bold(' Comment (markdown supported): '); - console.log(borderCh + bodyLabel + ' '.repeat(cols - stripAnsi(bodyLabel).length - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); - + const bodyLabel = chalk.yellow.bold(" Comment (markdown supported): "); + console.log( + borderCh + + bodyLabel + + " ".repeat(cols - stripAnsi(bodyLabel).length - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); + // Body Content (multiline) const errorOffset = this.state.error ? 2 : 0; const bodyRows = rows - 8 - errorOffset; // available space for body - const lines = form.body.split('\n'); - + const lines = form.body.split("\n"); + // Pagination for body if it gets too long const startLine = Math.max(0, lines.length - bodyRows); - + for (let i = 0; i < bodyRows; i++) { const lineIdx = startLine + i; - let lineContent = ''; + let lineContent = ""; if (lineIdx < lines.length) { lineContent = lines[lineIdx]; } - + let visLine = lineContent; if (visLine.length > cols - 5) { visLine = visLine.substring(visLine.length - (cols - 5)); } - + if (lineIdx === lines.length - 1) { - visLine = visLine + chalk.inverse(' ') + ' '.repeat(Math.max(0, cols - 5 - stripAnsi(visLine).length)); + visLine = + visLine + + chalk.inverse(" ") + + " ".repeat(Math.max(0, cols - 5 - stripAnsi(visLine).length)); } else { visLine = visLine.padEnd(cols - 4); } - - console.log(borderCh + ' ' + visLine + ' ' + borderCh); + + console.log(borderCh + " " + visLine + " " + borderCh); } - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - + + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + // Help line - const helpLine = chalk.gray(' [Ctrl+S] Submit [Esc] Cancel'); - const issueStr = this.state.selectedIssue ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) : ''; + const helpLine = chalk.gray(" [Ctrl+S] Submit [Esc] Cancel"); + const issueStr = this.state.selectedIssue + ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) + : ""; const issueLen = stripAnsi(issueStr).length; - + const maxHelpLen = cols - issueLen - 1; let helpLinePlain = helpLine; if (stripAnsi(helpLinePlain).length > maxHelpLen) { - helpLinePlain = chalk.gray(stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + '...'); + helpLinePlain = chalk.gray( + stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + "...", + ); } - + const spaces = cols - stripAnsi(helpLinePlain).length - issueLen - 1; - process.stdout.write(helpLinePlain + ' '.repeat(Math.max(0, spaces)) + issueStr); + process.stdout.write( + helpLinePlain + " ".repeat(Math.max(0, spaces)) + issueStr, + ); } private async handleAddCommentKeypress(str: string, key: any) { - if (key && key.name === 'escape') { - this.state.screen = 'details'; + if (key && key.name === "escape") { + this.state.screen = "details"; this.state.error = null; this.render(); return; } - - if (key && key.ctrl && key.name === 's') { + + if (key && key.ctrl && key.name === "s") { // Submit comment if (!this.state.addCommentForm.body.trim()) { - this.state.error = 'Comment cannot be empty.'; + this.state.error = "Comment cannot be empty."; this.render(); return; } - + if (!this.state.selectedIssue) { - this.state.error = 'No issue selected.'; + this.state.error = "No issue selected."; this.render(); return; } - + this.state.error = null; this.state.loading = true; this.render(); try { - await createIssueComment(this.state.config, this.state.selectedIssue.number, this.state.addCommentForm.body); - this.state.screen = 'details'; + await createIssueComment( + this.state.config, + this.state.selectedIssue.number, + this.state.addCommentForm.body, + ); + this.state.screen = "details"; this.state.loading = false; // Refresh comments this.loadComments(this.state.selectedIssue); @@ -2112,23 +2588,23 @@ export class TuiEngine { } return; } - + let val = this.state.addCommentForm.body; - - if (key && key.name === 'backspace') { + + if (key && key.name === "backspace") { if (val.length > 0) { this.state.addCommentForm.body = val.slice(0, -1); this.render(); } return; } - - if (key && key.name === 'return') { - this.state.addCommentForm.body += '\n'; + + if (key && key.name === "return") { + this.state.addCommentForm.body += "\n"; this.render(); return; } - + if (str && str.length === 1 && !key.ctrl && !key.meta) { this.state.error = null; this.state.addCommentForm.body += str; @@ -2137,70 +2613,85 @@ export class TuiEngine { } private renderEditIssueScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - + const borderCh = chalk.bold.hex("#4A90E2")("│"); + // Header - const title = ' Edit Issue '; + const title = " Edit Issue "; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + if (this.state.error) { - console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log( + borderCh + + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); } const form = this.state.editIssueForm; - + // Title Field - let titleLabel = ' Title: '; + let titleLabel = " Title: "; let titleVisible = form.title; - let titleContent = ''; - - if (form.activeField === 'title') { - titleLabel = chalk.yellow(' Title: '); + let titleContent = ""; + + if (form.activeField === "title") { + titleLabel = chalk.yellow(" Title: "); const c = Math.min(form.cursor || 0, form.title.length); - + let startIdx = 0; if (c > cols - 13) { - startIdx = c - (cols - 13) + 1; + startIdx = c - (cols - 13) + 1; } - + const visTitle = form.title.substring(startIdx); const visCursor = c - startIdx; - + const visBefore = visTitle.substring(0, visCursor); - const visChar = visCursor < visTitle.length ? visTitle[visCursor] : ' '; - const visAfter = visTitle.substring(visCursor + 1).padEnd(Math.max(0, cols - 13 - visBefore.length - 1), ' ').substring(0, Math.max(0, cols - 13 - visBefore.length - 1)); - + const visChar = visCursor < visTitle.length ? visTitle[visCursor] : " "; + const visAfter = visTitle + .substring(visCursor + 1) + .padEnd(Math.max(0, cols - 13 - visBefore.length - 1), " ") + .substring(0, Math.max(0, cols - 13 - visBefore.length - 1)); + titleContent = visBefore + chalk.inverse(visChar) + visAfter; } else { - titleVisible = titleVisible.length > cols - 13 ? titleVisible.substring(titleVisible.length - (cols - 13)) : titleVisible; + titleVisible = + titleVisible.length > cols - 13 + ? titleVisible.substring(titleVisible.length - (cols - 13)) + : titleVisible; titleContent = titleVisible.padEnd(cols - 12); } - + console.log(borderCh + titleLabel + titleContent + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); - + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); + // Body Field - let bodyLabel = chalk.bold(' Body (markdown supported): '); - if (form.activeField === 'body') { - bodyLabel = chalk.yellow.bold(' Body (markdown supported): '); + let bodyLabel = chalk.bold(" Body (markdown supported): "); + if (form.activeField === "body") { + bodyLabel = chalk.yellow.bold(" Body (markdown supported): "); } - console.log(borderCh + bodyLabel + ' '.repeat(cols - stripAnsi(bodyLabel).length - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); - + console.log( + borderCh + + bodyLabel + + " ".repeat(cols - stripAnsi(bodyLabel).length - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); + // Body Content (multiline) const errorOffset = this.state.error ? 2 : 0; const bodyRows = rows - 10 - errorOffset; // available space for body - - const lines = form.body.split('\n'); + + const lines = form.body.split("\n"); let cursorLine = 0; let cursorCol = 0; let remaining = Math.min(form.cursor || 0, form.body.length); - + for (let i = 0; i < lines.length; i++) { if (remaining <= lines[i].length) { cursorLine = i; @@ -2209,106 +2700,122 @@ export class TuiEngine { } remaining -= lines[i].length + 1; } - + let startLine = Math.max(0, lines.length - bodyRows); - if (form.activeField === 'body') { - if (cursorLine < startLine) { - startLine = cursorLine; - } else if (cursorLine >= startLine + bodyRows) { - startLine = cursorLine - bodyRows + 1; - } + if (form.activeField === "body") { + if (cursorLine < startLine) { + startLine = cursorLine; + } else if (cursorLine >= startLine + bodyRows) { + startLine = cursorLine - bodyRows + 1; + } } - + for (let i = 0; i < bodyRows; i++) { const lineIdx = startLine + i; - let lineContent = ''; + let lineContent = ""; if (lineIdx < lines.length) { lineContent = lines[lineIdx]; } - - if (form.activeField === 'body' && lineIdx === cursorLine) { + + if (form.activeField === "body" && lineIdx === cursorLine) { let startCol = 0; if (cursorCol > cols - 6) { - startCol = cursorCol - (cols - 6) + 1; + startCol = cursorCol - (cols - 6) + 1; } - + const visLineStr = lineContent.substring(startCol); const visCursorCol = cursorCol - startCol; - + const before = visLineStr.substring(0, visCursorCol); - const char = visCursorCol < visLineStr.length ? visLineStr[visCursorCol] : ' '; + const char = + visCursorCol < visLineStr.length ? visLineStr[visCursorCol] : " "; const after = visLineStr.substring(visCursorCol + 1); - + let visLine = before + chalk.inverse(char) + after; const plainLen = stripAnsi(visLine).length; if (plainLen < cols - 4) { - visLine += ' '.repeat(cols - 4 - plainLen); + visLine += " ".repeat(cols - 4 - plainLen); } - - console.log(borderCh + ' ' + visLine + ' ' + borderCh); + + console.log(borderCh + " " + visLine + " " + borderCh); } else { let visLineStr = lineContent; if (visLineStr.length > cols - 5) { visLineStr = visLineStr.substring(visLineStr.length - (cols - 5)); } visLineStr = visLineStr.padEnd(cols - 4); - console.log(borderCh + ' ' + visLineStr + ' ' + borderCh); + console.log(borderCh + " " + visLineStr + " " + borderCh); } } - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - + + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + // Help line - const helpLine = chalk.gray(' [Tab] Switch Fields [Ctrl+S] Save [Esc] Cancel'); - const issueStr = this.state.selectedIssue ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) : ''; + const helpLine = chalk.gray( + " [Tab] Switch Fields [Ctrl+S] Save [Esc] Cancel", + ); + const issueStr = this.state.selectedIssue + ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) + : ""; const issueLen = stripAnsi(issueStr).length; - + const maxHelpLen = cols - issueLen - 1; let helpLinePlain = helpLine; if (stripAnsi(helpLinePlain).length > maxHelpLen) { - helpLinePlain = chalk.gray(stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + '...'); + helpLinePlain = chalk.gray( + stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + "...", + ); } - + const spaces = cols - stripAnsi(helpLinePlain).length - issueLen - 1; - process.stdout.write(helpLinePlain + ' '.repeat(Math.max(0, spaces)) + issueStr); + process.stdout.write( + helpLinePlain + " ".repeat(Math.max(0, spaces)) + issueStr, + ); } private async handleEditIssueKeypress(str: string, key: any) { - if (key && key.name === 'escape') { - this.state.screen = 'confirm-cancel-edit'; + if (key && key.name === "escape") { + this.state.screen = "confirm-cancel-edit"; this.state.error = null; this.render(); return; } - - if (key && key.ctrl && key.name === 's') { + + if (key && key.ctrl && key.name === "s") { // Save issue if (!this.state.editIssueForm.title.trim()) { - this.state.error = 'Title is required.'; + this.state.error = "Title is required."; this.render(); return; } - + if (!this.state.selectedIssue) { - this.state.error = 'No issue selected.'; + this.state.error = "No issue selected."; this.render(); return; } - + this.state.error = null; this.state.loading = true; this.render(); try { - const updatedIssue = await editIssue(this.state.config, this.state.selectedIssue.number, this.state.editIssueForm.title, this.state.editIssueForm.body); - + const updatedIssue = await editIssue( + this.state.config, + this.state.selectedIssue.number, + this.state.editIssueForm.title, + this.state.editIssueForm.body, + ); + // Update the list and selected issue with the new data this.state.selectedIssue = updatedIssue; - const listIndex = this.state.issues.findIndex((i) => i.number === updatedIssue.number); + const listIndex = this.state.issues.findIndex( + (i) => i.number === updatedIssue.number, + ); if (listIndex !== -1) { this.state.issues[listIndex] = updatedIssue; } - - this.state.screen = 'details'; + + this.state.screen = "details"; this.state.loading = false; this.render(); } catch (err: any) { @@ -2318,35 +2825,43 @@ export class TuiEngine { } return; } - - if (key && key.name === 'tab') { - this.state.editIssueForm.activeField = this.state.editIssueForm.activeField === 'title' ? 'body' : 'title'; - this.state.editIssueForm.cursor = this.state.editIssueForm[this.state.editIssueForm.activeField].length; + + if (key && key.name === "tab") { + this.state.editIssueForm.activeField = + this.state.editIssueForm.activeField === "title" ? "body" : "title"; + this.state.editIssueForm.cursor = + this.state.editIssueForm[this.state.editIssueForm.activeField].length; this.render(); return; } - + // Text input handling const activeField = this.state.editIssueForm.activeField; let val = this.state.editIssueForm[activeField]; let cursor = this.state.editIssueForm.cursor; if (cursor === undefined || cursor > val.length) cursor = val.length; - - if (key && key.name === 'left') { + + if (key && key.name === "left") { this.state.editIssueForm.cursor = Math.max(0, cursor - 1); this.render(); return; } - - if (key && key.name === 'right') { + + if (key && key.name === "right") { this.state.editIssueForm.cursor = Math.min(val.length, cursor + 1); this.render(); return; } - if (key && (key.name === 'up' || key.name === 'down') && activeField === 'body') { - const lines = val.split('\n'); - let lineIdx = 0, col = 0, count = 0; + if ( + key && + (key.name === "up" || key.name === "down") && + activeField === "body" + ) { + const lines = val.split("\n"); + let lineIdx = 0, + col = 0, + count = 0; for (let i = 0; i < lines.length; i++) { if (count + lines[i].length >= cursor) { lineIdx = i; @@ -2355,15 +2870,15 @@ export class TuiEngine { } count += lines[i].length + 1; } - - if (key.name === 'up' && lineIdx > 0) { + + if (key.name === "up" && lineIdx > 0) { const prevLine = lines[lineIdx - 1]; const newCol = Math.min(col, prevLine.length); let newCursor = 0; for (let i = 0; i < lineIdx - 1; i++) newCursor += lines[i].length + 1; this.state.editIssueForm.cursor = newCursor + newCol; this.render(); - } else if (key.name === 'down' && lineIdx < lines.length - 1) { + } else if (key.name === "down" && lineIdx < lines.length - 1) { const nextLine = lines[lineIdx + 1]; const newCol = Math.min(col, nextLine.length); let newCursor = 0; @@ -2373,146 +2888,173 @@ export class TuiEngine { } return; } - - if (key && (key.name === 'backspace' || key.name === 'delete')) { - if (key.name === 'backspace' && cursor > 0) { - this.state.editIssueForm[activeField] = val.slice(0, cursor - 1) + val.slice(cursor); + + if (key && (key.name === "backspace" || key.name === "delete")) { + if (key.name === "backspace" && cursor > 0) { + this.state.editIssueForm[activeField] = + val.slice(0, cursor - 1) + val.slice(cursor); this.state.editIssueForm.cursor = cursor - 1; this.render(); - } else if (key.name === 'delete' && cursor < val.length) { - this.state.editIssueForm[activeField] = val.slice(0, cursor) + val.slice(cursor + 1); + } else if (key.name === "delete" && cursor < val.length) { + this.state.editIssueForm[activeField] = + val.slice(0, cursor) + val.slice(cursor + 1); this.render(); } return; } - - if (key && key.name === 'return') { - if (activeField === 'body') { - this.state.editIssueForm.body = val.slice(0, cursor) + '\n' + val.slice(cursor); + + if (key && key.name === "return") { + if (activeField === "body") { + this.state.editIssueForm.body = + val.slice(0, cursor) + "\n" + val.slice(cursor); this.state.editIssueForm.cursor = cursor + 1; this.render(); } else { - this.state.editIssueForm.activeField = 'body'; + this.state.editIssueForm.activeField = "body"; this.state.editIssueForm.cursor = this.state.editIssueForm.body.length; this.render(); } return; } - + if (str && str.length === 1 && !key.ctrl && !key.meta) { this.state.error = null; - this.state.editIssueForm[activeField] = val.slice(0, cursor) + str + val.slice(cursor); + this.state.editIssueForm[activeField] = + val.slice(0, cursor) + str + val.slice(cursor); this.state.editIssueForm.cursor = cursor + 1; this.render(); } } private renderAddTimeScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - + const borderCh = chalk.bold.hex("#4A90E2")("│"); + // Header - const title = ' Add Time '; + const title = " Add Time "; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + if (this.state.error) { - console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log( + borderCh + + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); } const form = this.state.addTimeForm; - + // Time Field - const timeLabel = chalk.yellow.bold(' Time Spent (e.g. 1h 30m, 45m): '); - console.log(borderCh + timeLabel + ' '.repeat(Math.max(0, cols - stripAnsi(timeLabel).length - 2)) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); - + const timeLabel = chalk.yellow.bold(" Time Spent (e.g. 1h 30m, 45m): "); + console.log( + borderCh + + timeLabel + + " ".repeat(Math.max(0, cols - stripAnsi(timeLabel).length - 2)) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); + // Input Box let visLine = form.timeInput; if (visLine.length > cols - 5) { visLine = visLine.substring(visLine.length - (cols - 5)); } - - visLine = visLine + chalk.inverse(' ') + ' '.repeat(Math.max(0, cols - 5 - stripAnsi(visLine).length)); - - console.log(borderCh + ' ' + visLine + ' ' + borderCh); - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - + + visLine = + visLine + + chalk.inverse(" ") + + " ".repeat(Math.max(0, cols - 5 - stripAnsi(visLine).length)); + + console.log(borderCh + " " + visLine + " " + borderCh); + + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + // Help line - const helpLine = chalk.gray(' [Ctrl+S] Submit [Esc] Cancel'); - const issueStr = this.state.selectedIssue ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) : ''; + const helpLine = chalk.gray(" [Ctrl+S] Submit [Esc] Cancel"); + const issueStr = this.state.selectedIssue + ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) + : ""; const issueLen = stripAnsi(issueStr).length; - + const maxHelpLen = cols - issueLen - 1; let helpLinePlain = helpLine; if (stripAnsi(helpLinePlain).length > maxHelpLen) { - helpLinePlain = chalk.gray(stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + '...'); + helpLinePlain = chalk.gray( + stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + "...", + ); } - + const spaces = cols - stripAnsi(helpLinePlain).length - issueLen - 1; - process.stdout.write(helpLinePlain + ' '.repeat(Math.max(0, spaces)) + issueStr); + process.stdout.write( + helpLinePlain + " ".repeat(Math.max(0, spaces)) + issueStr, + ); } private async handleAddTimeKeypress(str: string, key: any) { - if (key && key.name === 'escape') { - this.state.screen = 'details'; + if (key && key.name === "escape") { + this.state.screen = "details"; this.state.error = null; this.render(); return; } - - if (key && key.ctrl && key.name === 's') { + + if (key && key.ctrl && key.name === "s") { const input = this.state.addTimeForm.timeInput.trim(); if (!input) { - this.state.error = 'Time input cannot be empty.'; + this.state.error = "Time input cannot be empty."; this.render(); return; } - + if (!this.state.selectedIssue) { - this.state.error = 'No issue selected.'; + this.state.error = "No issue selected."; this.render(); return; } - + // Parse time input (e.g. 1h 30m, 45m, 2h) let totalSeconds = 0; const hMatch = input.match(/(\d+)\s*h/); const mMatch = input.match(/(\d+)\s*m/); - + if (hMatch) { totalSeconds += parseInt(hMatch[1], 10) * 3600; } if (mMatch) { totalSeconds += parseInt(mMatch[1], 10) * 60; } - + // If neither h nor m matched, but it's just a number, assume minutes if (!hMatch && !mMatch && /^\d+$/.test(input)) { totalSeconds += parseInt(input, 10) * 60; } else if (!hMatch && !mMatch) { - this.state.error = "Invalid format. Use '1h 30m', '45m', or just minutes like '45'."; + this.state.error = + "Invalid format. Use '1h 30m', '45m', or just minutes like '45'."; this.render(); return; } - + if (totalSeconds <= 0) { this.state.error = "Time must be greater than 0."; this.render(); return; } - + this.state.error = null; this.state.loading = true; this.render(); - + try { - await addIssueTime(this.state.config, this.state.selectedIssue.number, totalSeconds); - this.state.screen = 'details'; + await addIssueTime( + this.state.config, + this.state.selectedIssue.number, + totalSeconds, + ); + this.state.screen = "details"; this.state.loading = false; // Reload to show the time tracked this.reloadSingleIssue(); @@ -2523,18 +3065,25 @@ export class TuiEngine { } return; } - + let val = this.state.addTimeForm.timeInput; - - if (key && key.name === 'backspace') { + + if (key && key.name === "backspace") { if (val.length > 0) { this.state.addTimeForm.timeInput = val.slice(0, -1); this.render(); } return; } - - if (str && str.length === 1 && !key.ctrl && !key.meta && str !== '\n' && str !== '\r') { + + if ( + str && + str.length === 1 && + !key.ctrl && + !key.meta && + str !== "\n" && + str !== "\r" + ) { this.state.error = null; this.state.addTimeForm.timeInput += str; this.render(); @@ -2542,56 +3091,71 @@ export class TuiEngine { } private renderConfirmStateChangeScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - + const borderCh = chalk.bold.hex("#4A90E2")("│"); + const issue = this.state.selectedIssue; if (!issue) return; - const action = issue.state === 'open' ? 'Close' : 'Reopen'; + const action = issue.state === "open" ? "Close" : "Reopen"; const title = ` Confirm ${action} `; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + if (this.state.error) { - console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log( + borderCh + + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); } const question = `Are you sure you want to ${action.toLowerCase()} issue #${issue.number}?`; const promptPlain = ` ${question} [y/N] `; - + const paddingRows = Math.floor((rows - 8) / 2); - for (let i = 0; i < paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - - const contentLine = (' '.repeat(Math.max(0, Math.floor((cols - 2 - promptPlain.length) / 2))) + chalk.yellow.bold(question) + ' [y/N] ').padEnd(cols - 2 + (chalk.yellow.bold(question).length - question.length)); + for (let i = 0; i < paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + + const contentLine = ( + " ".repeat(Math.max(0, Math.floor((cols - 2 - promptPlain.length) / 2))) + + chalk.yellow.bold(question) + + " [y/N] " + ).padEnd(cols - 2 + (chalk.yellow.bold(question).length - question.length)); console.log(borderCh + contentLine + borderCh); - - for (let i = 0; i < (rows - 8) - paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - - const helpLinePlain = ' [y] Yes [n/Esc] No'; + + for (let i = 0; i < rows - 8 - paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + + const helpLinePlain = " [y] Yes [n/Esc] No"; process.stdout.write(chalk.gray(helpLinePlain)); } private async handleConfirmStateChangeKeypress(str: string, key: any) { - if ((key && (key.name === 'escape' || key.name === 'n')) || str === 'n' || str === 'N') { - this.state.screen = 'details'; + if ( + (key && (key.name === "escape" || key.name === "n")) || + str === "n" || + str === "N" + ) { + this.state.screen = "details"; this.state.error = null; this.render(); return; } - if (str === 'y' || str === 'Y') { + if (str === "y" || str === "Y") { const issue = this.state.selectedIssue; if (!issue) return; this.state.error = null; - const newState = issue.state === 'open' ? 'closed' : 'open'; - this.state.screen = newState === 'closed' ? 'animating-close' : 'animating-reopen'; + const newState = issue.state === "open" ? "closed" : "open"; + this.state.screen = + newState === "closed" ? "animating-close" : "animating-reopen"; this.animationFrame = 0; this.render(); @@ -2601,29 +3165,32 @@ export class TuiEngine { this.animationInterval = setInterval(() => { this.animationFrame++; - const frameCount = newState === 'closed' ? GRAVESTONE_FRAMES.length : ZOMBIE_FRAMES.length; + const frameCount = + newState === "closed" + ? GRAVESTONE_FRAMES.length + : ZOMBIE_FRAMES.length; this.render(); - + if (this.animationFrame >= frameCount - 1) { if (this.animationInterval) { clearInterval(this.animationInterval); this.animationInterval = null; } - + setTimeout(async () => { - this.state.loading = true; - this.render(); - try { - await changeIssueState(this.state.config, issue.number, newState); - this.state.screen = 'details'; - this.state.loading = false; - this.reloadSingleIssue(); - } catch (err: any) { - this.state.error = err.message; - this.state.screen = 'details'; - this.state.loading = false; - this.render(); - } + this.state.loading = true; + this.render(); + try { + await changeIssueState(this.state.config, issue.number, newState); + this.state.screen = "details"; + this.state.loading = false; + this.reloadSingleIssue(); + } catch (err: any) { + this.state.error = err.message; + this.state.screen = "details"; + this.state.loading = false; + this.render(); + } }, 300); } }, 300); @@ -2633,43 +3200,56 @@ export class TuiEngine { } private renderConfirmCancelCreateScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - - const title = ' Confirm Cancel '; + const borderCh = chalk.bold.hex("#4A90E2")("│"); + + const title = " Confirm Cancel "; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - - const question = 'Are you sure you want to cancel creating this issue?'; + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + + const question = "Are you sure you want to cancel creating this issue?"; const promptPlain = ` ${question} [y/N] `; - + const paddingRows = Math.floor((rows - 8) / 2); - for (let i = 0; i < paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - - const padContentLeft = Math.max(0, Math.floor((cols - 2 - promptPlain.length) / 2)); - const contentLine = (' '.repeat(padContentLeft) + chalk.yellow.bold(question) + ' [y/N] ').padEnd(cols - 2 + (chalk.yellow.bold(question).length - question.length)); + for (let i = 0; i < paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + + const padContentLeft = Math.max( + 0, + Math.floor((cols - 2 - promptPlain.length) / 2), + ); + const contentLine = ( + " ".repeat(padContentLeft) + + chalk.yellow.bold(question) + + " [y/N] " + ).padEnd(cols - 2 + (chalk.yellow.bold(question).length - question.length)); console.log(borderCh + contentLine + borderCh); - - for (let i = 0; i < (rows - 8) - paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - - const helpLinePlain = ' [y] Yes [n/Esc] No'; + + for (let i = 0; i < rows - 8 - paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + + const helpLinePlain = " [y] Yes [n/Esc] No"; process.stdout.write(chalk.gray(helpLinePlain)); } private async handleConfirmCancelCreateKeypress(str: string, key: any) { - if ((key && (key.name === 'escape' || key.name === 'n')) || str === 'n' || str === 'N') { - this.state.screen = 'create-issue'; + if ( + (key && (key.name === "escape" || key.name === "n")) || + str === "n" || + str === "N" + ) { + this.state.screen = "create-issue"; this.state.error = null; this.render(); return; } - if (str === 'y' || str === 'Y') { - this.state.screen = 'list'; + if (str === "y" || str === "Y") { + this.state.screen = "list"; this.state.error = null; this.render(); return; @@ -2677,43 +3257,56 @@ export class TuiEngine { } private renderConfirmCancelEditScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - - const title = ' Confirm Cancel '; + const borderCh = chalk.bold.hex("#4A90E2")("│"); + + const title = " Confirm Cancel "; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - - const question = 'Are you sure you want to cancel editing this issue?'; + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + + const question = "Are you sure you want to cancel editing this issue?"; const promptPlain = ` ${question} [y/N] `; - + const paddingRows = Math.floor((rows - 8) / 2); - for (let i = 0; i < paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - - const padContentLeft = Math.max(0, Math.floor((cols - 2 - promptPlain.length) / 2)); - const contentLine = (' '.repeat(padContentLeft) + chalk.yellow.bold(question) + ' [y/N] ').padEnd(cols - 2 + (chalk.yellow.bold(question).length - question.length)); + for (let i = 0; i < paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + + const padContentLeft = Math.max( + 0, + Math.floor((cols - 2 - promptPlain.length) / 2), + ); + const contentLine = ( + " ".repeat(padContentLeft) + + chalk.yellow.bold(question) + + " [y/N] " + ).padEnd(cols - 2 + (chalk.yellow.bold(question).length - question.length)); console.log(borderCh + contentLine + borderCh); - - for (let i = 0; i < (rows - 8) - paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - - const helpLinePlain = ' [y] Yes [n/Esc] No'; + + for (let i = 0; i < rows - 8 - paddingRows; i++) + console.log(borderCh + " ".repeat(cols - 2) + borderCh); + + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + + const helpLinePlain = " [y] Yes [n/Esc] No"; process.stdout.write(chalk.gray(helpLinePlain)); } private async handleConfirmCancelEditKeypress(str: string, key: any) { - if ((key && (key.name === 'escape' || key.name === 'n')) || str === 'n' || str === 'N') { - this.state.screen = 'edit-issue'; + if ( + (key && (key.name === "escape" || key.name === "n")) || + str === "n" || + str === "N" + ) { + this.state.screen = "edit-issue"; this.state.error = null; this.render(); return; } - if (str === 'y' || str === 'Y') { - this.state.screen = 'details'; + if (str === "y" || str === "Y") { + this.state.screen = "details"; this.state.error = null; this.render(); return; @@ -2721,54 +3314,71 @@ export class TuiEngine { } private renderSetAssigneesScreen(cols: number, rows: number) { - const borderCh = chalk.bold.hex('#4A90E2')('│'); - - const title = ' Set Assignees '; + const borderCh = chalk.bold.hex("#4A90E2")("│"); + + const title = " Set Assignees "; const leftLen = stripAnsi(title).length; - let leftHeader = chalk.bold.hex('#4A90E2')(title); - - console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); - console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); - + let leftHeader = chalk.bold.hex("#4A90E2")(title); + + console.log(leftHeader + " ".repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex("#4A90E2")("┌" + "─".repeat(cols - 2) + "┐")); + if (this.state.error) { - console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + console.log( + borderCh + + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); } - const label = chalk.yellow(' Assignees (comma-separated usernames): '); - console.log(borderCh + label + ' '.repeat(cols - stripAnsi(label).length - 2) + borderCh); - console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + const label = chalk.yellow(" Assignees (comma-separated usernames): "); + console.log( + borderCh + + label + + " ".repeat(cols - stripAnsi(label).length - 2) + + borderCh, + ); + console.log(chalk.bold.hex("#4A90E2")("├" + "─".repeat(cols - 2) + "┤")); const inputVal = this.state.assigneesInput; let visVal = inputVal; if (visVal.length > cols - 5) { visVal = visVal.substring(visVal.length - (cols - 5)); } - visVal = visVal + chalk.inverse(' ') + ' '.repeat(Math.max(0, cols - 5 - stripAnsi(visVal).length)); - console.log(borderCh + ' ' + visVal + ' ' + borderCh); - + visVal = + visVal + + chalk.inverse(" ") + + " ".repeat(Math.max(0, cols - 5 - stripAnsi(visVal).length)); + console.log(borderCh + " " + visVal + " " + borderCh); + const fillRows = rows - (this.state.error ? 8 : 6) - 1; for (let i = 0; i < fillRows; i++) { - console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + console.log(borderCh + " ".repeat(cols - 2) + borderCh); } - - console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); - const helpLine = chalk.gray(' [Enter] Submit [Esc] Cancel (leave blank to clear assignees)'); - const issueStr = this.state.selectedIssue ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) : ''; - const spaces = cols - stripAnsi(helpLine).length - stripAnsi(issueStr).length; - process.stdout.write(helpLine + ' '.repeat(Math.max(0, spaces)) + issueStr); + console.log(chalk.bold.hex("#4A90E2")("└" + "─".repeat(cols - 2) + "┘")); + + const helpLine = chalk.gray( + " [Enter] Submit [Esc] Cancel (leave blank to clear assignees)", + ); + const issueStr = this.state.selectedIssue + ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) + : ""; + const spaces = + cols - stripAnsi(helpLine).length - stripAnsi(issueStr).length; + process.stdout.write(helpLine + " ".repeat(Math.max(0, spaces)) + issueStr); } private async handleSetAssigneesKeypress(str: string, key: any) { - if (key && key.name === 'escape') { - this.state.screen = 'details'; + if (key && key.name === "escape") { + this.state.screen = "details"; this.state.error = null; this.render(); return; } - if (key && key.name === 'backspace') { + if (key && key.name === "backspace") { if (this.state.assigneesInput.length > 0) { this.state.assigneesInput = this.state.assigneesInput.slice(0, -1); this.render(); @@ -2776,7 +3386,7 @@ export class TuiEngine { return; } - if (key && key.name === 'return') { + if (key && key.name === "return") { const issue = this.state.selectedIssue; if (!issue) return; @@ -2785,13 +3395,13 @@ export class TuiEngine { this.render(); const assignees = this.state.assigneesInput - .split(',') - .map(s => s.trim()) - .filter(s => s.length > 0); + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); try { await setIssueAssignees(this.state.config, issue.number, assignees); - this.state.screen = 'details'; + this.state.screen = "details"; this.state.loading = false; this.reloadSingleIssue(); } catch (err: any) { @@ -2829,13 +3439,13 @@ export class TuiEngine { clearInterval(this.launchInterval); this.launchInterval = null; } - + // Transition to target destination screen - if (this.launchDestScreen === 'list') { - this.state.screen = 'list'; + if (this.launchDestScreen === "list") { + this.state.screen = "list"; this.loadIssues(); } else { - this.state.screen = 'setup'; + this.state.screen = "setup"; this.render(); } } @@ -2845,46 +3455,36 @@ export class TuiEngine { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Resolve path to the root folder where ascii-art.txt is located - const artPath = path.resolve(__dirname, '..', 'ascii-art.txt'); + const artPath = path.resolve(__dirname, "..", "ascii-art.txt"); if (fs.existsSync(artPath)) { - const rawArt = fs.readFileSync(artPath, 'utf8').split(/\r?\n/); + const rawArt = fs.readFileSync(artPath, "utf8").split(/\r?\n/); this.launchFrame = this.preprocessArt(rawArt); } else { // Fallback frames if file not found - this.launchFrame = [ - " ( ( ", - " ) ) ", - " [===] ", - " \\___/ " - ]; + this.launchFrame = [" ( ( ", " ) ) ", " [===] ", " \\___/ "]; } } catch (e) { - this.launchFrame = [ - " ( ( ", - " ) ) ", - " [===] ", - " \\___/ " - ]; + this.launchFrame = [" ( ( ", " ) ) ", " [===] ", " \\___/ "]; } } private preprocessArt(lines: string[]): string[] { let startIdx = 0; - while (startIdx < lines.length && lines[startIdx].trim() === '') { + while (startIdx < lines.length && lines[startIdx].trim() === "") { startIdx++; } let endIdx = lines.length - 1; - while (endIdx >= 0 && lines[endIdx].trim() === '') { + while (endIdx >= 0 && lines[endIdx].trim() === "") { endIdx--; } - + if (startIdx > endIdx) return []; const activeLines = lines.slice(startIdx, endIdx + 1); let minPadding = Infinity; for (const line of activeLines) { - if (line.trim() === '') continue; + if (line.trim() === "") continue; const match = line.match(/^[ \t]*/); const padding = match ? match[0].length : 0; if (padding < minPadding) { @@ -2893,7 +3493,9 @@ export class TuiEngine { } if (minPadding === Infinity || minPadding === 0) return activeLines; - return activeLines.map(line => line.length >= minPadding ? line.substring(minPadding) : ''); + return activeLines.map((line) => + line.length >= minPadding ? line.substring(minPadding) : "", + ); } private renderLaunchScreen(cols: number, rows: number) { @@ -2904,14 +3506,14 @@ export class TuiEngine { const frame = this.launchFrame; if (frame.length === 0) return; - const W = Math.max(...frame.map(line => line.length)); + const W = Math.max(...frame.map((line) => line.length)); const H = frame.length; const padLeftCount = Math.max(0, Math.floor((cols - W) / 2)); const padTopCount = Math.max(0, Math.floor((rows - H - 5) / 2)); for (let i = 0; i < padTopCount; i++) { - console.log(''); + console.log(""); } // HSL helper function to generate HSL-based hex colors @@ -2921,7 +3523,9 @@ export class TuiEngine { const f = (n: number) => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); - return Math.round(255 * color).toString(16).padStart(2, '0'); + return Math.round(255 * color) + .toString(16) + .padStart(2, "0"); }; return `#${f(0)}${f(8)}${f(4)}`; }; @@ -2930,51 +3534,62 @@ export class TuiEngine { for (let i = 0; i < frame.length; i++) { const line = frame[i]; - const leftPad = ' '.repeat(padLeftCount); - - let coloredLine = ''; + const leftPad = " ".repeat(padLeftCount); + + let coloredLine = ""; if (i < 20) { // Steam gradient: waving light intensity using sine wave based on time and row index const factor = i / 20; // 0 to 1 - + // Wave lightness of the steam const wave = Math.sin(step * 0.4 - i * 0.3); - const lightness = Math.max(10, Math.min(80, Math.round(25 + wave * 15 + factor * 25))); - + const lightness = Math.max( + 10, + Math.min(80, Math.round(25 + wave * 15 + factor * 25)), + ); + // Soft cyan/blue steam hue = 195 const hex = hslToHex(195, 30, lightness); coloredLine = chalk.hex(hex)(line); } else { // Cup body gradient: shifting rainbow wave const rowOffset = i - 20; - + // Cycle the hue over time and rows const hue = (step * 8 + rowOffset * 12) % 360; const hex = hslToHex(hue, 95, 55); coloredLine = chalk.hex(hex).bold(line); } - + console.log(leftPad + coloredLine); } - console.log(''); - const titlePlain = ' G I T E A & F O R G E J O T U I '; - const subtitlePlain = 'Interactive Issue & Pull Request Explorer'; - + console.log(""); + const titlePlain = " G I T E A & F O R G E J O T U I "; + const subtitlePlain = "Interactive Issue & Pull Request Explorer"; + // Cycle the title box background color too! const titleHue = (step * 6) % 360; const titleColor = hslToHex(titleHue, 95, 50); - const titleLeftPad = Math.max(0, Math.floor((cols - titlePlain.length) / 2)); - const subtitleLeftPad = Math.max(0, Math.floor((cols - subtitlePlain.length) / 2)); + const titleLeftPad = Math.max( + 0, + Math.floor((cols - titlePlain.length) / 2), + ); + const subtitleLeftPad = Math.max( + 0, + Math.floor((cols - subtitlePlain.length) / 2), + ); - console.log(' '.repeat(titleLeftPad) + chalk.bold.bgHex(titleColor).black(titlePlain)); - console.log(' '.repeat(subtitleLeftPad) + chalk.gray(subtitlePlain)); - console.log(''); + console.log( + " ".repeat(titleLeftPad) + chalk.bold.bgHex(titleColor).black(titlePlain), + ); + console.log(" ".repeat(subtitleLeftPad) + chalk.gray(subtitlePlain)); + console.log(""); - const skipPlain = 'Press any key to skip animation'; + const skipPlain = "Press any key to skip animation"; const skipLeftPad = Math.max(0, Math.floor((cols - skipPlain.length) / 2)); - console.log(' '.repeat(skipLeftPad) + chalk.gray.italic(skipPlain)); + console.log(" ".repeat(skipLeftPad) + chalk.gray.italic(skipPlain)); const printedRows = H + padTopCount + 5; const remainingRows = Math.max(0, rows - printedRows - 1); @@ -2998,13 +3613,18 @@ export class TuiEngine { private async handleLabelsListKeypress(str: string, key: any) { if (this.state.labelsLoading) return; - if ((key && key.name === 'escape') || str === '\u001b' || str === 'q' || str === 'Q') { - this.state.screen = 'list'; + if ( + (key && key.name === "escape") || + str === "\u001b" || + str === "q" || + str === "Q" + ) { + this.state.screen = "list"; this.render(); return; } - if (key && key.name === 'up') { + if ((key && key.name === "up") || str === "k") { if (this.state.selectedLabelIndex > 0) { this.state.selectedLabelIndex--; this.render(); @@ -3012,7 +3632,7 @@ export class TuiEngine { return; } - if (key && key.name === 'down') { + if ((key && key.name === "down") || str === "j") { if (this.state.selectedLabelIndex < this.state.labels.length - 1) { this.state.selectedLabelIndex++; this.render(); @@ -3020,36 +3640,36 @@ export class TuiEngine { return; } - if (str === 'c' || str === 'C') { - this.state.screen = 'create-label'; + if (str === "c" || str === "C") { + this.state.screen = "create-label"; this.state.labelForm = { - name: '', - color: '000000', - description: '', + name: "", + color: "000000", + description: "", exclusive: false, - activeField: 'name' + activeField: "name", }; this.render(); return; } - if (str === 'e' || str === 'E') { + if (str === "e" || str === "E") { if (this.state.labels.length > 0) { const lbl = this.state.labels[this.state.selectedLabelIndex]; - this.state.screen = 'edit-label'; + this.state.screen = "edit-label"; this.state.labelForm = { name: lbl.name, color: lbl.color, - description: lbl.description || '', + description: lbl.description || "", exclusive: lbl.exclusive || false, - activeField: 'name' + activeField: "name", }; this.render(); } return; } - if ((key && key.name === 'delete') || str === 'd' || str === 'D') { + if ((key && key.name === "delete") || str === "d" || str === "D") { if (this.state.labels.length > 0) { const lbl = this.state.labels[this.state.selectedLabelIndex]; this.state.labelsLoading = true; @@ -3068,48 +3688,54 @@ export class TuiEngine { } private async handleLabelFormKeypress(str: string, key: any) { - if ((key && key.name === 'escape') || str === '\u001b') { - this.state.screen = 'labels-list'; + if ((key && key.name === "escape") || str === "\u001b") { + this.state.screen = "labels-list"; this.render(); return; } const form = this.state.labelForm; - const fields: Array<'name' | 'color' | 'description' | 'exclusive'> = ['name', 'color', 'description', 'exclusive']; + const fields: Array<"name" | "color" | "description" | "exclusive"> = [ + "name", + "color", + "description", + "exclusive", + ]; const currentIdx = fields.indexOf(form.activeField); - if (key && key.name === 'up') { - form.activeField = fields[(currentIdx - 1 + fields.length) % fields.length]; + if (key && key.name === "up") { + form.activeField = + fields[(currentIdx - 1 + fields.length) % fields.length]; this.render(); return; } - if (key && key.name === 'down') { + if (key && key.name === "down") { form.activeField = fields[(currentIdx + 1) % fields.length]; this.render(); return; } - if (key && key.name === 'tab') { + if (key && key.name === "tab") { form.activeField = fields[(currentIdx + 1) % fields.length]; this.render(); return; } - if (str === ' ' && form.activeField === 'exclusive') { + if (str === " " && form.activeField === "exclusive") { form.exclusive = !form.exclusive; this.render(); return; } - if ((key && key.name === 'return') || str === '\r' || str === '\n') { - if (form.activeField === 'exclusive') { + if ((key && key.name === "return") || str === "\r" || str === "\n") { + if (form.activeField === "exclusive") { form.exclusive = !form.exclusive; this.render(); return; } if (!form.name.trim() || !form.color.trim()) { - this.state.error = 'Name and Color are required.'; + this.state.error = "Name and Color are required."; this.render(); return; } @@ -3118,12 +3744,12 @@ export class TuiEngine { this.render(); try { - if (this.state.screen === 'create-label') { + if (this.state.screen === "create-label") { await createLabel(this.state.config, { name: form.name.trim(), color: form.color.trim(), description: form.description.trim(), - exclusive: form.exclusive + exclusive: form.exclusive, }); } else { const lbl = this.state.labels[this.state.selectedLabelIndex]; @@ -3131,10 +3757,10 @@ export class TuiEngine { name: form.name.trim(), color: form.color.trim(), description: form.description.trim(), - exclusive: form.exclusive + exclusive: form.exclusive, }); } - this.state.screen = 'labels-list'; + this.state.screen = "labels-list"; await this.loadLabels(); } catch (err: any) { this.state.error = err.message; @@ -3144,32 +3770,46 @@ export class TuiEngine { return; } - if (key && key.name === 'backspace') { - if (form.activeField === 'name') form.name = form.name.slice(0, -1); - if (form.activeField === 'color') form.color = form.color.slice(0, -1); - if (form.activeField === 'description') form.description = form.description.slice(0, -1); + if (key && key.name === "backspace") { + if (form.activeField === "name") form.name = form.name.slice(0, -1); + if (form.activeField === "color") form.color = form.color.slice(0, -1); + if (form.activeField === "description") + form.description = form.description.slice(0, -1); this.render(); return; } - if (str && !key.ctrl && !key.meta && str.length === 1 && str.charCodeAt(0) >= 32) { - if (form.activeField === 'name') form.name += str; - if (form.activeField === 'color') form.color += str; - if (form.activeField === 'description') form.description += str; + if ( + str && + !key.ctrl && + !key.meta && + str.length === 1 && + str.charCodeAt(0) >= 32 + ) { + if (form.activeField === "name") form.name += str; + if (form.activeField === "color") form.color += str; + if (form.activeField === "description") form.description += str; this.render(); } } private renderLabelsList(cols: number, rows: number) { - const header = chalk.bgBlue.white.bold(` Repository Labels - ${this.state.config.owner}/${this.state.config.repo} `); - const headerPad = Math.max(0, Math.floor((cols - stripAnsi(header).length) / 2)); - console.log(' '.repeat(headerPad) + header); - console.log(chalk.gray('─'.repeat(cols))); + const header = chalk.bgBlue.white.bold( + ` Repository Labels - ${this.state.config.owner}/${this.state.config.repo} `, + ); + const headerPad = Math.max( + 0, + Math.floor((cols - stripAnsi(header).length) / 2), + ); + console.log(" ".repeat(headerPad) + header); + console.log(chalk.gray("─".repeat(cols))); if (this.state.labelsLoading) { - console.log(chalk.yellow(` Loading labels... ${SPINNER_FRAMES[spinnerIndex]}`)); + console.log( + chalk.yellow(` Loading labels... ${SPINNER_FRAMES[spinnerIndex]}`), + ); } else if (this.state.labels.length === 0) { - console.log(chalk.gray(' No labels found.')); + console.log(chalk.gray(" No labels found.")); } else { const listHeight = rows - 6; // header(2), footer(2), errors etc. let startIdx = 0; @@ -3180,21 +3820,25 @@ export class TuiEngine { for (let i = 0; i < listHeight; i++) { const itemIdx = startIdx + i; if (itemIdx >= this.state.labels.length) break; - + const lbl = this.state.labels[itemIdx]; const isSelected = itemIdx === this.state.selectedLabelIndex; - - let prefix = isSelected ? chalk.green(' > ') : ' '; - let lblDisplay = chalk.bgHex('#' + lbl.color).black(` ${lbl.name} `); - if (lbl.exclusive) lblDisplay += ' (exclusive)'; - + + let prefix = isSelected ? chalk.green(" > ") : " "; + let lblDisplay = chalk.bgHex("#" + lbl.color).black(` ${lbl.name} `); + if (lbl.exclusive) lblDisplay += " (exclusive)"; + let line = prefix + lblDisplay; if (lbl.description) { - line += chalk.gray(` - ${truncate(lbl.description, cols - 30)}`); + line += chalk.gray(` - ${truncate(lbl.description, cols - 30)}`); } - + if (isSelected) { - console.log(chalk.bgGray(line + ' '.repeat(Math.max(0, cols - stripAnsi(line).length)))); + console.log( + chalk.bgGray( + line + " ".repeat(Math.max(0, cols - stripAnsi(line).length)), + ), + ); } else { console.log(line); } @@ -3202,150 +3846,198 @@ export class TuiEngine { } // Fill remaining space - const currentLine = this.state.labelsLoading ? 3 : Math.min(this.state.labels.length, rows - 6) + 2; - for (let i = currentLine; i < rows - 3; i++) console.log(''); + const currentLine = this.state.labelsLoading + ? 3 + : Math.min(this.state.labels.length, rows - 6) + 2; + for (let i = currentLine; i < rows - 3; i++) console.log(""); if (this.state.error) { console.log(chalk.red(` Error: ${this.state.error}`)); } else { - console.log(''); + console.log(""); } - console.log(chalk.gray('─'.repeat(cols))); - console.log(chalk.gray(' [c] Create [e] Edit [d] Delete [esc] Back to Issues')); + console.log(chalk.gray("─".repeat(cols))); + console.log( + chalk.gray(" [c] Create [e] Edit [d] Delete [esc] Back to Issues"), + ); } private renderLabelForm(cols: number, rows: number) { - const isEdit = this.state.screen === 'edit-label'; - const title = isEdit ? 'Edit Label' : 'Create Label'; + const isEdit = this.state.screen === "edit-label"; + const title = isEdit ? "Edit Label" : "Create Label"; const header = chalk.bgBlue.white.bold(` ${title} `); - const headerPad = Math.max(0, Math.floor((cols - stripAnsi(header).length) / 2)); - console.log(' '.repeat(headerPad) + header); - console.log(chalk.gray('─'.repeat(cols))); + const headerPad = Math.max( + 0, + Math.floor((cols - stripAnsi(header).length) / 2), + ); + console.log(" ".repeat(headerPad) + header); + console.log(chalk.gray("─".repeat(cols))); const form = this.state.labelForm; const width = Math.min(60, cols - 4); - const leftPadStr = ' '.repeat(Math.max(0, Math.floor((cols - width) / 2))); + const leftPadStr = " ".repeat(Math.max(0, Math.floor((cols - width) / 2))); + + console.log(""); - console.log(''); - // Name - const nameLabel = form.activeField === 'name' ? chalk.cyan.bold('> Name: ') : ' Name: '; - console.log(leftPadStr + nameLabel + (form.name || chalk.gray('(empty)'))); - console.log(''); + const nameLabel = + form.activeField === "name" ? chalk.cyan.bold("> Name: ") : " Name: "; + console.log(leftPadStr + nameLabel + (form.name || chalk.gray("(empty)"))); + console.log(""); // Color - const colorLabel = form.activeField === 'color' ? chalk.cyan.bold('> Color (hex without #): ') : ' Color: '; - console.log(leftPadStr + colorLabel + (form.color || chalk.gray('e.g. ff0000'))); + const colorLabel = + form.activeField === "color" + ? chalk.cyan.bold("> Color (hex without #): ") + : " Color: "; + console.log( + leftPadStr + colorLabel + (form.color || chalk.gray("e.g. ff0000")), + ); if (form.color && form.color.length >= 3) { try { - console.log(leftPadStr + ' Preview: ' + chalk.bgHex('#' + form.color).black(` ${form.name || 'Label'} `)); + console.log( + leftPadStr + + " Preview: " + + chalk.bgHex("#" + form.color).black(` ${form.name || "Label"} `), + ); } catch (e) { - console.log(leftPadStr + ' Preview: ' + chalk.red('Invalid color')); + console.log(leftPadStr + " Preview: " + chalk.red("Invalid color")); } } - console.log(''); + console.log(""); // Description - const descLabel = form.activeField === 'description' ? chalk.cyan.bold('> Description: ') : ' Description: '; - console.log(leftPadStr + descLabel + (form.description || chalk.gray('(empty)'))); - console.log(''); + const descLabel = + form.activeField === "description" + ? chalk.cyan.bold("> Description: ") + : " Description: "; + console.log( + leftPadStr + descLabel + (form.description || chalk.gray("(empty)")), + ); + console.log(""); // Exclusive - const exclLabel = form.activeField === 'exclusive' ? chalk.cyan.bold('> Exclusive: ') : ' Exclusive: '; - console.log(leftPadStr + exclLabel + (form.exclusive ? chalk.green('[x]') : '[ ]')); - console.log(''); + const exclLabel = + form.activeField === "exclusive" + ? chalk.cyan.bold("> Exclusive: ") + : " Exclusive: "; + console.log( + leftPadStr + exclLabel + (form.exclusive ? chalk.green("[x]") : "[ ]"), + ); + console.log(""); - for (let i = 13; i < rows - 3; i++) console.log(''); + for (let i = 13; i < rows - 3; i++) console.log(""); if (this.state.error) { console.log(leftPadStr + chalk.red(` Error: ${this.state.error}`)); } else if (this.state.labelsLoading) { - console.log(leftPadStr + chalk.yellow(` Saving... ${SPINNER_FRAMES[spinnerIndex]}`)); + console.log( + leftPadStr + chalk.yellow(` Saving... ${SPINNER_FRAMES[spinnerIndex]}`), + ); } else { - console.log(''); + console.log(""); } - console.log(chalk.gray('─'.repeat(cols))); - console.log(chalk.gray(' [↑/↓/Tab] Navigate [Enter] Save [Esc] Cancel')); + console.log(chalk.gray("─".repeat(cols))); + console.log(chalk.gray(" [↑/↓/Tab] Navigate [Enter] Save [Esc] Cancel")); } private renderHelpScreen(cols: number, rows: number) { - const isDetails = this.state.previousScreen === 'details'; - + const isDetails = this.state.previousScreen === "details"; + // Command lists: const commands: Array<{ key: string; desc: string }> = isDetails ? [ - { key: 'Esc / Backspace', desc: 'Back to Issues List' }, - { key: 'C', desc: 'Add Comment to Issue' }, - { key: 'A', desc: 'Assign User(s)' }, - { key: 'E', desc: 'Edit Title & Description' }, - { key: 'T', desc: 'Track Time (add time)' }, - { key: 'X', desc: 'Close / Reopen Issue' }, - { key: 'R', desc: 'Reload Single Issue' }, - { key: 'O', desc: 'Global Setup / Connection Settings' }, - { key: '↑ / ↓', desc: 'Scroll description & comments' }, - { key: 'PgUp / PgDn', desc: 'Scroll faster (10 lines)' }, - { key: '?', desc: 'Show / Close Help Menu' } + { key: "Esc / Backspace", desc: "Back to Issues List" }, + { key: "C", desc: "Add Comment to Issue" }, + { key: "A", desc: "Assign User(s)" }, + { key: "E", desc: "Edit Title & Description" }, + { key: "T", desc: "Track Time (add time)" }, + { key: "X", desc: "Close / Reopen Issue" }, + { key: "R", desc: "Reload Single Issue" }, + { key: "O", desc: "Global Setup / Connection Settings" }, + { key: "↑ / ↓", desc: "Scroll description & comments" }, + { key: "PgUp / PgDn", desc: "Scroll faster (10 lines)" }, + { key: "?", desc: "Show / Close Help Menu" }, ] : [ - { key: '↑ / ↓', desc: 'Navigate issues list' }, - { key: 'Enter', desc: 'View selected issue details' }, - { key: 'C', desc: 'Create new issue' }, - { key: '/', desc: 'Focus Search input field' }, - { key: 'S', desc: 'Cycle Sort fields and order' }, - { key: 'F', desc: 'Cycle State filter (open/closed/all)' }, - { key: 'T', desc: 'Cycle Type filter (issues/pulls/all)' }, - { key: 'L', desc: 'View and manage Labels list' }, - { key: 'N / PageDown', desc: 'Next page of issues' }, - { key: 'P / PageUp', desc: 'Previous page of issues' }, - { key: 'R', desc: 'Reload issues list' }, - { key: 'Tab', desc: 'Toggle focus (Issues List <=> Settings)' }, - { key: 'O', desc: 'Global Setup / Connection Settings' }, - { key: 'Q / Esc', desc: 'Quit application' }, - { key: '?', desc: 'Show / Close Help Menu' } + { key: "↑ / ↓ / j/k", desc: "Navigate issues list" }, + { key: "Enter", desc: "View selected issue details" }, + { key: "C", desc: "Create new issue" }, + { key: "/", desc: "Focus Search input field" }, + { key: "S", desc: "Cycle Sort fields and order" }, + { key: "F", desc: "Cycle State filter (open/closed/all)" }, + { key: "T", desc: "Cycle Type filter (issues/pulls/all)" }, + { key: "L", desc: "View and manage Labels list" }, + { key: "N / PageDown", desc: "Next page of issues" }, + { key: "P / PageUp", desc: "Previous page of issues" }, + { key: "R", desc: "Reload issues list" }, + { key: "Tab", desc: "Toggle focus (Issues List <=> Settings)" }, + { key: "O", desc: "Global Setup / Connection Settings" }, + { key: "Q / Esc", desc: "Quit application" }, + { key: "?", desc: "Show / Close Help Menu" }, ]; - const title = ` KEYBOARD COMMANDS HELP (${isDetails ? 'DETAILS VIEW' : 'LIST VIEW'}) `; + const title = ` KEYBOARD COMMANDS HELP (${isDetails ? "DETAILS VIEW" : "LIST VIEW"}) `; const width = Math.min(74, cols - 4); const height = commands.length + 6; // list + borders + title + instructions - - const leftPadStr = ' '.repeat(Math.max(0, Math.floor((cols - width) / 2))); - + + const leftPadStr = " ".repeat(Math.max(0, Math.floor((cols - width) / 2))); + // Top border - console.log('\n'); // blank line - const borderCh = chalk.bold.hex('#4A90E2'); - - const titlePadding = '─'.repeat(Math.max(0, Math.floor((width - 2 - title.length) / 2))); - const titleLine = borderCh('┌' + titlePadding + title + titlePadding) + borderCh('─'.repeat(Math.max(0, width - 2 - title.length - 2 * titlePadding.length)) + '┐'); + console.log("\n"); // blank line + const borderCh = chalk.bold.hex("#4A90E2"); + + const titlePadding = "─".repeat( + Math.max(0, Math.floor((width - 2 - title.length) / 2)), + ); + const titleLine = + borderCh("┌" + titlePadding + title + titlePadding) + + borderCh( + "─".repeat( + Math.max(0, width - 2 - title.length - 2 * titlePadding.length), + ) + "┐", + ); console.log(leftPadStr + titleLine); - + // Render lines for (const cmd of commands) { const keyStr = chalk.bold.cyan(cmd.key.padStart(22)); const descStr = chalk.white(cmd.desc); const row = ` ${keyStr} ─ ${descStr}`; const rowPlainLen = stripAnsi(row).length; - const rowPadding = ' '.repeat(Math.max(0, width - 2 - rowPlainLen)); - console.log(leftPadStr + borderCh('│') + row + rowPadding + borderCh('│')); + const rowPadding = " ".repeat(Math.max(0, width - 2 - rowPlainLen)); + console.log( + leftPadStr + borderCh("│") + row + rowPadding + borderCh("│"), + ); } - + // Separator line - console.log(leftPadStr + borderCh('├' + '─'.repeat(width - 2) + '┤')); - + console.log(leftPadStr + borderCh("├" + "─".repeat(width - 2) + "┤")); + // Instructions line - const instructions = 'Press [Esc], [q], or [?] to close'; - const instrPadding = ' '.repeat(Math.max(0, Math.floor((width - 2 - instructions.length) / 2))); - const instrLine = borderCh('│') + instrPadding + chalk.yellow.bold(instructions) + ' '.repeat(Math.max(0, width - 2 - instructions.length - instrPadding.length)) + borderCh('│'); + const instructions = "Press [Esc], [q], or [?] to close"; + const instrPadding = " ".repeat( + Math.max(0, Math.floor((width - 2 - instructions.length) / 2)), + ); + const instrLine = + borderCh("│") + + instrPadding + + chalk.yellow.bold(instructions) + + " ".repeat( + Math.max(0, width - 2 - instructions.length - instrPadding.length), + ) + + borderCh("│"); console.log(leftPadStr + instrLine); - + // Bottom border - console.log(leftPadStr + borderCh('└' + '─'.repeat(width - 2) + '┘')); - + console.log(leftPadStr + borderCh("└" + "─".repeat(width - 2) + "┘")); + // Fill remaining rows const filledRows = 1 + 1 + height + 1; // offset + top border + height + newlines for (let i = filledRows; i < rows; i++) { - console.log(''); + console.log(""); } } }