Skip to content

Latest commit

 

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

omaVNC — Oh Mah VNC!

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:

  1. Serve — every connected output with a switch each, showing the address:port it 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.
  2. 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.

Keyboard

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. Enter on 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-9 connect 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 / R toggles 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 the remote submap 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.

Serve

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.

Servers this plugin did not start

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.

Port scheme

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.

Connect

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

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.

{
  // 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}"]
    }
  ]
}
  • A host takes either ports (explicit) or basePort + 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 ports list 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 read and shows no bookmarks. 64 KiB is hundreds of them, an order of magnitude past the 64 that are ever drawn.
  • headNames entries are stripped of <, > and & before they are drawn. Head buttons are the shell's own component and label themselves through a Text this 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. Every Text the plugin does own declares textFormat: Text.PlainText, without exception, so nothing else out of this file is ever sniffed for markup.
  • command is 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's vncviewer works too, per-host or as the top-level default — it wants {host}::{port} for a raw port rather than a display number.
  • The IPC save verb always writes a one-head bookmark (it calls with no heads argument at all), regardless of what any open panel's stepper happens to be standing at — see IPC.

Requirements

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 command in omavnc.json changes (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 own command may 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-host command is never looked up, so a bookmark naming a binary that is not installed fails exactly like a missing server does — silently, since it too is execDetached without 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.

Remote input passthrough

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.

Registering the submap

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, remote

Here 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 registered

Settings

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

IPC

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 panel

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

Install

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 right

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

Removal

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 bookmarks

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

Dev workflow

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 right

omarchy 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

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 (Timer in a file that never imported QtQuick): 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 (bar is typed QtObject, so every bar.run reads 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 a hyprctl -j binds listing 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 servers stopAll derives 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 what stopAll trusting 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 the stopAll fan-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 a Process is 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 the singleton line in qmldir the 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 on true, 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: the FileView carries preload: false — without it a FileView opens and reads the whole file the moment its path is set, whatever handlers it has — and calls neither text() nor data(), 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: every Text in the panel declaring textFormat: Text.PlainText — asserted per item, so the nineteenth one added without it fails the suite — and stopAll reaching pkill only 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/pkill behaviour 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.

History

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.

License

MIT — see LICENSE.

About

Oh Mah VNC!

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages