Note
This repo has been archived by the author. It's no longer receiving updates and remains public just for reference. Please continue to respect LICENSE.
An unofficial Omarchy plugin for controlling studio hardware: lights, stream deck, CamLink and teleprompter.
Note
I am not a programmer. I used Claude Code and Codex to build this for myself. The code and docs are fully generated by AI. Excuse my ignorance to etiquette or common practice here. Any feedback and contributions welcome!
Legend: ✓ yes, ✗ no, ? plausible but unconfirmed.
I could only test hardware that I own.
Note
If you own hardware that is not supported, please feel free to test, add it, and submit a PR.
Layout comes from the elgato-streamdeck crate, so every model it recognises is
laid out correctly with no change here. All are USB vendor 0fd9.
| Device | Grid | USB ID | Supported | Tested | What the plugin does with it |
|---|---|---|---|---|---|
| Stream Deck MK.2 | 5x3 | 0080 |
✓ | ✓ | Key grid, drag to rearrange, pages, multi-key editing, brightness, display power |
| Stream Deck | 5x3 | 0060 |
✓ | ✗ | Same grid and paging as the MK.2 |
| Stream Deck V2 | 5x3 | 006d |
✓ | ✗ | Same grid and paging as the MK.2 |
| Stream Deck Scissor Keys | 5x3 | 00a5 |
✓ | ✗ | Same as the MK.2; only the key switches differ |
| Stream Deck Mini | 3x2 | 0063 |
✓ | ✗ | Three by two grid, paging, same editing surface |
| Stream Deck Mini MK.2 | 3x2 | 0090 |
✓ | ✗ | Three by two grid, paging, same editing surface |
| Stream Deck Mini: Discord Edition | 3x2 | 00b3 |
✓ | ✗ | Driven identically to the Mini |
| Stream Deck XL | 8x4 | 006c |
✓ | ✗ | Eight by four grid, paging, same editing surface |
| Stream Deck XL V2 | 8x4 | 008f |
✓ | ✗ | Eight by four grid, paging, same editing surface |
| Stream Deck Neo | 4x2 | 009a |
✓ | ✗ | Key grid and paging; the two touch keys are not addressable |
| Stream Deck + | 4x2 | 0084 |
✓ | ✗ | Key grid and paging; dials and touch strip are not addressable |
| Stream Deck Pedal | 3 | 0086 |
✓ | ✓ | Tap, hold and double bindings for each of the three pedals |
| Stream Deck Module (6 keys) | 3x2 | 00b8 |
✓ | ✗ | OEM panel-mount module, driven as its desktop equivalent |
| Stream Deck Module (15 keys) | 5x3 | 00b9 |
✓ | ✗ | OEM panel-mount module, driven as its desktop equivalent |
| Stream Deck Module (32 keys) | 8x4 | 00ba |
✓ | ✗ | OEM panel-mount module, driven as its desktop equivalent |
| Stream Deck + XL | 9x4 | 00c6 |
✗ | ✗ | Not yet in the device library, so nothing here recognises it |
| Stream Deck Studio | 16x2 | 00aa |
✗ | ✗ | Not in the device library; the rack unit is unrecognised |
| Stream Deck Mobile, Virtual Stream Deck | - | - | ✗ | ✗ | Software, not USB hardware; out of scope |
Auto-pagination puts its arrows at the two ends of the bottom row, which is sensible on every grid but has only been seen on a 5x3.
Discovered over mDNS as _elg._tcp and driven over the local /elgato/lights
HTTP API on port 9123. The plugin reads and writes on, brightness and
temperature, so temperature-based lights are fully covered and hue-based ones
are not.
| Device | Colour model | Supported | Tested | What the plugin does with it |
|---|---|---|---|---|
| Key Light | Temperature | ✓ | ✓ | Power, brightness, temperature, saved default, rename and reorder |
| Key Light Air | Temperature | ✓ | ✓ | Power, brightness, temperature, saved default, rename and reorder |
| Key Light Mini | Temperature | ✓ | ✗ | Same API; discontinued; battery level is not read |
| Ring Light | Temperature | ✓ | ✗ | Power, brightness and temperature; discontinued |
| Light Strip | Hue | ✓ | ✗ | Power and brightness only; it is hue based, so no colour control |
| Key Light Neo | Temperature | ? | ✗ | Local HTTP API is not confirmed on this model |
| Key Light Air MK.2 | Temperature | ? | ✗ | Local HTTP API is not confirmed on this model |
| Light Strip Pro | Hue, addressable | ? | ✗ | Per-LED addressing does not fit the flat API this plugin speaks |
Three of these are marked ? because the vendor does not document the local API and
the newer models are absent from every source that lists it. One command settles
it on your own hardware, and the answer is worth an issue either way:
curl -s http://<light-ip>:9123/elgato/lightscamlink-ctl matches /dev/v4l/by-id against a device_pattern, so any device the
kernel exposes as a UVC webcam can drive the overlay. You can change it to something else
by editing ~/.config/camlink-ctl/config.toml:
device_pattern = "Facecam"| Device | Supported | Tested | What the plugin does with it |
|---|---|---|---|
| Cam Link 4K | ✓ | ✓ | Overlay placement by corner or dragged area, plus recording |
| Facecam MK.2 | ✓ | ✗ | UVC 1.5; set device_pattern and the overlay works |
| Facecam 4K | ✓ | ✗ | UVC 1.5; set device_pattern and the overlay works |
| Facecam Neo | ✓ | ✗ | UVC 1.5; set device_pattern and the overlay works |
| Facecam | ✓ | ✗ | UVC; discontinued |
| Facecam Pro | ✓ | ✗ | UVC; discontinued |
| Cam Link Pro | ✗ | ✗ | PCIe card exposing DirectShow, not UVC; no Linux support |
These present as video devices too, so the same overlay and recording path applies where the kernel binds them.
| Device | Supported | Tested | What the plugin does with it |
|---|---|---|---|
| Game Capture HD60 X | ✓ | ✗ | UVC compatible; usable as an overlay source |
| Game Capture 4K X | ✓ | ✗ | UVC; needs the mainline USB_QUIRK_NO_BOS fix for full bandwidth |
| Game Capture 4K S | ✓ | ✗ | UVC compliant; usable as an overlay source |
| Video Capture | ✓ | ✗ | Analogue capture bound by em28xx or cx231xx in mainline |
| Game Capture Neo | ? | ✗ | Plug and play, but UVC is not documented |
| Game Capture 4K Pro | ✗ | ✗ | PCIe with a proprietary Windows-only driver |
Nothing in this plugin yet drives audio hardware.
| Device | Supported | Tested | Why not |
|---|---|---|---|
| Wave:3, Wave:3 MK.2 | ✗ | ✗ | USB audio class; capture works, hardware controls do not |
| Wave XLR, Wave XLR MK.2, Wave XLR Pro | ✗ | ✗ | Interface controls need a protocol this plugin does not speak |
| Wave Neo | ✗ | ✗ | USB audio class; capture works, hardware controls do not |
| Wave DX | ✗ | ✗ | A passive XLR microphone; there is nothing to control |
| XLR Dock MK.2 | ✗ | ✗ | Attaches to a Stream Deck +, but exposes no controls here |
| Device | Supported | Tested | Notes |
|---|---|---|---|
| Prompter | ✓ | ✓ | Through the DisplayLink driver; tested on an NVIDIA desktop |
| Prompter XL | ? | ✗ | Same DisplayLink path, untested |
The Prompter has no video input: it is a DisplayLink screen over USB-C, which
only the closed Synaptics driver can talk to. Install the AUR packages
evdi-dkms and displaylink, then the udev rule the installer prints. The rule
moves the virtual DisplayLink card off your seat, so Hyprland never opens it
(on NVIDIA, Hyprland cannot draw to it and would leave a dead monitor), and it
starts prompter-ctl.service whenever the Prompter appears.
prompter-ctl then owns the glass in one of three modes:
script: draws a saved script itself, white on black, with a marker on the
reading line; scrolls only while playing, so it idles at no cost
monitor: adds a 1024x600 Hyprland output named PROMPTER and copies it to the
glass, so any window can go there
off: blanks the glass and removes the PROMPTER outputBrightness dims the picture rather than the backlight: the DisplayLink driver passes no DDC/CI, so the Prompter's backlight cannot be reached from Linux.
Scripts live in ~/.local/share/prompter-ctl/scripts/ as Markdown. Write them in
the panel, or open them in the Omarchy editor from it; saving the loaded script
redraws the glass in place. For monitor mode, give the output its size in
~/.config/hypr/monitors.lua:
hl.monitor({ output = "PROMPTER", mode = "1024x600@60", position = "auto", scale = 1 })Stream Deck and Pedal keys can drive it: exec:prompter-ctl toggle starts and
stops scrolling, and exec:prompter-ctl back and exec:prompter-ctl forward step
one line, for example on the left and right Pedal taps.
omarchy plugin add https://github.com/data-goblin/omgato --enable
~/.config/omarchy/plugins/io.github.data-goblin.omgato/scripts/install
streamdeck-ctl enableomarchy plugin add only clones the plugin and places the widget on the right of
the bar. scripts/install is what builds the workspace, links the five binaries
onto PATH, installs the systemd user units, and applies a starter Stream Deck
layout only if you have no configuration yet; pass --no-preset to skip it.
It links the agent skill only when you pass --with-skill, or run
scripts/install-skill yourself. It prints the one privileged step it will not
take for you: installing the udev rule.
Omarchy installs plugins as git checkouts and never pulls them, so an update is two steps. The second one rebuilds the binaries, and skipping it leaves you running the old ones:
omarchy plugin update io.github.data-goblin.omgato
~/.config/omarchy/plugins/io.github.data-goblin.omgato/scripts/installOrder matters. scripts/uninstall uses the binaries to undo their own Hyprland
edits, so run it before the plugin directory goes away:
~/.config/omarchy/plugins/io.github.data-goblin.omgato/scripts/uninstall
omarchy plugin remove io.github.data-goblin.omgatoWhat survives on purpose: your Stream Deck and Pedal configuration, your light
names and display order, and the udev rule at
/etc/udev/rules.d/70-streamdeck-ctl.rules, which needs root to remove. Pass
--purge to scripts/uninstall to drop the saved state as well.
Ten changes of undo history are kept, and separately you can pin one arrangement as the default and come back to it whenever a session has wandered:
omgato-panel save-default # remember the lights exactly as they are
omgato-panel restore-default # put every light back to it
omgato-panel undo # or step back one change at a timeBoth are buttons in the Key Lights view. Restore is greyed out until a default has been saved.
A page can name its own background, which every key on that page picks up
unless it sets bg itself:
streamdeck-ctl deck page-bg media '#221a2c'
streamdeck-ctl deck page-bg media # back to the deck-wide bg_color[deck.pages.media]
bg = "#221a2c"A tint per page makes the current page obvious at a glance. Generated
pagination keys and empty keys deliberately keep the deck-wide bg_color, so
they stay visually separate from the page's own keys.
Because the colour lives on the page rather than on every button, a script can recolour the deck without touching the layout, for instance to follow the desktop theme from a hook.
Drag a key in the grid and drop it where you want it. Dropping on an empty key moves it there; dropping on another key swaps the two. Nothing is retyped, and the deck's undo button steps the move back. The same thing from a script:
streamdeck-ctl deck move omarchy 3 7 # 3 and 7 trade places
streamdeck-ctl deck move omarchy 3 0 --to-page appsA click still selects a key for editing, so only a drag of a few pixels starts a move, and only real keys move; the paging arrows are generated, not configured.
streamdeck-ctl tui has the same move on the Deck tab: m picks the selected
key up, the arrows and [ ] choose where it lands, including on another page,
then Enter drops it and Esc puts it back.
The panel already retints itself when the theme changes. The keys can too:
streamdeck-ctl deck follow-theme true # opt in; off by default
streamdeck-ctl deck theme # apply now
streamdeck-ctl deck theme --dry-run # print the colours, change nothing
streamdeck-ctl deck follow-theme false # opt out and remove the hookOpting in installs a theme-set hook through omarchy-hook-install, so from
then on the deck recolours itself whenever you switch theme. Opting out removes
it again.
Colours come from omarchy-theme-color, so they resolve exactly as every other
Omarchy consumer sees them. The deck background and text follow the theme's
background and foreground; each page takes a colour of its own.
Choosing those page colours is the fiddly part. Reading four fixed palette keys falls apart on a theme whose palette is warm or monochrome throughout, where they all land on the same colour. So the whole palette is considered and the entries furthest apart perceptually are chosen. Where a palette genuinely holds nothing distinct, Vantablack say, the pages are separated by weight instead, one accent laid on progressively thicker. Either way each colour is eased back toward the background if it would not leave the label readable.
theme_strength in the config, or --strength, sets how strongly a colour
tints its page. It defaults to 0.38; lower is subtler.
streamdeck-ctl deck theme --dry-run --strength 0.5
streamdeck-ctl deck theme --dry-run --colors ~/.config/omarchy/themes/nord/colors.tomlThe panel opens over the top-right corner, which is where the camera overlay
usually sits. Rather than hiding it, the panel tells camlink-ctl the rectangle it
occupies and the overlay slides clear, returning to its own position when the
panel closes. A placement that does not overlap is left exactly where you put
it, and an overlay switched on while the panel is already open still lands clear
of it.
The overlay switch is the toggle in the top right of the Cam Link view.
Each section can be switched off, so a rig with only Key Lights shows only Key Lights. Set them in the widget settings, or from a script:
omarchy bar set io.github.data-goblin.omgato showDeck false --json| Setting | Default | What it controls |
|---|---|---|
showLights |
true |
Brightness, temperature and per-light control for Key Lights |
showDeck |
true |
Key grid, pages and pedal bindings for a Stream Deck |
showCamera |
true |
Camera overlay placement and quick screen recording |
showPrompter |
true |
Prompter scripts, scrolling, mirroring and monitor mode |
manifest.json plugin manifest, entry point and settings schema
ui/Panel.qml the bar widget and panel
src/keylight-ctl/ Key Lights over their local HTTP API
src/streamdeck-ctl/ Stream Deck and Pedal daemons, rendering, TUI, preset
src/camlink-ctl/ Cam Link overlay and status
src/prompter-ctl/ Prompter daemon: scripts, scrolling, monitor mode, udev rule
src/omgato-panel/ status aggregation, names, shortcuts, undo/redo history
src/skill/ the agent skill describing the tools
scripts/install build, link, install units; the skill with --with-skill
scripts/install-skill link only the agent skill
scripts/uninstall reverse the installer
omgato-panel is the only crate the panel asks for state. It gathers one JSON
document from the other four tools, keeps local light names and display order
keyed by MAC so a DHCP lease change cannot orphan them, and owns the undo
histories. Device commands go straight from the panel to
keylight-ctl, streamdeck-ctl and camlink-ctl, so nothing sits between a click and
the hardware.
The plugin writes ~/.config/hypr/omgato-bindings.lua and adds a single guarded
line to your bindings.lua to source it. Your own bindings file is never
rewritten, and removing the plugin cannot break your config. Install them from
the panel, or:
omgato-panel install-shortcuts
omgato-panel set-shortcut --id lights.toggle --keys "SUPER + ALT + L"
omgato-panel uninstall-shortcutsOnly a handful are bound out of the box: one for the lights, none for the deck, and the camera and recording ones. Everything else is listed unbound in the panel, ready to be given a combination. Shortcuts are listed per section, rebindable from the panel, and warn you when a combination is already claimed.
Plugins run unsandboxed with your user permissions, so here is everything this one touches outside its own directory.
~/.local/bin/: symlinks to the five binaries it builds
~/.config/systemd/user/: streamdeck-ctl.service and streamdeck-ctl-deck.service,
the Stream Deck and Pedal daemons, and prompter-ctl.service,
started only by its udev rule
~/.local/share/prompter-ctl/scripts/: your Prompter scripts
~/.local/state/prompter-ctl/settings.json: Prompter mode, script, speed, size, mirror
$XDG_RUNTIME_DIR/prompter-ctl/: the Prompter control socket, mode 0700
~/.config/hypr/: omgato-bindings.lua, plus one guarded pcall line added
to bindings.lua, only when you install shortcuts
~/.local/state/omgato-panel/: light display names, display order, undo history
~/.cache/omgato-panel/: rendered key previews
~/.config/streamdeck-ctl/config.toml: your deck and pedal configuration
~/.config/omarchy/hooks/theme-set.d/omgato-deck.hook: recolours the deck on a
theme change, only after `deck follow-theme true`;
`deck follow-theme false` or scripts/uninstall removes it
$XDG_RUNTIME_DIR/camlink-ctl/: overlay position and pid, cleared on reboot
agent skill directories: a symlink to src/skill/, only when you pass --with-skill
or run scripts/install-skill, and only where the
directory already existsscripts/uninstall reverses all of it. It keeps your device configuration
unless you pass --purge.
No sudoers rule ships with the plugin, and scripts/install performs no
privileged step itself. Three operations need root:
-
installing the udev rule that grants access to Stream Deck hardware. The installer prints these and leaves them to you:
sudo install -m 0644 \ ~/.config/omarchy/plugins/io.github.data-goblin.omgato/src/streamdeck-ctl/udev/70-streamdeck-ctl.rules \ /etc/udev/rules.d/70-streamdeck-ctl.rules sudo udevadm control --reload-rules && sudo udevadm trigger
-
installing the Prompter udev rule, only if you have a Prompter. It assigns the DisplayLink card to a seat of its own, hands it to your user and starts
prompter-ctl.service. The installer prints it with your user name filled in:sed "s/@USER@/$USER/" \ ~/.config/omarchy/plugins/io.github.data-goblin.omgato/src/prompter-ctl/udev/72-prompter-ctl.rules \ | sudo tee /etc/udev/rules.d/72-prompter-ctl.rules >/dev/null sudo udevadm control --reload && sudo systemctl restart displaylink
-
re-authorizing the Cam Link over USB, which clears the wedged state the device enters when its capture stream is cut. This is the one place the code calls
sudofor you:camlink-ctlrunssudo -n tee /sys/bus/usb/devices/<dev>/authorized, writing0and then1. It fires oncamlink-ctl reset, and again insidecamlink-ctl showwhen the previous session left the device wedged. The-nmeans it never prompts, so with no cached credential and no passwordless rule of your own the reset fails, a warning goes to stderr, and the overlay starts anyway.
Runtime pid state lives in $XDG_RUNTIME_DIR/camlink-ctl/, created mode 0700.
With XDG_RUNTIME_DIR unset it falls back to a uid-suffixed directory under the
temp dir, also 0700.
keylight-ctl talks HTTP to Key Lights on your local network, discovered over mDNS
with avahi-browse (_elg._tcp). No other code in the plugin makes network calls.
Discovery accepts at most 32 unique services from 256 KiB of browse output, and
each HTTP JSON response is limited to 16 KiB. Device names, addresses and the
64 KiB cache are validated before retention. The cache is opened without
following symlinks or blocking on special files, then accepted only when that
same descriptor is a regular file owned by the current user. Live probes use a
fixed pool of at most four threads, so an untrusted or noisy LAN peer cannot grow
memory, cached device state or worker creation without bound.
The daemons are ordinary systemd user services; no second Quickshell process is
ever started.
Omarchy Quattro: the Quickshell shell this plugin draws into
Rust 1.89+: to build the workspace (rust)
avahi-browse: Key Light discovery over mDNS (avahi)
mpv: the Cam Link overlay window (mpv)
pw-dump: PipeWire state for camera status (pipewire)
gpu-screen-recorder: screen recording (gpu-screen-recorder)
ffmpeg: recording post-pass (ffmpeg)
slurp: region picking for the overlay and recording (slurp)
fonts: a text font and a Nerd Font for rendering deck keys;
fontconfig supplies a stand-in if the configured paths
do not exist
systemd --user: the Stream Deck, Pedal and Prompter daemons
evdi-dkms, displaylink: the Prompter only (AUR); displaylink is closed source
fc-match: the Prompter script font (fontconfig)Package names in brackets are the Arch packages that provide each binary.
No Stream Deck found: the udev rule is missing. Install it as shown above,
then unplug and replug the device
Keys never update: the daemons are not running. Check with
systemctl --user status streamdeck-ctl-deck
No lights discovered: run keylight-ctl discover. Key Lights answer over mDNS,
so the machine must be on the same subnet as the lights
A light reads unreachable: probes retry inside a 500ms budget, then every command
rediscovers over mDNS once and retries against the
addresses it finds, so a DHCP lease change heals itself.
A light that still reports unreachable is genuinely not
answering. Confirm with keylight-ctl ls --json
Camera overlay is black: the Cam Link is single-open. If a user service holds
it, the overlay borrows the device and gives it back
on hide. A plain process is named rather than killed.
If it stays wedged, camlink-ctl resetIssues and pull requests are welcome, particularly for hardware not marked tested
above. Please run cargo clippy --release --workspace and cargo build --release
before opening one. Release history is in CHANGELOG.md.
MIT. See LICENSE.
Elgato, Stream Deck, Key Light and Cam Link are trademarks of Corsair Gaming and its Elgato brand, which does not sponsor or endorse this project.
This is an unofficial, community-built plugin and is not affiliated with Elgato.



