Skip to content

app: suspend / resume — hand the terminal to $EDITOR or a pager, and handle Ctrl+Z #29

Description

@mellonis

Need

An app sometimes has to hand the terminal to another program: open $VISUAL / $EDITOR on a file, run a pager, shell out to git commit. A terminal editor needs the real terminal — the normal screen, cooked mode, no mouse reporting, no bracketed paste, no modifyOtherKeys — and the app needs all of it back afterwards, with a full redraw. The same sequence is what Ctrl+Z (SIGTSTP / SIGCONT) requires.

Today there is no way to do this short of unmount() + a fresh render(), which loses all component state. Ctrl+Z is not handled either: in raw mode it arrives as a key and nothing suspends.

Proposal

  1. Backend capability (optional, feature-detected like the others): suspend(): void / resume(): void on TtyBackend and InlineTtyBackend.
    • suspend: turn off mouse reporting, bracketed paste and modifyOtherKeys, show the cursor, leave the alt screen (inline: clear the live region), leave raw mode, pause input decoding and detach the stdin listener so the child's keystrokes never reach useInput handlers.
    • resume: the reverse, then invalidate the frame-diff baseline so the next paint is a full frame; re-read size() and notify resize subscribers (the terminal may have been resized meanwhile).
  2. Render handle: await app.suspend(async () => { … }) — calls backend.suspend(), awaits the callback, calls backend.resume() in a finally, forces a repaint. Also reachable from components: useApp().suspend(fn). While suspended, draw() is a no-op (state updates still apply; the frame is painted on resume). Nested calls reject.
  3. Ctrl+Z: in raw mode decode \x1a as a key as today, but make the default (no app handler consumed it — needs a decision on how to express "consumed") suspend the backend and process.kill(process.pid, 'SIGTSTP'); on SIGCONT, resume and repaint. At minimum, handle SIGCONT so a kill -STOP / fg cycle does not leave a broken screen.

Notes

  • TestBackend: record suspend / resume calls so an app can test its editor flow.
  • Signals during suspension (SIGTERM, SIGINT) must still restore the terminal exactly once — dispose() after suspend() must not re-emit "leave alt screen".
  • Windows is out of scope here (tracked with the other hostile-environment work).
  • Docs: docs/app.md (a section on handing the terminal over), docs/writing-a-backend.md (the optional capability).

Done when

  • await app.suspend(() => spawnSync('vim', [file], { stdio: 'inherit' })) from a key handler opens vim on the normal screen and returns to an intact, fully repainted app with its state.
  • Keys typed into the child never reach the app.
  • A pty test covers the byte sequence on suspend and resume.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions