diff --git a/plugins/power-button/.gitignore b/plugins/power-button/.gitignore new file mode 100644 index 0000000..ce56ced --- /dev/null +++ b/plugins/power-button/.gitignore @@ -0,0 +1,31 @@ +# OS / editor +.DS_Store +Thumbs.db +*.swp +*~ +.idea/ +.vscode/ +*.code-workspace + +# Logs / temp +*.log +*.tmp +*.bak +.cache/ + +# Secrets / local env (never commit) +.env +.env.* +!.env.example +*.pem +*.key +credentials.json +secrets.json +*_secret* +*_token* + +# Build / package artifacts +dist/ +build/ +*.qmlc +*.jsc diff --git a/plugins/power-button/AGENTS.md b/plugins/power-button/AGENTS.md new file mode 100644 index 0000000..0f53d86 --- /dev/null +++ b/plugins/power-button/AGENTS.md @@ -0,0 +1,84 @@ +# Agent guide for the power-button 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. Public / canonical source +is https://github.com/leandrofelix-dev/power-button ; local authoring clone is +`~/Work/power-button` (not the store install root). Never write into +`~/.local/share/ryoku/plugins/` (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/power-button/CLAUDE.md b/plugins/power-button/CLAUDE.md new file mode 100644 index 0000000..3c35c32 --- /dev/null +++ b/plugins/power-button/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/power-button/LICENSE b/plugins/power-button/LICENSE new file mode 100644 index 0000000..de70bac --- /dev/null +++ b/plugins/power-button/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Felix + +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/power-button/README.md b/plugins/power-button/README.md new file mode 100644 index 0000000..71acead --- /dev/null +++ b/plugins/power-button/README.md @@ -0,0 +1,105 @@ +# Session + +Session menu on the QS Bar: shut down, reboot, hibernate, or lock the screen. + +![Session menu open on the QS Bar](assets/preview-widget.png) + +## What it does + +- **Widget** — a power glyph on the QS Bar. A click only opens the session panel; + the glyph never runs an action by itself. +- **Panel** — Shut Down, Restart, Hibernate, Lock screen. Labels follow the + system locale (English source strings; Portuguese in `content/I18n.qml`). + Escape or click outside dismisses via the host `PluginPanel`. +- **Service** — on an explicit panel row tap, runs one of the declared commands + below, then closes the panel. + +No mutation ever happens on a bar-mark click. + +## Install + +From this repository on a Ryoku desktop: + +``` +ryoku plugin validate . +ryoku plugin add . --bar --yes +``` + +Or install from **Ryostore → Plugins** once it is listed. Enable and place it +under **QS Bar Settings → Community**. + +## How it plugs in + +The shell owns the bar host and the panel card. This plugin ships: + +- `service/Main.qml` — headless session actions (`systemctl` / `ryoku-shell`). +- `content/Widget.qml` — the bar glyph (`density: glyph`); click calls + `pluginApi.togglePanel()`. +- `content/Panel.qml` — the session list (`density: full`); rows call the + service, then `pluginApi.closePanel()`. +- `content/I18n.qml` — plugin-local translations (R4 forbids shell I18n). + +`hosts` is `topbarGlyph` only; `defaults.host` is `topbarGlyph`. + +## Settings + +None. `metadata.settings` is empty. + +## What it runs + +Every external command, and exactly when (only after a panel row tap): + +| Command | When | +| --- | --- | +| `systemctl poweroff` | Shut Down | +| `systemctl reboot` | Restart | +| `systemctl hibernate` | Hibernate | +| `ryoku-shell lock` | Lock screen (same path as Super+L / native quick settings) | + +## What it never does + +- No network access. +- No privileged escalation (`pkexec` / `sudo` / `doas` / `su`). +- No writes under the plugin state dir, cache, or elsewhere. +- No compositor-specific tools (`hyprctl`, etc.). + +## Requirements + +- A running Ryoku shell with `Ryoku.PluginKit`. +- `systemctl` and `ryoku-shell` on `PATH` (declared in `dependencies.commands`). + +## Source + +Canonical / public source: +[https://github.com/leandrofelix-dev/power-button](https://github.com/leandrofelix-dev/power-button) + +Local development clone (optional): `~/Work/power-button`. + +## Where it lists + +Community plugin (`official: false`). It appears under **QS Bar Settings > +Community** with the store's community warning, its author, and its switch. + +## Develop + +``` +power-button/ + manifest.json # id, version, hosts, entry points, files + service/Main.qml # main: session actions + content/Widget.qml # bar glyph + content/Panel.qml # session panel + content/I18n.qml # en / pt / pt_BR labels + assets/preview-widget.png # README / Store preview + LICENSE # MIT +``` + +## Maintenance & safety + +Session is a community contribution. Its author maintains and updates it — not +the Ryoku team. Ryostore screens submissions against the plugin rules, but +screening is a safeguard, not a guarantee: review what a plugin does before +enabling it. + +## Credits + +Felix \. MIT licensed. See [LICENSE](LICENSE). diff --git a/plugins/power-button/assets/preview-widget.png b/plugins/power-button/assets/preview-widget.png new file mode 100644 index 0000000..4121d66 Binary files /dev/null and b/plugins/power-button/assets/preview-widget.png differ diff --git a/plugins/power-button/content/I18n.qml b/plugins/power-button/content/I18n.qml new file mode 100644 index 0000000..565a92e --- /dev/null +++ b/plugins/power-button/content/I18n.qml @@ -0,0 +1,46 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +// Plugin-local i18n (R4 forbids Ryoku.Ui.Singletons.I18n). Same contract as the +// shell/greeter: English source keys, Qt.locale() for system language, miss → key. +QtObject { + id: i18n + + // Exact regional codes first (pt_BR), then language base (pt), else English. + readonly property string lang: { + var n = Qt.locale().name; // e.g. pt_BR, en_US + if (i18n.catalogs[n]) + return n; + var base = n.split("_")[0]; + return i18n.catalogs[base] ? base : "en"; + } + + // en is implicit (keys are the strings). pt + pt_BR cover Portuguese locales. + readonly property var catalogs: ({ + "pt": { + "Session": "Sessão", + "Shut Down": "Desligar", + "Restart": "Reiniciar", + "Hibernate": "Hibernar", + "Lock screen": "Bloquear o ecrã" + }, + "pt_BR": { + "Session": "Sessão", + "Shut Down": "Desligar", + "Restart": "Reiniciar", + "Hibernate": "Hibernar", + "Lock screen": "Bloquear a tela" + } + }) + + function tr(s) { + if (s === undefined || s === null || s === "") + return s; + var map = i18n.catalogs[i18n.lang]; + if (!map) + return s; + var v = map["" + s]; + return v === undefined ? s : v; + } +} diff --git a/plugins/power-button/content/Panel.qml b/plugins/power-button/content/Panel.qml new file mode 100644 index 0000000..47fff95 --- /dev/null +++ b/plugins/power-button/content/Panel.qml @@ -0,0 +1,154 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Ryoku.PluginKit +import Ryoku.PluginKit.Singletons + +// Session menu. Host PluginPanel anchors the card under the glyph (no placement +// API for a true screen-center modal); Escape / click-outside dismiss via host. +Item { + id: root + + property var pluginApi + property string density: "full" + property real s: 1 + property real widthBudget: 300 + property bool active: false + + readonly property var service: pluginApi ? pluginApi.mainInstance : null + readonly property real w: widthBudget > 0 ? widthBudget : 300 + + implicitWidth: root.w + implicitHeight: col.implicitHeight + + I18n { + id: i18n + } + + function run(action) { + if (!root.service) + return; + if (action === "powerOff") + root.service.powerOff(); + else if (action === "reboot") + root.service.reboot(); + else if (action === "hibernate") + root.service.hibernate(); + else if (action === "lock") + root.service.lock(); + if (root.pluginApi) + root.pluginApi.closePanel(); + } + + // Full-width action row: GlyphIcon + label (native session glyphs). + component ActionRow: Rectangle { + id: row + property string label: "" + property string icon: "" + property bool danger: false + signal tapped() + + width: parent ? parent.width : 0 + implicitHeight: 34 * root.s + radius: 0 + color: rowHover.hovered + ? (row.danger ? Qt.rgba(Theme.sun.r, Theme.sun.g, Theme.sun.b, 0.14) : Theme.sheen) + : "transparent" + border.width: 1 + border.color: row.danger + ? (rowHover.hovered ? Theme.sun : Theme.lineStrong) + : (rowHover.hovered ? Theme.lineStrong : Theme.border) + Behavior on color { ColorAnimation { duration: 120 } } + Behavior on border.color { ColorAnimation { duration: 120 } } + + Row { + anchors.left: parent.left + anchors.leftMargin: 12 * root.s + anchors.verticalCenter: parent.verticalCenter + spacing: 10 * root.s + + GlyphIcon { + anchors.verticalCenter: parent.verticalCenter + width: 15 * root.s + height: 15 * root.s + name: row.icon + color: row.danger ? Theme.sun : (rowHover.hovered ? Theme.cream : Theme.iconDim) + stroke: 1.8 + Behavior on color { ColorAnimation { duration: 120 } } + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: row.label + color: row.danger ? Theme.sun : Theme.cream + font.family: Theme.mono + font.pixelSize: 11 * root.s + font.weight: Font.DemiBold + font.letterSpacing: 1.2 * root.s + } + } + + HoverHandler { + id: rowHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: row.tapped() + } + } + + Column { + id: col + width: root.w + spacing: 12 * root.s + + Text { + width: parent.width + text: i18n.tr("Session") + color: Theme.cream + font.family: Theme.mono + font.pixelSize: 13 * root.s + font.weight: Font.DemiBold + font.letterSpacing: 2.4 * root.s + font.capitalization: Font.AllUppercase + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.hair + } + + Column { + width: parent.width + spacing: 6 * root.s + + ActionRow { + label: i18n.tr("Shut Down") + icon: "shutdown" + danger: true + onTapped: root.run("powerOff") + } + + ActionRow { + label: i18n.tr("Restart") + icon: "reboot" + onTapped: root.run("reboot") + } + + ActionRow { + // GlyphKit has no "hibernate"; "suspend" is the sleep/moon mark. + label: i18n.tr("Hibernate") + icon: "suspend" + onTapped: root.run("hibernate") + } + + ActionRow { + label: i18n.tr("Lock screen") + icon: "lock" + onTapped: root.run("lock") + } + } + } +} diff --git a/plugins/power-button/content/Widget.qml b/plugins/power-button/content/Widget.qml new file mode 100644 index 0000000..399d4d9 --- /dev/null +++ b/plugins/power-button/content/Widget.qml @@ -0,0 +1,54 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Ryoku.PluginKit +import Ryoku.PluginKit.Singletons + +// Bar glyph. Click opens the session menu panel (never runs an action directly). +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 bool panelOpen: pluginApi ? pluginApi.panelOpen : false + readonly property bool lit: root.panelOpen || hover.hovered + + implicitWidth: 22 * root.s + implicitHeight: 22 * root.s + + // Quiet hover plate — same dossier language as native pill glyphs. + Rectangle { + anchors.centerIn: parent + width: 20 * root.s + height: 20 * root.s + radius: 0 + color: root.lit ? Theme.sheen : "transparent" + border.width: root.panelOpen ? 1 : 0 + border.color: Theme.hair + Behavior on color { ColorAnimation { duration: 120 } } + } + + GlyphIcon { + anchors.centerIn: parent + width: 15 * root.s + height: 15 * root.s + name: "shutdown" + color: root.panelOpen ? Theme.accent : (hover.hovered ? Theme.cream : Theme.iconDim) + stroke: 1.8 + Behavior on color { ColorAnimation { duration: 120 } } + } + + HoverHandler { + id: hover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: if (root.pluginApi) root.pluginApi.togglePanel() + } +} diff --git a/plugins/power-button/manifest.json b/plugins/power-button/manifest.json new file mode 100644 index 0000000..00ec891 --- /dev/null +++ b/plugins/power-button/manifest.json @@ -0,0 +1,49 @@ +{ + "id": "power-button", + "name": "Session", + "version": "0.2.2", + "author": "Felix ", + "description": "Session menu on the QS Bar: shut down, reboot, hibernate, or lock via ryoku-shell.", + "license": "MIT", + "official": false, + "tags": [ + "session", + "power", + "bar-widget" + ], + "hosts": [ + "topbarGlyph" + ], + "entryPoints": { + "main": "service/Main.qml", + "content": "content/Widget.qml", + "panel": "content/Panel.qml" + }, + "panel": { + "width": 300 + }, + "files": [ + "content/I18n.qml", + "assets/preview-widget.png" + ], + "capabilities": { + "densities": [ + "glyph", + "full" + ] + }, + "defaults": { + "host": "topbarGlyph", + "icon": "power_settings_new", + "label": "Session" + }, + "dependencies": { + "commands": [ + "systemctl", + "ryoku-shell" + ] + }, + "metadata": { + "settings": [] + } +} diff --git a/plugins/power-button/product-manifest.json b/plugins/power-button/product-manifest.json new file mode 100644 index 0000000..1e0ccbc --- /dev/null +++ b/plugins/power-button/product-manifest.json @@ -0,0 +1,97 @@ +{ + "schema": 1, + "id": "power-button", + "category": "plugins", + "version": "0.2.2", + "destination": "ryoku/plugins/power-button", + "files": [ + { + "source": ".gitignore", + "destination": ".gitignore", + "sha256": "38496ccd4fe21653a7f478ab5027836ac107b495b96bf6eb34540454d960f5cd", + "mode": "0644", + "size": 297, + "install": true + }, + { + "source": "AGENTS.md", + "destination": "AGENTS.md", + "sha256": "c049ede874ffaf5033d7e4c2d64b6074d63fd50da78b55e40b7ae19120bba0fa", + "mode": "0644", + "size": 3950, + "install": true + }, + { + "source": "CLAUDE.md", + "destination": "CLAUDE.md", + "sha256": "09ffb8ebdb8adf50bf4bde26dfe2109b4af3a4a8cc3f30cc536be42bb912c033", + "mode": "0644", + "size": 152, + "install": true + }, + { + "source": "LICENSE", + "destination": "LICENSE", + "sha256": "97483adbcfff65229fdf21ea76178690f1bfe3ccd5fbde97e344e04d625dc4bd", + "mode": "0644", + "size": 1062, + "install": false + }, + { + "source": "README.md", + "destination": "README.md", + "sha256": "44c96615c4e8f6a98a51108d049c6b0e0165d932056ec733d288819594cc2701", + "mode": "0644", + "size": 3289, + "install": false + }, + { + "source": "assets/preview-widget.png", + "destination": "assets/preview-widget.png", + "sha256": "f69e09d776b7243b3a2fec7c7ac5f607db53953a03f571bf694b6babe044bc39", + "mode": "0644", + "size": 27366, + "install": false + }, + { + "source": "content/I18n.qml", + "destination": "content/I18n.qml", + "sha256": "44f9942a4a808c359abcf7c6c2f94c5ef88a9beab7016bc72fe572455fac7ef0", + "mode": "0644", + "size": 1384, + "install": true + }, + { + "source": "content/Panel.qml", + "destination": "content/Panel.qml", + "sha256": "8fb79dd618ca787eed13cd69e1aa3b490e138e013a6198ed61fd74cf4a5139f1", + "mode": "0644", + "size": 4481, + "install": true + }, + { + "source": "content/Widget.qml", + "destination": "content/Widget.qml", + "sha256": "55c4f2091f0eabae0bde763ffbee1b1c714b82ed123fefefa64643b597b52308", + "mode": "0644", + "size": 1482, + "install": true + }, + { + "source": "manifest.json", + "destination": "manifest.json", + "sha256": "4a8162e52d522f00dabf5ee48e97d97420e474d950f97e9b2842d65714cbdc25", + "mode": "0644", + "size": 904, + "install": true + }, + { + "source": "service/Main.qml", + "destination": "service/Main.qml", + "sha256": "743bb125c13a59d1a1f4ee62a1dc3f40bf1c1bf9991f8d1de9caded0b69729bb", + "mode": "0644", + "size": 626, + "install": true + } + ] +} diff --git a/plugins/power-button/service/Main.qml b/plugins/power-button/service/Main.qml new file mode 100644 index 0000000..bfed65e --- /dev/null +++ b/plugins/power-button/service/Main.qml @@ -0,0 +1,28 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +// Session power actions. No UI here — the panel lists choices, then calls these. +Item { + id: svc + + property var pluginApi + + function powerOff() { + Quickshell.execDetached(["systemctl", "poweroff"]); + } + + function reboot() { + Quickshell.execDetached(["systemctl", "reboot"]); + } + + function hibernate() { + Quickshell.execDetached(["systemctl", "hibernate"]); + } + + // Same path as QS Bar / Super+L: ryoku-shell → qylock. + function lock() { + Quickshell.execDetached(["ryoku-shell", "lock"]); + } +} diff --git a/plugins/registry.json b/plugins/registry.json index ee1011f..0aebe91 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1651,6 +1651,36 @@ "topbarGlyph" ], "lastUpdated": "2026-10-07" + }, + { + "id": "power-button", + "name": "Session", + "version": "0.2.2", + "path": "plugins/power-button", + "author": "Felix ", + "upstream": "https://github.com/leandrofelix-dev/power-button", + "summary": "Session menu on the QS Bar: shut down, reboot, hibernate, or lock via ryoku-shell.", + "description": "Session menu on the QS Bar: shut down, reboot, hibernate, or lock via ryoku-shell.", + "tags": [ + "session", + "power", + "bar-widget" + ], + "accent": "#8f8378", + "surface": "#101010", + "preview": "assets/preview-widget.png", + "screenshots": [ + "assets/preview-widget.png" + ], + "manifest": "product-manifest.json", + "manifestSha256": "b8460b8499a0c361bc043a838cdf8255a474ae91b1073a9a9cfe7dfb5327ff96", + "official": false, + "tagline": "Session menu on the QS Bar: shut down, reboot, hibernate, or lock via ryoku-shell.", + "icon": "power_settings_new", + "hosts": [ + "topbarGlyph" + ], + "lastUpdated": "2026-10-08" } ] }