A custom Wayle bar module for managing your Tailscale VPN directly from the status bar. It shows connection state at a glance and lets you connect, disconnect, switch exit nodes, and copy your host IP without leaving the bar.
- Always-visible status indicator — a shield icon that reflects the current
BackendState(connected, routing through an exit node, starting, or disconnected). When Tailscale is down it also shows anofflabel, so a dropped connection is impossible to miss. - Rich tooltip — hover to see your Tailscale IP, the active exit node, the number of online peers, and any health warnings reported by the daemon.
- Left-click — toggle the connection (
tailscale up/tailscale down). - Right-click — open a rofi menu to connect/disconnect or pick an exit node (including Mullvad nodes if enabled).
- Middle-click — copy your host's Tailscale IP to the clipboard, with a desktop notification for confirmation.
The module is a set of shell scripts driven by Wayle's custom-module system. It relies on the following command-line tools being available in the session PATH:
| Tool | Provides | Used for |
|---|---|---|
tailscale |
Tailscale CLI + daemon | Querying and controlling the VPN |
jq |
jq |
Parsing tailscale status --json |
wl-copy |
wl-clipboard |
Copying the IP (Wayland clipboard) |
notify-send |
libnotify |
The copy confirmation popup |
rofi |
rofi (Wayland build) |
The exit-node picker menu |
Wayle itself must be installed and running. See the Wayle getting-started guide if you don't have it set up yet.
| File | Role |
|---|---|
scripts/tailscale-status.sh |
Poll emitter. Prints one JSON line describing the current state (icon key, label, CSS class, tooltip) for the bar to render. |
scripts/tailscale-toggle.sh |
Left-click handler. Connects or disconnects depending on the current state. |
scripts/tailscale-menu.sh |
Right-click handler. Builds and shows the rofi menu, then applies the chosen action. |
scripts/tailscale-copyip.sh |
Middle-click handler. Copies the host IP and fires a notification. |
config.toml |
The module declaration to paste into ~/.config/wayle/config.toml (manual, non-Nix setups). |
default.nix |
The same module declaration expressed as a NixOS module for services.wayle, for declarative setups. |
The four scripts must be placed in ~/.config/wayle/scripts/ regardless of which installation method you use below — default.nix only declares the module configuration, it does not deploy the scripts.
mkdir -p ~/.config/wayle/scripts
# copy the four .sh files into ~/.config/wayle/scripts/
chmod +x ~/.config/wayle/scripts/tailscale-*.shBy default tailscale up, down, and set require root. Register your user as the Tailscale operator once, so the click actions run without privilege escalation:
sudo tailscale set --operator=$USERPick whichever matches your setup — both produce the same configuration.
Option A — manual config.toml edit
Copy the contents of config.toml into ~/.config/wayle/config.toml:
[[modules.custom]]
id = "tailscale"
interval-ms = 3000
command = "~/.config/wayle/scripts/tailscale-status.sh"
left-click = "~/.config/wayle/scripts/tailscale-toggle.sh"
right-click = "~/.config/wayle/scripts/tailscale-menu.sh"
middle-click = "~/.config/wayle/scripts/tailscale-copyip.sh"
on-action = "~/.config/wayle/scripts/tailscale-status.sh"
format = "{{ label }}"
icon-map = { up = "ld-shield-check-symbolic", exit = "ld-globe-symbolic", starting = "ld-shield-symbolic", down = "ld-shield-off-symbolic" }If the tilde paths don't resolve on your setup, replace ~ with the absolute path (for example $HOME/.config/wayle/scripts/...).
Option B — NixOS module (default.nix)
If your NixOS configuration already exposes the services.wayle option (i.e. Wayle is managed as a NixOS module), import default.nix instead of hand-editing config.toml — see NixOS notes below for details and caveats.
Reference the module in a bar layout as custom-tailscale:
[[bar.layout]]
monitor = "*"
right = ["custom-tailscale", "volume", "network", "battery"]Wayle reloads on save, so the module should appear as soon as you write the file.
The four icons in icon-map (ld-shield-check-symbolic, ld-globe-symbolic, ld-shield-symbolic, ld-shield-off-symbolic) need to exist in your local icon theme before Wayle can render them:
wayle icons syncThis scans your config for every referenced icon name and downloads whatever is missing. Without it, the module still works but shows an empty box instead of an icon — see Customization for details and Troubleshooting if sync reports a failure.
The command script runs every interval-ms (3 s) and emits a JSON object. Wayle maps its fields onto the widget:
altselects the icon fromicon-map(up,exit,starting,down).labelis the visible text, rendered throughformat = "{{ label }}".classadds CSS classes to the widget (tailscale-connected,tailscale-exit,tailscale-disconnected, pluswarningwhen the daemon reports a health issue).tooltipis shown on hover.
Each click handler runs its own script, and on-action re-runs the status emitter immediately afterwards so the bar updates without waiting for the next poll tick.
| Action | Result |
|---|---|
| Hover | Show IP, exit node, peer count, and health details |
| Left-click | Connect if down, disconnect if up |
| Right-click | Open the rofi menu (connect/disconnect + exit-node selection) |
| Middle-click | Copy the host's Tailscale IP and show a notification |
On NixOS, install the dependencies declaratively rather than imperatively. Because Wayle runs inside your session, it inherits this PATH:
# Home Manager
home.packages = with pkgs; [
jq # parses the Tailscale status
wl-clipboard # provides wl-copy
libnotify # provides notify-send
rofi-wayland # rofi, Wayland build (see below)
];
# Tailscale itself is best enabled as a service (installs the CLI and daemon):
services.tailscale.enable = true;Two NixOS-specific details:
- Use
rofi-wayland, notrofi. Under Hyprland (Wayland), the plainrofipackage is the X11 build and only works through XWayland. Therofi-waylandpackage still provides a binary namedrofi, so no script change is needed — it just has to be the version in yourPATH. (Custom rofi plugins don't work with the Wayland port without an overlay, but the simple-dmenumenu used here is unaffected.) - The
tailscale set --operator=$USERstep still applies once the service is running.
default.nix declares the same [[modules.custom]] block from step 3 through the services.wayle NixOS option, for setups where Wayle itself is configured as a NixOS module (rather than through ~/.config/wayle/config.toml directly):
{ pkgs, lib, ... }:
{
services.wayle = {
enable = true;
settings = {
bar.layout = [
{
monitor = "DP-1";
show = true;
right = [ "battery" "network" "custom-tailscale" "bluetooth" "volume" "systray" ];
}
];
modules.custom = [
{
id = "tailscale";
interval-ms = 3000;
command = "~/.config/wayle/scripts/tailscale-status.sh";
left-click = "~/.config/wayle/scripts/tailscale-toggle.sh";
right-click = "~/.config/wayle/scripts/tailscale-menu.sh";
middle-click = "~/.config/wayle/scripts/tailscale-copyip.sh";
on-action = "~/.config/wayle/scripts/tailscale-status.sh";
format = "{{ label }}";
icon-map = {
up = "ld-shield-check-symbolic";
exit = "ld-globe-symbolic";
starting = "ld-shield-symbolic";
down = "ld-shield-off-symbolic";
};
}
];
};
};
}To use it:
-
Copy
default.nixinto your NixOS configuration (for examplemodules/wayle-tailscale.nix), then import it:# configuration.nix { imports = [ ./modules/wayle-tailscale.nix ]; }
-
Merge the
bar.layoutblock into your existing layout instead of copy-pasting it verbatim — the example above defines a full layout (monitor = "DP-1", a specific set of modules) that will overwrite any layout you already declare elsewhere inservices.wayle.settings. Only the"custom-tailscale"entry and themodules.customblock are specific to this module. -
default.nixonly sets configuration — it does not installjq,wl-clipboard,libnotify,rofi-wayland, or the four scripts. Keep those declared separately (home.packagesabove, and the scripts copied per step 1 or managed viahome.file."/.config/wayle/scripts/...".source). -
services.waylemust already exist as an option in your configuration (i.e. you have the Wayle NixOS/Home Manager module imported). If you configure Wayle purely through~/.config/wayle/config.toml, use Option A instead. -
rebuild switch(or your usualhome-manager switch/nixos-rebuild switch) to apply, then reload Wayle.
The middle-click popup is emitted with notify-send and rendered by whichever notification daemon owns the org.freedesktop.Notifications D-Bus name. Wayle ships its own notification daemon, so if Wayle is the active one the popup is drawn natively by Wayle.
Only one notification daemon can own that D-Bus name at a time. If another daemon (mako, dunst, swaync, …) is running, it will handle the popup instead. Check with:
pgrep -l 'mako|dunst|swaync'If repeated copies stack multiple bubbles instead of replacing one, swap the hint in tailscale-copyip.sh from x-canonical-private-synchronous to x-dunst-stack-tag (the exact hint honored depends on the active daemon).
-
Icons. The
ld-*-symbolicnames inicon-mapare Lucide icons (theld-prefix), one of Wayle's five built-in icon sources (tb-Tabler,tbf-Tabler Filled,si-Simple Icons,md-Material,ld-Lucide). Icon names resolve against your local icon theme at~/.local/share/wayle/icons/— they aren't fetched on the fly, so a name has to be installed there before it will render. Run:wayle icons sync
This reads your effective config, collects every icon name it references (including the four names in this module's
icon-map), and downloads whatever is missing from the matching CDN source. It reports each icon as installed, failed, or skipped — imported icons (cm-prefix) are skipped because their source SVG only exists on the machine that imported them viawayle icons import, not on the CDN. Re-run it after changingicon-mapor moving your config to a new machine.If you'd rather use a different icon, swap the name in
icon-mapand re-runwayle icons sync(orwayle icons install <source> <slug>for a one-off fetch) — see Wayle's Custom icons guide. -
Colors. Because the scripts emit CSS classes, you can style each state from a Wayle custom stylesheet, for example:
.tailscale-disconnected { color: #f7768e; } .tailscale-exit { color: #7aa2f7; } .warning { color: #e0af68; }
-
Label. By default the bar shows nothing when connected without an exit node, the exit node's short name when routing, and
offwhen disconnected. Adjust thelabelbranches intailscale-status.shto taste.
- Nothing happens on click / permission errors — make sure you ran
sudo tailscale set --operator=$USERand re-checked withtailscale status. - The rofi menu doesn't appear — confirm you're using the Wayland build (
rofi-waylandon NixOS) and thatrofiis in Wayle'sPATH. - No copy notification — verify
notify-sendexists (libnotify) and that a notification daemon is running (see Notifications). - Icon shows an empty square — the icon named in
icon-maphasn't been downloaded to~/.local/share/wayle/icons/yet. Runwayle icons syncto fetch every icon referenced in your config (see Customization). If it reports the icon as failed, double-check the name is spelled correctly and exists in its source set, then replace it inicon-map. - Module doesn't render at all — run Wayle in the foreground (
wayle panel restart, then watch the logs) and confirm the status script prints valid JSON:~/.config/wayle/scripts/tailscale-status.sh.
The exit-node picker uses rofi because it works on any Wayle version and gives a flexible, action-per-item menu. Recent Wayle versions also added native dropdown support for custom modules, which renders inside Wayle's own themed popup. If you'd rather use that, inspect the dropdown fields available in your installed version with:
wayle config default --stdout | grep -iA5 dropdown
grep -i dropdown ~/.config/wayle/schema.jsonBoth are generated from your installed binary, so they match your exact version.