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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user