One Omarchy shell bar widget for both ends of a VNC session: share this
machine's monitors, and connect to bookmarked remote heads. It drives
wayvnc to serve and wlvncc (or another VNC
client) to connect — see Requirements below for what has to
be installed.
The same repo is meant to be cloned on every machine that takes part — the widget does not care which end it is on, and a machine that both serves and connects (a laptop driving a desktop, say) uses one instance for both.
Everything is managed from one dialog. Any click on the bar icon opens or closes the panel — no click on the button changes anything by itself. The panel puts each end of a session in a tab of its own, under a header that belongs to neither:
- Serve — every connected output with a switch each, showing the
address:portit is shared on. The monitor the panel was opened on is listed first and highlighted. Flipping a switch starts or stops wayvnc for that output. Anything a server refuses over — wayvnc missing, or one running that this plugin did not start — is reported here. - Connect — quick connect and bookmarked hosts, one button per head, led by the passthrough switch that hands SUPER and every other key to the focused window. Passthrough lives on this tab because it is a client-side control: handing the whole keyboard over is only ever wanted while the focused window is somebody else's screen. It needs a few lines in your own Hyprland config — see Remote input passthrough — and says so, with the switch dead, until they are there.
The panel opens on Serve, since that is the side the bar icon reports on,
and remembers the tab last used until the shell restarts. The widget exists once
per monitor, so that memory is per bar surface: each monitor's copy keeps its own
tab, and the SERVE chip carries the number of outputs being shared (SERVE · N)
so the state is legible from the Connect side too.
The icon is urgent-coloured while this bar's monitor is being shared — the one thing a per-monitor button can report that the panel cannot. It swaps to a keyboard glyph while input passthrough is on, because that is a modal state that swallows SUPER and has to be distinguishable at a glance from "sharing".
Keyboard navigation is one model shared by both tabs — see Keyboard below.
The cursor walks top to bottom: the tab strip, then — on Connect only — the passthrough switch, then the rows of whichever tab is showing (the monitors on Serve, the bookmarks on Connect). The hidden tab's rows are never a place the cursor can be, and neither is the header: it holds a title and a line of key shorthand, and a stop with nothing to act on is a keypress that does nothing.
- Up / Down move the cursor along that order. A stop only exists where there is something to act on: a tab with no rows at all (no bookmarks, no outputs) has no row stop, and Serve has no passthrough stop because the switch is Connect's.
- Left / Right switch tabs outright from the passthrough switch or a row.
From the tab strip they instead move a ring between the two chips — the one
place where moving is not the same as choosing.
Enteron the strip picks the chip the ring is on. Leaving Connect takes the cursor off the passthrough switch and back to the strip, so nothing on Serve is pointing at a control that is not on screen. - Enter acts on whatever the cursor is on: switches to the chip under the ring from the strip, toggles passthrough from the Connect switch, flips that output's switch from a Serve row, and connects head 1 of that bookmark from a Connect row — not whichever head was last used, always the first. Reaching a different head from the keyboard is what the digit keys are for.
1-9connect that numbered head of the bookmark the cursor last visited, and do nothing unless the Connect tab is the one showing — a head number fired at a hidden bookmark list would launch a client out of nowhere. A number past the bookmark's head count is a no-op./switches to Connect and focuses the quick-connect field, since a hidden field cannot take focus anyway and/is a request to type a host.r/Rtoggles passthrough, and only while Connect is showing — the switch it stands for is that tab's, so from Serve the key does nothing. It is refused, silently, while theremotesubmap is not registered: see Remote input passthrough.- Esc first returns focus from the quick-connect field to the keyboard cursor; pressed again (or pressed anywhere the field is not focused) it closes the panel.
The header shows the shorthand for the keys that are not also visible as
controls: ←→ tabs · R passthrough on Connect, and just ←→ tabs on Serve,
where R would do nothing. It reads Remote input mode — Right Ctrl releases
instead while passthrough is on, since that matters more in the moment than the
reminder.
The bar builds one surface per monitor, so this widget exists once per output.
Each instance knows which output it is on — it reads the name off the window it
was placed in (QsWindow.window.screen.name, the Hyprland output name) — and
uses that for its own icon state and to lead the monitor list. Managing a server,
though, is not restricted to its own output: any instance can start or stop any
of them, because the panel lists them all.
The server is started as:
setsid wayvnc --output=<name> -S <runtimeDir>/wayvnc-<name>.sock --max-fps=<fps> --render-cursor <address> <port> >/dev/null 2>&1 &
-S gives this head its own wayvnc control socket. wayvnc's control socket
otherwise defaults to one fixed path per user ($XDG_RUNTIME_DIR/wayvncctl),
so starting a second head without -S fails immediately with Another wayvnc process is already running — before it ever touches a port — and since the
launch above discards its own output, that death is silent: the toggle just
flips itself back off 500ms later, same as a missing binary. <runtimeDir> is
$XDG_RUNTIME_DIR, falling back to /tmp when it is unset. This is also what
lets several monitors share at once: without a socket of its own, every head
after the first would refuse to start.
--render-cursor draws the pointer into the framebuffer. Clients that do not
composite the VNC cursor pseudo-encoding themselves — TigerVNC among them — show
no cursor at all without it.
setsid detaches the server from the shell that launched it, and that is a
trade-off worth stating plainly: a wayvnc started here is not the shell's child,
so it survives the shell restarting, the plugin hot-reloading, and the plugin
being removed. The first two are the point — a shell restart or a plugin reload
must not drop everybody's screen share mid-session — but the third means
uninstalling the plugin while it is serving leaves an unauthenticated screen
share running until the machine is rebooted. Nothing is hooked to the widget's
destruction to fix that, because the widget is destroyed on every one of those
reloads too, where surviving is exactly what is wanted. Stopping is asked for
instead: the panel's switches, or omarchy-shell io.github.rsd.omavnc stopAll
— see Removal.
wayvnc missing, and how the panel reports it, is covered under Requirements below.
State is probed with pgrep -af '^wayvnc( |$)': one listing for every output,
taken once for the whole shell rather than once per bar. The panel manages every
output and a bar surface exists per monitor, so a probe per output per bar would
be outputs × monitors processes a cycle — nine here. The listing lives in a
singleton (ServerProbe.qml, made one by the singleton line in the qmldir
beside it — without it the name resolves to the component type, every property
read off it is undefined, and the first binding on one throws) that every bar
binds to, so it is one process however many monitors are plugged in, and no two
bars can disagree about what is running. Only lines whose command actually
begins with wayvnc count, so the bash -lc wrapper that launches a server is
never read as the server itself.
Every line of that listing is a command line, which is a string anybody who can start a process on this machine gets to choose — and the ones this plugin did not start are kept verbatim, to be shown on the panel. So the listing is bounded at both ends: the pgrep runs under a timeout with its output cut to 8 KiB in the pipe, so the shell never holds more than that however much there was to say, and what is parsed out of it is capped again at 64 lines, 512 characters of each, and 16 servers of either kind. Identical unmanaged lines are listed once. None of those ceilings is reachable by a desk that is actually sharing screens; they are there because a listing is not a thing this plugin's own behaviour bounds.
A cut lands wherever it lands, so the last line of a listing that was cut is the
front of somebody's command line rather than a command line. Only terminated
lines are read: pgrep ends every line it prints with a newline, so one that does
not end in a newline is one nothing finished writing, and it is dropped unread
rather than reported as a server nobody is running — which is what it would
otherwise be, both as a row on the panel and as an externalRunning that locks
every serve switch on the machine.
That listing is refreshed every 5s, but only while the answer is worth having: while a panel is open somewhere, or while something is running to watch — a server of ours, or a wayvnc this plugin did not start — and only while at least one bar widget is there to read it, so the singleton a plugin reload strands goes quiet instead of pgrepping for the life of the shell. It is taken immediately when a panel opens, and again 500ms after any toggle, which is when a server has actually come up or gone away. With every panel closed and nothing being served, nothing is probed at all: a shell that shares nothing costs nothing to have this installed.
The trade-off is worth stating. In that idle state nothing is looking, so a
wayvnc somebody starts by hand is not noticed — no bar icon reacts to it, and
the panel's block on serving while somebody else's server holds the ports stays
off. Opening a panel probes at once and puts every bar right before anything in
it can be clicked. The IPC verbs are the exception: serve and serveOutput
open no panel and answer synchronously, so in the idle state they decide on a
listing of no particular age — ten minutes old, if that is when the last panel
closed. A scripted serve on while somebody's hand-started wayvnc holds the
port is answered ok, and the server dies at the bind a moment later with
nothing said, which is the same silence the external-server check exists to
prevent. Open the panel, or toggle from it, when something else may be sharing.
Once anything is running, the poll is on again by the same rule, which is also
what keeps stopAll honest: it acts on the last listing without probing again,
and there is no state where something is served, a widget exists to be asked,
and nothing is refreshing that list.
Stopping still matches one output at a time, with pkill -f '^wayvnc --output=<name> '. That pattern is anchored at both ends — the trailing space is
what stops DP-1 from matching DP-11 — which is why --output must stay the
first argument: any option added to the launch goes after it, or the widget can
no longer find and stop the server it started.
The probe lists every wayvnc, not only the shape above, and each line is
classified: one of that exact shape is the plugin's own, anything else running
wayvnc — started by hand, or from a config file, or with the arguments in
another order — is external.
An external server holds ports this plugin hands out, and a second server on a
taken port dies at the bind with nothing said, so while one is running the panel
shows wayvnc already running outside the plugin — stop it to share from here
with its command line, and no monitor switch can start anything. Switches for
outputs this plugin is already serving stay live, because stopping its own
servers remains safe.
Nothing here ever stops or manages an external server. That is not a policy the
code has to remember: the stop pattern is the exact shape startCommand builds,
so it cannot match one. A server whose command line the stop pattern could not
match — wayvnc --output=DP-1 with nothing after it, say — is classified
external for the same reason, since calling it managed would draw a switch that
cannot switch off.
A wayvnc started by absolute path (/usr/bin/wayvnc …) is not in the listing at
all: the probe is anchored on the command as invoked. It is invisible rather than
misread.
The port is basePort + <index of this output in the alphabetically sorted list of connected outputs>. Alphabetical order is something every instance can agree
on without coordinating, so ports stay stable across restarts. On this machine:
| Output | Port |
|---|---|
| DP-1 | 5900 |
| DP-2 | 5901 |
| eDP-1 | 5902 |
Plugging or unplugging a monitor renumbers every widget at once; a server that was already running keeps its old port until it is restarted.
The Connect tab holds a quick-connect field (host, host:port or host port), a stepper for how many heads the next save should have, and the
bookmark list below it — one row per host, one button per head. Saving a
target from the field adds it as a bookmark; exactly what gets written and how
heads are counted is Bookmarks below. Removing a bookmark is the
trash icon on its row.
The default client missing, and how the panel reports it, is covered under Requirements below.
Bookmarks live in ~/.config/omarchy/omavnc.json, outside the plugin, so they
survive plugin updates and can be hand-edited — a watched FileView hot-reloads
the panel on save. The FileView only watches: the file itself is read by a
short-lived process, which refuses anything at that path that is not a plain file
(a symlink, a FIFO, a directory) and stops at 64 KiB, under a timeout. Left at
its defaults a FileView reads whatever is at its path, all of it, into the
shell and onto the UI thread of every bar on every monitor — preload: false is
what stops it doing that, and there is no property that would have bounded the
read or refused the kind of file. A file that will not parse as JSON shows Could not parse omavnc.json: <error> on the Connect tab; a missing file and one with no hosts
both show the same No bookmarks yet. Add hosts to ~/.config/omarchy/omavnc.json — the panel does not distinguish "not there yet"
from "empty", since both mean the same thing to write. The file itself is only
ever created by a save from the panel; nothing writes it on its own.
- A host takes either
ports(explicit) orbasePort+heads(counted) —ports, if present, wins outright. Either way at most 64 heads are drawn per bookmark, and a bookmark cut at that line says how many it asked for. - A
portslist is read no further than its first 1000 entries. The count a cut row reports comes from walking that list, on the UI thread of every bar on every monitor, every time this file is saved — so the walk is bounded too, an order of magnitude past what is drawn. A longer list is reported as a floor rather than a total:showing 64 of 1000+ heads. - At most 64 bookmarks are drawn, for the same reason one row's heads are capped:
every entry is a row built on the UI thread the moment the panel is opened, and
the panel is the only place a bookmark can be removed from — so a file long
enough to wedge it would be the file it takes to undo it. A longer file draws
its first 64 and says
Showing 64 of 300 bookmarks — the rest are in ~/.config/omarchy/omavnc.json; the rest are edited there. - At most 64 KiB of the file is read at all — the ceiling before every ceiling
above, since those are applied to a file that has already been read and parsed
on the UI thread of every bar, every time it is saved. A longer file is not
parsed at half a document: the panel says
omavnc.json is larger than 64 KiB and was not readand shows no bookmarks. 64 KiB is hundreds of them, an order of magnitude past the 64 that are ever drawn. headNamesentries are stripped of<,>and&before they are drawn. Head buttons are the shell's own component and label themselves through aTextthis plugin cannot reach, and Qt renders a string that looks like markup as rich text — where an<img src=...>in a bookmark name would fetch a URL as the row was drawn. EveryTextthe plugin does own declarestextFormat: Text.PlainText, without exception, so nothing else out of this file is ever sniffed for markup.commandis the client argv: top-level is the default, per-host overrides it. Placeholders{host},{port},{name},{head}. Executed as an argv array, never through a shell.- The default client is
wlvncc(the Wayland-native viewer). TigerVNC'svncviewerworks too, per-host or as the top-level default — it wants{host}::{port}for a raw port rather than a display number. - The IPC
saveverb always writes a one-head bookmark (it calls with noheadsargument at all), regardless of what any open panel's stepper happens to be standing at — see IPC.
Neither binary is installed by the plugin, and neither end reports a missing
one at the point of use — the server is launched detached with its own output
discarded, and the client is exec'd without a shell, so "command not found"
reaches nobody either way. Both are looked up once with command -v instead,
and the panel says what a swallowed failure could not.
- wayvnc — checked once at startup. Until that first lookup answers, wayvnc
is assumed present, so the panel does not open on a warning that is wrong
most of the time. If it comes back missing, the Serve tab reads
wayvnc not installed — omarchy pkg add wayvnc, every monitor switch goes dead (interactive: false— pressing one does nothing, not even a failed launch), and no server is ever attempted. - The default client — same mechanism, re-run whenever the top-level
commandinomavnc.jsonchanges (the file hot-reloads, so the binary to look up can change under it). If it is missing, the Connect tab reads<binary> not installed — omarchy pkg add <binary>(e.g.wlvncc not installed — omarchy pkg add wlvncc) — but nothing is disabled: a bookmark carrying its owncommandmay name a client that is installed, and connecting must stay reachable for it. Known limitation: only the top-level default is ever checked. A per-hostcommandis never looked up, so a bookmark naming a binary that is not installed fails exactly like a missing server does — silently, since it too isexecDetachedwithout a shell.
A wayvnc already running outside the plugin blocks sharing for an unrelated reason — it is running fine, it just holds ports this plugin hands out, and a second server on a taken port dies at the bind with nothing said. See Servers this plugin did not start above for the message and what stays usable while one is up.
The switch at the top of the Connect tab (or r while that tab is showing)
runs hyprctl dispatch hl.dsp.submap("remote"), which hands every key including
SUPER to the focused window, so shortcuts land on the far end instead of the
local compositor. The widget follows Hyprland's submap event, so the icon is
correct even when the mode is toggled from outside the plugin.
The remote submap is yours to register. This plugin ships no Hyprland
config and never edits one, and Hyprland refuses to enter a submap it was never
told about — Cannot set submap remote, submap doesn't exist (wasn't registered!). The dispatch above is detached, so that refusal used to arrive
nowhere: the switch flipped straight back and explained nothing. So the panel
asks first. On every open, and again whenever Hyprland reloads its config
(the configreloaded event), it runs hyprctl -j binds and looks for an entry
whose submap is remote — the binds are where a submap is visible, there
being no listing of submaps themselves. Until one is there the switch is dead,
r does nothing, the row reads Hyprland has no remote submap, and
omarchy-shell io.github.rsd.omavnc remote on answers no remote submap registered in Hyprland — see the README instead of ok.
The submap needs exactly one bind: Right Ctrl, to get back out. While
passthrough is on every other key belongs to the focused window, so without it
the only ways back are the panel switch, omarchy-shell io.github.rsd.omavnc remote off (from a tty or another machine), or the mouse.
A modifier key bound on its own needs both its own modmask and the release
flag — CTRL + Control_R with release = true, or bindr = CTRL, Control_R.
That is what Hyprland's Binds
page calls binding modkeys
only; a plain Control_R with no modmask registers happily and then never
fires.
Lua config (the Omarchy "quattro" layout — ~/.config/hypr/bindings.lua):
hl.define_submap("remote", function()
hl.bind("CTRL + Control_R", hl.dsp.submap("reset"),
{ release = true, description = "Leave remote input mode" })
end)
-- Optional: enter the mode from the keyboard as well as from the panel.
-- The trade-off is that Right Ctrl then belongs to Hyprland and is never
-- delivered to a local app again, so leave it out if anything wants it.
-- hl.bind("CTRL + Control_R", hl.dsp.submap("remote"),
-- { release = true, description = "Remote input mode" })Classic hyprland.conf:
submap = remote
bindr = CTRL, Control_R, submap, reset
submap = reset
# Optional, same trade-off as above:
# bindr = CTRL, Control_R, submap, remoteHere submap = remote opens the block and submap = reset closes it: every
bind between those two lines belongs to the submap, and anything after is
global again. A submap holding nothing but its own exit is Hyprland's own
recipe for "no keybinds until I say so" — which is exactly what passthrough is.
Right Ctrl is the way out, so it is the one key the far end never gets while passthrough is on: use Left Ctrl for the Ctrl-combos you want it to receive.
Then reload Hyprland (hyprctl reload) — the panel notices without a shell
restart. To check by hand:
hyprctl -j binds | grep -c '"submap": "remote"' # 0 means not registeredRead from this widget's entry in ~/.config/omarchy/shell.json
(bar.layout.<section>[] = { "id": "io.github.rsd.omavnc", ... }) — these are
the serve side only; the connect side is configured through omavnc.json
above.
| Key | Default | Meaning |
|---|---|---|
address |
100.101.77.1 |
Bind address passed to wayvnc (Tailscale IP — do not bind this to a public interface, enable_auth=false is typical here) |
basePort |
5900 |
Port of the first output in sorted order |
fps |
60 |
--max-fps |
The manifest's barWidget.defaults and barWidget.schema are documentation
only: this shell version drives widget settings UI from barWidget.settingsForm
and ignores both keys, so the effective defaults are the fallbacks passed to
setting() in BarWidget.qml. To change a value, edit the widget's entry in
shell.json, e.g. { "id": "io.github.rsd.omavnc", "basePort": 6000 }.
address is this machine's own Tailscale IP and is the one value worth changing
per machine when the repo is cloned elsewhere.
The widget owns the io.github.rsd.omavnc IPC target, so scripts and
keybindings can drive it without opening the panel:
omarchy-shell io.github.rsd.omavnc connect 10.0.0.9 5901 # launch the client
omarchy-shell io.github.rsd.omavnc save 10.0.0.9 5900 # add a bookmark (one head)
omarchy-shell io.github.rsd.omavnc remote on # input passthrough
omarchy-shell io.github.rsd.omavnc serveOutput DP-1 on # share a named output
omarchy-shell io.github.rsd.omavnc serve # toggle the answering monitor
omarchy-shell io.github.rsd.omavnc stopAll # stop every server this plugin started
omarchy-shell io.github.rsd.omavnc toggle # the panelsave always writes a one-head bookmark: the head stepper belongs to one open
panel on one monitor, and any instance may be the one that answers here, so a
scripted save would otherwise mean whatever some bar happened to be showing.
Widen it in the panel or in omavnc.json.
remote on answers no remote submap registered in Hyprland — see the README
and dispatches nothing when the submap is missing, so a script gets the refusal
the switch would have shown; remote off is never refused, since leaving needs
no submap and a keyboard nobody can get back is worse than a wasted dispatch.
See Remote input passthrough for the setup.
serveOutput <name> <on|off|toggle> names its output and is the one to script
against. The name has to be a connected output: anything else is answered with
no output named "x" on this machine — connected: DP-1, DP-2 and nothing is
started or stopped. That is a refusal, not pedantry — the stop side turns the
name it is given into the pkill -f pattern that finds the server, and a
pattern is only ever as narrow as the name in it.
stopAll stops every server this plugin has running and answers in words:
stopped 2: DP-1 eDP-1, or nothing running when there was nothing to stop.
The names come from the same pgrep listing the panel is drawn from rather than
from the connected outputs, so it also reaches a server whose monitor has since
been unplugged — still running, still holding its port, and reachable from no row
in the panel. Each name leaves through the same escaped pkill -f pattern every
other stop uses. This is the verb to run before removing the plugin; see
Removal.
serve acts on whichever instance answered, which is not predictable:
a bar surface exists per monitor, several instances register the same target, and
the shell routes to one of them.
Two things about that registration are worth knowing. The shell logs a warning
per losing instance (Handler was registered but will not be used) — first-party
widgets such as omarchy.clock do exactly the same, and it is not a fault. And
the winning handler keeps the target across omarchy-shell shell rescanPlugins,
so a newly added IPC verb will not appear in quickshell ipc show, or be
callable, until the shell restarts — the reload swaps the widget but not the
registration. The verb is not broken; the shell is still advertising the set it
saw when the target was first claimed.
Clone the repo straight into the shell's plugin directory and enable it:
git clone https://github.com/rsd/omaVNC.git ~/.config/omarchy/plugins/io.github.rsd.omavnc
omarchy plugin enable io.github.rsd.omavnc rightThat is the whole install for using the plugin as-is. Working on the plugin itself — editing it in place, running the tests — uses a separate layout; see Dev workflow below.
Servers started here are detached and outlive the plugin (see Serve), so stop the sharing before removing anything:
omarchy-shell io.github.rsd.omavnc stopAll # stops sharing immediately
omarchy plugin remove io.github.rsd.omavnc # asks to confirm; --yes skips
rm ~/.config/omarchy/omavnc.json # optional: the bookmarksstopAll is what makes the removal immediate: it kills every wayvnc this plugin
started, including one whose monitor has since been unplugged, and answers with
what it stopped (stopped 2: DP-1 eDP-1) or nothing running. Removing the
plugin without it leaves those servers up — sharing, unauthenticated — until the
machine is rebooted, because nothing stops them when the widget goes away.
omarchy plugin remove disables the plugin and deletes the folder, and it asks
to confirm first (--yes answers for you). A checkout cloned as above is deleted
outright; a plugin folder that is not a git checkout is moved to a timestamped
backup beside it instead. The bookmarks file is not in that
folder: it lives at ~/.config/omarchy/omavnc.json and survives on purpose, so a
reinstall finds its bookmarks where it left them. Delete it if you want it gone.
If the shell does not recognise stopAll, it is the registration quirk described
under IPC — a verb added by an update is not callable until the shell
restarts. Restart the shell and call it again, or switch every monitor off in the
panel before removing.
This section is for working on the plugin itself, not for installing it — see
Install above for that. The author's checkout lives outside
~/.config/omarchy/plugins and is symlinked into the shell's scan dir instead
of being cloned there directly, so edits happen in one place and the shell
still finds the plugin. For this you would clone the repo to your own working
copy, e.g. ~/Devel/Omarchy/plugins/io.github.rsd.omavnc, and symlink from
there. Symlinked plugin dirs are supported (the CLI walks with find -L), but
no symlink may exist inside the plugin folder, and the directory name has to
equal the manifest id.
ln -s ~/Devel/Omarchy/plugins/io.github.rsd.omavnc ~/.config/omarchy/plugins/io.github.rsd.omavnc
# validate against the real path, not the symlink
omarchy plugin validate ~/Devel/Omarchy/plugins/io.github.rsd.omavnc
# inotify does not watch through the symlink, so rescan by hand after edits
omarchy-shell shell rescanPlugins
# ...but rescanPlugins does NOT re-read changed QML. Quickshell's own reloading
# is deliberately off (see omarchy-launch-shell), so editing a file in place and
# rescanning leaves the old widget running, with the old IPC verbs. Restart to
# actually load an edit:
omarchy-restart-shell
omarchy plugin list --json | grep -i omavnc
omarchy plugin enable io.github.rsd.omavnc rightomarchy plugin list reports active: false for every bar widget — active is
only meaningful for plugins of kind bar. enabled: true is the signal that the
widget is placed in the bar.
tests/run.sh- manifest validation
qmllint— Qt6's (/usr/lib/qt6/bin/qmllint), not/usr/bin/qmllint, which is Qt5's syntax-only checker and rejects perfectly good Qt6 QML. Both QML files, the widget and the probe singleton. Syntax faults fail the build, and so does a missing import (Timerin a file that never importedQtQuick): the linter calls that a warning and exits 0, the engine calls it a file that will not load, which in the shell is a widget that is silently not there. The rest of the warnings are inherent to the bar's injection idiom (baris typedQtObject, so everybar.runreads as a missing property) and first-party widgets produce the same ones.- the model (
tests/model-test.js, plain node) — port maths, the wayvnc command strings and per-output control socket paths, classifying a probe listing into plugin-managed and external servers, binary lookup argv, bookmark head expansion and the save-time head clamp, client argv building, quick-connect parsing, reading ahyprctl -j bindslisting for the passthrough submap (an answer that is missing, failed or unparseable being the same "not registered"), the panel's tab and cursor walk, the ceilings that keep a hand-edited file from freezing every bar (a port list counted only so far and reported as a floor past it, a bookmark list drawn only so far, both timed against a multi-million-entry array), the stripping that keeps markup out of a head label, which serversstopAllderives from a probe listing — including one whose monitor is gone and one whose name is full of regex metacharacters — and when the probe polls at all, including that no served output can leave it asleep (which is whatstopAlltrusting the last listing rests on) and that a probe nothing is reading stops whatever its last listing said. The bounds on what a listing and a config file may cost are covered here too: a listing walked no further than its line ceiling, one absurdly long line kept only to its own, an unmanaged server named twenty times listed once, both lists and thestopAllfan-out stopping at their caps, a listing cut mid-line dropping the line the cut landed inside — asserted at the byte ceiling and again well short of it, since the cut counts bytes and a string counts characters, with the same line read as a server once it is terminated — and the config reader run for real against a plain file, a symlink, a FIFO, a directory, a missing path, one carrying shell syntax and one four times the ceiling - the QML/model bindings (
tests/qml-bindings-test.js) — that every helper the QML calls exists in the JS module, that ids/targets match the manifest, and that the gating the panel relies on is still wired (start refused with no wayvnc or an external one running, stop never gated, warnings shown, each tab's rows out of the other tab's reach, passthrough never entered while its submap is unregistered and never trapped once it is). Both QML files, so the probe that moved into the singleton is still held to the rule that aProcessis only ever armed while idle — that an ask arriving while a listing is in flight is remembered and re-run a turn later rather than dropped, and that it stays a singleton, since without thesingletonline inqmldirthe name is the component type and the panel's first binding on it throws. The polling gate is checked here too: that the repeating timer runs on the model's decision rather than ontrue, that an open panel holds it on and probes as it does, that a bar torn down with its panel open hands the hold back instead of pinning the poll on for the life of the shell, and that a widget attaches when it is built and detaches when it is destroyed, which is what silences the singleton a reload strands. The bookmarks file is held to its split as well: theFileViewcarriespreload: false— without it aFileViewopens and reads the whole file the moment its path is set, whatever handlers it has — and calls neithertext()nordata(), the other way in; the read goes through the model's bounded argv instead, with a buffer filled to the ceiling never reaching the parser. Also the two invariants a later edit could quietly break: everyTextin the panel declaringtextFormat: Text.PlainText— asserted per item, so the nineteenth one added without it fails the suite — andstopAllreachingpkillonly through the escaping path, with the bookmark list drawn from the capped one. qmllint cannot catch these: a renamed helper degrades into an "unqualified access" warning indistinguishable from the ~35 benign ones, and only fails at the click that calls it. - the real
pgrep/pkillbehaviour against stand-in processes that rewrite their own command line with perl (tests/process-pattern-test.sh), including that the broad probe sees a hand-started wayvnc and that stopping a plugin server leaves it alone. Stand-in names carry the run's pid, because pgrep reads the machine-wide process table and two concurrent runs would otherwise see each other.
No VNC server or client is started by the tests.
The port maths, command strings, bookmark expansion, panel navigation, and the
decision of whether the probe should be polling at all live in VncModel.js
precisely so they can be tested without a running shell; BarWidget.qml holds
the wiring and the UI, and ServerProbe.qml the one wayvnc listing every bar
reads.
This plugin is the merge of two: a per-monitor wayvnc toggle, and rsd.vnc, a
bookmarks panel written on the remote machine. Both are in the git history.
MIT — see LICENSE.
{ // Top-level default client, used by any host below that does not name its // own. Placeholders: {host} {port} {name} {head}. Run as an argv array, // never through a shell, so a malformed or hostile entry cannot smuggle in // a second command. Falls back to ["wlvncc", "{host}", "{port}"] if omitted. "command": ["wlvncc", "{host}", "{port}"], "hosts": [ { // Shown as the row's title; falls back to `host` if omitted. "name": "desk", // What every {host} placeholder expands to. "host": "100.101.77.1", // Counted form: heads sit at basePort, basePort+1, basePort+2, ... // Defaults to 5900 if omitted. "basePort": 5900, // How many heads to expand to. Omitted means 1 — a bookmark never // guesses at a multi-monitor machine. The count has to be stated by // hand because nothing here can ask a remote how many outputs it has: // a machine only answers about the ports it happens to be listening // on, and one that is not sharing yet answers nothing at all. The // panel's stepper writes this key on every save — even when it is 1 — // so a written 1 reads as "chosen" and an absent key reads as "nobody // chose, this was hand-written". The stepper itself is capped at 9 // because heads are connected by digit key 1-9 and a tenth would carry // no shortcut, but that cap is a UI limit, not a file one: a // hand-edited value above 9 here is still expanded and stays clickable, // it just has no digit shortcut past 9. The panel draws at most 64 heads // per bookmark whatever this says, and a row that was cut there says so // ("showing 64 of 200 heads") rather than looking narrow — the row is // built on the UI thread of every bar on every monitor each time this // file is saved, so an unbounded count here would freeze the shell on // the save rather than on the click. "heads": 3 }, { // Explicit form: lists ports outright instead of counting from a base. // Takes priority over basePort/heads when both are present. "name": "server", "host": "10.0.0.9", "ports": [5900, 5910], // Labels the head buttons in order; without it they are numbered 1, 2, ... "headNames": ["left", "right"], // Per-host override of the top-level command — e.g. TigerVNC's // vncviewer, which wants a raw port as {host}::{port} rather than a // display number. "command": ["vncviewer", "{host}::{port}"] } ] }