From 3bd3288e90bf84d18f90f7709f98d0d452a6ae38 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 17 Jul 2026 11:02:36 +0200 Subject: feat: PyQt6 main window and entry point Co-Authored-By: Claude Opus 4.8 --- hyprsunset_qt/ui.py | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 hyprsunset_qt/ui.py (limited to 'hyprsunset_qt/ui.py') diff --git a/hyprsunset_qt/ui.py b/hyprsunset_qt/ui.py new file mode 100644 index 0000000..5281f94 --- /dev/null +++ b/hyprsunset_qt/ui.py @@ -0,0 +1,238 @@ +"""PyQt6 GUI for hyprsunset-qt.""" + +from __future__ import annotations + +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QPushButton, + QGroupBox, QScrollArea, +) + +from . import config as cfg +from . import daemon +from . import sun +from .settings import Settings + + +class ProfileRow(QWidget): + def __init__(self, profile: cfg.Profile): + super().__init__() + lay = QHBoxLayout(self) + lay.setContentsMargins(0, 0, 0, 0) + + self.time = QLineEdit(profile.time) + self.time.setFixedWidth(70) + + self.identity = QCheckBox("identity") + self.identity.setChecked(profile.identity) + + self.temp_on = QCheckBox("temp") + self.temp_on.setChecked(profile.temperature is not None) + self.temp = QSpinBox() + self.temp.setRange(1000, 20000) + self.temp.setValue(profile.temperature or 5500) + + self.gamma_on = QCheckBox("gamma") + self.gamma_on.setChecked(profile.gamma is not None) + self.gamma = QDoubleSpinBox() + self.gamma.setRange(0.0, 2.0) + self.gamma.setSingleStep(0.05) + self.gamma.setValue(profile.gamma if profile.gamma is not None else 1.0) + + self.remove = QPushButton("✕") + self.remove.setFixedWidth(28) + + for w in (QLabel("time"), self.time, self.identity, + self.temp_on, self.temp, self.gamma_on, self.gamma, + self.remove): + lay.addWidget(w) + lay.addStretch() + + self.temp_on.toggled.connect(self.temp.setEnabled) + self.gamma_on.toggled.connect(self.gamma.setEnabled) + self.temp.setEnabled(self.temp_on.isChecked()) + self.gamma.setEnabled(self.gamma_on.isChecked()) + + def to_profile(self) -> cfg.Profile: + return cfg.Profile( + time=self.time.text().strip(), + identity=self.identity.isChecked(), + temperature=self.temp.value() if self.temp_on.isChecked() else None, + gamma=self.gamma.value() if self.gamma_on.isChecked() else None, + ) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("hyprsunset-qt") + self.settings = Settings.load() + self.rows: list[ProfileRow] = [] + + central = QWidget() + self.setCentralWidget(central) + root = QVBoxLayout(central) + + # Daemon status bar + bar = QHBoxLayout() + self.status = QLabel() + restart_btn = QPushButton("Restart") + restart_btn.clicked.connect(self.on_restart) + bar.addWidget(self.status) + bar.addStretch() + bar.addWidget(restart_btn) + root.addLayout(bar) + + # Location group + loc = QGroupBox("Location") + loc_lay = QVBoxLayout(loc) + coords = QHBoxLayout() + self.lat = QLineEdit(self.settings.lat) + self.lon = QLineEdit(self.settings.lon) + self.auto = QCheckBox("auto") + self.auto.setChecked(self.settings.auto_detect) + detect = QPushButton("Detect") + detect.clicked.connect(self.on_detect) + for w in (QLabel("Lat"), self.lat, QLabel("Lon"), self.lon, + self.auto, detect): + coords.addWidget(w) + loc_lay.addLayout(coords) + fetch_row = QHBoxLayout() + fetch_btn = QPushButton("Fetch sun times") + fetch_btn.clicked.connect(self.on_fetch) + self.sun_label = QLabel("sunrise — sunset —") + fetch_row.addWidget(fetch_btn) + fetch_row.addWidget(self.sun_label) + fetch_row.addStretch() + loc_lay.addLayout(fetch_row) + root.addWidget(loc) + + # Profiles group + prof_box = QGroupBox("Profiles") + prof_outer = QVBoxLayout(prof_box) + add_row = QHBoxLayout() + add_btn = QPushButton("+ Add") + add_btn.clicked.connect(lambda: self.add_row(cfg.Profile(time="0:00"))) + add_row.addStretch() + add_row.addWidget(add_btn) + prof_outer.addLayout(add_row) + self.rows_lay = QVBoxLayout() + prof_outer.addLayout(self.rows_lay) + root.addWidget(prof_box) + + # Bottom actions + actions = QHBoxLayout() + preview_btn = QPushButton("Live preview") + preview_btn.clicked.connect(self.on_preview) + save_btn = QPushButton("Save + Restart") + save_btn.clicked.connect(self.on_save) + actions.addWidget(preview_btn) + actions.addStretch() + actions.addWidget(save_btn) + root.addLayout(actions) + + for p in cfg.read_config(): + self.add_row(p) + self.refresh_status() + self.load_cached_sun() + + # --- helpers --- + def add_row(self, profile: cfg.Profile) -> None: + row = ProfileRow(profile) + row.remove.clicked.connect(lambda: self.remove_row(row)) + self.rows.append(row) + self.rows_lay.addWidget(row) + + def remove_row(self, row: ProfileRow) -> None: + self.rows.remove(row) + row.setParent(None) + + def profiles(self) -> list[cfg.Profile]: + return [r.to_profile() for r in self.rows] + + def refresh_status(self) -> None: + running = daemon.is_running() + self.status.setText( + "Daemon: ● running" if running else "Daemon: ○ stopped" + ) + + def save_settings(self) -> None: + self.settings.lat = self.lat.text().strip() + self.settings.lon = self.lon.text().strip() + self.settings.auto_detect = self.auto.isChecked() + self.settings.save() + + # --- actions --- + def on_restart(self) -> None: + daemon.restart(self.settings.daemon_command) + self.refresh_status() + + def on_detect(self) -> None: + try: + lat, lon = sun.geolocate() + self.lat.setText(lat) + self.lon.setText(lon) + except Exception as e: # noqa: BLE001 + self.sun_label.setText(f"detect failed: {e}") + + def on_fetch(self) -> None: + self.save_settings() + lat, lon = self.lat.text().strip(), self.lon.text().strip() + try: + data = sun.fetch_sun(lat, lon) + except Exception as e: # noqa: BLE001 + self.sun_label.setText(f"fetch failed: {e}") + return + data["lat"], data["lon"] = lat, lon + sun.write_cache(data, self.settings.cache_path_expanded()) + self.apply_sun(data) + + def load_cached_sun(self) -> None: + data = sun.read_cache(self.settings.cache_path_expanded()) + if data: + self.apply_sun(data) + + def apply_sun(self, data: dict) -> None: + r = data.get("results", {}) + sunrise = sun.to_local_hm(r["sunrise"]) if r.get("sunrise") else "—" + sunset = sun.to_local_hm(r["sunset"]) if r.get("sunset") else "—" + self.sun_label.setText(f"sunrise {sunrise} sunset {sunset}") + profiles = self.profiles() + di = cfg.day_index(profiles) + ni = cfg.night_index(profiles) + if di is not None and sunrise != "—": + self.rows[di].time.setText(sunrise) + if ni is not None and sunset != "—": + self.rows[ni].time.setText(sunset) + + def on_preview(self) -> None: + profiles = self.profiles() + ni = cfg.night_index(profiles) + target = profiles[ni] if ni is not None else ( + profiles[0] if profiles else None + ) + if target is None: + return + daemon.live_preview( + temperature=target.temperature, + gamma=target.gamma, + identity=target.identity, + ) + + def on_save(self) -> None: + profiles = self.profiles() + for p in profiles: + if not cfg.valid_time(p.time): + self.sun_label.setText(f"invalid time: {p.time!r}") + return + if p.temperature is not None and not cfg.valid_temperature(p.temperature): + self.sun_label.setText(f"invalid temperature: {p.temperature}") + return + if p.gamma is not None and not cfg.valid_gamma(p.gamma): + self.sun_label.setText(f"invalid gamma: {p.gamma}") + return + cfg.write_config(profiles) + self.save_settings() + daemon.restart(self.settings.daemon_command) + self.refresh_status() + self.sun_label.setText("saved + restarted") -- cgit v1.2.3