Merge branch 'end-4:main' into main

This commit is contained in:
jwihardi
2025-11-07 15:32:13 -05:00
committed by GitHub
28 changed files with 764 additions and 210 deletions
+2
View File
@@ -23,3 +23,5 @@ exec-once = wl-paste --type image --watch bash -c 'cliphist store && qs -c $qsCo
# Cursor
exec-once = hyprctl setcursor Bibata-Modern-Classic 24
# Fix dock pinned apps not launching properly (https://github.com/end-4/dots-hyprland/issues/2200)
exec-once = sleep 3.5 && hyprctl reload && sleep 0.5 && touch ~/.config/quickshell/ii/shell.qml
@@ -14,9 +14,50 @@ Item {
property real padding: 4
implicitWidth: row.implicitWidth + padding * 2
implicitHeight: row.implicitHeight + padding * 2
// Excellent symbol explaination and source :
// http://xahlee.info/comp/unicode_computing_symbols.html
// https://www.nerdfonts.com/cheat-sheet
property var macSymbolMap: ({
"Ctrl": "󰘴",
"Alt": "󰘵",
"Shift": "󰘶",
"Space": "󱁐",
"Tab": "↹",
"Equal": "󰇼",
"Minus": "",
"Print": "",
"BackSpace": "󰭜",
"Delete": "⌦",
"Return": "󰌑",
"Period": ".",
"Escape": "⎋"
})
property var functionSymbolMap: ({
"F1": "󱊫",
"F2": "󱊬",
"F3": "󱊭",
"F4": "󱊮",
"F5": "󱊯",
"F6": "󱊰",
"F7": "󱊱",
"F8": "󱊲",
"F9": "󱊳",
"F10": "󱊴",
"F11": "󱊵",
"F12": "󱊶",
})
property var mouseSymbolMap: ({
"mouse_up": "󱕐",
"mouse_down": "󱕑",
"mouse:272": "L󰍽",
"mouse:273": "R󰍽",
"Scroll ↑/↓": "󱕒",
"Page_↑/↓": "⇞/⇟",
})
property var keyBlacklist: ["Super_L"]
property var keySubstitutions: ({
property var keySubstitutions: Object.assign({
"Super": "󰖳",
"mouse_up": "Scroll ↓", // ikr, weird
"mouse_down": "Scroll ↑", // trust me bro
@@ -27,7 +68,14 @@ Item {
"Hash": "#",
"Return": "Enter",
// "Shift": "",
})
},
!!Config.options.cheatsheet.superKey ? {
"Super": Config.options.cheatsheet.superKey,
}: {},
Config.options.cheatsheet.useMacSymbol ? macSymbolMap : {},
Config.options.cheatsheet.useFnSymbol ? functionSymbolMap : {},
Config.options.cheatsheet.useMouseSymbol ? mouseSymbolMap : {},
)
Row { // Keybind columns
id: row
@@ -77,6 +125,17 @@ Item {
var result = [];
for (var i = 0; i < keybindSection.modelData.keybinds.length; i++) {
const keybind = keybindSection.modelData.keybinds[i];
if (!Config.options.cheatsheet.splitButtons) {
for (var j = 0; j < keybind.mods.length; j++) {
keybind.mods[j] = keySubstitutions[keybind.mods[j]] || keybind.mods[j];
}
keybind.mods = [keybind.mods.join(' ') ]
keybind.mods[0] += !keyBlacklist.includes(keybind.key) && keybind.mods[0].length ? ' ' : ''
keybind.mods[0] += !keyBlacklist.includes(keybind.key) ? (keySubstitutions[keybind.key] || keybind.key) : ''
}
result.push({
"type": "keys",
"mods": keybind.mods,
@@ -108,17 +167,19 @@ Item {
delegate: KeyboardKey {
required property var modelData
key: keySubstitutions[modelData] || modelData
pixelSize: Config.options.cheatsheet.fontSize.key
}
}
StyledText {
id: keybindPlus
visible: !keyBlacklist.includes(modelData.key) && modelData.mods.length > 0
visible: Config.options.cheatsheet.splitButtons && !keyBlacklist.includes(modelData.key) && modelData.mods.length > 0
text: "+"
}
KeyboardKey {
id: keybindKey
visible: !keyBlacklist.includes(modelData.key)
visible: Config.options.cheatsheet.splitButtons && !keyBlacklist.includes(modelData.key)
key: keySubstitutions[modelData.key] || modelData.key
pixelSize: Config.options.cheatsheet.fontSize.key
color: Appearance.colors.colOnLayer0
}
}
@@ -134,7 +195,7 @@ Item {
StyledText {
id: commentText
anchors.centerIn: parent
font.pixelSize: Appearance.font.pixelSize.smaller
font.pixelSize: Config.options.cheatsheet.fontSize.comment || Appearance.font.pixelSize.smaller
text: modelData.comment
}
}
@@ -152,4 +213,4 @@ Item {
}
}
}
}
@@ -267,6 +267,22 @@ Singleton {
property int suspend: 3
}
property JsonObject cheatsheet: JsonObject {
// Use a nerdfont to see the icons
// 0: 󰖳 | 1: 󰌽 | 2: 󰘳 | 3:  | 4: 󰨡
// 5:  | 6:  | 7: 󰣇 | 8:  | 9: 
// 10:  | 11:  | 12:  | 13:  | 14: 󱄛
property string superKey: "󰖳"
property bool useMacSymbol: false
property bool splitButtons: true
property bool useMouseSymbol: false
property bool useFnSymbol: false
property JsonObject fontSize: JsonObject {
property int key: Appearance.font.pixelSize.smaller
property int comment: Appearance.font.pixelSize.smaller
}
}
property JsonObject conflictKiller: JsonObject {
property bool autoKillNotificationDaemons: false
property bool autoKillTrays: false
@@ -364,6 +380,7 @@ Singleton {
property JsonObject overlay: JsonObject {
property bool openingZoomAnimation: true
property bool darkenScreen: true
property real clickthroughOpacity: 0.7
}
property JsonObject overview: JsonObject {
@@ -395,6 +412,7 @@ Singleton {
property JsonObject resources: JsonObject {
property int updateInterval: 3000
property int historyLength: 60
}
property JsonObject musicRecognition: JsonObject {
@@ -80,7 +80,7 @@ Singleton {
}
property JsonObject overlay: JsonObject {
property list<string> open: ["crosshair"]
property list<string> open: ["crosshair", "recorder", "volumeMixer", "resources"]
property JsonObject crosshair: JsonObject {
property bool pinned: false
property bool clickthrough: true
@@ -90,14 +90,28 @@ Singleton {
property JsonObject recorder: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 100
property real y: 130
property real x: 80
property real y: 80
}
property JsonObject resources: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 1500
property real y: 770
property int tabIndex: 0
}
property JsonObject volumeMixer: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 100
property real y: 320
property real x: 80
property real y: 280
property int tabIndex: 0
}
property JsonObject fpsLimiter: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 1576
property real y: 630
}
}
@@ -0,0 +1,51 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
/*
* Simple one value line graph
*/
Canvas {
id: root
enum Alignment { Left, Right }
required property list<real> values
property int points: values.length
property color color: Appearance.colors.colPrimary
property real fillOpacity: 0.5
property var alignment: Graph.Alignment.Left
onValuesChanged: root.requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
if (!root.values || root.values.length < 2)
return
var n = root.points
var dx = width / (n - 1)
ctx.strokeStyle = root.color
ctx.fillStyle = ColorUtils.transparentize(root.color, 1 - root.fillOpacity)
ctx.lineWidth = 2
ctx.beginPath()
for (var i = 0; i < n; ++i) {
var valueIndex = (root.alignment === Graph.Alignment.Right) ? root.values.length - n + i : i
if (valueIndex < 0 || valueIndex >= root.values.length) {
continue; // No data for this point
}
var x = i * dx
var norm = root.values[valueIndex] // already in 0-1 range
var y = height - norm * height
if (valueIndex === 0) {
ctx.moveTo(x, height)
ctx.lineTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.stroke()
ctx.lineTo(width, height)
ctx.fill()
}
}
@@ -117,7 +117,7 @@ Button {
};
}
property bool tabbedTo: root.focus && (focusReason === Qt.TabFocusReason || focusReason === Qt.BacktabFocusReason)
background: Rectangle {
id: buttonBackground
topLeftRadius: root.leftRadius
@@ -130,6 +130,9 @@ Button {
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
border.width: root.tabbedTo ? 2 : 0
border.color: Appearance.colors.colSecondary
}
contentItem: StyledText {
@@ -18,5 +18,6 @@ ToolbarButton {
iconSize: 22
text: iconBtn.text
color: iconBtn.colText
animateChange: true
}
}
@@ -11,6 +11,7 @@ Rectangle {
property real extraBottomBorderWidth: 2
property color borderColor: Appearance.colors.colOnLayer0
property real borderRadius: 5
property real pixelSize: Appearance.font.pixelSize.smaller
property color keyColor: Appearance.m3colors.m3surfaceContainerLow
implicitWidth: keyFace.implicitWidth + borderWidth * 2
implicitHeight: keyFace.implicitHeight + borderWidth * 2 + extraBottomBorderWidth
@@ -35,7 +36,7 @@ Rectangle {
id: keyText
anchors.centerIn: parent
font.family: Appearance.font.family.monospace
font.pixelSize: Appearance.font.pixelSize.smaller
font.pixelSize: root.pixelSize
text: key
}
}
@@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.models
TabBar {
id: root
property real indicatorPadding: 8
Layout.fillWidth: true
background: Item {
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0) root.incrementCurrentIndex();
else if (event.angleDelta.y > 0) root.decrementCurrentIndex();
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
Rectangle {
id: activeIndicator
z: 9999
anchors.bottom: parent.bottom
topLeftRadius: height
topRightRadius: height
bottomLeftRadius: 0
bottomRightRadius: 0
color: Appearance.colors.colPrimary
// Animation
property real baseWidth: root.width / root.count
AnimatedTabIndexPair {
id: idxPair
index: root.currentIndex
}
height: 3
x: Math.min(idxPair.idx1, idxPair.idx2) * baseWidth + root.indicatorPadding
width: ((Math.max(idxPair.idx1, idxPair.idx2) + 1) * baseWidth - root.indicatorPadding) - x
}
Rectangle { // Tabbar bottom border
id: tabBarBottomBorder
z: 9998
anchors.bottom: parent.bottom
height: 1
anchors {
left: parent.left
right: parent.right
}
color: Appearance.colors.colOutlineVariant
}
}
}
@@ -1,6 +1,6 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.common.widgets
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
@@ -10,14 +10,12 @@ TabButton {
id: root
property string buttonText
property string buttonIcon
property bool selected: false
property int rippleDuration: 1200
height: buttonBackground.height
property int tabContentWidth: buttonBackground.width - buttonBackground.radius*2
property color colBackground: ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
property color colBackgroundHover: Appearance.colors.colLayer1Hover
property color colRipple: Appearance.colors.colLayer1Active
property color colBackground: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer)
property color colBackgroundHover: ColorUtils.transparentize(Appearance.colors.colOnSurface, root.checked ? 1 : 0.95)
property color colRipple: ColorUtils.transparentize(Appearance.colors.colOnSurface, 0.95)
PointingHandInteraction {}
@@ -91,8 +89,12 @@ TabButton {
background: Rectangle {
id: buttonBackground
anchors {
fill: parent
margins: 3
}
radius: Appearance?.rounding.normal
implicitHeight: 37
implicitHeight: 42
color: (root.hovered ? root.colBackgroundHover : root.colBackground)
layer.enabled: true
layer.effect: OpacityMask {
@@ -156,8 +158,8 @@ TabButton {
verticalAlignment: Text.AlignVCenter
text: buttonIcon
iconSize: Appearance.font.pixelSize.huge
fill: selected ? 1 : 0
color: selected ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
fill: root.checked ? 1 : 0
color: root.checked ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
@@ -167,7 +169,7 @@ TabButton {
id: buttonTextWidget
verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.small
color: selected ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
color: root.checked ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
text: buttonText
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
@@ -25,7 +25,7 @@ Scope {
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:overlay"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
visible: true
color: "transparent"
@@ -43,6 +43,30 @@ Scope {
right: true
}
HyprlandFocusGrab {
id: grab
windows: [overlayWindow]
active: false
onCleared: () => {
if (!active) GlobalStates.overlayOpen = false;
}
}
Connections {
target: GlobalStates
function onOverlayOpenChanged() {
delayedGrabTimer.restart();
}
}
Timer {
id: delayedGrabTimer
interval: Appearance.animation.elementMoveFast.duration
onTriggered: {
grab.active = GlobalStates.overlayOpen;
}
}
OverlayContent {
id: overlayContent
anchors.fill: parent
@@ -12,6 +12,7 @@ import qs.modules.overlay.crosshair
Item {
id: root
focus: true
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
Keys.onPressed: (event) => { // Esc to close
@@ -6,9 +6,11 @@ Singleton {
id: root
readonly property list<var> availableWidgets: [
{ identifier: "crosshair", materialSymbol: "point_scan" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
{ identifier: "recorder", materialSymbol: "screen_record" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
{ identifier: "crosshair", materialSymbol: "point_scan" },
{ identifier: "fpsLimiter", materialSymbol: "animation" },
{ identifier: "resources", materialSymbol: "browse_activity" }
]
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
@@ -8,7 +8,9 @@ import Quickshell
import Quickshell.Bluetooth
import qs.modules.overlay.crosshair
import qs.modules.overlay.volumeMixer
import qs.modules.overlay.fpsLimiter
import qs.modules.overlay.recorder
import qs.modules.overlay.resources
DelegateChooser {
id: root
@@ -16,5 +18,7 @@ DelegateChooser {
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} }
DelegateChoice { roleValue: "recorder"; Recorder {} }
DelegateChoice { roleValue: "resources"; Resources {} }
}
@@ -21,6 +21,9 @@ AbstractOverlayWidget {
id: root
required property Item contentItem
property bool fancyBorders: true
property bool showCenterButton: false
property bool showClickabilityButton: true
required property var modelData
readonly property string identifier: modelData.identifier
@@ -29,6 +32,8 @@ AbstractOverlayWidget {
property var persistentStateEntry: Persistent.states.overlay[identifier]
property real radius: Appearance.rounding.windowRounding
property real minWidth: 250
property real padding: 6
property real contentRadius: radius - padding
draggable: GlobalStates.overlayOpen
x: Math.round(persistentStateEntry.x) // Round or it'll be blurry
@@ -38,9 +43,10 @@ AbstractOverlayWidget {
drag {
minimumX: 0
minimumY: 0
maximumX: root.parent.width - root.width
maximumY: root.parent.height - root.height
maximumX: root.parent?.width - root.width
maximumY: root.parent?.height - root.height
}
opacity: (GlobalStates.overlayOpen || !clickthrough) ? 1.0 : Config.options.overlay.clickthroughOpacity
// Guarded states & registration funcs
property bool open: Persistent.states.overlay.open
@@ -96,11 +102,15 @@ AbstractOverlayWidget {
Rectangle {
id: border
anchors.fill: parent
color: "transparent"
color: (root.fancyBorders && GlobalStates.overlayOpen) ? Appearance.colors.colLayer1 : "transparent"
radius: root.radius
border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1)
border.width: 1
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
layer.enabled: GlobalStates.overlayOpen
layer.effect: OpacityMask {
maskSource: Rectangle {
@@ -110,25 +120,23 @@ AbstractOverlayWidget {
}
}
Column {
ColumnLayout {
id: contentColumn
z: -1
z: root.fancyBorders ? 0 : -1
anchors.fill: parent
spacing: 0
// Title bar
Rectangle {
id: titleBar
opacity: GlobalStates.overlayOpen ? 1 : 0
anchors {
left: parent.left
right: parent.right
}
property real padding: 2
Layout.fillWidth: true
property real padding: 6
implicitWidth: titleBarRow.implicitWidth + padding * 2
implicitHeight: titleBarRow.implicitHeight + padding * 2
color: Appearance.m3colors.m3surfaceContainer
border.color: Appearance.colors.colOutlineVariant
border.width: 1
color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1
// border.color: Appearance.colors.colOutlineVariant
// border.width: 1
RowLayout {
id: titleBarRow
@@ -136,8 +144,9 @@ AbstractOverlayWidget {
fill: parent
margins: titleBar.padding
leftMargin: titleBar.padding + 8
bottomMargin: root.fancyBorders ? 0 : titleBar.padding
}
spacing: 0
spacing: 2
MaterialSymbol {
text: root.materialSymbol
@@ -153,6 +162,7 @@ AbstractOverlayWidget {
}
TitlebarButton {
visible: root.showCenterButton
materialSymbol: "recenter"
onClicked: root.center()
StyledToolTip {
@@ -161,6 +171,7 @@ AbstractOverlayWidget {
}
TitlebarButton {
visible: (root.pinned && root.showClickabilityButton)
materialSymbol: "mouse"
toggled: !root.clickthrough
onClicked: root.toggleClickthrough()
@@ -191,7 +202,10 @@ AbstractOverlayWidget {
// Content
Item {
id: contentContainer
anchors.horizontalCenter: parent.horizontalCenter
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: root.fancyBorders ? root.padding : 0
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
implicitWidth: root.contentItem.implicitWidth
implicitHeight: root.contentItem.implicitHeight
children: [root.contentItem]
@@ -1,9 +1,18 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.overlay
StyledOverlayWidget {
id: root
contentItem: CrosshairContent {}
fancyBorders: false // Crosshair should be see-through
showCenterButton: true
opacity: 1 // The crosshair itself already has transparency if configured
showClickabilityButton: false
clickthrough: true
contentItem: CrosshairContent {
anchors.centerIn: parent
}
}
@@ -0,0 +1,12 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.overlay
StyledOverlayWidget {
id: root
title: "MangoHud FPS"
contentItem: FpsLimiterContent {
radius: root.contentRadius
}
}
@@ -0,0 +1,97 @@
import qs.services
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.widgets
Rectangle {
id: root
enum State { Normal, Success, Error }
anchors.fill: parent
property real padding: 16
property var currentState: FpsLimiterContent.State.Normal
color: Appearance.m3colors.m3surfaceContainer
implicitWidth: content.implicitWidth + (padding * 2)
implicitHeight: content.implicitHeight + (padding * 2)
Timer {
id: iconResetTimer
interval: 1000
onTriggered: {
root.currentState = FpsLimiterContent.State.Normal;
}
}
function applyLimit() {
var fpsValue = parseInt(fpsField.text);
if (isNaN(fpsValue) || fpsValue < 0) {
root.currentState = FpsLimiterContent.State.Error;
iconResetTimer.restart();
fpsField.text = "";
return;
}
var cfgPaths = [
"~/.config/MangoHud/MangoHud.conf",
]; // MangoHud config files
var updateCommands = cfgPaths.map(path => {
return "if grep -q '^fps_limit=' " + path + "; " +
"then sed -i 's/^fps_limit=.*/fps_limit=" + fpsValue + "/' " + path + "; " +
"else echo 'fps_limit=" + fpsValue + "' >> " + path + "; fi";
}).join("; ");
var cmd = updateCommands + "; pkill -SIGUSR2 mangohud";
fpsSetter.command = ["bash", "-c", cmd];
fpsSetter.startDetached();
root.currentState = FpsLimiterContent.State.Success;
iconResetTimer.restart();
// Clear the field after applying
fpsField.text = "";
}
Process {
id: fpsSetter
}
RowLayout {
id: content
anchors.centerIn: parent
spacing: 4
ToolbarTextField {
id: fpsField
Layout.fillWidth: true
Layout.preferredWidth: 200
placeholderText: root.currentState === FpsLimiterContent.State.Error ? Translation.tr("Enter a valid number") : Translation.tr("Set FPS limit")
inputMethodHints: Qt.ImhDigitsOnly
focus: true
onAccepted: {
root.applyLimit();
}
}
IconToolbarButton {
id: applyButton
text: switch (root.currentState) {
case FpsLimiterContent.State.Error: return "close";
case FpsLimiterContent.State.Success: return "check";
case FpsLimiterContent.State.Normal:
default: return "save";
}
enabled: root.currentState === FpsLimiterContent.State.Normal && fpsField.text.length > 0
onClicked: {
root.applyLimit();
}
}
}
}
@@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import qs
import qs.modules.common
import qs.modules.common.widgets
@@ -14,6 +13,7 @@ StyledOverlayWidget {
contentItem: Rectangle {
id: contentItem
anchors.centerIn: parent
radius: root.contentRadius
color: Appearance.m3colors.m3surfaceContainer
property real padding: 8
implicitHeight: contentColumn.implicitHeight + padding * 2
@@ -0,0 +1,135 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Qt5Compat.GraphicalEffects
import Qt.labs.synchronizer
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.overlay
StyledOverlayWidget {
id: root
property list<var> resources: [
{
"icon": "planner_review",
"name": Translation.tr("CPU"),
"history": ResourceUsage.cpuUsageHistory,
"maxAvailableString": ResourceUsage.maxAvailableCpuString
},
{
"icon": "memory",
"name": Translation.tr("RAM"),
"history": ResourceUsage.memoryUsageHistory,
"maxAvailableString": ResourceUsage.maxAvailableMemoryString
},
{
"icon": "swap_horiz",
"name": Translation.tr("Swap"),
"history": ResourceUsage.swapUsageHistory,
"maxAvailableString": ResourceUsage.maxAvailableSwapString
},
]
contentItem: Rectangle {
id: contentItem
anchors.centerIn: parent
color: Appearance.m3colors.m3surfaceContainer
radius: root.contentRadius
property real padding: 4
implicitWidth: 350
implicitHeight: 200
// implicitHeight: contentColumn.implicitHeight + padding * 2
ColumnLayout {
id: contentColumn
anchors {
fill: parent
margins: parent.padding
}
spacing: 8
SecondaryTabBar {
id: tabBar
currentIndex: Persistent.states.overlay.resources.tabIndex
onCurrentIndexChanged: {
Persistent.states.overlay.resources.tabIndex = tabBar.currentIndex;
}
Repeater {
model: root.resources.length
delegate: SecondaryTabButton {
required property int index
property var modelData: root.resources[index]
buttonIcon: modelData.icon
buttonText: modelData.name
}
}
}
ResourceSummary {
Layout.margins: 8
history: root.resources[tabBar.currentIndex]?.history ?? []
maxAvailableString: root.resources[tabBar.currentIndex]?.maxAvailableString ?? "--"
}
}
}
component ResourceSummary: RowLayout {
id: resourceSummary
required property list<real> history
required property string maxAvailableString
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 12
ColumnLayout {
spacing: 2
StyledText {
text: (resourceSummary.history[resourceSummary.history.length - 1] * 100).toFixed(1) + "%"
font {
family: Appearance.font.family.numbers
variableAxes: Appearance.font.variableAxes.numbers
pixelSize: Appearance.font.pixelSize.huge
}
}
StyledText {
text: Translation.tr("of %1").arg(resourceSummary.maxAvailableString)
font {
// family: Appearance.font.family.numbers
// variableAxes: Appearance.font.variableAxes.numbers
pixelSize: Appearance.font.pixelSize.smallie
}
color: Appearance.colors.colSubtext
}
Item {
Layout.fillHeight: true
}
}
Rectangle {
id: graphBg
Layout.fillWidth: true
Layout.fillHeight: true
radius: Appearance.rounding.small
color: Appearance.colors.colSecondaryContainer
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: graphBg.width
height: graphBg.height
radius: graphBg.radius
}
}
Graph {
anchors.fill: parent
values: root.resources[tabBar.currentIndex]?.history ?? []
points: ResourceUsage.historyLength
alignment: Graph.Alignment.Right
}
}
}
}
@@ -1,7 +1,10 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.overlay
import qs.modules.sidebarRight.volumeMixer
@@ -10,15 +13,49 @@ StyledOverlayWidget {
contentItem: Rectangle {
anchors.centerIn: parent
color: Appearance.m3colors.m3surfaceContainer
radius: root.contentRadius
property real padding: 16
implicitHeight: 600
implicitWidth: 350
VolumeDialogContent {
anchors.fill: parent
anchors.margins: parent.padding
isSink: true
}
ColumnLayout {
id: contentColumn
anchors {
fill: parent
margins: parent.padding
}
spacing: 8
SecondaryTabBar {
id: tabBar
currentIndex: Persistent.states.overlay.volumeMixer.tabIndex
onCurrentIndexChanged: {
Persistent.states.overlay.volumeMixer.tabIndex = tabBar.currentIndex;
}
SecondaryTabButton {
buttonIcon: "media_output"
buttonText: Translation.tr("Output")
}
SecondaryTabButton {
buttonIcon: "mic"
buttonText: Translation.tr("Input")
}
}
SwipeView {
id: swipeView
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: Persistent.states.overlay.volumeMixer.tabIndex
onCurrentIndexChanged: {
Persistent.states.overlay.volumeMixer.tabIndex = swipeView.currentIndex;
}
clip: true
VolumeDialogContent { isSink: true }
VolumeDialogContent { isSink: false }
}
}
}
}
@@ -90,4 +90,6 @@ ContentPage {
}
}
}
@@ -7,6 +7,98 @@ import qs.modules.common.widgets
ContentPage {
forceWidth: true
ContentSection {
icon: "keyboard"
title: Translation.tr("Cheat sheet")
ContentSubsection {
title: Translation.tr("Super key symbol")
tooltip: Translation.tr("You can also manually edit cheatsheet.superKey")
ConfigSelectionArray {
currentValue: Config.options.cheatsheet.superKey
onSelected: newValue => {
Config.options.cheatsheet.superKey = newValue;
}
// Use a nerdfont to see the icons
options: ([
"󰖳", "", "󰨡", "", "󰌽", "󰣇", "", "", "",
"", "", "󱄛", "", "", "⌘", "󰀲", "󰟍", ""
]).map(icon => { return {
displayName: icon,
value: icon
}
})
}
}
ConfigSwitch {
buttonIcon: "󰘵"
text: Translation.tr("Use macOS-like symbols for mods keys")
checked: Config.options.cheatsheet.useMacSymbol
onCheckedChanged: {
Config.options.cheatsheet.useMacSymbol = checked;
}
StyledToolTip {
text: Translation.tr("e.g. 󰘴 for Ctrl, 󰘵 for Alt, 󰘶 for Shift, etc")
}
}
ConfigSwitch {
buttonIcon: "󱊶"
text: Translation.tr("Use symbols for function keys")
checked: Config.options.cheatsheet.useFnSymbol
onCheckedChanged: {
Config.options.cheatsheet.useFnSymbol = checked;
}
StyledToolTip {
text: Translation.tr("e.g. 󱊫 for F1, 󱊶 for F12")
}
}
ConfigSwitch {
buttonIcon: "󰍽"
text: Translation.tr("Use symbols for mouse")
checked: Config.options.cheatsheet.useMouseSymbol
onCheckedChanged: {
Config.options.cheatsheet.useMouseSymbol = checked;
}
StyledToolTip {
text: Translation.tr("Replace 󱕐 for \"Scroll ↓\", 󱕑 \"Scroll ↑\", L󰍽 \"LMB\", R󰍽 \"RMB\", 󱕒 \"Scroll ↑/↓\" and ⇞/⇟ for \"Page_↑/↓\"")
}
}
ConfigSwitch {
buttonIcon: "highlight_keyboard_focus"
text: Translation.tr("Split buttons")
checked: Config.options.cheatsheet.splitButtons
onCheckedChanged: {
Config.options.cheatsheet.splitButtons = checked;
}
StyledToolTip {
text: Translation.tr("Display modifiers and keys in multiple keycap (e.g., \"Ctrl + A\" instead of \"Ctrl A\" or \"󰘴 + A\" instead of \"󰘴 A\")")
}
}
ConfigSpinBox {
text: Translation.tr("Keybind font size")
value: Config.options.cheatsheet.fontSize.key
from: 8
to: 30
stepSize: 1
onValueChanged: {
Config.options.cheatsheet.fontSize.key = value;
}
}
ConfigSpinBox {
text: Translation.tr("Description font size")
value: Config.options.cheatsheet.fontSize.comment
from: 8
to: 30
stepSize: 1
onValueChanged: {
Config.options.cheatsheet.fontSize.comment = value;
}
}
}
ContentSection {
icon: "call_to_action"
title: Translation.tr("Dock")
@@ -647,4 +739,5 @@ ContentPage {
}
}
}
}
@@ -7,7 +7,6 @@ import QtQuick.Layouts
Item {
id: root
property int currentTab: 0
property var tabButtonList: [
{"name": Translation.tr("Pomodoro"), "icon": "search_activity"},
{"name": Translation.tr("Stopwatch"), "icon": "timer"}
@@ -17,20 +16,20 @@ Item {
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) { // Switch tabs
if (event.key === Qt.Key_PageDown) {
currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
tabBar.incrementCurrentIndex();
} else if (event.key === Qt.Key_PageUp) {
currentTab = Math.max(currentTab - 1, 0)
tabBar.decrementCurrentIndex();
}
event.accepted = true
} else if (event.key === Qt.Key_Space || event.key === Qt.Key_S) { // Pause/resume with Space or S
if (currentTab === 0) {
if (tabBar.currentIndex === 0) {
TimerService.togglePomodoro()
} else {
TimerService.toggleStopwatch()
}
event.accepted = true
} else if (event.key === Qt.Key_R) { // Reset with R
if (currentTab === 0) {
if (tabBar.currentIndex === 0) {
TimerService.resetPomodoro()
} else {
TimerService.stopwatchReset()
@@ -46,82 +45,19 @@ Item {
anchors.fill: parent
spacing: 0
TabBar {
SecondaryTabBar {
id: tabBar
Layout.fillWidth: true
currentIndex: currentTab
onCurrentIndexChanged: currentTab = currentIndex
background: Item {
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
else if (event.angleDelta.y > 0)
tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
}
currentIndex: swipeView.currentIndex
Repeater {
model: root.tabButtonList
delegate: SecondaryTabButton {
selected: (index == currentTab)
buttonText: modelData.name
buttonIcon: modelData.icon
}
}
}
Item { // Tab indicator
id: tabIndicator
Layout.fillWidth: true
height: 3
property bool enableIndicatorAnimation: false
Connections {
target: root
function onCurrentTabChanged() {
tabIndicator.enableIndicatorAnimation = true
}
}
Rectangle {
id: indicator
property int tabCount: root.tabButtonList.length
property real fullTabSize: root.width / tabCount;
property real targetWidth: tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth
implicitWidth: targetWidth
anchors {
top: parent.top
bottom: parent.bottom
}
x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
color: Appearance.colors.colPrimary
radius: Appearance.rounding.full
Behavior on x {
enabled: tabIndicator.enableIndicatorAnimation
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on implicitWidth {
enabled: tabIndicator.enableIndicatorAnimation
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
Rectangle { // Tabbar bottom border
id: tabBarBottomBorder
Layout.fillWidth: true
height: 1
color: Appearance.colors.colOutlineVariant
}
SwipeView {
id: swipeView
Layout.topMargin: 10
@@ -129,11 +65,7 @@ Item {
Layout.fillHeight: true
spacing: 10
clip: true
currentIndex: currentTab
onCurrentIndexChanged: {
tabIndicator.enableIndicatorAnimation = true
currentTab = currentIndex
}
currentIndex: tabBar.currentIndex
// Tabs
PomodoroTimer {}
@@ -7,7 +7,6 @@ import QtQuick.Layouts
Item {
id: root
property int currentTab: 0
property var tabButtonList: [{"icon": "checklist", "name": Translation.tr("Unfinished")}, {"name": Translation.tr("Done"), "icon": "check_circle"}]
property bool showAddDialog: false
property int dialogMargins: 20
@@ -17,9 +16,9 @@ Item {
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageDown) {
currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
tabBar.incrementCurrentIndex();
} else if (event.key === Qt.Key_PageUp) {
currentTab = Math.max(currentTab - 1, 0)
tabBar.decrementCurrentIndex();
}
event.accepted = true;
}
@@ -39,82 +38,19 @@ Item {
anchors.fill: parent
spacing: 0
TabBar {
SecondaryTabBar {
id: tabBar
Layout.fillWidth: true
currentIndex: currentTab
onCurrentIndexChanged: currentTab = currentIndex
background: Item {
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
else if (event.angleDelta.y > 0)
tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
}
currentIndex: swipeView.currentIndex
Repeater {
model: root.tabButtonList
delegate: SecondaryTabButton {
selected: (index == currentTab)
buttonText: modelData.name
buttonIcon: modelData.icon
}
}
}
Item { // Tab indicator
id: tabIndicator
Layout.fillWidth: true
height: 3
property bool enableIndicatorAnimation: false
Connections {
target: root
function onCurrentTabChanged() {
tabIndicator.enableIndicatorAnimation = true
}
}
Rectangle {
id: indicator
property int tabCount: root.tabButtonList.length
property real fullTabSize: root.width / tabCount;
property real targetWidth: tabBar?.contentItem?.children[0]?.children[tabBar.currentIndex]?.tabContentWidth ?? 0
implicitWidth: targetWidth
anchors {
top: parent.top
bottom: parent.bottom
}
x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
color: Appearance.colors.colPrimary
radius: Appearance.rounding.full
Behavior on x {
enabled: tabIndicator.enableIndicatorAnimation
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on implicitWidth {
enabled: tabIndicator.enableIndicatorAnimation
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
Rectangle { // Tabbar bottom border
id: tabBarBottomBorder
Layout.fillWidth: true
height: 1
color: Appearance.colors.colOutlineVariant
}
SwipeView {
id: swipeView
Layout.topMargin: 10
@@ -122,11 +58,7 @@ Item {
Layout.fillHeight: true
spacing: 10
clip: true
currentIndex: currentTab
onCurrentIndexChanged: {
tabIndicator.enableIndicatorAnimation = true
currentTab = currentIndex
}
currentIndex: tabBar.currentIndex
// To Do tab
TaskList {
@@ -215,7 +147,7 @@ Item {
Todo.addTask(todoInput.text)
todoInput.text = ""
root.showAddDialog = false
root.currentTab = 0 // Show unfinished tasks
tabBar.setCurrentIndex(0) // Show unfinished tasks
}
}
@@ -29,6 +29,7 @@ ColumnLayout {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
color: Appearance.colors.colOutlineVariant
}
DialogSectionListView {
@@ -56,6 +57,7 @@ ColumnLayout {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
color: Appearance.colors.colOutlineVariant
}
DialogSectionListView {
@@ -28,10 +28,10 @@ Item {
sourceSize.height: size
source: {
let icon;
icon = AppSearch.guessIcon(root.node.properties["application.icon-name"]);
icon = AppSearch.guessIcon(root.node?.properties["application.icon-name"] ?? "");
if (AppSearch.iconExists(icon))
return Quickshell.iconPath(icon, "image-missing");
icon = AppSearch.guessIcon(root.node.properties["node.name"]);
icon = AppSearch.guessIcon(root.node?.properties["node.name"] ?? "");
return Quickshell.iconPath(icon, "image-missing");
}
}
@@ -47,7 +47,7 @@ Item {
elide: Text.ElideRight
text: {
// application.name -> description -> name
const app = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name);
const app = root.node?.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name);
const media = root.node.properties["media.name"];
return media != undefined ? `${app} ${media}` : app;
}
@@ -55,7 +55,7 @@ Item {
StyledSlider {
id: slider
value: root.node.audio.volume
value: root.node?.audio.volume ?? 0
onMoved: root.node.audio.volume = value
configuration: StyledSlider.Configuration.S
}
@@ -10,17 +10,55 @@ import Quickshell.Io
* Simple polled resource usage service with RAM, Swap, and CPU usage.
*/
Singleton {
property double memoryTotal: 1
property double memoryFree: 1
property double memoryUsed: memoryTotal - memoryFree
property double memoryUsedPercentage: memoryUsed / memoryTotal
property double swapTotal: 1
property double swapFree: 1
property double swapUsed: swapTotal - swapFree
property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0
property double cpuUsage: 0
id: root
property real memoryTotal: 1
property real memoryFree: 0
property real memoryUsed: memoryTotal - memoryFree
property real memoryUsedPercentage: memoryUsed / memoryTotal
property real swapTotal: 1
property real swapFree: 0
property real swapUsed: swapTotal - swapFree
property real swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0
property real cpuUsage: 0
property var previousCpuStats
property string maxAvailableMemoryString: kbToGbString(ResourceUsage.memoryTotal)
property string maxAvailableSwapString: kbToGbString(ResourceUsage.swapTotal)
property string maxAvailableCpuString: "--"
readonly property int historyLength: Config?.options.resources.historyLength ?? 60
property list<real> cpuUsageHistory: []
property list<real> memoryUsageHistory: []
property list<real> swapUsageHistory: []
function kbToGbString(kb) {
return (kb / (1024 * 1024)).toFixed(1) + " GB";
}
function updateMemoryUsageHistory() {
memoryUsageHistory = [...memoryUsageHistory, memoryUsedPercentage]
if (memoryUsageHistory.length > historyLength) {
memoryUsageHistory.shift()
}
}
function updateSwapUsageHistory() {
swapUsageHistory = [...swapUsageHistory, swapUsedPercentage]
if (swapUsageHistory.length > historyLength) {
swapUsageHistory.shift()
}
}
function updateCpuUsageHistory() {
cpuUsageHistory = [...cpuUsageHistory, cpuUsage]
if (cpuUsageHistory.length > historyLength) {
cpuUsageHistory.shift()
}
}
function updateHistories() {
updateMemoryUsageHistory()
updateSwapUsageHistory()
updateCpuUsageHistory()
}
Timer {
interval: 1
running: true
@@ -53,10 +91,24 @@ Singleton {
previousCpuStats = { total, idle }
}
root.updateHistories()
interval = Config.options?.resources?.updateInterval ?? 3000
}
}
FileView { id: fileMeminfo; path: "/proc/meminfo" }
FileView { id: fileStat; path: "/proc/stat" }
Process {
id: findCpuMaxFreqProc
command: ["bash", "-c", "lscpu | grep 'CPU max MHz' | awk '{print $4}'"]
running: true
stdout: StdioCollector {
id: outputCollector
onStreamFinished: {
root.maxAvailableCpuString = (parseFloat(outputCollector.text) / 1000).toFixed(0) + " GHz"
}
}
}
}