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/README.md CHANGED
@@ -15,19 +15,19 @@ npm install glimpse-sdk
15
15
  Create and live-run a TypeScript applet project with the Glimpse tooling:
16
16
 
17
17
  ```sh
18
- glimpse-applet new counter --lang typescript
18
+ glimpse-shell applets new counter --lang typescript
19
19
  cd counter
20
- glimpse-applet dev
20
+ glimpse-shell applets dev
21
21
  ```
22
22
 
23
- Read `docs/custom-applets/tooling.md` for project layout, `applet.toml`, dev applets, linking, and diagnostics.
23
+ Read `docs/custom-applets/tooling.md` for project layout, `applet.toml`, dev applets, local linking, distribution, and diagnostics.
24
24
 
25
25
  ## Goals
26
26
 
27
27
  - typed protocol models
28
28
  - typed widget builders
29
29
  - async runtime
30
- - explicit typed handler registration
30
+ - widget-local callbacks, with explicit typed handler registration available when needed
31
31
  - separate `status(state)` and `popover(state)` methods; state mutation via `await this.setState(...)`
32
32
 
33
33
  ## Example
@@ -35,12 +35,11 @@ Read `docs/custom-applets/tooling.md` for project layout, `applet.toml`, dev app
35
35
  ```ts
36
36
  import {
37
37
  Applet,
38
- Box,
39
- Button,
38
+ Column,
40
39
  Hero,
41
- Icon,
42
40
  Label,
43
41
  StatusItem,
42
+ Tile,
44
43
  type TreeNode,
45
44
  } from "glimpse-sdk";
46
45
 
@@ -50,42 +49,43 @@ interface DeployState {
50
49
  }
51
50
 
52
51
  class DeployApplet extends Applet<DeployState> {
53
- protected initialState(): DeployState {
54
- return { version: "2026.04.07", status: "Ready" };
55
- }
56
-
57
52
  constructor() {
58
53
  super();
59
- this.onClick("deploy_now", async () => {
60
- await this.setState({ status: "Deploying" });
61
- });
54
+ }
55
+
56
+ protected initialState(): DeployState {
57
+ return { version: "2026.04.07", status: "Ready" };
62
58
  }
63
59
 
64
60
  protected async status(state: DeployState): Promise<StatusItem[]> {
65
61
  return [
66
62
  new StatusItem({
67
63
  id: "deploy",
68
- icon: Icon.name("software-update-available-symbolic"),
64
+ icon: "software-update-available-symbolic",
69
65
  label: state.status,
70
66
  }),
71
67
  ];
72
68
  }
73
69
 
74
70
  protected async popover(state: DeployState): Promise<TreeNode | null> {
75
- return Box.vertical([
76
- new Hero({
77
- icon: Icon.name("software-update-available-symbolic"),
78
- title: "Deploy",
79
- subtitle: state.version,
80
- }),
81
- new Label("Version"),
82
- new Button({
83
- id: "deploy_now",
84
- label: "Deploy now",
85
- icon: "media-playback-start-symbolic",
86
- variant: "primary",
87
- }),
88
- ]);
71
+ return new Column({
72
+ children: [
73
+ new Hero({
74
+ icon: "software-update-available-symbolic",
75
+ title: "Deploy",
76
+ subtitle: state.version,
77
+ }),
78
+ new Label("Version"),
79
+ new Tile({
80
+ id: "deploy_now",
81
+ primary: "Deploy now",
82
+ left_icon: "media-playback-start-symbolic",
83
+ on_click: async () => {
84
+ await this.setState({ status: "Deploying" });
85
+ },
86
+ }),
87
+ ],
88
+ });
89
89
  }
90
90
  }
91
91
 
@@ -94,7 +94,9 @@ await new DeployApplet().run();
94
94
 
95
95
  ## Handler Registration
96
96
 
97
- Use explicit registration helpers instead of decorators:
97
+ Prefer widget-local callbacks for controls rendered in `popover(state)`.
98
+ Explicit registration helpers are still available when the handler should live
99
+ outside the widget tree:
98
100
 
99
101
  - `this.onClick(id, handler)`
100
102
  - `this.onScroll(id, handler)`
package/dist/src/app.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CallbackEvent, type ChangeEvent, type ClickEvent, type InitEvent, type InputEvent, type ScrollEvent, type ToggleEvent } from "./events.js";
1
+ import { type CallbackEvent, type ChangeEvent, type ClickEvent, type InitEvent, type InputEvent, type PopoverEvent, type ScrollEvent, type ToggleEvent } from "./events.js";
2
2
  import { StatusItem } from "./protocol.js";
3
3
  import { type TreeNode } from "./widgets.js";
4
4
  type Handler<EventT> = (event: EventT) => void | Promise<void>;
@@ -7,16 +7,13 @@ interface OutgoingMessage {
7
7
  data: unknown;
8
8
  line: string;
9
9
  }
10
- interface ShowNotificationArgs {
11
- summary: string;
10
+ interface ShowNotificationOptions {
12
11
  body?: string;
13
- urgency?: string;
14
12
  }
15
- interface OpenUriArgs {
16
- uri: string;
17
- }
18
- interface CopyToClipboardArgs {
19
- text: string;
13
+ export interface CommandResult {
14
+ stdout: string;
15
+ stderr: string;
16
+ rc: number;
20
17
  }
21
18
  interface DismissNotificationArgs {
22
19
  id: number;
@@ -24,6 +21,7 @@ interface DismissNotificationArgs {
24
21
  export declare abstract class Applet<State extends object> {
25
22
  state: State;
26
23
  private readonly handlerMap;
24
+ private inlineHandlerMap;
27
25
  private readonly outgoing;
28
26
  private flushPromise;
29
27
  private renderQueued;
@@ -32,6 +30,7 @@ export declare abstract class Applet<State extends object> {
32
30
  private popoverOpen;
33
31
  protected constructor();
34
32
  protected abstract initialState(): State;
33
+ protected cssClass(): string | null;
35
34
  protected onStart(): Promise<void>;
36
35
  protected onInit(_event: InitEvent): Promise<void>;
37
36
  protected onCallback(_event: CallbackEvent): Promise<void>;
@@ -43,12 +42,16 @@ export declare abstract class Applet<State extends object> {
43
42
  onInput(id: string, handler: Handler<InputEvent>): void;
44
43
  onChange(id: string, handler: Handler<ChangeEvent>): void;
45
44
  onToggle(id: string, handler: Handler<ToggleEvent>): void;
45
+ onPopover(handler: Handler<PopoverEvent>): void;
46
46
  isPopoverOpen(): boolean;
47
- protected showNotification(args: ShowNotificationArgs): void;
48
- protected openUri(args: OpenUriArgs): void;
49
- protected copyToClipboard(args: CopyToClipboardArgs): void;
47
+ log(...args: unknown[]): void;
48
+ protected showNotification(summary: string, options?: ShowNotificationOptions): Promise<void>;
49
+ protected openUri(uri: string): Promise<void>;
50
+ protected copyToClipboard(text: string): Promise<void>;
51
+ protected runCommand(command: string[]): Promise<CommandResult>;
50
52
  protected dismissNotification(args: DismissNotificationArgs): void;
51
53
  protected closePopover(): void;
54
+ protected runDesktopCommand(command: string, args: string[], input?: string): Promise<void>;
52
55
  run(): Promise<void>;
53
56
  protected drainOutgoingForTest(): Promise<OutgoingMessage[]>;
54
57
  private register;
@@ -59,4 +62,5 @@ export declare abstract class Applet<State extends object> {
59
62
  private emit;
60
63
  private emitAction;
61
64
  }
65
+ export declare function runCommand(command: string[]): Promise<CommandResult>;
62
66
  export {};
package/dist/src/app.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { createInterface } from "node:readline";
2
+ import { spawn } from "node:child_process";
2
3
  import { parseCallbackEvent, parseInitEvent, } from "./events.js";
4
+ import { InlineHandlerRegistry } from "./widgets.js";
3
5
  export class Applet {
4
6
  state;
5
7
  handlerMap = new Map();
8
+ inlineHandlerMap = new Map();
6
9
  outgoing = [];
7
10
  flushPromise = null;
8
11
  renderQueued = false;
@@ -12,6 +15,9 @@ export class Applet {
12
15
  constructor() {
13
16
  this.state = this.initialState();
14
17
  }
18
+ cssClass() {
19
+ return null;
20
+ }
15
21
  async onStart() { }
16
22
  async onInit(_event) { }
17
23
  async onCallback(_event) { }
@@ -40,17 +46,31 @@ export class Applet {
40
46
  onToggle(id, handler) {
41
47
  this.register("toggle", id, handler);
42
48
  }
49
+ onPopover(handler) {
50
+ this.register("open", "popover", handler);
51
+ this.register("close", "popover", handler);
52
+ }
43
53
  isPopoverOpen() {
44
54
  return this.popoverOpen;
45
55
  }
46
- showNotification(args) {
47
- this.emitAction("show_notification", args);
56
+ log(...args) {
57
+ process.stderr.write(args.map(String).join(" ") + "\n");
58
+ }
59
+ async showNotification(summary, options = {}) {
60
+ const args = [summary];
61
+ if (options.body !== undefined) {
62
+ args.push(options.body);
63
+ }
64
+ await this.runDesktopCommand("notify-send", args);
65
+ }
66
+ async openUri(uri) {
67
+ await this.runDesktopCommand("xdg-open", [uri]);
48
68
  }
49
- openUri(args) {
50
- this.emitAction("open_uri", args);
69
+ async copyToClipboard(text) {
70
+ await this.runDesktopCommand("wl-copy", [], text);
51
71
  }
52
- copyToClipboard(args) {
53
- this.emitAction("copy_to_clipboard", args);
72
+ async runCommand(command) {
73
+ return await runCommand(command);
54
74
  }
55
75
  dismissNotification(args) {
56
76
  this.emitAction("dismiss_notification", args);
@@ -58,6 +78,9 @@ export class Applet {
58
78
  closePopover() {
59
79
  this.emitAction("close_popover", {});
60
80
  }
81
+ async runDesktopCommand(command, args, input) {
82
+ await runDesktopCommand([command, ...args], input);
83
+ }
61
84
  async run() {
62
85
  process.stdout.on("error", (err) => {
63
86
  if (err.code === "EPIPE") {
@@ -65,6 +88,10 @@ export class Applet {
65
88
  }
66
89
  });
67
90
  await this.onStart();
91
+ const cssClass = this.cssClass();
92
+ if (cssClass !== null) {
93
+ process.stdout.write(`class ${cssClass}\n`);
94
+ }
68
95
  await this.scheduleRender();
69
96
  const rl = createInterface({
70
97
  input: process.stdin,
@@ -104,7 +131,13 @@ export class Applet {
104
131
  this.handlerMap.set(`${event}:${id}`, handler);
105
132
  }
106
133
  async dispatchCallback(event) {
107
- const handler = this.handlerMap.get(`${event.event}:${event.id}`);
134
+ const key = `${event.event}:${event.id}`;
135
+ const inlineHandler = this.inlineHandlerMap.get(key);
136
+ if (inlineHandler !== undefined) {
137
+ await inlineHandler(event);
138
+ return;
139
+ }
140
+ const handler = this.handlerMap.get(key);
108
141
  if (handler !== undefined) {
109
142
  await handler(event);
110
143
  return;
@@ -151,6 +184,14 @@ export class Applet {
151
184
  this.emit("status", { items: status });
152
185
  }
153
186
  const widget = await this.popover(this.state);
187
+ if (widget !== null) {
188
+ const registry = new InlineHandlerRegistry();
189
+ widget.bindHandlers?.(registry, []);
190
+ this.inlineHandlerMap = registry.handlers;
191
+ }
192
+ else {
193
+ this.inlineHandlerMap = new Map();
194
+ }
154
195
  const tree = { root: widget?.toProtocol() ?? null };
155
196
  if (!deepEqual(tree, this.lastTree)) {
156
197
  this.lastTree = tree;
@@ -194,3 +235,38 @@ function deepEqual(left, right) {
194
235
  function isPopoverEvent(event) {
195
236
  return event.event === "open" || event.event === "close";
196
237
  }
238
+ export async function runCommand(command) {
239
+ return await runCommandWithInput(command);
240
+ }
241
+ async function runDesktopCommand(command, input) {
242
+ const result = await runCommandWithInput(command, input);
243
+ if (result.rc !== 0) {
244
+ throw new Error(`${command[0]} exited with status ${result.rc}`);
245
+ }
246
+ }
247
+ async function runCommandWithInput(command, input) {
248
+ if (command.length === 0) {
249
+ throw new Error("command must not be empty");
250
+ }
251
+ return await new Promise((resolve, reject) => {
252
+ const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
253
+ const stdout = [];
254
+ const stderr = [];
255
+ child.on("error", reject);
256
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
257
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
258
+ child.on("close", (code) => {
259
+ resolve({
260
+ stdout: Buffer.concat(stdout).toString("utf8"),
261
+ stderr: Buffer.concat(stderr).toString("utf8"),
262
+ rc: code ?? -1,
263
+ });
264
+ });
265
+ if (input !== undefined) {
266
+ child.stdin.end(input);
267
+ }
268
+ else {
269
+ child.stdin.end();
270
+ }
271
+ });
272
+ }
@@ -8,7 +8,7 @@ interface CallbackEventBase {
8
8
  }
9
9
  export interface ClickEvent extends CallbackEventBase {
10
10
  event: "click";
11
- button?: string;
11
+ button?: string | null;
12
12
  }
13
13
  export interface ScrollEvent extends CallbackEventBase {
14
14
  event: "scroll";
@@ -8,7 +8,7 @@ export function parseCallbackEvent(payload) {
8
8
  const event = String(payload.type ?? payload.event ?? "");
9
9
  const id = String(payload.id ?? "");
10
10
  if (event === "click") {
11
- return { id, event, button: payload.button === undefined ? undefined : String(payload.button) };
11
+ return { id, event, button: payload.button === undefined ? null : String(payload.button) };
12
12
  }
13
13
  if (event === "scroll") {
14
14
  return {
@@ -1,5 +1,5 @@
1
- export { Applet } from "./app.js";
1
+ export { Applet, runCommand } from "./app.js";
2
2
  export { type CallbackEvent, type ChangeEvent, type ClickEvent, type InitEvent, type InputEvent, type PopoverEvent, type ScrollEvent, type ToggleEvent, parseCallbackEvent, parseInitEvent, } from "./events.js";
3
- export { Icon, StatusItem } from "./protocol.js";
4
- export { type Event, IpcError, Subscriber, ipc } from "./ipc.js";
5
- export { ActionItem, type Align, Badge, Box, Button, type ButtonVariant, Card, Checkbox, Column, type ContentFit, Copyable, EmptyState, Expander, Grid, GridChild, Hero, Image, Item, Label, LevelBar, type LevelBarMode, LinkButton, ListBox, MenuButton, Meter, Overlay, type Orientation, type PagerAppearance, PagerItem, PagerStrip, Picture, Progress, type Properties, PropertyList, Row, Scroll, Section, Select, SelectOption, Separator, Slider, Spinner, StatusDot, Switch, ToggleButton, TreeExpander, type TreeNode, type Variant, } from "./widgets.js";
3
+ export { ipc, IpcError, Subscriber } from "./ipc.js";
4
+ export { StatusItem } from "./protocol.js";
5
+ export { Badge, type BadgeKind, BatteryHero, BoxedList, ButtonRow, Calendar, CameraIndicator, Choice, ChoiceList, ChoiceTile, CircleBox, Column, Container, DateHero, EmptyState, EventItem, Events, ExpanderTile, Header, Hero, KeyValueGrid, Label, LocationIndicator, Meter, MicIndicator, MutedIndicator, type PagerAppearance, PagerItem, PagerStrip, PanelIndicator, PopoverShell, type PopoverSize, Row, ScreenCastIndicator, Scroll, SegmentedTile, Separator, SliderTile, Spinner, StatusDot, type StatusDotStatus, SwitchTile, Tile, type TreeNode, WeatherForecastItem, WeatherForecastList, WeatherHourlyItem, WeatherHourlyStrip, WidgetBase, WorldClock, WorldClockRow, } from "./widgets.js";
package/dist/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { Applet } from "./app.js";
1
+ export { Applet, runCommand } from "./app.js";
2
2
  export { parseCallbackEvent, parseInitEvent, } from "./events.js";
3
- export { Icon, StatusItem } from "./protocol.js";
4
- export { IpcError, Subscriber, ipc } from "./ipc.js";
5
- export { ActionItem, Badge, Box, Button, Card, Checkbox, Column, Copyable, EmptyState, Expander, Grid, GridChild, Hero, Image, Item, Label, LevelBar, LinkButton, ListBox, MenuButton, Meter, Overlay, PagerItem, PagerStrip, Picture, Progress, PropertyList, Row, Scroll, Section, Select, SelectOption, Separator, Slider, Spinner, StatusDot, Switch, ToggleButton, TreeExpander, } from "./widgets.js";
3
+ export { ipc, IpcError, Subscriber } from "./ipc.js";
4
+ export { StatusItem } from "./protocol.js";
5
+ export { Badge, BatteryHero, BoxedList, ButtonRow, Calendar, CameraIndicator, Choice, ChoiceList, ChoiceTile, CircleBox, Column, Container, DateHero, EmptyState, EventItem, Events, ExpanderTile, Header, Hero, KeyValueGrid, Label, LocationIndicator, Meter, MicIndicator, MutedIndicator, PagerItem, PagerStrip, PanelIndicator, PopoverShell, Row, ScreenCastIndicator, Scroll, SegmentedTile, Separator, SliderTile, Spinner, StatusDot, SwitchTile, Tile, WeatherForecastItem, WeatherForecastList, WeatherHourlyItem, WeatherHourlyStrip, WidgetBase, WorldClock, WorldClockRow, } from "./widgets.js";
@@ -1,28 +1,23 @@
1
- export type IconKind = "name" | "path";
2
- export declare class Icon {
3
- readonly kind: IconKind;
4
- readonly value: string;
5
- constructor(kind: IconKind, value: string);
6
- static name(value: string): Icon;
7
- static path(value: string): Icon;
8
- toProtocol(): {
9
- name: string;
10
- } | {
11
- path: string;
12
- };
13
- }
14
1
  export declare class StatusItem {
15
2
  readonly options: {
16
3
  id?: string;
17
- icon?: Icon;
4
+ icon?: string;
18
5
  label?: string;
19
6
  tooltip?: string;
7
+ cssClasses?: string[];
20
8
  };
21
9
  constructor(options?: {
22
10
  id?: string;
23
- icon?: Icon;
11
+ icon?: string;
24
12
  label?: string;
25
13
  tooltip?: string;
14
+ cssClasses?: string[];
26
15
  });
16
+ /**
17
+ * Return a copy with `cssClass` appended to `cssClasses`. Immutable-update
18
+ * form that mirrors the Rust SDK's `css_class()` builder so SDK-hopping
19
+ * users see equivalent ergonomics across languages.
20
+ */
21
+ withCssClass(cssClass: string): StatusItem;
27
22
  toProtocol(): Record<string, unknown>;
28
23
  }
@@ -1,32 +1,26 @@
1
- export class Icon {
2
- kind;
3
- value;
4
- constructor(kind, value) {
5
- this.kind = kind;
6
- this.value = value;
7
- }
8
- static name(value) {
9
- return new Icon("name", value);
10
- }
11
- static path(value) {
12
- return new Icon("path", value);
13
- }
14
- toProtocol() {
15
- return this.kind === "name" ? { name: this.value } : { path: this.value };
16
- }
17
- }
18
1
  export class StatusItem {
19
2
  options;
20
3
  constructor(options = {}) {
21
4
  this.options = options;
22
5
  }
6
+ /**
7
+ * Return a copy with `cssClass` appended to `cssClasses`. Immutable-update
8
+ * form that mirrors the Rust SDK's `css_class()` builder so SDK-hopping
9
+ * users see equivalent ergonomics across languages.
10
+ */
11
+ withCssClass(cssClass) {
12
+ return new StatusItem({
13
+ ...this.options,
14
+ cssClasses: [...(this.options.cssClasses ?? []), cssClass],
15
+ });
16
+ }
23
17
  toProtocol() {
24
18
  const payload = {};
25
19
  if (this.options.id !== undefined) {
26
20
  payload.id = this.options.id;
27
21
  }
28
22
  if (this.options.icon !== undefined) {
29
- payload.icon = this.options.icon.toProtocol();
23
+ payload.icon = this.options.icon;
30
24
  }
31
25
  if (this.options.label !== undefined) {
32
26
  payload.label = this.options.label;
@@ -34,6 +28,9 @@ export class StatusItem {
34
28
  if (this.options.tooltip !== undefined) {
35
29
  payload.tooltip = this.options.tooltip;
36
30
  }
31
+ if (this.options.cssClasses && this.options.cssClasses.length > 0) {
32
+ payload.css_classes = [...this.options.cssClasses];
33
+ }
37
34
  return payload;
38
35
  }
39
36
  }