Skip to content

Exec Applet — Full Reference

The exec applet runs a long-running child process. The child controls the applet's panel status item and popover content over a line-based JSON protocol on stdin and stdout. Use it for custom widgets that need live state, custom controls, or reactions to user input.

This page is self-contained. You do not need to read any other page to write a working exec applet — config, raw protocol, every component, every event, and SDK starters in four languages are all here.


Table Of Contents

  1. Quickstart
  2. Choosing Command Or Exec
  3. Applet Project Directories
  4. glimpse-shell applets CLI Workflow
  5. Configuration
  6. Line Protocol
  7. Messages From Glimpse To Child
  8. Messages From Child To Glimpse
  9. Component Reference
  10. Event Reference
  11. Lifecycle And Restart Semantics
  12. Best Practices
  13. Raw Shell Starter
  14. Python SDK Starter
  15. TypeScript SDK Starter
  16. Rust SDK Starter
  17. Go SDK Starter
  18. IPC Client

Quickstart

The shortest possible exec applet is a shell script that prints one status line. Save it as ~/.config/glimpse/scripts/hello:

sh
#!/bin/sh
printf 'status {"items":[{"id":"hello","label":"hi","icon":{"name":"face-smile-symbolic"}}]}\n'
exec sleep infinity

Then create an applet package file and reference its id from panel config:

toml
# ~/.config/glimpse/applets/hello.toml
id = "hello"
type = "exec"

[exec]
command = ["sh", "-c", "~/.config/glimpse/scripts/hello"]
toml
# ~/.config/glimpse/config.toml
[[panels]]
right = ["hello"]

That's a complete, working exec applet. To go beyond, read the rest of this page.


Choosing Command Or Exec

Choose the applet type before writing code:

NeedApplet typeWhy
Launch an app or URLcommandA static button can run one configured command.
Show a small static menu of commandscommandMenu items are TOML config, not a child process.
Show changing statusexecA child process can emit new status lines over time.
Show custom popover widgetsexecThe child process owns a component tree.
React to clicks, sliders, toggles, or popover open/closeexecGlimpse sends event lines to the child.
Use Python, TypeScript, Rust, or Go SDK helpersexecSDKs wrap the exec protocol.

LLM guidance: if the requested applet has state, live data, custom UI, or interactive controls, create an exec applet. Use command only for launchers and command menus.


Applet Project Directories

Prefer project directories for custom applets. A project directory keeps the applet package, source code, and install metadata together.

txt
counter/
  applet.toml
  main.py

The root file is always applet.toml. For an exec SDK applet it looks like:

toml
id = "counter"
type = "exec"

[exec]
command = ["uv", "run", "main.py"]

For a command applet it looks like:

toml
id = "terminal"
type = "command"

[command]
icon = "utilities-terminal-symbolic"
tooltip = "Open terminal"
command = ["ghostty"]

Project directories can be discovered in two ways:

MethodFile writtenWhen to use
glimpse-shell applets dev~/.config/glimpse/applets/<id>.dev.tomlLive development with rebuild and restart.
glimpse-shell applets link~/.config/glimpse/applets/<id>.toml symlinkNormal use after the applet is ready.

Add linked applets by id in a panel section:

toml
[[panels]]
right = ["counter", "network", "battery"]

Add active dev applets with __dev__:

toml
[[panels]]
right = ["network", "__dev__", "battery"]

If the same applet id appears in ~/.config/glimpse/config.toml and in the discovered applets directory, the explicit config.toml entry wins. During development, remove or rename the explicit entry if it shadows the dev applet.


glimpse-shell applets CLI Workflow

Use glimpse-shell applets to create, run, install, inspect, and debug applets.

Create

sh
glimpse-shell applets new counter --lang python
glimpse-shell applets new counter --lang typescript
glimpse-shell applets new counter --lang rust
glimpse-shell applets new counter --lang go
glimpse-shell applets new terminal --type command

new creates a project directory and writes applet.toml. Exec scaffolds include a starter SDK applet for the selected language.

Develop

sh
cd counter
glimpse-shell applets dev

dev behavior:

LanguageBuild stepRuntime commandWatched paths
Rustcargo build --quietcargo run --quietsrc, Cargo.toml
Pythonnoneuv run main.pymain.py
TypeScriptnpx tscnode dist/main.jssrc, tsconfig.json
Gogo build -o .dev-build.dev-buildproject directory

The dev command:

  • writes ~/.config/glimpse/applets/<id>.dev.toml while it runs;
  • watches source files with a debounce window;
  • rebuilds and restarts the child after source changes;
  • forwards Glimpse stdin/stdout between the shell and applet;
  • caches the init line and replays it to every restarted child;
  • removes the generated .dev.toml file when the interactive dev process exits.
sh
glimpse-shell applets link
glimpse-shell applets link /path/to/counter
glimpse-shell applets unlink
glimpse-shell applets unlink /path/to/counter
glimpse-shell applets unlink counter
glimpse-shell applets unlink counter --yes

link symlinks the project applet.toml into ~/.config/glimpse/applets/<id>.toml. unlink removes that symlink or removes an installed applet file by id; asks for confirmation unless --yes is provided.

Inspect And Diagnose

sh
glimpse-shell applets ls
glimpse-shell applets doctor
glimpse-shell applets doctor --lang python
glimpse-shell applets doctor --strict

ls shows linked and active dev applets. doctor checks glimpse-shell and language toolchains; --strict exits non-zero on failures.

IPC Debugging

sh
glimpse-shell watch
glimpse-shell watch bluetooth.*
glimpse-shell dispatch open_uri uri=https://example.com

watch subscribes to shell events and prints them. dispatch sends a shell IPC command and waits for an acknowledgement. Both commands use GLIMPSE_IPC_SOCKET if set, then $XDG_RUNTIME_DIR/glimpse/ipc.sock, then /tmp/glimpse/ipc.sock.


Configuration

toml
# ~/.config/glimpse/applets/sysinfo.toml
id = "sysinfo"
type = "exec"

[exec]
command = ["sh", "-c", "~/.config/glimpse/scripts/sysinfo"]
restart_delay_ms = 1000
env_clear = false

[exec.env]
PATH = "/usr/bin:/bin"
LANG = "C.UTF-8"

[exec.options]
interval = 5
unit = "celsius"
OptionTypeDefaultMeaning
typestringrequiredMust be "exec".
commandarray of stringsrequiredArgv to spawn the child process. No shell expansion — wrap with ["sh", "-c", "..."] for shell features.
restart_delay_msint1000Delay before restarting a crashed/exited child. Minimum 50.
env_clearboolfalseIf true, the child's environment starts empty (only env entries are kept).
envtable of strings{}Extra env vars set on the child. Applied after env_clear.
optionsTOML table{}Arbitrary per-instance configuration. Glimpse does not interpret it; it is forwarded verbatim in the first init line. Use it for polling intervals, units, thresholds, feature flags.

The applet <name> is the instance identifier and is sent to the child as instance in the init line.


Line Protocol

Glimpse and the child exchange messages over the child's stdin/stdout. Each message is one line: a command word, a single space, a JSON object, and a newline.

command {"field":"value"}

Specifics:

  • Each line must end in \n and the child must flush after each write.
  • Bytes between newlines that do not match ^[a-z_]+ \{.*\}$ are ignored and logged. Bad JSON is also ignored.
  • Unknown commands are ignored.
  • The order of messages matters only insofar as last write wins per channel (status replaces previous status, popover replaces previous popover).
  • The child should print its initial status immediately on startup; if it has popover content, it should also print popover at least once or when the popover lifecycle event arrives.
  • stderr is free for diagnostics. Avoid noisy stderr; Glimpse logs it but does not surface it in the UI.

Direction Summary

DirectionCommandPurpose
Glimpse → childinitOne-time startup announcement with instance name + options.
Glimpse → childeventUser interaction or popover lifecycle.
Child → GlimpsestatusReplace panel status items.
Child → GlimpsepopoverReplace the popover content tree.

Messages From Glimpse To Child

init

Sent exactly once, immediately after the child starts. Always precedes any event line.

init {"instance":"sysinfo","options":{"interval":5,"unit":"celsius"}}
FieldTypeMeaning
instancestringThe applet package id.
optionsobjectVerbatim copy of [exec.options]. Empty {} if omitted in the package file.

event

Sent when the user interacts with an interactive status item or a popover component, or when the popover opens/closes.

event {"id":"submit","type":"click","source":"popover","button":"left"}
event {"id":"volume","type":"change","source":"popover","value":0.72}
event {"id":"popover","type":"open","source":"popover"}
event {"id":"popover","type":"close","source":"popover"}

Common fields:

FieldTypeMeaning
idstringThe id of the component that fired the event. For popover lifecycle, always "popover".
typestringOne of click, scroll, input, change, toggle, open, close.
sourcestring"status" for status-item events, "popover" for popover events.

Type-specific fields are listed under Event Reference.


Messages From Child To Glimpse

status

Replaces the entire set of status items for this applet. Send a complete list every time — partial updates are not supported.

status {"items":[
  {"id":"cpu","icon":{"name":"cpu-symbolic"},"label":"42%","tooltip":"CPU"},
  {"id":"mem","icon":{"name":"memory-symbolic"},"label":"51%","tooltip":"Memory"}
]}

Each item:

FieldTypeDefaultMeaning
idstringunsetRequired if the item should emit click/scroll events.
iconobjectunset{"name":"icon-name-symbolic"} or {"path":"/absolute/path.png"}.
labelstringunsetShort text shown beside the icon.
tooltipstringunsetHover text.

Left-click on a status item opens the popover (if the applet has any).

popover

Replaces the popover content tree. Sent on first render and on every change to the tree. Glimpse only re-renders the popover when it is open or about to open — the child should still send updates whenever its model changes; Glimpse will buffer them.

popover {"root":{"type":"section","data":{
  "title":"System",
  "children":[
    {"type":"row","data":{"spacing":8,"children":[{"type":"label","data":{"text":"CPU"}},{"type":"badge","data":{"label":"42%"}}]}},
    {"type":"meter","data":{"label":"Memory","value":0.51,"text":"51%"}}
  ]
}}}

Top-level shape:

FieldMeaning
rootA single component node (the full popover tree). May be null to clear.

Every component node has the same envelope:

json
{"type":"<component-name>","data":{ /* component-specific fields */ }}

Component Reference

Every component accepts these common fields in its data object. They default to unset unless noted.

FieldTypeValuesMeaning
idstringRequired for interactive components (button, switch, toggle_button, slider, checkbox, select, interactive meter). Used as the id in events.
visiblebooltrue/false; default trueHide the component without removing it.
hexpandbooldefault falseLet the component grow horizontally.
vexpandbooldefault falseLet the component grow vertically.
halignstringfill, start, end, center, baselineHorizontal alignment.
valignstringsameVertical alignment.
tooltipstringHover text.
variantstringnormal, muted, accent, success, warning, dangerVisual emphasis. Default normal.

The component-specific fields below are all in addition to these.

Only the component names listed in this section are valid. Internal GTK component names such as action_row and action_menu are not exec protocol widget types; use action_item, section, button, menu_button, row, or column instead.

Layout Components

box

Explicit horizontal-or-vertical layout container.

json
{"type":"box","data":{
  "orientation":"vertical",
  "spacing":8,
  "children":[ /* nodes */ ]
}}
FieldTypeDefaultMeaning
orientationstringverticalvertical or horizontal.
spacingint0Pixel gap between children.
childrenarray[]Child nodes.

row

Horizontal layout (sugar for box with orientation: horizontal).

json
{"type":"row","data":{"spacing":8,"children":[ /* nodes */ ]}}
FieldTypeDefault
spacingint0
childrenarray[]

column

Vertical layout (sugar for box with orientation: vertical).

json
{"type":"column","data":{"spacing":8,"children":[ /* nodes */ ]}}
FieldTypeDefault
spacingint0
childrenarray[]

grid

Two-dimensional layout.

json
{"type":"grid","data":{
  "row_spacing":4,
  "column_spacing":4,
  "children":[
    {"row":0,"column":0,"width":1,"height":1,"child":{"type":"label","data":{"text":"CPU"}}},
    {"row":0,"column":1,"width":1,"height":1,"child":{"type":"badge","data":{"label":"42%"}}}
  ]
}}
FieldTypeDefault
row_spacingint0
column_spacingint0
childrenarray of grid-children[]

Grid child shape:

FieldTypeDefaultMeaning
rowint0Row index.
columnint0Column index.
widthint1Column span.
heightint1Row span.
childnoderequiredThe contained component.

scroll

Wrap a node in a scrollable region.

json
{"type":"scroll","data":{"child":{"type":"column","data":{"children":[ /* many nodes */ ]}}}}
FieldTypeMeaning
childnodeRequired. The content to scroll.

overlay

Stack render-only overlay nodes above a base child.

json
{"type":"overlay","data":{"child":{"type":"picture","data":{"path":"/home/me/.cache/avatar.png","content_fit":"cover"}},"overlays":[{"type":"badge","data":{"label":"Live","halign":"end","valign":"start"}}]}}
FieldTypeDefault
childnoderequired
overlaysarray of nodes[]

list_box

GTK list with one row per child.

json
{"type":"list_box","data":{"children":[{"type":"label","data":{"text":"First"}},{"type":"badge","data":{"label":"Second"}}]}}
FieldTypeDefault
childrenarray of nodes[]

expander

Collapsible disclosure container backed by gtk::Expander.

json
{"type":"expander","data":{"label":"Details","expanded":true,"child":{"type":"label","data":{"text":"More"}}}}
FieldTypeDefault
labelstringrequired
expandedboolfalse
childnoderequired

tree_expander

GTK tree expander wrapper around one child.

json
{"type":"tree_expander","data":{
  "child":{"type":"label","data":{"text":"Nested"}},
  "hide_expander":true,
  "indent_for_depth":true,
  "indent_for_icon":true
}}
FieldTypeDefault
childnoderequired
hide_expanderboolfalse
indent_for_depthboolfalse
indent_for_iconboolfalse

section

Titled group with optional subtitle.

json
{"type":"section","data":{
  "title":"Network",
  "subtitle":"Connected",
  "children":[ /* nodes */ ]
}}
FieldTypeDefault
titlestring""
subtitlestring""
childrenarray of nodes[]

card

A framed group with no header.

json
{"type":"card","data":{"children":[ /* nodes */ ]}}
FieldTypeDefault
childrenarray of nodes[]

separator

Visual divider.

json
{"type":"separator","data":{"orientation":"horizontal"}}
FieldTypeDefault
orientationstringunset (auto)

Display Components

hero

Large header at the top of a popover. Place it as the first child of a column/section.

json
{"type":"hero","data":{
  "title":"VPN",
  "subtitle":"Connected to wg0",
  "icon":{"name":"network-vpn-symbolic"}
}}
FieldTypeDefault
titlestringrequired
subtitlestring""
iconobjectunset

label

Plain text.

json
{"type":"label","data":{"text":"CPU usage","wrap":false,"selectable":false}}
FieldTypeDefault
textstringrequired
wrapboolfalse
xalignfloat 0.01.0unset
selectableboolfalse

icon

Symbolic icon rendered as a tree node.

json
{"type":"icon","data":{"icon":{"name":"network-wireless-symbolic"},"pixel_size":24}}
FieldTypeDefault
iconobjectrequired ({"name":...} or {"path":...})
pixel_sizeintunset

image

Image from icon name or file path. Same shape as icon.

json
{"type":"image","data":{"icon":{"path":"/home/me/.cache/avatar.png"},"pixel_size":64}}

picture

Image file rendered with gtk::Picture.

json
{"type":"picture","data":{"path":"/home/me/.cache/avatar.png","content_fit":"cover"}}
FieldTypeDefault
pathstring file pathrequired
content_fitstring: fill, contain, cover, scale_downcontain

badge

Small inline pill, typically used in rows, headers, or as a status indicator.

json
{"type":"badge","data":{"label":"42%","variant":"success"}}
FieldTypeDefault
labelstringrequired

status

Small status marker (a colored dot). Use variant to color it.

json
{"type":"status","data":{"variant":"success"}}

No specific fields beyond the common ones.

meter

Progress row with label and value. Can be made interactive (slider behavior).

json
{"type":"meter","data":{
  "icon":{"name":"audio-volume-medium-symbolic"},
  "label":"Volume",
  "value":0.42,
  "min":0.0,
  "max":1.0,
  "step":0.01,
  "text":"42%",
  "interactive":false
}}
FieldTypeDefault
iconobjectunset
labelstring""
valuefloatrequired
minfloat0.0
maxfloat1.0
stepfloat0.01
textstringunset (defaults to a formatted percent)
interactiveboolfalse

When interactive: true, dragging emits a change event with the new value.

progress

Plain progress bar.

json
{"type":"progress","data":{"value":0.7,"max":1.0,"show_text":true,"text":"70%"}}
FieldTypeDefault
valuefloatrequired
maxfloat1.0
show_textboolfalse
textstringunset

level_bar

Native GTK level indicator.

json
{"type":"level_bar","data":{"value":0.7,"min":0.0,"max":1.0,"mode":"continuous"}}
FieldTypeDefault
valuefloatrequired
minfloat0.0
maxfloat1.0
modestring: continuous, discretecontinuous

spinner

Loading indicator.

json
{"type":"spinner","data":{"spinning":true}}
FieldTypeDefault
spinningbooltrue

copyable

Label + value pair with a copy-to-clipboard affordance on the value.

json
{"type":"copyable","data":{"label":"IPv4","value":"10.0.0.42"}}
FieldTypeDefault
labelstring""
valuestringrequired

empty_state

Friendly placeholder when there's nothing to show.

json
{"type":"empty_state","data":{"title":"No devices","subtitle":"Plug in a USB device to start."}}
FieldTypeDefault
titlestringrequired
subtitlestring""

property_list

Two-column key/value table.

json
{"type":"property_list","data":{"title":"Network","rows":[
  {"key":"SSID","value":"home-5G"},
  {"key":"IPv4","value":"10.0.0.42"},
  {"key":"Gateway","value":"10.0.0.1"}
]}}
FieldTypeDefault
titlestring""
rowsarray of {key, value}[]

item

Display-only row with optional icon, sublabel, and a right child slot.

json
{"type":"item","data":{
  "icon":"network-wireless-symbolic",
  "label":"Wi-Fi",
  "sublabel":"Connected",
  "right":{"type":"badge","data":{"label":"home-5G"}}
}}
FieldTypeDefault
iconstring icon name""
labelstringrequired
sublabelstring""
rightcomponent nodeunset

action_item

Clickable row with optional icon, sublabel, and a render-only right child slot.

json
{"type":"action_item","data":{
  "id":"wifi",
  "icon":"network-wireless-symbolic",
  "label":"Wi-Fi",
  "sublabel":"Connected",
  "right":{"type":"badge","data":{"label":"home-5G"}}
}}
FieldTypeDefault
idstringrequired for events
iconstring icon name""
labelstringrequired
sublabelstring""
rightcomponent nodeunset
enabledbooltrue

Emits click with button: "left" from the row. The right node is visual only.

Interactive Controls

button

json
{"type":"button","data":{"id":"deploy","label":"Deploy","icon":"rocket-symbolic","variant":"primary"}}
FieldTypeDefault
idstringrequired for events
labelstringunset
iconstringunset
enabledbooltrue
variantstringflat; one of primary, secondary, compact, flat, danger

Emits click with button: "left".

Link-style button that opens a URI through GTK. It does not emit applet events.

json
{"type":"link_button","data":{"uri":"https://example.com/docs","label":"Docs"}}
FieldTypeDefault
uristring URIrequired
labelstringunset

Button that opens a rendered nested popover. The menu_button itself does not emit applet events; controls inside its popover node keep their normal behavior.

json
{"type":"menu_button","data":{
  "label":"More",
  "icon":"open-menu-symbolic",
  "popover":{"type":"label","data":{"text":"Menu content"}}
}}
FieldTypeDefault
labelstringunset
iconstring icon nameunset
popovernoderequired

switch

json
{"type":"switch","data":{"id":"vpn","label":"VPN","active":false}}
FieldTypeDefault
idstringrequired
labelstringunset
activeboolfalse

Emits toggle with active (and equivalently value).

toggle_button

Button-like toggle control.

json
{"type":"toggle_button","data":{"id":"focus","label":"Focus mode","active":false}}

Same fields and events as switch.

checkbox

json
{"type":"checkbox","data":{"id":"autostart","label":"Run at login","active":true}}

Same fields and events as switch.

slider

Slider.

json
{"type":"slider","data":{
  "id":"brightness",
  "min":0.0,
  "max":1.0,
  "step":0.05,
  "value":0.6,
  "orientation":"horizontal",
  "draw_value":true
}}
FieldTypeDefault
idstringrequired
minfloat0.0
maxfloat1.0
stepfloat0.1
valuefloat0.0
orientationstringunset (horizontal)
draw_valueboolfalse

Emits change with numeric value.

select

json
{"type":"select","data":{
  "id":"network",
  "selected":1,
  "items":[
    {"id":"home","label":"home-5G"},
    {"id":"office","label":"office"}
  ]
}}
FieldTypeDefault
idstringrequired
itemsarray of {id, label}[]
selectedintunset

Emits change with the selected item's id, label, and index.


Event Reference

All events have at minimum id, type, source. Type-specific fields:

Event sourcetypeExtra fieldsEmitted by
Status itemclickbutton: "left"|"middle"|"right"status item with id
Status itemscrolldelta_y: floatstatus item with id
Popover buttonclickbutton: "left"button
Popover action itemclickbutton: "left"action_item
Popover switchtoggleactive: bool, value: boolswitch
Popover toggle buttontoggleactive: bool, value: booltoggle_button
Popover checkboxtoggleactive: bool, value: boolcheckbox
Popover sliderchangevalue: floatslider
Popover interactive meterchangevalue: floatmeter with interactive:true
Popover selectchangevalue: {id, label, index}select
Popover inputinputtext: string(reserved for future input components)
Popover lifecycleopen / closeid: "popover"popover open/close

Example wire forms:

event {"id":"cpu","type":"click","source":"status","button":"middle"}
event {"id":"cpu","type":"scroll","source":"status","delta_y":-1.0}
event {"id":"deploy","type":"click","source":"popover","button":"left"}
event {"id":"vpn","type":"toggle","source":"popover","active":true}
event {"id":"brightness","type":"change","source":"popover","value":0.7}
event {"id":"network","type":"change","source":"popover","value":{"id":"office","label":"office","index":1}}
event {"id":"popover","type":"open","source":"popover"}
event {"id":"popover","type":"close","source":"popover"}

Lifecycle And Restart Semantics

  • Glimpse spawns the child process on panel startup, or when the applet is hot-reloaded into a panel.
  • The child receives init once, then any number of event lines over its lifetime.
  • When the child exits (any cause: crash, normal exit, signal), Glimpse waits restart_delay_ms and respawns. Restarts are unbounded by default; if the child keeps crashing in a loop, the delay throttles the rate.
  • A respawned child receives a fresh init with the current options. The previous state is lost — persist anything you need to keep across restarts in a file or external store.
  • Glimpse closes the child's stdin when the applet is removed or the panel shuts down. The child should treat stdin EOF as a shutdown signal.
  • The child should never write to stdout outside of valid protocol lines. Stray bytes corrupt nothing but waste log space.

Best Practices

PracticeWhy
Print initial status immediately, before any blocking work.The panel should not sit empty while the child warms up.
Send complete status and popover payloads every time.Each message replaces the previous one. There are no diffs.
Keep status labels short (1–6 chars).Long labels make the panel jump and crowd neighbors.
Put detail in the popover, glanceable summary in the panel.The panel is for at-a-glance state; popovers carry explanations and controls.
Use stable component ids across renders.Events become harder to reason about when ids drift.
Throttle polling (5–30 s for most stats).Sub-second polling wastes CPU.
Use variants sparingly.warning/danger should mean something needs attention.
Treat stdin EOF as shutdown.The child should exit cleanly so Glimpse does not restart it during teardown.
Use env_clear + env when the child should not inherit the user env.Reproducible behavior, smaller attack surface.
Log to stderr, not stdout.stdout is the protocol channel. Anything non-protocol there is wasted.
Validate your JSON before publishing.Bad JSON is silently dropped.
Prefer many small applets over one mega-script.Easier to debug, easier to fail in isolation.

Raw Shell Starter

This is a complete, self-contained shell applet that drives a CPU-temperature status item and shows a basic popover with one toggle button. No SDK is needed: it uses sh, printf, and a loop.

sh
#!/bin/sh
# ~/.config/glimpse/scripts/cpu-temp

set -eu

render_status() {
  printf 'status {"items":[{"id":"cpu","icon":{"name":"temperature-symbolic"},"label":"%s","tooltip":"CPU temperature"}]}\n' "$1"
}

render_popover() {
  printf 'popover {"root":{"type":"section","data":{"title":"CPU","children":[{"type":"row","data":{"spacing":8,"children":[{"type":"label","data":{"text":"Temperature"}},{"type":"badge","data":{"label":"%s"}}]}},{"type":"button","data":{"id":"refresh","label":"Refresh"}}]}}}\n' "$1"
}

read_temp() {
  sensors 2>/dev/null | awk '/Package id 0/ {print $4; exit}' | tr -d '+°C'
}

last=""

# Initial paint.
temp="$(read_temp)"
temp="${temp:-n/a}"
render_status "$temp"
render_popover "$temp"
last="$temp"

# Poll + react to events.
while true; do
  # Drain any pending events for 5 seconds.
  if IFS= read -r -t 5 line; then
    case "$line" in
      init\ *)
        : ;;
      event\ *)
        case "$line" in
          *'"id":"refresh"'*)
            temp="$(read_temp)"
            temp="${temp:-n/a}"
            render_status "$temp"
            render_popover "$temp"
            last="$temp"
            ;;
        esac
        ;;
    esac
  else
    temp="$(read_temp)"
    temp="${temp:-n/a}"
    if [ "$temp" != "$last" ]; then
      render_status "$temp"
      render_popover "$temp"
      last="$temp"
    fi
  fi
done

Config:

toml
id = "cpu-temp"
type = "exec"

[exec]
command = ["sh", "-c", "~/.config/glimpse/scripts/cpu-temp"]
restart_delay_ms = 1000

Python SDK Starter

Install:

sh
pip install glimpse-applet-sdk

Applet:

python
from dataclasses import dataclass

from glimpse_sdk import (
    Applet,
    AppletState,
    Button,
    ButtonVariant,
    Column,
    Hero,
    Icon,
    Label,
    Section,
    StatusItem,
    click,
)


@dataclass
class CounterState(AppletState):
    count: int = 0


class CounterApplet(Applet[CounterState]):
    def initial_state(self) -> CounterState:
        return CounterState()

    async def status(self, state: CounterState):
        return [
            StatusItem(
                id="counter",
                icon=Icon.name("view-refresh-symbolic"),
                label=str(state.count),
            )
        ]

    async def popover(self, state: CounterState):
        return Column(
            spacing=8,
            children=[
                Hero(
                    icon=Icon.name("view-refresh-symbolic"),
                    title="Counter",
                    subtitle=f"Value: {state.count}",
                ),
                Section(
                    title="Controls",
                    children=[
                        Label(text="Current"),
                        Button(
                            id="increment",
                            label="Increment",
                            icon="list-add-symbolic",
                            variant=ButtonVariant.PRIMARY,
                        ),
                    ],
                ),
            ],
        )

    @click("increment")
    async def on_increment(self, _event) -> None:
        await self.set_state(count=self.state.count + 1)


if __name__ == "__main__":
    CounterApplet().run()

Config:

toml
id = "counter"
type = "exec"

[exec]
command = ["python", "/home/me/applets/counter.py"]

SDK essentials:

  • Subclass Applet[YourState] where YourState is a @dataclass extending AppletState.
  • Implement initial_state(), plus async status(state) returning a list of StatusItem and async popover(state) returning a widget or None.
  • Register handlers with decorators: @click(id), @scroll(id), @input(id), @change(id), @toggle(id), @event(id, type).
  • Mutate state with await self.set_state(**fields) — this triggers a re-render.
  • The init payload is exposed via self.options (a dict) after on_init.
  • Override async on_init(event) for one-shot setup; event.options is the options dict.

TypeScript SDK Starter

Install:

sh
npm install glimpse-sdk

Applet:

ts
import {
  Applet,
  Button,
  Column,
  Hero,
  Icon,
  Label,
  Section,
  StatusItem,
  type TreeNode,
} from "glimpse-sdk";

interface CounterState {
  count: number;
}

class CounterApplet extends Applet<CounterState> {
  protected initialState(): CounterState {
    return { count: 0 };
  }

  constructor() {
    super();
    this.onClick("increment", async () => {
      await this.setState({ count: this.state.count + 1 });
    });
  }

  protected async status(state: CounterState): Promise<StatusItem[]> {
    return [
      new StatusItem({
        id: "counter",
        icon: Icon.name("view-refresh-symbolic"),
        label: String(state.count),
      }),
    ];
  }

  protected async popover(state: CounterState): Promise<TreeNode | null> {
    return new Column({
      spacing: 8,
      children: [
        new Hero({
          icon: Icon.name("view-refresh-symbolic"),
          title: "Counter",
          subtitle: `Value: ${state.count}`,
        }),
        new Section({
          title: "Controls",
          children: [
            new Label("Current"),
            new Button({
              id: "increment",
              label: "Increment",
              icon: "list-add-symbolic",
              variant: "primary",
            }),
          ],
        }),
      ],
    });
  }
}

await new CounterApplet().run();

Config:

toml
id = "counter"
type = "exec"

[exec]
command = ["node", "/home/me/applets/counter.js"]

SDK essentials:

  • Extend Applet<YourState> (state is a plain interface).
  • Implement initialState(), plus protected async status(state) returning StatusItem[] and protected async popover(state) returning TreeNode | null.
  • Register handlers in the constructor with this.onClick(id, fn), this.onScroll, this.onInput, this.onChange, this.onToggle.
  • Mutate state with await this.setState({ ... }) — partial patch object.
  • Override async onInit(event) for one-shot setup.

Rust SDK Starter

Cargo.toml:

toml
[dependencies]
async-trait = "0.1"
glimpse-sdk = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

src/main.rs:

rust
use async_trait::async_trait;
use glimpse_sdk::{
    Applet, AppletResult, Button, ButtonVariant, CallbackEvent, Column, Hero, Icon, Label, Section,
    StatusItem, TreeNode, run, tree,
};

#[derive(Debug, Clone, Default)]
struct CounterState {
    count: u32,
}

struct CounterApplet;

#[async_trait]
impl Applet for CounterApplet {
    type State = CounterState;

    async fn status(&self, state: &Self::State) -> AppletResult<Vec<StatusItem>> {
        Ok(vec![
            StatusItem::new("counter")
                .icon(Icon::name("view-refresh-symbolic"))
                .label(state.count.to_string()),
        ])
    }

    async fn popover(&self, state: &Self::State) -> AppletResult<Option<TreeNode>> {
        Ok(Some(
            Column::new(tree![
                Hero::new("Counter", format!("Value: {}", state.count))
                    .icon(Icon::name("view-refresh-symbolic")),
                Section::new(
                    "Controls",
                    tree![
                        Label::new("Current"),
                        Button::new("increment")
                            .label("Increment")
                            .icon("list-add-symbolic")
                            .variant(ButtonVariant::Primary),
                    ],
                ),
            ])
            .spacing(8)
            .into(),
        ))
    }

    async fn on_callback(
        &mut self,
        state: &mut Self::State,
        event: CallbackEvent,
    ) -> AppletResult<()> {
        if let CallbackEvent::Click(click) = event {
            if click.id == "increment" {
                state.count += 1;
            }
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() -> AppletResult<()> {
    run(CounterApplet, CounterState::default()).await
}

Config:

toml
id = "counter"
type = "exec"

[exec]
command = ["/home/me/.cargo/bin/counter-applet"]

SDK essentials:

  • Implement the Applet trait: status(&state) returning a Vec<StatusItem> and popover(&state) returning Option<TreeNode>. The applet struct itself is usually empty (struct CounterApplet;) since state lives in Self::State and is passed in by the runtime.
  • Match incoming events in on_callback(&mut state, event); mutate the passed-in &mut state directly.
  • Wrap any widget into TreeNode via TreeNode::from(widget) or .into(), or use the tree! macro for collections.
  • Hand the applet + initial state to run(applet, state).await from main.

Go SDK Starter

Install:

sh
go get github.com/alex-oleshkevich/glimpse/sdk/sdk-go

main.go:

go
package main

import (
	"context"
	"fmt"

	sdk "github.com/alex-oleshkevich/glimpse/sdk/sdk-go/sdk"
)

type counterState struct {
	Count int
}

type counterApplet struct {
	sdk.BaseApplet[counterState]
}

func newCounterApplet() *counterApplet {
	return &counterApplet{
		BaseApplet: sdk.NewBaseApplet(counterState{}),
	}
}

func (a *counterApplet) OnStart(context.Context) error               { return nil }
func (a *counterApplet) OnInit(context.Context, sdk.InitEvent) error { return nil }

func (a *counterApplet) OnCallback(_ context.Context, event sdk.CallbackEvent) error {
	if click, ok := event.(sdk.ClickEvent); ok && click.ID == "increment" {
		a.SetState(func(state *counterState) { state.Count++ })
	}
	return nil
}

func (a *counterApplet) Status(_ context.Context, state *counterState) ([]sdk.StatusItem, error) {
	return []sdk.StatusItem{{
		ID:    "counter",
		Icon:  sdk.IconName("view-refresh-symbolic"),
		Label: fmt.Sprintf("%d", state.Count),
	}}, nil
}

func (a *counterApplet) Popover(_ context.Context, state *counterState) (sdk.Widget, error) {
	return sdk.Column{
		Spacing: 8,
		Children: []sdk.Widget{
			sdk.Hero{Title: "Counter", Subtitle: fmt.Sprintf("Value: %d", state.Count)},
			sdk.Section{
				Title: "Controls",
				Children: []sdk.Widget{
					sdk.Label{Text: "Current"},
					sdk.Button{
						CommonProps: sdk.CommonProps{ID: "increment"},
						Label:       "Increment",
						Icon:        "list-add-symbolic",
						Variant:     sdk.ButtonVariantPrimary,
					},
				},
			},
		},
	}, nil
}

func main() {
	if err := sdk.Run[counterState](context.Background(), newCounterApplet()); err != nil {
		panic(err)
	}
}

Config:

toml
id = "counter"
type = "exec"

[exec]
command = ["/home/me/applets/counter"]

SDK essentials:

  • Embed sdk.BaseApplet[YourState] in your struct.
  • Implement OnStart, OnInit, OnCallback, plus Status(ctx, *state) ([]sdk.StatusItem, error) and Popover(ctx, *state) (sdk.Widget, error). The base provides State() and SetState(func(*State)).
  • Popover returns any value implementing sdk.Widget, or nil for no popover.
  • Compose trees with struct literals: sdk.Hero{Title: "..."}, sdk.Column{Children: []sdk.Widget{...}}, etc. Every widget type satisfies sdk.Widget.
  • Call sdk.Run[State](ctx, applet) from main.

IPC Client

Applets can connect to the Glimpse shell IPC socket to listen for events from other subsystems or dispatch commands without going through the exec protocol. This is useful for reacting to audio, network, Bluetooth, or any other shell event in real time.

Concepts

  • ipc(service) / IPC(service) takes a service name. Use "shell" (or the empty string, which also resolves to "shell") to connect to the panel. The socket path is resolved as $GLIMPSE_IPC_DIR/<service>.sock, falling back to $XDG_RUNTIME_DIR/glimpse/ipc.sock.
  • No connection is made until listen or dispatch is called.
  • listen(channel) subscribes to events by exact name, prefix glob ("audio.*"), or wildcard ("*"). Returns an async stream of events; each event has name (string), ts (unix timestamp int), and fields (key/value string map).
  • dispatch(action, params) sends a command to the shell and awaits the server acknowledgement. Rejects/errors if the server responds with ok=false.

Python

python
from glimpse_sdk import ipc

sub = ipc("shell")  # or ipc() — defaults to "shell"
async for event in await sub.listen("audio.*"):
    # event.name: str, event.ts: int, event.fields: dict[str, str]
    volume = event.fields.get("volume")
    await self.set_state(volume=int(volume or 0))

# Dispatch a command:
ack = await sub.dispatch("set_volume", {"level": "50"})

TypeScript

ts
import { ipc } from "glimpse-sdk";

const sub = ipc("shell"); // or ipc() — defaults to "shell"
for await (const event of sub.listen("audio.*")) {
  // event.name, event.ts, event.fields
  await this.setState({ volume: Number(event.fields.volume ?? 0) });
}
// Dispatch a command:
await sub.dispatch("set_volume", { level: "50" });

Rust

rust
use glimpse_sdk::ipc;

let sub = ipc("shell")?; // or ipc("") — both resolve to shell
let mut stream = sub.listen("audio.*").await?;
while let Some(event) = stream.next().await {
    let event = event?;
    // event.name, event.ts, event.fields
}
// Dispatch:
let _ack = sub.dispatch("set_volume", [("level", "50")]).await?;

Go

go
sub := sdk.IPC("shell") // or sdk.IPC("")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
events, err := sub.Listen(ctx, "audio.*")
if err != nil { /* handle */ }
for event := range events {
    // event.Name, event.Ts, event.Fields
}
// Dispatch:
ack, err := sub.Dispatch(ctx, "set_volume", map[string]string{"level": "50"})