Port the AGS widgets into the shell as plugins
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user