feat(mullvad): add Mullvad VPN plugin#795
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
Bar widget + panel + settings to control Mullvad VPN through the `mullvad` CLI. Replaces the official Mullvad GUI on Linux desktops. Features: - Bar widget: state-driven shield icon (green=connected, yellow= connecting, red=lockdown blocking traffic, grey=disconnected) - Click opens panel (configurable: toggle vs panel) - Right-click context menu (Connect/Disconnect, Open panel, Settings) - Tooltip via TooltipService showing relay + IP when connected - Panel header: state, current relay constraint, connect/disconnect button, account-expiry banner when fewer than N days remain - Quick toggles (round-trip with the CLI): - Lockdown mode (kill switch) - Auto-connect - LAN sharing - Search-first relay picker (~700 relays). Country rows by default, expand on search. Click a row to set the relay constraint and optionally connect immediately. - Advanced (collapsed): multihop on/off + entry country, IP version - Settings UI exposes 10 knobs (refresh interval, click action, click-connects, lockdown confirm, expiry warning days, etc.) - IPC handlers at `plugin:mullvad`: toggle / connect / disconnect / reconnect / togglePanel / refresh / status / setLocation / setLockdown - i18n: English + German Disclosure: This plugin was written entirely by an LLM (Claude). The maintainer of the fork (@OCSPG) does not have prior QML experience, so structural review of the QML is appreciated. The plugin was tested manually on Noctalia Shell 4.7.6 with mullvad-cli 2026.1; toggling, relay selection, multihop, and quick toggles all round-trip with the CLI. Recommended install is the daemon-only package (mullvad-vpn-daemon) since the plugin replaces the GUI.
0ff58c4 to
98de276
Compare
spiros132
left a comment
There was a problem hiding this comment.
Some feedback about the PR! :D
| "Bar", | ||
| "Network", | ||
| "Panel", | ||
| "VPN" |
There was a problem hiding this comment.
There isn't any VPN tag, so please do not include that one since it doesn't exist.
| } else { | ||
| parts.push(root.pluginApi?.tr("action.auto-select")) | ||
| } | ||
| var tr = root.pluginApi ? root.pluginApi.tr.bind(root.pluginApi) : function (k) { return k } |
There was a problem hiding this comment.
Do not use this. Just use the regular root.pluginApi?.tr("key")
| } | ||
| var tr = root.pluginApi ? root.pluginApi.tr.bind(root.pluginApi) : function (k) { return k } | ||
| if (_mh) parts.push(_mhe ? tr("badges.multihop-via").replace("{country}", root._countryName(_mhe)) : tr("badges.multihop")) | ||
| if (_iv !== "any") parts.push(tr("badges.ip-version").replace("{version}", _iv)) |
There was a problem hiding this comment.
Do not use replace, prefer to use translation interpolations. For example root.pluginApi?.tr("key", { foo: "bar" })
| } | ||
| } | ||
|
|
||
| ListView { |
There was a problem hiding this comment.
Do prefer to use the noctalia widgets instead of the qml ones to keep it consistent with the rest of the shell. For example instead of ListView prefer to use NListView.
| property bool relayClickConnects: _resolve("relayClickConnects", true) | ||
| property bool confirmDisconnectInLockdown: _resolve("confirmDisconnectInLockdown", true) | ||
| property var favoriteCountries: _resolve("favoriteCountries", []) | ||
| property int expiryWarningDays: _resolve("expiryWarningDays", 7) |
There was a problem hiding this comment.
I don't believe all of this boilerplate is needed. Instead prefer to use the already there reactivity of qml. For example:
readonly property var cfg: pluginApi?.pluginSettings ?? ({})
readonly property var defaults: pluginApi?.manifest?.metadata?.defaultSettings ?? ({})
readonly property string foo: cfg.foo ?? defaults.foo ?? "bar"| return fallback | ||
| } | ||
|
|
||
| onSettingsVersionChanged: { |
There was a problem hiding this comment.
As the comment before, I don't believe all of the _resolve functionality is needed.
| property var currentLocation: null // { country, city, hostname, ipv4, ipv6, mullvad_exit_ip } | ||
| property var visibleLocation: null // populated when disconnected | ||
| property bool isRefreshing: false | ||
| property string lastToggleAction: "" |
There was a problem hiding this comment.
I might be wrong, but where is this lastToggleAction used?
|
Addressed the review feedback in
Validated with |
Alright, when you feel ready that this PR is ready for review / merging in, press the ready for review to remove the PR from draft "mode". |
|
Looks good to me, thank you for the PR! :D |
Summary
Adds a
mullvadplugin that provides a bar widget, panel and settings UI for controlling Mullvad VPN through themullvadCLI on Linux. Goal: replace the official Mullvad GUI on machines that already run Noctalia.Features
mullvad relay list)plugin:mullvad:toggle,connect,disconnect,reconnect,togglePanel,refresh,status,setLocation,setLockdownTested on: Noctalia Shell 4.7.6, mullvad-cli 2026.1, Arch Linux. Toggling, relay selection, multihop, lockdown/auto-connect/LAN switches all round-trip with the CLI.
Disclosure
This plugin was written entirely by an LLM (Claude). I (the author of the fork, @OCSPG) don't have prior QML experience, so I'd really appreciate a structural review of the QML side - especially anything that's idiomatic for Noctalia plugins but I might have missed. I followed
AGENTS.mdand modeled the structure ontailscale,arch-updaterandprivacy-indicator, but a second pair of eyes would be good.Happy to iterate on review feedback.
Test plan
manifest.jsonvalidates againstschema.jsonqs ipc call plugin:mullvad toggleconnects / disconnects daemonmullvad relay set location <code>and connectsmullvad <subcmd> getmullvad relay getsystemctl stop mullvad-daemon) renders gracefullyNotes
mullvad-vpn-daemonover the fullmullvad-vpnpackage since the plugin replaces the GUI.NConfirmDialogwidget yet.🤖 Generated with Claude Code