Trading the Swiss Army Knife for a Paring Knife: Why My Stream Deck Runs on 1,100 Lines of Python
OpenDeck wasn't really at fault for my stutter. But once I'd looked at what 25 buttons were costing (1.2 GB, 15 processes, 10% CPU), I replaced it with a small daemon and a config file in git.
In part 1 I chased a stutter on my desktop down to a Qt and OpenSSL 4.1 bug, triggered by my Stream Deck app polling the compositor three times a second. I patched the app, the stutter went away, and I had a fix I was ready to send upstream.
And then I uninstalled the app anyway.
Let me be clear up front: OpenDeck didn't do anything wrong here, not really. Asking KWin for the active window is how you build per-app profiles on KDE Wayland, and the real bug lives in Qt and OpenSSL. OpenDeck is a genuinely good open-source project that fills a real gap, since Elgato ships nothing for Linux. But the hunt had me staring at a process list, and once I'd looked at what that app was actually doing on my machine, I couldn't unsee it.
A desktop I actually understand
Some context. This year my main desktop, Deadpool, went from Windows to Ubuntu with GNOME, and then, the day before all this started, to CachyOS with KDE Plasma. The second move happened because the Steam Frame's wireless adapter needs a newer kernel than Ubuntu had. But "while I'm in here" turned into the real project: a desktop where I know what every running thing is for.
That's been the fun part of Linux for me. I built a desktop theme from scratch (Walnut: near-black with a brown cast and one copper accent), down to the boot splash, the terminal, the panel clock and the browser. My dotfiles are in git. Workstation scripts live in a repo, not in a forgotten ~/bin. Every time I find something running that I can't explain, I either learn why it's there or I remove it.
So when I stopped OpenDeck to prove it was causing the freezes, the next question was obvious: what is all of this?
What 25 buttons cost
| Part | Memory | CPU | What it was |
|---|---|---|---|
| OpenDeck itself | 256 MB | ~1% | The Rust core |
| Four WebKit processes | ~540 MB | 0% | The settings UI, kept alive while hidden in the tray |
| PipeWire audio plugin (Node) | 148 MB | ~9.5%, constantly | Two volume controls and a mute |
| My two light plugins (Node) | 170 MB | 0% | Hue and Key Light dials |
| A Litra Glow plugin (Node) | 59 MB | 0% | Unused. Installed once, never bound to a key |
| OBS tools, Starter Pack | ~10 MB | 0% | Native, light |
About 1.2 GB across 15 processes, and ~10% of a CPU core forever, plus three script injections into the compositor every second.
And what does my Stream Deck actually do? A Stream Deck + (8 keys, 4 dials, a touch strip) and the 3-button pedal, with about 25 bindings that come down to six kinds of action:
- switch pages,
- run a shell script,
- PipeWire volume and mute,
- HTTP calls to the Hue bridge, the Key Lights and Home Assistant,
- OBS over its websocket,
- draw icons on the keys and readouts on the touch strip.
That's a macro box. OpenDeck is a Swiss Army knife: a plugin host compatible with Elgato's whole marketplace, a drag-and-drop editor, per-app profiles, a store, multi-actions, Wine support for Windows-only plugins. All great features for somebody. I was using the toothpick.
And honestly, the editor was the part I liked least. Dragging actions onto keys, finding the right property inspector, discovering a plugin draws its own images so your custom key art won't stick. I'd already been keeping notes in my homelab docs on what was bound where, because the UI was a worse source of truth than a text file.
The options
- Trim OpenDeck. Remove the unused plugin, replace the busy audio plugin, keep my polling patch. Fixes most of the CPU. Doesn't touch the 540 MB of hidden WebKit, or the clunky editor.
- Switch to another full app, like StreamController. It's good, it supports the Stream Deck + and the pedal, and it would mean the same shape with a new set of quirks, plus porting my light logic into yet another plugin system.
- Write the small thing I actually need.
I went with 3. When I described it, I called it "a sledgehammer for a dead simple macro box with the simplest endpoints."
slimdeck
The hard part, talking to the hardware, is already solved: python-elgato-streamdeck is mature and fully supports the Stream Deck +'s dials and touch strip and the pedal. Everything else is small:
- One Python process, about 1,100 lines. A config file, a handful of action types, a renderer, and thin clients for Hue, Key Lights, Home Assistant, PipeWire and OBS.
- The layout is a TOML file in git. Save it and the deck updates within a second. No editor, no restart. That's the UI now, and I like it much better.
- Pushed state, not polling, wherever possible. OBS has a websocket with events, so the record and scene keys update the instant something changes in OBS. Audio comes from
pactl subscribe. Only the things that can't push (Hue, the Key Lights, Home Assistant) get checked every few seconds, and only for the page currently on screen. - Dials that don't lag or snap back. This was the one real lesson carried over from my OpenDeck plugin: keep the dialled value locally, send only the newest value with one request in flight, and ignore polled state for a couple of seconds after a write. Lights accept commands slower than a dial spins, so naive "one request per tick" dials always rubber-band.
- Same key art. The copper-on-walnut icons I'd already made dropped straight in, and slimdeck draws the titles and text-only keys in the same palette.

The result:
| OpenDeck | slimdeck | |
|---|---|---|
| Memory | ~1.2 GB across 15 processes | ~70 MB |
| CPU at idle | ~10% | near zero |
| Calls into the compositor | 3 script loads a second | none |
| Where the layout lives | JSON behind a GUI | A TOML file in git |
My first reaction after pressing a few keys: "spectacular, very responsive and so much smaller." It runs as a systemd user service now, and OpenDeck is uninstalled.
Things the move shook loose
Replacing something forces you to read everything it did, which is a nice side effect:
- The "work mode" key had a quiet bug. It holds off screen sleep and locking while I'm working. The hold was a background process that inherited whatever launched it, so if it was ever started from a terminal, closing that terminal silently ended the hold while the key still said WORK. Now it runs as its own small systemd unit, and the key asks systemd whether it's on.
- The audio-switch script finally went into git. It had been living only in
~/.local/binfor months. - The Stream Deck docs got rewritten around a config file instead of screenshots of an editor, which is a much better thing to document.
What I gave up
The marketplace and the GUI. If I ever want a new kind of button, I write a small Python class instead of downloading a plugin. For me that's a feature. For most people it isn't, and that's fine: if you have per-app profiles, a dozen plugins and no wish to edit TOML, OpenDeck is exactly the right tool. I'll probably still send them the polling patch, because "don't poll when nothing uses the answer" is worth having no matter whose bug made the polling expensive.
It also means the two OpenDeck plugins I wrote (one for Elgato Key Lights, one for Hue) aren't running on my own desk anymore. One is public, and I'll have to decide how much I want to maintain something I don't use daily.
But the desk is quieter than it's ever been. The mouse is smooth. The deck wakes instantly. And when I look at the process list now, I know what every line is for. That was the point of the whole move.