├── 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 |
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 | [](https://timothycrosley.github.io/streamdeck-ui/)
2 | _________________
3 |
4 | [](http://badge.fury.io/py/streamdeck-ui)
5 | [](https://github.com/timothycrosley/streamdeck-ui/actions?query=workflow%3ATest)
6 | [](https://codecov.io/gh/timothycrosley/streamdeck-ui)
7 | [](https://gitter.im/timothycrosley/streamdeck-ui?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8 | [](https://pypi.python.org/pypi/streamdeck-ui/)
9 | [](https://pepy.tech/project/streamdeck-ui)
10 | [](https://github.com/psf/black)
11 | [](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 | 
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 | 
138 |
139 | ## Known issues
140 | Confirm you are running the latest release with `pip show streamdeck-ui`. Compare it to: [](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 | [](https://timothycrosley.github.io/streamdeck-ui/)
2 | _________________
3 |
4 | [](http://badge.fury.io/py/streamdeck-ui)
5 | [](https://github.com/timothycrosley/streamdeck-ui/actions?query=workflow%3ATest)
6 | [](https://codecov.io/gh/timothycrosley/streamdeck-ui)
7 | [](https://gitter.im/timothycrosley/streamdeck-ui?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8 | [](https://pypi.python.org/pypi/streamdeck-ui/)
9 | [](https://pepy.tech/project/streamdeck-ui)
10 | [](https://github.com/psf/black)
11 | [](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 | 
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 | 
183 |
184 | ## bekannte Probleme
185 | Stellen sie sicher, dass sie die neueste Version verwenden mit `pip3 show streamdeck-ui`. Vergleichen sie es mit: [](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 |
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 |
--------------------------------------------------------------------------------