{"id":51019292,"url":"https://github.com/bryanaustin/streamdeck-basic","last_synced_at":"2026-06-21T15:01:34.761Z","repository":{"id":363447188,"uuid":"1259797411","full_name":"bryanaustin/streamdeck-basic","owner":"bryanaustin","description":"Headless Python-based Stream Deck management with yaml","archived":false,"fork":false,"pushed_at":"2026-06-08T23:35:33.000Z","size":60,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-09T01:21:52.764Z","etag":null,"topics":["elgato-stream-deck","headless","python","python3","yaml"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bryanaustin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T21:43:48.000Z","updated_at":"2026-06-08T23:35:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bryanaustin/streamdeck-basic","commit_stats":null,"previous_names":["bryanaustin/streamdeck-basic"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bryanaustin/streamdeck-basic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanaustin%2Fstreamdeck-basic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanaustin%2Fstreamdeck-basic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanaustin%2Fstreamdeck-basic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanaustin%2Fstreamdeck-basic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bryanaustin","download_url":"https://codeload.github.com/bryanaustin/streamdeck-basic/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanaustin%2Fstreamdeck-basic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34614593,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elgato-stream-deck","headless","python","python3","yaml"],"created_at":"2026-06-21T15:01:33.771Z","updated_at":"2026-06-21T15:01:34.754Z","avatar_url":"https://github.com/bryanaustin.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Stream Deck Basic\n\nDrive an Elgato Stream Deck from a simple YAML file on **Linux**. It is fully\n**headless** — there is no GUI, tray app, or configuration window. The entire\nlayout is **declared in one YAML file** and applied directly to the device, so it\nruns happily on a server, a kiosk, or any always-on box with no desktop attached.\nEach button can show an image and a text label, and run a bash command when\npressed. Buttons can also switch between **pages**, so you get folder-style\nnavigation. The app is built to **survive disconnects gracefully** — unplug the\ndeck, suspend/resume the machine, replug it, and it just reconnects and carries on.\n\nBuilt on the [`python-elgato-streamdeck`](https://github.com/abcminiuser/python-elgato-streamdeck)\nlibrary (`pip install streamdeck`).\n\n## Features\n\n- **Headless, YAML-driven** — no GUI; the whole layout lives in one declarative\n  file, ideal for servers and kiosks. Edit the file, restart, done.\n- YAML configuration with multiple pages and `goto` navigation.\n- Per-button **image** (auto-scaled), **text label**, and **bash command**.\n- **Execution states** — command buttons show a spinner while running, then a\n  success/error image; a second press stops a running command.\n- **Animated keys** — drop in a GIF/APNG/animated-WebP and it plays automatically.\n- Press or release triggers (`trigger: press|release`).\n- Resilient to USB disconnects and suspend/resume — automatic reconnect that\n  re-applies the whole layout.\n- Runs in the foreground or as a systemd **user** service.\n\n## Requirements\n\n- Linux, Python 3.9+.\n- A HID backend system library — on Debian/Ubuntu:\n\n  ```sh\n  sudo apt install libhidapi-libusb0      # or: libhidapi-hidraw0\n  ```\n\n## Install\n\n```sh\n# from the project directory\npython3 -m venv .venv \u0026\u0026 . .venv/bin/activate\npip install .                # or: pip install -r requirements.txt\n```\n\nThis installs the `streamdeck-basic` console command.\n\n### udev rule (non-root access)\n\nWithout this you would have to run as root. Install the bundled rule for Elgato\ndevices (USB vendor `0fd9`):\n\n```sh\nsudo cp udev/70-streamdeck.rules /etc/udev/rules.d/\nsudo udevadm control --reload-rules\nsudo udevadm trigger\n```\n\nThen unplug and replug the Stream Deck.\n\n## Configure\n\nStart from the example and edit it:\n\n```sh\nmkdir -p ~/.config/streamdeck\ncp config/example.yaml ~/.config/streamdeck/config.yaml\n# generate the placeholder icons referenced by the example (optional):\npython assets/generate_placeholders.py\n```\n\n### Config reference\n\n```yaml\nbrightness: 50              # 0-100\ndevice:\n  serial: null             # null = first deck found; or a specific serial string\ntiming:\n  poll_interval: 1.0       # connection health-check cadence (seconds)\n  reconnect_interval: 2.0  # how often to look for the deck while disconnected\ndefaults:                  # appearance defaults, overridable per button later\n  font: /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\n  font_size: 14\n  text_color: white\n  background: black\n  margins: [0, 0, 20, 0]   # top, right, bottom, left (bottom gap leaves room for the label)\nstart_page: main\npages:\n  main:\n    - key: 0\n      label: Apps\n      action: {goto: apps}        # navigate to another page\n    - key: 1\n      image: ../assets/terminal.png\n      label: Terminal\n      command: x-terminal-emulator\n  apps:\n    - key: 5\n      label: Back\n      action: {goto: main}\n```\n\n**Button fields:** `key` (required, 0-based), `image` (optional path, resolved\nrelative to the config file), `label` (optional text), `command` (optional bash,\nrun via the shell), `action: {goto: \u003cpage\u003e}` (optional navigation), and `trigger`\n(`press` default, or `release`). A button may have both `command` and a `goto`.\nKeys not listed on a page are left blank; keys beyond your device's range are\nskipped with a warning.\n\n**Animated keys:** if `image` points to a multi-frame file (animated GIF, APNG,\nor animated WebP) the key plays the animation while that page is visible — looping\nfrom the start each time you navigate to it. By default it uses the file's own\nper-frame timing; tune it with an `animation` block, or freeze the key to its\nfirst frame with `animate: false`:\n\n```yaml\n- key: 5\n  image: ../assets/spinner.gif\n  label: Busy\n  animation:\n    fps: 15      # override the embedded frame rate (optional)\n    loop: true   # repeat forever (default); false stops on the last frame\n```\n\n**Execution states:** a button with a `command` reflects the command's progress.\nPressing it starts the command and shows a **running** image (a built-in animated\nspinner unless you supply one); when it finishes the key shows a **completed**\n(exit 0) or **errored** (non-zero) image and stays there until pressed again, which\nre-runs the command. Pressing a button *while it is running* stops the command (and\nits child processes) and returns the key to its idle `image`. Each state image is\noptional — `running` defaults to the spinner, and `errored`/`completed` fall back to\nthe idle `image` when omitted:\n\n```yaml\n- key: 2\n  label: Build\n  command: make -C \"$HOME/project\"\n  image: ../assets/terminal.png    # idle / default\n  states:\n    running:   {image: ../assets/spinner.gif}   # optional; omit for the built-in spinner\n    errored:   {image: ../assets/error.png}\n    completed: {image: ../assets/ok.png}\n```\n\n## Run\n\n```sh\nstreamdeck-basic --config ~/.config/streamdeck/config.yaml\n```\n\nWithout `--config` it looks at `$STREAMDECK_CONFIG`, then `./config.yaml`, then\n`~/.config/streamdeck/config.yaml`. Use `--log-level DEBUG` for more detail.\nPress `Ctrl-C` to stop; the deck is reset on exit.\n\n## Run as a systemd user service\n\n```sh\npip install --user .\nmkdir -p ~/.config/systemd/user\ncp systemd/streamdeck.service ~/.config/systemd/user/\nsystemctl --user daemon-reload\nsystemctl --user enable --now streamdeck.service\nsystemctl --user status streamdeck.service\n```\n\nOn a headless/always-on box also run `sudo loginctl enable-linger \"$USER\"`.\n`Restart=on-failure` is just an outer safety net — the app reconnects on its own.\n\n## How disconnect handling works\n\nThe Stream Deck library has no hotplug support: when a device is unplugged its\ninternal read thread stops silently while the object still *looks* alive, and the\nnext write raises `TransportError`. Stream Deck Basic handles all of this itself:\n\n- a **supervisor loop** re-enumerates devices forever and re-applies the full\n  layout on every (re)connect;\n- a **health loop** polls `connected()` to detect silent disconnects;\n- every device write is guarded, so a disconnect mid-update drops cleanly back to\n  reconnecting instead of crashing;\n- the button callback is fully guarded so a disconnect during a press can't kill\n  input handling.\n\nThe same path covers laptop **suspend/resume**.\n\n## Troubleshooting\n\n- **No device found / permission denied:** confirm the udev rule is installed and\n  you replugged; check `lsusb | grep -i elgato`. Without the rule, try `sudo` once\n  to confirm the device otherwise works.\n- **GUI commands don't launch under systemd:** GUI apps need the graphical\n  session environment (`DISPLAY`/`WAYLAND_DISPLAY`). Running as a *user* service\n  inside your graphical session is the simplest fix.\n- **Labels render in a fallback font:** install the DejaVu fonts\n  (`sudo apt install fonts-dejavu-core`) or point `defaults.font` at a TTF you have.\n\n## Development\n\n```sh\npip install -e \".[dev]\"\npytest                      # config loader/validator tests (no hardware needed)\n```\n\n## Scope\n\nTargets the standard key grid (Original/MK.2/Mini/XL — auto-adapts to key count).\nStream Deck **Plus** dials/touchscreen and **Neo** extra controls are not handled\nin this version, and the layout is read once at startup (no live config reload).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbryanaustin%2Fstreamdeck-basic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbryanaustin%2Fstreamdeck-basic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbryanaustin%2Fstreamdeck-basic/lists"}