Tiny TypeScript helper for OSC 9;4 terminal progress sequences.
npm install osc-progressTiny TypeScript helper for OSC 9;4 terminal progress (Ghostty / WezTerm / Windows Terminal).
``bash`
pnpm add osc-progress
`ts
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:
`ts
import { startOscProgress } from 'osc-progress'
const stop = startOscProgress({ label: 'Waiting', indeterminate: true })
// ...
stop()
`
Strip OSC progress from stored logs:
`ts
import { sanitizeOscProgress } from 'osc-progress'
const clean = sanitizeOscProgress(text, /keepOsc/ process.stdout.isTTY)
`
Returns true when emitting OSC 9;4 progress makes sense.
Heuristics:
- requires a TTY
- enables for TERM_PROGRAM=ghostty, TERM_PROGRAM=wezterm, or WT_SESSION (Windows Terminal)
Optional overrides:
- options.disabled / options.forceoptions.disableEnvVar
- / options.forceEnvVar (expects = "1")
Starts a best-effort progress indicator and returns stop(): void.
Notes:
- label is appended as extra payload; not part of the canonical OSC 9;4 spec (many terminals ignore it, some show it).0% → 99%
- default is a timer-driven progression (never completes by itself).terminator
- defaults to st (ESC \\); bel is also supported.
Returns a small stateful controller:
- setIndeterminate(label)setPercent(label, percent)
- setPaused(label)
- (state 4)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 falsepercent
- is rounded and clamped to 0..100clear()
- uses the last label (or the initial options.label if nothing was set yet)stallAfterMs
- progress updates are throttled by default (deduped + max ~1 update/150ms)
- emits a paused/stalled state when updates stopclearDelayMs
- controls how long done() / fail() wait before clearingautoClearOnExit
- clears on process exit
`ts
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 before done()/fail() clears.autoClearOnExit
- : clear progress on process exit.
Removes OSC 9;4 progress sequences (terminated by BEL, ST (ESC \\), or 0x9c).
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.