diff --git a/Makefile b/Makefile index e4add7f..35968b9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ bindir = /usr/bin +datadir = /usr/share # Set this to the desired Qt-Version (5 or 6). # Zero means all versions shall be supported. @@ -65,12 +66,18 @@ install: install -Dm 755 nbfc-qt.py $(DESTDIR)$(bindir)/nbfc-qt install -Dm 755 nbfc-qt-tray.py $(DESTDIR)$(bindir)/nbfc-qt-tray #install -Dm 755 nbfc-qt-config.py $(DESTDIR)$(bindir)/nbfc-qt-config + install -Dm 644 share/applications/nbfc-qt.desktop $(DESTDIR)$(datadir)/applications/nbfc-qt.desktop + install -Dm 644 share/applications/nbfc-qt-tray.desktop $(DESTDIR)$(datadir)/applications/nbfc-qt-tray.desktop + install -Dm 644 share/icons/hicolor/256x256/apps/nbfc-qt.png $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps/nbfc-qt.png uninstall: rm -f $(DESTDIR)$(bindir)/nbfc-qt rm -f $(DESTDIR)$(bindir)/nbfc-qt-tray rm -f $(DESTDIR)$(bindir)/nbfc-qt-config - + rm -f $(DESTDIR)$(datadir)/applications/nbfc-qt.desktop + rm -f $(DESTDIR)$(datadir)/applications/nbfc-qt-tray.desktop + rm -f $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps/nbfc-qt.png + clean: rm -rf __pycache__ rm -f nbfc-qt.py nbfc-qt-tray.py nbfc-qt-config.py diff --git a/share/applications/nbfc-qt-tray.desktop b/share/applications/nbfc-qt-tray.desktop new file mode 100644 index 0000000..d35ca5c --- /dev/null +++ b/share/applications/nbfc-qt-tray.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=NBFC Qt Tray +Comment=NBFC fan control tray icon +Exec=nbfc-qt-tray +Icon=nbfc-qt +Terminal=false +Type=Application +Categories=System;Hardware;Settings; +StartupNotify=false +NoDisplay=true diff --git a/share/applications/nbfc-qt.desktop b/share/applications/nbfc-qt.desktop new file mode 100644 index 0000000..5449ad2 --- /dev/null +++ b/share/applications/nbfc-qt.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=NBFC Qt +Comment=Qt-based GUI for NBFC-Linux fan control +Exec=nbfc-qt +Icon=nbfc-qt +Terminal=false +Type=Application +Categories=System;Hardware;Settings; +StartupNotify=false diff --git a/share/icons/hicolor/256x256/apps/nbfc-qt.png b/share/icons/hicolor/256x256/apps/nbfc-qt.png new file mode 100644 index 0000000..366f6dd Binary files /dev/null and b/share/icons/hicolor/256x256/apps/nbfc-qt.png differ diff --git a/src/client/main.py b/src/client/main.py index 7a1e13a..6654d4a 100644 --- a/src/client/main.py +++ b/src/client/main.py @@ -79,40 +79,40 @@ def make_qt5_compatible(): if opts.qt_version is None: try: from PyQt6.QtWidgets import * - from PyQt6.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal - from PyQt6.QtGui import QAction, QPixmap + from PyQt6.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal, QSettings + from PyQt6.QtGui import QAction, QIcon, QPixmap make_qt5_compatible() except ImportError: try: from PyQt5.QtWidgets import * - from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal - from PyQt5.QtGui import QPixmap + from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal, QSettings + from PyQt5.QtGui import QIcon, QPixmap except ImportError: print("Please install Python Qt bindings (PyQt5 or PyQt6)") sys.exit(1) elif opts.qt_version == 5: from PyQt5.QtWidgets import * - from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal - from PyQt5.QtGui import QPixmap + from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal, QSettings + from PyQt5.QtGui import QIcon, QPixmap elif opts.qt_version == 6: from PyQt6.QtWidgets import * - from PyQt6.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal - from PyQt6.QtGui import QAction, QPixmap + from PyQt6.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal, QSettings + from PyQt6.QtGui import QAction, QIcon, QPixmap make_qt5_compatible() #endif #ifeq QT_VERSION 5 from PyQt5.QtWidgets import * -from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal -from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal, QSettings +from PyQt5.QtGui import QIcon, QPixmap #endif #ifeq QT_VERSION 6 from PyQt6.QtWidgets import * -from PyQt6.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal -from PyQt6.QtGui import QAction, QPixmap +from PyQt6.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal, QSettings +from PyQt6.QtGui import QAction, QIcon, QPixmap make_qt5_compatible() #endif diff --git a/src/client/widgets/fan_widget.py b/src/client/widgets/fan_widget.py index e33bf2f..23e4b1b 100644 --- a/src/client/widgets/fan_widget.py +++ b/src/client/widgets/fan_widget.py @@ -1,3 +1,28 @@ +class JumpSlider(QSlider): + """QSlider that jumps to the click position instead of page step.""" + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + if self.orientation() == Qt.Horizontal: + pos = event.pos().x() + else: + pos = event.pos().y() + + # Click on track -> jump to position + val = QStyle.sliderValueFromPosition( + self.minimum(), self.maximum(), pos, + self.width() if self.orientation() == Qt.Horizontal else self.height() + ) + cur = self.value() + diff = abs(val - cur) + if diff > 1: + self.setValue(val) + + super().mousePressEvent(event) + # Always emit on press so update_fan_speed disables auto mode + self.valueChanged.emit(self.value()) + + class FanWidget(QWidget): def __init__(self): super().__init__() @@ -68,7 +93,7 @@ def __init__(self): # Slider # ===================================================================== - self.speed_slider = QSlider(Qt.Horizontal) + self.speed_slider = JumpSlider(Qt.Horizontal) self.speed_slider.setMinimum(0) self.speed_slider.setMaximum(100) self.speed_slider.setTickInterval(1) @@ -76,23 +101,36 @@ def __init__(self): layout.addWidget(self.speed_slider) def update_fan_speed(self, *_): + # Disable auto mode when user moves the slider + if self.sender() == self.speed_slider: + self.auto_mode_checkbox.setChecked(False) + auto_mode = self.auto_mode_checkbox.isChecked() if auto_mode: - GLOBALS.nbfc_client.set_fan_speed('auto', self.fan_index) + GLOBALS.nbfc_client.set_fan_speed("auto", self.fan_index) + op = QGraphicsOpacityEffect() + op.setOpacity(0.4) + self.speed_slider.setGraphicsEffect(op) + self.auto_mode_checkbox.setGraphicsEffect(None) else: GLOBALS.nbfc_client.set_fan_speed(self.speed_slider.value(), self.fan_index) - - self.speed_slider.setEnabled(not auto_mode) + self.speed_slider.setGraphicsEffect(None) + op = QGraphicsOpacityEffect() + op.setOpacity(0.4) + self.auto_mode_checkbox.setGraphicsEffect(op) def update(self, fan_index, fan_data): self.fan_index = fan_index - self.name_label.setText(fan_data['Name']) - self.temperature_label.setText(f'{fan_data['Temperature']:.2f}') - self.auto_mode_label.setText(str(fan_data['AutoMode'])) - self.critical_label.setText(str(fan_data['Critical'])) - self.current_speed_label.setText(f'{fan_data['CurrentSpeed']:.2f}') - self.target_speed_label.setText(f'{fan_data['TargetSpeed']:.2f}') - self.speed_steps_label.setText(str(fan_data['SpeedSteps'])) - self.auto_mode_checkbox.setChecked(fan_data['AutoMode']) - self.speed_slider.setValue(int(fan_data['RequestedSpeed'])) + self.name_label.setText(fan_data["Name"]) + self.temperature_label.setText(f"{fan_data['Temperature']:.2f}") + self.auto_mode_label.setText(str(fan_data["AutoMode"])) + self.critical_label.setText(str(fan_data["Critical"])) + self.current_speed_label.setText(f"{fan_data['CurrentSpeed']:.2f}") + self.target_speed_label.setText(f"{fan_data['TargetSpeed']:.2f}") + self.speed_steps_label.setText(str(fan_data["SpeedSteps"])) + self.auto_mode_checkbox.setChecked(fan_data["AutoMode"]) + # Block signals so setValue doesn't trigger valueChanged -> update_fan_speed + self.speed_slider.blockSignals(True) + self.speed_slider.setValue(int(fan_data["RequestedSpeed"])) + self.speed_slider.blockSignals(False) diff --git a/src/client/widgets/main_window.py b/src/client/widgets/main_window.py index 5e44748..17be967 100644 --- a/src/client/widgets/main_window.py +++ b/src/client/widgets/main_window.py @@ -1,5 +1,6 @@ import subprocess + class ImageLoaderWorker(QObject): finished = pyqtSignal(bytes) @@ -18,6 +19,7 @@ def run(self): except Exception: pass + class SponsorWidget(QLabel): def __init__(self, parent): super().__init__(parent) @@ -27,16 +29,16 @@ def __init__(self, parent): self.setAlignment(Qt.AlignCenter) try: - sponsor = GLOBALS.nbfc_client.get_model_configuration()['Sponsor'] - self.url = sponsor['URL'] + sponsor = GLOBALS.nbfc_client.get_model_configuration()["Sponsor"] + self.url = sponsor["URL"] - if 'Description' in sponsor: + if "Description" in sponsor: self.setToolTip(f"{sponsor['Name']} - {sponsor['Description']}") else: - self.setToolTip(sponsor['Name']) + self.setToolTip(sponsor["Name"]) self.thread = QThread() - self.worker = ImageLoaderWorker(sponsor['BannerURL']) + self.worker = ImageLoaderWorker(sponsor["BannerURL"]) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.on_image_loaded) @@ -44,7 +46,7 @@ def __init__(self, parent): self.thread.start() except Exception: pass - + def on_image_loaded(self, content): pixmap = QPixmap() pixmap.loadFromData(content) @@ -54,7 +56,8 @@ def on_image_loaded(self, content): def mousePressEvent(self, event): if self.url: - subprocess.run(['xdg-open', self.url]) + subprocess.run(["xdg-open", self.url]) + class MainWindow(QMainWindow): def __init__(self): @@ -66,6 +69,15 @@ def __init__(self): self.setWindowTitle("NBFC Client") self.resize(400, 400) + self.setWindowIcon(QIcon.fromTheme("nbfc-qt")) + + # Restore window geometry from previous session + settings = QSettings("nbfc-qt", "nbfc-qt") + geom = settings.value("window/geometry") + if geom is not None: + self.restoreGeometry(geom) + else: + self.resize(400, 400) # ===================================================================== # Container widget @@ -94,22 +106,28 @@ def __init__(self): # ===================================================================== self.widgets = {} - self.widgets['service'] = ServiceControlWidget() - self.widgets['fans'] = FanControlWidget() - self.widgets['basic'] = BasicConfigWidget() - self.widgets['sensors'] = TemperatureSourcesWidget() - self.widgets['update'] = UpdateWidget() - self.widgets['rated'] = RateConfigsWidget() - - self.tab_widget.addTab(self.widgets['service'], "Service") - self.tab_widget.addTab(self.widgets['fans'], "Fans") - self.tab_widget.addTab(self.widgets['basic'], "Basic Configuration") - self.tab_widget.addTab(self.widgets['rated'], "Rated Configs") - self.tab_widget.addTab(self.widgets['sensors'], "Sensors") - self.tab_widget.addTab(self.widgets['update'], "Update") + self.widgets["service"] = ServiceControlWidget() + self.widgets["fans"] = FanControlWidget() + self.widgets["basic"] = BasicConfigWidget() + self.widgets["sensors"] = TemperatureSourcesWidget() + self.widgets["update"] = UpdateWidget() + self.widgets["rated"] = RateConfigsWidget() + + self.tab_widget.addTab(self.widgets["service"], "Service") + self.tab_widget.addTab(self.widgets["fans"], "Fans") + self.tab_widget.addTab(self.widgets["basic"], "Basic Configuration") + self.tab_widget.addTab(self.widgets["rated"], "Rated Configs") + self.tab_widget.addTab(self.widgets["sensors"], "Sensors") + self.tab_widget.addTab(self.widgets["update"], "Update") + + settings = QSettings("nbfc-qt", "nbfc-qt") + initial_tab = int(settings.value("window/active_tab", 0)) + if initial_tab >= self.tab_widget.count(): + initial_tab = 0 self.tab_widget.currentChanged.connect(self.tab_widget_changed) - self.tab_widget_changed(0) + self.tab_widget.setCurrentIndex(initial_tab) + self.tab_widget_changed(initial_tab) # ===================================================================== # Set widget @@ -133,6 +151,15 @@ def __init__(self): quitAction.triggered.connect(lambda: QApplication.quit()) applicationMenu.addAction(quitAction) + # ========================================================================= + # Events + # ========================================================================= + + def closeEvent(self, event): + settings = QSettings("nbfc-qt", "nbfc-qt") + settings.setValue("window/geometry", self.saveGeometry()) + super().closeEvent(event) + # ========================================================================= # Public functions # ========================================================================= @@ -154,3 +181,7 @@ def tab_widget_changed(self, current_index): widget.start() else: widget.stop() + + # Remember last active tab + settings = QSettings("nbfc-qt", "nbfc-qt") + settings.setValue("window/active_tab", current_index) diff --git a/src/client/widgets/temperature_sources_widget.py b/src/client/widgets/temperature_sources_widget.py index ed2fb61..05f4540 100644 --- a/src/client/widgets/temperature_sources_widget.py +++ b/src/client/widgets/temperature_sources_widget.py @@ -2,7 +2,9 @@ class TemperatureSourcesWidget(QStackedWidget): def __init__(self): super().__init__() - GLOBALS.model_config_changed.connect(self.setup_ui) + self._has_setup = False + + GLOBALS.model_config_changed.connect(self._on_model_config_changed) # ===================================================================== # Error Widget @@ -42,21 +44,39 @@ def __init__(self): self.apply_buttons_widget = ApplyButtonsWidget() self.apply_buttons_widget.save_button.clicked.connect(self.save_button_clicked) - self.apply_buttons_widget.apply_button.clicked.connect(self.apply_button_clicked) + self.apply_buttons_widget.apply_button.clicked.connect( + self.apply_button_clicked + ) main_layout.addWidget(self.apply_buttons_widget) self.addWidget(self.main_widget) - self.setup_ui() + # Auto-refresh timer (retries every 5s when on error widget) + self._refresh_timer = QTimer(self) + self._refresh_timer.setInterval(5000) + self._refresh_timer.timeout.connect(self._auto_refresh) # ========================================================================= # Widget start / stop # ========================================================================= def start(self): - pass + if not self._has_setup: + self._has_setup = True + self.setup_ui() + self._refresh_timer.start() def stop(self): - pass + self._refresh_timer.stop() + + def _auto_refresh(self): + """Silently retry when on error widget.""" + if self.currentWidget() != self.error_widget: + return + self.setup_ui() + + def _on_model_config_changed(self): + self._has_setup = False + self.setup_ui() # ========================================================================= # Helper functions @@ -64,9 +84,9 @@ def stop(self): def save(self): config = GLOBALS.nbfc_client.get_service_config() - config['FanTemperatureSources'] = self.get_fan_temperature_sources() - if not len(config['FanTemperatureSources']): - del config['FanTemperatureSources'] + config["FanTemperatureSources"] = self.get_fan_temperature_sources() + if not len(config["FanTemperatureSources"]): + del config["FanTemperatureSources"] GLOBALS.nbfc_client.set_service_config(config) def setup_ui(self, fix_errors=False): @@ -81,42 +101,47 @@ def setup_ui(self, fix_errors=False): try: config = GLOBALS.nbfc_client.get_service_config() - fan_temperature_sources = config.get('FanTemperatureSources', []) + fan_temperature_sources = config.get("FanTemperatureSources", []) model_config = GLOBALS.nbfc_client.get_model_configuration() + # Get available temperature sensors + available_sensors = GLOBALS.nbfc_client.get_available_sensors() except Exception as e: self.setCurrentWidget(self.error_widget) - self.error_label.setText(str(e)) + msg = str(e) + if "timed out" in msg.lower(): + msg = "The NBFC service is not responding. Start it in the Service tab first." + elif "Could not find" in msg: + msg = "The `nbfc` CLI program is not installed or not in PATH." + elif "Is the service running" in msg: + msg = "The NBFC service is not running. Start it in the Service tab first." + elif "No temperature sources" in msg: + msg = "No temperature sensors detected on your system." + self.error_label.setText(msg) self.fix_button.setEnabled(False) self.retry_button.setEnabled(True) self.apply_buttons_widget.disable("") return - # ===================================================================== - # Get available temperature sensors - # ===================================================================== - - available_sensors = GLOBALS.nbfc_client.get_available_sensors() - # ===================================================================== # Ensure that the FanTemperatureSources in the config are valid. # Give the user the chance to fix it or fix it automatically. # ===================================================================== errors = validate_fan_temperature_sources( - fan_temperature_sources, - len(model_config['FanConfigurations'])) + fan_temperature_sources, len(model_config["FanConfigurations"]) + ) if errors and not fix_errors: self.setCurrentWidget(self.error_widget) - self.error_label.setText('\n\n'.join(errors)) + self.error_label.setText("\n\n".join(errors)) self.fix_button.setEnabled(True) self.retry_button.setEnabled(True) self.apply_buttons_widget.disable("") return elif errors and fix_errors: fan_temperature_sources = fix_fan_temperature_sources( - fan_temperature_sources, - len(model_config['FanConfigurations'])) + fan_temperature_sources, len(model_config["FanConfigurations"]) + ) self.setCurrentWidget(self.main_widget) @@ -124,11 +149,11 @@ def setup_ui(self, fix_errors=False): # Add widgets to self.tab_widget # ===================================================================== - while self.tab_widget.count() < len(model_config['FanConfigurations']): + while self.tab_widget.count() < len(model_config["FanConfigurations"]): widget = TemperatureSourceWidget() self.tab_widget.addTab(widget, "") - while self.tab_widget.count() > len(model_config['FanConfigurations']): + while self.tab_widget.count() > len(model_config["FanConfigurations"]): last_index = self.tab_widget.count() - 1 widget = self.tab_widget.widget(last_index) self.tab_widget.removeTab(last_index) @@ -138,18 +163,20 @@ def setup_ui(self, fix_errors=False): # Set fan names to tabs # ===================================================================== - for i, fan_config in enumerate(model_config['FanConfigurations']): + for i, fan_config in enumerate(model_config["FanConfigurations"]): widget = self.tab_widget.widget(i) - self.tab_widget.setTabText(i, fan_config.get('FanDisplayName', 'Fan #%d' % i)) + self.tab_widget.setTabText( + i, fan_config.get("FanDisplayName", "Fan #%d" % i) + ) widget.set_available_sensors(available_sensors) widget.set_fan_index(i) # ===================================================================== - # Update TemperatureSourceWidget + # Update TemperatureSourceWidget # ===================================================================== for fan_temperature_source in fan_temperature_sources: - fan_index = fan_temperature_source['FanIndex'] + fan_index = fan_temperature_source["FanIndex"] widget = self.tab_widget.widget(fan_index) widget.update(fan_temperature_source) @@ -179,7 +206,9 @@ def save_button_clicked(self): def apply_button_clicked(self): try: self.save() - GLOBALS.restart_service.emit(self.apply_buttons_widget.read_only_checkbox.isChecked()) + GLOBALS.restart_service.emit( + self.apply_buttons_widget.read_only_checkbox.isChecked() + ) except Exception as e: show_error_message(self, "Error", str(e)) diff --git a/src/common/nbfc_client.py b/src/common/nbfc_client.py index 417fa54..a7c3cca 100644 --- a/src/common/nbfc_client.py +++ b/src/common/nbfc_client.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 +import json import os import re -import json import socket import subprocess from collections import namedtuple -Sensor = namedtuple('Sensor', ['name', 'description']) +Sensor = namedtuple("Sensor", ["name", "description"]) + class NbfcClientError(Exception): pass + class NbfcClient: """ A client to interact with the NBFC service using Unix sockets. @@ -26,16 +28,16 @@ def __init__(self): See `call_nbfc` for further information. """ - self.socket_file = self.get_compile_time_variable('socket_file') - self.config_file = self.get_compile_time_variable('config_file') - self.model_configs_dir = self.get_compile_time_variable('model_configs_dir') - self.model_configs_dir_mutable = '/var/lib/nbfc/configs' + self.socket_file = self.get_compile_time_variable("socket_file") + self.config_file = self.get_compile_time_variable("config_file") + self.model_configs_dir = self.get_compile_time_variable("model_configs_dir") + self.model_configs_dir_mutable = "/var/lib/nbfc/configs" # ========================================================================= # Helper methods # ========================================================================= - def call_nbfc(self, args): + def call_nbfc(self, args, timeout=5): """ Calls the `nbfc` binary with the given arguments and returns the output. @@ -53,14 +55,24 @@ def call_nbfc(self, args): - If the client command returns a non-zero exit code. The exception's text is the output written to STDERR. + + - If the command times out. """ - command = ['nbfc'] + args + command = ["nbfc"] + args try: - result = subprocess.run(command, capture_output=True, text=True, check=False) + result = subprocess.run( + command, capture_output=True, text=True, check=False, timeout=timeout + ) + except subprocess.TimeoutExpired: + raise NbfcClientError( + f"Command `nbfc {' '.join(args)}` timed out after {timeout}s" + ) from None except FileNotFoundError as e: - raise NbfcClientError('Could not find the `nbfc` program. Is NBFC-Linux installed?') from e + raise NbfcClientError( + "Could not find the `nbfc` program. Is NBFC-Linux installed?" + ) from e if result.returncode != 0: raise NbfcClientError(result.stderr.rstrip()) @@ -95,16 +107,17 @@ def socket_communicate(self, data): """ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: - try: sock.connect(self.socket_file) except FileNotFoundError: - raise NbfcClientError(f'Could not find {self.socket_file}. Is the service running?') from None + raise NbfcClientError( + f"Could not find {self.socket_file}. Is the service running?" + ) from None message = "%s\nEND" % json.dumps(data) - sock.sendall(message.encode('utf-8')) + sock.sendall(message.encode("utf-8")) - response = b'' + response = b"" while True: data = sock.recv(1024) if not data: @@ -112,11 +125,11 @@ def socket_communicate(self, data): response += data - if b'\nEND' in response: + if b"\nEND" in response: break - response = response.decode('utf-8') - response = response.replace('\nEND', '') + response = response.decode("utf-8") + response = response.replace("\nEND", "") response = json.loads(response) return response @@ -140,7 +153,7 @@ def get_compile_time_variable(self, variable): NbfcClientError: See `call_nbfc` for further information. """ - return self.call_nbfc(['show-variable', variable]) + return self.call_nbfc(["show-variable", variable]) def get_version(self): """ @@ -155,12 +168,12 @@ def get_version(self): See `call_nbfc` for further information. """ - output = self.call_nbfc(['--version']) + output = self.call_nbfc(["--version"]) - match = re.search(r'\d+\.\d+\.\d+', output) + match = re.search(r"\d+\.\d+\.\d+", output) if not match: - raise NbfcClientError('Could not extract version') + raise NbfcClientError("Could not extract version") return match[0] @@ -177,10 +190,10 @@ def start(self, readonly=False): See `call_nbfc` for further information. """ - args = ['start'] + args = ["start"] if readonly: - args.append('-r') + args.append("-r") self.call_nbfc(args) @@ -197,10 +210,10 @@ def restart(self, readonly=False): See `call_nbfc` for further information. """ - args = ['restart'] + args = ["restart"] if readonly: - args.append('-r') + args.append("-r") self.call_nbfc(args) @@ -213,7 +226,7 @@ def stop(self): See `call_nbfc` for further information. """ - self.call_nbfc(['stop']) + self.call_nbfc(["stop"]) def get_model_name(self): """ @@ -228,7 +241,7 @@ def get_model_name(self): See `call_nbfc` for further information. """ - return self.call_nbfc(['get-model-name']) + return self.call_nbfc(["get-model-name"]) def list_configs(self): """ @@ -243,11 +256,11 @@ def list_configs(self): See `call_nbfc` for further information. """ - configs = self.call_nbfc(['config', '-l']) + configs = self.call_nbfc(["config", "-l"]) configs = configs.strip() if configs: - return configs.split('\n') + return configs.split("\n") else: return [] @@ -269,11 +282,11 @@ def recommended_configs(self): of the configurations beyond the string matching. """ - configs = self.call_nbfc(['config', '-r']) + configs = self.call_nbfc(["config", "-r"]) configs = configs.strip() if configs: - return configs.split('\n') + return configs.split("\n") else: return [] @@ -292,17 +305,17 @@ def get_available_sensors(self): sensors = [] - output = self.call_nbfc(['complete-sensors']) + output = self.call_nbfc(["complete-sensors"]) - for line in output.split('\n'): - parts = line.split('\t', maxsplit=1) + for line in output.split("\n"): + parts = line.split("\t", maxsplit=1) if len(parts) == 2: name, description = parts # This is needed for old version of NBFC-Linux where the # `complete-sensors` outputs a `none` sensor - if name == 'none': + if name == "none": continue sensors.append(Sensor(name, description)) @@ -321,7 +334,7 @@ def rate_configs(self): See `call_nbfc` for further information. """ - output = self.call_nbfc(['rate-config', '--all', '--json', '--min-score', '0']) + output = self.call_nbfc(["rate-config", "--all", "--json", "--min-score", "0"]) return json.loads(output) # ========================================================================= @@ -341,10 +354,10 @@ def get_status(self): If there is an error in the response from the service. """ - response = self.socket_communicate({'Command': 'status'}) + response = self.socket_communicate({"Command": "status"}) - if 'Error' in response: - raise NbfcClientError(response['Error']) + if "Error" in response: + raise NbfcClientError(response["Error"]) return response @@ -366,15 +379,15 @@ def set_fan_speed(self, speed, fan=None): If there is an error in the response from the service. """ - request = {'Command': 'set-fan-speed', 'Speed': speed} + request = {"Command": "set-fan-speed", "Speed": speed} if fan is not None: - request['Fan'] = fan + request["Fan"] = fan response = self.socket_communicate(request) - if 'Error' in response: - raise NbfcClientError(response['Error']) + if "Error" in response: + raise NbfcClientError(response["Error"]) # ========================================================================= # Methods for accessing / setting the configuration @@ -400,7 +413,7 @@ def get_service_config(self): """ try: - with open(self.config_file, 'r', encoding='UTF-8') as fh: + with open(self.config_file, "r", encoding="UTF-8") as fh: return json.load(fh) except FileNotFoundError: return {} @@ -424,7 +437,7 @@ def set_service_config(self, config): If `config` could not be serialized to JSON. """ - with open(self.config_file, 'w', encoding='UTF-8') as fh: + with open(self.config_file, "w", encoding="UTF-8") as fh: json.dump(config, fh, indent=1) def get_model_configuration_file(self): @@ -442,23 +455,29 @@ def get_model_configuration_file(self): config = self.get_service_config() - if 'SelectedConfigId' not in config: - raise NbfcClientError('Configuration has no model configuration ("SelectedConfigId") set') + if "SelectedConfigId" not in config: + raise NbfcClientError( + 'Configuration has no model configuration ("SelectedConfigId") set' + ) - config_id = config['SelectedConfigId'] + config_id = config["SelectedConfigId"] - if config_id.startswith('/'): + if config_id.startswith("/"): return config_id - model_config_path = os.path.join(self.model_configs_dir_mutable, config_id + '.json') + model_config_path = os.path.join( + self.model_configs_dir_mutable, config_id + ".json" + ) if os.path.exists(model_config_path): return model_config_path - model_config_path = os.path.join(self.model_configs_dir, config_id + '.json') + model_config_path = os.path.join(self.model_configs_dir, config_id + ".json") if os.path.exists(model_config_path): return model_config_path - raise NbfcClientError(f'No configuration file found for SelectedConfigId = "{config_id}"') + raise NbfcClientError( + f'No configuration file found for SelectedConfigId = "{config_id}"' + ) def get_model_configuration(self): """ @@ -484,5 +503,5 @@ def get_model_configuration(self): config_file = self.get_model_configuration_file() - with open(config_file, 'r', encoding='UTF-8') as fh: + with open(config_file, "r", encoding="UTF-8") as fh: return json.load(fh) diff --git a/src/tray/main.py b/src/tray/main.py index b61e773..c1d5a69 100644 --- a/src/tray/main.py +++ b/src/tray/main.py @@ -146,12 +146,15 @@ def _on_auto_toggle(self, state): class FanControlWidget(QWidget): def __init__(self): - super().__init__(flags=Qt.Popup) + super().__init__(None, flags=Qt.WindowType.Window | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint) self.layout = QHBoxLayout(self) self.layout.setContentsMargins(5, 5, 5, 5) self.layout.setSpacing(10) self.fan_widgets = [] + def focusOutEvent(self, event): + self.hide() + def load(self): # Attempt to retrieve status try: @@ -188,6 +191,8 @@ def show_at_cursor(self): y = max(screen.top(), min(y, screen.bottom() - h)) self.move(x, y) self.show() + self.activateWindow() + self.raise_() class TrayApp: