aboutsummaryrefslogtreecommitdiffstats
path: root/appearance/Hyprsunset.qml
diff options
context:
space:
mode:
Diffstat (limited to 'appearance/Hyprsunset.qml')
-rw-r--r--appearance/Hyprsunset.qml326
1 files changed, 0 insertions, 326 deletions
diff --git a/appearance/Hyprsunset.qml b/appearance/Hyprsunset.qml
deleted file mode 100644
index fc0d44a..0000000
--- a/appearance/Hyprsunset.qml
+++ /dev/null
@@ -1,326 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-pragma Singleton
-
-import Quickshell
-import Quickshell.Io
-import QtQuick
-
-// The hyprsunset side: profiles in ~/.config/hypr/hyprsunset.conf, the
-// location in hyprsunset-qt's own config, and the daemon. The file format is
-// byte-for-byte what ~/Programming/GIT/sunset-qt writes, so both tools edit
-// the same file.
-Singleton {
- id: root
-
- readonly property string home: Quickshell.env("HOME")
- readonly property string confPath: `${home}/.config/hypr/hyprsunset.conf`
- readonly property string appConfPath: `${home}/.config/hyprsunset-qt/config`
-
- property var profiles: []
- property string lat: ""
- property string lon: ""
- property bool autoDetect: true
- property string daemonCommand: "hyprsunset"
- property string cachePathRaw: "~/.config/hyprsunset-qt/sun.json"
- property string cachePath: `${home}/.config/hyprsunset-qt/sun.json`
- property string notice: ""
- property string sunSummary: ""
- property bool busy: false
- property bool confLoaded: false
- property bool appLoaded: false
-
- readonly property string header:
- "# Managed by hyprsunset-qt. Edits here are overwritten on save.\n"
-
- // QML has no String.matchAll; everything here is exec loops.
- function getField(body, key) {
- const m = body.match(new RegExp(`^\\s*${key}\\s*=\\s*(.+?)\\s*$`, "m"));
- return m ? m[1] : null;
- }
-
- function parseProfiles(text) {
- const list = [];
- const re = /profile\s*\{([\s\S]*?)\}/g;
- let m;
- while ((m = re.exec(text)) !== null) {
- const body = m[1];
- const t = root.getField(body, "temperature");
- const g = root.getField(body, "gamma");
- list.push({
- time: root.getField(body, "time") ?? "",
- identity: (root.getField(body, "identity") ?? "").toLowerCase() === "true",
- temperature: t === null ? null : parseInt(t, 10),
- gamma: g === null ? null : parseFloat(g),
- });
- }
- return list;
- }
-
- // Byte-for-byte the same construction as sunset-qt's config.serialize():
- // a list of parts joined with newlines, so a no-op save writes the file
- // back exactly as it was.
- function serializeProfiles(list) {
- const di = root.dayIndex(list);
- const ni = root.nightIndex(list);
- const parts = [root.header];
- for (let i = 0; i < list.length; i++) {
- const p = list[i];
- if (i === di) parts.push("\n# day profile -- sunrise");
- else if (i === ni) parts.push("\n# night profile -- sunset");
- else parts.push("\n# profile");
- const lines = ["profile {", ` time = ${p.time}`];
- if (p.identity) lines.push(" identity = true");
- if (p.temperature !== null && p.temperature !== undefined)
- lines.push(` temperature = ${p.temperature}`);
- if (p.gamma !== null && p.gamma !== undefined)
- lines.push(` gamma = ${p.gamma}`);
- lines.push("}");
- parts.push(lines.join("\n"));
- }
- return parts.join("\n") + "\n";
- }
-
- function dayIndex(list) {
- for (let i = 0; i < list.length; i++) if (list[i].identity) return i;
- return null;
- }
-
- function nightIndex(list) {
- for (let i = 0; i < list.length; i++)
- if (list[i].temperature !== null && list[i].temperature !== undefined) return i;
- return null;
- }
-
- function validTime(s) { return /^([01]?\d|2[0-3]):([0-5]\d)$/.test(s); }
- function validTemperature(t) { return t >= 1000 && t <= 20000; }
- function validGamma(g) { return g >= 0.0 && g <= 2.0; }
-
- function localHM(iso) {
- const d = new Date(iso);
- if (isNaN(d.getTime())) return "";
- return ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2);
- }
-
- function selftest(): string {
- // A file exactly as hyprsunset-qt writes it. String.raw keeps the
- // backslashes, none here, but keeps this fixture readable.
- // Two blank lines after the header: sunset-qt's serializer joins parts
- // with a newline and each part starts with one, so the file really has
- // them.
- const fixture = String.raw`# Managed by hyprsunset-qt. Edits here are overwritten on save.
-
-
-# day profile -- sunrise
-profile {
- time = 05:42
- identity = true
-}
-
-# night profile -- sunset
-profile {
- time = 21:02
- temperature = 5500
- gamma = 0.8
-}
-`;
- const parsed = root.parseProfiles(fixture);
- if (parsed.length !== 2) return `SELFTEST Hyprsunset FAIL: parsed ${parsed.length} profiles`;
- if (parsed[0].time !== "05:42" || parsed[0].identity !== true)
- return "SELFTEST Hyprsunset FAIL: day profile wrong";
- if (parsed[1].temperature !== 5500 || parsed[1].gamma !== 0.8)
- return "SELFTEST Hyprsunset FAIL: night profile wrong";
- if (root.dayIndex(parsed) !== 0 || root.nightIndex(parsed) !== 1)
- return "SELFTEST Hyprsunset FAIL: day/night index wrong";
- if (root.serializeProfiles(parsed) !== fixture)
- return "SELFTEST Hyprsunset FAIL: round-trip not byte-identical";
- if (!root.validTime("05:42") || root.validTime("24:00") || root.validTime("5:6"))
- return "SELFTEST Hyprsunset FAIL: validTime";
- if (!root.validTemperature(1000) || root.validTemperature(20001))
- return "SELFTEST Hyprsunset FAIL: validTemperature";
- if (!root.validGamma(0.8) || root.validGamma(2.1))
- return "SELFTEST Hyprsunset FAIL: validGamma";
- return "SELFTEST Hyprsunset PASS";
- }
-
- function refresh() {
- confProc.running = false;
- confProc.running = true;
- appProc.running = false;
- appProc.running = true;
- }
-
- Process {
- id: confProc
- command: ["cat", root.confPath]
- stdout: StdioCollector {
- onStreamFinished: {
- root.profiles = root.parseProfiles(text);
- root.confLoaded = true;
- }
- }
- }
-
- // hyprsunset-qt's own settings: location + daemon command.
- function parseAppConf(text) {
- const out = {};
- let section = "";
- for (const raw of text.split("\n")) {
- const line = raw.trim();
- if (!line || line.startsWith("#") || line.startsWith(";")) continue;
- const sec = line.match(/^\[(.+)\]$/);
- if (sec) { section = sec[1]; continue; }
- const kv = line.match(/^([^=]+)=\s*(.*)$/);
- if (kv) out[`${section}.${kv[1].trim()}`] = kv[2].trim();
- }
- return out;
- }
-
- function expandTilde(p) {
- return p.startsWith("~/") ? root.home + p.slice(1) : p;
- }
-
- Process {
- id: appProc
- command: ["cat", root.appConfPath]
- stdout: StdioCollector {
- onStreamFinished: {
- const c = root.parseAppConf(text);
- root.lat = c["location.lat"] ?? "";
- root.lon = c["location.lon"] ?? "";
- root.autoDetect = (c["location.auto_detect"] ?? "true") === "true";
- root.cachePathRaw = c["cache.path"] ?? "~/.config/hyprsunset-qt/sun.json";
- root.cachePath = root.expandTilde(root.cachePathRaw);
- root.daemonCommand = c["daemon.command"] ?? "hyprsunset";
- root.appLoaded = true;
- }
- }
- }
-
- FileView { id: confFile; path: root.confPath }
- FileView { id: appFile; path: root.appConfPath }
- FileView { id: cacheFile; path: root.cachePath }
-
- function appConfText() {
- return `[location]\nlat = ${root.lat}\nlon = ${root.lon}\n` +
- `auto_detect = ${root.autoDetect}\n\n` +
- `[cache]\npath = ${root.cachePathRaw}\n\n` +
- `[daemon]\ncommand = ${root.daemonCommand}\n\n`;
- }
- function saveSettings() { appFile.setText(root.appConfText()); }
-
- // hyprsunset holds Hyprland's CTM manager exclusively, and pkill only
- // sends SIGTERM, so the replacement must wait for the old process to
- // actually exit or it dies with "A CTM manager is already running".
- function restart() {
- Quickshell.execDetached(["sh", "-c",
- `pkill -x hyprsunset; ` +
- `for i in $(seq 50); do ` +
- `pgrep -x hyprsunset >/dev/null 2>&1 || break; sleep 0.1; done; ` +
- `setsid -f ${root.daemonCommand}`]);
- }
-
- function save() {
- if (!root.confLoaded || !root.appLoaded) return;
- for (const p of root.profiles) {
- if (!root.validTime(p.time)) { root.notice = `invalid time: ${p.time}`; return; }
- if (p.temperature !== null && p.temperature !== undefined &&
- !root.validTemperature(p.temperature)) {
- root.notice = `invalid temperature: ${p.temperature}`; return;
- }
- if (p.gamma !== null && p.gamma !== undefined && !root.validGamma(p.gamma)) {
- root.notice = `invalid gamma: ${p.gamma}`; return;
- }
- }
- confFile.setText(root.serializeProfiles(root.profiles));
- root.saveSettings();
- root.restart();
- root.notice = "saved + restarted";
- }
-
- // Live preview via the daemon's IPC. Identity wins, then temperature, then
- // gamma, matching hyprsunset-qt. Nothing is written.
- function preview() {
- const i = root.nightIndex(root.profiles);
- const target = i !== null ? root.profiles[i]
- : (root.profiles.length ? root.profiles[0] : null);
- if (!target) return;
- if (target.identity) {
- previewProc.command = ["hyprctl", "hyprsunset", "identity"];
- } else {
- const parts = [];
- if (target.temperature !== null && target.temperature !== undefined)
- parts.push(`hyprctl hyprsunset temperature ${target.temperature}`);
- if (target.gamma !== null && target.gamma !== undefined)
- parts.push(`hyprctl hyprsunset gamma ${Math.round(target.gamma * 100)}`);
- if (!parts.length) return;
- previewProc.command = ["sh", "-c", parts.join("; ")];
- }
- previewProc.running = false;
- previewProc.running = true;
- }
- Process { id: previewProc }
-
- function detect() {
- detectProc.running = false;
- detectProc.running = true;
- }
- Process {
- id: detectProc
- command: ["curl", "-fsS", "http://ip-api.com/json"]
- stdout: StdioCollector {
- onStreamFinished: {
- try {
- const d = JSON.parse(text);
- root.lat = String(d.lat);
- root.lon = String(d.lon);
- root.saveSettings();
- root.notice = `located ${root.lat}, ${root.lon}`;
- } catch (e) { root.notice = `detect failed: ${e}`; }
- }
- }
- }
-
- // sunrise-sunset.org is behind Cloudflare and 403s curl's default UA.
- function fetchSun() {
- const url = "https://api.sunrise-sunset.org/json?lat=" +
- encodeURIComponent(root.lat) + "&lng=" + encodeURIComponent(root.lon) +
- "&formatted=0";
- fetchProc.command = ["curl", "-fsS", "-A",
- "Mozilla/5.0 (X11; Linux x86_64) hyprsunset-qt", url];
- fetchProc.running = false;
- fetchProc.running = true;
- root.busy = true;
- }
- Process {
- id: fetchProc
- stdout: StdioCollector {
- onStreamFinished: {
- root.busy = false;
- let data;
- try { data = JSON.parse(text); }
- catch (e) { root.notice = `fetch failed: ${e}`; return; }
- cacheFile.setText(JSON.stringify(data, null, 2));
- const r = data.results ?? {};
- const rise = r.sunrise ? root.localHM(r.sunrise) : "";
- const set = r.sunset ? root.localHM(r.sunset) : "";
- root.sunSummary = `sunrise ${rise || "—"} sunset ${set || "—"}`;
- const di = root.dayIndex(root.profiles);
- const ni = root.nightIndex(root.profiles);
- const next = root.profiles.slice();
- if (di !== null && rise) next[di] = Object.assign({}, next[di], { time: rise });
- if (ni !== null && set) next[ni] = Object.assign({}, next[ni], { time: set });
- root.profiles = next;
- }
- }
- }
-}