├── tests ├── __init__.py ├── assets │ ├── chat.png │ ├── dog.gif │ ├── smile.jpg │ ├── smile.png │ ├── firework.gif │ └── smile.svg ├── conftest.py ├── test_stream_deck_monitor.py ├── test_dimmer.py ├── test_api.py ├── test_gui.py └── test_filter.py ├── streamdeck_ui ├── display │ ├── __init__.py │ ├── empty_filter.py │ ├── pulse_filter.py │ ├── keypress_filter.py │ ├── filter.py │ ├── pipeline.py │ ├── text_filter.py │ ├── image_filter.py │ └── display_grid.py ├── logo.png ├── icons │ ├── cross.png │ ├── gear.png │ └── vertical-align.png ├── __init__.py ├── fonts │ └── roboto │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-BlackItalic.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-LightItalic.ttf │ │ ├── Roboto-ThinItalic.ttf │ │ ├── Roboto-MediumItalic.ttf │ │ └── LICENSE.txt ├── resources.qrc ├── config.py ├── semaphore.py ├── dimmer.py ├── settings.ui ├── ui_settings.py ├── mock_streamdeck.py ├── stream_deck_monitor.py ├── resources_rc.py ├── ui_main.py └── main.ui ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md └── workflows │ └── test.yaml ├── art ├── logo.png ├── nope.gif ├── example.gif ├── logo_large.png └── logo_large.xcf ├── scripts ├── done.sh ├── test.sh ├── streamdeck.service ├── clean.sh ├── compile_ui.sh ├── fedora_install.sh ├── ubuntu_install.sh └── lint.sh ├── .coveragerc ├── setup.cfg ├── docs ├── contributing │ ├── 4.-acknowledgements.md │ ├── 1.-contributing-guide.md │ ├── 2.-coding-standard.md │ └── 3.-code-of-conduct.md ├── installation │ ├── fedora.md │ ├── opensuse.md │ ├── arch.md │ ├── ubuntu.md │ ├── centos.md │ └── systemd.md └── troubleshooting.md ├── .cruft.json ├── LICENSE ├── .gitignore ├── pyproject.toml ├── CHANGELOG.md ├── notebooks ├── .ipynb_checkpoints │ └── device_information-checkpoint.ipynb └── device_information.ipynb ├── README.md └── README-de.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /streamdeck_ui/display/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: "timothycrosley" 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /art/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/art/logo.png -------------------------------------------------------------------------------- /art/nope.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/art/nope.gif -------------------------------------------------------------------------------- /art/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/art/example.gif -------------------------------------------------------------------------------- /art/logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/art/logo_large.png -------------------------------------------------------------------------------- /art/logo_large.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/art/logo_large.xcf -------------------------------------------------------------------------------- /scripts/done.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | ./scripts/clean.sh 5 | ./scripts/test.sh 6 | -------------------------------------------------------------------------------- /streamdeck_ui/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/logo.png -------------------------------------------------------------------------------- /tests/assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/tests/assets/chat.png -------------------------------------------------------------------------------- /tests/assets/dog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/tests/assets/dog.gif -------------------------------------------------------------------------------- /tests/assets/smile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/tests/assets/smile.jpg -------------------------------------------------------------------------------- /tests/assets/smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/tests/assets/smile.png -------------------------------------------------------------------------------- /tests/assets/firework.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/tests/assets/firework.gif -------------------------------------------------------------------------------- /streamdeck_ui/icons/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/icons/cross.png -------------------------------------------------------------------------------- /streamdeck_ui/icons/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/icons/gear.png -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | omit = 5 | *tests* 6 | streamdeck_ui/ui_main.py 7 | 8 | -------------------------------------------------------------------------------- /streamdeck_ui/__init__.py: -------------------------------------------------------------------------------- 1 | """**streamdeck_ui** 2 | 3 | A Linux compatible UI for the Elgato Stream Deck. 4 | """ 5 | __version__ = "1.0.2" 6 | -------------------------------------------------------------------------------- /streamdeck_ui/icons/vertical-align.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/icons/vertical-align.png -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 200 3 | extend-ignore = 4 | E203 # https://github.com/psf/black/blob/master/docs/the_black_code_style.md#slices 5 | -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/streamdeck-ui/HEAD/streamdeck_ui/fonts/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | ./scripts/lint.sh 5 | poetry run pytest -s --cov=streamdeck_ui/ --cov=tests --cov-report=term-missing ${@-} --cov-report html 6 | -------------------------------------------------------------------------------- /streamdeck_ui/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/cross.png 4 | icons/gear.png 5 | icons/vertical-align.png 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | from streamdeck_ui import api 4 | 5 | api.DeviceManager = MagicMock() 6 | api.StreamDeck = MagicMock() 7 | api.ImageHelpers = MagicMock() 8 | api.export_config = MagicMock() 9 | -------------------------------------------------------------------------------- /scripts/streamdeck.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=A Linux compatible UI for the Elgato Stream Deck. 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart= -n 7 | Restart=on-failure 8 | 9 | [Install] 10 | WantedBy=default.target 11 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | poetry run isort streamdeck_ui/ tests/ --skip ui_main.py --skip resources_rc.py --skip ui_settings.py 5 | poetry run black streamdeck_ui/ tests/ --exclude 'ui_main.py|resources_rc.py|ui_settings.py' --line-length 200 6 | -------------------------------------------------------------------------------- /scripts/compile_ui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | poetry run pyside6-uic streamdeck_ui/main.ui --from-imports streamdeck_ui -o streamdeck_ui/ui_main.py 4 | poetry run pyside6-uic streamdeck_ui/settings.ui --from-imports streamdeck_ui -o streamdeck_ui/ui_settings.py 5 | poetry run pyside6-rcc streamdeck_ui/resources.qrc -o streamdeck_ui/resources_rc.py -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: STOP! We are no longer accepting bug reports 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **STOP** 11 | 12 | This project is no longer actively maintained and your bug report will be ignored. 13 | Please see https://github.com/timothycrosley/streamdeck-ui/issues/360 14 | 15 | -------------------------------------------------------------------------------- /scripts/fedora_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | echo "Installing libraries" 3 | sudo dnf install python3-pip python3-devel hidapi 4 | 5 | echo "Adding udev rules and reloading" 6 | sudo tee /etc/udev/rules.d/70-streamdeck.rules << EOF 7 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess" 8 | EOF 9 | sudo udevadm trigger 10 | 11 | pip3 install --user streamdeck_ui 12 | echo "If the installation was successful, run 'streamdeck' to start." 13 | -------------------------------------------------------------------------------- /scripts/ubuntu_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | echo "Installing libraries" 3 | sudo apt install python3-pip libhidapi-libusb0 libxcb-xinerama0 4 | 5 | echo "Adding udev rules and reloading" 6 | sudo tee /etc/udev/rules.d/70-streamdeck.rules << EOF 7 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess" 8 | EOF 9 | sudo udevadm trigger 10 | 11 | pip3 install --user streamdeck_ui 12 | echo "If the installation was successful, run 'streamdeck' to start." 13 | -------------------------------------------------------------------------------- /tests/test_stream_deck_monitor.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from time import sleep 3 | from unittest import TestCase 4 | from unittest.mock import patch 5 | 6 | from streamdeck_ui.stream_deck_monitor import StreamDeckMonitor 7 | 8 | 9 | def test_start_stop(): 10 | lock: threading.Lock = threading.Lock() 11 | monitor = StreamDeckMonitor(lock, lambda serial, deck: None, lambda serial: None) 12 | monitor.start() 13 | # FIXME: Need to find a better way to find "evidence" that it worked 14 | sleep(1) 15 | monitor.stop() 16 | -------------------------------------------------------------------------------- /streamdeck_ui/config.py: -------------------------------------------------------------------------------- 1 | """Defines shared configuration variables for the streamdeck_ui project""" 2 | import os 3 | 4 | PROJECT_PATH = os.path.dirname(os.path.abspath(__file__)) 5 | LOGO = os.path.join(PROJECT_PATH, "logo.png") 6 | FONTS_PATH = os.path.join(PROJECT_PATH, "fonts") 7 | DEFAULT_FONT = os.path.join("roboto", "Roboto-Regular.ttf") 8 | STATE_FILE = os.environ.get("STREAMDECK_UI_CONFIG", os.path.expanduser("~/.streamdeck_ui.json")) 9 | CONFIG_FILE_VERSION = 1 # Update only if backward incompatible changes are made to the config file 10 | -------------------------------------------------------------------------------- /docs/contributing/4.-acknowledgements.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | =================== 3 | 4 | ## Core Developers 5 | - Pieter Venter (@dodgyrabbit) 6 | - Timothy Crosley (@timothycrosley) 7 | 8 | ## Notable Bug Reporters 9 | - 10 | 11 | ## Code Contributors 12 | - 13 | 14 | ## Documenters 15 | - @xorbital 16 | - Chris Rogers (@chrisprad) 17 | - @simonCor 18 | - Afonso F. Garcia (@AfonsoFGarcia) 19 | 20 | -------------------------------------------- 21 | 22 | A sincere thanks to everyone who helps make streamdeck_ui into a great Python3 project! 23 | 24 | ~Timothy Crosley 25 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/timothycrosley/cookiecutter-python/", 3 | "commit": "71391fd9999067ef4b38aa05e7116087fac431f8", 4 | "context": { 5 | "cookiecutter": { 6 | "full_name": "Timothy Crosley", 7 | "email": "timothy.crosley@gmail.com", 8 | "github_username": "timothycrosley", 9 | "project_name": "streamdeck_ui", 10 | "description": "A service, Web Interface, and UI for interacting with your computer using a Stream Deck", 11 | "version": "0.0.1", 12 | "_template": "https://github.com/timothycrosley/cookiecutter-python/" 13 | } 14 | }, 15 | "checkout": null 16 | } 17 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | poetry run cruft check 5 | poetry run mypy --ignore-missing-imports streamdeck_ui/ --exclude 'ui_main.py|resources_rc.py|ui_settings.py' 6 | poetry run isort --check --diff streamdeck_ui/ tests/ --skip ui_main.py --skip resources_rc.py --skip ui_settings.py --line-length 200 7 | poetry run black --check --diff streamdeck_ui/ tests/ --exclude 'ui_main.py|resources_rc.py|ui_settings.py' --line-length 200 8 | poetry run flake8 streamdeck_ui/ tests/ --ignore F403,F401,W503 --exclude ui_main.py,resources_rc.py,ui_settings.py 9 | poetry run safety check -i 39462 -i 40291 -i 44715 -i 47794 -i 51457 10 | poetry run bandit -r streamdeck_ui/ 11 | -------------------------------------------------------------------------------- /tests/test_dimmer.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import pytest 4 | 5 | from streamdeck_ui.dimmer import Dimmer 6 | 7 | 8 | @pytest.mark.parametrize("brightness, dim_percent, dimmed", [(100, 0, 0), (100, 50, 50), (100, 100, 100), (50, 50, 25)]) 9 | def test_dim_to_zero(brightness: int, dim_percent: int, dimmed: int): 10 | call_count = 0 11 | last_value = -1 12 | 13 | def dim(value: int): 14 | nonlocal last_value 15 | nonlocal call_count 16 | call_count = call_count + 1 17 | last_value = value 18 | 19 | dimmer = Dimmer(1, brightness, dim_percent, dim) 20 | dimmer.reset() 21 | sleep(1.1) 22 | assert dimmer.dimmed 23 | assert call_count == 2 24 | assert last_value == dimmed 25 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from hypothesis_auto import auto_pytest_magic 4 | 5 | from streamdeck_ui import api 6 | 7 | server = api.StreamDeckServer() 8 | 9 | auto_pytest_magic(server.set_button_command) 10 | auto_pytest_magic(server.get_button_command) 11 | auto_pytest_magic(server.set_button_switch_page) 12 | auto_pytest_magic(server.get_button_switch_page) 13 | auto_pytest_magic(server.set_button_keys) 14 | auto_pytest_magic(server.get_button_keys) 15 | auto_pytest_magic(server.set_button_write) 16 | auto_pytest_magic(server.get_button_write) 17 | auto_pytest_magic(server.set_brightness, auto_allow_exceptions_=(KeyError,)) 18 | auto_pytest_magic(server.get_brightness) 19 | auto_pytest_magic(server.change_brightness, auto_allow_exceptions_=(KeyError,)) 20 | auto_pytest_magic(server.get_page) 21 | -------------------------------------------------------------------------------- /tests/test_gui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest.mock import MagicMock 3 | 4 | import pytest 5 | from hypothesis_auto import auto_pytest_magic 6 | 7 | from streamdeck_ui import api, gui 8 | 9 | pytestmark = pytest.mark.skipif(sys.platform == "linux", reason="tests for mac only due to travis issues") 10 | 11 | gui.selected_button = MagicMock() 12 | 13 | auto_pytest_magic(gui.update_button_text, ui=MagicMock()) 14 | auto_pytest_magic(gui.update_button_command, ui=MagicMock()) 15 | auto_pytest_magic(gui.update_button_keys, ui=MagicMock()) 16 | auto_pytest_magic(gui.update_button_write, ui=MagicMock()) 17 | auto_pytest_magic(gui.update_change_brightness, ui=MagicMock()) 18 | auto_pytest_magic(gui.change_page, ui=MagicMock()) 19 | auto_pytest_magic(gui.set_brightness, ui=MagicMock(), auto_allow_exceptions_=(KeyError,)) 20 | auto_pytest_magic(gui.queue_update_button_text, ui=MagicMock()) 21 | 22 | 23 | def test_start(): 24 | api.decks = {None: MagicMock()} 25 | gui.start(_exit=True) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Timothy Crosley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /streamdeck_ui/semaphore.py: -------------------------------------------------------------------------------- 1 | import fcntl 2 | import os 3 | 4 | 5 | class SemaphoreAcquireError(Exception): 6 | pass 7 | 8 | 9 | class Semaphore: 10 | def __init__(self, semaphore_file): 11 | self.semaphore_file = semaphore_file 12 | self.semaphore_fd = None 13 | 14 | def __enter__(self): 15 | # create the semaphore file if it does not exist 16 | if not os.path.exists(self.semaphore_file): 17 | open(self.semaphore_file, "w").close() 18 | 19 | # open the file descriptor for the semaphore file 20 | self.semaphore_fd = os.open(self.semaphore_file, os.O_CREAT) 21 | 22 | try: 23 | fcntl.flock(self.semaphore_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 24 | except OSError: 25 | raise SemaphoreAcquireError("Could not acquire semaphore lock") 26 | 27 | def __exit__(self, exc_type, exc_value, traceback): 28 | # release the semaphore 29 | fcntl.flock(self.semaphore_fd, fcntl.LOCK_UN) 30 | 31 | # close the file descriptor 32 | os.close(self.semaphore_fd) 33 | 34 | # reset the file descriptor to None 35 | self.semaphore_fd = None 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | .DS_Store 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | MANIFEST 21 | 22 | # Installer logs 23 | pip-log.txt 24 | npm-debug.log 25 | pip-selfcheck.json 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | htmlcov 32 | .cache 33 | .pytest_cache 34 | .mypy_cache 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | 44 | # SQLite 45 | test_exp_framework 46 | 47 | # npm 48 | node_modules/ 49 | 50 | # dolphin 51 | .directory 52 | libpeerconnection.log 53 | 54 | # setuptools 55 | dist 56 | 57 | # IDE Files 58 | atlassian-ide-plugin.xml 59 | .idea/ 60 | *.swp 61 | *.kate-swp 62 | .ropeproject/ 63 | 64 | # Python3 Venv Files 65 | .venv/ 66 | bin/ 67 | include/ 68 | lib/ 69 | lib64 70 | pyvenv.cfg 71 | share/ 72 | venv/ 73 | .python-version 74 | 75 | # Cython 76 | *.c 77 | 78 | # Emacs backup 79 | *~ 80 | 81 | # VSCode 82 | /.vscode 83 | 84 | # Automatically generated files 85 | docs/preconvert 86 | site/ 87 | out 88 | .hypothesis 89 | 90 | # QT 91 | .qt_for_python/ 92 | -------------------------------------------------------------------------------- /docs/installation/fedora.md: -------------------------------------------------------------------------------- 1 | # Installing on Fedora 2 | This has been tested on Fedora 36, 37. 3 | 4 | ## Install hidapi 5 | ``` bash 6 | sudo dnf install python3-pip python3-devel hidapi 7 | ``` 8 | 9 | ## Upgrade pip 10 | You need to upgrade pip, using pip. In my experience, old versions of pip may fail to properly install some of the required Python dependencies. 11 | ``` 12 | python -m pip install --upgrade pip 13 | ``` 14 | ## Configure access to Elgato devices 15 | The following will create a file called `/etc/udev/rules.d/70-streamdeck.rules` and add the following text to it: `SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess"`. Creating this file adds a udev rule that provides your user with access to USB devices created by Elgato. 16 | ``` bash 17 | sudo sh -c 'echo "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"0fd9\", TAG+=\"uaccess\"" > /etc/udev/rules.d/70-streamdeck.rules' 18 | ``` 19 | For the rule to take immediate effect, run the following command: 20 | ``` bash 21 | sudo udevadm trigger 22 | ``` 23 | If the software is having problems later to detect the Stream Deck, you can try unplugging/plugging it back in. 24 | 25 | ## Install Stream Deck UI 26 | ``` 27 | python -m pip install streamdeck-ui --user 28 | ``` 29 | See [system tray](../troubleshooting.md#no-system-tray-indicator) installation. 30 | 31 | Launch with 32 | ``` 33 | streamdeck 34 | ``` 35 | See [troubleshooting](../troubleshooting.md) for tips if you're stuck. 36 | -------------------------------------------------------------------------------- /docs/installation/opensuse.md: -------------------------------------------------------------------------------- 1 | # Installing on openSUSE 2 | This has been tested on Tumbleweed. 3 | 4 | ## Install hidapi 5 | ``` console 6 | sudo zypper install libhidapi-libusb0 python310-devel kernel-devel 7 | ``` 8 | > `python310-devel` and `kernel-devel` are required because pip is going to have to build `evdev`. 9 | 10 | ## Upgrade pip 11 | You may need to upgrade pip, using pip. 12 | ``` 13 | python3 -m pip install --upgrade pip 14 | ``` 15 | ## Configure access to Elgato devices 16 | The following will create a file called `/etc/udev/rules.d/70-streamdeck.rules` and add the following text to it: `SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess"`. Creating this file adds a udev rule that provides your user with access to USB devices created by Elgato. 17 | ``` bash 18 | sudo sh -c 'echo "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"0fd9\", TAG+=\"uaccess\"" > /etc/udev/rules.d/70-streamdeck.rules' 19 | ``` 20 | For the rule to take immediate effect, run the following command: 21 | ``` bash 22 | sudo udevadm trigger 23 | ``` 24 | If the software is having problems later to detect the Stream Deck, you can try unplugging/plugging it back in. 25 | 26 | ## Install Stream Deck UI 27 | ``` 28 | python3 -m pip install streamdeck-ui --user 29 | ``` 30 | 31 | See [system tray](../troubleshooting.md#no-system-tray-indicator) installation. 32 | 33 | Launch with 34 | ``` 35 | streamdeck 36 | ``` 37 | See [troubleshooting](../troubleshooting.md) for tips if you're stuck. -------------------------------------------------------------------------------- /streamdeck_ui/display/empty_filter.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from typing import Callable, Tuple 3 | 4 | from PIL import Image 5 | 6 | from streamdeck_ui.display import filter 7 | 8 | 9 | class EmptyFilter(filter.Filter): 10 | """ 11 | This is the empty (base) filter where all pipelines start from. 12 | 13 | :param str name: The name of the filter. The name is useful for debugging purposes. 14 | """ 15 | 16 | def __init__(self): 17 | super(EmptyFilter, self).__init__() 18 | 19 | # For EmptyFilter - create a unique hashcode based on the name of the type 20 | # This will create "some value" that uniquely identifies this filter output 21 | # Since it never changes, this works. 22 | # Calculate it once for speed 23 | self.hashcode = hash(self.__class__) 24 | 25 | def initialize(self, size: Tuple[int, int]): 26 | self.image = Image.new("RGB", size) 27 | 28 | def transform(self, get_input: Callable[[], Image.Image], get_output: Callable[[int], Image.Image], input_changed: bool, time: Fraction) -> Tuple[Image.Image, int]: 29 | """ 30 | Returns an empty Image object. 31 | 32 | :param Fraction time: The current time in seconds, expressed as a fractional number since 33 | the start of the pipeline. 34 | """ 35 | if not input_changed: 36 | return (None, self.hashcode) 37 | return ((self.image), self.hashcode) 38 | -------------------------------------------------------------------------------- /docs/installation/arch.md: -------------------------------------------------------------------------------- 1 | # Installing on Arch 2 | This has been tested on Arch with XFCE, Manjaro in Feb 2023. 3 | 4 | ## Install hidapi 5 | ``` console 6 | sudo pacman -S hidapi python-pip qt6-base 7 | ``` 8 | ## Set path 9 | You need to add `~/.local/bin` to your path. Be sure to add this to your `.bashrc` (or equivalent) file so it automatically sets it for you in future. 10 | ``` console 11 | PATH=$PATH:$HOME/.local/bin 12 | ``` 13 | 14 | ## Upgrade pip 15 | You may need to upgrade pip, using pip. On Arch this is usually not required. 16 | Setuptools is required but may not be installed on Arch. 17 | ``` 18 | python -m pip install --upgrade pip 19 | python -m pip install setuptools 20 | ``` 21 | ## Configure access to Elgato devices 22 | The following will create a file called `/etc/udev/rules.d/70-streamdeck.rules` and add the following text to it: `SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess"`. Creating this file adds a udev rule that provides your user with access to USB devices created by Elgato. 23 | ``` bash 24 | sudo sh -c 'echo "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"0fd9\", TAG+=\"uaccess\"" > /etc/udev/rules.d/70-streamdeck.rules' 25 | ``` 26 | For the rule to take immediate effect, run the following command: 27 | ``` bash 28 | sudo udevadm trigger 29 | ``` 30 | If the software is having problems later to detect the Stream Deck, you can try unplugging/plugging it back in. 31 | 32 | ## Install Stream Deck UI 33 | ``` 34 | python -m pip install streamdeck-ui --user 35 | ``` 36 | 37 | Launch with 38 | ``` 39 | streamdeck 40 | ``` 41 | See [troubleshooting](../troubleshooting.md) for tips if you're stuck. 42 | -------------------------------------------------------------------------------- /streamdeck_ui/display/pulse_filter.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from typing import Callable, Tuple 3 | 4 | from PIL import Image, ImageEnhance 5 | 6 | from streamdeck_ui.display.filter import Filter 7 | 8 | 9 | class PulseFilter(Filter): 10 | def __init__(self): 11 | super(PulseFilter, self).__init__() 12 | self.last_time: Fraction = Fraction() 13 | self.pulse_delay = 0.5 14 | self.brightness = 1 15 | self.dim_brightness = 0.5 16 | self.filter_hash = hash(self.__class__) 17 | 18 | def initialize(self, size: Tuple[int, int]): 19 | pass 20 | 21 | def transform(self, get_input: Callable[[], Image.Image], get_output: Callable[[int], Image.Image], input_changed: bool, time: Fraction) -> Tuple[Image.Image, int]: 22 | brightness_changed = False 23 | if time - self.last_time > self.pulse_delay: 24 | brightness_changed = True 25 | self.last_time = time 26 | 27 | if self.brightness == self.dim_brightness: 28 | self.brightness = 1 29 | else: 30 | self.brightness = self.dim_brightness 31 | 32 | frame_hash = hash((self.filter_hash, self.brightness)) 33 | if input_changed or brightness_changed: 34 | image = get_output(frame_hash) 35 | if image: 36 | return (image, frame_hash) 37 | 38 | input = get_input() 39 | enhancer = ImageEnhance.Brightness(input) 40 | input = enhancer.enhance(self.brightness) 41 | return (input, frame_hash) 42 | return (None, frame_hash) 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "streamdeck_ui" 3 | version = "2.0.16" 4 | description = "A service, Web Interface, and UI for interacting with your computer using a Stream Deck" 5 | authors = ["Timothy Crosley "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.8,<3.12" 11 | streamdeck = "^0.9.3" 12 | pillow = "^9.4.0" 13 | pynput = "^1.7.6" 14 | pyside6 = "^6.4.2" 15 | CairoSVG = "^2.5.2" 16 | filetype = "^1.0.10" 17 | python-xlib = "^0.33" 18 | 19 | [tool.poetry.dev-dependencies] 20 | vulture = ">=1.0" 21 | bandit = ">=1.6" 22 | safety = ">=1.8" 23 | isort = ">=5.3" 24 | flake8-bugbear = ">=19.8" 25 | black = {version = ">=18.3-alpha.0", allow-prereleases = true} 26 | mypy = ">=0.730.0" 27 | ipython = ">=7.7" 28 | pytest = ">=5.0" 29 | pytest-cov = ">=2.7" 30 | pytest-mock = ">=1.10" 31 | pep8-naming = ">=0.8.2" 32 | portray = ">=1.3.0" 33 | cruft = ">=2.2" 34 | jupyter = "^1.0" 35 | ipdb = "^0.12.2" 36 | hypothesis-auto = "^1.1" 37 | pydantic = "^1.6" 38 | numpy = "^1.22.1" 39 | types-pkg-resources = "^0.1.3" 40 | 41 | [tool.poetry.scripts] 42 | streamdeck = "streamdeck_ui.gui:start" 43 | [build-system] 44 | requires = ["poetry-core"] 45 | build-backend = "poetry.core.masonry.api" 46 | 47 | [tool.black] 48 | line-length = 100 49 | 50 | [tool.isort] 51 | profile = "hug" 52 | 53 | [tool.portray] 54 | output_dir = "site" 55 | include_reference_documentation = false 56 | extra_dirs = ["art", "images", "media", "scripts"] 57 | 58 | [tool.portray.mkdocs] 59 | repo_url = "https://github.com/timothycrosley/streamdeck-ui" 60 | edit_uri = "edit/master/" 61 | -------------------------------------------------------------------------------- /docs/installation/ubuntu.md: -------------------------------------------------------------------------------- 1 | # Installing on Ubuntu 2 | This has been tested on Ubuntu 2004, 2204, Linux Mint 20. 3 | 4 | ## Install hidapi 5 | ``` console 6 | sudo apt install libhidapi-libusb0 python3-pip 7 | ``` 8 | 9 | > Note that for version `2.0.6` and below, you also need to install `libxcb-xinerama0` (include it with apt in the line above). 10 | ## Set path 11 | You need to add `~/.local/bin` to your path. Be sure to add this to your `.bashrc` (or equivalent) file so it automatically sets it for you in future. 12 | ``` console 13 | PATH=$PATH:$HOME/.local/bin 14 | ``` 15 | ## Upgrade pip 16 | You need to upgrade pip, using pip. In my experience, old versions of pip may fail to properly install some of the required Python dependencies. 17 | ``` 18 | python3 -m pip install --upgrade pip 19 | ``` 20 | 21 | 22 | ## Configure access to Elgato devices 23 | The following will create a file called `/etc/udev/rules.d/70-streamdeck.rules` and add the following text to it: `SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess"`. Creating this file adds a udev rule that provides your user with access to USB devices created by Elgato. 24 | ``` bash 25 | sudo sh -c 'echo "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"0fd9\", TAG+=\"uaccess\"" > /etc/udev/rules.d/70-streamdeck.rules' 26 | ``` 27 | For the rule to take immediate effect, run the following command: 28 | ``` bash 29 | sudo udevadm trigger 30 | ``` 31 | If the software is having problems later to detect the Stream Deck, you can try unplugging/plugging it back in. 32 | 33 | ## Install Stream Deck UI 34 | ``` 35 | python3 -m pip install streamdeck-ui --user 36 | ``` 37 | 38 | Launch with 39 | ``` 40 | streamdeck 41 | ``` 42 | See [troubleshooting](../troubleshooting.md) for tips if you're stuck. 43 | -------------------------------------------------------------------------------- /tests/test_filter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fractions import Fraction 3 | 4 | import pytest 5 | 6 | from streamdeck_ui.display import empty_filter, image_filter, pipeline 7 | 8 | 9 | def get_asset(file_name): 10 | """Resolve the given file name to a full path.""" 11 | return os.path.join(os.path.dirname(__file__), "assets", file_name) 12 | 13 | 14 | def test_default(): 15 | default = empty_filter.EmptyFilter() 16 | default.initialize((16, 16)) 17 | image, _ = default.transform(lambda: None, lambda hash: None, True, Fraction(0)) 18 | assert image is not None 19 | assert default.is_complete is False 20 | 21 | 22 | # TODO: Incorrect file locations default to "empty". Probably better that it throws. 23 | @pytest.mark.parametrize("image", ["smile.png", "smile.jpg", "smile.svg", "dog.gif"]) 24 | def test_image_filter(image: str): 25 | size = (72, 72) 26 | pipe = pipeline.Pipeline() 27 | 28 | filter = empty_filter.EmptyFilter() 29 | filter.initialize(size) 30 | 31 | pipe.add(filter) 32 | time = Fraction(0) 33 | 34 | filter = image_filter.ImageFilter(get_asset(image)) 35 | filter.initialize(size) 36 | pipe.add(filter) 37 | image, _ = pipe.execute(time) 38 | image, _ = pipe.execute(1) 39 | image, _ = pipe.execute(2) 40 | 41 | 42 | def test_pipeline(): 43 | size = (72, 72) 44 | pipe = pipeline.Pipeline() 45 | 46 | filter = empty_filter.EmptyFilter() 47 | filter.initialize(size) 48 | pipe.add(filter) 49 | 50 | filter = image_filter.ImageFilter(os.path.join(os.path.dirname(__file__), "assets/smile.png")) 51 | filter.initialize(size) 52 | pipe.add(filter) 53 | time = Fraction(0) 54 | final_image, _ = pipe.execute(time) 55 | assert final_image is not None 56 | -------------------------------------------------------------------------------- /streamdeck_ui/display/keypress_filter.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from typing import Callable, Tuple 3 | 4 | from PIL import Image, ImageEnhance 5 | 6 | from streamdeck_ui.display.filter import Filter 7 | 8 | 9 | class KeypressFilter(Filter): 10 | """This filter is applied whenever a key is being pressed""" 11 | 12 | def __init__(self): 13 | super(KeypressFilter, self).__init__() 14 | self.last_time: Fraction = Fraction() 15 | self.brightness = 1 16 | self.dim_brightness = 0.5 17 | self.filter_hash = hash(self.__class__) 18 | self.active = False 19 | self.last_state = False 20 | 21 | def initialize(self, size: Tuple[int, int]): 22 | self.blank_image = Image.new("RGB", size) 23 | self.size = size 24 | pass 25 | 26 | def transform(self, get_input: Callable[[], Image.Image], get_output: Callable[[int], Image.Image], input_changed: bool, time: Fraction) -> Tuple[Image.Image, int]: 27 | frame_hash = hash((self.filter_hash, self.active)) 28 | if input_changed or self.active != self.last_state: 29 | self.last_state = self.active 30 | image = get_output(frame_hash) 31 | if image: 32 | return (image, frame_hash) 33 | 34 | input = get_input() 35 | if self.active: 36 | input = get_input() 37 | background = self.blank_image.copy() 38 | input.thumbnail((self.size[0] - 10, self.size[1] - 10), Image.ANTIALIAS) 39 | # Reduce the image by 10px 40 | 41 | enhancer = ImageEnhance.Brightness(input) 42 | input = enhancer.enhance(2) 43 | # Light it up a bit 44 | 45 | background.paste(input, (5, 5)) 46 | # Center the image 47 | 48 | return (background, frame_hash) 49 | else: 50 | return (input, frame_hash) 51 | return (None, frame_hash) 52 | -------------------------------------------------------------------------------- /docs/installation/centos.md: -------------------------------------------------------------------------------- 1 | # Installing on CentOS 2 | This has been tested on CentOS 7, 8. 3 | 4 | ## Install hidapi 5 | ``` bash 6 | sudo yum install epel-release 7 | sudo yum update 8 | sudo yum install hidapi 9 | ``` 10 | 11 | > ### Note for CentOS7 12 | > 13 | > If you're having trouble installing hdapi, try installing the epel from the Fedora site as follows: 14 | > 15 | ``` console 16 | sudo rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 17 | ``` 18 | > and try the hdapi install again. 19 | 20 | ## Install python 3.8 21 | CentOS 7/8 ships with Python 3.6. We need to build version 3.8 (or later if you prefer). 22 | ``` bash 23 | sudo yum -y groupinstall "Development Tools" 24 | sudo yum -y install openssl-devel bzip2-devel libffi-devel 25 | wget https://www.python.org/ftp/python/3.8.9/Python-3.8.9.tgz 26 | tar xvf Python-3.8.9.tgz 27 | cd Python-3.8.9/ 28 | ./configure --enable-optimizations 29 | sudo make altinstall 30 | ``` 31 | ## Upgrade pip 32 | You need to upgrade pip, using pip. In my experience, old versions of pip may fail to properly install some of the required Python dependencies. 33 | ``` 34 | python3.8 -m pip install --upgrade pip 35 | ``` 36 | ## Configure access to Elgato devices 37 | The following will create a file called `/etc/udev/rules.d/70-streamdeck.rules` and add the following text to it: `SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess"`. Creating this file adds a udev rule that provides your user with access to USB devices created by Elgato. 38 | ``` bash 39 | sudo sh -c 'echo "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"0fd9\", TAG+=\"uaccess\"" > /etc/udev/rules.d/70-streamdeck.rules' 40 | ``` 41 | For the rule to take immediate effect, run the following command: 42 | ``` bash 43 | sudo udevadm trigger 44 | ``` 45 | If the software is having problems later to detect the Stream Deck, you can try unplugging/plugging it back in. 46 | 47 | ## Install Stream Deck UI 48 | ``` 49 | python3.8 -m pip install streamdeck-ui --user 50 | ``` 51 | Launch with 52 | ``` 53 | streamdeck 54 | ``` 55 | See [troubleshooting](../troubleshooting.md) for tips if you're stuck. -------------------------------------------------------------------------------- /docs/contributing/1.-contributing-guide.md: -------------------------------------------------------------------------------- 1 | Contributing to streamdeck_ui 2 | ======== 3 | 4 | Looking for a useful open source project to contribute to? 5 | Want your contributions to be warmly welcomed and acknowledged? 6 | Welcome! You have found the right place. 7 | 8 | ## Getting streamdeck_ui set up for local development 9 | The first step when contributing to any project is getting it set up on your local machine. streamdeck_ui aims to make this as simple as possible. 10 | 11 | Account Requirements: 12 | 13 | - [A valid GitHub account](https://github.com/join) 14 | 15 | Base System Requirements: 16 | 17 | - Python3.8+ 18 | - poetry 19 | - bash or a bash compatible shell (should be auto-installed on Linux / Mac) 20 | 21 | Once you have verified that you system matches the base requirements you can start to get the project working by following these steps: 22 | 23 | 1. [Fork the project on GitHub](https://github.com/timothycrosley/streamdeck_ui/fork). 24 | 2. Clone your fork to your local file system: 25 | `git clone https://github.com/$GITHUB_ACCOUNT/streamdeck_ui.git` 26 | 3. `cd streamdeck_ui` 27 | 4. `poetry install` 28 | 29 | ## Making a contribution 30 | Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request: 31 | 32 | 1. Check the [issues page](https://github.com/timothycrosley/streamdeck_ui/issues) on GitHub to see if the task you want to complete is listed there. 33 | - If it's listed there, write a comment letting others know you are working on it. 34 | - If it's not listed in GitHub issues, go ahead and log a new issue. Then add a comment letting everyone know you have it under control. 35 | - If you're not sure if it's something that is good for the main streamdeck_ui project and want immediate feedback, you can discuss it [here](https://gitter.im/timothycrosley/streamdeck-ui). 36 | 2. Create an issue branch for your local work `git checkout -b issue/$ISSUE-NUMBER`. 37 | 3. Do your magic here. 38 | 4. Ensure your code matches the [HOPE-8 Coding Standard](https://github.com/hugapi/HOPE/blob/master/all/HOPE-8--Style-Guide-for-Hug-Code.md#hope-8----style-guide-for-hug-code) used by the project. 39 | 5. Submit a pull request to the main project repository via GitHub. 40 | 41 | Thanks for the contribution! It will quickly get reviewed, and, once accepted, will result in your name being added to the acknowledgments list :). 42 | 43 | ## Thank you! 44 | I can not tell you how thankful I am for the hard work done by streamdeck_ui contributors like *you*. 45 | 46 | Thank you! 47 | 48 | ~Timothy Crosley 49 | 50 | -------------------------------------------------------------------------------- /streamdeck_ui/display/filter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from fractions import Fraction 3 | from typing import Callable, Tuple 4 | 5 | from PIL import Image 6 | 7 | 8 | class Filter(ABC): 9 | """ 10 | A filter transforms a given input image to the desired output image. A filter can signal that it 11 | is complete and will be removed from the pipeline. 12 | 13 | :param str name: The name of the filter. The name is useful for debugging purposes. 14 | """ 15 | 16 | size: Tuple[int, int] 17 | "The image size (width, height) in pixels that this filter transforms." 18 | 19 | is_complete: bool 20 | "Indicates if the filter is complete and should no longer be processed." 21 | 22 | def __init__(self): 23 | self.is_complete = False 24 | 25 | @abstractmethod 26 | def initialize(self, size: Tuple[int, int]): 27 | """Initializes the filter with the provided frame size. Since the construction 28 | of the filter can happen before the size of the display is known, initialization 29 | should be done here. 30 | 31 | :param size: The filter image size 32 | :type size: Tuple[int, int] 33 | """ 34 | pass 35 | 36 | @abstractmethod 37 | def transform(self, get_input: Callable[[], Image.Image], get_output: Callable[[int], Image.Image], input_changed: bool, time: Fraction) -> Tuple[Image.Image, int]: 38 | """ 39 | Transforms the given input image to the desired output image. 40 | The default behaviour is to return the orignal image. 41 | 42 | :param Callable[[], PIL.Image] get_input: A function that returns the input image to transform. Note that calling 43 | this will create a copy of the input image, and it is safe to manipulate directly. 44 | 45 | :param Callable[[int], PIL.Image] get_output: Provide the hashcode of the new frame and it will 46 | return the output frame if it already exists. This avoids having to redraw an output frame that is already 47 | cached. 48 | 49 | :param bool input_changed: True if the input is different from previous run, False otherwise. 50 | When true, you have to return an Image. 51 | 52 | :param Fraction time: The current time in seconds, expressed as a fractional number since 53 | the start of the pipeline. 54 | 55 | :rtype: PIL.Image 56 | :return: The transformed output image. If this filter did not modify the input, return None. This signals to the 57 | pipeline manager that there was no change and a cached version will be moved to the next stage. 58 | """ 59 | pass 60 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | > Note you may need to use `python`, `python3` or `python3.8` in the commands shown below, depending on your distribution. The examples simply use `python` for simplicity's sake. 4 | 5 | ## Basics 6 | There are **four** important things you need to get a working system. 7 | 8 | 1. You need a working Python 3.8 or higher with pip installed. 9 | 2. You need to install hidapi. 10 | 3. You need a udev rule that allows access to your Stream Deck. 11 | 4. You need to install streamdeck-ui and all its dependencies with pip. 12 | 13 | ## Key Press and Write Text do not work 14 | Streamdeck uses [pynput](https://github.com/moses-palmer/pynput) for simulating **Key Presses** but it was not designed for [Wayland](https://github.com/moses-palmer/pynput/issues/189). Generally your results will be good when using X, but it seems like most new releases of Linux are switching away from it. 15 | 16 | ## ImportError 17 | 18 | If you get an error such as: 19 | ``` 20 | ImportError: cannot import name 'QtWidgets' from 'PySide6' 21 | ``` 22 | This usually means a problem with PySide6. Try resolving with this: 23 | ``` console 24 | python -m pip install --force-reinstall --no-cache-dir pyside6 25 | ``` 26 | 27 | ## No System Tray Indicator 28 | You may receive an error like this on start-up: 29 | ``` 30 | qt.core.qobject.connect: QObject::connect: No such signal QPlatformNativeInterface::systemTrayWindowChanged(QScreen*) 31 | ``` 32 | 33 | This is because gnome does not provide a System Tray out the box and you will need an extension 34 | [KStatusNotifierItem/AppIndicator Support](https://extensions.gnome.org/extension/615/appindicator-support/) to make the system tray icon show up. 35 | 36 | ## Could not load the Qt platform plugin "xcb" 37 | 38 | You may get the following error: 39 | ``` 40 | qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found. 41 | This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem. 42 | ``` 43 | On Ubuntu, resolve this problem by installing: 44 | ``` console 45 | sudo apt install libxcb-xinerama0 46 | ``` 47 | > You may encounter a similar error where the cause is "libxcb-cursor.so.0: cannot open shared object file: No such file or directory". This can be resolved by installing the `libxcb-cursor0` package. 48 | 49 | On Arch, resolve this problem by installing: 50 | ``` 51 | sudo pacman -S qt6-base 52 | ``` 53 | You could also try [`qt5-x11extras`](https://archlinux.org/packages/extra/x86_64/qt5-x11extras/) if `qt6-base` didn't work for you. 54 | 55 | ## ModuleNotFoundError: No module named 'pkg_resources' 56 | This module is part of `setuptools` but may be missing on your system. 57 | ``` 58 | python -m pip install setuptools 59 | ``` 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/installation/systemd.md: -------------------------------------------------------------------------------- 1 | # systemd installation 2 | Once you have a working streamdeck_ui installation, you can also configure it to run as a systemd user service. It will automatically run when you login and you can manage it using `systemctl`. 3 | ## Installation 4 | Make a directory where the systemd user configuration will be stored. 5 | ``` bash 6 | mkdir -p $HOME/.local/share/systemd/user/ 7 | ``` 8 | Create (an empty) configuration file. 9 | ``` bash 10 | touch $HOME/.local/share/systemd/user/streamdeck.service 11 | ``` 12 | Use your favorite editor and paste the following content into the `streamdeck.service` file (rembember replace ``): 13 | ``` 14 | [Unit] 15 | Description=A Linux compatible UI for the Elgato Stream Deck. 16 | 17 | [Service] 18 | Type=simple 19 | ExecStart=/home//.local/bin/streamdeck -n 20 | Restart=on-failure 21 | 22 | [Install] 23 | WantedBy=default.target 24 | ``` 25 | To make the configuration take effect and install the service into systemd, run the following commands: 26 | ``` 27 | systemctl --user daemon-reload 28 | systemctl --user enable streamdeck 29 | ``` 30 | > Tip: Before you continue, make sure you are not already running streamdeck_ui. If it's open, click File > Exit. Only one instance of streamdeck_ui can be running at a time. 31 | 32 | You are now all set. To start the service, run the following command: 33 | ``` 34 | systemctl --user start streamdeck 35 | ``` 36 | There are some additional commands that may be useful. 37 | 38 | To see the status of the service, run: 39 | ``` 40 | systemctl --user status streamdeck 41 | ``` 42 | To review the service log file (newest entries at the top) for troubleshooting, run: 43 | ``` 44 | journalctl --user -r 45 | ``` 46 | To stop the service, run: 47 | ``` 48 | systemctl --user stop streamdeck 49 | ``` 50 | ## Installation in virtual environment 51 | If you have installed streamdeck_ui in a virtual environment, you can still use it in a systemd service. 52 | 53 | Assume you are in the following directory: 54 | ``` 55 | /home/johnsmith/streamdeck_ui 56 | ``` 57 | You create a virtual environment, called `.venv` and activate it as follows: 58 | ``` 59 | python3 -m venv .venv 60 | source .venv/bin/activate 61 | ``` 62 | and finally install streamdeck_ui like this: 63 | ``` 64 | python3 -m pip install streamdeck_ui 65 | ``` 66 | 67 | > Your virtual environment is now configured and located in `/home/johnsmith/streamdeck_ui/.venv` 68 | 69 | The steps for installing the systemd service is exactly the same. The only difference is you have to point the `ExecStart=` to the streamdeck executable inside the virtual environment, like so: 70 | ``` 71 | ExecStart=/home/johnsmith/streamdeck_ui/.venv/bin/streamdeck -n 72 | ``` 73 | ## Uninstalling 74 | The following steps will stop, disable, remove the configuration file and finally reload the settings: 75 | ``` 76 | systemctl --user stop streamdeck 77 | systemctl --user disable streamdeck 78 | rm $HOME/.local/share/systemd/user/streamdeck.service 79 | systemctl --user daemon-reload 80 | ``` -------------------------------------------------------------------------------- /streamdeck_ui/display/pipeline.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from typing import Dict, List, Tuple 3 | 4 | from PIL.Image import Image 5 | 6 | from streamdeck_ui.display.filter import Filter 7 | 8 | 9 | class Pipeline: 10 | def __init__(self) -> None: 11 | self.filters: List[Tuple[Filter, Image]] = [] 12 | self.first_run = True 13 | self.output_cache: Dict[int, Image] = {} 14 | 15 | def add(self, filter: Filter) -> None: 16 | self.filters.append((filter, None)) 17 | self.first_run = True 18 | 19 | def execute(self, time: Fraction) -> Tuple[Image, int]: 20 | """ 21 | Executes all the filter in the pipeline and returns the final image, or None if the pipeline did not yield any changes. 22 | """ 23 | 24 | image: Image = None 25 | is_modified = False 26 | pipeline_hash = 0 27 | 28 | # To avoid flake8 B023 (https://docs.python-guide.org/writing/gotchas/#late-binding-closures), we need to 29 | # capture the variable going into the lambda. However, as a result of that, we have a lambda that 30 | # technically takes an argument (with a default) that does not match the signature we declared 31 | # for the transform() method. There are likely other solutions to avoid the warning this produces, 32 | # like using functools.partial, but this needs to be investigated. 33 | 34 | for i, (current_filter, cached) in enumerate(self.filters): 35 | (image, hashcode) = current_filter.transform( 36 | lambda input_image=image: input_image.copy(), # type: ignore [misc] 37 | lambda output_hash, pipeline_hash=pipeline_hash: self.output_cache.get(hash((output_hash, pipeline_hash)), None), # type: ignore [misc] 38 | is_modified | self.first_run, 39 | time, 40 | ) 41 | 42 | pipeline_hash = hash((hashcode, pipeline_hash)) 43 | 44 | if not image: 45 | # Filter indicated that it did NOT change anything, pull up the last 46 | # cached value for the next step in the pipeline 47 | image = cached 48 | else: 49 | # The filter changed the image, cache it for future use 50 | # Update tuple with cached image 51 | self.filters[i] = (current_filter, image) 52 | is_modified = True 53 | 54 | # Store this image with pipeline hash if we haven't seen it. 55 | if pipeline_hash not in self.output_cache: 56 | self.output_cache[pipeline_hash] = image 57 | 58 | if self.first_run: 59 | # Force an update the first time the pipeline runs 60 | is_modified = True 61 | self.first_run = False 62 | 63 | return (image if is_modified else None, pipeline_hash) 64 | 65 | def last_result(self) -> Image: 66 | """ 67 | Returns the last known output of the pipeline 68 | """ 69 | return self.filters[-1][1] 70 | -------------------------------------------------------------------------------- /docs/contributing/2.-coding-standard.md: -------------------------------------------------------------------------------- 1 | # HOPE 8 -- Style Guide for Hug Code 2 | 3 | | | | 4 | | ------------| ------------------------------------------- | 5 | | HOPE: | 8 | 6 | | Title: | Style Guide for Hug Code | 7 | | Author(s): | Timothy Crosley | 8 | | Status: | Active | 9 | | Type: | Process | 10 | | Created: | 19-May-2019 | 11 | | Updated: | 17-August-2019 | 12 | 13 | ## Introduction 14 | 15 | This document gives coding conventions for the Hug code comprising the Hug core as well as all official interfaces, extensions, and plugins for the framework. 16 | Optionally, projects that use Hug are encouraged to follow this HOPE and link to it as a reference. 17 | 18 | ## PEP 8 Foundation 19 | 20 | All guidelines in this document are in addition to those defined in Python's [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/) guidelines. 21 | 22 | ## Line Length 23 | 24 | Too short of lines discourage descriptive variable names where they otherwise make sense. 25 | Too long of lines reduce overall readability and make it hard to compare 2 files side by side. 26 | There is no perfect number: but for Hug, we've decided to cap the lines at 100 characters. 27 | 28 | ## Descriptive Variable names 29 | 30 | Naming things is hard. Hug has a few strict guidelines on the usage of variable names, which hopefully will reduce some of the guesswork: 31 | - No one character variable names. 32 | - Except for x, y, and z as coordinates. 33 | - It's not okay to override built-in functions. 34 | - Except for `id`. Guido himself thought that shouldn't have been moved to the system module. It's too commonly used, and alternatives feel very artificial. 35 | - Avoid Acronyms, Abbreviations, or any other short forms - unless they are almost universally understand. 36 | 37 | ## Adding new modules 38 | 39 | New modules added to the a project that follows the HOPE-8 standard should all live directly within the base `PROJECT_NAME/` directory without nesting. If the modules are meant only for internal use within the project, they should be prefixed with a leading underscore. For example, def _internal_function. Modules should contain a docstring at the top that gives a general explanation of the purpose and then restates the project's use of the MIT license. 40 | There should be a `tests/test_$MODULE_NAME.py` file created to correspond to every new module that contains test coverage for the module. Ideally, tests should be 1:1 (one test object per code object, one test method per code method) to the extent cleanly possible. 41 | 42 | ## Automated Code Cleaners 43 | 44 | All code submitted to Hug should be formatted using Black and isort. 45 | Black should be run with the line length set to 100, and isort with Black compatible settings in place. 46 | 47 | ## Automated Code Linting 48 | 49 | All code submitted to hug should run through the following tools: 50 | 51 | - Black and isort verification. 52 | - Flake8 53 | - flake8-bugbear 54 | - Bandit 55 | - pep8-naming 56 | - vulture 57 | - safety 58 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | #---------------------------------------------- 11 | # check-out repo and set-up python 12 | #---------------------------------------------- 13 | - name: Check out repository 14 | uses: actions/checkout@v2 15 | - name: Set up python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.8 19 | #---------------------------------------------- 20 | # ----- install & configure poetry ----- 21 | #---------------------------------------------- 22 | - name: Install Poetry 23 | uses: snok/install-poetry@v1 24 | with: 25 | virtualenvs-create: true 26 | virtualenvs-in-project: true 27 | #---------------------------------------------- 28 | # load cached venv if cache exists 29 | #---------------------------------------------- 30 | - name: Load cached venv 31 | id: cached-poetry-dependencies 32 | uses: actions/cache@v2 33 | with: 34 | path: .venv 35 | key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} 36 | #---------------------------------------------- 37 | # install dependencies if cache does not exist 38 | #---------------------------------------------- 39 | - name: Install dependencies 40 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 41 | run: poetry install --no-interaction --no-root --no-ansi 42 | #---------------------------------------------- 43 | # install your root project, if required 44 | #---------------------------------------------- 45 | - name: Install library 46 | run: poetry install --no-interaction --no-ansi 47 | #---------------------------------------------- 48 | # Install required libraries for ImageQt 49 | # This avoids: 50 | # ImportError: cannot import name 'ImageQt' from 'PIL.ImageQt' 51 | #---------------------------------------------- 52 | - name: Install library 53 | run: | 54 | sudo apt-get update 55 | sudo apt-get install -y libegl1 libgl1 libxkbcommon0 56 | #---------------------------------------------- 57 | # run Lint tests 58 | #---------------------------------------------- 59 | - name: Lint 60 | run: ./scripts/lint.sh 61 | #---------------------------------------------- 62 | # run test suite and output coverage file 63 | #---------------------------------------------- 64 | - name: Test 65 | run: bash -c "poetry run pytest tests/ -s --cov=streamdeck_ui/ --cov-report=term-missing ${@-}" 66 | #---------------------------------------------- 67 | # generate coverage stats 68 | #---------------------------------------------- 69 | - name: Generate Coverage Report 70 | run: poetry run coverage xml 71 | #---------------------------------------------- 72 | # upload coverage stats 73 | #---------------------------------------------- 74 | - name: Upload coverage 75 | uses: codecov/codecov-action@v1 76 | with: 77 | file: ./coverage.xml 78 | fail_ci_if_error: true 79 | -------------------------------------------------------------------------------- /streamdeck_ui/dimmer.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Callable, Optional 3 | 4 | from StreamDeck.Transport.Transport import TransportError 5 | 6 | 7 | class Dimmer: 8 | def __init__(self, timeout: int, brightness: int, brightness_dimmed: int, brightness_callback: Callable[[int], None]): 9 | """Constructs a new Dimmer instance 10 | 11 | :param int timeout: The time in seconds before the dimmer starts. 12 | :param int brightness: The normal brightness level. 13 | :param int brightness_dimmed: The percentage of normal brightness when dimmed. 14 | :param Callable[[int], None] brightness_callback: Callback that receives the current 15 | brightness level. 16 | """ 17 | self.timeout = timeout 18 | self.brightness = brightness 19 | "The brightness when not dimmed" 20 | self.brightness_dimmed = brightness_dimmed 21 | "The percentage of normal brightness when dimmed" 22 | self.brightness_callback = brightness_callback 23 | self.__stopped = False 24 | self.dimmed = True 25 | "True if the Stream Deck is dimmed, False otherwise" 26 | self.__timer: Optional[threading.Timer] = None 27 | 28 | def dimmed_brightness(self) -> int: 29 | """Calculates the effective brightness when dimmed. 30 | 31 | :return: The brightness value when applying the dim percentage to the normal brightness. 32 | :rtype: int 33 | """ 34 | return int(self.brightness * (self.brightness_dimmed / 100)) 35 | 36 | def stop(self) -> None: 37 | """Stops the dimmer and sets the brightness back to normal. Call 38 | reset to start normal dimming operation.""" 39 | if self.__timer: 40 | self.__timer.cancel() 41 | self.__timer = None 42 | 43 | try: 44 | self.brightness_callback(self.brightness) 45 | except KeyError: 46 | # During detach cleanup, this is likely to happen 47 | pass 48 | except TransportError: 49 | pass 50 | self.__stopped = True 51 | 52 | def reset(self) -> bool: 53 | """Reset the dimmer and start counting down again. If it was busy dimming, it will 54 | immediately stop dimming. Callback fires to set brightness back to normal.""" 55 | 56 | self.__stopped = False 57 | if self.__timer: 58 | self.__timer.cancel() 59 | self.__timer = None 60 | 61 | if self.timeout: 62 | self.__timer = threading.Timer(self.timeout, self.dim) 63 | self.__timer.start() 64 | 65 | if self.dimmed: 66 | self.brightness_callback(self.brightness) 67 | self.dimmed = False 68 | if self.dimmed_brightness() < 20: 69 | # The screen was "too dark" so reset and let caller know 70 | return True 71 | 72 | return False 73 | # Returning false means "I didn't have to reset it" 74 | 75 | def dim(self, toggle: bool = False): 76 | """Manually initiate a dim event. 77 | If the dimmer is stopped, this has no effect.""" 78 | 79 | if self.__stopped: 80 | return 81 | 82 | if toggle and self.dimmed: 83 | # Don't dim 84 | self.reset() 85 | elif self.__timer: 86 | # No need for the timer anymore, stop it 87 | self.__timer.cancel() 88 | self.__timer = None 89 | 90 | self.brightness_callback(self.dimmed_brightness()) 91 | self.dimmed = True 92 | -------------------------------------------------------------------------------- /streamdeck_ui/display/text_filter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fractions import Fraction 3 | from typing import Callable, Tuple 4 | 5 | from PIL import Image, ImageDraw, ImageFilter, ImageFont 6 | 7 | from streamdeck_ui.config import FONTS_PATH 8 | from streamdeck_ui.display.filter import Filter 9 | 10 | 11 | class TextFilter(Filter): 12 | font_blur: ImageFilter.Kernel = None 13 | # Static instance - no need to create one per Filter instance 14 | 15 | image: Image 16 | 17 | def __init__(self, text: str, font: str, vertical_align: str): 18 | super(TextFilter, self).__init__() 19 | self.text = text 20 | self.vertical_align = vertical_align 21 | self.true_font = ImageFont.truetype(os.path.join(FONTS_PATH, font), 14) 22 | # fmt: off 23 | kernel = [ 24 | 0, 1, 2, 1, 0, 25 | 1, 2, 4, 2, 1, 26 | 2, 4, 8, 4, 1, 27 | 1, 2, 4, 2, 1, 28 | 0, 1, 2, 1, 0] 29 | # fmt: on 30 | TextFilter.font_blur = ImageFilter.Kernel((5, 5), kernel, scale=0.1 * sum(kernel)) 31 | self.offset = 0.0 32 | self.offset_direction = 1 33 | self.image = None 34 | 35 | # Hashcode should be created for anything that makes this frame unique 36 | self.hashcode = hash((self.__class__, text, font, vertical_align)) 37 | 38 | def initialize(self, size: Tuple[int, int]): 39 | self.image = Image.new("RGBA", size) 40 | backdrop_draw = ImageDraw.Draw(self.image) 41 | 42 | # Calculate the height and width of the text we're drawing, using the font itself 43 | label_w, _ = backdrop_draw.textsize(self.text, font=self.true_font) 44 | 45 | # Calculate dimensions for text that include ascender (above the line) 46 | # and below the line (descender) characters. This is used to adust the 47 | # font placement and should allow for button text to horizontally align 48 | # across buttons. Basically we want to figure out what is the tallest 49 | # text we will need to draw. 50 | _, label_h = backdrop_draw.textsize("lLpgyL|", font=self.true_font) 51 | 52 | gap = (size[1] - 5 * label_h) // 4 53 | 54 | if self.vertical_align == "top": 55 | label_y = 0 56 | elif self.vertical_align == "middle-top": 57 | label_y = gap + label_h 58 | elif self.vertical_align == "middle": 59 | label_y = size[1] // 2 - (label_h // 2) 60 | elif self.vertical_align == "middle-bottom": 61 | label_y = (gap + label_h) * 3 62 | else: 63 | label_y = size[1] - label_h 64 | # Default or "bottom" 65 | 66 | label_pos = ((size[0] - label_w) // 2, label_y) 67 | 68 | backdrop_draw.text(label_pos, text=self.text, font=self.true_font, fill="black") 69 | self.image = self.image.filter(TextFilter.font_blur) 70 | 71 | foreground_draw = ImageDraw.Draw(self.image) 72 | foreground_draw.text(label_pos, text=self.text, font=self.true_font, fill="white") 73 | 74 | def transform(self, get_input: Callable[[], Image.Image], get_output: Callable[[int], Image.Image], input_changed: bool, time: Fraction) -> Tuple[Image.Image, int]: 75 | """ 76 | The transformation returns the loaded image, ando overwrites whatever came before. 77 | """ 78 | 79 | if input_changed: 80 | image = get_output(self.hashcode) 81 | if image: 82 | return (image, self.hashcode) 83 | 84 | input = get_input() 85 | input.paste(self.image, self.image) 86 | return (input, self.hashcode) 87 | return (None, self.hashcode) 88 | -------------------------------------------------------------------------------- /tests/assets/smile.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 45 | 47 | 56 | 57 | 60 | 62 | 69 | 76 | 83 | 84 | 85 | 86 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /docs/contributing/3.-code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # HOPE 11 -- Code of Conduct 2 | 3 | | | | 4 | | ------------| ------------------------------------------- | 5 | | HOPE: | 11 | 6 | | Title: | Code of Conduct | 7 | | Author(s): | Timothy Crosley | 8 | | Status: | Active | 9 | | Type: | Process | 10 | | Created: | 17-August-2019 | 11 | | Updated: | 17-August-2019 | 12 | 13 | ## Abstract 14 | 15 | Defines the Code of Conduct for Hug and all related projects. 16 | 17 | ## Our Pledge 18 | 19 | In the interest of fostering an open and welcoming environment, we as 20 | contributors and maintainers pledge to making participation in our project and 21 | our community a harassment-free experience for everyone, regardless of age, body 22 | size, disability, ethnicity, sex characteristics, gender identity and expression, 23 | level of experience, education, socio-economic status, nationality, personal 24 | appearance, race, religion, or sexual identity and orientation. 25 | 26 | ## Our Standards 27 | 28 | Examples of behavior that contributes to creating a positive environment 29 | include: 30 | 31 | * Using welcoming and inclusive language 32 | * Being respectful of differing viewpoints and experiences 33 | * Gracefully accepting constructive criticism 34 | * Focusing on what is best for the community 35 | * Showing empathy towards other community members 36 | 37 | Examples of unacceptable behavior by participants include: 38 | 39 | * The use of sexualized language or imagery and unwelcome sexual attention or 40 | advances 41 | * Trolling, insulting/derogatory comments, and personal or political attacks 42 | * Public or private harassment 43 | * Publishing others' private information, such as a physical or electronic 44 | address, without explicit permission 45 | * Other conduct which could reasonably be considered inappropriate in a 46 | professional setting 47 | 48 | ## Our Responsibilities 49 | 50 | Project maintainers are responsible for clarifying the standards of acceptable 51 | behavior and are expected to take appropriate and fair corrective action in 52 | response to any instances of unacceptable behavior. 53 | 54 | Project maintainers have the right and responsibility to remove, edit, or 55 | reject comments, commits, code, wiki edits, issues, and other contributions 56 | that are not aligned to this Code of Conduct, or to ban temporarily or 57 | permanently any contributor for other behaviors that they deem inappropriate, 58 | threatening, offensive, or harmful. 59 | 60 | ## Scope 61 | 62 | This Code of Conduct applies both within project spaces and in public spaces 63 | when an individual is representing the project or its community. Examples of 64 | representing a project or community include using an official project e-mail 65 | address, posting via an official social media account, or acting as an appointed 66 | representative at an online or offline event. Representation of a project may be 67 | further defined and clarified by project maintainers. 68 | 69 | ## Enforcement 70 | 71 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 72 | reported by contacting [timothy.crosley@gmail.com](mailto:timothy.crosley@gmail.com). All 73 | complaints will be reviewed and investigated and will result in a response that 74 | is deemed necessary and appropriate to the circumstances. Confidentiality will be maintained 75 | with regard to the reporter of an incident. 76 | Further details of specific enforcement policies may be posted separately. 77 | 78 | Project maintainers who do not follow or enforce the Code of Conduct in good 79 | faith may face temporary or permanent repercussions as determined by other 80 | members of the project's leadership. 81 | 82 | ## Attribution 83 | 84 | This Code of Conduct is adapted from the [Contributor Covenant][https://www.contributor-covenant.org], version 1.4, 85 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 86 | 87 | For answers to common questions about this code of conduct, see 88 | https://www.contributor-covenant.org/faq 89 | -------------------------------------------------------------------------------- /streamdeck_ui/settings.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsDialog 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 452 13 | 156 14 | 15 | 16 | 17 | 18 | 0 19 | 0 20 | 21 | 22 | 23 | Stream Deck Settings 24 | 25 | 26 | 27 | :/icons/icons/gear.png:/icons/icons/gear.png 28 | 29 | 30 | 31 | 9 32 | 33 | 34 | 35 | 36 | 37 | 38 | 30 39 | 40 | 41 | 6 42 | 43 | 44 | 45 | 46 | Stream Deck: 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Brightness: 61 | 62 | 63 | 64 | 65 | 66 | 67 | Qt::Horizontal 68 | 69 | 70 | 71 | 72 | 73 | 74 | Auto dim after: 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Dim to %: 89 | 90 | 91 | 92 | 93 | 94 | 95 | Qt::Horizontal 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Qt::Horizontal 107 | 108 | 109 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 110 | 111 | 112 | false 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | buttonBox 124 | accepted() 125 | SettingsDialog 126 | accept() 127 | 128 | 129 | 248 130 | 254 131 | 132 | 133 | 157 134 | 274 135 | 136 | 137 | 138 | 139 | buttonBox 140 | rejected() 141 | SettingsDialog 142 | reject() 143 | 144 | 145 | 316 146 | 260 147 | 148 | 149 | 286 150 | 274 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /streamdeck_ui/display/image_filter.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | from fractions import Fraction 4 | from io import BytesIO 5 | from typing import Callable, Tuple 6 | 7 | import cairosvg 8 | import filetype 9 | from PIL import Image, ImageSequence 10 | 11 | from streamdeck_ui.display.filter import Filter 12 | 13 | 14 | class ImageFilter(Filter): 15 | """ 16 | Represents a static image. It transforms the input image by replacing it with a static image. 17 | """ 18 | 19 | def __init__(self, file: str): 20 | super(ImageFilter, self).__init__() 21 | self.file = os.path.expanduser(file) 22 | 23 | def initialize(self, size: Tuple[int, int]): 24 | # Each frame needs to have a unique hashcode. Start with file name as baseline. 25 | image_hash = hash((self.__class__, self.file)) 26 | frame_duration = [] 27 | frame_hash = [] 28 | 29 | try: 30 | kind = filetype.guess(self.file) 31 | if kind is None: 32 | svg_code = open(self.file).read() 33 | png = cairosvg.svg2png(svg_code, output_height=size[1], output_width=size[0]) 34 | image_file = BytesIO(png) 35 | image = Image.open(image_file) 36 | frame_duration.append(-1) 37 | frame_hash.append(image_hash) 38 | else: 39 | image = Image.open(self.file) 40 | image.seek(0) 41 | # Frame number is used to create unique hash 42 | frame_number = 1 43 | while True: 44 | try: 45 | frame_duration.append(image.info["duration"]) 46 | # Create tuple and hash it, to combine the image and frame hashcodes 47 | frame_hash.append(hash((image_hash, frame_number))) 48 | image.seek(image.tell() + 1) 49 | frame_number += 1 50 | except EOFError: 51 | # Reached the final frame 52 | break 53 | except KeyError: 54 | # If the key 'duration' can't be found, it's not an animation 55 | frame_duration.append(-1) 56 | frame_hash.append(image_hash) 57 | break 58 | 59 | except OSError as icon_error: 60 | # FIXME: caller should handle this? 61 | print(f"Unable to load icon {self.file} with error {icon_error}") 62 | image = Image.new("RGB", size) 63 | frame_duration.append(-1) 64 | frame_hash.append(image_hash) 65 | 66 | frames = ImageSequence.Iterator(image) 67 | 68 | # Scale all the frames to the target size 69 | self.frames = [] 70 | for frame, milliseconds, hashcode in zip(frames, frame_duration, frame_hash): 71 | frame = frame.copy() 72 | frame.thumbnail(size, Image.LANCZOS) 73 | self.frames.append((frame, milliseconds, hashcode)) 74 | 75 | self.frame_cycle = itertools.cycle(self.frames) 76 | self.current_frame = next(self.frame_cycle) 77 | self.frame_time = Fraction() 78 | 79 | def transform(self, get_input: Callable[[], Image.Image], get_output: Callable[[int], Image.Image], input_changed: bool, time: Fraction) -> Tuple[Image.Image, int]: 80 | """ 81 | The transformation returns the loaded image, ando overwrites whatever came before. 82 | """ 83 | 84 | # Unpack tuple to make code a bit easier to understand 85 | frame, duration, hashcode = self.current_frame 86 | 87 | if duration >= 0 and time - self.frame_time > duration / 1000: 88 | self.frame_time = time 89 | self.current_frame = next(self.frame_cycle) 90 | 91 | # Unpack updated value 92 | frame, duration, hashcode = self.current_frame 93 | 94 | image = get_output(hashcode) 95 | if image: 96 | return (image, hashcode) 97 | 98 | input = get_input() 99 | if frame.mode == "RGBA": 100 | # Use the transparency mask of the image to paste 101 | input.paste(frame, frame) 102 | else: 103 | input.paste(frame) 104 | return (input, hashcode) 105 | 106 | if input_changed: 107 | image = get_output(hashcode) 108 | if image: 109 | return (image, hashcode) 110 | 111 | input = get_input() 112 | 113 | if frame.mode == "RGBA": 114 | # Use the transparency mask of the image to paste 115 | input.paste(frame, frame) 116 | else: 117 | input.paste(frame) 118 | return (input, hashcode) 119 | else: 120 | return (None, hashcode) 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Upgrade 2 | 3 | To upgrade to the latest version, run: 4 | 5 | ``` 6 | python -m pip install streamdeck-ui --user --upgrade 7 | ``` 8 | 9 | > Note you may have to use `python3`, `python3.8` etc. depending on your operating system and Python version. 10 | 11 | # Change log 12 | ## 2.0.16 - TBD 13 | ## Features 14 | - Added ~ path expansion for icons. 15 | ## Fixes 16 | - Fix error when pressing Streamdeck Pedal. 17 | ## 2.0.15 - 8 March 2023 18 | ### Features 19 | - Added support for Stream Deck Pedal. 20 | 21 | ## 2.0.14 - 4 Mar 2023 22 | ### Features 23 | - Ensure only one instance can run (prevents settings file corruption). 24 | - Press `Ctrl+c` in terminal to quit. 25 | - Supports systemd install and added documentation. 26 | 27 | ## 2.0.13 - 27 Feb 2023 28 | ### Fixes 29 | - Requirement for Python < 3.11 removed. 30 | - Switched to pyside6. 31 | ### Features 32 | - Added support for a new sub-variant of the StreamDeck XL. 33 | - Allow Stream Deck UI to start, even if virtual keyboard won't work. 34 | - Improved troubleshooting messages. 35 | - Updated documentation and installation guides. 36 | ## 2.0.6 - 23 Sep 2022 37 | ### Fixes 38 | - Image drag/drop from external applications. 39 | - Dimmer not working properly. 40 | ## 2.0.5 - 18 Sep 2022 41 | ### Features 42 | - Support for new Stream Deck Mini. 43 | ### Fixes 44 | - Fix install under Fedora 36 (pillow dependency version bump). 45 | ## 2.0.4 - 29 Apr 2022 46 | ### Features 47 | - Recover from a suspend/resume cycle. 48 | 49 | ### Fixes 50 | - Button icon stuck in pushed state when changing from page 1. 51 | - Remove python3-xlib dependency. 52 | 53 | ## 2.0.3 - 6 Mar 2022 54 | 55 | ### Features 56 | - UI starts up even if no Stream Deck attached. 57 | - SVG file type support. 58 | - Keys widget now has examples built in. 59 | - Help menu with links to websites. 60 | - About dialog shows application version and primary dependency versions. 61 | - Support hex key codes in Key Press. For example, 0x74. 62 | - Support vertical text alignment. 63 | - New display system: 64 | - User interface shows same image as Stream Deck. 65 | - Text overlay on top of image, with automatic font outline. 66 | - Buttons image change when pressed (visual feedback). 67 | - Animated GIF support. 68 | - CPU indicator for display processing. 69 | 70 | ### Fixes 71 | - Tray context menu not interrupted by window activation. 72 | ## 1.1.3 - 2 Feb 2022 73 | 74 | ### Features 75 | - Support for Stream Deck MK.2 added. 76 | - Remember previous image selection directory. 77 | - Auto dim to a configurable percentage. 78 | - Drag and drop icons onto buttons from file browser. 79 | - Follow the settings file location if symbolic link. 80 | 81 | ### Fixes 82 | - Works with Python 3.10 (resolves Fedora 35 install). 83 | ## 1.1.2 - April 30, 2021 84 | ### Fixes 85 | - Regression with multi-character keys. 86 | ## 1.1.1 - April 29, 2021 87 | ### Features 88 | - Open main window from tray with Configure... menu item. 89 | - Dim the display from tray. 90 | - Supports variable delay duration in Key press action. 91 | ### Fixes 92 | - On exit, reset the display to 50% brightness. 93 | - Documentation for Ubuntu 18.04 added. 94 | 95 | ## 1.1.0 - April 20, 2021 96 | ### Features 97 | - Automatically dim the display after a configurable amount of time. 98 | 99 | ## 1.0.7 - April 6, 2021 100 | ### Features 101 | - Drag and drop support for rearranging buttons around in UI. 102 | - Launches minimized with `-n` or `--no-ui`. 103 | - Window title has been updated to `Stream Deck UI`. 104 | - A remove image button has been added. Cancelling during image selection no longer removes image. 105 | - Image selection button defaults to previous image path, if there is one. 106 | - Reset to the standby image after exiting. This makes it easy to see if streamdeck-ui is running or not. 107 | - Supports `delay` in Key Press action to add a 0.5 second delay. 108 | - Supports `plus` and `comma` in the Key Press action to output `+` and `,` respectively. 109 | - Separator added between Exit and other menu items. 110 | - Avoid unnecessary writes to settings file. 111 | - Improved parsing of command line arguments for launching programs. 112 | ### Fixes 113 | - Missing button image error handling added. 114 | - Avoid losing configuration if there is an exception while writing file. 115 | - Updated to streamdeck 0.8.4 to improve stability. 116 | - Updated to Pillow 8.2 to improve stability and fixes jpeg artifacts. 117 | - Fixed race condition where streamdeck buttons get scrambled. 118 | - Fixed `core dumped` error when exiting. 119 | - Improved error handling for invalid command or key press actions. 120 | - Fixed black on black color issue on UI buttons. 121 | - Removed requirement for plugdev group. 122 | - Waits for Stream Deck to be attached on start up. 123 | 124 | ## 1.0.2 - November 25, 2019 125 | - Updated driver requirement to enable full compatibility with XL. 126 | 127 | ## 1.0.1 - October 8, 2019 128 | - Initial API stable release. 129 | -------------------------------------------------------------------------------- /streamdeck_ui/ui_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'settings.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.4.2 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, 19 | QDialogButtonBox, QFormLayout, QLabel, QSizePolicy, 20 | QSlider, QVBoxLayout, QWidget) 21 | from . import resources_rc 22 | 23 | class Ui_SettingsDialog(object): 24 | def setupUi(self, SettingsDialog): 25 | if not SettingsDialog.objectName(): 26 | SettingsDialog.setObjectName(u"SettingsDialog") 27 | SettingsDialog.setWindowModality(Qt.ApplicationModal) 28 | SettingsDialog.resize(452, 156) 29 | sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 30 | sizePolicy.setHorizontalStretch(0) 31 | sizePolicy.setVerticalStretch(0) 32 | sizePolicy.setHeightForWidth(SettingsDialog.sizePolicy().hasHeightForWidth()) 33 | SettingsDialog.setSizePolicy(sizePolicy) 34 | icon = QIcon() 35 | icon.addFile(u":/icons/icons/gear.png", QSize(), QIcon.Normal, QIcon.Off) 36 | SettingsDialog.setWindowIcon(icon) 37 | self.verticalLayout = QVBoxLayout(SettingsDialog) 38 | self.verticalLayout.setObjectName(u"verticalLayout") 39 | self.verticalLayout.setContentsMargins(9, -1, -1, -1) 40 | self.verticalLayout_2 = QVBoxLayout() 41 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 42 | self.formLayout = QFormLayout() 43 | self.formLayout.setObjectName(u"formLayout") 44 | self.formLayout.setHorizontalSpacing(30) 45 | self.formLayout.setVerticalSpacing(6) 46 | self.label = QLabel(SettingsDialog) 47 | self.label.setObjectName(u"label") 48 | 49 | self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label) 50 | 51 | self.label_streamdeck = QLabel(SettingsDialog) 52 | self.label_streamdeck.setObjectName(u"label_streamdeck") 53 | 54 | self.formLayout.setWidget(0, QFormLayout.FieldRole, self.label_streamdeck) 55 | 56 | self.label_brightness = QLabel(SettingsDialog) 57 | self.label_brightness.setObjectName(u"label_brightness") 58 | 59 | self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_brightness) 60 | 61 | self.brightness = QSlider(SettingsDialog) 62 | self.brightness.setObjectName(u"brightness") 63 | self.brightness.setOrientation(Qt.Horizontal) 64 | 65 | self.formLayout.setWidget(1, QFormLayout.FieldRole, self.brightness) 66 | 67 | self.label_dim = QLabel(SettingsDialog) 68 | self.label_dim.setObjectName(u"label_dim") 69 | 70 | self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label_dim) 71 | 72 | self.dim = QComboBox(SettingsDialog) 73 | self.dim.setObjectName(u"dim") 74 | 75 | self.formLayout.setWidget(2, QFormLayout.FieldRole, self.dim) 76 | 77 | self.label_brightness_dimmed = QLabel(SettingsDialog) 78 | self.label_brightness_dimmed.setObjectName(u"label_brightness_dimmed") 79 | 80 | self.formLayout.setWidget(3, QFormLayout.LabelRole, self.label_brightness_dimmed) 81 | 82 | self.brightness_dimmed = QSlider(SettingsDialog) 83 | self.brightness_dimmed.setObjectName(u"brightness_dimmed") 84 | self.brightness_dimmed.setOrientation(Qt.Horizontal) 85 | 86 | self.formLayout.setWidget(3, QFormLayout.FieldRole, self.brightness_dimmed) 87 | 88 | 89 | self.verticalLayout_2.addLayout(self.formLayout) 90 | 91 | 92 | self.verticalLayout.addLayout(self.verticalLayout_2) 93 | 94 | self.buttonBox = QDialogButtonBox(SettingsDialog) 95 | self.buttonBox.setObjectName(u"buttonBox") 96 | self.buttonBox.setOrientation(Qt.Horizontal) 97 | self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) 98 | self.buttonBox.setCenterButtons(False) 99 | 100 | self.verticalLayout.addWidget(self.buttonBox) 101 | 102 | 103 | self.retranslateUi(SettingsDialog) 104 | self.buttonBox.accepted.connect(SettingsDialog.accept) 105 | self.buttonBox.rejected.connect(SettingsDialog.reject) 106 | 107 | QMetaObject.connectSlotsByName(SettingsDialog) 108 | # setupUi 109 | 110 | def retranslateUi(self, SettingsDialog): 111 | SettingsDialog.setWindowTitle(QCoreApplication.translate("SettingsDialog", u"Stream Deck Settings", None)) 112 | self.label.setText(QCoreApplication.translate("SettingsDialog", u"Stream Deck:", None)) 113 | self.label_streamdeck.setText("") 114 | self.label_brightness.setText(QCoreApplication.translate("SettingsDialog", u"Brightness:", None)) 115 | self.label_dim.setText(QCoreApplication.translate("SettingsDialog", u"Auto dim after:", None)) 116 | self.dim.setCurrentText("") 117 | self.label_brightness_dimmed.setText(QCoreApplication.translate("SettingsDialog", u"Dim to %:", None)) 118 | # retranslateUi 119 | 120 | -------------------------------------------------------------------------------- /notebooks/.ipynb_checkpoints/device_information-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 23, 6 | "metadata": { 7 | "scrolled": true 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "Found 1 Stream Deck(s).\n", 15 | "\n", 16 | "Deck 0 - Stream Deck (Original).\n", 17 | "\t - ID: b'0001:0009:00'\n", 18 | "\t - Serial: 'AL07I1C0830'\n", 19 | "\t - Firmware Version: '1.0.170133\u0000\u0000'\n", 20 | "\t - Key Count: 15 (3x5 grid)\n", 21 | "\t - Key Images: 72x72 pixels, BMP format, rotated 0 degrees, mirrored horizontally/vertically\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "#!/usr/bin/env python3\n", 27 | "\n", 28 | "# Python Stream Deck Library\n", 29 | "# Released under the MIT license\n", 30 | "#\n", 31 | "# dean [at] fourwalledcubicle [dot] com\n", 32 | "# www.fourwalledcubicle.com\n", 33 | "#\n", 34 | "\n", 35 | "# Example script that prints out information about any discovered StreamDeck\n", 36 | "# devices to the console.\n", 37 | "\n", 38 | "from StreamDeck.DeviceManager import DeviceManager\n", 39 | "\n", 40 | "\n", 41 | "# Prints diagnostic information about a given StreamDeck\n", 42 | "def print_deck_info(index, deck):\n", 43 | " image_format = deck.key_image_format()\n", 44 | "\n", 45 | " flip_description = {\n", 46 | " (False, False): \"not mirrored\",\n", 47 | " (True, False): \"mirrored horizontally\",\n", 48 | " (False, True): \"mirrored vertically\",\n", 49 | " (True, True): \"mirrored horizontally/vertically\",\n", 50 | " }\n", 51 | "\n", 52 | " print(\"Deck {} - {}.\".format(index, deck.deck_type()), flush=True)\n", 53 | " print(\"\\t - ID: {}\".format(deck.id()), flush=True)\n", 54 | " print(\"\\t - Serial: '{}'\".format(deck.get_serial_number()), flush=True)\n", 55 | " print(\"\\t - Firmware Version: '{}'\".format(deck.get_firmware_version()), flush=True)\n", 56 | " print(\"\\t - Key Count: {} ({}x{} grid)\".format(\n", 57 | " deck.key_count(),\n", 58 | " deck.key_layout()[0],\n", 59 | " deck.key_layout()[1]), flush=True)\n", 60 | " print(\"\\t - Key Images: {}x{} pixels, {} format, rotated {} degrees, {}\".format(\n", 61 | " image_format['size'][0],\n", 62 | " image_format['size'][1],\n", 63 | " image_format['format'],\n", 64 | " image_format['rotation'],\n", 65 | " flip_description[image_format['flip']]), flush=True)\n", 66 | "\n", 67 | "\n", 68 | "if __name__ == \"__main__\":\n", 69 | " streamdecks = DeviceManager().enumerate()\n", 70 | "\n", 71 | " print(\"Found {} Stream Deck(s).\\n\".format(len(streamdecks)))\n", 72 | "\n", 73 | " for index, deck in enumerate(streamdecks):\n", 74 | " deck.open()\n", 75 | " deck.reset()\n", 76 | "\n", 77 | " print_deck_info(index, deck)\n", 78 | "\n", 79 | " deck.close()" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 24, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "deck = streamdecks[0]\n", 89 | "\n", 90 | "deck.close()\n", 91 | "deck.open()\n" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 28, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "name": "stdout", 101 | "output_type": "stream", 102 | "text": [ 103 | "(, 5, True)\n", 104 | "{}\n", 105 | "(, 5, False)\n", 106 | "{}\n", 107 | "(, 6, True)\n", 108 | "{}\n", 109 | "(, 6, False)\n", 110 | "{}\n", 111 | "(, 10, True)\n", 112 | "{}\n", 113 | "(, 10, False)\n", 114 | "{}\n" 115 | ] 116 | } 117 | ], 118 | "source": [ 119 | "dir(deck)\n", 120 | "\n", 121 | "def my_key_callback(*args, **kwargs):\n", 122 | " print(args)\n", 123 | " print(kwargs)\n", 124 | "\n", 125 | "\n", 126 | "deck.set_key_callback(my_key_callback)" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 13, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "from subprocess import run\n", 136 | "\n", 137 | "\n", 138 | "def my_callback(*args, **kwargs):\n", 139 | " print(\"hi\")\n", 140 | "\n", 141 | "from StreamDeck.DeviceManager import DeviceManager\n", 142 | "streamdecks = DeviceManager().enumerate()\n", 143 | "deck = streamdecks[0]\n", 144 | "if not deck.connected():\n", 145 | " deck.connect()\n", 146 | "deck.set_key_callback(my_callback)" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [] 155 | } 156 | ], 157 | "metadata": { 158 | "kernelspec": { 159 | "display_name": "Python 3", 160 | "language": "python", 161 | "name": "python3" 162 | }, 163 | "language_info": { 164 | "codemirror_mode": { 165 | "name": "ipython", 166 | "version": 3 167 | }, 168 | "file_extension": ".py", 169 | "mimetype": "text/x-python", 170 | "name": "python", 171 | "nbconvert_exporter": "python", 172 | "pygments_lexer": "ipython3", 173 | "version": "3.7.3" 174 | } 175 | }, 176 | "nbformat": 4, 177 | "nbformat_minor": 2 178 | } 179 | -------------------------------------------------------------------------------- /notebooks/device_information.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "scrolled": true 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "Found 1 Stream Deck(s).\n", 15 | "\n", 16 | "Deck 0 - Stream Deck (Original).\n", 17 | "\t - ID: b'0001:0010:00'\n", 18 | "\t - Serial: 'AL07I1C0830'\n", 19 | "\t - Firmware Version: '1.0.170133\u0000\u0000'\n", 20 | "\t - Key Count: 15 (3x5 grid)\n", 21 | "\t - Key Images: 72x72 pixels, BMP format, rotated 0 degrees, mirrored horizontally/vertically\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "#!/usr/bin/env python3\n", 27 | "\n", 28 | "# Python Stream Deck Library\n", 29 | "# Released under the MIT license\n", 30 | "#\n", 31 | "# dean [at] fourwalledcubicle [dot] com\n", 32 | "# www.fourwalledcubicle.com\n", 33 | "#\n", 34 | "\n", 35 | "# Example script that prints out information about any discovered StreamDeck\n", 36 | "# devices to the console.\n", 37 | "\n", 38 | "from StreamDeck.DeviceManager import DeviceManager\n", 39 | "\n", 40 | "\n", 41 | "# Prints diagnostic information about a given StreamDeck\n", 42 | "def print_deck_info(index, deck):\n", 43 | " image_format = deck.key_image_format()\n", 44 | "\n", 45 | " flip_description = {\n", 46 | " (False, False): \"not mirrored\",\n", 47 | " (True, False): \"mirrored horizontally\",\n", 48 | " (False, True): \"mirrored vertically\",\n", 49 | " (True, True): \"mirrored horizontally/vertically\",\n", 50 | " }\n", 51 | "\n", 52 | " print(\"Deck {} - {}.\".format(index, deck.deck_type()), flush=True)\n", 53 | " print(\"\\t - ID: {}\".format(deck.id()), flush=True)\n", 54 | " print(\"\\t - Serial: '{}'\".format(deck.get_serial_number()), flush=True)\n", 55 | " print(\"\\t - Firmware Version: '{}'\".format(deck.get_firmware_version()), flush=True)\n", 56 | " print(\"\\t - Key Count: {} ({}x{} grid)\".format(\n", 57 | " deck.key_count(),\n", 58 | " deck.key_layout()[0],\n", 59 | " deck.key_layout()[1]), flush=True)\n", 60 | " print(\"\\t - Key Images: {}x{} pixels, {} format, rotated {} degrees, {}\".format(\n", 61 | " image_format['size'][0],\n", 62 | " image_format['size'][1],\n", 63 | " image_format['format'],\n", 64 | " image_format['rotation'],\n", 65 | " flip_description[image_format['flip']]), flush=True)\n", 66 | "\n", 67 | "\n", 68 | "if __name__ == \"__main__\":\n", 69 | " streamdecks = DeviceManager().enumerate()\n", 70 | "\n", 71 | " print(\"Found {} Stream Deck(s).\\n\".format(len(streamdecks)))\n", 72 | "\n", 73 | " for index, deck in enumerate(streamdecks):\n", 74 | " deck.open()\n", 75 | " deck.reset()\n", 76 | "\n", 77 | " print_deck_info(index, deck)\n", 78 | "\n", 79 | " deck.close()" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 2, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "deck = streamdecks[0]\n", 89 | "\n", 90 | "deck.close()\n", 91 | "deck.open()\n" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 3, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "name": "stdout", 101 | "output_type": "stream", 102 | "text": [ 103 | "(, 6, True)\n", 104 | "{}\n", 105 | "(, 6, False)\n", 106 | "{}\n" 107 | ] 108 | } 109 | ], 110 | "source": [ 111 | "dir(deck)\n", 112 | "\n", 113 | "from subprocess import run\n", 114 | "\n", 115 | "\n", 116 | "def my_key_callback(*args, **kwargs):\n", 117 | " print(args)\n", 118 | " print(kwargs)\n", 119 | " run([\"kwrite\"])\n", 120 | "\n", 121 | "\n", 122 | "deck.set_key_callback(my_key_callback)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 20, 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "name": "stdout", 132 | "output_type": "stream", 133 | "text": [ 134 | "\n", 135 | "True\n" 136 | ] 137 | } 138 | ], 139 | "source": [ 140 | "from subprocess import run\n", 141 | "\n", 142 | "\n", 143 | "def my_callback(*args, **kwargs):\n", 144 | " print(\"hi\")\n", 145 | "\n", 146 | "from StreamDeck.DeviceManager import DeviceManager\n", 147 | "streamdecks = DeviceManager().enumerate()\n", 148 | "deck = streamdecks[0]\n", 149 | "if not deck.connected():\n", 150 | " deck.connect()\n", 151 | "deck.set_key_callback(my_callback)\n", 152 | "print(deck)\n", 153 | "print(deck.connected())" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 4, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "STREAMDECK_TEMPLATE = \"/home/timothyedmundcrosley/Projects/streamdeck_ui/streamdeck_ui/main.ui\"\n", 163 | "\n", 164 | "import sys\n", 165 | "from PySide6 import QtUiTools\n", 166 | "from PySide6.QtGui import *\n", 167 | "from PySide6.QtWidgets import QApplication\n", 168 | "\n", 169 | "\n", 170 | "def start():\n", 171 | " app = QApplication(sys.argv)\n", 172 | " window = QtUiTools.QUiLoader().load(STREAMDECK_TEMPLATE)\n", 173 | " window.show()\n", 174 | " return app" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [ 183 | "app.exec()" 184 | ] 185 | } 186 | ], 187 | "metadata": { 188 | "kernelspec": { 189 | "display_name": ".venv", 190 | "language": "python", 191 | "name": "python3" 192 | }, 193 | "language_info": { 194 | "codemirror_mode": { 195 | "name": "ipython", 196 | "version": 3 197 | }, 198 | "file_extension": ".py", 199 | "mimetype": "text/x-python", 200 | "name": "python", 201 | "nbconvert_exporter": "python", 202 | "pygments_lexer": "ipython3", 203 | "version": "3.10.6" 204 | }, 205 | "vscode": { 206 | "interpreter": { 207 | "hash": "d90e39cc08b73d3478c0eb0c2e5312ef716da975abace5bc347293093a0c98c2" 208 | } 209 | } 210 | }, 211 | "nbformat": 4, 212 | "nbformat_minor": 2 213 | } 214 | -------------------------------------------------------------------------------- /streamdeck_ui/mock_streamdeck.py: -------------------------------------------------------------------------------- 1 | from StreamDeck.Devices import StreamDeck 2 | 3 | 4 | class StreamDeckMock(StreamDeck.StreamDeck): 5 | """ 6 | Represents a physically attached StreamDeck Original device. 7 | """ 8 | 9 | KEY_COUNT = 24 10 | KEY_COLS = 6 11 | KEY_ROWS = 4 12 | 13 | KEY_PIXEL_WIDTH = 72 14 | KEY_PIXEL_HEIGHT = 72 15 | KEY_IMAGE_FORMAT = "BMP" 16 | KEY_FLIP = (True, True) 17 | KEY_ROTATION = 0 18 | 19 | DECK_TYPE = "Stream Deck Original" 20 | 21 | IMAGE_REPORT_LENGTH = 8191 22 | IMAGE_REPORT_HEADER_LENGTH = 16 23 | 24 | # fmt: off 25 | # 72 x 72 black BMP 26 | BLANK_KEY_IMAGE = [ 27 | 0x42, 0x4d, 0xf6, 0x3c, 0x00, 0x00, 0x00, 0x00, 28 | 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 29 | 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 30 | 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 31 | 0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0xc4, 0x0e, 32 | 0x00, 0x00, 0xc4, 0x0e, 0x00, 0x00, 0x00, 0x00, 33 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 34 | ] + [0] * (KEY_PIXEL_WIDTH * KEY_PIXEL_HEIGHT * 3) 35 | # fmt: on 36 | 37 | def _convert_key_id_origin(self, key): 38 | """ 39 | Converts a key index from or to a origin at the physical top-left of 40 | the StreamDeck device. 41 | 42 | :param int key: Index of the button with either a device or top-left origin. 43 | 44 | :rtype: int 45 | :return: Key index converted to the opposite key origin (device or top-left). 46 | """ 47 | 48 | key_col = key % self.KEY_COLS 49 | return (key - key_col) + ((self.KEY_COLS - 1) - key_col) 50 | 51 | def _read_key_states(self): 52 | """ 53 | Reads the key states of the StreamDeck. This is used internally by 54 | :func:`~StreamDeck._read` to talk to the actual device. 55 | 56 | :rtype: list(bool) 57 | :return: Button states, with the origin at the top-left of the deck. 58 | """ 59 | 60 | return None 61 | 62 | def __del__(self): 63 | """ 64 | Delete handler for the StreamDeck, automatically closing the transport 65 | if it is currently open and terminating the transport reader thread. 66 | """ 67 | pass 68 | 69 | # states = self.device.read(1 + self.KEY_COUNT) 70 | # if states is None: 71 | # return None 72 | 73 | # states = states[1:] 74 | # return [bool(states[s]) for s in map(self._convert_key_id_origin, range(self.KEY_COUNT))] 75 | 76 | def open(self): 77 | """ 78 | Opens the device for input/output. This must be called prior to setting 79 | or retrieving any device state. 80 | 81 | .. seealso:: See :func:`~StreamDeck.close` for the corresponding close method. 82 | """ 83 | # self.device.open() 84 | # self._reset_key_stream() 85 | # self._setup_reader(self._read) 86 | 87 | def close(self): 88 | """ 89 | Closes the device for input/output. 90 | 91 | .. seealso:: See :func:`~StreamDeck.open` for the corresponding open method. 92 | """ 93 | pass 94 | 95 | def is_open(self): 96 | """ 97 | Indicattes if the StreamDeck device is currently open and ready for use.. 98 | 99 | :rtype: bool 100 | :return: `True` if the deck is open, `False` otherwise. 101 | """ 102 | return True 103 | 104 | def connected(self): 105 | """ 106 | Indicates if the physical StreamDeck device this instance is attached to 107 | is still connected to the host. 108 | 109 | :rtype: bool 110 | :return: `True` if the deck is still connected, `False` otherwise. 111 | """ 112 | return True 113 | 114 | def id(self): 115 | """ 116 | Retrieves the physical ID of the attached StreamDeck. This can be used 117 | to differentiate one StreamDeck from another. 118 | 119 | :rtype: str 120 | :return: Identifier for the attached device. 121 | """ 122 | return "/dev/dummy" 123 | 124 | def _reset_key_stream(self): 125 | """ 126 | Sends a blank key report to the StreamDeck, resetting the key image 127 | streamer in the device. This prevents previously started partial key 128 | writes that were not completed from corrupting images sent from this 129 | application. 130 | """ 131 | 132 | payload = bytearray(self.IMAGE_REPORT_LENGTH) 133 | payload[0] = 0x02 134 | # self.device.write(payload) 135 | 136 | def reset(self): 137 | """ 138 | Resets the StreamDeck, clearing all button images and showing the 139 | standby image. 140 | """ 141 | 142 | payload = bytearray(17) 143 | payload[0:2] = [0x0B, 0x63] 144 | # self.device.write_feature(payload) 145 | 146 | def set_brightness(self, percent): 147 | """ 148 | Sets the global screen brightness of the StreamDeck, across all the 149 | physical buttons. 150 | 151 | :param int/float percent: brightness percent, from [0-100] as an `int`, 152 | or normalized to [0.0-1.0] as a `float`. 153 | """ 154 | 155 | print(f"Dummy brightness changed to: {percent}") 156 | if isinstance(percent, float): 157 | percent = int(100.0 * percent) 158 | 159 | percent = min(max(percent, 0), 100) 160 | 161 | payload = bytearray(17) 162 | payload[0:6] = [0x05, 0x55, 0xAA, 0xD1, 0x01, percent] 163 | # self.device.write_feature(payload) 164 | 165 | def get_serial_number(self): 166 | """ 167 | Gets the serial number of the attached StreamDeck. 168 | 169 | :rtype: str 170 | :return: String containing the serial number of the attached device. 171 | """ 172 | 173 | # serial = self.device.read_feature(0x03, 17) 174 | # return self._extract_string(serial[5:]) 175 | return "FAKE" 176 | 177 | def get_firmware_version(self): 178 | """ 179 | Gets the firmware version of the attached StreamDeck. 180 | 181 | :rtype: str 182 | :return: String containing the firmware version of the attached device. 183 | """ 184 | 185 | # version = self.device.read_feature(0x04, 17) 186 | # return self._extract_string(version[5:]) 187 | return "1.0" 188 | 189 | def set_key_image(self, key, image): 190 | """ 191 | Sets the image of a button on the StreamDeck to the given image. The 192 | image being set should be in the correct format for the device, as an 193 | enumerable collection of bytes. 194 | 195 | .. seealso:: See :func:`~StreamDeck.get_key_image_format` method for 196 | information on the image format accepted by the device. 197 | 198 | :param int key: Index of the button whose image is to be updated. 199 | :param enumerable image: Raw data of the image to set on the button. 200 | If `None`, the key will be cleared to a black 201 | color. 202 | """ 203 | 204 | pass 205 | -------------------------------------------------------------------------------- /streamdeck_ui/stream_deck_monitor.py: -------------------------------------------------------------------------------- 1 | from threading import Event, Lock, Thread 2 | from time import sleep 3 | from typing import Callable, Dict, Optional 4 | 5 | from StreamDeck import DeviceManager 6 | from StreamDeck.Devices.StreamDeck import StreamDeck 7 | from StreamDeck.Transport.Transport import TransportError 8 | 9 | 10 | class StreamDeckMonitor: 11 | """Periodically checks if Stream Decks are attached or 12 | removed and raises the corresponding events. 13 | """ 14 | 15 | streamdecks: Dict[str, StreamDeck] 16 | "A dictionary with the key as device id and value as StreamDeck" 17 | 18 | monitor_thread: Optional[Thread] 19 | "The thread the monitors Stream Decks" 20 | 21 | def __init__(self, lock: Lock, attached: Callable[[str, StreamDeck], None], detached: Callable[[str], None]): 22 | """Creates a new StreamDeckMonitor instance 23 | 24 | :param lock: A lock object that will be used to get exclusive access while enumerating 25 | Stream Decks. This lock must be shared by any object that will read or write to the 26 | Stream Deck. 27 | :type lock: threading.Lock 28 | :param attached: A callback function that is called when a new StreamDeck is attached. Note 29 | this runs on a background thread. 30 | :type attached: Callable[[StreamDeck], None] 31 | :param detached: A callback function that is called when a previously attached StreamDeck 32 | is detached. Note this runs on a background thread. The id of the device is passed as 33 | the only argument. 34 | :type detached: Callable[[str], None] 35 | """ 36 | self.quit = Event() 37 | self.streamdecks = {} 38 | self.monitor_thread = None 39 | self.attached = attached 40 | self.detached = detached 41 | self.lock = lock 42 | 43 | def start(self): 44 | """Starts the monitor thread. If it is already running, nothing 45 | happens. 46 | """ 47 | if not self.quit.is_set: 48 | return 49 | 50 | self.monitor_thread = Thread(target=self._run) 51 | # Won't prevent application from exiting, although we will always 52 | # attempt to gracefully shut down thread anyways 53 | self.monitor_thread.isDaemon = True 54 | self.quit.clear() 55 | self.monitor_thread.start() 56 | 57 | def stop(self): 58 | """Stops the monitor thread. If it is not running, nothing happens. 59 | Stopping will wait for the run thread to complete before returning. 60 | """ 61 | if self.quit.is_set(): 62 | return 63 | 64 | self.quit.set() 65 | try: 66 | self.monitor_thread.join() 67 | except RuntimeError: 68 | pass 69 | self.pipelmonitor_thread = None 70 | 71 | for streamdeck_id in self.streamdecks: 72 | self.detached(streamdeck_id) 73 | 74 | self.streamdecks = {} 75 | 76 | def _run(self): 77 | """Runs the internal monitor thread until completion""" 78 | showed_open_help: bool = False 79 | showed_enumeration_help: bool = False 80 | showed_libusb_help: bool = False 81 | while not self.quit.is_set(): 82 | with self.lock: 83 | attached_streamdecks = [] 84 | try: 85 | attached_streamdecks = DeviceManager.DeviceManager().enumerate() 86 | showed_libusb_help = False 87 | except DeviceManager.ProbeError: 88 | if not showed_libusb_help: 89 | print("\n------------------------") 90 | print("*** Problem detected ***") 91 | print("------------------------") 92 | print("A suitable LibUSB installation could not be found.") 93 | print("Check installation instructions:") 94 | print("https://github.com/timothycrosley/streamdeck-ui") 95 | showed_libusb_help = True 96 | 97 | # No point showing the next help if we can't even enumerate 98 | showed_enumeration_help = True 99 | continue 100 | 101 | if len(attached_streamdecks) == 0: 102 | if not showed_enumeration_help: 103 | print("No Stream Deck(s) detected. Attach a Stream Deck.") 104 | showed_enumeration_help = True 105 | else: 106 | showed_enumeration_help = False 107 | 108 | # Look for new StreamDecks 109 | for streamdeck in attached_streamdecks: 110 | streamdeck_id = streamdeck.id() 111 | if streamdeck_id not in self.streamdecks: 112 | try: 113 | self.attached(streamdeck_id, streamdeck) 114 | self.streamdecks[streamdeck_id] = streamdeck 115 | showed_open_help = False 116 | except TransportError: 117 | if not showed_open_help: 118 | print("\n------------------------") 119 | print("*** Problem detected ***") 120 | print("------------------------") 121 | print("A Stream Deck is attached, but it could not be opened.") 122 | print("Check installation instructions and ensure a udev rule has been added and loaded.") 123 | print("https://github.com/timothycrosley/streamdeck-ui") 124 | showed_open_help = True 125 | pass 126 | 127 | # Look for suspended/resumed StreamDecks 128 | for streamdeck in list(self.streamdecks.values()): 129 | # Note that streamdeck.connected() will enumerate the devices attached. 130 | # Enumeration must not be done while other device operations on other 131 | # threads are running. Protect with the lock. 132 | # Note that it will only enumerate when is_open() returns false (short circuit), 133 | # so it won't do it uncessarily anyways. 134 | 135 | # Use a flag so we don't hold the lock while executing callback 136 | failed_but_attached = False 137 | with self.lock: 138 | if not streamdeck.is_open() and streamdeck.connected(): 139 | failed_but_attached = True 140 | 141 | # The recovery strategy is to treat this as a detach and let the 142 | # next enumeration pick up the device and reinitialize. 143 | if failed_but_attached: 144 | del self.streamdecks[streamdeck.id()] 145 | self.detached(streamdeck.id()) 146 | 147 | # Remove unplugged StreamDecks 148 | for streamdeck_id in list(self.streamdecks.keys()): 149 | if streamdeck_id not in [deck.id() for deck in attached_streamdecks]: 150 | streamdeck = self.streamdecks[streamdeck_id] 151 | del self.streamdecks[streamdeck_id] 152 | self.detached(streamdeck_id) 153 | sleep(1) 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![streamdeck_ui - Linux compatible UI for the Elgato Stream Deck](art/logo_large.png)](https://timothycrosley.github.io/streamdeck-ui/) 2 | _________________ 3 | 4 | [![PyPI version](https://badge.fury.io/py/streamdeck-ui.svg)](http://badge.fury.io/py/streamdeck-ui) 5 | [![Test Status](https://github.com/timothycrosley/streamdeck-ui/workflows/Test/badge.svg?branch=master)](https://github.com/timothycrosley/streamdeck-ui/actions?query=workflow%3ATest) 6 | [![codecov](https://codecov.io/gh/timothycrosley/streamdeck-ui/branch/master/graph/badge.svg)](https://codecov.io/gh/timothycrosley/streamdeck-ui) 7 | [![Join the chat at https://gitter.im/timothycrosley/streamdeck-ui](https://badges.gitter.im/timothycrosley/streamdeck-ui.svg)](https://gitter.im/timothycrosley/streamdeck-ui?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.python.org/pypi/streamdeck-ui/) 9 | [![Downloads](https://pepy.tech/badge/streamdeck-ui)](https://pepy.tech/project/streamdeck-ui) 10 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 11 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) 12 | 13 | _________________ 14 | 15 | [Read Latest Documentation](https://timothycrosley.github.io/streamdeck-ui/) 16 | [Release notes](CHANGELOG.md) 17 | _________________ 18 | 19 | **streamdeck_ui** A Linux compatible UI for the Elgato Stream Deck. 20 | 21 | ![Streamdeck UI Usage Example](art/example.gif) 22 | 23 | ## Key Features 24 | 25 | * **Linux Compatible**: Enables usage of Stream Deck devices (Original, MK2, Mini and XL) on Linux. 26 | * **Multi-device**: Enables connecting and configuring multiple Stream Decks on one computer. 27 | * **Brightness Control**: Supports controlling the brightness from both the configuration UI and buttons on the device itself. 28 | * **Configurable Button Display**: Icons + Text, Icon Only, and Text Only configurable per button on the Stream Deck. 29 | * **Multi-Action Support**: Run commands, write text and press hotkey combinations at the press of a single button on your Stream Deck. 30 | * **Button Pages**: streamdeck_ui supports multiple pages of buttons and dynamically setting up buttons to switch between those pages. 31 | * **Auto Reconnect**: Automatically and gracefully reconnects, in the case the device is unplugged and replugged in. 32 | * **Import/Export**: Supports saving and restoring Stream Deck configuration. 33 | * **Drag/Drop**: Move buttons by simply drag and drop. 34 | * **Drag/Drop Image**: Configure a button image by dragging it from your file manager onto the button. 35 | * **Auto Dim**: Configure the Stream Deck to automatically dim the display after a period of time. A button press wakes it up again. 36 | * **Animated icons**: Use an animated gif to liven things up a bit. 37 | * **Runs under systemd**: Run automatically in the background as a systemd --user service. 38 | * **Stream Deck Pedal**: Supports actions when pressing pedals. 39 | 40 | Communication with the Stream Deck is powered by the [Python Elgato Stream Deck Library](https://github.com/abcminiuser/python-elgato-streamdeck#python-elgato-stream-deck-library). 41 | ## Installation Guides 42 | * [Arch/Manjaro](docs/installation/arch.md) 43 | * [CentOS](docs/installation/centos.md) 44 | * [Fedora](docs/installation/fedora.md) 45 | * [openSUSE](docs/installation/opensuse.md) 46 | * [Ubuntu/Mint](docs/installation/ubuntu.md) 47 | 48 | Once you're up and running, consider installing a [systemd service](docs/installation/systemd.md). 49 | 50 | > Use the [troubleshooting](docs/troubleshooting.md) guide or [search](https://github.com/timothycrosley/streamdeck-ui/issues?q=is%3Aissue) the issues for guidance. 51 | 52 | ### Precooked Scripts 53 | There are scripts for setting up streamdeck_ui on [Debian/Ubuntu](scripts/ubuntu_install.sh) and [Fedora](scripts/fedora_install.sh). 54 | 55 | ## Help 56 | 57 | ### Start without showing the user interface 58 | 59 | Note you can start streamdeck_ui without showing the configuration user interface as follows: 60 | ``` 61 | streamdeck -n 62 | ``` 63 | ### Command 64 | Enter a value in the command field to execute a command. For example, `gnome-terminal` will launch a new terminal on Ubuntu/Fedora or `obs` will launch OBS. 65 | 66 | #### Examples 67 | > These examples are for Ubuntu using xorg. 68 | You can use a tool like `xdotool` to interact with other applications. 69 | 70 | Find the window with a title starting with `Meet - ` and bring it to focus. This helps if you have a Google Meet session on a tab somewhere but you lost it behind another window. 71 | ``` console 72 | xdotool search --name '^Meet - .+$' windowactivate 73 | ``` 74 | > The meeting tab must be active one if you have multiple tabs open, since the window title is set by the currently active tab. 75 | 76 | Find the window with a title starting with `Meet - ` and then send `ctrl+d` to it. This has the effect of toggling the mute button in Google Meet. 77 | ``` console 78 | xdotool search --name '^Meet - .+$' windowactivate --sync key ctrl+d 79 | ``` 80 | 81 | Change the system volume up (or down) by a certain percentage. Assumes you're using PulseAudio/Alsa Mixer. 82 | ``` console 83 | amixer -D pulse sset Master 20%+ 84 | ``` 85 | If you want you invoke a command that uses shell-script specific things like `&&` or `|`, run it via bash. This command will shift focus to firefox using the `wmctrl`, and then shifts focus to the first tab: 86 | 87 | ``` console 88 | bash -c "wmctrl -a firefox && xdotool key alt+1" 89 | ``` 90 | 91 | ### Press Keys 92 | Simulates key press combinations (hot keys). The basic format is a group of keys, separated by a `+` sign to press simultaneously. Separate key combination groups with a `,` if additional key combinations are needed. For example, `alt+F4,f` means press and hold `alt`, followed by `F4` and then release both. Next, press and release `f`. 93 | 94 | You can also specify a KeyCode in hex format, for example, `0x74` is the KeyCode for `t`. This is also sometimes called the keysym value. 95 | 96 | > You can use the `xev` tool and capture the key you are looking for. 97 | > In the output, look for the **keysym hex value**, for example `(keysym 0x74, t)` 98 | > 99 | > Use `comma` or `plus` if you want to actually *output* `,` or `+` respectively. 100 | > 101 | > Use `delay ` to add a delay, where `` is the number (float or integer) of seconds to delay. If `` is not specified, 0.5 second default is used. If `` fails to parse as a valid number, it will result in no delay. 102 | > 103 | 104 | #### Examples 105 | - `F11` - Press F11. If you have focus on a browser, this will toggle full screen. 106 | - `alt+F4` - Closes the current window. 107 | - `ctrl+w` - Closes the current browser tab. 108 | - `cmd+left` - View split on left. Note `cmd` is the **super** key (equivalent of the Windows key). 109 | - `alt+plus` - Presses the alt and the `+` key at the same time. 110 | - `alt+delay+F4` - Press alt, then wait 0.5 seconds, then press F4. Release both. 111 | - `1,delay,delay,2,delay,delay,3` - Type 123 with a 1-second delay between key presses (using default delay). 112 | - `1,delay 1,2,delay 1,3` - Type 123 with a 1-second delay between key presses (using custom delay). 113 | - `e,c,h,o,space,",t,e,s,t,",enter` - Type `echo "test"` and press enter. 114 | - `ctrl+alt+0x74` - Opens a new terminal window. `0x74` is the KeyCode for `t`. TIP: If the character doesn't work, try using the KeyCode instead. 115 | - `0xffe5` - Toggle Caps Lock. 116 | - `0xffaf` - The `/` key on the numeric key pad. 117 | 118 | The standard list of keys can be found [at the source](https://pynput.readthedocs.io/en/latest/_modules/pynput/keyboard/_base.html#Key). 119 | 120 | The `super` key (Windows key) can be problematic on some versions of Linux. Instead of using the Key Press feature, you could use the Command feature as follows. In this example, it will press `Super` and `4`, which launches application number 4 in your favorites (Ubuntu). 121 | ``` 122 | xdotool key "Super_L+4" 123 | ``` 124 | 125 | ### Write Text 126 | A quick way of typing longer pieces of text (verbatim). Note that unlike the *Press Keys* action, 127 | write text does not accept special (modifier) keys. However, if you type Enter (causing a new line) it will 128 | press enter during the output. 129 | 130 | #### Examples 131 | 132 | ``` 133 | Unfortunately that's a hard no. 134 | Kind regards, 135 | Joe 136 | ``` 137 | ![nope](art/nope.gif) 138 | 139 | ## Known issues 140 | Confirm you are running the latest release with `pip show streamdeck-ui`. Compare it to: [![PyPI version](https://badge.fury.io/py/streamdeck-ui.svg)](http://badge.fury.io/py/streamdeck-ui) 141 | 142 | - Streamdeck uses [pynput](https://github.com/moses-palmer/pynput) for simulating **Key Presses** but it lacks proper [support for Wayland](https://github.com/moses-palmer/pynput/issues/189). Generally your results will be good when using X (Ubuntu/Linux Mint). [This thread](https://github.com/timothycrosley/streamdeck-ui/issues/47) may be useful. 143 | - **Key Press** or **Write Text** does not work on Fedora (outside of the streamdeck itself), which is not particularly useful. However, still do a lot with the **Command** feature. 144 | - Some users have reported that the Stream Deck device does not work on all on specific USB ports, as it draws quite a bit of power and/or has [strict bandwidth requirements](https://github.com/timothycrosley/streamdeck-ui/issues/69#issuecomment-715887397). Try a different port. 145 | - If you are executing a shell script from the Command feature - remember to add the shebang at the top of your file, for the language in question. `#!/bin/bash` or `#!/usr/bin/python3` etc. The streamdeck may appear to lock up if you don't under some distros. -------------------------------------------------------------------------------- /streamdeck_ui/resources_rc.py: -------------------------------------------------------------------------------- 1 | # Resource object code (Python 3) 2 | # Created by: object code 3 | # Created by: The Resource Compiler for Qt version 6.4.2 4 | # WARNING! All changes made in this file will be lost! 5 | 6 | from PySide6 import QtCore 7 | 8 | qt_resource_data = b"\ 9 | \x00\x00\x049\ 10 | \x89\ 11 | PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ 12 | \x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0w=\xf8\ 13 | \x00\x00\x00\x19tEXtSoftware\ 14 | \x00Adobe ImageRead\ 15 | yq\xc9e<\x00\x00\x03\xdbIDATx\xda\xb4\ 16 | VMHTQ\x14>3o4g*R\xe9\xc7n\ 17 | \x10\xb5)\x15\x0c\xe9\x16L\xe9P\x86\xe2\x18FE\x14\ 18 | Q\x10\xad\x86 h\xd1\xa6MK\x97mZE\xed\x8a\ 19 | \x886\x11\xad\x8a@\xb0 \x87\x19|jjX6I\ 20 | \x09\xea\x8c\xc3\xcc\xd8\xa4\xf6\xd4\x99y\x9ds\xdf\xbd\xaf\ 21 | \xe7\x9b\xa46]\xf8|\xde{\xcf\xf9\xbes\xef9\xef\ 22 | \xbc\xf1\x98\xa6\x09\xffs\xf8\x9ex<\xe0\xc1\x7f\xbc\x16\ 23 | \xc2\xf8\xe8A<(Y\x80\xa2\x0b44\x17\xa4o\x04\ 24 | ,\xdcF\xbfW\xe4k\xca=5\xc2\xb8\xd8s\xfe\xd6\ 25 | -^\xfam\xfc\xaf#B>gn\xde$_\x0a0\ 26 | \xac6\xb4s\xf8\x07O\x10F\xb5\x9e\x8bH>;;\ 27 | \x0b\xc1p\x98\x8d\xbe{\xc7\xc0\x8aB7e4\xea2\ 28 | \xbdk\x11\xc1\xf5\xc8\xe9\x1b7x6\x9b\x85\xa6P\x88\ 29 | }\x8c\xc7\x1bpk\x0a\x91\xd0\xceZ\x8eO/#\xf9\ 30 | \xe4\xe4$\x14\x0a\x05\xc8\xe5r\xd0r\xf2$\x1b\xe9\xef\ 31 | /\x13\x81?\x90w_\xbb\xc6\xe7\xe6\xe6\x84\xef\xe2\xe2\ 32 | \x224\x1e9\xc2>\x0f\x0d5\xe0\xde\x03\xed\x8ct\x1c\ 33 | \xc6\x88C]]lff\x06VWW!\x9dNC\ 34 | +\xce\xc7b\xb15\x22\xe0\x22\xef\xbcz\x95+\x1fB\ 35 | \x10\x80\xd7\x8f\x1e\xe9\x94G\xf2Q'\xd0))\xef\ 36 | \xa3Q\x16\xea\xecd\xd3\xd3\xd3\xc28\x95JA\xb0\xbd\ 37 | \x9d\x8d\x0f\x0e\xda\x22\xe0 ?~\xe1\x02W\xb6\x04\xbf\ 38 | \xdf\x0fo\x9f=\x13\xe4\x1e\x0b \x04h\x94\xa4\xc8h\ 39 | <\xce\x82mm,\x99L\x8a#\xd3\xd1\x0f\xb6\xb6\xb2\ 40 | \xc4\xd8\x18\x93\xa6\x9c\xc8\x8f\x9e:%\xf2E6\x84\xaa\ 41 | \xaa*\x88\xbd|i\x93{\xad\xdc\xfe\x160\xa5\x08\x95\ 42 | \xe2\xf8\xf00;\x88\xf7\x98\xc9d\xa0T*\x01%\xaf\ 43 | \x91s\xf6ub\x82\xa1\x1d?\xdc\xd1!\xee\x9c\xf6\x08\ 44 | D>\xd4\xd7GWh\x93+\x01\xcfCI\xee\xaaw\ 45 | q\x05m\xdd\xdd\x22J5\xaa\xab\xab\x05a>\x9f\xb7\ 46 | \xd7*++a$\x1a\xd5\x89X\xb3`\xbf\x1fe'\ 47 | 0]'\xf9\x82\x11\xefkld\x86a\x08\x1bz.\ 48 | //\xdb\xe4\x9a\xa6\xc1p,V\x16\xb9\xf3\x04\xbe\xf5\ 49 | \xde\x1cr\x22\x91\xbe\xde\xde\xc8\xe1`\x90\x7f\x9f\x9f\x87\ 50 | \xa5\xa5%;\xa1t\x92\xf9l\xd6\x8e|\xddV\xd1/\ 51 | \x95*\x1d\xa8\x90\xcaJ\xbd\xc2\xeb\x85\x0cV\x94\x1a\x1e\ 52 | y\x05\x059\xff)\xff_E\xac8@\xb7\xa1\x1dr\ 53 | 8\xb8\xfa\x8bx\xfd[\x9a\x9b\xf9T\x22Q\xb6O\xd8\ 54 | \xe2\xf7\xb3\xaca0g\x15\x96\xf5\xadu\x04\x04y\xa8\ 55 | \xbe\x9e\xa7\xb1\xce\xd5\xfaF\xacs\x13K\xd2i\xbb\x15\ 56 | ER\x0e\x91\xbf\x0a\xa8\xc8O\xec\xdd\xcb\x17\xb0L\xd5\ 57 | Um\xf0\xf9 \x9aJ\xe9I\xc3\x98\xdd\x83\xa4>\xc7\ 58 | 52\x9cO\xa1\x88\xe9\x12)\x13P\xe4]\xbbv\xf1\ 59 | \x95\x1f?l\x82\x0al\xe9\xbd\x99\x8c.[\xb8\xfe\x0d\ 60 | \xc9\xf6\xb9DH\xf4\xb3Kd\x8d\x80\x22?\xb7};\ 61 | 7\xb1\x14\x9d\xce/r9A\xee\xb5\xaaE\xa7dN\ 62 | \xd9\x01\x97\xc8~\x9c\x8f9D\x9c\x02\x82\xfcJm\ 63 | -\xf7\x16\x8bk\xc8\x1f;\xc85\x19UQ\x8a\x8c\x22\ 64 | \xd9!\x97H\x13\xce\x07eN(\x18\xad\xd9R\xbb\x7f\ 65 | \xbd\xa6\x86;\x0d\x09\xf7\x90\xdc\x94\xe4\xf4>\xbfF|\ 66 | @l\xa6\x84\xa33\x95\xa6\x8ed-.\x11\x12\x8d\xe2\ 67 | :\x05\xa6\xed\xb0\x8c\xa7p\xa1\xe1\x18n\xa8d\xdf\x91\ 68 | \xe4I\xc4\x1b\x9c\xc7\x11y\x89q\xc4\x8c\x14\x09\xe0\x93\ 69 | \xc8\xfe\xe0{\x1b\x83J\x88s\xd4!\xda\xf1\xabv\x09\ 70 | ``\xa2\xa6\xc6\xa4'\xce#u\xff\xf0\xad\x94\xbe\x11\ 71 | \x97o\xb8\xce\xf1\xfd\xaeX\x00\xd84\x09\xf0\x1d\x8f7\ 72 | 7b\x18\xbb\xa7\x01\x9e\xf7a\x97\xc0\xf5Zy#~\ 73 | \xaaT\xf9\x92\xd3\x93\x02\xafFlC\x9b\x9d\xe8\x9b\xc2\ 74 | Iq\xc00\xb6|\x02\xb8\x8b\xdd\xa1\x7fA~\x05T\ 75 | \x85\xfa\xa4\xa3\xea\x14\x1e+5\xa2\x9c\x0b\x12E\xb9\xa6\ 76 | ~,h\x8e\xae\xa2~\x5c\x94d\xc7X\x96\xdd\xa2\xf0\ 77 | K\x80\x01\x00\x9en\x09\xdb\x0f\x1d\x92\xbe\x00\x00\x00\x00\ 78 | IEND\xaeB`\x82\ 79 | \x00\x00\x00\xec\ 80 | \x89\ 81 | PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ 82 | \x00\x00\x1a\x00\x00\x00\x1a\x08\x04\x00\x00\x00\x03C\x84E\ 83 | \x00\x00\x00\xb3IDAT8\xcbc`\xa0\x1c8\x80\ 84 | 0\xa3\x03\x0b\x90\x9c\xe20\x11H\xb280BD\x89\ 85 | \xd1R\xe7\xf0\x1f\x08k\x08jC\xd2\xd2\x08\xd4\xf0\x07\ 86 | \x08\xff;\xd4\x13\xa1\x0d\xac\xb1\x16\xa8\xf8\xa7\xc3? \ 87 | \xfc\x09dU\x81\x8d\xc2\xa3\x85\xc9\x81\xc3a2\xd8a\ 88 | \xc8p\xa2\x03;P\x06\x87&\x90#\xe4\x1dZ\x1c\xaa\ 89 | \x1d\xaa\x1c\xce\x81\x95\x9f\x03\xb2\xaa\x81\x22\xb2\x10Y\x9c\ 90 | \xbe\x82\xb2f\x825\xcd$\x22\xb8\x1d\xc0\x01\x01t\x22\ 91 | \x8b\xc3\x22\xb0\xa6EP\x1e\xa3\x03\xa1`\x07\x87\xdf\x02\ 92 | \xb0\xa6\x05\xe0\xb0#&zG5\xd1M\x13\x99\x91K\ 93 | N2\x22'\xc1\x92\x955\xc8\xcc\x84dfw\x92\x0b\ 94 | \x162\x8b0b\x01\x00\xb7\xde\xa5\xf5\xa1bS{\x00\ 95 | \x00\x00\x00IEND\xaeB`\x82\ 96 | \x00\x00\x05=\ 97 | \x89\ 98 | PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ 99 | \x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0w=\xf8\ 100 | \x00\x00\x00\x19tEXtSoftware\ 101 | \x00Adobe ImageRead\ 102 | yq\xc9e<\x00\x00\x04\xdfIDATx\xda\xb4\ 103 | U]L\x9be\x14>\xfd\x87V\xba\x96\xd22\x08#\ 104 | \x19\xcb4\x8b\xd94Qf\x82\x8af@\x8a4\x5cL\ 105 | \x1d\x5cL#\xe1nD\xaa\xd1\xab\x11\x22\x81\x90\xe8\x85\ 106 | Q,\x86lW\xb2M\x09ht!$\x1d\x1d-1\ 107 | F\xb8\xda2\x98\x17K\x81a\xc6XX\xf8i+\x7f\ 108 | \xed\xd7B\xeby\xde\xef\xfb\xba\x8eh\xe2\x8d_r\xfa\ 109 | \x9e\x9e\x9f\xe7\xbc\xefy\xcf9\xaf&\x93\xc9\xd0\xff\xf9\ 110 | \xe9\xf1\xd3\xd8\xd8H\x1a\x8d\x86t:\x9d\xa0\xdcO\xab\ 111 | \xd5by\x8b\xc9\xcedV\xc4\xbbLQ\xa6\x1b\xe9t\ 112 | \xfa)\xfb\xfd\xfd}A\xd8\xf8\xd8\xd8\x98\x1c\xe0\xc0\xf7\ 113 | 2\xd3KL\x93l8\xaf\xc8\xca\x07\x07\x07{\xcdf\ 114 | \xb3M\xa0\xef\xee\xc6ZZZ:U@\xde\xd4qf\ 115 | \xcf0\xddf\xba\xf5\xd4\x06\x0f\x80W\xb2C\xe5\xf0\xf0\ 116 | \xf0%^k\xf9\xff\xf1T*\x05\x10m<\x1e/Z\ 117 | XX\xd0\x83\xc0C\x06\x1dl`\xab\xf8T\x02\xe3\xdf\ 118 | \x02\x9c\x86\xc1\xd0\xd0\xd0@8\x1c&\xac\xfc\xbf\x8e\x8f\ 119 | \xda\xc6\xab\x06`\xb9\x04\x99\xa2\xab;\xe0\x83\x00\xa7U\ 120 | P\x18QCC\x03\xf8\x0f\x07\x06\x06\xfa\xa3\xd1h6\ 121 | bEE\x05\xf5\xf3W__\xfffII\xc9II\ 122 | \x92\x84\xdcd2\xd1\xca\xca\xca\x1f\xe3\xe3\xe3\xbf\xb6\xf3\ 123 | \xb7\xb8\xb8\x98\xf5\xb1\xdb\xed\xd4\xd6\xd6\xd6\xce\xec\xb7~\ 124 | \xbf_\x0e\xe0v\xbb\xa1{\x96\xc9\xdd\xdf\xef\xf3\xa9A\ 125 | \x8cF\x139\x1c\x85\x827\x18\x8c\xd9\x02@\xdeS\xa9\ 126 | \xa4\xe076\x22\x94LJY\xf0\xf6v\xaf\x97\xd9\x00\ 127 | \xd3\x5c \x10\x90/\x99s\xca`\xc69\x9c\xe8\xc2\x85\ 128 | \xb6\x8f\xfb\xfa\xbe\xee\xdb\xd9\xd9aP=\xa1J67\ 129 | 7iii\x89\xd4\xc0\x00*//'\xab\xd5*l\ 130 | $)A\x16\x8b\x85\xe0\xcb\xd5x\x13\xe0\xc9d\xf2\xc9\ 131 | \x1d`G\x08\xc2)\x08s~\xb5W\xaf^\x1b\x80\x83\ 132 | F\xa3\xa5\xfb\xf7\x17\xe9\xce\x9d\x19\x11\xa8\xb8\xb8X\x10\ 133 | x\xc8\xa0\x83\x0dl\xe1\x03_`\x00\x0b\x98\xd9>\xe0\ 134 | 4\xbd\xcd\xcbQUX]\xfd\xfa\x1b\xdb\xdb\xdb\x22\x0d\ 135 | \xcb\xcb\x0f\xa9\xa0\xa0 9::\xfa\xdb\xc4D\xf0\x1e\ 136 | \xf4uu\xb5'<\x1eO5\xeb\x8c\x06\x83N\xa4\x0f\ 137 | >SSS\xb8\x8cO\x95\xeb\xf8\x93\xe9\x17\x11`o\ 138 | o\xaf\xc8\xe7\xfb\xe6\x22\xef\xc4\xa1\xa6\x0c\x17\xba\xb2\xf2\ 139 | X\xec\xce\xef\xbf118x\xe5;\xf4\x06\xf4\xcc\x9f\ 140 | \xe1\x9dK\xf5\xf5n\x0flJKKq\xb2\xe7/_\ 141 | \xbe\xf4%\xf4\x9c\xde\x0d\xaf\xf7\xa3\x8el\x8a\xf8hV\ 142 | \xde\x85mnn\x9e@\xeb\xeb\xeb\xa2\x14\xe3\xf1\x04\xdf\ 143 | M\x1e\x85B!4OP\xe9^P\x102\xe8`\x03\ 144 | [\xf8\xa8\xfe\xc0\x02f6E|\x02-\xf2\xaa\xb6}\ 145 | :\x9d!\x8c(\x8c\x09\x93\xc9\xcc\xabn\xff\xe0\xcc\x82\ 146 | L\xd6E\x85-|\x9e\xf8\xa7\x05f\xee\x09\xb8\xfbw\ 147 | \xff\xb2\xdb\x0b\x09\x94\x9foaG=\xaffqYM\ 148 | M\xe7_`\xb3*\x0eb\x05\x81\x87\x0c:\xd8\xc8\xb6\ 149 | \x16R\xfd\x81\x05\xcc\xec\x09\xf8O\xa4\xb5\xb5\xb5O\xdd\ 150 | ]ww\xef\xf9#G\xca\x9fs8\x5c\xb4\xb6\xb6J\ 151 | \xcd\xcd\xefy8\x1d\x99\x91\x91k'\xa1on~\xbf\ 152 | \xf2\xec\xd9w=\xe1\xf0,9\x9d.\x9c\x86\x1e=Z\ 153 | \x0awuu\xfe\x90s\xc8H\xb6\x93\xd1\xb1j\xb9\xf2\ 154 | \xd7UU\xf5\xdaQ\xaf\xf7\x93\x0f\xf2\xf2\xcc\x14\x89\xac\ 155 | \xd3\xd6\xd6\x16\x1d;v\x82\x0a\x0b]\xb2gd\x95K\ 156 | \xf4\x1e\xaa\x8beE\x94H\xec\x92\xcf\xf7\xd5\x95\xe9\xe9\ 157 | \xdfQ9\xddjC\xa2\xc3E\x80\xb2\xb22Bc\xe8\ 158 | \xf5\xfaS\xdc(\xef\xf8\xfd\xc1\xcfP\x1d6\x9bC9\ 159 | \xf2\x0e\x9f\xe41\x8e.\x1cy\xaa\xf2\xce\x0f\xf3j\xe1\ 160 | \xe6\x8bP,\xb6A%%\x87y\xe4\xd4\xf60\xde\xcf\ 161 | \x9c\xff\xbb\xdc\xb8\x5c\xe2\xcbr\x00\xa7\xd3\x89\xf7\xe0\x14\ 162 | \xfb\x9e\x0b\x85\xa6;\xd5\xd9b6?C.\x97\x8b/\ 163 | :_\x80\xea\xf5\x06\x92\x8b\x22%\x82IR\x9cVW\ 164 | W\x99\xdf\xce\xce\xae\x9a\x9a\xaa^f\x7fb\xdc\xbbk\ 165 | kk\xf2%\xe3\xd69=M\xd7\xaf\xdf\xec|\xf0\xe0\ 166 | !\xcf\x1c\x83 \x80\xf6\xf4t|?;{{>\x16\ 167 | \x8b\x0a0\x10x\xc8\xa0\x83\x8dj\x0f_`\x00K\xad\ 168 | (q\x02\x9bM\xbc#5\xcc\xd7\x04\x02\xd3\x171s\ 169 | 0o\xdc\xee\xaa\xcf\xd5\x1bS\xe5\xea,\xfa'\x9d*\ 170 | \xe7l\x84X\x1c\x8a\xc5b\xf2\x09\xd0\xb5\x9c\xb7\x10G\ 171 | \x9d\xac\xad}\xe5\x8b\xa2\x22'a\xe5\xff?r\xd0\x0e\ 172 | \xcc~\x9dNO\xb9\xa4\xbc\x07\x1d\xb09\xe03\x09,\ 173 | u\xb4\x8b2M$\x12\xe2\x0f\xdf~\x90/ZS]\ 174 | \xfd\xa2\x96\x9dG\xd8pF\x1e\xd5\x06m\x22!\xc5\x0e\ 175 | \x1d\xb2\x17\xc8\xf6\xd2\x16^4\xc5o\x86}H\xf1\x01\ 176 | xP\x9di\xd9\x14i0\x12\x89,Lh\xa2\x02\xe5\ 177 | q\xd7\xa8F\x1c\xe0U\xb6\xb1*6b\xdc\xb0\xdf&\ 178 | \xf7\xcfTN\xdd#\xe9;L\x9b\x0a\xc5\xd9&\xa3\x06\ 179 | \x00\x98\x11\x8f\x95\xb2\x1ar\x03\xfc\xc7\x0f\xb3\x04\x8f\x80\ 180 | \xa4\xd0\x1e\x02\xfc-\xc0\x00\xc9p\x09& Image.Image: 87 | with self.lock: 88 | # REVIEW: Consider returning not the last result, but a thumbnail 89 | # or something that represents the current "static" look of 90 | # a button. This will need to be added to the interface 91 | # of a filter. 92 | return self.pages[page][button].last_result() 93 | 94 | def set_keypress(self, button: int, active: bool): 95 | with self.lock: 96 | for filter in self.pages[self.current_page][button].filters: 97 | if isinstance(filter[0], KeypressFilter): 98 | filter[0].active = active 99 | 100 | def synchronize(self): 101 | # Wait until the next cycle is complete. 102 | # To *guarantee* that you have one complete pass, two waits are needed. 103 | # The first gets you to the end of one cycle (you could have called it 104 | # mid cycle). The second gets you one pass through. Worst case, you 105 | # do two full cycles. Best case, you do 1 full and one partial. 106 | self.sync.wait() 107 | self.sync.wait() 108 | 109 | def _run(self): 110 | """Method that runs on background thread and updates the pipelines.""" 111 | frames = 0 112 | start = time() 113 | last_page = -1 114 | execution_time = 0 115 | frame_cache = {} 116 | 117 | while not self.quit.isSet(): 118 | current_time = time() 119 | 120 | with self.lock: 121 | page = self.pages[self.current_page] 122 | 123 | force_update = False 124 | 125 | if last_page != page: 126 | # When a page switch happen, force the pipelines to redraw so icons update 127 | force_update = True 128 | last_page = page 129 | 130 | pipeline_cache_count = 0 131 | 132 | for button, pipeline in page.items(): 133 | # Process all the steps in the pipeline and return the resulting image 134 | with self.lock: 135 | image, hashcode = pipeline.execute(current_time) 136 | 137 | pipeline_cache_count += len(pipeline.output_cache) 138 | 139 | # If none of the filters in the pipeline yielded a change, use 140 | # the last known result 141 | if force_update and image is None: 142 | image = pipeline.last_result() 143 | 144 | if image: 145 | # We cannot afford to do this conversion on every final frame. 146 | # Since we want the flexibilty of a pipeline engine that can mutate the 147 | # images along a chain of filters, the outcome can be somewhat unpredicatable 148 | # For example - a clock that changes time or an animation that changes 149 | # the frame and font that overlays. In many instances there is a finite 150 | # number of frames per pipeline (a looping GIF with image, a pulsing icon etc) 151 | # Some may also be virtually have infinite mutations. A cache per pipeline 152 | # with an eviction policy of the oldest would likely suffice. 153 | # The main problem is since the pipeline can mutate it's too expensive to 154 | # calculate the actual hash of the final frame. 155 | # Create a hash function that the filter itself defines. It has to 156 | # update the hashcode with the unique attributes of the input it requires 157 | # to make the frame. This could be time, text, frame number etc. 158 | # The hash can then be passed to the next step and XOR'd or combined 159 | # with the next hash. This yields a final hash code that can then be 160 | # used to cache the output. At the end of the pipeline the hash can 161 | # be checked and final bytes will be ready to pipe to the device. 162 | 163 | if self.streamdeck.is_visual(): 164 | # FIXME: This will be unbounded, old frames will need to be evicted 165 | if hashcode not in frame_cache: 166 | image = PILHelper.to_native_format(self.streamdeck, image) 167 | frame_cache[hashcode] = image 168 | else: 169 | image = frame_cache[hashcode] 170 | 171 | try: 172 | with self.lock: 173 | self.streamdeck.set_key_image(button, image) 174 | except TransportError: 175 | # Review - deadlock if you wait on yourself? 176 | self.stop() 177 | pass 178 | return 179 | 180 | self.sync.set() 181 | self.sync.clear() 182 | # Calculate how long we took to process the pipeline 183 | elapsed_time = time() - current_time 184 | execution_time += elapsed_time 185 | 186 | # Calculate how much we have to sleep between processing cycles to maintain the desired FPS 187 | # If we have less than 5ms left, don't bother sleeping, as the context switch and 188 | # overhead of sleeping/waking up is consumed 189 | time_left = self.time_per_frame - elapsed_time 190 | if time_left > 0.005: 191 | sleep(time_left) 192 | 193 | frames += 1 194 | if time() - start > 1.0: 195 | execution_time_ms = int(execution_time * 1000) 196 | if self.cpu_callback: 197 | self.cpu_callback(self.serial_number, int(execution_time_ms / 1000 * 100)) 198 | # execution_time_ms = int(execution_time * 1000) 199 | # print(f"FPS: {frames} Execution time: {execution_time_ms} ms Execution %: {int(execution_time_ms/1000 * 100)}") 200 | # print(f"Output cache size: {len(frame_cache)}") 201 | # print(f"Pipeline cache size: {pipeline_cache_count}") 202 | execution_time = 0 203 | frames = 0 204 | start = time() 205 | 206 | def set_page(self, page: int): 207 | """Switches to the given page. Pipelines for that page starts running, 208 | other page pipelines stop. 209 | 210 | Args: 211 | page (int): The page number to switch to. 212 | """ 213 | with self.lock: 214 | if self.current_page >= 0: 215 | # Ensure none of the button filters are active anymore 216 | old_page = self.pages[self.current_page] 217 | for _, pipeline in old_page.items(): 218 | for filter in pipeline.filters: 219 | if isinstance(filter[0], KeypressFilter): 220 | filter[0].active = False 221 | # REVIEW: We could detect the active key on the last page, and make it active 222 | # on the target page 223 | self.current_page = page 224 | 225 | def start(self): 226 | if self.pipeline_thread is not None: 227 | self.quit.set() 228 | try: 229 | self.pipeline_thread.join() 230 | except RuntimeError: 231 | pass 232 | 233 | self.quit.clear() 234 | self.pipeline_thread = threading.Thread(target=self._run) 235 | self.pipeline_thread.daemon = True 236 | self.pipeline_thread.start() 237 | self.synchronize() 238 | # Wait for first frames to become ready 239 | 240 | def stop(self): 241 | if self.pipeline_thread is not None: 242 | self.quit.set() 243 | try: 244 | self.pipeline_thread.join() 245 | except RuntimeError: 246 | pass 247 | self.pipeline_thread = None 248 | -------------------------------------------------------------------------------- /streamdeck_ui/fonts/roboto/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README-de.md: -------------------------------------------------------------------------------- 1 | [![streamdeck_ui - Linux kompatibles UI für das Elgato Stream Deck](art/logo_large.png)](https://timothycrosley.github.io/streamdeck-ui/) 2 | _________________ 3 | 4 | [![PyPI version](https://badge.fury.io/py/streamdeck-ui.svg)](http://badge.fury.io/py/streamdeck-ui) 5 | [![Test Status](https://github.com/timothycrosley/streamdeck-ui/workflows/Test/badge.svg?branch=master)](https://github.com/timothycrosley/streamdeck-ui/actions?query=workflow%3ATest) 6 | [![codecov](https://codecov.io/gh/timothycrosley/streamdeck-ui/branch/master/graph/badge.svg)](https://codecov.io/gh/timothycrosley/streamdeck-ui) 7 | [![Join the chat at https://gitter.im/timothycrosley/streamdeck-ui](https://badges.gitter.im/timothycrosley/streamdeck-ui.svg)](https://gitter.im/timothycrosley/streamdeck-ui?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.python.org/pypi/streamdeck-ui/) 9 | [![Downloads](https://pepy.tech/badge/streamdeck-ui)](https://pepy.tech/project/streamdeck-ui) 10 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 11 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) 12 | 13 | _________________ 14 | 15 | [Lese die neueste Dokumentation](https://timothycrosley.github.io/streamdeck-ui/) 16 | [Release notes](CHANGELOG.md) 17 | _________________ 18 | 19 | > WARNUNG: Diese Dokumentation ist veraltet und möglicherweise nicht korrekt. 20 | 21 | **streamdeck_ui** Ein Linux kompatibles UserInterface für das Elgato Stream Deck. 22 | 23 | ![Streamdeck UI Usage Example](art/example.gif) 24 | 25 | ## Eigenschaften 26 | 27 | * **Linux Kompatibel**: Ermöglicht die Nutzung aller Stream Deck Geräte mit Linux ohne code zu benötigen. 28 | * **Mehrere Geräte**: Ermöglicht die Verbindung und Konfiguration mehrere Stream Deck Geräte an einem Computer. 29 | * **Helligkeits-Steuerung**: Unterstützt die Einstellung der Helligkeit von der Konfigurations-Oberfläche und den Knöpfen am Gerät selbst. 30 | * **Konfigurierbares Tastenbild**: Icon + Text, nur Icon und nur Text sind pro Taste des Stream Decks konfigurierbar. 31 | * **Multi-Action Unterstützung**: Kommandos starten, Text schreiben und Hotkey-Kombinationen drücken mit einem einzigen Tastendruck auf dem Stream Deck. 32 | * **Tasten-Seiten**: streamdeck_ui bietet mehrere Seiten von Tasten mit dynamischer Einstellung von Tasten zum Umschalten zwischen ihnen. 33 | * **Automatisches Wiederverbinden**: Das Gerät wird automatisch und problemlos wieder verbunden, falls das Gerät ab- und wieder angesteckt wurde. 34 | * **Import/Export**: Bietet das Abspeichern und Wiederherstellen ganzer Stream Deck Konfigurationen. 35 | 36 | Die Kommunikation mit dem Streamdeck erfolgt durch die [Python Elgato Stream Deck Library](https://github.com/abcminiuser/python-elgato-streamdeck#python-elgato-stream-deck-library). 37 | 38 | ## Linux Schnellstart 39 | **Python 3.8** wird benötigt. Sie können die Version, die sie installiert haben, überprüfen mit `python3 --version`. 40 | ### Vorgefertigte Skripte 41 | Es gibt fertige Skripte um streamdeck_ui auf [Debian/Ubuntu](scripts/ubuntu_install.sh) und [Fedora](scripts/fedora_install.sh) zu installieren. 42 | ### Manuelle Installation 43 | Um streamdeck_ui unter Linux zu verwenden, müssen einige System-Bibliotheken als Voraussetzung installiert werden. 44 | Die Namen dieser Bibliotheken können, abhängig von ihrem Betriebssystem, variieren. 45 | Debian / Ubuntu: 46 | ``` console 47 | sudo apt install python3-pip libhidapi-libusb0 libxcb-xinerama0 48 | ``` 49 | Fedora: 50 | ``` console 51 | sudo dnf install python3-pip python3-devel hidapi 52 | ``` 53 | Wenn sie die GNOME shell verwenden, könnten sie eine Erweiterung, die den [KStatusNotifierItem/AppIndicator Support](https://extensions.gnome.org/extension/615/appindicator-support/) bietet, manuell installieren müssen um das Tray-Icon anzuzeigen. 54 | 55 | Um streamdeck_ui ohne root-Rechte zu benutzen, müssen sie ihrem user vollen Zugriff auf das Gerät erlauben. 56 | 57 | Fügen sie die folgenden udev rules mit Hilfe ihres Editors hinzu: 58 | ``` console 59 | sudoedit /etc/udev/rules.d/70-streamdeck.rules 60 | # Wenn das nicht funktioniert, versuchen sie: 61 | sudo nano /etc/udev/rules.d/70-streamdeck.rules 62 | ``` 63 | Fügen sie die folgenden Zeilen ein: 64 | ``` console 65 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", TAG+="uaccess" 66 | ``` 67 | Aktivieren sie die Regeln: 68 | ``` console 69 | sudo udevadm trigger 70 | ``` 71 | 72 | Die Installation der Anwendung selbst erfolgt via pip: 73 | ``` console 74 | pip3 install streamdeck-ui --user 75 | ``` 76 | Stellen sie sicher, dass `$HOME/.local/bin` in ihrem PATH enthalten ist. 77 | Wenn das nicht der Fall ist, fügen sie 78 | ``` console 79 | PATH=$PATH:$HOME/.local/bin 80 | ``` 81 | an das Ende ihrer shell Konfigurationsdatei (wahrscheinlich .bashrc in ihrem home directory) hinzu. 82 | 83 | Jetzt können sie `streamdeck` starten um mit der Konfiguration zu beginnen. 84 | 85 | ``` console 86 | streamdeck 87 | ``` 88 | 89 | Es wird empfohlen `streamdeck` in die Autostart-Liste ihrer Fenster-Umgebung aufzunehmen. Wenn sie es verwenden wollen ohne dass das Benutzer-Interface angezeigt wird, verwenden sie`streamdeck -n`. 90 | 91 | ## Allgemeiner Schnellstart 92 | 93 | Auf anderen Betriebssystemen müssen sie die benötigten [Abhängigkeiten](https://github.com/abcminiuser/python-elgato-streamdeck#package-dependencies) der Bibliothek installieren. 94 | Danach verwenden sie pip zur Installation der Anwendung: 95 | 96 | ``` console 97 | pip3 install streamdeck-ui --user 98 | streamdeck 99 | ``` 100 | 101 | Beachten sie auch die Anleitungen für 102 | 103 | * [Arch/Manjaro](docs/installation/arch.md) 104 | * [CentOS](docs/installation/centos.md) 105 | * [Fedora](docs/installation/fedora.md) 106 | * [openSUSE](docs/installation/opensuse.md) 107 | * [Ubuntu/Mint](docs/installation/ubuntu.md) 108 | 109 | ## Hilfe 110 | ### Befehl (Command) 111 | Geben sie einen Befehl in das Feld "Command" ein, um ihn auszuführen. In Ubuntu/Fedora starten sie ein Terminal mit `gnome-terminal`, `obs` startet OBS. 112 | 113 | #### Beispiele (Ubuntu) 114 | Sie können ein tool wie `xdotool` verwenden, um mit anderen Programmen zu interagieren. 115 | 116 | Finden sie das Fenster, das mit `Meet - ` beginnt, und setzen sie den Fokus darauf. Das hilft ihnen, wenn sie eine Google Meet Sitzung auf irgend einem Tab haben, die aber hinter anderen Fenstern verloren gegangen ist. 117 | ``` console 118 | xdotool search --name '^Meet - .+$' windowactivate 119 | ``` 120 | > Der Meeting-Tab muss aktiv sein wenn sie mehrere Tabs offen haben, da der Fenstertitel vom derzeit aktiven Tab gesetzt wird. 121 | 122 | Finden sie das Fenster, das mit `Meet - ` beginnt, und senden sie `ctrl+d` dorthin. Das bewirkt das Umschalten der Stummschaltung (mute button) in Google Meet. 123 | ``` console 124 | xdotool search --name '^Meet - .+$' windowactivate --sync key ctrl+d 125 | ``` 126 | 127 | Drehen sie die System-Lautstärke um einen gewissen Prozentsatz hoch (oder runter). Wir nehmen an, sie verwenden PulseAudio/Alsa Mixer. 128 | ``` console 129 | amixer -D pulse sset Master 20%+ 130 | ``` 131 | Wenn sie einen Befehl abgeben wollen der shell-script spezifische Dinge wie `&&` oder `|` enthält, dann starten sie ihn via bash. Dieser Befehl wird de Fokus auf Firefox setzen, indem es `wmctrl` nutzt, und dann den Fokus auf den ersten Tab verschieben: 132 | 133 | ``` console 134 | bash -c "wmctrl -a firefox && xdotool key alt+1" 135 | ``` 136 | 137 | ### Tasten drücken 138 | Simuliert Tasten-Kombinationen (hot keys). Grundsätzlich werden Tasten, die gleichzeitig betätigt werden, mit einem `+` Zeichen verbunden. Trennen sie Tasten-Kombinationen mit einem `,` , wenn zusätzliche Kombinationen benötigt werden. Die Zeichenfolge `alt+F4,f` zum Beispiel bedeutet drücke und halte `alt`, gefolgt von `F4` und lass dann beide los. Drücke anschließend `f` und lass es wieder los. 139 | 140 | Sie können auch einen Tasten-Code im hex Format verwenden, `0x74` ist zum Beispiel der Tasten-Code für `t`. Dieser Wert wird auch manchmal als keysym bezeichnet. 141 | 142 | > Sie können das tool `xev` verwenden um den Key-Code einer Taste zu ermitteln. 143 | > Suchen sie in der Ausgabe nach **keysym hex value**, zum Beispiel `(keysym 0x74, t)` 144 | > 145 | > Verwenden sie `comma` oder `plus`, wenn sie ein `,` oder ein `+` *ausgeben* wollen. 146 | > 147 | > Verwenden sie `delay ` um eine Verzögerung einzufügen, wobei `` die Anzahl (float oder integer) der Sekunden ist. Wenn `` nicht angegeben wird, wird eine Standardverzögerung von 0.5 Sekundenverwendet. Wenn `` nicht als gültige Zahl erkannt wird, erfolgt keine Verzögerung. 148 | > 149 | 150 | #### Beispiele 151 | - `F11` - drückt F11. Wenn der Fokus auf einem Browser ist, schaltet das zwischen Vollbild und Normalbild hin und her. 152 | - `alt+F4` - schließt das aktuelle Fenster. 153 | - `ctrl+w` - schließt den aktuellen Browser-Tab. 154 | - `cmd+left` - verkleinert das Fenster auf seine linke Hälfte. Achtung, `cmd` ist die **super** Taste (entsprechend der Windows Taste). 155 | - `alt+plus` - drückt die alt und die `+` Taste gleichzeitin. 156 | - `alt+delay+F4` - drücke alt, warte dann 0.5 Sekunden, drücke dann F4. Lass beide Tasten los. 157 | - `1,delay,delay,2,delay,delay,3` - tippe 123 mit 1-Sekunden Pausen zwischen den Tastendrucken (unter Verwendung der Standardpausen). 158 | - `1,delay 1,2,delay 1,3` - tippe 123 mit 1-Sekunden Pausen zwischen den Tastendrucken (unter Verwendung selbst definierter Pausen). 159 | - `e,c,h,o,space,",t,e,s,t,",enter` - tippe `echo "test"` und drücke Enter. 160 | - `ctrl+alt+0x74` - öffnet ein neues Terminalfenster. `0x74` ist der Tasten-Code von `t`. TIP: Verwenden sie den Tasten-Code, wenn der Buchstabe nicht funktioniert. 161 | - `0xffe5` - Caps Lock umschalten. 162 | - `0xffaf` - Die `/` Taste im Ziffernblock der Tastatur. 163 | 164 | Die Standardliste der Tasten finden sie [im source-code](https://pynput.readthedocs.io/en/latest/_modules/pynput/keyboard/_base.html#Key). 165 | 166 | Die `super` Taste (Windows-Taste) kann bei einigen Linux-Versionen problematisch sein. Statt der Tastendruck-Funktion können sie dann die Befehls-Funktion wie folgt benutzen. In diesem Beispiel wollen wir die `Super` Taste und `4` drücken, was die Anwendung Nummer 4 ihrer Favoriten startet (Ubuntu). 167 | ``` 168 | xdotool key "Super_L+4" 169 | ``` 170 | 171 | ### Text schreiben: 172 | Das ist ein schneller Weg um längere Textstücke zu schreiben (Wort für Wort). Beachten sie, dass anders als in der Tastendruck-Funtion, 173 | hier keine Spezial-(Modifikations-)Tasten akzeptiert werden. Wenn sie jedoch Enter drücken (um eine neue Zeile zu beginnen) wird auch Enter bei der Ausgabe ausgegeben. 174 | 175 | #### Beispiele 176 | 177 | ``` 178 | Unfortunately that's a hard no. 179 | Kind regards, 180 | Joe 181 | ``` 182 | ![nope](art/nope.gif) 183 | 184 | ## bekannte Probleme 185 | Stellen sie sicher, dass sie die neueste Version verwenden mit `pip3 show streamdeck-ui`. Vergleichen sie es mit: [![PyPI version](https://badge.fury.io/py/streamdeck-ui.svg)](http://badge.fury.io/py/streamdeck-ui) 186 | 187 | - Streamdeck verwendet [pynput](https://github.com/moses-palmer/pynput) zur Simulation derf **Tasten-Betätigungen** wodurch ordentliche [Unterstützung für Wayland](https://github.com/moses-palmer/pynput/issues/189) fehlt. Im Allgemeinen werden sie gute Ergebnisse bei Verwendung von X haben (Ubuntu/Linux Mint). [Dieser thread](https://github.com/timothycrosley/streamdeck-ui/issues/47) lönnte nützlich sein. 188 | - **Taste drücken** oder **Text schreiben** funktioniert nicht unter Fedora (außerhalb von streamdeck selbst), was nicht besonders hilfreich ist. Die **Befehls-Funktion** kann aber trotzdem eine Menge. 189 | - Version [1.0.2](https://pypi.org/project/streamdeck-ui/) hat keine Fehler-Behandlung bei der **Befehls-** und der **Taste drücken** Funktion. Deshalb müssen sie vorsichtig sein - ein ungültiger Befehl oder Tastendruck stoppt auch alle anderen Prozesse. Bitte upgraden sie zur neuesten Version. 190 | - Einige Anwender haben berichtet, dass das Stream Deck Gerätnicht an allen USB-ports funktioniert, da es einiges an Strom verbraucht und/oder [strenge Bandbreitenanforderungen](https://github.com/timothycrosley/streamdeck-ui/issues/69#issuecomment-715887397) hat. Versuchen sie einen anderen Anschluß. 191 | - Wenn sie einen shell script mit der Befehls-Funktion ausführen, vergessen sie nicht das shebang für die entsprechende Sprache am Anfangihrer Datei haben. `#!/bin/bash` oder `#!/usr/bin/python3` etc. Das streamdeck könnte sich andernfalls unter einigen Distros aufhängen. 192 | -------------------------------------------------------------------------------- /streamdeck_ui/ui_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'main.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.4.2 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, 15 | QCursor, QFont, QFontDatabase, QGradient, 16 | QIcon, QImage, QKeySequence, QLinearGradient, 17 | QPainter, QPalette, QPixmap, QRadialGradient, 18 | QTransform) 19 | from PySide6.QtWidgets import (QApplication, QComboBox, QFormLayout, QGridLayout, 20 | QGroupBox, QHBoxLayout, QLabel, QLayout, 21 | QLineEdit, QMainWindow, QMenu, QMenuBar, 22 | QPlainTextEdit, QProgressBar, QPushButton, QSizePolicy, 23 | QSpinBox, QStatusBar, QTabWidget, QVBoxLayout, 24 | QWidget) 25 | from . import resources_rc 26 | 27 | class Ui_MainWindow(object): 28 | def setupUi(self, MainWindow): 29 | if not MainWindow.objectName(): 30 | MainWindow.setObjectName(u"MainWindow") 31 | MainWindow.resize(940, 490) 32 | self.actionImport = QAction(MainWindow) 33 | self.actionImport.setObjectName(u"actionImport") 34 | self.actionExport = QAction(MainWindow) 35 | self.actionExport.setObjectName(u"actionExport") 36 | self.actionExit = QAction(MainWindow) 37 | self.actionExit.setObjectName(u"actionExit") 38 | self.actionDocs = QAction(MainWindow) 39 | self.actionDocs.setObjectName(u"actionDocs") 40 | self.actionGithub = QAction(MainWindow) 41 | self.actionGithub.setObjectName(u"actionGithub") 42 | self.actionAbout = QAction(MainWindow) 43 | self.actionAbout.setObjectName(u"actionAbout") 44 | self.centralwidget = QWidget(MainWindow) 45 | self.centralwidget.setObjectName(u"centralwidget") 46 | self.centralwidget.setAutoFillBackground(False) 47 | self.verticalLayout_2 = QVBoxLayout(self.centralwidget) 48 | self.verticalLayout_2.setSpacing(6) 49 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 50 | self.verticalLayout_2.setContentsMargins(9, -1, -1, 3) 51 | self.main_horizontalLayout = QHBoxLayout() 52 | self.main_horizontalLayout.setSpacing(12) 53 | self.main_horizontalLayout.setObjectName(u"main_horizontalLayout") 54 | self.main_horizontalLayout.setContentsMargins(0, 0, 0, 0) 55 | self.left_verticalLayout = QVBoxLayout() 56 | self.left_verticalLayout.setSpacing(6) 57 | self.left_verticalLayout.setObjectName(u"left_verticalLayout") 58 | self.left_verticalLayout.setSizeConstraint(QLayout.SetDefaultConstraint) 59 | self.left_verticalLayout.setContentsMargins(-1, 0, -1, -1) 60 | self.deviceSettings_horizontalLayout = QHBoxLayout() 61 | self.deviceSettings_horizontalLayout.setObjectName(u"deviceSettings_horizontalLayout") 62 | self.deviceSettings_horizontalLayout.setContentsMargins(0, 0, 0, 0) 63 | self.device_list = QComboBox(self.centralwidget) 64 | self.device_list.setObjectName(u"device_list") 65 | self.device_list.setMinimumSize(QSize(400, 0)) 66 | 67 | self.deviceSettings_horizontalLayout.addWidget(self.device_list) 68 | 69 | self.settingsButton = QPushButton(self.centralwidget) 70 | self.settingsButton.setObjectName(u"settingsButton") 71 | sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) 72 | sizePolicy.setHorizontalStretch(0) 73 | sizePolicy.setVerticalStretch(0) 74 | sizePolicy.setHeightForWidth(self.settingsButton.sizePolicy().hasHeightForWidth()) 75 | self.settingsButton.setSizePolicy(sizePolicy) 76 | self.settingsButton.setMinimumSize(QSize(0, 0)) 77 | self.settingsButton.setMaximumSize(QSize(30, 16777215)) 78 | icon = QIcon() 79 | icon.addFile(u":/icons/icons/gear.png", QSize(), QIcon.Normal, QIcon.Off) 80 | self.settingsButton.setIcon(icon) 81 | 82 | self.deviceSettings_horizontalLayout.addWidget(self.settingsButton) 83 | 84 | self.cpu_usage = QProgressBar(self.centralwidget) 85 | self.cpu_usage.setObjectName(u"cpu_usage") 86 | sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) 87 | sizePolicy1.setHorizontalStretch(0) 88 | sizePolicy1.setVerticalStretch(0) 89 | sizePolicy1.setHeightForWidth(self.cpu_usage.sizePolicy().hasHeightForWidth()) 90 | self.cpu_usage.setSizePolicy(sizePolicy1) 91 | self.cpu_usage.setMaximumSize(QSize(25, 25)) 92 | self.cpu_usage.setMaximum(130) 93 | self.cpu_usage.setValue(0) 94 | self.cpu_usage.setOrientation(Qt.Vertical) 95 | 96 | self.deviceSettings_horizontalLayout.addWidget(self.cpu_usage) 97 | 98 | 99 | self.left_verticalLayout.addLayout(self.deviceSettings_horizontalLayout) 100 | 101 | self.pages = QTabWidget(self.centralwidget) 102 | self.pages.setObjectName(u"pages") 103 | sizePolicy2 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) 104 | sizePolicy2.setHorizontalStretch(0) 105 | sizePolicy2.setVerticalStretch(0) 106 | sizePolicy2.setHeightForWidth(self.pages.sizePolicy().hasHeightForWidth()) 107 | self.pages.setSizePolicy(sizePolicy2) 108 | self.pages.setAutoFillBackground(False) 109 | self.pages.setStyleSheet(u"b") 110 | self.page_1 = QWidget() 111 | self.page_1.setObjectName(u"page_1") 112 | self.gridLayout_2 = QGridLayout(self.page_1) 113 | self.gridLayout_2.setObjectName(u"gridLayout_2") 114 | self.pages.addTab(self.page_1, "") 115 | self.page_2 = QWidget() 116 | self.page_2.setObjectName(u"page_2") 117 | self.gridLayout_3 = QGridLayout(self.page_2) 118 | self.gridLayout_3.setObjectName(u"gridLayout_3") 119 | self.pages.addTab(self.page_2, "") 120 | self.page_3 = QWidget() 121 | self.page_3.setObjectName(u"page_3") 122 | self.gridLayout_11 = QGridLayout(self.page_3) 123 | self.gridLayout_11.setObjectName(u"gridLayout_11") 124 | self.pages.addTab(self.page_3, "") 125 | self.page_4 = QWidget() 126 | self.page_4.setObjectName(u"page_4") 127 | self.gridLayout_10 = QGridLayout(self.page_4) 128 | self.gridLayout_10.setObjectName(u"gridLayout_10") 129 | self.pages.addTab(self.page_4, "") 130 | self.page_5 = QWidget() 131 | self.page_5.setObjectName(u"page_5") 132 | self.gridLayout_9 = QGridLayout(self.page_5) 133 | self.gridLayout_9.setObjectName(u"gridLayout_9") 134 | self.pages.addTab(self.page_5, "") 135 | self.page_6 = QWidget() 136 | self.page_6.setObjectName(u"page_6") 137 | self.gridLayout_8 = QGridLayout(self.page_6) 138 | self.gridLayout_8.setObjectName(u"gridLayout_8") 139 | self.pages.addTab(self.page_6, "") 140 | self.page_7 = QWidget() 141 | self.page_7.setObjectName(u"page_7") 142 | self.gridLayout_7 = QGridLayout(self.page_7) 143 | self.gridLayout_7.setObjectName(u"gridLayout_7") 144 | self.pages.addTab(self.page_7, "") 145 | self.page_8 = QWidget() 146 | self.page_8.setObjectName(u"page_8") 147 | self.gridLayout_6 = QGridLayout(self.page_8) 148 | self.gridLayout_6.setObjectName(u"gridLayout_6") 149 | self.pages.addTab(self.page_8, "") 150 | self.page_9 = QWidget() 151 | self.page_9.setObjectName(u"page_9") 152 | self.gridLayout_5 = QGridLayout(self.page_9) 153 | self.gridLayout_5.setObjectName(u"gridLayout_5") 154 | self.pages.addTab(self.page_9, "") 155 | self.tab_10 = QWidget() 156 | self.tab_10.setObjectName(u"tab_10") 157 | self.gridLayout_4 = QGridLayout(self.tab_10) 158 | self.gridLayout_4.setObjectName(u"gridLayout_4") 159 | self.pages.addTab(self.tab_10, "") 160 | 161 | self.left_verticalLayout.addWidget(self.pages) 162 | 163 | self.left_verticalLayout.setStretch(1, 1) 164 | 165 | self.main_horizontalLayout.addLayout(self.left_verticalLayout) 166 | 167 | self.right_horizontalLayout = QHBoxLayout() 168 | self.right_horizontalLayout.setObjectName(u"right_horizontalLayout") 169 | self.groupBox = QGroupBox(self.centralwidget) 170 | self.groupBox.setObjectName(u"groupBox") 171 | self.groupBox.setMinimumSize(QSize(250, 0)) 172 | self.verticalLayout_3 = QVBoxLayout(self.groupBox) 173 | self.verticalLayout_3.setObjectName(u"verticalLayout_3") 174 | self.formLayout = QFormLayout() 175 | self.formLayout.setObjectName(u"formLayout") 176 | self.label = QLabel(self.groupBox) 177 | self.label.setObjectName(u"label") 178 | 179 | self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label) 180 | 181 | self.horizontalLayout_2 = QHBoxLayout() 182 | self.horizontalLayout_2.setSpacing(6) 183 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 184 | self.imageButton = QPushButton(self.groupBox) 185 | self.imageButton.setObjectName(u"imageButton") 186 | 187 | self.horizontalLayout_2.addWidget(self.imageButton) 188 | 189 | self.removeButton = QPushButton(self.groupBox) 190 | self.removeButton.setObjectName(u"removeButton") 191 | sizePolicy3 = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 192 | sizePolicy3.setHorizontalStretch(0) 193 | sizePolicy3.setVerticalStretch(0) 194 | sizePolicy3.setHeightForWidth(self.removeButton.sizePolicy().hasHeightForWidth()) 195 | self.removeButton.setSizePolicy(sizePolicy3) 196 | self.removeButton.setMaximumSize(QSize(30, 16777215)) 197 | icon1 = QIcon() 198 | icon1.addFile(u":/icons/icons/cross.png", QSize(), QIcon.Normal, QIcon.Off) 199 | self.removeButton.setIcon(icon1) 200 | 201 | self.horizontalLayout_2.addWidget(self.removeButton) 202 | 203 | 204 | self.formLayout.setLayout(0, QFormLayout.FieldRole, self.horizontalLayout_2) 205 | 206 | self.label_2 = QLabel(self.groupBox) 207 | self.label_2.setObjectName(u"label_2") 208 | 209 | self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_2) 210 | 211 | self.label_3 = QLabel(self.groupBox) 212 | self.label_3.setObjectName(u"label_3") 213 | 214 | self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label_3) 215 | 216 | self.command = QLineEdit(self.groupBox) 217 | self.command.setObjectName(u"command") 218 | 219 | self.formLayout.setWidget(2, QFormLayout.FieldRole, self.command) 220 | 221 | self.label_5 = QLabel(self.groupBox) 222 | self.label_5.setObjectName(u"label_5") 223 | 224 | self.formLayout.setWidget(3, QFormLayout.LabelRole, self.label_5) 225 | 226 | self.keys = QComboBox(self.groupBox) 227 | self.keys.addItem(u"") 228 | self.keys.addItem(u"F11") 229 | self.keys.addItem(u"alt+F4") 230 | self.keys.addItem(u"ctrl+w") 231 | self.keys.addItem(u"cmd+left") 232 | self.keys.addItem(u"alt+plus") 233 | self.keys.addItem(u"alt+delay+F3") 234 | self.keys.addItem(u"backspace") 235 | self.keys.addItem(u"right") 236 | self.keys.addItem(u"page_up") 237 | self.keys.addItem(u"media_volume_up") 238 | self.keys.addItem(u"media_volume_down") 239 | self.keys.addItem(u"media_volume_mute") 240 | self.keys.addItem(u"media_previous") 241 | self.keys.addItem(u"media_next") 242 | self.keys.addItem(u"media_play_pause") 243 | self.keys.setObjectName(u"keys") 244 | self.keys.setEditable(True) 245 | 246 | self.formLayout.setWidget(3, QFormLayout.FieldRole, self.keys) 247 | 248 | self.label_8 = QLabel(self.groupBox) 249 | self.label_8.setObjectName(u"label_8") 250 | 251 | self.formLayout.setWidget(4, QFormLayout.LabelRole, self.label_8) 252 | 253 | self.switch_page = QSpinBox(self.groupBox) 254 | self.switch_page.setObjectName(u"switch_page") 255 | self.switch_page.setMinimum(0) 256 | self.switch_page.setMaximum(10) 257 | self.switch_page.setValue(0) 258 | 259 | self.formLayout.setWidget(4, QFormLayout.FieldRole, self.switch_page) 260 | 261 | self.label_7 = QLabel(self.groupBox) 262 | self.label_7.setObjectName(u"label_7") 263 | 264 | self.formLayout.setWidget(5, QFormLayout.LabelRole, self.label_7) 265 | 266 | self.change_brightness = QSpinBox(self.groupBox) 267 | self.change_brightness.setObjectName(u"change_brightness") 268 | self.change_brightness.setMinimum(-99) 269 | 270 | self.formLayout.setWidget(5, QFormLayout.FieldRole, self.change_brightness) 271 | 272 | self.label_6 = QLabel(self.groupBox) 273 | self.label_6.setObjectName(u"label_6") 274 | 275 | self.formLayout.setWidget(6, QFormLayout.LabelRole, self.label_6) 276 | 277 | self.write = QPlainTextEdit(self.groupBox) 278 | self.write.setObjectName(u"write") 279 | 280 | self.formLayout.setWidget(6, QFormLayout.FieldRole, self.write) 281 | 282 | self.horizontalLayout_3 = QHBoxLayout() 283 | self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") 284 | self.text = QLineEdit(self.groupBox) 285 | self.text.setObjectName(u"text") 286 | 287 | self.horizontalLayout_3.addWidget(self.text) 288 | 289 | self.textButton = QPushButton(self.groupBox) 290 | self.textButton.setObjectName(u"textButton") 291 | self.textButton.setMinimumSize(QSize(30, 0)) 292 | self.textButton.setMaximumSize(QSize(30, 16777215)) 293 | icon2 = QIcon() 294 | icon2.addFile(u":/icons/icons/vertical-align.png", QSize(), QIcon.Normal, QIcon.Off) 295 | self.textButton.setIcon(icon2) 296 | 297 | self.horizontalLayout_3.addWidget(self.textButton) 298 | 299 | 300 | self.formLayout.setLayout(1, QFormLayout.FieldRole, self.horizontalLayout_3) 301 | 302 | 303 | self.verticalLayout_3.addLayout(self.formLayout) 304 | 305 | 306 | self.right_horizontalLayout.addWidget(self.groupBox) 307 | 308 | 309 | self.main_horizontalLayout.addLayout(self.right_horizontalLayout) 310 | 311 | 312 | self.verticalLayout_2.addLayout(self.main_horizontalLayout) 313 | 314 | MainWindow.setCentralWidget(self.centralwidget) 315 | self.menubar = QMenuBar(MainWindow) 316 | self.menubar.setObjectName(u"menubar") 317 | self.menubar.setGeometry(QRect(0, 0, 940, 22)) 318 | self.menuFile = QMenu(self.menubar) 319 | self.menuFile.setObjectName(u"menuFile") 320 | self.menuHelp = QMenu(self.menubar) 321 | self.menuHelp.setObjectName(u"menuHelp") 322 | MainWindow.setMenuBar(self.menubar) 323 | self.statusbar = QStatusBar(MainWindow) 324 | self.statusbar.setObjectName(u"statusbar") 325 | MainWindow.setStatusBar(self.statusbar) 326 | QWidget.setTabOrder(self.device_list, self.settingsButton) 327 | QWidget.setTabOrder(self.settingsButton, self.pages) 328 | QWidget.setTabOrder(self.pages, self.imageButton) 329 | QWidget.setTabOrder(self.imageButton, self.removeButton) 330 | QWidget.setTabOrder(self.removeButton, self.text) 331 | QWidget.setTabOrder(self.text, self.textButton) 332 | QWidget.setTabOrder(self.textButton, self.command) 333 | QWidget.setTabOrder(self.command, self.keys) 334 | QWidget.setTabOrder(self.keys, self.switch_page) 335 | QWidget.setTabOrder(self.switch_page, self.change_brightness) 336 | QWidget.setTabOrder(self.change_brightness, self.write) 337 | 338 | self.menubar.addAction(self.menuFile.menuAction()) 339 | self.menubar.addAction(self.menuHelp.menuAction()) 340 | self.menuFile.addAction(self.actionImport) 341 | self.menuFile.addAction(self.actionExport) 342 | self.menuFile.addSeparator() 343 | self.menuFile.addAction(self.actionExit) 344 | self.menuHelp.addAction(self.actionDocs) 345 | self.menuHelp.addAction(self.actionGithub) 346 | self.menuHelp.addSeparator() 347 | self.menuHelp.addAction(self.actionAbout) 348 | 349 | self.retranslateUi(MainWindow) 350 | 351 | self.pages.setCurrentIndex(0) 352 | 353 | 354 | QMetaObject.connectSlotsByName(MainWindow) 355 | # setupUi 356 | 357 | def retranslateUi(self, MainWindow): 358 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Stream Deck UI", None)) 359 | self.actionImport.setText(QCoreApplication.translate("MainWindow", u"Import", None)) 360 | self.actionExport.setText(QCoreApplication.translate("MainWindow", u"Export", None)) 361 | self.actionExit.setText(QCoreApplication.translate("MainWindow", u"Exit", None)) 362 | self.actionDocs.setText(QCoreApplication.translate("MainWindow", u"Documentation", None)) 363 | self.actionGithub.setText(QCoreApplication.translate("MainWindow", u"Github", None)) 364 | self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"About...", None)) 365 | self.settingsButton.setText("") 366 | self.cpu_usage.setFormat("") 367 | self.pages.setTabText(self.pages.indexOf(self.page_1), QCoreApplication.translate("MainWindow", u"Page 1", None)) 368 | self.pages.setTabText(self.pages.indexOf(self.page_2), QCoreApplication.translate("MainWindow", u"2", None)) 369 | self.pages.setTabText(self.pages.indexOf(self.page_3), QCoreApplication.translate("MainWindow", u"3", None)) 370 | self.pages.setTabText(self.pages.indexOf(self.page_4), QCoreApplication.translate("MainWindow", u"4", None)) 371 | self.pages.setTabText(self.pages.indexOf(self.page_5), QCoreApplication.translate("MainWindow", u"5", None)) 372 | self.pages.setTabText(self.pages.indexOf(self.page_6), QCoreApplication.translate("MainWindow", u"6", None)) 373 | self.pages.setTabText(self.pages.indexOf(self.page_7), QCoreApplication.translate("MainWindow", u"7", None)) 374 | self.pages.setTabText(self.pages.indexOf(self.page_8), QCoreApplication.translate("MainWindow", u"8", None)) 375 | self.pages.setTabText(self.pages.indexOf(self.page_9), QCoreApplication.translate("MainWindow", u"9", None)) 376 | self.pages.setTabText(self.pages.indexOf(self.tab_10), QCoreApplication.translate("MainWindow", u"10", None)) 377 | self.groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Configure Button", None)) 378 | self.label.setText(QCoreApplication.translate("MainWindow", u"Image:", None)) 379 | self.imageButton.setText(QCoreApplication.translate("MainWindow", u"Image...", None)) 380 | #if QT_CONFIG(tooltip) 381 | self.removeButton.setToolTip(QCoreApplication.translate("MainWindow", u"Remove the image from the button", None)) 382 | #endif // QT_CONFIG(tooltip) 383 | self.removeButton.setText("") 384 | self.label_2.setText(QCoreApplication.translate("MainWindow", u"Label:", None)) 385 | self.label_3.setText(QCoreApplication.translate("MainWindow", u"Command:", None)) 386 | self.label_5.setText(QCoreApplication.translate("MainWindow", u"Press Keys:", None)) 387 | 388 | self.label_8.setText(QCoreApplication.translate("MainWindow", u"Switch Page:", None)) 389 | self.label_7.setText(QCoreApplication.translate("MainWindow", u"Brightness +/-:", None)) 390 | self.label_6.setText(QCoreApplication.translate("MainWindow", u"Write Text:", None)) 391 | #if QT_CONFIG(tooltip) 392 | self.textButton.setToolTip(QCoreApplication.translate("MainWindow", u"Text vertical alignment", None)) 393 | #endif // QT_CONFIG(tooltip) 394 | self.textButton.setText("") 395 | self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) 396 | self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) 397 | # retranslateUi 398 | 399 | -------------------------------------------------------------------------------- /streamdeck_ui/main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 940 10 | 490 11 | 12 | 13 | 14 | Stream Deck UI 15 | 16 | 17 | 18 | false 19 | 20 | 21 | 22 | 6 23 | 24 | 25 | 9 26 | 27 | 28 | 3 29 | 30 | 31 | 32 | 33 | 12 34 | 35 | 36 | 0 37 | 38 | 39 | 0 40 | 41 | 42 | 0 43 | 44 | 45 | 0 46 | 47 | 48 | 49 | 50 | 6 51 | 52 | 53 | QLayout::SetDefaultConstraint 54 | 55 | 56 | 0 57 | 58 | 59 | 60 | 61 | 0 62 | 63 | 64 | 0 65 | 66 | 67 | 0 68 | 69 | 70 | 0 71 | 72 | 73 | 74 | 75 | 76 | 400 77 | 0 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 0 87 | 0 88 | 89 | 90 | 91 | 92 | 0 93 | 0 94 | 95 | 96 | 97 | 98 | 30 99 | 16777215 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | :/icons/icons/gear.png:/icons/icons/gear.png 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 0 116 | 0 117 | 118 | 119 | 120 | 121 | 25 122 | 25 123 | 124 | 125 | 126 | 130 127 | 128 | 129 | 0 130 | 131 | 132 | Qt::Vertical 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 0 146 | 0 147 | 148 | 149 | 150 | false 151 | 152 | 153 | b 154 | 155 | 156 | 0 157 | 158 | 159 | 160 | Page 1 161 | 162 | 163 | 164 | 165 | 166 | 2 167 | 168 | 169 | 170 | 171 | 172 | 3 173 | 174 | 175 | 176 | 177 | 178 | 4 179 | 180 | 181 | 182 | 183 | 184 | 5 185 | 186 | 187 | 188 | 189 | 190 | 6 191 | 192 | 193 | 194 | 195 | 196 | 7 197 | 198 | 199 | 200 | 201 | 202 | 8 203 | 204 | 205 | 206 | 207 | 208 | 9 209 | 210 | 211 | 212 | 213 | 214 | 10 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 250 229 | 0 230 | 231 | 232 | 233 | Configure Button 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | Image: 242 | 243 | 244 | 245 | 246 | 247 | 248 | 6 249 | 250 | 251 | 252 | 253 | Image... 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 0 262 | 0 263 | 264 | 265 | 266 | 267 | 30 268 | 16777215 269 | 270 | 271 | 272 | Remove the image from the button 273 | 274 | 275 | 276 | 277 | 278 | 279 | :/icons/icons/cross.png:/icons/icons/cross.png 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | Label: 289 | 290 | 291 | 292 | 293 | 294 | 295 | Command: 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | Press Keys: 306 | 307 | 308 | 309 | 310 | 311 | 312 | true 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | F11 322 | 323 | 324 | 325 | 326 | alt+F4 327 | 328 | 329 | 330 | 331 | ctrl+w 332 | 333 | 334 | 335 | 336 | cmd+left 337 | 338 | 339 | 340 | 341 | alt+plus 342 | 343 | 344 | 345 | 346 | alt+delay+F3 347 | 348 | 349 | 350 | 351 | backspace 352 | 353 | 354 | 355 | 356 | right 357 | 358 | 359 | 360 | 361 | page_up 362 | 363 | 364 | 365 | 366 | media_volume_up 367 | 368 | 369 | 370 | 371 | media_volume_down 372 | 373 | 374 | 375 | 376 | media_volume_mute 377 | 378 | 379 | 380 | 381 | media_previous 382 | 383 | 384 | 385 | 386 | media_next 387 | 388 | 389 | 390 | 391 | media_play_pause 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | Switch Page: 400 | 401 | 402 | 403 | 404 | 405 | 406 | 0 407 | 408 | 409 | 10 410 | 411 | 412 | 0 413 | 414 | 415 | 416 | 417 | 418 | 419 | Brightness +/-: 420 | 421 | 422 | 423 | 424 | 425 | 426 | -99 427 | 428 | 429 | 430 | 431 | 432 | 433 | Write Text: 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 30 450 | 0 451 | 452 | 453 | 454 | 455 | 30 456 | 16777215 457 | 458 | 459 | 460 | Text vertical alignment 461 | 462 | 463 | 464 | 465 | 466 | 467 | :/icons/icons/vertical-align.png:/icons/icons/vertical-align.png 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 0 488 | 0 489 | 940 490 | 22 491 | 492 | 493 | 494 | 495 | File 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | Help 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | Import 518 | 519 | 520 | 521 | 522 | Export 523 | 524 | 525 | 526 | 527 | Exit 528 | 529 | 530 | 531 | 532 | Documentation 533 | 534 | 535 | 536 | 537 | Github 538 | 539 | 540 | 541 | 542 | About... 543 | 544 | 545 | 546 | 547 | device_list 548 | settingsButton 549 | pages 550 | imageButton 551 | removeButton 552 | text 553 | textButton 554 | command 555 | keys 556 | switch_page 557 | change_brightness 558 | write 559 | 560 | 561 | 562 | 563 | 564 | 565 | --------------------------------------------------------------------------------