+3
.gitignore
+3
.gitignore
+1
-20
README.md
+1
-20
README.md
···
10
10
11
11
<br />
12
12
13
-

14
-
15
-
<details>
16
-
<summary>more previews</summary>
17
-
18
-
<p align="center">
19
-
light mode
20
-
<img src="./docs/src/images/lightmode.png" width="800px" />
21
-
</p>
22
-
23
-
<p align="center">
24
-
wezterm + chromium
25
-
<img src="./docs/src/images/blur.png" width="800px" />
26
-
</p>
27
-
28
-
<p align="center">
29
-
neovim
30
-
<img src="./docs/src/images/nvim.png" width="800px" />
31
-
</p>
32
-
</details>
13
+

33
14
34
15

35
16
+8
docs/src/README.md
+8
docs/src/README.md
···
8
8
<img alt="nixos-unstable" src="https://img.shields.io/badge/NixOS-unstable-blue.svg?style=for-the-badge&labelColor=303446&logo=NixOS&logoColor=white&color=91D7E3" />
9
9
</div>
10
10
11
+
<br />
12
+
13
+

14
+
15
+

16
+
11
17
### Foreword
12
18
13
19
This repository contains my **personal** configuration for my systems, so its really important that you know it's **personal** and not everything will fit your needs.
···
22
28
- Sensible defaults, so you can get started quickly
23
29
- Docs kind of
24
30
- [Catppuccin](https://github.com/catppuccin/catppuccin) everywhere.
31
+
32
+
[](https://star-history.com/#isabelroses/dotfiles&Date)
-1
docs/src/SUMMARY.md
-1
docs/src/SUMMARY.md
docs/src/images/blur.png
docs/src/images/blur.png
This is a binary file and will not be displayed.
docs/src/images/blur.webp
docs/src/images/blur.webp
This is a binary file and will not be displayed.
docs/src/images/lightmode.png
docs/src/images/lightmode.png
This is a binary file and will not be displayed.
docs/src/images/lightmode.webp
docs/src/images/lightmode.webp
This is a binary file and will not be displayed.
docs/src/images/main.png
docs/src/images/main.png
This is a binary file and will not be displayed.
docs/src/images/main.webp
docs/src/images/main.webp
This is a binary file and will not be displayed.
docs/src/images/nvim.png
docs/src/images/nvim.png
This is a binary file and will not be displayed.
docs/src/images/nvim.webp
docs/src/images/nvim.webp
This is a binary file and will not be displayed.
-5
docs/src/misc/previews.md
-5
docs/src/misc/previews.md
+51
-50
flake.lock
+51
-50
flake.lock
···
24
24
]
25
25
},
26
26
"locked": {
27
-
"lastModified": 1765990358,
28
-
"narHash": "sha256-l8x0gU8mnYaGMl+gWrsSHKBJlZWD8KXJfHTkRlFiPI0=",
27
+
"lastModified": 1767967164,
28
+
"narHash": "sha256-Cx4VETh9dGoQYDtWhre7g66d7SAr+h1h6f+SSHxVrck=",
29
29
"owner": "catppuccin",
30
30
"repo": "nix",
31
-
"rev": "de1b60ca45a578f59f7d84c8d338b346017b2161",
31
+
"rev": "e973584280e3b0e1d5b5a1a5e9948dc222c54af7",
32
32
"type": "github"
33
33
},
34
34
"original": {
···
39
39
},
40
40
"crane": {
41
41
"locked": {
42
-
"lastModified": 1766774972,
43
-
"narHash": "sha256-8qxEFpj4dVmIuPn9j9z6NTbU+hrcGjBOvaxTzre5HmM=",
42
+
"lastModified": 1767461147,
43
+
"narHash": "sha256-TH/xTeq/RI+DOzo+c+4F431eVuBpYVwQwBxzURe7kcI=",
44
44
"owner": "ipetkov",
45
45
"repo": "crane",
46
-
"rev": "01bc1d404a51a0a07e9d8759cd50a7903e218c82",
46
+
"rev": "7d59256814085fd9666a2ae3e774dc5ee216b630",
47
47
"type": "github"
48
48
},
49
49
"original": {
···
59
59
]
60
60
},
61
61
"locked": {
62
-
"lastModified": 1767028240,
63
-
"narHash": "sha256-0/fLUqwJ4Z774muguUyn5t8AQ6wyxlNbHexpje+5hRo=",
64
-
"owner": "nix-darwin",
62
+
"lastModified": 1767469770,
63
+
"narHash": "sha256-Rv1kumaBqlqCvLjVYuzdA38btSyjSRvXZ8UtwalGcHo=",
64
+
"owner": "isabelroses",
65
65
"repo": "nix-darwin",
66
-
"rev": "c31afa6e76da9bbc7c9295e39c7de9fca1071ea1",
66
+
"rev": "1d46ab42afd10adf9751ab6cfba6b426bfb17e33",
67
67
"type": "github"
68
68
},
69
69
"original": {
70
-
"owner": "nix-darwin",
70
+
"owner": "isabelroses",
71
+
"ref": "darwin-rebuild",
71
72
"repo": "nix-darwin",
72
73
"type": "github"
73
74
}
···
90
91
"flake-compat": {
91
92
"flake": false,
92
93
"locked": {
93
-
"lastModified": 1761588595,
94
-
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
95
-
"owner": "edolstra",
94
+
"lastModified": 1767039857,
95
+
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
96
+
"owner": "NixOS",
96
97
"repo": "flake-compat",
97
-
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
98
+
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
98
99
"type": "github"
99
100
},
100
101
"original": {
101
-
"owner": "edolstra",
102
+
"owner": "NixOS",
102
103
"repo": "flake-compat",
103
104
"type": "github"
104
105
}
···
110
111
]
111
112
},
112
113
"locked": {
113
-
"lastModified": 1765835352,
114
-
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
114
+
"lastModified": 1767609335,
115
+
"narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=",
115
116
"owner": "hercules-ci",
116
117
"repo": "flake-parts",
117
-
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
118
+
"rev": "250481aafeb741edfe23d29195671c19b36b6dca",
118
119
"type": "github"
119
120
},
120
121
"original": {
···
173
174
]
174
175
},
175
176
"locked": {
176
-
"lastModified": 1767104570,
177
-
"narHash": "sha256-GKgwu5//R+cLdKysZjGqvUEEOGXXLdt93sNXeb2M/Lk=",
177
+
"lastModified": 1767971841,
178
+
"narHash": "sha256-TwDXF4MkmjI9c3Sly9FOWXf4sPbre6ZujG87v39G1Ig=",
178
179
"owner": "nix-community",
179
180
"repo": "home-manager",
180
-
"rev": "e4e78a2cbeaddd07ab7238971b16468cc1d14daf",
181
+
"rev": "0e4217b2c4827e71e2e612accccb01981c16afda",
181
182
"type": "github"
182
183
},
183
184
"original": {
···
211
212
]
212
213
},
213
214
"locked": {
214
-
"lastModified": 1767230498,
215
-
"narHash": "sha256-eAENy8+m5KfcD/3HCDvF8e0BLlBSd5Se3snzGkaInoI=",
215
+
"lastModified": 1768007596,
216
+
"narHash": "sha256-8es5mAWk+UMlo9O3pEbdLhXVk5CTs3yE7S1531aD1ZQ=",
216
217
"owner": "isabelroses",
217
218
"repo": "izlix",
218
-
"rev": "d57e291079ef70c0f77ad3200b5b7db586e26922",
219
+
"rev": "450899c93ac2b101b1db2061f3bddbd369728815",
219
220
"type": "github"
220
221
},
221
222
"original": {
···
232
233
]
233
234
},
234
235
"locked": {
235
-
"lastModified": 1767151551,
236
-
"narHash": "sha256-OevEstP+kG3nTWYi41n+PIuscIVBXDffzIQ2ew10uL8=",
236
+
"lastModified": 1767756498,
237
+
"narHash": "sha256-a7SYG/6uUeeyfg1Xiq/sXpRb2RXs+kQ0GKrKMVlMw+4=",
237
238
"owner": "isabelroses",
238
239
"repo": "nvim",
239
-
"rev": "d87c265a3ee73dfe6eddc63c0e3cda91249d3c31",
240
+
"rev": "725ef38a0ea9b5bd8218aad272bd7458d430b2be",
240
241
"type": "github"
241
242
},
242
243
"original": {
···
255
256
"rust-overlay": "rust-overlay"
256
257
},
257
258
"locked": {
258
-
"lastModified": 1767013031,
259
-
"narHash": "sha256-p8ANXBakAtfX/aEhLbU6w0tuQe3nrBvLdHbKirJP7ug=",
259
+
"lastModified": 1767697030,
260
+
"narHash": "sha256-0iVZ99H3kR5h6Lhw8kDDuUc5C/k6iismeWgCS1qWTQ4=",
260
261
"owner": "nix-community",
261
262
"repo": "lanzaboote",
262
-
"rev": "c2a82339373daee8cbbcad5f51f22ae6b71069e0",
263
+
"rev": "657469e8f036334db768daaf7732b1174676054b",
263
264
"type": "github"
264
265
},
265
266
"original": {
···
291
292
},
292
293
"nixpkgs": {
293
294
"locked": {
294
-
"lastModified": 1767151656,
295
-
"narHash": "sha256-enBX1DxL9cttnrJ+rzOds/GXWqSj/vcB97+X9mgiJho=",
296
-
"rev": "f665af0cdb70ed27e1bd8f9fdfecaf451260fc55",
295
+
"lastModified": 1767966113,
296
+
"narHash": "sha256-mb9aE5Y/wRCZz0cTB9EOe4BEKWpXAAl5Yai4TFrEf1E=",
297
+
"rev": "5f02c91314c8ba4afe83b256b023756412218535",
297
298
"type": "tarball",
298
-
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre920165.f665af0cdb70/nixexprs.tar.xz?lastModified=1767151656&rev=f665af0cdb70ed27e1bd8f9fdfecaf451260fc55"
299
+
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre924980.5f02c91314c8/nixexprs.tar.xz?lastModified=1767966113&rev=5f02c91314c8ba4afe83b256b023756412218535"
299
300
},
300
301
"original": {
301
302
"type": "tarball",
···
312
313
]
313
314
},
314
315
"locked": {
315
-
"lastModified": 1765911976,
316
-
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
316
+
"lastModified": 1767281941,
317
+
"narHash": "sha256-6MkqajPICgugsuZ92OMoQcgSHnD6sJHwk8AxvMcIgTE=",
317
318
"owner": "cachix",
318
319
"repo": "pre-commit-hooks.nix",
319
-
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
320
+
"rev": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa",
320
321
"type": "github"
321
322
},
322
323
"original": {
···
352
353
]
353
354
},
354
355
"locked": {
355
-
"lastModified": 1766976750,
356
-
"narHash": "sha256-w+o3AIBI56tzfMJRqRXg9tSXnpQRN5hAT15o2t9rxYw=",
356
+
"lastModified": 1767495280,
357
+
"narHash": "sha256-hEEgtE/RSRigw8xscchGymf/t1nluZwTfru4QF6O1CQ=",
357
358
"owner": "oxalica",
358
359
"repo": "rust-overlay",
359
-
"rev": "9fe44e7f05b734a64a01f92fc51ad064fb0a884f",
360
+
"rev": "cb24c5cc207ba8e9a4ce245eedd2d37c3a988bc1",
360
361
"type": "github"
361
362
},
362
363
"original": {
···
395
396
]
396
397
},
397
398
"locked": {
398
-
"lastModified": 1766896955,
399
-
"narHash": "sha256-BbAUnNjaBmfR7Mvho9BN0RfvDi5fpP19wd/Hs6DMX8k=",
399
+
"lastModified": 1767848616,
400
+
"narHash": "sha256-rAVR2ucz5vn9TOjvj5hVyEl0MnXrwIJo3ee12+p0EZE=",
400
401
"owner": "Mic92",
401
402
"repo": "sops-nix",
402
-
"rev": "861c32b27cce26c4bb828dfd21bd23df0dba7df2",
403
+
"rev": "994c9f014e5ce073de4bca7ebe21cf1cf90f2b0a",
403
404
"type": "github"
404
405
},
405
406
"original": {
···
417
418
"systems": "systems"
418
419
},
419
420
"locked": {
420
-
"lastModified": 1767195736,
421
-
"narHash": "sha256-0xvPSbhIGeJzsJXNTkgJ3PjwdVItKm85wzYKA9NmSzI=",
421
+
"lastModified": 1767502559,
422
+
"narHash": "sha256-om0IPjW850vhhIrNZ5tiXjsYuqyoI44IdE+I9AwZ96I=",
422
423
"owner": "Gerg-L",
423
424
"repo": "spicetify-nix",
424
-
"rev": "465adc0ab6ff0c4b9b1db1c6e7fd7eeb553b3261",
425
+
"rev": "806c1fdeb7af3e013215d14f5d9f06685fa6650f",
425
426
"type": "github"
426
427
},
427
428
"original": {
···
452
453
]
453
454
},
454
455
"locked": {
455
-
"lastModified": 1767229092,
456
-
"narHash": "sha256-HFi+WvYK7mLVL6Cka2m9wKpy9lt2K5wPQ+oN/bmuc0Q=",
456
+
"lastModified": 1768006445,
457
+
"narHash": "sha256-WQ1yhsbYQM+EvhaWPSuS4UZPD4SjsskMWNG6azbEAgM=",
457
458
"owner": "tgirlcloud",
458
459
"repo": "pkgs",
459
-
"rev": "1d1d8e13541eaf85492e758be2a032fd20412a33",
460
+
"rev": "ae2de15f76978b6564c3eecdbe68d5ed0d0d2500",
460
461
"type": "github"
461
462
},
462
463
"original": {
+2
-1
flake.nix
+2
-1
flake.nix
-5
home/isabel/cli/shell/shellAlias.nix
-5
home/isabel/cli/shell/shellAlias.nix
···
4
4
mkdir = "mkdir -pv"; # always create pearent directory
5
5
df = "df -h"; # human readblity
6
6
rs = "systemctl reboot";
7
-
sysctl = "sudo systemctl";
8
7
jctl = "journalctl -p 3 -xb"; # get error messages from journalctl
9
8
lg = "lazygit";
10
9
11
10
zzzpl = "cd ~/.local/share/zzz ; git pull ; git push ; cd -";
12
11
zzzbk = "cd ~/.local/share/zzz ; git add . ; git commit -m 'chore: sync changes' ; git push ; cd -";
13
-
14
-
# Remap docker to podman
15
-
docker = "podman";
16
-
docker-compose = "podman-compose";
17
12
};
18
13
}
+7
-1
home/isabel/gui/chromium.nix
+7
-1
home/isabel/gui/chromium.nix
···
1
1
{
2
2
lib,
3
3
pkgs,
4
+
config,
4
5
...
5
6
}:
6
7
let
···
187
188
};
188
189
};
189
190
191
+
xdg.configFile = mkIf (pkgs.stdenv.hostPlatform.isLinux && config.programs.chromium.enable) {
192
+
"chromium/NativeMessagingHosts/ff2mpv.json".source =
193
+
"${pkgs.ff2mpv-rust}/etc/chromium/native-messaging-hosts/ff2mpv.json";
194
+
};
195
+
190
196
home.file = mkIf pkgs.stdenv.hostPlatform.isDarwin {
191
197
"Library/Application Support/Chromium/NativeMessagingHosts/ff2mpv.json".source =
192
-
"${pkgs.ff2mpv}/etc/chromium/native-messaging-hosts/ff2mpv.json";
198
+
"${pkgs.ff2mpv-rust}/etc/chromium/native-messaging-hosts/ff2mpv.json";
193
199
};
194
200
}
+17
-31
home/isabel/gui/hyprland.nix
+17
-31
home/isabel/gui/hyprland.nix
···
215
215
};
216
216
217
217
layerrule = [
218
-
"blur,vicinae"
219
-
"ignorealpha 0, vicinae"
220
-
"noanim, vicinae"
218
+
"blur on, ignore_alpha 0, match:namespace vicinae"
219
+
"no_anim on, match:namespace vicinae"
221
220
];
222
221
223
222
general = {
···
225
224
gaps_out = 8;
226
225
gaps_workspaces = 0;
227
226
border_size = 2;
228
-
no_border_on_floating = true;
229
227
230
228
"col.active_border" = "$pink";
231
229
"col.inactive_border" = "$surface1";
···
294
292
disable_autoreload = true; # autoreload is unnecessary on nixos, because the config is readonly anyway
295
293
};
296
294
297
-
windowrulev2 = [
298
-
"float, title:^(nm-connection-editor)$"
299
-
"float, title:^(Network)$"
300
-
"float, title:^(xdg-desktop-portal-gtk)$"
301
-
"float, class:gay.vaskel.soteria"
302
-
"float, title:^(Picture-in-Picture)$"
303
-
"float, class:^(download)$"
304
-
305
-
"center(1), initialTitle:(Open Files)"
306
-
"float, initialTitle:(Open Files)"
307
-
"size 40% 60%, initialTitle:(Open Files)"
308
-
309
-
"center(1), class:.blueman-manager-wrapped"
310
-
"float, class:.blueman-manager-wrapped"
311
-
"size 40% 60%, class:.blueman-manager-wrapped"
312
-
313
-
"center(1), class:com.saivert.pwvucontrol"
314
-
"float, class:com.saivert.pwvucontrol"
315
-
"size 40% 60%, class:com.saivert.pwvucontrol"
295
+
windowrule = [
296
+
"float on, match:title ^(nm-connection-editor)$"
297
+
"float on, match:title ^(Network)$"
298
+
"float on, match:title ^(xdg-desktop-portal-gtk)$"
299
+
"float on, match:class gay.vaskel.soteria"
300
+
"float on, match:title ^(Picture-in-Picture)$"
301
+
"float on, match:class ^(download)$"
316
302
317
-
# we can't just use the tag because we want to capture the popup window
318
-
"float, title:Bitwarden"
319
-
"size 800 600, title:Bitwarden"
320
-
# "no_screenshare on, tag:bitwarden"
303
+
"center on, float on, size (monitor_w*0.4) (monitor_h*0.6), match:initial_title (Open Files)"
304
+
"center on, float on, size (monitor_w*0.4) (monitor_h*0.6), match:class .blueman-manager-wrapped"
305
+
"center on, float on, size (monitor_w*0.4) (monitor_h*0.6), match:class com.saivert.pwvucontrol"
306
+
"float on, size 800 600, match:title Bitwarden"
321
307
322
-
"workspace 6, class:discord" # move discord to workspace 6
323
-
"workspace 7, class:spotify" # move spotify to workspace 7
308
+
"workspace 6, match:class discord" # move discord to workspace 6
309
+
"workspace 7, match:class spotify" # move spotify to workspace 7
324
310
325
311
# throw sharing indicators away
326
-
"workspace special silent, title:^(Firefox โ Sharing Indicator)$"
327
-
"workspace special silent, title:^(.*is sharing (your screen|a window)\.)$"
312
+
"workspace special silent, match:title ^(Firefox โ Sharing Indicator)$"
313
+
"workspace special silent, match:title ^(.*is sharing (your screen|a window).)$"
328
314
];
329
315
};
330
316
+11
-2
home/isabel/gui/media/listening.nix
+11
-2
home/isabel/gui/media/listening.nix
···
1
1
{
2
2
lib,
3
+
pkgs,
3
4
config,
4
5
inputs,
5
6
inputs',
···
16
17
config = mkIf config.garden.profiles.media.watching.enable {
17
18
programs.spicetify = {
18
19
enable = true;
20
+
21
+
spotifyPackage =
22
+
if pkgs.stdenv.hostPlatform.isLinux then
23
+
pkgs.spotify.override { ffmpeg_4 = pkgs.ffmpeg; }
24
+
else
25
+
pkgs.spotify;
26
+
27
+
colorScheme = "mocha";
28
+
theme = spicePkgs.themes.catppuccin;
29
+
19
30
enabledExtensions = with spicePkgs.extensions; [
20
31
shuffle
21
32
copyToClipboard
···
25
36
volumePercentage
26
37
aiBandBlocker
27
38
];
28
-
theme = spicePkgs.themes.catppuccin;
29
-
colorScheme = "mocha";
30
39
};
31
40
};
32
41
}
+4
-1
home/isabel/gui/media/watching.nix
+4
-1
home/isabel/gui/media/watching.nix
···
32
32
33
33
scripts =
34
34
(with pkgs.mpvScripts; [
35
-
videoclip
36
35
sponsorblock
36
+
37
+
# unify my clipboard
38
+
(videoclip.override { wl-clipboard = pkgs.wl-clipboard-rs; })
37
39
38
40
# modern ui
39
41
modernz
···
111
113
stop-screensaver = "yes";
112
114
cursor-autohide = 100; # auto hide cursor after 100ms
113
115
reset-on-next-file = "video-zoom,panscan,video-unscaled,video-rotate,video-align-x,video-align-y";
116
+
ytdl-raw-options = "cookies=~/documents/yt-dlp-cookies.txt";
114
117
};
115
118
116
119
profiles = {
+25
-5
home/isabel/gui/quickshell/components/Clock.qml
+25
-5
home/isabel/gui/quickshell/components/Clock.qml
···
2
2
import QtQuick.Layouts
3
3
import "root:/data"
4
4
5
-
Text {
6
-
id: clock
7
-
font.pointSize: 13
8
-
color: Settings.colors.foreground
5
+
Item {
6
+
id: root
7
+
8
+
signal clicked()
9
+
9
10
Layout.alignment: Qt.AlignCenter
11
+
implicitWidth: 30
12
+
implicitHeight: 50
10
13
11
-
text: Time.time
14
+
Text {
15
+
id: clockText
16
+
anchors.centerIn: parent
17
+
text: Time.time
18
+
color: Settings.colors.foreground
19
+
font {
20
+
pixelSize: 14
21
+
weight: Font.Medium
22
+
}
23
+
horizontalAlignment: Text.AlignHCenter
24
+
}
25
+
26
+
MouseArea {
27
+
anchors.fill: parent
28
+
hoverEnabled: true
29
+
cursorShape: Qt.PointingHandCursor
30
+
onClicked: root.clicked()
31
+
}
12
32
}
+60
home/isabel/gui/quickshell/components/ExpandablePanel.qml
+60
home/isabel/gui/quickshell/components/ExpandablePanel.qml
···
1
+
import QtQuick
2
+
import QtQuick.Layouts
3
+
import Quickshell
4
+
import Quickshell.Widgets
5
+
import "root:/data"
6
+
7
+
Rectangle {
8
+
id: root
9
+
10
+
property string title: ""
11
+
property string subtitle: ""
12
+
property bool expanded: false
13
+
property alias content: contentLoader.sourceComponent
14
+
property int contentHeight: 150
15
+
16
+
Layout.fillWidth: true
17
+
Layout.preferredHeight: expanded ? contentLoader.item?.height + 48 : 0
18
+
visible: expanded
19
+
radius: 8
20
+
color: Settings.colors.backgroundLighter
21
+
clip: true
22
+
23
+
Behavior on Layout.preferredHeight {
24
+
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
25
+
}
26
+
27
+
ColumnLayout {
28
+
anchors.fill: parent
29
+
anchors.margins: 12
30
+
spacing: 8
31
+
32
+
RowLayout {
33
+
Layout.fillWidth: true
34
+
35
+
Text {
36
+
text: root.title
37
+
color: Settings.colors.foreground
38
+
font {
39
+
pixelSize: 13
40
+
weight: Font.Medium
41
+
}
42
+
}
43
+
44
+
Item { Layout.fillWidth: true }
45
+
46
+
Text {
47
+
text: root.subtitle
48
+
color: Settings.colors.accent
49
+
font.pixelSize: 11
50
+
visible: root.subtitle !== ""
51
+
}
52
+
}
53
+
54
+
Loader {
55
+
id: contentLoader
56
+
Layout.fillWidth: true
57
+
Layout.preferredHeight: Math.min(item?.implicitHeight ?? 0, root.contentHeight)
58
+
}
59
+
}
60
+
}
+29
-15
home/isabel/gui/quickshell/components/IconButton.qml
+29
-15
home/isabel/gui/quickshell/components/IconButton.qml
···
2
2
import Quickshell
3
3
import Quickshell.Widgets
4
4
import "root:/data"
5
+
import "root:/components"
5
6
6
7
Item {
7
-
id: iconButton
8
-
width: 16
9
-
height: 16
8
+
id: root
10
9
11
-
property string icon: ""
12
-
signal clicked
10
+
property string icon: ""
11
+
property int size: 18
12
+
property bool invert: false
13
13
14
-
MouseArea {
15
-
anchors.fill: parent
16
-
onClicked: iconButton.clicked()
17
-
}
14
+
signal clicked
18
15
19
-
Text {
20
-
anchors.fill: parent
21
-
text: iconButton.icon
22
-
color: Settings.colors.foreground
23
-
font.pixelSize: 18
24
-
}
16
+
implicitWidth: size
17
+
implicitHeight: size
18
+
19
+
MyIcon {
20
+
anchors.centerIn: parent
21
+
icon: root.icon
22
+
size: root.size
23
+
invert: root.invert
24
+
}
25
+
26
+
MouseArea {
27
+
id: mouseArea
28
+
anchors.fill: parent
29
+
hoverEnabled: true
30
+
cursorShape: Qt.PointingHandCursor
31
+
onClicked: root.clicked()
32
+
}
33
+
34
+
scale: mouseArea.pressed ? 0.9 : 1.0
35
+
36
+
Behavior on scale {
37
+
NumberAnimation { duration: 100 }
38
+
}
25
39
}
+16
-17
home/isabel/gui/quickshell/components/Launcher.qml
+16
-17
home/isabel/gui/quickshell/components/Launcher.qml
···
2
2
import QtQuick.Layouts
3
3
import Quickshell
4
4
import Quickshell.Io
5
-
import Quickshell.Widgets
5
+
import "root:/data"
6
6
7
-
IconImage {
8
-
id: launcher
9
-
source: Quickshell.iconPath("nix-snowflake")
7
+
Item {
8
+
id: root
10
9
11
-
Layout.alignment: Qt.AlignCenter
10
+
Layout.alignment: Qt.AlignCenter
11
+
implicitWidth: 24
12
+
implicitHeight: 24
12
13
13
-
width: 16
14
-
height: 16
14
+
IconButton {
15
+
anchors.centerIn: parent
16
+
icon: "nix-snowflake"
17
+
size: 20
18
+
onClicked: launcherProcess.running = true
19
+
}
15
20
16
-
Process {
17
-
id: launcherProcess
18
-
command: ["vicinae", "toggle"]
19
-
}
20
-
21
-
MouseArea {
22
-
anchors.fill: parent
23
-
hoverEnabled: true
24
-
onClicked: launcherProcess.running = true;
25
-
}
21
+
Process {
22
+
id: launcherProcess
23
+
command: ["vicinae", "toggle"]
24
+
}
26
25
}
+186
home/isabel/gui/quickshell/components/MediaPlayers.qml
+186
home/isabel/gui/quickshell/components/MediaPlayers.qml
···
1
+
import QtQuick
2
+
import QtQuick.Layouts
3
+
import Quickshell.Widgets
4
+
import Quickshell.Services.Mpris
5
+
import "root:/data"
6
+
import "root:/services"
7
+
8
+
ColumnLayout {
9
+
id: root
10
+
11
+
Layout.fillWidth: true
12
+
spacing: 8
13
+
visible: Media.players.length > 0
14
+
15
+
RowLayout {
16
+
Layout.fillWidth: true
17
+
spacing: 8
18
+
19
+
Text {
20
+
text: "Now Playing"
21
+
color: Settings.colors.foreground
22
+
font {
23
+
pixelSize: 14
24
+
weight: Font.Bold
25
+
}
26
+
}
27
+
28
+
Item { Layout.fillWidth: true }
29
+
30
+
// Player selector (if multiple players)
31
+
Row {
32
+
spacing: 4
33
+
visible: Media.players.length > 1
34
+
35
+
Repeater {
36
+
model: Media.players
37
+
38
+
Rectangle {
39
+
required property MprisPlayer modelData
40
+
required property int index
41
+
width: 8
42
+
height: 8
43
+
radius: 4
44
+
color: Media.selectedPlayer === modelData ? Settings.colors.accent : Settings.colors.backgroundLightest
45
+
46
+
MouseArea {
47
+
anchors.fill: parent
48
+
cursorShape: Qt.PointingHandCursor
49
+
onClicked: Media.selectedPlayer = modelData
50
+
}
51
+
}
52
+
}
53
+
}
54
+
}
55
+
56
+
// Current Player
57
+
ListView {
58
+
id: mediaPlayerList
59
+
Layout.fillWidth: true
60
+
Layout.preferredHeight: contentHeight
61
+
model: Media.players
62
+
spacing: 8
63
+
clip: true
64
+
interactive: false
65
+
66
+
delegate: Rectangle {
67
+
required property MprisPlayer modelData
68
+
required property int index
69
+
width: ListView.view.width
70
+
height: 90
71
+
radius: 8
72
+
color: Settings.colors.backgroundLighter
73
+
opacity: Media.selectedPlayer === modelData ? 1.0 : 0.6
74
+
75
+
MouseArea {
76
+
anchors.fill: parent
77
+
onClicked: Media.selectedPlayer = modelData
78
+
}
79
+
80
+
RowLayout {
81
+
anchors {
82
+
fill: parent
83
+
margins: 10
84
+
}
85
+
spacing: 10
86
+
87
+
// Album Art
88
+
ClippingWrapperRectangle {
89
+
radius: 6
90
+
Layout.preferredWidth: 70
91
+
Layout.preferredHeight: 70
92
+
93
+
Image {
94
+
anchors.fill: parent
95
+
source: modelData.trackArtUrl
96
+
fillMode: Image.PreserveAspectCrop
97
+
}
98
+
}
99
+
100
+
// Track Info & Controls
101
+
ColumnLayout {
102
+
Layout.fillWidth: true
103
+
Layout.fillHeight: true
104
+
spacing: 2
105
+
106
+
RowLayout {
107
+
Layout.fillWidth: true
108
+
spacing: 4
109
+
110
+
Text {
111
+
text: modelData.trackTitle ?? "Unknown"
112
+
color: Settings.colors.foreground
113
+
font {
114
+
pixelSize: 13
115
+
weight: Font.Medium
116
+
}
117
+
elide: Text.ElideRight
118
+
Layout.fillWidth: true
119
+
}
120
+
121
+
// Player app name badge
122
+
Rectangle {
123
+
visible: Media.players.length > 1
124
+
Layout.preferredHeight: 16
125
+
Layout.preferredWidth: appNameText.width + 8
126
+
radius: 4
127
+
color: Settings.colors.backgroundLightest
128
+
129
+
Text {
130
+
id: appNameText
131
+
anchors.centerIn: parent
132
+
text: modelData.identity ?? ""
133
+
color: Settings.colors.foreground
134
+
opacity: 0.7
135
+
font.pixelSize: 9
136
+
}
137
+
}
138
+
}
139
+
140
+
Text {
141
+
text: modelData.trackArtist ?? ""
142
+
color: Settings.colors.foreground
143
+
opacity: 0.7
144
+
font.pixelSize: 11
145
+
elide: Text.ElideRight
146
+
Layout.fillWidth: true
147
+
}
148
+
149
+
Item { Layout.fillHeight: true }
150
+
151
+
// Playback Controls
152
+
RowLayout {
153
+
spacing: 14
154
+
Layout.alignment: Qt.AlignLeft
155
+
156
+
IconButton {
157
+
icon: "media-skip-backward-symbolic"
158
+
size: 14
159
+
onClicked: modelData.previous()
160
+
}
161
+
162
+
IconButton {
163
+
icon: modelData.playbackState === MprisPlaybackState.Playing
164
+
? "media-playback-pause-symbolic"
165
+
: "media-playback-start-symbolic"
166
+
size: 18
167
+
onClicked: {
168
+
if (modelData.playbackState === MprisPlaybackState.Playing) {
169
+
modelData.pause();
170
+
} else {
171
+
modelData.play();
172
+
}
173
+
}
174
+
}
175
+
176
+
IconButton {
177
+
icon: "media-skip-forward-symbolic"
178
+
size: 14
179
+
onClicked: modelData.next()
180
+
}
181
+
}
182
+
}
183
+
}
184
+
}
185
+
}
186
+
}
+28
home/isabel/gui/quickshell/components/MyIcon.qml
+28
home/isabel/gui/quickshell/components/MyIcon.qml
···
1
+
import org.kde.kirigami
2
+
import QtQuick
3
+
import QtQuick.Effects
4
+
import Quickshell
5
+
import Quickshell.Widgets
6
+
import "root:/data"
7
+
8
+
Item {
9
+
id: root
10
+
11
+
property string icon: ""
12
+
property int size: 18
13
+
property bool invert: false
14
+
15
+
implicitWidth: size
16
+
implicitHeight: size
17
+
18
+
Icon {
19
+
id: iconSource
20
+
anchors.fill: parent
21
+
source: Quickshell.iconPath(root.icon)
22
+
23
+
isMask: true
24
+
// FIXME:
25
+
// color: Settings.colors.foreground
26
+
color: invert ? "#1e1e2e" : "#cdd6f4"
27
+
}
28
+
}
+28
-10
home/isabel/gui/quickshell/components/Network.qml
+28
-10
home/isabel/gui/quickshell/components/Network.qml
···
1
-
//@ pragma IconTheme Cosmic
2
-
1
+
import QtQuick
3
2
import QtQuick.Layouts
4
-
import Quickshell
5
-
import Quickshell.Widgets
3
+
import QtQuick.Controls.Basic
4
+
import "root:/data"
5
+
import "root:/components"
6
6
import "root:/services"
7
7
8
-
IconImage {
9
-
id: networkIcon
10
-
source: Quickshell.iconPath(Networking.active.icon)
8
+
Item {
9
+
id: root
11
10
12
-
width: 16
13
-
height: 16
14
-
Layout.alignment: Qt.AlignCenter
11
+
Layout.alignment: Qt.AlignCenter
12
+
implicitWidth: 20
13
+
implicitHeight: 20
14
+
15
+
MyIcon {
16
+
anchors.centerIn: parent
17
+
icon: Networking.icon
18
+
size: 18
19
+
}
20
+
21
+
MouseArea {
22
+
id: mouseArea
23
+
anchors.fill: parent
24
+
hoverEnabled: true
25
+
cursorShape: Qt.PointingHandCursor
26
+
}
27
+
28
+
ToolTip {
29
+
visible: mouseArea.containsMouse
30
+
text: Networking.statusText
31
+
delay: 500
32
+
}
15
33
}
+139
-327
home/isabel/gui/quickshell/components/Noti.qml
+139
-327
home/isabel/gui/quickshell/components/Noti.qml
···
1
1
import QtQuick
2
2
import QtQuick.Layouts
3
3
import QtQuick.Controls
4
-
import QtQuick.Controls.Basic
5
4
import Quickshell
6
5
import Quickshell.Widgets
7
-
import Quickshell.Services.Notifications
8
-
import Quickshell.Services.Mpris
6
+
import Quickshell.Services.Notifications as QsNotifications
9
7
import "root:/data"
10
8
import "root:/services"
11
-
import "root:/components"
12
9
13
10
Item {
14
-
id: noti
11
+
id: root
15
12
16
-
property bool showIndicator: Notifications.list.length > 0 || Media.players.length > 0
13
+
property bool hasNotifications: Notifications.list.length > 0
17
14
18
-
Layout.alignment: Qt.AlignCenter
19
-
20
-
Text {
21
-
text: "๏ถ"
22
-
color: Settings.colors.foreground
23
-
font.pixelSize: 16
15
+
// Expose function to toggle popup (for Clock)
16
+
function togglePopup() {
17
+
notificationLoader.item.visible = !notificationLoader.item.visible;
18
+
}
24
19
25
-
anchors.horizontalCenter: parent.horizontalCenter
20
+
visible: hasNotifications
26
21
27
-
visible: noti.showIndicator
22
+
Layout.alignment: Qt.AlignCenter
23
+
implicitWidth: 24
24
+
implicitHeight: 24
28
25
29
-
MouseArea {
30
-
anchors.fill: parent
31
-
onClicked: notificationLoader.item.visible = !notificationLoader.item.visible
26
+
IconButton {
27
+
anchors.centerIn: parent
28
+
icon: "preferences-system-notifications-symbolic"
29
+
size: 18
30
+
onClicked: root.togglePopup()
32
31
}
33
-
}
34
32
35
-
LazyLoader {
36
-
id: notificationLoader
33
+
LazyLoader {
34
+
id: notificationLoader
35
+
loading: true
37
36
38
-
loading: true
37
+
PopupWindow {
38
+
id: popup
39
+
anchor.window: root.QsWindow.window
40
+
anchor.rect.x: parentWindow.width * 1.2
41
+
visible: false
42
+
color: "transparent"
39
43
40
-
PopupWindow {
41
-
id: popup
42
-
anchor.window: noti.QsWindow.window
43
-
anchor.rect.x: parentWindow.width * 1.2
44
-
45
-
visible: false
44
+
implicitWidth: 400
45
+
implicitHeight: root.QsWindow.window.height
46
46
47
-
color: "transparent"
48
-
49
-
implicitWidth: 400
50
-
implicitHeight: noti.QsWindow.window.height
51
-
52
-
Rectangle {
53
-
anchors.fill: parent
54
-
radius: 10
55
-
color: Settings.colors.background
56
-
57
-
ColumnLayout {
58
-
spacing: 10
59
-
60
-
anchors {
61
-
fill: parent
62
-
topMargin: 15
63
-
bottomMargin: 15
64
-
leftMargin: 15
65
-
rightMargin: 15
66
-
}
67
-
68
-
Rectangle {
69
-
width: parent.width
70
-
height: 30
71
-
color: Settings.colors.background
72
-
radius: 5
73
-
74
-
Text {
75
-
text: "Notifications"
76
-
color: Settings.colors.foreground
77
-
font.pixelSize: 20
78
-
font.bold: true
79
-
80
-
anchors.centerIn: parent
81
-
}
82
-
}
83
-
84
-
Rectangle {
85
-
width: parent.width
86
-
height: 120
87
-
radius: 5
88
-
color: Settings.colors.backgroundLighter
89
-
90
-
ColumnLayout {
91
-
id: mprisRoot
92
-
spacing: 7
93
-
94
-
width: parent.width
95
-
height: parent.height
96
-
97
-
anchors {
98
-
fill: parent
99
-
leftMargin: 10
100
-
rightMargin: 20
101
-
topMargin: 10
102
-
bottomMargin: 10
103
-
}
104
-
105
-
Row {
106
-
spacing: 10
107
-
108
-
Layout.preferredWidth: parent.width
109
-
Layout.preferredHeight: 80
110
-
111
-
ClippingWrapperRectangle {
112
-
radius: 5
113
-
width: parent.height
114
-
height: parent.height
115
-
116
-
Image {
117
-
source: Media.selectedPlayer.trackArtUrl
118
-
fillMode: Image.PreserveAspectFit
119
-
width: parent.width
120
-
height: parent.height
121
-
}
122
-
}
47
+
Rectangle {
48
+
anchors.fill: parent
49
+
radius: 12
50
+
color: Settings.colors.background
123
51
124
52
ColumnLayout {
125
-
spacing: 5
126
-
127
-
height: 80
128
-
width: parent.width
129
-
130
-
Text {
131
-
text: Media.selectedPlayer.trackTitle
132
-
color: Settings.colors.foreground
133
-
font.pixelSize: 20
134
-
}
135
-
136
-
Text {
137
-
text: Media.selectedPlayer.trackArtist
138
-
color: Settings.colors.foreground
139
-
font.pixelSize: 14
140
-
}
141
-
142
-
Row {
143
-
spacing: (parent.width / 3) - 10
144
-
145
-
IconButton {
146
-
// icon: "media-skip-backward-symbolic"
147
-
icon: "๓ฐซ"
148
-
onClicked: Media.selectedPlayer.previous()
149
-
}
150
-
151
-
IconButton {
152
-
// icon: "media-playback-start-symbolic"
153
-
icon: "๓ฐ"
154
-
onClicked: Media.selectedPlayer.play()
155
-
visible: Media.selectedPlayer.playbackState == MprisPlaybackState.Paused
156
-
}
157
-
158
-
IconButton {
159
-
// icon: "media-playback-pause-symbolic"
160
-
icon: "๓ฐค"
161
-
onClicked: Media.selectedPlayer.pause()
162
-
visible: Media.selectedPlayer.playbackState == MprisPlaybackState.Playing
163
-
}
164
-
165
-
IconButton {
166
-
// icon: "media-skip-forward-symbolic"
167
-
icon: "๓ฐฌ"
168
-
onClicked: Media.selectedPlayer.next()
53
+
spacing: 12
54
+
anchors {
55
+
fill: parent
56
+
margins: 16
169
57
}
170
-
}
171
-
}
172
-
}
173
-
174
-
RowLayout {
175
-
id: progressBar
176
-
spacing: 10
177
-
178
-
Layout.preferredWidth: parent.width
179
-
Layout.preferredHeight: parent.height
180
-
181
-
Text {
182
-
id: positionText
183
-
text: Math.round(Media.selectedPlayer.position) + "s"
184
-
color: Settings.colors.foreground
185
-
}
186
-
187
-
Slider {
188
-
id: progress
189
-
from: 0
190
-
to: Media.selectedPlayer.length
191
-
value: Media.selectedPlayer.position
192
58
193
-
onMoved: Media.selectedPlayer.position = value
59
+
// Header
60
+
RowLayout {
61
+
Layout.fillWidth: true
62
+
spacing: 8
194
63
195
-
implicitHeight: parent.height
196
-
implicitWidth: parent.width - positionText.width - lengthText.width - 10
64
+
Text {
65
+
text: "Notifications"
66
+
color: Settings.colors.foreground
67
+
font {
68
+
pixelSize: 18
69
+
weight: Font.Bold
70
+
}
71
+
}
197
72
198
-
background: Rectangle {
199
-
id: sliderBackground
200
-
color: Settings.colors.backgroundDarker
201
-
x: progress.leftPadding
202
-
y: progress.topPadding + progress.availableHeight / 2 - height / 2
203
-
implicitWidth: parent.width
204
-
implicitHeight: 4
205
-
width: progress.availableWidth
206
-
height: implicitHeight
207
-
radius: 2
73
+
Item { Layout.fillWidth: true }
208
74
209
-
Rectangle {
210
-
implicitWidth: progress.visualPosition * parent.width
211
-
implicitHeight: parent.height
212
-
color: Settings.colors.accent
213
-
radius: 2
75
+
IconButton {
76
+
icon: "edit-clear-all-symbolic"
77
+
size: 16
78
+
onClicked: {
79
+
for (const n of Notifications.list) {
80
+
n.dismiss();
81
+
}
82
+
popup.visible = false;
83
+
}
84
+
}
214
85
}
215
-
}
216
86
217
-
handle: Rectangle {
218
-
x: progress.leftPadding + progress.visualPosition * progress.availableWidth
219
-
y: progress.topPadding + progress.availableHeight / 2 - height / 2
220
-
implicitWidth: 20
221
-
implicitHeight: 20
222
-
radius: 13
223
-
color: Settings.colors.backgroundLightest
224
-
border.color: Settings.colors.border
225
-
}
226
-
}
87
+
// Notifications List
88
+
ListView {
89
+
id: notiList
90
+
model: Notifications.list
91
+
Layout.fillWidth: true
92
+
Layout.fillHeight: true
93
+
spacing: 8
94
+
clip: true
227
95
228
-
Text {
229
-
id: lengthText
230
-
text: Math.round(Media.selectedPlayer.length) + "s"
231
-
color: Settings.colors.foreground
232
-
}
96
+
ScrollBar.vertical: ScrollBar {
97
+
policy: ScrollBar.AsNeeded
98
+
}
233
99
234
-
FrameAnimation {
235
-
running: Media.selectedPlayer.playbackState == MprisPlaybackState.Playing
236
-
onTriggered: {
237
-
progress.value = Media.selectedPlayer.position;
238
-
positionText.text = Math.round(Media.selectedPlayer.position) + "s";
239
-
}
240
-
}
241
-
}
242
-
}
243
-
}
100
+
delegate: Rectangle {
101
+
required property QsNotifications.Notification modelData
102
+
width: ListView.view.width
103
+
height: 72
104
+
radius: 8
105
+
color: Settings.colors.backgroundLighter
244
106
245
-
ListView {
246
-
id: notiList
247
-
model: Notifications.list
107
+
RowLayout {
108
+
anchors {
109
+
fill: parent
110
+
margins: 10
111
+
}
112
+
spacing: 10
248
113
249
-
Layout.alignment: Qt.AlignCenter
250
-
Layout.preferredWidth: parent.width
251
-
Layout.preferredHeight: parent.height
114
+
IconImage {
115
+
source: Quickshell.iconPath(modelData.appIcon)
116
+
implicitSize: 40
117
+
Layout.alignment: Qt.AlignVCenter
118
+
}
252
119
253
-
ScrollBar.vertical: ScrollBar {}
120
+
ColumnLayout {
121
+
Layout.fillWidth: true
122
+
Layout.fillHeight: true
123
+
spacing: 2
254
124
255
-
spacing: 15
256
-
257
-
delegate: Item {
258
-
required property Notification modelData
125
+
Text {
126
+
text: modelData.appName
127
+
color: Settings.colors.foreground
128
+
font {
129
+
pixelSize: 13
130
+
weight: Font.Medium
131
+
}
132
+
elide: Text.ElideRight
133
+
Layout.fillWidth: true
134
+
}
259
135
260
-
width: parent.width
261
-
height: 80
136
+
Text {
137
+
text: modelData.body
138
+
color: Settings.colors.foreground
139
+
opacity: 0.8
140
+
font.pixelSize: 12
141
+
elide: Text.ElideRight
142
+
maximumLineCount: 2
143
+
wrapMode: Text.WordWrap
144
+
Layout.fillWidth: true
145
+
}
146
+
}
262
147
263
-
Rectangle {
264
-
anchors.fill: parent
265
-
color: Settings.colors.backgroundLighter
266
-
radius: 5
267
-
268
-
IconImage {
269
-
anchors {
270
-
left: parent.left
271
-
leftMargin: 10
272
-
verticalCenter: parent.verticalCenter
273
-
}
274
-
width: 48
275
-
height: 48
276
-
source: Quickshell.iconPath(modelData.appIcon)
277
-
}
278
-
279
-
ColumnLayout {
280
-
anchors {
281
-
left: parent.left
282
-
leftMargin: 75
283
-
top: parent.top
284
-
topMargin: 10
285
-
}
286
-
287
-
spacing: 5
288
-
289
-
Text {
290
-
text: modelData.appName
291
-
color: Settings.colors.foreground
292
-
font {
293
-
pixelSize: 18
294
-
bold: true
148
+
IconButton {
149
+
icon: "window-close-symbolic"
150
+
size: 14
151
+
Layout.alignment: Qt.AlignTop
152
+
onClicked: {
153
+
modelData.dismiss();
154
+
if (Notifications.list.length <= 0) {
155
+
popup.visible = false;
156
+
}
157
+
}
158
+
}
159
+
}
160
+
}
295
161
}
296
-
}
297
162
298
-
Text {
299
-
text: modelData.body
300
-
color: Settings.colors.foreground
301
-
font.pixelSize: 13
302
-
}
303
-
}
304
-
305
-
Text {
306
-
text: "x"
307
-
color: Settings.colors.error
308
-
font.pixelSize: 16
309
-
310
-
anchors {
311
-
top: parent.top
312
-
topMargin: 5
313
-
right: parent.right
314
-
rightMargin: 10
315
-
}
316
-
317
-
MouseArea {
318
-
anchors.fill: parent
319
-
onClicked: {
320
-
modelData.dismiss();
321
-
if (Notifications.list.length <= 0) {
322
-
popup.visible = false;
323
-
}
163
+
// Separator before Media
164
+
Rectangle {
165
+
Layout.fillWidth: true
166
+
Layout.preferredHeight: 1
167
+
color: Settings.colors.backgroundLightest
168
+
visible: Media.players.length > 0
324
169
}
325
-
}
326
-
}
327
170
328
-
Repeater {
329
-
model: modelData.actions
171
+
// Media Players Section
172
+
MediaPlayers {}
330
173
331
-
Item {
332
-
required property NotificationAction actionData
333
-
334
-
width: 100
335
-
height: 30
336
-
337
-
anchors {
338
-
left: parent.left
339
-
leftMargin: 5
340
-
top: parent.top
341
-
topMargin: 5
342
-
}
343
-
174
+
// Separator
344
175
Rectangle {
345
-
anchors.fill: parent
346
-
color: Settings.colors.backgroundDarker
347
-
radius: 5
176
+
Layout.fillWidth: true
177
+
Layout.preferredHeight: 1
178
+
color: Settings.colors.backgroundLightest
179
+
}
348
180
349
-
Text {
350
-
text: actionData.text
351
-
color: Settings.colors.foreground
352
-
font.pixelSize: 12
353
-
354
-
anchors {
355
-
left: parent.left
356
-
leftMargin: 10
357
-
verticalCenter: parent.verticalCenter
358
-
}
359
-
}
360
-
361
-
MouseArea {
362
-
anchors.fill: parent
363
-
onClicked: actionData.invoke()
364
-
}
365
-
}
366
-
}
181
+
// Quick Settings Section
182
+
QuickSettings {}
367
183
}
368
-
}
369
184
}
370
-
}
371
185
}
372
-
}
373
186
}
374
-
}
375
187
}
+57
home/isabel/gui/quickshell/components/PanelListItem.qml
+57
home/isabel/gui/quickshell/components/PanelListItem.qml
···
1
+
import QtQuick
2
+
import QtQuick.Layouts
3
+
import "root:/data"
4
+
5
+
Rectangle {
6
+
id: root
7
+
8
+
property string icon: ""
9
+
property string text: ""
10
+
property string badge: ""
11
+
property bool active: false
12
+
property bool showBadge: badge !== ""
13
+
14
+
signal clicked()
15
+
16
+
width: parent?.width ?? 100
17
+
height: 36
18
+
radius: 6
19
+
color: active ? Settings.colors.accent : "transparent"
20
+
21
+
MouseArea {
22
+
id: mouseArea
23
+
anchors.fill: parent
24
+
cursorShape: Qt.PointingHandCursor
25
+
hoverEnabled: true
26
+
onEntered: if (!root.active) parent.color = Settings.colors.backgroundLightest
27
+
onExited: parent.color = root.active ? Settings.colors.accent : "transparent"
28
+
onClicked: root.clicked()
29
+
}
30
+
31
+
RowLayout {
32
+
anchors.fill: parent
33
+
anchors.margins: 8
34
+
spacing: 8
35
+
36
+
MyIcon {
37
+
icon: root.icon
38
+
size: 16
39
+
}
40
+
41
+
Text {
42
+
text: root.text
43
+
color: root.active ? Settings.colors.background : Settings.colors.foreground
44
+
font.pixelSize: 12
45
+
elide: Text.ElideRight
46
+
Layout.fillWidth: true
47
+
}
48
+
49
+
Text {
50
+
visible: root.showBadge
51
+
text: root.badge
52
+
color: root.active ? Settings.colors.background : Settings.colors.foreground
53
+
opacity: 0.7
54
+
font.pixelSize: 11
55
+
}
56
+
}
57
+
}
+387
home/isabel/gui/quickshell/components/QuickSettings.qml
+387
home/isabel/gui/quickshell/components/QuickSettings.qml
···
1
+
import QtQuick
2
+
import QtQuick.Layouts
3
+
import QtQuick.Controls.Basic
4
+
import Quickshell.Io
5
+
import Quickshell.Services.Pipewire
6
+
import "root:/data"
7
+
import "root:/services"
8
+
9
+
ColumnLayout {
10
+
id: root
11
+
12
+
Layout.fillWidth: true
13
+
spacing: 12
14
+
15
+
Text {
16
+
text: "Quick Settings"
17
+
color: Settings.colors.foreground
18
+
font {
19
+
pixelSize: 14
20
+
weight: Font.Bold
21
+
}
22
+
}
23
+
24
+
// Connection Status Row (Ethernet indicator)
25
+
RowLayout {
26
+
Layout.fillWidth: true
27
+
spacing: 8
28
+
visible: Networking.ethernetConnected
29
+
30
+
MyIcon {
31
+
icon: "network-wired-symbolic"
32
+
size: 16
33
+
}
34
+
35
+
Text {
36
+
text: "Ethernet: " + Networking.ethernetDevice
37
+
color: Settings.colors.foreground
38
+
font.pixelSize: 12
39
+
}
40
+
41
+
Item { Layout.fillWidth: true }
42
+
43
+
Rectangle {
44
+
width: 8
45
+
height: 8
46
+
radius: 4
47
+
color: Settings.colors.success
48
+
}
49
+
}
50
+
51
+
// Network Panel (expandable)
52
+
Rectangle {
53
+
id: networkPanel
54
+
Layout.fillWidth: true
55
+
Layout.preferredHeight: visible ? Math.min(networkColumn.implicitHeight + 20, 350) : 0
56
+
visible: false
57
+
radius: 8
58
+
color: Settings.colors.backgroundLighter
59
+
clip: true
60
+
61
+
Behavior on Layout.preferredHeight {
62
+
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
63
+
}
64
+
65
+
ColumnLayout {
66
+
id: networkColumn
67
+
anchors.fill: parent
68
+
anchors.margins: 12
69
+
spacing: 8
70
+
71
+
RowLayout {
72
+
Layout.fillWidth: true
73
+
74
+
Text {
75
+
text: "Networks"
76
+
color: Settings.colors.foreground
77
+
font {
78
+
pixelSize: 13
79
+
weight: Font.Medium
80
+
}
81
+
}
82
+
83
+
Item { Layout.fillWidth: true }
84
+
85
+
IconButton {
86
+
icon: "view-refresh-symbolic"
87
+
size: 14
88
+
onClicked: Networking.reload()
89
+
}
90
+
}
91
+
92
+
ScrollView {
93
+
Layout.fillWidth: true
94
+
Layout.preferredHeight: Math.min(networkListView.contentHeight, 280)
95
+
clip: true
96
+
97
+
ListView {
98
+
id: networkListView
99
+
width: parent.width
100
+
model: Networking.networks
101
+
spacing: 4
102
+
103
+
delegate: PanelListItem {
104
+
required property var modelData
105
+
width: ListView.view.width
106
+
icon: modelData.icon
107
+
text: modelData.ssid || "Unknown"
108
+
badge: modelData.strength + "%"
109
+
active: modelData.active
110
+
onClicked: {
111
+
if (!modelData.active) {
112
+
Networking.connectToNetwork(modelData.ssid);
113
+
}
114
+
}
115
+
}
116
+
}
117
+
}
118
+
119
+
Text {
120
+
text: Networking.networks?.count === 0 ? "No networks found" : ""
121
+
color: Settings.colors.foreground
122
+
opacity: 0.5
123
+
font.pixelSize: 12
124
+
visible: Networking.networks?.count === 0
125
+
Layout.alignment: Qt.AlignHCenter
126
+
}
127
+
128
+
Item { Layout.fillHeight: true }
129
+
}
130
+
}
131
+
132
+
// Bluetooth Panel (expandable)
133
+
Rectangle {
134
+
id: bluetoothPanel
135
+
Layout.fillWidth: true
136
+
Layout.preferredHeight: visible ? Math.min(bluetoothColumn.implicitHeight + 20, 350) : 0
137
+
visible: false
138
+
radius: 8
139
+
color: Settings.colors.backgroundLighter
140
+
clip: true
141
+
142
+
Behavior on Layout.preferredHeight {
143
+
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
144
+
}
145
+
146
+
ColumnLayout {
147
+
id: bluetoothColumn
148
+
anchors.fill: parent
149
+
anchors.margins: 12
150
+
spacing: 8
151
+
152
+
RowLayout {
153
+
Layout.fillWidth: true
154
+
155
+
Text {
156
+
text: "Bluetooth Devices"
157
+
color: Settings.colors.foreground
158
+
font {
159
+
pixelSize: 13
160
+
weight: Font.Medium
161
+
}
162
+
}
163
+
164
+
Item { Layout.fillWidth: true }
165
+
166
+
// Scan button
167
+
IconButton {
168
+
icon: Bluetooth.adapter?.discovering ? "process-stop-symbolic" : "view-refresh-symbolic"
169
+
size: 14
170
+
onClicked: {
171
+
if (Bluetooth.adapter?.discovering) {
172
+
Bluetooth.stopDiscovery();
173
+
} else {
174
+
Bluetooth.startDiscovery();
175
+
}
176
+
}
177
+
}
178
+
179
+
Text {
180
+
text: Bluetooth.connected ? Bluetooth.connectedDevice?.name ?? "" : ""
181
+
color: Settings.colors.accent
182
+
font.pixelSize: 11
183
+
visible: Bluetooth.connected
184
+
}
185
+
}
186
+
187
+
ScrollView {
188
+
Layout.fillWidth: true
189
+
Layout.preferredHeight: Math.min(bluetoothListView.contentHeight, 280)
190
+
clip: true
191
+
192
+
ListView {
193
+
id: bluetoothListView
194
+
width: parent.width
195
+
model: Bluetooth.devices
196
+
spacing: 8
197
+
clip: true
198
+
199
+
delegate: PanelListItem {
200
+
required property var modelData
201
+
width: ListView.view.width
202
+
icon: modelData.icon || "bluetooth-symbolic"
203
+
text: modelData.name
204
+
badge: modelData.connected ? "Connected" : (modelData.paired ? "Paired" : "")
205
+
active: modelData.connected
206
+
onClicked: {
207
+
if (modelData.connected) {
208
+
modelData.disconnect();
209
+
} else if (modelData.paired) {
210
+
modelData.connect();
211
+
} else {
212
+
// Pair and connect
213
+
modelData.pair();
214
+
}
215
+
}
216
+
}
217
+
}
218
+
}
219
+
220
+
Text {
221
+
text: {
222
+
if (!Bluetooth.adapter) return "No Bluetooth adapter";
223
+
if (Bluetooth.devices?.count === 0) return "No devices found";
224
+
return "";
225
+
}
226
+
color: Settings.colors.foreground
227
+
opacity: 0.5
228
+
font.pixelSize: 12
229
+
visible: !Bluetooth.adapter || Bluetooth.devices?.count === 0
230
+
Layout.alignment: Qt.AlignHCenter
231
+
}
232
+
233
+
Item { Layout.fillHeight: true }
234
+
}
235
+
}
236
+
237
+
// Toggle Buttons Row
238
+
RowLayout {
239
+
Layout.fillWidth: true
240
+
spacing: 8
241
+
242
+
// WiFi Toggle
243
+
QuickSettingButton {
244
+
icon: Networking.wifiEnabled
245
+
? (Networking.activeWifi?.icon ?? "network-wireless-acquiring-symbolic")
246
+
: "network-wireless-disabled-symbolic"
247
+
label: "WiFi"
248
+
active: Networking.wifiEnabled
249
+
onClicked: Networking.toggleWifi()
250
+
onPressAndHold: networkPanel.visible = !networkPanel.visible
251
+
}
252
+
253
+
// Bluetooth Toggle
254
+
QuickSettingButton {
255
+
icon: Bluetooth.icon
256
+
label: "Bluetooth"
257
+
active: Bluetooth.powered
258
+
onClicked: Bluetooth.toggle()
259
+
onPressAndHold: bluetoothPanel.visible = !bluetoothPanel.visible
260
+
}
261
+
262
+
// Do Not Disturb Toggle
263
+
QuickSettingButton {
264
+
icon: Notifications.dndEnabled ? "notifications-disabled-symbolic" : "preferences-system-notifications-symbolic"
265
+
label: "DND"
266
+
active: Notifications.dndEnabled
267
+
onClicked: Notifications.dndEnabled = !Notifications.dndEnabled
268
+
}
269
+
}
270
+
271
+
// Volume Slider
272
+
PwObjectTracker {
273
+
objects: [Pipewire.defaultAudioSink]
274
+
}
275
+
276
+
StyledSlider {
277
+
icon: {
278
+
const muted = Pipewire.defaultAudioSink?.audio?.muted ?? false;
279
+
const vol = Pipewire.defaultAudioSink?.audio?.volume ?? 0;
280
+
if (muted) return "audio-volume-muted-symbolic";
281
+
if (vol > 0.66) return "audio-volume-high-symbolic";
282
+
if (vol > 0.33) return "audio-volume-medium-symbolic";
283
+
if (vol > 0) return "audio-volume-low-symbolic";
284
+
return "audio-volume-muted-symbolic";
285
+
}
286
+
value: Pipewire.defaultAudioSink?.audio?.volume ?? 0
287
+
iconClickable: true
288
+
onIconClicked: {
289
+
if (Pipewire.defaultAudioSink?.audio) {
290
+
Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
291
+
}
292
+
}
293
+
onMoved: (val) => {
294
+
if (Pipewire.defaultAudioSink?.audio) {
295
+
Pipewire.defaultAudioSink.audio.volume = val;
296
+
}
297
+
}
298
+
}
299
+
300
+
// Brightness Slider
301
+
BrightnessHelper { id: brightnessHelper }
302
+
303
+
StyledSlider {
304
+
visible: brightnessHelper.available
305
+
icon: "display-brightness-symbolic"
306
+
value: brightnessHelper.brightness
307
+
from: 0.05
308
+
accentColor: Settings.colors.warning
309
+
onMoved: (val) => brightnessHelper.setBrightness(val)
310
+
}
311
+
312
+
// Quick Setting Button Component
313
+
component QuickSettingButton: Rectangle {
314
+
id: qsButton
315
+
Layout.fillWidth: true
316
+
Layout.preferredHeight: 64
317
+
radius: 8
318
+
color: active ? Settings.colors.accent : Settings.colors.backgroundLighter
319
+
320
+
property string icon: ""
321
+
property string label: ""
322
+
property bool active: false
323
+
324
+
signal clicked()
325
+
signal pressAndHold()
326
+
327
+
ColumnLayout {
328
+
anchors.centerIn: parent
329
+
spacing: 6
330
+
331
+
MyIcon {
332
+
icon: qsButton.icon
333
+
size: 20
334
+
Layout.alignment: Qt.AlignHCenter
335
+
invert: active
336
+
}
337
+
338
+
Text {
339
+
text: qsButton.label
340
+
color: qsButton.active ? Settings.colors.background : Settings.colors.foreground
341
+
font.pixelSize: 11
342
+
Layout.alignment: Qt.AlignHCenter
343
+
}
344
+
}
345
+
346
+
MouseArea {
347
+
anchors.fill: parent
348
+
cursorShape: Qt.PointingHandCursor
349
+
onClicked: qsButton.clicked()
350
+
onPressAndHold: qsButton.pressAndHold()
351
+
}
352
+
}
353
+
354
+
// Brightness Helper Component
355
+
component BrightnessHelper: QtObject {
356
+
id: brightnessObj
357
+
property real brightness: 1.0
358
+
property bool available: false
359
+
360
+
function setBrightness(value) {
361
+
brightness = value;
362
+
setBrightnessProc.command = ["brightnessctl", "set", Math.round(value * 100) + "%"];
363
+
setBrightnessProc.running = true;
364
+
}
365
+
366
+
Component.onCompleted: getBrightness.running = true
367
+
368
+
property Process getBrightness: Process {
369
+
id: getBrightness
370
+
command: ["bash", "-c", "brightnessctl -m 2>/dev/null | cut -d, -f4 | tr -d '%'"]
371
+
stdout: SplitParser {
372
+
onRead: {
373
+
const val = parseInt(data.trim());
374
+
if (!isNaN(val)) {
375
+
brightnessObj.brightness = val / 100;
376
+
brightnessObj.available = true;
377
+
}
378
+
}
379
+
}
380
+
}
381
+
382
+
property Process setBrightnessProc: Process {
383
+
id: setBrightnessProc
384
+
}
385
+
}
386
+
}
387
+
+73
home/isabel/gui/quickshell/components/StyledSlider.qml
+73
home/isabel/gui/quickshell/components/StyledSlider.qml
···
1
+
import QtQuick
2
+
import QtQuick.Layouts
3
+
import QtQuick.Controls.Basic
4
+
import "root:/data"
5
+
import "root:/components"
6
+
7
+
RowLayout {
8
+
id: root
9
+
10
+
property string icon: ""
11
+
property real value: 0
12
+
property real from: 0
13
+
property real to: 1
14
+
property color accentColor: Settings.colors.accent
15
+
property bool showPercentage: true
16
+
property bool iconClickable: false
17
+
18
+
signal moved(real value)
19
+
signal iconClicked()
20
+
21
+
Layout.fillWidth: true
22
+
spacing: 12
23
+
24
+
IconButton {
25
+
icon: root.icon
26
+
size: 18
27
+
onClicked: root.iconClicked()
28
+
}
29
+
30
+
Slider {
31
+
id: slider
32
+
Layout.fillWidth: true
33
+
from: root.from
34
+
to: root.to
35
+
value: root.value
36
+
37
+
onMoved: root.moved(value)
38
+
39
+
background: Rectangle {
40
+
x: slider.leftPadding
41
+
y: slider.topPadding + slider.availableHeight / 2 - height / 2
42
+
width: slider.availableWidth
43
+
height: 4
44
+
radius: 2
45
+
color: Settings.colors.backgroundLightest
46
+
47
+
Rectangle {
48
+
width: slider.visualPosition * parent.width
49
+
height: parent.height
50
+
radius: 2
51
+
color: root.accentColor
52
+
}
53
+
}
54
+
55
+
handle: Rectangle {
56
+
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
57
+
y: slider.topPadding + slider.availableHeight / 2 - height / 2
58
+
width: 14
59
+
height: 14
60
+
radius: 7
61
+
color: slider.pressed ? root.accentColor : Settings.colors.foreground
62
+
}
63
+
}
64
+
65
+
Text {
66
+
visible: root.showPercentage
67
+
text: Math.round((slider.value - root.from) / (root.to - root.from) * 100) + "%"
68
+
color: Settings.colors.foreground
69
+
opacity: 0.7
70
+
font.pixelSize: 12
71
+
Layout.preferredWidth: 36
72
+
}
73
+
}
+212
-82
home/isabel/gui/quickshell/components/SysTray.qml
+212
-82
home/isabel/gui/quickshell/components/SysTray.qml
···
1
1
import QtQuick
2
2
import QtQuick.Layouts
3
-
import Quickshell
4
3
import QtQuick.Controls
4
+
import Quickshell
5
5
import Quickshell.Widgets
6
6
import Quickshell.Services.SystemTray
7
7
import "root:/data"
8
8
9
9
ColumnLayout {
10
-
id: systray
10
+
id: root
11
11
12
-
Layout.alignment: Qt.AlignCenter
12
+
Layout.alignment: Qt.AlignCenter
13
+
spacing: 6
13
14
14
-
Repeater {
15
-
model: SystemTray.items
15
+
Repeater {
16
+
model: SystemTray.items
16
17
17
-
delegate: Item {
18
-
id: delagate
19
-
required property SystemTrayItem modelData
18
+
delegate: Item {
19
+
id: trayItem
20
+
required property SystemTrayItem modelData
20
21
21
-
width: 24
22
-
height: 24
22
+
Layout.alignment: Qt.AlignHCenter
23
+
implicitWidth: 24
24
+
implicitHeight: 24
23
25
24
-
IconImage {
25
-
source: modelData.icon
26
-
width: 16
27
-
height: 16
28
-
anchors.centerIn: parent
29
-
}
26
+
IconImage {
27
+
source: modelData.icon
28
+
implicitWidth: 18
29
+
implicitHeight: 18
30
+
anchors.centerIn: parent
31
+
smooth: true
32
+
}
30
33
31
-
MouseArea {
32
-
anchors.fill: parent
33
-
hoverEnabled: true
34
+
MouseArea {
35
+
anchors.fill: parent
36
+
hoverEnabled: true
37
+
cursorShape: Qt.PointingHandCursor
38
+
acceptedButtons: Qt.LeftButton | Qt.RightButton
39
+
onClicked: (mouse) => {
40
+
if (mouse.button === Qt.RightButton || modelData.onlyMenu) {
41
+
popupLoader.item.visible = !popupLoader.item.visible;
42
+
} else {
43
+
modelData.activate();
44
+
}
45
+
}
46
+
}
34
47
35
-
onClicked: popupLoader.item.visible = !popupLoader.item.visible
36
-
}
48
+
ToolTip {
49
+
visible: trayMouseArea.containsMouse && modelData.tooltipTitle !== ""
50
+
text: modelData.tooltipTitle
51
+
delay: 500
52
+
}
37
53
38
-
QsMenuOpener {
39
-
id: menu
40
-
menu: modelData.menu
41
-
}
54
+
MouseArea {
55
+
id: trayMouseArea
56
+
anchors.fill: parent
57
+
hoverEnabled: true
58
+
acceptedButtons: Qt.NoButton
59
+
}
42
60
43
-
LazyLoader {
44
-
id: popupLoader
61
+
QsMenuOpener {
62
+
id: menu
63
+
menu: modelData.menu
64
+
}
45
65
46
-
loading: true
66
+
LazyLoader {
67
+
id: popupLoader
68
+
loading: true
47
69
48
-
PopupWindow {
49
-
id: popup
50
-
anchor.window: delagate.QsWindow.window
51
-
anchor.rect.x: parentWindow.width * 1.15
52
-
anchor.rect.y: parentWindow.height / 1.25
70
+
PopupWindow {
71
+
id: popup
72
+
anchor.window: trayItem.QsWindow.window
73
+
anchor.rect.x: parentWindow.width * 1.15
74
+
anchor.rect.y: parentWindow.height / 1.25
75
+
visible: false
76
+
color: "transparent"
53
77
54
-
color: "transparent"
78
+
implicitWidth: 220
79
+
implicitHeight: menuColumn.implicitHeight + 16
55
80
56
-
implicitWidth: 200
57
-
implicitHeight: 200
81
+
Rectangle {
82
+
anchors.fill: parent
83
+
color: Settings.colors.background
84
+
radius: 10
58
85
59
-
Rectangle {
60
-
anchors.fill: parent
61
-
color: Settings.colors.background
62
-
radius: 5
63
-
}
86
+
// Shadow effect
87
+
layer.enabled: true
88
+
layer.effect: null
89
+
}
64
90
65
-
ListView {
66
-
model: menu.children
91
+
ColumnLayout {
92
+
id: menuColumn
93
+
anchors {
94
+
fill: parent
95
+
margins: 8
96
+
}
97
+
spacing: 2
67
98
68
-
anchors {
69
-
top: parent.top
70
-
topMargin: 5
71
-
bottom: parent.bottom
72
-
bottomMargin: 5
73
-
}
99
+
// Header with app name
100
+
Text {
101
+
text: modelData.title || modelData.id
102
+
color: Settings.colors.foreground
103
+
font {
104
+
pixelSize: 12
105
+
weight: Font.Bold
106
+
}
107
+
opacity: 0.7
108
+
Layout.fillWidth: true
109
+
Layout.leftMargin: 8
110
+
Layout.bottomMargin: 4
111
+
elide: Text.ElideRight
112
+
visible: text !== ""
113
+
}
74
114
75
-
width: parent.width
76
-
height: parent.height
77
-
spacing: 5
115
+
Rectangle {
116
+
Layout.fillWidth: true
117
+
Layout.preferredHeight: 1
118
+
Layout.leftMargin: 4
119
+
Layout.rightMargin: 4
120
+
Layout.bottomMargin: 4
121
+
color: Settings.colors.foreground
122
+
opacity: 0.1
123
+
visible: (modelData.title || modelData.id) !== ""
124
+
}
78
125
79
-
ScrollBar.vertical: ScrollBar {}
126
+
Repeater {
127
+
model: menu.children
128
+
129
+
delegate: Loader {
130
+
id: menuItemLoader
131
+
required property var modelData
132
+
133
+
Layout.fillWidth: true
134
+
sourceComponent: modelData.isSeparator ? separatorComponent : menuItemComponent
135
+
136
+
Component {
137
+
id: separatorComponent
138
+
139
+
Rectangle {
140
+
height: 9
141
+
color: "transparent"
80
142
81
-
delegate: Item {
82
-
required property QsMenuHandle modelData
143
+
Rectangle {
144
+
anchors.centerIn: parent
145
+
width: parent.width - 16
146
+
height: 1
147
+
color: Settings.colors.foreground
148
+
opacity: 0.1
149
+
}
150
+
}
151
+
}
83
152
84
-
width: parent.width
85
-
height: 40
153
+
Component {
154
+
id: menuItemComponent
86
155
87
-
Rectangle {
88
-
anchors {
89
-
fill: parent
90
-
leftMargin: 5
91
-
rightMargin: 5
92
-
}
156
+
Rectangle {
157
+
id: menuItemRect
158
+
height: 32
159
+
radius: 6
160
+
color: menuMouse.containsMouse && menuItemLoader.modelData.enabled
161
+
? Settings.colors.backgroundLighter
162
+
: "transparent"
163
+
opacity: menuItemLoader.modelData.enabled ? 1.0 : 0.5
164
+
165
+
RowLayout {
166
+
anchors {
167
+
fill: parent
168
+
leftMargin: 10
169
+
rightMargin: 10
170
+
}
171
+
spacing: 8
172
+
173
+
// Checkbox/Radio indicator
174
+
Rectangle {
175
+
visible: menuItemLoader.modelData.buttonType !== 0 // QsMenuButtonType.None
176
+
Layout.preferredWidth: 16
177
+
Layout.preferredHeight: 16
178
+
radius: menuItemLoader.modelData.buttonType === 2 ? 8 : 3 // RadioButton = 2
179
+
color: "transparent"
180
+
border.width: 1.5
181
+
border.color: menuItemLoader.modelData.checkState !== Qt.Unchecked
182
+
? Settings.colors.accent
183
+
: Settings.colors.foreground
184
+
opacity: menuItemLoader.modelData.checkState !== Qt.Unchecked ? 1.0 : 0.5
185
+
186
+
Rectangle {
187
+
anchors.centerIn: parent
188
+
width: 8
189
+
height: 8
190
+
radius: menuItemLoader.modelData.buttonType === 2 ? 4 : 2
191
+
color: Settings.colors.accent
192
+
visible: menuItemLoader.modelData.checkState !== Qt.Unchecked
193
+
}
194
+
}
93
195
94
-
color: Settings.colors.backgroundLighter
95
-
radius: 5
196
+
// Icon
197
+
Image {
198
+
visible: menuItemLoader.modelData.icon !== "" && menuItemLoader.modelData.buttonType === 0
199
+
source: menuItemLoader.modelData.icon
200
+
Layout.preferredWidth: 16
201
+
Layout.preferredHeight: 16
202
+
sourceSize.width: 16
203
+
sourceSize.height: 16
204
+
smooth: true
205
+
}
96
206
97
-
Text {
98
-
anchors.centerIn: parent
99
-
text: modelData.text
100
-
color: Settings.colors.foreground
101
-
font.pointSize: 12
102
-
}
207
+
// Text
208
+
Text {
209
+
text: menuItemLoader.modelData.text
210
+
color: Settings.colors.foreground
211
+
font.pixelSize: 13
212
+
elide: Text.ElideRight
213
+
Layout.fillWidth: true
214
+
}
103
215
104
-
MouseArea {
105
-
anchors.fill: parent
106
-
hoverEnabled: true
216
+
// Submenu indicator
217
+
MyIcon {
218
+
visible: menuItemLoader.modelData.hasChildren
219
+
icon: "go-next-symbolic"
220
+
size: 12
221
+
opacity: 0.6
222
+
}
223
+
}
107
224
108
-
onClicked: {
109
-
modelData.triggered();
110
-
popup.visible = false;
111
-
}
225
+
MouseArea {
226
+
id: menuMouse
227
+
anchors.fill: parent
228
+
hoverEnabled: true
229
+
cursorShape: menuItemLoader.modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
230
+
onClicked: {
231
+
if (!menuItemLoader.modelData.enabled) return;
232
+
233
+
if (menuItemLoader.modelData.hasChildren) {
234
+
// TODO: Handle submenu
235
+
} else {
236
+
menuItemLoader.modelData.triggered();
237
+
popup.visible = false;
238
+
}
239
+
}
240
+
}
241
+
}
242
+
}
243
+
}
244
+
}
245
+
}
112
246
}
113
-
}
114
247
}
115
-
}
116
248
}
117
-
}
118
249
}
119
-
}
120
250
}
+36
-8
home/isabel/gui/quickshell/components/Volume.qml
+36
-8
home/isabel/gui/quickshell/components/Volume.qml
···
1
-
import Quickshell
2
-
import Quickshell.Widgets
1
+
import QtQuick
3
2
import QtQuick.Layouts
3
+
import Quickshell
4
+
import Quickshell.Services.Pipewire
5
+
import "root:/data"
4
6
5
-
IconImage {
6
-
id: volumeIcon
7
-
source: Quickshell.iconPath("audio-volume-high-symbolic")
8
-
width: 16
9
-
height: 16
7
+
Item {
8
+
id: root
9
+
10
+
Layout.alignment: Qt.AlignCenter
11
+
implicitWidth: 20
12
+
implicitHeight: 20
10
13
11
-
Layout.alignment: Qt.AlignCenter
14
+
PwObjectTracker {
15
+
objects: [Pipewire.defaultAudioSink]
16
+
}
17
+
18
+
property PwNode sink: Pipewire.defaultAudioSink
19
+
property real volume: sink?.audio?.volume ?? 0
20
+
property bool muted: sink?.audio?.muted ?? false
21
+
22
+
property string iconName: {
23
+
if (muted) return "audio-volume-muted-symbolic";
24
+
if (volume > 0.66) return "audio-volume-high-symbolic";
25
+
if (volume > 0.33) return "audio-volume-medium-symbolic";
26
+
if (volume > 0) return "audio-volume-low-symbolic";
27
+
return "audio-volume-muted-symbolic";
28
+
}
29
+
30
+
IconButton {
31
+
anchors.centerIn: parent
32
+
icon: root.iconName
33
+
size: 18
34
+
onClicked: {
35
+
if (root.sink?.audio) {
36
+
root.sink.audio.muted = !root.sink.audio.muted;
37
+
}
38
+
}
39
+
}
12
40
}
+53
-28
home/isabel/gui/quickshell/components/Workspaces.qml
+53
-28
home/isabel/gui/quickshell/components/Workspaces.qml
···
4
4
import QtQuick.Layouts
5
5
import Quickshell.Hyprland
6
6
import "root:/data"
7
-
import "root:/services"
8
7
9
8
Item {
10
-
id: workspaces
9
+
id: root
11
10
12
-
Layout.alignment: Qt.AlignCenter
11
+
Layout.alignment: Qt.AlignCenter
12
+
implicitWidth: 24
13
+
implicitHeight: workspaceColumn.implicitHeight
13
14
14
-
width: 20
15
-
height: 20
15
+
ColumnLayout {
16
+
id: workspaceColumn
17
+
anchors.horizontalCenter: parent.horizontalCenter
18
+
spacing: 4
16
19
17
-
ColumnLayout {
18
-
spacing: 20
20
+
Repeater {
21
+
model: Hyprland.workspaces
19
22
20
-
anchors.horizontalCenter: parent.horizontalCenter
23
+
delegate: Item {
24
+
id: workspaceItem
25
+
required property HyprlandWorkspace modelData
21
26
22
-
Repeater {
23
-
model: Hyprland.workspaces
27
+
Layout.alignment: Qt.AlignHCenter
28
+
implicitWidth: 24
29
+
implicitHeight: 24
24
30
25
-
delegate: Item {
26
-
id: workspace
27
-
required property HyprlandWorkspace modelData
31
+
Rectangle {
32
+
anchors.centerIn: parent
33
+
width: 22
34
+
height: 22
35
+
radius: 6
36
+
color: workspaceItem.modelData.focused
37
+
? Settings.colors.accent
38
+
: "transparent"
28
39
29
-
implicitWidth: 10
30
-
implicitHeight: 10
31
-
32
-
MouseArea {
33
-
anchors.fill: parent
34
-
hoverEnabled: true
40
+
Behavior on color {
41
+
ColorAnimation { duration: 150 }
42
+
}
35
43
36
-
onClicked: modelData.activate()
37
-
}
44
+
Text {
45
+
anchors.centerIn: parent
46
+
text: workspaceItem.modelData.id
47
+
color: workspaceItem.modelData.focused
48
+
? Settings.colors.background
49
+
: Settings.colors.foreground
50
+
font {
51
+
pixelSize: 12
52
+
weight: Font.Medium
53
+
}
54
+
opacity: workspaceItem.modelData.focused ? 1.0 : 0.6
38
55
39
-
Text {
40
-
font.pointSize: 13
41
-
Layout.alignment: Qt.AlignCenter
56
+
Behavior on color {
57
+
ColorAnimation { duration: 150 }
58
+
}
59
+
Behavior on opacity {
60
+
NumberAnimation { duration: 150 }
61
+
}
62
+
}
63
+
}
42
64
43
-
color: modelData.focused === modelData.id ? Settings.colors.accent : Settings.colors.foreground
44
-
text: modelData.id
65
+
MouseArea {
66
+
anchors.fill: parent
67
+
hoverEnabled: true
68
+
cursorShape: Qt.PointingHandCursor
69
+
onClicked: workspaceItem.modelData.activate()
70
+
}
71
+
}
45
72
}
46
-
}
47
73
}
48
-
}
49
74
}
+5
home/isabel/gui/quickshell/data/Settings.qml
+5
home/isabel/gui/quickshell/data/Settings.qml
+76
-58
home/isabel/gui/quickshell/modules/Bar.qml
+76
-58
home/isabel/gui/quickshell/modules/Bar.qml
···
7
7
import "root:/components"
8
8
9
9
Scope {
10
-
id: root
10
+
id: root
11
11
12
-
Variants {
13
-
model: Quickshell.screens
12
+
Variants {
13
+
model: Quickshell.screens
14
14
15
-
PanelWindow {
16
-
property var modelData
17
-
screen: modelData
18
-
implicitWidth: 40
19
-
color: "transparent"
15
+
PanelWindow {
16
+
property var modelData
17
+
screen: modelData
18
+
implicitWidth: 48
19
+
color: "transparent"
20
20
21
-
anchors {
22
-
top: true
23
-
left: true
24
-
bottom: true
25
-
}
21
+
anchors {
22
+
top: true
23
+
left: true
24
+
bottom: true
25
+
}
26
26
27
-
margins {
28
-
left: 8
29
-
right: 8
30
-
top: 8
31
-
bottom: 8
32
-
}
27
+
margins {
28
+
left: 10
29
+
top: 10
30
+
bottom: 10
31
+
}
33
32
34
-
Rectangle {
35
-
id: bar
36
-
anchors.fill: parent
37
-
radius: 10
38
-
color: Settings.colors.background
33
+
Rectangle {
34
+
id: bar
35
+
anchors.fill: parent
36
+
radius: 12
37
+
color: Settings.colors.background
39
38
40
-
ColumnLayout {
41
-
anchors {
42
-
left: parent.left
43
-
top: parent.top
44
-
right: parent.right
45
-
topMargin: 15
46
-
}
39
+
// Top section - Launcher & Workspaces
40
+
ColumnLayout {
41
+
anchors {
42
+
horizontalCenter: parent.horizontalCenter
43
+
top: parent.top
44
+
topMargin: 12
45
+
}
46
+
spacing: 16
47
47
48
-
spacing: 15
48
+
Launcher {}
49
49
50
-
Launcher {}
51
-
Workspaces {}
52
-
}
50
+
Rectangle {
51
+
Layout.alignment: Qt.AlignHCenter
52
+
width: 20
53
+
height: 1
54
+
color: Settings.colors.foreground
55
+
opacity: 0.2
56
+
}
53
57
54
-
ColumnLayout {
55
-
anchors {
56
-
left: parent.left
57
-
right: parent.right
58
-
top: parent.verticalCenter
59
-
}
58
+
Workspaces {}
59
+
}
60
60
61
-
spacing: 20
61
+
// Center section - Clock & Notifications
62
+
ColumnLayout {
63
+
anchors {
64
+
horizontalCenter: parent.horizontalCenter
65
+
verticalCenter: parent.verticalCenter
66
+
}
67
+
spacing: 16
62
68
63
-
Clock {}
64
-
Noti {}
65
-
}
69
+
Clock {
70
+
onClicked: notiPanel.togglePopup()
71
+
}
72
+
Noti {
73
+
id: notiPanel
74
+
}
75
+
}
76
+
77
+
// Bottom section - System tray, Volume, Network
78
+
ColumnLayout {
79
+
anchors {
80
+
horizontalCenter: parent.horizontalCenter
81
+
bottom: parent.bottom
82
+
bottomMargin: 12
83
+
}
84
+
spacing: 12
66
85
67
-
ColumnLayout {
68
-
anchors {
69
-
left: parent.left
70
-
bottom: parent.bottom
71
-
right: parent.right
72
-
bottomMargin: 15
73
-
}
86
+
SysTray {}
74
87
75
-
spacing: 15
88
+
Rectangle {
89
+
Layout.alignment: Qt.AlignHCenter
90
+
width: 20
91
+
height: 1
92
+
color: Settings.colors.foreground
93
+
opacity: 0.2
94
+
}
76
95
77
-
SysTray {}
78
-
Volume {}
79
-
Network {}
96
+
Volume {}
97
+
Network {}
98
+
}
99
+
}
80
100
}
81
-
}
82
101
}
83
-
}
84
102
}
+276
home/isabel/gui/quickshell/modules/Osd.qml
+276
home/isabel/gui/quickshell/modules/Osd.qml
···
1
+
import QtQuick
2
+
import QtQuick.Layouts
3
+
import Quickshell
4
+
import Quickshell.Widgets
5
+
import Quickshell.Wayland
6
+
import Quickshell.Services.Pipewire
7
+
import Quickshell.Services.Notifications as QsNotifications
8
+
import "root:/data"
9
+
import "root:/components"
10
+
import "root:/services"
11
+
12
+
Scope {
13
+
id: root
14
+
15
+
// OSD State
16
+
property string osdType: ""
17
+
property real progress: 0
18
+
property string iconSource: ""
19
+
property string osdText: ""
20
+
property bool osdVisible: false
21
+
22
+
// Notification State
23
+
property var currentNotification: null
24
+
property bool notificationVisible: false
25
+
26
+
// Pipewire
27
+
PwObjectTracker {
28
+
objects: [Pipewire.defaultAudioSink]
29
+
}
30
+
31
+
property PwNode sink: Pipewire.defaultAudioSink
32
+
property real volume: sink?.audio?.volume ?? 0
33
+
property bool muted: sink?.audio?.muted ?? false
34
+
35
+
onVolumeChanged: showVolumeOsd()
36
+
onMutedChanged: showVolumeOsd()
37
+
38
+
function showVolumeOsd(): void {
39
+
root.osdType = "volume";
40
+
root.progress = root.volume;
41
+
if (root.muted) {
42
+
root.iconSource = "audio-volume-muted-symbolic";
43
+
} else if (root.volume > 0.66) {
44
+
root.iconSource = "audio-volume-high-symbolic";
45
+
} else if (root.volume > 0.33) {
46
+
root.iconSource = "audio-volume-medium-symbolic";
47
+
} else if (root.volume > 0) {
48
+
root.iconSource = "audio-volume-low-symbolic";
49
+
} else {
50
+
root.iconSource = "audio-volume-muted-symbolic";
51
+
}
52
+
root.osdText = Math.round(root.volume * 100) + "%";
53
+
root.osdVisible = true;
54
+
osdHideTimer.restart();
55
+
}
56
+
57
+
function showNotification(notification: QsNotifications.Notification): void {
58
+
root.currentNotification = notification;
59
+
root.notificationVisible = true;
60
+
notificationHideTimer.restart();
61
+
}
62
+
63
+
Timer {
64
+
id: osdHideTimer
65
+
interval: 1500
66
+
onTriggered: root.osdVisible = false
67
+
}
68
+
69
+
Timer {
70
+
id: notificationHideTimer
71
+
interval: 4000
72
+
onTriggered: root.notificationVisible = false
73
+
}
74
+
75
+
// Connect to the shared notification service
76
+
Connections {
77
+
target: Notifications
78
+
function onNewNotification(notification) {
79
+
root.showNotification(notification);
80
+
}
81
+
}
82
+
83
+
Variants {
84
+
model: Quickshell.screens
85
+
86
+
PanelWindow {
87
+
id: osdWindow
88
+
property var modelData
89
+
screen: modelData
90
+
91
+
visible: root.osdVisible
92
+
color: "transparent"
93
+
94
+
WlrLayershell.namespace: "osd"
95
+
WlrLayershell.layer: WlrLayer.Overlay
96
+
exclusionMode: ExclusionMode.Ignore
97
+
98
+
anchors {
99
+
bottom: true
100
+
}
101
+
102
+
implicitWidth: 320
103
+
implicitHeight: 80
104
+
105
+
margins {
106
+
bottom: 80
107
+
}
108
+
109
+
Rectangle {
110
+
id: osdContainer
111
+
anchors.centerIn: parent
112
+
width: 300
113
+
height: 56
114
+
radius: 14
115
+
color: Settings.colors.background
116
+
117
+
RowLayout {
118
+
anchors {
119
+
fill: parent
120
+
leftMargin: 18
121
+
rightMargin: 18
122
+
}
123
+
spacing: 14
124
+
125
+
MyIcon {
126
+
icon: root.iconSource
127
+
size: 22
128
+
Layout.alignment: Qt.AlignVCenter
129
+
}
130
+
131
+
Rectangle {
132
+
Layout.fillWidth: true
133
+
Layout.preferredHeight: 6
134
+
Layout.alignment: Qt.AlignVCenter
135
+
radius: 3
136
+
color: Settings.colors.backgroundLighter
137
+
138
+
Rectangle {
139
+
width: parent.width * Math.min(root.progress, 1.0)
140
+
height: parent.height
141
+
radius: 3
142
+
color: root.muted ? Settings.colors.error : Settings.colors.accent
143
+
144
+
Behavior on width {
145
+
SmoothedAnimation { velocity: 600 }
146
+
}
147
+
}
148
+
}
149
+
150
+
Text {
151
+
text: root.osdText
152
+
color: Settings.colors.foreground
153
+
font {
154
+
pixelSize: 13
155
+
weight: Font.Medium
156
+
}
157
+
Layout.preferredWidth: 40
158
+
Layout.alignment: Qt.AlignVCenter
159
+
horizontalAlignment: Text.AlignRight
160
+
}
161
+
}
162
+
}
163
+
}
164
+
}
165
+
166
+
// Notification Popup
167
+
Variants {
168
+
model: Quickshell.screens
169
+
170
+
PanelWindow {
171
+
id: notificationWindow
172
+
property var modelData
173
+
screen: modelData
174
+
175
+
visible: root.notificationVisible && root.currentNotification !== null
176
+
color: "transparent"
177
+
178
+
WlrLayershell.namespace: "notification"
179
+
WlrLayershell.layer: WlrLayer.Overlay
180
+
exclusionMode: ExclusionMode.Ignore
181
+
182
+
anchors {
183
+
top: true
184
+
}
185
+
186
+
implicitWidth: 420
187
+
implicitHeight: 100
188
+
189
+
margins {
190
+
top: 16
191
+
}
192
+
193
+
Rectangle {
194
+
id: notificationContainer
195
+
anchors {
196
+
fill: parent
197
+
margins: 8
198
+
}
199
+
radius: 12
200
+
color: Settings.colors.background
201
+
202
+
RowLayout {
203
+
anchors {
204
+
fill: parent
205
+
margins: 14
206
+
}
207
+
spacing: 12
208
+
209
+
IconImage {
210
+
source: Quickshell.iconPath(root.currentNotification.appIcon)
211
+
implicitSize: 40
212
+
Layout.alignment: Qt.AlignTop
213
+
}
214
+
215
+
ColumnLayout {
216
+
Layout.fillWidth: true
217
+
Layout.fillHeight: true
218
+
spacing: 4
219
+
220
+
Text {
221
+
text: root.currentNotification?.appName ?? ""
222
+
color: Settings.colors.foreground
223
+
font {
224
+
pixelSize: 13
225
+
weight: Font.Bold
226
+
}
227
+
elide: Text.ElideRight
228
+
Layout.fillWidth: true
229
+
}
230
+
231
+
Text {
232
+
text: root.currentNotification?.summary ?? ""
233
+
color: Settings.colors.foreground
234
+
font {
235
+
pixelSize: 12
236
+
weight: Font.Medium
237
+
}
238
+
elide: Text.ElideRight
239
+
Layout.fillWidth: true
240
+
visible: text !== ""
241
+
}
242
+
243
+
Text {
244
+
text: root.currentNotification?.body ?? ""
245
+
color: Settings.colors.foreground
246
+
opacity: 0.8
247
+
font.pixelSize: 11
248
+
elide: Text.ElideRight
249
+
wrapMode: Text.WordWrap
250
+
maximumLineCount: 2
251
+
Layout.fillWidth: true
252
+
}
253
+
}
254
+
255
+
IconButton {
256
+
icon: "window-close-symbolic"
257
+
size: 14
258
+
Layout.alignment: Qt.AlignTop
259
+
onClicked: {
260
+
root.currentNotification?.dismiss();
261
+
root.notificationVisible = false;
262
+
}
263
+
}
264
+
}
265
+
266
+
// Click to dismiss
267
+
MouseArea {
268
+
anchors.fill: parent
269
+
z: -1
270
+
onClicked: root.notificationVisible = false
271
+
}
272
+
}
273
+
}
274
+
}
275
+
}
276
+
+71
home/isabel/gui/quickshell/services/Bluetooth.qml
+71
home/isabel/gui/quickshell/services/Bluetooth.qml
···
1
+
pragma Singleton
2
+
3
+
import Quickshell
4
+
import Quickshell.Bluetooth
5
+
import QtQuick
6
+
7
+
Singleton {
8
+
id: root
9
+
10
+
// Adapter state
11
+
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
12
+
readonly property bool powered: adapter?.enabled ?? false
13
+
readonly property int state: adapter?.state ?? BluetoothAdapterState.Disabled
14
+
15
+
// Device state
16
+
readonly property var devices: adapter?.devices ?? []
17
+
readonly property BluetoothDevice connectedDevice: {
18
+
if (!adapter) return null;
19
+
for (let i = 0; i < devices.count; i++) {
20
+
const device = devices.values[i];
21
+
if (device.connected) return device;
22
+
}
23
+
return null;
24
+
}
25
+
readonly property bool connected: connectedDevice !== null
26
+
27
+
// Icon based on state
28
+
readonly property string icon: {
29
+
if (state === BluetoothAdapterState.Blocked) return "bluetooth-disabled-symbolic";
30
+
if (!powered) return "bluetooth-disabled-symbolic";
31
+
if (connected) return "bluetooth-active-symbolic";
32
+
return "bluetooth-symbolic";
33
+
}
34
+
35
+
readonly property string statusText: {
36
+
if (state === BluetoothAdapterState.Blocked) return "Blocked";
37
+
if (state === BluetoothAdapterState.Enabling) return "Enabling...";
38
+
if (state === BluetoothAdapterState.Disabling) return "Disabling...";
39
+
if (!powered) return "Bluetooth Off";
40
+
if (connected) return connectedDevice.name;
41
+
return "Not Connected";
42
+
}
43
+
44
+
reloadableId: "bluetooth"
45
+
46
+
function toggle() {
47
+
if (adapter) {
48
+
adapter.enabled = !adapter.enabled;
49
+
}
50
+
}
51
+
52
+
function connectDevice(device) {
53
+
device.connect();
54
+
}
55
+
56
+
function disconnectDevice(device) {
57
+
device.disconnect();
58
+
}
59
+
60
+
function startDiscovery() {
61
+
if (adapter) {
62
+
adapter.discovering = true;
63
+
}
64
+
}
65
+
66
+
function stopDiscovery() {
67
+
if (adapter) {
68
+
adapter.discovering = false;
69
+
}
70
+
}
71
+
}
+122
-25
home/isabel/gui/quickshell/services/Networking.qml
+122
-25
home/isabel/gui/quickshell/services/Networking.qml
···
1
-
// stolen from https://github.com/caelestia-dots/shell/blob/cf1180df7ba9c8f1180ab3a902746d4f169c556f/services/Network.qml
2
-
// thankies :D
1
+
// Network service with WiFi and Ethernet support
3
2
4
3
pragma Singleton
5
4
···
10
9
Singleton {
11
10
id: root
12
11
12
+
// WiFi
13
13
readonly property list<AccessPoint> networks: []
14
-
readonly property AccessPoint active: networks.find(n => n.active) ?? null
14
+
readonly property AccessPoint activeWifi: networks.find(n => n.active) ?? null
15
+
readonly property bool wifiEnabled: internal.wifiEnabled
16
+
17
+
// Ethernet
18
+
readonly property bool ethernetConnected: internal.ethernetConnected
19
+
readonly property string ethernetDevice: internal.ethernetDevice
20
+
21
+
// Combined state
22
+
readonly property bool connected: ethernetConnected || activeWifi !== null
23
+
readonly property string connectionType: ethernetConnected ? "ethernet" : (activeWifi ? "wifi" : "none")
24
+
25
+
readonly property string icon: {
26
+
if (ethernetConnected) return "network-wired-symbolic";
27
+
if (activeWifi) return activeWifi.icon;
28
+
if (!wifiEnabled) return "network-wireless-disabled-symbolic";
29
+
return "network-wireless-offline-symbolic";
30
+
}
31
+
32
+
readonly property string statusText: {
33
+
if (ethernetConnected) return ethernetDevice;
34
+
if (activeWifi) return activeWifi.ssid;
35
+
return "Disconnected";
36
+
}
15
37
16
38
reloadableId: "network"
17
39
40
+
function toggleWifi() {
41
+
toggleWifiProc.running = true;
42
+
}
43
+
44
+
function reload() {
45
+
checkStatus.running = true;
46
+
}
47
+
48
+
function connectToNetwork(ssid) {
49
+
connectProc.command = ["nmcli", "d", "wifi", "connect", ssid];
50
+
connectProc.running = true;
51
+
}
52
+
53
+
QtObject {
54
+
id: internal
55
+
property bool wifiEnabled: true
56
+
property bool ethernetConnected: false
57
+
property string ethernetDevice: ""
58
+
}
59
+
60
+
// Monitor for network changes
18
61
Process {
19
62
running: true
20
63
command: ["nmcli", "m"]
21
64
stdout: SplitParser {
22
-
onRead: getNetworks.running = true
65
+
onRead: checkStatus.running = true
66
+
}
67
+
}
68
+
69
+
// Check overall network status
70
+
Process {
71
+
id: checkStatus
72
+
running: true
73
+
command: ["bash", "-c", `
74
+
# Check WiFi status
75
+
wifi_status=$(nmcli radio wifi 2>/dev/null)
76
+
echo "WIFI:$wifi_status"
77
+
78
+
# Check Ethernet
79
+
eth_info=$(nmcli -t -f TYPE,STATE,DEVICE,CONNECTION device 2>/dev/null | grep '^ethernet:connected' | head -1)
80
+
if [ -n "$eth_info" ]; then
81
+
eth_name=$(echo "$eth_info" | cut -d: -f4)
82
+
echo "ETH:connected:$eth_name"
83
+
else
84
+
echo "ETH:disconnected:"
85
+
fi
86
+
`]
87
+
environment: ({ LANG: "C", LC_ALL: "C" })
88
+
stdout: SplitParser {
89
+
onRead: {
90
+
const line = data.trim();
91
+
if (line.startsWith("WIFI:")) {
92
+
internal.wifiEnabled = line.includes("enabled");
93
+
} else if (line.startsWith("ETH:")) {
94
+
const parts = line.split(":");
95
+
internal.ethernetConnected = parts[1] === "connected";
96
+
internal.ethernetDevice = parts[2] || "";
97
+
}
98
+
}
99
+
}
100
+
onExited: {
101
+
if (internal.wifiEnabled) {
102
+
getNetworks.running = true;
103
+
}
23
104
}
24
105
}
25
106
26
107
Process {
108
+
id: toggleWifiProc
109
+
command: ["nmcli", "radio", "wifi", internal.wifiEnabled ? "off" : "on"]
110
+
onExited: checkStatus.running = true
111
+
}
112
+
113
+
Process {
114
+
id: connectProc
115
+
onExited: checkStatus.running = true
116
+
}
117
+
118
+
Process {
27
119
id: getNetworks
28
-
running: true
29
120
command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID", "d", "w"]
30
-
environment: ({
31
-
LANG: "C",
32
-
LC_ALL: "C"
33
-
})
121
+
environment: ({ LANG: "C", LC_ALL: "C" })
34
122
stdout: StdioCollector {
35
123
onStreamFinished: {
36
124
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
37
125
const rep = new RegExp("\\\\:", "g");
38
126
const rep2 = new RegExp(PLACEHOLDER, "g");
39
127
40
-
const networks = text.trim().split("\n").map(n => {
128
+
const networks = text.trim().split("\n").filter(l => l.length > 0).map(n => {
41
129
const net = n.replace(rep, PLACEHOLDER).split(":");
42
130
return {
43
131
active: net[0] === "yes",
44
-
strength: parseInt(net[1]),
45
-
frequency: parseInt(net[2]),
46
-
ssid: net[3],
132
+
strength: parseInt(net[1]) || 0,
133
+
frequency: parseInt(net[2]) || 0,
134
+
ssid: net[3] || "",
47
135
bssid: net[4]?.replace(rep2, ":") ?? ""
48
136
};
49
-
});
137
+
}).filter(n => n.ssid !== "");
138
+
50
139
const rNetworks = root.networks;
51
140
52
-
const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid));
53
-
for (const network of destroyed)
54
-
rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy());
141
+
// Remove networks that no longer exist
142
+
const destroyed = rNetworks.filter(rn => !networks.find(n =>
143
+
n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid
144
+
));
145
+
for (const network of destroyed) {
146
+
const idx = rNetworks.indexOf(network);
147
+
if (idx >= 0) {
148
+
rNetworks.splice(idx, 1);
149
+
network.destroy();
150
+
}
151
+
}
55
152
153
+
// Update or add networks
56
154
for (const network of networks) {
57
-
const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid);
155
+
const match = rNetworks.find(n =>
156
+
n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid
157
+
);
58
158
if (match) {
59
159
match.lastIpcObject = network;
60
160
} else {
61
-
rNetworks.push(apComp.createObject(root, {
62
-
lastIpcObject: network
63
-
}));
161
+
rNetworks.push(apComp.createObject(root, { lastIpcObject: network }));
64
162
}
65
163
}
66
164
}
···
75
173
readonly property int frequency: lastIpcObject.frequency
76
174
readonly property bool active: lastIpcObject.active
77
175
readonly property string icon: {
78
-
if (!active) return "network-wireless-signal-none-symbolic";
79
-
80
176
if (strength >= 75) return "network-wireless-signal-excellent-symbolic";
81
177
if (strength >= 50) return "network-wireless-signal-good-symbolic";
82
178
if (strength >= 25) return "network-wireless-signal-ok-symbolic";
83
-
return "network-wireless-signal-weak-symbolic";
179
+
if (strength > 0) return "network-wireless-signal-weak-symbolic";
180
+
return "network-wireless-signal-none-symbolic";
84
181
}
85
182
}
86
183
87
184
Component {
88
185
id: apComp
89
-
90
186
AccessPoint {}
91
187
}
92
188
}
189
+
+12
-1
home/isabel/gui/quickshell/services/Notifications.qml
+12
-1
home/isabel/gui/quickshell/services/Notifications.qml
···
3
3
import Quickshell
4
4
import Quickshell.Io
5
5
import Quickshell.Services.Notifications
6
+
import "root:/data"
6
7
7
8
Singleton {
8
9
id: root
9
10
10
-
property list<Notification> list: notifactionServer.trackedNotifications.values.filter(notification => notification.tracked)
11
+
property bool dndEnabled: false
12
+
property list<Notification> list: notifactionServer.trackedNotifications.values.filter(
13
+
notification => notification.tracked && !root.dndEnabled && !Settings.notificationBlacklist.includes(notification.appName)
14
+
)
15
+
16
+
signal newNotification(Notification notification)
11
17
12
18
NotificationServer {
13
19
id: notifactionServer
14
20
onNotification: (notification) => {
15
21
notification.tracked = true
22
+
// Only emit signal if not in DND and not blacklisted
23
+
if (!root.dndEnabled && !Settings.notificationBlacklist.includes(notification.appName)) {
24
+
root.newNotification(notification)
25
+
}
16
26
}
17
27
18
28
actionsSupported: true
···
28
38
}
29
39
}
30
40
}
41
+
+4
-3
home/isabel/gui/quickshell/shell.qml
+4
-3
home/isabel/gui/quickshell/shell.qml
+2
home/isabel/gui/quickshell.nix
+2
home/isabel/gui/quickshell.nix
+6
-5
home/isabel/gui/vicinae/default.nix
+6
-5
home/isabel/gui/vicinae/default.nix
···
14
14
extensions = map mkMyVicinaeExt [
15
15
{
16
16
extName = "nix";
17
-
npmDepsHash = "sha256-Zx+QPVWWppz6mvQKyu4c6ND8E4TeeK12assE2khE/sA=";
17
+
npmDepsHash = "sha256-HPWNUznCWVPz39PlPEBR7GpgbC0DuIAvVBdB2GAs47A=";
18
18
}
19
19
{
20
20
extName = "wifi-commander";
···
24
24
extName = "bluetooth";
25
25
npmDepsHash = "sha256-cpyuJTc3a7oLibKUY2EhD33w8/35frfwIaGFKFezvts=";
26
26
}
27
-
{
28
-
extName = "mullvad";
29
-
npmDepsHash = "sha256-WbnZtsTUMDHh2BojAjHUrca8aBw+OGXMMgX79Ek8wQ0=";
30
-
}
27
+
# broken
28
+
# {
29
+
# extName = "mullvad";
30
+
# npmDepsHash = "sha256-WbnZtsTUMDHh2BojAjHUrca8aBw+OGXMMgX79Ek8wQ0=";
31
+
# }
31
32
];
32
33
};
33
34
}
+2
-2
home/isabel/gui/vicinae/extension.nix
+2
-2
home/isabel/gui/vicinae/extension.nix
···
22
22
fetchFromGitHub {
23
23
owner = "vicinaehq";
24
24
repo = "extensions";
25
-
rev = "ec7334e9bb636f4771580238bd3569b58dbce879";
26
-
hash = "sha256-C2b6upygLE6xUP/cTSKZfVjMXOXOOqpP5Xmgb9r2dhA=";
25
+
rev = "62f81e63d0420d6a310092746a96d7c105f7a53e";
26
+
hash = "sha256-Tqd5BOxfCtVWY19Gl32Fq5xsV3sTepItub20OQYgPmU=";
27
27
}
28
28
+ "/extensions/${extName}"
29
29
);
+1
-1
home/isabel/packages.nix
+1
-1
home/isabel/packages.nix
+1
-1
home/isabel/system/ssh.nix
+1
-1
home/isabel/system/ssh.nix
+63
-27
justfile
+63
-27
justfile
···
3
3
# rebuild is also set as a var so you can add --set to change it if you need to
4
4
5
5
rebuild := if os() == "macos" { "sudo darwin-rebuild" } else { "nixos-rebuild" }
6
-
system-args := if os() == "macos" { "" } else { "--sudo --no-reexec --log-format internal-json" }
7
-
nom-cmd := if os() == "macos" { "nom" } else { "nom --json" }
6
+
system-args := if os() == "macos" { "" } else { "--sudo --no-reexec" }
8
7
9
8
[private]
10
9
default:
···
13
12
# rebuild group
14
13
15
14
[group('rebuild')]
15
+
[no-exit-message]
16
16
[private]
17
17
builder goal *args:
18
+
#!/usr/bin/env bash
19
+
set -euo pipefail
18
20
{{ rebuild }} {{ goal }} \
19
21
--flake {{ flake }} \
22
+
--log-format internal-json \
20
23
{{ system-args }} \
21
24
{{ args }} \
22
-
|& {{ nom-cmd }}
25
+
|& nom --json
23
26
24
27
[group('rebuild')]
28
+
[no-exit-message]
25
29
deploy host *args:
26
-
#!/usr/bin/env bash
27
-
before=$(ssh {{ host }} 'readlink -f /run/current-system')
28
-
before=$(echo "$before" | tail -n 1)
29
-
just builder switch --target-host {{ host }} --use-substitutes {{ args }}
30
-
[ $? -eq 0 ] && ssh {{ host }} 'lix-diff "$before" /run/current-system'
30
+
#!/usr/bin/env bash
31
+
set -euo pipefail
32
+
before=$(ssh -q {{ host }} 'readlink /run/current-system')
33
+
just builder switch --target-host {{ host }} --use-substitutes {{ args }}
34
+
35
+
if [[ -n "${DEPLOY_SUMMARY:-}" ]]; then
36
+
{
37
+
echo "===== {{ host }} ====="
38
+
ssh -q {{ host }} lix diff "$before"
39
+
echo
40
+
} >> "$DEPLOY_SUMMARY"
41
+
else
42
+
ssh {{ host }} lix diff "$before"
43
+
fi
31
44
32
45
[group('rebuild')]
46
+
[no-exit-message]
33
47
deploy-all:
34
-
just deploy minerva
35
-
just deploy aphrodite
36
-
just deploy skadi
37
-
just deploy hephaestus
38
-
just deploy isis
48
+
#!/usr/bin/env bash
49
+
set -euo pipefail
50
+
export DEPLOY_SUMMARY=".deploy-summary"
51
+
: > "$DEPLOY_SUMMARY"
52
+
53
+
just deploy minerva
54
+
just deploy athena
55
+
just deploy aphrodite
56
+
just deploy skadi
57
+
just deploy hephaestus
58
+
just deploy isis
59
+
60
+
echo
61
+
echo "===== DEPLOYMENT SUMMARY ====="
62
+
cat "$DEPLOY_SUMMARY"
63
+
rm "$DEPLOY_SUMMARY"
39
64
40
65
# rebuild the boot
41
66
[group('rebuild')]
67
+
[no-exit-message]
42
68
boot *args: (builder "boot" args)
43
69
44
70
# test what happens when you switch
45
71
[group('rebuild')]
72
+
[no-exit-message]
46
73
test *args: (builder "test" args)
47
74
48
75
# switch the new system configuration
49
76
[group('rebuild')]
77
+
[no-exit-message]
50
78
switch *args:
51
-
#!/usr/bin/env bash
52
-
before="$(readlink -f /run/current-system)"
53
-
just builder switch {{ args }}
54
-
[ $? -eq 0 ] && lix-diff "$before" /run/current-system
79
+
#!/usr/bin/env bash
80
+
set -euo pipefail
81
+
before=$(readlink /run/current-system)
82
+
just builder switch {{ args }}
83
+
lix diff "$before"
55
84
56
85
[group('rebuild')]
57
86
[macos]
87
+
[no-exit-message]
58
88
provision host:
59
89
sudo nix run github:LnL7/nix-darwin -- switch --flake {{ flake }}#{{ host }}
60
90
sudo -i nix-env --uninstall lix # we need to remove the none declarative install of lix
61
91
62
92
# package group
63
-
64
93
# build the package, you must specify the package you want to build
65
-
[group('package')]
66
-
build pkg:
67
-
nix build {{ flake }}#{{ pkg }} \
68
-
--log-format internal-json \
69
-
-v \
70
-
|& nom --json
71
94
72
95
# build the iso image, you must specify the image you want to build
73
96
[group('package')]
74
-
iso image: (build "nixosConfigurations." + image + ".config.system.build.isoImage")
97
+
[no-exit-message]
98
+
iso image:
99
+
nom build {{ flake }}#nixosConfigurations.{{ image }}.config.system.build.isoImage
75
100
76
101
# build the tarball, you must specify the host you want to build
77
102
[group('package')]
103
+
[no-exit-message]
78
104
tar host:
79
105
sudo nix run {{ flake }}#nixosConfigurations.{{ host }}.config.system.build.tarballBuilder
80
106
···
82
108
83
109
# check the flake for errors
84
110
[group('dev')]
111
+
[no-exit-message]
85
112
check *args:
86
113
nix flake check --option allow-import-from-derivation false {{ args }}
87
114
88
115
[group('dev')]
116
+
[no-exit-message]
89
117
repl-host host=`hostname`:
90
118
nix repl .#nixosConfigurations.{{ host }}
91
119
92
120
# update a set of given inputs
93
121
[group('dev')]
122
+
[no-exit-message]
94
123
update *input:
95
124
nix flake update {{ input }} \
96
125
--refresh \
···
100
129
101
130
# build & serve the docs locally
102
131
[group('dev')]
132
+
[no-exit-message]
103
133
serve:
104
134
nix run {{ flake }}#docs.serve
105
135
106
136
# push to the mirrors
107
137
[group('dev')]
138
+
[no-exit-message]
108
139
push-mirrors:
109
140
git push git@gitlab.com:isabelroses/dotfiles.git
110
141
git push --mirror ssh://git@codeberg.org/isabel/dotfiles.git
···
112
143
113
144
# rotate all secrets
114
145
[group('dev')]
146
+
[no-exit-message]
115
147
roate-secrets:
116
-
find secrets/ -name "*.yaml" | xargs -I {} sops rotate -i {}
148
+
find secrets/ -name "*.yaml" | xargs -I {} sops rotate -i {}
117
149
118
150
# update the secret keys
119
151
[group('dev')]
152
+
[no-exit-message]
120
153
update-secrets:
121
-
find secrets/ -name "*.yaml" | xargs -I {} sops updatekeys -y {}
154
+
find secrets/ -name "*.yaml" | xargs -I {} sops updatekeys -y {}
122
155
123
156
# utils group
124
157
···
126
159
127
160
# verify the integrity of the nix store
128
161
[group('utils')]
162
+
[no-exit-message]
129
163
verify *args:
130
164
nix-store --verify {{ args }}
131
165
132
166
# repairs the nix store from any breakages it may have
133
167
[group('utils')]
168
+
[no-exit-message]
134
169
repair: (verify "--check-contents --repair")
135
170
136
171
# clean the nix store and optimise it
137
172
[group('utils')]
173
+
[no-exit-message]
138
174
clean:
139
175
nix-collect-garbage --delete-older-than 3d
140
176
nix store optimise
+1
-1
modules/nixos/hardware/cpu/default.nix
+1
-1
modules/nixos/hardware/cpu/default.nix
+2
modules/nixos/hardware/gpu/nvidia.nix
+2
modules/nixos/hardware/gpu/nvidia.nix
+17
-15
modules/nixos/networking/firewall.nix
+17
-15
modules/nixos/networking/firewall.nix
···
1
1
{
2
2
lib,
3
-
pkgs,
4
3
config,
5
4
...
6
5
}:
···
16
15
# inactive until opensnitch UI is opened
17
16
# services.opensnitch.enable = device.type != "server";
18
17
19
-
networking.firewall = {
20
-
enable = true;
21
-
package = pkgs.iptables;
18
+
networking = {
19
+
nftables.enable = true;
22
20
23
-
allowedTCPPorts = [ ];
24
-
allowedUDPPorts = [ ];
21
+
firewall = {
22
+
enable = true;
25
23
26
-
allowedTCPPortRanges = [ ];
27
-
allowedUDPPortRanges = [ ];
24
+
allowedTCPPorts = [ ];
25
+
allowedUDPPorts = [ ];
28
26
29
-
# allow servers to be pinnged, but not our clients
30
-
allowPing = config.garden.profiles.server.enable;
27
+
allowedTCPPortRanges = [ ];
28
+
allowedUDPPortRanges = [ ];
31
29
32
-
# make a much smaller and easier to read log
33
-
logReversePathDrops = true;
34
-
logRefusedConnections = false;
30
+
# allow servers to be pinnged, but not our clients
31
+
allowPing = config.garden.profiles.server.enable;
35
32
36
-
# Don't filter DHCP packets, according to nixops-libvirtd
37
-
checkReversePath = mkForce false;
33
+
# make a much smaller and easier to read log
34
+
logReversePathDrops = true;
35
+
logRefusedConnections = false;
36
+
37
+
# Don't filter DHCP packets, according to nixops-libvirtd
38
+
checkReversePath = mkForce false;
39
+
};
38
40
};
39
41
};
40
42
}
+2
-2
modules/nixos/networking/networkmanager.nix
+2
-2
modules/nixos/networking/networkmanager.nix
···
5
5
...
6
6
}:
7
7
let
8
-
inherit (lib) mkIf optionalAttrs;
8
+
inherit (lib) optionalAttrs;
9
9
in
10
10
{
11
11
garden.packages = optionalAttrs config.garden.profiles.graphical.enable {
···
40
40
};
41
41
42
42
# causes server to be unreachable over SSH
43
-
ethernet.macAddress = mkIf (!config.garden.profiles.server.enable) "random";
43
+
# ethernet.macAddress = mkIf (!config.garden.profiles.server.enable) "random";
44
44
};
45
45
}
+1
-2
modules/nixos/networking/systemd.nix
+1
-2
modules/nixos/networking/systemd.nix
+4
-1
modules/nixos/networking/tailscale.nix
+4
-1
modules/nixos/networking/tailscale.nix
+49
-16
modules/nixos/nix.nix
+49
-16
modules/nixos/nix.nix
···
1
1
{
2
-
nix = {
3
-
# set the nix store to clean every Monday at 3am
4
-
gc.dates = "Mon *-*-* 03:00";
2
+
lib,
3
+
config,
4
+
options,
5
+
modulesPath,
6
+
...
7
+
}:
8
+
let
9
+
lixModuleMerged = lib.pathExists "${modulesPath}/programs/lix.nix";
10
+
nixDaemonCfg = config.systemd.services.nix-daemon;
11
+
in
12
+
{
13
+
config = lib.mkMerge [
14
+
{
15
+
nix = {
16
+
# set the nix store to clean every Monday at 3am
17
+
gc.dates = "Mon *-*-* 03:00";
18
+
19
+
# automatically optimize /nix/store by removing hard links
20
+
optimise = {
21
+
automatic = true;
22
+
dates = [ "04:00" ];
23
+
};
5
24
6
-
# automatically optimize /nix/store by removing hard links
7
-
optimise = {
8
-
automatic = true;
9
-
dates = [ "04:00" ];
10
-
};
25
+
# Make builds run with a low priority, keeping the system fast
26
+
# daemonCPUSchedPolicy = "idle";
27
+
# daemonIOSchedClass = "idle";
28
+
# daemonIOSchedPriority = 7;
29
+
30
+
# set the build dir to /var/tmp to avoid issues on tmpfs
31
+
# https://github.com/NixOS/nixpkgs/issues/293114#issuecomment-2663470083
32
+
settings.build-dir = "/var/tmp";
33
+
};
34
+
}
11
35
12
-
# Make builds run with a low priority, keeping the system fast
13
-
# daemonCPUSchedPolicy = "idle";
14
-
# daemonIOSchedClass = "idle";
15
-
# daemonIOSchedPriority = 7;
36
+
# https://github.com/NixOS/nixpkgs/pull/469067
37
+
(lib.mkIf (!lixModuleMerged) {
38
+
systemd.services."nix-daemon@" = {
39
+
path = lib.subtractLists (options.systemd.services.type.getSubOptions "").path.value nixDaemonCfg.path;
40
+
environment = lib.filterAttrs (n: _v: n != "PATH") nixDaemonCfg.environment;
41
+
inherit (nixDaemonCfg) serviceConfig unitConfig;
42
+
stopIfChanged = false;
43
+
restartIfChanged = false;
44
+
};
16
45
17
-
# set the build dir to /var/tmp to avoid issues on tmpfs
18
-
# https://github.com/NixOS/nixpkgs/issues/293114#issuecomment-2663470083
19
-
settings.build-dir = "/var/tmp";
20
-
};
46
+
# stc can't restart socket units. it can only reload them, but reloading sockets is in invalid operation!
47
+
systemd.services.lix-daemon-socket-permissions = {
48
+
overrideStrategy = "asDropin";
49
+
inherit (nixDaemonCfg) restartTriggers;
50
+
stopIfChanged = false;
51
+
};
52
+
})
53
+
];
21
54
}
+1
-1
modules/nixos/services/akkoma/default.nix
+1
-1
modules/nixos/services/akkoma/default.nix
···
29
29
30
30
"favicon.png" = pkgs.fetchurl {
31
31
url = "https://gravatar.com/avatar/c487c810e09878b4bd90df713a7c9523?size=512";
32
-
hash = "sha256-psRjtuG+U8KJtWWfa3HAYjOQrjAYQAymDEc6HqhQmnk=";
32
+
hash = "sha256-LRDHPMkJmtkjA/Bd0fpMm5dLf4nhNSIXAAOG8u2ZSqs=";
33
33
};
34
34
35
35
"emoji/blobs" = pkgs.blobs_gg;
+93
modules/nixos/services/arr.nix
+93
modules/nixos/services/arr.nix
···
1
+
{ lib, config, ... }:
2
+
let
3
+
inherit (lib)
4
+
mkEnableOption
5
+
mkOption
6
+
types
7
+
genAttrs
8
+
;
9
+
10
+
cfg = config.garden.services.arr;
11
+
in
12
+
{
13
+
options.garden.services.arr = {
14
+
enable = mkEnableOption "arr services";
15
+
16
+
mediaDir = mkOption {
17
+
type = types.str;
18
+
default = "/media";
19
+
description = "Directory for storing media files managed by arr services";
20
+
};
21
+
22
+
contentDir = mkOption {
23
+
type = types.str;
24
+
default = "${cfg.mediaDir}/content";
25
+
defaultText = "\${cfg.mediaDir}/content";
26
+
description = "Directory for storing application data for arr services";
27
+
};
28
+
29
+
mediaOwner = mkOption {
30
+
type = types.str;
31
+
default = "root";
32
+
description = "User that owns the media and content directories";
33
+
};
34
+
35
+
mediaGroup = mkOption {
36
+
type = types.str;
37
+
default = "media";
38
+
description = "Group that owns the media and content directories";
39
+
};
40
+
41
+
openFirewall = mkEnableOption "open the firewall for the arr services" // {
42
+
default = true;
43
+
defaultText = "true";
44
+
};
45
+
};
46
+
47
+
config = lib.mkIf cfg.enable {
48
+
garden.services = {
49
+
jellyfin.enable = true;
50
+
sonarr.enable = true;
51
+
radarr.enable = true;
52
+
prowlarr.enable = true;
53
+
transmission.enable = true;
54
+
};
55
+
56
+
users.groups.media = { };
57
+
58
+
systemd.tmpfiles.settings = {
59
+
"media-content-dirs" =
60
+
genAttrs
61
+
[
62
+
"${cfg.mediaDir}/content"
63
+
"${cfg.mediaDir}/content/tv"
64
+
"${cfg.mediaDir}/content/movies"
65
+
"${cfg.mediaDir}/content/home"
66
+
]
67
+
(_: {
68
+
d = {
69
+
mode = "0775";
70
+
user = cfg.mediaOwner;
71
+
group = cfg.mediaGroup;
72
+
};
73
+
});
74
+
75
+
"media-downloads-dirs" =
76
+
genAttrs
77
+
[
78
+
"${cfg.mediaDir}/downloads"
79
+
"${cfg.mediaDir}/downloads/incomplete"
80
+
"${cfg.mediaDir}/downloads/watch"
81
+
"${cfg.mediaDir}/downloads/sonarr"
82
+
"${cfg.mediaDir}/downloads/radarr"
83
+
]
84
+
(_: {
85
+
d = {
86
+
mode = "0755";
87
+
user = "transmission";
88
+
group = "media";
89
+
};
90
+
});
91
+
};
92
+
};
93
+
}
+5
modules/nixos/services/default.nix
+5
modules/nixos/services/default.nix
···
3
3
# keep-sorted start
4
4
./akkoma
5
5
./anubis.nix
6
+
./arr.nix
6
7
./attic.nix
7
8
./atuin.nix
8
9
./blahaj.nix
···
23
24
./piper.nix
24
25
./port-collector.nix
25
26
./postgresql.nix
27
+
./prowlarr.nix
28
+
./radarr.nix
26
29
./redis.nix
30
+
./sonarr.nix
31
+
./transmission.nix
27
32
./uptime-kuma.nix
28
33
./vaultwarden.nix
29
34
./wakapi.nix
+2
modules/nixos/services/jellyfin.nix
+2
modules/nixos/services/jellyfin.nix
+26
modules/nixos/services/pds/default.nix
+26
modules/nixos/services/pds/default.nix
···
44
44
enable = true;
45
45
pdsadmin.enable = true;
46
46
47
+
package = pkgs.bluesky-pds.overrideAttrs (
48
+
finalAttrs: _: {
49
+
src = pkgs.fetchFromGitHub {
50
+
owner = "isabelroses";
51
+
repo = "pds-fork";
52
+
rev = "66c026acfcfd290ea962dd4d03379f0990d80071";
53
+
hash = "sha256-x+oh3YKcz2eWkAqg+jZKrh35UoGI6dLQXcEiGItTHKc=";
54
+
};
55
+
56
+
pnpmDeps = pkgs.fetchPnpmDeps {
57
+
inherit (finalAttrs)
58
+
pname
59
+
version
60
+
src
61
+
sourceRoot
62
+
;
63
+
pnpm = pkgs.pnpm_9;
64
+
fetcherVersion = 2;
65
+
hash = "sha256-4qKWkINpUHzatiMa7ZNYp1NauU2641W0jHDjmRL9ipI=";
66
+
};
67
+
}
68
+
);
69
+
47
70
environmentFiles = [ config.sops.secrets.pds-env.path ];
48
71
49
72
settings = {
···
75
98
PDS_OAUTH_PROVIDER_ERROR_COLOR = "#F6598E";
76
99
77
100
PDS_SERVICE_HANDLE_DOMAINS = ".tgirl.beauty";
101
+
102
+
# custom session duration: 30 days
103
+
PDS_OAUTH_AUTHENTICATION_MAX_AGE = "2592000000";
78
104
};
79
105
};
80
106
+1
-1
modules/nixos/services/port-collector.nix
+1
-1
modules/nixos/services/port-collector.nix
+30
modules/nixos/services/prowlarr.nix
+30
modules/nixos/services/prowlarr.nix
···
1
+
{
2
+
lib,
3
+
self,
4
+
config,
5
+
...
6
+
}:
7
+
let
8
+
inherit (lib) mkIf;
9
+
inherit (self.lib) mkServiceOption;
10
+
11
+
cfg = config.garden.services.prowlarr;
12
+
in
13
+
{
14
+
options.garden.services.prowlarr = mkServiceOption "prowlarr" {
15
+
port = 3022;
16
+
};
17
+
18
+
config = mkIf config.garden.services.prowlarr.enable {
19
+
services.prowlarr = {
20
+
enable = true;
21
+
inherit (config.garden.services.arr) openFirewall;
22
+
settings.server.port = cfg.port;
23
+
};
24
+
25
+
systemd.services.prowlarr.serviceConfig = {
26
+
User = "prowlarr";
27
+
Group = "media";
28
+
};
29
+
};
30
+
}
+27
modules/nixos/services/radarr.nix
+27
modules/nixos/services/radarr.nix
···
1
+
{
2
+
lib,
3
+
self,
4
+
config,
5
+
...
6
+
}:
7
+
let
8
+
inherit (lib) mkIf;
9
+
inherit (self.lib) mkServiceOption;
10
+
11
+
cfg = config.garden.services.radarr;
12
+
in
13
+
{
14
+
options.garden.services.radarr = mkServiceOption "radarr" {
15
+
port = 3021;
16
+
};
17
+
18
+
config = mkIf config.garden.services.radarr.enable {
19
+
services.radarr = {
20
+
inherit (cfg) enable;
21
+
group = "media";
22
+
dataDir = "/srv/storage/ranarr";
23
+
inherit (config.garden.services.arr) openFirewall;
24
+
settings.server.port = cfg.port;
25
+
};
26
+
};
27
+
}
+27
modules/nixos/services/sonarr.nix
+27
modules/nixos/services/sonarr.nix
···
1
+
{
2
+
lib,
3
+
self,
4
+
config,
5
+
...
6
+
}:
7
+
let
8
+
inherit (lib) mkIf;
9
+
inherit (self.lib) mkServiceOption;
10
+
11
+
cfg = config.garden.services.sonarr;
12
+
in
13
+
{
14
+
options.garden.services.sonarr = mkServiceOption "sonarr" {
15
+
port = 3020;
16
+
};
17
+
18
+
config = mkIf config.garden.services.sonarr.enable {
19
+
services.sonarr = {
20
+
inherit (cfg) enable;
21
+
group = "media";
22
+
dataDir = "/srv/storage/sonarr";
23
+
inherit (config.garden.services.arr) openFirewall;
24
+
settings.server.port = cfg.port;
25
+
};
26
+
};
27
+
}
+85
modules/nixos/services/transmission.nix
+85
modules/nixos/services/transmission.nix
···
1
+
{
2
+
lib,
3
+
self,
4
+
config,
5
+
...
6
+
}:
7
+
let
8
+
inherit (lib) mkIf genAttrs mkForce;
9
+
inherit (self.lib) mkServiceOption;
10
+
11
+
cfg = config.garden.services.transmission;
12
+
inherit (config.garden.services) arr;
13
+
in
14
+
{
15
+
options.garden.services.transmission = mkServiceOption "transmission" {
16
+
port = 3019;
17
+
host = "0.0.0.0";
18
+
};
19
+
20
+
config = mkIf config.garden.services.transmission.enable {
21
+
# i'm replacing this with systemd tempfiles
22
+
system.activationScripts.transmission-daemon = mkForce "";
23
+
24
+
systemd.tmpfiles.settings."media-downloads-config" =
25
+
genAttrs
26
+
[
27
+
"${config.services.transmission.home}"
28
+
"${config.services.transmission.home}/.config"
29
+
"${config.services.transmission.home}/.config/transmission-daemon"
30
+
]
31
+
(_: {
32
+
d = {
33
+
mode = "0750";
34
+
user = "transmission";
35
+
group = "media";
36
+
};
37
+
});
38
+
39
+
services.transmission = {
40
+
enable = true;
41
+
group = "media";
42
+
43
+
inherit (config.garden.services.arr) openFirewall;
44
+
home = "/srv/storage/transmission";
45
+
46
+
settings = {
47
+
download-dir = "${arr.mediaDir}/downloads";
48
+
incomplete-dir-enabled = true;
49
+
incomplete-dir = "${arr.mediaDir}/downloads/incomplete";
50
+
watch-dir-enabled = true;
51
+
watch-dir = "${arr.mediaDir}/downloads/watch";
52
+
53
+
rpc-port = cfg.port;
54
+
rpc-bind-address = cfg.host;
55
+
rpc-authentication-required = false;
56
+
57
+
rpc-whitelist-enabled = true;
58
+
rpc-whitelist = lib.concatStringsSep "," [
59
+
"127.0.0.1"
60
+
"::1"
61
+
"192.168.1.*"
62
+
];
63
+
64
+
blocklist-enabled = true;
65
+
blocklist-url = "https://github.com/Naunter/BT_BlockLists/raw/master/bt_blocklists.gz";
66
+
67
+
anti-brute-force-enabled = true;
68
+
anti-brute-force-threshold = 10;
69
+
70
+
encryption = 1;
71
+
port-forwarding-enabled = false;
72
+
73
+
utp-enabled = false;
74
+
umask = "002";
75
+
peer-limit-global = 500;
76
+
cache-size-mb = 256;
77
+
download-queue-enabled = true;
78
+
download-queue-size = 20;
79
+
speed-limit-up = 500;
80
+
speed-limit-up-enabled = true;
81
+
message-level = 4;
82
+
};
83
+
};
84
+
};
85
+
}
+2
-2
modules/nixos/system/scheduler.nix
+2
-2
modules/nixos/system/scheduler.nix
+3
-3
secrets/services/piper.yaml
+3
-3
secrets/services/piper.yaml
···
1
-
env: ENC[AES256_GCM,data:scYfFEXl6esjrYaTLbFbeqQVnOw6h2ZzZVQ2EUsCxnYzaI+rhCTVqkrqXgowFAILWwQOiYM7B5ukgqTytiB5h6C2Bh/xa/VdMOPdY3EXqRed7NzZC96LZt3+dKDKKDbkw1wyddWvX/phQFk/cHq6BR5CQZfQ7nPXRr2ofZGoK/3m+GhouyJq2I4RHHRGxv0WXYniHkR9zgdc+myg1KsFphu9XaoR4WN/uaGACv59pguMJWRcBzr+wKPxdWtrzb4vTIVQiF8wpTSdWBmnwmKq29y/pBSNivDdhWB5N1/VHGFDRge5RzxXCg98Hv4fegebx4+YPiVlwoC2peGSi/q8EeNLFES+waywaKmbQ1r54AAFqGnXIIp16Ib5lCUa+VwY2jQl9c+QZOcNEJkVZ6jWmuoUguO1q2y4WbLvcaWksjRXJHs/NOsQoF4=,iv:b8O+3GksPP4hTuH57IxgAeYe6MAiYGO83nJKn7MVn6Q=,tag:V1dFrgABjL6SXN7af1DtgA==,type:str]
1
+
env: ENC[AES256_GCM,data:/bJSpMnpO/P5pen9RKdsRF47veCmkRstBFUwbdyu+fp3UDayUrL2u+/397XQQxBQYl266wJOQB7sjJ5lsOI/yRiM/89yv4zow7Qh/akPnsYyhxiLAjdIdJHFzh6tQMBZiHEHNF16edhejKMgIz0qOIvIQZu0yHX6ozvWfc7KKzYH3CUohbhIjgjqbJfzP/YMFNnqu6Aa4mlksprCeVPg/ZX3vf4z/DS9vIhmUbkc91064gjytMq3EFZvVt01aD3Wror/ApBCtCZ1Pt8zAbH/mrQd2Dsx9pOKueqYhTGcF6nsU5fxkKG62K0nK3xGOS2vOpl4k9m220k5BF1CbSKbWXFBafvtvorYbBBK+N4caBKI6DQz66/eA75D0DwA0KbBKadboqzRZOWFsvqQwIuiqnkzJottJZ2swGbyVjhLsAQEmZI/b6Ie0I0=,iv:Z1gnEHoJ8eBDWn1IorUn/pT+ghxrB7+nk9boHx7efWg=,tag:zNJRN/JeWP98TDxfFDdElw==,type:str]
2
2
sops:
3
3
age:
4
4
- recipient: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMQDiHbMSinj8twL9cTgPOfI6OMexrTZyHX27T8gnMj2
···
71
71
lm9UxzoNda1OtptX52t1sWVYkx1pwvlTbTmuNy/8uVufZVekdPBz+pywuJbSJlh2
72
72
bH8K6Q==
73
73
-----END AGE ENCRYPTED FILE-----
74
-
lastmodified: "2025-11-14T16:08:46Z"
75
-
mac: ENC[AES256_GCM,data:jd968uS+JcdIg1eDyo1ssHSVw/cxXVwuIDE6YdJrZCHpCf/XSHIWvlu6+L3LILixhV45eMdI81GKCMR3uBnXaXf6SOEPWZ2BJc9cT+p1seAPR1surmVA8d2GGKSWuGmi7mFW30kLsDJhRk1pybKpIHf/Jxdgx0cuh95DEoOtoPg=,iv:+XUddLha4hj68zCcnHiH5aEJU6buzKV+FNUO8ZXQofY=,tag:wlmooDkxQJyPD883cwRdSA==,type:str]
74
+
lastmodified: "2026-01-07T01:01:47Z"
75
+
mac: ENC[AES256_GCM,data:ThIfvJcDdWqprQ0i66bTy29QOcPzWjIYCB8aYhdeEIPzBOjQDKgVh3YNXVaMC7/2tBJmC/IQbxvJ/pIPP/UICSxFvqS+Ta6wipRlTs2A23gaoaVXwj7nY8oMwUH4w04NbWuNFavQ/gnKYBEOmqDBsLrZ6+v5IPxfxx/fRapMKMM=,iv:OYGE7Ja0X6WNHswwYwQ8VsDSlb3U/sm47tsTnJ5wspc=,tag:5Pcfd0/eSL36rFNp8sH6qA==,type:str]
76
76
unencrypted_suffix: _unencrypted
77
77
version: 3.11.0
+1
-3
systems/amaterasu/default.nix
+1
-3
systems/amaterasu/default.nix
···
25
25
keyboard = "us";
26
26
};
27
27
28
-
services.xmrig.enable = true;
28
+
# services.xmrig.enable = true;
29
29
30
30
system = {
31
31
boot = {
···
43
43
bluetooth.enable = true;
44
44
printing.enable = false;
45
45
emulation.enable = true;
46
-
47
-
security.auditd.enable = true;
48
46
};
49
47
};
50
48
}
+10
-11
systems/athena/default.nix
+10
-11
systems/athena/default.nix
···
1
+
{ lib, ... }:
1
2
{
2
3
imports = [ ./hardware.nix ];
3
4
···
19
20
};
20
21
};
21
22
22
-
system = {
23
-
boot = {
24
-
loader = "systemd-boot";
25
-
secureBoot = false;
26
-
loadRecommendedModules = true;
27
-
enableKernelTweaks = true;
28
-
initrd.enableTweaks = true;
29
-
};
30
-
31
-
security.auditd.enable = true;
23
+
system.boot = {
24
+
loader = "systemd-boot";
25
+
secureBoot = false;
26
+
loadRecommendedModules = true;
27
+
enableKernelTweaks = true;
28
+
initrd.enableTweaks = true;
32
29
};
33
30
34
31
services = {
35
32
cloudflared.enable = true;
36
33
immich.enable = true;
37
-
jellyfin.enable = true;
34
+
arr.enable = true;
38
35
borgbackup.enable = true;
39
36
};
40
37
};
38
+
39
+
services.smartd.enable = lib.mkForce false;
41
40
}
+5
systems/athena/hardware.nix
+5
systems/athena/hardware.nix
···
9
9
device = "/dev/disk/by-uuid/2524-71ED";
10
10
fsType = "vfat";
11
11
};
12
+
13
+
"/media" = {
14
+
device = "/dev/disk/by-uuid/c4f7c302-f492-47dd-8bfd-e3073c1923bd";
15
+
fsType = "ext4";
16
+
};
12
17
};
13
18
14
19
swapDevices = [ { device = "/dev/disk/by-uuid/e45cd5a5-ec02-4933-9adb-5d968f270f54"; } ];
+1
-1
systems/skadi/default.nix
+1
-1
systems/skadi/default.nix