Port the AGS widgets into the shell as plugins

This commit is contained in:
2026-09-20 00:09:41 -04:00
parent c5434026a8
commit 0c422e7345
31 changed files with 1740 additions and 17 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
dynamic_theme_dir="$HOME/.config/blob/themes/blob-dynamic" dynamic_theme_dir="$HOME/.config/blob/themes/blob-dynamic"
if [ ! -f "$dynamic_theme_dir/colors.toml" ]; then if [ ! -f "$dynamic_theme_dir/colors.toml" ]; then
echo "No dynamic theme yet. Pick a wallpaper first: blob-bg-menu" >&2 echo "No dynamic theme yet. Pick a wallpaper first: blob-wallpaper-set --menu" >&2
exit 1 exit 1
fi fi
+2 -2
View File
@@ -128,7 +128,7 @@ case "${1:-toggle}" in
select_mode all select_mode all
;; ;;
set) set)
[ -n "$2" ] || { echo "Usage: blob-toggle-tablet-follow set <monitor|follow|all>" >&2; exit 1; } [ -n "$2" ] || { echo "Usage: blob-toggle-tablet set <monitor|follow|all>" >&2; exit 1; }
select_mode "$2" select_mode "$2"
;; ;;
status|--status) status|--status)
@@ -141,7 +141,7 @@ case "${1:-toggle}" in
[ "$(selected_mode)" = "all" ] || start_daemon [ "$(selected_mode)" = "all" ] || start_daemon
;; ;;
*) *)
echo "Usage: blob-toggle-tablet-follow [toggle|on|off|set <monitor|follow|all>|status|monitors|autostart]" >&2 echo "Usage: blob-toggle-tablet [toggle|on|off|set <monitor|follow|all>|status|monitors|autostart]" >&2
exit 1 exit 1
;; ;;
esac esac
+1 -14
View File
@@ -19,21 +19,8 @@ mkdir -p "$THEME_DIR/backgrounds"
echo "PATH=$PATH" echo "PATH=$PATH"
} >> "$HOME/.cache/blob-wallpaper.log" } >> "$HOME/.cache/blob-wallpaper.log"
# Rows for blob-menu-select, as "<label><TAB><path>". The menu returns the
# label and the subtext, so the full path comes back as a stable key.
list_wallpaper_rows() {
find "$WALLPAPER_DIR" -maxdepth 1 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \) \
2>/dev/null | sort | while read -r background; do
name=$(basename "$background")
label=$(echo "${name%.*}" | sed -E 's/^[0-9]+//; s/^[-_]//; s/[-_]/ /g; s/(^| )([a-z])/\1\u\2/g')
printf '%s\t%s\n' "$label" "$background"
done
}
if [ -z "$1" ] || [ "$1" = "--menu" ]; then if [ -z "$1" ] || [ "$1" = "--menu" ]; then
selection=$(list_wallpaper_rows | blob-menu-select "Select Wallpaper" -- --width 800) || exit 0 IMAGE_PATH=$(blob-menu-images --show-labels --filterable "$WALLPAPER_DIR") || exit 0
IMAGE_PATH=$(printf '%s' "$selection" | cut -f2)
if [ -z "$IMAGE_PATH" ] || [ ! -f "$IMAGE_PATH" ]; then if [ -z "$IMAGE_PATH" ] || [ ! -f "$IMAGE_PATH" ]; then
exit 0 exit 0
fi fi
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# blob:summary=Print a one-line weather summary for the quick settings card
# blob:hidden=true
LOCATION="Great+Falls,VA"
icon=$(blob-weather-icon 2>/dev/null)
weather=$(curl -fsS --max-time 4 "https://wttr.in/${LOCATION}?format=%l|%t|%w" 2>/dev/null | tr -d '\n')
if [[ -z $weather ]]; then
echo "Weather unavailable"
exit 1
fi
IFS='|' read -r place temperature wind <<< "$weather"
place=${place%%,*}
temperature=${temperature#+}
echo "$icon $place · Temp $temperature · Wind $wind"
+54
View File
@@ -0,0 +1,54 @@
# Ported widgets
The GTK widgets that used to run as a separate AGS process are now plugins
inside the shell. A panel opens over IPC into a process that is already running
rather than starting a second toolkit, and `ags` is no longer installed or
autostarted.
## Where each one went
| Was | Is | Note |
| --- | --- | --- |
| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 9 files, none over 250 lines |
| `SysMonitor.tsx` (142) | `shell/plugins/panels/sysmon/` | CPU, memory, disk, temperature |
| `Notifications.tsx` (160) | `shell/plugins/notification-center/` | reads the notification service's own history files |
| `WallPicker.tsx` (106) | the existing image picker | `blob-wallpaper-set --menu` now calls `blob-menu-images`, so wallpapers get the image grid instead of a text list |
| `ThemePicker.tsx` (178) | the menu and `blob-theme-switcher` | the shell already had a theme switcher; a second picker was duplicate surface |
| `ClaudeUsage.tsx` (372) | dropped | it existed to read agent usage records, and the agents feature is gone |
| `Media.tsx` (104) | dropped | `SHOW_DESKTOP_MEDIA` was already false, and the quick settings media row covers it |
| `WidgetCard.tsx` (22) | `Ui/Card.qml`, `Ui/CardHeader.qml`, `Ui/StatRow.qml` | shared, so no plugin repeats the card |
| `style.css` (759) | `Commons/Style.qml` tokens | see below |
## The look
`ags/style.css` was the visual identity worth keeping: square corners, a 2px
border, and an `alpha(background, 0.6)` fill. Those are now named tokens on
`Style` - `cardBorderWidth`, `cardRadius`, `cardFillAlpha`, `cardBorderAlpha`,
`panelFillAlpha` - so every ported panel reads the same numbers instead of
repeating literals.
The CSS was written against pywal's `@color0`, `@color4` and `@color5` aliases.
`blob-theme-color` resolves `color4` to blue and `color5` to magenta, so
`Commons/Color.qml` now exposes `blue` and `magenta` with that same cascade,
falling back to `accent` for themes that define neither. `color0` is
`background`, which Color already had.
## Opening them
| Panel | Opened by |
| --- | --- |
| Quick settings | left-click the bar clock, or `Super + Ctrl + Q` |
| Notification centre | left-click the bar bell |
| System monitor | `Super + Ctrl + M` |
Both new bar click actions are already in `shell.json`. The system monitor had no
bar entry point in the AGS setup either, so the keybinding is how it is reached.
## Deliberately not ported
The tiles still poll shell commands the way the GTK widgets did - `pamixer` for
volume, `brightnessctl` for brightness, `playerctl` for media - rather than
binding to the shell's own audio, brightness and media services. Polling only
runs while a panel is open, so the cost is bounded, but wiring these to the
services is the obvious next refinement and would remove the last per-tick
process spawns.
+2
View File
@@ -2,6 +2,8 @@
-- See current bindings and descriptions: blob menu keybindings --print -- See current bindings and descriptions: blob menu keybindings --print
o.bind("SUPER + ALT + W", "Wallpaper picker", "blob-wallpaper-set --menu") o.bind("SUPER + ALT + W", "Wallpaper picker", "blob-wallpaper-set --menu")
o.bind("SUPER + CTRL + M", "System monitor", "blob-shell shell toggle blob.sysmon")
o.bind("SUPER + CTRL + Q", "Quick settings", "blob-shell shell toggle blob.quick-settings")
-- Apps on SUPER + SPACE, root menu on ALT, swapping the shipped defaults back. -- Apps on SUPER + SPACE, root menu on ALT, swapping the shipped defaults back.
hl.unbind("SUPER + SPACE") hl.unbind("SUPER + SPACE")
+16
View File
@@ -21,6 +21,8 @@ QtObject {
property color accent: "#cacccc" property color accent: "#cacccc"
property color urgent: "#a55555" property color urgent: "#a55555"
property color muted: "#707880" property color muted: "#707880"
property color blue: "#7aa2f7"
property color magenta: "#ad8ee6"
// Flat dictionary of "section.key" -> raw string from shell.toml. // Flat dictionary of "section.key" -> raw string from shell.toml.
// Reassigning this whole property is what makes surface bindings below // Reassigning this whole property is what makes surface bindings below
@@ -142,6 +144,8 @@ QtObject {
var color4Value = "" var color4Value = ""
var color7Value = "" var color7Value = ""
var color8Value = "" var color8Value = ""
var blueValue = ""
var magentaValue = ""
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/) var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/)
if (!match) continue if (!match) continue
@@ -157,11 +161,23 @@ QtObject {
else if (match[1] === "color7") color7Value = match[2] else if (match[1] === "color7") color7Value = match[2]
else if (match[1] === "color8") color8Value = match[2] else if (match[1] === "color8") color8Value = match[2]
else if (match[1] === "red" || match[1] === "color1") urgent = match[2] else if (match[1] === "red" || match[1] === "color1") urgent = match[2]
else if (match[1] === "blue") blueValue = match[2]
else if (match[1] === "magenta" || match[1] === "purple" || match[1] === "color5") magentaValue = match[2]
} }
if (!loadedBackground && color0Value.length > 0) background = color0Value if (!loadedBackground && color0Value.length > 0) background = color0Value
if (!loadedForeground && color7Value.length > 0) foreground = color7Value if (!loadedForeground && color7Value.length > 0) foreground = color7Value
if (!foundAccent && color4Value.length > 0) accent = color4Value if (!foundAccent && color4Value.length > 0) accent = color4Value
if (!foundMuted) muted = color8Value.length > 0 ? color8Value : foreground if (!foundMuted) muted = color8Value.length > 0 ? color8Value : foreground
// color4/color5 are the pywal aliases the widgets were designed against;
// blob-theme-color resolves color4 to blue and color5 to magenta, so the
// same cascade is applied here rather than a second convention.
if (blueValue.length > 0) blue = blueValue
else if (color4Value.length > 0) blue = color4Value
else blue = accent
if (magentaValue.length > 0) magenta = magentaValue
else magenta = accent
} }
// Last theme-supplied and user-supplied shell.toml dicts, kept separate so // Last theme-supplied and user-supplied shell.toml dicts, kept separate so
+11
View File
@@ -508,6 +508,17 @@ QtObject {
onLoadFailed: refreshTimer.restart() onLoadFailed: refreshTimer.restart()
} }
// Card geometry for the ported widget panels. Square corners, a 2px border at
// half alpha on the blue slot, and a 0.6 fill: the look the GTK widgets had.
readonly property int cardBorderWidth: 2
readonly property int cardRadius: 0
readonly property real cardFillAlpha: 0.6
readonly property real cardBorderAlpha: 0.5
readonly property real panelFillAlpha: 0.92
readonly property int cardPadding: spaceReal(14)
readonly property int cardSpacing: spaceReal(10)
readonly property int tilePadding: spaceReal(8)
Component.onCompleted: { Component.onCompleted: {
refresh() refresh()
resolveFontFamily() resolveFontFamily()
+26
View File
@@ -0,0 +1,26 @@
import QtQuick
import qs.Commons
Rectangle {
id: root
property color fillColor: Util.alpha(Color.background, Style.cardFillAlpha)
property color borderColor: Util.alpha(Color.blue, Style.cardBorderAlpha)
default property alias content: contentColumn.data
property alias spacing: contentColumn.spacing
property int padding: Style.cardPadding
color: fillColor
radius: Style.cardRadius
border.width: Style.cardBorderWidth
border.color: borderColor
implicitWidth: contentColumn.implicitWidth + padding * 2
implicitHeight: contentColumn.implicitHeight + padding * 2
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: root.padding
spacing: Style.cardSpacing
}
}
+38
View File
@@ -0,0 +1,38 @@
import QtQuick
import qs.Commons
Row {
id: root
property string icon: ""
property string title: ""
spacing: Style.spaceReal(10)
Rectangle {
width: Style.spaceReal(28)
height: width
radius: Style.cardRadius
color: Util.alpha(Color.blue, 0.2)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: root.icon
textFormat: Text.PlainText
color: Color.blue
font.family: Style.font.family
font.pixelSize: Style.font.body
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.title
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
}
+34
View File
@@ -0,0 +1,34 @@
import QtQuick
import qs.Commons
Rectangle {
id: root
property string icon: ""
property bool active: false
signal activated()
implicitWidth: Style.spaceReal(26)
implicitHeight: Style.spaceReal(24)
radius: Style.cardRadius
color: root.active
? Color.accent
: (hover.hovered ? Util.alpha(Color.blue, 0.25) : "transparent")
Text {
anchors.centerIn: parent
text: root.icon
textFormat: Text.PlainText
color: root.active ? Color.background : (hover.hovered ? Color.blue : Color.magenta)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
HoverHandler {
id: hover
}
TapHandler {
onTapped: root.activated()
}
}
+47
View File
@@ -0,0 +1,47 @@
import QtQuick
import qs.Commons
Item {
id: root
property string icon: ""
property string label: ""
property string value: ""
implicitHeight: Math.max(iconText.implicitHeight, labelText.implicitHeight)
Text {
id: iconText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Style.spaceReal(22)
text: root.icon
textFormat: Text.PlainText
color: Color.magenta
font.family: Style.font.family
font.pixelSize: Style.font.body
}
Text {
id: labelText
anchors.left: iconText.right
anchors.leftMargin: Style.spaceReal(8)
anchors.verticalCenter: parent.verticalCenter
text: root.label
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: root.value
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
}
+4
View File
@@ -33,3 +33,7 @@ TextField 1.0 TextField.qml
Toggle 1.0 Toggle.qml Toggle 1.0 Toggle.qml
ToggleSwitch 1.0 ToggleSwitch.qml ToggleSwitch 1.0 ToggleSwitch.qml
WidgetButton 1.0 WidgetButton.qml WidgetButton 1.0 WidgetButton.qml
Card 1.0 Card.qml
CardHeader 1.0 CardHeader.qml
StatRow 1.0 StatRow.qml
CardIconButton 1.0 CardIconButton.qml
@@ -0,0 +1,27 @@
.pragma library
// Each history file is one line of JSON, so the reader concatenates the
// directory and parses line by line. Newest first, by timestamp.
function parseHistory(raw) {
var lines = String(raw || "").split("\n")
var entries = []
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (!line) continue
try {
var value = JSON.parse(line)
if (!value || typeof value !== "object") continue
entries.push({
id: value.id || 0,
app: value.app || "",
summary: value.summary || "",
body: value.body || "",
timestamp: Number(value.timestamp) || 0
})
} catch (e) {
continue
}
}
entries.sort(function(a, b) { return b.timestamp - a.timestamp })
return entries
}
@@ -0,0 +1,198 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Ui
import "HistoryModel.js" as HistoryModel
Item {
id: root
property var shell: null
property var manifest: null
property bool opened: false
readonly property string pluginId: (manifest && manifest.id) || "blob.notification-center"
readonly property string historyDir: Quickshell.env("HOME") + "/.local/state/blob/notifications/history"
property var entries: []
property bool doNotDisturb: false
readonly property bool empty: root.entries.length === 0
function open(payloadJson) {
root.opened = true
root.refresh()
}
function close() {
root.opened = false
}
function dismiss() {
root.opened = false
if (root.shell && typeof root.shell.hide === "function")
root.shell.hide(root.pluginId)
}
function refresh() {
if (!historyProbe.running) historyProbe.running = true
if (!dndProbe.running) dndProbe.running = true
}
function run(command) {
Quickshell.execDetached(["bash", "-c", command])
}
function clearAll() {
root.run("blob-shell notifications clearHistory")
root.entries = []
}
function toggleSilence() {
root.run("blob-shell notifications toggleDnd")
refreshTimer.restart()
}
function dismissEntry(index) {
var entry = root.entries[index]
if (!entry) return
root.run("blob-notify-dismiss " + entry.id)
var next = []
for (var i = 0; i < root.entries.length; i++)
if (i !== index) next.push(root.entries[i])
root.entries = next
}
Process {
id: historyProbe
command: ["bash", "-c", "cat " + root.historyDir + "/*.json 2>/dev/null || true"]
stdout: StdioCollector {
onStreamFinished: root.entries = HistoryModel.parseHistory(text)
}
}
Process {
id: dndProbe
command: ["bash", "-c", "blob-shell notifications dndState 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: root.doNotDisturb = String(text).trim() === "on"
}
}
Timer {
id: refreshTimer
interval: 250
onTriggered: root.refresh()
}
Timer {
interval: 5000
running: root.opened
repeat: true
onTriggered: root.refresh()
}
PanelWindow {
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "blob-notification-center"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Card {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Style.spaceReal(10)
anchors.rightMargin: Style.spaceReal(10)
implicitWidth: Style.spaceReal(440)
implicitHeight: Style.spaceReal(420)
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
spacing: Style.spaceReal(10)
Item {
width: parent.width
implicitHeight: titleText.implicitHeight
Text {
id: titleText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Notifications"
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(4)
CardIconButton {
icon: root.doNotDisturb ? "" : ""
active: root.doNotDisturb
onActivated: root.toggleSilence()
}
CardIconButton {
icon: ""
onActivated: root.clearAll()
}
}
}
ListView {
width: parent.width
height: Style.spaceReal(340)
clip: true
spacing: Style.spaceReal(8)
visible: !root.empty
model: root.entries
delegate: NotificationItem {
required property var modelData
required property int index
width: ListView.view.width
app: modelData.app
summary: modelData.summary
body: modelData.body
onDismissRequested: root.dismissEntry(index)
}
}
Column {
width: parent.width
visible: root.empty
spacing: Style.spaceReal(8)
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: ""
textFormat: Text.PlainText
color: Util.alpha(Color.blue, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.heading
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "No notifications"
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
}
}
}
@@ -0,0 +1,91 @@
import QtQuick
import qs.Commons
Rectangle {
id: root
property string app: ""
property string summary: ""
property string body: ""
signal dismissRequested()
radius: Style.cardRadius
color: Util.alpha(Color.background, Style.cardFillAlpha)
implicitHeight: layout.implicitHeight + Style.spaceReal(20)
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: Style.spaceReal(3)
color: Color.accent
}
Column {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Style.spaceReal(12)
anchors.rightMargin: Style.spaceReal(12)
anchors.topMargin: Style.spaceReal(10)
spacing: Style.spaceReal(2)
Item {
width: parent.width
implicitHeight: appText.implicitHeight
Text {
id: appText
anchors.left: parent.left
text: root.app.length > 0 ? root.app : "Notification"
textFormat: Text.PlainText
color: Color.accent
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Text {
anchors.right: parent.right
text: ""
textFormat: Text.PlainText
color: closeHover.hovered ? Color.urgent : Color.magenta
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
HoverHandler {
id: closeHover
}
TapHandler {
onTapped: root.dismissRequested()
}
}
}
Text {
width: parent.width
text: root.summary
textFormat: Text.PlainText
wrapMode: Text.WordWrap
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Text {
width: parent.width
visible: root.body.length > 0
text: root.body
textFormat: Text.PlainText
wrapMode: Text.WordWrap
maximumLineCount: 4
elide: Text.ElideRight
color: Util.alpha(Color.foreground, 0.8)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
}
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"name": "Notification centre",
"version": "1.0.0",
"author": "Blob",
"description": "List of recent notifications with clear-all and silence controls",
"id": "blob.notification-center",
"kinds": [
"overlay"
],
"keepLoaded": true,
"entryPoints": {
"overlay": "NotificationCenter.qml"
}
}
@@ -0,0 +1,115 @@
import QtQuick
import Quickshell.Io
import qs.Commons
import qs.Ui
Item {
id: root
property int monthOffset: 0
property string monthName: ""
property string monthGrid: ""
property bool polling: false
implicitHeight: layout.implicitHeight
function loadMonth() {
if (monthProbe.running) return
monthProbe.running = true
}
function shiftMonth(delta) {
root.monthOffset += delta
root.loadMonth()
}
function resetMonth() {
root.monthOffset = 0
root.loadMonth()
}
Process {
id: monthProbe
command: ["bash", "-c",
'first="$(date +%Y-%m-01) ' + root.monthOffset + ' months"; ' +
'date -d "$first" "+%B %Y"; cal $(date -d "$first" "+%m %Y") | sed "1d"']
stdout: StdioCollector {
onStreamFinished: {
var lines = String(text).replace(/\s+$/, "").split("\n")
root.monthName = (lines[0] || "").trim()
root.monthGrid = lines.slice(1).join("\n")
}
}
}
Timer {
interval: 3600000
running: root.polling
repeat: true
onTriggered: if (root.monthOffset === 0) root.loadMonth()
}
Component.onCompleted: root.loadMonth()
WheelHandler {
onWheel: function(event) {
root.shiftMonth(event.angleDelta.y > 0 ? -1 : 1)
}
}
Column {
id: layout
width: parent.width
spacing: Style.spaceReal(6)
Item {
width: parent.width
implicitHeight: Math.max(monthLabel.implicitHeight, navigation.implicitHeight)
Text {
id: monthLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.monthName
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
Row {
id: navigation
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(4)
CardIconButton {
icon: "<"
onActivated: root.shiftMonth(-1)
}
CardIconButton {
icon: "Today"
visible: root.monthOffset !== 0
implicitWidth: Style.spaceReal(44)
onActivated: root.resetMonth()
}
CardIconButton {
icon: ">"
onActivated: root.shiftMonth(1)
}
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.monthGrid
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.85)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
}
@@ -0,0 +1,61 @@
import QtQuick
import qs.Commons
import qs.Ui
Item {
id: root
property string clock: ""
property string today: ""
signal lockRequested()
signal logoutRequested()
signal closeRequested()
implicitHeight: Math.max(dateTime.implicitHeight, buttons.implicitHeight)
Column {
id: dateTime
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(2)
Text {
text: root.clock
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.heading
font.bold: true
}
Text {
text: root.today
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.75)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
Row {
id: buttons
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(4)
CardIconButton {
icon: ""
onActivated: root.lockRequested()
}
CardIconButton {
icon: ""
onActivated: root.logoutRequested()
}
CardIconButton {
icon: ""
onActivated: root.closeRequested()
}
}
}
@@ -0,0 +1,69 @@
import QtQuick
import qs.Commons
import qs.Ui
Item {
id: root
property string title: ""
property string artist: ""
property string status: "Stopped"
readonly property bool playing: root.status === "Playing"
signal controlRequested(string action)
visible: root.title.length > 0 && root.title !== "No Media"
implicitHeight: visible ? Math.max(info.implicitHeight, controls.implicitHeight) : 0
Column {
id: info
anchors.left: parent.left
anchors.right: controls.left
anchors.rightMargin: Style.spaceReal(12)
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(2)
Text {
width: parent.width
text: root.title
textFormat: Text.PlainText
elide: Text.ElideRight
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Text {
width: parent.width
text: root.artist
textFormat: Text.PlainText
elide: Text.ElideRight
visible: root.artist.length > 0
color: Util.alpha(Color.foreground, 0.75)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
Row {
id: controls
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(8)
CardIconButton {
icon: "⏮"
onActivated: root.controlRequested("previous")
}
CardIconButton {
icon: root.playing ? "⏸" : "▶"
onActivated: root.controlRequested("play-pause")
}
CardIconButton {
icon: "⏭"
onActivated: root.controlRequested("next")
}
}
}
@@ -0,0 +1,32 @@
import QtQuick
import Quickshell.Io
Item {
id: root
property string command: ""
property int intervalMs: 2000
property bool value: false
property bool polling: false
function refresh() {
if (root.command.length > 0 && !probe.running) probe.running = true
}
Process {
id: probe
command: ["bash", "-c", root.command]
stdout: StdioCollector {
onStreamFinished: root.value = text.trim() === "on"
}
}
Timer {
interval: root.intervalMs
running: root.polling
repeat: true
onTriggered: root.refresh()
}
onPollingChanged: if (root.polling) root.refresh()
}
@@ -0,0 +1,32 @@
import QtQuick
import Quickshell.Io
Item {
id: root
property string command: ""
property int intervalMs: 1000
property string value: ""
property bool polling: false
function refresh() {
if (root.command.length > 0 && !probe.running) probe.running = true
}
Process {
id: probe
command: ["bash", "-c", root.command]
stdout: StdioCollector {
onStreamFinished: root.value = text.trim()
}
}
Timer {
interval: root.intervalMs
running: root.polling
repeat: true
onTriggered: root.refresh()
}
onPollingChanged: if (root.polling) root.refresh()
}
@@ -0,0 +1,271 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Ui
Item {
id: root
property string blobPath: Quickshell.env("BLOB_PATH")
property var shell: null
property var manifest: null
property bool opened: false
readonly property string pluginId: (manifest && manifest.id) || "blob.quick-settings"
function open(payloadJson) {
root.opened = true
}
function close() {
root.opened = false
}
function dismiss() {
root.opened = false
if (root.shell && typeof root.shell.hide === "function")
root.shell.hide(root.pluginId)
}
function run(command) {
Quickshell.execDetached(["bash", "-c", command])
}
function runAndDismiss(command) {
root.run(command)
root.dismiss()
}
function openPanel(pluginId) {
root.dismiss()
if (root.shell && typeof root.shell.summon === "function") root.shell.summon(pluginId, "")
else root.run("blob-shell shell summon " + pluginId)
}
function refreshState() {
bluetoothFlag.refresh()
dndFlag.refresh()
nightLightFlag.refresh()
stayAwakeFlag.refresh()
tabletFlag.refresh()
recordingFlag.refresh()
volumeValue.refresh()
mutedFlag.refresh()
brightnessValue.refresh()
mediaValue.refresh()
}
function toggleAndRefresh(command) {
root.run(command)
stateSettleTimer.restart()
}
Timer {
id: stateSettleTimer
interval: 250
onTriggered: root.refreshState()
}
PolledFlag {
id: bluetoothFlag
polling: root.opened
command: "bluetoothctl show 2>/dev/null | grep -q 'Powered: yes' && echo on || echo off"
}
PolledFlag {
id: dndFlag
polling: root.opened
command: "blob-shell notifications dndState 2>/dev/null"
}
PolledFlag {
id: nightLightFlag
polling: root.opened
command: "blob-toggle-nightlight --status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
}
PolledFlag {
id: stayAwakeFlag
polling: root.opened
command: "blob-toggle-idle status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
}
PolledFlag {
id: tabletFlag
polling: root.opened
command: "blob-toggle-tablet status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
}
PolledFlag {
id: recordingFlag
polling: root.opened
command: "pgrep -f '^gpu-screen-recorder' >/dev/null && echo on || echo off"
}
PolledFlag {
id: mutedFlag
polling: root.opened
intervalMs: 1000
command: "pamixer --get-mute 2>/dev/null | grep -qx true && echo on || echo off"
}
PolledText {
id: volumeValue
polling: root.opened
intervalMs: 1000
command: "pamixer --get-volume 2>/dev/null || echo 0"
}
PolledText {
id: brightnessValue
polling: root.opened
intervalMs: 2000
command: "brightnessctl -m 2>/dev/null | cut -d, -f4 | tr -d '%' || echo 0"
}
PolledText {
id: clockValue
polling: root.opened
intervalMs: 1000
command: "date '+%H:%M'"
}
PolledText {
id: dateValue
polling: root.opened
intervalMs: 10000
command: "date '+%A, %B %-d'"
}
PolledText {
id: mediaValue
polling: root.opened
intervalMs: 1000
command: "playerctl metadata -f '{{title}}|||{{artist}}|||{{status}}' 2>/dev/null || echo 'No Media||||||Stopped'"
}
PolledText {
id: weatherValue
polling: root.opened
intervalMs: 600000
command: "blob-weather-card 2>/dev/null"
}
readonly property var mediaParts: String(mediaValue.value).split("|||")
// blob-weather-card prints "<glyph> <place> . Temp <t> . Wind <w>",
// with the separator spelled with a middle dot. Anything shorter than three
// fields is the failure line, which the card shows as unavailable.
readonly property var weatherFields: String(weatherValue.value).split(" \u00b7 ")
readonly property bool weatherAvailable: weatherFields.length >= 3
readonly property var weatherHead: weatherAvailable
? String(weatherFields[0]).match(/^(\S+)\s*(.*)$/) : null
readonly property string weatherGlyph: weatherHead ? weatherHead[1] : ""
readonly property string weatherPlace: weatherHead
? String(weatherHead[2]).trim() : (weatherValue.value.length > 0 ? weatherValue.value : "Loading...")
readonly property string weatherTemperature: weatherAvailable
? String(weatherFields[1]).replace(/^Temp\s*/, "") : ""
readonly property string weatherWind: weatherAvailable
? String(weatherFields[2]).replace(/^Wind\s*/, "") : ""
PanelWindow {
id: panel
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "blob-quick-settings"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
exclusionMode: ExclusionMode.Ignore
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Row {
anchors.top: parent.top
anchors.topMargin: Style.spaceReal(10)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.spaceReal(12)
WeatherCard {
available: root.weatherAvailable
glyph: root.weatherGlyph
place: root.weatherPlace
temperature: root.weatherTemperature
wind: root.weatherWind
}
Card {
id: controlCentre
implicitWidth: Style.spaceReal(400)
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
spacing: Style.spaceReal(12)
Header {
width: parent.width
clock: clockValue.value
today: dateValue.value
onLockRequested: root.runAndDismiss("blob-system-lock")
onLogoutRequested: root.runAndDismiss("blob-system-logout")
onCloseRequested: root.dismiss()
}
Calendar {
width: parent.width
polling: root.opened
}
Tiles {
bluetoothOn: bluetoothFlag.value
doNotDisturb: dndFlag.value
nightLight: nightLightFlag.value
recording: recordingFlag.value
stayAwake: stayAwakeFlag.value
tabletFollow: tabletFlag.value
onRunRequested: function(command) { root.runAndDismiss(command) }
onPanelRequested: function(pluginId) { root.openPanel(pluginId) }
onToggleRequested: function(command) { root.toggleAndRefresh(command) }
}
SliderRow {
width: parent.width
icon: mutedFlag.value ? "" : ""
dimmed: mutedFlag.value
value: Number(volumeValue.value) || 0
onIconActivated: root.toggleAndRefresh("pamixer --toggle-mute")
onValueRequested: function(next) {
root.run("pamixer --set-volume " + next)
volumeValue.value = String(next)
}
}
SliderRow {
width: parent.width
icon: ""
value: Number(brightnessValue.value) || 0
onValueRequested: function(next) {
root.run("brightnessctl set " + next + "%")
brightnessValue.value = String(next)
}
}
MediaRow {
width: parent.width
title: root.mediaParts.length > 0 ? root.mediaParts[0] : ""
artist: root.mediaParts.length > 1 ? root.mediaParts[1] : ""
status: root.mediaParts.length > 2 ? root.mediaParts[2] : "Stopped"
onControlRequested: function(action) {
root.run("playerctl " + action)
stateSettleTimer.restart()
}
}
}
}
}
onOpenedChanged: if (root.opened) root.refreshState()
}
@@ -0,0 +1,85 @@
import QtQuick
import qs.Commons
Item {
id: root
property string icon: ""
property int value: 0
property bool dimmed: false
signal iconActivated()
signal valueRequested(int value)
implicitHeight: Style.spaceReal(24)
Text {
id: iconText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Style.spaceReal(22)
text: root.icon
textFormat: Text.PlainText
color: root.dimmed ? Util.alpha(Color.magenta, 0.5) : Color.magenta
font.family: Style.font.family
font.pixelSize: Style.font.body
TapHandler {
onTapped: root.iconActivated()
}
}
Rectangle {
id: trough
anchors.left: iconText.right
anchors.leftMargin: Style.spaceReal(10)
anchors.right: valueText.left
anchors.rightMargin: Style.spaceReal(10)
anchors.verticalCenter: parent.verticalCenter
height: Style.spaceReal(8)
radius: Style.cardRadius
color: Util.alpha(Color.foreground, 0.2)
Rectangle {
width: parent.width * Math.max(0, Math.min(100, root.value)) / 100
height: parent.height
radius: Style.cardRadius
color: Color.accent
}
Rectangle {
x: Math.max(0, Math.min(parent.width - width, parent.width * root.value / 100 - width / 2))
anchors.verticalCenter: parent.verticalCenter
width: Style.spaceReal(14)
height: width
radius: Style.cardRadius
color: Color.foreground
}
TapHandler {
onTapped: function(point) {
root.valueRequested(Math.round(point.position.x / trough.width * 100))
}
}
DragHandler {
target: null
onCentroidChanged: {
if (!active) return
root.valueRequested(Math.round(centroid.position.x / trough.width * 100))
}
}
}
Text {
id: valueText
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Style.spaceReal(34)
horizontalAlignment: Text.AlignRight
text: root.value + "%"
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
@@ -0,0 +1,56 @@
import QtQuick
import qs.Commons
Rectangle {
id: root
property string icon: ""
property string label: ""
property bool active: false
property string tooltip: ""
signal activated()
implicitWidth: Style.spaceReal(96)
implicitHeight: Style.spaceReal(58)
radius: Style.cardRadius
color: root.active
? Color.accent
: (hover.hovered ? Util.alpha(Color.blue, 0.25) : Util.alpha(Color.background, Style.cardFillAlpha))
Behavior on color {
ColorAnimation { duration: 150 }
}
readonly property color contentColor: root.active ? Color.background : Color.foreground
Column {
anchors.centerIn: parent
spacing: Style.spaceReal(4)
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.icon
textFormat: Text.PlainText
color: root.contentColor
font.family: Style.font.family
font.pixelSize: Style.font.body
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.label
textFormat: Text.PlainText
color: root.contentColor
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
HoverHandler {
id: hover
}
TapHandler {
onTapped: root.activated()
}
}
@@ -0,0 +1,92 @@
import QtQuick
import qs.Commons
Grid {
id: root
property bool bluetoothOn: false
property bool doNotDisturb: false
property bool nightLight: false
property bool recording: false
property bool stayAwake: false
property bool tabletFollow: false
signal runRequested(string command)
signal panelRequested(string pluginId)
signal toggleRequested(string command)
columns: 3
columnSpacing: Style.spaceReal(8)
rowSpacing: Style.spaceReal(8)
Tile {
icon: ""
label: "Wi-Fi"
onActivated: root.panelRequested("blob.network")
}
Tile {
icon: ""
label: "Bluetooth"
active: root.bluetoothOn
onActivated: root.panelRequested("blob.bluetooth")
}
Tile {
icon: ""
label: "Wallpaper"
onActivated: root.runRequested("blob-wallpaper-set --menu")
}
Tile {
icon: ""
label: "Silence"
active: root.doNotDisturb
onActivated: root.toggleRequested("blob-shell notifications toggleDnd")
}
Tile {
icon: ""
label: "Night Light"
active: root.nightLight
onActivated: root.toggleRequested("blob-toggle-nightlight")
}
Tile {
icon: ""
label: "Record"
active: root.recording
tooltip: root.recording ? "Stop screen recording" : "Start a screen recording"
onActivated: root.runRequested(root.recording
? "blob-capture-record --stop-recording"
: "blob-menu toggle trigger.capture.screenrecord")
}
Tile {
icon: "󰅶"
label: "Stay Awake"
active: root.stayAwake
tooltip: root.stayAwake ? "Allow idle lock and screensaver" : "Keep the screen awake"
onActivated: root.toggleRequested("blob-toggle-idle toggle")
}
Tile {
icon: ""
label: "Pick Color"
onActivated: root.runRequested("hyprpicker -a")
}
Tile {
icon: ""
label: "Theme"
onActivated: root.runRequested("blob-theme-menu")
}
Tile {
icon: ""
label: "Tablet Lock"
active: root.tabletFollow
tooltip: root.tabletFollow ? "Tablet follows focused monitor" : "Tablet spans all monitors"
onActivated: root.toggleRequested("blob-toggle-tablet toggle")
}
}
@@ -0,0 +1,45 @@
import QtQuick
import qs.Commons
import qs.Ui
Card {
id: root
property string place: "Loading..."
property string glyph: ""
property string temperature: ""
property string wind: ""
property bool available: false
implicitWidth: Style.spaceReal(190)
CardHeader {
icon: root.available ? root.glyph : ""
title: root.place
}
Text {
visible: !root.available
text: "Weather unavailable"
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.7)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
StatRow {
width: parent.width
visible: root.available
icon: ""
label: "Temperature"
value: root.temperature
}
StatRow {
width: parent.width
visible: root.available
icon: ""
label: "Wind"
value: root.wind
}
}
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"name": "Quick settings",
"version": "1.0.0",
"author": "Blob",
"description": "Control centre with tiles, sliders, calendar, media, and weather",
"id": "blob.quick-settings",
"kinds": [
"overlay"
],
"keepLoaded": true,
"entryPoints": {
"overlay": "QuickSettings.qml"
}
}
+71
View File
@@ -0,0 +1,71 @@
import QtQuick
import qs.Commons
Column {
id: root
property string icon: ""
property string name: ""
property int percent: 0
property string detail: ""
spacing: Style.spaceReal(4)
Item {
width: parent.width
implicitHeight: nameText.implicitHeight
Text {
id: iconText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Style.spaceReal(22)
text: root.icon
textFormat: Text.PlainText
color: Color.magenta
font.family: Style.font.family
font.pixelSize: Style.font.body
}
Text {
id: nameText
anchors.left: iconText.right
anchors.leftMargin: Style.spaceReal(8)
anchors.verticalCenter: parent.verticalCenter
text: root.name
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: root.detail
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.8)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
Rectangle {
width: parent.width
height: Style.spaceReal(8)
radius: Style.cardRadius
color: Util.alpha(Color.foreground, 0.2)
Rectangle {
width: parent.width * Math.max(0, Math.min(100, root.percent)) / 100
height: parent.height
radius: Style.cardRadius
color: Color.accent
Behavior on width {
NumberAnimation { duration: 200 }
}
}
}
}
+194
View File
@@ -0,0 +1,194 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Ui
Item {
id: root
property var shell: null
property var manifest: null
property bool opened: false
readonly property string pluginId: (manifest && manifest.id) || "blob.sysmon"
property int cpuPercent: 0
property int memoryPercent: 0
property string memoryDetail: ""
property int diskPercent: 0
property string diskDetail: ""
property int temperature: 0
property real previousIdle: 0
property real previousTotal: 0
function open(payloadJson) {
root.opened = true
}
function close() {
root.opened = false
}
function dismiss() {
root.opened = false
if (root.shell && typeof root.shell.hide === "function")
root.shell.hide(root.pluginId)
}
function readCpu(raw) {
var values = String(raw).trim().split(/\s+/).slice(1).map(Number)
var idle = (values[3] || 0) + (values[4] || 0)
var total = 0
for (var i = 0; i < values.length; i++) total += values[i] || 0
var idleDelta = idle - root.previousIdle
var totalDelta = total - root.previousTotal
root.previousIdle = idle
root.previousTotal = total
if (totalDelta <= 0) return
root.cpuPercent = Math.round((1 - idleDelta / totalDelta) * 100)
}
function readMemory(raw) {
var parts = String(raw).trim().split(/\s+/).map(Number)
var used = parts[0] || 0
var total = parts[1] || 0
if (!total) {
root.memoryPercent = 0
root.memoryDetail = ""
return
}
root.memoryPercent = Math.round(used / total * 100)
root.memoryDetail = (used / 1024).toFixed(1) + "G / " + (total / 1024).toFixed(1) + "G"
}
function readDisk(raw) {
var parts = String(raw).trim().split(/\s+/)
root.diskPercent = Number(String(parts[0] || "").replace("%", "")) || 0
root.diskDetail = (parts[1] || "0") + " / " + (parts[2] || "0")
}
Process {
id: cpuProbe
command: ["bash", "-c", "grep '^cpu ' /proc/stat"]
stdout: StdioCollector { onStreamFinished: root.readCpu(text) }
}
Process {
id: memoryProbe
command: ["bash", "-c", "free -m | awk '/^Mem:/ {print $3\" \"$2}'"]
stdout: StdioCollector { onStreamFinished: root.readMemory(text) }
}
Process {
id: diskProbe
command: ["bash", "-c", "df -h --output=pcent,used,size / | tail -1"]
stdout: StdioCollector { onStreamFinished: root.readDisk(text) }
}
Process {
id: temperatureProbe
command: ["bash", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"]
stdout: StdioCollector {
onStreamFinished: root.temperature = Math.round(Number(String(text).trim()) / 1000) || 0
}
}
function refreshFast() {
if (!cpuProbe.running) cpuProbe.running = true
if (!memoryProbe.running) memoryProbe.running = true
if (!temperatureProbe.running) temperatureProbe.running = true
}
function refreshSlow() {
if (!diskProbe.running) diskProbe.running = true
}
Timer {
interval: 2000
running: root.opened
repeat: true
onTriggered: root.refreshFast()
}
Timer {
interval: 30000
running: root.opened
repeat: true
onTriggered: root.refreshSlow()
}
onOpenedChanged: {
if (!root.opened) return
root.refreshFast()
root.refreshSlow()
}
PanelWindow {
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "blob-sysmon"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Card {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Style.spaceReal(10)
anchors.rightMargin: Style.spaceReal(10)
implicitWidth: Style.spaceReal(400)
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
spacing: Style.spaceReal(12)
Text {
text: "System Monitor"
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
Metric {
width: parent.width
icon: "󰍛"
name: "CPU"
percent: root.cpuPercent
detail: root.cpuPercent + "%"
}
Metric {
width: parent.width
icon: "󰘚"
name: "Memory"
percent: root.memoryPercent
detail: root.memoryDetail
}
Metric {
width: parent.width
icon: "󰋊"
name: "Disk"
percent: root.diskPercent
detail: root.diskDetail
}
Metric {
width: parent.width
icon: ""
name: "Temperature"
percent: root.temperature
detail: root.temperature + "°C"
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"name": "System monitor",
"version": "1.0.0",
"author": "Blob",
"description": "CPU, memory, disk, and temperature meters",
"id": "blob.sysmon",
"kinds": [
"overlay"
],
"keepLoaded": true,
"entryPoints": {
"overlay": "SysMonitor.qml"
}
}