Skip to content

feat(mullvad): add Mullvad VPN plugin#795

Merged
spiros132 merged 2 commits into
noctalia-dev:mainfrom
OCSPG:add-mullvad-plugin
May 25, 2026
Merged

feat(mullvad): add Mullvad VPN plugin#795
spiros132 merged 2 commits into
noctalia-dev:mainfrom
OCSPG:add-mullvad-plugin

Conversation

@OCSPG

@OCSPG OCSPG commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a mullvad plugin that provides a bar widget, panel and settings UI for controlling Mullvad VPN through the mullvad CLI on Linux. Goal: replace the official Mullvad GUI on machines that already run Noctalia.

Features

  • Bar widget with state-driven shield icon (connected / connecting / disconnected / lockdown / error)
  • Left-click opens panel, right-click context menu
  • Panel: state header, current relay subtitle (with multihop / IP-version / lockdown / auto-connect / LAN-blocked badges), connect/disconnect button, account-expiry banner
  • Quick toggles: lockdown mode, auto-connect, LAN sharing
  • Search-first relay picker (~700 relays parsed from mullvad relay list)
  • Advanced section: multihop on/off + entry country, IP version
  • IPC handlers at plugin:mullvad: toggle, connect, disconnect, reconnect, togglePanel, refresh, status, setLocation, setLockdown
  • i18n: English + German

Tested 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.md and modeled the structure on tailscale, arch-updater and privacy-indicator, but a second pair of eyes would be good.

Happy to iterate on review feedback.

Test plan

  • manifest.json validates against schema.json
  • Plugin loads in Noctalia Shell 4.7.6 with no QML warnings on the mullvad files
  • Bar widget renders, click opens panel, right-click shows context menu
  • qs ipc call plugin:mullvad toggle connects / disconnects daemon
  • Relay picker click sets mullvad relay set location <code> and connects
  • Lockdown / auto-connect / LAN switches in panel round-trip with mullvad <subcmd> get
  • Multihop + entry country + IP version round-trip with mullvad relay get
  • Subtitle updates live when relay constraint or any toggle changes
  • Daemon-down state (systemctl stop mullvad-daemon) renders gracefully
  • Account-expiry banner (untested - account has 19 days left, threshold is 7)
  • CI checks (will appear after PR is opened)

Notes

  • README recommends mullvad-vpn-daemon over the full mullvad-vpn package since the plugin replaces the GUI.
  • Out of scope for this initial drop (flagged for future PRs): split tunneling, custom WireGuard relays, anti-censorship modes, beta program toggle, factory reset, DNS editing UI. Confirmation modal on disconnect-with-lockdown was also dropped because there's no NConfirmDialog widget yet.

🤖 Generated with Claude Code

@github-actions

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.
@OCSPG OCSPG force-pushed the add-mullvad-plugin branch from 0ff58c4 to 98de276 Compare April 30, 2026 09:59

@spiros132 spiros132 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some feedback about the PR! :D

Comment thread mullvad/manifest.json Outdated
"Bar",
"Network",
"Panel",
"VPN"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't any VPN tag, so please do not include that one since it doesn't exist.

Comment thread mullvad/Panel.qml Outdated
} else {
parts.push(root.pluginApi?.tr("action.auto-select"))
}
var tr = root.pluginApi ? root.pluginApi.tr.bind(root.pluginApi) : function (k) { return k }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use this. Just use the regular root.pluginApi?.tr("key")

Comment thread mullvad/Panel.qml Outdated
}
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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use replace, prefer to use translation interpolations. For example root.pluginApi?.tr("key", { foo: "bar" })

Comment thread mullvad/Panel.qml Outdated
}
}

ListView {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mullvad/Main.qml Outdated
property bool relayClickConnects: _resolve("relayClickConnects", true)
property bool confirmDisconnectInLockdown: _resolve("confirmDisconnectInLockdown", true)
property var favoriteCountries: _resolve("favoriteCountries", [])
property int expiryWarningDays: _resolve("expiryWarningDays", 7)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Comment thread mullvad/Main.qml Outdated
return fallback
}

onSettingsVersionChanged: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the comment before, I don't believe all of the _resolve functionality is needed.

Comment thread mullvad/Main.qml Outdated
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: ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but where is this lastToggleAction used?

@spiros132 spiros132 marked this pull request as draft May 10, 2026 20:22
@OCSPG

OCSPG commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in d2d0a59c:

  • removed the invalid VPN tag
  • simplified settings bindings and removed unused lastToggleAction
  • replaced manual translation .replace(...) calls with interpolations
  • switched the relay list to NListView

Validated with jq and qmllint, restarted Noctalia locally, and confirmed the plugin still works.

@spiros132

Copy link
Copy Markdown
Collaborator

Addressed the review feedback in d2d0a59c:

* removed the invalid `VPN` tag

* simplified settings bindings and removed unused `lastToggleAction`

* replaced manual translation `.replace(...)` calls with interpolations

* switched the relay list to `NListView`

Validated with jq and qmllint, restarted Noctalia locally, and confirmed the plugin still works.

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".

@OCSPG OCSPG marked this pull request as ready for review May 24, 2026 00:27
@spiros132

Copy link
Copy Markdown
Collaborator

Looks good to me, thank you for the PR! :D

@spiros132 spiros132 merged commit 8a347a2 into noctalia-dev:main May 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants