glimpse-sdk 0.5.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glimpse-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "description": "Typed async framework for building Glimpse exec applets",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
package/src/app.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createInterface } from "node:readline";
2
+ import { spawn } from "node:child_process";
2
3
 
3
4
  import {
4
5
  type CallbackEvent,
@@ -13,7 +14,7 @@ import {
13
14
  parseInitEvent,
14
15
  } from "./events.js";
15
16
  import { StatusItem } from "./protocol.js";
16
- import { type TreeNode } from "./widgets.js";
17
+ import { type TreeNode, InlineHandlerRegistry } from "./widgets.js";
17
18
 
18
19
  type Handler<EventT> = (event: EventT) => void | Promise<void>;
19
20
 
@@ -23,18 +24,14 @@ interface OutgoingMessage {
23
24
  line: string;
24
25
  }
25
26
 
26
- interface ShowNotificationArgs {
27
- summary: string;
27
+ interface ShowNotificationOptions {
28
28
  body?: string;
29
- urgency?: string;
30
29
  }
31
30
 
32
- interface OpenUriArgs {
33
- uri: string;
34
- }
35
-
36
- interface CopyToClipboardArgs {
37
- text: string;
31
+ export interface CommandResult {
32
+ stdout: string;
33
+ stderr: string;
34
+ rc: number;
38
35
  }
39
36
 
40
37
  interface DismissNotificationArgs {
@@ -45,6 +42,7 @@ export abstract class Applet<State extends object> {
45
42
  state: State;
46
43
 
47
44
  private readonly handlerMap = new Map<string, Handler<CallbackEvent>>();
45
+ private inlineHandlerMap = new Map<string, (event: CallbackEvent) => void | Promise<void>>();
48
46
  private readonly outgoing: OutgoingMessage[] = [];
49
47
  private flushPromise: Promise<void> | null = null;
50
48
  private renderQueued = false;
@@ -58,6 +56,10 @@ export abstract class Applet<State extends object> {
58
56
 
59
57
  protected abstract initialState(): State;
60
58
 
59
+ protected cssClass(): string | null {
60
+ return null;
61
+ }
62
+
61
63
  protected async onStart(): Promise<void> {}
62
64
 
63
65
  protected async onInit(_event: InitEvent): Promise<void> {}
@@ -97,20 +99,37 @@ export abstract class Applet<State extends object> {
97
99
  this.register("toggle", id, handler as Handler<CallbackEvent>);
98
100
  }
99
101
 
102
+ onPopover(handler: Handler<PopoverEvent>): void {
103
+ this.register("open", "popover", handler as Handler<CallbackEvent>);
104
+ this.register("close", "popover", handler as Handler<CallbackEvent>);
105
+ }
106
+
100
107
  isPopoverOpen(): boolean {
101
108
  return this.popoverOpen;
102
109
  }
103
110
 
104
- protected showNotification(args: ShowNotificationArgs): void {
105
- this.emitAction("show_notification", args);
111
+ log(...args: unknown[]): void {
112
+ process.stderr.write(args.map(String).join(" ") + "\n");
106
113
  }
107
114
 
108
- protected openUri(args: OpenUriArgs): void {
109
- this.emitAction("open_uri", args);
115
+ protected async showNotification(summary: string, options: ShowNotificationOptions = {}): Promise<void> {
116
+ const args = [summary];
117
+ if (options.body !== undefined) {
118
+ args.push(options.body);
119
+ }
120
+ await this.runDesktopCommand("notify-send", args);
110
121
  }
111
122
 
112
- protected copyToClipboard(args: CopyToClipboardArgs): void {
113
- this.emitAction("copy_to_clipboard", args);
123
+ protected async openUri(uri: string): Promise<void> {
124
+ await this.runDesktopCommand("xdg-open", [uri]);
125
+ }
126
+
127
+ protected async copyToClipboard(text: string): Promise<void> {
128
+ await this.runDesktopCommand("wl-copy", [], text);
129
+ }
130
+
131
+ protected async runCommand(command: string[]): Promise<CommandResult> {
132
+ return await runCommand(command);
114
133
  }
115
134
 
116
135
  protected dismissNotification(args: DismissNotificationArgs): void {
@@ -121,6 +140,10 @@ export abstract class Applet<State extends object> {
121
140
  this.emitAction("close_popover", {});
122
141
  }
123
142
 
143
+ protected async runDesktopCommand(command: string, args: string[], input?: string): Promise<void> {
144
+ await runDesktopCommand([command, ...args], input);
145
+ }
146
+
124
147
  async run(): Promise<void> {
125
148
  process.stdout.on("error", (err: NodeJS.ErrnoException) => {
126
149
  if (err.code === "EPIPE") {
@@ -129,6 +152,10 @@ export abstract class Applet<State extends object> {
129
152
  });
130
153
 
131
154
  await this.onStart();
155
+ const cssClass = this.cssClass();
156
+ if (cssClass !== null) {
157
+ process.stdout.write(`class ${cssClass}\n`);
158
+ }
132
159
  await this.scheduleRender();
133
160
 
134
161
  const rl = createInterface({
@@ -171,7 +198,13 @@ export abstract class Applet<State extends object> {
171
198
  }
172
199
 
173
200
  private async dispatchCallback(event: CallbackEvent): Promise<void> {
174
- const handler = this.handlerMap.get(`${event.event}:${event.id}`);
201
+ const key = `${event.event}:${event.id}`;
202
+ const inlineHandler = this.inlineHandlerMap.get(key);
203
+ if (inlineHandler !== undefined) {
204
+ await inlineHandler(event);
205
+ return;
206
+ }
207
+ const handler = this.handlerMap.get(key);
175
208
  if (handler !== undefined) {
176
209
  await handler(event);
177
210
  return;
@@ -221,6 +254,13 @@ export abstract class Applet<State extends object> {
221
254
  }
222
255
 
223
256
  const widget = await this.popover(this.state);
257
+ if (widget !== null) {
258
+ const registry = new InlineHandlerRegistry();
259
+ widget.bindHandlers?.(registry, []);
260
+ this.inlineHandlerMap = registry.handlers;
261
+ } else {
262
+ this.inlineHandlerMap = new Map();
263
+ }
224
264
  const tree = { root: widget?.toProtocol() ?? null };
225
265
  if (!deepEqual(tree, this.lastTree)) {
226
266
  this.lastTree = tree;
@@ -268,3 +308,40 @@ function deepEqual(left: unknown, right: unknown): boolean {
268
308
  function isPopoverEvent(event: CallbackEvent): event is PopoverEvent {
269
309
  return event.event === "open" || event.event === "close";
270
310
  }
311
+
312
+ export async function runCommand(command: string[]): Promise<CommandResult> {
313
+ return await runCommandWithInput(command);
314
+ }
315
+
316
+ async function runDesktopCommand(command: string[], input?: string): Promise<void> {
317
+ const result = await runCommandWithInput(command, input);
318
+ if (result.rc !== 0) {
319
+ throw new Error(`${command[0]} exited with status ${result.rc}`);
320
+ }
321
+ }
322
+
323
+ async function runCommandWithInput(command: string[], input?: string): Promise<CommandResult> {
324
+ if (command.length === 0) {
325
+ throw new Error("command must not be empty");
326
+ }
327
+ return await new Promise<CommandResult>((resolve, reject) => {
328
+ const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
329
+ const stdout: Buffer[] = [];
330
+ const stderr: Buffer[] = [];
331
+ child.on("error", reject);
332
+ child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
333
+ child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
334
+ child.on("close", (code) => {
335
+ resolve({
336
+ stdout: Buffer.concat(stdout).toString("utf8"),
337
+ stderr: Buffer.concat(stderr).toString("utf8"),
338
+ rc: code ?? -1,
339
+ });
340
+ });
341
+ if (input !== undefined) {
342
+ child.stdin.end(input);
343
+ } else {
344
+ child.stdin.end();
345
+ }
346
+ });
347
+ }
package/src/events.ts CHANGED
@@ -10,7 +10,7 @@ interface CallbackEventBase {
10
10
 
11
11
  export interface ClickEvent extends CallbackEventBase {
12
12
  event: "click";
13
- button?: string;
13
+ button?: string | null;
14
14
  }
15
15
 
16
16
  export interface ScrollEvent extends CallbackEventBase {
@@ -57,7 +57,7 @@ export function parseCallbackEvent(payload: Record<string, unknown>): CallbackEv
57
57
  const event = String(payload.type ?? payload.event ?? "");
58
58
  const id = String(payload.id ?? "");
59
59
  if (event === "click") {
60
- return { id, event, button: payload.button === undefined ? undefined : String(payload.button) };
60
+ return { id, event, button: payload.button === undefined ? null : String(payload.button) };
61
61
  }
62
62
  if (event === "scroll") {
63
63
  return {
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Applet } from "./app.js";
1
+ export { Applet, runCommand } from "./app.js";
2
2
  export {
3
3
  type CallbackEvent,
4
4
  type ChangeEvent,
@@ -11,55 +11,58 @@ export {
11
11
  parseCallbackEvent,
12
12
  parseInitEvent,
13
13
  } from "./events.js";
14
- export { Icon, StatusItem } from "./protocol.js";
15
- export { type Event, IpcError, Subscriber, ipc } from "./ipc.js";
14
+ export { ipc, IpcError, Subscriber } from "./ipc.js";
15
+ export { StatusItem } from "./protocol.js";
16
16
  export {
17
- ActionItem,
18
- type Align,
19
17
  Badge,
20
- Box,
21
- Button,
22
- type ButtonVariant,
23
- Card,
24
- Checkbox,
18
+ type BadgeKind,
19
+ BatteryHero,
20
+ BoxedList,
21
+ ButtonRow,
22
+ Calendar,
23
+ CameraIndicator,
24
+ Choice,
25
+ ChoiceList,
26
+ ChoiceTile,
27
+ CircleBox,
25
28
  Column,
26
- type ContentFit,
27
- Copyable,
29
+ Container,
30
+ DateHero,
28
31
  EmptyState,
29
- Expander,
30
- Grid,
31
- GridChild,
32
+ EventItem,
33
+ Events,
34
+ ExpanderTile,
35
+ Header,
32
36
  Hero,
33
- Image,
34
- Item,
37
+ KeyValueGrid,
35
38
  Label,
36
- LevelBar,
37
- type LevelBarMode,
38
- LinkButton,
39
- ListBox,
40
- MenuButton,
39
+ LocationIndicator,
41
40
  Meter,
42
- Overlay,
43
- type Orientation,
41
+ MicIndicator,
42
+ MutedIndicator,
44
43
  type PagerAppearance,
45
44
  PagerItem,
46
45
  PagerStrip,
47
- Picture,
48
- Progress,
49
- type Properties,
50
- PropertyList,
46
+ PanelIndicator,
47
+ PopoverShell,
48
+ type PopoverSize,
51
49
  Row,
50
+ ScreenCastIndicator,
52
51
  Scroll,
53
- Section,
54
- Select,
55
- SelectOption,
52
+ SegmentedTile,
56
53
  Separator,
57
- Slider,
54
+ SliderTile,
58
55
  Spinner,
59
56
  StatusDot,
60
- Switch,
61
- ToggleButton,
62
- TreeExpander,
57
+ type StatusDotStatus,
58
+ SwitchTile,
59
+ Tile,
63
60
  type TreeNode,
64
- type Variant,
61
+ WeatherForecastItem,
62
+ WeatherForecastList,
63
+ WeatherHourlyItem,
64
+ WeatherHourlyStrip,
65
+ WidgetBase,
66
+ WorldClock,
67
+ WorldClockRow,
65
68
  } from "./widgets.js";
package/src/protocol.ts CHANGED
@@ -1,41 +1,33 @@
1
- export type IconKind = "name" | "path";
2
-
3
- export class Icon {
4
- constructor(
5
- public readonly kind: IconKind,
6
- public readonly value: string,
7
- ) {}
8
-
9
- static name(value: string): Icon {
10
- return new Icon("name", value);
11
- }
12
-
13
- static path(value: string): Icon {
14
- return new Icon("path", value);
15
- }
16
-
17
- toProtocol(): { name: string } | { path: string } {
18
- return this.kind === "name" ? { name: this.value } : { path: this.value };
19
- }
20
- }
21
-
22
1
  export class StatusItem {
23
2
  constructor(
24
3
  public readonly options: {
25
4
  id?: string;
26
- icon?: Icon;
5
+ icon?: string;
27
6
  label?: string;
28
7
  tooltip?: string;
8
+ cssClasses?: string[];
29
9
  } = {},
30
10
  ) {}
31
11
 
12
+ /**
13
+ * Return a copy with `cssClass` appended to `cssClasses`. Immutable-update
14
+ * form that mirrors the Rust SDK's `css_class()` builder so SDK-hopping
15
+ * users see equivalent ergonomics across languages.
16
+ */
17
+ withCssClass(cssClass: string): StatusItem {
18
+ return new StatusItem({
19
+ ...this.options,
20
+ cssClasses: [...(this.options.cssClasses ?? []), cssClass],
21
+ });
22
+ }
23
+
32
24
  toProtocol(): Record<string, unknown> {
33
25
  const payload: Record<string, unknown> = {};
34
26
  if (this.options.id !== undefined) {
35
27
  payload.id = this.options.id;
36
28
  }
37
29
  if (this.options.icon !== undefined) {
38
- payload.icon = this.options.icon.toProtocol();
30
+ payload.icon = this.options.icon;
39
31
  }
40
32
  if (this.options.label !== undefined) {
41
33
  payload.label = this.options.label;
@@ -43,6 +35,9 @@ export class StatusItem {
43
35
  if (this.options.tooltip !== undefined) {
44
36
  payload.tooltip = this.options.tooltip;
45
37
  }
38
+ if (this.options.cssClasses && this.options.cssClasses.length > 0) {
39
+ payload.css_classes = [...this.options.cssClasses];
40
+ }
46
41
  return payload;
47
42
  }
48
43
  }