Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Test plugin

on:
push:
pull_request:

jobs:
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Shell syntax
run: bash -n backend.sh tests/*.sh tests/fake/*
- name: Backend regression suite
run: bash tests/run.sh
44 changes: 27 additions & 17 deletions Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,15 @@ Panel {
if (kind === "name") {
if (!requestRename(profile)) return
} else {
handOffToEditor(profile)
if (!handOffToEditor(profile)) return
}
close()
}

function handOffToEditor(profile) {
if (!wireguard.editConfig(profile, "")) return false
editHandedOff = true
wireguard.editConfig(profile, "")
return true
}

// Returns whether the prompt opened, so callers know whether the panel has
Expand Down Expand Up @@ -351,6 +352,10 @@ Panel {
root.editHandedOff = false
if (!root.opened) root.open()
}
// Cancel, no-change and completed saves are terminal but not failures.
// Retire the UI-only marker so a later headless editor failure cannot
// mistake this panel for the caller that needs reopening.
function onEditFinished() { root.editHandedOff = false }
}

IpcHandler {
Expand All @@ -361,9 +366,15 @@ Panel {
function hide(): void { root.close() }
// VPN toggle, not panel visibility — open/close/show/hide already cover
// the popup, and the bar's left click promises the same thing.
function toggle(): void { wireguard.toggle() }
function refresh(): string { wireguard.refresh(); return "ok" }
function down(): string { wireguard.disconnectAll(); return "ok" }
function toggle(): string {
return wireguard.toggle() ? "ok" : "error: " + wireguard.actionRejection
}
function refresh(): string {
return wireguard.refresh() ? "ok" : "error: " + wireguard.actionRejection
}
function down(): string {
return wireguard.disconnectAll() ? "ok" : "error: " + wireguard.actionRejection
}
function status(): string { return wireguard.statusText }
// The connection grid without the panel. Rates and ping only move while
// something is watching them, so a headless caller sees the totals and
Expand All @@ -375,8 +386,7 @@ Panel {
var name = wireguard.sanitizeName(path)
if (!wireguard.isValidName(name)) return "error: cannot derive an interface name from " + path
if (wireguard.countByIfname(name) > 1) return "error: ambiguous: several profiles use the interface " + name
wireguard.importFile(path, name)
return name
return wireguard.importFile(path, name) ? name : "error: " + wireguard.actionRejection
}
// Takes a connection name or a profile UUID; a name shared by several
// profiles is refused rather than resolved to an arbitrary one.
Expand All @@ -388,8 +398,7 @@ Panel {
if (n > 1) return "error: ambiguous name: " + target + " — use a UUID: " + wireguard.uuidsForName(target).join(" ")
profile = wireguard.findByName(target)
}
wireguard.editConfig(profile, "")
return "ok"
return wireguard.editConfig(profile, "") ? "ok" : "error: " + wireguard.actionRejection
}
// Same target resolution as edit; the new name is a display label, so
// anything single-line goes — except a name another profile already
Expand All @@ -405,11 +414,14 @@ Panel {
var value = String(newName || "").trim()
if (value === "") return "error: the new name must not be empty"
if (value !== profile.name && wireguard.countByName(value) > 0) return "error: a profile named " + value + " already exists"
wireguard.renameConfig(profile, value)
return "ok"
return wireguard.renameConfig(profile, value) ? "ok" : "error: " + wireguard.actionRejection
}
function importPick(): string {
return wireguard.pickConfigFile() ? "ok" : "error: " + wireguard.actionRejection
}
function importPaste(): string {
return wireguard.pasteConfig() ? "ok" : "error: " + wireguard.actionRejection
}
function importPick(): string { wireguard.pickConfigFile(); return "ok" }
function importPaste(): string { wireguard.pasteConfig(); return "ok" }
// Headless export — no warning dialog: an explicit path in argv is
// already deliberate in a way a panel click is not. The file lands 0600.
function exportConfig(target: string, path: string): string {
Expand All @@ -421,8 +433,7 @@ Panel {
profile = wireguard.findByName(target)
}
if (String(path || "") === "") return "error: no destination path"
wireguard.exportToPath(profile, path)
return "ok"
return wireguard.exportToPath(profile, path) ? "ok" : "error: " + wireguard.actionRejection
}
// The QR has its own window, so this never touches the panel — a
// headless caller gets the code centred on screen and nothing else.
Expand Down Expand Up @@ -504,8 +515,7 @@ Panel {
// in the way of zenity and of the rename window as it is of the QR.
else if (t === "e" || t === "E") {
if (root.cursorActive && root.focusSection === "configs") {
root.handOffToEditor(root.selectedProfile())
root.close()
if (root.handOffToEditor(root.selectedProfile())) root.close()
}
}
else if (t === "n" || t === "N") {
Expand Down
55 changes: 36 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ containing hook directives are rejected with a clear message.
- **`wireguard-tools`** — only `wg` is used, to validate keys before import.
- **`zenity`** — optional, for the file picker and the config editor.
`kdialog` or `yad` also work for the file picker.
- **`wl-clipboard`** — optional, for importing a config from the clipboard.
- **`wl-clipboard`** — optional, for importing a config from the clipboard
and copying connection details.
- **`qrencode`** — optional, for showing a profile as a QR code.
- **`notify-send`** (libnotify) — optional, for the toast when a tunnel is
deactivated externally.
Expand All @@ -43,13 +44,12 @@ No new privileges for any of it: everything still runs as your user.

```bash
omarchy plugin add https://github.com/glafeara/omarchy-wireguard.git
omarchy plugin enable glafeara.wireguard
omarchy bar plugin add glafeara.wireguard right
omarchy plugin enable glafeara.wireguard right
```

Plugins land disabled so you can read the code before enabling it — this one
is three QML files and one shell script. Add `--yes` to any of the commands
to skip the prompts.
`plugin add` is interactive by default; add `--yes` to that command to skip
its prompt. The explicit plugin id and `right` placement make `plugin enable`
non-interactive. The plugin itself is five QML files and one shell script.

## Using it

Expand Down Expand Up @@ -139,6 +139,15 @@ itself failed — a plain statement that the state is unknown. A successful
activation confirms NetworkManager's state, not the peer's reachability:
WireGuard has no connected/disconnected handshake state to report.

Replacing a profile during import is transactional too. If NetworkManager
refuses deletion of the old active profile and also refuses its rollback,
the operation reports that the state is unknown (backend exit `6`) instead
of implying that only the deletion failed. Both the old and fully built
replacement profiles are retained for manual recovery; the error names their
UUIDs rather than discarding the only new configuration. Exit `6` also marks
a failed cleanup of an incomplete replacement, so editor saves never retry
automatically on top of a profile that NetworkManager refused to remove.

**Renaming** changes the profile's display name (`connection.id`) only —
spaces are fine, duplicates are refused. The interface name never changes;
that one obeys kernel rules and belongs to import. The prompt opens in its
Expand Down Expand Up @@ -189,8 +198,8 @@ they were.
| `pingHost` | `1.1.1.1` | any host, or empty to disable the probe |

```bash
omarchy bar plugin set glafeara.wireguard refreshIntervalSec 30
omarchy bar plugin set glafeara.wireguard pingHost "" # no latency probe
omarchy bar set glafeara.wireguard refreshIntervalSec 30
omarchy bar set glafeara.wireguard pingHost "" # no latency probe
```

## IPC
Expand All @@ -211,7 +220,12 @@ omarchy-shell glafeara.wireguard exportConfig kz ~/kz.conf # 0600, private key
omarchy-shell glafeara.wireguard qr kz # QR window, centred on screen
```

Name-based commands refuse an ambiguous name and list the matching UUIDs
Commands that start an asynchronous action return `ok` only when it has
actually been accepted. Control actions reject another running control action;
picker, clipboard import, QR and export reject only their own already-running
worker, while an editor may open during a control action and queues its save.
An editor rejects a second editor or a queued editor save. `rename` to the
current name is an idempotent `ok`. Name-based commands refuse an ambiguous name and list the matching UUIDs
instead — pass a UUID to disambiguate. `details` answers with the addresses
whether or not the panel is open, and with `--` for everything sampled —
rates, totals and ping — because sampling stops with the panel. A figure
Expand All @@ -228,10 +242,16 @@ an old one.
connected, so the bar's quick toggle reconnects what you actually used.
- `$XDG_RUNTIME_DIR/omarchy-wireguard.<uid>.{lock,intent,notified}` —
the cross-instance lock, the short-lived "this deactivation was ours"
markers behind the notifications, and the toast cooldown stamp. tmpfs,
gone at reboot.
- `$XDG_RUNTIME_DIR/wg-qr.*.png` — the QR image while its window is open;
deleted on close.
markers behind the notifications, and the toast cooldown stamp. The
backend requires a private, current-user runtime directory (or its safe
`/run/user/<uid>` fallback) and refuses to use `/tmp`. These files are
private and gone at reboot.
- `$XDG_RUNTIME_DIR/wg-qr.<shell-pid>.*.png` — the QR image while its window
is open; deleted on close. On the next shell startup, images whose owner
PID is dead are safely reaped without touching another live monitor's QR.
- `$XDG_RUNTIME_DIR/wg-edit.<shell-pid>.*` — private editor buffers and
result files while zenity is open; deleted on every editor exit and reaped
on the next shell startup after a crash.
- `/sys/class/net/<iface>/statistics/{rx,tx}_bytes` — read-only, for the
traffic line, plus the interface's address and MTU for the detail grid.
- **One ICMP echo to `pingHost` every three seconds while the panel is
Expand All @@ -246,22 +266,19 @@ scripts, no services, no telemetry.

## Tests

`tests/` holds four suites that run the backend against a fake `nmcli` on
`tests/` holds backend suites that run against fake `nmcli`, `wg`, and
`qrencode` commands on
`PATH` — the only way to exercise the switch rollback paths, since a dead
endpoint does not make `nmcli connection up` fail — plus a manual
checklist (`tests/checklist.md`) for what needs real tunnels:

```bash
bash tests/test-status.sh
bash tests/test-connect.sh
bash tests/test-notify.sh
bash tests/test-details.sh
bash tests/run.sh
```

## Uninstall

```bash
omarchy bar plugin remove glafeara.wireguard
omarchy plugin remove glafeara.wireguard
```

Expand Down
Loading
Loading