A `panel` shell plugin drawing its own bottom layer-shell surface, one per screen, with an exclusive zone so Hyprland tiles above it. Clocks are laid out as a single timeline around local time: side and position derive from each zone's live UTC offset, so config order is irrelevant and a DST shift re-sorts the bar on its own. Qt's QML engine ships no `Intl`, so an IANA zone cannot be rendered from inside QML at all. Offsets come from the system's own tzdata via `date +%z` and the arithmetic is done here, which stays correct across DST and tzdata updates without encoding any of the rules. The probe runs on a slow timer and on hour boundaries; the minute tick advances the clocks in between. Config lives in ~/.config/omarchy/clockbar.json and is edited either by hand or through a click-to-edit popup with a timezone picker. Both routes write the same file and the bar reloads from it, so they cannot disagree. Failures are shown rather than papered over: an unresolvable zone renders `LABEL --:--` in the urgent colour instead of silently falling back to UTC, and an unparseable config reddens the bar's hairline and flags the local clock while the last good config keeps ticking. Ordering, formatting, day offsets and working hours live in ClockModel.js, which touches no QML types and is covered by tests that run under node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MhxtL7WzL5XmuTd8GHbcTY
139 lines
5.4 KiB
Markdown
139 lines
5.4 KiB
Markdown
# Clock bar
|
||
|
||
A second bar along the bottom edge of every screen, carrying world clocks laid
|
||
out as **one timeline**: furthest behind your local time at the far left,
|
||
furthest ahead at the far right, your own time dead centre.
|
||
|
||
```
|
||
LA 08:24 NYC 11:24 LDN 16:24 17:24 SG 23:24 TOK 00:24 +1 AUS 01:24 +1 NZ 03:24 +1
|
||
```
|
||
|
||
An Omarchy 4 shell plugin (Quickshell). It reserves its own space, so Hyprland
|
||
tiles windows above it exactly as it does for the top bar.
|
||
|
||
## What it does
|
||
|
||
- **Timeline layout.** Side and position are derived from each zone's live UTC
|
||
offset, not from config order. Reading left to right always moves forward in
|
||
time, and a DST shift re-sorts the bar on its own.
|
||
- **24-hour times**, with `+1` / `-1` against clocks on a different date to you.
|
||
- **Labels** to the left of each time. The centre clock is your local time and
|
||
carries no label.
|
||
- **Dimmed outside working hours**, evaluated in each clock's *own* timezone
|
||
(weekends included), so a glance tells you who is actually around.
|
||
- **Themed.** Colours, fonts, spacing and bar height all come from the active
|
||
Omarchy theme; it follows a theme switch with no restart.
|
||
|
||
## Install
|
||
|
||
On any Omarchy 4 machine:
|
||
|
||
```bash
|
||
omarchy plugin add <git-url-of-this-repo> --enable --yes
|
||
```
|
||
|
||
The plugin lands in `~/.config/omarchy/plugins/nerdnation.clockbar/`, and
|
||
`~/.config/omarchy/clockbar.json` is seeded on first run.
|
||
|
||
To update it later, and to remove it:
|
||
|
||
```bash
|
||
omarchy plugin update nerdnation.clockbar
|
||
omarchy plugin remove nerdnation.clockbar
|
||
```
|
||
|
||
> Plugins run as unsandboxed QML inside `omarchy-shell`, so `omarchy plugin add`
|
||
> clones disabled by default and wants you to read the code first. `--enable`
|
||
> skips that.
|
||
|
||
## Configuring it
|
||
|
||
Two routes, one source of truth — both read and write
|
||
`~/.config/omarchy/clockbar.json`, and the bar reloads from the file either way.
|
||
|
||
**Click the bar.** Opens an editor with a searchable timezone picker: add,
|
||
rename, retime and remove clocks, and set the working-hours window. Each row
|
||
shows that zone's current offset so you can see the effect before saving.
|
||
|
||
**Edit the file.** It hot-reloads on save, no restart:
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"clocks": [
|
||
{ "label": "LA", "timezone": "America/Los_Angeles" },
|
||
{ "label": "NYC", "timezone": "America/New_York" },
|
||
{ "label": "SG", "timezone": "Asia/Singapore" },
|
||
{ "label": "AUS", "timezone": "Australia/Sydney" },
|
||
{ "label": "NZ", "timezone": "Pacific/Auckland" }
|
||
],
|
||
"workingHours": { "start": 8, "end": 18, "workdays": [1, 2, 3, 4, 5] },
|
||
"dimOutsideWorkingHours": true,
|
||
"showSeconds": false,
|
||
"barSize": 0
|
||
}
|
||
```
|
||
|
||
| Key | Meaning |
|
||
|---|---|
|
||
| `clocks[].label` | Shown left of the time. Defaults to the last part of the zone name. |
|
||
| `clocks[].timezone` | Any IANA zone (`timedatectl list-timezones`). |
|
||
| `workingHours.start` / `.end` | Hours, 0–24, in each clock's own zone. A window that wraps midnight (`22`–`6`) is understood. |
|
||
| `workingHours.workdays` | Days counted as working. `0` = Sunday. |
|
||
| `dimOutsideWorkingHours` | Mute clocks outside that window. |
|
||
| `showSeconds` | `HH:MM:SS` and a per-second tick, instead of `HH:MM`. |
|
||
| `barSize` | Bar height in px. `0` follows the theme's bar height. |
|
||
|
||
There is no `left` / `right` setting: which side a clock lands on is a fact
|
||
about its offset, not a preference.
|
||
|
||
**When something is wrong**, the bar says so rather than lying. A zone tzdata
|
||
cannot resolve renders as `LABEL --:--` in the theme's urgent colour instead of
|
||
silently falling back to UTC. A `clockbar.json` that will not parse turns the
|
||
bar's top hairline red and puts `config error` beside your local time, while
|
||
the last good config keeps ticking — and the broken file is never overwritten.
|
||
|
||
## IPC
|
||
|
||
```bash
|
||
omarchy-shell nerdnation.clockbar edit # open the editor
|
||
omarchy-shell nerdnation.clockbar toggle
|
||
omarchy-shell nerdnation.clockbar refresh # re-probe timezone offsets
|
||
omarchy-shell nerdnation.clockbar reload # re-read clockbar.json
|
||
```
|
||
|
||
Bind the editor to a key in `~/.config/hypr/bindings.lua` if you want one.
|
||
|
||
## How it works, and why
|
||
|
||
Qt's QML engine ships **no `Intl`**, so there is no way to render an IANA
|
||
timezone from inside QML — the usual `Intl.DateTimeFormat({ timeZone })` route
|
||
does not exist. Rather than bundle a copy of the tz database, the plugin asks
|
||
the system's own tzdata for each zone's current UTC offset (`TZ=<zone> date +%z`)
|
||
and then does the arithmetic itself. That keeps it correct across DST changes
|
||
and tzdata updates without knowing any of the rules.
|
||
|
||
The probe is one short-lived shell, and its answer stays valid until a
|
||
transition, so it runs on a slow timer — every ten minutes, on each hour
|
||
boundary (where transitions land), and whenever the config changes. In between,
|
||
the clocks advance by arithmetic on the minute tick.
|
||
|
||
`ClockModel.js` holds all of that logic and touches no QML types, so it runs
|
||
unchanged under node:
|
||
|
||
```bash
|
||
node tests/model.test.js
|
||
```
|
||
|
||
## Layout
|
||
|
||
| File | Role |
|
||
|---|---|
|
||
| `manifest.json` | Plugin manifest — `kind: panel`, `keepLoaded` |
|
||
| `Clockbar.qml` | Entry point: the bottom layer-shell surface, one per screen |
|
||
| `Editor.qml` | The click-to-edit popup |
|
||
| `Config.qml` | `clockbar.json` — read, watch, write |
|
||
| `Zones.qml` | Timezone offset probe and the zone list for the picker |
|
||
| `ClockModel.js` | Ordering, formatting, day offsets, working hours (pure JS) |
|
||
| `tests/model.test.js` | Unit tests for the above |
|