193 lines
6.1 KiB
QML
193 lines
6.1 KiB
QML
pragma Singleton
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Current conditions from Open-Meteo (no API key, no account).
|
|
//
|
|
// Fetched with curl rather than XMLHttpRequest: curl is guaranteed present and
|
|
// the response is a single small JSON blob, so there is nothing to stream.
|
|
//
|
|
// Failure is silent by design — a weather widget that can't reach the network
|
|
// must not produce error popups or retry storms. A failed fetch just leaves
|
|
// `available` false until the next scheduled refresh.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import qs.config
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// False until a fetch has succeeded, and again after one fails.
|
|
property bool available: false
|
|
|
|
property real temperature: 0
|
|
property int weatherCode: -1
|
|
|
|
// Nerd Font glyph and a short human label for `weatherCode`.
|
|
readonly property string icon: root._codeIcon(root.weatherCode)
|
|
readonly property string description: root._codeDescription(root.weatherCode)
|
|
|
|
readonly property string unitSuffix: Settings.temperatureUnit === "fahrenheit" ? "°F" : "°C"
|
|
|
|
readonly property string url: "https://api.open-meteo.com/v1/forecast" + "?latitude=" + Settings.latitude + "&longitude=" + Settings.longitude + "¤t=temperature_2m,weather_code" + "&temperature_unit=" + Settings.temperatureUnit
|
|
|
|
// The timer is the *only* thing that starts a fetch, so a hard failure can
|
|
// never retry faster than the refresh interval.
|
|
Timer {
|
|
interval: Settings.weatherRefreshMinutes * 60 * 1000
|
|
running: true
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
Process {
|
|
id: fetch
|
|
// -s silences the progress meter, --max-time keeps a black-holed
|
|
// connection from leaving the process alive until the next refresh.
|
|
command: ["curl", "-s", "--max-time", "10", root.url]
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root._parse(this.text)
|
|
}
|
|
|
|
onExited: (exitCode, exitStatus) => {
|
|
if (exitCode !== 0)
|
|
root.available = false;
|
|
}
|
|
}
|
|
|
|
function refresh(): void {
|
|
// Skip if the previous fetch is somehow still in flight.
|
|
if (!fetch.running)
|
|
fetch.running = true;
|
|
}
|
|
|
|
function _parse(text: string): void {
|
|
if (!text) {
|
|
root.available = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const current = JSON.parse(text).current;
|
|
if (!current || current.temperature_2m === undefined) {
|
|
root.available = false;
|
|
return;
|
|
}
|
|
|
|
root.temperature = current.temperature_2m;
|
|
root.weatherCode = current.weather_code;
|
|
root.available = true;
|
|
} catch (e) {
|
|
root.available = false;
|
|
}
|
|
}
|
|
|
|
// ── WMO 4677 weather codes ──────────────────────────────────────────────
|
|
// Open-Meteo's `weather_code` is the WMO set. Grouped here into the same
|
|
// buckets GNOME Weather uses, because the distinctions finer than this are
|
|
// not legible at bar size.
|
|
// Glyphs come from the nf-weather range. They are written as escapes rather
|
|
// than literal characters so tooling that mishandles private-use codepoints
|
|
// cannot silently eat them.
|
|
function _codeIcon(code: int): string {
|
|
switch (code) {
|
|
case 0:
|
|
return "\u{E30D}"; // weather-day_sunny
|
|
case 1:
|
|
return "\u{E30C}"; // weather-day_sunny_overcast
|
|
case 2:
|
|
return "\u{E302}"; // weather-day_cloudy
|
|
case 3:
|
|
return "\u{E312}"; // weather-cloudy
|
|
case 45:
|
|
case 48:
|
|
return "\u{E313}"; // weather-fog
|
|
case 51:
|
|
case 53:
|
|
case 55:
|
|
return "\u{E31B}"; // weather-sprinkle
|
|
case 56:
|
|
case 57:
|
|
case 66:
|
|
case 67:
|
|
return "\u{E316}"; // weather-rain_mix (freezing)
|
|
case 61:
|
|
case 63:
|
|
case 65:
|
|
return "\u{E318}"; // weather-rain
|
|
case 71:
|
|
case 73:
|
|
case 75:
|
|
case 77:
|
|
case 85:
|
|
case 86:
|
|
return "\u{E31A}"; // weather-snow
|
|
case 80:
|
|
case 81:
|
|
case 82:
|
|
return "\u{E319}"; // weather-showers
|
|
case 95:
|
|
case 96:
|
|
case 99:
|
|
return "\u{E31D}"; // weather-thunderstorm
|
|
default:
|
|
return "\u{E374}"; // weather-na — unknown, or nothing fetched yet
|
|
}
|
|
}
|
|
|
|
function _codeDescription(code: int): string {
|
|
switch (code) {
|
|
case 0:
|
|
return "Clear";
|
|
case 1:
|
|
return "Mainly clear";
|
|
case 2:
|
|
return "Partly cloudy";
|
|
case 3:
|
|
return "Overcast";
|
|
case 45:
|
|
return "Fog";
|
|
case 48:
|
|
return "Rime fog";
|
|
case 51:
|
|
case 53:
|
|
case 55:
|
|
return "Drizzle";
|
|
case 56:
|
|
case 57:
|
|
return "Freezing drizzle";
|
|
case 61:
|
|
case 63:
|
|
case 65:
|
|
return "Rain";
|
|
case 66:
|
|
case 67:
|
|
return "Freezing rain";
|
|
case 71:
|
|
case 73:
|
|
case 75:
|
|
return "Snow";
|
|
case 77:
|
|
return "Snow grains";
|
|
case 80:
|
|
case 81:
|
|
case 82:
|
|
return "Showers";
|
|
case 85:
|
|
case 86:
|
|
return "Snow showers";
|
|
case 95:
|
|
return "Thunderstorm";
|
|
case 96:
|
|
case 99:
|
|
return "Thunderstorm, hail";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
}
|