Compare commits

..

5 Commits

Author SHA1 Message Date
9992bc9aa2 Remomve incorrect CSS property 2026-09-07 11:51:05 +01:00
9b8cffcc39 Make password optional for a RDP Config 2026-09-07 11:41:41 +01:00
1436f4ecb8 Fix: Shutdown/Restart actions
Feat: Add USB Forwarding selection
2026-09-07 11:32:41 +01:00
cc6d70baaf Introduce updater.py 2026-08-31 14:14:08 +01:00
0ad044b7e0 Adjust data display size to match clock display 2026-08-31 14:12:22 +01:00
7 changed files with 345 additions and 16 deletions

View File

@@ -2,15 +2,14 @@ import subprocess
import shutil import shutil
class ActionDispatcher: class ActionDispatcher:
def dispatch(self, config): def dispatch(self, config, usb_devices):
service_type = config.get("type", "command") service_type = config.get("type", "command")
print(f"Executing action -> Type: {service_type}") print(f"Executing action -> Type: {service_type}")
if service_type == "browser": if service_type == "browser":
self._launch_app_browser(config['target']) self._launch_app_browser(config['target'])
elif service_type == "rdp": elif service_type == "rdp":
## Refactor required to pass 'config' through to this level self._launch_xfreerdp(config, usb_devices)
self._launch_xfreerdp(config)
elif service_type == "command": elif service_type == "command":
self._run_command(config['cmd'].split()) self._run_command(config['cmd'].split())
elif service_type == "power": elif service_type == "power":
@@ -18,6 +17,12 @@ class ActionDispatcher:
else: else:
print(f"Service type: {service_type}, not known") print(f"Service type: {service_type}, not known")
def power_actions(self, action):
if action == "shutdown":
self._run_command(["systemctl", "poweroff"])
elif action == "reboot":
self._run_command(["systemctl", "reboot"])
def _launch_app_browser(self, url): def _launch_app_browser(self, url):
print(f"URL: {url}") print(f"URL: {url}")
browser_candidates = [ browser_candidates = [
@@ -48,7 +53,7 @@ class ActionDispatcher:
print(f"Launching browser command: {' '.join(cmd)}") print(f"Launching browser command: {' '.join(cmd)}")
self._run_command(cmd) self._run_command(cmd)
def _launch_xfreerdp(self, config): def _launch_xfreerdp(self, config, usb_devices):
rdp_candidates = [ rdp_candidates = [
"sdl-freerdp3", "sdl-freerdp3",
"sdl-freerdp", "sdl-freerdp",
@@ -70,7 +75,6 @@ class ActionDispatcher:
rdp_bin, rdp_bin,
f"/v:{config['target']}", f"/v:{config['target']}",
f"/u:{config['user']}", f"/u:{config['user']}",
f"/p:{config['password']}",
"/auth-pkg-list:!kerberos,!u2u", "/auth-pkg-list:!kerberos,!u2u",
"/sound", "/sound",
"/audio-mode:0", "/audio-mode:0",
@@ -80,9 +84,17 @@ class ActionDispatcher:
"/f" "/f"
] ]
if config.get('password', '') != "":
cmd.append(f"/p:{config['password']}")
if config.get('scale_desktop', '') != "": if config.get('scale_desktop', '') != "":
cmd.append(f"/scale-desktop:{config['scale_desktop']}") cmd.append(f"/scale-desktop:{config['scale_desktop']}")
for vendor_id, product_id in usb_devices:
if vendor_id and product_id:
print(f"Forwarding USB Device {vendor_id}:{product_id}")
cmd.append(f"/usb:id,dev:{vendor_id}:{product_id}")
self._run_command(cmd) self._run_command(cmd)
def _handle_power_action(self, action): def _handle_power_action(self, action):

View File

@@ -26,7 +26,7 @@ class LauncherApp(Gtk.Application):
self.ui = UIController( self.ui = UIController(
system_checks=self.checks, system_checks=self.checks,
power_action_callback=self.dispatcher.dispatch, power_action_callback=self.dispatcher.power_actions,
service_action_callback=self.dispatcher.dispatch, service_action_callback=self.dispatcher.dispatch,
services=self.config.get("services", []), services=self.config.get("services", []),
) )

View File

@@ -22,9 +22,10 @@ window {
/* Large, prominent clock */ /* Large, prominent clock */
label.date-display { label.date-display {
font-size: 28px; font-size: 32px;
opacity: 0.75; opacity: 0.75;
margin-right: 16px; margin-right: 16px;
color: #fafafa;
} }
label.clock-display { label.clock-display {
font-size: 32px; font-size: 32px;
@@ -115,9 +116,10 @@ button.image-card:active {
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
} }
/* Status Bar Icon Buttons */
/* Power Menu Button in the Status Bar - Pure Icon Only */ menubutton.flat,
menubutton.flat { button.status-bar-icon {
background: transparent; background: transparent;
background-color: transparent; background-color: transparent;
background-image: none; background-image: none;
@@ -126,7 +128,8 @@ menubutton.flat {
padding: 0; padding: 0;
} }
menubutton.flat > button { menubutton.flat > button,
button.status-bar-icon {
background: transparent; background: transparent;
background-color: transparent; background-color: transparent;
background-image: none; background-image: none;
@@ -134,19 +137,21 @@ menubutton.flat > button {
border: none; border: none;
box-shadow: none; box-shadow: none;
border-radius: 8px; border-radius: 8px;
padding: 6px 12px; /* Expands the clickable hit target across the full box */ padding: 6px 12px;
transition: all 200ms ease; transition: all 200ms ease;
} }
menubutton.flat:hover > button, menubutton.flat:hover > button,
menubutton.flat > button:hover { menubutton.flat > button:hover,
button.status-bar-icon:hover {
background-color: #27272a; background-color: #27272a;
color: #ffffff; color: #ffffff;
border: none; border: none;
box-shadow: none; box-shadow: none;
} }
/* When the popover menu is open (:checked or :active) */ /* Power menu active state */
menubutton.flat:checked > button, menubutton.flat:checked > button,
menubutton.flat:active > button, menubutton.flat:active > button,
menubutton.flat > button:checked, menubutton.flat > button:checked,
@@ -199,3 +204,105 @@ popover button.error-label:hover {
background-color: rgba(248, 113, 113, 0.15); background-color: rgba(248, 113, 113, 0.15);
color: #fca5a5; color: #fca5a5;
} }
/* USB Selection Dialog */
.usb-dialog {
background-color: #18181b;
color: #f4f4f5;
border: 1px solid #3f3f46;
border-radius: 16px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
}
/* Content */
.usb-dialog-content {
padding: 24px;
}
/* Description */
.usb-dialog-description {
font-size: 13px;
color: #a1a1aa;
margin-bottom: 16px;
}
/* Device list */
.usb-device-list {
background-color: #27272a;
border: 1px solid #3f3f46;
border-radius: 10px;
}
/* Device rows */
.usb-device-row {
background-color: transparent;
color: #f4f4f5;
border: none;
border-radius: 8px;
padding: 4px;
}
.usb-device-row:hover {
background-color: #3f3f46;
}
.usb-device-row:selected {
background-color: transparent;
}
/* Device checkboxes */
.usb-device-checkbox {
background-color: transparent;
color: #f4f4f5;
font-size: 14px;
padding: 8px 10px;
}
/* Dialog buttons */
.usb-dialog-buttons {
margin-top: 16px;
margin-bottom: 4px;
}
.usb-dialog-buttons button {
min-width: 90px;
padding: 8px 20px;
border-radius: 8px;
font-weight: 600;
background-image: none;
box-shadow: none;
text-shadow: none;
}
/* Cancel */
.usb-dialog-buttons button.usb-dialog-cancel {
background-color: #27272a;
color: #d4d4d8;
border: 1px solid #3f3f46;
}
.usb-dialog-buttons button.usb-dialog-cancel:hover {
background-color: #3f3f46;
color: #ffffff;
}
/* Select */
.usb-dialog-buttons button.usb-dialog-select {
background-color: #6366f1;
color: #ffffff;
border: 1px solid #6366f1;
}
.usb-dialog-buttons button.usb-dialog-select:hover {
background-color: #818cf8;
}

View File

@@ -3,6 +3,8 @@ import sys
import gi import gi
import os import os
from usbdev import USBDevices
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0") gi.require_version("Gdk", "4.0")
from gi.repository import Gdk, GLib, Gtk from gi.repository import Gdk, GLib, Gtk
@@ -21,6 +23,7 @@ class UIController:
self.window = self.builder.get_object("main_window") self.window = self.builder.get_object("main_window")
self._load_styles(css_file) self._load_styles(css_file)
self._setup_clock() self._setup_clock()
self._initialise_usb_options()
self._initialise_power_options() self._initialise_power_options()
self.power_action_callback = power_action_callback self.power_action_callback = power_action_callback
self.system_checks = system_checks self.system_checks = system_checks
@@ -29,6 +32,7 @@ class UIController:
self._cards_created = False self._cards_created = False
self._services_visible = False self._services_visible = False
self.usb_devices = []
if self.system_checks: if self.system_checks:
self._setup_system_status() self._setup_system_status()
@@ -108,7 +112,7 @@ class UIController:
title, title,
desc, desc,
bg_img, bg_img,
lambda svc=service: self.service_action_callback(svc), lambda svc=service: self.service_action_callback(svc, self.usb_devices),
) )
def _load_styles(self, css_file): def _load_styles(self, css_file):
@@ -133,18 +137,27 @@ class UIController:
update() update()
GLib.timeout_add_seconds(10, update) GLib.timeout_add_seconds(10, update)
def _initialise_usb_options(self):
btn_usb = self.builder.get_object("usb_menu_button")
if btn_usb:
btn_usb.connect(
"clicked",
lambda _: self._show_usb_selection()
)
def _initialise_power_options(self): def _initialise_power_options(self):
btn_reboot = self.builder.get_object("btn_reboot") btn_reboot = self.builder.get_object("btn_reboot")
btn_shutdown = self.builder.get_object("btn_shutdown") btn_shutdown = self.builder.get_object("btn_shutdown")
if btn_reboot: if btn_reboot:
btn_reboot.connect( btn_reboot.connect(
"clicked", lambda _: self.power_action_callback("power", "reboot") "clicked", lambda _: self.power_action_callback("reboot")
) )
if btn_shutdown: if btn_shutdown:
btn_shutdown.connect( btn_shutdown.connect(
"clicked", lambda _: self.power_action_callback("power", "shutdown") "clicked", lambda _: self.power_action_callback("shutdown")
) )
def set_app_title(self, title): def set_app_title(self, title):
@@ -214,6 +227,127 @@ class UIController:
self.window.set_application(app) self.window.set_application(app)
self.window.present() self.window.present()
def _show_usb_selection(self):
usb_devices = USBDevices()
devices = usb_devices.enumerate()
dialog = USBSelectionDialog(
self.window,
devices,
self.usb_devices,
)
def on_response(dialog, response):
if response == Gtk.ResponseType.OK:
self.usb_devices = dialog.get_selected_devices()
print("Selected USB devices:", self.usb_devices)
dialog.destroy()
dialog.connect("response", on_response)
dialog.present()
class USBSelectionDialog(Gtk.Dialog):
def __init__(self, parent, devices, selected_devices):
super().__init__(
transient_for=parent,
modal=True,
title="",
)
self.set_decorated(False)
self.add_css_class("usb-dialog")
self.devices = devices
self.selected_devices = selected_devices
self.set_default_size(450, 350)
self.content = self.get_content_area()
self.content.add_css_class("usb-dialog-content")
info_label = Gtk.Label(
label="Select the USB devices you wish to make available "
"to Windows RDP sessions."
)
info_label.set_wrap(True)
info_label.set_xalign(0.0)
info_label.add_css_class("usb-dialog-description")
self.content.append(info_label)
self.list_box = Gtk.ListBox()
self.list_box.set_vexpand(True)
self.list_box.set_selection_mode(Gtk.SelectionMode.NONE)
self.list_box.add_css_class("usb-device-list")
self.content.append(self.list_box)
self._populate_devices()
## Button Box
button_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=12,
)
button_box.set_halign(Gtk.Align.CENTER)
button_box.add_css_class("usb-dialog-buttons")
cancel_button = Gtk.Button(label="Cancel")
cancel_button.add_css_class("usb-dialog-cancel")
cancel_button.connect(
"clicked",
lambda _: self.response(Gtk.ResponseType.CANCEL),
)
select_button = Gtk.Button(label="Select")
select_button.add_css_class("usb-dialog-select")
select_button.connect(
"clicked",
lambda _: self.response(Gtk.ResponseType.OK),
)
button_box.append(cancel_button)
button_box.append(select_button)
self.content.append(button_box)
##
def _populate_devices(self):
for device in self.devices:
check_button = Gtk.CheckButton(label=device.name)
check_button.add_css_class("usb-device-checkbox")
check_button.usb_device = device
if (device.vendor_id, device.product_id) in self.selected_devices:
check_button.set_active(True)
self.list_box.append(check_button)
row = check_button.get_parent()
row.add_css_class("usb-device-row")
def get_selected_devices(self):
selected_devices = []
row = self.list_box.get_first_child()
while row:
check_button = row.get_child()
if check_button.get_active():
device = check_button.usb_device
selected_devices.append(
(device.vendor_id, device.product_id)
)
row = row.get_next_sibling()
return selected_devices
# Allow direct execution for isolated UI testing # Allow direct execution for isolated UI testing
if __name__ == "__main__": if __name__ == "__main__":

5
launcher/updater.py Normal file
View File

@@ -0,0 +1,5 @@
# updater.py
"""
Updates the application during runtime, ensuring the latest version will be available after the next restart.
It does not replace the currently running instance with the new version.
"""

61
launcher/usbdev.py Normal file
View File

@@ -0,0 +1,61 @@
from dataclasses import dataclass
import pyudev
@dataclass
class USBDevice:
vendor_id: str | None
product_id: str | None
vendor: str | None
product: str | None
serial: str | None
@property
def id(self) -> str | None:
if self.vendor_id is None or self.product_id is None:
return None
return f"{self.vendor_id}:{self.product_id}"
@property
def name(self) -> str:
return f"{self.vendor} {self.product}"
class USBDevices:
def __init__(self):
self.context = pyudev.Context()
def enumerate(self) -> list[USBDevice]:
devices = []
for device in self.context.list_devices(
subsystem="usb",
DEVTYPE="usb_device"
):
usb_device = USBDevice(
vendor_id=device.get("ID_VENDOR_ID"),
product_id=device.get("ID_MODEL_ID"),
vendor=device.get("ID_VENDOR"),
product=device.get("ID_MODEL"),
serial=device.get("ID_SERIAL_SHORT"),
)
if self._is_valid_device(usb_device):
devices.append(usb_device)
return devices
@staticmethod
def _is_valid_device(device: USBDevice) -> bool:
# Exclude Linux USB root hubs
if device.vendor_id == "1d6b":
return False
return True
if __name__ == "__main__":
devices = USBDevices()
enumerated = devices.enumerate()
#print(devices.enumerate())
for device in enumerated:
print(device.id, device.name)

View File

@@ -164,6 +164,16 @@
</object> </object>
</child> </child>
<!-- Power Menu Button with Popover --> <!-- Power Menu Button with Popover -->
<child>
<object class="GtkButton" id="usb_menu_button">
<property name="icon-name">drive-removable-media-symbolic</property>
<property name="tooltip-text">USB Devices</property>
<style>
<class name="flat"/>
<class name="status-bar-icon"/>
</style>
</object>
</child>
<child> <child>
<object class="GtkMenuButton" id="power_menu_button"> <object class="GtkMenuButton" id="power_menu_button">
<property name="icon-name">system-shutdown-symbolic</property> <property name="icon-name">system-shutdown-symbolic</property>