From 8c7858bf4622b08c5285af1ca4b954a414019db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sun, 23 Aug 2026 17:32:26 +0200 Subject: [PATCH] 04: Add live terminal resize handling with SIGWINCH - Change const termHeight to let termHeight for runtime updates - Add let termWidth for runtime updates - Install SIGWINCH signal handler before event loop - Handler updates dimensions and triggers redraw on terminal resize - Create local exit() helper function for proper cleanup - Call process.off(SIGWINCH) in all exit paths - Ensure raw mode is disabled and terminal is cleared on exit - User can now resize window during interactive session and see immediate redraw Fixes: #144 --- src/tui.ts | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/tui.ts b/src/tui.ts index f22d77f..ef72740 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -130,7 +130,8 @@ export async function runInteractive( let cursor = 0; let scrollOffset = 0; - const termHeight = process.stdout.rows ?? 40; + let termHeight = process.stdout.rows ?? 40; + let termWidth = process.stdout.columns ?? 120; // HEADER_LINES (4) + position indicator (2) = 6 fixed lines consumed by renderGroups. // filterBarLines (0–2) and the sticky repo line (0–1) are added dynamically below. // Use getViewportHeight() for scroll decisions so they match what renderGroups actually renders. @@ -258,6 +259,27 @@ export async function runInteractive( redraw(); + // ─── Exit handler for cleanup ──────────────────────────────────────────── + const exit = () => { + process.stdout.write(ANSI_CLEAR); + process.stdin.setRawMode(false); + process.off("SIGWINCH", onResize); + process.exit(0); + }; + + // ─── Live terminal resize handler ──────────────────────────────────────── + const onResize = () => { + const newHeight = process.stdout.rows ?? 40; + const newWidth = process.stdout.columns ?? 120; + if (newHeight !== termHeight || newWidth !== termWidth) { + termHeight = newHeight; + termWidth = newWidth; + redraw(); + } + }; + + process.on("SIGWINCH", onResize); + for await (const chunk of process.stdin) { const key = chunk.toString(); @@ -271,9 +293,7 @@ export async function runInteractive( // Feat: team pick mode — resolve multi-team section ownership — see issue #85 if (teamPickMode.active) { if (key === KEY_CTRL_C) { - process.stdout.write(ANSI_CLEAR); - process.stdin.setRawMode(false); - process.exit(0); + exit(); } else if (key === ANSI_ARROW_LEFT) { // ← — cycle candidate teams backwards teamPickMode = { @@ -312,9 +332,7 @@ export async function runInteractive( // Feat: re-pick mode — re-assign a picked (◈) repo to a different team — see issue #87 if (repickMode.active) { if (key === KEY_CTRL_C) { - process.stdout.write(ANSI_CLEAR); - process.stdin.setRawMode(false); - process.exit(0); + exit(); } else if (key === ANSI_ARROW_LEFT) { // ← — cycle candidate teams backwards repickMode = { @@ -366,9 +384,7 @@ export async function runInteractive( // ── Filter input mode ──────────────────────────────────────────────────── if (filterMode) { if (key === KEY_CTRL_C) { - process.stdout.write(ANSI_CLEAR); - process.stdin.setRawMode(false); - process.exit(0); + exit(); } else if (key === "\x1b" && !key.startsWith("\x1b[") && !key.startsWith("\x1b\x1b")) { // ESC (bare) — cancel filter input filterMode = false; @@ -481,9 +497,7 @@ export async function runInteractive( const row = rows[cursor]; if (key === KEY_CTRL_C || key === "q") { - process.stdout.write(ANSI_CLEAR); - process.stdin.setRawMode(false); - process.exit(0); + exit(); } if (key === KEY_ENTER_CR || key === KEY_ENTER_LF) { @@ -495,6 +509,7 @@ export async function runInteractive( } process.stdout.write(ANSI_CLEAR); process.stdin.setRawMode(false); + process.off("SIGWINCH", onResize); console.log( buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType, { includeArchived,