Terminal Lifecycle
Silvery handles terminal lifecycle events (suspend/resume, interrupt) automatically. When stdin is in raw mode, Ctrl+Z and Ctrl+C don't generate OS signals (SIGTSTP/SIGINT). Silvery intercepts the raw bytes and manages the full terminal state save/restore cycle.
Quick Start
Both run() (Layer 2) and createApp().run() (Layer 3) enable lifecycle handling by default:
tsx
// Ctrl+Z suspends, Ctrl+C exits — no config needed
await run(<App />)Options
| Option | Type | Default | Description |
|---|---|---|---|
suspendOnCtrlZ | boolean | true | Handle Ctrl+Z by suspending the process |
exitOnCtrlC | boolean | true | Handle Ctrl+C by exiting |
onSuspend | () => boolean | void | — | Called before suspend. Return false to prevent |
onResume | () => void | — | Called after resume |
onInterrupt | () => boolean | void | — | Called on Ctrl+C. Return false to prevent exit |
Suspend/Resume Flow
When the user presses Ctrl+Z:
onSuspendhook is called (if provided). If it returnsfalse, suspend is cancelled.- Terminal state is captured (protocols, modes, cursor visibility).
- Terminal is restored to normal: raw mode off, alt screen exit, cursor shown, all protocols disabled.
SIGTSTPis sent to the process (it actually suspends).- On
SIGCONT(resume): terminal state is restored, screen is cleared, synthetic resize triggers full redraw. onResumehook is called.
Protocols Saved/Restored
- Raw mode (stdin)
- Alternate screen buffer (DEC mode 1049)
- Cursor visibility (DEC mode 25)
- Mouse tracking (modes 1000, 1002, 1006)
- Kitty keyboard protocol (with original flags)
- Bracketed paste (DEC mode 2004)
- SGR attributes (reset on suspend, not restored — redraw handles this)
Interrupt Flow
When the user presses Ctrl+C:
onInterrupthook is called (if provided). If it returnsfalse, exit is cancelled.- The app's exit function is called, which triggers normal cleanup (unmount, restore terminal, exit).
Hooks Example
tsx
await run(<App />, {
onSuspend: () => {
// Pause background timers before suspend
pauseTimers()
},
onResume: () => {
// Refresh data that may have changed while suspended
refetchData()
},
onInterrupt: () => {
if (hasUnsavedChanges) {
showConfirmDialog()
return false // Prevent exit
}
},
})Low-Level API
For custom lifecycle management (e.g., framework integrations), the building blocks are exported from silvery/runtime:
tsx
import {
captureTerminalState,
restoreTerminalState,
resumeTerminalState,
performSuspend,
CTRL_C,
CTRL_Z,
type TerminalLifecycleOptions,
type TerminalState,
} from "@silvery/term/runtime"
// Capture current state
const state = captureTerminalState({
alternateScreen: true,
mouse: true,
kitty: true,
kittyFlags: 3,
bracketedPaste: true,
})
// Manual suspend/resume
restoreTerminalState(stdout, stdin) // Before suspend
// ... process suspends ...
resumeTerminalState(state, stdout, stdin) // After resume
// Or use the all-in-one helper
performSuspend(state, stdout, stdin, () => {
console.log("Resumed!")
})TerminalState
typescript
interface TerminalState {
rawMode: boolean
alternateScreen: boolean
cursorHidden: boolean
mouseEnabled: boolean
kittyEnabled: boolean
kittyFlags: number
bracketedPaste: boolean
}Terminal Compatibility
Suspend/resume works on all terminals that support the standard escape sequences. Tested with:
- Ghostty
- Kitty
- WezTerm
- iTerm2
- Alacritty
- macOS Terminal.app
- tmux / screen (nested sessions work correctly)
Implementation Notes
writeSyncis used for restore sequences during signal handlers (async writes may not complete before suspend)- The
SIGCONThandler is registered as a one-time handler BEFORE sendingSIGTSTP - Screen clear (
CSI 2 J+CSI H) is always included on resume to ensure a clean slate - Synthetic resize (
stdout.emit("resize")) triggers a full redraw after resume - The runtime's
invalidate()is called to force a fresh render (not incremental diff)