Ghost Assembly QuickRem

GNOME Shell extension

QuickRem

Remmina connections in GNOME Quick Settings.

Open the system menu, click the Remmina tile, pick a saved connection. The list is read from Remmina’s own profile directory and follows it as it changes, so a connection saved in Remmina shows up here without a reload.

  • GNOME Shell 49–50
  • Flatpak or native Remmina
  • GPL-3.0-or-later

Install Source on GitHub

On this page
  1. 01Overview
  2. 02Install
  3. 03Profiles
  4. 04Where profiles live
  5. 05Launching
  6. 06Preferences
  7. 07Security
  8. 08Architecture
  9. 09Testing
  10. 10Packaging
  11. 11Contributing
  12. 12Releasing
  13. 13Development

01

Overview

QuickRem adds one tile to Quick Settings. Its menu lists every connection saved in Remmina; pick one and Remmina connects. There is no top bar icon: Remmina is not a status, and an icon that never changes is noise.

Every saved profile

Sorted by name, with an icon per protocol and user@host beside each. A long list scrolls, capped at half the screen.

Follows Remmina

Saving, editing or removing a profile updates the menu straight away. A Remmina installed later, or a new datadir_path, is picked up too.

Opens it the way Remmina does

Through the handler registered for application/x-remmina, so a Flatpak and a distribution package work alike and a running Remmina is reused.

Never reads your passwords

Remmina encrypts them. QuickRem drops every password, passphrase and secret key while parsing, so none can reach a label or the log.

02

Install

Requires

  • GNOME Shell 49 or 50.
  • Remmina, as the Flatpak (org.remmina.Remmina) or a distribution package.

From a release

No clone and no toolchain: gnome-extensions ships with GNOME Shell. Installing also compiles the settings schema.

Shell
$ curl -LO https://github.com/Ghost-Assembly/quickrem/releases/latest/download/[email protected]
$ gnome-extensions install --force [email protected]
# log out and back in, then
$ gnome-extensions enable [email protected]

From a clone

Shell
$ just setup
$ just install
$ just enable

Log out and back in. Wayland cannot reload the Shell in place, so a newly installed extension does not exist for the running Shell: the first enable after a fresh install says so. Enable it in the new session.

03

Profiles

A Remmina profile is a .remmina file: a GKeyFile with one [remmina] group. QuickRem reads four of its keys — name, protocol, server and username — and ignores the rest. A profile with no usable name is listed by its filename stem, which Remmina’s default template (%G_%P_%N_%h) makes a fair description anyway.

The list is sorted by name the way your locale collates, and each row shows user@host, or the host alone, dimmed beside the name. Long names and details end in an ellipsis rather than widening the menu.

Protocol Icon
SSH utilities-terminal-symbolic
SFTP folder-remote-symbolic
RDP, VNC, GVNC, SPICE preferences-desktop-remote-desktop-symbolic
WWW web-browser-symbolic
EXEC system-run-symbolic
Anything else network-server-symbolic

Only regular files are read — a symlink to one is followed — and at most 256 KiB of each. A profile that cannot be read is skipped and the rest of the list stays. An empty list says why: Remmina not found and Profile directory setting is invalid both open the preferences; No profiles saved means Remmina is there with nothing in it yet.

04

Where profiles live

The directory is found rather than configured. The first of these that applies wins:

  1. The Profile directory setting. It must be absolute or start with ~/; anything else is reported, not guessed at.
  2. datadir_path from remmina.pref. The first readable of the native and the Flatpak copy decides — the native one first when remmina is on PATH.
  3. A native install, when remmina is on PATH: $XDG_DATA_HOME/remmina, usually ~/.local/share/remmina. It wins over the Flatpak because its profiles are the ones remmina -c would open.
  4. The Flatpak, when ~/.var/app/org.remmina.Remmina/data/remmina exists.

With none of them, the menu says Remmina not found.

Nothing needs a restart. A fresh Remmina has no profile directory until it saves a profile, so QuickRem watches the nearest folder that exists and moves onto the directory once it appears. It also watches where detection looks — the Flatpak data directory and both remmina.pref files — so a Remmina installed after the extension, or a datadir_path set later, is picked up on its own. A native install only appears on PATH, which cannot be watched, so opening the menu while it says Remmina not found looks again.

05

Launching

Remmina has no D-Bus interface and is not D-Bus activatable, so something has to be run. Shelling out to flatpak run org.remmina.Remmina -c <path> would hard-code the packaging, and a path handed straight to a sandboxed process is not necessarily one it can see.

So QuickRem opens a profile through whatever is registered for application/x-remmina. On a Flatpak install that is Remmina’s own connect entry:

org.remmina.Remmina-file.desktop
Exec=flatpak run --command=remmina-file-wrapper --file-forwarding \
     org.remmina.Remmina -c @@u %U @@

That entry is the -c connect action, and --file-forwarding has the portal map the path into the sandbox. The same call works unchanged for a distribution package and reuses an already-running Remmina. Open Remmina… goes through Shell.AppSystem, whose launch context puts the window on the workspace the click came from.

The Launch command setting overrides both. It is split with GLib.shell_parse_argv and the profile path is appended as a separate argument, never interpolated — a profile named with a semicolon is an argument, not a second command.

06

Preferences

Open them from Settings at the bottom of the menu, from the Extensions app, or with just prefs from a clone. The first row shows which directory QuickRem resolved and how, and says so when it does not exist yet or the setting is not a usable path — a wrong guess is visible rather than silent.

Setting Empty means Notes
Profile directory Detect it An absolute path, or one starting with ~/.
Launch command Open a profile with the application/x-remmina handler For an unusual install. The profile path is appended as its own argument. With it empty, Open Remmina… goes through org.remmina.Remmina.desktop via Shell.AppSystem; a command set here is used for both.

07

Security

The profile directory is trusted. Clicking a profile hands it to Remmina, and Remmina does what it says: protocol=EXEC runs its execcommand, and SSH profiles can run commands before and after connecting. Anyone who can write a profile there, or point profile-dir or datadir_path somewhere they can write, can get a command run the next time it is clicked. Keep it writable only by you.

Passwords are never read

Remmina encrypts stored passwords with a key kept in remmina.pref. QuickRem cannot decrypt them and does not try: modules/profiles.js drops every key matching password, passphrase or secret while parsing, so a later code path that forgets to filter has nothing to leak into a label or the journal. remmina.pref itself is read for one key, datadir_path; the secret= beside it is never even unescaped. A unit test holds this: the parsed profile has only its five expected keys and none of the ciphertext.

What else QuickRem promises

  • A FIFO, a device node or a symlink to one — /dev/zero, say — is skipped without being opened, and no read goes past 256 KiB, so nothing in the profile directory can stall or exhaust the Shell.
  • Log lines never name a profile: Remmina’s default filename embeds the server’s hostname.
  • A profile path is always its own argument, never part of a command string.

Report a vulnerability privately through the security advisory form; the security policy has the full scope.

08

Architecture

The decisions live in modules that import nothing, so the suite tests them on plain Node. The files around them only move data between those rules and the disk or the Shell.

paths.js · profiles.js · keyfile.jsEvery decision about the files: which directory, what a profile says, how a key file reads. Import nothing; tested on Node.
File Job
extension.js Pairs construction with teardown, nothing else.
modules/keyfile.js Reads one group of a GKeyFile, exact keys only.
modules/profiles.js Parses a profile, sorts, maps protocol to icon.
modules/paths.js Decides which directory to read.
modules/io.js Asynchronous probes, and bounded reads of regular files.
modules/detect.js Probes the system and applies the rules in paths.js.
modules/store.js Scans and watches; publishes profiles and source.
modules/launch.js Decides what to run, and with which arguments.
modules/panel.js The tile, its menu and its rows.
prefs.js Preferences, in their own process.

Every read and probe is asynchronous: a profile directory lives on whatever the home directory is mounted from, and a synchronous read would stall the whole desktop. File events are debounced for 300 ms, because Remmina rewrites a profile in several steps when it saves, and generation counters drop a scan or a resolve that a newer one has overtaken. The store emits one changed signal per update, and the panel rebuilds from it, holding no profile state of its own.

Why the list scrolls

A Quick Settings menu has no scrolling of its own: measured on a 1080p screen, thirty profiles want 1296px of a 1048px work area and the rest is clipped. GNOME’s Wi-Fi menu shows eight networks and sends you to Settings for the rest, which suits a list you skim but not one you pick from. So modules/panel.js swaps its section’s actor for an St.ScrollView around the same box, capped at half the work area. The cap is applied just before the menu opens — the menu measures the height it animates to before it says it is opening — so a monitor or text-scaling change is picked up without watching for either.

09

Testing

The unit suite runs on Node with Vitest. The pure modules run exactly as they ship. modules/store.js runs against an in-memory Gio in tests/stubs/ — FIFOs, device nodes and symlinks included — with timeouts queued rather than scheduled, so “five events caused one rescan” is an assertion, not a sleep.

  • modules/launch.js is split out so its one security invariant is testable: a profile path is its own argument-vector element. A test fails if that stops being true.
  • prefs.js is tested mostly for one trap: a translated string built while the module loads throws, and the preferences window then never opens, silently.
  • modules/panel.js runs against stubs of St and the Shell’s menus that model what the Shell really does — it never destroys a quick toggle’s menu, and a menu measures its height before it announces it is opening.

just test-live adds what CI cannot have: it boots a throwaway headless GNOME Shell in private XDG directories, enables, disables and re-enables the extension, adds and removes a profile while it runs, and fails on a JavaScript error or a lifetime warning. just test-docs checks this site in Chromium and Firefox.

10

Packaging

just build makes the zip with plain zip, because gnome-extensions ships inside the gnome-shell package and CI has no GNOME. The cost is that nothing checks the archive layout, and extensions.gnome.org is strict about it. So scripts/pack-check.sh, part of just test-live, keeps the official packer as the authority: it diffs the two archives and checks that metadata.json sits at the root.

11

Contributing

main is protected: no direct pushes, no force-pushes and no merge commits. Everything lands through a pull request whose title is a Conventional Commit — it becomes the squashed commit subject.

Shell
$ git switch -c type/short-description
$ just ci
$ gh pr create --fill
$ gh pr merge --squash --auto

ci and CodeQL have to pass and the branch has to be current with main. No approving review is required, so a single maintainer is not locked out.

12

Releasing

Set the version in metadata.json and package.json and land that through a pull request. Then tag the merged commit and push the tag:

Shell
$ git switch main && git pull
$ git tag -a v0.1.2 -m 'release v0.1.2'
$ git push origin v0.1.2

The release workflow checks the tag against both files before building anything — otherwise a release titled v0.1.2 could ship a zip that tells GNOME it is 0.1.0 — and runs the full suite before it publishes.

13

Development

justfile
just             # list every recipe
just test        # unit suite, on Node
just test-docs   # this site, in Chromium and Firefox
just lint        # eslint, prettier, gschema, shellcheck
just security    # gitleaks, trivy, osv-scanner, actionlint, zizmor
just ci          # what CI runs: lint, tests, test-docs, security, build
just test-live   # headless gnome-shell, then the packer check
just fixtures 5  # throwaway profiles for the menu and watcher
just run         # a gnome-shell in a window (needs mutter-devkit)
just logs        # follow the extension's output
just docs        # serve this site on localhost:8000

mise.toml installs the toolchain: Node is pinned to its major, the other tools track their latest release on purpose. gjs, glib-compile-schemas, gnome-shell and gnome-extensions come from the system, because they have to match the Shell you target.