letsss GOOO

This commit is contained in:
2026-09-21 16:50:56 +02:00
commit 62e760f126
14 changed files with 1560 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
DankFlickable {
id: mixer
readonly property var streams: Pipewire.nodes.values.filter(node => node.audio && node.isSink && node.isStream)
readonly property int streamCount: streams.length
function appName(node) {
if (!node)
return "";
const props = node.properties || {};
return props["application.name"] || AudioService.displayName(node) || "Unknown";
}
function mediaName(node) {
const props = (node && node.properties) ? node.properties : {};
return props["media.name"] || "";
}
function appIconValue(node) {
const props = (node && node.properties) ? node.properties : {};
const candidates = [props["application.icon-name"], props["application.process.binary"], props["application.name"]];
for (const candidate of candidates) {
if (!candidate)
continue;
const entry = DesktopEntries.heuristicLookup(String(candidate));
if (entry && entry.icon)
return entry.icon;
}
return "material:graphic_eq";
}
clip: true
contentHeight: streamColumn.height
contentWidth: width
Column {
id: streamColumn
width: mixer.width
spacing: Theme.spacingS
Repeater {
model: ScriptModel {
values: mixer.streams
}
delegate: Rectangle {
id: streamItem
required property var modelData
width: streamColumn.width
height: 86
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: Theme.layerOutlineWidth
AppIconRenderer {
id: streamIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.top: parent.top
anchors.topMargin: Theme.spacingM
width: Theme.iconSize
height: Theme.iconSize
iconValue: mixer.appIconValue(streamItem.modelData)
iconSize: Theme.iconSize
fallbackText: mixer.appName(streamItem.modelData).charAt(0).toUpperCase()
}
Column {
anchors.left: streamIcon.right
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.top: parent.top
anchors.topMargin: Theme.spacingM
spacing: 1
StyledText {
width: parent.width
text: mixer.appName(streamItem.modelData)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: mixer.mediaName(streamItem.modelData)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: text !== ""
}
}
GameBarVolumeRow {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingXS
node: streamItem.modelData
maxPercent: 100
activeIcon: "volume_up"
mutedIcon: "volume_off"
}
PwObjectTracker {
objects: [streamItem.modelData]
}
}
}
Item {
width: streamColumn.width
height: 120
visible: mixer.streamCount === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "music_off"
size: Theme.iconSizeLarge
color: Theme.surfaceVariantText
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "No apps are playing audio"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: widget
readonly property real sectionSpacing: Theme.spacingS
GameBarVolumeRow {
id: outputVolume
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
node: AudioService.sink
maxPercent: AudioService.sinkMaxVolume
activeIcon: "volume_up"
mutedIcon: "volume_off"
}
StyledText {
id: outputLabel
anchors.top: outputVolume.bottom
anchors.left: parent.left
text: "Output"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
GameBarDeviceList {
id: outputDevices
anchors.top: outputLabel.bottom
anchors.topMargin: Theme.spacingXS
anchors.left: parent.left
anchors.right: parent.right
height: Math.max(52, (parent.height - outputVolume.height - inputVolume.height - outputLabel.height - inputLabel.height - widget.sectionSpacing * 3 - Theme.spacingXS * 2) * 0.5)
isSink: true
}
GameBarVolumeRow {
id: inputVolume
anchors.top: outputDevices.bottom
anchors.topMargin: widget.sectionSpacing
anchors.left: parent.left
anchors.right: parent.right
node: AudioService.source
maxPercent: 100
activeIcon: "mic"
mutedIcon: "mic_off"
}
StyledText {
id: inputLabel
anchors.top: inputVolume.bottom
anchors.left: parent.left
text: "Input"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
GameBarDeviceList {
anchors.top: inputLabel.bottom
anchors.topMargin: Theme.spacingXS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
isSink: false
}
}
+205
View File
@@ -0,0 +1,205 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modules.Plugins
import qs.Services
PluginComponent {
id: root
pluginId: "gameBar"
pluginService: PluginService
readonly property bool overlayOpen: overlay.shouldBeVisible
// Widget catalogue. `id` is the persistence key, the rest drives the top bar
// toggles and the default placement (fractions of the screen size).
readonly property var widgetDefs: [
{
"id": "audio",
"title": "Audio",
"icon": "volume_up",
"width": 430,
"height": 470,
"x": 0.04,
"y": 0.14,
"defaultVisible": true
},
{
"id": "apps",
"title": "Apps",
"icon": "tune",
"width": 400,
"height": 360,
"x": 0.30,
"y": 0.14,
"defaultVisible": true
},
{
"id": "system",
"title": "Performance",
"icon": "speed",
"width": 380,
"height": 330,
"x": 0.56,
"y": 0.14,
"defaultVisible": false
}
]
function widgetDef(widgetId) {
return widgetDefs.find(def => def.id === widgetId) || null;
}
// ── Visibility ───────────────────────────────────────────────────────────
function savePluginValue(key, value) {
if (pluginService)
pluginService.savePluginData(pluginId, key, value);
}
function isWidgetVisible(widgetId) {
const stored = pluginData.visibleWidgets;
if (stored && stored[widgetId] !== undefined)
return stored[widgetId] === true;
const def = widgetDef(widgetId);
return def ? def.defaultVisible === true : false;
}
function setWidgetVisible(widgetId, visible) {
const stored = Object.assign({}, pluginData.visibleWidgets || {});
stored[widgetId] = visible === true;
root.savePluginValue("visibleWidgets", stored);
}
function toggleWidget(widgetId) {
root.setWidgetVisible(widgetId, !root.isWidgetVisible(widgetId));
}
// ── Positions ────────────────────────────────────────────────────────────
// Stored per screen as fractions of the screen size, so the same layout
// survives monitors of different resolutions (and is re-clamped on load).
function screenKey(screen) {
if (!screen)
return "default";
return SettingsData.getScreenDisplayName(screen) || screen.name || "default";
}
function widgetPosition(screen, widgetId) {
const layout = pluginData.layout || {};
const perScreen = layout[root.screenKey(screen)];
const saved = perScreen ? perScreen[widgetId] : null;
if (saved && saved.x !== undefined && saved.y !== undefined)
return saved;
const def = widgetDef(widgetId);
return def ? ({
"x": def.x,
"y": def.y
}) : ({
"x": 0.1,
"y": 0.15
});
}
function saveWidgetPosition(screen, widgetId, fractionX, fractionY) {
const layout = JSON.parse(JSON.stringify(pluginData.layout || {}));
const key = root.screenKey(screen);
if (!layout[key])
layout[key] = {};
layout[key][widgetId] = {
"x": fractionX,
"y": fractionY
};
root.savePluginValue("layout", layout);
}
function resetLayout() {
root.savePluginValue("layout", {});
}
// ── Overlay ──────────────────────────────────────────────────────────────
function openOverlay() {
if (overlay.shouldBeVisible)
return;
if (typeof PopoutService !== "undefined" && PopoutService)
PopoutService.closeControlCenter();
overlay.shouldBeVisible = true;
overlay.open();
}
function closeOverlay() {
if (!overlay.shouldBeVisible)
return;
overlay.shouldBeVisible = false;
overlay.close();
}
function toggleOverlay() {
if (overlay.shouldBeVisible)
root.closeOverlay();
else
root.openOverlay();
}
IpcHandler {
target: "gameBar"
enabled: true
function toggle() : string {
root.toggleOverlay();
return "SUCCESS";
}
function open() : string {
root.openOverlay();
return "SUCCESS";
}
function close() : string {
root.closeOverlay();
return "SUCCESS";
}
function widget(name: string) : string {
if (!root.widgetDef(name))
return "UNKNOWN_WIDGET: " + name;
root.toggleWidget(name);
return "SUCCESS";
}
function resetLayout() : string {
root.resetLayout();
return "SUCCESS";
}
}
GameBarOverlay {
id: overlay
daemon: root
}
Component.onCompleted: {
if (pluginService && pluginId) {
const instances = Object.assign({}, pluginService.pluginInstances);
instances[pluginId] = root;
pluginService.pluginInstances = instances;
}
}
Component.onDestruction: {
if (pluginService && pluginService.pluginInstances[pluginId] === root) {
const instances = Object.assign({}, pluginService.pluginInstances);
delete instances[pluginId];
pluginService.pluginInstances = instances;
}
}
}
+147
View File
@@ -0,0 +1,147 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
DankFlickable {
id: deviceList
property bool isSink: true
readonly property var activeNode: isSink ? AudioService.sink : AudioService.source
readonly property var deviceNodes: {
const hidden = isSink ? (SessionData.hiddenOutputDeviceNames ?? []) : (SessionData.hiddenInputDeviceNames ?? []);
const nodes = Pipewire.nodes.values.filter(node => {
if (!node.audio || node.isStream)
return false;
if (node.isSink !== isSink)
return false;
return !hidden.includes(node.name);
});
const active = activeNode;
return [...nodes].sort((a, b) => {
if (a === active)
return -1;
if (b === active)
return 1;
return AudioService.displayName(a).localeCompare(AudioService.displayName(b));
});
}
function activate(node) {
if (!node || !node.name)
return;
if (deviceList.isSink)
AudioService.setDefaultSinkByName(node.name);
else
AudioService.setDefaultSourceByName(node.name);
}
clip: true
contentHeight: deviceColumn.height
contentWidth: width
Column {
id: deviceColumn
width: deviceList.width
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
values: deviceList.deviceNodes
}
delegate: Rectangle {
id: deviceItem
required property var modelData
readonly property bool isActive: modelData === deviceList.activeNode
width: deviceColumn.width
height: 46
radius: Theme.cornerRadius
color: isActive ? Theme.primarySelected : (deviceArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency))
border.color: isActive ? Theme.primary : Theme.outlineLight
border.width: isActive ? 2 : Theme.layerOutlineWidth
DankIcon {
id: deviceIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
name: deviceList.isSink ? AudioService.sinkIcon(deviceItem.modelData) : "mic"
size: Theme.iconSize - 4
color: deviceItem.isActive ? Theme.primary : Theme.surfaceText
}
DankIcon {
id: activeCheck
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
name: "check_circle"
size: Theme.iconSize - 6
color: Theme.primary
visible: deviceItem.isActive
}
Column {
anchors.left: deviceIcon.right
anchors.leftMargin: Theme.spacingS
anchors.right: activeCheck.visible ? activeCheck.left : parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: 1
StyledText {
width: parent.width
text: AudioService.displayName(deviceItem.modelData)
font.pixelSize: Theme.fontSizeMedium
font.weight: deviceItem.isActive ? Font.Medium : Font.Normal
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: AudioService.subtitle(deviceItem.modelData.name)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: text !== ""
}
}
MouseArea {
id: deviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: deviceList.activate(deviceItem.modelData)
}
PwObjectTracker {
objects: [deviceItem.modelData]
}
}
}
StyledText {
width: deviceColumn.width
text: deviceList.isSink ? "No output devices found" : "No input devices found"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: deviceList.deviceNodes.length === 0
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modals.Common
DankModal {
id: overlay
property var daemon: null
readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({})
readonly property real availableWidth: screenWidth > 0 ? screenWidth : 1920
readonly property real availableHeight: screenHeight > 0 ? screenHeight : 1080
function requestClose() {
if (overlay.daemon)
overlay.daemon.closeOverlay();
else
overlay.close();
}
layerNamespace: "dms:plugins:gameBar"
// Overlay layer puts the widgets above fullscreen windows, like the Xbox Game Bar.
useOverlayLayer: true
positioning: "center"
shouldBeVisible: false
closeOnEscapeKey: true
closeOnBackgroundClick: true
showBackground: true
backgroundOpacity: Math.max(0, Math.min(0.95, (pluginData.backdropOpacity ?? 70) / 100))
onBackgroundClicked: overlay.requestClose()
// The modal itself is the full screen; the widgets inside are the visible chrome.
backgroundColor: "transparent"
borderWidth: 0
cornerRadius: 0
enableShadow: false
// A full-screen surface should fade, not scale/slide in.
animationScaleCollapsed: 1
animationOffset: 0
modalWidth: availableWidth
modalHeight: availableHeight
content: Component {
GameBarPanel {
overlayRef: overlay
}
}
}
+132
View File
@@ -0,0 +1,132 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
FocusScope {
id: root
property var overlayRef: null
readonly property var daemon: overlayRef ? overlayRef.daemon : null
readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({})
readonly property var targetScreen: overlayRef ? overlayRef.effectiveScreen : null
readonly property real edgeMargin: Theme.spacingM
readonly property real topLimit: topBar.y + topBar.height + Theme.spacingM
property int windowZCounter: 1
function nextWindowZ() {
root.windowZCounter = root.windowZCounter + 1;
return root.windowZCounter;
}
function requestClose() {
if (overlayRef)
overlayRef.requestClose();
}
function isWidgetVisible(widgetId) {
return daemon ? daemon.isWidgetVisible(widgetId) : false;
}
function toggleWidget(widgetId) {
if (daemon)
daemon.toggleWidget(widgetId);
}
function widgetPosition(widgetId) {
if (daemon)
return daemon.widgetPosition(root.targetScreen, widgetId);
return {
"x": 0.1,
"y": 0.15
};
}
function saveWidgetPosition(widgetId, fractionX, fractionY) {
if (daemon)
daemon.saveWidgetPosition(root.targetScreen, widgetId, fractionX, fractionY);
}
anchors.fill: parent
focus: true
// The modal's own Escape handler sits on a sibling focus scope, so the key
// never reaches it once this content holds focus - handle it here instead.
Keys.onEscapePressed: event => {
root.requestClose();
event.accepted = true;
}
Component.onCompleted: root.forceActiveFocus()
// Keeps the default sink/source properties live while the overlay is open.
PwObjectTracker {
objects: [AudioService.sink, AudioService.source].filter(node => !!node)
}
// Clicking empty space closes, like clicking outside a dialog.
MouseArea {
anchors.fill: parent
onClicked: root.requestClose()
}
GameBarTopBar {
id: topBar
panel: root
anchors.top: parent.top
anchors.topMargin: Theme.spacingL
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
model: root.daemon ? root.daemon.widgetDefs : []
delegate: GameBarWindow {
required property var modelData
panel: root
widgetId: modelData.id
title: modelData.title
icon: modelData.icon
widgetWidth: modelData.width
widgetHeight: modelData.height
minimumY: root.topLimit
shown: root.isWidgetVisible(modelData.id)
contentComponent: {
switch (modelData.id) {
case "audio":
return audioWidgetComponent;
case "apps":
return appsWidgetComponent;
case "system":
return systemWidgetComponent;
}
return null;
}
}
}
Component {
id: audioWidgetComponent
GameBarAudioWidget {}
}
Component {
id: appsWidgetComponent
GameBarAppMixer {}
}
Component {
id: systemWidgetComponent
GameBarSystemWidget {}
}
}
+91
View File
@@ -0,0 +1,91 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
PluginSettings {
id: settings
pluginId: "gameBar"
Rectangle {
width: parent.width
height: hintColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
Column {
id: hintColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: "Keybind setup"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Commands: dms ipc call gameBar toggle / open / close / widget <audio|apps|system> / resetLayout"
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.monoFontFamily
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
}
SliderSetting {
settingKey: "backdropOpacity"
label: "Background dimming"
description: "How much the desktop behind the overlay is darkened"
defaultValue: 70
minimum: 0
maximum: 95
unit: "%"
leftIcon: "light_mode"
rightIcon: "dark_mode"
}
Item {
width: parent.width
height: resetColumn.implicitHeight
Column {
id: resetColumn
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.spacingXS
StyledText {
text: "Widget layout"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Widget positions are saved per monitor. Reset them to the default arrangement."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankButton {
text: "Reset widget positions"
iconName: "restart_alt"
onClicked: settings.saveValue("layout", {})
}
}
}
}
+197
View File
@@ -0,0 +1,197 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: widget
readonly property var activeModules: ["system", "cpu", "memory", "network", "disk", "diskmounts"]
readonly property var rootMount: {
const mounts = DgopService.diskMounts || [];
return mounts.find(mount => mount.mount === "/") || mounts[0] || null;
}
readonly property real diskPercent: {
if (!rootMount || !rootMount.percent)
return 0;
return parseFloat(String(rootMount.percent).replace("%", "")) || 0;
}
function formatRate(bytesPerSecond) {
const value = bytesPerSecond || 0;
if (value < 1024)
return value.toFixed(0) + " B/s";
if (value < 1024 * 1024)
return (value / 1024).toFixed(0) + " K/s";
if (value < 1024 * 1024 * 1024)
return (value / (1024 * 1024)).toFixed(1) + " M/s";
return (value / (1024 * 1024 * 1024)).toFixed(1) + " G/s";
}
function formatGiB(megabytes) {
return ((megabytes || 0) / 1024).toFixed(1) + " GiB";
}
// Stats only get polled while this widget exists.
Component.onCompleted: DgopService.addRef(widget.activeModules)
Component.onDestruction: DgopService.removeRef(widget.activeModules)
component StatTile: Rectangle {
id: tile
property string icon: ""
property string label: ""
property string value: ""
property string detail: ""
property real fraction: 0
property color accent: Theme.primary
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: Theme.layerOutlineWidth
DankIcon {
id: tileIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.top: parent.top
anchors.topMargin: Theme.spacingM
name: tile.icon
size: Theme.iconSize - 6
color: tile.accent
}
StyledText {
anchors.left: tileIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: tileIcon.verticalCenter
text: tile.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
id: tileValue
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: tileIcon.verticalCenter
text: tile.value
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
id: tileDetail
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.bottom: tileTrack.top
anchors.bottomMargin: Theme.spacingXS
text: tile.detail
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
Rectangle {
id: tileTrack
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
height: 5
radius: height / 2
color: Theme.withAlpha(Theme.outline, 0.4)
Rectangle {
width: parent.width * Math.max(0, Math.min(1, tile.fraction))
height: parent.height
radius: parent.radius
color: tile.accent
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
}
}
}
Grid {
anchors.fill: parent
columns: 2
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingS
readonly property real tileWidth: (width - columnSpacing) / 2
readonly property real tileHeight: (height - rowSpacing) / 2
StatTile {
width: parent.tileWidth
height: parent.tileHeight
icon: "memory"
label: "CPU"
value: Math.round(DgopService.cpuUsage || 0) + "%"
detail: DgopService.cpuTemperature > 0 ? DgopService.cpuTemperature.toFixed(0) + "°C" : (DgopService.cpuCores + " cores")
fraction: (DgopService.cpuUsage || 0) / 100
accent: DgopService.cpuUsage > 85 ? Theme.error : (DgopService.cpuUsage > 60 ? Theme.warning : Theme.primary)
}
StatTile {
width: parent.tileWidth
height: parent.tileHeight
icon: "developer_board"
label: "Memory"
value: Math.round(DgopService.memoryUsage || 0) + "%"
detail: widget.formatGiB(DgopService.usedMemoryMB) + " / " + widget.formatGiB(DgopService.totalMemoryMB)
fraction: (DgopService.memoryUsage || 0) / 100
accent: DgopService.memoryUsage > 85 ? Theme.error : (DgopService.memoryUsage > 60 ? Theme.warning : Theme.primary)
}
StatTile {
width: parent.tileWidth
height: parent.tileHeight
icon: "swap_vert"
label: "Network"
value: "↓ " + widget.formatRate(DgopService.networkRxRate)
detail: "↑ " + widget.formatRate(DgopService.networkTxRate)
// Scaled against 100 MiB/s so the bar stays readable on fast links.
fraction: Math.min(1, ((DgopService.networkRxRate || 0) + (DgopService.networkTxRate || 0)) / (100 * 1024 * 1024))
accent: Theme.primary
}
StatTile {
width: parent.tileWidth
height: parent.tileHeight
icon: "storage"
label: widget.rootMount ? widget.rootMount.mount : "Disk"
value: Math.round(widget.diskPercent) + "%"
detail: "R " + widget.formatRate(DgopService.diskReadRate) + " W " + widget.formatRate(DgopService.diskWriteRate)
fraction: widget.diskPercent / 100
accent: widget.diskPercent > 90 ? Theme.error : (widget.diskPercent > 75 ? Theme.warning : Theme.primary)
}
}
StyledText {
anchors.centerIn: parent
width: parent.width - Theme.spacingL * 2
text: "dgop is not available - install it to see system stats."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
visible: !DgopService.dgopAvailable
}
}
+143
View File
@@ -0,0 +1,143 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: bar
property var panel: null
readonly property var widgetDefs: (panel && panel.daemon) ? panel.daemon.widgetDefs : []
width: barRow.width + Theme.spacingL * 2
height: 56
radius: height / 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
SystemClock {
id: clock
precision: SystemClock.Minutes
}
component BarSeparator: Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 26
color: Theme.outlineMedium
}
Row {
id: barRow
anchors.centerIn: parent
spacing: Theme.spacingM
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "sports_esports"
size: Theme.iconSize
color: Theme.primary
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 0
StyledText {
text: Qt.formatDateTime(clock.date, "HH:mm")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: Qt.formatDateTime(clock.date, "ddd d MMM")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
BarSeparator {}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Repeater {
model: bar.widgetDefs
delegate: Rectangle {
id: toggleButton
required property var modelData
readonly property bool active: bar.panel ? bar.panel.isWidgetVisible(modelData.id) : false
width: toggleRow.width + Theme.spacingM * 2
height: 38
radius: Theme.cornerRadius
color: active ? Theme.primarySelected : (toggleArea.containsMouse ? Theme.surfaceHover : "transparent")
border.color: active ? Theme.primary : "transparent"
border.width: active ? 1 : 0
Row {
id: toggleRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: toggleButton.modelData.icon
size: Theme.iconSize - 6
color: toggleButton.active ? Theme.primary : Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: toggleButton.modelData.title
font.pixelSize: Theme.fontSizeSmall
font.weight: toggleButton.active ? Font.Medium : Font.Normal
color: toggleButton.active ? Theme.primary : Theme.surfaceText
}
}
MouseArea {
id: toggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bar.panel)
bar.panel.toggleWidget(toggleButton.modelData.id);
}
}
}
}
}
BarSeparator {}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
buttonSize: 34
iconSize: 18
iconColor: Theme.surfaceText
onClicked: {
if (bar.panel)
bar.panel.requestClose();
}
}
}
}
+99
View File
@@ -0,0 +1,99 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: row
property var node: null
property int maxPercent: 100
property string activeIcon: "volume_up"
property string mutedIcon: "volume_off"
readonly property var audio: (node && node.audio) ? node.audio : null
readonly property bool muted: audio ? audio.muted : false
readonly property int percent: audio ? Math.round(audio.volume * 100) : 0
readonly property int sliderMaximum: Math.max(100, maxPercent)
// DankSlider assigns its own `value` while dragging, which breaks a declarative
// binding, so the slider is synced imperatively whenever the node volume changes.
function syncSlider() {
slider.value = Math.min(row.sliderMaximum, row.percent);
}
function applyVolume(newValue) {
if (!row.audio)
return;
row.audio.volume = newValue / 100;
if (newValue > 0 && row.audio.muted)
row.audio.muted = false;
}
height: 48
onPercentChanged: row.syncSlider()
onSliderMaximumChanged: row.syncSlider()
onNodeChanged: row.syncSlider()
Rectangle {
id: muteButton
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
radius: width / 2
color: muteArea.containsMouse ? Theme.primaryHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: (row.muted || row.percent === 0) ? row.mutedIcon : row.activeIcon
size: Theme.iconSize
color: row.muted ? Theme.error : Theme.primary
}
MouseArea {
id: muteArea
anchors.fill: parent
hoverEnabled: true
enabled: row.audio !== null
cursorShape: Qt.PointingHandCursor
onClicked: {
if (row.audio)
row.audio.muted = !row.audio.muted;
}
}
}
StyledText {
id: valueLabel
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 48
horizontalAlignment: Text.AlignRight
text: row.percent + "%"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: row.muted ? Theme.surfaceVariantText : Theme.surfaceText
}
DankSlider {
id: slider
anchors.left: muteButton.right
anchors.leftMargin: Theme.spacingS
anchors.right: valueLabel.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
enabled: row.audio !== null
minimum: 0
maximum: row.sliderMaximum
showValue: false
onSliderValueChanged: newValue => row.applyVolume(newValue)
Component.onCompleted: row.syncSlider()
}
}
+191
View File
@@ -0,0 +1,191 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: win
property var panel: null
property string widgetId: ""
property string title: ""
property string icon: ""
property real widgetWidth: 360
property real widgetHeight: 320
property real minimumY: 0
property bool shown: false
property Component contentComponent: null
readonly property real margin: panel ? panel.edgeMargin : 12
readonly property real panelWidth: panel ? panel.width : 0
readonly property real panelHeight: panel ? panel.height : 0
readonly property real maxX: Math.max(margin, (panel ? panel.width : width) - width - margin)
readonly property real maxY: Math.max(minimumY, (panel ? panel.height : height) - height - margin)
function clampX(value) {
return Math.max(margin, Math.min(maxX, value));
}
function clampY(value) {
return Math.max(minimumY, Math.min(maxY, value));
}
// Positions are stored as fractions of the screen, so a layout made on a 4K
// display still lands somewhere sensible on a 1440p one (and vice versa).
function applyStoredPosition() {
if (!panel || panel.width <= 0 || panel.height <= 0)
return;
const stored = panel.widgetPosition(win.widgetId);
win.x = win.clampX(stored.x * panel.width);
win.y = win.clampY(stored.y * panel.height);
}
function raise() {
if (panel)
win.z = panel.nextWindowZ();
}
function storePosition() {
if (!panel || panel.width <= 0 || panel.height <= 0)
return;
panel.saveWidgetPosition(win.widgetId, win.x / panel.width, win.y / panel.height);
}
width: widgetWidth
height: widgetHeight
visible: shown && opacity > 0
opacity: shown ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Component.onCompleted: win.applyStoredPosition()
onShownChanged: {
if (shown) {
win.applyStoredPosition();
win.raise();
}
}
// The panel has no size yet while the delegate is being created, so the
// stored fraction has to be re-applied once the screen size is known.
onPanelWidthChanged: win.applyStoredPosition()
onPanelHeightChanged: win.applyStoredPosition()
Rectangle {
id: frame
anchors.fill: parent
radius: Theme.cornerRadius * 1.25
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: dragArea.pressed ? Theme.primary : Theme.outlineMedium
border.width: dragArea.pressed ? 2 : Theme.layerOutlineWidth
Rectangle {
id: titleBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 38
radius: frame.radius
color: dragArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
// Square off the bottom corners so the title bar blends into the body.
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: parent.radius
color: parent.color
}
DankIcon {
id: titleIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
name: win.icon
size: Theme.iconSize - 6
color: Theme.primary
}
StyledText {
anchors.left: titleIcon.right
anchors.leftMargin: Theme.spacingS
anchors.right: closeButton.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: win.title
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
DankActionButton {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
buttonSize: 26
iconSize: 15
iconColor: Theme.surfaceVariantText
onClicked: {
if (win.panel)
win.panel.toggleWidget(win.widgetId);
}
}
MouseArea {
id: dragArea
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: closeButton.left
hoverEnabled: true
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
drag.target: win
drag.axis: Drag.XAndYAxis
drag.minimumX: win.margin
drag.maximumX: win.maxX
drag.minimumY: win.minimumY
drag.maximumY: win.maxY
drag.threshold: 0
onPressed: win.raise()
onReleased: win.storePosition()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
propagateComposedEvents: true
z: -1
onPressed: mouse => {
win.raise();
mouse.accepted = false;
}
}
Loader {
anchors.top: titleBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
active: win.shown
sourceComponent: win.contentComponent
}
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 keule2
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+32
View File
@@ -0,0 +1,32 @@
# Game Bar
An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/plugins-overview).
## Widgets
| Widget | Contents |
|---|---|
| **Audio** | Master output volume + mute, output device switching, microphone volume + mute, input device switching |
| **Apps** | Per-application volume sliders with individual mute, app icon and what is playing |
| **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) |
### IPC commands
| Command | Effect |
|---|---|
| `dms ipc call gameBar toggle` | Toggle the overlay |
| `dms ipc call gameBar open` | Open the overlay |
| `dms ipc call gameBar close` | Close the overlay |
| `dms ipc call gameBar widget audio\|apps\|system` | Show/hide one widget |
| `dms ipc call gameBar resetLayout` | Forget all saved widget positions |
## Settings
Settings → Plugins → Game Bar (gear icon):
- **Background dimming** — how dark the desktop behind the overlay gets (0–95%).
- **Reset widget positions** — back to the default arrangement on every monitor.
## License
MIT
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "https://github.com/AvengeMedia/DankMaterialShell/raw/refs/heads/master/PLUGINS/plugin-schema.json",
"id": "gameBar",
"name": "Game Bar",
"description": "Game Bar style overlay with quick actions and system monitoring",
"version": "1.0.0",
"license": "MIT",
"author": "keule",
"icon": "sports_esports",
"type": "daemon",
"capabilities": [
"daemon",
"ipc"
],
"component": "./GameBarDaemon.qml",
"settings": "./GameBarSettings.qml",
"requires_dms": ">=1.5.0",
"permissions": [
"settings_read",
"settings_write"
]
}