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 ""; } } }