diff --git a/src/cli.ts b/src/cli.ts index cc2fe9f..45e06b7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,14 +1,22 @@ #!/usr/bin/env node -import { exitAlternateScreenBuffer } from "./utils/screen.js"; +import { + exitAlternateScreenBuffer, + isInAlternateScreenBuffer, +} from "./utils/screen.js"; import { processUtils } from "./utils/processUtils.js"; import { createProgram } from "./utils/commands.js"; import { getApiKeyErrorMessage, checkBaseDomain } from "./utils/config.js"; // Global Ctrl+C handler to ensure it always exits processUtils.on("SIGINT", () => { - // Force exit immediately, clearing alternate screen buffer - exitAlternateScreenBuffer(); + // Only restore the alternate screen buffer if we actually entered it. + // Unconditionally sending the exit sequence when no TUI is active causes + // the terminal to restore a stale saved-cursor position, jumping the + // cursor and garbling any plain-text output (e.g. `rli d ssh` wait loop). + if (isInAlternateScreenBuffer()) { + exitAlternateScreenBuffer(); + } processUtils.stdout.write("\n"); processUtils.exit(130); // Standard exit code for SIGINT }); diff --git a/src/commands/benchmark-job/watch.ts b/src/commands/benchmark-job/watch.ts index 307f043..6de7939 100644 --- a/src/commands/benchmark-job/watch.ts +++ b/src/commands/benchmark-job/watch.ts @@ -9,6 +9,13 @@ import { type BenchmarkJob, } from "../../services/benchmarkJobService.js"; import { outputError } from "../../utils/output.js"; +import { + enterAlternateScreenBuffer, + exitAlternateScreenBuffer, + hideCursor, + showCursor, + clearScreen, +} from "../../utils/screen.js"; import { isJobCompleted, fetchAllRunsProgress, @@ -25,28 +32,23 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -// Terminal control utilities for full-screen mode +// Terminal control utilities for in-place rendering const ANSI = { - hideCursor: "\x1b[?25l", - showCursor: "\x1b[?25h", - clearScreen: "\x1b[2J", moveTo: (row: number, col: number) => `\x1b[${row};${col}H`, clearLine: "\x1b[2K", - enterAltScreen: "\x1b[?1049h", - exitAltScreen: "\x1b[?1049l", }; // Enter full-screen mode function enterFullScreen(): void { - process.stdout.write(ANSI.enterAltScreen); - process.stdout.write(ANSI.hideCursor); - process.stdout.write(ANSI.clearScreen); + enterAlternateScreenBuffer(); + hideCursor(); + clearScreen(); } // Exit full-screen mode function exitFullScreen(): void { - process.stdout.write(ANSI.showCursor); - process.stdout.write(ANSI.exitAltScreen); + showCursor(); + exitAlternateScreenBuffer(); } // Track how many lines the last render wrote so we can clear stale lines diff --git a/src/utils/screen.ts b/src/utils/screen.ts index e89a93d..18cae86 100644 --- a/src/utils/screen.ts +++ b/src/utils/screen.ts @@ -9,12 +9,20 @@ import { processUtils } from "./processUtils.js"; +let _inAlternateScreenBuffer = false; + +/** Returns true if the alternate screen buffer is currently active. */ +export function isInAlternateScreenBuffer(): boolean { + return _inAlternateScreenBuffer; +} + /** * Enter the alternate screen buffer. * This provides a fullscreen experience where content won't mix with * previous terminal output. Like vim or top. */ export function enterAlternateScreenBuffer(): void { + _inAlternateScreenBuffer = true; processUtils.stdout.write("\x1b[?1049h"); } @@ -23,6 +31,7 @@ export function enterAlternateScreenBuffer(): void { * This returns the terminal to its original state before enterAlternateScreen() was called. */ export function exitAlternateScreenBuffer(): void { + _inAlternateScreenBuffer = false; processUtils.stdout.write("\x1b[?1049l"); } diff --git a/tests/__tests__/utils/screen.test.ts b/tests/__tests__/utils/screen.test.ts new file mode 100644 index 0000000..7749f64 --- /dev/null +++ b/tests/__tests__/utils/screen.test.ts @@ -0,0 +1,37 @@ +/** + * Tests for terminal screen buffer utilities. + */ + +import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; +import { processUtils } from "../../../src/utils/processUtils.js"; +import { + isInAlternateScreenBuffer, + enterAlternateScreenBuffer, + exitAlternateScreenBuffer, +} from "../../../src/utils/screen.js"; + +describe("alternate screen buffer tracking", () => { + let originalWrite: typeof processUtils.stdout.write; + + beforeEach(() => { + // Swallow the escape sequences so they don't leak into test output. + originalWrite = processUtils.stdout.write; + processUtils.stdout.write = () => true; + }); + + afterEach(() => { + // Leave the buffer flag in a clean state for other tests. + exitAlternateScreenBuffer(); + processUtils.stdout.write = originalWrite; + }); + + it("tracks enter and exit transitions", () => { + expect(isInAlternateScreenBuffer()).toBe(false); + + enterAlternateScreenBuffer(); + expect(isInAlternateScreenBuffer()).toBe(true); + + exitAlternateScreenBuffer(); + expect(isInAlternateScreenBuffer()).toBe(false); + }); +});