Omahub
← All plugins
H

SL Departures

by Henrik Sjökvist

Live departure times for a Stockholm (SL) bus stop, metro, tram, train, or ferry berth.

Security review

Potentially dangerous behavior detected · 2 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
2666054
Scanned
4 days ago
  • high decode_and_execute bin/sl-sites:67

    Interpreter evaluated with an execution builtin.

    python3 -c '
  • Docs external_hosts README.md:32

    Downloads or connects to an external HTTP(S) host.

    git clone https://github.com/henrrrik/sl-departures-widget.git

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

None
AI risk level
None
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
2666054
Reviewed
4 days ago

The deterministic scan's high finding is a false positive: the `python3 -c` invocations in `bin/sl-sites` are static literal scripts for JSON parsing and a hardened cache read, not dynamic code execution, and the README `git clone` line is documentation only. The plugin only fetches from SL's public transport API, writes to its own config/cache paths, and its install script is scoped to the plugin directory. No malicious, destructive, or hidden behavior was found.

How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/henrrrik/sl-departures-widget --enable
Widgets #bar #quickshell

SL Departures — an Omarchy bar widget

Live departure times for a Stockholm public transport stop, in the Omarchy bar.

 18 3′ · 19 7′

Click it and you get the full board: line, destination, berth, minutes left, cancellations, and any service messages for the station — plus a searchable stop picker, so you never have to look up a stop id by hand.

<img src="preview.png" alt="The widget in the bar, and the popup departure board for T-Centralen" width="420">

It reads SL's open Transport API, which needs no account and no API key.

Install

omarchy plugin add https://github.com/henrrrik/sl-departures-widget.git
omarchy plugin enable io.github.henrrrik.sl-departures --section center

omarchy plugin add clones into ~/.config/omarchy/plugins/io.github.henrrrik.sl-departures/ — named after the manifest id, not the repository — so that is the id every omarchy plugin and omarchy bar command wants.

Or, from a local checkout:

git clone https://github.com/henrrrik/sl-departures-widget.git
cd sl-departures-widget
./install
omarchy plugin enable io.github.henrrrik.sl-departures --section center

Later updates:

omarchy plugin update io.github.henrrrik.sl-departures    # installed with `plugin add`
git pull && ./install                  # installed from a checkout

Then click the widget and pick your stop. Nothing else is required — the picker writes the stop into ~/.config/omarchy/shell.json for you.

Uninstall

omarchy plugin remove io.github.henrrrik.sl-departures

This disables the widget and deletes ~/.config/omarchy/plugins/io.github.henrrrik.sl-departures/. The only other things the plugin ever creates are its own entry in ~/.config/omarchy/shell.json (removed by the command above) and the stop-list cache at ~/.cache/omarchy/sl-sites.json, which is safe to delete.

Dependencies

Everything the widget needs ships with a stock Omarchy install: curl, bash, and python3 at runtime (python performs the descriptor-validated, size-bounded read of the stop-list cache), and rsync (with a plain-cp fallback) for the local ./install script. There are no QML dependencies beyond the Omarchy shell itself.

Using it

Action What it does
Left click Open / close the departure board
Right click Refresh now
Middle click Open the stop picker
r Refresh (board open)
s Open the stop picker (board open)
j / k, arrows Scroll the board
Esc Close, or leave the picker

The board refreshes every 30 seconds and counts down every 15, so the minutes stay honest between fetches. A failed refresh leaves the last board on screen rather than blanking it. If SL answers with a rate limit (HTTP 429), the widget stops fetching for a minute, doubling the pause on each further 429 up to ten minutes, and resumes normally after the next successful fetch.

Configuration

Settings live inline on the widget's entry in ~/.config/omarchy/shell.json:

{
  "id": "io.github.henrrrik.sl-departures",
  "siteId": 9192,
  "siteName": "Slussen",
  "transport": "METRO",
  "direction": 1,
  "lines": "13, 14",
  "walkMinutes": 5,
  "barCount": 2,
  "panelCount": 12,
  "forecastMinutes": 90,
  "refreshIntervalSec": 30,
  "barFormat": "{line} {wait}",
  "showIcon": true
}
Key Default What it does
siteId – The SL site to watch. Set by the picker; sl-sites <name> finds it too.
siteName – Display name. On its own (no siteId) it is resolved to an id on first run.
transport all BUS, METRO, TRAM, TRAIN, SHIP, or FERRY.
direction both 1 or 2 — SL's direction codes for the stop.
lines all Comma-separated line designations, e.g. "4, 74".
walkMinutes 0 Hides departures leaving sooner than this, so the board only shows what you could still catch.
barCount 2 Departures shown in the bar.
panelCount 12 Departures shown in the popup.
forecastMinutes 90 How far ahead to ask for.
refreshIntervalSec 30 Seconds between fetches (minimum 15).
barFormat {line} {wait} Template per departure. Tokens: {line} {wait} (4′ / now) {min} (bare number) {clock} {destination} {icon}.
showIcon true Mode icon in front of the bar label.

The widget allows multiple instances, so a second entry with a different siteId gives you home and work side by side. Instances in the same shell share one fetch per distinct stop/filter combination, so duplicates cost nothing extra. Put multiple instances in different bar sections: the shell's live settings propagation is per-section, and two identically-named widgets in one section can briefly mirror each other's settings changes until a restart.

Finding a stop id from the terminal

bin/sl-sites gullmars              # 9189  Gullmarsplan
bin/sl-sites --departures 9189     # what is leaving right now
bin/sl-sites --refresh             # re-download the stop list

The stop list is cached in ~/.cache/omarchy/sl-sites.json and shared with the widget, which refreshes it weekly on its own.

How it works

File Role
Model.js All parsing, filtering, and formatting. Pure functions, no QML types.
SlHub.qml Singleton: the shared fetch loop, clock anchoring, and the site directory.
Service.qml Per-instance subscriber: config, filtered rows, name resolution.
Panel.qml The bar button and the popup.
bin/sl-sites Terminal helper for looking up stop ids.

Shared fetching

All widget instances in a shell process — including the copy each monitor's bar surface mounts — share one SlHub singleton. Instances subscribe to their departures URL; the hub runs a single fetch loop per distinct URL, parses each payload once, and keeps the parsed departures across failed refreshes so the board keeps counting down through an outage instead of freezing. The site list is likewise parsed once per process, only when the picker or a name lookup actually needs it.

Clock anchoring

SL returns departure times as naive Europe/Stockholm wall-clock strings, so subtracting the machine's clock is only correct on a machine set to Stockholm. Instead the widget anchors itself to SL's own clock: a departure that reports both "4 min" and an absolute expected time pins down what "now" was when the server answered. The median across every such departure becomes the offset applied to Date.now(), and it is kept across fetches — so the countdown is right on a laptop in any timezone, and keeps ticking between fetches.

Development

The plugin is a plain directory of QML plus a manifest. ./install mirrors the checkout into ~/.config/omarchy/plugins/io.github.henrrrik.sl-departures/ and reloads the shell; it copies everything but the repository's own scaffolding, so a new QML file is never left behind by a stale file list.

Editing files in the installed copy hot-reloads, but the QML engine can serve a cached compilation unit for nested components — omarchy restart shell is the reliable way to pick up a change. Installing via a symlink does not work at all: the shell's file watcher does not follow symlinks.

Two non-obvious pieces of wiring: the qmldir declaring the SlHub singleton replaces QML's automatic same-directory type discovery, so every sibling type a file uses must be listed in it; and the settings defaults live in Model.resolveConfig alone — the manifest's schema mirrors them for the settings UI, but resolveConfig is the authority at runtime.

Check the manifest against Omarchy's schema before pushing:

omarchy plugin validate "$PWD"

Model.js is deliberately free of QML types so it can be exercised directly:

sed '1d' Model.js > /tmp/model.js   # drop the .pragma line
node -e 'const M = require("/tmp/model.js"); ...'

License

MIT — see LICENSE.

Departure data comes from SL via Trafiklab; this project is not affiliated with or endorsed by SL or Region Stockholm.