@navbytes/vee 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 +118 -0
- package/dist/vee.d.ts +413 -0
- package/dist/vee.js +356 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Vee TypeScript SDK
|
|
2
|
+
|
|
3
|
+
A tiny, zero-dependency TypeScript SDK for writing Vee plugins with typed
|
|
4
|
+
builders instead of hand-formatting the xbar/SwiftBar text protocol. It mirrors
|
|
5
|
+
the [Python](../python) and [Go](../go) SDKs — same builder shape, option names,
|
|
6
|
+
encoding order, and quoting — and produces byte-identical output. Node runs the
|
|
7
|
+
TypeScript directly (type-stripping), so there is no build step.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node 24+ (for native TypeScript execution). No dependencies.
|
|
12
|
+
|
|
13
|
+
## Installing
|
|
14
|
+
|
|
15
|
+
A Vee plugin is a single executable dropped in your plugins folder — no build
|
|
16
|
+
step, no `node_modules`. The SDK therefore travels *with* the plugin as a
|
|
17
|
+
sibling file rather than being resolved from a package manager:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
vee sdk ts --out ~/path/to/your/plugins # writes vee.ts there
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Menu } from "./vee.ts";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`vee new --lang ts --out DIR` does both at once — it scaffolds a plugin and
|
|
28
|
+
writes `vee.ts` beside it, so the result runs immediately.
|
|
29
|
+
|
|
30
|
+
**Or from npm**, if your plugin is part of a project that already has a
|
|
31
|
+
`node_modules` — a bundled plugin, or one you build before dropping in:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install @navbytes/vee
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Menu } from "@navbytes/vee";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The package ships compiled JavaScript with type declarations, because Node
|
|
42
|
+
refuses to strip types under `node_modules`. It has no dependencies, and it
|
|
43
|
+
carries the same version as the app it was released with — the SDK and the
|
|
44
|
+
parser that reads its output ship from one commit, so a matching pair is a
|
|
45
|
+
guaranteed-compatible pair.
|
|
46
|
+
|
|
47
|
+
The examples in this repository import `../vee.ts` because they sit next to the
|
|
48
|
+
SDK here. Copying one out means running `vee sdk ts` beside it and changing that
|
|
49
|
+
import to `./vee.ts`.
|
|
50
|
+
|
|
51
|
+
## Layout
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
plugins/typescript/
|
|
55
|
+
├─ vee.ts # the SDK: Menu, Section, ItemOptions
|
|
56
|
+
├─ examples/*.ts # example plugins; each exports build() -> string
|
|
57
|
+
├─ test/*.test.ts # drift guard (node --test)
|
|
58
|
+
└─ package.json # npm test, npm run build:fixtures
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Golden fixtures live one level up in [`../fixtures/`](../fixtures) and are shared
|
|
62
|
+
by all three SDKs — see the [plugins README](../README.md).
|
|
63
|
+
|
|
64
|
+
## Hello world
|
|
65
|
+
|
|
66
|
+
Create `cpu.5s.ts` in your plugins folder:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
#!/usr/bin/env node
|
|
70
|
+
import { Menu } from "./vee.ts";
|
|
71
|
+
|
|
72
|
+
const menu = new Menu();
|
|
73
|
+
menu.title("CPU 12%", { color: "green", sfimage: "cpu" });
|
|
74
|
+
|
|
75
|
+
const d = menu.dropdown;
|
|
76
|
+
d.item("Top processes", { href: "https://example.com/procs" });
|
|
77
|
+
d.separator();
|
|
78
|
+
|
|
79
|
+
const details = d.submenu("Details");
|
|
80
|
+
details.item("Load: 1.20");
|
|
81
|
+
details.item("Cores: 8");
|
|
82
|
+
|
|
83
|
+
d.item("Refresh", { refresh: true });
|
|
84
|
+
|
|
85
|
+
menu.print();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Make it executable (`chmod +x cpu.5s.ts`) and drop it in your plugins folder.
|
|
89
|
+
The `.5s` sets a 5-second refresh, exactly as with any other plugin.
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
The three SDKs expose the same `Menu` / `Section` / options surface, method for
|
|
94
|
+
method, and are checked against each other so they cannot drift. Rather than
|
|
95
|
+
restate a third of that contract here, the full cross-language reference —
|
|
96
|
+
every method, every option, and the TypeScript spelling of each — lives in one
|
|
97
|
+
place:
|
|
98
|
+
|
|
99
|
+
**[Plugin SDKs reference](https://vee.navbytes.io/guide/sdk/)**
|
|
100
|
+
|
|
101
|
+
For the parameters themselves — what each one accepts, its default, and which
|
|
102
|
+
chart it belongs to — see the [plugin authoring
|
|
103
|
+
reference](https://vee.navbytes.io/guide/plugin-authoring/) and
|
|
104
|
+
[Charts](https://vee.navbytes.io/guide/charts/), both generated from
|
|
105
|
+
`docs/api/params.json`, the same record this SDK is verified against.
|
|
106
|
+
|
|
107
|
+
## Tests
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
cd plugins/typescript
|
|
111
|
+
npm test # fixture drift guard (node --test)
|
|
112
|
+
npm run build:fixtures # regenerate ../fixtures from the examples
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The drift guard runs each example's `build()` and asserts the output matches its
|
|
116
|
+
golden fixture in `../fixtures/`. Because those fixtures are shared with the
|
|
117
|
+
Python and Go SDKs and parsed by the Swift `VeePluginFormat` tests, this keeps
|
|
118
|
+
every SDK, the fixtures, and the parser in lockstep.
|
package/dist/vee.d.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
export type Color = string;
|
|
2
|
+
export interface ItemOptions {
|
|
3
|
+
color?: Color;
|
|
4
|
+
size?: number;
|
|
5
|
+
font?: string;
|
|
6
|
+
length?: number;
|
|
7
|
+
/** Trim surrounding whitespace from the text. */
|
|
8
|
+
trim?: boolean;
|
|
9
|
+
/** Interpret ANSI colour escapes in the text. */
|
|
10
|
+
ansi?: boolean;
|
|
11
|
+
/** Expand `:emoji:` shortcodes in the text. */
|
|
12
|
+
emojize?: boolean;
|
|
13
|
+
href?: string;
|
|
14
|
+
/** Shell command to run on click; `params` become param1..N. */
|
|
15
|
+
shell?: string;
|
|
16
|
+
params?: string[];
|
|
17
|
+
terminal?: boolean;
|
|
18
|
+
refresh?: boolean;
|
|
19
|
+
/** Show this line in the dropdown only, never in the menu bar. */
|
|
20
|
+
dropdown?: boolean;
|
|
21
|
+
alternate?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
checked?: boolean;
|
|
24
|
+
key?: string;
|
|
25
|
+
tooltip?: string;
|
|
26
|
+
/** An image for the row: a base64 payload or a file path. */
|
|
27
|
+
image?: string;
|
|
28
|
+
/** Like `image`, but rendered as a template image (adapts to the theme). */
|
|
29
|
+
templateImage?: string;
|
|
30
|
+
/** SF Symbol name (SwiftBar/Vee extension). */
|
|
31
|
+
sfimage?: string;
|
|
32
|
+
/**
|
|
33
|
+
* SF Symbol colour(s) → `sfcolor=`. A list supplies one colour per layer of
|
|
34
|
+
* a multicolour symbol; Vee reads it as a comma-separated list, so keep
|
|
35
|
+
* commas out of colour names.
|
|
36
|
+
*/
|
|
37
|
+
sfColor?: Color | Color[];
|
|
38
|
+
/** SF Symbol point size → `sfsize=`. */
|
|
39
|
+
sfSize?: number;
|
|
40
|
+
/** SF Symbol configuration string → `sfconfig=`. */
|
|
41
|
+
sfConfig?: string;
|
|
42
|
+
/** Render the text as inline Markdown. */
|
|
43
|
+
md?: boolean;
|
|
44
|
+
/** Trailing badge chip. */
|
|
45
|
+
badge?: string;
|
|
46
|
+
/** Render `:sf.symbol:` tokens in the text as inline SF Symbols. */
|
|
47
|
+
symbolize?: boolean;
|
|
48
|
+
/** Open this web URL in a web view on click → `webview=`. */
|
|
49
|
+
webview?: string;
|
|
50
|
+
/** Web view width in points → `webvieww=`. */
|
|
51
|
+
webviewW?: number;
|
|
52
|
+
/** Web view height in points → `webviewh=`. */
|
|
53
|
+
webviewH?: number;
|
|
54
|
+
/** Name of a macOS Shortcut to run on click → `shortcut=`. */
|
|
55
|
+
shortcut?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Render as a native, non-interactive section header (`header=true`) — a
|
|
58
|
+
* real `NSMenuItem.sectionHeader`, not a disabled row dressed up as one.
|
|
59
|
+
*/
|
|
60
|
+
header?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Which edge this row's visual accessory anchors to → `accessory=`. Applies
|
|
63
|
+
* uniformly to `sparkline`, `progress`, and the `chart` shapes, since they
|
|
64
|
+
* share the same in-row geometry. Omitted, the accessory sits trailing.
|
|
65
|
+
*/
|
|
66
|
+
accessory?: "leading" | "trailing";
|
|
67
|
+
/** Inline data series → `sparkline=1,2,3`. */
|
|
68
|
+
sparkline?: number[];
|
|
69
|
+
/**
|
|
70
|
+
* Sparkline width in points. `"full"` stretches the chart to the row's own
|
|
71
|
+
* width instead. Emitted as `accessoryw=`, which sizes whichever accessory a
|
|
72
|
+
* row carries; `progressW`, `chart.w` and `accessoryW` are the same knob
|
|
73
|
+
* reached from different option sets.
|
|
74
|
+
*/
|
|
75
|
+
sparklineW?: number | "full";
|
|
76
|
+
/**
|
|
77
|
+
* Width in points for whichever accessory this row carries — gauge,
|
|
78
|
+
* sparkline, chart, or slider → `accessoryw=`. `"full"` stretches it to the
|
|
79
|
+
* row's own width. Use this to size a `slider`, which has no option of its
|
|
80
|
+
* own; for the others the per-accessory options are equivalent.
|
|
81
|
+
*/
|
|
82
|
+
accessoryW?: number | "full";
|
|
83
|
+
/** Height in points for this row's accessory → `accessoryh=`. Ignored for a toggle or slider. */
|
|
84
|
+
accessoryH?: number;
|
|
85
|
+
/** Sparkline height in points, emitted as `accessoryh=`. */
|
|
86
|
+
sparklineH?: number;
|
|
87
|
+
/** Sparkline line colour → `sparklinecolor=`. Falls back to the row's `color`. */
|
|
88
|
+
sparklineColor?: Color;
|
|
89
|
+
/** On/off switch → `toggle=on` / `toggle=off`. */
|
|
90
|
+
toggle?: boolean;
|
|
91
|
+
/** Continuous control → `slider=min,max,value`. */
|
|
92
|
+
slider?: {
|
|
93
|
+
min: number;
|
|
94
|
+
max: number;
|
|
95
|
+
value: number;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Progress gauge. Pass a fraction directly → `progress=<fraction>`, or
|
|
99
|
+
* `{ value, max }` → `progress=<value>,<max>`, which the format accepts
|
|
100
|
+
* natively and Vee divides on parse. The two-argument form keeps the
|
|
101
|
+
* author's own numbers on the wire instead of a pre-divided float.
|
|
102
|
+
*/
|
|
103
|
+
progress?: number | {
|
|
104
|
+
value: number;
|
|
105
|
+
max: number;
|
|
106
|
+
};
|
|
107
|
+
/** Progress track (background) colour → `progresstrackcolor=`. */
|
|
108
|
+
progressTrackColor?: Color;
|
|
109
|
+
/**
|
|
110
|
+
* @deprecated The pre-v2 spelling of `progressTrackColor`. Still accepted and
|
|
111
|
+
* still emitted as `progresstrackcolor=`; will be removed in the next major
|
|
112
|
+
* version.
|
|
113
|
+
*/
|
|
114
|
+
trackColor?: Color;
|
|
115
|
+
/**
|
|
116
|
+
* Progress bar width in points, emitted as `accessoryw=`. `"full"` stretches the bar to
|
|
117
|
+
* the row's own width instead, the same knob `chart.w` takes.
|
|
118
|
+
*/
|
|
119
|
+
progressW?: number | "full";
|
|
120
|
+
/** Progress bar height in points, emitted as `accessoryh=`. */
|
|
121
|
+
progressH?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Categorical share chart → `pie=` / `donut=` / `stackedbar=`. All three
|
|
124
|
+
* shapes take the same data — one series of non-negative values read as
|
|
125
|
+
* shares of a whole — so switching `kind` needs no other change.
|
|
126
|
+
*
|
|
127
|
+
* `labels`/`colors` are positional against `values`. Vee reads both as
|
|
128
|
+
* comma-separated lists, so a label containing a comma would be read as two
|
|
129
|
+
* labels: keep commas out of segment names.
|
|
130
|
+
*/
|
|
131
|
+
chart?: {
|
|
132
|
+
kind: "pie" | "donut" | "stackedbar";
|
|
133
|
+
values: number[];
|
|
134
|
+
labels?: string[];
|
|
135
|
+
colors?: Color[];
|
|
136
|
+
/**
|
|
137
|
+
* Inline size in points, emitted as `accessoryw=`/`accessoryh=`. A pie/donut is a circle, so
|
|
138
|
+
* either knob sizes both sides; a stacked bar takes them independently.
|
|
139
|
+
* Omitted, a chart takes its per-kind default (24pt circle, 110×12 bar).
|
|
140
|
+
* `w: "full"` stretches the chart to the row's own width instead — a
|
|
141
|
+
* stacked bar only, since a circle has no free width (Vee warns and falls
|
|
142
|
+
* back to points on `pie`/`donut`).
|
|
143
|
+
*/
|
|
144
|
+
w?: number | "full";
|
|
145
|
+
h?: number;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** A menu section at a given submenu depth (0 = top level). */
|
|
149
|
+
export declare class Section {
|
|
150
|
+
private readonly lines;
|
|
151
|
+
private readonly depth;
|
|
152
|
+
constructor(lines: string[], depth: number);
|
|
153
|
+
private prefix;
|
|
154
|
+
item(text: string, options?: ItemOptions): this;
|
|
155
|
+
separator(): this;
|
|
156
|
+
/** Adds an item and returns a `Section` for its submenu. */
|
|
157
|
+
submenu(text: string, options?: ItemOptions): Section;
|
|
158
|
+
}
|
|
159
|
+
/** The top-level menu: title line(s) plus a dropdown. */
|
|
160
|
+
export declare class Menu {
|
|
161
|
+
private readonly titles;
|
|
162
|
+
private readonly body;
|
|
163
|
+
title(text: string, options?: ItemOptions): this;
|
|
164
|
+
get dropdown(): Section;
|
|
165
|
+
toString(): string;
|
|
166
|
+
print(): void;
|
|
167
|
+
}
|
|
168
|
+
export type WidgetTemplate = "stat" | "gauge" | "trend" | "list" | "board";
|
|
169
|
+
export type WidgetStatus = "ok" | "warning" | "error";
|
|
170
|
+
export type WidgetActionKind = "refresh" | "href" | "shortcut";
|
|
171
|
+
export interface WidgetCardItem {
|
|
172
|
+
label: string;
|
|
173
|
+
value?: string;
|
|
174
|
+
symbol?: string;
|
|
175
|
+
tint?: Color;
|
|
176
|
+
}
|
|
177
|
+
export interface WidgetCardAction {
|
|
178
|
+
kind: WidgetActionKind;
|
|
179
|
+
label: string;
|
|
180
|
+
/** The URL to open, for `kind: "href"`. Scheme-filtered by Vee on parse. */
|
|
181
|
+
url?: string;
|
|
182
|
+
/** The Shortcut name to run, for `kind: "shortcut"`. */
|
|
183
|
+
name?: string;
|
|
184
|
+
}
|
|
185
|
+
export interface WidgetCardOptions {
|
|
186
|
+
template?: WidgetTemplate;
|
|
187
|
+
title?: string;
|
|
188
|
+
/** SF Symbol name for the glyph. */
|
|
189
|
+
symbol?: string;
|
|
190
|
+
tint?: Color;
|
|
191
|
+
/** The headline value, already formatted (e.g. `"$18.2k"`). */
|
|
192
|
+
value?: string;
|
|
193
|
+
caption?: string;
|
|
194
|
+
detail?: string;
|
|
195
|
+
status?: WidgetStatus;
|
|
196
|
+
/** `0…1`; clamped by Vee if out of range. */
|
|
197
|
+
progress?: number;
|
|
198
|
+
trend?: number[];
|
|
199
|
+
/** Rows for the `list`/`board` templates. */
|
|
200
|
+
items?: WidgetCardItem[];
|
|
201
|
+
/** Up to two are rendered as buttons; the templates decide which. */
|
|
202
|
+
actions?: WidgetCardAction[];
|
|
203
|
+
/** Seconds — a hint for the next widget reload. */
|
|
204
|
+
refreshAfter?: number;
|
|
205
|
+
/** Seconds — when the tile should show a stale treatment. */
|
|
206
|
+
staleAfter?: number;
|
|
207
|
+
/**
|
|
208
|
+
* An optional composable **layout tree** — the escape hatch alongside the
|
|
209
|
+
* five preset templates, for layouts the presets can't express (two columns,
|
|
210
|
+
* a date rail, activity rings, a KPI grid). Build it with the node helpers
|
|
211
|
+
* (`VStack`/`HStack`/`Text`/`Image`/`Gauge`/…). When present, Vee renders the
|
|
212
|
+
* tree instead of `template`. See docs/design/widget-surface-contract.md.
|
|
213
|
+
*/
|
|
214
|
+
layout?: WidgetNode;
|
|
215
|
+
}
|
|
216
|
+
/** A font token, or an explicit point size (clamped 8…96) when a token won't fit. */
|
|
217
|
+
export interface NodeFont {
|
|
218
|
+
size?: "caption2" | "caption" | "footnote" | "subheadline" | "body" | "headline" | "title3" | "title2" | "title" | "largeTitle";
|
|
219
|
+
pointSize?: number;
|
|
220
|
+
weight?: "regular" | "medium" | "semibold" | "bold";
|
|
221
|
+
design?: "default" | "rounded" | "monospaced" | "serif";
|
|
222
|
+
}
|
|
223
|
+
/** Per-element modifiers. Only bounded, SwiftUI-cheap options are exposed. */
|
|
224
|
+
export interface NodeStyle {
|
|
225
|
+
font?: NodeFont;
|
|
226
|
+
tint?: Color;
|
|
227
|
+
/** Multiline text alignment. */
|
|
228
|
+
align?: "leading" | "center" | "trailing";
|
|
229
|
+
/** Uniform padding in points (clamped 0…64). */
|
|
230
|
+
padding?: number;
|
|
231
|
+
/** Maximum text lines (clamped 1…20). */
|
|
232
|
+
lineLimit?: number;
|
|
233
|
+
/** Keep numeric columns from jittering. */
|
|
234
|
+
monospacedDigit?: boolean;
|
|
235
|
+
/** Let a headline shrink to fit rather than truncate (clamped 0.3…1). */
|
|
236
|
+
minScale?: number;
|
|
237
|
+
/** Grow to fill available width (the only, bounded, width control). */
|
|
238
|
+
fill?: boolean;
|
|
239
|
+
}
|
|
240
|
+
export type NodeType = "vstack" | "hstack" | "zstack" | "grid" | "text" | "image" | "gauge" | "sparkline" | "spacer" | "divider";
|
|
241
|
+
export interface WidgetNode {
|
|
242
|
+
type: NodeType;
|
|
243
|
+
text?: string;
|
|
244
|
+
/** SF Symbol name, for an `image` node (v1 renders SF Symbols only). */
|
|
245
|
+
symbol?: string;
|
|
246
|
+
/** `0…1` fill, for a `gauge` node. */
|
|
247
|
+
value?: number;
|
|
248
|
+
/** Series, for a `sparkline` node. */
|
|
249
|
+
values?: number[];
|
|
250
|
+
/** `"linear"` (default) or `"circular"`, for a `gauge` node. */
|
|
251
|
+
gaugeStyle?: "linear" | "circular";
|
|
252
|
+
/** Cross-axis alignment, for a container. */
|
|
253
|
+
align?: string;
|
|
254
|
+
/** Inter-child spacing, for a container. */
|
|
255
|
+
spacing?: number;
|
|
256
|
+
/** Column count, for a `grid` (default 2; clamped 1…4). */
|
|
257
|
+
columns?: number;
|
|
258
|
+
/** Minimum length, for a `spacer`. */
|
|
259
|
+
minLength?: number;
|
|
260
|
+
/** Families this node renders in (`small`/`medium`/`large`); absent = all. */
|
|
261
|
+
families?: Array<"small" | "medium" | "large">;
|
|
262
|
+
style?: NodeStyle;
|
|
263
|
+
children?: WidgetNode[];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The widget-mode payload (see `WidgetCardOptions`). Call `.toString()`/
|
|
267
|
+
* `.print()` exactly once per `VEE_TARGET=widget` run with the richest data
|
|
268
|
+
* available — each native template (small/medium/large) takes what fits.
|
|
269
|
+
*/
|
|
270
|
+
export declare class WidgetCard {
|
|
271
|
+
private readonly options;
|
|
272
|
+
constructor(options?: WidgetCardOptions);
|
|
273
|
+
toString(): string;
|
|
274
|
+
print(): void;
|
|
275
|
+
}
|
|
276
|
+
/** Builds a widget card. Equivalent to `new WidgetCard(options)`. */
|
|
277
|
+
export declare function widgetCard(options?: WidgetCardOptions): WidgetCard;
|
|
278
|
+
type ContainerOpts = {
|
|
279
|
+
align?: string;
|
|
280
|
+
spacing?: number;
|
|
281
|
+
families?: WidgetNode["families"];
|
|
282
|
+
style?: NodeStyle;
|
|
283
|
+
};
|
|
284
|
+
type LeafOpts = {
|
|
285
|
+
families?: WidgetNode["families"];
|
|
286
|
+
style?: NodeStyle;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Builders for the layout tree. Namespaced (`Node.VStack(…)`) so they don't
|
|
290
|
+
* collide with the card-level template builders (`Stat`/`Gauge`/…) and stay
|
|
291
|
+
* clearly node-level. Each returns a `WidgetNode`; `widgetCard({ layout })`
|
|
292
|
+
* serializes it in the canonical key order the three SDKs share.
|
|
293
|
+
*/
|
|
294
|
+
export declare const Node: {
|
|
295
|
+
/** A vertical stack. */
|
|
296
|
+
VStack: (children: WidgetNode[], opts?: ContainerOpts) => WidgetNode;
|
|
297
|
+
/** A horizontal stack — side-by-side regions (two columns, a date rail, a row of cells). */
|
|
298
|
+
HStack: (children: WidgetNode[], opts?: ContainerOpts) => WidgetNode;
|
|
299
|
+
/** A depth stack — overlays and rings (e.g. concentric gauges). */
|
|
300
|
+
ZStack: (children: WidgetNode[], opts?: ContainerOpts) => WidgetNode;
|
|
301
|
+
/** A grid of `columns` (default 2, clamped 1…4) — KPI boards. */
|
|
302
|
+
Grid: (children: WidgetNode[], opts?: ContainerOpts & {
|
|
303
|
+
columns?: number;
|
|
304
|
+
}) => WidgetNode;
|
|
305
|
+
/** A text run. */
|
|
306
|
+
Text: (text: string, opts?: LeafOpts) => WidgetNode;
|
|
307
|
+
/** An SF Symbol glyph (v1 renders SF Symbols only). */
|
|
308
|
+
Image: (symbol: string, opts?: LeafOpts) => WidgetNode;
|
|
309
|
+
/** A gauge — `linear` (default) or `circular`. `value` is `0…1`. */
|
|
310
|
+
Gauge: (value: number, opts?: {
|
|
311
|
+
gaugeStyle?: "linear" | "circular";
|
|
312
|
+
} & LeafOpts) => WidgetNode;
|
|
313
|
+
/** A dependency-free line chart from `values`. */
|
|
314
|
+
Sparkline: (values: number[], opts?: LeafOpts) => WidgetNode;
|
|
315
|
+
/** Flexible empty space. */
|
|
316
|
+
Spacer: (opts?: {
|
|
317
|
+
minLength?: number;
|
|
318
|
+
families?: WidgetNode["families"];
|
|
319
|
+
}) => WidgetNode;
|
|
320
|
+
/** A hairline divider. */
|
|
321
|
+
Divider: (opts?: {
|
|
322
|
+
families?: WidgetNode["families"];
|
|
323
|
+
}) => WidgetNode;
|
|
324
|
+
};
|
|
325
|
+
type TemplatelessOptions = Omit<WidgetCardOptions, "template">;
|
|
326
|
+
/** Glyph, big `value` in `tint`, `title`/`caption`. The default template. */
|
|
327
|
+
export declare function Stat(options: TemplatelessOptions): WidgetCard;
|
|
328
|
+
/** Stat + a native gauge from `progress`. */
|
|
329
|
+
export declare function Gauge(options: TemplatelessOptions): WidgetCard;
|
|
330
|
+
/** Stat + a sparkline from `trend`. */
|
|
331
|
+
export declare function Trend(options: TemplatelessOptions): WidgetCard;
|
|
332
|
+
/** `title` header + `items` as rows. */
|
|
333
|
+
export declare function List(options: TemplatelessOptions): WidgetCard;
|
|
334
|
+
/** A compact grid of `items` as stat cells (KPI board). */
|
|
335
|
+
export declare function Board(options: TemplatelessOptions): WidgetCard;
|
|
336
|
+
/** Options for a JSON menu item.
|
|
337
|
+
*
|
|
338
|
+
* The JSON protocol carries a subset of the text protocol's parameters, so this
|
|
339
|
+
* is a distinct type rather than `ItemOptions`: an option JSON cannot express
|
|
340
|
+
* is a compile error here, not a key that is silently dropped on the way out.
|
|
341
|
+
*/
|
|
342
|
+
export interface JSONItemOptions {
|
|
343
|
+
color?: Color;
|
|
344
|
+
size?: number;
|
|
345
|
+
href?: string;
|
|
346
|
+
shell?: string;
|
|
347
|
+
params?: string[];
|
|
348
|
+
terminal?: boolean;
|
|
349
|
+
refresh?: boolean;
|
|
350
|
+
sfimage?: string;
|
|
351
|
+
disabled?: boolean;
|
|
352
|
+
checked?: boolean;
|
|
353
|
+
tooltip?: string;
|
|
354
|
+
header?: boolean;
|
|
355
|
+
accessory?: "leading" | "trailing";
|
|
356
|
+
sparkline?: number[];
|
|
357
|
+
/** Sparkline width in points; `"full"` stretches it to the row's width. */
|
|
358
|
+
/** @deprecated Use `accessoryWidth`. */
|
|
359
|
+
sparklineWidth?: number | "full";
|
|
360
|
+
/** Width for whichever accessory this item carries, or `"full"`. */
|
|
361
|
+
accessoryWidth?: number | "full";
|
|
362
|
+
/** Height for whichever accessory this item carries. */
|
|
363
|
+
accessoryHeight?: number;
|
|
364
|
+
sparklineHeight?: number;
|
|
365
|
+
sparklineColor?: Color;
|
|
366
|
+
toggle?: boolean;
|
|
367
|
+
slider?: {
|
|
368
|
+
min: number;
|
|
369
|
+
max: number;
|
|
370
|
+
value: number;
|
|
371
|
+
};
|
|
372
|
+
/** A completion fraction, clamped to `0…1` by Vee. */
|
|
373
|
+
progress?: number;
|
|
374
|
+
progressTrackColor?: Color;
|
|
375
|
+
/** Progress bar width in points; `"full"` stretches it to the row's width. */
|
|
376
|
+
progressWidth?: number | "full";
|
|
377
|
+
progressHeight?: number;
|
|
378
|
+
chart?: {
|
|
379
|
+
kind: "pie" | "donut" | "stackedbar";
|
|
380
|
+
values: number[];
|
|
381
|
+
labels?: string[];
|
|
382
|
+
colors?: Color[];
|
|
383
|
+
w?: number | "full";
|
|
384
|
+
h?: number;
|
|
385
|
+
};
|
|
386
|
+
/** An alternate row, shown while ⌥ is held. */
|
|
387
|
+
alternate?: JSONItem;
|
|
388
|
+
}
|
|
389
|
+
/** One item in a JSON menu: `JSONItemOptions` plus the structural keys. */
|
|
390
|
+
export interface JSONItem extends JSONItemOptions {
|
|
391
|
+
text?: string;
|
|
392
|
+
separator?: boolean;
|
|
393
|
+
submenu?: JSONItem[];
|
|
394
|
+
}
|
|
395
|
+
/** A JSON menu section at a given submenu depth. Mirrors `Section`. */
|
|
396
|
+
export declare class JSONSection {
|
|
397
|
+
private readonly items;
|
|
398
|
+
constructor(items: JSONItem[]);
|
|
399
|
+
item(text: string, options?: JSONItemOptions): this;
|
|
400
|
+
separator(): this;
|
|
401
|
+
/** Adds an item and returns a `JSONSection` for its submenu. */
|
|
402
|
+
submenu(text: string, options?: JSONItemOptions): JSONSection;
|
|
403
|
+
}
|
|
404
|
+
/** The top-level JSON menu: title line(s) plus a dropdown. Mirrors `Menu`. */
|
|
405
|
+
export declare class JSONMenu {
|
|
406
|
+
private readonly titles;
|
|
407
|
+
private readonly body;
|
|
408
|
+
title(text: string, options?: JSONItemOptions): this;
|
|
409
|
+
get dropdown(): JSONSection;
|
|
410
|
+
toString(): string;
|
|
411
|
+
print(): void;
|
|
412
|
+
}
|
|
413
|
+
export {};
|
package/dist/vee.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Vee plugin SDK — typed builders that emit the xbar/SwiftBar text format Vee
|
|
2
|
+
// parses. Zero dependencies; runs directly on Node (which strips the types).
|
|
3
|
+
// Vee's parser (LineParser.splitTextAndParams/parseParams) reads `\|`, `\n`,
|
|
4
|
+
// and `\\` as escapes — for a literal `|` (which would otherwise be read as
|
|
5
|
+
// the text/params delimiter) and a literal newline (which would otherwise
|
|
6
|
+
// split a plugin's single stdout line into two corrupted ones). Order matters:
|
|
7
|
+
// backslashes must be escaped first, or the backslash `escapeText` inserts for
|
|
8
|
+
// `|`/newline would itself get re-escaped.
|
|
9
|
+
function escapeText(value) {
|
|
10
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, "\\n");
|
|
11
|
+
}
|
|
12
|
+
// The characters that force a value through the quoted path. `\s` here is the
|
|
13
|
+
// reference definition the Python and Go SDKs mirror explicitly — each
|
|
14
|
+
// language's own "whitespace" class differs at the edges (Python's adds
|
|
15
|
+
// U+001C–U+001F, Go's `unicode.IsSpace` omits U+FEFF), so the set is written
|
|
16
|
+
// out there rather than inherited.
|
|
17
|
+
const NEEDS_QUOTE = /[\s|\\]/;
|
|
18
|
+
function quote(value) {
|
|
19
|
+
const escaped = escapeText(value);
|
|
20
|
+
// Backslash also forces quoting: an unquoted (bare) value is never
|
|
21
|
+
// unescaped by the parser, so anything containing an escape must go through
|
|
22
|
+
// the quoted path, which is.
|
|
23
|
+
//
|
|
24
|
+
// A leading quote character forces it too: the parser decides a value is
|
|
25
|
+
// quoted by looking at its first character, so emitting `"a"` bare would
|
|
26
|
+
// round-trip back as `a` with the quotes eaten. Values that merely *contain*
|
|
27
|
+
// a quote are safe bare — only the first position is read as a delimiter.
|
|
28
|
+
if (NEEDS_QUOTE.test(value) || value.startsWith('"') || value.startsWith("'")) {
|
|
29
|
+
return `"${escaped.replace(/"/g, '\\"')}"`;
|
|
30
|
+
}
|
|
31
|
+
return escaped;
|
|
32
|
+
}
|
|
33
|
+
function encode(options) {
|
|
34
|
+
if (!options)
|
|
35
|
+
return "";
|
|
36
|
+
const parts = [];
|
|
37
|
+
const push = (key, value) => {
|
|
38
|
+
if (value !== undefined && value !== null)
|
|
39
|
+
parts.push(`${key}=${quote(String(value))}`);
|
|
40
|
+
};
|
|
41
|
+
push("color", options.color);
|
|
42
|
+
push("size", options.size);
|
|
43
|
+
push("font", options.font);
|
|
44
|
+
push("length", options.length);
|
|
45
|
+
push("trim", options.trim);
|
|
46
|
+
push("ansi", options.ansi);
|
|
47
|
+
push("emojize", options.emojize);
|
|
48
|
+
push("href", options.href);
|
|
49
|
+
if (options.shell !== undefined) {
|
|
50
|
+
push("shell", options.shell);
|
|
51
|
+
(options.params ?? []).forEach((p, i) => push(`param${i + 1}`, p));
|
|
52
|
+
}
|
|
53
|
+
push("terminal", options.terminal);
|
|
54
|
+
push("refresh", options.refresh);
|
|
55
|
+
push("dropdown", options.dropdown);
|
|
56
|
+
push("alternate", options.alternate);
|
|
57
|
+
push("disabled", options.disabled);
|
|
58
|
+
push("checked", options.checked);
|
|
59
|
+
push("key", options.key);
|
|
60
|
+
push("tooltip", options.tooltip);
|
|
61
|
+
push("image", options.image);
|
|
62
|
+
push("templateimage", options.templateImage);
|
|
63
|
+
push("sfimage", options.sfimage);
|
|
64
|
+
if (options.sfColor !== undefined) {
|
|
65
|
+
push("sfcolor", Array.isArray(options.sfColor) ? options.sfColor.join(",") : options.sfColor);
|
|
66
|
+
}
|
|
67
|
+
push("sfsize", options.sfSize);
|
|
68
|
+
push("sfconfig", options.sfConfig);
|
|
69
|
+
push("md", options.md);
|
|
70
|
+
push("badge", options.badge);
|
|
71
|
+
push("symbolize", options.symbolize);
|
|
72
|
+
push("webview", options.webview);
|
|
73
|
+
push("webvieww", options.webviewW);
|
|
74
|
+
push("webviewh", options.webviewH);
|
|
75
|
+
push("shortcut", options.shortcut);
|
|
76
|
+
push("header", options.header);
|
|
77
|
+
push("accessory", options.accessory);
|
|
78
|
+
if (options.sparkline !== undefined)
|
|
79
|
+
push("sparkline", options.sparkline.map(String).join(","));
|
|
80
|
+
push("sparklinecolor", options.sparklineColor);
|
|
81
|
+
if (options.toggle !== undefined)
|
|
82
|
+
push("toggle", options.toggle ? "on" : "off");
|
|
83
|
+
if (options.slider !== undefined) {
|
|
84
|
+
const s = options.slider;
|
|
85
|
+
push("slider", `${s.min},${s.max},${s.value}`);
|
|
86
|
+
}
|
|
87
|
+
if (options.progress !== undefined) {
|
|
88
|
+
const p = options.progress;
|
|
89
|
+
push("progress", typeof p === "number" ? String(p) : `${p.value},${p.max}`);
|
|
90
|
+
}
|
|
91
|
+
push("progresstrackcolor", options.progressTrackColor ?? options.trackColor);
|
|
92
|
+
if (options.chart !== undefined) {
|
|
93
|
+
const c = options.chart;
|
|
94
|
+
push(c.kind, c.values.map(String).join(","));
|
|
95
|
+
if (c.labels !== undefined)
|
|
96
|
+
push("chartlabels", c.labels.join(","));
|
|
97
|
+
if (c.colors !== undefined)
|
|
98
|
+
push("chartcolors", c.colors.join(","));
|
|
99
|
+
}
|
|
100
|
+
// One wire parameter sizes whichever accessory the row carries, so the
|
|
101
|
+
// per-accessory options above all funnel here. They stay separate in the API
|
|
102
|
+
// because a typed builder already knows which accessory you are describing —
|
|
103
|
+
// the ambiguity `accessoryw=` solves for hand-written lines cannot arise.
|
|
104
|
+
push("accessoryw", options.accessoryW ?? options.sparklineW ?? options.progressW ?? options.chart?.w);
|
|
105
|
+
push("accessoryh", options.accessoryH ?? options.sparklineH ?? options.progressH ?? options.chart?.h);
|
|
106
|
+
return parts.length ? " | " + parts.join(" ") : "";
|
|
107
|
+
}
|
|
108
|
+
/** A menu section at a given submenu depth (0 = top level). */
|
|
109
|
+
export class Section {
|
|
110
|
+
lines;
|
|
111
|
+
depth;
|
|
112
|
+
constructor(lines, depth) {
|
|
113
|
+
this.lines = lines;
|
|
114
|
+
this.depth = depth;
|
|
115
|
+
}
|
|
116
|
+
prefix() {
|
|
117
|
+
return "-".repeat(this.depth * 2);
|
|
118
|
+
}
|
|
119
|
+
item(text, options) {
|
|
120
|
+
this.lines.push(this.prefix() + escapeText(text) + encode(options));
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
separator() {
|
|
124
|
+
this.lines.push(this.prefix() + "---");
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
/** Adds an item and returns a `Section` for its submenu. */
|
|
128
|
+
submenu(text, options) {
|
|
129
|
+
this.item(text, options);
|
|
130
|
+
return new Section(this.lines, this.depth + 1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** The top-level menu: title line(s) plus a dropdown. */
|
|
134
|
+
export class Menu {
|
|
135
|
+
titles = [];
|
|
136
|
+
body = [];
|
|
137
|
+
title(text, options) {
|
|
138
|
+
this.titles.push(escapeText(text) + encode(options));
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
get dropdown() {
|
|
142
|
+
return new Section(this.body, 0);
|
|
143
|
+
}
|
|
144
|
+
toString() {
|
|
145
|
+
const head = this.titles.join("\n");
|
|
146
|
+
return this.body.length ? `${head}\n---\n${this.body.join("\n")}` : head;
|
|
147
|
+
}
|
|
148
|
+
print() {
|
|
149
|
+
process.stdout.write(this.toString() + "\n");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The widget-mode payload (see `WidgetCardOptions`). Call `.toString()`/
|
|
154
|
+
* `.print()` exactly once per `VEE_TARGET=widget` run with the richest data
|
|
155
|
+
* available — each native template (small/medium/large) takes what fits.
|
|
156
|
+
*/
|
|
157
|
+
export class WidgetCard {
|
|
158
|
+
options;
|
|
159
|
+
constructor(options = {}) {
|
|
160
|
+
this.options = options;
|
|
161
|
+
}
|
|
162
|
+
toString() {
|
|
163
|
+
const o = this.options;
|
|
164
|
+
const payload = { vee_widget: 1 };
|
|
165
|
+
const push = (key, value) => {
|
|
166
|
+
if (value !== undefined)
|
|
167
|
+
payload[key] = value;
|
|
168
|
+
};
|
|
169
|
+
push("template", o.template);
|
|
170
|
+
push("title", o.title);
|
|
171
|
+
push("symbol", o.symbol);
|
|
172
|
+
push("tint", o.tint);
|
|
173
|
+
push("value", o.value);
|
|
174
|
+
push("caption", o.caption);
|
|
175
|
+
push("detail", o.detail);
|
|
176
|
+
push("status", o.status);
|
|
177
|
+
push("progress", o.progress);
|
|
178
|
+
push("trend", o.trend);
|
|
179
|
+
push("items", o.items);
|
|
180
|
+
push("actions", o.actions);
|
|
181
|
+
push("refresh_after", o.refreshAfter);
|
|
182
|
+
push("stale_after", o.staleAfter);
|
|
183
|
+
push("layout", o.layout ? orderNode(o.layout) : undefined);
|
|
184
|
+
return JSON.stringify(payload);
|
|
185
|
+
}
|
|
186
|
+
print() {
|
|
187
|
+
process.stdout.write(this.toString() + "\n");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Builds a widget card. Equivalent to `new WidgetCard(options)`. */
|
|
191
|
+
export function widgetCard(options) {
|
|
192
|
+
return new WidgetCard(options);
|
|
193
|
+
}
|
|
194
|
+
// ── Layout node serialization + builders ─────────────────────────────────────
|
|
195
|
+
/** Rebuilds a node with keys in the canonical order the three SDKs share, so
|
|
196
|
+
* output is byte-identical regardless of how the node object was constructed.
|
|
197
|
+
* `undefined` keys are dropped; `0`/`false` are kept. */
|
|
198
|
+
function orderNode(n) {
|
|
199
|
+
const o = {};
|
|
200
|
+
const put = (k, v) => { if (v !== undefined)
|
|
201
|
+
o[k] = v; };
|
|
202
|
+
put("type", n.type);
|
|
203
|
+
put("text", n.text);
|
|
204
|
+
put("symbol", n.symbol);
|
|
205
|
+
put("value", n.value);
|
|
206
|
+
put("values", n.values);
|
|
207
|
+
put("gauge_style", n.gaugeStyle);
|
|
208
|
+
put("align", n.align);
|
|
209
|
+
put("spacing", n.spacing);
|
|
210
|
+
put("columns", n.columns);
|
|
211
|
+
put("min_length", n.minLength);
|
|
212
|
+
put("families", n.families);
|
|
213
|
+
put("style", n.style ? orderStyle(n.style) : undefined);
|
|
214
|
+
put("children", n.children ? n.children.map(orderNode) : undefined);
|
|
215
|
+
return o;
|
|
216
|
+
}
|
|
217
|
+
function orderStyle(s) {
|
|
218
|
+
const o = {};
|
|
219
|
+
const put = (k, v) => { if (v !== undefined)
|
|
220
|
+
o[k] = v; };
|
|
221
|
+
put("font", s.font ? orderFont(s.font) : undefined);
|
|
222
|
+
put("tint", s.tint);
|
|
223
|
+
put("align", s.align);
|
|
224
|
+
put("padding", s.padding);
|
|
225
|
+
put("line_limit", s.lineLimit);
|
|
226
|
+
put("monospaced_digit", s.monospacedDigit);
|
|
227
|
+
put("min_scale", s.minScale);
|
|
228
|
+
put("fill", s.fill);
|
|
229
|
+
return o;
|
|
230
|
+
}
|
|
231
|
+
function orderFont(f) {
|
|
232
|
+
const o = {};
|
|
233
|
+
const put = (k, v) => { if (v !== undefined)
|
|
234
|
+
o[k] = v; };
|
|
235
|
+
put("size", f.size);
|
|
236
|
+
put("point_size", f.pointSize);
|
|
237
|
+
put("weight", f.weight);
|
|
238
|
+
put("design", f.design);
|
|
239
|
+
return o;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Builders for the layout tree. Namespaced (`Node.VStack(…)`) so they don't
|
|
243
|
+
* collide with the card-level template builders (`Stat`/`Gauge`/…) and stay
|
|
244
|
+
* clearly node-level. Each returns a `WidgetNode`; `widgetCard({ layout })`
|
|
245
|
+
* serializes it in the canonical key order the three SDKs share.
|
|
246
|
+
*/
|
|
247
|
+
export const Node = {
|
|
248
|
+
/** A vertical stack. */
|
|
249
|
+
VStack: (children, opts = {}) => ({ type: "vstack", children, ...opts }),
|
|
250
|
+
/** A horizontal stack — side-by-side regions (two columns, a date rail, a row of cells). */
|
|
251
|
+
HStack: (children, opts = {}) => ({ type: "hstack", children, ...opts }),
|
|
252
|
+
/** A depth stack — overlays and rings (e.g. concentric gauges). */
|
|
253
|
+
ZStack: (children, opts = {}) => ({ type: "zstack", children, ...opts }),
|
|
254
|
+
/** A grid of `columns` (default 2, clamped 1…4) — KPI boards. */
|
|
255
|
+
Grid: (children, opts = {}) => ({ type: "grid", children, ...opts }),
|
|
256
|
+
/** A text run. */
|
|
257
|
+
Text: (text, opts = {}) => ({ type: "text", text, ...opts }),
|
|
258
|
+
/** An SF Symbol glyph (v1 renders SF Symbols only). */
|
|
259
|
+
Image: (symbol, opts = {}) => ({ type: "image", symbol, ...opts }),
|
|
260
|
+
/** A gauge — `linear` (default) or `circular`. `value` is `0…1`. */
|
|
261
|
+
Gauge: (value, opts = {}) => ({ type: "gauge", value, ...opts }),
|
|
262
|
+
/** A dependency-free line chart from `values`. */
|
|
263
|
+
Sparkline: (values, opts = {}) => ({ type: "sparkline", values, ...opts }),
|
|
264
|
+
/** Flexible empty space. */
|
|
265
|
+
Spacer: (opts = {}) => ({ type: "spacer", ...opts }),
|
|
266
|
+
/** A hairline divider. */
|
|
267
|
+
Divider: (opts = {}) => ({ type: "divider", ...opts }),
|
|
268
|
+
};
|
|
269
|
+
/** Glyph, big `value` in `tint`, `title`/`caption`. The default template. */
|
|
270
|
+
export function Stat(options) {
|
|
271
|
+
return new WidgetCard({ ...options, template: "stat" });
|
|
272
|
+
}
|
|
273
|
+
/** Stat + a native gauge from `progress`. */
|
|
274
|
+
export function Gauge(options) {
|
|
275
|
+
return new WidgetCard({ ...options, template: "gauge" });
|
|
276
|
+
}
|
|
277
|
+
/** Stat + a sparkline from `trend`. */
|
|
278
|
+
export function Trend(options) {
|
|
279
|
+
return new WidgetCard({ ...options, template: "trend" });
|
|
280
|
+
}
|
|
281
|
+
/** `title` header + `items` as rows. */
|
|
282
|
+
export function List(options) {
|
|
283
|
+
return new WidgetCard({ ...options, template: "list" });
|
|
284
|
+
}
|
|
285
|
+
/** A compact grid of `items` as stat cells (KPI board). */
|
|
286
|
+
export function Board(options) {
|
|
287
|
+
return new WidgetCard({ ...options, template: "board" });
|
|
288
|
+
}
|
|
289
|
+
// The key order every SDK emits, so the three produce byte-identical JSON.
|
|
290
|
+
const JSON_ITEM_KEYS = [
|
|
291
|
+
"text", "separator", "color", "size", "href", "shell", "params", "terminal",
|
|
292
|
+
"refresh", "sfimage", "disabled", "checked", "tooltip", "header", "accessory",
|
|
293
|
+
"sparkline", "sparklineWidth", "sparklineHeight", "sparklineColor",
|
|
294
|
+
"accessoryWidth", "accessoryHeight",
|
|
295
|
+
"toggle", "slider", "progress", "progressTrackColor", "progressWidth",
|
|
296
|
+
"progressHeight", "chart", "submenu", "alternate",
|
|
297
|
+
];
|
|
298
|
+
/** Rebuilds an item with keys in the shared canonical order, dropping absent
|
|
299
|
+
* ones and recursing into `submenu`/`alternate`. */
|
|
300
|
+
function orderJSONItem(item) {
|
|
301
|
+
const out = {};
|
|
302
|
+
for (const key of JSON_ITEM_KEYS) {
|
|
303
|
+
const value = item[key];
|
|
304
|
+
if (value === undefined)
|
|
305
|
+
continue;
|
|
306
|
+
if (key === "submenu")
|
|
307
|
+
out[key] = value.map(orderJSONItem);
|
|
308
|
+
else if (key === "alternate")
|
|
309
|
+
out[key] = orderJSONItem(value);
|
|
310
|
+
else
|
|
311
|
+
out[key] = value;
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
/** A JSON menu section at a given submenu depth. Mirrors `Section`. */
|
|
316
|
+
export class JSONSection {
|
|
317
|
+
items;
|
|
318
|
+
constructor(items) {
|
|
319
|
+
this.items = items;
|
|
320
|
+
}
|
|
321
|
+
item(text, options) {
|
|
322
|
+
this.items.push({ text, ...options });
|
|
323
|
+
return this;
|
|
324
|
+
}
|
|
325
|
+
separator() {
|
|
326
|
+
this.items.push({ separator: true });
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
329
|
+
/** Adds an item and returns a `JSONSection` for its submenu. */
|
|
330
|
+
submenu(text, options) {
|
|
331
|
+
const submenu = [];
|
|
332
|
+
this.items.push({ text, ...options, submenu });
|
|
333
|
+
return new JSONSection(submenu);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** The top-level JSON menu: title line(s) plus a dropdown. Mirrors `Menu`. */
|
|
337
|
+
export class JSONMenu {
|
|
338
|
+
titles = [];
|
|
339
|
+
body = [];
|
|
340
|
+
title(text, options) {
|
|
341
|
+
this.titles.push({ text, ...options });
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
get dropdown() {
|
|
345
|
+
return new JSONSection(this.body);
|
|
346
|
+
}
|
|
347
|
+
toString() {
|
|
348
|
+
const payload = { vee: 1, title: this.titles.map(orderJSONItem) };
|
|
349
|
+
if (this.body.length)
|
|
350
|
+
payload.items = this.body.map(orderJSONItem);
|
|
351
|
+
return JSON.stringify(payload);
|
|
352
|
+
}
|
|
353
|
+
print() {
|
|
354
|
+
process.stdout.write(this.toString() + "\n");
|
|
355
|
+
}
|
|
356
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@navbytes/vee",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Typed builders for authoring Vee plugins — the xbar/SwiftBar text protocol, the structured-JSON menu format, and widget cards.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vee",
|
|
8
|
+
"xbar",
|
|
9
|
+
"swiftbar",
|
|
10
|
+
"menubar",
|
|
11
|
+
"macos",
|
|
12
|
+
"plugin"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Naveen Kumar",
|
|
16
|
+
"homepage": "https://vee.navbytes.io",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/navbytes/vee.git",
|
|
20
|
+
"directory": "plugins/typescript"
|
|
21
|
+
},
|
|
22
|
+
"bugs": "https://github.com/navbytes/vee/issues",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/vee.d.ts",
|
|
27
|
+
"default": "./dist/vee.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "npx -y -p typescript@5 tsc -p tsconfig.build.json",
|
|
39
|
+
"test": "node --test \"test/**/*.test.ts\"",
|
|
40
|
+
"check:dist": "node scripts/check-dist.mjs",
|
|
41
|
+
"build:fixtures": "for f in examples/*.ts; do node \"$f\" > \"../fixtures/$(basename \"${f%.ts}\").txt\"; done",
|
|
42
|
+
"prepublishOnly": "npm run build && npm run check:dist"
|
|
43
|
+
}
|
|
44
|
+
}
|