diff options
Diffstat (limited to 'appearance/Hyprsunset.qml')
| -rw-r--r-- | appearance/Hyprsunset.qml | 326 |
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; - } - } - } -} |
