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
+16
View File
@@ -21,6 +21,8 @@ QtObject {
property color accent: "#cacccc"
property color urgent: "#a55555"
property color muted: "#707880"
property color blue: "#7aa2f7"
property color magenta: "#ad8ee6"
// Flat dictionary of "section.key" -> raw string from shell.toml.
// Reassigning this whole property is what makes surface bindings below
@@ -142,6 +144,8 @@ QtObject {
var color4Value = ""
var color7Value = ""
var color8Value = ""
var blueValue = ""
var magentaValue = ""
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})/)
if (!match) continue
@@ -157,11 +161,23 @@ QtObject {
else if (match[1] === "color7") color7Value = 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] === "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 (!loadedForeground && color7Value.length > 0) foreground = color7Value
if (!foundAccent && color4Value.length > 0) accent = color4Value
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
+11
View File
@@ -508,6 +508,17 @@ QtObject {
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: {
refresh()
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
ToggleSwitch 1.0 ToggleSwitch.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"
}
}