Compare commits

...

69 Commits

Author SHA1 Message Date
eeb18679e6 add option to debug command output 2024-01-01 01:52:19 +01:00
2046adfc79 add update page button to update button states only, delay button update state to let process to finish, overwrite button text if attr description is defined, empty and label class inherit from tile class, process with callback option 2024-01-01 00:11:17 +01:00
bb8709bdf5 fix systemd unit file 2023-12-28 19:10:25 +01:00
212a6ab426 update systemd service unit to wait for env DISPLAY and XAUTHORITY 2023-12-28 19:00:03 +01:00
2622d514c4 fix cast of command which might not be a number 2023-12-28 15:42:01 +01:00
b8d94b271f fix slider config cast 2023-12-24 01:47:57 +01:00
7c3cd04c1b working slider 2023-12-24 00:34:46 +01:00
6d502ea67e fix source sliders 2023-12-23 16:10:20 +01:00
165879eb0d fix color buttons 2023-12-23 15:18:14 +01:00
ed851b0eed add source volume slider, add gui config flags, add gui menu 2023-12-23 14:36:36 +01:00
6abaf515ae fix config 2023-12-21 13:26:55 +01:00
b972354e97 first step for slider class 2023-12-19 21:43:21 +01:00
dec5642e26 fix volume input sinks to handle also mono streams 2023-12-10 04:19:05 +01:00
310d396407 fix static dir 2023-12-09 12:50:16 +01:00
4594b55bf4 change colors 2023-12-09 09:29:32 +01:00
d1581e866f add label class, move empty buton to empty label 2023-10-18 20:32:51 +02:00
c50b5e1ce9 command output 2023-05-29 19:35:55 +02:00
bbda532246 move test button to debug 2023-01-16 13:02:07 +01:00
ead2b06eca cli args for host and port 2023-01-16 12:51:54 +01:00
db0a3b1655 fix app name 2022-11-20 18:24:45 +01:00
180f571018 fix json load of string including an stderr output 2022-11-08 19:43:15 +01:00
707453a513 fix json load of string including an stderr output 2022-11-08 19:39:42 +01:00
21c91b0b9c fix justpy config overwrite 2022-11-06 15:51:47 +01:00
b32a63d9b4 fix config file variable 2022-10-30 16:55:18 +01:00
a9636a4112 fix static directory 2022-10-30 16:45:24 +01:00
18af7036bb comment out xdg ref and profile 2022-10-03 02:24:49 +02:00
310d31e98d fix controldeck entry 2022-10-01 13:35:39 +02:00
6a3995f69a change style to Quasar 2022-10-01 13:16:19 +02:00
bc7a5d7239 fix to fit text into button using js and fix classname setting to not find wrong ids and update README 2021-09-07 23:25:52 +02:00
596e188c75 fix to also change icon in window titlebar 2021-08-26 22:37:11 +02:00
9e0ecced8b add windowclassname 2021-08-26 22:00:02 +02:00
a21babbf8d rename .desktop file to indicate local setting and create a new file for a system installation 2021-08-26 00:36:22 +02:00
7b205c761b rename systemd service file to indicate local setting and create a new file for a system installation 2021-08-25 22:43:59 +02:00
528dccb255 add version file and refer to it for the setup 2021-08-24 23:54:09 +02:00
00cdcd0cc9 multi-user.taget not working for user service. Autostart will not work. 2021-08-08 13:48:54 +02:00
c78b767958 update style (border) and logic of buttons 2021-07-24 20:24:35 +02:00
cd13c8d021 update button switch logic 2021-07-24 19:52:31 +02:00
78e27a8460 add example for the optinal status field 2021-07-22 15:22:45 +02:00
fba0e91224 create message box if controldeck process is not running 2021-07-19 14:22:20 +02:00
e83c347442 improve status output for command 2021-07-19 14:10:29 +02:00
87a2e4542e change group style of sound buttons 2021-07-14 21:49:43 +02:00
d9bdd5c9e8 add an optional status field to display command and state of a button 2021-07-14 21:05:37 +02:00
65c37e636c add cairosvg fallback 2021-04-20 01:09:55 +02:00
fec87df9c9 add pid and start flag for gui, add systemd service file and .desktop file with new setup.sh 2021-04-19 21:18:25 +02:00
96829d0b6a import only used funtions 2021-04-15 11:01:16 +02:00
b8f26d6965 add cli with optional config path 2021-04-13 13:34:57 +02:00
7342721715 add config parameter for the gui url 2021-04-13 12:47:52 +02:00
ad79872631 add microphone / source buttons 2021-04-09 00:48:15 +02:00
2f257a1ee7 fix button command execution without altative command 2021-04-08 14:42:49 +02:00
25834e99da normal buttons with alternative text, icon or image 2021-04-07 21:22:18 +02:00
b31e12d148 volume buttons with alternative text, icon or image 2021-04-07 18:17:16 +02:00
b09e1d680e icon and image for volume buttons 2021-04-07 15:18:32 +02:00
e5ddc47587 remove old commented lines 2021-04-07 14:17:06 +02:00
7581078bba change volume button style, volume status with mute and toggle button and move button logic to button class 2021-04-07 14:12:50 +02:00
bbc8d22abb fix wrong catch for unset icon 2021-04-06 15:37:29 +02:00
d82503b4ff make it possible to use home alias ~ for icon-image 2021-04-05 22:26:15 +02:00
67ff81900d add icon-image for svg icon buttons 2021-04-05 22:19:20 +02:00
3447f5b545 add example for empty buttons 2021-04-05 18:47:51 +02:00
1b3d915de6 add empty buttons 2021-04-05 18:46:08 +02:00
bd738d600e add color-bg and color-fg in conf file for volume and normal buttons 2021-04-05 17:14:10 +02:00
cfed2fb677 add flag to process function to not ask for output, add reload button and close button only for gui, change icon config to be able to specify style 2021-04-05 15:53:53 +02:00
8d7c2523f4 add possibility to use icons instead 2021-04-04 23:32:00 +02:00
f108910b87 fix process recognizer 2021-04-04 22:07:53 +02:00
e563470b64 put config block in a function, add config for gui and update example conf 2021-04-04 21:46:50 +02:00
ea1b8f558e add webview gui, define text font, remove unused import 2021-04-04 20:31:19 +02:00
8c4a6a495c fix 'sink not found' for pamixer 2021-04-04 12:36:35 +02:00
c54817edc2 fix pactl fallback for increase and decrease 2021-04-04 00:18:52 +02:00
4ceaad99a3 fix pactl fallback for pamixer 2021-04-04 00:05:16 +02:00
431fa906b1 add setup.cfg 2021-04-03 23:41:09 +02:00
13 changed files with 1678 additions and 140 deletions

25
README
View File

@@ -1,16 +1,33 @@
Install
pip install --user -e .
Requirements:
- For volume buttons: libpulse
- controldeck
- python package justpy, the framework: pip install justpy --upgrade
- (optionally) for volume buttons: libpulse
- controldeck-gui
- render engine
- GTK: python-gobject, python-cairo, webkit2gtk>=2.22
- QT: qt5-webkit, python-qtpy
- QT5: python-pyqt5-webengine, python-pyqt5
- QT: pyside2, pyside6
local:
./setup.sh
Uninstall
pip uninstall controldeck
rm ~/.local/share/application/controldeck.desktop
rm ~/.config/systemd/user/controldeck.service
Configuration
~/.config/controldeck/controldeck.conf
See example in example directory.
Start (autostart) with systemd
systemctl --user start controldeck.service
systemctl --user enable controldeck.service
journalctl --user-unit=controldeck -e
journalctl --user-unit=controldeck -f

1
VERSION Normal file
View File

@@ -0,0 +1 @@
2022.10.01

1377
controldeck.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

215
controldeck_gui.py Executable file
View File

@@ -0,0 +1,215 @@
#!/usr/bin/env python
import sys
import os
import argparse
from tkinter import Tk, messagebox
import webview
from controldeck import config_load, process
import threading
import time
def thread_function(name):
# print("Thread %s: starting", name)
for i in range(10):
# print("Thread %s: finishing", name)
# p = process("xdotool search --name 'ControlDeck'")
# intersection of ControlDeck window name and empty classname
p = process("comm -12 <(xdotool search --name 'ControlDeck' | sort) <(xdotool search --classname '^$' | sort)", shell=True)
if p:
# print(p)
# process("xdotool search --name 'ControlDeck' set_window --class 'controldeck'", output=False)
# process("xdotool search --name 'ControlDeck' set_window --classname 'controldeck' --class 'ControlDeck' windowunmap windowmap", output=False) # will find to many wrong ids
process(f"xdotool set_window --classname 'controldeck' --class 'ControlDeck' {p} windowunmap {p} windowmap {p}", shell=True, output=False)
time.sleep(0.1)
def main(args, pid=-1):
config = config_load(conf=args.config)
host = config.get('default', 'host', fallback='0.0.0.0')
port = config.get('default', 'port', fallback='8000')
url = f"http://{host}:{port}/?gui&pid={str(pid)}"
try:
width = config.getint('gui', 'width', fallback=800)
except ValueError as e:
width = 800
print(f"Error width: {e}. fallback to: {width}")
try:
height = config.getint('gui', 'height', fallback=600)
except ValueError as e:
width = 600
print(f"Error height: {e}. fallback to {width}")
try:
x = config.getint('gui', 'x', fallback='')
except ValueError as e:
x = None
print(f"Error x: {e}. fallback to {x}")
try:
y = config.getint('gui', 'y', fallback='')
except ValueError as e:
y = None
print(f"Error y: {e}. fallback to: {y}")
resizable = config.get('gui', 'resizable', fallback='True').title() == 'True'
fullscreen = config.get('gui', 'fullscreen', fallback='False').title() == 'True'
try:
min_width = config.getint('gui', 'min_width', fallback=200)
except ValueError as e:
min_width = 200
print(f"Error min_width: {e}. fallback to: {min_width}")
try:
min_height = config.getint('gui', 'min_height', fallback=100)
except ValueError as e:
min_height = 100
print(f"Error min_height: {e}. fallback to: {min_height}")
min_size = (min_width, min_height)
frameless = config.get('gui', 'frameless', fallback='False').title() == 'True'
minimized = config.get('gui', 'minimized', fallback='False').title() == 'True'
maximized = config.get('gui', 'maximized', fallback='False').title() == 'True'
on_top = config.get('gui', 'always_on_top', fallback='False').title() == 'True'
confirm_close = config.get('gui', 'confirm_close', fallback='False').title() == 'True'
transparent = config.get('gui', 'transparent', fallback='True').title() == 'True'
gui_type = config.get('gui', 'gui_type', fallback=None)
gui_type = gui_type if gui_type != "" else None
menu = config.get('gui', 'menu', fallback='True').title() == 'True'
if args.debug:
print(f"config file [default]: {config.items('default')}")
print(f"config file [gui]: {config.items('gui')}")
#controldeck_process = process("ps --no-headers -C controldeck")
controldeck_process = process("ps --no-headers -C controldeck || ps aux | grep -e 'python.*controldeck.py' | grep -v grep", shell=True, output=True)
if args.start and controldeck_process == "":
cmd = "controldeck"
cmd += " --config={args.config}" if args.config else ""
print(cmd)
process(cmd, shell=True, output=False)
elif controldeck_process == "":
# cli output
print("controldeck is not running!")
# gui output
# Tkinter must have a root window. If you don't create one, one will be created for you. If you don't want this root window, create it and then hide it:
root = Tk()
root.withdraw()
messagebox.showinfo("ControlDeck", "controldeck is not running!")
# Other option would be to use the root window to display the information (Label, Button)
sys.exit(2)
window = webview.create_window(
title="ControlDeck",
url=url,
html=None,
js_api=None,
width=width,
height=height,
x=x,
y=y,
screen=None,
resizable=resizable,
fullscreen=fullscreen,
min_size=min_size,
hidden=False,
frameless=frameless,
easy_drag=True,
focus=True,
minimized=minimized,
maximized=maximized,
on_top=on_top,
confirm_close=confirm_close,
background_color='#000000',
transparent=transparent, # TODO: bug in qt; menu bar is transparent
text_select=False,
zoomable=False, # zoom via js
draggable=False,
vibrancy=False,
localization=None,
)
x = threading.Thread(target=thread_function, args=(1,))
x.start()
def menu_reload():
window = webview.active_window()
if window:
url = window.get_current_url()
window.load_url(url)
print(window.get_current_url())
print(window)
print(dir(window))
def menu_zoomin():
window = webview.active_window()
if window:
zoom = window.evaluate_js('document.documentElement.style.zoom')
if zoom == "":
zoom = 1.0
else:
zoom = float(zoom)
zoom += 0.1
print(f"zoom-in: {zoom}")
window.evaluate_js(f'document.documentElement.style.zoom = {zoom}')
print(f"set document.documentElement.style.zoom = {zoom}")
def menu_zoomout():
window = webview.active_window()
if window:
zoom = window.evaluate_js('document.documentElement.style.zoom')
if zoom == "":
zoom = 1.0
else:
zoom = float(zoom)
zoom -= 0.1
print(f"zoom-out: {zoom}")
window.evaluate_js(f'document.documentElement.style.zoom = {zoom}')
print(f"set document.documentElement.style.zoom = {zoom}")
def menu_zoomreset():
window = webview.active_window()
if window:
zoom = 1.0
print(f"zoom-reset: {zoom}")
window.evaluate_js(f'document.documentElement.style.zoom = {zoom}')
print(f"set document.documentElement.style.zoom = {zoom}")
menu_items = []
if menu:
menu_items = [webview.menu.Menu(
'Main', [
webview.menu.MenuAction('Reload', menu_reload),
webview.menu.MenuAction('zoom +', menu_zoomin),
webview.menu.MenuAction('zoom -', menu_zoomout),
webview.menu.MenuAction('zoom reset', menu_zoomreset),
]
)]
# TODO: zoom reset on reload (both from menu and within justpy)
# TODO: add zoom in config
# TODO: move zoom logic to justpy but then it is fix for all,
# maybe better a zoom argument in url address
def win_func(window):
print(window.get_current_url())
webview.start(
func=win_func,
args=window,
gui=gui_type, # TODO: bug in qt; any menu action is always the last action
debug=args.debug,
menu=menu_items,
)
def cli():
parser = argparse.ArgumentParser(
description=__doc__, prefix_chars='-',
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument('-c', '--config', nargs='?', type=str, default='',
help="Specify a path to a custom config file (default: ~/.config/controldeck/controldeck.conf)")
parser.add_argument('-s', '--start', action="store_true",
help="Start also controldeck program")
parser.add_argument('-v', '--verbose', action="store_true", help="Verbose output")
parser.add_argument('-D', '--debug', action='store_true', help=argparse.SUPPRESS)
args = parser.parse_args()
if args.debug:
print(args)
main(args, pid=os.getpid())
return 0
if __name__ == '__main__':
sys.exit(cli())

BIN
data/controldeck-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

8
data/controldeck.desktop Normal file
View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Name=ControlDeck
Exec=/usr/bin/controldeck-gui
Terminal=false
Type=Application
StartupNotify=true
StartupWMClass=controldeck
Icon=controldeck

View File

@@ -0,0 +1,7 @@
[Desktop Entry]
Name=ControlDeck
Exec=${HOME}/.local/bin/controldeck-gui
Terminal=false
Type=Application
StartupNotify=true
StartupWMClass=controldeck

22
data/controldeck.service Normal file
View File

@@ -0,0 +1,22 @@
[Unit]
Description=ControlDeck
ConditionFileIsExecutable=/usr/bin/controldeck
After=systemd-user-sessions.service getty@tty1.service plymouth-quit.service systemd-logind.service network.target
PartOf=graphical-session.target
[Service]
Environment=PYTHONUNBUFFERED=1
# add a pause to assure /etc/X11/xinit/xinitrc.d/50-systemd-user.sh to run
# this will add DISPLAY and XAUTHORITY env
# ExecStartPre=/bin/sleep 5
ExecStartPre=/bin/sh -c '(while test ! -v DISPLAY -o ! -v XAUTHORITY; do echo "wait for DISPLAY and XAUTHORITY"; sleep 2; done; echo "DISPLAY and XAUTHORITY found");'
# allowed time for the start
TimeoutStartSec=30
ExecStart=/usr/bin/controldeck -D
Restart=on-failure
RestartSec=4
StandardOutput=journal
[Install]
#WantedBy=default.target
WantedBy=graphical-session.target

View File

@@ -0,0 +1,16 @@
[Unit]
Description=ControlDeck
ConditionFileIsExecutable=%h/.local/bin/controldeck
After=network.target
[Service]
Environment=PYTHONUNBUFFERED=1
TimeoutStartSec=30
#ExecStartPre=/bin/sh -c 'source /etc/profile'
ExecStart=%h/.local/bin/controldeck -D
Restart=on-failure
RestartSec=4
StandardOutput=journal
[Install]
WantedBy=default.target

View File

@@ -1,20 +1,105 @@
# Examples:
#
# [N.volume.NAME]
# name = sink_name
# : N. optional group/row specification
# : NAME name of the button
# : name sink name, see name with either:
# pactl list sinks short
# pamixer --list-sinks
# [TAB:N.empty.NAME]
# : TAB optional tab name to specify tab group
# : N. optional number to specify group/row
# : NAME id of the empty spot
#
# [N.button.NAME]
# command = shell command
# second command
# ...
# : N. optional group/row specification
# : NAME name of the button
# : command command(s) to run
# [TAB:N.label.NAME]
# : TAB optional tab name to specify tab group
# : N. optional number to specify group/row
# : NAME id, name of the label
#
# [TAB:N.sink.NAME]
# : TAB optional tab name to specify tab group
# : N. optional number to specify group/row
# : NAME sink id name, see name with either:
# pactl list sinks short
# pamixer --list-sinks
# description = text for the sink
#
# [TAB:N.source.NAME]
# : TAB optional tab name to specify tab group
# : N. optional number to specify group/row
# : NAME source id name, see name with either:
# pactl list sources short
# pamixer --list-sources
# description = text for the source
#
# [TAB:N.sink-inputs]
# : TAB optional tab name to specify tab group
# : N. optional number to specify group/row
#
# [TAB:N.button.NAME]
# : TAB optional tab name to specify tab group
# : N. optional group/row specification
# : NAME id, name of the button
# text-alt = optional alternative burron text
# color-fg = foreground color as hex color code, e.g. #aa5500
# color-bg = background color as hex color code, e.g. #0055aa
# command = command(s) to run, seperated by new lines: shell command
# second command ...
# command-alt = optinal back-switch command(s) to run: shell command ...
# state-alt = string to define the alternative state (pressed)
# state-command = command to get the state: shell command ...
# icon = add icon in front of NAME, e.g. fas fa-play
# icon-alt = optional alternative icon
# image = absolte path to image file (svg, png)
# image-alt = optional alternative absolue path to image file
# [TAB:N.slider.NAME]
# : TAB optional tab name to specify tab group
# : N. optional group/row specification
# : NAME id, name of the button
# description = text for the slider
# icon = add icon in front of slider, e.g. tune
# min = minimum int value, e.g. 0
# max = maximum int value, e.g. 100
# step = step size, e.g. 1
# state-command = command to get the state: shell command
# command = command to run to get the value, using {value} in the command to
# interpolate the value: shell command
[default]
host = 0.0.0.0
port = 8000
# status = False
# volume-decrease-icon = fas fa-volume-down
# volume-increase-icon = fas fa-volume-up
# volume-mute-icon = fas fa-volume-off
# volume-mute-icon-alt = fas fa-volume-mute
# volume-decrease-image =
# volume-increase-image =
# volume-mute-image =
# volume-mute-image-alt =
# mic-decrease-icon = fas fa-volume-down
# mic-increase-icon = fas fa-volume-up
# mic-mute-icon = fas fa-microphone-alt
# mic-mute-icon-alt = fas fa-microphone-alt-slash
# mic-decrease-image =
# mic-increase-image =
# mic-mute-image =
# mic-mute-image-alt =
[gui]
width = 800
height = 600
# x and y specifying the window coordinate (empty = centered)
x =
y =
resizable = True
fullscreen = False
min_width = 200
min_height = 100
frameless = False
minimized = False
maximized = False
always_on_top = False
confirm_close = False
transparent = True
# gui_type: qt, gtk, cef, mshtml, edgechromium. or set env PYWEBVIEW_GUI
gui_type =
menu = True
[4.button.Test]
command = notify-send -a foo baz

16
setup.cfg Normal file
View File

@@ -0,0 +1,16 @@
[metadata]
name = ControlDeck
version = file: VERSION
[options]
install_requires =
justpy
pywebview
py_modules =
controldeck
controldeck_gui
[options.entry_points]
console_scripts =
controldeck = controldeck:cli
controldeck-gui = controldeck_gui:cli

View File

@@ -1,8 +1,2 @@
from setuptools import setup
setup(
name='ControlDeck',
py_modules=['controldeck'],
entry_points={
'console_scripts': ['controldeck = controldeck:main', ],},
)
setup()

10
setup.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
pip install --user -e .
mkdir -p $HOME/.local/share/applications
#ln -sf $PWD/data/controldeck.desktop $HOME/.local/share/applications/controldeck.desktop
cp data/controldeck.desktop.local $HOME/.local/share/applications/controldeck.desktop
sed -i "s|\${HOME}|${HOME}|" ~/.local/share/applications/controldeck.desktop
mkdir -p $HOME/.config/systemd/user
ln -sf $PWD/data/controldeck.service.local $HOME/.config/systemd/user/controldeck.service
#cp data/controldeck.service $HOME/.config/systemd/user/controldeck.service