Skip to content

About

Omarchy bar widget for NetBird — status, toggle, peers; modeled on the first-party omarchy.tailscale panel

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

NetBird for Omarchy

NetBird in the Omarchy bar, built the way the first-party Tailscale widget is built: a bar glyph that tells the daemon's state at a glance, a keyboard-driven panel with the tunnel switch in the hero, every peer with its transport, latency and routes, the network routes you can flip, profiles, SSO login when the daemon asks, and copy, SSH and ping a keystroke away. Everything it shows comes from the netbird CLI, as your own user: no sudo, no pkexec, no daemon handshake.

NetBird panel in the Omarchy bar

The bar glyph alone tells you what the tunnel is doing: connected, degraded (management or signal down), signed out, and off.

Bar glyph states: connected, degraded, needs login, disconnected

Features

  • Shows the NetBird daemon's state in the bar, with the connected/total peer count in the bar tooltip and in the hero
  • Left click opens a keyboard-friendly panel
  • Right click toggles NetBird on/off, middle click refreshes
  • Hero line carries the management host and how long the current session has left ("session expires in 1d 6h")
  • This device's FQDN, NetBird address, and relay availability on one row, with a copy menu
  • Every peer from netbird status --json, sorted by name, each with a status glyph — bright for Connected, dimmed for Connecting and Idle, crossed for Disconnected. Lazy connections leave healthy peers idle for hours, so an online-only list would look like an empty network.
  • Copy a peer's name, FQDN, or NetBird address
  • SSO login: when the daemon needs to re-authenticate, the widget runs netbird up --no-browser, picks the login URL out of its output, and hands it to omarchy-launch-browser. The device code the CLI prints is shown large in the panel with a copy button and a cancel button beside it.
  • Filter the peer list with / — matches name, FQDN, address, transport and state, and the section header counts what is showing. The filter is typed through the panel's own key handling, not a text input, so the panel never touches the clipboard
  • Tells you when the daemon itself is not running, instead of showing a stale peer list
  • Networks: every route the daemon offers, with a switch per row and select/deselect-all in the header
  • Per-peer detail: why a peer is relayed — transport and relay, the negotiated ICE pair, last handshake, transfer, latency, advertised routes
  • Expandable relay list behind the relay count, showing which one failed
  • A notice when the CLI and daemon versions disagree
  • Profiles: switch NetBird accounts, when more than one profile exists
  • SSH or ping a connected peer in a new terminal
  • One key to the admin console

Requirements

Needs Why Arch package
Omarchy with the quickshell shell Hosts the plugin; provides omarchy plugin, omarchy bar and omarchy-launch-browser omarchy on the quattro channel from 2026-10-12 or later (the panel card is built lazily through KeyboardPanel.content, which arrived then); 1.0.0 was developed against the 2026-10-15 tip
netbird CLI and a running daemon Every piece of state comes from netbird status --json; the toggle runs netbird up / netbird down netbird-bin (developed against 0.77.0 and 0.77.1)
wl-copy The copy actions on the device and peer rows wl-clipboard
omarchy-launch-browser Opens the SSO URL when the daemon needs to re-authenticate ships with omarchy

No sudo or pkexec, ever. NetBird's daemon socket (/var/run/netbird.sock) is created world-writable (srw-rw-rw-), so status, up and down all succeed as your own user. There is no equivalent of Tailscale's tailscale set --operator handshake and the widget never asks for one.

The widget reads and writes nothing outside the NetBird CLI, your clipboard, and its own entry in ~/.config/omarchy/shell.json.

Install

omarchy plugin add https://github.com/mikebenner/omarchy-netbird.git --enable

Then place it in the bar, e.g. next to the network widget:

omarchy plugin enable mikebenner.netbird --section right --before omarchy.network

Removal

omarchy plugin disable mikebenner.netbird   # keep it installed, take it off the bar
omarchy plugin remove mikebenner.netbird    # remove it entirely

Neither touches NetBird itself — the daemon, the CLI and your login stay as they are. If you had disabled the netbird-ui tray app (see below) and want it back, set Hidden=false in ~/.config/autostart/netbird.desktop, or reinstall it if you removed the file:

sed -i 's/^Hidden=true$/Hidden=false/' ~/.config/autostart/netbird.desktop

Settings

Settings live inline on the widget's entry in ~/.config/omarchy/shell.json. Change them with omarchy bar set:

Key Type Default Values What it does
refreshIntervalSec integer 30 5–3600 How often netbird status --json is polled
iconStyle enum theme theme, color Themed vector mark, or NetBird's own coloured artwork
adminConsoleUrl string (empty) any URL Where a opens. Empty derives it from the management URL
omarchy bar set mikebenner.netbird refreshIntervalSec 15
omarchy bar set mikebenner.netbird iconStyle color

Settings are read live from shell.json, so a change takes effect without restarting the shell.

IPC

omarchy-shell mikebenner.netbird open      # open the panel
omarchy-shell mikebenner.netbird close     # close the panel
omarchy-shell mikebenner.netbird toggle    # toggle the panel
omarchy-shell mikebenner.netbird refresh   # re-poll netbird status
omarchy-shell mikebenner.netbird up        # netbird up (SSO login when needed)
omarchy-shell mikebenner.netbird down      # netbird down
omarchy-shell mikebenner.netbird status    # one-line summary of the mesh

up and down answer ok, or busy when the other one is still running — the plugin's service runs at most one of them at a time, whichever monitor's bar or IPC call asked. status answers with the daemon state, this device's name and address, the peer count, and the session clock:

Connected · laptop · 100.64.0.9 · 1/4 peers · session expires in 1d 6h

Keyboard shortcuts

Inside the panel:

  • j / k or arrows: move cursor

  • enter / space: activate the current row — the switch on the hero, the copy menu on the device row, the detail on a peer row, the toggle on a network, the switch on a profile

  • c: copy the focused row's NetBird address

  • n: copy the focused row's short name

  • d: copy the focused row's FQDN

    These three act on the device row or a peer row. With the header, a network or a profile focused they do nothing, rather than copying from whichever peer the cursor last visited. The peer copy menu — which adds the public key — is on the row's copy button.

  • /: filter the peer list; type to narrow, enter or ↓ to move into the results, esc to clear and close

  • enter on a peer row: show or hide that peer's detail

  • N: jump to the NETWORKS section (space/enter toggles the focused row)

  • e: show or hide the relay list

  • s / p: SSH to, or ping, the focused connected peer in a new terminal

  • a: open the admin console in your browser

  • t: toggle NetBird

  • r: refresh status

  • esc: close the filter if it is open, else cancel a sign-in in progress, else close the panel

Replaces netbird-ui-bin

This widget is meant to take the place of the netbird-ui tray app. Nothing here uninstalls it — disable its autostart yourself once you're happy with the widget.

The autostart entry is a user file, not a packaged one. It is not launched directly, though: systemd-xdg-autostart-generator turns it into a systemd user unit at login, so the desktop file is what to edit and the unit is what to stop.

# what the generator made of it
systemctl --user list-units 'app-netbird*' --all
#   UNIT                          LOAD   ACTIVE   SUB     DESCRIPTION
#   app-netbird@autostart.service loaded <active> <state> NetBird

Stop autostarting it, by setting Hidden=true in the desktop file — the generator then skips it at the next login:

sed -i 's/^Hidden=false$/Hidden=true/' ~/.config/autostart/netbird.desktop

Removing the file (rm ~/.config/autostart/netbird.desktop) works too, but Hidden=true is reversible.

Either way the tray app already running in this session keeps running. Stop it through its unit rather than by signalling the process, so systemd does not count it as a crash:

systemctl --user stop 'app-netbird@autostart.service'

The netbird daemon and the netbird CLI are untouched — the widget needs both.

Icon

The bar shows the NetBird mark, one icon per connection state. Two styles:

  • theme (default): the mark is drawn as vector paths in the colours your Omarchy theme is using — foreground when connected, dimmed when connecting or disconnected, urgent on error — and it follows a theme switch live. Being vector, it is crisp at any bar size.
  • color: NetBird's own coloured tray artwork, exactly what netbird-ui shows.

Set it with the iconStyle setting above. Either way the icon carries the state itself; there is no crossed-out overlay drawn on top.

Widget state Icon When
connected connected daemon Connected, control plane healthy
connecting connecting daemon Connecting, a toggle still in flight, or every peer still negotiating
disconnected disconnected daemon Idle/Disconnected
error error daemon Connected but management or signal is not
needs login needs-login daemon NeedsLogin, SessionExpired or LoginFailed
daemon not running error the netbird CLI is there but the daemon is not answering
not installed disconnected, faded no netbird on PATH

In theme style the badges are drawn to mirror the official artwork: a check when connected, three dots while connecting, a cross on error or needs-login, nothing when simply disconnected.

Both the vector geometry and the coloured bitmaps are © NetBird GmbH, BSD-3-Clause, taken unmodified from netbirdio/netbird — see assets/NOTICE for the pinned commit, the exact upstream paths, checksums, and licence.

Networks

Every route the daemon offers appears under NETWORKS, one switch per row, with the selected count in the header and select-all / deselect-all beside it. A row shows its route (10.0.0.0/16) or its domains, plus how many addresses those domains resolved to.

Toggling one row uses netbird networks select -a <id>. The -a matters: upstream's select replaces the whole selection by default, so selecting a second network without it would silently deselect the first. The header's select-all deliberately omits it, because select all is special-cased upstream to mean "accept everything, including new networks".

NetBird has no separate exit-node concept — a default route is simply a network whose range is 0.0.0.0/0.

The section is hidden when the daemon reports no networks, and the list is only read while the panel is open.

Profiles

A PROFILES section appears when the daemon knows more than one profile — with a single profile there is nothing to choose, so it stays hidden.

The list comes from netbird profile list --show-id, and picking a row runs netbird profile select <id>. Selecting by ID rather than by display name is deliberate: NetBird names are free-form and need not be unique, so two profiles can be called the same thing, and only the ID says which account a row is. On a CLI too old to know --show-id the widget drops the flag and falls back to the two-column table; there the display name is the only handle available, and two rows that share one are shown but not selectable — the panel says to run netbird profile list --show-id instead of guessing which account you meant.

Both the PROFILES and NETWORKS sections disappear while a switch is in flight and come back when their lists have been re-read: the networks of the account you just left are not selectable, so they are not left on screen to be clicked.

Switching re-cycles the engine, so expect a brief reconnect, and expect the target profile to ask for a login if its session has expired. The widget does not treat that as an error: the ordinary needs-login state carries it, and the sign-in flow is the same one described above.

Admin console

a, or the button in the PEERS header, opens the admin console. The URL is derived from the management URL, which is right for most self-hosted deployments, where the dashboard is served from the management host. NetBird Cloud is recognised as a special case — a management host of api.netbird.io maps to https://app.netbird.io, because Cloud serves its API and its dashboard from different hosts — so it needs no configuration either.

Set adminConsoleUrl explicitly only for a split dashboard host of your own. Only the default :443 is dropped when deriving; a management URL on another port keeps it.

When the daemon is not running

netbird status does not fail when the daemon is stopped — it retries the socket forever, printing gRPC warnings and never exiting. Every call the widget makes is therefore wrapped in timeout, and a call that times out or that reports it could not dial the daemon socket puts the widget into a daemon not running state, distinct from "disconnected":

  • the bar icon shows the error treatment
  • the hero reads "NetBird daemon is not running"
  • the peer list is cleared rather than left showing what was true before
  • the toggle is disabled — there is nothing for it to talk to
  • polling backs off, but never below your refresh interval: at the 30 s default that is 30 s, 30, 30, then 40 and a 60 s cap; only a shorter interval sees the earlier 5/10/20 steps. It snaps straight back on the first poll that returns a real status document — a clean exit carrying unusable output does not count as recovery.

The panel points you at systemctl status netbird. The widget never starts or stops the daemon itself.

Limitations

Deliberate edges, each chosen over guessing:

  • SSH and ping are offered only for a peer whose address parses as an IP or a DNS name. A peer's address is whatever the management server put in the daemon's status output, so it is not the local user's input, and ssh reads an argument beginning with - as an option however it is passed — an address of -oProxyCommand=… would run that command. Both defences are kept, because either alone is a single point of failure: every launch puts -- immediately before the address, and an address that is not an IPv4, IPv6 or LDH domain name builds no command at all. A peer described that way shows no SSH or ping button, and s / p do nothing on it. Deliberately strict in the safe direction: leading-zero octets, a zone id (fd00::1%eth0), a bare single-label hostname, and anything carrying @, / or a port are all refused rather than passed on.

  • Every command built from a value the daemon reported ends its option parsing with --. The peer address is not the only such value: a network id comes from netbird networks list, whose rows the management server supplies, and netbird's own flag parser reads an operand beginning with - as a flag wherever it sits — on 0.77.0 netbird networks deselect --help prints help rather than looking for a route by that name, so an id of --daemon-addr=tcp://host:port would have pointed the command at another daemon. -- is therefore in the networks select and deselect commands and in profile select too, even though a profile is local rather than management-supplied — one rule for every builder beats reasoning about each one separately whenever the threat model shifts. For the same reason the admin console link is only ever built for an http/https management URL: it is handed to a browser, and a management server answering file://localhost would otherwise have the plugin open a local file.

  • Retained process output is capped per stream: whole lines are dropped from the front, and every failure message that reaches you — a status error, a failed up/down, a networks command, a profile switch — ends with (output truncated) when lines were dropped, so a shortened message is never presented as the whole story. One thing that cap cannot reach: until a newline arrives, Quickshell buffers the growing line inside its own process object, and no QML-side setting bounds that. A command emitting one endless unterminated line would grow that buffer for as long as it runs — which is why the real ceiling is time, not memory: every call is wrapped in timeout -k 2 8 (timeout -k 5 130 for the SSO login), so the process is killed seconds in.

  • netbird profile list has no --json either, so its table is parsed as text against what Go's text/tabwriter emits — measured in code points, the way tabwriter measured when it laid the table out, so names carrying emoji or other astral-plane characters are read correctly. The table is accepted or rejected whole: every line under the header must be a row that printer could have produced, in its cell padding, in its ID (letters, digits, _, -, never a space, never longer than the eight characters --show-id prints) and in its column widths, which tabwriter always sets to the widest cell plus two. One line that does not fit voids the list rather than being skipped — a warning printed under a real table would otherwise add a clickable row that is not a profile. A voided read changes nothing on screen: the previous list stays until a good one arrives. Without an ID column — an older CLI — the name's own shape is the only thing marking where a row starts, so there a profile named with a leading space voids the table instead of appearing. NetBird trims profile names when it stores them, so that name cannot occur in practice.

  • The management-host check that stops the SSO flow opening the daemon's own endpoint compares ASCII hostnames. There is no IDNA canonicalisation, so an internationalised management URL and its punycode spelling (bücher.example vs xn--bcher-kva.example) do not compare equal.

  • netbird networks list has no --json, so its output is parsed as text against the format NetBird 0.77.0 and 0.77.1 print (the two are identical here). A row ships only when its block carries an id, a Network: or Domains: line, and a Status: value the printer actually emits — so a release that renames those fields shows an empty NETWORKS section rather than rows that lie.

  • When netbird status --json prefixes its document with chatter, the recovery sweep tries at most 32 candidate object starts. Brace-bearing prose (WARNING grpc target {Addr:"/run/netbird.sock"}) is skipped without spending that budget, but a document preceded by more than 32 genuine JSON fragments is reported as a parse error rather than recovered. It fails safely: the widget shows a status error and keeps the CLI's own message, never an invented state.

What was dropped from the Tailscale widget

NetBird has no analogue for these, so they are simply gone: exit nodes, Mullvad regions, Taildrop file sending, and the operator authorization row.

Tests

Model.js holds all the parsing and formatting and is loadable by both QML (import "Model.js" as Model) and Node.

node --test test/
bash test/smoke.sh   # loads Service.qml and Panel.qml in a throwaway quickshell against the installed kit

Fixtures under test/fixtures/ are synthetic netbird status --json documents — example hostnames, CGNAT addresses, and fake keys. No real mesh data is committed.

License

MIT — see LICENSE.

About

Omarchy bar widget for NetBird — status, toggle, peers; modeled on the first-party omarchy.tailscale panel

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages