me like nix
0

Configure Feed

Select the types of activity you want to include in your feed.

add quickshell stuff

author
Sean Aye
date (Mar 22, 2026, 8:03 PM -0400) commit d5444a9f parent 87169ad1 change-id qyksomnq
+3421 -535
+9
hosts/common/common.nix
··· 2 2 3 3 let 4 4 berkeley-mono-typeface = pkgs.callPackage ../../berkely-mono/berkeley.nix { }; 5 + 6 + # Steam/gamescope calls steamos-session-select when the user presses 7 + # "Switch to Desktop". Without this script, the button does nothing. 8 + # Returning 0 lets gamescope proceed to exit, returning to greetd/regreet. 9 + steamos-session-select = pkgs.writeShellScriptBin "steamos-session-select" '' 10 + echo "Switching session to: $1" 11 + ''; 5 12 in 6 13 { 7 14 ··· 190 197 191 198 programs.gamescope = { 192 199 enable = true; 200 + capSysNice = true; 193 201 }; 194 202 195 203 programs.fish.enable = true; ··· 208 216 # $ nix search wget 209 217 environment.systemPackages = with pkgs; [ 210 218 wl-clipboard 219 + steamos-session-select 211 220 ]; 212 221 environment.variables = { 213 222 EDITOR = "hx";
+44 -1
hosts/common/home.nix
··· 21 21 jujutsu # jj-cli 22 22 htop 23 23 iotop 24 + ncdu 25 + youtube-tui 26 + yt-dlp # youtube-tui and mpv need this to resolve YouTube URLs 27 + mpv # video player 24 28 zellij # terminal multiplexer 25 29 alacritty 26 30 inputs.fsel.packages.${pkgs.system}.default # App launcher / fuzzy finder ··· 38 42 ripgrep 39 43 yazi # tui file browser 40 44 gh # github cli 45 + gh-dash # github dashboard TUI 46 + diffnav # git diff viewer 41 47 signal-desktop 42 48 xwayland-satellite # for running x11 apps 43 49 nixfmt # nix formatter ··· 364 370 ''; 365 371 366 372 # Quickshell status bar 367 - xdg.configFile."quickshell/shell.qml".source = ./quickshell/shell.qml; 373 + xdg.configFile."quickshell" = { 374 + source = ./quickshell; 375 + recursive = true; 376 + }; 368 377 369 378 systemd.user.services.quickshell = { 370 379 Unit = { ··· 511 520 }; 512 521 }; 513 522 523 + xdg.configFile."zellij/layouts/split.kdl".text = '' 524 + layout { 525 + tab { 526 + pane size="50%" 527 + pane split_direction="vertical" size="50%" { 528 + pane 529 + pane 530 + } 531 + } 532 + } 533 + ''; 534 + 535 + xdg.configFile."gh-dash/config.yml".text = '' 536 + prSections: 537 + - title: My Pull Requests 538 + filters: is:open author:@me 539 + - title: Review Requested 540 + filters: is:open review-requested:@me 541 + issuesSections: 542 + - title: My Issues 543 + filters: is:open author:@me 544 + pager: 545 + diff: diffnav 546 + keybindings: 547 + prs: 548 + - key: T 549 + name: enhance 550 + command: >- 551 + zellij run -- gh enhance -R {{.RepoName}} {{.PrNumber}} 552 + ''; 553 + 514 554 programs.zen-browser.enable = true; 515 555 # programs.swww.enable = true; 516 556 programs.zoxide = { ··· 538 578 gpg.format = "ssh"; 539 579 user.signingKey = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOIgEteUEW06dnBHe2z8vNLwz2iMKe8bba6JgMmOUpcBAAAABHNzaDo= sean@framework16"; 540 580 gpg.ssh.allowedSignersFile = "${config.home.homeDirectory}/.ssh/allowed_signers"; 581 + diff.tool = "diffnav"; 582 + difftool.prompt = false; 583 + "difftool \"diffnav\"".cmd = "diffnav \"$LOCAL\" \"$REMOTE\""; 541 584 }; 542 585 }; 543 586 programs.jujutsu = {
+8
hosts/common/quickshell/components/Anim.qml
··· 1 + import QtQuick 2 + import "../config" as Config 3 + 4 + NumberAnimation { 5 + duration: Config.Appearance.anim.durations.normal 6 + easing.type: Easing.BezierSpline 7 + easing.bezierCurve: Config.Appearance.anim.curves.standard 8 + }
+8
hosts/common/quickshell/components/CAnim.qml
··· 1 + import QtQuick 2 + import "../config" as Config 3 + 4 + ColorAnimation { 5 + duration: Config.Appearance.anim.durations.normal 6 + easing.type: Easing.BezierSpline 7 + easing.bezierCurve: Config.Appearance.anim.curves.standard 8 + }
+33
hosts/common/quickshell/components/StateLayer.qml
··· 1 + import QtQuick 2 + import "../config" as Config 3 + 4 + Rectangle { 5 + id: stateLayer 6 + 7 + property alias hoverEnabled: mouseArea.hoverEnabled 8 + property alias containsMouse: mouseArea.containsMouse 9 + property alias acceptedButtons: mouseArea.acceptedButtons 10 + property alias cursorShape: mouseArea.cursorShape 11 + 12 + signal clicked(var mouse) 13 + signal entered() 14 + signal exited() 15 + 16 + color: Config.Colours.overlay0 17 + opacity: mouseArea.containsMouse ? (mouseArea.pressed ? 0.16 : 0.08) : 0.0 18 + radius: parent.radius 19 + 20 + Behavior on opacity { 21 + Anim { duration: Config.Appearance.anim.durations.small } 22 + } 23 + 24 + MouseArea { 25 + id: mouseArea 26 + anchors.fill: parent 27 + hoverEnabled: true 28 + cursorShape: Qt.PointingHandCursor 29 + onClicked: mouse => stateLayer.clicked(mouse) 30 + onEntered: stateLayer.entered() 31 + onExited: stateLayer.exited() 32 + } 33 + }
+7
hosts/common/quickshell/components/StyledRect.qml
··· 1 + import QtQuick 2 + import "../config" as Config 3 + 4 + Rectangle { 5 + color: Config.Colours.surface0 6 + radius: Config.Appearance.rounding.normal 7 + }
+8
hosts/common/quickshell/components/StyledText.qml
··· 1 + import QtQuick 2 + import "../config" as Config 3 + 4 + Text { 5 + color: Config.Colours.text 6 + font.family: Config.Appearance.font.family 7 + font.pixelSize: Config.Appearance.font.size.normal 8 + }
+5
hosts/common/quickshell/components/qmldir
··· 1 + Anim 1.0 Anim.qml 2 + CAnim 1.0 CAnim.qml 3 + StyledRect 1.0 StyledRect.qml 4 + StyledText 1.0 StyledText.qml 5 + StateLayer 1.0 StateLayer.qml
+66
hosts/common/quickshell/config/Appearance.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + 5 + QtObject { 6 + readonly property QtObject rounding: QtObject { 7 + readonly property int small: 4 8 + readonly property int normal: 8 9 + readonly property int large: 12 10 + readonly property int full: 999 11 + } 12 + 13 + readonly property QtObject spacing: QtObject { 14 + readonly property int small: 4 15 + readonly property int normal: 8 16 + readonly property int large: 12 17 + } 18 + 19 + readonly property QtObject padding: QtObject { 20 + readonly property int small: 4 21 + readonly property int normal: 8 22 + readonly property int large: 12 23 + } 24 + 25 + readonly property QtObject font: QtObject { 26 + readonly property string family: "BerkeleyMono Nerd Font" 27 + readonly property QtObject size: QtObject { 28 + readonly property int small: 11 29 + readonly property int normal: 14 30 + readonly property int large: 18 31 + } 32 + } 33 + 34 + readonly property QtObject anim: QtObject { 35 + // Legacy aliases (match old short_/normal/long_ naming) 36 + readonly property int short_: durations.small 37 + readonly property int normal: durations.normal 38 + readonly property int long_: durations.large 39 + 40 + // Legacy easing type (for any remaining references) 41 + readonly property int type: Easing.BezierSpline 42 + 43 + readonly property QtObject durations: QtObject { 44 + readonly property int small: 200 45 + readonly property int normal: 400 46 + readonly property int large: 600 47 + readonly property int extraLarge: 1000 48 + readonly property int expressiveFastSpatial: 350 49 + readonly property int expressiveDefaultSpatial: 500 50 + } 51 + 52 + readonly property QtObject curves: QtObject { 53 + readonly property var emphasized: [0.05, 0, 2/15, 0.06, 1/6, 0.4, 5/24, 0.82, 0.25, 1, 1, 1] 54 + readonly property var emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] 55 + readonly property var emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] 56 + readonly property var standard: [0.2, 0, 0, 1, 1, 1] 57 + readonly property var standardAccel: [0.3, 0, 1, 1, 1, 1] 58 + readonly property var standardDecel: [0, 0, 0, 1, 1, 1] 59 + readonly property var expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] 60 + readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] 61 + readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] 62 + } 63 + } 64 + 65 + readonly property real barOpacity: 0.85 66 + }
+34
hosts/common/quickshell/config/Colours.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + 5 + QtObject { 6 + // Catppuccin Frappe palette 7 + readonly property color rosewater: "#f2d5cf" 8 + readonly property color flamingo: "#eebebe" 9 + readonly property color pink: "#f4b8e4" 10 + readonly property color mauve: "#ca9ee6" 11 + readonly property color red: "#e78284" 12 + readonly property color maroon: "#ea999c" 13 + readonly property color peach: "#ef9f76" 14 + readonly property color yellow: "#e5c890" 15 + readonly property color green: "#a6d189" 16 + readonly property color teal: "#81c8be" 17 + readonly property color sky: "#99d1db" 18 + readonly property color sapphire: "#85c1dc" 19 + readonly property color blue: "#8caaee" 20 + readonly property color lavender: "#babbf1" 21 + 22 + readonly property color text: "#c6d0f5" 23 + readonly property color subtext1: "#b5bfe2" 24 + readonly property color subtext0: "#a5adce" 25 + readonly property color overlay2: "#949cbb" 26 + readonly property color overlay1: "#838ba7" 27 + readonly property color overlay0: "#737994" 28 + readonly property color surface2: "#626880" 29 + readonly property color surface1: "#51576d" 30 + readonly property color surface0: "#414559" 31 + readonly property color base: "#303446" 32 + readonly property color mantle: "#292c3c" 33 + readonly property color crust: "#232634" 34 + }
+2
hosts/common/quickshell/config/qmldir
··· 1 + singleton Colours 1.0 Colours.qml 2 + singleton Appearance 1.0 Appearance.qml
+238
hosts/common/quickshell/modules/dashboard/Dashboard.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import "../../config" as Config 5 + import "../../components" 6 + import "../../services" as Services 7 + import "tabs" 8 + 9 + PanelWindow { 10 + id: topTrigger 11 + 12 + required property var modelData 13 + screen: modelData 14 + 15 + anchors { 16 + top: true 17 + left: true 18 + right: true 19 + } 20 + margins.left: 60 21 + margins.right: 8 22 + margins.top: 0 23 + implicitHeight: 2 24 + exclusiveZone: 0 25 + color: "transparent" 26 + 27 + property bool dashboardOpen: false 28 + property int activeTab: 0 29 + 30 + MouseArea { 31 + anchors.fill: parent 32 + hoverEnabled: true 33 + onEntered: { closeTimer.stop(); topTrigger.dashboardOpen = true } 34 + onExited: closeTimer.restart() 35 + } 36 + 37 + onDashboardOpenChanged: { 38 + if (dashboardOpen) { 39 + contentRect._opening = true 40 + fadeInTimer.start() 41 + } else { 42 + fadeInTimer.stop() 43 + contentRect._opening = false 44 + contentRect.opacity = 0 45 + activeTab = 0 46 + } 47 + } 48 + 49 + Timer { 50 + id: closeTimer 51 + interval: 400 52 + onTriggered: topTrigger.dashboardOpen = false 53 + } 54 + 55 + PopupWindow { 56 + id: dashPanel 57 + anchor.window: topTrigger 58 + anchor.onAnchoring: { 59 + anchor.rect.x = (topTrigger.width - implicitWidth) / 2 60 + anchor.rect.y = topTrigger.height + 4 61 + } 62 + 63 + visible: topTrigger.dashboardOpen || contentRect.opacity > 0 64 + implicitWidth: Math.min(topTrigger.width, 900) 65 + implicitHeight: contentCol.implicitHeight + 32 66 + color: "transparent" 67 + 68 + Timer { 69 + id: fadeInTimer 70 + interval: 16 71 + onTriggered: contentRect.opacity = 1 72 + } 73 + 74 + Rectangle { 75 + id: contentRect 76 + anchors.fill: parent 77 + color: Config.Colours.base 78 + radius: Config.Appearance.rounding.large 79 + border.width: 1 80 + border.color: Config.Colours.surface0 81 + opacity: 0 82 + 83 + property bool _opening: false 84 + Behavior on opacity { 85 + Anim { 86 + duration: contentRect._opening ? Config.Appearance.anim.durations.normal : Config.Appearance.anim.durations.small 87 + easing.bezierCurve: contentRect._opening ? Config.Appearance.anim.curves.standardDecel : Config.Appearance.anim.curves.standardAccel 88 + } 89 + } 90 + 91 + HoverHandler { 92 + onHoveredChanged: { 93 + if (hovered) closeTimer.stop() 94 + else closeTimer.restart() 95 + } 96 + } 97 + 98 + ColumnLayout { 99 + id: contentCol 100 + anchors.fill: parent 101 + anchors.margins: 16 102 + spacing: 0 103 + 104 + // Tab bar 105 + RowLayout { 106 + id: tabBar 107 + Layout.fillWidth: true 108 + spacing: 0 109 + 110 + Repeater { 111 + model: [ 112 + { icon: "\uf0e4", label: "Dashboard" }, 113 + { icon: "\uf001", label: "Media" }, 114 + { icon: "\uf2db", label: "Performance" }, 115 + { icon: "\uf0c2", label: "Weather" } 116 + ] 117 + delegate: Rectangle { 118 + required property var modelData 119 + required property int index 120 + Layout.fillWidth: true 121 + height: 36 122 + radius: Config.Appearance.rounding.normal 123 + color: topTrigger.activeTab === index 124 + ? Config.Colours.surface0 125 + : (tabMouse.containsMouse ? Config.Colours.surface0 : "transparent") 126 + opacity: topTrigger.activeTab === index ? 1.0 : (tabMouse.containsMouse ? 0.7 : 0.5) 127 + Behavior on color { CAnim {} } 128 + Behavior on opacity { Anim { duration: Config.Appearance.anim.durations.small } } 129 + 130 + RowLayout { 131 + anchors.centerIn: parent 132 + spacing: 6 133 + Text { 134 + color: topTrigger.activeTab === index ? Config.Colours.blue : Config.Colours.subtext0 135 + font.family: Config.Appearance.font.family 136 + font.pixelSize: 14 137 + text: modelData.icon 138 + } 139 + Text { 140 + color: topTrigger.activeTab === index ? Config.Colours.text : Config.Colours.subtext0 141 + font.family: Config.Appearance.font.family 142 + font.pixelSize: Config.Appearance.font.size.small 143 + text: modelData.label 144 + } 145 + } 146 + 147 + MouseArea { 148 + id: tabMouse 149 + anchors.fill: parent 150 + hoverEnabled: true 151 + cursorShape: Qt.PointingHandCursor 152 + onClicked: topTrigger.activeTab = index 153 + } 154 + } 155 + } 156 + } 157 + 158 + // Tab indicator 159 + Rectangle { 160 + Layout.fillWidth: true 161 + height: 2 162 + color: "transparent" 163 + Layout.topMargin: 4 164 + Layout.bottomMargin: 8 165 + 166 + Rectangle { 167 + height: 2 168 + radius: 1 169 + color: Config.Colours.blue 170 + width: parent.width / 4 - 16 171 + x: (parent.width / 4) * topTrigger.activeTab + 8 172 + Behavior on x { 173 + Anim { 174 + duration: Config.Appearance.anim.durations.normal 175 + easing.bezierCurve: Config.Appearance.anim.curves.emphasized 176 + } 177 + } 178 + } 179 + } 180 + 181 + // Tab content 182 + Item { 183 + id: tabContent 184 + Layout.fillWidth: true 185 + clip: true 186 + implicitHeight: { 187 + if (topTrigger.activeTab === 0) return dashTab.implicitHeight 188 + if (topTrigger.activeTab === 1) return mediaTab.implicitHeight 189 + if (topTrigger.activeTab === 2) return perfTab.implicitHeight 190 + return weatherTab.implicitHeight 191 + } 192 + 193 + DashTab { 194 + id: dashTab 195 + width: parent.width 196 + visible: topTrigger.activeTab === 0 197 + opacity: visible ? 1 : 0 198 + isOpen: topTrigger.dashboardOpen 199 + Behavior on opacity { Anim { duration: Config.Appearance.anim.durations.small } } 200 + } 201 + 202 + MediaTab { 203 + id: mediaTab 204 + width: parent.width 205 + visible: topTrigger.activeTab === 1 206 + opacity: visible ? 1 : 0 207 + Behavior on opacity { Anim { duration: Config.Appearance.anim.durations.small } } 208 + } 209 + 210 + PerformanceTab { 211 + id: perfTab 212 + width: parent.width 213 + visible: topTrigger.activeTab === 2 214 + opacity: visible ? 1 : 0 215 + Behavior on opacity { Anim { duration: Config.Appearance.anim.durations.small } } 216 + } 217 + 218 + WeatherTab { 219 + id: weatherTab 220 + width: parent.width 221 + visible: topTrigger.activeTab === 3 222 + opacity: visible ? 1 : 0 223 + Behavior on opacity { Anim { duration: Config.Appearance.anim.durations.small } } 224 + } 225 + } 226 + } 227 + 228 + Keys.onEscapePressed: topTrigger.dashboardOpen = false 229 + } 230 + 231 + // Click outside to close 232 + MouseArea { 233 + anchors.fill: parent 234 + z: -1 235 + onClicked: { closeTimer.stop(); topTrigger.dashboardOpen = false } 236 + } 237 + } 238 + }
+1
hosts/common/quickshell/modules/dashboard/qmldir
··· 1 + Dashboard 1.0 Dashboard.qml
+364
hosts/common/quickshell/modules/dashboard/tabs/DashTab.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import "../../../config" as Config 4 + import "../../../components" 5 + import "../../../services" as Services 6 + 7 + Item { 8 + id: root 9 + 10 + property bool isOpen: false 11 + implicitHeight: mainLayout.implicitHeight 12 + 13 + property int calYear: new Date().getFullYear() 14 + property int calMonth: new Date().getMonth() 15 + property var calModel: calendarDays() 16 + 17 + onIsOpenChanged: if (isOpen) resetMonth() 18 + 19 + function calendarDays() { 20 + var firstDay = new Date(calYear, calMonth, 1).getDay() 21 + var daysInMonth = new Date(calYear, calMonth + 1, 0).getDate() 22 + var prevDays = new Date(calYear, calMonth, 0).getDate() 23 + var today = new Date() 24 + var isCurrentMonth = (calYear === today.getFullYear() && calMonth === today.getMonth()) 25 + var days = [] 26 + for (var i = 0; i < 42; i++) { 27 + var dayNum = i - firstDay + 1 28 + if (dayNum < 1) 29 + days.push({ day: prevDays + dayNum, inMonth: false, isToday: false }) 30 + else if (dayNum > daysInMonth) 31 + days.push({ day: dayNum - daysInMonth, inMonth: false, isToday: false }) 32 + else 33 + days.push({ day: dayNum, inMonth: true, isToday: isCurrentMonth && dayNum === today.getDate() }) 34 + } 35 + return days 36 + } 37 + 38 + function prevMonth() { 39 + if (calMonth === 0) { calMonth = 11; calYear-- } 40 + else calMonth-- 41 + calModel = calendarDays() 42 + } 43 + 44 + function nextMonth() { 45 + if (calMonth === 11) { calMonth = 0; calYear++ } 46 + else calMonth++ 47 + calModel = calendarDays() 48 + } 49 + 50 + function resetMonth() { 51 + var now = new Date() 52 + calYear = now.getFullYear() 53 + calMonth = now.getMonth() 54 + calModel = calendarDays() 55 + } 56 + 57 + // Resource bar component 58 + component ResourceBar: ColumnLayout { 59 + property string icon 60 + property string label 61 + property string valueText 62 + property real pct: 0 63 + property color barColor 64 + 65 + Layout.fillWidth: true 66 + spacing: 2 67 + 68 + // Animated value that smoothly transitions between updates 69 + property real animatedPct: pct 70 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 71 + 72 + RowLayout { 73 + Layout.fillWidth: true 74 + Text { color: barColor; font.family: Config.Appearance.font.family; font.pixelSize: 12; text: icon } 75 + Text { color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: 12; text: label } 76 + Item { Layout.fillWidth: true } 77 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: 12; text: valueText } 78 + } 79 + 80 + Item { 81 + Layout.fillWidth: true 82 + height: 4 83 + Rectangle { anchors.fill: parent; radius: 2; color: Config.Colours.surface1 } 84 + Rectangle { 85 + width: parent.width * (animatedPct / 100) 86 + height: parent.height; radius: 2 87 + color: barColor 88 + } 89 + } 90 + } 91 + 92 + RowLayout { 93 + id: mainLayout 94 + width: parent.width 95 + spacing: 16 96 + 97 + // Left: DateTime 98 + ColumnLayout { 99 + Layout.preferredWidth: 80 100 + Layout.fillHeight: true 101 + Layout.alignment: Qt.AlignVCenter 102 + spacing: 0 103 + 104 + Text { 105 + id: dashHour 106 + Layout.alignment: Qt.AlignHCenter 107 + color: Config.Colours.blue 108 + font.family: Config.Appearance.font.family 109 + font.pixelSize: 48 110 + font.bold: true 111 + text: Qt.formatDateTime(new Date(), "HH") 112 + } 113 + 114 + Text { 115 + Layout.alignment: Qt.AlignHCenter 116 + color: Config.Colours.blue 117 + font.family: Config.Appearance.font.family 118 + font.pixelSize: 14 119 + text: "\u2022\u2022\u2022" 120 + } 121 + 122 + Text { 123 + id: dashMin 124 + Layout.alignment: Qt.AlignHCenter 125 + color: Config.Colours.blue 126 + font.family: Config.Appearance.font.family 127 + font.pixelSize: 48 128 + font.bold: true 129 + text: Qt.formatDateTime(new Date(), "mm") 130 + } 131 + 132 + Text { 133 + Layout.alignment: Qt.AlignHCenter 134 + Layout.topMargin: 4 135 + color: Config.Colours.subtext0 136 + font.family: Config.Appearance.font.family 137 + font.pixelSize: Config.Appearance.font.size.small 138 + text: Qt.formatDateTime(new Date(), "dddd") 139 + } 140 + 141 + Text { 142 + Layout.alignment: Qt.AlignHCenter 143 + color: Config.Colours.overlay0 144 + font.family: Config.Appearance.font.family 145 + font.pixelSize: Config.Appearance.font.size.small 146 + text: Qt.formatDateTime(new Date(), "MMMM d, yyyy") 147 + } 148 + 149 + Timer { 150 + interval: 1000 151 + running: root.isOpen 152 + repeat: true 153 + onTriggered: { 154 + var now = new Date() 155 + dashHour.text = Qt.formatDateTime(now, "HH") 156 + dashMin.text = Qt.formatDateTime(now, "mm") 157 + } 158 + } 159 + } 160 + 161 + Rectangle { width: 1; Layout.fillHeight: true; color: Config.Colours.surface1 } 162 + 163 + // Center: Calendar 164 + ColumnLayout { 165 + Layout.fillWidth: true 166 + Layout.fillHeight: true 167 + spacing: 4 168 + 169 + RowLayout { 170 + Layout.fillWidth: true 171 + 172 + Text { 173 + color: Config.Colours.overlay0 174 + font.family: Config.Appearance.font.family 175 + font.pixelSize: 16 176 + text: "\uf053" 177 + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.prevMonth() } 178 + } 179 + Item { Layout.fillWidth: true } 180 + Text { 181 + color: Config.Colours.text 182 + font.family: Config.Appearance.font.family 183 + font.pixelSize: Config.Appearance.font.size.normal 184 + font.bold: true 185 + text: new Date(root.calYear, root.calMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") 186 + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.resetMonth() } 187 + } 188 + Item { Layout.fillWidth: true } 189 + Text { 190 + color: Config.Colours.overlay0 191 + font.family: Config.Appearance.font.family 192 + font.pixelSize: 16 193 + text: "\uf054" 194 + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.nextMonth() } 195 + } 196 + } 197 + 198 + Row { 199 + Layout.fillWidth: true 200 + Repeater { 201 + model: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] 202 + delegate: Text { 203 + required property string modelData 204 + width: parent.width / 7 205 + horizontalAlignment: Text.AlignHCenter 206 + color: Config.Colours.overlay0 207 + font.family: Config.Appearance.font.family 208 + font.pixelSize: 11 209 + text: modelData 210 + } 211 + } 212 + } 213 + 214 + Grid { 215 + Layout.fillWidth: true 216 + columns: 7 217 + rowSpacing: 2 218 + 219 + Repeater { 220 + model: root.calModel 221 + delegate: Item { 222 + required property var modelData 223 + width: parent.width / 7 224 + height: 28 225 + 226 + Rectangle { 227 + anchors.centerIn: parent 228 + width: 24; height: 24; radius: 12 229 + color: modelData.isToday ? Config.Colours.blue : "transparent" 230 + } 231 + 232 + Text { 233 + anchors.centerIn: parent 234 + color: modelData.isToday ? Config.Colours.crust 235 + : (modelData.inMonth ? Config.Colours.text : Config.Colours.overlay0) 236 + font.family: Config.Appearance.font.family 237 + font.pixelSize: 12 238 + font.bold: modelData.isToday 239 + text: modelData.day 240 + } 241 + } 242 + } 243 + } 244 + } 245 + 246 + Rectangle { width: 1; Layout.fillHeight: true; color: Config.Colours.surface1 } 247 + 248 + // Right: Weather + Resources + Power 249 + ColumnLayout { 250 + Layout.preferredWidth: 200 251 + Layout.fillHeight: true 252 + spacing: 10 253 + 254 + // Weather 255 + RowLayout { 256 + Layout.fillWidth: true 257 + spacing: 8 258 + visible: Services.Weather.weather !== "" 259 + 260 + Text { 261 + color: Config.Colours.yellow 262 + font.family: Config.Appearance.font.family 263 + font.pixelSize: 20 264 + text: "\uf0eb" 265 + } 266 + Text { 267 + Layout.fillWidth: true 268 + color: Config.Colours.text 269 + font.family: Config.Appearance.font.family 270 + font.pixelSize: Config.Appearance.font.size.normal 271 + text: Services.Weather.weather 272 + wrapMode: Text.WordWrap 273 + } 274 + } 275 + 276 + Rectangle { 277 + Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 278 + visible: Services.Weather.weather !== "" 279 + } 280 + 281 + // Resources 282 + Text { 283 + color: Config.Colours.text 284 + font.family: Config.Appearance.font.family 285 + font.pixelSize: Config.Appearance.font.size.normal 286 + font.bold: true 287 + text: "Resources" 288 + } 289 + 290 + ResourceBar { 291 + icon: "\uf2db"; label: "CPU" 292 + valueText: Services.SystemUsage.cpuUsage + "%" 293 + pct: Services.SystemUsage.cpuUsage 294 + barColor: Config.Colours.peach 295 + } 296 + 297 + ResourceBar { 298 + icon: "\uefc5"; label: "Memory" 299 + valueText: Services.SystemUsage.memUsage + "%" 300 + pct: Services.SystemUsage.memUsage 301 + barColor: Config.Colours.mauve 302 + } 303 + 304 + ResourceBar { 305 + icon: "\uf0a0"; label: "Storage" 306 + valueText: Services.Storage.usagePercent + "%" 307 + pct: Services.Storage.usagePercent 308 + barColor: Config.Colours.sapphire 309 + } 310 + 311 + ResourceBar { 312 + icon: "\uf2c9"; label: "Temp" 313 + valueText: Services.SystemUsage.temperature + "\u00b0C" 314 + pct: Math.min(Services.SystemUsage.temperature, 100) 315 + barColor: Config.Colours.red 316 + } 317 + 318 + // Power profile 319 + Rectangle { 320 + Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 321 + } 322 + 323 + RowLayout { 324 + Layout.fillWidth: true 325 + spacing: 8 326 + 327 + Text { 328 + color: { 329 + if (Services.PowerProfile.profile === "performance") return Config.Colours.peach 330 + if (Services.PowerProfile.profile === "power-saver") return Config.Colours.green 331 + return Config.Colours.blue 332 + } 333 + font.family: Config.Appearance.font.family 334 + font.pixelSize: 14 335 + text: { 336 + if (Services.PowerProfile.profile === "performance") return "\uf0e7" 337 + if (Services.PowerProfile.profile === "power-saver") return "\uf06c" 338 + return "\uf24e" 339 + } 340 + } 341 + Text { 342 + color: Config.Colours.subtext0 343 + font.family: Config.Appearance.font.family 344 + font.pixelSize: 12 345 + text: { 346 + if (Services.PowerProfile.profile === "performance") return "Performance" 347 + if (Services.PowerProfile.profile === "power-saver") return "Power Saver" 348 + return "Balanced" 349 + } 350 + } 351 + Item { Layout.fillWidth: true } 352 + Text { 353 + color: Config.Colours.text 354 + font.family: Config.Appearance.font.family 355 + font.pixelSize: 12 356 + text: Services.Battery.percent + "%" 357 + visible: Services.Battery.hasBattery 358 + } 359 + } 360 + 361 + Item { Layout.fillHeight: true } 362 + } 363 + } 364 + }
+242
hosts/common/quickshell/modules/dashboard/tabs/MediaTab.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import "../../../config" as Config 4 + import "../../../components" 5 + import "../../../services" as Services 6 + 7 + Item { 8 + id: root 9 + 10 + implicitHeight: mediaLayout.implicitHeight 11 + 12 + function formatTime(seconds) { 13 + if (!seconds || seconds <= 0) return "0:00" 14 + var m = Math.floor(seconds / 60) 15 + var s = Math.floor(seconds % 60) 16 + return m + ":" + (s < 10 ? "0" : "") + s 17 + } 18 + 19 + ColumnLayout { 20 + id: mediaLayout 21 + width: parent.width 22 + spacing: 16 23 + 24 + // No player state 25 + ColumnLayout { 26 + Layout.fillWidth: true 27 + Layout.alignment: Qt.AlignHCenter 28 + Layout.topMargin: 32 29 + Layout.bottomMargin: 32 30 + visible: !Services.Mpris.hasPlayer 31 + spacing: 8 32 + 33 + Text { 34 + Layout.alignment: Qt.AlignHCenter 35 + color: Config.Colours.overlay0 36 + font.family: Config.Appearance.font.family 37 + font.pixelSize: 32 38 + text: "\uf001" 39 + } 40 + Text { 41 + Layout.alignment: Qt.AlignHCenter 42 + color: Config.Colours.subtext0 43 + font.family: Config.Appearance.font.family 44 + font.pixelSize: Config.Appearance.font.size.normal 45 + text: "No media playing" 46 + } 47 + Text { 48 + Layout.alignment: Qt.AlignHCenter 49 + color: Config.Colours.overlay0 50 + font.family: Config.Appearance.font.family 51 + font.pixelSize: Config.Appearance.font.size.small 52 + text: "Play something to see controls here" 53 + } 54 + } 55 + 56 + // Player content 57 + RowLayout { 58 + Layout.fillWidth: true 59 + visible: Services.Mpris.hasPlayer 60 + spacing: 16 61 + 62 + // Album art 63 + Rectangle { 64 + width: 140; height: 140 65 + radius: Config.Appearance.rounding.large 66 + color: Config.Colours.surface0 67 + clip: true 68 + 69 + Image { 70 + anchors.fill: parent 71 + source: Services.Mpris.artUrl 72 + fillMode: Image.PreserveAspectCrop 73 + visible: status === Image.Ready 74 + sourceSize.width: 140 75 + sourceSize.height: 140 76 + } 77 + 78 + Text { 79 + anchors.centerIn: parent 80 + visible: Services.Mpris.artUrl === "" || albumArt.status !== Image.Ready 81 + color: Config.Colours.overlay0 82 + font.family: Config.Appearance.font.family 83 + font.pixelSize: 40 84 + text: "\uf001" 85 + 86 + property var albumArt: parent.children[0] 87 + } 88 + } 89 + 90 + // Track info + controls 91 + ColumnLayout { 92 + Layout.fillWidth: true 93 + Layout.fillHeight: true 94 + spacing: 8 95 + 96 + // Title 97 + Text { 98 + Layout.fillWidth: true 99 + color: Config.Colours.text 100 + font.family: Config.Appearance.font.family 101 + font.pixelSize: Config.Appearance.font.size.large 102 + font.bold: true 103 + text: Services.Mpris.title || "Unknown" 104 + elide: Text.ElideRight 105 + maximumLineCount: 1 106 + } 107 + 108 + // Artist - Album 109 + Text { 110 + Layout.fillWidth: true 111 + color: Config.Colours.subtext0 112 + font.family: Config.Appearance.font.family 113 + font.pixelSize: Config.Appearance.font.size.normal 114 + text: { 115 + var parts = [] 116 + if (Services.Mpris.artist) parts.push(Services.Mpris.artist) 117 + if (Services.Mpris.album) parts.push(Services.Mpris.album) 118 + return parts.join(" \u2022 ") || "Unknown" 119 + } 120 + elide: Text.ElideRight 121 + maximumLineCount: 1 122 + } 123 + 124 + Item { Layout.fillHeight: true } 125 + 126 + // Progress bar 127 + Item { 128 + Layout.fillWidth: true 129 + height: 4 130 + 131 + property real animatedFraction: Services.Mpris.length > 0 ? root._trackedPos / Services.Mpris.length : 0 132 + Behavior on animatedFraction { 133 + Anim { 134 + duration: Config.Appearance.anim.durations.large 135 + easing.bezierCurve: Config.Appearance.anim.curves.standardDecel 136 + } 137 + } 138 + 139 + Rectangle { 140 + anchors.fill: parent 141 + radius: 2 142 + color: Config.Colours.surface1 143 + } 144 + Rectangle { 145 + width: parent.width * parent.animatedFraction 146 + height: parent.height 147 + radius: 2 148 + color: Config.Colours.blue 149 + } 150 + } 151 + 152 + // Time labels 153 + RowLayout { 154 + Layout.fillWidth: true 155 + Text { 156 + color: Config.Colours.overlay0 157 + font.family: Config.Appearance.font.family 158 + font.pixelSize: 10 159 + text: formatTime(root._trackedPos) 160 + } 161 + Item { Layout.fillWidth: true } 162 + Text { 163 + color: Config.Colours.overlay0 164 + font.family: Config.Appearance.font.family 165 + font.pixelSize: 10 166 + text: formatTime(Services.Mpris.length) 167 + } 168 + } 169 + 170 + // Playback controls 171 + RowLayout { 172 + Layout.alignment: Qt.AlignHCenter 173 + spacing: 24 174 + 175 + // Previous 176 + Text { 177 + color: Services.Mpris.canPrev ? Config.Colours.text : Config.Colours.overlay0 178 + font.family: Config.Appearance.font.family 179 + font.pixelSize: 20 180 + text: "\uf04a" 181 + MouseArea { 182 + anchors.fill: parent; anchors.margins: -8 183 + cursorShape: Qt.PointingHandCursor 184 + enabled: Services.Mpris.canPrev 185 + onClicked: Services.Mpris.previous() 186 + } 187 + } 188 + 189 + // Play/Pause 190 + Rectangle { 191 + width: 44; height: 44 192 + radius: 22 193 + color: Config.Colours.blue 194 + Text { 195 + anchors.centerIn: parent 196 + color: Config.Colours.crust 197 + font.family: Config.Appearance.font.family 198 + font.pixelSize: 20 199 + text: Services.Mpris.isPlaying ? "\uf04c" : "\uf04b" 200 + } 201 + MouseArea { 202 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 203 + onClicked: Services.Mpris.togglePlaying() 204 + } 205 + } 206 + 207 + // Next 208 + Text { 209 + color: Services.Mpris.canNext ? Config.Colours.text : Config.Colours.overlay0 210 + font.family: Config.Appearance.font.family 211 + font.pixelSize: 20 212 + text: "\uf04e" 213 + MouseArea { 214 + anchors.fill: parent; anchors.margins: -8 215 + cursorShape: Qt.PointingHandCursor 216 + enabled: Services.Mpris.canNext 217 + onClicked: Services.Mpris.next() 218 + } 219 + } 220 + } 221 + } 222 + } 223 + } 224 + 225 + // Position tracking for smooth progress 226 + property real _trackedPos: Services.Mpris.position 227 + 228 + Timer { 229 + interval: 1000 230 + running: Services.Mpris.isPlaying && root.visible 231 + repeat: true 232 + onTriggered: root._trackedPos += 1.0 233 + } 234 + 235 + Connections { 236 + target: Services.Mpris 237 + function onPositionChanged() { 238 + if (Math.abs(Services.Mpris.position - root._trackedPos) > 2) 239 + root._trackedPos = Services.Mpris.position 240 + } 241 + } 242 + }
+190
hosts/common/quickshell/modules/dashboard/tabs/PerformanceTab.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import "../../../config" as Config 4 + import "../../../components" 5 + import "../../../services" as Services 6 + 7 + Item { 8 + id: root 9 + 10 + implicitHeight: perfLayout.implicitHeight 11 + 12 + ColumnLayout { 13 + id: perfLayout 14 + width: parent.width 15 + spacing: 12 16 + 17 + // CPU 18 + Rectangle { 19 + Layout.fillWidth: true 20 + implicitHeight: cpuCol.implicitHeight + 24 21 + radius: Config.Appearance.rounding.normal 22 + color: Config.Colours.surface0 23 + 24 + ColumnLayout { 25 + id: cpuCol 26 + anchors.fill: parent 27 + anchors.margins: 12 28 + spacing: 8 29 + 30 + RowLayout { 31 + Layout.fillWidth: true 32 + Text { color: Config.Colours.peach; font.family: Config.Appearance.font.family; font.pixelSize: 18; text: "\uf2db" } 33 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: "CPU" } 34 + Item { Layout.fillWidth: true } 35 + Text { color: Config.Colours.peach; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.large; font.bold: true; text: Services.SystemUsage.cpuUsage + "%" } 36 + } 37 + 38 + Item { 39 + Layout.fillWidth: true; height: 8 40 + property real animatedPct: Services.SystemUsage.cpuUsage 41 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 42 + Rectangle { anchors.fill: parent; radius: 4; color: Config.Colours.surface1 } 43 + Rectangle { 44 + width: parent.width * parent.animatedPct / 100 45 + height: parent.height; radius: 4; color: Config.Colours.peach 46 + } 47 + } 48 + } 49 + } 50 + 51 + // Memory 52 + Rectangle { 53 + Layout.fillWidth: true 54 + implicitHeight: memCol.implicitHeight + 24 55 + radius: Config.Appearance.rounding.normal 56 + color: Config.Colours.surface0 57 + 58 + ColumnLayout { 59 + id: memCol 60 + anchors.fill: parent 61 + anchors.margins: 12 62 + spacing: 8 63 + 64 + RowLayout { 65 + Layout.fillWidth: true 66 + Text { color: Config.Colours.mauve; font.family: Config.Appearance.font.family; font.pixelSize: 18; text: "\uefc5" } 67 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: "Memory" } 68 + Item { Layout.fillWidth: true } 69 + Text { color: Config.Colours.mauve; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.large; font.bold: true; text: Services.SystemUsage.memUsage + "%" } 70 + } 71 + 72 + Item { 73 + Layout.fillWidth: true; height: 8 74 + property real animatedPct: Services.SystemUsage.memUsage 75 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 76 + Rectangle { anchors.fill: parent; radius: 4; color: Config.Colours.surface1 } 77 + Rectangle { 78 + width: parent.width * parent.animatedPct / 100 79 + height: parent.height; radius: 4; color: Config.Colours.mauve 80 + } 81 + } 82 + } 83 + } 84 + 85 + // Temperature 86 + Rectangle { 87 + Layout.fillWidth: true 88 + implicitHeight: tempCol.implicitHeight + 24 89 + radius: Config.Appearance.rounding.normal 90 + color: Config.Colours.surface0 91 + 92 + ColumnLayout { 93 + id: tempCol 94 + anchors.fill: parent 95 + anchors.margins: 12 96 + spacing: 8 97 + 98 + RowLayout { 99 + Layout.fillWidth: true 100 + Text { color: Config.Colours.red; font.family: Config.Appearance.font.family; font.pixelSize: 18; text: "\uf2c9" } 101 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: "Temperature" } 102 + Item { Layout.fillWidth: true } 103 + Text { color: Config.Colours.red; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.large; font.bold: true; text: Services.SystemUsage.temperature + "\u00b0C" } 104 + } 105 + 106 + Item { 107 + Layout.fillWidth: true; height: 8 108 + property real animatedPct: Math.min(Services.SystemUsage.temperature, 100) 109 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 110 + Rectangle { anchors.fill: parent; radius: 4; color: Config.Colours.surface1 } 111 + Rectangle { 112 + width: parent.width * parent.animatedPct / 100 113 + height: parent.height; radius: 4; color: Config.Colours.red 114 + } 115 + } 116 + } 117 + } 118 + 119 + // Storage 120 + Rectangle { 121 + Layout.fillWidth: true 122 + implicitHeight: storageCol.implicitHeight + 24 123 + radius: Config.Appearance.rounding.normal 124 + color: Config.Colours.surface0 125 + 126 + ColumnLayout { 127 + id: storageCol 128 + anchors.fill: parent 129 + anchors.margins: 12 130 + spacing: 8 131 + 132 + RowLayout { 133 + Layout.fillWidth: true 134 + Text { color: Config.Colours.sapphire; font.family: Config.Appearance.font.family; font.pixelSize: 18; text: "\uf0a0" } 135 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: "Storage" } 136 + Item { Layout.fillWidth: true } 137 + Text { color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.small; text: Services.Storage.usedStr + " / " + Services.Storage.totalStr } 138 + Text { color: Config.Colours.sapphire; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.large; font.bold: true; text: Services.Storage.usagePercent + "%" } 139 + } 140 + 141 + Item { 142 + Layout.fillWidth: true; height: 8 143 + property real animatedPct: Services.Storage.usagePercent 144 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 145 + Rectangle { anchors.fill: parent; radius: 4; color: Config.Colours.surface1 } 146 + Rectangle { 147 + width: parent.width * parent.animatedPct / 100 148 + height: parent.height; radius: 4; color: Config.Colours.sapphire 149 + } 150 + } 151 + } 152 + } 153 + 154 + // Battery 155 + Rectangle { 156 + Layout.fillWidth: true 157 + visible: Services.Battery.hasBattery 158 + implicitHeight: batCol.implicitHeight + 24 159 + radius: Config.Appearance.rounding.normal 160 + color: Config.Colours.surface0 161 + 162 + ColumnLayout { 163 + id: batCol 164 + anchors.fill: parent 165 + anchors.margins: 12 166 + spacing: 8 167 + 168 + RowLayout { 169 + Layout.fillWidth: true 170 + Text { color: Config.Colours.green; font.family: Config.Appearance.font.family; font.pixelSize: 18; text: Services.Battery.status === "Charging" ? "\uf0e7" : "\uf241" } 171 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: "Battery" } 172 + Item { Layout.fillWidth: true } 173 + Text { color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.small; text: Services.Battery.status } 174 + Text { color: Config.Colours.green; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.large; font.bold: true; text: Services.Battery.percent + "%" } 175 + } 176 + 177 + Item { 178 + Layout.fillWidth: true; height: 8 179 + property real animatedPct: Services.Battery.percent 180 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 181 + Rectangle { anchors.fill: parent; radius: 4; color: Config.Colours.surface1 } 182 + Rectangle { 183 + width: parent.width * parent.animatedPct / 100 184 + height: parent.height; radius: 4; color: Config.Colours.green 185 + } 186 + } 187 + } 188 + } 189 + } 190 + }
+165
hosts/common/quickshell/modules/dashboard/tabs/WeatherTab.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell.Io 4 + import "../../../config" as Config 5 + import "../../../components" 6 + import "../../../services" as Services 7 + 8 + Item { 9 + id: root 10 + 11 + implicitHeight: weatherLayout.implicitHeight 12 + 13 + property string _condition: "" 14 + property string _feelsLike: "" 15 + property string _humidity: "" 16 + property string _wind: "" 17 + property bool _fetched: false 18 + 19 + onVisibleChanged: { 20 + if (visible && !_fetched) { 21 + weatherDetailProc.running = true 22 + _fetched = true 23 + } 24 + } 25 + 26 + Process { 27 + id: weatherDetailProc 28 + command: ["sh", "-c", "curl -sf 'wttr.in/?format=%c|%C|%t|%f|%h|%w' | tr -d '+'"] 29 + stdout: SplitParser { 30 + onRead: data => { 31 + if (!data) return 32 + var parts = data.trim().split("|") 33 + if (parts.length >= 6) { 34 + root._condition = parts[1] || "" 35 + root._feelsLike = parts[3] || "" 36 + root._humidity = parts[4] || "" 37 + root._wind = parts[5] || "" 38 + } 39 + } 40 + } 41 + } 42 + 43 + ColumnLayout { 44 + id: weatherLayout 45 + width: parent.width 46 + spacing: 16 47 + 48 + // No data 49 + ColumnLayout { 50 + Layout.fillWidth: true 51 + Layout.alignment: Qt.AlignHCenter 52 + Layout.topMargin: 32 53 + Layout.bottomMargin: 32 54 + visible: Services.Weather.weather === "" 55 + spacing: 8 56 + 57 + Text { 58 + Layout.alignment: Qt.AlignHCenter 59 + color: Config.Colours.overlay0 60 + font.family: Config.Appearance.font.family 61 + font.pixelSize: 32 62 + text: "\uf0c2" 63 + } 64 + Text { 65 + Layout.alignment: Qt.AlignHCenter 66 + color: Config.Colours.subtext0 67 + font.family: Config.Appearance.font.family 68 + font.pixelSize: Config.Appearance.font.size.normal 69 + text: "Weather data unavailable" 70 + } 71 + } 72 + 73 + // Current weather 74 + ColumnLayout { 75 + Layout.fillWidth: true 76 + visible: Services.Weather.weather !== "" 77 + spacing: 16 78 + 79 + // Large display 80 + RowLayout { 81 + Layout.fillWidth: true 82 + spacing: 16 83 + 84 + Text { 85 + color: Config.Colours.yellow 86 + font.family: Config.Appearance.font.family 87 + font.pixelSize: 48 88 + text: "\uf0eb" 89 + } 90 + ColumnLayout { 91 + spacing: 4 92 + Text { 93 + color: Config.Colours.text 94 + font.family: Config.Appearance.font.family 95 + font.pixelSize: 28 96 + font.bold: true 97 + text: Services.Weather.weather 98 + } 99 + Text { 100 + color: Config.Colours.subtext0 101 + font.family: Config.Appearance.font.family 102 + font.pixelSize: Config.Appearance.font.size.normal 103 + text: root._condition || "Loading..." 104 + } 105 + } 106 + } 107 + 108 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 } 109 + 110 + // Detail cards 111 + RowLayout { 112 + Layout.fillWidth: true 113 + spacing: 12 114 + 115 + // Feels Like 116 + Rectangle { 117 + Layout.fillWidth: true 118 + implicitHeight: 80 119 + radius: Config.Appearance.rounding.normal 120 + color: Config.Colours.surface0 121 + 122 + ColumnLayout { 123 + anchors.centerIn: parent 124 + spacing: 4 125 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.peach; font.family: Config.Appearance.font.family; font.pixelSize: 20; text: "\uf2c9" } 126 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: root._feelsLike || "--" } 127 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.overlay0; font.family: Config.Appearance.font.family; font.pixelSize: 10; text: "Feels like" } 128 + } 129 + } 130 + 131 + // Humidity 132 + Rectangle { 133 + Layout.fillWidth: true 134 + implicitHeight: 80 135 + radius: Config.Appearance.rounding.normal 136 + color: Config.Colours.surface0 137 + 138 + ColumnLayout { 139 + anchors.centerIn: parent 140 + spacing: 4 141 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.sapphire; font.family: Config.Appearance.font.family; font.pixelSize: 20; text: "\uf043" } 142 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: root._humidity || "--" } 143 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.overlay0; font.family: Config.Appearance.font.family; font.pixelSize: 10; text: "Humidity" } 144 + } 145 + } 146 + 147 + // Wind 148 + Rectangle { 149 + Layout.fillWidth: true 150 + implicitHeight: 80 151 + radius: Config.Appearance.rounding.normal 152 + color: Config.Colours.surface0 153 + 154 + ColumnLayout { 155 + anchors.centerIn: parent 156 + spacing: 4 157 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.teal; font.family: Config.Appearance.font.family; font.pixelSize: 20; text: "\uf72e" } 158 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: root._wind || "--" } 159 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.overlay0; font.family: Config.Appearance.font.family; font.pixelSize: 10; text: "Wind" } 160 + } 161 + } 162 + } 163 + } 164 + } 165 + }
+4
hosts/common/quickshell/modules/dashboard/tabs/qmldir
··· 1 + DashTab 1.0 DashTab.qml 2 + MediaTab 1.0 MediaTab.qml 3 + PerformanceTab 1.0 PerformanceTab.qml 4 + WeatherTab 1.0 WeatherTab.qml
+322
hosts/common/quickshell/modules/rightpanel/RightPanel.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import Quickshell.Io 5 + import "../../config" as Config 6 + import "../../components" 7 + import "../../services" as Services 8 + 9 + PanelWindow { 10 + id: rightTrigger 11 + 12 + required property var modelData 13 + screen: modelData 14 + 15 + anchors { 16 + right: true 17 + top: true 18 + bottom: true 19 + } 20 + margins.right: 0 21 + margins.top: 8 22 + margins.bottom: 8 23 + implicitWidth: 2 24 + exclusiveZone: 0 25 + color: "transparent" 26 + 27 + property bool panelOpen: false 28 + 29 + MouseArea { 30 + anchors.fill: parent 31 + hoverEnabled: true 32 + onEntered: { closeTimer.stop(); rightTrigger.panelOpen = true } 33 + onExited: closeTimer.restart() 34 + } 35 + 36 + onPanelOpenChanged: { 37 + if (panelOpen) { 38 + contentRect._opening = true 39 + fadeInTimer.start() 40 + } else { 41 + fadeInTimer.stop() 42 + contentRect._opening = false 43 + contentRect.opacity = 0 44 + } 45 + } 46 + 47 + Timer { 48 + id: closeTimer 49 + interval: 400 50 + onTriggered: rightTrigger.panelOpen = false 51 + } 52 + 53 + PopupWindow { 54 + id: rightPanel 55 + anchor.window: rightTrigger 56 + anchor.onAnchoring: { 57 + anchor.rect.x = -(implicitWidth + 8) 58 + anchor.rect.y = (rightTrigger.height - implicitHeight) / 2 59 + } 60 + 61 + visible: rightTrigger.panelOpen || contentRect.opacity > 0 62 + implicitWidth: 280 63 + implicitHeight: panelContent.implicitHeight + 32 64 + color: "transparent" 65 + 66 + Timer { 67 + id: fadeInTimer 68 + interval: 16 69 + onTriggered: contentRect.opacity = 1 70 + } 71 + 72 + Rectangle { 73 + id: contentRect 74 + anchors.fill: parent 75 + color: Config.Colours.base 76 + radius: Config.Appearance.rounding.large 77 + border.width: 1 78 + border.color: Config.Colours.surface0 79 + opacity: 0 80 + 81 + property bool _opening: false 82 + Behavior on opacity { 83 + Anim { 84 + duration: contentRect._opening ? Config.Appearance.anim.durations.normal : Config.Appearance.anim.durations.small 85 + easing.bezierCurve: contentRect._opening ? Config.Appearance.anim.curves.standardDecel : Config.Appearance.anim.curves.standardAccel 86 + } 87 + } 88 + 89 + HoverHandler { 90 + onHoveredChanged: { 91 + if (hovered) closeTimer.stop() 92 + else closeTimer.restart() 93 + } 94 + } 95 + 96 + ColumnLayout { 97 + id: panelContent 98 + anchors.fill: parent 99 + anchors.margins: 16 100 + spacing: 12 101 + 102 + // Header 103 + Text { 104 + color: Config.Colours.text 105 + font.family: Config.Appearance.font.family 106 + font.pixelSize: Config.Appearance.font.size.large 107 + font.bold: true 108 + text: "Quick Settings" 109 + } 110 + 111 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 } 112 + 113 + // Volume 114 + ColumnLayout { 115 + Layout.fillWidth: true 116 + spacing: 6 117 + 118 + RowLayout { 119 + Layout.fillWidth: true 120 + spacing: 8 121 + Text { color: Config.Colours.maroon; font.family: Config.Appearance.font.family; font.pixelSize: 16; text: Services.Audio.muted ? "\uf026" : "\uf028" } 122 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; text: "Volume" } 123 + Item { Layout.fillWidth: true } 124 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: Services.Audio.muted ? "Muted" : Services.Audio.volume + "%" } 125 + } 126 + 127 + Item { 128 + Layout.fillWidth: true 129 + height: 6 130 + property real animatedPct: Services.Audio.volume 131 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 132 + Rectangle { anchors.fill: parent; radius: 3; color: Config.Colours.surface1 } 133 + Rectangle { 134 + width: parent.width * parent.animatedPct / 100 135 + height: parent.height; radius: 3; color: Config.Colours.maroon 136 + } 137 + } 138 + 139 + Rectangle { 140 + Layout.fillWidth: true 141 + height: 28 142 + radius: Config.Appearance.rounding.normal 143 + color: muteMouse.containsMouse ? Config.Colours.surface1 : Config.Colours.surface0 144 + Behavior on color { CAnim {} } 145 + Text { anchors.centerIn: parent; color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.small; text: Services.Audio.muted ? "Unmute" : "Mute" } 146 + MouseArea { id: muteMouse; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: muteProc.running = true } 147 + } 148 + } 149 + 150 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 } 151 + 152 + // Brightness 153 + ColumnLayout { 154 + Layout.fillWidth: true 155 + spacing: 6 156 + 157 + RowLayout { 158 + Layout.fillWidth: true 159 + spacing: 8 160 + Text { color: Config.Colours.yellow; font.family: Config.Appearance.font.family; font.pixelSize: 16; text: Services.Brightness.brightness > 50 ? "\uf0eb" : "\uf0ec" } 161 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; text: "Brightness" } 162 + Item { Layout.fillWidth: true } 163 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; font.bold: true; text: Services.Brightness.brightness + "%" } 164 + } 165 + 166 + Item { 167 + Layout.fillWidth: true 168 + height: 6 169 + property real animatedPct: Services.Brightness.brightness 170 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 171 + Rectangle { anchors.fill: parent; radius: 3; color: Config.Colours.surface1 } 172 + Rectangle { 173 + width: parent.width * parent.animatedPct / 100 174 + height: parent.height; radius: 3; color: Config.Colours.yellow 175 + } 176 + } 177 + } 178 + 179 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 } 180 + 181 + // Network 182 + RowLayout { 183 + Layout.fillWidth: true 184 + spacing: 8 185 + 186 + Text { 187 + color: Config.Colours.green 188 + font.family: Config.Appearance.font.family 189 + font.pixelSize: 16 190 + text: { 191 + if (!Services.Network.connected) return "\uf071" 192 + if (Services.Network.isEthernet) return "\udb80\ude00" 193 + return "\uf1eb" 194 + } 195 + } 196 + 197 + ColumnLayout { 198 + spacing: 2 199 + Text { 200 + color: Config.Colours.text 201 + font.family: Config.Appearance.font.family 202 + font.pixelSize: Config.Appearance.font.size.normal 203 + text: Services.Network.connected ? Services.Network.name : "Disconnected" 204 + } 205 + Text { 206 + color: Config.Colours.subtext0 207 + font.family: Config.Appearance.font.family 208 + font.pixelSize: Config.Appearance.font.size.small 209 + text: { 210 + if (!Services.Network.connected) return "No connection" 211 + if (Services.Network.isEthernet) return "Wired connection" 212 + return "Signal: " + Services.Network.strength + "%" 213 + } 214 + } 215 + } 216 + } 217 + 218 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1; visible: Services.Battery.hasBattery } 219 + 220 + // Battery + Power Profile 221 + RowLayout { 222 + Layout.fillWidth: true 223 + spacing: 8 224 + visible: Services.Battery.hasBattery 225 + 226 + Text { 227 + color: { 228 + if (Services.Battery.percent <= 20) return Config.Colours.red 229 + if (Services.Battery.status === "Charging") return Config.Colours.green 230 + return Config.Colours.green 231 + } 232 + font.family: Config.Appearance.font.family 233 + font.pixelSize: 16 234 + text: { 235 + if (Services.Battery.status === "Charging") return "\uf0e7" 236 + if (Services.Battery.percent > 75) return "\uf240" 237 + if (Services.Battery.percent > 50) return "\uf241" 238 + if (Services.Battery.percent > 25) return "\uf242" 239 + if (Services.Battery.percent > 10) return "\uf243" 240 + return "\uf244" 241 + } 242 + } 243 + 244 + ColumnLayout { 245 + spacing: 2 246 + Text { 247 + color: Config.Colours.text 248 + font.family: Config.Appearance.font.family 249 + font.pixelSize: Config.Appearance.font.size.normal 250 + text: Services.Battery.percent > 0 ? Services.Battery.percent + "%" : "No battery" 251 + } 252 + Text { 253 + color: Config.Colours.subtext0 254 + font.family: Config.Appearance.font.family 255 + font.pixelSize: Config.Appearance.font.size.small 256 + text: Services.Battery.status 257 + } 258 + } 259 + 260 + Item { Layout.fillWidth: true } 261 + 262 + // Power profile cycle 263 + Rectangle { 264 + width: 32; height: 32 265 + radius: Config.Appearance.rounding.full 266 + color: profileMouse.containsMouse ? Config.Colours.surface1 : Config.Colours.surface0 267 + Behavior on color { CAnim {} } 268 + 269 + Text { 270 + anchors.centerIn: parent 271 + color: { 272 + if (Services.PowerProfile.profile === "performance") return Config.Colours.peach 273 + if (Services.PowerProfile.profile === "power-saver") return Config.Colours.green 274 + return Config.Colours.blue 275 + } 276 + font.family: Config.Appearance.font.family 277 + font.pixelSize: 14 278 + text: { 279 + if (Services.PowerProfile.profile === "performance") return "\uf0e7" 280 + if (Services.PowerProfile.profile === "power-saver") return "\uf06c" 281 + return "\uf24e" 282 + } 283 + } 284 + 285 + MouseArea { 286 + id: profileMouse 287 + anchors.fill: parent 288 + hoverEnabled: true 289 + cursorShape: Qt.PointingHandCursor 290 + onClicked: Services.PowerProfile.cycleProfile() 291 + } 292 + } 293 + } 294 + 295 + // Power profile label 296 + Text { 297 + Layout.alignment: Qt.AlignRight 298 + color: Config.Colours.overlay0 299 + font.family: Config.Appearance.font.family 300 + font.pixelSize: 10 301 + text: { 302 + if (Services.PowerProfile.profile === "performance") return "Performance" 303 + if (Services.PowerProfile.profile === "power-saver") return "Power Saver" 304 + return "Balanced" 305 + } 306 + } 307 + } 308 + 309 + Keys.onEscapePressed: rightTrigger.panelOpen = false 310 + } 311 + 312 + // Click outside to close 313 + MouseArea { 314 + anchors.fill: parent 315 + z: -1 316 + onClicked: rightTrigger.panelOpen = false 317 + } 318 + } 319 + 320 + // Volume/brightness control processes 321 + Process { id: muteProc; command: ["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"] } 322 + }
+1
hosts/common/quickshell/modules/rightpanel/qmldir
··· 1 + RightPanel 1.0 RightPanel.qml
+213
hosts/common/quickshell/modules/sidebar/Sidebar.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import "../../config" as Config 5 + import "../../services" as Services 6 + import "components" 7 + import "popouts" 8 + 9 + PanelWindow { 10 + id: sidebar 11 + 12 + required property var modelData 13 + screen: modelData 14 + 15 + anchors { 16 + left: true 17 + top: true 18 + bottom: true 19 + } 20 + margins.left: 0 21 + margins.top: 0 22 + margins.bottom: 0 23 + implicitWidth: 48 24 + color: "transparent" 25 + 26 + property string activePopout: "" 27 + property real popoutAnchorY: height / 2 28 + 29 + function togglePopout(name) { 30 + if (activePopout === name) 31 + activePopout = "" 32 + else 33 + activePopout = name 34 + } 35 + 36 + function openPopoutAt(name, item) { 37 + var pos = item.mapToItem(barBg, 0, item.height / 2) 38 + popoutAnchorY = pos.y 39 + togglePopout(name) 40 + } 41 + 42 + Rectangle { 43 + id: barBg 44 + anchors.fill: parent 45 + radius: Config.Appearance.rounding.large 46 + color: Qt.rgba( 47 + Config.Colours.mantle.r, 48 + Config.Colours.mantle.g, 49 + Config.Colours.mantle.b, 50 + Config.Appearance.barOpacity 51 + ) 52 + 53 + ColumnLayout { 54 + anchors.fill: parent 55 + anchors.leftMargin: 4 56 + anchors.rightMargin: 4 57 + anchors.topMargin: 10 58 + anchors.bottomMargin: 10 59 + spacing: Config.Appearance.spacing.normal 60 + 61 + // NixOS Logo 62 + LogoButton { 63 + id: logoButton 64 + Layout.alignment: Qt.AlignHCenter 65 + onClicked: sidebar.openPopoutAt("session", logoButton) 66 + } 67 + 68 + // Top spacer 69 + Item { Layout.fillHeight: true } 70 + 71 + // Vertical Clock 72 + VerticalClock { 73 + Layout.alignment: Qt.AlignHCenter 74 + onClicked: sidebar.togglePopout("dashboard") 75 + } 76 + 77 + // Weather 78 + Text { 79 + Layout.alignment: Qt.AlignHCenter 80 + font.pixelSize: 14 81 + text: Services.Weather.icon 82 + visible: Services.Weather.icon !== "" 83 + } 84 + Text { 85 + Layout.alignment: Qt.AlignHCenter 86 + color: Config.Colours.subtext0 87 + font.family: Config.Appearance.font.family 88 + font.pixelSize: 10 89 + text: Services.Weather.temp 90 + visible: Services.Weather.temp !== "" 91 + } 92 + 93 + // Bottom spacer 94 + Item { Layout.fillHeight: true } 95 + 96 + // Tray pill 97 + Rectangle { 98 + Layout.fillWidth: true 99 + implicitHeight: trayLayout.implicitHeight + Config.Appearance.padding.normal * 2 100 + radius: Config.Appearance.rounding.full 101 + color: Config.Colours.surface0 102 + clip: true 103 + visible: tray.count > 0 104 + 105 + ColumnLayout { 106 + id: trayLayout 107 + anchors.horizontalCenter: parent.horizontalCenter 108 + anchors.verticalCenter: parent.verticalCenter 109 + spacing: Config.Appearance.spacing.small 110 + 111 + Tray { id: tray } 112 + } 113 + } 114 + 115 + // Status icons pill 116 + Rectangle { 117 + Layout.fillWidth: true 118 + implicitHeight: statusCol.implicitHeight + Config.Appearance.padding.normal * 2 119 + radius: Config.Appearance.rounding.full 120 + color: Config.Colours.surface0 121 + clip: true 122 + 123 + ColumnLayout { 124 + id: statusCol 125 + anchors.horizontalCenter: parent.horizontalCenter 126 + anchors.verticalCenter: parent.verticalCenter 127 + spacing: Config.Appearance.spacing.small 128 + 129 + // Volume 130 + SidebarIcon { 131 + id: volumeIcon 132 + Layout.alignment: Qt.AlignHCenter 133 + iconText: Services.Audio.muted ? "\uf026" : (Services.Audio.volume > 50 ? "\uf028" : "\uf027") 134 + iconColor: Config.Colours.maroon 135 + onClicked: sidebar.openPopoutAt("audio", volumeIcon) 136 + } 137 + 138 + // Network 139 + SidebarIcon { 140 + id: networkIcon 141 + Layout.alignment: Qt.AlignHCenter 142 + iconText: { 143 + if (!Services.Network.connected) return "\uf071" 144 + if (Services.Network.isEthernet) return "\udb80\ude00" 145 + return "\uf1eb" 146 + } 147 + iconColor: Config.Colours.green 148 + onClicked: sidebar.openPopoutAt("network", networkIcon) 149 + } 150 + 151 + // Battery / Power Profile 152 + SidebarIcon { 153 + id: batteryIcon 154 + Layout.alignment: Qt.AlignHCenter 155 + visible: Services.Battery.hasBattery 156 + iconText: { 157 + if (Services.Battery.status === "Charging") return "\uf0e7" 158 + var p = Services.Battery.percent 159 + if (p > 75) return "\uf240" 160 + if (p > 50) return "\uf241" 161 + if (p > 25) return "\uf242" 162 + if (p > 10) return "\uf243" 163 + return "\uf244" 164 + } 165 + iconColor: { 166 + if (Services.Battery.status === "Charging") return Config.Colours.green 167 + if (Services.Battery.percent <= 20) return Config.Colours.red 168 + return Config.Colours.green 169 + } 170 + onClicked: sidebar.openPopoutAt("powerprofile", batteryIcon) 171 + } 172 + } 173 + } 174 + 175 + } 176 + } 177 + 178 + // Popout windows 179 + DashboardPopout { 180 + show: sidebar.activePopout === "dashboard" 181 + parentWindow: sidebar 182 + anchorY: sidebar.height / 2 183 + onClose: sidebar.activePopout = "" 184 + } 185 + 186 + SessionPopout { 187 + show: sidebar.activePopout === "session" 188 + parentWindow: sidebar 189 + anchorY: sidebar.popoutAnchorY 190 + onClose: sidebar.activePopout = "" 191 + } 192 + 193 + AudioPopout { 194 + show: sidebar.activePopout === "audio" 195 + parentWindow: sidebar 196 + anchorY: sidebar.popoutAnchorY 197 + onClose: sidebar.activePopout = "" 198 + } 199 + 200 + NetworkPopout { 201 + show: sidebar.activePopout === "network" 202 + parentWindow: sidebar 203 + anchorY: sidebar.popoutAnchorY 204 + onClose: sidebar.activePopout = "" 205 + } 206 + 207 + PowerProfilePopout { 208 + show: sidebar.activePopout === "powerprofile" 209 + parentWindow: sidebar 210 + anchorY: sidebar.popoutAnchorY 211 + onClose: sidebar.activePopout = "" 212 + } 213 + }
+39
hosts/common/quickshell/modules/sidebar/components/LogoButton.qml
··· 1 + import QtQuick 2 + import "../../../config" as Config 3 + import "../../../components" 4 + 5 + Item { 6 + id: root 7 + signal clicked() 8 + 9 + width: 24 10 + height: 24 11 + 12 + Rectangle { 13 + anchors.centerIn: parent 14 + width: parent.width + 8 15 + height: parent.height + 8 16 + radius: Config.Appearance.rounding.normal 17 + color: logoMouse.containsMouse 18 + ? Qt.rgba(Config.Colours.blue.r, Config.Colours.blue.g, Config.Colours.blue.b, 0.15) 19 + : "transparent" 20 + Behavior on color { CAnim {} } 21 + } 22 + 23 + Text { 24 + anchors.centerIn: parent 25 + color: Config.Colours.blue 26 + font.family: Config.Appearance.font.family 27 + font.pixelSize: 22 28 + text: "\uf313" 29 + } 30 + 31 + MouseArea { 32 + id: logoMouse 33 + anchors.fill: parent 34 + anchors.margins: -4 35 + hoverEnabled: true 36 + cursorShape: Qt.PointingHandCursor 37 + onClicked: root.clicked() 38 + } 39 + }
+39
hosts/common/quickshell/modules/sidebar/components/PowerButton.qml
··· 1 + import QtQuick 2 + import "../../../config" as Config 3 + import "../../../components" 4 + 5 + Item { 6 + id: root 7 + signal clicked() 8 + 9 + width: 24 10 + height: 24 11 + 12 + Rectangle { 13 + anchors.centerIn: parent 14 + width: parent.width + 8 15 + height: parent.height + 8 16 + radius: Config.Appearance.rounding.full 17 + color: powerMouse.containsMouse 18 + ? Qt.rgba(Config.Colours.red.r, Config.Colours.red.g, Config.Colours.red.b, 0.2) 19 + : "transparent" 20 + Behavior on color { CAnim {} } 21 + } 22 + 23 + Text { 24 + anchors.centerIn: parent 25 + color: Config.Colours.red 26 + font.family: Config.Appearance.font.family 27 + font.pixelSize: 18 28 + text: "\u23fb" 29 + } 30 + 31 + MouseArea { 32 + id: powerMouse 33 + anchors.fill: parent 34 + anchors.margins: -4 35 + hoverEnabled: true 36 + cursorShape: Qt.PointingHandCursor 37 + onClicked: root.clicked() 38 + } 39 + }
+42
hosts/common/quickshell/modules/sidebar/components/SidebarIcon.qml
··· 1 + import QtQuick 2 + import "../../../config" as Config 3 + import "../../../components" 4 + 5 + Item { 6 + id: root 7 + 8 + property string iconText: "" 9 + property color iconColor: Config.Colours.text 10 + signal clicked() 11 + 12 + width: 22 13 + height: 22 14 + 15 + Rectangle { 16 + anchors.centerIn: parent 17 + width: parent.width + 4 18 + height: parent.height + 4 19 + radius: Config.Appearance.rounding.normal 20 + color: iconMouse.containsMouse 21 + ? Qt.rgba(root.iconColor.r, root.iconColor.g, root.iconColor.b, 0.15) 22 + : "transparent" 23 + Behavior on color { CAnim {} } 24 + } 25 + 26 + Text { 27 + anchors.centerIn: parent 28 + color: root.iconColor 29 + font.family: Config.Appearance.font.family 30 + font.pixelSize: 14 31 + text: root.iconText 32 + } 33 + 34 + MouseArea { 35 + id: iconMouse 36 + anchors.fill: parent 37 + anchors.margins: -2 38 + hoverEnabled: true 39 + cursorShape: Qt.PointingHandCursor 40 + onClicked: root.clicked() 41 + } 42 + }
+15
hosts/common/quickshell/modules/sidebar/components/Tray.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell.Services.SystemTray 4 + 5 + ColumnLayout { 6 + property alias count: repeater.count 7 + 8 + spacing: 4 9 + 10 + Repeater { 11 + id: repeater 12 + model: SystemTray.items.values 13 + delegate: TrayItem {} 14 + } 15 + }
+59
hosts/common/quickshell/modules/sidebar/components/TrayItem.qml
··· 1 + import QtQuick 2 + import Quickshell 3 + import Quickshell.Widgets 4 + import Quickshell.Io 5 + import "../../../config" as Config 6 + import "../../../components" 7 + 8 + Item { 9 + id: trayItem 10 + 11 + required property var modelData 12 + 13 + width: 22 14 + height: 22 15 + 16 + Rectangle { 17 + anchors.centerIn: parent 18 + width: parent.width + 4 19 + height: parent.height + 4 20 + radius: Config.Appearance.rounding.normal 21 + color: trayMouse.containsMouse 22 + ? Qt.rgba(Config.Colours.text.r, Config.Colours.text.g, Config.Colours.text.b, 0.1) 23 + : "transparent" 24 + Behavior on color { CAnim {} } 25 + } 26 + 27 + IconImage { 28 + implicitSize: 16 29 + anchors.centerIn: parent 30 + source: Quickshell.iconPath(trayItem.modelData.icon, true) || trayItem.modelData.icon 31 + } 32 + 33 + MouseArea { 34 + id: trayMouse 35 + anchors.fill: parent 36 + anchors.margins: -2 37 + acceptedButtons: Qt.LeftButton | Qt.RightButton 38 + hoverEnabled: true 39 + cursorShape: Qt.PointingHandCursor 40 + onClicked: mouse => { 41 + if (mouse.button === Qt.RightButton) { 42 + if (trayItem.modelData.hasMenu) { 43 + trayItem.modelData.display(trayItem.Window.window, trayItem.width, trayItem.height / 2) 44 + } 45 + } else { 46 + if (trayItem.modelData.id.toLowerCase().indexOf("steam") !== -1) { 47 + steamProc.running = true 48 + } else { 49 + trayItem.modelData.activate() 50 + } 51 + } 52 + } 53 + } 54 + 55 + Process { 56 + id: steamProc 57 + command: ["sh", "-c", "WID=$(niri msg --json windows | jq -r '.[] | select(.app_id | test(\"steam\"; \"i\")) | .id' | head -n1); if [ -n \"$WID\" ]; then niri msg action focus-window --id \"$WID\"; else steam steam://open/games; fi"] 58 + } 59 + }
+35
hosts/common/quickshell/modules/sidebar/components/VerticalClock.qml
··· 1 + import QtQuick 2 + import "../../../config" as Config 3 + 4 + Item { 5 + id: root 6 + signal clicked() 7 + 8 + width: 40 9 + implicitHeight: clockText.implicitHeight 10 + 11 + Text { 12 + id: clockText 13 + anchors.horizontalCenter: parent.horizontalCenter 14 + horizontalAlignment: Text.AlignHCenter 15 + color: Config.Colours.blue 16 + font.family: Config.Appearance.font.family 17 + font.pixelSize: 16 18 + font.bold: true 19 + lineHeight: 1.0 20 + text: Qt.formatDateTime(new Date(), "HH\nmm") 21 + } 22 + 23 + Timer { 24 + interval: 1000 25 + running: true 26 + repeat: true 27 + onTriggered: clockText.text = Qt.formatDateTime(new Date(), "HH\nmm") 28 + } 29 + 30 + MouseArea { 31 + anchors.fill: parent 32 + cursorShape: Qt.PointingHandCursor 33 + onClicked: root.clicked() 34 + } 35 + }
+6
hosts/common/quickshell/modules/sidebar/components/qmldir
··· 1 + LogoButton 1.0 LogoButton.qml 2 + VerticalClock 1.0 VerticalClock.qml 3 + SidebarIcon 1.0 SidebarIcon.qml 4 + Tray 1.0 Tray.qml 5 + TrayItem 1.0 TrayItem.qml 6 + PowerButton 1.0 PowerButton.qml
+98
hosts/common/quickshell/modules/sidebar/popouts/AudioPopout.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import Quickshell.Wayland 5 + import Quickshell.Io 6 + import "../../../config" as Config 7 + import "../../../components" 8 + import "../../../services" as Services 9 + 10 + PopupWindow { 11 + id: audioPopout 12 + 13 + property bool show: false 14 + property var parentWindow 15 + property real anchorY: 0 16 + signal close() 17 + 18 + visible: show || contentRect.opacity > 0 19 + anchor.window: parentWindow 20 + anchor.onAnchoring: { 21 + anchor.rect.x = parentWindow.width + 8 22 + anchor.rect.y = Math.max(0, anchorY - implicitHeight / 2) 23 + } 24 + 25 + implicitWidth: 220 26 + implicitHeight: audioContent.implicitHeight + 32 27 + color: "transparent" 28 + 29 + onShowChanged: { 30 + if (show) { contentRect._opening = true; fadeIn.start() } 31 + else { fadeIn.stop(); contentRect._opening = false; contentRect.opacity = 0 } 32 + } 33 + Timer { id: fadeIn; interval: 16; onTriggered: contentRect.opacity = 1 } 34 + 35 + Rectangle { 36 + id: contentRect 37 + anchors.fill: parent 38 + color: Config.Colours.base 39 + radius: Config.Appearance.rounding.large 40 + border.width: 1 41 + border.color: Config.Colours.surface0 42 + opacity: 0 43 + property bool _opening: false 44 + Behavior on opacity { 45 + Anim { 46 + duration: contentRect._opening ? Config.Appearance.anim.durations.normal : Config.Appearance.anim.durations.small 47 + easing.bezierCurve: contentRect._opening ? Config.Appearance.anim.curves.standardDecel : Config.Appearance.anim.curves.standardAccel 48 + } 49 + } 50 + 51 + ColumnLayout { 52 + id: audioContent 53 + anchors.fill: parent 54 + anchors.margins: 16 55 + spacing: 12 56 + 57 + Text { 58 + color: Config.Colours.text 59 + font.family: Config.Appearance.font.family 60 + font.pixelSize: Config.Appearance.font.size.normal 61 + font.bold: true 62 + text: "Volume" 63 + } 64 + 65 + RowLayout { 66 + Layout.fillWidth: true 67 + spacing: 8 68 + Text { color: Config.Colours.maroon; font.family: Config.Appearance.font.family; font.pixelSize: 20; text: Services.Audio.muted ? "\uf026" : "\uf028" } 69 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.large; font.bold: true; text: Services.Audio.muted ? "Muted" : Services.Audio.volume + "%" } 70 + } 71 + 72 + Item { 73 + Layout.fillWidth: true 74 + height: 6 75 + property real animatedPct: Services.Audio.volume 76 + Behavior on animatedPct { Anim { duration: Config.Appearance.anim.durations.large } } 77 + Rectangle { anchors.fill: parent; radius: 3; color: Config.Colours.surface1 } 78 + Rectangle { width: parent.width * (parent.animatedPct / 100); height: parent.height; radius: 3; color: Config.Colours.maroon } 79 + } 80 + 81 + Rectangle { 82 + Layout.fillWidth: true 83 + height: 32 84 + radius: Config.Appearance.rounding.normal 85 + color: muteMouse.containsMouse ? Config.Colours.surface1 : Config.Colours.surface0 86 + Behavior on color { CAnim {} } 87 + Text { anchors.centerIn: parent; color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.small; text: Services.Audio.muted ? "Unmute" : "Mute" } 88 + MouseArea { id: muteMouse; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: muteProc.running = true } 89 + } 90 + 91 + Process { id: muteProc; command: ["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"] } 92 + } 93 + 94 + Keys.onEscapePressed: audioPopout.close() 95 + } 96 + 97 + MouseArea { anchors.fill: parent; z: -1; onClicked: audioPopout.close() } 98 + }
+279
hosts/common/quickshell/modules/sidebar/popouts/DashboardPopout.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import Quickshell.Wayland 5 + import "../../../config" as Config 6 + import "../../../components" 7 + import "../../../services" as Services 8 + 9 + PopupWindow { 10 + id: dashPopout 11 + 12 + property bool show: false 13 + property var parentWindow 14 + property real anchorY: 0 15 + signal close() 16 + 17 + visible: show || contentRect.opacity > 0 18 + anchor.window: parentWindow 19 + anchor.onAnchoring: { 20 + anchor.rect.x = parentWindow.width + 8 21 + anchor.rect.y = Math.max(0, anchorY - implicitHeight / 2) 22 + } 23 + 24 + implicitWidth: 280 25 + implicitHeight: dashContent.implicitHeight + 32 26 + color: "transparent" 27 + 28 + onShowChanged: { 29 + if (show) { contentRect._opening = true; fadeIn.start(); resetMonth() } 30 + else { fadeIn.stop(); contentRect._opening = false; contentRect.opacity = 0 } 31 + } 32 + Timer { id: fadeIn; interval: 16; onTriggered: contentRect.opacity = 1 } 33 + 34 + property int calYear: new Date().getFullYear() 35 + property int calMonth: new Date().getMonth() 36 + 37 + function calendarDays() { 38 + var firstDay = new Date(calYear, calMonth, 1).getDay() 39 + var daysInMonth = new Date(calYear, calMonth + 1, 0).getDate() 40 + var prevDays = new Date(calYear, calMonth, 0).getDate() 41 + var today = new Date() 42 + var isCurrentMonth = (calYear === today.getFullYear() && calMonth === today.getMonth()) 43 + var days = [] 44 + for (var i = 0; i < 42; i++) { 45 + var dayNum = i - firstDay + 1 46 + if (dayNum < 1) 47 + days.push({ day: prevDays + dayNum, inMonth: false, isToday: false }) 48 + else if (dayNum > daysInMonth) 49 + days.push({ day: dayNum - daysInMonth, inMonth: false, isToday: false }) 50 + else 51 + days.push({ day: dayNum, inMonth: true, isToday: isCurrentMonth && dayNum === today.getDate() }) 52 + } 53 + return days 54 + } 55 + 56 + function prevMonth() { 57 + if (calMonth === 0) { calMonth = 11; calYear-- } 58 + else calMonth-- 59 + calModel = calendarDays() 60 + } 61 + 62 + function nextMonth() { 63 + if (calMonth === 11) { calMonth = 0; calYear++ } 64 + else calMonth++ 65 + calModel = calendarDays() 66 + } 67 + 68 + function resetMonth() { 69 + var now = new Date() 70 + calYear = now.getFullYear() 71 + calMonth = now.getMonth() 72 + calModel = calendarDays() 73 + } 74 + 75 + property var calModel: calendarDays() 76 + 77 + Rectangle { 78 + id: contentRect 79 + anchors.fill: parent 80 + color: Config.Colours.base 81 + radius: Config.Appearance.rounding.large 82 + border.width: 1 83 + border.color: Config.Colours.surface0 84 + opacity: 0 85 + property bool _opening: false 86 + Behavior on opacity { 87 + Anim { 88 + duration: contentRect._opening ? Config.Appearance.anim.durations.normal : Config.Appearance.anim.durations.small 89 + easing.bezierCurve: contentRect._opening ? Config.Appearance.anim.curves.standardDecel : Config.Appearance.anim.curves.standardAccel 90 + } 91 + } 92 + 93 + ColumnLayout { 94 + id: dashContent 95 + anchors.fill: parent 96 + anchors.margins: 16 97 + spacing: 12 98 + 99 + // Time + Date 100 + ColumnLayout { 101 + Layout.fillWidth: true 102 + spacing: 2 103 + 104 + Text { 105 + id: dashTime 106 + color: Config.Colours.blue 107 + font.family: Config.Appearance.font.family 108 + font.pixelSize: 32 109 + font.bold: true 110 + text: Qt.formatDateTime(new Date(), "HH:mm") 111 + Timer { 112 + interval: 1000 113 + running: dashPopout.show 114 + repeat: true 115 + onTriggered: dashTime.text = Qt.formatDateTime(new Date(), "HH:mm") 116 + } 117 + } 118 + 119 + Text { 120 + color: Config.Colours.subtext0 121 + font.family: Config.Appearance.font.family 122 + font.pixelSize: Config.Appearance.font.size.small 123 + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") 124 + } 125 + } 126 + 127 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 } 128 + 129 + // Calendar 130 + Column { 131 + Layout.fillWidth: true 132 + spacing: 4 133 + 134 + RowLayout { 135 + width: parent.width 136 + 137 + Text { 138 + color: Config.Colours.overlay0 139 + font.family: Config.Appearance.font.family 140 + font.pixelSize: 14 141 + text: "\uf053" 142 + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: dashPopout.prevMonth() } 143 + } 144 + Item { Layout.fillWidth: true } 145 + Text { 146 + color: Config.Colours.text 147 + font.family: Config.Appearance.font.family 148 + font.pixelSize: Config.Appearance.font.size.normal 149 + font.bold: true 150 + text: new Date(dashPopout.calYear, dashPopout.calMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") 151 + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: dashPopout.resetMonth() } 152 + } 153 + Item { Layout.fillWidth: true } 154 + Text { 155 + color: Config.Colours.overlay0 156 + font.family: Config.Appearance.font.family 157 + font.pixelSize: 14 158 + text: "\uf054" 159 + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: dashPopout.nextMonth() } 160 + } 161 + } 162 + 163 + Row { 164 + spacing: 0 165 + Repeater { 166 + model: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] 167 + delegate: Text { 168 + required property string modelData 169 + width: (dashContent.width - 32) / 7 170 + horizontalAlignment: Text.AlignHCenter 171 + color: Config.Colours.overlay0 172 + font.family: Config.Appearance.font.family 173 + font.pixelSize: 10 174 + text: modelData 175 + } 176 + } 177 + } 178 + 179 + Grid { 180 + columns: 7 181 + rowSpacing: 2 182 + 183 + Repeater { 184 + model: dashPopout.calModel 185 + delegate: Item { 186 + required property var modelData 187 + width: (dashContent.width - 32) / 7 188 + height: 24 189 + 190 + Rectangle { 191 + anchors.centerIn: parent 192 + width: 22; height: 22; radius: 11 193 + color: modelData.isToday ? Config.Colours.blue : "transparent" 194 + } 195 + 196 + Text { 197 + anchors.centerIn: parent 198 + color: modelData.isToday ? Config.Colours.crust 199 + : (modelData.inMonth ? Config.Colours.text : Config.Colours.overlay0) 200 + font.family: Config.Appearance.font.family 201 + font.pixelSize: 11 202 + font.bold: modelData.isToday 203 + text: modelData.day 204 + } 205 + } 206 + } 207 + } 208 + } 209 + 210 + Rectangle { 211 + Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 212 + visible: Services.Weather.weather !== "" 213 + } 214 + 215 + // Weather 216 + RowLayout { 217 + Layout.fillWidth: true 218 + spacing: 8 219 + visible: Services.Weather.weather !== "" 220 + 221 + Text { 222 + color: Config.Colours.yellow 223 + font.family: Config.Appearance.font.family 224 + font.pixelSize: 16 225 + text: "\uf0eb" 226 + } 227 + Text { 228 + Layout.fillWidth: true 229 + color: Config.Colours.text 230 + font.family: Config.Appearance.font.family 231 + font.pixelSize: Config.Appearance.font.size.normal 232 + text: Services.Weather.weather 233 + wrapMode: Text.WordWrap 234 + } 235 + } 236 + 237 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 } 238 + 239 + // System 240 + ColumnLayout { 241 + Layout.fillWidth: true 242 + spacing: 6 243 + 244 + Text { 245 + color: Config.Colours.text 246 + font.family: Config.Appearance.font.family 247 + font.pixelSize: Config.Appearance.font.size.normal 248 + font.bold: true 249 + text: "System" 250 + } 251 + 252 + Repeater { 253 + model: { 254 + var items = [ 255 + { icon: "\uf2db", label: "CPU", value: Services.SystemUsage.cpuUsage + "%", color: Config.Colours.peach }, 256 + { icon: "\uefc5", label: "Memory", value: Services.SystemUsage.memUsage + "%", color: Config.Colours.mauve }, 257 + { icon: "\uf2c9", label: "Temp", value: Services.SystemUsage.temperature + "\u00b0C", color: Config.Colours.red } 258 + ] 259 + if (Services.Battery.hasBattery) 260 + items.push({ icon: "\uf241", label: "Battery", value: Services.Battery.percent + "%", color: Config.Colours.green }) 261 + return items 262 + } 263 + delegate: RowLayout { 264 + required property var modelData 265 + Layout.fillWidth: true 266 + Text { color: modelData.color; font.family: Config.Appearance.font.family; font.pixelSize: 12; text: modelData.icon } 267 + Text { color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: 12; text: modelData.label } 268 + Item { Layout.fillWidth: true } 269 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: 12; text: modelData.value } 270 + } 271 + } 272 + } 273 + } 274 + 275 + Keys.onEscapePressed: dashPopout.close() 276 + } 277 + 278 + MouseArea { anchors.fill: parent; z: -1; onClicked: dashPopout.close() } 279 + }
+91
hosts/common/quickshell/modules/sidebar/popouts/NetworkPopout.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import Quickshell.Wayland 5 + import "../../../config" as Config 6 + import "../../../components" 7 + import "../../../services" as Services 8 + 9 + PopupWindow { 10 + id: networkPopout 11 + 12 + property bool show: false 13 + property var parentWindow 14 + property real anchorY: 0 15 + signal close() 16 + 17 + visible: show || contentRect.opacity > 0 18 + anchor.window: parentWindow 19 + anchor.onAnchoring: { 20 + anchor.rect.x = parentWindow.width + 8 21 + anchor.rect.y = Math.max(0, anchorY - implicitHeight / 2) 22 + } 23 + 24 + implicitWidth: 220 25 + implicitHeight: netContent.implicitHeight + 32 26 + color: "transparent" 27 + 28 + onShowChanged: { 29 + if (show) { contentRect._opening = true; fadeIn.start() } 30 + else { fadeIn.stop(); contentRect._opening = false; contentRect.opacity = 0 } 31 + } 32 + Timer { id: fadeIn; interval: 16; onTriggered: contentRect.opacity = 1 } 33 + 34 + Rectangle { 35 + id: contentRect 36 + anchors.fill: parent 37 + color: Config.Colours.base 38 + radius: Config.Appearance.rounding.large 39 + border.width: 1 40 + border.color: Config.Colours.surface0 41 + opacity: 0 42 + property bool _opening: false 43 + Behavior on opacity { 44 + Anim { 45 + duration: contentRect._opening ? Config.Appearance.anim.durations.normal : Config.Appearance.anim.durations.small 46 + easing.bezierCurve: contentRect._opening ? Config.Appearance.anim.curves.standardDecel : Config.Appearance.anim.curves.standardAccel 47 + } 48 + } 49 + 50 + ColumnLayout { 51 + id: netContent 52 + anchors.fill: parent 53 + anchors.margins: 16 54 + spacing: 12 55 + 56 + Text { 57 + color: Config.Colours.text 58 + font.family: Config.Appearance.font.family 59 + font.pixelSize: Config.Appearance.font.size.normal 60 + font.bold: true 61 + text: "Network" 62 + } 63 + 64 + RowLayout { 65 + Layout.fillWidth: true 66 + spacing: 8 67 + 68 + Text { 69 + color: Config.Colours.green 70 + font.family: Config.Appearance.font.family 71 + font.pixelSize: 20 72 + text: { 73 + if (!Services.Network.connected) return "\uf071" 74 + if (Services.Network.isEthernet) return "\udb80\ude00" 75 + return "\uf1eb" 76 + } 77 + } 78 + 79 + ColumnLayout { 80 + spacing: 2 81 + Text { color: Config.Colours.text; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.normal; text: Services.Network.connected ? Services.Network.name : "Disconnected" } 82 + Text { color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.small; text: { if (!Services.Network.connected) return "No connection"; if (Services.Network.isEthernet) return "Wired connection"; return "Signal: " + Services.Network.strength + "%" } } 83 + } 84 + } 85 + } 86 + 87 + Keys.onEscapePressed: networkPopout.close() 88 + } 89 + 90 + MouseArea { anchors.fill: parent; z: -1; onClicked: networkPopout.close() } 91 + }
+163
hosts/common/quickshell/modules/sidebar/popouts/PowerProfilePopout.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import "../../../config" as Config 5 + import "../../../components" 6 + import "../../../services" as Services 7 + 8 + PopupWindow { 9 + id: profilePopout 10 + 11 + property bool show: false 12 + property var parentWindow 13 + property real anchorY: 0 14 + signal close() 15 + 16 + visible: show || contentRect.opacity > 0 17 + anchor.window: parentWindow 18 + anchor.onAnchoring: { 19 + anchor.rect.x = parentWindow.width + 8 20 + anchor.rect.y = Math.max(0, anchorY - implicitHeight / 2) 21 + } 22 + 23 + implicitWidth: 240 24 + implicitHeight: profileContent.implicitHeight + 32 25 + color: "transparent" 26 + 27 + onShowChanged: { 28 + if (show) { contentRect._opening = true; fadeIn.start() } 29 + else { fadeIn.stop(); contentRect._opening = false; contentRect.opacity = 0 } 30 + } 31 + Timer { id: fadeIn; interval: 16; onTriggered: contentRect.opacity = 1 } 32 + 33 + Rectangle { 34 + id: contentRect 35 + anchors.fill: parent 36 + color: Config.Colours.base 37 + radius: Config.Appearance.rounding.large 38 + border.width: 1 39 + border.color: Config.Colours.surface0 40 + opacity: 0 41 + property bool _opening: false 42 + Behavior on opacity { 43 + Anim { 44 + duration: contentRect._opening ? Config.Appearance.anim.durations.normal : Config.Appearance.anim.durations.small 45 + easing.bezierCurve: contentRect._opening ? Config.Appearance.anim.curves.standardDecel : Config.Appearance.anim.curves.standardAccel 46 + } 47 + } 48 + 49 + ColumnLayout { 50 + id: profileContent 51 + anchors.fill: parent 52 + anchors.margins: 16 53 + spacing: 12 54 + 55 + // Battery info 56 + RowLayout { 57 + Layout.fillWidth: true 58 + spacing: 8 59 + 60 + Text { 61 + color: { 62 + if (Services.Battery.percent <= 20) return Config.Colours.red 63 + if (Services.Battery.status === "Charging") return Config.Colours.green 64 + return Config.Colours.green 65 + } 66 + font.family: Config.Appearance.font.family 67 + font.pixelSize: 20 68 + text: { 69 + if (Services.Battery.status === "Charging") return "\uf0e7" 70 + if (Services.Battery.percent > 75) return "\uf240" 71 + if (Services.Battery.percent > 50) return "\uf241" 72 + if (Services.Battery.percent > 25) return "\uf242" 73 + if (Services.Battery.percent > 10) return "\uf243" 74 + return "\uf244" 75 + } 76 + } 77 + 78 + ColumnLayout { 79 + spacing: 2 80 + Text { 81 + color: Config.Colours.text 82 + font.family: Config.Appearance.font.family 83 + font.pixelSize: Config.Appearance.font.size.normal 84 + text: Services.Battery.percent > 0 ? Services.Battery.percent + "%" : "No battery detected" 85 + } 86 + Text { 87 + color: Config.Colours.subtext0 88 + font.family: Config.Appearance.font.family 89 + font.pixelSize: Config.Appearance.font.size.small 90 + text: { 91 + if (Services.PowerProfile.profile === "power-saver") return "Power profile: Power Saver" 92 + if (Services.PowerProfile.profile === "performance") return "Power profile: Performance" 93 + if (Services.PowerProfile.profile === "balanced") return "Power profile: Balanced" 94 + return "Power profile: " + Services.PowerProfile.profile 95 + } 96 + } 97 + } 98 + } 99 + 100 + Rectangle { Layout.fillWidth: true; height: 1; color: Config.Colours.surface1 } 101 + 102 + // Profile buttons 103 + RowLayout { 104 + Layout.fillWidth: true 105 + Layout.alignment: Qt.AlignHCenter 106 + spacing: 12 107 + 108 + Repeater { 109 + model: [ 110 + { id: "performance", icon: "\uf0e7", label: "Perf", color: Config.Colours.peach }, 111 + { id: "balanced", icon: "\uf24e", label: "Bal", color: Config.Colours.blue }, 112 + { id: "power-saver", icon: "\uf06c", label: "Saver", color: Config.Colours.green } 113 + ] 114 + 115 + delegate: Rectangle { 116 + required property var modelData 117 + width: 56; height: 56 118 + radius: Config.Appearance.rounding.full 119 + color: { 120 + if (Services.PowerProfile.profile === modelData.id) 121 + return Qt.rgba(modelData.color.r, modelData.color.g, modelData.color.b, 0.25) 122 + return profileMouse.containsMouse ? Config.Colours.surface1 : Config.Colours.surface0 123 + } 124 + border.width: Services.PowerProfile.profile === modelData.id ? 2 : 0 125 + border.color: modelData.color 126 + Behavior on color { CAnim {} } 127 + 128 + ColumnLayout { 129 + anchors.centerIn: parent 130 + spacing: 2 131 + Text { 132 + Layout.alignment: Qt.AlignHCenter 133 + color: modelData.color 134 + font.family: Config.Appearance.font.family 135 + font.pixelSize: 18 136 + text: modelData.icon 137 + } 138 + Text { 139 + Layout.alignment: Qt.AlignHCenter 140 + color: Config.Colours.subtext0 141 + font.family: Config.Appearance.font.family 142 + font.pixelSize: 9 143 + text: modelData.label 144 + } 145 + } 146 + 147 + MouseArea { 148 + id: profileMouse 149 + anchors.fill: parent 150 + hoverEnabled: true 151 + cursorShape: Qt.PointingHandCursor 152 + onClicked: Services.PowerProfile.setProfile(modelData.id) 153 + } 154 + } 155 + } 156 + } 157 + } 158 + 159 + Keys.onEscapePressed: profilePopout.close() 160 + } 161 + 162 + MouseArea { anchors.fill: parent; z: -1; onClicked: profilePopout.close() } 163 + }
+107
hosts/common/quickshell/modules/sidebar/popouts/SessionPopout.qml
··· 1 + import QtQuick 2 + import QtQuick.Layouts 3 + import Quickshell 4 + import Quickshell.Wayland 5 + import Quickshell.Io 6 + import "../../../config" as Config 7 + import "../../../components" 8 + 9 + PopupWindow { 10 + id: sessionPopout 11 + 12 + property bool show: false 13 + property var parentWindow 14 + property real anchorY: 0 15 + signal close() 16 + 17 + visible: show || contentRect.opacity > 0 18 + anchor.window: parentWindow 19 + anchor.onAnchoring: { 20 + anchor.rect.x = parentWindow.width + 8 21 + anchor.rect.y = Math.max(0, anchorY - implicitHeight / 2) 22 + } 23 + 24 + implicitWidth: content.implicitWidth + 48 25 + implicitHeight: content.implicitHeight + 48 26 + color: "transparent" 27 + 28 + onShowChanged: { 29 + if (show) { contentRect._opening = true; fadeIn.start() } 30 + else { fadeIn.stop(); contentRect._opening = false; contentRect.opacity = 0 } 31 + } 32 + Timer { id: fadeIn; interval: 16; onTriggered: contentRect.opacity = 1 } 33 + 34 + Rectangle { 35 + id: contentRect 36 + anchors.fill: parent 37 + color: Config.Colours.base 38 + radius: Config.Appearance.rounding.large 39 + border.width: 1 40 + border.color: Config.Colours.surface0 41 + opacity: 0 42 + property bool _opening: false 43 + Behavior on opacity { 44 + Anim { 45 + duration: contentRect._opening ? Config.Appearance.anim.durations.normal : Config.Appearance.anim.durations.small 46 + easing.bezierCurve: contentRect._opening ? Config.Appearance.anim.curves.standardDecel : Config.Appearance.anim.curves.standardAccel 47 + } 48 + } 49 + 50 + ColumnLayout { 51 + id: content 52 + anchors.centerIn: parent 53 + spacing: 12 54 + 55 + Text { 56 + Layout.alignment: Qt.AlignHCenter 57 + color: Config.Colours.text 58 + font.family: Config.Appearance.font.family 59 + font.pixelSize: Config.Appearance.font.size.large 60 + text: "Session" 61 + } 62 + 63 + Grid { 64 + columns: 2 65 + spacing: 8 66 + 67 + Repeater { 68 + model: [ 69 + { label: "Logout", icon: "\udb81\ude99", color: Config.Colours.yellow, cmd: "loginctl terminate-user $USER" }, 70 + { label: "Suspend", icon: "\udb81\udf86", color: Config.Colours.blue, cmd: "systemctl suspend" }, 71 + { label: "Reboot", icon: "\udb80\udd30", color: Config.Colours.peach, cmd: "systemctl reboot" }, 72 + { label: "Shutdown", icon: "\u23fb", color: Config.Colours.red, cmd: "systemctl poweroff" } 73 + ] 74 + 75 + delegate: Rectangle { 76 + required property var modelData 77 + width: 80; height: 80 78 + radius: Config.Appearance.rounding.normal 79 + color: sessionMouse.containsMouse ? Config.Colours.surface1 : Config.Colours.surface0 80 + Behavior on color { CAnim {} } 81 + 82 + ColumnLayout { 83 + anchors.centerIn: parent 84 + spacing: 4 85 + Text { Layout.alignment: Qt.AlignHCenter; color: modelData.color; font.family: Config.Appearance.font.family; font.pixelSize: 24; text: modelData.icon } 86 + Text { Layout.alignment: Qt.AlignHCenter; color: Config.Colours.subtext0; font.family: Config.Appearance.font.family; font.pixelSize: Config.Appearance.font.size.small; text: modelData.label } 87 + } 88 + 89 + MouseArea { 90 + id: sessionMouse 91 + anchors.fill: parent 92 + hoverEnabled: true 93 + cursorShape: Qt.PointingHandCursor 94 + onClicked: { sessionPopout.close(); execProc.command = ["sh", "-c", modelData.cmd]; execProc.running = true } 95 + } 96 + } 97 + } 98 + } 99 + 100 + Process { id: execProc } 101 + } 102 + 103 + Keys.onEscapePressed: sessionPopout.close() 104 + } 105 + 106 + MouseArea { anchors.fill: parent; z: -1; onClicked: sessionPopout.close() } 107 + }
+5
hosts/common/quickshell/modules/sidebar/popouts/qmldir
··· 1 + DashboardPopout 1.0 DashboardPopout.qml 2 + SessionPopout 1.0 SessionPopout.qml 3 + AudioPopout 1.0 AudioPopout.qml 4 + NetworkPopout 1.0 NetworkPopout.qml 5 + PowerProfilePopout 1.0 PowerProfilePopout.qml
+1
hosts/common/quickshell/modules/sidebar/qmldir
··· 1 + Sidebar 1.0 Sidebar.qml
+33
hosts/common/quickshell/services/Audio.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property int volume: _volume 9 + readonly property bool muted: _muted 10 + 11 + property int _volume: 0 12 + property bool _muted: false 13 + 14 + property var _proc: Process { 15 + command: ["sh", "-c", "wpctl get-volume @DEFAULT_AUDIO_SINK@"] 16 + stdout: SplitParser { 17 + onRead: data => { 18 + if (!data) return 19 + _muted = data.indexOf("[MUTED]") !== -1 20 + var match = data.match(/Volume:\s+([\d.]+)/) 21 + if (match) _volume = Math.round(parseFloat(match[1]) * 100) 22 + } 23 + } 24 + Component.onCompleted: running = true 25 + } 26 + 27 + property var _timer: Timer { 28 + interval: 2000 29 + running: true 30 + repeat: true 31 + onTriggered: _proc.running = true 32 + } 33 + }
+58
hosts/common/quickshell/services/Battery.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property int percent: _percent 9 + readonly property string status: _status 10 + readonly property bool hasBattery: _hasBattery 11 + 12 + property int _percent: 0 13 + property string _status: "Unknown" 14 + property bool _hasBattery: false 15 + 16 + property var _detectProc: Process { 17 + command: ["sh", "-c", "test -d /sys/class/power_supply/BAT1 && echo yes || echo no"] 18 + stdout: SplitParser { 19 + onRead: data => { 20 + if (!data) return 21 + _hasBattery = data.trim() === "yes" 22 + } 23 + } 24 + Component.onCompleted: running = true 25 + } 26 + 27 + property var _capProc: Process { 28 + command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/capacity 2>/dev/null || echo 0"] 29 + stdout: SplitParser { 30 + onRead: data => { 31 + if (!data) return 32 + _percent = parseInt(data.trim()) || 0 33 + } 34 + } 35 + Component.onCompleted: running = true 36 + } 37 + 38 + property var _statusProc: Process { 39 + command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/status 2>/dev/null || echo Unknown"] 40 + stdout: SplitParser { 41 + onRead: data => { 42 + if (!data) return 43 + _status = data.trim() 44 + } 45 + } 46 + Component.onCompleted: running = true 47 + } 48 + 49 + property var _timer: Timer { 50 + interval: 2000 51 + running: true 52 + repeat: true 53 + onTriggered: { 54 + _capProc.running = true 55 + _statusProc.running = true 56 + } 57 + } 58 + }
+29
hosts/common/quickshell/services/Brightness.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property int brightness: _brightness 9 + 10 + property int _brightness: 0 11 + 12 + property var _proc: Process { 13 + command: ["sh", "-c", "brightnessctl -m | cut -d, -f4 | tr -d '%'"] 14 + stdout: SplitParser { 15 + onRead: data => { 16 + if (!data) return 17 + _brightness = parseInt(data.trim()) || 0 18 + } 19 + } 20 + Component.onCompleted: running = true 21 + } 22 + 23 + property var _timer: Timer { 24 + interval: 2000 25 + running: true 26 + repeat: true 27 + onTriggered: _proc.running = true 28 + } 29 + }
+63
hosts/common/quickshell/services/Mpris.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Services.Mpris as MprisService 6 + 7 + QtObject { 8 + id: root 9 + 10 + property MprisService.MprisPlayer _trackedPlayer: null 11 + 12 + readonly property MprisService.MprisPlayer activePlayer: { 13 + // If tracked player is valid and playing, prefer it 14 + if (_trackedPlayer && _trackedPlayer.isPlaying) return _trackedPlayer 15 + // Otherwise find any playing player 16 + var players = MprisService.Mpris.players.values 17 + for (var i = 0; i < players.length; i++) { 18 + if (players[i].isPlaying) return players[i] 19 + } 20 + // Fall back to tracked or first available 21 + if (_trackedPlayer) return _trackedPlayer 22 + return players.length > 0 ? players[0] : null 23 + } 24 + 25 + readonly property string title: activePlayer ? activePlayer.trackTitle : "" 26 + readonly property string artist: activePlayer ? activePlayer.trackArtist : "" 27 + readonly property string album: activePlayer ? activePlayer.trackAlbum : "" 28 + readonly property string artUrl: activePlayer ? activePlayer.trackArtUrl : "" 29 + readonly property bool isPlaying: activePlayer ? activePlayer.isPlaying : false 30 + readonly property real position: activePlayer ? activePlayer.position : 0 31 + readonly property real length: activePlayer ? activePlayer.length : 0 32 + readonly property bool hasPlayer: activePlayer !== null 33 + readonly property bool canNext: activePlayer ? activePlayer.canGoNext : false 34 + readonly property bool canPrev: activePlayer ? activePlayer.canGoPrevious : false 35 + 36 + // Track player changes reactively via Instantiator 37 + property var _tracker: Instantiator { 38 + model: MprisService.Mpris.players 39 + delegate: QtObject { 40 + required property MprisService.MprisPlayer modelData 41 + 42 + property var _conn: Connections { 43 + target: modelData 44 + 45 + Component.onCompleted: { 46 + if (root._trackedPlayer === null || modelData.isPlaying) { 47 + root._trackedPlayer = modelData 48 + } 49 + } 50 + 51 + function onPlaybackStateChanged() { 52 + if (modelData.isPlaying) { 53 + root._trackedPlayer = modelData 54 + } 55 + } 56 + } 57 + } 58 + } 59 + 60 + function togglePlaying() { if (activePlayer) activePlayer.togglePlaying() } 61 + function next() { if (activePlayer) activePlayer.next() } 62 + function previous() { if (activePlayer) activePlayer.previous() } 63 + }
+53
hosts/common/quickshell/services/Network.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property bool connected: _connected 9 + readonly property bool isEthernet: _isEthernet 10 + readonly property string name: _name 11 + readonly property int strength: _strength 12 + 13 + property bool _connected: false 14 + property bool _isEthernet: false 15 + property string _name: "" 16 + property int _strength: 0 17 + 18 + property var _proc: Process { 19 + command: ["sh", "-c", "eth=$(nmcli -t -f TYPE,STATE,CONNECTION dev | grep '^ethernet:connected:' | head -1); if [ -n \"$eth\" ]; then echo \"ethernet:$(echo $eth | cut -d: -f3)\"; else nmcli -t -f ACTIVE,SSID,SIGNAL dev wifi | grep '^yes' | head -1; fi"] 20 + stdout: SplitParser { 21 + onRead: data => { 22 + if (!data || data.trim() === "") { 23 + _connected = false 24 + _isEthernet = false 25 + _name = "Disconnected" 26 + _strength = 0 27 + return 28 + } 29 + var trimmed = data.trim() 30 + if (trimmed.startsWith("ethernet:")) { 31 + _connected = true 32 + _isEthernet = true 33 + _name = trimmed.split(":")[1] || "Ethernet" 34 + _strength = 100 35 + } else { 36 + var parts = trimmed.split(":") 37 + _connected = true 38 + _isEthernet = false 39 + _name = parts[1] || "" 40 + _strength = parseInt(parts[2]) || 0 41 + } 42 + } 43 + } 44 + Component.onCompleted: running = true 45 + } 46 + 47 + property var _timer: Timer { 48 + interval: 2000 49 + running: true 50 + repeat: true 51 + onTriggered: _proc.running = true 52 + } 53 + }
+65
hosts/common/quickshell/services/PowerProfile.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property string profile: _profile 9 + readonly property string backend: _backend 10 + 11 + property string _profile: "" 12 + property string _backend: "" 13 + 14 + function setProfile(name) { 15 + if (_backend === "system76") { 16 + var s76profile = name === "power-saver" ? "battery" : name 17 + _setProc.command = ["system76-power", "profile", s76profile] 18 + } else { 19 + _setProc.command = ["powerprofilesctl", "set", name] 20 + } 21 + _setProc.running = true 22 + _profile = name 23 + } 24 + 25 + function cycleProfile() { 26 + var next = "balanced" 27 + if (_profile === "balanced") next = "performance" 28 + else if (_profile === "performance") next = "power-saver" 29 + else next = "balanced" 30 + setProfile(next) 31 + } 32 + 33 + property var _backendProc: Process { 34 + command: ["sh", "-c", "command -v system76-power >/dev/null 2>&1 && echo system76 || echo ppd"] 35 + stdout: SplitParser { 36 + onRead: data => { 37 + if (!data) return 38 + _backend = data.trim() 39 + } 40 + } 41 + Component.onCompleted: running = true 42 + } 43 + 44 + property var _profileProc: Process { 45 + command: ["sh", "-c", "if command -v system76-power >/dev/null 2>&1; then p=$(system76-power profile 2>/dev/null | grep -oiE 'performance|balanced|battery' | tr '[:upper:]' '[:lower:]'); [ \"$p\" = \"battery\" ] && p=\"power-saver\"; echo \"${p:-unknown}\"; else powerprofilesctl get 2>/dev/null || echo unknown; fi"] 46 + stdout: SplitParser { 47 + onRead: data => { 48 + if (!data) return 49 + _profile = data.trim() 50 + } 51 + } 52 + Component.onCompleted: running = true 53 + } 54 + 55 + property var _setProc: Process { 56 + id: _setProc 57 + } 58 + 59 + property var _timer: Timer { 60 + interval: 2000 61 + running: true 62 + repeat: true 63 + onTriggered: _profileProc.running = true 64 + } 65 + }
+36
hosts/common/quickshell/services/Storage.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property int usagePercent: _usagePercent 9 + readonly property string usedStr: _usedStr 10 + readonly property string totalStr: _totalStr 11 + 12 + property int _usagePercent: 0 13 + property string _usedStr: "" 14 + property string _totalStr: "" 15 + 16 + property var _proc: Process { 17 + command: ["sh", "-c", "df -h / | tail -1"] 18 + stdout: SplitParser { 19 + onRead: data => { 20 + if (!data) return 21 + var parts = data.trim().split(/\s+/) 22 + _totalStr = parts[1] || "" 23 + _usedStr = parts[2] || "" 24 + _usagePercent = parseInt((parts[4] || "0").replace("%", "")) || 0 25 + } 26 + } 27 + Component.onCompleted: running = true 28 + } 29 + 30 + property var _timer: Timer { 31 + interval: 30000 32 + running: true 33 + repeat: true 34 + onTriggered: _proc.running = true 35 + } 36 + }
+71
hosts/common/quickshell/services/SystemUsage.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property int cpuUsage: _cpuUsage 9 + readonly property int memUsage: _memUsage 10 + readonly property int temperature: _temperature 11 + 12 + property int _cpuUsage: 0 13 + property int _memUsage: 0 14 + property int _temperature: 0 15 + property var _lastCpuIdle: 0 16 + property var _lastCpuTotal: 0 17 + 18 + property var _cpuProc: Process { 19 + command: ["sh", "-c", "head -1 /proc/stat"] 20 + stdout: SplitParser { 21 + onRead: data => { 22 + if (!data) return 23 + var p = data.trim().split(/\s+/) 24 + var idle = parseInt(p[4]) + parseInt(p[5]) 25 + var total = p.slice(1, 8).reduce((a, b) => a + parseInt(b), 0) 26 + if (_lastCpuTotal > 0) { 27 + _cpuUsage = Math.round(100 * (1 - (idle - _lastCpuIdle) / (total - _lastCpuTotal))) 28 + } 29 + _lastCpuTotal = total 30 + _lastCpuIdle = idle 31 + } 32 + } 33 + Component.onCompleted: running = true 34 + } 35 + 36 + property var _memProc: Process { 37 + command: ["sh", "-c", "free | grep Mem"] 38 + stdout: SplitParser { 39 + onRead: data => { 40 + if (!data) return 41 + var parts = data.trim().split(/\s+/) 42 + var total = parseInt(parts[1]) || 1 43 + var used = parseInt(parts[2]) || 0 44 + _memUsage = Math.round(100 * used / total) 45 + } 46 + } 47 + Component.onCompleted: running = true 48 + } 49 + 50 + property var _tempProc: Process { 51 + command: ["sh", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"] 52 + stdout: SplitParser { 53 + onRead: data => { 54 + if (!data) return 55 + _temperature = Math.round(parseInt(data.trim()) / 1000) 56 + } 57 + } 58 + Component.onCompleted: running = true 59 + } 60 + 61 + property var _timer: Timer { 62 + interval: 2000 63 + running: true 64 + repeat: true 65 + onTriggered: { 66 + _cpuProc.running = true 67 + _memProc.running = true 68 + _tempProc.running = true 69 + } 70 + } 71 + }
+41
hosts/common/quickshell/services/Weather.qml
··· 1 + pragma Singleton 2 + 3 + import QtQuick 4 + import Quickshell 5 + import Quickshell.Io 6 + 7 + QtObject { 8 + readonly property string weather: _weather 9 + readonly property string icon: _icon 10 + readonly property string temp: _temp 11 + 12 + property string _weather: "" 13 + property string _icon: "" 14 + property string _temp: "" 15 + 16 + property var _proc: Process { 17 + command: ["sh", "-c", "curl -sf 'wttr.in/?format=%c|%t&m' | tr -d '+'"] 18 + stdout: SplitParser { 19 + onRead: data => { 20 + if (!data) return 21 + var raw = data.trim() 22 + var parts = raw.split("|") 23 + if (parts.length >= 2) { 24 + _icon = parts[0].trim() 25 + _temp = parts[1].trim() 26 + _weather = _icon + " " + _temp 27 + } else { 28 + _weather = raw 29 + } 30 + } 31 + } 32 + Component.onCompleted: running = true 33 + } 34 + 35 + property var _timer: Timer { 36 + interval: 900000 37 + running: true 38 + repeat: true 39 + onTriggered: _proc.running = true 40 + } 41 + }
+9
hosts/common/quickshell/services/qmldir
··· 1 + singleton Audio 1.0 Audio.qml 2 + singleton Brightness 1.0 Brightness.qml 3 + singleton Network 1.0 Network.qml 4 + singleton SystemUsage 1.0 SystemUsage.qml 5 + singleton Battery 1.0 Battery.qml 6 + singleton PowerProfile 1.0 PowerProfile.qml 7 + singleton Weather 1.0 Weather.qml 8 + singleton Mpris 1.0 Mpris.qml 9 + singleton Storage 1.0 Storage.qml
+12 -534
hosts/common/quickshell/shell.qml
··· 1 1 import Quickshell 2 - import Quickshell.Wayland 3 - import Quickshell.Widgets 4 - import Quickshell.Io 5 - import Quickshell.Services.SystemTray 6 - import QtQuick 7 - import QtQuick.Layouts 2 + import "modules/sidebar" 3 + import "modules/dashboard" 4 + import "modules/rightpanel" 8 5 9 6 ShellRoot { 10 7 Variants { 11 8 model: Quickshell.screens 12 - 13 - PanelWindow { 14 - id: root 15 - screen: modelData 16 - 17 - // Catppuccin Frappe palette 18 - readonly property color colBase: "#303446" 19 - readonly property color colMantle: "#292c3c" 20 - readonly property color colSurface0: "#414559" 21 - readonly property color colText: "#c6d0f5" 22 - readonly property color colSubtext0: "#a5adce" 23 - readonly property color colOverlay0: "#737994" 24 - readonly property color colBlue: "#8caaee" 25 - readonly property color colGreen: "#a6d189" 26 - readonly property color colPeach: "#ef9f76" 27 - readonly property color colMauve: "#ca9ee6" 28 - readonly property color colRed: "#e78284" 29 - readonly property color colYellow: "#e5c890" 30 - readonly property color colMaroon: "#ea999c" 31 - readonly property color colLavender: "#babbf1" 32 - readonly property color colSky: "#99d1db" 33 - readonly property color colSapphire: "#85c1dc" 34 - 35 - readonly property string fontFamily: "BerkeleyMono Nerd Font" 36 - readonly property int fontSize: 14 37 - 38 - // Nerd Font icons (using Unicode escapes) 39 - // Volume 40 - readonly property string iconVolHigh: "\uf028" 41 - readonly property string iconVolLow: "\uf027" 42 - readonly property string iconVolMute: "\uf6a9" 43 - // Network 44 - readonly property string iconWifi: "\uf1eb" 45 - readonly property string iconEthernet: "\udb80\ude00" 46 - // Power profiles 47 - readonly property string iconBolt: "\uf0e7" 48 - readonly property string iconBalance: "\uf24e" 49 - readonly property string iconLeaf: "\uf06c" 50 - // CPU 51 - readonly property string iconCpu: "\uf2db" 52 - // Memory 53 - readonly property string iconMem: "\uefc5" 54 - // Temperature 55 - readonly property string iconTempLow: "\uf2cb" 56 - readonly property string iconTempMed: "\uf2c9" 57 - readonly property string iconTempHigh: "\uf2c7" 58 - // Backlight 59 - readonly property string iconSun: "\uf185" 60 - // Battery 61 - readonly property string iconBatEmpty: "\uf244" 62 - readonly property string iconBatQuarter: "\uf243" 63 - readonly property string iconBatHalf: "\uf242" 64 - readonly property string iconBatThreeQ: "\uf241" 65 - readonly property string iconBatFull: "\uf240" 66 - readonly property string iconBatCharge: "\uf0e7" 67 - // Power 68 - readonly property string iconPower: "\u23fb" 69 - // System data 70 - property int cpuUsage: 0 71 - property int memUsage: 0 72 - property int temperature: 0 73 - property var lastCpuIdle: 0 74 - property var lastCpuTotal: 0 75 - property string networkName: "" 76 - property int networkStrength: 0 77 - property bool networkConnected: false 78 - property bool networkIsEthernet: false 79 - property int volume: 0 80 - property bool volumeMuted: false 81 - property int brightness: 0 82 - property int batteryPercent: 0 83 - property string batteryStatus: "" 84 - property string powerProfile: "" 85 - property string powerBackend: "" // "system76" or "ppd" 86 - property string weather: "" 87 - property bool barHovered: false 88 - 89 - anchors { 90 - top: true 91 - left: true 92 - right: true 93 - } 94 - margins.top: 0 95 - margins.bottom: 0 96 - margins.left: 0 97 - margins.right: 0 98 - implicitHeight: 30 99 - color: Qt.rgba(0.188, 0.204, 0.275, 0.85) 100 - 101 - MouseArea { 102 - z: -1 103 - anchors.fill: parent 104 - hoverEnabled: true 105 - acceptedButtons: Qt.NoButton 106 - onContainsMouseChanged: { 107 - if (containsMouse) { trayHideTimer.stop(); root.barHovered = true } 108 - else { trayHideTimer.restart() } 109 - } 110 - } 111 - 112 - Timer { 113 - id: trayHideTimer 114 - interval: 500 115 - onTriggered: root.barHovered = false 116 - } 117 - 118 - // CPU process 119 - Process { 120 - id: cpuProc 121 - command: ["sh", "-c", "head -1 /proc/stat"] 122 - stdout: SplitParser { 123 - onRead: data => { 124 - if (!data) return 125 - var p = data.trim().split(/\s+/) 126 - var idle = parseInt(p[4]) + parseInt(p[5]) 127 - var total = p.slice(1, 8).reduce((a, b) => a + parseInt(b), 0) 128 - if (root.lastCpuTotal > 0) { 129 - root.cpuUsage = Math.round(100 * (1 - (idle - root.lastCpuIdle) / (total - root.lastCpuTotal))) 130 - } 131 - root.lastCpuTotal = total 132 - root.lastCpuIdle = idle 133 - } 134 - } 135 - Component.onCompleted: running = true 136 - } 137 - 138 - // Memory process 139 - Process { 140 - id: memProc 141 - command: ["sh", "-c", "free | grep Mem"] 142 - stdout: SplitParser { 143 - onRead: data => { 144 - if (!data) return 145 - var parts = data.trim().split(/\s+/) 146 - var total = parseInt(parts[1]) || 1 147 - var used = parseInt(parts[2]) || 0 148 - root.memUsage = Math.round(100 * used / total) 149 - } 150 - } 151 - Component.onCompleted: running = true 152 - } 153 - 154 - // Temperature 155 - Process { 156 - id: tempProc 157 - command: ["sh", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"] 158 - stdout: SplitParser { 159 - onRead: data => { 160 - if (!data) return 161 - root.temperature = Math.round(parseInt(data.trim()) / 1000) 162 - } 163 - } 164 - Component.onCompleted: running = true 165 - } 166 - 167 - // Network - check ethernet first, then wifi 168 - Process { 169 - id: netProc 170 - command: ["sh", "-c", "eth=$(nmcli -t -f TYPE,STATE,CONNECTION dev | grep '^ethernet:connected:' | head -1); if [ -n \"$eth\" ]; then echo \"ethernet:$(echo $eth | cut -d: -f3)\"; else nmcli -t -f ACTIVE,SSID,SIGNAL dev wifi | grep '^yes' | head -1; fi"] 171 - stdout: SplitParser { 172 - onRead: data => { 173 - if (!data || data.trim() === "") { 174 - root.networkConnected = false 175 - root.networkIsEthernet = false 176 - root.networkName = "Disconnected" 177 - root.networkStrength = 0 178 - return 179 - } 180 - var trimmed = data.trim() 181 - if (trimmed.startsWith("ethernet:")) { 182 - root.networkConnected = true 183 - root.networkIsEthernet = true 184 - root.networkName = trimmed.split(":")[1] || "Ethernet" 185 - root.networkStrength = 100 186 - } else { 187 - var parts = trimmed.split(":") 188 - root.networkConnected = true 189 - root.networkIsEthernet = false 190 - root.networkName = parts[1] || "" 191 - root.networkStrength = parseInt(parts[2]) || 0 192 - } 193 - } 194 - } 195 - Component.onCompleted: running = true 196 - } 197 - 198 - // Volume 199 - Process { 200 - id: volProc 201 - command: ["sh", "-c", "wpctl get-volume @DEFAULT_AUDIO_SINK@"] 202 - stdout: SplitParser { 203 - onRead: data => { 204 - if (!data) return 205 - root.volumeMuted = data.indexOf("[MUTED]") !== -1 206 - var match = data.match(/Volume:\s+([\d.]+)/) 207 - if (match) { 208 - root.volume = Math.round(parseFloat(match[1]) * 100) 209 - } 210 - } 211 - } 212 - Component.onCompleted: running = true 213 - } 214 - 215 - // Brightness 216 - Process { 217 - id: brightProc 218 - command: ["sh", "-c", "brightnessctl -m | cut -d, -f4 | tr -d '%'"] 219 - stdout: SplitParser { 220 - onRead: data => { 221 - if (!data) return 222 - root.brightness = parseInt(data.trim()) || 0 223 - } 224 - } 225 - Component.onCompleted: running = true 226 - } 227 - 228 - // Battery 229 - Process { 230 - id: batProc 231 - command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/capacity 2>/dev/null || echo 0"] 232 - stdout: SplitParser { 233 - onRead: data => { 234 - if (!data) return 235 - root.batteryPercent = parseInt(data.trim()) || 0 236 - } 237 - } 238 - Component.onCompleted: running = true 239 - } 240 - 241 - Process { 242 - id: batStatusProc 243 - command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/status 2>/dev/null || echo Unknown"] 244 - stdout: SplitParser { 245 - onRead: data => { 246 - if (!data) return 247 - root.batteryStatus = data.trim() 248 - } 249 - } 250 - Component.onCompleted: running = true 251 - } 252 - 253 - // Detect power backend once at startup 254 - Process { 255 - id: powerBackendProc 256 - command: ["sh", "-c", "command -v system76-power >/dev/null 2>&1 && echo system76 || echo ppd"] 257 - stdout: SplitParser { 258 - onRead: data => { 259 - if (!data) return 260 - root.powerBackend = data.trim() 261 - } 262 - } 263 - Component.onCompleted: running = true 264 - } 265 - 266 - // Power profile 267 - Process { 268 - id: powerProc 269 - command: ["sh", "-c", "if command -v system76-power >/dev/null 2>&1; then p=$(system76-power profile 2>/dev/null | grep -oiE 'performance|balanced|battery' | tr '[:upper:]' '[:lower:]'); [ \"$p\" = \"battery\" ] && p=\"power-saver\"; echo \"${p:-unknown}\"; else powerprofilesctl get 2>/dev/null || echo unknown; fi"] 270 - stdout: SplitParser { 271 - onRead: data => { 272 - if (!data) return 273 - root.powerProfile = data.trim() 274 - } 275 - } 276 - Component.onCompleted: running = true 277 - } 9 + Sidebar {} 10 + } 278 11 279 - // Weather 280 - Process { 281 - id: weatherProc 282 - command: ["sh", "-c", "curl -sf 'wttr.in/?format=%c+%t' | tr -d '+'"] 283 - stdout: SplitParser { 284 - onRead: data => { 285 - if (!data) return 286 - root.weather = data.trim() 287 - } 288 - } 289 - Component.onCompleted: running = true 290 - } 12 + Variants { 13 + model: Quickshell.screens 14 + Dashboard {} 15 + } 291 16 292 - // Open Steam window via niri focus, fallback to spawning 293 - Process { 294 - id: steamOpenProc 295 - command: ["sh", "-c", "WID=$(niri msg --json windows | jq -r '.[] | select(.app_id | test(\"steam\"; \"i\")) | .id' | head -n1); if [ -n \"$WID\" ]; then niri msg action focus-window --id \"$WID\"; else steam steam://open/games; fi"] 296 - } 297 - 298 - // Weather timer (refresh every 15 minutes) 299 - Timer { 300 - interval: 900000 301 - running: true 302 - repeat: true 303 - onTriggered: weatherProc.running = true 304 - } 305 - 306 - // Update timer 307 - Timer { 308 - interval: 2000 309 - running: true 310 - repeat: true 311 - onTriggered: { 312 - cpuProc.running = true 313 - memProc.running = true 314 - tempProc.running = true 315 - netProc.running = true 316 - volProc.running = true 317 - brightProc.running = true 318 - batProc.running = true 319 - batStatusProc.running = true 320 - powerProc.running = true 321 - } 322 - } 323 - 324 - // Left section - time, weather, tray 325 - RowLayout { 326 - anchors.left: parent.left 327 - anchors.leftMargin: 12 328 - anchors.verticalCenter: parent.verticalCenter 329 - spacing: 12 330 - 331 - Text { 332 - id: clockText 333 - color: root.colBlue 334 - font { family: root.fontFamily; pixelSize: root.fontSize } 335 - text: Qt.formatDateTime(new Date(), "dd-MM-yyyy HH:mm") 336 - Timer { 337 - interval: 1000 338 - running: true 339 - repeat: true 340 - onTriggered: clockText.text = Qt.formatDateTime(new Date(), "dd-MM-yyyy HH:mm") 341 - } 342 - } 343 - 344 - Rectangle { width: 1; height: 16; color: root.colOverlay0; visible: root.weather !== "" } 345 - 346 - Text { 347 - color: root.colText 348 - font { family: root.fontFamily; pixelSize: root.fontSize } 349 - text: root.weather 350 - visible: root.weather !== "" 351 - } 352 - 353 - Rectangle { 354 - width: 1; height: 16; color: root.colOverlay0 355 - visible: trayRepeater.count > 0 356 - opacity: root.barHovered ? 1.0 : 0.0 357 - Behavior on opacity { NumberAnimation { duration: 200 } } 358 - } 359 - 360 - Repeater { 361 - id: trayRepeater 362 - model: SystemTray.items.values 363 - delegate: Item { 364 - id: trayDelegate 365 - required property var modelData 366 - Layout.preferredWidth: root.barHovered ? 28 : 0 367 - Layout.preferredHeight: 30 368 - opacity: root.barHovered ? 1.0 : 0.0 369 - clip: true 370 - 371 - Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } 372 - Behavior on opacity { NumberAnimation { duration: 200 } } 373 - 374 - IconImage { 375 - id: trayIcon 376 - implicitSize: 16 377 - anchors.centerIn: parent 378 - source: Quickshell.iconPath(trayDelegate.modelData.icon, true) || trayDelegate.modelData.icon 379 - } 380 - 381 - MouseArea { 382 - anchors.fill: parent 383 - onClicked: { 384 - if (trayDelegate.modelData.id.toLowerCase().indexOf("steam") !== -1) { 385 - steamOpenProc.running = true 386 - } else { 387 - trayDelegate.modelData.activate() 388 - } 389 - } 390 - } 391 - } 392 - } 393 - } 394 - 395 - // Right section - anchored to right edge 396 - RowLayout { 397 - anchors.right: parent.right 398 - anchors.rightMargin: 12 399 - anchors.verticalCenter: parent.verticalCenter 400 - spacing: 12 401 - 402 - // Volume 403 - Text { 404 - color: root.colMaroon 405 - font { family: root.fontFamily; pixelSize: root.fontSize } 406 - text: root.volume + "% " + (root.volumeMuted ? root.iconVolMute : (root.volume > 50 ? root.iconVolHigh : root.iconVolLow)) 407 - MouseArea { 408 - anchors.fill: parent 409 - cursorShape: Qt.PointingHandCursor 410 - onClicked: volClickProc.running = true 411 - } 412 - } 413 - 414 - Process { 415 - id: volClickProc 416 - command: ["pavucontrol"] 417 - } 418 - 419 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 420 - 421 - // Network 422 - Text { 423 - color: root.colGreen 424 - font { family: root.fontFamily; pixelSize: root.fontSize } 425 - text: { 426 - if (!root.networkConnected) return "Disconnected \u26a0" 427 - if (root.networkIsEthernet) return root.networkName + " " + root.iconEthernet 428 - return root.networkName + " (" + root.networkStrength + "%) " + root.iconWifi 429 - } 430 - } 431 - 432 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 433 - 434 - // Power Profile 435 - Text { 436 - color: root.colText 437 - font { family: root.fontFamily; pixelSize: root.fontSize } 438 - text: { 439 - if (root.powerProfile === "performance") return root.iconBolt 440 - if (root.powerProfile === "balanced") return root.iconBalance 441 - if (root.powerProfile === "power-saver") return root.iconLeaf 442 - return root.iconBalance 443 - } 444 - MouseArea { 445 - anchors.fill: parent 446 - cursorShape: Qt.PointingHandCursor 447 - onClicked: { 448 - var next = "balanced" 449 - if (root.powerProfile === "balanced") next = "performance" 450 - else if (root.powerProfile === "performance") next = "power-saver" 451 - else next = "balanced" 452 - if (root.powerBackend === "system76") { 453 - var s76profile = next === "power-saver" ? "battery" : next 454 - powerSetProc.command = ["system76-power", "profile", s76profile] 455 - } else { 456 - powerSetProc.command = ["powerprofilesctl", "set", next] 457 - } 458 - powerSetProc.running = true 459 - root.powerProfile = next 460 - } 461 - } 462 - } 463 - 464 - Process { 465 - id: powerSetProc 466 - } 467 - 468 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 469 - 470 - // CPU 471 - Text { 472 - color: root.colPeach 473 - font { family: root.fontFamily; pixelSize: root.fontSize } 474 - text: root.cpuUsage + "% " + root.iconCpu 475 - } 476 - 477 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 478 - 479 - // Memory 480 - Text { 481 - color: root.colMauve 482 - font { family: root.fontFamily; pixelSize: root.fontSize } 483 - text: root.memUsage + "% " + root.iconMem 484 - } 485 - 486 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 487 - 488 - // Temperature 489 - Text { 490 - color: root.colRed 491 - font { family: root.fontFamily; pixelSize: root.fontSize } 492 - text: { 493 - var icon = root.temperature >= 80 ? root.iconTempHigh : (root.temperature >= 50 ? root.iconTempMed : root.iconTempLow) 494 - return root.temperature + "\u00b0C " + icon 495 - } 496 - } 497 - 498 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 499 - 500 - // Backlight 501 - Text { 502 - color: root.colYellow 503 - font { family: root.fontFamily; pixelSize: root.fontSize } 504 - text: root.brightness + "% " + root.iconSun 505 - } 506 - 507 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 508 - 509 - // Battery 510 - Text { 511 - color: { 512 - if (root.batteryStatus === "Charging") return root.colGreen 513 - if (root.batteryPercent <= 15) return root.colRed 514 - if (root.batteryPercent <= 30) return root.colRed 515 - return root.colGreen 516 - } 517 - font { family: root.fontFamily; pixelSize: root.fontSize } 518 - text: { 519 - var icon 520 - if (root.batteryStatus === "Charging") { 521 - icon = root.iconBatCharge 522 - } else { 523 - var icons = [root.iconBatEmpty, root.iconBatQuarter, root.iconBatHalf, root.iconBatThreeQ, root.iconBatFull] 524 - var idx = Math.min(Math.floor(root.batteryPercent / 25), 4) 525 - icon = icons[idx] 526 - } 527 - return root.batteryPercent + "% " + icon 528 - } 529 - } 530 - 531 - Rectangle { width: 1; height: 16; color: root.colOverlay0 } 532 - 533 - // Power button 534 - Text { 535 - color: root.colRed 536 - font { family: root.fontFamily; pixelSize: root.fontSize } 537 - text: root.iconPower 538 - } 539 - } 540 - } 17 + Variants { 18 + model: Quickshell.screens 19 + RightPanel {} 541 20 } 542 21 } 543 -
+8
hosts/mira/hardware-configuration.nix
··· 54 54 # reduce write ops 55 55 options = [ "noatime" ]; 56 56 }; 57 + fileSystems."/mnt/ssd" = { 58 + device = "/dev/disk/by-uuid/f71b3774-2192-498e-b67f-f6b575accdda"; 59 + fsType = "ext4"; 60 + options = [ "noatime" ]; 61 + }; 57 62 58 63 systemd.tmpfiles.rules = [ 59 64 "d /mnt/storage1 0755 sean users -" 60 65 "d /mnt/storage2 0755 sean users -" 66 + "d /mnt/ssd 0755 sean users -" 67 + # Fix ownership on mounted filesystems (d only affects the hidden mount point) 68 + "z /mnt/ssd 0755 sean users -" 61 69 ]; 62 70 63 71 swapDevices = [ ];