flagstaff 0.0.1 → 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/README.md +333 -20
- package/THIRD-PARTY.md +44 -0
- package/dist/box.d.ts +22 -0
- package/dist/box.js +59 -0
- package/dist/boxen.d.ts +56 -0
- package/dist/boxen.js +251 -0
- package/dist/builtins.d.ts +13 -0
- package/dist/builtins.js +19 -0
- package/dist/cli-table3.d.ts +138 -0
- package/dist/cli-table3.js +785 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +126 -0
- package/dist/cursor.d.ts +13 -0
- package/dist/cursor.js +49 -0
- package/dist/import.d.ts +50 -0
- package/dist/import.js +20 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +8 -7
- package/dist/log-update.d.ts +29 -0
- package/dist/log-update.js +173 -0
- package/dist/loop.d.ts +30 -0
- package/dist/loop.js +67 -0
- package/dist/ora.d.ts +82 -0
- package/dist/ora.js +515 -0
- package/dist/plugin.d.ts +107 -0
- package/dist/plugin.js +163 -0
- package/dist/progress.d.ts +19 -0
- package/dist/progress.js +25 -0
- package/dist/projection.d.ts +46 -0
- package/dist/projection.js +83 -0
- package/dist/schema.json +1 -0
- package/dist/spinner.d.ts +9 -0
- package/dist/spinner.js +24 -0
- package/dist/spinners.json +1 -0
- package/dist/table.d.ts +18 -0
- package/dist/table.js +69 -0
- package/dist/tasks.d.ts +18 -0
- package/dist/tasks.js +31 -0
- package/dist/width.d.ts +13 -0
- package/dist/width.js +86 -0
- package/dist/wrap.d.ts +10 -0
- package/dist/wrap.js +447 -0
- package/package.json +77 -4
package/README.md
CHANGED
|
@@ -1,25 +1,338 @@
|
|
|
1
1
|
# flagstaff
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
ora animates a spinner and, off a terminal, prints frames anyway — `\r` after `\r` into the
|
|
4
|
+
log an agent reads back. Ink fixes the terminal by shipping React and a layout engine.
|
|
5
|
+
**flagstaff** is the staff the flag flies from: a frame loop that hoists a component, holds
|
|
6
|
+
it, changes it and lowers it, and a **static projection** that is what every mode but the
|
|
7
|
+
terminal gets — one line per state on a pipe, one event per transition under `--json`,
|
|
8
|
+
plain text for a screen reader. Plugins are data. No layout engine. One dependency, and it
|
|
9
|
+
is [roundel](../roundel/README.md).
|
|
10
|
+
|
|
11
|
+
A **flagstaff** is the simplest part of the whole apparatus and the only one that is always
|
|
12
|
+
in view: a flag is hoisted on it, held there, changed, and lowered when it is done. That is a
|
|
13
|
+
terminal render loop — the place frames are hoisted, held, changed and lowered, in view of
|
|
14
|
+
whoever is reading.
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { hoist } from 'flagstaff/loop';
|
|
20
|
+
import { spinner } from 'flagstaff/spinner';
|
|
21
|
+
|
|
22
|
+
const flag = hoist(spinner(), rt, { text: 'building' });
|
|
23
|
+
// ... work ...
|
|
24
|
+
flag.update({ text: 'linking' });
|
|
25
|
+
flag.lower({ text: 'built', status: 'ok' });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`rt` is a runtime — `{ env, isTTY: { stdout }, stdout, stderr, clock }`; burgee's satisfies
|
|
29
|
+
it, so does a literal in a test. Which of the five modes runs is decided once by roundel's
|
|
30
|
+
output policy, never by this package:
|
|
31
|
+
|
|
32
|
+
| mode | what the same three calls write |
|
|
33
|
+
| :-- | :-- |
|
|
34
|
+
| `tty` | `⠋ building` repainted in place on the clock, then `✔ built` left on screen |
|
|
35
|
+
| `pipe`, `ci` | `… building` ⏎ `… linking` ⏎ `✔ built` — one line per state change, no `\r`, no escape |
|
|
36
|
+
| `json` | `{"event":"spinner","state":{"text":"building"}}` … one NDJSON event per transition, on stderr |
|
|
37
|
+
| `accessible` | the static text, never a redraw |
|
|
38
|
+
|
|
39
|
+
## What is here
|
|
40
|
+
|
|
41
|
+
### The loop
|
|
42
|
+
|
|
43
|
+
`hoist(component, rt, initial, { json })` returns `{ update(state), lower(state?), mode }`.
|
|
44
|
+
A component is `{ name, static(state), frame?(t, state), interval? }`. `static` is
|
|
45
|
+
required and is the artifact: what a pipe, an agent, a screen reader and the docs gallery
|
|
46
|
+
read. `frame` is optional and decorative. Nothing in the loop reads `process`; time comes
|
|
47
|
+
from `rt.clock`, so `manualClock()` makes a spinner's terminal output a fixed string a test
|
|
48
|
+
can assert byte for byte.
|
|
49
|
+
|
|
50
|
+
### The plugin host
|
|
51
|
+
|
|
52
|
+
A plugin is one plain object, validated against [`schema.json`](./src/schema.json) — the
|
|
53
|
+
same file that ships in the tarball — and registered once:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
// a third-party plugin, in full
|
|
57
|
+
export default {
|
|
58
|
+
name: 'nyan',
|
|
59
|
+
spinners: { nyan: { frames: ['≋', '≈', '~'], interval: 80, static: '…' } },
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { register } from 'flagstaff/plugin';
|
|
65
|
+
register(nyan);
|
|
66
|
+
spinner('nyan');
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Keys: `spinners`, `borders`, `glyphs` (`ok`, `fail`, `warn`, `info`, `running` — change them
|
|
70
|
+
and every built-in that draws one changes), `tokens` (a roundel theme), `components`. A spinner
|
|
71
|
+
or component without a `static` is refused at `register()` with `E_NO_STATIC_PROJECTION`
|
|
72
|
+
and a fix. The built-in `dots` and `line` styles are a plugin of exactly this shape,
|
|
73
|
+
registered through the same door, so the built-ins cannot grow an API a plugin cannot reach.
|
|
74
|
+
|
|
75
|
+
`register()` is the **only** way in, and that is a property rather than a convention:
|
|
76
|
+
`registered()` hands back a copy — new maps over the frozen objects `register()` stored — so
|
|
77
|
+
`registered().spinners.set(…)` puts nothing in the registry and `.clear()` empties nothing.
|
|
78
|
+
A contribution that never met `validate()` cannot be reached by `spinner()`, `box()` or the
|
|
79
|
+
gallery, which is what makes "refused at the door" (U3) a fact about the code rather than
|
|
80
|
+
advice. Freezing also means the object you registered stays yours: edit it afterwards and
|
|
81
|
+
the registry does not change.
|
|
82
|
+
|
|
83
|
+
A component may declare `sample: { running, done }` — the two states `flagstaff check` and
|
|
84
|
+
the docs gallery *show* it with. The loop never reads it; a running program's state comes
|
|
85
|
+
from the program. Without one, both assume `{ phase: 'running' }` / `{ phase: 'done' }` and
|
|
86
|
+
say so in the output, rather than rendering an invented state as if it were yours.
|
|
87
|
+
|
|
88
|
+
### The built-ins
|
|
89
|
+
|
|
90
|
+
Five components, each on its own subpath, each answering the static projection for itself:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { progress } from 'flagstaff/progress';
|
|
94
|
+
import { tasks } from 'flagstaff/tasks';
|
|
95
|
+
import { box, boxComponent } from 'flagstaff/box';
|
|
96
|
+
import { table, tableComponent } from 'flagstaff/table';
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| component | on a terminal | everywhere else |
|
|
100
|
+
| :-- | :-- | :-- |
|
|
101
|
+
| `spinner` | `⠹ building` | `… building`, then `✔ built` |
|
|
102
|
+
| `progress` | a bar of blocks | `12/30 files · 40%` |
|
|
103
|
+
| `tasks` | every task, the running one animated | one line per task that has **settled** |
|
|
104
|
+
| `box` | the border, padding and title | `title: text` |
|
|
105
|
+
| `table` | the grid | one line per row of `header: value` pairs |
|
|
106
|
+
|
|
107
|
+
That table is the package's argument in one place. A bar of `█` in a log file tells an agent
|
|
108
|
+
nothing and tells a screen reader less; the count and the percentage tell both.
|
|
109
|
+
|
|
110
|
+
`box` and `table` are also plain string functions, because most callers want the string:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
box('Ready on :3000', { title: 'dev', width: 40 });
|
|
114
|
+
table([['ora', '99'], ['log-update', '99']], { head: ['host', 'tests'] });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
No layout engine, and there will not be one: these measure with `width()`, wrap with
|
|
118
|
+
`wrap()`, and join strings.
|
|
119
|
+
|
|
120
|
+
### The ora path
|
|
121
|
+
|
|
122
|
+
`flagstaff/ora` is ora 9's whole API, graded **99 / 99 by ora's own test suite** through
|
|
123
|
+
[`compat-oracle`](../compat-oracle/README.md). One import changes:
|
|
124
|
+
|
|
125
|
+
```diff
|
|
126
|
+
-import ora from 'ora';
|
|
127
|
+
+import ora from 'flagstaff/ora';
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Everything else stays: `ora({ text, spinner, color, indent, prefixText, suffixText })`,
|
|
131
|
+
`.start() .stop() .succeed() .fail() .warn() .info() .stopAndPersist()`, `oraPromise()`,
|
|
132
|
+
the `spinners` corpus, the stream hooks that keep a `console.log` above the frame, the
|
|
133
|
+
synchronized-output sequences, the render deferral, the stdin discarder.
|
|
134
|
+
|
|
135
|
+
What changes is the bill. ora 9.4.1 ships 113,577 B of JavaScript across **seventeen
|
|
136
|
+
packages** — ora, chalk, cli-spinners, string-width, log-symbols, cli-cursor,
|
|
137
|
+
restore-cursor, onetime, mimic-function, signal-exit, is-interactive, is-unicode-supported,
|
|
138
|
+
stdin-discarder, yoctocolors, strip-ansi, ansi-regex, get-east-asian-width. `flagstaff/ora`
|
|
139
|
+
is 55,641 B across **two** — itself and roundel — **49% of ora's**, and 20,250 B of that is
|
|
140
|
+
the spinner corpus ora's API re-exports. Nothing in it reaches the frame loop, so a program
|
|
141
|
+
that migrates its spinner today can adopt `hoist()` a file at a time, or never.
|
|
142
|
+
|
|
143
|
+
Both sides are counted the same way, so the number reproduces: shipped code and data —
|
|
144
|
+
`.js`/`.mjs`/`.cjs` plus the `.json` a module imports, `package.json` never counted. Ours is
|
|
145
|
+
the import graph walked from `dist/` by `weight.test.ts`; ora's is every package that graph
|
|
146
|
+
touches in ora's own resolved tree, counted whole. Counting ora the stricter way — only the
|
|
147
|
+
27 files its graph actually reaches — gives 101,809 B, and `flagstaff/ora` is still 55% of
|
|
148
|
+
that.
|
|
149
|
+
|
|
150
|
+
The cursor comes back the way ora's does. A spinner that hid the cursor restores it on a
|
|
151
|
+
clean exit **and** on `SIGINT`, `SIGTERM` and `SIGHUP` — node does not run `'exit'`
|
|
152
|
+
listeners for a signalled process, and Ctrl+C is how a spinner usually dies — then re-raises
|
|
153
|
+
the signal so the process still terminates, unless the program installed its own handler for
|
|
154
|
+
it. That is what ora buys with `restore-cursor` → `signal-exit`; here it is twenty lines and
|
|
155
|
+
no dependency. ora's own 99 never kill a process, so `ora.test.ts` grades it instead.
|
|
156
|
+
|
|
157
|
+
**The repo's accessible switch does not reach this surface.** `CLI_ACCESSIBLE=1` changes
|
|
158
|
+
what `roundel/policy` decides for every other entry point in the stack, but `flagstaff/ora`
|
|
159
|
+
imports `roundel/chalk` and never the policy, because a façade that reinterpreted its host
|
|
160
|
+
would fail the host's suite — so on a terminal `CLI_ACCESSIBLE=1` still animates and still
|
|
161
|
+
writes cursor escapes (measured: 4 escapes, cursor hidden), where `CI=true` disables the
|
|
162
|
+
spinner outright (0 escapes) because that is ora's own rule. Off a terminal it is moot. Use
|
|
163
|
+
`hoist()` when you want the switch to be honoured.
|
|
164
|
+
|
|
165
|
+
The static projection is the reason to move on eventually, not the reason to move:
|
|
166
|
+
`hoist()` is what gives a pipe one line per state instead of frames. `flagstaff/ora` is the
|
|
167
|
+
door, and it is deliberately ora's behaviour to the byte.
|
|
168
|
+
|
|
169
|
+
### The boxen path
|
|
170
|
+
|
|
171
|
+
`flagstaff/boxen` is boxen 8's API, graded **84 / 84 by boxen's own test suite** — every one
|
|
172
|
+
of whose cases is a snapshot of the exact characters the box comes out as.
|
|
173
|
+
|
|
174
|
+
```diff
|
|
175
|
+
-import boxen from 'boxen';
|
|
176
|
+
+import boxen from 'flagstaff/boxen';
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`borderStyle` (all eight of cli-boxes', a style object, or `none`), `borderColor`,
|
|
180
|
+
`backgroundColor`, `dimBorder`, `title` and `titleAlignment`, `textAlignment`, `padding`,
|
|
181
|
+
`margin`, `width`, `height`, `float`, `fullscreen`, and `_borderStyles`.
|
|
182
|
+
|
|
183
|
+
The drawing **is** the contract here, and matching it byte for byte is the compatibility
|
|
184
|
+
claim rather than a way of avoiding one: a user leaving boxen cares about one thing, whether
|
|
185
|
+
the box still looks the same. It carries cli-boxes' table itself rather than reading the
|
|
186
|
+
plugin registry — a façade whose drawing changed when somebody registered a plugin would be
|
|
187
|
+
reinterpreting its host. Named borders through the registry are `flagstaff/box`'s job.
|
|
188
|
+
|
|
189
|
+
### The cli-table3 path
|
|
190
|
+
|
|
191
|
+
`flagstaff/cli-table3` is cli-table3 0.6.5's API, graded **29 / 29 by cli-table3's own test
|
|
192
|
+
suite** — the 38 of its cases that go through the public surface, less the nine that grade
|
|
193
|
+
`cli-table`, the *legacy* incumbent, and so pass whatever the target is. The other 197
|
|
194
|
+
`require('../src/…')` and test its four internal modules directly; those are reported beside
|
|
195
|
+
the number and never gate it, because passing them would mean copying cli-table3's file
|
|
196
|
+
layout rather than matching its behaviour.
|
|
197
|
+
|
|
198
|
+
```diff
|
|
199
|
+
-const Table = require('cli-table3');
|
|
200
|
+
+import Table from 'flagstaff/cli-table3';
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`head`, `chars`, `style` (padding, `head`, `border`, `compact`), `colWidths`, `rowHeights`,
|
|
204
|
+
`colAligns`, `rowAligns`, `truncate`, `wordWrap`, `wrapOnWordBoundary`, per-cell `colSpan`,
|
|
205
|
+
`rowSpan`, `hAlign`, `vAlign`, `href`, and the `debug` channel with `table.messages` and
|
|
206
|
+
`Table.reset()`. It extends `Array`, because cli-table3 does and its callers push rows onto
|
|
207
|
+
it.
|
|
208
|
+
|
|
209
|
+
Four dependencies folded into one module rather than four: cli-table3's `table.js`,
|
|
210
|
+
`layout-manager.js`, `cell.js` and `utils.js` become one file, because the architecture is
|
|
211
|
+
not the contract — the drawing is.
|
|
212
|
+
|
|
213
|
+
### The log-update path
|
|
214
|
+
|
|
215
|
+
`flagstaff/log-update` is log-update 8's API, graded **99 / 99 by log-update's own test
|
|
216
|
+
suite** — which renders every frame through a real terminal emulator and asserts the
|
|
217
|
+
screen, not the bytes.
|
|
218
|
+
|
|
219
|
+
```diff
|
|
220
|
+
-import logUpdate from 'log-update';
|
|
221
|
+
+import logUpdate from 'flagstaff/log-update';
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`logUpdate()`, `.clear()`, `.done()`, `.persist()`, `createLogUpdate(stream, options)` and
|
|
225
|
+
`logUpdateStderr`, with the row-level diffing intact: a five-row frame whose last row is a
|
|
226
|
+
counter costs one row of output per tick, not five.
|
|
227
|
+
|
|
228
|
+
log-update ships 113.4 KB across **sixteen** packages. This is 29.6 KB across **none** —
|
|
229
|
+
the subpath reaches no package at all, not even roundel.
|
|
230
|
+
It carries no port of `slice-ansi` — the wrapper already makes every row self-contained,
|
|
231
|
+
so clipping a frame to the terminal's height is an array slice. `signal-exit`, 22.0 KB of
|
|
232
|
+
those sixteen, is 1.4 KB here: `cursor.ts`, shared with the ora façade because both
|
|
233
|
+
incumbents port the same `cli-cursor` → `restore-cursor` → `signal-exit` chain. Ctrl+C
|
|
234
|
+
mid-frame puts your cursor back, and still terminates — unless your program installed its
|
|
235
|
+
own `SIGINT` handler, in which case it is delivered once, to you, and this stays out of it.
|
|
236
|
+
|
|
237
|
+
**This is the one façade that lowers a layer guarantee, and it says so.** R5 — no cursor
|
|
238
|
+
escape off a terminal — cannot survive here: log-update's own suite asserts erase sequences
|
|
239
|
+
on a plain non-TTY stream, so a façade that suppressed them would fail the suite that is
|
|
240
|
+
the whole claim. What survives is the half the complaint behind R5 was actually about:
|
|
241
|
+
nothing this writes is ever a `\r`, **and never an absolute cursor-home** — every move is
|
|
242
|
+
a relative row move — on a terminal or off one, so a captured transcript stays parseable.
|
|
243
|
+
`log-update.test.ts` asserts both halves, and `hoist()` is what gives you the whole
|
|
244
|
+
guarantee.
|
|
245
|
+
|
|
246
|
+
### Bringing a corpus with you
|
|
247
|
+
|
|
248
|
+
The ecosystem already has ~80 spinner styles and eight border sets, as plain JSON. Neither
|
|
249
|
+
is bundled here — the weight of a corpus nobody asked for is the thing this package exists
|
|
250
|
+
to avoid — so `flagstaff/import` turns the one you have into an ordinary plugin:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
import cliSpinners from 'cli-spinners';
|
|
254
|
+
import { fromCliSpinners } from 'flagstaff/import';
|
|
255
|
+
import { register } from 'flagstaff/plugin';
|
|
256
|
+
|
|
257
|
+
register(fromCliSpinners(cliSpinners));
|
|
258
|
+
spinner('moon');
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`fromCliBoxes(cliBoxes)` does the same for borders, after which `box('…', { border:
|
|
262
|
+
'arrow' })` draws with one. Both go through the same `register()` and the same schema, so
|
|
263
|
+
the gallery opens full and a third-party plugin starts as a copy of one of these. The
|
|
264
|
+
importer is 838 B and reaches nothing.
|
|
265
|
+
|
|
266
|
+
### `flagstaff check`
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
npx flagstaff check ./nyan.mjs
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Loads the file, validates it, and prints every contribution in all five modes side by side —
|
|
273
|
+
escapes made visible — so an author, or an agent that just wrote one, sees the static
|
|
274
|
+
projection next to the animation before anything ships. Exit 1 on a refusal, with the code
|
|
275
|
+
and the fix.
|
|
276
|
+
|
|
277
|
+
It opens with a census of what it found and closes with the verdict, so `ok` is never
|
|
278
|
+
printed before the rendering that would justify it:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
nyan — 1 spinner, 0 borders, 0 components, 0 glyphs, 0 tokens
|
|
282
|
+
spinner nyan
|
|
283
|
+
tty ␛[?25l≋ working␛[1G␛[0J…
|
|
284
|
+
pipe ~nyan~ working⏎ ✔ done⏎
|
|
285
|
+
…
|
|
286
|
+
nyan: ok
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`0 spinners, 0 components` is how a misspelled key tells on itself. The schema is
|
|
290
|
+
`additionalProperties: true` on purpose — a key another package in the family reads belongs
|
|
291
|
+
in the same object — so a typo cannot be refused by the schema, and `check` is the surface
|
|
292
|
+
that has to notice:
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
typo — 0 spinners, 0 borders, 0 components, 0 glyphs, 0 tokens
|
|
296
|
+
unknown componets, spinner — flagstaff reads none of these; a key another package in the family reads is allowed here
|
|
297
|
+
E_NO_CONTRIBUTION: typo registers, but contributes nothing flagstaff can render
|
|
298
|
+
fix: flagstaff reads spinners, borders, components, glyphs and tokens; check those spellings. …
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Each component block names the state it was rendered with — the component's own `sample`
|
|
302
|
+
when it declares one, and otherwise the assumed `{ phase }` shape, said out loud. A `static`
|
|
303
|
+
that throws on the state it is handed is a refusal like any other, naming the modes it broke
|
|
304
|
+
in (`json` emits the state and never calls `static`, so it is usually the one that survives):
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
E_COMPONENT_THREW: g threw in tty, pipe, ci, accessible
|
|
308
|
+
fix: `static(state)` must return a string for the state it is rendered with; …
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Weight
|
|
312
|
+
|
|
313
|
+
Every subpath is a lock, not a convention, and the numbers below are asserted by
|
|
314
|
+
`weight.test.ts` against `dist/`, not estimated: `flagstaff/loop` reaches 4.4 KB on disk and
|
|
315
|
+
never the plugin registry; `flagstaff/plugin` 8.4 KB, of which 2.4 KB is the schema;
|
|
316
|
+
`flagstaff/spinner` 9.4 KB; `flagstaff/ora` 46.5 KB — 55.9 KB with roundel counted, against
|
|
317
|
+
ora's own 113.6 KB; `flagstaff/log-update` 29.6 KB, reaching **no package at all**, against
|
|
318
|
+
log-update's own 113.4 KB across sixteen; `flagstaff/boxen` 33.7 KB — 43.0 KB with roundel
|
|
319
|
+
counted, against boxen's own 151.4 KB across fourteen; `flagstaff/cli-table3` 32.9 KB —
|
|
320
|
+
42.3 KB with roundel, against cli-table3's own 161.7 KB across seven. The three façades share `wrap.js` and
|
|
321
|
+
`width.js`, and the first two share `cursor.js`; none reaches another's port, and none
|
|
322
|
+
reaches the core. `sideEffects: false` lets a
|
|
323
|
+
bundler drop what a program does not use. ESM with a `default` condition, so
|
|
324
|
+
`require('flagstaff/spinner')` works from CommonJS on Node ≥ 24.
|
|
325
|
+
|
|
326
|
+
## What is next
|
|
327
|
+
|
|
328
|
+
- **Drop-in paths** for boxen and cli-table3, graded by their own suites through
|
|
329
|
+
`compat-oracle` the way ora and log-update already are.
|
|
330
|
+
|
|
331
|
+
Every component, every registered plugin and every border is on the
|
|
332
|
+
[gallery](https://github.com/ofri-peretz/burgee/blob/main/apps/docs/content/docs/gallery.mdx),
|
|
333
|
+
which is generated by running them — the static projection beside the animation, in all
|
|
334
|
+
five modes.
|
|
22
335
|
|
|
23
336
|
Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on burgee declares
|
|
24
|
-
what it is,
|
|
337
|
+
what it is, roundel carries its colours, flagstaff flies it, caique answers back. Each is
|
|
25
338
|
an independent package; none requires the others.
|
package/THIRD-PARTY.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Third-party material shipped in this package
|
|
2
|
+
|
|
3
|
+
flagstaff has one runtime dependency (`roundel`) and writes its own code. Two things in
|
|
4
|
+
the tarball are not ours, and are here with their licence.
|
|
5
|
+
|
|
6
|
+
## `dist/spinners.json` — cli-spinners
|
|
7
|
+
|
|
8
|
+
The spinner corpus reached through `flagstaff/ora` is `spinners.json` from
|
|
9
|
+
[cli-spinners](https://github.com/sindresorhus/cli-spinners) 3.2.0, unedited. It is data,
|
|
10
|
+
not code: `flagstaff/ora` re-exports it as `spinners` because ora does, and ora's own test
|
|
11
|
+
suite reads it. Nothing else in the package loads it — the core (`flagstaff`,
|
|
12
|
+
`flagstaff/loop`, `flagstaff/plugin`, `flagstaff/spinner`) ships two spinners of its own
|
|
13
|
+
and a lock that fails if any entry reaches this file.
|
|
14
|
+
|
|
15
|
+
> MIT License
|
|
16
|
+
>
|
|
17
|
+
> Copyright (c) Sindre Sorhus \<sindresorhus@gmail.com\> (https://sindresorhus.com)
|
|
18
|
+
>
|
|
19
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
20
|
+
> software and associated documentation files (the "Software"), to deal in the Software
|
|
21
|
+
> without restriction, including without limitation the rights to use, copy, modify,
|
|
22
|
+
> merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
23
|
+
> permit persons to whom the Software is furnished to do so, subject to the following
|
|
24
|
+
> conditions:
|
|
25
|
+
>
|
|
26
|
+
> The above copyright notice and this permission notice shall be included in all copies or
|
|
27
|
+
> substantial portions of the Software.
|
|
28
|
+
>
|
|
29
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
30
|
+
> INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
31
|
+
> PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
32
|
+
> LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
|
33
|
+
> OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
34
|
+
> OTHER DEALINGS IN THE SOFTWARE.
|
|
35
|
+
|
|
36
|
+
## The East Asian Width ranges in `dist/width.js`
|
|
37
|
+
|
|
38
|
+
The `WIDE` table is derived from the Unicode Character Database's `EastAsianWidth.txt`
|
|
39
|
+
(Unicode 17), the same derivation `get-east-asian-width` publishes. The Unicode data files
|
|
40
|
+
are distributed under the [Unicode licence](https://www.unicode.org/license.txt), which
|
|
41
|
+
permits redistribution of derived data with the notice above kept.
|
|
42
|
+
|
|
43
|
+
Neither package is a dependency: nothing is installed, and both are here as data compiled
|
|
44
|
+
into the build.
|
package/dist/box.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type BorderStyle, type Component } from './plugin.js';
|
|
2
|
+
export interface BoxOptions {
|
|
3
|
+
/** A registered border's name, or a style of your own. Default `round`. */
|
|
4
|
+
border?: string | BorderStyle;
|
|
5
|
+
/** Cells of padding left and right of the text, and rows above and below. Default 1 / 0. */
|
|
6
|
+
padding?: {
|
|
7
|
+
x?: number;
|
|
8
|
+
y?: number;
|
|
9
|
+
};
|
|
10
|
+
/** A title written into the top border. Truncated with `…` if the box is too narrow. */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** Columns the whole box may occupy, borders included. Text wraps to fit. Default 80. */
|
|
13
|
+
width?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Draw `text` in a box, as a string. The many callers who want only this want only this. */
|
|
16
|
+
export declare function box(text: string, options?: BoxOptions): string;
|
|
17
|
+
export interface BoxState {
|
|
18
|
+
text: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A box as a component: the text off a terminal, the drawing on one (R1). */
|
|
22
|
+
export declare function boxComponent(options?: BoxOptions): Component<BoxState>;
|
package/dist/box.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { muted } from 'roundel/tokens';
|
|
2
|
+
import { lookupBorder } from './plugin.js';
|
|
3
|
+
import { width } from './width.js';
|
|
4
|
+
import { wrap } from './wrap.js';
|
|
5
|
+
const DEFAULT_WIDTH = 80;
|
|
6
|
+
const DEFAULT_PAD_X = 1;
|
|
7
|
+
const DEFAULT_PAD_Y = 0;
|
|
8
|
+
const BORDER_CELLS = 2;
|
|
9
|
+
const ELLIPSIS = '…';
|
|
10
|
+
const resolve = (border) => (typeof border === 'string' ? lookupBorder(border) : border);
|
|
11
|
+
const padEnd = (line, cells) => line + ' '.repeat(Math.max(0, cells - width(line)));
|
|
12
|
+
function fitTitle(title, cells) {
|
|
13
|
+
if (width(title) <= cells)
|
|
14
|
+
return title;
|
|
15
|
+
if (cells <= 1)
|
|
16
|
+
return '';
|
|
17
|
+
let out = '';
|
|
18
|
+
for (const character of title) {
|
|
19
|
+
if (width(out + character) > cells - 1)
|
|
20
|
+
break;
|
|
21
|
+
out += character;
|
|
22
|
+
}
|
|
23
|
+
return out + ELLIPSIS;
|
|
24
|
+
}
|
|
25
|
+
function topBorder(style, inner, title) {
|
|
26
|
+
if (style.top === '')
|
|
27
|
+
return '';
|
|
28
|
+
if (title === undefined || title === '')
|
|
29
|
+
return style.topLeft + style.top.repeat(inner) + style.topRight;
|
|
30
|
+
const fitted = fitTitle(title, Math.max(0, inner - BORDER_CELLS - BORDER_CELLS));
|
|
31
|
+
if (fitted === '')
|
|
32
|
+
return style.topLeft + style.top.repeat(inner) + style.topRight;
|
|
33
|
+
const label = ` ${fitted} `;
|
|
34
|
+
const rest = Math.max(0, inner - width(label) - 1);
|
|
35
|
+
return `${style.topLeft}${style.top}${label}${style.top.repeat(rest)}${style.topRight}`;
|
|
36
|
+
}
|
|
37
|
+
export function box(text, options = {}) {
|
|
38
|
+
const style = resolve(options.border ?? 'round');
|
|
39
|
+
const padX = options.padding?.x ?? DEFAULT_PAD_X;
|
|
40
|
+
const padY = options.padding?.y ?? DEFAULT_PAD_Y;
|
|
41
|
+
const total = options.width ?? DEFAULT_WIDTH;
|
|
42
|
+
const borderCells = style.left === '' ? 0 : BORDER_CELLS;
|
|
43
|
+
const inner = Math.max(1, total - borderCells);
|
|
44
|
+
const content = Math.max(1, inner - padX * BORDER_CELLS);
|
|
45
|
+
const wrapped = wrap(text, content, { hard: true, trim: false }).split('\n');
|
|
46
|
+
const blank = Array.from({ length: padY }, () => '');
|
|
47
|
+
const pad = ' '.repeat(padX);
|
|
48
|
+
const rows = [...blank, ...wrapped, ...blank].map((line) => `${style.left}${pad}${padEnd(line, content)}${pad}${style.right}`);
|
|
49
|
+
const top = topBorder(style, inner, options.title);
|
|
50
|
+
const bottom = style.bottom === '' ? '' : style.bottomLeft + style.bottom.repeat(inner) + style.bottomRight;
|
|
51
|
+
return [top, ...rows, bottom].filter((row) => row !== '').join('\n');
|
|
52
|
+
}
|
|
53
|
+
export function boxComponent(options = {}) {
|
|
54
|
+
return {
|
|
55
|
+
name: 'box',
|
|
56
|
+
static: (state) => (state.title === undefined || state.title === '' ? state.text : `${state.title}: ${state.text}`),
|
|
57
|
+
frame: (_t, state) => muted(box(state.text, state.title === undefined ? options : { ...options, title: state.title })),
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/boxen.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface BoxenBorderStyle {
|
|
2
|
+
topLeft: string;
|
|
3
|
+
top: string;
|
|
4
|
+
topRight: string;
|
|
5
|
+
left: string;
|
|
6
|
+
right: string;
|
|
7
|
+
bottomLeft: string;
|
|
8
|
+
bottom: string;
|
|
9
|
+
bottomRight: string;
|
|
10
|
+
/** Retro-compatibility: sets `left` and `right` together. */
|
|
11
|
+
vertical?: string;
|
|
12
|
+
/** Retro-compatibility: sets `top` and `bottom` together. */
|
|
13
|
+
horizontal?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Spacing {
|
|
16
|
+
top?: number;
|
|
17
|
+
right?: number;
|
|
18
|
+
bottom?: number;
|
|
19
|
+
left?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface BoxenOptions {
|
|
22
|
+
borderStyle?: string | BoxenBorderStyle;
|
|
23
|
+
borderColor?: string;
|
|
24
|
+
backgroundColor?: string;
|
|
25
|
+
dimBorder?: boolean;
|
|
26
|
+
title?: string;
|
|
27
|
+
titleAlignment?: 'left' | 'center' | 'right';
|
|
28
|
+
textAlignment?: 'left' | 'center' | 'right';
|
|
29
|
+
/** @deprecated boxen's own name for `textAlignment`, still honoured. */
|
|
30
|
+
align?: 'left' | 'center' | 'right';
|
|
31
|
+
padding?: number | Spacing;
|
|
32
|
+
margin?: number | Spacing;
|
|
33
|
+
width?: number;
|
|
34
|
+
height?: number;
|
|
35
|
+
float?: 'left' | 'center' | 'right';
|
|
36
|
+
fullscreen?: boolean | ((columns: number, rows: number) => [number, number]);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* cli-boxes 4, as data. boxen re-exports this as `_borderStyles`, so it is part of the
|
|
40
|
+
* surface a caller can reach and not an implementation detail to be hidden.
|
|
41
|
+
*/
|
|
42
|
+
declare const BOXES: Record<string, BoxenBorderStyle>;
|
|
43
|
+
/**
|
|
44
|
+
* Draw a box around `text`, exactly as boxen 8 draws it.
|
|
45
|
+
*
|
|
46
|
+
* An invalid `borderColor` or `backgroundColor` throws rather than drawing something
|
|
47
|
+
* plausible, because boxen throws: a colour name that is not one is a typo, and a box drawn
|
|
48
|
+
* in the wrong colour is a bug somebody ships.
|
|
49
|
+
*/
|
|
50
|
+
/** boxen re-exports cli-boxes under this name, so it is surface a caller can reach. */
|
|
51
|
+
export { BOXES as _borderStyles };
|
|
52
|
+
/**
|
|
53
|
+
* `import boxen from 'boxen'` is the incumbent's surface. A named export here would break
|
|
54
|
+
* every migration this file exists to serve, so the house rule yields to the host.
|
|
55
|
+
*/
|
|
56
|
+
export default function boxen(text: string, options?: BoxenOptions): string;
|