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