Tiny TypeScript helper for OSC 9;4 terminal progress (Ghostty / WezTerm / Windows Terminal).
Install
Usage
import process from 'node:process' import { startOscProgress } from 'osc-progress' const stop = startOscProgress({ label: 'Fetching', write: (chunk) => process.stderr.write(chunk), env: process.env, isTty: process.stderr.isTTY, }) // ...do work... stop()
Indeterminate (spinner-like) mode:
import { startOscProgress } from 'osc-progress' const stop = startOscProgress({ label: 'Waiting', indeterminate: true }) // ... stop()
Strip OSC progress from stored logs:
import { sanitizeOscProgress } from 'osc-progress' const clean = sanitizeOscProgress(text, /*keepOsc*/ process.stdout.isTTY)
API
supportsOscProgress(env?, isTty?, options?)
Returns true when emitting OSC 9;4 progress makes sense.
Heuristics:
- requires a TTY
- enables for
TERM_PROGRAM=ghostty*,TERM_PROGRAM=wezterm*, orWT_SESSION(Windows Terminal)
Optional overrides:
options.disabled/options.forceoptions.disableEnvVar/options.forceEnvVar(expects= "1")
startOscProgress(options?)
Starts a best-effort progress indicator and returns stop(): void.
Notes:
labelis appended as extra payload; not part of the canonical OSC 9;4 spec (many terminals ignore it, some show it).- default is a timer-driven
0% → 99%progression (never completes by itself). terminatordefaults tost(ESC \\);belis also supported.
createOscProgressController(options?)
Returns a small stateful controller:
setIndeterminate(label)setPercent(label, percent)setPaused(label)(state4)done(label?)(emit 100% then clear)fail(label?)(emit error state then clear)clear()dispose()(cleanup timers/listeners)
Use this when you already have real progress (bytes/total, seconds/total) and want determinate terminal progress instead of the timer-based ramp.
Notes:
- returns no-op methods when
supportsOscProgress(...)is false percentis rounded and clamped to0..100clear()uses the last label (or the initialoptions.labelif nothing was set yet)- progress updates are throttled by default (deduped + max ~1 update/150ms)
stallAfterMsemits a paused/stalled state when updates stopclearDelayMscontrols how longdone()/fail()wait before clearingautoClearOnExitclears on process exit
import process from 'node:process' import { createOscProgressController } from 'osc-progress' const osc = createOscProgressController({ env: process.env, isTty: process.stderr.isTTY, write: (chunk) => process.stderr.write(chunk), stallAfterMs: 10_000, clearDelayMs: 200, autoClearOnExit: true, }) osc.setIndeterminate('Connecting') osc.setPercent('Downloading', 12) osc.setPercent('Downloading', 67) osc.done()
Controller options
stallAfterMs: emit state=4 when no updates are seen within this window.stalledLabel: override stalled label (string or formatter).clearDelayMs: delay beforedone()/fail()clears.autoClearOnExit: clear progress on process exit.
sanitizeOscProgress(text, keepOsc)
Removes OSC 9;4 progress sequences (terminated by BEL, ST (ESC \\), or 0x9c).
Semantics / portability
OSC 9;4 is widely implemented, but state 4 is ambiguous across terminals (some treat it as paused, some as warning).
This library exposes the raw numeric state and does not try to reinterpret it.