feat(menu): VPN setup & management submenu (WireGuard/OpenVPN/Tailscale)
A dedicated System → VPN flow (nomarchy-vpn) past the wifi picker: - NetworkManager VPN/WireGuard connections shown ● active / ○ inactive, toggled up/down on select (networkmanager-group users need no sudo). - Import a *.conf / *.ovpn via the fd picker → nmcli connection import, type chosen by extension. networkmanager-openvpn ships system-side (networking.networkmanager.plugins, mkDefault) so the openvpn type is available; WireGuard is native to NetworkManager. - Tailscale block, self-gated on the CLI (= nomarchy.services.tailscale): read-only status + up/down + exit-node; privileged actions go through a sudo terminal (the snapshot pattern), no operator wiring. Waybar custom/vpn shield (nomarchy-vpn-status): self-hides unless a NM tunnel or Tailscale is up; @good tone; click opens the submenu. Wired into the generated bar and both summer whole-swaps. Decided import-first; creation deferred to nm-connection-editor. ROADMAP marked shipped. Eval + home/system builds green; both scripts pass bash -n. Pending an on-machine check (live nmcli import/connect + Tailscale paths). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,32 +97,36 @@ how to override it. Items marked ✓ are shipped.
|
||||
UI (an unthemed browser page). Validated: the generated menu script's
|
||||
`bash -n` build check passes, and an eval confirms printing-on puts
|
||||
system-config-printer in `systemPackages`. Pending an on-machine check.
|
||||
- **VPN setup & management menu:** a dedicated **System → VPN** flow that goes
|
||||
past today's Network module (`networkmanager_dmenu` only connects/disconnects
|
||||
*existing* VPN connections) to a guided **setup + management** surface across
|
||||
the three common kinds:
|
||||
- ✓ **VPN setup & management menu:** a dedicated **System → VPN** flow
|
||||
(`nomarchy-vpn`, `modules/home/rofi.nix`) that goes past the Network module
|
||||
(`networkmanager_dmenu` only connects/disconnects *existing* VPNs) to a guided
|
||||
setup + management surface across the three common kinds:
|
||||
- **WireGuard:** import a `.conf` into NetworkManager (`nmcli connection import
|
||||
type wireguard file …` — NM handles wg tunnels natively, no extra plugin)
|
||||
and toggle it up/down.
|
||||
- **OpenVPN:** import an `.ovpn` (`nmcli connection import type openvpn file …`),
|
||||
which needs the `networkmanager-openvpn` plugin shipped — gate the import
|
||||
entry on it (the wireguard path has no such dependency).
|
||||
- **Tailscale:** `tailscale up/down`, status, and **exit-node** selection — it
|
||||
lives outside NetworkManager, so the menu drives the `tailscale` CLI
|
||||
directly, self-gated on `nomarchy.services.tailscale` (already an opt-in
|
||||
service) like the other hardware-gated System entries.
|
||||
Shape: a `nomarchy-menu vpn` rofi submenu under **System** (Connect/Disconnect
|
||||
with each connection's live up/down state like the wifi picker · Import config
|
||||
via the existing Files/`fd` picker pattern · the Tailscale block when present),
|
||||
ending in `↩ Back`. A self-gating Waybar `custom/vpn` indicator (shield glyph
|
||||
while a tunnel or Tailscale is up) for parity with the other status modules,
|
||||
added to the summer whole-swaps too. Secrets stay in the connection manager's
|
||||
own store (NetworkManager / Tailscale state) — no new secret manager (cf. the
|
||||
deferred rofi-rbw/pass note above). Open question: how much in-menu *creation*
|
||||
(vs import) to support — a from-scratch WireGuard keypair/peer editor is a lot
|
||||
of rofi surface, so likely import-first, deferring creation to
|
||||
`nm-connection-editor`. Complements the existing Network item rather than
|
||||
replacing it.
|
||||
type wireguard file …` — NM handles wg tunnels natively, no plugin) and
|
||||
toggle it up/down.
|
||||
- **OpenVPN:** import an `.ovpn` (`nmcli connection import type openvpn file …`);
|
||||
the `networkmanager-openvpn` plugin ships system-side
|
||||
(`networking.networkmanager.plugins`, mkDefault) so the openvpn type is
|
||||
available — import type is chosen by file extension.
|
||||
- **Tailscale:** status (read-only, no privilege) + `up`/`down` + **exit-node**
|
||||
selection. It lives outside NetworkManager, so the menu drives the
|
||||
`tailscale` CLI directly, **self-gated** on the CLI being present
|
||||
(= `nomarchy.services.tailscale`). Privileged actions go through a terminal +
|
||||
`sudo` (the snapshot-tool pattern) — no operator wiring.
|
||||
Shape: a `nomarchy-menu vpn` rofi submenu under **System** — NM
|
||||
VPN/WireGuard connections shown ● active / ○ inactive and toggled on select
|
||||
(networkmanager-group users need no sudo), Import via the Files/`fd` picker
|
||||
(`*.conf`/`*.ovpn`), the Tailscale block when present — ending in `↩ Back`. A
|
||||
self-gating Waybar **`custom/vpn`** shield (`nomarchy-vpn-status`: shown only
|
||||
while a NM tunnel or Tailscale is up; `@good` tone; click opens the submenu),
|
||||
wired into the generated bar **and the summer whole-swaps**. Secrets stay in
|
||||
the connection manager's own store (NetworkManager / Tailscale) — no new secret
|
||||
manager (cf. the deferred rofi-rbw/pass note above). **Decided: import-first**
|
||||
— a from-scratch WireGuard keypair/peer editor is too much rofi surface, so
|
||||
creation is deferred to `nm-connection-editor`. Eval + build green; **pending
|
||||
an on-machine check** (the nmcli import/up-down + Tailscale paths need a live
|
||||
session with real configs). Remaining (optional): operator-set Tailscale so its
|
||||
toggles skip the sudo terminal; richer exit-node display (country/city).
|
||||
- **Theme parity with legacy:** summer-day/night now carry their legacy
|
||||
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
|
||||
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
|
||||
|
||||
@@ -246,6 +246,11 @@ ${themeRows}
|
||||
# its config from xdg.configFile below (told to drive rofi).
|
||||
exec networkmanager_dmenu ;;
|
||||
|
||||
vpn)
|
||||
# VPN setup + management (nomarchy-vpn): NM VPN/WireGuard connect +
|
||||
# config import + Tailscale. Distinct from `network` (the wifi picker).
|
||||
exec nomarchy-vpn ;;
|
||||
|
||||
bluetooth)
|
||||
# blueman-manager GUI (services.blueman.enable, system-side).
|
||||
exec blueman-manager ;;
|
||||
@@ -373,6 +378,7 @@ ${themeRows}
|
||||
bats=(/sys/class/power_supply/BAT*)
|
||||
choice=$( {
|
||||
row "Network" network-wireless
|
||||
row "VPN" network-vpn
|
||||
row "Bluetooth" bluetooth
|
||||
[ -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ] \
|
||||
&& row "Audio" audio-volume-high
|
||||
@@ -395,6 +401,7 @@ ${themeRows}
|
||||
case "$choice" in
|
||||
"$BACK") exec "$0" ;;
|
||||
*Network*) exec "$0" network ;;
|
||||
*VPN*) exec "$0" vpn ;;
|
||||
*Bluetooth*) exec "$0" bluetooth ;;
|
||||
*Audio*) exec "$0" audio ;;
|
||||
*Printers*) exec "$0" printers ;;
|
||||
@@ -427,15 +434,104 @@ ${themeRows}
|
||||
esac ;;
|
||||
|
||||
*)
|
||||
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|printers|capture|keybinds|ask|dnd|nightlight|autotimezone|snapshot]" >&2
|
||||
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|printers|capture|keybinds|ask|dnd|nightlight|autotimezone|vpn|snapshot]" >&2
|
||||
exit 64 ;;
|
||||
esac
|
||||
'';
|
||||
|
||||
# VPN setup + management — a dedicated System submenu (and the custom/vpn
|
||||
# Waybar indicator's click target). Goes past networkmanager_dmenu (which
|
||||
# only connects existing VPNs) to importing configs and driving Tailscale:
|
||||
# · NetworkManager VPN/WireGuard connections, ● active / ○ inactive, toggled
|
||||
# up/down (networkmanager-group users need no sudo).
|
||||
# · Import a WireGuard .conf or OpenVPN .ovpn (nmcli import; OpenVPN needs the
|
||||
# networkmanager-openvpn plugin, shipped system-side).
|
||||
# · Tailscale (self-gated on the CLI being present = nomarchy.services.tailscale):
|
||||
# status is read-only; up/down/exit-node need root, so they go via a
|
||||
# terminal + sudo (the snapshot-tool pattern), no operator wiring needed.
|
||||
# Secrets stay in NetworkManager's / Tailscale's own store — no new manager.
|
||||
nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" ''
|
||||
set -u
|
||||
BACK="↩ Back"
|
||||
|
||||
conn_rows() {
|
||||
nmcli -t -f NAME,TYPE,STATE connection show 2>/dev/null \
|
||||
| awk -F: '$2=="vpn"||$2=="wireguard"{ printf "%s %s\n", ($3=="activated"?"●":"○"), $1 }'
|
||||
}
|
||||
|
||||
toggle_conn() {
|
||||
mark=''${1%% *}; name=''${1#* }
|
||||
if [ "$mark" = "●" ]; then
|
||||
nmcli connection down "$name" >/dev/null 2>&1 \
|
||||
&& notify-send "VPN" "Disconnected $name" || notify-send "VPN" "Could not disconnect $name"
|
||||
else
|
||||
nmcli connection up "$name" >/dev/null 2>&1 \
|
||||
&& notify-send "VPN" "Connected $name" || notify-send "VPN" "Could not connect $name (auth or bad config?)"
|
||||
fi
|
||||
}
|
||||
|
||||
import_config() {
|
||||
file=$( { fd -e conf -e ovpn . "$HOME" --type f --hidden \
|
||||
--exclude .git --exclude .cache 2>/dev/null \
|
||||
| sed "s|^$HOME/||"; printf '%s\n' "$BACK"; } \
|
||||
| rofi -dmenu -p "Import VPN config" ) || return
|
||||
{ [ -z "$file" ] || [ "$file" = "$BACK" ]; } && return
|
||||
case "$file" in *.ovpn) type=openvpn ;; *) type=wireguard ;; esac
|
||||
if nmcli connection import type "$type" file "$HOME/$file" >/dev/null 2>&1; then
|
||||
notify-send "VPN" "Imported $type config: $(basename "$file")"
|
||||
else
|
||||
notify-send "VPN" "Import failed ($type plugin missing, or bad file?)"
|
||||
fi
|
||||
}
|
||||
|
||||
ts_state() { tailscale status --json 2>/dev/null | jq -r '.BackendState // "Unknown"'; }
|
||||
ts_exit_node() {
|
||||
node=$( { printf 'none\n'; tailscale exit-node list 2>/dev/null | awk 'NR>1 && $2 {print $2}'; \
|
||||
printf '%s\n' "$BACK"; } | rofi -dmenu -p "Exit node" ) || return
|
||||
{ [ -z "$node" ] || [ "$node" = "$BACK" ]; } && return
|
||||
[ "$node" = none ] && node=""
|
||||
${cfg.terminal} -e sudo tailscale set --exit-node="$node"
|
||||
}
|
||||
tailscale_menu() {
|
||||
while :; do
|
||||
st=$(ts_state)
|
||||
choice=$( {
|
||||
if [ "$st" = Running ]; then printf 'Disconnect\n'; else printf 'Connect\n'; fi
|
||||
printf 'Exit node…\n'
|
||||
printf '%s\n' "$BACK"
|
||||
} | rofi -dmenu -p "Tailscale ($st)" ) || return
|
||||
case "$choice" in
|
||||
"$BACK"|"") return ;;
|
||||
Connect) ${cfg.terminal} -e sudo tailscale up ;;
|
||||
Disconnect) ${cfg.terminal} -e sudo tailscale down ;;
|
||||
"Exit node…") ts_exit_node ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
while :; do
|
||||
have_ts=""
|
||||
command -v tailscale >/dev/null 2>&1 && have_ts=1
|
||||
choice=$( {
|
||||
conn_rows
|
||||
printf 'Import config…\n'
|
||||
[ -n "$have_ts" ] && printf 'Tailscale ›\n'
|
||||
printf '%s\n' "$BACK"
|
||||
} | rofi -dmenu -p VPN ) || exit 0
|
||||
case "$choice" in
|
||||
"$BACK"|"") exec nomarchy-menu system ;;
|
||||
"Import config…") import_config ;;
|
||||
"Tailscale ›") tailscale_menu ;;
|
||||
"● "*|"○ "*) toggle_conn "$choice" ;;
|
||||
esac
|
||||
done
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.rofi.enable {
|
||||
home.packages = [
|
||||
nomarchy-menu
|
||||
nomarchy-vpn # VPN submenu (NM connect/import + Tailscale)
|
||||
pkgs.fd # files module (fuzzy search over $HOME)
|
||||
pkgs.xdg-utils # xdg-open for the files + web modules
|
||||
pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm)
|
||||
|
||||
@@ -51,6 +51,19 @@ let
|
||||
[ -n "$next" ] && powerprofilesctl set "$next"
|
||||
'';
|
||||
|
||||
# VPN indicator — shows a shield when a NetworkManager VPN/WireGuard
|
||||
# connection is active OR Tailscale is up; prints nothing otherwise so the
|
||||
# module self-hides (like nightlight/updates). Click opens the VPN submenu.
|
||||
vpnStatus = pkgs.writeShellScriptBin "nomarchy-vpn-status" ''
|
||||
active=$(nmcli -t -f TYPE connection show --active 2>/dev/null | grep -Exm1 'vpn|wireguard')
|
||||
ts=""
|
||||
if command -v tailscale >/dev/null 2>&1; then
|
||||
[ "$(tailscale status --json 2>/dev/null | jq -r '.BackendState // empty')" = Running ] && ts=1
|
||||
fi
|
||||
[ -n "$active" ] || [ -n "$ts" ] || exit 0
|
||||
printf '{"text":"","tooltip":"VPN active (click to manage)","class":"on"}\n'
|
||||
'';
|
||||
|
||||
# Per-theme override probe.
|
||||
assetDir = config.nomarchy.themesDir + "/${t.slug}";
|
||||
styleOverride = assetDir + "/waybar.css";
|
||||
@@ -77,7 +90,7 @@ let
|
||||
|
||||
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
|
||||
modules-center = [ "clock" ];
|
||||
modules-right = [ "tray" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ]
|
||||
modules-right = [ "tray" "custom/vpn" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ]
|
||||
++ lib.optional showLanguage "hyprland/language"
|
||||
++ [ "battery" "custom/updates" "custom/notification" ];
|
||||
|
||||
@@ -131,6 +144,15 @@ let
|
||||
on-click = "nomarchy-powerprofile-cycle";
|
||||
};
|
||||
|
||||
# VPN shield — self-hides unless a NM VPN/WireGuard tunnel or Tailscale is
|
||||
# up. Click opens the VPN submenu (nomarchy-vpn). 5s poll like powerprofile.
|
||||
"custom/vpn" = {
|
||||
exec = "nomarchy-vpn-status";
|
||||
return-type = "json";
|
||||
interval = 5;
|
||||
on-click = "nomarchy-vpn";
|
||||
};
|
||||
|
||||
# Night-light (hyprsunset) indicator. Self-gates: the moon shows only while
|
||||
# the schedule runs; otherwise the status helper prints nothing => hidden
|
||||
# (enable / re-enable from the System menu). Click toggles instantly — writes
|
||||
@@ -226,11 +248,14 @@ let
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #custom-updates, #language, #battery, #custom-notification {
|
||||
#tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #custom-updates, #custom-vpn, #language, #battery, #custom-notification {
|
||||
color: @subtext;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* VPN active → accent green, reading as "connected / protected". */
|
||||
#custom-vpn.on { color: @good; }
|
||||
|
||||
/* Night-light active → warm tone, matching the filter it represents. */
|
||||
#custom-nightlight.on { color: @warn; }
|
||||
|
||||
@@ -285,5 +310,5 @@ in
|
||||
# The power-profile helpers on PATH, so both the generated bar and the
|
||||
# whole-swap themes' static waybar.jsonc can exec them by name.
|
||||
home.packages = lib.optionals config.nomarchy.waybar.enable
|
||||
[ powerProfileStatus powerProfileCycle ];
|
||||
[ powerProfileStatus powerProfileCycle vpnStatus ];
|
||||
}
|
||||
|
||||
@@ -127,6 +127,11 @@ in
|
||||
services.fwupd.enable = lib.mkDefault true;
|
||||
|
||||
networking.networkmanager.enable = lib.mkDefault true;
|
||||
# OpenVPN support for the VPN menu's import/connect flow (nomarchy-vpn).
|
||||
# WireGuard needs no plugin (NetworkManager imports wg .conf natively);
|
||||
# this adds the openvpn type so `nmcli connection import type openvpn` works.
|
||||
# mkDefault so a downstream can drop it to slim the closure.
|
||||
networking.networkmanager.plugins = lib.mkDefault [ pkgs.networkmanager-openvpn ];
|
||||
|
||||
# No double-unlock on hibernate. Locking the session before sleep is
|
||||
# right for suspend (resumes from RAM, no other gate), but an encrypted
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
"modules-left": ["custom/launcher", "clock", "clock#date"],
|
||||
"modules-center": ["hyprland/workspaces"],
|
||||
"modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"],
|
||||
"modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"],
|
||||
|
||||
"hyprland/workspaces": {
|
||||
"disable-scroll": true,
|
||||
@@ -79,6 +79,13 @@
|
||||
"on-click": "nomarchy-powerprofile-cycle"
|
||||
},
|
||||
|
||||
"custom/vpn": {
|
||||
"return-type": "json",
|
||||
"interval": 5,
|
||||
"exec": "nomarchy-vpn-status",
|
||||
"on-click": "nomarchy-vpn"
|
||||
},
|
||||
|
||||
"custom/nightlight": {
|
||||
"return-type": "json",
|
||||
"interval": 3,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
"modules-left": ["custom/nomarchy", "clock", "clock#date"],
|
||||
"modules-center": ["hyprland/workspaces"],
|
||||
"modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"],
|
||||
"modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"],
|
||||
|
||||
"hyprland/workspaces": {
|
||||
"disable-scroll": true,
|
||||
@@ -86,6 +86,13 @@
|
||||
"on-click": "nomarchy-powerprofile-cycle"
|
||||
},
|
||||
|
||||
"custom/vpn": {
|
||||
"return-type": "json",
|
||||
"interval": 5,
|
||||
"exec": "nomarchy-vpn-status",
|
||||
"on-click": "nomarchy-vpn"
|
||||
},
|
||||
|
||||
"custom/nightlight": {
|
||||
"return-type": "json",
|
||||
"interval": 3,
|
||||
|
||||
Reference in New Issue
Block a user