Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/kdeconnect-status/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
*.log
*.tmp
84 changes: 84 additions & 0 deletions plugins/kdeconnect-status/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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/<id>/` (fallback `~/ryoku-plugins/<id>/`);
`ryoku plugin new <id>` 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/<id>`), `$XDG_CACHE_HOME/ryoku/plugins/<id>`, 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 <mail>`. README says what runs, what it reads, what it writes, and every
privileged/network capability.

## The workflow

1. `ryoku plugin new <id>` 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 <dir>` and fix every blocking finding (warnings are
advisory but read them).
4. `ryoku plugin add <dir> --bar --yes` to install it, then verify it on the bar
and under **QS Bar Settings > Community**.
5. `ryoku plugin share <id>` 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/<id>`; 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()`.
4 changes: 4 additions & 0 deletions plugins/kdeconnect-status/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions plugins/kdeconnect-status/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
70 changes: 70 additions & 0 deletions plugins/kdeconnect-status/README.md
Original file line number Diff line number Diff line change
@@ -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 <vampirejsv@local>: this plugin is community-made (`official` is false).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions plugins/kdeconnect-status/bin/poll.sh
Original file line number Diff line number Diff line change
@@ -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}'
86 changes: 86 additions & 0 deletions plugins/kdeconnect-status/content/Panel.qml
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Loading