glimpse-custom-applet-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Oleshkevich
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 ADDED
@@ -0,0 +1,80 @@
1
+ # Glimpse Applet TypeScript SDK
2
+
3
+ Small async framework for building Glimpse `exec` applets without touching stdio or raw JSON.
4
+
5
+ ## Goals
6
+
7
+ - typed protocol models
8
+ - typed widget builders
9
+ - async runtime
10
+ - explicit typed handler registration
11
+ - state-driven rendering via `await this.setState(...)`
12
+ - single `render()` method that returns all panel state
13
+
14
+ ## Example
15
+
16
+ ```ts
17
+ import {
18
+ Applet,
19
+ Box,
20
+ Button,
21
+ Hero,
22
+ Icon,
23
+ Label,
24
+ RenderResult,
25
+ StatusItem,
26
+ } from "./src/index.js";
27
+
28
+ interface DeployState {
29
+ version: string;
30
+ status: string;
31
+ }
32
+
33
+ class DeployApplet extends Applet<DeployState> {
34
+ protected initialState(): DeployState {
35
+ return { version: "2026.04.07", status: "Ready" };
36
+ }
37
+
38
+ constructor() {
39
+ super();
40
+ this.onClick("deploy_now", async () => {
41
+ await this.setState({ status: "Deploying" });
42
+ });
43
+ }
44
+
45
+ protected async render(): Promise<RenderResult> {
46
+ return new RenderResult({
47
+ status: [
48
+ new StatusItem({
49
+ id: "deploy",
50
+ icon: Icon.name("software-update-available-symbolic"),
51
+ label: this.state.status,
52
+ }),
53
+ ],
54
+ hero: new Hero({
55
+ icon: Icon.name("software-update-available-symbolic"),
56
+ title: "Deploy",
57
+ subtitle: this.state.version,
58
+ }),
59
+ tree: Box.vertical([
60
+ new Label("Version"),
61
+ new Button({ id: "deploy_now", label: "Deploy now" }),
62
+ ]),
63
+ });
64
+ }
65
+ }
66
+
67
+ await new DeployApplet().run();
68
+ ```
69
+
70
+ ## Handler Registration
71
+
72
+ Use explicit registration helpers instead of decorators:
73
+
74
+ - `this.onClick(id, handler)`
75
+ - `this.onScroll(id, handler)`
76
+ - `this.onInput(id, handler)`
77
+ - `this.onChange(id, handler)`
78
+ - `this.onToggle(id, handler)`
79
+
80
+ The SDK owns the line transport and writes `status {json}` and `popover {json}` messages derived from `render()`.
@@ -0,0 +1,53 @@
1
+ import { type CallbackEvent, type ChangeEvent, type ClickEvent, type InitEvent, type InputEvent, type ScrollEvent, type ToggleEvent } from "./events.js";
2
+ import { StatusItem } from "./protocol.js";
3
+ import { type TreeNode } from "./widgets.js";
4
+ type Handler<EventT> = (event: EventT) => void | Promise<void>;
5
+ interface OutgoingMessage {
6
+ command: string;
7
+ data: unknown;
8
+ line: string;
9
+ }
10
+ export declare class RenderResult {
11
+ readonly options: {
12
+ status?: StatusItem[];
13
+ tree?: TreeNode | null;
14
+ };
15
+ constructor(options?: {
16
+ status?: StatusItem[];
17
+ tree?: TreeNode | null;
18
+ });
19
+ get status(): StatusItem[];
20
+ get tree(): TreeNode | null;
21
+ }
22
+ export declare abstract class Applet<State extends object> {
23
+ state: State;
24
+ private readonly handlerMap;
25
+ private readonly outgoing;
26
+ private flushPromise;
27
+ private renderQueued;
28
+ private lastStatus;
29
+ private lastTree;
30
+ private popoverOpen;
31
+ protected constructor();
32
+ protected abstract initialState(): State;
33
+ protected onStart(): Promise<void>;
34
+ protected onInit(_event: InitEvent): Promise<void>;
35
+ protected onCallback(_event: CallbackEvent): Promise<void>;
36
+ protected render(): Promise<RenderResult>;
37
+ setState(patch: Partial<State>): Promise<void>;
38
+ onClick(id: string, handler: Handler<ClickEvent>): void;
39
+ onScroll(id: string, handler: Handler<ScrollEvent>): void;
40
+ onInput(id: string, handler: Handler<InputEvent>): void;
41
+ onChange(id: string, handler: Handler<ChangeEvent>): void;
42
+ onToggle(id: string, handler: Handler<ToggleEvent>): void;
43
+ isPopoverOpen(): boolean;
44
+ run(): Promise<void>;
45
+ protected drainOutgoingForTest(): Promise<OutgoingMessage[]>;
46
+ private register;
47
+ private dispatchCallback;
48
+ private handleIncoming;
49
+ private scheduleRender;
50
+ private flushRender;
51
+ private emit;
52
+ }
53
+ export {};
@@ -0,0 +1,187 @@
1
+ import { createInterface } from "node:readline";
2
+ import { parseCallbackEvent, parseInitEvent, } from "./events.js";
3
+ export class RenderResult {
4
+ options;
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ }
8
+ get status() {
9
+ return this.options.status ?? [];
10
+ }
11
+ get tree() {
12
+ return this.options.tree ?? null;
13
+ }
14
+ }
15
+ export class Applet {
16
+ state;
17
+ handlerMap = new Map();
18
+ outgoing = [];
19
+ flushPromise = null;
20
+ renderQueued = false;
21
+ lastStatus = null;
22
+ lastTree = null;
23
+ popoverOpen = false;
24
+ constructor() {
25
+ this.state = this.initialState();
26
+ }
27
+ async onStart() { }
28
+ async onInit(_event) { }
29
+ async onCallback(_event) { }
30
+ async render() {
31
+ return new RenderResult();
32
+ }
33
+ async setState(patch) {
34
+ this.state = { ...this.state, ...patch };
35
+ await this.scheduleRender();
36
+ }
37
+ onClick(id, handler) {
38
+ this.register("click", id, handler);
39
+ }
40
+ onScroll(id, handler) {
41
+ this.register("scroll", id, handler);
42
+ }
43
+ onInput(id, handler) {
44
+ this.register("input", id, handler);
45
+ }
46
+ onChange(id, handler) {
47
+ this.register("change", id, handler);
48
+ }
49
+ onToggle(id, handler) {
50
+ this.register("toggle", id, handler);
51
+ }
52
+ isPopoverOpen() {
53
+ return this.popoverOpen;
54
+ }
55
+ async run() {
56
+ process.stdout.on("error", (err) => {
57
+ if (err.code === "EPIPE") {
58
+ process.exit(0);
59
+ }
60
+ });
61
+ await this.onStart();
62
+ await this.scheduleRender();
63
+ const rl = createInterface({
64
+ input: process.stdin,
65
+ crlfDelay: Infinity,
66
+ });
67
+ for await (const line of rl) {
68
+ if (!line) {
69
+ continue;
70
+ }
71
+ let raw;
72
+ try {
73
+ raw = parseLine(line);
74
+ }
75
+ catch (err) {
76
+ process.stderr.write(`glimpse-applet: ignoring malformed input: ${err}\n`);
77
+ continue;
78
+ }
79
+ if (raw === null) {
80
+ continue;
81
+ }
82
+ const data = raw.data;
83
+ try {
84
+ await this.handleIncoming(raw.command, data);
85
+ }
86
+ catch (err) {
87
+ process.stderr.write(`glimpse-applet: error handling input: ${err}\n`);
88
+ }
89
+ }
90
+ }
91
+ async drainOutgoingForTest() {
92
+ await this.scheduleRender();
93
+ const drained = [...this.outgoing];
94
+ this.outgoing.length = 0;
95
+ return drained;
96
+ }
97
+ register(event, id, handler) {
98
+ this.handlerMap.set(`${event}:${id}`, handler);
99
+ }
100
+ async dispatchCallback(event) {
101
+ const handler = this.handlerMap.get(`${event.event}:${event.id}`);
102
+ if (handler !== undefined) {
103
+ await handler(event);
104
+ return;
105
+ }
106
+ await this.onCallback(event);
107
+ }
108
+ async handleIncoming(type, data) {
109
+ if (type === "init") {
110
+ await this.onInit(parseInitEvent(data));
111
+ await this.scheduleRender();
112
+ return;
113
+ }
114
+ if (type === "event") {
115
+ const event = parseCallbackEvent(data);
116
+ if (isPopoverEvent(event)) {
117
+ this.popoverOpen = event.open;
118
+ }
119
+ await this.dispatchCallback(event);
120
+ await this.scheduleRender();
121
+ }
122
+ }
123
+ async scheduleRender() {
124
+ this.renderQueued = true;
125
+ if (this.flushPromise === null) {
126
+ this.flushPromise = Promise.resolve().then(async () => {
127
+ try {
128
+ while (this.renderQueued) {
129
+ this.renderQueued = false;
130
+ await this.flushRender();
131
+ }
132
+ }
133
+ finally {
134
+ this.flushPromise = null;
135
+ }
136
+ });
137
+ }
138
+ await this.flushPromise;
139
+ }
140
+ async flushRender() {
141
+ const rendered = await this.render();
142
+ const status = rendered.status.map((item) => item.toProtocol());
143
+ const tree = { root: rendered.tree?.toProtocol() ?? null };
144
+ if (!deepEqual(status, this.lastStatus)) {
145
+ this.lastStatus = status;
146
+ this.emit("status", { items: status });
147
+ }
148
+ const publishPopover = this.popoverOpen || this.lastTree === null || tree.root === null;
149
+ if (publishPopover && !deepEqual(tree, this.lastTree)) {
150
+ this.lastTree = tree;
151
+ this.emit("popover", tree);
152
+ }
153
+ }
154
+ emit(command, data) {
155
+ const line = `${command} ${JSON.stringify(data)}`;
156
+ this.outgoing.push({ command, data, line });
157
+ try {
158
+ process.stdout.write(`${line}\n`);
159
+ }
160
+ catch (err) {
161
+ const code = err?.code;
162
+ if (code !== "EPIPE") {
163
+ throw err;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ function parseLine(line) {
169
+ const trimmed = line.trim();
170
+ if (trimmed === "") {
171
+ return null;
172
+ }
173
+ const split = trimmed.search(/\s/);
174
+ if (split < 0) {
175
+ throw new Error("missing command payload");
176
+ }
177
+ return {
178
+ command: trimmed.slice(0, split),
179
+ data: JSON.parse(trimmed.slice(split).trimStart()),
180
+ };
181
+ }
182
+ function deepEqual(left, right) {
183
+ return JSON.stringify(left) === JSON.stringify(right);
184
+ }
185
+ function isPopoverEvent(event) {
186
+ return event.event === "open" || event.event === "close";
187
+ }
@@ -0,0 +1,36 @@
1
+ export interface InitEvent {
2
+ instance: string;
3
+ options: Record<string, unknown>;
4
+ }
5
+ interface CallbackEventBase {
6
+ id: string;
7
+ event: string;
8
+ }
9
+ export interface ClickEvent extends CallbackEventBase {
10
+ event: "click";
11
+ button?: string;
12
+ }
13
+ export interface ScrollEvent extends CallbackEventBase {
14
+ event: "scroll";
15
+ delta_y?: number;
16
+ }
17
+ export interface InputEvent extends CallbackEventBase {
18
+ event: "input";
19
+ text: string;
20
+ }
21
+ export interface ChangeEvent extends CallbackEventBase {
22
+ event: "change";
23
+ value: unknown;
24
+ }
25
+ export interface ToggleEvent extends CallbackEventBase {
26
+ event: "toggle";
27
+ value: boolean;
28
+ }
29
+ export interface PopoverEvent extends CallbackEventBase {
30
+ event: "open" | "close";
31
+ open: boolean;
32
+ }
33
+ export type CallbackEvent = ClickEvent | ScrollEvent | InputEvent | ChangeEvent | ToggleEvent | PopoverEvent;
34
+ export declare function parseInitEvent(payload: Record<string, unknown>): InitEvent;
35
+ export declare function parseCallbackEvent(payload: Record<string, unknown>): CallbackEvent;
36
+ export {};
@@ -0,0 +1,37 @@
1
+ export function parseInitEvent(payload) {
2
+ return {
3
+ instance: String(payload.instance ?? ""),
4
+ options: (payload.options ?? {}),
5
+ };
6
+ }
7
+ export function parseCallbackEvent(payload) {
8
+ const event = String(payload.type ?? payload.event ?? "");
9
+ const id = String(payload.id ?? "");
10
+ if (event === "click") {
11
+ return { id, event, button: payload.button === undefined ? undefined : String(payload.button) };
12
+ }
13
+ if (event === "scroll") {
14
+ return {
15
+ id,
16
+ event,
17
+ delta_y: typeof payload.delta_y === "number" ? payload.delta_y : undefined,
18
+ };
19
+ }
20
+ if (event === "input") {
21
+ return { id, event, text: String(payload.text ?? "") };
22
+ }
23
+ if (event === "toggle") {
24
+ let value = false;
25
+ if (typeof payload.active === "boolean") {
26
+ value = payload.active;
27
+ }
28
+ else if (typeof payload.value === "boolean") {
29
+ value = payload.value;
30
+ }
31
+ return { id, event, value };
32
+ }
33
+ if (event === "open" || event === "close") {
34
+ return { id, event, open: event === "open" };
35
+ }
36
+ return { id, event: "change", value: payload.value };
37
+ }
@@ -0,0 +1,4 @@
1
+ export { Applet, RenderResult } from "./app.js";
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, MenuItem, StatusItem } from "./protocol.js";
4
+ export { ActionMenu, ActionMenuItem, ActionRow, type Align, Badge, Box, Button, Card, Checkbox, Collapsible, CollapsibleItem, Column, Copyable, DetailGrid, DetailGridItem, Dropdown, DropdownItem, EmptyState, Grid, GridChild, Header, Hero, IconWidget, Image, Item, Label, Meter, type Orientation, Progress, Row, Scale, Scroll, Section, Separator, Spinner, StatusDot, Switch, Toast, ToastAction, type TreeNode, type Variant, } from "./widgets.js";
@@ -0,0 +1,4 @@
1
+ export { Applet, RenderResult } from "./app.js";
2
+ export { parseCallbackEvent, parseInitEvent, } from "./events.js";
3
+ export { Icon, MenuItem, StatusItem } from "./protocol.js";
4
+ export { ActionMenu, ActionMenuItem, ActionRow, Badge, Box, Button, Card, Checkbox, Collapsible, CollapsibleItem, Column, Copyable, DetailGrid, DetailGridItem, Dropdown, DropdownItem, EmptyState, Grid, GridChild, Header, Hero, IconWidget, Image, Item, Label, Meter, Progress, Row, Scale, Scroll, Section, Separator, Spinner, StatusDot, Switch, Toast, ToastAction, } from "./widgets.js";
@@ -0,0 +1,43 @@
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
+ export declare class StatusItem {
15
+ readonly options: {
16
+ id?: string;
17
+ icon?: Icon;
18
+ label?: string;
19
+ tooltip?: string;
20
+ };
21
+ constructor(options?: {
22
+ id?: string;
23
+ icon?: Icon;
24
+ label?: string;
25
+ tooltip?: string;
26
+ });
27
+ toProtocol(): Record<string, unknown>;
28
+ }
29
+ export declare class MenuItem {
30
+ readonly options: {
31
+ id: string;
32
+ label: string;
33
+ visible?: boolean;
34
+ enabled?: boolean;
35
+ };
36
+ constructor(options: {
37
+ id: string;
38
+ label: string;
39
+ visible?: boolean;
40
+ enabled?: boolean;
41
+ });
42
+ toProtocol(): Record<string, unknown>;
43
+ }
@@ -0,0 +1,58 @@
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
+ export class StatusItem {
19
+ options;
20
+ constructor(options = {}) {
21
+ this.options = options;
22
+ }
23
+ toProtocol() {
24
+ const payload = {};
25
+ if (this.options.id !== undefined) {
26
+ payload.id = this.options.id;
27
+ }
28
+ if (this.options.icon !== undefined) {
29
+ payload.icon = this.options.icon.toProtocol();
30
+ }
31
+ if (this.options.label !== undefined) {
32
+ payload.label = this.options.label;
33
+ }
34
+ if (this.options.tooltip !== undefined) {
35
+ payload.tooltip = this.options.tooltip;
36
+ }
37
+ return payload;
38
+ }
39
+ }
40
+ export class MenuItem {
41
+ options;
42
+ constructor(options) {
43
+ this.options = options;
44
+ }
45
+ toProtocol() {
46
+ const payload = {
47
+ id: this.options.id,
48
+ label: this.options.label,
49
+ };
50
+ if (this.options.visible !== undefined) {
51
+ payload.visible = this.options.visible;
52
+ }
53
+ if (this.options.enabled !== undefined) {
54
+ payload.enabled = this.options.enabled;
55
+ }
56
+ return payload;
57
+ }
58
+ }