diff --git a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml index 13eb71f0..50617b33 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml @@ -312,7 +312,7 @@ Item { "calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" }, "notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false }, "systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false }, - "weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false }, + "weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false, settingsForm: "weatherSettings" }, "idleInhibitor": { displayName: "Keep awake", description: "Toggle idle inhibitor", category: "System", allowMultiple: false }, "microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false }, "activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false }, diff --git a/default/quickshell/omarchy-shell/plugins/bar/widgets/weatherFlyout.qml b/default/quickshell/omarchy-shell/plugins/bar/widgets/weatherFlyout.qml index e9f17c82..e2f733cf 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/widgets/weatherFlyout.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/widgets/weatherFlyout.qml @@ -13,62 +13,139 @@ Item { property bool popupOpen: false function closePopout() { popupOpen = false } - property string fullReport: "" + // Parsed wttr.in j1 response. Kept on failure so stale data stays visible. + property var report: null readonly property string label: bar ? bar.weatherText : "" readonly property string klass: bar ? bar.weatherClass : "" - // Pipe-delimited fields from wttr.in: location|condition|temp|feels|wind|humidity - readonly property var reportFields: String(fullReport || "").split("|") - readonly property string reportLocation: reportFields.length > 0 ? String(reportFields[0] || "").trim() : "" - readonly property string reportCondition: reportFields.length > 1 ? String(reportFields[1] || "").trim() : "" - readonly property string reportTemp: reportFields.length > 2 ? String(reportFields[2] || "").trim() : "" - readonly property string reportFeels: reportFields.length > 3 ? String(reportFields[3] || "").trim() : "" - readonly property string reportWind: reportFields.length > 4 ? String(reportFields[4] || "").trim() : "" - readonly property string reportHumidity: reportFields.length > 5 ? String(reportFields[5] || "").trim() : "" + readonly property var current: report && report.current_condition && report.current_condition[0] ? report.current_condition[0] : null + readonly property var areaInfo: report && report.nearest_area && report.nearest_area[0] ? report.nearest_area[0] : null + readonly property var forecastDays: report && report.weather ? report.weather : [] + + readonly property bool useImperial: { + var override = setting("unit", "") + if (override === "imperial") return true + if (override === "metric") return false + var name = String(Qt.locale().name || "") + return /^en_US/.test(name) || /^en_LR/.test(name) || /^my/.test(name) + } + + // Auto-refresh interval in minutes; clamped to a sane minimum. + readonly property int refreshMinutes: Math.max(1, parseInt(setting("refreshMinutes", 15), 10) || 15) + + readonly property string reportLocation: areaInfo && areaInfo.areaName && areaInfo.areaName[0] ? areaInfo.areaName[0].value : "" + readonly property string reportCondition: current && current.weatherDesc && current.weatherDesc[0] ? current.weatherDesc[0].value : "" + readonly property string reportTemp: current ? formatTemp(useImperial ? current.temp_F : current.temp_C) : "" + readonly property string reportTempNum: current ? String(useImperial ? current.temp_F : current.temp_C) : "" + readonly property string tempUnit: "°" + (useImperial ? "F" : "C") + readonly property string reportFeels: current ? formatTemp(useImperial ? current.FeelsLikeF : current.FeelsLikeC) : "" + readonly property string reportWind: current ? (useImperial ? (current.windspeedMiles + " mph") : (current.windspeedKmph + " km/h")) : "" + readonly property string reportHumidity: current ? (current.humidity + "%") : "" visible: label !== "" implicitWidth: button.implicitWidth + 8 implicitHeight: button.implicitHeight + function setting(name, fallback) { + var v = settings ? settings[name] : undefined + return v === undefined || v === null ? fallback : v + } + function refresh() { if (!forecastProc.running) forecastProc.running = true } - // Hover state across the trigger button and the popup. - property bool buttonHovered: false - property bool popupHovered: popup.containsMouse - - function showPopup() { - hideTimer.stop() - if (!popupOpen) refresh() - popupOpen = true + function formatTemp(value) { + if (value === undefined || value === null || value === "") return "" + return value + "°" + (useImperial ? "F" : "C") } - function scheduleHide() { - hideTimer.restart() + function dayName(dateString) { + if (!dateString) return "" + var d = new Date(dateString + "T12:00:00") + if (isNaN(d.getTime())) return "" + return Qt.formatDate(d, "ddd") } - Timer { - id: hideTimer - interval: 220 - onTriggered: { - if (!root.buttonHovered && !root.popupHovered) root.popupOpen = false + function maxTempForDay(day) { + if (!day) return "" + return formatTemp(useImperial ? day.maxtempF : day.maxtempC) + } + + function minTempForDay(day) { + if (!day) return "" + return formatTemp(useImperial ? day.mintempF : day.mintempC) + } + + // Bare degree value (no unit letter), used in the forecast row. + function bareTempForDay(day, kind) { + if (!day) return "" + var v = useImperial + ? (kind === "max" ? day.maxtempF : day.mintempF) + : (kind === "max" ? day.maxtempC : day.mintempC) + if (v === undefined || v === null || v === "") return "" + return v + "°" + } + + // Representative icon for a forecast day: the hourly entry nearest noon. + function dayIcon(day) { + if (!day || !day.hourly || day.hourly.length === 0) return "" + var best = day.hourly[0] + var bestDist = 9999 + for (var i = 0; i < day.hourly.length; ++i) { + var t = parseInt(String(day.hourly[i].time || "0"), 10) + var dist = Math.abs(t - 1200) + if (dist < bestDist) { bestDist = dist; best = day.hourly[i] } + } + return iconForCode(best.weatherCode, false) + } + + // Mirrors omarchy-weather-icon's wttr.in code → nerd-font glyph mapping. + function iconForCode(code, night) { + var c = parseInt(String(code || "0"), 10) + switch (c) { + case 113: return night ? "" : "" + case 116: return night ? "" : "" + case 119: case 122: return "" + case 143: case 248: case 260: return "" + case 176: case 263: case 353: return night ? "" : "" + case 179: case 227: case 230: case 323: case 326: case 368: return night ? "" : "" + case 182: case 185: case 281: case 284: case 311: case 314: + case 317: case 320: case 350: case 362: case 365: case 374: case 377: return "" + case 200: case 386: case 389: case 392: case 395: return "" + case 266: case 293: case 296: case 299: case 302: case 305: case 308: case 356: case 359: return "" + case 329: case 332: case 335: case 338: case 371: return "" + default: return "" } } - onButtonHoveredChanged: buttonHovered ? showPopup() : scheduleHide() - onPopupHoveredChanged: popupHovered ? hideTimer.stop() : scheduleHide() - Process { id: forecastProc - command: ["bash", "-lc", "curl -fsS --max-time 5 'wttr.in/?T0&format=%l|%C|%t|%f|%w|%h' 2>/dev/null"] + command: ["bash", "-lc", "curl -fsS --max-time 5 'https://wttr.in/?format=j1' 2>/dev/null"] stdout: StdioCollector { waitForEnd: true - onStreamFinished: root.fullReport = String(text || "").trim() + onStreamFinished: { + var raw = String(text || "").trim() + if (!raw) return + try { + root.report = JSON.parse(raw) + } catch (e) { + // Keep last-good report on parse failure so the popup isn't blanked. + } + } } } + Timer { + id: refreshTimer + interval: root.refreshMinutes * 60 * 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refresh() + } + Common.WidgetButton { id: button anchors.left: parent.left @@ -83,71 +160,163 @@ Item { tooltipText: "" onPressed: function(b) { - if (b === Qt.RightButton) root.bar.run("omarchy-notification-send \"$(omarchy-weather-status)\"") - else if (b === Qt.MiddleButton) root.refresh() + if (b === Qt.RightButton) { + root.bar.run("omarchy-notification-send \"$(omarchy-weather-status)\"") + } else if (b === Qt.MiddleButton) { + root.refresh() + } else { + var willOpen = !root.popupOpen + root.popupOpen = willOpen + if (willOpen) root.refresh() + } } } - HoverHandler { - id: hoverHandler - target: button - onHoveredChanged: root.buttonHovered = hovered - } - Common.PopupCard { id: popup anchorItem: button owner: root bar: root.bar open: root.popupOpen - triggerMode: "hover" - contentWidth: 320 + triggerMode: "click" + contentWidth: 520 contentHeight: card.implicitHeight + 28 Column { id: card anchors.fill: parent - spacing: 12 + spacing: 14 - Row { + // ---- Hero row: big icon + temp on the left; location and stats stacked on the right. + Item { width: parent.width - spacing: 10 + height: Math.max(heroLeft.height, heroRight.height) - Text { - id: glyph - text: root.label || "—" - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 24 - anchors.verticalCenter: location.verticalCenter - } - - Text { - id: location - text: root.reportLocation || "Weather" - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 13 - font.bold: true + Row { + id: heroLeft + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: parent.width - glyph.width - parent.spacing - elide: Text.ElideRight + spacing: 12 + + Text { + id: heroIcon + anchors.verticalCenter: parent.verticalCenter + text: root.label || "—" + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 64 + } + + Row { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Text { + id: tempBig + text: root.reportTempNum || "—" + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 56 + font.bold: true + } + Text { + text: root.current ? root.tempUnit : "" + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 22 + anchors.top: tempBig.top + anchors.topMargin: 10 + } + } + } + + Column { + id: heroRight + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 12 + + Row { + visible: root.reportLocation !== "" + spacing: 6 + + Text { + text: "" // nf-fa-map_marker + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: 12 + anchors.verticalCenter: parent.verticalCenter + } + Text { + text: (root.reportLocation || "").toUpperCase() + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: 12 + font.letterSpacing: 1 + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + visible: !!root.current + spacing: 36 + + Column { + spacing: 5 + Text { + text: "FEELS" + color: Qt.darker(root.bar.foreground, 1.5) + font.family: root.bar.fontFamily + font.pixelSize: 11 + font.letterSpacing: 1 + } + Text { + text: root.reportFeels + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 15 + } + } + + Column { + spacing: 5 + Text { + text: "WIND" + color: Qt.darker(root.bar.foreground, 1.5) + font.family: root.bar.fontFamily + font.pixelSize: 11 + font.letterSpacing: 1 + } + Text { + text: root.reportWind + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 15 + } + } + + Column { + spacing: 5 + Text { + text: "HUMIDITY" + color: Qt.darker(root.bar.foreground, 1.5) + font.family: root.bar.fontFamily + font.pixelSize: 11 + font.letterSpacing: 1 + } + Text { + text: root.reportHumidity + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 15 + } + } + } } } Text { - visible: text !== "" - text: root.reportCondition - color: Qt.darker(root.bar.foreground, 1.2) - font.family: root.bar.fontFamily - font.pixelSize: 11 - font.italic: true - wrapMode: Text.WordWrap - width: parent.width - } - - Text { - visible: root.fullReport === "" + visible: !root.current text: "Fetching forecast…" color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily @@ -155,74 +324,68 @@ Item { font.italic: true } - // Key/value pairs — each metric on its own line for scanability. - Grid { - visible: root.reportTemp !== "" + // ---- Divider between current conditions and forecast. + Rectangle { + visible: root.forecastDays.length > 0 width: parent.width - columns: 2 - columnSpacing: 12 - rowSpacing: 4 - - Text { - text: "Temperature" - color: Qt.darker(root.bar.foreground, 1.4) - font.family: root.bar.fontFamily - font.pixelSize: 11 - } - Text { - text: root.reportTemp + (root.reportFeels && root.reportFeels !== root.reportTemp ? " (feels " + root.reportFeels + ")" : "") - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 11 - } - - Text { - text: "Wind" - color: Qt.darker(root.bar.foreground, 1.4) - font.family: root.bar.fontFamily - font.pixelSize: 11 - } - Text { - text: root.reportWind - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 11 - } - - Text { - text: "Humidity" - color: Qt.darker(root.bar.foreground, 1.4) - font.family: root.bar.fontFamily - font.pixelSize: 11 - } - Text { - text: root.reportHumidity - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 11 - } + height: 1 + color: root.bar.foreground + opacity: 0.12 } + // ---- Forecast row: each cell has the day icon left of a day-name + hi/lo column. Row { + id: forecastRow + visible: root.forecastDays.length > 0 width: parent.width - spacing: 8 - Common.PillButton { - iconText: "󰑐" - text: "Refresh" - foreground: root.bar.foreground - horizontalPadding: 12 - verticalPadding: 6 - onClicked: root.refresh() - } + Repeater { + model: root.forecastDays - Common.PillButton { - iconText: "󰏌" - text: "wttr.in" - foreground: root.bar.foreground - horizontalPadding: 12 - verticalPadding: 6 - onClicked: { root.bar.run("xdg-open https://wttr.in"); root.popupOpen = false } + Row { + required property var modelData + required property int index + width: forecastRow.width / Math.max(1, root.forecastDays.length) + spacing: 10 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: root.dayIcon(modelData) + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 24 + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Text { + text: root.dayName(modelData.date).toUpperCase() + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: 10 + font.letterSpacing: 1 + } + + Row { + spacing: 6 + + Text { + text: root.bareTempForDay(modelData, "max") + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 12 + } + Text { + text: root.bareTempForDay(modelData, "min") + color: Qt.darker(root.bar.foreground, 1.5) + font.family: root.bar.fontFamily + font.pixelSize: 12 + } + } + } + } } } } diff --git a/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml b/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml index 7e201c4e..bf684db4 100644 --- a/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml +++ b/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml @@ -2497,6 +2497,7 @@ Item { switch (meta.settingsForm) { case "spacerSettings": return spacerSettingsComponent case "calendarSettings": return calendarSettingsComponent + case "weatherSettings": return weatherSettingsComponent } } var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null @@ -2644,6 +2645,33 @@ Item { } } + Component { + id: weatherSettingsComponent + + Column { + id: weatherForm + signal fieldChanged(string key, var value) + property var entry: ({}) + + spacing: 8 + width: parent ? parent.width : 0 + + Text { + text: "Auto-refresh interval (minutes)" + color: Qt.darker(root.foreground, 1.4) + font.family: root.fontFamily + font.pixelSize: 11 + } + + SpinBox { + from: 1 + to: 1440 + value: weatherForm.entry.refreshMinutes !== undefined ? weatherForm.entry.refreshMinutes : 15 + onValueModified: weatherForm.fieldChanged("refreshMinutes", value) + } + } + } + // ===================== plugins category ================================== component PluginManager: Column { id: pm