From 4443b5415c3b36880fdebc042ddafe65b5e7bbae Mon Sep 17 00:00:00 2001 From: Josevi Date: Thu, 24 Sep 2026 06:53:27 +0200 Subject: [PATCH 1/3] plugins: add KDE Connect 0.1.0 --- plugins/kdeconnect-status/.gitignore | 3 + plugins/kdeconnect-status/AGENTS.md | 84 ++++++++++++++++ plugins/kdeconnect-status/CLAUDE.md | 4 + plugins/kdeconnect-status/LICENSE | 21 ++++ plugins/kdeconnect-status/README.md | 70 +++++++++++++ plugins/kdeconnect-status/bin/poll.sh | 49 ++++++++++ plugins/kdeconnect-status/content/Panel.qml | 86 ++++++++++++++++ plugins/kdeconnect-status/content/Widget.qml | 98 +++++++++++++++++++ plugins/kdeconnect-status/manifest.json | 52 ++++++++++ .../kdeconnect-status/product-manifest.json | 89 +++++++++++++++++ plugins/kdeconnect-status/service/Main.qml | 66 +++++++++++++ plugins/registry.json | 25 +++++ 12 files changed, 647 insertions(+) create mode 100644 plugins/kdeconnect-status/.gitignore create mode 100644 plugins/kdeconnect-status/AGENTS.md create mode 100644 plugins/kdeconnect-status/CLAUDE.md create mode 100644 plugins/kdeconnect-status/LICENSE create mode 100644 plugins/kdeconnect-status/README.md create mode 100755 plugins/kdeconnect-status/bin/poll.sh create mode 100644 plugins/kdeconnect-status/content/Panel.qml create mode 100644 plugins/kdeconnect-status/content/Widget.qml create mode 100644 plugins/kdeconnect-status/manifest.json create mode 100644 plugins/kdeconnect-status/product-manifest.json create mode 100644 plugins/kdeconnect-status/service/Main.qml diff --git a/plugins/kdeconnect-status/.gitignore b/plugins/kdeconnect-status/.gitignore new file mode 100644 index 00000000..b8eb3e3c --- /dev/null +++ b/plugins/kdeconnect-status/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.log +*.tmp diff --git a/plugins/kdeconnect-status/AGENTS.md b/plugins/kdeconnect-status/AGENTS.md new file mode 100644 index 00000000..5ae58376 --- /dev/null +++ b/plugins/kdeconnect-status/AGENTS.md @@ -0,0 +1,84 @@ +# Agent guide for the kdeconnect-status plugin + +This folder is a Ryoku shell plugin. If you are an AI agent editing it, follow +every rule below. They are the same rules `ryoku plugin validate` enforces, so +work that breaks one will not install. This file is self-contained: you do not +need any other document to work here. + +## The rules + +R1 Place. A plugin is one folder named after its id. Author it under +`$(xdg-user-dir DOCUMENTS)/ryoku-plugins//` (fallback `~/ryoku-plugins//`); +`ryoku plugin new ` creates it there. Never write into +`~/.local/share/ryoku/plugins/` (the store's install root, receipt-owned) or +`~/.config/quickshell/` (the shipped shell). + +R2 Shape. `manifest.json` at the root; `service/Main.qml` (logic, no UI); +`content/Widget.qml` (the one view); optional `content/Panel.qml` (a bar panel); +`README.md`; `LICENSE`; `assets/preview-widget.png` (a real capture); scripts +only under `bin/` with a shebang and the exec bit. Every extra file listed in +manifest `files`. + +R3 Id. Lowercase `[a-z0-9][a-z0-9-]*`, unique, not a built-in widget id. + +R4 Imports. Only `QtQuick*`, `Quickshell*`, `Ryoku.PluginKit`, +`Ryoku.PluginKit.Singletons`, and files inside the plugin folder. Never +`shell.*`, `Ryoku.Ui*` internals, or a relative import that climbs out of the +folder. + +R5 Settings. Declared in `metadata.settings` (types the bar renders: toggle, +choice, multi, int, text); read through `pluginApi.pluginSettings` behind a +default; written only through `pluginApi.saveSetting(key, value)`. Never edit +`shell.json` or `plugins.json` directly. + +R6 Commands. Every external program the plugin runs is either in its own `bin/` +or listed in `dependencies.commands`. No `sudo`, `doas`, `su`. A privileged +action is allowed only through `pkexec`, only on an explicit click, only when +listed in manifest `capabilities.privileged` (exact command strings) and +explained in README. + +R7 Network. Every host the plugin talks to is listed in +`capabilities.network`. No `curl … | sh`, no downloading and running code. + +R8 Files. Write only under `pluginApi.stateDir` +(`$XDG_STATE_HOME/ryoku/plugins/`), `$XDG_CACHE_HOME/ryoku/plugins/`, or +a temp dir. Never touch `~/.ssh`, `/etc`, shell rc files, or another plugin's +folder. + +R9 Shell. No `sh -c` with a string built from settings or program output +(injection). Pass argv arrays. + +R10 Secrets and binaries. No tokens, keys or credentials in the tree; no +compiled binaries (ELF/Mach-O/.so); scripts only. + +R11 Honesty. `official` is never true for a community plugin. `author` is +`Name `. README says what runs, what it reads, what it writes, and every +privileged/network capability. + +## The workflow + +1. `ryoku plugin new ` scaffolds this folder (already done). +2. Edit: `service/Main.qml` for logic, `content/Widget.qml` for the view, and + `content/Panel.qml` for the bar panel. Capture `assets/preview-widget.png`. +3. `ryoku plugin validate ` and fix every blocking finding (warnings are + advisory but read them). +4. `ryoku plugin add --bar --yes` to install it, then verify it on the bar + and under **QS Bar Settings > Community**. +5. `ryoku plugin share ` ONLY when the user asks to publish. Never share a + plugin unless asked. + +## What the host sets + +Every entry point receives, from the host: `pluginApi` (see below), `s` (a scale +factor), `active`, `density`, and `widthBudget`. Read them; never assign them. + +`pluginApi` exposes: + +- `mainInstance`: the live `service/Main.qml` instance every view shares. +- `pluginSettings`: the resolved settings object; read behind a default. +- `pluginDir`: this plugin's folder. +- `stateDir`: `$XDG_STATE_HOME/ryoku/plugins/`; the only place to write. +- `saveSetting(key, value)`: persist one setting. +- `panelOpen` (bool), `openPanel()`, `closePanel()`, `togglePanel()`: the bar + panel controls (present but inert off the bar). The sanctioned widget click is + `onClicked: pluginApi.togglePanel()`. diff --git a/plugins/kdeconnect-status/CLAUDE.md b/plugins/kdeconnect-status/CLAUDE.md new file mode 100644 index 00000000..3c35c327 --- /dev/null +++ b/plugins/kdeconnect-status/CLAUDE.md @@ -0,0 +1,4 @@ +# CLAUDE.md + +Read `AGENTS.md` in this folder before editing anything here. +It holds every rule this plugin must follow and the full authoring workflow. diff --git a/plugins/kdeconnect-status/LICENSE b/plugins/kdeconnect-status/LICENSE new file mode 100644 index 00000000..2f26dd61 --- /dev/null +++ b/plugins/kdeconnect-status/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vampirejsv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/kdeconnect-status/README.md b/plugins/kdeconnect-status/README.md new file mode 100644 index 00000000..f7f33b0a --- /dev/null +++ b/plugins/kdeconnect-status/README.md @@ -0,0 +1,70 @@ +# KDE Connect + +A Ryoku shell plugin (`kdeconnect-status`) that shows the live status of the +first reachable/paired KDE Connect device on the bar: a connection glyph, the +device's battery percent, and its unread notification count. Purely +informative — no click action ever changes state, sends anything to the +device, or triggers a KDE Connect plugin action. + +## What it does + +- **Service** (`service/Main.qml`): polls `bin/poll.sh` on a timer (default + every 10s, configurable) and holds the parsed result: `found`, `deviceName`, + `reachable`, `charge`, `charging`, `notifCount`, `lastPollFailed`. +- **Widget** (`content/Widget.qml`): a diamond glyph (filled when reachable, + hollow otherwise), the battery percent (with a bolt while charging), and a + `(N)` unread-notification badge when N > 0. A left click only opens the + panel; it never mutates anything. +- **Panel** (`content/Panel.qml`): the device name, connection state, battery, + and notification count as plain text. No buttons. + +## What it reads and writes + +Reads only, over the user session D-Bus, via `bin/poll.sh`: + +- `org.kde.kdeconnect` service, `/modules/kdeconnect` `daemon.devices(bb)` to + find the first paired+reachable device id. +- That device's `org.kde.kdeconnect.device` `name` / `isReachable` properties. +- Its `.battery` interface's `charge` / `isCharging` properties. +- Its `.notifications` interface's `activeNotifications()` method, to count + unread notifications. + +`bin/poll.sh` never calls a KDE Connect method that sends anything (no ping, +no file share, no notification action, no pairing). It never writes any file; +all output is a single JSON line to stdout that `service/Main.qml` parses. +Settings are read through `pluginApi.pluginSettings` behind a default and +written only through `pluginApi.saveSetting` (the settings panel the bar +renders); the plugin itself never edits `shell.json` or `plugins.json`. + +## Requirements + +- `kdeconnect` installed and `kdeconnectd` running (autostarted by its own + package via `/etc/xdg/autostart`). +- At least one device paired via `kdeconnect-cli --pair` or the KDE Connect + app; otherwise the panel just says "No paired device found." +- `busctl` and `jq` on PATH (both listed in `dependencies.commands`). + +## Settings + +| key | type | default | description | +| ----------- | ---- | ------- | ----------------------- | +| pollSeconds | int | 10 | Refresh interval, seconds | + +## Preview + +Capture a real screenshot of the widget and save it as +`assets/preview-widget.png`, then list it under `files` in `manifest.json`. + +## Build, check, install + +``` +ryoku plugin validate . +ryoku plugin add . --bar --yes +``` + +It lists under **Community** in QS Bar Settings. Publish it only when asked: +`ryoku plugin share kdeconnect-status`. + +## Author + +vampirejsv : this plugin is community-made (`official` is false). diff --git a/plugins/kdeconnect-status/bin/poll.sh b/plugins/kdeconnect-status/bin/poll.sh new file mode 100755 index 00000000..0b28016d --- /dev/null +++ b/plugins/kdeconnect-status/bin/poll.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Polls kdeconnectd over the user D-Bus for the first reachable+paired device +# and prints one line of JSON the widget can parse directly: +# {"found":bool,"name":str,"reachable":bool,"charge":int,"charging":bool,"notifCount":int} +# Read-only: every busctl call here is a Get/call on kdeconnectd's own +# read-only properties and methods, nothing is ever set or sent. +set -euo pipefail + +fail_json() { + printf '{"found":false}\n' + exit 0 +} + +command -v busctl >/dev/null 2>&1 || fail_json +command -v jq >/dev/null 2>&1 || fail_json + +DEST=org.kde.kdeconnect + +# First device id kdeconnectd currently reports as reachable+paired. +DEVID=$(busctl --user --json=short call "$DEST" /modules/kdeconnect \ + org.kde.kdeconnect.daemon devices bb false true 2>/dev/null \ + | jq -r '.data[0][0] // empty') || fail_json + +[ -n "$DEVID" ] || fail_json + +BASE="/modules/kdeconnect/devices/$DEVID" + +get_prop() { + # $1 = object path suffix, $2 = interface, $3 = property + busctl --user --json=short get-property "$DEST" "$BASE$1" "$2" "$3" 2>/dev/null \ + | jq -r '.data' +} + +NAME=$(get_prop "" org.kde.kdeconnect.device name) +REACHABLE=$(get_prop "" org.kde.kdeconnect.device isReachable) +CHARGE=$(get_prop /battery org.kde.kdeconnect.device.battery charge 2>/dev/null || echo -1) +CHARGING=$(get_prop /battery org.kde.kdeconnect.device.battery isCharging 2>/dev/null || echo false) + +NOTIF_COUNT=$(busctl --user --json=short call "$DEST" "$BASE/notifications" \ + org.kde.kdeconnect.device.notifications activeNotifications 2>/dev/null \ + | jq -r '.data[0] | length' 2>/dev/null || echo 0) + +jq -nc \ + --arg name "${NAME:-}" \ + --argjson reachable "${REACHABLE:-false}" \ + --argjson charge "${CHARGE:--1}" \ + --argjson charging "${CHARGING:-false}" \ + --argjson notifCount "${NOTIF_COUNT:-0}" \ + '{found:true,name:$name,reachable:$reachable,charge:$charge,charging:$charging,notifCount:$notifCount}' diff --git a/plugins/kdeconnect-status/content/Panel.qml b/plugins/kdeconnect-status/content/Panel.qml new file mode 100644 index 00000000..ecf75d9f --- /dev/null +++ b/plugins/kdeconnect-status/content/Panel.qml @@ -0,0 +1,86 @@ +import QtQuick +import Ryoku.PluginKit.Singletons + +// content/Panel.qml: purely informative detail card, no buttons, no actions. +// Opens under the glyph on click. +Item { + id: root + + property var pluginApi + property string density: "full" + property real s: 1 + property real widthBudget: 280 + property bool active: false + + readonly property var service: pluginApi ? pluginApi.mainInstance : null + readonly property bool found: service ? service.found : false + readonly property string deviceName: service ? service.deviceName : "" + readonly property bool reachable: service ? service.reachable : false + readonly property int charge: service ? service.charge : -1 + readonly property bool charging: service ? service.charging : false + readonly property int notifCount: service ? service.notifCount : 0 + readonly property bool lastPollFailed: service ? service.lastPollFailed : false + + implicitWidth: root.widthBudget + implicitHeight: col.implicitHeight + 24 * root.s + + Column { + id: col + x: 14 * root.s + y: 12 * root.s + width: root.width - 28 * root.s + spacing: 8 * root.s + + Text { + text: "KDE Connect" + color: Theme.bright + font.family: Theme.display + font.pixelSize: 16 * root.s + } + + Text { + visible: !root.found + text: root.lastPollFailed ? "Could not reach kdeconnectd." : "No paired device found." + color: Theme.dim + font.family: Theme.font + font.pixelSize: 13 * root.s + wrapMode: Text.WordWrap + width: parent.width + } + + Column { + visible: root.found + width: parent.width + spacing: 6 * root.s + + Text { + text: root.deviceName || "Unknown device" + color: Theme.bright + font.family: Theme.font + font.pixelSize: 14 * root.s + } + + Text { + text: root.reachable ? "Connected" : "Not reachable" + color: root.reachable ? Theme.accent : Theme.dim + font.family: Theme.font + font.pixelSize: 12 * root.s + } + + Text { + visible: root.charge >= 0 + text: "Battery: " + root.charge + "%" + (root.charging ? " (charging)" : "") + color: Theme.dim + font.family: Theme.font + font.pixelSize: 12 * root.s + } + + Text { + text: "Notifications: " + root.notifCount + color: Theme.dim + font.family: Theme.font + font.pixelSize: 12 * root.s + } + } + } +} diff --git a/plugins/kdeconnect-status/content/Widget.qml b/plugins/kdeconnect-status/content/Widget.qml new file mode 100644 index 00000000..8f6857f8 --- /dev/null +++ b/plugins/kdeconnect-status/content/Widget.qml @@ -0,0 +1,98 @@ +import QtQuick +import QtQuick.Shapes +import Ryoku.PluginKit.Singletons + +// content/Widget.qml is the one view the host mounts (on the bar, this is the +// glyph). It reads live state from the service (pluginApi.mainInstance) and +// its only click action toggles the plugin's panel: read-only, no actions. +Item { + id: root + + property var pluginApi + property var screen + property bool active: false + property string density: "glyph" + property real s: 1 + property real widthBudget: 0 + + readonly property var service: pluginApi ? pluginApi.mainInstance : null + readonly property bool found: service ? service.found : false + readonly property bool reachable: service ? service.reachable : false + readonly property int charge: service ? service.charge : -1 + readonly property bool charging: service ? service.charging : false + readonly property int notifCount: service ? service.notifCount : 0 + + // Every built-in bar widget (CPU/storage/battery/volume) paints its icon + // AND its number in the bar's own accent colour ("seal"/widgetIconColor + // in the shipped widgets), never a flat grey — that accent is what makes + // a plain-white number stand out. Ryoku.PluginKit's Theme.accent resolves + // the same live accent role, so match it instead of Theme.dim/bright. + readonly property color glyphColor: Theme.accent + readonly property real glyphOpacity: root.found ? (root.reachable ? 1 : 0.55) : 0.4 + + implicitWidth: row.implicitWidth + implicitHeight: Math.max(row.implicitHeight, 18 * root.s) + + Row { + id: row + anchors.verticalCenter: parent.verticalCenter + spacing: 4 * root.s + + // A smartphone body: a stroke-only vector path in a 24x24 box, the + // same technique Ryoku's own GlyphIcon uses (baked SVG path data, no + // system icon theme dependency). Rounded rect + a home-indicator line + // near the bottom edge, so it reads as a phone, not an abstract mark. + Shape { + id: phoneShape + anchors.verticalCenter: parent.verticalCenter + width: 14 * root.s + height: 14 * root.s + antialiasing: true + preferredRendererType: Shape.CurveRenderer + opacity: root.glyphOpacity + Behavior on opacity { NumberAnimation { duration: 200 } } + + ShapePath { + strokeColor: root.glyphColor + fillColor: "transparent" + strokeWidth: 1.7 + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + scale: Qt.size(phoneShape.width / 24, phoneShape.height / 24) + PathSvg { + path: "M9.5 2h5A2.5 2.5 0 0 1 17 4.5v15A2.5 2.5 0 0 1 14.5 22h-5A2.5 2.5 0 0 1 7 19.5v-15A2.5 2.5 0 0 1 9.5 2z M11 19h2" + } + Behavior on strokeColor { ColorAnimation { duration: 200 } } + } + } + + // Battery percent, only once we actually have a reading. Theme.accent + // is the same colour every other bar percentage (CPU/storage/battery) + // is painted in, so this reads as part of the bar, not a foreign white. + Text { + visible: root.found && root.charge >= 0 + anchors.verticalCenter: parent.verticalCenter + text: root.charge + "%" + (root.charging ? "\u26A1" : "") + color: Theme.accent + font.family: Theme.font + font.pixelSize: 12 * root.s + elide: Text.ElideRight + width: root.widthBudget > 0 ? Math.min(implicitWidth, root.widthBudget) : implicitWidth + } + + // Unread notification count, only when there is at least one. + Text { + visible: root.found && root.notifCount > 0 + anchors.verticalCenter: parent.verticalCenter + text: "(" + root.notifCount + ")" + color: Theme.accent + font.family: Theme.font + font.pixelSize: 12 * root.s + } + } + + MouseArea { + anchors.fill: parent + onClicked: if (root.pluginApi) root.pluginApi.togglePanel() + } +} diff --git a/plugins/kdeconnect-status/manifest.json b/plugins/kdeconnect-status/manifest.json new file mode 100644 index 00000000..839b0b5c --- /dev/null +++ b/plugins/kdeconnect-status/manifest.json @@ -0,0 +1,52 @@ +{ + "id": "kdeconnect-status", + "name": "KDE Connect", + "version": "0.1.0", + "author": "vampirejsv ", + "description": "Live KDE Connect device status on the bar: connection, battery, unread notification count. Read-only, no actions.", + "license": "MIT", + "official": false, + "hosts": [ + "topbarGlyph" + ], + "entryPoints": { + "main": "service/Main.qml", + "content": "content/Widget.qml", + "panel": "content/Panel.qml" + }, + "panel": { + "width": 280 + }, + "files": [ + "bin/poll.sh" + ], + "capabilities": { + "densities": [ + "glyph", + "compact" + ] + }, + "defaults": { + "host": "topbarGlyph", + "label": "KDE Connect" + }, + "dependencies": { + "commands": [ + "busctl", + "jq" + ] + }, + "metadata": { + "settings": [ + { + "key": "pollSeconds", + "type": "int", + "label": "Refresh interval (s)", + "group": "KDE Connect", + "default": 10, + "min": 3, + "max": 60 + } + ] + } +} diff --git a/plugins/kdeconnect-status/product-manifest.json b/plugins/kdeconnect-status/product-manifest.json new file mode 100644 index 00000000..12c009d9 --- /dev/null +++ b/plugins/kdeconnect-status/product-manifest.json @@ -0,0 +1,89 @@ +{ + "schema": 1, + "id": "kdeconnect-status", + "category": "plugins", + "version": "0.1.0", + "destination": "ryoku/plugins/kdeconnect-status", + "files": [ + { + "source": ".gitignore", + "destination": ".gitignore", + "sha256": "379dfa9273952c08fa44042de676ed4addc26a7c4d4c90235d6ff2c73dffb810", + "mode": "0644", + "size": 22, + "install": true + }, + { + "source": "AGENTS.md", + "destination": "AGENTS.md", + "sha256": "e3deb2be2f6824d3233cd08604dfb581dddac498726f514eff183751dfb9e7b3", + "mode": "0644", + "size": 3966, + "install": true + }, + { + "source": "CLAUDE.md", + "destination": "CLAUDE.md", + "sha256": "09ffb8ebdb8adf50bf4bde26dfe2109b4af3a4a8cc3f30cc536be42bb912c033", + "mode": "0644", + "size": 152, + "install": true + }, + { + "source": "LICENSE", + "destination": "LICENSE", + "sha256": "3115cecc7b0a749c8ec85426e41e8ed7fbe44483f1e0df7d8d391fe58d95712e", + "mode": "0644", + "size": 1067, + "install": false + }, + { + "source": "README.md", + "destination": "README.md", + "sha256": "8a7692c9a8230e8cb2bc77e6c262c3c863352a4ac2014ddc028d514593dc902a", + "mode": "0644", + "size": 2885, + "install": false + }, + { + "source": "bin/poll.sh", + "destination": "bin/poll.sh", + "sha256": "2d0a77cd8884d7af67c877de12fba1d02f02dd8c5ce4d672c1c48d0ac7cf8006", + "mode": "0755", + "size": 1943, + "install": true + }, + { + "source": "content/Panel.qml", + "destination": "content/Panel.qml", + "sha256": "e668a7490a35724dbb1a6053f0ff4a87e4d4ccdfa309508ecde9d6c2b650755e", + "mode": "0644", + "size": 2746, + "install": true + }, + { + "source": "content/Widget.qml", + "destination": "content/Widget.qml", + "sha256": "52562bf55c771ee9e0e557c119796cb7b88158edc152616372d13f0c52389317", + "mode": "0644", + "size": 4118, + "install": true + }, + { + "source": "manifest.json", + "destination": "manifest.json", + "sha256": "c821df91722a42bd37a6185d41d8b86b6af73c1071df0e0834e2b0029726d5e8", + "mode": "0644", + "size": 1010, + "install": true + }, + { + "source": "service/Main.qml", + "destination": "service/Main.qml", + "sha256": "2b5f88fdf43756c16f3038008e3dc9161f025c1265697daa735fdea519f38b5a", + "mode": "0644", + "size": 2322, + "install": true + } + ] +} diff --git a/plugins/kdeconnect-status/service/Main.qml b/plugins/kdeconnect-status/service/Main.qml new file mode 100644 index 00000000..4cfb9c9c --- /dev/null +++ b/plugins/kdeconnect-status/service/Main.qml @@ -0,0 +1,66 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell.Io + +// service/Main.qml is the plugin's logic: no UI. The host loads one instance +// and hands it to every view as pluginApi.mainInstance, so the widget and the +// panel read the same live state. +// +// Polls kdeconnectd over the session D-Bus (via bin/poll.sh, R6/R9-clean: no +// shell string built from input, a fixed argv only) on a timer. Read-only: +// the script only issues busctl Get/call on kdeconnectd's own properties and +// methods, it never sends a ping, file, or notification action. +Item { + id: svc + + // Set by the host after this loads; the plugin's settings live behind it. + property var pluginApi + readonly property var settings: pluginApi ? pluginApi.pluginSettings : null + readonly property int pollSeconds: settings && settings.pollSeconds ? settings.pollSeconds : 10 + + // Live device state, defaults until the first poll lands. + property bool found: false + property string deviceName: "" + property bool reachable: false + property int charge: -1 + property bool charging: false + property int notifCount: 0 + property bool lastPollFailed: false + + Process { + id: pollProc + command: [(svc.pluginApi ? svc.pluginApi.pluginDir : "") + "/bin/poll.sh"] + stdout: StdioCollector { + id: pollOut + onStreamFinished: { + try { + const data = JSON.parse(pollOut.text); + svc.lastPollFailed = false; + svc.found = !!data.found; + svc.deviceName = data.name || ""; + svc.reachable = !!data.reachable; + svc.charge = (typeof data.charge === "number") ? data.charge : -1; + svc.charging = !!data.charging; + svc.notifCount = (typeof data.notifCount === "number") ? data.notifCount : 0; + } catch (e) { + svc.lastPollFailed = true; + svc.found = false; + } + } + } + } + + function poll() { + pollProc.running = false; + pollProc.running = true; + } + + Timer { + interval: svc.pollSeconds * 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: svc.poll() + } +} diff --git a/plugins/registry.json b/plugins/registry.json index aea0b2cf..2f969d2d 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1153,6 +1153,31 @@ "desktopWidget" ], "lastUpdated": "2026-09-21" + }, + { + "id": "kdeconnect-status", + "name": "KDE Connect", + "version": "0.1.0", + "path": "plugins/kdeconnect-status", + "author": "vampirejsv ", + "summary": "Live KDE Connect device status on the bar: connection, battery, unread notification count.", + "description": "Live KDE Connect device status on the bar: connection, battery, unread notification count. Read-only, no actions.", + "tags": [ + "bar-widget" + ], + "accent": "#8f8378", + "surface": "#101010", + "preview": "", + "screenshots": [], + "manifest": "product-manifest.json", + "manifestSha256": "3a5f611ccda2d851164db23702981f55738a9bf2435cd7fac70fbdc93bb325cd", + "official": false, + "tagline": "Live KDE Connect device status on the bar: connection, battery, unread notification count.", + "icon": "", + "hosts": [ + "topbarGlyph" + ], + "lastUpdated": "2026-09-24" } ] } From b1af78b189670419fc26e67b0b04cda85abececa Mon Sep 17 00:00:00 2001 From: vampirejsv Date: Thu, 24 Sep 2026 06:59:31 +0200 Subject: [PATCH 2/3] kdeconnect-status: add preview-widget.png --- .../kdeconnect-status/assets/preview-widget.png | Bin 0 -> 3059 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 plugins/kdeconnect-status/assets/preview-widget.png diff --git a/plugins/kdeconnect-status/assets/preview-widget.png b/plugins/kdeconnect-status/assets/preview-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..54f50ea295ffbb41c907fbe5ed726cdead64d3da GIT binary patch literal 3059 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 B?~VWf literal 0 HcmV?d00001 From 478da8fce4b0886f81095fab094bd4fa9e3ac439 Mon Sep 17 00:00:00 2001 From: vampirejsv Date: Thu, 24 Sep 2026 07:10:49 +0200 Subject: [PATCH 3/3] kdeconnect-status: declare preview asset and upstream in registry --- plugins/kdeconnect-status/product-manifest.json | 8 ++++++++ plugins/registry.json | 13 ++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/plugins/kdeconnect-status/product-manifest.json b/plugins/kdeconnect-status/product-manifest.json index 12c009d9..cab62abc 100644 --- a/plugins/kdeconnect-status/product-manifest.json +++ b/plugins/kdeconnect-status/product-manifest.json @@ -84,6 +84,14 @@ "mode": "0644", "size": 2322, "install": true + }, + { + "source": "assets/preview-widget.png", + "destination": "assets/preview-widget.png", + "sha256": "4c9acf9e3cd724de9a76756cf14c0e10a04e8d0ca43a851eba67715e6dd79370", + "mode": "0644", + "size": 3059, + "install": false } ] } diff --git a/plugins/registry.json b/plugins/registry.json index 2f969d2d..8aeea9bf 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1167,17 +1167,20 @@ ], "accent": "#8f8378", "surface": "#101010", - "preview": "", - "screenshots": [], + "preview": "assets/preview-widget.png", + "screenshots": [ + "assets/preview-widget.png" + ], "manifest": "product-manifest.json", - "manifestSha256": "3a5f611ccda2d851164db23702981f55738a9bf2435cd7fac70fbdc93bb325cd", + "manifestSha256": "6d5cea25a4e98a70d4554effd640c2752353d4331c93762871ff7fa7150b06fb", "official": false, "tagline": "Live KDE Connect device status on the bar: connection, battery, unread notification count.", - "icon": "", + "icon": "phone_android", "hosts": [ "topbarGlyph" ], - "lastUpdated": "2026-09-24" + "lastUpdated": "2026-09-24", + "upstream": "https://github.com/vampireJSV/ryoku-plugin-kdeconnect-status" } ] }