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.
GNOME Shell extension
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.
01
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.
Sorted by name, with an icon per protocol and
user@host beside each. A long list scrolls,
capped at half the screen.
Saving, editing or removing a profile updates the menu
straight away. A Remmina installed later, or a new
datadir_path, is picked up too.
Through the handler registered for
application/x-remmina, so a Flatpak and a
distribution package work alike and a running Remmina is
reused.
Remmina encrypts them. QuickRem drops every password, passphrase and secret key while parsing, so none can reach a label or the log.
02
org.remmina.Remmina) or a distribution
package.
No clone and no toolchain: gnome-extensions ships
with GNOME Shell. Installing also compiles the settings schema.
$ 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]
$ 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
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
The directory is found rather than configured. The first of these that applies wins:
~/; anything else
is reported, not guessed at.
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.
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.
~/.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
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:
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
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
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.
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.
/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.
Report a vulnerability privately through the security advisory form; the security policy has the full scope.
08
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.
| 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.
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
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
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
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.
$ 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
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:
$ 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
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.