Skip to content

feat: add suspendTerminal() to hand the terminal to a child process#972

Merged
sindresorhus merged 1 commit into
vadimdemedes:masterfrom
costajohnt:feat/suspend-terminal
Jun 16, 2026
Merged

feat: add suspendTerminal() to hand the terminal to a child process#972
sindresorhus merged 1 commit into
vadimdemedes:masterfrom
costajohnt:feat/suspend-terminal

Conversation

@costajohnt

Copy link
Copy Markdown
Contributor

Fixes #956.

Adds suspendTerminal() on useApp() so an Ink app can temporarily hand the terminal to a child process (e.g. $EDITOR, less, fzf) and then restore its own terminal state with a full redraw. This is the scoped terminal handoff discussed in the issue, rather than a stdout-only suspend.

API

Callback form (Ink restores the terminal even if the callback throws):

const {suspendTerminal} = useApp();

await suspendTerminal(async () => {
	await runEditor();
});

Disposable form, including await using:

await using suspension = await suspendTerminal();
await runEditor();

Behavior

On suspend, Ink flushes pending output, stops consuming input, and restores the terminal modes the child expects (raw mode off, cursor visible, bracketed paste off, alternate screen exited, kitty keyboard protocol off). On resume it reapplies its own terminal state and forces a full redraw rather than diffing against the stale pre-suspension frame. resume() is async because restoring ownership may need to wait for ordered writes and a redraw. Suspending while already suspended throws, and in non-interactive output the callback still runs but no terminal handoff happens.

Tests and docs

test/suspend-terminal.tsx covers the callback, disposable, and await using forms, throw-restores, the already-suspended error, alternate-screen exit and re-enter, the begin-suspend rollback path, keeping Ink off the terminal while suspended, and a PTY integration test that hands the terminal to a real child process and asserts the redraw on resume. There is a runnable example under examples/suspend-terminal/, and the README documents the API under useApp().

Adds useApp().suspendTerminal() so an Ink app can temporarily release the
terminal to a child process (e.g. $EDITOR, less, fzf) and then restore its
own terminal state with a full redraw.

- Callback form restores the terminal even if the callback throws.
- Disposable form (resume() and Symbol.asyncDispose) for `await using`.
- Releases raw mode, bracketed paste, the cursor, the alternate screen, and
  the kitty keyboard protocol, then reapplies them on resume.
- Suspending while already suspended throws; non-interactive output runs the
  callback without a terminal handoff.

Closes vadimdemedes#956
@costajohnt costajohnt force-pushed the feat/suspend-terminal branch from e365a71 to 0f3859f Compare June 16, 2026 16:38
@sindresorhus sindresorhus merged commit 9e8ed1f into vadimdemedes:master Jun 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: suspendOutput() / resumeOutput() on useStdout()

2 participants