osc-progress 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+ All notable changes to this project are documented in this file.
3
+
4
+ ## 0.1.0 - 2025-12-19
5
+ ### Added
6
+ - OSC 9;4 progress emitter (`startOscProgress`) with determinate (`0% → 99%`) and indeterminate modes.
7
+ - Terminal support detection (`supportsOscProgress`) with safe defaults (TTY-only) and heuristics for Ghostty / WezTerm / Windows Terminal.
8
+ - Environment overrides (`force`/`disabled` and `forceEnvVar`/`disableEnvVar`).
9
+ - OSC 9;4 stripping/sanitizing helpers (`stripOscProgress`, `sanitizeOscProgress`) for log storage.
10
+ - Sequence finder (`findOscProgressSequences`) supporting ST (`ESC \\`), BEL, and C1 ST terminators.
11
+ - Label sanitization (`sanitizeLabel`) to prevent control/terminator injection.
12
+ - Modern TypeScript ESM package with bundled types, Node 20+.
13
+ - Test suite with full coverage for core behavior.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peter Steinberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # ⏳ osc-progress — Tiny lib for OSC 9;4 terminal progress.
2
+
3
+ Tiny TypeScript helper for **OSC 9;4** terminal progress sequences (used by terminals like Ghostty / WezTerm / Windows Terminal).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add osc-progress
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import process from 'node:process'
15
+ import { startOscProgress } from 'osc-progress'
16
+
17
+ const stop = startOscProgress({
18
+ label: 'Fetching',
19
+ write: (chunk) => process.stderr.write(chunk),
20
+ env: process.env,
21
+ isTty: process.stderr.isTTY,
22
+ })
23
+
24
+ // ...do work...
25
+
26
+ stop()
27
+ ```
28
+
29
+ Indeterminate (spinner-like) mode:
30
+
31
+ ```ts
32
+ import { startOscProgress } from 'osc-progress'
33
+
34
+ const stop = startOscProgress({ label: 'Waiting', indeterminate: true })
35
+ // ...
36
+ stop()
37
+ ```
38
+
39
+ Strip OSC progress from stored logs:
40
+
41
+ ```ts
42
+ import { sanitizeOscProgress } from 'osc-progress'
43
+
44
+ const clean = sanitizeOscProgress(text, /*keepOsc*/ process.stdout.isTTY)
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `supportsOscProgress(env?, isTty?, options?)`
50
+
51
+ Returns `true` when emitting OSC 9;4 progress makes sense.
52
+
53
+ Heuristics:
54
+ - requires a TTY
55
+ - enables for `TERM_PROGRAM=ghostty*`, `TERM_PROGRAM=wezterm*`, or `WT_SESSION` (Windows Terminal)
56
+
57
+ Optional overrides:
58
+ - `options.disabled` / `options.force`
59
+ - `options.disableEnvVar` / `options.forceEnvVar` (expects `= "1"`)
60
+
61
+ ### `startOscProgress(options?)`
62
+
63
+ Starts a best-effort progress indicator and returns `stop(): void`.
64
+
65
+ Notes:
66
+ - `label` is appended as extra payload; **not part of the canonical OSC 9;4 spec** (many terminals ignore it, some show it).
67
+ - default is a timer-driven `0% → 99%` progression (never completes by itself).
68
+ - `terminator` defaults to `st` (`ESC \\`); `bel` is also supported.
69
+
70
+ ### `sanitizeOscProgress(text, keepOsc)`
71
+
72
+ Removes OSC 9;4 progress sequences (terminated by `BEL`, `ST` (`ESC \\`), or `0x9c`).
73
+
74
+ ## Semantics / portability
75
+
76
+ OSC 9;4 is widely implemented, but state `4` is ambiguous across terminals (some treat it as `paused`, some as `warning`).
77
+ This library exposes the raw numeric state and does not try to reinterpret it.
@@ -0,0 +1,2 @@
1
+ export { findOscProgressSequences, OSC_PROGRESS_BEL, OSC_PROGRESS_C1_ST, OSC_PROGRESS_PREFIX, OSC_PROGRESS_ST, sanitizeLabel, sanitizeOscProgress, startOscProgress, stripOscProgress, supportsOscProgress, } from './oscProgress.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EAKf,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,195 @@
1
+ import process from 'node:process';
2
+ /**
3
+ * OSC 9;4 progress prefix (`ESC ] 9 ; 4 ;`).
4
+ *
5
+ * Typical emitted forms:
6
+ * - `ESC ] 9;4;<state>;<percent>;<payload> ST`
7
+ * - `ESC ] 9;4;<state>;;<payload> ST` (indeterminate)
8
+ */
9
+ export const OSC_PROGRESS_PREFIX = '\u001b]9;4;';
10
+ /** String Terminator (ST): `ESC \\` */
11
+ export const OSC_PROGRESS_ST = '\u001b\\';
12
+ /** Bell (BEL): `0x07` */
13
+ export const OSC_PROGRESS_BEL = '\u0007';
14
+ /** C1 String Terminator (ST): `0x9c` */
15
+ export const OSC_PROGRESS_C1_ST = '\u009c';
16
+ function resolveTerminator(terminator) {
17
+ return terminator === 'bel' ? OSC_PROGRESS_BEL : OSC_PROGRESS_ST;
18
+ }
19
+ /**
20
+ * Sanitizes a label/payload so it can't break the surrounding OSC sequence.
21
+ * Removes escape chars and common OSC terminators; trims whitespace.
22
+ */
23
+ export function sanitizeLabel(label) {
24
+ const withoutSt = label.replaceAll(OSC_PROGRESS_ST, '');
25
+ const withoutEscape = withoutSt.split('\u001b').join('');
26
+ const withoutTerminators = withoutEscape
27
+ .replaceAll(OSC_PROGRESS_BEL, '')
28
+ .replaceAll(OSC_PROGRESS_C1_ST, '');
29
+ return withoutTerminators.replaceAll(']', '').trim();
30
+ }
31
+ /**
32
+ * Best-effort check whether OSC 9;4 progress output is likely to work.
33
+ *
34
+ * Default heuristics:
35
+ * - requires `isTty === true`
36
+ * - enables for Ghostty (`TERM_PROGRAM=ghostty*`), WezTerm (`TERM_PROGRAM=wezterm*`), Windows Terminal (`WT_SESSION`)
37
+ *
38
+ * Override knobs:
39
+ * - `options.force` / `options.disabled`
40
+ * - `options.forceEnvVar` / `options.disableEnvVar` (expects value `"1"`)
41
+ */
42
+ export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY, options = {}) {
43
+ if (!isTty)
44
+ return false;
45
+ if (options.disabled)
46
+ return false;
47
+ if (options.force)
48
+ return true;
49
+ if (options.disableEnvVar && env[options.disableEnvVar] === '1') {
50
+ return false;
51
+ }
52
+ if (options.forceEnvVar && env[options.forceEnvVar] === '1') {
53
+ return true;
54
+ }
55
+ const termProgram = (env.TERM_PROGRAM ?? '').toLowerCase();
56
+ if (termProgram.includes('ghostty'))
57
+ return true;
58
+ if (termProgram.includes('wezterm'))
59
+ return true;
60
+ if (env.WT_SESSION)
61
+ return true;
62
+ return false;
63
+ }
64
+ /**
65
+ * Finds OSC 9;4 progress sequences inside an arbitrary string.
66
+ *
67
+ * Supports three terminators:
68
+ * - ST (`ESC \\`)
69
+ * - BEL (`0x07`)
70
+ * - C1 ST (`0x9c`)
71
+ *
72
+ * Unterminated sequences are ignored (use `stripOscProgress` if you want to drop them).
73
+ */
74
+ export function findOscProgressSequences(text) {
75
+ const sequences = [];
76
+ const prefixLen = OSC_PROGRESS_PREFIX.length;
77
+ let searchFrom = 0;
78
+ while (searchFrom < text.length) {
79
+ const start = text.indexOf(OSC_PROGRESS_PREFIX, searchFrom);
80
+ if (start === -1)
81
+ break;
82
+ const after = start + prefixLen;
83
+ const candidates = [];
84
+ const stStart = text.indexOf(OSC_PROGRESS_ST, after);
85
+ if (stStart !== -1) {
86
+ candidates.push({ endExclusive: stStart + OSC_PROGRESS_ST.length, terminator: 'st' });
87
+ }
88
+ const belStart = text.indexOf(OSC_PROGRESS_BEL, after);
89
+ if (belStart !== -1) {
90
+ candidates.push({ endExclusive: belStart + OSC_PROGRESS_BEL.length, terminator: 'bel' });
91
+ }
92
+ const c1Start = text.indexOf(OSC_PROGRESS_C1_ST, after);
93
+ if (c1Start !== -1) {
94
+ candidates.push({ endExclusive: c1Start + OSC_PROGRESS_C1_ST.length, terminator: 'c1st' });
95
+ }
96
+ if (candidates.length === 0) {
97
+ searchFrom = after;
98
+ continue;
99
+ }
100
+ candidates.sort((a, b) => a.endExclusive - b.endExclusive);
101
+ const best = candidates[0];
102
+ sequences.push({
103
+ start,
104
+ end: best.endExclusive,
105
+ raw: text.slice(start, best.endExclusive),
106
+ terminator: best.terminator,
107
+ });
108
+ searchFrom = best.endExclusive;
109
+ }
110
+ return sequences;
111
+ }
112
+ /**
113
+ * Removes OSC 9;4 progress sequences from `text`.
114
+ *
115
+ * Behavior:
116
+ * - strips sequences terminated by ST/BEL/C1 ST
117
+ * - if a sequence is unterminated, it is removed until end-of-string
118
+ */
119
+ export function stripOscProgress(text) {
120
+ const prefixLen = OSC_PROGRESS_PREFIX.length;
121
+ let current = text;
122
+ while (current.includes(OSC_PROGRESS_PREFIX)) {
123
+ const start = current.indexOf(OSC_PROGRESS_PREFIX);
124
+ const after = start + prefixLen;
125
+ const stStart = current.indexOf(OSC_PROGRESS_ST, after);
126
+ const belStart = current.indexOf(OSC_PROGRESS_BEL, after);
127
+ const c1Start = current.indexOf(OSC_PROGRESS_C1_ST, after);
128
+ const ends = [];
129
+ if (stStart !== -1)
130
+ ends.push(stStart + OSC_PROGRESS_ST.length);
131
+ if (belStart !== -1)
132
+ ends.push(belStart + OSC_PROGRESS_BEL.length);
133
+ if (c1Start !== -1)
134
+ ends.push(c1Start + OSC_PROGRESS_C1_ST.length);
135
+ const cutEnd = ends.length === 0 ? current.length : Math.min(...ends);
136
+ current = `${current.slice(0, start)}${current.slice(cutEnd)}`;
137
+ }
138
+ return current;
139
+ }
140
+ /**
141
+ * Convenience helper:
142
+ * - if `keepOsc` is true, returns the input unchanged (useful when writing to a TTY)
143
+ * - otherwise, strips OSC 9;4 sequences (useful for logs/snapshots)
144
+ */
145
+ export function sanitizeOscProgress(text, keepOsc) {
146
+ return keepOsc ? text : stripOscProgress(text);
147
+ }
148
+ /**
149
+ * Emits a terminal progress indicator using OSC 9;4 and returns `stop()`.
150
+ *
151
+ * Notes:
152
+ * - no-op when `supportsOscProgress(...)` is false
153
+ * - determinate mode ramps `0% → 99%` on a timer; `stop()` clears progress
154
+ * - indeterminate mode emits `state=3` and `stop()` clears progress
155
+ */
156
+ export function startOscProgress(options = {}) {
157
+ const { label = 'Working…', targetMs = 10 * 60_000, write = (text) => process.stderr.write(text), indeterminate = false, state = 1, terminator, } = options;
158
+ if (!supportsOscProgress(options.env, options.isTty, options)) {
159
+ return () => { };
160
+ }
161
+ const cleanLabel = sanitizeLabel(label);
162
+ const end = resolveTerminator(terminator);
163
+ const send = (st, percent) => {
164
+ if (percent == null) {
165
+ write(`${OSC_PROGRESS_PREFIX}${st};;${cleanLabel}${end}`);
166
+ return;
167
+ }
168
+ const clamped = Math.max(0, Math.min(100, Math.round(percent)));
169
+ write(`${OSC_PROGRESS_PREFIX}${st};${clamped};${cleanLabel}${end}`);
170
+ };
171
+ if (indeterminate) {
172
+ send(3, null);
173
+ return () => {
174
+ send(0, 0);
175
+ };
176
+ }
177
+ const target = Math.max(targetMs, 1_000);
178
+ const startedAt = Date.now();
179
+ send(state, 0);
180
+ const timer = setInterval(() => {
181
+ const elapsed = Date.now() - startedAt;
182
+ const percent = Math.min(99, (elapsed / target) * 100);
183
+ send(state, percent);
184
+ }, 900);
185
+ timer.unref?.();
186
+ let stopped = false;
187
+ return () => {
188
+ if (stopped)
189
+ return;
190
+ stopped = true;
191
+ clearInterval(timer);
192
+ send(0, 0);
193
+ };
194
+ }
195
+ //# sourceMappingURL=oscProgress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oscProgress.js","sourceRoot":"","sources":["../../src/oscProgress.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { findOscProgressSequences, OSC_PROGRESS_BEL, OSC_PROGRESS_C1_ST, OSC_PROGRESS_PREFIX, OSC_PROGRESS_ST, type OscProgressOptions, type OscProgressSequence, type OscProgressSupportOptions, type OscProgressTerminator, sanitizeLabel, sanitizeOscProgress, startOscProgress, stripOscProgress, supportsOscProgress, } from './oscProgress.js';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * OSC 9;4 progress prefix (`ESC ] 9 ; 4 ;`).
3
+ *
4
+ * Typical emitted forms:
5
+ * - `ESC ] 9;4;<state>;<percent>;<payload> ST`
6
+ * - `ESC ] 9;4;<state>;;<payload> ST` (indeterminate)
7
+ */
8
+ export declare const OSC_PROGRESS_PREFIX = "\u001B]9;4;";
9
+ /** String Terminator (ST): `ESC \\` */
10
+ export declare const OSC_PROGRESS_ST = "\u001B\\";
11
+ /** Bell (BEL): `0x07` */
12
+ export declare const OSC_PROGRESS_BEL = "\u0007";
13
+ /** C1 String Terminator (ST): `0x9c` */
14
+ export declare const OSC_PROGRESS_C1_ST = "\u009C";
15
+ /** How to terminate the OSC sequence when emitting. */
16
+ export type OscProgressTerminator = 'st' | 'bel';
17
+ export interface OscProgressSupportOptions {
18
+ /** Force support on/off, overriding env heuristics. */
19
+ force?: boolean;
20
+ disabled?: boolean;
21
+ /** Name of env var which disables OSC progress when set to `"1"`. */
22
+ disableEnvVar?: string;
23
+ /** Name of env var which forces OSC progress when set to `"1"`. */
24
+ forceEnvVar?: string;
25
+ }
26
+ export interface OscProgressOptions extends OscProgressSupportOptions {
27
+ /**
28
+ * Extra payload appended to the OSC sequence (many terminals ignore this; a few show it).
29
+ * Defaults to `"Working…"`. Sanitized to avoid control chars and terminators.
30
+ */
31
+ label?: string;
32
+ /**
33
+ * Target duration in ms for the internal `0 → 99%` ramp.
34
+ * The implementation never emits 100% by itself; completion is via `stop()`.
35
+ */
36
+ targetMs?: number;
37
+ /** Write function (defaults to `process.stderr.write`). */
38
+ write?: (data: string) => void;
39
+ /** Environment lookup (defaults to `process.env`). */
40
+ env?: NodeJS.ProcessEnv;
41
+ /** TTY flag (defaults to `process.stdout.isTTY`). */
42
+ isTty?: boolean;
43
+ /** When true, emit an indeterminate progress indicator (no percentage). */
44
+ indeterminate?: boolean;
45
+ /**
46
+ * Numeric OSC 9;4 state.
47
+ * - 0: clear/hide
48
+ * - 1: normal
49
+ * - 2: error
50
+ * - 3: indeterminate
51
+ * - 4: ambiguous (paused/warning depending on terminal)
52
+ */
53
+ state?: 1 | 2 | 4;
54
+ /** OSC terminator to use. `st` = ESC \\, `bel` = BEL. */
55
+ terminator?: OscProgressTerminator;
56
+ }
57
+ export interface OscProgressSequence {
58
+ /** Inclusive start index in the input string. */
59
+ start: number;
60
+ /** Exclusive end index in the input string. */
61
+ end: number;
62
+ /** Raw substring that matched. */
63
+ raw: string;
64
+ /** Which terminator was encountered. */
65
+ terminator: 'st' | 'bel' | 'c1st';
66
+ }
67
+ /**
68
+ * Sanitizes a label/payload so it can't break the surrounding OSC sequence.
69
+ * Removes escape chars and common OSC terminators; trims whitespace.
70
+ */
71
+ export declare function sanitizeLabel(label: string): string;
72
+ /**
73
+ * Best-effort check whether OSC 9;4 progress output is likely to work.
74
+ *
75
+ * Default heuristics:
76
+ * - requires `isTty === true`
77
+ * - enables for Ghostty (`TERM_PROGRAM=ghostty*`), WezTerm (`TERM_PROGRAM=wezterm*`), Windows Terminal (`WT_SESSION`)
78
+ *
79
+ * Override knobs:
80
+ * - `options.force` / `options.disabled`
81
+ * - `options.forceEnvVar` / `options.disableEnvVar` (expects value `"1"`)
82
+ */
83
+ export declare function supportsOscProgress(env?: NodeJS.ProcessEnv, isTty?: boolean, options?: OscProgressSupportOptions): boolean;
84
+ /**
85
+ * Finds OSC 9;4 progress sequences inside an arbitrary string.
86
+ *
87
+ * Supports three terminators:
88
+ * - ST (`ESC \\`)
89
+ * - BEL (`0x07`)
90
+ * - C1 ST (`0x9c`)
91
+ *
92
+ * Unterminated sequences are ignored (use `stripOscProgress` if you want to drop them).
93
+ */
94
+ export declare function findOscProgressSequences(text: string): OscProgressSequence[];
95
+ /**
96
+ * Removes OSC 9;4 progress sequences from `text`.
97
+ *
98
+ * Behavior:
99
+ * - strips sequences terminated by ST/BEL/C1 ST
100
+ * - if a sequence is unterminated, it is removed until end-of-string
101
+ */
102
+ export declare function stripOscProgress(text: string): string;
103
+ /**
104
+ * Convenience helper:
105
+ * - if `keepOsc` is true, returns the input unchanged (useful when writing to a TTY)
106
+ * - otherwise, strips OSC 9;4 sequences (useful for logs/snapshots)
107
+ */
108
+ export declare function sanitizeOscProgress(text: string, keepOsc: boolean): string;
109
+ /**
110
+ * Emits a terminal progress indicator using OSC 9;4 and returns `stop()`.
111
+ *
112
+ * Notes:
113
+ * - no-op when `supportsOscProgress(...)` is false
114
+ * - determinate mode ramps `0% → 99%` on a timer; `stop()` clears progress
115
+ * - indeterminate mode emits `state=3` and `stop()` clears progress
116
+ */
117
+ export declare function startOscProgress(options?: OscProgressOptions): () => void;
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "osc-progress",
3
+ "version": "0.1.0",
4
+ "description": "Tiny TypeScript helper for OSC 9;4 terminal progress sequences.",
5
+ "license": "MIT",
6
+ "author": "Peter Steinberger",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/steipete/osc-progress.git"
10
+ },
11
+ "homepage": "https://github.com/steipete/osc-progress#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/steipete/osc-progress/issues"
14
+ },
15
+ "keywords": [
16
+ "osc",
17
+ "terminal",
18
+ "progress",
19
+ "cli",
20
+ "ghostty",
21
+ "wezterm",
22
+ "windows-terminal"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/esm/index.js",
26
+ "module": "./dist/esm/index.js",
27
+ "types": "./dist/types/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/types/index.d.ts",
31
+ "import": "./dist/esm/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "CHANGELOG.md",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "sideEffects": false,
44
+ "packageManager": "pnpm@10.25.0",
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.build.json",
47
+ "prepare": "pnpm build",
48
+ "prepack": "pnpm build",
49
+ "prepublishOnly": "pnpm check",
50
+ "typecheck": "tsc -p tsconfig.build.json --noEmit",
51
+ "format": "biome format --write .",
52
+ "lint": "biome check .",
53
+ "lint:fix": "biome check --write .",
54
+ "test": "pnpm build && vitest run",
55
+ "test:coverage": "pnpm build && vitest run --coverage",
56
+ "check": "pnpm lint && pnpm test:coverage"
57
+ },
58
+ "devDependencies": {
59
+ "@biomejs/biome": "^2.3.10",
60
+ "@types/node": "^25.0.3",
61
+ "@vitest/coverage-v8": "^4.0.16",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^4.0.16"
64
+ }
65
+ }