# Writing a bar widget (/docs/haus/rooms/bar-widgets)



A pill that is a line of text needs no framework: give
`haus.bar.widgets.<name>.command` a script whose stdout is the label and stop
reading. This page is the other tier, `script`, for a pill that wants a
dropdown, a gesture, a graph, or a colour that means something.

A widget is **one file**. It says how it is wired and the runtime owns the
rest, so a widget script never calls `sketchybar`, never reads `$BAR_NAME`, and
never names a hex.

## The whole shape [#the-whole-shape]

```bash
#!/bin/bash
# widget: interval   = 60
# widget: popup      = true

BAR_ITEM=pomodoro
source "$HOME/.config/sketchybar/barlib.sh"

fetch() {                                   # impure: gather the world
  emit left="12m" tone=ok
}

render() {                                  # pure: state in, components out
  pill --icon "" --label "$left" --tone "$tone"
}

on_click() { open -a Timer; }

barlib_main "$@"
```

```nix
haus.bar.widgets.pomodoro = {
  script = ./pomodoro.sh;          # the file above, header and all
  style."icon.color" = "$TEAL";    # its identity, and nothing else
  placement = "bottom-right";      # optional, the menu bar otherwise
};
```

`script` and `style` are **host-only**, both being code this machine runs, so a
shared desktop may place, retune and switch off any pill but may not bring a
new one.

<Callout type="info" title="Both tiers are the same option">
  Setting both `command` and `script` is an error, as is a leaf from the other
  tier: `icon` on a framework widget, which draws its own in `render`, or `style`
  on a command widget, which wears the bar's look.
</Callout>

## The header [#the-header]

The `# widget:` lines are the wiring, read **when your Mac is built**, so a key
that does not exist fails the build naming your file.

| key          | value                  | default       | what it does                                                         |
| ------------ | ---------------------- | ------------- | -------------------------------------------------------------------- |
| `interval`   | seconds                | none          | how often it ticks. Omit it for an event-only widget                 |
| `popup`      | `true` / `false`       | `false`       | gives it a dropdown, and the `popup_*` row functions                 |
| `subscribes` | names, comma-separated | `system_woke` | the events it wakes for                                              |
| `graph`      | a point count          | none          | draws the item as a graph that wide. Needs an `interval`             |
| `segments`   | names, comma-separated | none          | makes the pill a bracket over one item per name                      |
| `mark`       | one mark name          | none          | the widget's own hue, worn by headings and sparklines naming no tone |

Those six are the whole set. Each stands on its own line, leading spaces are
fine, and nothing comes before `widget:` but the `#`. A key at the tail of a
sentence fails the build rather than quietly wiring nothing, and prose *about* a
header is left alone as long as the example sits in backticks.

## fetch and render [#fetch-and-render]

`emit key=value…` accumulates this tick's state, which the runtime compares
against the last tick's cached state. Identical state means `render` never runs
and the bar hears nothing at all. Changed state comes back as shell variables,
`render` runs, and every component call goes out in one batch.

So `fetch` may be as expensive as it needs to be and `render` must be pure:
skipped whenever nothing moved, a side effect in it fires only sometimes, which
is the worst way for a bug to behave. `fetch` does not run on a click either, so
a click never shoves a graph's history sideways and a handler wanting fresh
numbers gathers its own.

## Reacting to the pointer [#reacting-to-the-pointer]

Define whichever you want and the runtime routes to them: `on_click`, the left
click and the fallback for any chord you did not define, with `on_right_click`
and `on_middle_click` beside it; the modifiers `on_cmd_click`, `on_alt_click`,
`on_shift_click` and `on_ctrl_click`, where the button outranks the modifier;
and `on_scroll`, `on_hover` and `on_unhover`.

`on_unhover` also fires for a pointer flicked straight off the bar, so write it
to be safe to run twice.

## Drawing [#drawing]

`pill` is the readout. Everything else is for a pill that needs more than a
glyph and a word.

```bash
pill --icon "" --label "3 PRs" --tone warn              # icon and label, one tone
pill --icon "" --label "3" --tone bad --label-tone dim  # two tones
pill --icon "" --label "" --mark plum                   # identity glyph, no label
pill --hide                                             # gone for now
```

* **`--tone` paints the icon, `--label-tone` the label**: the octocat saying how
  bad, the number how many.
* **`--mark` takes the icon off the ladder onto the identity axis.** The last
  colour named wins, so `--mark plum --tone dim` is "a podcast, and it is
  paused".
* **Empty means absent.** An empty `--label` stops drawing it *and* re-centres
  the icon, for a pill whose resting state hides a zero.
* **`--hide` is the safe one.** It pairs `drawing=off` with `updates=on`, so a
  hidden pill keeps ticking and can bring itself back.

`graph <percent>` pushes a point onto the rolling window behind the text, for a
header carrying `graph = <width>`. Push from `fetch`, never `render`, which is
diffed: a quiet stretch and a stalled pill would draw the same flat line. The
window is `width × interval` seconds wide only if every tick contributes. The
history lives in the running item, so a rebuild leaves the pill flat for a whole
window, and the line's colour is `style."graph.color"`.

`segment <name> …` is one member of a pill whose header carries `segments =`.
SketchyBar colours a label exactly once, so three counts in three colours cannot
be one item: the runtime brackets one item per segment, addressed by bare name.
One tone paints both halves of a segment, so two readings side by side means
two segments.

`sb_set <prop>=<val>` is the raw escape for a property no component covers, on
the same batch, later arguments winning.

## Tones, not colours [#tones-not-colours]

A widget names a **tone**, never a hex or a palette key. That is how your pill
follows the theme every other pill follows.

| tone     | what it claims                                   |
| -------- | ------------------------------------------------ |
| `mute`   | nothing there: inactive, stale, no verdict       |
| `dim`    | present but subordinate: a heading, a row's name |
| `text`   | a live readout with no alarm in it               |
| `ok`     | green, nothing needed                            |
| `busy`   | the machine has it, not you                      |
| `watch`  | worth knowing, nothing to do yet                 |
| `warn`   | wants a human here                               |
| `bad`    | the load-bearing thing is broken                 |
| `action` | a thing you press, not a status                  |
| `accent` | haus's own mark: identity, never status          |

A **mark** is the other axis, for a subject the bar cannot know until it runs:
which client wrote a row, which app is playing. They are named for hue families
rather than meanings, because a mark makes no claim.

| mark     | for                                               |
| -------- | ------------------------------------------------- |
| `warm`   | Anthropic's clay: Claude, and VLC                 |
| `rust`   | a muted red: video in a browser tab               |
| `pink`   | music with no app of its own                      |
| `violet` | Gemini's blue-violet, and a local video file      |
| `blue`   | music in a browser tab                            |
| `teal`   | OpenAI's green-teal: Codex, and Spotify           |
| `plum`   | podcasts, and any subject with no mark of its own |

<Callout type="warn" title="Identity and status never share a hue">
  haus enforces it at build time: a mark on a verdict's colour fails the build.
  Paint a heading yellow and it silently claims something is 60% of the way to
  broken.
</Callout>

## Dropdowns [#dropdowns]

Set `popup = true` and define `popup_rows`. The runtime owns the frame, the
grid, every row's font and height, the rebuild, and closing the panel after a
click.

```bash
popup_rows() {
  popup_heading --label "Pomodoro" --icon "" --badge "3 of 4"
  popup_bar --label "this round · ends 14:52" --percentage 62 --value "62%"
  popup_row --label "Started" --value "14:02"
  popup_separator
  popup_item --title "Write the release notes" --subtitle "next · 25m" --badge "up next"
  popup_button --label "Reset" --run "pomodoro reset"
}
```

| kind              | for                                                                                                             |
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
| `popup_heading`   | a section title in your widget's hue, glyph in a tinted well. `--count` appends ` · n`                          |
| `popup_row`       | a thing you can act on                                                                                          |
| `popup_item`      | a bold `--title` with a glyph, a small `--subtitle` under it, the reading on the right. Two lines, one hit area |
| `popup_action`    | a verb: Refresh, or a command to copy                                                                           |
| `popup_button`    | a filled, centred control, tinted. `--solid` for the one primary thing to do                                    |
| `popup_bar`       | a name and its `--value` on one line, and under them a real track as full as `--percentage`                     |
| `popup_graph`     | a sparkline the width of the panel, fed `--points "…"`                                                          |
| `popup_note`      | an aside: "nothing", "+4 more"                                                                                  |
| `popup_separator` | a hairline between two groups                                                                                   |
| `popup_space`     | nothing, that many points tall                                                                                  |
| `popup_slider`    | a track you aim, like a scrubber                                                                                |
| `popup_image`     | a row that is entirely a picture                                                                                |

Every panel is one grid: a glyph column, a text column, and a right edge where
`--value`, the capsule `--badge` and the small caption `--hint` land. You never
name a width or a padding; `popup_width <points>` at the top of the file buys a
wider one, and text that will not fit wears an ellipsis rather than spilling
out. When a name and its value cannot both fit, the value keeps what it needs,
up to under half the panel, and the name is the half cut, so `+2 unshipped` is
never `+2 …`.

A `popup_row` with no `--run` or `--open` does not light up under the pointer,
and does nothing. Give a `popup_item` a `--run` and both its lines are one
button. Every row closes the panel when clicked, `popup_slider` excepted,
because scrubbing is something you do twice.

With a `--value` the tone follows the **number**: the name on the left is the
question and stays dim, the value on the right is the answer and the only half
on the ladder. A heading with no tone of its own wears the `mark` your header
declares, so a pill that is teal in the bar opens a teal panel.

## Listening for things [#listening-for-things]

`subscribes` takes SketchyBar's own events (`front_app_switched`,
`volume_change`, `system_woke`, …) and any custom name, which is declared and
subscribed for you, so two widgets can talk without either knowing the other's
name:

```bash
# widget: subscribes = front_app_switched, pomodoro_finished
```

```bash
bar_emit pomodoro_finished round=3
```

`bar_emit` fires at **both** bars: a pill on the bottom bar is not woken by an
event sent only to the menu bar.

## Running it by hand [#running-it-by-hand]

The file the bar runs is the file you wrote, installed at
`~/.config/sketchybar/widgets/<name>.sh`, which is the whole debugging story:

```bash
BAR_ITEM=pomodoro ~/.config/sketchybar/widgets/pomodoro.sh
```

It reads its own header, so even a segmented pill draws as the bar draws it.
`clock.sh` in the haus repository is the smallest complete widget to read, and
`~/.config/sketchybar/barlib.sh` documents every function above in its own
comments.

## Options [#options]

[Every setting, with types and defaults](/docs/haus/reference/options#bar).
