diff --git a/src/config.ts b/src/config.ts index b1fcb2c..86b36a9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,6 +9,7 @@ export interface SavedConfig { apiKey?: string; repo?: string; repository?: string; + autoRefreshInterval?: number; } export const LOCAL_CONFIG_PATH = path.join(process.cwd(), 'fjtui.json'); @@ -18,7 +19,7 @@ export const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'fjtui', 'f * Loads configuration from either local or global fjtui.json. * Local configuration overrides global configuration. */ -export function loadSavedConfig(): { url: string; userid: string; token: string; repo: string } { +export function loadSavedConfig(): { url: string; userid: string; token: string; repo: string; autoRefreshInterval: number } { let localConfig: SavedConfig = {}; let globalConfig: SavedConfig = {}; @@ -48,13 +49,14 @@ export function loadSavedConfig(): { url: string; userid: string; token: string; userid: merged.userid || '', token: merged.token || merged.apiKey || '', repo: merged.repo || merged.repository || '', + autoRefreshInterval: merged.autoRefreshInterval || 0, }; } /** * Saves settings to the global configuration file. */ -export function saveGlobalConfig(config: { url: string; userid: string; token: string | null; repo: string }) { +export function saveGlobalConfig(config: { url: string; userid: string; token: string | null; repo: string; autoRefreshInterval?: number }) { try { const dir = path.dirname(GLOBAL_CONFIG_PATH); if (!fs.existsSync(dir)) { @@ -64,7 +66,8 @@ export function saveGlobalConfig(config: { url: string; userid: string; token: s url: config.url, userid: config.userid, token: config.token || '', - repo: config.repo + repo: config.repo, + autoRefreshInterval: config.autoRefreshInterval || 0 }, null, 2); fs.writeFileSync(GLOBAL_CONFIG_PATH, data, 'utf-8'); } catch (e) { diff --git a/src/index.ts b/src/index.ts index eb00ec0..eb4fd12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,12 +37,14 @@ async function bootstrap() { previousScreen: 'list', focusedPane: 'list', selectedSettingIndex: 0, + autoRefreshInterval: savedConfig.autoRefreshInterval || 0, config: { url: '', token: null, userid: '', owner: '', repo: '', + autoRefreshInterval: savedConfig.autoRefreshInterval || 0, }, issues: [], currentPage: 1, @@ -128,6 +130,7 @@ async function bootstrap() { userid: config.userid, token: config.token, repo: repo, + autoRefreshInterval: state.autoRefreshInterval, }); console.log(chalk.green('Settings saved successfully to global config file!')); } diff --git a/src/tui.ts b/src/tui.ts index 46e8ae2..d028563 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -199,6 +199,42 @@ export class TuiEngine { private launchFrameIndex = 0; private launchInterval: NodeJS.Timeout | null = null; private maxLaunchFrames = 60; + private autoRefreshTimer: NodeJS.Timeout | null = null; + + private restartAutoRefresh() { + if (this.autoRefreshTimer) { + 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'; + + if (isTextInputActive) return; + + // Background refresh + if (this.state.screen === 'list') { + this.loadIssues(); + } else if (this.state.screen === 'details' && this.state.selectedIssue) { + this.reloadSingleIssue(); + } + }, this.state.autoRefreshInterval * 1000); + } + } constructor(initialState: AppState) { this.state = initialState; @@ -232,6 +268,7 @@ export class TuiEngine { this.launchDestScreen = 'setup'; } + this.restartAutoRefresh(); this.state.screen = 'launch'; this.startLaunchAnimation(); } @@ -249,6 +286,10 @@ export class TuiEngine { clearInterval(this.launchInterval); this.launchInterval = null; } + if (this.autoRefreshTimer) { + clearInterval(this.autoRefreshTimer); + this.autoRefreshTimer = null; + } // Exit alternate screen buffer and show standard cursor process.stdout.write('\x1B[?1049l\x1B[?25h'); if (process.stdin.isTTY) { @@ -676,12 +717,12 @@ export class TuiEngine { if (this.state.focusedPane === 'settings') { if (key && key.name === 'left') { - this.state.selectedSettingIndex = (this.state.selectedSettingIndex - 1 + 4) % 4; + this.state.selectedSettingIndex = (this.state.selectedSettingIndex - 1 + 5) % 5; this.render(); return; } if (key && key.name === 'right') { - this.state.selectedSettingIndex = (this.state.selectedSettingIndex + 1) % 4; + this.state.selectedSettingIndex = (this.state.selectedSettingIndex + 1) % 5; this.render(); return; } @@ -727,6 +768,17 @@ export class TuiEngine { this.state.currentPage = 1; this.state.selectedIssueIndex = 0; this.loadIssues(); + } 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; + // Restart timer + this.restartAutoRefresh(); + // Save to config globally + saveGlobalConfig(this.state.config); + this.render(); } return; } @@ -1385,8 +1437,18 @@ export class TuiEngine { const seg2 = buildSegment(2, 'Type', typePlain, typeLabel); const seg3 = buildSegment(3, 'Sort', sortPlain, sortLabel); + let refreshStr = 'Off'; + if (this.state.autoRefreshInterval) { + if (this.state.autoRefreshInterval >= 60) { + refreshStr = `${this.state.autoRefreshInterval / 60}m`; + } else { + refreshStr = `${this.state.autoRefreshInterval}s`; + } + } + const seg4 = buildSegment(4, 'Refresh', refreshStr, chalk.bold(refreshStr)); + const separator = chalk.bold.hex('#4A90E2')(' ─ '); - const filtersText = ` ${seg0}${separator}${seg1}${separator}${seg2}${separator}${seg3}`; + const filtersText = ` ${seg0}${separator}${seg1}${separator}${seg2}${separator}${seg3}${separator}${seg4}`; const filtersPlainLen = stripAnsi(filtersText).length; const padding = Math.max(0, cols - 2 - filtersPlainLen); diff --git a/src/types.ts b/src/types.ts index 0331fbd..3f31a70 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ export interface Config { userid: string; owner: string; repo: string; + autoRefreshInterval?: number; } export interface User { @@ -95,6 +96,7 @@ export interface AppState { screen: ScreenType; previousScreen?: ScreenType; config: Config; + autoRefreshInterval: number; focusedPane: 'list' | 'settings'; selectedSettingIndex: number;