Applet protocol
Version 1 of the wire protocol between the panel and an exec applet: framing, every message in both directions, the tree operations, and the limits the panel enforces. You need it to write an SDK; an applet built on an SDK never sees it. The elements and their props are in Elements.
Transport and framing
The applet connects to GLIMPSE_APPLET_SOCKET; both directions carry UTF-8 NDJSON on that unix socket: one JSON object per line, separated by \n. Stdio carries the same stream in tests. The host also accepts a trailing \r. Stderr is not part of the protocol; it goes to the journal, or to the terminal of the glimpsectl applet run that attached the applet.
Each line is at most 1 MiB (1,048,576 bytes), not counting the newline. A longer line is rejected. A single JSON value nests at most 128 levels; a deeper tree is built across commits. The operation value op is a closed set: an unknown op, or a line that is not JSON, does not decode. A message with an unknown t is ignored.
The applet speaks first; both hellos carry v: 1.
How one conversation goes
- The applet connects to the unix socket in
GLIMPSE_APPLET_SOCKET. Both directions then carry NDJSON on that one connection. - The applet sends
hellowithv: 1. The host starts a new generation and clears the protocol tree. - The host answers with its own
hello(name,state,options,placement,entry), then sendspopoverif one is open. - The applet sends a
commit, one batch ofopsthat builds or changes its widget tree. - The host applies the batch to a copy of the tree, checks the whole batch, and only then publishes it to the exec widget. A rejected batch changes nothing on screen.
- A user gesture becomes an
event. Events carry the generation of the tree they came from, and the host drops any event from an older generation. - The applet replies with another
commit, or with a request such asopen-uri.
Object keys added by a newer peer are ignored. A commit is applied as a whole batch or not at all: if any operation in the batch is rejected, the tree does not change. The host then stops that applet. The caps below are what the validator enforces, except the line rate, which the host counts itself: past the cap it stops reading until the next second, so the applet blocks on its socket. More than 5 hellos within any 10 seconds kills the unit with the reason too many hellos. systemd restarts configured applets; development copies stay down.
Host to applet
Every host message is an object with a t tag. Message shapes below are written as TypeScript types; each is one JSON object on one line.
t | Sent when |
|---|---|
hello | In answer to the applet's hello |
options | The instance's options changed |
placement | The bar's placement changed |
event | A subscribed element handler fired |
popover | The instance's popover opened or closed |
hello
Sent in answer to the applet's hello, and again after the applet says hello on a reload.
type Hello = {
t: "hello";
v: 1;
name: string; // this instance's name
state: string; // absolute path to the applet's state directory, mode 0700
options: object; // the instance's options
placement: Placement; // where this instance is drawn
entry?: { id: string; name: string; icon: string | null } | null; // the desktop entry
};options
Sent when glimpsectl applet run reloads the project's options. The process is not restarted; a change to the panel configuration restarts it instead.
type Options = { t: "options"; options: object };placement
Sent when the bar's placement changes and the process stays up.
type PlacementMessage = { t: "placement"; placement: Placement };
type Placement = {
output: string | null; // connector name such as "DP-2", or null
position: "top" | "bottom" | "left" | "right"; // the edge the bar sits on
orientation: "horizontal" | "vertical"; // how the bar lays out
zone: "left" | "center" | "right";
size: number; // the bar's thickness, in logical pixels
};event
A subscribed handler fired.
type Event = {
t: "event";
id: number; // the element id
name: string; // the handler name, below
args: unknown[]; // the handler's arguments, in order; [] when it takes none
seq: number | null;
};seq is always present. It is a number only for onChange, onToggle and the choicelist and dropdown onSelect. The applet copies that number onto the next set of the same element when the set confirms the edited value. A set with no seq is a deliberate replacement and is stored as sent. The host keeps a newer local edit on screen when a later set of the element's controlled prop (value, active, open or selected) carries an older seq; the stored tree still records that set.
| Handler | Elements | args |
|---|---|---|
onPress | indicator | [button]: 2 middle, 3 right, otherwise the code as reported. A left click opens the popover and never fires onPress |
onScroll | indicator | [dx, dy]: scroll deltas, numbers |
onActivate | actionrow | [] |
onToggle | switch, togglerow, expanderrow | [active] or [open]: boolean. seq is a number |
onMove | sliderrow | [value]: number during a drag. seq is null |
onChange | scale, sliderrow | [value]: number. seq is a number |
onChange | entry, entryrow | [value]: string. seq is a number |
onSubmit | entry, entryrow | [value]: string |
onClick | button | [] |
onSelect | choicelist, dropdown | [index]: number. seq is a number |
onSelect | calendar | [date]: YYYY-MM-DD string |
onMonth | calendar | [year, month]: numbers |
onActivate | notice | [] |
onToggle | togglebutton, checkbutton | [active]: boolean. seq is a number |
A handler prop on an element is a boolean. "onPress": true subscribes. A missing or false handler does not.
The controlled props are entry.value, entryrow.value, scale.value, switch.active, togglebutton.active, checkbutton.active, dropdown.selected, choicelist.selected, togglerow.active, expanderrow.open and sliderrow.value. sliderrow.onMove reports motion without controlling value; onChange confirms the value. An expanderrow closed by the panel through the accordion or an outside press reports onToggle(false) with a bumped seq. An applet set of open does not emit onToggle.
popover
type Popover = { t: "popover"; open: boolean }; // whether this instance's popover is showingApplet to host
Every applet message is an object with a t tag.
t | Meaning |
|---|---|
hello | First message on a connection; starts a new generation |
commit | A batch of tree operations |
open-uri | Ask the host to open a URI |
launch | Ask the host to launch a desktop entry |
close-popover | Close this instance's popover |
hello
type Hello = { t: "hello"; v: 1 };The first message the applet sends. The host answers with its own hello. A later hello from the same process starts a new generation and an empty tree.
commit
type Commit = { t: "commit"; ops: Operation[] };Operations are applied in order to a copy of the tree. Ids, cycles, the parent table, the node cap and the child cap are checked before the copy replaces the live tree.
open-uri
type OpenUri = { t: "open-uri"; uri: string };Only http, https, mailto and file URIs of at most 2,048 bytes are opened; any other is dropped. A file URI opens the resolved path when it is a directory, or a regular file that is neither executable nor a type that starts a program: a desktop entry, a script, a binary, an installer or an archive that runs.
launch
type Launch = { t: "launch"; id: string }; // a desktop-file id, with or without .desktopclose-popover
No fields. {"t":"close-popover"}.
Operations
An operation is an object with an op tag. Ids are integers the applet chooses. 0 is the root and is never an element id. An id is unique for as long as its element exists.
before may be absent or null; either appends. A number is the sibling to insert in front of, and that sibling must already be a child of parent.
insert
{"op":"insert","parent":0,"before":null,"node":{"id":1,"type":"indicator","props":{},"children":[]}}type Insert = { op: "insert"; parent: number; before?: number | null; node: Element };
type Element = { id: number; type: string; props?: object; children?: Element[] };node is {id, type, props, children}. props defaults to {} and children to []. The whole subtree is new ids. Text of a label or button is the text prop, not a child node. The same is true of an indicator's label.
move
{"op":"move","parent":2,"id":5,"before":3}Takes id off its current parent and inserts it under parent, before before. A move under id itself, or under one of its descendants, is a cycle and rejects the batch. Reordering is a move whose parent is already the parent.
remove
{"op":"remove","parent":1,"id":7}id must be a child of parent. The element and every descendant are deleted.
set
{"op":"set","id":3,"props":{"title":"Sequenced"},"seq":4}type Set = { op: "set"; id: number; props: object; seq?: number };props is a merge, not a replacement. A value of null deletes that key. Any other key keeps its previous value. After the merge the element is derived again. The element type does not change. When seq is present it is stored on the element; when it is absent the stored seq is cleared.
A key the element does not take, or whose value has the wrong type, is dropped and not stored, and the other keys still apply. A number that is not finite is the same kind of drop. JSON itself cannot carry NaN or infinity; a non-finite number is still rejected if one is presented to the decoder.
Caps
| Cap | Value | Who enforces it |
|---|---|---|
| Line length | 1,048,576 bytes | The host, while framing |
| JSON nesting | 128 levels in one value | The decoder |
| Nodes | 300, counting the root | The commit |
| Children of one parent | 200 | The commit |
| Operations per commit | 1,200 | The commit |
| Lines read | 120 per second; more are delayed, not rejected | The host, not the tree |
| Text | 256 characters | The commit, via cleaning |
| Element name | 64 characters | The commit, via cleaning |
badge | 8 characters | The commit, via cleaning |
entry.value, entryrow.value | 4,096 characters | The commit, without trimming |
dot | 256 characters | The commit, without cleaning |
Rejection
A batch is rejected, and the previous tree kept, when it:
- names an id that is not in the tree, or reuses one
- moves an element under itself or under a descendant
- puts a child where the parent table does not allow it
- carries more than 1,200 operations
- would hold more than 300 nodes once the whole batch is applied
- would give one parent more than 200 children once the whole batch is applied
- tries to
set,moveorremovethe root - carries an array prop over its item cap (64, or 366 for chart
valuesandtooltips) - gives an element more children of a kind than its table allows, such as a second
addon
before that is not a child of parent is the same kind of rejection. A line that does not decode never reaches this list.