flagstaff 0.2.1 → 0.3.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/README.md +25 -7
- package/dist/box.d.ts +16 -0
- package/dist/box.js +11 -5
- package/dist/boxen.js +5 -3
- package/dist/cli-table3.d.ts +12 -1
- package/dist/cli-table3.js +6 -4
- package/dist/cli.js +7 -5
- package/dist/link.d.ts +77 -0
- package/dist/link.js +23 -0
- package/dist/log-update.d.ts +2 -2
- package/dist/log-update.js +11 -9
- package/dist/ora.d.ts +1 -1
- package/dist/ora.js +15 -13
- package/dist/plugin.d.ts +1 -1
- package/dist/projection.d.ts +1 -8
- package/dist/projection.js +3 -2
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.js +2 -0
- package/dist/schema.json +1 -1
- package/dist/table.d.ts +11 -1
- package/dist/table.js +22 -11
- package/package.json +6 -3
- package/dist/cursor.d.ts +0 -13
- package/dist/cursor.js +0 -49
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
15
|
<a href="https://www.npmjs.com/package/flagstaff"><img src="https://img.shields.io/npm/v/flagstaff?style=flat-square&color=0a6b47" alt="npm version" /></a>
|
|
16
|
-
<img src="https://img.shields.io/badge/dependencies-
|
|
16
|
+
<img src="https://img.shields.io/badge/dependencies-4%20in--family-0a6b47?style=flat-square" alt="Four dependencies, all in this repository: closeout, linegauge, paratext, roundel" />
|
|
17
17
|
<img src="https://img.shields.io/badge/Node.js-24+-green.svg?style=flat-square" alt="Node.js 24+" />
|
|
18
18
|
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
|
|
19
19
|
</p>
|
|
@@ -243,11 +243,11 @@ screen, not the bytes.
|
|
|
243
243
|
`logUpdateStderr`, with the row-level diffing intact: a five-row frame whose last row is a
|
|
244
244
|
counter costs one row of output per tick, not five.
|
|
245
245
|
|
|
246
|
-
log-update ships 113.4 KB across **sixteen** packages. This
|
|
247
|
-
|
|
246
|
+
log-update ships 113.4 KB across **sixteen** packages. This subpath reaches two, both from
|
|
247
|
+
this repository: `linegauge/wrap` for the wrapper, and `closeout` for the cursor.
|
|
248
248
|
It carries no port of `slice-ansi` — the wrapper already makes every row self-contained,
|
|
249
249
|
so clipping a frame to the terminal's height is an array slice. `signal-exit`, 22.0 KB of
|
|
250
|
-
those sixteen, is
|
|
250
|
+
those sixteen, is `closeout`'s to own, shared with the ora façade because both
|
|
251
251
|
incumbents port the same `cli-cursor` → `restore-cursor` → `signal-exit` chain. Ctrl+C
|
|
252
252
|
mid-frame puts your cursor back, and still terminates — unless your program installed its
|
|
253
253
|
own `SIGINT` handler, in which case it is delivered once, to you, and this stays out of it.
|
|
@@ -332,11 +332,11 @@ Every subpath is a lock, not a convention, and the numbers below are asserted by
|
|
|
332
332
|
`weight.test.ts` against `dist/`, not estimated: `flagstaff/loop` reaches 4.4 KB on disk and
|
|
333
333
|
never the plugin registry; `flagstaff/plugin` 8.4 KB, of which 2.4 KB is the schema;
|
|
334
334
|
`flagstaff/spinner` 9.4 KB; `flagstaff/ora` 46.5 KB — 55.9 KB with roundel counted, against
|
|
335
|
-
ora's own 113.6 KB; `flagstaff/log-update` 29.6 KB,
|
|
336
|
-
log-update's own 113.4 KB across sixteen
|
|
335
|
+
ora's own 113.6 KB; `flagstaff/log-update` 29.6 KB, against
|
|
336
|
+
log-update's own 113.4 KB across sixteen, reaching only `linegauge/wrap` and `closeout`; `flagstaff/boxen` 33.7 KB — 43.0 KB with roundel
|
|
337
337
|
counted, against boxen's own 132.4 KB across nineteen; `flagstaff/cli-table3` 32.9 KB —
|
|
338
338
|
42.3 KB with roundel, against cli-table3's own 106.0 KB across seven. The three façades share `wrap.js` and
|
|
339
|
-
`width.js`, and the first two share `
|
|
339
|
+
`width.js`, and the first two share `closeout`; none reaches another's port, and none
|
|
340
340
|
reaches the core. `sideEffects: false` lets a
|
|
341
341
|
bundler drop what a program does not use. ESM with a `default` condition, so
|
|
342
342
|
`require('flagstaff/spinner')` works from CommonJS on Node ≥ 24.
|
|
@@ -370,3 +370,21 @@ Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on
|
|
|
370
370
|
none requires the others.
|
|
371
371
|
|
|
372
372
|
MIT © Ofri Peretz — see [LICENSE](./LICENSE).
|
|
373
|
+
|
|
374
|
+
## Benchmarks
|
|
375
|
+
|
|
376
|
+
Every number here is produced by `npm run bench` and published at [/docs/benchmarks](/docs/benchmarks).
|
|
377
|
+
|
|
378
|
+
Graded by the incumbent's own test suite:
|
|
379
|
+
|
|
380
|
+
| suite | passing |
|
|
381
|
+
| :-- | --: |
|
|
382
|
+
| `boxen` | 84 / 84 |
|
|
383
|
+
| `cli-table3` | 29 / 29 |
|
|
384
|
+
| `log-update` | 99 / 99 |
|
|
385
|
+
| `ora` | 99 / 99 |
|
|
386
|
+
## Where it sits
|
|
387
|
+
|
|
388
|
+
Plugins register under the `tokens`, `glyphs`, `spinners`, `borders`, `components` keys, against the one schema the whole family shares.
|
|
389
|
+
|
|
390
|
+
Nothing in this family builds on it yet, and it builds on `closeout`, `linegauge`, `paratext`, `roundel`.
|
package/dist/box.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Terminal } from './link.js';
|
|
1
2
|
import { type BorderStyle, type Component } from './plugin.js';
|
|
2
3
|
export interface BoxOptions {
|
|
3
4
|
/** A registered border's name, or a style of your own. Default `round`. */
|
|
@@ -11,12 +12,27 @@ export interface BoxOptions {
|
|
|
11
12
|
title?: string;
|
|
12
13
|
/** Columns the whole box may occupy, borders included. Text wraps to fit. Default 80. */
|
|
13
14
|
width?: number;
|
|
15
|
+
/**
|
|
16
|
+
* A url or a path the box's text points at — `file://…` for a path a terminal should be
|
|
17
|
+
* able to open, which is the case R12 is named after. The title is left alone: it is a
|
|
18
|
+
* label for the box, and a link around it would claim the border is clickable too.
|
|
19
|
+
*/
|
|
20
|
+
href?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The terminal the link is rendered for. Omitted, the real process is read through this
|
|
23
|
+
* package's seam. Supply one and the whole path is pure.
|
|
24
|
+
*/
|
|
25
|
+
terminal?: Terminal;
|
|
14
26
|
}
|
|
15
27
|
/** Draw `text` in a box, as a string. The many callers who want only this want only this. */
|
|
16
28
|
export declare function box(text: string, options?: BoxOptions): string;
|
|
17
29
|
export interface BoxState {
|
|
18
30
|
text: string;
|
|
19
31
|
title?: string;
|
|
32
|
+
/** Per-state destination, overriding the one the component was built with. */
|
|
33
|
+
href?: string;
|
|
20
34
|
}
|
|
21
35
|
/** A box as a component: the text off a terminal, the drawing on one (R1). */
|
|
22
36
|
export declare function boxComponent(options?: BoxOptions): Component<BoxState>;
|
|
37
|
+
/** The runtime shape `BoxOptions.terminal` takes, so a caller can name it (R12). */
|
|
38
|
+
export type { Terminal } from './link.js';
|
package/dist/box.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { width } from 'linegauge';
|
|
2
2
|
import { wrap } from 'linegauge/wrap';
|
|
3
3
|
import { muted } from 'roundel/tokens';
|
|
4
|
+
import { laid, painted, painter, STATIC } from './link.js';
|
|
4
5
|
import { lookupBorder } from './plugin.js';
|
|
5
6
|
const DEFAULT_WIDTH = 80;
|
|
6
7
|
const DEFAULT_PAD_X = 1;
|
|
@@ -8,7 +9,7 @@ const DEFAULT_PAD_Y = 0;
|
|
|
8
9
|
const BORDER_CELLS = 2;
|
|
9
10
|
const ELLIPSIS = '…';
|
|
10
11
|
const resolve = (border) => (typeof border === 'string' ? lookupBorder(border) : border);
|
|
11
|
-
const padEnd = (line, cells) => line + ' '.repeat(Math.max(0, cells - width(
|
|
12
|
+
const padEnd = (line, cells, measured = line) => line + ' '.repeat(Math.max(0, cells - width(measured)));
|
|
12
13
|
function fitTitle(title, cells) {
|
|
13
14
|
if (width(title) <= cells)
|
|
14
15
|
return title;
|
|
@@ -42,18 +43,23 @@ export function box(text, options = {}) {
|
|
|
42
43
|
const borderCells = style.left === '' ? 0 : BORDER_CELLS;
|
|
43
44
|
const inner = Math.max(1, total - borderCells);
|
|
44
45
|
const content = Math.max(1, inner - padX * BORDER_CELLS);
|
|
45
|
-
const
|
|
46
|
+
const paint = painter(options.terminal);
|
|
47
|
+
const wrapped = wrap(laid(text, options.href, paint), content, { hard: true, trim: false }).split('\n');
|
|
46
48
|
const blank = Array.from({ length: padY }, () => '');
|
|
47
49
|
const pad = ' '.repeat(padX);
|
|
48
|
-
const rows = [...blank, ...wrapped, ...blank].map((line) => `${style.left}${pad}${padEnd(line, content)}${pad}${style.right}`);
|
|
50
|
+
const rows = [...blank, ...wrapped, ...blank].map((line) => `${style.left}${pad}${padEnd(painted(line, options.href, paint), content, line)}${pad}${style.right}`);
|
|
49
51
|
const top = topBorder(style, inner, options.title);
|
|
50
52
|
const bottom = style.bottom === '' ? '' : style.bottomLeft + style.bottom.repeat(inner) + style.bottomRight;
|
|
51
53
|
return [top, ...rows, bottom].filter((row) => row !== '').join('\n');
|
|
52
54
|
}
|
|
53
55
|
export function boxComponent(options = {}) {
|
|
56
|
+
const plain = painter(STATIC);
|
|
54
57
|
return {
|
|
55
58
|
name: 'box',
|
|
56
|
-
static: (state) =>
|
|
57
|
-
|
|
59
|
+
static: (state) => {
|
|
60
|
+
const body = laid(state.text, state.href ?? options.href, plain);
|
|
61
|
+
return state.title === undefined || state.title === '' ? body : `${state.title}: ${body}`;
|
|
62
|
+
},
|
|
63
|
+
frame: (_t, state) => muted(box(state.text, { ...options, ...(state.title === undefined ? {} : { title: state.title }), ...(state.href === undefined ? {} : { href: state.href }) })),
|
|
58
64
|
};
|
|
59
65
|
}
|
package/dist/boxen.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { width as stringWidth } from 'linegauge';
|
|
2
2
|
import { wrap as wrapAnsi } from 'linegauge/wrap';
|
|
3
3
|
import chalk from 'roundel/chalk';
|
|
4
|
+
import { processRuntime } from './runtime.js';
|
|
5
|
+
const rt = processRuntime();
|
|
4
6
|
const NEWLINE = '\n';
|
|
5
7
|
const PAD = ' ';
|
|
6
8
|
const NONE = 'none';
|
|
@@ -20,7 +22,7 @@ const BOXES = {
|
|
|
20
22
|
arrow: { topLeft: '↘', top: '↓', topRight: '↙', right: '←', bottomRight: '↖', bottom: '↑', bottomLeft: '↗', left: '→' },
|
|
21
23
|
};
|
|
22
24
|
function terminalColumns() {
|
|
23
|
-
const { env, stdout, stderr } =
|
|
25
|
+
const { env, stdout, stderr } = rt;
|
|
24
26
|
if (stdout?.columns)
|
|
25
27
|
return stdout.columns;
|
|
26
28
|
if (stderr?.columns)
|
|
@@ -173,8 +175,8 @@ function boxContent(content, contentWidth, options) {
|
|
|
173
175
|
return result;
|
|
174
176
|
}
|
|
175
177
|
function sanitizeOptions(options) {
|
|
176
|
-
if (options.fullscreen !== undefined && options.fullscreen !== false &&
|
|
177
|
-
let dimensions = [
|
|
178
|
+
if (options.fullscreen !== undefined && options.fullscreen !== false && rt.stdout) {
|
|
179
|
+
let dimensions = [rt.stdout.columns, rt.stdout.rows];
|
|
178
180
|
if (typeof options.fullscreen === 'function')
|
|
179
181
|
dimensions = options.fullscreen(...dimensions);
|
|
180
182
|
options.width ||= dimensions[0];
|
package/dist/cli-table3.d.ts
CHANGED
|
@@ -4,7 +4,18 @@ export declare function strlen(str: unknown): number;
|
|
|
4
4
|
export declare function pad(str: string, len: number, padChar: string, dir?: string): string;
|
|
5
5
|
export declare function truncate(str: string, desiredLength: number, truncateChar?: string): string;
|
|
6
6
|
export declare function wordWrap(maxLength: number, input: string, wrapOnWordBoundary?: boolean): string[];
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* OSC 8 — the terminal hyperlink escape, and **not this package's to spell** (R12). The
|
|
9
|
+
* sequence is paratext's `link` capability rendered against {@link EMITTING}; this function
|
|
10
|
+
* contributes the argument order and upstream's `url || text`, and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* Upstream's `hyperlink()` is an escape *builder*, not a policy decision: `utils-test.js`
|
|
13
|
+
* grades its exact bytes with no terminal anywhere in the call, and a cell given an `href`
|
|
14
|
+
* gets a sequence whatever `process.stdout` is. So this stays unconditional — the caller
|
|
15
|
+
* already decided — and the runtime handed to paratext says so out loud. `flagstaff/table`
|
|
16
|
+
* is the surface that *asks* whether the terminal can (rule 6); a façade that started asking
|
|
17
|
+
* would be reinterpreting its host, and `link.test.ts` pins these bytes against upstream's.
|
|
18
|
+
*/
|
|
8
19
|
export declare function hyperlink(url: string, text: string): string;
|
|
9
20
|
export interface TableChars {
|
|
10
21
|
[name: string]: string;
|
package/dist/cli-table3.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { measure } from 'linegauge';
|
|
2
|
+
import { linkFor } from 'paratext/link';
|
|
2
3
|
import chalk from 'roundel/chalk';
|
|
3
4
|
const ESC = '\u001B';
|
|
4
5
|
const SGR = /\u001B\[(?:\d*;){0,5}\d*m/g;
|
|
5
6
|
const SGR_CAPTURE = /\u001B\[((?:\d*;){0,5}\d*)m/g;
|
|
6
|
-
const HYPERLINK_TAG = `${ESC}]8;;\u0007`;
|
|
7
7
|
const HALF = 2;
|
|
8
|
+
const EMITTING = { env: { WT_SESSION: '1' }, isTTY: { stdout: true } };
|
|
9
|
+
const emitLink = linkFor(EMITTING);
|
|
10
|
+
const EMPTY_LINK = emitLink('', '');
|
|
11
|
+
const HYPERLINK_TAG = EMPTY_LINK.slice(0, EMPTY_LINK.length / HALF);
|
|
8
12
|
export function strlen(str) {
|
|
9
13
|
const stripped = String(str).replaceAll(SGR, '');
|
|
10
14
|
return stripped.split('\n').reduce((memo, s) => Math.max(memo, measure(s)), 0);
|
|
@@ -209,9 +213,7 @@ function colorizeLines(input) {
|
|
|
209
213
|
});
|
|
210
214
|
}
|
|
211
215
|
export function hyperlink(url, text) {
|
|
212
|
-
|
|
213
|
-
const BEL = '\u0007';
|
|
214
|
-
return [OSC, '8', ';', ';', url || text, BEL, text, OSC, '8', ';', ';', BEL].join('');
|
|
216
|
+
return emitLink(text, url || text);
|
|
215
217
|
}
|
|
216
218
|
const DEBUG_LEVEL = { WARN: 1, INFO: 2, DEBUG: 3 };
|
|
217
219
|
let debugMessages = [];
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,9 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { hoist, manualClock } from './loop.js';
|
|
5
5
|
import { PluginError, register, registered } from './plugin.js';
|
|
6
|
+
import { processRuntime } from './runtime.js';
|
|
6
7
|
import { spinner } from './spinner.js';
|
|
8
|
+
const rt = processRuntime();
|
|
7
9
|
const MODES = ['tty', 'pipe', 'ci', 'json', 'accessible'];
|
|
8
10
|
const ENV = { tty: {}, pipe: {}, ci: { CI: 'true' }, json: {}, accessible: { CLI_ACCESSIBLE: '1' } };
|
|
9
11
|
const LABEL_WIDTH = 12;
|
|
@@ -118,11 +120,11 @@ async function main(argv, write) {
|
|
|
118
120
|
write(`${name}: ok${before.has(name) ? ' (replaces an earlier registration)' : ''}\n`);
|
|
119
121
|
return EXIT_OK;
|
|
120
122
|
}
|
|
121
|
-
const [, , command, ...rest] =
|
|
123
|
+
const [, , command, ...rest] = rt.argv;
|
|
122
124
|
const args = command === 'check' ? rest : [command, ...rest].filter((a) => a !== undefined);
|
|
123
|
-
main(args, (s) =>
|
|
124
|
-
|
|
125
|
+
main(args, (s) => rt.stdout.write(s)).then((code) => {
|
|
126
|
+
rt.exitCode = code;
|
|
125
127
|
}, (e) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
rt.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
|
|
129
|
+
rt.exitCode = EXIT_RUNTIME;
|
|
128
130
|
});
|
package/dist/link.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place this package reaches OSC 8, and it does not implement it (R12).
|
|
3
|
+
*
|
|
4
|
+
* **Why `paratext/link` and not `paratext`.** The root entry reaches 20,221 B across ten
|
|
5
|
+
* modules and calls `registerBuiltins()` at import — a side effect, in a package that declares
|
|
6
|
+
* `sideEffects: false` so a bundler may drop what a program does not use. `paratext/link` is
|
|
7
|
+
* **2,410 B across four modules and registers nothing**, and it carries the whole of what the
|
|
8
|
+
* built-ins need: the guess (`supportsLink`), the bytes (`linkFor`) and the capability record.
|
|
9
|
+
* The 17,811 B difference would land on `flagstaff/table`, whose entire budget is 4,000 B, so
|
|
10
|
+
* this was not a close call — but it was measured rather than assumed, which is the rule.
|
|
11
|
+
* What the narrow entry gives up is the registry: a host that re-registered `link` to correct
|
|
12
|
+
* paratext's guess does not change what a box or a table emits. A program that needs that
|
|
13
|
+
* imports `paratext` and calls `emit()` itself.
|
|
14
|
+
*
|
|
15
|
+
* **What this module is allowed to decide: nothing.** `supportsLink` is asked, never
|
|
16
|
+
* re-derived — `box.ts` and `table.ts` read `live` to choose a *layout* (a url that is about
|
|
17
|
+
* to be carried by an escape must not also be carried by the columns), and `paint` for the
|
|
18
|
+
* bytes. Neither file names `isTTY`, `TERM`, or an escape. PRINCIPLES rule 6 is therefore
|
|
19
|
+
* paratext's to keep, and `link.test.ts` checks it was kept.
|
|
20
|
+
*/
|
|
21
|
+
import { linkFor } from 'paratext/link';
|
|
22
|
+
/**
|
|
23
|
+
* The slice of the world paratext needs in order to answer. Derived from `linkFor` rather
|
|
24
|
+
* than imported: `paratext/link` publishes the functions, not the type, and a second
|
|
25
|
+
* specifier for a type would put `paratext` in this package's graph for something
|
|
26
|
+
* `verbatimModuleSyntax` erases anyway.
|
|
27
|
+
*/
|
|
28
|
+
export type Terminal = Parameters<typeof linkFor>[0];
|
|
29
|
+
/**
|
|
30
|
+
* The real process, narrowed to what paratext asks of it. Read through this package's own
|
|
31
|
+
* seam (Y9), so `runtime.ts` stays the one file here that names the process and a caller
|
|
32
|
+
* that passes its own `terminal` reaches no global at all.
|
|
33
|
+
*/
|
|
34
|
+
export declare const terminalRuntime: () => Terminal;
|
|
35
|
+
/**
|
|
36
|
+
* The runtime a **static projection** is defined against: no terminal, so paratext renders
|
|
37
|
+
* the fallback. `static()` is the text a pipe, a log, an agent and a screen reader get (R1),
|
|
38
|
+
* which is the same question `linkFor` answers for a runtime that is not a terminal — so it
|
|
39
|
+
* is asked rather than answered here, and `text (url)` stays a string paratext owns.
|
|
40
|
+
*/
|
|
41
|
+
export declare const STATIC: Terminal;
|
|
42
|
+
export interface Painter {
|
|
43
|
+
/**
|
|
44
|
+
* Whether the sequence is going to be emitted — paratext's answer, for a caller deciding
|
|
45
|
+
* *layout*. Off a terminal the url is content and belongs in the columns; on one it is
|
|
46
|
+
* carried by an escape that measures zero.
|
|
47
|
+
*/
|
|
48
|
+
readonly live: boolean;
|
|
49
|
+
/** `(text, url) => string`. The bytes, whichever branch holds. */
|
|
50
|
+
readonly paint: (text: string, url: string) => string;
|
|
51
|
+
}
|
|
52
|
+
/** Bind both questions to one runtime, once per drawing. */
|
|
53
|
+
export declare function painter(terminal?: Terminal): Painter;
|
|
54
|
+
/**
|
|
55
|
+
* A cell or a body with a destination attached. `href` is a url or a path — `file://` for a
|
|
56
|
+
* path a terminal should be able to open, which is the case R12 is named after.
|
|
57
|
+
*/
|
|
58
|
+
export interface Linked {
|
|
59
|
+
readonly text: string;
|
|
60
|
+
readonly href: string;
|
|
61
|
+
}
|
|
62
|
+
/** A table cell: text, or text with somewhere to go. A plain string stays a plain string. */
|
|
63
|
+
export type Cell = string | Linked;
|
|
64
|
+
export declare const cellText: (cell: Cell | undefined) => string;
|
|
65
|
+
export declare const cellHref: (cell: Cell | undefined) => string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* What the content reads as **before** the drawing measures it: the bare text where the
|
|
68
|
+
* sequence will carry the destination, paratext's fallback where it will not. This is the
|
|
69
|
+
* only consequence `live` has on layout, and it is the whole of it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function laid(text: string, href: string | undefined, painted: Painter): string;
|
|
72
|
+
/**
|
|
73
|
+
* The same content once it has been wrapped and is about to be padded. A line that survived
|
|
74
|
+
* wrapping gets the sequence; an empty one does not, because a link around nothing is an
|
|
75
|
+
* escape a terminal still has to parse and a screen reader still has to skip.
|
|
76
|
+
*/
|
|
77
|
+
export declare function painted(line: string, href: string | undefined, painter_: Painter): string;
|
package/dist/link.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { linkFor, supportsLink } from 'paratext/link';
|
|
2
|
+
import { processRuntime } from './runtime.js';
|
|
3
|
+
export const terminalRuntime = () => {
|
|
4
|
+
const runtime = processRuntime();
|
|
5
|
+
return { env: runtime.env, isTTY: { stdout: runtime.stdout.isTTY === true } };
|
|
6
|
+
};
|
|
7
|
+
export const STATIC = { env: {}, isTTY: { stdout: false } };
|
|
8
|
+
export function painter(terminal) {
|
|
9
|
+
const runtime = terminal ?? terminalRuntime();
|
|
10
|
+
return { live: supportsLink(runtime), paint: linkFor(runtime) };
|
|
11
|
+
}
|
|
12
|
+
export const cellText = (cell) => (typeof cell === 'string' ? cell : (cell?.text ?? ''));
|
|
13
|
+
export const cellHref = (cell) => (typeof cell === 'string' || cell === undefined ? undefined : cell.href);
|
|
14
|
+
export function laid(text, href, painted) {
|
|
15
|
+
if (href === undefined || href === '')
|
|
16
|
+
return text;
|
|
17
|
+
return painted.live ? text : painted.paint(text, href);
|
|
18
|
+
}
|
|
19
|
+
export function painted(line, href, painter_) {
|
|
20
|
+
if (!painter_.live || href === undefined || href === '' || line === '')
|
|
21
|
+
return line;
|
|
22
|
+
return painter_.paint(line, href);
|
|
23
|
+
}
|
package/dist/log-update.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** What log-update writes to:
|
|
1
|
+
/** What log-update writes to: the runtime's stdout by default, anything stream-shaped in a test. */
|
|
2
2
|
export interface LogUpdateStream {
|
|
3
3
|
write(chunk: string): unknown;
|
|
4
4
|
columns?: number;
|
|
@@ -22,7 +22,7 @@ export interface LogUpdate {
|
|
|
22
22
|
/** Replace the frame with text that stays, unclipped, and start fresh after it. */
|
|
23
23
|
persist(...text: unknown[]): void;
|
|
24
24
|
}
|
|
25
|
-
/** A renderer bound to one stream. `logUpdate` is this over
|
|
25
|
+
/** A renderer bound to one stream. `logUpdate` is this over the runtime's stdout. */
|
|
26
26
|
export declare function createLogUpdate(stream: LogUpdateStream, { showCursor: keepCursor, defaultWidth, defaultHeight }?: LogUpdateOptions): LogUpdate;
|
|
27
27
|
declare const logUpdate: LogUpdate;
|
|
28
28
|
export declare const logUpdateStderr: LogUpdate;
|
package/dist/log-update.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { HIDE_CURSOR, SHOW_CURSOR } from 'closeout/cursor';
|
|
2
|
+
import restoreCursor from 'closeout/restore-cursor';
|
|
2
3
|
import { wrap } from 'linegauge/wrap';
|
|
3
|
-
import {
|
|
4
|
+
import { processRuntime } from './runtime.js';
|
|
5
|
+
const rt = processRuntime();
|
|
4
6
|
const CSI = '\u001B[';
|
|
5
7
|
const SYNCHRONIZED_OUTPUT_ENABLE = `${CSI}?2026h`;
|
|
6
8
|
const SYNCHRONIZED_OUTPUT_DISABLE = `${CSI}?2026l`;
|
|
@@ -18,15 +20,15 @@ const eraseLines = (count) => {
|
|
|
18
20
|
return count > 0 ? sequence + CURSOR_LEFT : sequence;
|
|
19
21
|
};
|
|
20
22
|
function hideCursor() {
|
|
21
|
-
if (
|
|
23
|
+
if (rt.stderr.isTTY !== true)
|
|
22
24
|
return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
restoreCursor();
|
|
26
|
+
rt.stderr.write(HIDE_CURSOR);
|
|
25
27
|
}
|
|
26
28
|
function showCursor() {
|
|
27
|
-
if (
|
|
29
|
+
if (rt.stderr.isTTY !== true)
|
|
28
30
|
return;
|
|
29
|
-
|
|
31
|
+
rt.stderr.write(SHOW_CURSOR);
|
|
30
32
|
}
|
|
31
33
|
const countLines = (text) => text.split('\n').length;
|
|
32
34
|
function fitToHeight(wrapped, terminalHeight) {
|
|
@@ -168,6 +170,6 @@ export function createLogUpdate(stream, { showCursor: keepCursor = false, defaul
|
|
|
168
170
|
};
|
|
169
171
|
return render;
|
|
170
172
|
}
|
|
171
|
-
const logUpdate = createLogUpdate(
|
|
172
|
-
export const logUpdateStderr = createLogUpdate(
|
|
173
|
+
const logUpdate = createLogUpdate(rt.stdout);
|
|
174
|
+
export const logUpdateStderr = createLogUpdate(rt.stderr);
|
|
173
175
|
export default logUpdate;
|
package/dist/ora.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface SpinnerDefinition {
|
|
|
5
5
|
/** Every spinner cli-spinners ships, unedited — ora re-exports it and so do we. */
|
|
6
6
|
export declare const spinners: Record<string, SpinnerDefinition>;
|
|
7
7
|
export type Color = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray';
|
|
8
|
-
/** What ora writes to:
|
|
8
|
+
/** What ora writes to: the runtime's stderr by default, anything stream-shaped in a test. */
|
|
9
9
|
export interface OraStream {
|
|
10
10
|
write(chunk: string, encoding?: unknown, callback?: unknown): boolean;
|
|
11
11
|
isTTY?: boolean;
|
package/dist/ora.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import
|
|
2
|
+
import { HIDE_CURSOR, SHOW_CURSOR } from 'closeout/cursor';
|
|
3
|
+
import restoreCursor from 'closeout/restore-cursor';
|
|
3
4
|
import { lineCount } from 'linegauge';
|
|
4
5
|
import chalk from 'roundel/chalk';
|
|
5
|
-
import {
|
|
6
|
+
import { processRuntime } from './runtime.js';
|
|
6
7
|
import spinnerCorpus from './spinners.json' with { type: 'json' };
|
|
8
|
+
const rt = processRuntime();
|
|
7
9
|
export const spinners = spinnerCorpus;
|
|
8
10
|
function isUnicodeSupported() {
|
|
9
|
-
const { env } =
|
|
11
|
+
const { env } = rt;
|
|
10
12
|
const { TERM, TERM_PROGRAM } = env;
|
|
11
|
-
if (
|
|
13
|
+
if (rt.platform !== 'win32')
|
|
12
14
|
return TERM !== 'linux';
|
|
13
15
|
return (Boolean(env['WT_SESSION']) ||
|
|
14
16
|
Boolean(env['TERMINUS_SUBLIME']) ||
|
|
@@ -22,7 +24,7 @@ function isUnicodeSupported() {
|
|
|
22
24
|
env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm');
|
|
23
25
|
}
|
|
24
26
|
function isInteractive(stream) {
|
|
25
|
-
return Boolean(stream?.isTTY) &&
|
|
27
|
+
return Boolean(stream?.isTTY) && rt.env['TERM'] !== 'dumb' && !('CI' in rt.env);
|
|
26
28
|
}
|
|
27
29
|
const UNICODE = isUnicodeSupported();
|
|
28
30
|
const logSymbols = {
|
|
@@ -43,7 +45,7 @@ class StdinDiscarder {
|
|
|
43
45
|
return;
|
|
44
46
|
const code = typeof chunk === 'string' ? chunk.codePointAt(0) : chunk[0];
|
|
45
47
|
if (code === ASCII_ETX_CODE)
|
|
46
|
-
|
|
48
|
+
rt.kill(rt.pid, 'SIGINT');
|
|
47
49
|
};
|
|
48
50
|
start() {
|
|
49
51
|
this.#activeCount += 1;
|
|
@@ -58,8 +60,8 @@ class StdinDiscarder {
|
|
|
58
60
|
this.#realStop();
|
|
59
61
|
}
|
|
60
62
|
#realStart() {
|
|
61
|
-
const stdin =
|
|
62
|
-
if (
|
|
63
|
+
const stdin = rt.stdin;
|
|
64
|
+
if (rt.platform === 'win32' || stdin?.isTTY !== true || typeof stdin.setRawMode !== 'function') {
|
|
63
65
|
this.#stdin = undefined;
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
@@ -115,7 +117,7 @@ export class Ora {
|
|
|
115
117
|
#color;
|
|
116
118
|
constructor(options) {
|
|
117
119
|
const given = typeof options === 'string' ? { text: options } : options;
|
|
118
|
-
this.#options = { color: 'cyan', stream:
|
|
120
|
+
this.#options = { color: 'cyan', stream: rt.stderr, discardStdin: true, hideCursor: true, isEnabled: false, isSilent: false, indent: 0, ...given };
|
|
119
121
|
this.color = this.#options.color;
|
|
120
122
|
this.#stream = this.#options.stream;
|
|
121
123
|
if (typeof given?.isEnabled !== 'boolean')
|
|
@@ -132,7 +134,7 @@ export class Ora {
|
|
|
132
134
|
this.prefixText = this.#options.prefixText;
|
|
133
135
|
this.suffixText = this.#options.suffixText;
|
|
134
136
|
this.indent = this.#options.indent;
|
|
135
|
-
if (
|
|
137
|
+
if (rt.env['NODE_ENV'] === 'test')
|
|
136
138
|
this.#exposeTestProperties();
|
|
137
139
|
}
|
|
138
140
|
#exposeTestProperties() {
|
|
@@ -319,7 +321,7 @@ export class Ora {
|
|
|
319
321
|
return this;
|
|
320
322
|
if (this.#options.hideCursor)
|
|
321
323
|
this.#hideCursor();
|
|
322
|
-
if (this.#options.discardStdin &&
|
|
324
|
+
if (this.#options.discardStdin && rt.stdin.isTTY) {
|
|
323
325
|
stdinDiscarder.start();
|
|
324
326
|
this.#isDiscardingStdin = true;
|
|
325
327
|
}
|
|
@@ -390,7 +392,7 @@ export class Ora {
|
|
|
390
392
|
#hideCursor() {
|
|
391
393
|
if (this.#stream.isTTY !== true)
|
|
392
394
|
return;
|
|
393
|
-
|
|
395
|
+
restoreCursor();
|
|
394
396
|
this.#stream.write(HIDE_CURSOR);
|
|
395
397
|
}
|
|
396
398
|
#showCursor() {
|
|
@@ -434,7 +436,7 @@ export class Ora {
|
|
|
434
436
|
#installHook() {
|
|
435
437
|
if (!this.isEnabled || this.#hookedStreams.size > 0)
|
|
436
438
|
return;
|
|
437
|
-
for (const stream of new Set([this.#stream,
|
|
439
|
+
for (const stream of new Set([this.#stream, rt.stdout, rt.stderr]))
|
|
438
440
|
this.#hookStream(stream);
|
|
439
441
|
}
|
|
440
442
|
#hookStream(stream) {
|
package/dist/plugin.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface Plugin {
|
|
|
60
60
|
* the lock has to read the source to work across the family, and it does. A `const` array
|
|
61
61
|
* would therefore buy nothing and cost ~195 B on `./spinner`, which has 82 B of headroom.
|
|
62
62
|
*/
|
|
63
|
-
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_NO_STATIC_PROJECTION' | 'E_PLUGIN_CONTRACT' | 'E_UNKNOWN_SPINNER' | 'E_UNKNOWN_BORDER' | 'E_NO_CONTRIBUTION' | 'E_COMPONENT_THREW';
|
|
63
|
+
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_NO_STATIC_PROJECTION' | 'E_PLUGIN_CONTRACT' | 'E_UNKNOWN_SPINNER' | 'E_UNKNOWN_BORDER' | 'E_NO_CONTRIBUTION' | 'E_COMPONENT_THREW' | 'E_UNKNOWN_KIND';
|
|
64
64
|
/** A refused plugin says what is wrong, where, and what to do about it. */
|
|
65
65
|
export declare class PluginError extends Error {
|
|
66
66
|
readonly code: PluginErrorCode;
|
package/dist/projection.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* The per-mode writers (R1). This is the only module in the package that emits a cursor
|
|
3
|
-
* operation, and it does so only in `tty`; every other projection is text and a newline,
|
|
4
|
-
* which is what R5 greps for. Streams and time come in as arguments — nothing here knows
|
|
5
|
-
* `process` exists. The three sequences are written by hand rather than through
|
|
6
|
-
* `node:readline`, whose helpers want a `Writable` where the loop only has a `Writer`.
|
|
7
|
-
*/
|
|
8
|
-
import { HIDE_CURSOR, SHOW_CURSOR } from './cursor.js';
|
|
1
|
+
import { HIDE_CURSOR, SHOW_CURSOR } from 'closeout/cursor';
|
|
9
2
|
import { type Component } from './plugin.js';
|
|
10
3
|
export interface Writer {
|
|
11
4
|
write(chunk: string): unknown;
|
package/dist/projection.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { onExit } from 'closeout';
|
|
2
|
+
import { HIDE_CURSOR, SHOW_CURSOR } from 'closeout/cursor';
|
|
2
3
|
export const DEFAULT_INTERVAL = 80;
|
|
3
4
|
const CSI = '\u001B[';
|
|
4
5
|
const NO_NET = () => undefined;
|
|
@@ -27,7 +28,7 @@ class TtyProjection {
|
|
|
27
28
|
open(state) {
|
|
28
29
|
this.#current = state;
|
|
29
30
|
this.#out.write(HIDE_CURSOR);
|
|
30
|
-
this.#dropCursorNet =
|
|
31
|
+
this.#dropCursorNet = onExit(() => void this.#out.write(SHOW_CURSOR), { phase: 'restore' });
|
|
31
32
|
this.#paint();
|
|
32
33
|
if (this.#component.frame !== undefined)
|
|
33
34
|
this.#cancel = this.#clock.schedule(() => this.#repaint(), this.#interval);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Only the members this package actually reads. Anything not named here — `process.exit`,
|
|
3
|
+
* `cwd`, `chdir`, `emit` — is unreachable through the seam by construction, because the
|
|
4
|
+
* declared type is this and not `NodeJS.Process`.
|
|
5
|
+
*/
|
|
6
|
+
export interface Runtime {
|
|
7
|
+
readonly env: NodeJS.ProcessEnv;
|
|
8
|
+
readonly argv: string[];
|
|
9
|
+
readonly platform: NodeJS.Platform;
|
|
10
|
+
readonly pid: number;
|
|
11
|
+
readonly stdin: NodeJS.ReadStream & {
|
|
12
|
+
fd: 0;
|
|
13
|
+
};
|
|
14
|
+
readonly stdout: NodeJS.WriteStream & {
|
|
15
|
+
fd: 1;
|
|
16
|
+
};
|
|
17
|
+
readonly stderr: NodeJS.WriteStream & {
|
|
18
|
+
fd: 2;
|
|
19
|
+
};
|
|
20
|
+
/** Set by `cli.ts` on the way out. Typed as node types it, string included. */
|
|
21
|
+
exitCode?: number | string | null | undefined;
|
|
22
|
+
kill(pid: number, signal: NodeJS.Signals): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The real process, narrowed. Nothing is read here: every access through the returned
|
|
26
|
+
* `Runtime` reaches the live process at the moment the caller makes it, which is what keeps
|
|
27
|
+
* `boxen`'s width lazy and lets ora's suite swap `process.kill` under a running spinner.
|
|
28
|
+
*/
|
|
29
|
+
export declare const processRuntime: () => Runtime;
|
package/dist/runtime.js
ADDED
package/dist/schema.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}}}}
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]}}}
|
package/dist/table.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type Cell, type Terminal } from './link.js';
|
|
1
2
|
import { type Component } from './plugin.js';
|
|
2
|
-
|
|
3
|
+
/** A row of cells. A plain `string[]` is still a row — a destination is opt-in per cell. */
|
|
4
|
+
export type Row = Cell[];
|
|
3
5
|
export interface TableOptions {
|
|
4
6
|
/** Column headers. Omitted, the table is drawn without a header row. */
|
|
5
7
|
head?: string[];
|
|
@@ -7,6 +9,12 @@ export interface TableOptions {
|
|
|
7
9
|
width?: number;
|
|
8
10
|
/** Alignment per column; `left` for any column not named. */
|
|
9
11
|
align?: ('left' | 'right')[];
|
|
12
|
+
/**
|
|
13
|
+
* The terminal a linked cell is rendered for. Omitted, the real process is read through
|
|
14
|
+
* this package's seam. Supply one and the whole path is pure — which is how `link.test.ts`
|
|
15
|
+
* grades both branches without a terminal in sight.
|
|
16
|
+
*/
|
|
17
|
+
terminal?: Terminal;
|
|
10
18
|
}
|
|
11
19
|
/** Draw `rows` as a table, as a string. */
|
|
12
20
|
export declare function table(rows: Row[], options?: TableOptions): string;
|
|
@@ -16,3 +24,5 @@ export interface TableState {
|
|
|
16
24
|
}
|
|
17
25
|
/** A table as a component: `header: value` pairs off a terminal, the grid on one (R1). */
|
|
18
26
|
export declare function tableComponent(options?: TableOptions): Component<TableState>;
|
|
27
|
+
/** The cell shapes, re-exported so a caller of `flagstaff/table` can name them (R12). */
|
|
28
|
+
export type { Cell, Linked } from './link.js';
|
package/dist/table.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { width } from 'linegauge';
|
|
2
2
|
import { wrap } from 'linegauge/wrap';
|
|
3
3
|
import { heading, muted } from 'roundel/tokens';
|
|
4
|
+
import { cellHref, cellText, laid, painted, painter, STATIC } from './link.js';
|
|
4
5
|
const DEFAULT_WIDTH = 80;
|
|
5
6
|
const CHROME_PER_COLUMN = 3;
|
|
6
7
|
const CHROME_FIXED = 1;
|
|
@@ -8,15 +9,15 @@ const MIN_COLUMN = 3;
|
|
|
8
9
|
const V = '│';
|
|
9
10
|
const H = '─';
|
|
10
11
|
const CORNERS = { topLeft: '┌', topRight: '┐', bottomLeft: '└', bottomRight: '┘', top: '┬', bottom: '┴', left: '├', right: '┤', cross: '┼' };
|
|
11
|
-
const padTo = (line, cells, align) => {
|
|
12
|
-
const gap = ' '.repeat(Math.max(0, cells - width(
|
|
12
|
+
const padTo = (line, cells, align, measured = line) => {
|
|
13
|
+
const gap = ' '.repeat(Math.max(0, cells - width(measured)));
|
|
13
14
|
return align === 'right' ? gap + line : line + gap;
|
|
14
15
|
};
|
|
15
|
-
function naturalWidths(rows, columns) {
|
|
16
|
+
function naturalWidths(rows, columns, paint) {
|
|
16
17
|
const widths = Array.from({ length: columns }, () => 0);
|
|
17
18
|
for (const row of rows) {
|
|
18
19
|
for (let index = 0; index < columns; index += 1)
|
|
19
|
-
widths[index] = Math.max(widths[index] ?? 0, width(row[index]
|
|
20
|
+
widths[index] = Math.max(widths[index] ?? 0, width(laid(cellText(row[index]), cellHref(row[index]), paint)));
|
|
20
21
|
}
|
|
21
22
|
return widths;
|
|
22
23
|
}
|
|
@@ -35,10 +36,15 @@ function fitWidths(natural, available) {
|
|
|
35
36
|
}
|
|
36
37
|
return widths;
|
|
37
38
|
}
|
|
38
|
-
function layoutRow(row, widths, align) {
|
|
39
|
-
const cells = widths.map((w, index) => wrap(row[index]
|
|
39
|
+
function layoutRow(row, widths, align, paint) {
|
|
40
|
+
const cells = widths.map((w, index) => wrap(laid(cellText(row[index]), cellHref(row[index]), paint), w, { hard: true, trim: false }).split('\n'));
|
|
40
41
|
const height = Math.max(1, ...cells.map((lines) => lines.length));
|
|
41
|
-
return Array.from({ length: height }, (_, line) => `${V} ${widths
|
|
42
|
+
return Array.from({ length: height }, (_, line) => `${V} ${widths
|
|
43
|
+
.map((w, index) => {
|
|
44
|
+
const plain = cells[index]?.[line] ?? '';
|
|
45
|
+
return padTo(painted(plain, cellHref(row[index]), paint), w, align[index] ?? 'left', plain);
|
|
46
|
+
})
|
|
47
|
+
.join(` ${V} `)} ${V}`);
|
|
42
48
|
}
|
|
43
49
|
const rule = (widths, left, mid, right) => left + widths.map((w) => H.repeat(w + 2)).join(mid) + right;
|
|
44
50
|
export function table(rows, options = {}) {
|
|
@@ -46,23 +52,28 @@ export function table(rows, options = {}) {
|
|
|
46
52
|
const total = options.width ?? DEFAULT_WIDTH;
|
|
47
53
|
const align = options.align ?? [];
|
|
48
54
|
const available = Math.max(columns * MIN_COLUMN, total - columns * CHROME_PER_COLUMN - CHROME_FIXED);
|
|
55
|
+
const paint = painter(options.terminal);
|
|
49
56
|
const all = options.head === undefined ? rows : [options.head, ...rows];
|
|
50
|
-
const widths = fitWidths(naturalWidths(all, columns), available);
|
|
57
|
+
const widths = fitWidths(naturalWidths(all, columns, paint), available);
|
|
51
58
|
const out = [rule(widths, CORNERS.topLeft, CORNERS.top, CORNERS.topRight)];
|
|
52
59
|
if (options.head !== undefined) {
|
|
53
|
-
out.push(...layoutRow(options.head.map((cell) => heading(cell)), widths, align), rule(widths, CORNERS.left, CORNERS.cross, CORNERS.right));
|
|
60
|
+
out.push(...layoutRow(options.head.map((cell) => heading(cell)), widths, align, paint), rule(widths, CORNERS.left, CORNERS.cross, CORNERS.right));
|
|
54
61
|
}
|
|
55
62
|
for (const row of rows)
|
|
56
|
-
out.push(...layoutRow(row, widths, align));
|
|
63
|
+
out.push(...layoutRow(row, widths, align, paint));
|
|
57
64
|
out.push(rule(widths, CORNERS.bottomLeft, CORNERS.bottom, CORNERS.bottomRight));
|
|
58
65
|
return out.join('\n');
|
|
59
66
|
}
|
|
60
67
|
export function tableComponent(options = {}) {
|
|
68
|
+
const plain = painter(STATIC);
|
|
61
69
|
return {
|
|
62
70
|
name: 'table',
|
|
63
71
|
static: (state) => {
|
|
64
72
|
const head = state.head ?? options.head;
|
|
65
|
-
|
|
73
|
+
const show = (cell) => laid(cellText(cell), cellHref(cell), plain);
|
|
74
|
+
return state.rows
|
|
75
|
+
.map((row) => (head === undefined ? row.map((cell) => show(cell)).join('\t') : row.map((cell, index) => `${head[index] ?? index}: ${show(cell)}`).join(', ')))
|
|
76
|
+
.join('\n');
|
|
66
77
|
},
|
|
67
78
|
frame: (_t, state) => muted(table(state.rows, state.head === undefined ? options : { ...options, head: state.head })),
|
|
68
79
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagstaff",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The staff the flag flies from. A terminal frame loop with a static projection for agents and screen readers, and a plugin host for spinners, progress, boxes and tables. Drop-in paths for ora, log-update, boxen and cli-table3. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -115,10 +115,13 @@
|
|
|
115
115
|
"plugins"
|
|
116
116
|
],
|
|
117
117
|
"dependencies": {
|
|
118
|
-
"
|
|
119
|
-
"
|
|
118
|
+
"closeout": "^0.2.0",
|
|
119
|
+
"linegauge": "^0.3.0",
|
|
120
|
+
"paratext": "^0.3.0",
|
|
121
|
+
"roundel": "^0.3.1"
|
|
120
122
|
},
|
|
121
123
|
"devDependencies": {
|
|
124
|
+
"fast-check": "^4.10.0",
|
|
122
125
|
"vitest": "^5.0.0"
|
|
123
126
|
}
|
|
124
127
|
}
|
package/dist/cursor.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export declare const HIDE_CURSOR = "\u001B[?25l";
|
|
2
|
-
export declare const SHOW_CURSOR = "\u001B[?25h";
|
|
3
|
-
/**
|
|
4
|
-
* Installed once, the first time a cursor is hidden; puts it back however the process dies.
|
|
5
|
-
*
|
|
6
|
-
* `write` is how the caller's own surface reaches the terminal, and passing it is what makes
|
|
7
|
-
* this usable from the core. `hoist()` already knows its stream — the Runtime gave it one and
|
|
8
|
-
* the output mode was decided from it once (R1/U2) — so it must not be re-detected here: a
|
|
9
|
-
* second detector is precisely what the policy exists to prevent. The façades pass nothing
|
|
10
|
-
* and get the detection below, because that is their incumbents' contract — ora and
|
|
11
|
-
* log-update restore *the process's* cursor whichever stream the caller handed them.
|
|
12
|
-
*/
|
|
13
|
-
export declare function restoreCursorOnExit(write?: (s: string) => void): () => void;
|
package/dist/cursor.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { constants } from 'node:os';
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
export const HIDE_CURSOR = '\u001B[?25l';
|
|
4
|
-
export const SHOW_CURSOR = '\u001B[?25h';
|
|
5
|
-
const TERMINATION_SIGNALS = ['SIGHUP', 'SIGINT', 'SIGTERM', 'SIGBREAK'].filter((signal) => signal in constants.signals);
|
|
6
|
-
function terminalStream() {
|
|
7
|
-
if (process.stderr.isTTY)
|
|
8
|
-
return process.stderr;
|
|
9
|
-
if (process.stdout.isTTY)
|
|
10
|
-
return process.stdout;
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
let cursorRestoreInstalled = false;
|
|
14
|
-
const NOTHING_TO_UNDO = () => undefined;
|
|
15
|
-
export function restoreCursorOnExit(write) {
|
|
16
|
-
if (cursorRestoreInstalled)
|
|
17
|
-
return NOTHING_TO_UNDO;
|
|
18
|
-
cursorRestoreInstalled = true;
|
|
19
|
-
const terminal = write === undefined ? terminalStream() : { write };
|
|
20
|
-
if (terminal === undefined)
|
|
21
|
-
return NOTHING_TO_UNDO;
|
|
22
|
-
let restored = false;
|
|
23
|
-
const restore = () => {
|
|
24
|
-
if (restored)
|
|
25
|
-
return;
|
|
26
|
-
restored = true;
|
|
27
|
-
terminal.write(SHOW_CURSOR);
|
|
28
|
-
};
|
|
29
|
-
const installed = new Map();
|
|
30
|
-
const uninstall = () => {
|
|
31
|
-
for (const [name, fn] of installed)
|
|
32
|
-
process.removeListener(name, fn);
|
|
33
|
-
installed.clear();
|
|
34
|
-
process.removeListener('exit', restore);
|
|
35
|
-
cursorRestoreInstalled = false;
|
|
36
|
-
};
|
|
37
|
-
for (const signal of TERMINATION_SIGNALS) {
|
|
38
|
-
const handler = () => {
|
|
39
|
-
restore();
|
|
40
|
-
uninstall();
|
|
41
|
-
if (process.listenerCount(signal) === 0)
|
|
42
|
-
process.kill(process.pid, signal);
|
|
43
|
-
};
|
|
44
|
-
process.on(signal, handler);
|
|
45
|
-
installed.set(signal, handler);
|
|
46
|
-
}
|
|
47
|
-
process.once('exit', restore);
|
|
48
|
-
return uninstall;
|
|
49
|
-
}
|