busybar-kit 1.1.0 → 2.0.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/LICENSE +21 -21
- package/README.md +148 -148
- package/dist/config-spec.d.ts +63 -19
- package/dist/config-spec.d.ts.map +1 -1
- package/dist/config-spec.js +64 -41
- package/dist/config-spec.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/rules.d.ts +7 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +71 -0
- package/dist/rules.js.map +1 -0
- package/dist/summary.d.ts +18 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +63 -0
- package/dist/summary.js.map +1 -0
- package/package.json +5 -2
- package/scripts/emit-config-spec.mjs +31 -0
- package/scripts/fix-esm-extensions.mjs +33 -33
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 viirtualp1
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 viirtualp1
|
|
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
CHANGED
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
# busybar-kit
|
|
2
|
-
|
|
3
|
-
> [!IMPORTANT]
|
|
4
|
-
> **Unofficial community project.** Built and maintained by [@viirtualp1](https://github.com/viirtualp1), **not** an official Flipper Devices / BUSY product, and not affiliated with, endorsed by, or supported by them. "BUSY Bar" remains their trademark. For the real hardware and official apps, visit **[busy.app](https://busy.app/)**.
|
|
5
|
-
|
|
6
|
-
Shared building blocks for [BUSY Bar](https://busy.bar) apps. Everything here is
|
|
7
|
-
app-agnostic: screen geometry, the palette neutrals, formatters, ticker text,
|
|
8
|
-
the Bar's error taxonomy, the connection half of a config, and an offline PNG
|
|
9
|
-
renderer that draws the same elements the device does.
|
|
10
|
-
|
|
11
|
-
It is the common half of [busybar-dota](https://github.com/viirtualp1/busybar-dota),
|
|
12
|
-
[busybar-mydota](https://github.com/viirtualp1/busybar-mydota) and
|
|
13
|
-
[busybar-livesplit](https://github.com/viirtualp1/busybar-livesplit).
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install busybar-kit
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
`@busy-app/busy-lib` is a peer dependency — the app owns that version.
|
|
20
|
-
|
|
21
|
-
## What is in it
|
|
22
|
-
|
|
23
|
-
| Import | What it gives you |
|
|
24
|
-
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
25
|
-
| `busybar-kit/device` | `FRONT` / `BACK` screen sizes, `FONT_WIDTH`, `textWidth`, `clipToWidth`, `fittingChars`, `rowY`, `backRowGrid` |
|
|
26
|
-
| `busybar-kit/colors` | `BASE_COLORS` — neutrals, chrome, and the transparent value that hides an element |
|
|
27
|
-
| `busybar-kit/format` | `formatClock`, `formatGold`, `formatCountdown`, `formatStartTime`, `formatKda`, `formatPercent`, … |
|
|
28
|
-
| `busybar-kit/elements` | `AnyElement`, `band()`, `fillWidth()` |
|
|
29
|
-
| `busybar-kit/ticker` | `paginate` / `pageAt` / `scrollAt` / `tickerLine`, and `EventTicker` |
|
|
30
|
-
| `busybar-kit/errors` | `BarApiError`, `toBarError`, `isLowPriority`, `isClientError`, `isForbidden` |
|
|
31
|
-
| `busybar-kit/config` | `loadEnvFile`, `envReader`, `loadBarConfig` |
|
|
32
|
-
| `busybar-kit/image` | `ditherToPanel` — a picture, in the back panel's 16 greys |
|
|
33
|
-
| `busybar-kit/preview` | `renderFront` / `renderBack` / `Bitmap` — device-accurate PNGs with no hardware |
|
|
34
|
-
| `busybar-kit/tsconfig.json`, `busybar-kit/eslint`, `busybar-kit/prettier` | the shared toolchain |
|
|
35
|
-
| `busybar-fix-esm` (bin) | rewrites relative imports in `dist/` to carry `.js`, for Node ESM |
|
|
36
|
-
|
|
37
|
-
## Geometry is hardware, not layout
|
|
38
|
-
|
|
39
|
-
`FRONT` (72×16) and `BACK` (160×80, with its header rows and 12px grid) are
|
|
40
|
-
facts about the device. An app spreads its own named slots on top:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
import { FRONT as DEVICE, BACK as DEVICE_BACK } from 'busybar-kit/device';
|
|
44
|
-
|
|
45
|
-
export const FRONT = {
|
|
46
|
-
...DEVICE,
|
|
47
|
-
topY: 0,
|
|
48
|
-
bottomY: 11,
|
|
49
|
-
clockWidth: 24,
|
|
50
|
-
} as const;
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
An app whose header is a different height lays out its own row grid, and reads
|
|
54
|
-
rows off that instead of the default:
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
import { backRowGrid, rowY as gridRowY } from 'busybar-kit/device';
|
|
58
|
-
|
|
59
|
-
export const BACK = { ...DEVICE_BACK, ...backRowGrid(16), nameX: 8 } as const;
|
|
60
|
-
|
|
61
|
-
export const rowY = (index: number) => gridRowY(index, BACK);
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
`clipToWidth` takes the truncation mark as a fourth argument (`'.'` by
|
|
65
|
-
default, `'..'` where names are cut often enough to want the emphasis).
|
|
66
|
-
|
|
67
|
-
Colours work the same way — `BASE_COLORS` holds what every app needs, and the
|
|
68
|
-
app adds its own semantics:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import { BASE_COLORS } from 'busybar-kit/colors';
|
|
72
|
-
|
|
73
|
-
export const COLORS = {
|
|
74
|
-
...BASE_COLORS,
|
|
75
|
-
radiant: '#3FBF5FFF',
|
|
76
|
-
dire: '#E14B3AFF',
|
|
77
|
-
} as const;
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Config
|
|
81
|
-
|
|
82
|
-
`loadBarConfig` resolves which Bar to talk to and over which transport, and
|
|
83
|
-
warns about credentials that transport will ignore. That is all it covers —
|
|
84
|
-
render cadence and request timeouts are the app's own tempo (a speedrun timer
|
|
85
|
-
wants 60ms frames where a match ticker is happy at 200), so they stay in the
|
|
86
|
-
app's config with `DEFAULTS` / `LIMITS` as the values worth starting from.
|
|
87
|
-
|
|
88
|
-
It hands back the same env reader, so an app's own settings collect their
|
|
89
|
-
warnings into one list:
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
import { loadBarConfig, loadEnvFile } from 'busybar-kit/config';
|
|
93
|
-
|
|
94
|
-
loadEnvFile();
|
|
95
|
-
|
|
96
|
-
export function loadConfig(env = process.env) {
|
|
97
|
-
const warnings: string[] = [];
|
|
98
|
-
const { bar, env: read } = loadBarConfig(env, warnings);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
warnings,
|
|
102
|
-
config: {
|
|
103
|
-
...bar,
|
|
104
|
-
gsiPort: read.number('GSI_PORT', 3080, { min: 1024, max: 65_535 }),
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Pictures
|
|
111
|
-
|
|
112
|
-
The back panel has 16 greys and quantises whatever you upload to them, so a
|
|
113
|
-
photo sent as-is comes back in bands. `ditherToPanel` does the quantising here,
|
|
114
|
-
with error diffusion, which leaves every pixel already sitting on a level:
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
import { ditherToPanel } from 'busybar-kit/image';
|
|
118
|
-
|
|
119
|
-
const cover = ditherToPanel(decodedRgba); // 80x80 Bitmap, panel-ready
|
|
120
|
-
await bar.AssetsUpload({ application_name: 'app', file: 'art.png', data: cover.toPng() });
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
It takes pixels, not files. Which image decoder to pay for is the app's
|
|
124
|
-
choice — the kit stays dependency-free.
|
|
125
|
-
|
|
126
|
-
`Bitmap.fromRgba` wraps pixels from anywhere, and `bitmap.blit(other, x, y)`
|
|
127
|
-
draws one into another: the raster below cannot follow an image element's path,
|
|
128
|
-
so a preview composites the picture itself.
|
|
129
|
-
|
|
130
|
-
## Preview
|
|
131
|
-
|
|
132
|
-
`renderFront` / `renderBack` rasterise the very element list you send to the
|
|
133
|
-
device — a 3×5 pixel font, the same anchoring rules, and the back panel
|
|
134
|
-
flattened to the 16 greys the real OLED shows. Useful for screenshots in a
|
|
135
|
-
README and for asserting a layout in tests without a Bar on the desk.
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
import { renderFront } from 'busybar-kit/preview';
|
|
139
|
-
|
|
140
|
-
writeFileSync('front.png', renderFront(frontElements(frame)).scale(8).toPng());
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Scripts
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
npm run check # lint + typecheck + test
|
|
147
|
-
npm run build # tsc + .js extension fixup
|
|
148
|
-
```
|
|
1
|
+
# busybar-kit
|
|
2
|
+
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> **Unofficial community project.** Built and maintained by [@viirtualp1](https://github.com/viirtualp1), **not** an official Flipper Devices / BUSY product, and not affiliated with, endorsed by, or supported by them. "BUSY Bar" remains their trademark. For the real hardware and official apps, visit **[busy.app](https://busy.app/)**.
|
|
5
|
+
|
|
6
|
+
Shared building blocks for [BUSY Bar](https://busy.bar) apps. Everything here is
|
|
7
|
+
app-agnostic: screen geometry, the palette neutrals, formatters, ticker text,
|
|
8
|
+
the Bar's error taxonomy, the connection half of a config, and an offline PNG
|
|
9
|
+
renderer that draws the same elements the device does.
|
|
10
|
+
|
|
11
|
+
It is the common half of [busybar-dota](https://github.com/viirtualp1/busybar-dota),
|
|
12
|
+
[busybar-mydota](https://github.com/viirtualp1/busybar-mydota) and
|
|
13
|
+
[busybar-livesplit](https://github.com/viirtualp1/busybar-livesplit).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install busybar-kit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`@busy-app/busy-lib` is a peer dependency — the app owns that version.
|
|
20
|
+
|
|
21
|
+
## What is in it
|
|
22
|
+
|
|
23
|
+
| Import | What it gives you |
|
|
24
|
+
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `busybar-kit/device` | `FRONT` / `BACK` screen sizes, `FONT_WIDTH`, `textWidth`, `clipToWidth`, `fittingChars`, `rowY`, `backRowGrid` |
|
|
26
|
+
| `busybar-kit/colors` | `BASE_COLORS` — neutrals, chrome, and the transparent value that hides an element |
|
|
27
|
+
| `busybar-kit/format` | `formatClock`, `formatGold`, `formatCountdown`, `formatStartTime`, `formatKda`, `formatPercent`, … |
|
|
28
|
+
| `busybar-kit/elements` | `AnyElement`, `band()`, `fillWidth()` |
|
|
29
|
+
| `busybar-kit/ticker` | `paginate` / `pageAt` / `scrollAt` / `tickerLine`, and `EventTicker` |
|
|
30
|
+
| `busybar-kit/errors` | `BarApiError`, `toBarError`, `isLowPriority`, `isClientError`, `isForbidden` |
|
|
31
|
+
| `busybar-kit/config` | `loadEnvFile`, `envReader`, `loadBarConfig` |
|
|
32
|
+
| `busybar-kit/image` | `ditherToPanel` — a picture, in the back panel's 16 greys |
|
|
33
|
+
| `busybar-kit/preview` | `renderFront` / `renderBack` / `Bitmap` — device-accurate PNGs with no hardware |
|
|
34
|
+
| `busybar-kit/tsconfig.json`, `busybar-kit/eslint`, `busybar-kit/prettier` | the shared toolchain |
|
|
35
|
+
| `busybar-fix-esm` (bin) | rewrites relative imports in `dist/` to carry `.js`, for Node ESM |
|
|
36
|
+
|
|
37
|
+
## Geometry is hardware, not layout
|
|
38
|
+
|
|
39
|
+
`FRONT` (72×16) and `BACK` (160×80, with its header rows and 12px grid) are
|
|
40
|
+
facts about the device. An app spreads its own named slots on top:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { FRONT as DEVICE, BACK as DEVICE_BACK } from 'busybar-kit/device';
|
|
44
|
+
|
|
45
|
+
export const FRONT = {
|
|
46
|
+
...DEVICE,
|
|
47
|
+
topY: 0,
|
|
48
|
+
bottomY: 11,
|
|
49
|
+
clockWidth: 24,
|
|
50
|
+
} as const;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
An app whose header is a different height lays out its own row grid, and reads
|
|
54
|
+
rows off that instead of the default:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { backRowGrid, rowY as gridRowY } from 'busybar-kit/device';
|
|
58
|
+
|
|
59
|
+
export const BACK = { ...DEVICE_BACK, ...backRowGrid(16), nameX: 8 } as const;
|
|
60
|
+
|
|
61
|
+
export const rowY = (index: number) => gridRowY(index, BACK);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`clipToWidth` takes the truncation mark as a fourth argument (`'.'` by
|
|
65
|
+
default, `'..'` where names are cut often enough to want the emphasis).
|
|
66
|
+
|
|
67
|
+
Colours work the same way — `BASE_COLORS` holds what every app needs, and the
|
|
68
|
+
app adds its own semantics:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { BASE_COLORS } from 'busybar-kit/colors';
|
|
72
|
+
|
|
73
|
+
export const COLORS = {
|
|
74
|
+
...BASE_COLORS,
|
|
75
|
+
radiant: '#3FBF5FFF',
|
|
76
|
+
dire: '#E14B3AFF',
|
|
77
|
+
} as const;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Config
|
|
81
|
+
|
|
82
|
+
`loadBarConfig` resolves which Bar to talk to and over which transport, and
|
|
83
|
+
warns about credentials that transport will ignore. That is all it covers —
|
|
84
|
+
render cadence and request timeouts are the app's own tempo (a speedrun timer
|
|
85
|
+
wants 60ms frames where a match ticker is happy at 200), so they stay in the
|
|
86
|
+
app's config with `DEFAULTS` / `LIMITS` as the values worth starting from.
|
|
87
|
+
|
|
88
|
+
It hands back the same env reader, so an app's own settings collect their
|
|
89
|
+
warnings into one list:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { loadBarConfig, loadEnvFile } from 'busybar-kit/config';
|
|
93
|
+
|
|
94
|
+
loadEnvFile();
|
|
95
|
+
|
|
96
|
+
export function loadConfig(env = process.env) {
|
|
97
|
+
const warnings: string[] = [];
|
|
98
|
+
const { bar, env: read } = loadBarConfig(env, warnings);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
warnings,
|
|
102
|
+
config: {
|
|
103
|
+
...bar,
|
|
104
|
+
gsiPort: read.number('GSI_PORT', 3080, { min: 1024, max: 65_535 }),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Pictures
|
|
111
|
+
|
|
112
|
+
The back panel has 16 greys and quantises whatever you upload to them, so a
|
|
113
|
+
photo sent as-is comes back in bands. `ditherToPanel` does the quantising here,
|
|
114
|
+
with error diffusion, which leaves every pixel already sitting on a level:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { ditherToPanel } from 'busybar-kit/image';
|
|
118
|
+
|
|
119
|
+
const cover = ditherToPanel(decodedRgba); // 80x80 Bitmap, panel-ready
|
|
120
|
+
await bar.AssetsUpload({ application_name: 'app', file: 'art.png', data: cover.toPng() });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
It takes pixels, not files. Which image decoder to pay for is the app's
|
|
124
|
+
choice — the kit stays dependency-free.
|
|
125
|
+
|
|
126
|
+
`Bitmap.fromRgba` wraps pixels from anywhere, and `bitmap.blit(other, x, y)`
|
|
127
|
+
draws one into another: the raster below cannot follow an image element's path,
|
|
128
|
+
so a preview composites the picture itself.
|
|
129
|
+
|
|
130
|
+
## Preview
|
|
131
|
+
|
|
132
|
+
`renderFront` / `renderBack` rasterise the very element list you send to the
|
|
133
|
+
device — a 3×5 pixel font, the same anchoring rules, and the back panel
|
|
134
|
+
flattened to the 16 greys the real OLED shows. Useful for screenshots in a
|
|
135
|
+
README and for asserting a layout in tests without a Bar on the desk.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { renderFront } from 'busybar-kit/preview';
|
|
139
|
+
|
|
140
|
+
writeFileSync('front.png', renderFront(frontElements(frame)).scale(8).toPng());
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Scripts
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm run check # lint + typecheck + test
|
|
147
|
+
npm run build # tsc + .js extension fixup
|
|
148
|
+
```
|
package/dist/config-spec.d.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How an app describes its own settings, so
|
|
2
|
+
* How an app describes its own settings — as data, so anything can read it.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The app owns the description. `busybar-flights` knows that a route reads
|
|
5
|
+
* `SVO-JFK` and that a departure is a local time with no zone; an editor that
|
|
6
|
+
* had to know that for every app would need editing every time a new app
|
|
7
|
+
* appeared. With a spec, installing a package is enough.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Everything here is JSON. Checks are rules rather than functions and a list
|
|
10
|
+
* entry's one-line summary is a template rather than a callback, so a browser
|
|
11
|
+
* or a Dart client can interpret a spec without running the app's JavaScript.
|
|
12
|
+
* An app points at its own from package.json:
|
|
12
13
|
*
|
|
13
|
-
* "busybar": {
|
|
14
|
+
* "busybar": {
|
|
15
|
+
* "config": "./dist/config-spec.js",
|
|
16
|
+
* "configJson": "./dist/config-spec.json"
|
|
17
|
+
* }
|
|
14
18
|
*/
|
|
15
19
|
export type FieldType = 'text' | 'secret' | 'number' | 'boolean' | 'select' | 'datetime' | 'path';
|
|
16
20
|
export type FieldOption = {
|
|
@@ -18,6 +22,24 @@ export type FieldOption = {
|
|
|
18
22
|
label: string;
|
|
19
23
|
hint?: string;
|
|
20
24
|
};
|
|
25
|
+
/** Checks a client interprets. Never code — see `validateValue`. */
|
|
26
|
+
export type ValidationRule = {
|
|
27
|
+
kind: 'required';
|
|
28
|
+
message?: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'pattern';
|
|
31
|
+
pattern: string;
|
|
32
|
+
message: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'integer';
|
|
35
|
+
min: number;
|
|
36
|
+
max: number;
|
|
37
|
+
message?: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'datetime';
|
|
40
|
+
format: 'local';
|
|
41
|
+
message?: string;
|
|
42
|
+
};
|
|
21
43
|
export type ConfigField = {
|
|
22
44
|
/** The env variable, or the key inside a record. */
|
|
23
45
|
key: string;
|
|
@@ -34,9 +56,14 @@ export type ConfigField = {
|
|
|
34
56
|
options?: readonly FieldOption[];
|
|
35
57
|
/** What the app itself would do with the setting left out. */
|
|
36
58
|
fallback?: string;
|
|
37
|
-
|
|
38
|
-
validate?: (value: string) => string | undefined;
|
|
59
|
+
rules?: ValidationRule[];
|
|
39
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Whether a change lands without help. Declared by the app because only the
|
|
63
|
+
* app knows: dota re-reads its schedule on every poll, everything else reads
|
|
64
|
+
* its settings once, at startup.
|
|
65
|
+
*/
|
|
66
|
+
export type Reloads = 'live' | 'restart';
|
|
40
67
|
/** A file of records you add to, edit and remove — flights, fixtures. */
|
|
41
68
|
export type ListSection = {
|
|
42
69
|
kind: 'list';
|
|
@@ -51,11 +78,12 @@ export type ListSection = {
|
|
|
51
78
|
at?: string;
|
|
52
79
|
/** Settings that live beside the array in the same file, not in `.env`. */
|
|
53
80
|
header?: ConfigField[];
|
|
54
|
-
/**
|
|
55
|
-
summary:
|
|
81
|
+
/** How one entry reads in a picker. See `renderSummary` for the syntax. */
|
|
82
|
+
summary: string;
|
|
56
83
|
fields: ConfigField[];
|
|
57
84
|
/** Shown instead of the picker when the file has nothing in it yet. */
|
|
58
85
|
empty?: string;
|
|
86
|
+
reloads?: Reloads;
|
|
59
87
|
};
|
|
60
88
|
/** Plain settings, one key to a line. */
|
|
61
89
|
export type EnvSection = {
|
|
@@ -63,19 +91,35 @@ export type EnvSection = {
|
|
|
63
91
|
file: string;
|
|
64
92
|
title: string;
|
|
65
93
|
fields: ConfigField[];
|
|
94
|
+
reloads?: Reloads;
|
|
66
95
|
};
|
|
67
96
|
export type ConfigSection = ListSection | EnvSection;
|
|
68
97
|
export type AppConfigSpec = {
|
|
98
|
+
/** The contract version. A client refuses one it does not know. */
|
|
99
|
+
specVersion: number;
|
|
69
100
|
/** The `application_name` the app draws with — the join with everything else. */
|
|
70
101
|
name: string;
|
|
71
102
|
/** One line about what the app is, for the app picker. */
|
|
72
103
|
summary?: string;
|
|
73
104
|
sections: ConfigSection[];
|
|
74
105
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
export declare function
|
|
106
|
+
export declare const SPEC_VERSION = 1;
|
|
107
|
+
/**
|
|
108
|
+
* Stamps the current version onto a spec, so an author cannot get it wrong and
|
|
109
|
+
* a client always has one to check.
|
|
110
|
+
*/
|
|
111
|
+
export declare function defineConfigSpec(spec: Omit<AppConfigSpec, 'specVersion'>): AppConfigSpec;
|
|
112
|
+
/**
|
|
113
|
+
* Reads a spec that came from outside — a file, a package, an HTTP response.
|
|
114
|
+
* The version check is the whole point: once something else reads these, the
|
|
115
|
+
* shape is a contract, and a client that guesses at an unknown one is worse
|
|
116
|
+
* than a client that says which package needs updating.
|
|
117
|
+
*/
|
|
118
|
+
export declare function parseSpecJson(text: string, source?: string): AppConfigSpec;
|
|
119
|
+
export declare function parseSpec(raw: unknown, source?: string): AppConfigSpec;
|
|
120
|
+
export declare function required(label?: string): ValidationRule;
|
|
121
|
+
export declare function integerIn(min: number, max: number, message?: string): ValidationRule;
|
|
122
|
+
export declare function matching(pattern: RegExp | string, message: string): ValidationRule;
|
|
123
|
+
/** `2026-09-08 19:30` — a local time at the airport, with no zone on it. */
|
|
124
|
+
export declare const localDateTime: ValidationRule;
|
|
81
125
|
//# sourceMappingURL=config-spec.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-spec.d.ts","sourceRoot":"","sources":["../src/config-spec.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config-spec.d.ts","sourceRoot":"","sources":["../src/config-spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,SAAS,GACnB,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;AAE5E,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,oEAAoE;AACpE,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,MAAM,MAAM,WAAW,GAAG;IACxB,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB;IACpB,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;CAC1B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEzC,yEAAyE;AACzE,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,yCAAyC;AACzC,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAErD,MAAM,MAAM,aAAa,GAAG;IAC1B,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC,GACvC,aAAa,CAEf;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG,aAAa,CAS1E;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,SAAS,GAAG,aAAa,CAmBtE;AAID,wBAAgB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,CAIvD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc,CAEpF;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAMlF;AAED,4EAA4E;AAC5E,eAAO,MAAM,aAAa,EAAE,cAAsD,CAAC"}
|
package/dist/config-spec.js
CHANGED
|
@@ -1,54 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How an app describes its own settings, so
|
|
2
|
+
* How an app describes its own settings — as data, so anything can read it.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The app owns the description. `busybar-flights` knows that a route reads
|
|
5
|
+
* `SVO-JFK` and that a departure is a local time with no zone; an editor that
|
|
6
|
+
* had to know that for every app would need editing every time a new app
|
|
7
|
+
* appeared. With a spec, installing a package is enough.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Everything here is JSON. Checks are rules rather than functions and a list
|
|
10
|
+
* entry's one-line summary is a template rather than a callback, so a browser
|
|
11
|
+
* or a Dart client can interpret a spec without running the app's JavaScript.
|
|
12
|
+
* An app points at its own from package.json:
|
|
12
13
|
*
|
|
13
|
-
* "busybar": {
|
|
14
|
+
* "busybar": {
|
|
15
|
+
* "config": "./dist/config-spec.js",
|
|
16
|
+
* "configJson": "./dist/config-spec.json"
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
export const SPEC_VERSION = 1;
|
|
20
|
+
/**
|
|
21
|
+
* Stamps the current version onto a spec, so an author cannot get it wrong and
|
|
22
|
+
* a client always has one to check.
|
|
14
23
|
*/
|
|
15
|
-
/** Identity, for the types. Specs are data; this only saves the annotation. */
|
|
16
24
|
export function defineConfigSpec(spec) {
|
|
17
|
-
return spec;
|
|
18
|
-
}
|
|
19
|
-
// --- Validators the apps kept writing for themselves -------------------------
|
|
20
|
-
export function required(label) {
|
|
21
|
-
return (value) => (value.trim() ? undefined : `${label} is needed`);
|
|
22
|
-
}
|
|
23
|
-
export function integerIn(min, max) {
|
|
24
|
-
return (value) => {
|
|
25
|
-
if (!value.trim()) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
const parsed = Number(value);
|
|
29
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
30
|
-
return 'a whole number, please';
|
|
31
|
-
}
|
|
32
|
-
return parsed < min || parsed > max ? `between ${min} and ${max}` : undefined;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
export function matching(pattern, message) {
|
|
36
|
-
return (value) => !value.trim() || pattern.test(value.trim()) ? undefined : message;
|
|
25
|
+
return { specVersion: SPEC_VERSION, ...spec };
|
|
37
26
|
}
|
|
38
27
|
/**
|
|
39
|
-
*
|
|
40
|
-
* is
|
|
28
|
+
* Reads a spec that came from outside — a file, a package, an HTTP response.
|
|
29
|
+
* The version check is the whole point: once something else reads these, the
|
|
30
|
+
* shape is a contract, and a client that guesses at an unknown one is worse
|
|
31
|
+
* than a client that says which package needs updating.
|
|
41
32
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
export function parseSpecJson(text, source = 'spec') {
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = JSON.parse(text);
|
|
46
37
|
}
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new Error(`${source}: not valid JSON`, { cause: error });
|
|
40
|
+
}
|
|
41
|
+
return parseSpec(raw, source);
|
|
42
|
+
}
|
|
43
|
+
export function parseSpec(raw, source = 'spec') {
|
|
44
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
45
|
+
throw new Error(`${source}: a spec must be an object`);
|
|
49
46
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
:
|
|
47
|
+
const candidate = raw;
|
|
48
|
+
if (candidate.specVersion !== SPEC_VERSION) {
|
|
49
|
+
throw new Error(`${source}: specVersion ${String(candidate.specVersion)} is not ${SPEC_VERSION} — update whichever side is older`);
|
|
50
|
+
}
|
|
51
|
+
if (typeof candidate.name !== 'string' || !candidate.name.trim()) {
|
|
52
|
+
throw new Error(`${source}: a spec needs the application name it configures`);
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(candidate.sections)) {
|
|
55
|
+
throw new Error(`${source}: a spec needs a sections array`);
|
|
56
|
+
}
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
// --- Rules, written as data ---------------------------------------------------
|
|
60
|
+
export function required(label) {
|
|
61
|
+
return label
|
|
62
|
+
? { kind: 'required', message: `${label} is needed` }
|
|
63
|
+
: { kind: 'required' };
|
|
64
|
+
}
|
|
65
|
+
export function integerIn(min, max, message) {
|
|
66
|
+
return message ? { kind: 'integer', min, max, message } : { kind: 'integer', min, max };
|
|
67
|
+
}
|
|
68
|
+
export function matching(pattern, message) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'pattern',
|
|
71
|
+
pattern: typeof pattern === 'string' ? pattern : pattern.source,
|
|
72
|
+
message,
|
|
73
|
+
};
|
|
53
74
|
}
|
|
75
|
+
/** `2026-09-08 19:30` — a local time at the airport, with no zone on it. */
|
|
76
|
+
export const localDateTime = { kind: 'datetime', format: 'local' };
|
|
54
77
|
//# sourceMappingURL=config-spec.js.map
|
package/dist/config-spec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-spec.js","sourceRoot":"","sources":["../src/config-spec.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config-spec.js","sourceRoot":"","sources":["../src/config-spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmFH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAwC;IAExC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,IAAI,EAAE,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,MAAM,GAAG,MAAM;IACzD,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,kBAAkB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAY,EAAE,MAAM,GAAG,MAAM;IACrD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,4BAA4B,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,SAAS,GAAG,GAA6B,CAAC;IAEhD,IAAI,SAAS,CAAC,WAAW,KAAK,YAAY,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,iBAAiB,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,WAAW,YAAY,mCAAmC,CAClH,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,mDAAmD,CAAC,CAAC;IAChF,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,iCAAiC,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,SAA0B,CAAC;AACpC,CAAC;AAED,iFAAiF;AAEjF,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,KAAK;QACV,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,KAAK,YAAY,EAAE;QACrD,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,GAAW,EAAE,OAAgB;IAClE,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAwB,EAAE,OAAe;IAChE,OAAO;QACL,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM;QAC/D,OAAO;KACR,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,MAAM,aAAa,GAAmB,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConfigField, ValidationRule } from './config-spec.js';
|
|
2
|
+
/**
|
|
3
|
+
* One interpreter for every client — CLI, daemon, and the browser.
|
|
4
|
+
* Empty values are fine unless a `required` rule (or the field flag) says otherwise.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateValue(rules: readonly ValidationRule[] | undefined, value: string, field?: Pick<ConfigField, 'label' | 'required' | 'type'>, current?: string): string | undefined;
|
|
7
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAIjE;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,EAC5C,KAAK,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC,EACxD,OAAO,SAAK,GACX,MAAM,GAAG,SAAS,CAmBpB"}
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const LOCAL_DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{1,2}:\d{2}$/;
|
|
2
|
+
/**
|
|
3
|
+
* One interpreter for every client — CLI, daemon, and the browser.
|
|
4
|
+
* Empty values are fine unless a `required` rule (or the field flag) says otherwise.
|
|
5
|
+
*/
|
|
6
|
+
export function validateValue(rules, value, field, current = '') {
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
if (field?.required && !trimmed && !(field.type === 'secret' && current)) {
|
|
9
|
+
return `${field.label} is needed`;
|
|
10
|
+
}
|
|
11
|
+
if (!rules?.length) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
for (const rule of rules) {
|
|
15
|
+
const error = checkRule(rule, trimmed, field?.label);
|
|
16
|
+
if (error) {
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function checkRule(rule, value, label) {
|
|
23
|
+
switch (rule.kind) {
|
|
24
|
+
case 'required':
|
|
25
|
+
return value
|
|
26
|
+
? undefined
|
|
27
|
+
: (rule.message ?? (label ? `${label} is needed` : 'this is needed'));
|
|
28
|
+
case 'pattern': {
|
|
29
|
+
if (!value) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return new RegExp(rule.pattern).test(value) ? undefined : rule.message;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return rule.message;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
case 'integer': {
|
|
40
|
+
if (!value) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
45
|
+
return rule.message ?? 'a whole number, please';
|
|
46
|
+
}
|
|
47
|
+
return parsed < rule.min || parsed > rule.max
|
|
48
|
+
? (rule.message ?? `between ${rule.min} and ${rule.max}`)
|
|
49
|
+
: undefined;
|
|
50
|
+
}
|
|
51
|
+
case 'datetime': {
|
|
52
|
+
if (!value) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (rule.format !== 'local') {
|
|
56
|
+
return rule.message ?? 'unsupported datetime format';
|
|
57
|
+
}
|
|
58
|
+
if (!LOCAL_DATETIME.test(value)) {
|
|
59
|
+
return rule.message ?? 'YYYY-MM-DD HH:MM';
|
|
60
|
+
}
|
|
61
|
+
return Number.isNaN(Date.parse(value.replace(' ', 'T')))
|
|
62
|
+
? (rule.message ?? 'that is not a real date')
|
|
63
|
+
: undefined;
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
const _exhaustive = rule;
|
|
67
|
+
return `unknown rule ${JSON.stringify(_exhaustive)}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one line that stands for a record in a picker.
|
|
3
|
+
*
|
|
4
|
+
* A template rather than a function, because every client has to render it and
|
|
5
|
+
* only one of them can run JavaScript. Deliberately small — anything that needs
|
|
6
|
+
* more than this is a sign the app should be storing a different shape:
|
|
7
|
+
*
|
|
8
|
+
* {key} the value, or nothing
|
|
9
|
+
* {a|b} a if it has a value, else b — `{route|from-to}`
|
|
10
|
+
* {a-b} both joined by what sits between them, or nothing if either
|
|
11
|
+
* is missing, so a half-filled route is not rendered as `SVO-`
|
|
12
|
+
* BO{bo} a literal glued to a token goes when the token does
|
|
13
|
+
*
|
|
14
|
+
* Whitespace left behind by a missing value collapses; whitespace you put
|
|
15
|
+
* between two values that are both there is kept, so columns still line up.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderSummary(template: string, entry: Record<string, string>): string;
|
|
18
|
+
//# sourceMappingURL=summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CA6BrF"}
|
package/dist/summary.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one line that stands for a record in a picker.
|
|
3
|
+
*
|
|
4
|
+
* A template rather than a function, because every client has to render it and
|
|
5
|
+
* only one of them can run JavaScript. Deliberately small — anything that needs
|
|
6
|
+
* more than this is a sign the app should be storing a different shape:
|
|
7
|
+
*
|
|
8
|
+
* {key} the value, or nothing
|
|
9
|
+
* {a|b} a if it has a value, else b — `{route|from-to}`
|
|
10
|
+
* {a-b} both joined by what sits between them, or nothing if either
|
|
11
|
+
* is missing, so a half-filled route is not rendered as `SVO-`
|
|
12
|
+
* BO{bo} a literal glued to a token goes when the token does
|
|
13
|
+
*
|
|
14
|
+
* Whitespace left behind by a missing value collapses; whitespace you put
|
|
15
|
+
* between two values that are both there is kept, so columns still line up.
|
|
16
|
+
*/
|
|
17
|
+
export function renderSummary(template, entry) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
let carry = '';
|
|
20
|
+
// One pass: a token plus whatever literal is glued to its left, so the
|
|
21
|
+
// literal can leave with it.
|
|
22
|
+
const pattern = /([^\s{}]*)\{([^{}]+)\}/g;
|
|
23
|
+
let at = 0;
|
|
24
|
+
for (let match = pattern.exec(template); match; match = pattern.exec(template)) {
|
|
25
|
+
carry += template.slice(at, match.index);
|
|
26
|
+
at = match.index + match[0].length;
|
|
27
|
+
const value = resolve(match[2] ?? '', entry);
|
|
28
|
+
if (value) {
|
|
29
|
+
parts.push(`${carry}${match[1] ?? ''}${value}`);
|
|
30
|
+
carry = '';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// The token and its glued prefix are dropped; the separator before them
|
|
34
|
+
// is dropped with them rather than doubling up.
|
|
35
|
+
carry = carry.trimEnd() ? carry : '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const tail = `${carry}${template.slice(at)}`;
|
|
39
|
+
if (tail.trim()) {
|
|
40
|
+
parts.push(tail);
|
|
41
|
+
}
|
|
42
|
+
return parts.join('').replace(/^\s+|\s+$/g, '');
|
|
43
|
+
}
|
|
44
|
+
function resolve(token, entry) {
|
|
45
|
+
const bar = token.indexOf('|');
|
|
46
|
+
if (bar !== -1) {
|
|
47
|
+
const left = resolve(token.slice(0, bar), entry);
|
|
48
|
+
return left || resolve(token.slice(bar + 1), entry);
|
|
49
|
+
}
|
|
50
|
+
// `{from-to}` and friends: two keys with a literal between them, present only
|
|
51
|
+
// when both sides are.
|
|
52
|
+
const joined = /^([A-Za-z_]\w*)(\W+)([A-Za-z_]\w*)$/.exec(token);
|
|
53
|
+
if (joined) {
|
|
54
|
+
const left = value(entry, joined[1]);
|
|
55
|
+
const right = value(entry, joined[3]);
|
|
56
|
+
return left && right ? `${left}${joined[2] ?? ''}${right}` : '';
|
|
57
|
+
}
|
|
58
|
+
return value(entry, token);
|
|
59
|
+
}
|
|
60
|
+
function value(entry, key) {
|
|
61
|
+
return key ? (entry[key] ?? '').trim() : '';
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.js","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busybar-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Shared building blocks for BUSY Bar apps: device geometry, colours, formatters, ticker text, error taxonomy and an offline PNG preview renderer",
|
|
5
5
|
"author": "viirtualp1",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"./colors": "./dist/colors.js",
|
|
21
21
|
"./config": "./dist/config/index.js",
|
|
22
22
|
"./config-spec": "./dist/config-spec.js",
|
|
23
|
+
"./rules": "./dist/rules.js",
|
|
24
|
+
"./summary": "./dist/summary.js",
|
|
23
25
|
"./device": "./dist/device.js",
|
|
24
26
|
"./elements": "./dist/elements.js",
|
|
25
27
|
"./errors": "./dist/errors.js",
|
|
@@ -34,7 +36,8 @@
|
|
|
34
36
|
"./package.json": "./package.json"
|
|
35
37
|
},
|
|
36
38
|
"bin": {
|
|
37
|
-
"busybar-fix-esm": "scripts/fix-esm-extensions.mjs"
|
|
39
|
+
"busybar-fix-esm": "scripts/fix-esm-extensions.mjs",
|
|
40
|
+
"busybar-emit-spec": "scripts/emit-config-spec.mjs"
|
|
38
41
|
},
|
|
39
42
|
"files": [
|
|
40
43
|
"dist",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Writes `dist/config-spec.json` from the built `dist/config-spec.js`.
|
|
3
|
+
//
|
|
4
|
+
// Specs are authored in TypeScript for the types and the autocompletion, but a
|
|
5
|
+
// browser or a Dart client cannot import a JS module out of node_modules. The
|
|
6
|
+
// JSON beside it is what makes the spec readable by anything at all — and
|
|
7
|
+
// emitting it from the built module rather than hand-maintaining a second copy
|
|
8
|
+
// is what keeps the two from drifting.
|
|
9
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const argument = process.argv[2] ?? 'dist/config-spec.js';
|
|
14
|
+
const source = isAbsolute(argument) ? argument : resolve(process.cwd(), argument);
|
|
15
|
+
|
|
16
|
+
if (!existsSync(source)) {
|
|
17
|
+
console.error(`busybar-emit-spec: ${source} is not there — build first`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const module = await import(pathToFileURL(source).href);
|
|
22
|
+
const spec = module.default;
|
|
23
|
+
|
|
24
|
+
if (!spec || typeof spec !== 'object' || typeof spec.name !== 'string') {
|
|
25
|
+
console.error(`busybar-emit-spec: ${source} has no config spec as its default export`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const target = source.replace(/\.js$/, '.json');
|
|
30
|
+
writeFileSync(target, `${JSON.stringify(spec, null, 2)}\n`);
|
|
31
|
+
console.log(`busybar-emit-spec: wrote ${target}`);
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
|
|
5
|
-
// Node's ESM loader wants a real file name; tsc leaves extensionless specifiers.
|
|
6
|
-
const argument = process.argv[2] ?? 'dist';
|
|
7
|
-
const dist = isAbsolute(argument) ? argument : resolve(process.cwd(), argument);
|
|
8
|
-
const EXTENSIONS = /\.(?:js|json|mjs|cjs|node)$/;
|
|
9
|
-
|
|
10
|
-
function walk(dir) {
|
|
11
|
-
for (const name of readdirSync(dir)) {
|
|
12
|
-
const path = join(dir, name);
|
|
13
|
-
if (statSync(path).isDirectory()) {
|
|
14
|
-
walk(path);
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
if (!/\.(?:js|d\.ts)$/.test(name)) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const source = readFileSync(path, 'utf8');
|
|
22
|
-
const next = source.replace(
|
|
23
|
-
/(from\s+|import\s*\()(['"])(\.[^'"]+)\2/g,
|
|
24
|
-
(match, prefix, quote, spec) =>
|
|
25
|
-
EXTENSIONS.test(spec) ? match : `${prefix}${quote}${spec}.js${quote}`,
|
|
26
|
-
);
|
|
27
|
-
if (next !== source) {
|
|
28
|
-
writeFileSync(path, next);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
walk(dist);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Node's ESM loader wants a real file name; tsc leaves extensionless specifiers.
|
|
6
|
+
const argument = process.argv[2] ?? 'dist';
|
|
7
|
+
const dist = isAbsolute(argument) ? argument : resolve(process.cwd(), argument);
|
|
8
|
+
const EXTENSIONS = /\.(?:js|json|mjs|cjs|node)$/;
|
|
9
|
+
|
|
10
|
+
function walk(dir) {
|
|
11
|
+
for (const name of readdirSync(dir)) {
|
|
12
|
+
const path = join(dir, name);
|
|
13
|
+
if (statSync(path).isDirectory()) {
|
|
14
|
+
walk(path);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!/\.(?:js|d\.ts)$/.test(name)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const source = readFileSync(path, 'utf8');
|
|
22
|
+
const next = source.replace(
|
|
23
|
+
/(from\s+|import\s*\()(['"])(\.[^'"]+)\2/g,
|
|
24
|
+
(match, prefix, quote, spec) =>
|
|
25
|
+
EXTENSIONS.test(spec) ? match : `${prefix}${quote}${spec}.js${quote}`,
|
|
26
|
+
);
|
|
27
|
+
if (next !== source) {
|
|
28
|
+
writeFileSync(path, next);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
walk(dist);
|