Skip to content

Getting Started: Build Your First Applet

This walkthrough takes you from an empty directory to a working counter applet in your panel. The counter shows a number; clicking "Increment" inside its popover bumps the number by one.

You can do this in any of four languages — Rust, Python, TypeScript, or Go. Pick the one you're comfortable with; the protocol underneath is identical, so applets behave the same regardless.

If you only want a launcher button that runs a single command, you don't need an SDK at all — read Command Applet first and come back here only if you need live state or custom controls.

Prerequisites

  • A working Glimpse install (glimpse-shell running, your panel visible). See Installation.
  • A ~/.config/glimpse/config.toml you can edit.
  • glimpse-shell in your PATH. Run glimpse-shell applets doctor to check your host and language toolchain.
  • One language toolchain installed:
    • Rust: rustc 1.93+ and cargo.
    • Python: 3.14+.
    • TypeScript: Node.js 20+.
    • Go: 1.24+.

Fast Path With glimpse-shell applets

Create a generated project:

sh
glimpse-shell applets new counter --lang python
cd counter

Run it in live development mode:

sh
glimpse-shell applets dev

Make sure your panel includes __dev__ so active dev applets are visible:

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

When the applet is ready for normal use, install it:

sh
glimpse-shell applets link

Then replace __dev__ or add the applet id directly in a panel section:

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

The rest of this page shows the code shape behind the generated project. Read Applet Tooling for the full glimpse-shell applets command reference.

What you're building

Your applet is a separate program that Glimpse spawns. It reads JSON lines from stdin (events) and writes JSON lines to stdout (status items and popover content). The SDK hides the line protocol behind a small applet base class: you implement status, popover, and event handlers, and the SDK takes care of stdio and JSON.

The final result, in your panel:

  • A status item with a refresh icon and the current count.
  • Left-click opens a popover with a hero, the value, and an Increment button.
  • Clicking Increment updates the panel and the popover.

Path A — Rust

Create a new binary crate:

sh
cargo new --bin counter
cd counter

Edit Cargo.toml:

toml
[package]
name = "counter"
version = "0.1.0"
edition = "2024"

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

Replace src/main.rs with:

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>> {
        let count = state.count;
        Ok(Some(
            Column::new(tree![
                Hero::new("Counter", format!("Value: {count}")),
                Section::new(
                    "Controls",
                    tree![
                        Label::new(format!("Current: {count}")),
                        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
}

Build a release binary:

sh
cargo build --release

The binary is at target/release/counter. Skip to Wire it into your panel and use that path as command.


Path B — Python

Create a new project directory:

sh
mkdir counter && cd counter

Install the SDK (uv recommended, but pip install --user works too):

sh
uv init --bare
uv add glimpse-applet-sdk

Create main.py:

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(title="Counter", subtitle=f"Value: {state.count}"),
                Section(
                    title="Controls",
                    children=[
                        Label(text=f"Current: {state.count}"),
                        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()

Verify it runs (send EOF to exit):

sh
echo "" | uv run python main.py

You should see one status {...} line on stdout. Skip to Wire it into your panel and use ["uv", "run", "--directory", "/path/to/counter", "python", "main.py"] as command, or absolute paths to a venv interpreter.


Path C — TypeScript

Create a new project:

sh
mkdir counter && cd counter
npm init -y
npm pkg set type=module
npm install glimpse-sdk
npm install --save-dev typescript @types/node

Create tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

Create src/main.ts:

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({ title: "Counter", subtitle: `Value: ${state.count}` }),
        new Section({
          title: "Controls",
          children: [
            new Label(`Current: ${state.count}`),
            new Button({
              id: "increment",
              label: "Increment",
              icon: "list-add-symbolic",
              variant: "primary",
            }),
          ],
        }),
      ],
    });
  }
}

await new CounterApplet().run();

Build:

sh
npx tsc

Skip to Wire it into your panel and use ["node", "/path/to/counter/dist/main.js"] as command.


Path D — Go

Initialize a module:

sh
mkdir counter && cd counter
go mod init example.com/counter
go get github.com/alex-oleshkevich/glimpse/sdk/sdk-go

Create 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(s *counterState) { s.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: fmt.Sprintf("Current: %d", state.Count)},
                    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)
    }
}

Build:

sh
go build -o counter

Skip to the next section and use /path/to/counter/counter as command.


Wire it into your panel

If your project has an applet.toml, run glimpse-shell applets link from the project directory to symlink it into ~/.config/glimpse/applets/ automatically. Otherwise, create the applet package file manually:

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

[exec]
command = ["/absolute/path/to/your/counter"]

Add the applet name to a panel section:

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

Save the config. Glimpse picks up changes on the next panel reload — either click your existing reload button or restart glimpse-shell (systemctl --user restart glimpse-shell).

You should now see your counter in the panel. Left-click opens the popover; click Increment and the number updates in both places.

What happened

When Glimpse started your applet it sent one init line on stdin with the applet's name and any options from your config (we didn't set any). Your render() then printed a status line and (because the popover opened later) a popover line. When you clicked Increment, Glimpse sent an event line back; the SDK routed it to your click handler; you mutated state; the SDK re-rendered and pushed fresh status + popover lines.

You never wrote JSON or touched stdin/stdout. The SDK handles the line transport, while your applet provides status items, popover content, and event handlers.

Common follow-ups

  • Pass per-applet config. Add a [applets.counter.options] table in your TOML; the SDK exposes it to your applet during init.
  • Refresh on a timer, not only on events. Spawn a background task that mutates state every N seconds — the SDK re-renders automatically.
  • Use richer widgets. The full component reference is at Components. The most useful ones for live state are item (display row with a right slot), action_item (clickable row with a render-only right slot), meter (progress + slider), property_list (key/value facts), slider (numeric control), and select (mode switcher).
  • Restart on crash. The restart_delay_ms config option controls how soon Glimpse re-spawns your applet after it exits. The default is 1 second.
  • Debug. Write to stderr from your applet — Glimpse logs it but doesn't display it. Don't write non-protocol bytes to stdout; they corrupt the line stream.

Where to go from here

TopicPage
Scaffold, run, link, and remove applets with glimpse-shell appletsApplet Tooling
Configure the exec appletExec Applet
Line protocol referenceLine Protocol
Every widget with its fieldsComponents
Single-page SDK reference per languageExec SDK
LLM-friendly single-file referenceLLM exec docs