Initial commit: Weather && Stats KDE Plasma 6 panel widget

This commit is contained in:
2026-03-22 20:58:49 -04:00
commit e0877c25a4
6 changed files with 569 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
Item {
id: root
implicitHeight: col.implicitHeight
property double cfg_latitude: 0
property double cfg_longitude: 0
property string cfg_locationName: ""
property bool cfg_useFahrenheit: true
property bool cfg_showCondition: false
property int cfg_weatherRefresh: 5
property bool cfg_cpuTempFahrenheit: false
property int cfg_cpuTempThreshold: 80
property int cfg_statsRefresh: 3
property bool cfg_showCpuTemp: true
property bool cfg_showCpuUsage: true
property bool cfg_showMemory: true
property bool cfg_showNetwork: true
component SectionHeader: ColumnLayout {
property string title: ""
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 4
QQC2.Label {
text: title
font.bold: true
font.pointSize: 9
font.letterSpacing: 2
opacity: 0.5
}
Rectangle {
Layout.fillWidth: true
height: 1
opacity: 0.15
color: "white"
}
}
ColumnLayout {
id: col
anchors.fill: parent
anchors.margins: 20
anchors.topMargin: 16
spacing: 10
// ── WEATHER ──────────────────────────────────────────────────────────
SectionHeader { title: "WEATHER" }
QQC2.Label {
text: cfg_locationName ? "📍 " + cfg_locationName : "No location set"
opacity: 0.7
font.italic: !cfg_locationName
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
QQC2.TextField {
id: searchField
Layout.fillWidth: true
placeholderText: "Search for a city..."
onAccepted: doSearch()
}
QQC2.Button {
text: "Search"
onClicked: doSearch()
}
}
ListView {
id: resultsList
Layout.fillWidth: true
Layout.preferredHeight: Math.min(resultsModel.count * 40, 160)
visible: resultsModel.count > 0
clip: true
model: ListModel { id: resultsModel }
delegate: QQC2.ItemDelegate {
width: resultsList.width
text: model.name
onClicked: {
cfg_latitude = model.lat
cfg_longitude = model.lon
cfg_locationName = model.name
resultsModel.clear()
searchField.text = ""
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 12
QQC2.Label { text: "Temperature:" }
QQC2.RadioButton {
text: "°F"
checked: cfg_useFahrenheit
onToggled: if (checked) cfg_useFahrenheit = true
}
QQC2.RadioButton {
text: "°C"
checked: !cfg_useFahrenheit
onToggled: if (checked) cfg_useFahrenheit = false
}
Item { Layout.fillWidth: true }
QQC2.Label { text: "Refresh every" }
QQC2.SpinBox {
value: cfg_weatherRefresh
from: 1; to: 60
onValueChanged: cfg_weatherRefresh = value
}
QQC2.Label { text: "min" }
}
QQC2.CheckBox {
text: "Show condition text (e.g. \"Partly Cloudy\")"
checked: cfg_showCondition
onToggled: cfg_showCondition = checked
}
// ── STATS ─────────────────────────────────────────────────────────────
SectionHeader { title: "STATS" }
RowLayout {
Layout.fillWidth: true
spacing: 12
QQC2.Label { text: "CPU Temp:" }
QQC2.RadioButton {
text: "°F"
checked: cfg_cpuTempFahrenheit
onToggled: if (checked) cfg_cpuTempFahrenheit = true
}
QQC2.RadioButton {
text: "°C"
checked: !cfg_cpuTempFahrenheit
onToggled: if (checked) cfg_cpuTempFahrenheit = false
}
Item { Layout.fillWidth: true }
QQC2.Label { text: "Refresh every" }
QQC2.SpinBox {
value: cfg_statsRefresh
from: 2; to: 60
onValueChanged: cfg_statsRefresh = value
}
QQC2.Label { text: "sec" }
}
RowLayout {
Layout.fillWidth: true
spacing: 12
QQC2.Label { text: "Alert when CPU temp exceeds:" }
QQC2.SpinBox {
from: cfg_cpuTempFahrenheit ? 122 : 50
to: cfg_cpuTempFahrenheit ? 230 : 110
value: cfg_cpuTempFahrenheit ? Math.round(cfg_cpuTempThreshold * 9/5 + 32) : cfg_cpuTempThreshold
onValueChanged: cfg_cpuTempThreshold = cfg_cpuTempFahrenheit ? Math.round((value - 32) * 5/9) : value
}
QQC2.Label { text: cfg_cpuTempFahrenheit ? "°F" : "°C" }
}
QQC2.Label {
text: "Visible stats:"
opacity: 0.7
Layout.topMargin: 2
}
GridLayout {
columns: 2
Layout.fillWidth: true
columnSpacing: 32
rowSpacing: 2
QQC2.CheckBox {
text: "CPU Temperature"
checked: cfg_showCpuTemp
onToggled: cfg_showCpuTemp = checked
}
QQC2.CheckBox {
text: "CPU Usage"
checked: cfg_showCpuUsage
onToggled: cfg_showCpuUsage = checked
}
QQC2.CheckBox {
text: "Memory Usage"
checked: cfg_showMemory
onToggled: cfg_showMemory = checked
}
QQC2.CheckBox {
text: "Network Speed"
checked: cfg_showNetwork
onToggled: cfg_showNetwork = checked
}
}
Item { Layout.fillHeight: true; Layout.minimumHeight: 16 }
}
function doSearch() {
var query = searchField.text.trim()
if (query.length < 2) return
var url = "https://geocoding-api.open-meteo.com/v1/search"
+ "?name=" + encodeURIComponent(query)
+ "&count=8&language=en&format=json"
var req = new XMLHttpRequest()
req.open("GET", url)
req.onreadystatechange = function() {
if (req.readyState === XMLHttpRequest.DONE && req.status === 200) {
var data = JSON.parse(req.responseText)
resultsModel.clear()
if (data.results) {
data.results.forEach(function(r) {
var label = r.name
if (r.admin1) label += ", " + r.admin1
if (r.country) label += ", " + r.country
resultsModel.append({ name: label, lat: r.latitude, lon: r.longitude })
})
}
}
}
req.send()
}
}
+198
View File
@@ -0,0 +1,198 @@
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.plasmoid
import org.kde.plasma.plasma5support as P5Support
import org.kde.plasma.components as PlasmaComponents
import org.kde.kirigami as Kirigami
PlasmoidItem {
id: root
// ── Config bindings ───────────────────────────────────────────────────────
property double lat: Plasmoid.configuration.latitude
property double lon: Plasmoid.configuration.longitude
property bool fahrenheit: Plasmoid.configuration.useFahrenheit
property bool showCondition: Plasmoid.configuration.showCondition || false
property int weatherRefresh: Plasmoid.configuration.weatherRefresh || 5
property bool cpuFahrenheit: Plasmoid.configuration.cpuTempFahrenheit || false
property int cpuTempThreshold: Plasmoid.configuration.cpuTempThreshold || 80
property int statsRefresh: Plasmoid.configuration.statsRefresh || 10
property bool showCpuTemp: Plasmoid.configuration.showCpuTemp !== false
property bool showCpuUsage: Plasmoid.configuration.showCpuUsage !== false
property bool showMemory: Plasmoid.configuration.showMemory !== false
property bool showNetwork: Plasmoid.configuration.showNetwork !== false
// ── Live state ────────────────────────────────────────────────────────────
property string weatherIcon: ""
property string temperature: "--"
property string weatherCondition: ""
property int cpuTempRaw: -1
property int cpuUsage: -1
property int memUsage: -1
property int netDown: -1
property int netUp: -1
property string cpuTempDisplay: {
if (cpuTempRaw < 0) return "--"
if (cpuFahrenheit) return Math.round(cpuTempRaw * 9/5 + 32) + "°F"
return cpuTempRaw + "°C"
}
function padPct(val) {
var s = val + "%"
if (val < 10) return "\u00a0\u00a0" + s
if (val < 100) return "\u00a0" + s
return s
}
function formatNetSpeed(kbs) {
if (kbs < 0) return "--"
if (kbs < 1000) return kbs + " KB/s"
return (kbs / 1024).toFixed(1) + " MB/s"
}
// ── Panel display ─────────────────────────────────────────────────────────
preferredRepresentation: fullRepresentation
fullRepresentation: PlasmaComponents.Label {
Layout.fillHeight: true
Layout.preferredWidth: implicitWidth + 16
verticalAlignment: Text.AlignVCenter
textFormat: Text.RichText
text: {
// CPU temp is red when above threshold
var hot = root.cpuTempRaw > 0 && root.cpuTempRaw >= root.cpuTempThreshold
// Weather section
var s = root.weatherIcon + "\u00a0\u00a0" + root.temperature + "°" + (root.fahrenheit ? "F" : "C")
if (root.showCondition && root.weatherCondition !== "")
s += " " + root.weatherCondition
// CPU + Memory stats
var cpuMem = []
if (root.showCpuTemp) {
var tempStr = "\uf2c8\u00a0\u00a0" + root.cpuTempDisplay
cpuMem.push(hot ? "<font color='#ff5555'>" + tempStr + "</font>" : tempStr)
}
if (root.showCpuUsage)
cpuMem.push("\uf2db\u00a0\u00a0" + (root.cpuUsage >= 0 ? padPct(root.cpuUsage) : "\u00a0--"))
if (root.showMemory)
cpuMem.push("\ue266\u00a0\u00a0" + (root.memUsage >= 0 ? padPct(root.memUsage) : "\u00a0--"))
var sp = "\u00a0\u00a0\u00a0\u00a0"
var div = "\u00a0\u00a0\u2502\u00a0\u00a0"
if (cpuMem.length > 0) s += div + cpuMem.join(sp)
// Network stats
if (root.showNetwork)
s += div + "\uf0ac" + sp + "\uf063\u00a0\u00a0" + formatNetSpeed(root.netDown) + sp + "\uf062\u00a0\u00a0" + formatNetSpeed(root.netUp)
return s + " "
}
}
// ── Weather ───────────────────────────────────────────────────────────────
function iconForCode(code) {
if (code === 0) return "\ue30d"
if (code <= 3) return "\ue312"
if (code === 45 || code === 48) return "\ue313"
if ([51,53,55,61,63,65].indexOf(code) >= 0) return "\ue318"
if ([71,73,75].indexOf(code) >= 0) return "\ue31a"
return "\ue32e"
}
function conditionForCode(code) {
var map = {
0: "Clear", 1: "Mostly Clear", 2: "Partly Cloudy", 3: "Overcast",
45: "Foggy", 48: "Icy Fog",
51: "Light Drizzle", 53: "Drizzle", 55: "Heavy Drizzle",
61: "Light Rain", 63: "Rain", 65: "Heavy Rain",
71: "Light Snow", 73: "Snow", 75: "Heavy Snow", 77: "Snow Grains",
80: "Light Showers", 81: "Showers", 82: "Heavy Showers",
85: "Snow Showers", 86: "Heavy Snow Showers",
95: "Thunderstorm", 96: "Thunderstorm w/ Hail", 99: "Heavy Thunderstorm"
}
return map[code] || ""
}
function fetchWeather() {
var unit = root.fahrenheit ? "fahrenheit" : "celsius"
var url = "https://api.open-meteo.com/v1/forecast"
+ "?latitude=" + root.lat
+ "&longitude=" + root.lon
+ "&current_weather=true"
+ "&temperature_unit=" + unit
+ "&timezone=auto"
var req = new XMLHttpRequest()
req.open("GET", url)
req.onreadystatechange = function() {
if (req.readyState === XMLHttpRequest.DONE && req.status === 200) {
var cw = JSON.parse(req.responseText).current_weather
root.temperature = Math.round(cw.temperature).toString()
root.weatherIcon = root.iconForCode(cw.weathercode)
root.weatherCondition = root.conditionForCode(cw.weathercode)
}
}
req.send()
}
// ── System Stats ──────────────────────────────────────────────────────────
P5Support.DataSource {
id: sysStatsSource
engine: "executable"
connectedSources: []
onNewData: function(source, data) {
var out = data["stdout"].trim().split(/\s+/)
if (out.length >= 5) {
root.cpuTempRaw = parseInt(out[0]) || -1
root.cpuUsage = parseInt(out[1]) || -1
root.memUsage = parseInt(out[2]) || -1
root.netDown = parseInt(out[3])
root.netUp = parseInt(out[4])
}
sysStatsSource.disconnectSource(source)
}
}
function fetchSysStats() {
var path = Qt.resolvedUrl("../stats.sh").toString().replace("file://", "")
sysStatsSource.connectSource("bash " + path)
}
// ── Timers ────────────────────────────────────────────────────────────────
Timer {
id: weatherTimer
interval: Math.max(1, root.weatherRefresh) * 60000
repeat: true
running: true
onTriggered: root.fetchWeather()
}
Timer {
id: statsTimer
interval: Math.max(2, root.statsRefresh) * 1000
repeat: true
running: true
onTriggered: root.fetchSysStats()
}
onWeatherRefreshChanged: weatherTimer.restart()
onStatsRefreshChanged: statsTimer.restart()
Component.onCompleted: {
fetchWeather()
fetchSysStats()
}
Connections {
target: Plasmoid.configuration
function onLatitudeChanged() { root.fetchWeather() }
function onLongitudeChanged() { root.fetchWeather() }
function onUseFahrenheitChanged() { root.fetchWeather() }
}
}