summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--accounts/gkleen@sif/default.nix13
-rw-r--r--accounts/gkleen@sif/niri/default.nix20
-rw-r--r--accounts/gkleen@sif/shell/default.nix97
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt23
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp35
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp14
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp102
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp52
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp18
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp21
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp16
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp13
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/default.nix8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml13
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml341
-rw-r--r--accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml61
-rw-r--r--accounts/gkleen@sif/shell/quickshell/LockSurface.qml223
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml90
-rw-r--r--accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml24
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml354
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml18
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml14
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/SystemTray.qml34
-rw-r--r--accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml134
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml85
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml198
-rw-r--r--accounts/gkleen@sif/shell/quickshell/displaymanager.qml115
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml27
-rw-r--r--accounts/gkleen@sif/systemd.nix2
-rw-r--r--hosts/sif/default.nix4
-rw-r--r--hosts/sif/greetd/default.nix123
-rw-r--r--hosts/sif/greetd/wallpaper.pngbin6073128 -> 0 bytes
-rw-r--r--overlays/quickshell/default.nix10
-rw-r--r--overlays/quickshell/greetd-response.patch16
-rw-r--r--overlays/quickshell/lock-state-changed.patch12
-rw-r--r--overlays/quickshell/pipewire.patch460
-rw-r--r--user-profiles/mpv/default.nix1
-rw-r--r--user-profiles/yt-dlp.nix4
39 files changed, 2460 insertions, 343 deletions
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index f2978b6e..c786c629 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -49,7 +49,8 @@ let
49 ]; 49 ];
50 }; 50 };
51 51
52 lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service"; 52 # lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service";
53 lockCommand = "${lib.getExe' cfg.programs.quickshell.package "qs"} ipc call Lockscreen setLocked true";
53 54
54 editor = pkgs.symlinkJoin { 55 editor = pkgs.symlinkJoin {
55 inherit (cfg.services.emacs.package) name; 56 inherit (cfg.services.emacs.package) name;
@@ -374,7 +375,7 @@ in {
374 375
375 services = { 376 services = {
376 wpaperd = { 377 wpaperd = {
377 enable = true; 378 enable = false;
378 settings.default = { 379 settings.default = {
379 path = "~/.wallpapers"; 380 path = "~/.wallpapers";
380 duration = "15m"; 381 duration = "15m";
@@ -594,10 +595,10 @@ in {
594 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass) 595 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass)
595 ]; 596 ];
596 }; 597 };
597 "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = '' 598 # "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = ''
598 [Unit] 599 # [Unit]
599 After=graphical-session.target 600 # After=graphical-session.target
600 ''; 601 # '';
601 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = '' 602 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = ''
602 [Unit] 603 [Unit]
603 Before=graphical-session-pre.target 604 Before=graphical-session-pre.target
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 35a3d799..e1eca4c4 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -231,25 +231,7 @@ in {
231 }; 231 };
232 232
233 config = { 233 config = {
234 systemd.user.services.xwayland-satellite = { 234 home.packages = [ pkgs.xwayland-satellite-unstable ];
235 Unit = {
236 BindsTo = [ "graphical-session.target" ];
237 PartOf = [ "graphical-session.target" ];
238 After = [ "graphical-session.target" ];
239 Requisite = [ "graphical-session.target" ];
240 };
241 Service = {
242 Type = "notify";
243 NotifyAccess = "all";
244 Environment = [ "DISPLAY=:0" ];
245 ExecStart = ''${lib.getExe pkgs.xwayland-satellite-unstable} ''${DISPLAY}'';
246 ExecStartPre = "${systemctl} --user import-environment DISPLAY";
247 StandardOutput = "journal";
248 };
249 Install = {
250 WantedBy = [ "graphical-session.target" ];
251 };
252 };
253 235
254 services.swayidle = { 236 services.swayidle = {
255 events = [ 237 events = [
diff --git a/accounts/gkleen@sif/shell/default.nix b/accounts/gkleen@sif/shell/default.nix
index 26c8bd98..5025dd90 100644
--- a/accounts/gkleen@sif/shell/default.nix
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -4,17 +4,110 @@
4 config = { 4 config = {
5 programs.quickshell = { 5 programs.quickshell = {
6 enable = true; 6 enable = true;
7 package = pkgs.symlinkJoin {
8 pname = pkgs.quickshell.pname + "-wrapped";
9 inherit (pkgs.quickshell) version meta;
10 paths = [ pkgs.quickshell ];
11 buildInputs = [ pkgs.makeWrapper ];
12 postBuild = ''
13 for binary in quickshell qs; do
14 wrapProgram $out/bin/$binary \
15 --prefix QML_IMPORT_PATH : ${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix}
16 done
17 '';
18 };
7 config = { 19 config = {
8 src = ./quickshell; 20 src = ./quickshell;
9 replacements = { 21 replacements = {
10 coreutils = toString pkgs.coreutils;
11 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces); 22 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces);
23 wallpapers = builtins.toJSON (pkgs.stdenvNoCC.mkDerivation {
24 name = "wallpapers";
25 srcs = [
26 (pkgs.fetchurl {
27 url = "https://esawebb.org/media/archives/images/publicationtiff10k/carinanebula3.tif";
28 hash = "sha256-YxZEweDKJfvfrdxb/QFmgJhcZDEJYxotoHrG+RRn1tw=";
29 })
30 (pkgs.fetchurl {
31 url = "https://esawebb.org/media/archives/images/original/pillarsofcreation_composite.tif";
32 hash = "sha256-qRiODxR0lZWdxgYXna0fNRFFDErpBJDwOJuQl6sNjRc=";
33 })
34 (pkgs.fetchurl {
35 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2212a.tif";
36 hash = "sha256-l2fqE/z//C1a0xkvZwsnwPbTSb+WuA11h+SUl3E1dhw=";
37 })
38 (pkgs.fetchurl {
39 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2415a.tif";
40 hash = "sha256-onBy7cPoUpDuzQStbY2E+qmlGgSLXPwFCLX53ukAb4c=";
41 })
42 (pkgs.fetchurl {
43 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2330a.tif";
44 hash = "sha256-nn0ZtjZIrPcpj3YcLTsrL7XiXvyh3QYgCSmdDMD+3OM=";
45 })
46 (pkgs.fetchurl {
47 url = "https://esawebb.org/media/archives/images/original/weic2426a.tif";
48 hash = "sha256-EDnfPn3GE9jt6XPqiGInP7E2F3Az7d25NqATSWltDv0=";
49 })
50 (pkgs.fetchurl {
51 url = "https://esawebb.org/media/archives/images/original/weic2503a.tif";
52 hash = "sha256-3/RX6RQp8naELcgReHPd5/zhJkoCjnA10w5BEnNo+qI=";
53 })
54 (pkgs.fetchurl {
55 url = "https://esawebb.org/media/archives/images/original/weic2506a.tif";
56 hash = "sha256-aDld0aoY1owRxDVf7Jcyw71TH42M1foYotxn2thyFYw=";
57 })
58 (pkgs.fetchurl {
59 url = "https://esawebb.org/media/archives/images/original/weic2514a.tif";
60 hash = "sha256-jTi1G1Ofo5xsF6ggrbtYJHxqLaCQ7edM5B3uORiVQtg=";
61 })
62 (pkgs.fetchurl {
63 url = "https://esawebb.org/media/archives/images/original/weic2425c.tif";
64 hash = "sha256-oaEOexSJHEGj090dJF3ct5HAoR+Y5gRiPrUlxdvnTRo=";
65 })
66 ];
67
68 dontUnpack = true;
69
70 buildInputs = [ pkgs.imagemagick ];
71 buildPhase = ''
72 runHook preBuild
73
74 typeset sources=($srcs)
75
76 mkdir -p $out
77 magick ''${sources[0]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/carinanebula3.jpeg
78 magick ''${sources[1]} -crop 6716x3778+329+80 +repage -define jpeg:extent=10MB $out/pillarsofcreation_composite.jpeg
79 magick ''${sources[2]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/weic2212a.jpeg
80 magick ''${sources[3]} -crop 7650x4302+1166+389 +repage -define jpeg:extent=10MB $out/weic2415a.jpeg
81 magick ''${sources[4]} -crop 8732x4912+0+434 +repage -define jpeg:extent=10MB $out/weic2330a.jpeg
82 magick ''${sources[5]} -crop 5302x2982+636+0 +repage -define jpeg:extent=10MB $out/weic2426a.jpeg
83 magick ''${sources[6]} -crop 4328x2434+0+906 +repage -define jpeg:extent=10MB $out/weic2503a.jpeg
84 magick ''${sources[7]} -crop 4152x2335+0+666 +repage -define jpeg:extent=10MB $out/weic2506a.jpeg
85 magick ''${sources[8]} -crop 4320x2430+0+0 +repage -define jpeg:extent=10MB $out/weic2514a.jpeg
86 magick ''${sources[9]} -crop 5863x3298+0+477 +repage -define jpeg:extent=10MB $out/weic2425c.jpeg
87
88 runHook postBuild
89 '';
90 });
91 niri_session = builtins.toJSON [
92 (pkgs.writeShellScript "niri-session" ''
93 exec ${lib.getExe pkgs.dex} -w ${config.programs.niri.package}/share/wayland-sessions/niri.desktop &>/tmp/niri-session-$$.log
94 '')
95 # (lib.getExe pkgs.dex)
96 # "${config.programs.niri.package}/share/wayland-sessions/niri.desktop"
97 ];
98 username = builtins.toJSON config.home.username;
99 mdi = builtins.toJSON (pkgs.fetchFromGitHub {
100 owner = "Templarian";
101 repo = "MaterialDesign";
102 rev = "2424e748e0cc63ab7b9c095a099b9fe239b737c0";
103 hash = "sha256-QMGl7soAhErrrnY3aKOZpt49yebkSNzy10p/v5OaqQ0=";
104 });
12 }; 105 };
13 }; 106 };
14 }; 107 };
15 systemd.user.services.quickshell = { 108 systemd.user.services.quickshell = {
16 Service = { 109 Service = {
17 Environment = "QML_IMPORT_PATH=${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix}"; 110 RuntimeDirectory = "quickshell";
18 }; 111 };
19 }; 112 };
20 }; 113 };
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
index aa363c4c..a7e88fa7 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
@@ -92,7 +92,7 @@ endfunction()
92cmake_minimum_required(VERSION 3.20) 92cmake_minimum_required(VERSION 3.20)
93project(custom LANGUAGES CXX) 93project(custom LANGUAGES CXX)
94 94
95find_package(Qt6 REQUIRED COMPONENTS Core Qml) 95find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus)
96 96
97qt_standard_project_setup(REQUIRES 6.6) 97qt_standard_project_setup(REQUIRES 6.6)
98 98
@@ -102,15 +102,32 @@ qt6_add_qml_module(customplugin
102 PLUGIN_TARGET customplugin 102 PLUGIN_TARGET customplugin
103) 103)
104 104
105target_sources(customplugin PRIVATE 105set_source_files_properties(org.keepassxc.KeePassXC.MainWindow.xml PROPERTIES
106 Chrono.cpp Chrono.hpp 106 CLASSNAME DBusKeePassXC
107 NO_NAMESPACE TRUE
107) 108)
108 109
110qt_add_dbus_interface(DBUS_INTERFACES
111 org.keepassxc.KeePassXC.MainWindow.xml
112 dbus_keepassxc
113)
114
115include_directories(${CMAKE_SOURCE_DIR}/build)
116
109target_compile_features(customplugin PUBLIC cxx_std_26) 117target_compile_features(customplugin PUBLIC cxx_std_26)
110 118
111target_link_libraries(customplugin PRIVATE 119target_link_libraries(customplugin PRIVATE
112 Qt6::Core 120 Qt6::Core
113 Qt6::Qml 121 Qt6::Qml
122 Qt6::DBus
123)
124
125target_sources(customplugin PRIVATE
126 Chrono.cpp Chrono.hpp
127 FileSelector.cpp FileSelector.hpp
128 KeePassXC.cpp KeePassXC.hpp
129 Systemd.cpp Systemd.hpp
130 ${DBUS_INTERFACES}
114) 131)
115 132
116install_qml_module(customplugin) 133install_qml_module(customplugin)
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
index 929b7be6..22b3469b 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
@@ -42,9 +42,11 @@ void Chrono::update() {
42} 42}
43 43
44void Chrono::setTime(const std::chrono::time_point<std::chrono::system_clock>& targetTime) { 44void Chrono::setTime(const std::chrono::time_point<std::chrono::system_clock>& targetTime) {
45 using namespace std::chrono_literals;
46
45 auto currentTime = std::chrono::system_clock::now(); 47 auto currentTime = std::chrono::system_clock::now();
46 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime); 48 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime);
47 this->currentTime = abs(offset.count()) < 500 ? targetTime : currentTime; 49 this->currentTime = abs(offset) < 500ms ? targetTime : currentTime;
48 50
49 switch (this->mPrecision) { 51 switch (this->mPrecision) {
50 case Chrono::Hours: this->currentTime = std::chrono::time_point_cast<std::chrono::hours>(this->currentTime); 52 case Chrono::Hours: this->currentTime = std::chrono::time_point_cast<std::chrono::hours>(this->currentTime);
@@ -56,33 +58,26 @@ void Chrono::setTime(const std::chrono::time_point<std::chrono::system_clock>& t
56} 58}
57 59
58void Chrono::schedule(const std::chrono::time_point<std::chrono::system_clock>& targetTime) { 60void Chrono::schedule(const std::chrono::time_point<std::chrono::system_clock>& targetTime) {
61 using namespace std::chrono_literals;
62
59 auto currentTime = std::chrono::system_clock::now(); 63 auto currentTime = std::chrono::system_clock::now();
60 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime); 64 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime);
61 auto nextTime = abs(offset.count()) < 500 ? targetTime : currentTime; 65 auto nextTime = abs(offset) < 500ms ? targetTime : currentTime;
62
63 {
64 using namespace std::chrono_literals;
65 66
66 switch (this->mPrecision) { 67 switch (this->mPrecision) {
67 case Chrono::Hours: nextTime = std::chrono::time_point_cast<std::chrono::hours>(nextTime) + 1h; 68 case Chrono::Hours: nextTime = std::chrono::time_point_cast<std::chrono::hours>(nextTime) + 1h;
68 case Chrono::Minutes: nextTime = std::chrono::time_point_cast<std::chrono::minutes>(nextTime) + 1min; 69 case Chrono::Minutes: nextTime = std::chrono::time_point_cast<std::chrono::minutes>(nextTime) + 1min;
69 case Chrono::Seconds: nextTime = std::chrono::time_point_cast<std::chrono::seconds>(nextTime) + 1s; 70 case Chrono::Seconds: nextTime = std::chrono::time_point_cast<std::chrono::seconds>(nextTime) + 1s;
70 }
71 } 71 }
72 72
73 this->targetTime = nextTime; 73 this->targetTime = nextTime;
74 auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime); 74 this->timer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime));
75 this->timer.start(delay);
76} 75}
77 76
78QString Chrono::format() const { return this->mFormat; } 77QString Chrono::format(const QString& fmt) const {
79void Chrono::setFormat(QString format) { 78 return QString::fromStdString(std::format(std::runtime_format(fmt.toStdString()), std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::time_point_cast<std::chrono::seconds>(this->currentTime))));
80 if (format == this->mFormat) return;
81 this->mFormat = format;
82 emit this->formatChanged();
83 this->update();
84} 79}
85 80
86QString Chrono::date() const { 81QDateTime Chrono::date() const {
87 return QString::fromStdString(std::format(std::runtime_format(this->mFormat.toStdString()), std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::time_point_cast<std::chrono::seconds>(this->currentTime)))); 82 return QDateTime::fromStdTimePoint(std::chrono::time_point_cast<std::chrono::milliseconds>(this->currentTime));
88} 83}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
index 788fa88e..04080187 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
@@ -1,17 +1,18 @@
1#pragma once 1#pragma once
2 2
3#include <chrono>
4
5#include <QDateTime>
3#include <QObject> 6#include <QObject>
4#include <QTimer> 7#include <QTimer>
5 8
6#include <qqmlintegration.h> 9#include <qqmlintegration.h>
7#include <chrono>
8 10
9class Chrono : public QObject { 11class Chrono : public QObject {
10 Q_OBJECT; 12 Q_OBJECT;
11 Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); 13 Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
12 Q_PROPERTY(Chrono::Precision precision READ precision WRITE setPrecision NOTIFY precisionChanged); 14 Q_PROPERTY(Chrono::Precision precision READ precision WRITE setPrecision NOTIFY precisionChanged);
13 Q_PROPERTY(QString format READ format WRITE setFormat NOTIFY formatChanged); 15 Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged)
14 Q_PROPERTY(QString date READ date NOTIFY dateChanged);
15 QML_ELEMENT; 16 QML_ELEMENT;
16 17
17public: 18public:
@@ -30,15 +31,13 @@ public:
30 Chrono::Precision precision() const; 31 Chrono::Precision precision() const;
31 void setPrecision(Chrono::Precision precision); 32 void setPrecision(Chrono::Precision precision);
32 33
33 QString format() const; 34 Q_INVOKABLE QString format(const QString& fmt) const;
34 void setFormat (QString format);
35 35
36 QString date() const; 36 QDateTime date() const;
37 37
38signals: 38signals:
39 void enabledChanged(); 39 void enabledChanged();
40 void precisionChanged(); 40 void precisionChanged();
41 void formatChanged();
42 void dateChanged(); 41 void dateChanged();
43 42
44private slots: 43private slots:
@@ -47,7 +46,6 @@ private slots:
47private: 46private:
48 bool mEnabled = true; 47 bool mEnabled = true;
49 Chrono::Precision mPrecision = Chrono::Seconds; 48 Chrono::Precision mPrecision = Chrono::Seconds;
50 QString mFormat = "{:%c}";
51 QTimer timer; 49 QTimer timer;
52 std::chrono::time_point<std::chrono::system_clock> currentTime, targetTime; 50 std::chrono::time_point<std::chrono::system_clock> currentTime, targetTime;
53 51
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp
new file mode 100644
index 00000000..d7051d2a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp
@@ -0,0 +1,102 @@
1#include "FileSelector.hpp"
2
3#include <sstream>
4#include <vector>
5#include <random>
6#include <algorithm>
7
8#include <iostream>
9#include <format>
10
11namespace fs = std::filesystem;
12
13FileSelector::FileSelector(QObject* parent): QObject(parent) {
14 QObject::connect(&this->timer, &QTimer::timeout, this, &FileSelector::onTimeout);
15 this->timer.setTimerType(Qt::PreciseTimer);
16}
17
18QString FileSelector::directory() const {
19 return QString::fromStdString(this->mDirectory->string());
20}
21void FileSelector::setDirectory(QString directory) {
22 this->mDirectory = directory.toStdString();
23 if (this->mDirectory && this->mEpoch)
24 this->update();
25 emit this->directoryChanged();
26}
27
28unsigned int FileSelector::epoch() const {
29 return std::chrono::duration_cast<std::chrono::milliseconds>(*this->mEpoch).count();
30}
31void FileSelector::setEpoch(unsigned int epoch) {
32 this->mEpoch = std::chrono::milliseconds{epoch};
33 if (this->mDirectory && this->mEpoch)
34 this->update();
35 emit this->epochChanged();
36}
37
38QString FileSelector::seed() const {
39 return this->mSeed;
40}
41void FileSelector::setSeed(QString seed) {
42 this->mSeed = seed;
43 emit this->seedChanged();
44 if (this->mDirectory && this->mEpoch)
45 emit this->selectedChanged();
46}
47
48QString FileSelector::selected() const {
49 if (!this->mDirectory || !this->mEpoch)
50 return QString();
51
52 std::vector<fs::path> shuffled(this->mFiles.begin(), this->mFiles.end());
53 std::sort(shuffled.begin(), shuffled.end());
54
55 auto currentTime = std::chrono::system_clock::now();
56 uint64_t currentEpoch = currentTime.time_since_epoch() / *this->mEpoch;
57 std::chrono::milliseconds timeInEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime.time_since_epoch()) % *this->mEpoch;
58
59 std::ostringstream seed;
60 seed << this->mSeed.size() << ";" << this->mSeed.toStdString() << ";";
61 seed << *this->mEpoch << ";";
62 seed << currentEpoch << ";";
63 seed << this->mDirectory->string().size() << ";" << *this->mDirectory << ";";
64 seed << this->mFiles.size() << ";";
65 for (const fs::path& p: this->mFiles)
66 seed << p.string().size() << ";" << p << ";";
67
68 std::vector<std::seed_seq::result_type> v;
69 v.reserve(seed.str().size());
70 for (const char& c: seed.str())
71 v.push_back(c);
72
73 std::seed_seq engine_seed(v.begin(), v.end());
74 std::mt19937 g(engine_seed);
75 std::shuffle(shuffled.begin(), shuffled.end(), g);
76
77 std::vector<fs::path>::size_type ix = shuffled.size() * timeInEpoch / *this->mEpoch;
78 return QString::fromStdString((*this->mDirectory / shuffled[ix]).string());
79}
80
81void FileSelector::onTimeout() {
82 if (!this->mFiles.size())
83 return;
84
85 auto currentTime = std::chrono::system_clock::now();
86 uint64_t currentMinorEpoch = currentTime.time_since_epoch() / (*this->mEpoch / this->mFiles.size());
87 auto nextTime = std::chrono::time_point<std::chrono::system_clock>((currentMinorEpoch + 1) * (*this->mEpoch / this->mFiles.size()));
88 this->timer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime));
89
90 emit this->selectedChanged();
91}
92
93void FileSelector::update() {
94 this->mFiles = std::set<fs::path>{};
95 for (const fs::directory_entry& entry:
96 fs::recursive_directory_iterator(*this->mDirectory, fs::directory_options::follow_directory_symlink))
97 {
98 this->mFiles.insert(fs::relative(entry, *this->mDirectory));
99 }
100
101 this->onTimeout();
102}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp
new file mode 100644
index 00000000..72c4f2a7
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp
@@ -0,0 +1,52 @@
1#pragma once
2
3#include <filesystem>
4#include <chrono>
5#include <set>
6#include <optional>
7
8#include <QObject>
9#include <QTimer>
10
11#include <qqmlintegration.h>
12
13class FileSelector : public QObject {
14 Q_OBJECT;
15 Q_PROPERTY(QString directory READ directory WRITE setDirectory NOTIFY directoryChanged REQUIRED);
16 Q_PROPERTY(unsigned int epoch READ epoch WRITE setEpoch NOTIFY epochChanged REQUIRED);
17 Q_PROPERTY(QString seed READ seed WRITE setSeed NOTIFY seedChanged);
18 Q_PROPERTY(QString selected READ selected NOTIFY selectedChanged);
19 QML_ELEMENT;
20
21public:
22 explicit FileSelector(QObject* parent = nullptr);
23
24 QString directory() const;
25 void setDirectory(QString directory);
26
27 unsigned int epoch() const;
28 void setEpoch(unsigned int epoch);
29
30 QString seed() const;
31 void setSeed(QString seed);
32
33 QString selected() const;
34
35signals:
36 void directoryChanged();
37 void epochChanged();
38 void seedChanged();
39 void selectedChanged();
40
41private slots:
42 void onTimeout();
43
44private:
45 std::optional<std::filesystem::path> mDirectory;
46 std::optional<std::chrono::milliseconds> mEpoch;
47 std::set<std::filesystem::path> mFiles;
48 QString mSeed;
49 QTimer timer;
50
51 void update();
52};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
new file mode 100644
index 00000000..f6e4dd6e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
@@ -0,0 +1,18 @@
1#include "KeePassXC.hpp"
2
3#include <QDBusConnection>
4
5KeePassXC::KeePassXC() {
6 this->service = new DBusKeePassXC(DBusKeePassXC::staticInterfaceName(), "/keepassxc", QDBusConnection::sessionBus(), this);
7}
8KeePassXC::~KeePassXC() {
9 if (this->service)
10 delete this->service;
11}
12
13void KeePassXC::lockAllDatabases() {
14 if (!this->service)
15 return;
16
17 this->service->lockAllDatabases();
18}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
new file mode 100644
index 00000000..c4cd71e0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
@@ -0,0 +1,21 @@
1#pragma once
2
3#include "dbus_keepassxc.h"
4
5#include <QObject>
6#include <QtQmlIntegration/qqmlintegration.h>
7
8class KeePassXC : public QObject {
9 Q_OBJECT;
10 QML_SINGLETON;
11 QML_ELEMENT;
12
13public:
14 explicit KeePassXC();
15 ~KeePassXC();
16
17 Q_INVOKABLE void lockAllDatabases();
18
19private:
20 DBusKeePassXC* service = nullptr;
21};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
new file mode 100644
index 00000000..9ccd8ba0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
@@ -0,0 +1,16 @@
1#include "Systemd.hpp"
2
3#include <QDBusConnection>
4#include <QDBusMessage>
5
6void Systemd::stopUserUnit(const QString& unit, const QString& mode) {
7 QDBusMessage m = QDBusMessage::createMethodCall(
8 "org.freedesktop.systemd1",
9 "/org/freedesktop/systemd1",
10 "org.freedesktop.systemd1.Manager",
11 "StopUnit"
12 );
13 m << unit;
14 m << mode;
15 QDBusConnection::sessionBus().send(m);
16}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp
new file mode 100644
index 00000000..883a96f3
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp
@@ -0,0 +1,13 @@
1#pragma once
2
3#include <QObject>
4#include <QtQmlIntegration/qqmlintegration.h>
5
6class Systemd : public QObject {
7 Q_OBJECT;
8 QML_SINGLETON;
9 QML_ELEMENT;
10
11public:
12 Q_INVOKABLE void stopUserUnit(const QString& unit, const QString& mode);
13};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/default.nix b/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
index fafea90e..33b76f61 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
@@ -3,11 +3,19 @@
3, cmake 3, cmake
4, qt6 4, qt6
5, fmt 5, fmt
6, keepassxc
7, systemd
6}: 8}:
9
7stdenv.mkDerivation rec { 10stdenv.mkDerivation rec {
8 name = "quickshell-custom"; 11 name = "quickshell-custom";
9 12
10 src = ./.; 13 src = ./.;
14
15 prePatch = ''
16 cp ${keepassxc.src}/src/gui/org.keepassxc.KeePassXC.MainWindow.xml .
17 '';
18
11 nativeBuildInputs = [ cmake qt6.wrapQtAppsHook ]; 19 nativeBuildInputs = [ cmake qt6.wrapQtAppsHook ];
12 buildInputs = [ 20 buildInputs = [
13 qt6.qtbase 21 qt6.qtbase
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index aab1607f..3652af54 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -7,8 +7,6 @@ PanelWindow {
7 7
8 required property var screen 8 required property var screen
9 9
10 property var calendarMouseArea: clock.calendarMouseArea
11
12 anchors { 10 anchors {
13 top: true 11 top: true
14 left: true 12 left: true
@@ -66,6 +64,13 @@ PanelWindow {
66 anchors.verticalCenter: parent.verticalCenter 64 anchors.verticalCenter: parent.verticalCenter
67 spacing: 0 65 spacing: 0
68 66
67 PipewireWidget {}
68
69 Item {
70 height: parent.height
71 width: 4
72 }
73
69 SystemTray {} 74 SystemTray {}
70 75
71 Item { 76 Item {
@@ -80,8 +85,6 @@ PanelWindow {
80 width: 4 85 width: 4
81 } 86 }
82 87
83 Clock { 88 Clock {}
84 id: clock
85 }
86 } 89 }
87} \ No newline at end of file 90} \ No newline at end of file
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
index 58600adb..4644d5e7 100644
--- a/accounts/gkleen@sif/shell/quickshell/Clock.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -7,240 +7,271 @@ import QtQuick.Layouts
7import Quickshell.Widgets 7import Quickshell.Widgets
8 8
9Item { 9Item {
10 id: clockItem
11
12 property bool calendarPopup: true
13
10 width: clock.contentWidth 14 width: clock.contentWidth
11 height: parent.height 15 height: parent.height
12 anchors.verticalCenter: parent.verticalCenter 16 anchors.verticalCenter: parent.verticalCenter
13 17
14 MouseArea { 18 WrapperMouseArea {
15 id: clockMouseArea 19 id: clockMouseArea
16 20
17 anchors.fill: parent 21 anchors.fill: parent
18 hoverEnabled: true 22 hoverEnabled: true
19 enabled: true 23 enabled: clockItem.calendarPopup
20 24
21 property real angleRem: 0 25 Item {
22 property real sensitivity: 1 / 120 26 anchors.fill: parent
23 27
24 function scrollYear(event) { 28 Text {
25 angleRem += event.angleDelta.y; 29 id: clock
26 const d = Math.round(angleRem * sensitivity); 30 color: "white"
27 yearCalendar.year += d;
28 angleRem -= d / sensitivity;
29 }
30 31
31 onWheel: event => scrollYear(event) 32 anchors.verticalCenter: parent.verticalCenter
32 }
33 33
34 Text { 34 Custom.Chrono {
35 id: clock 35 id: chrono
36 color: "white"
37 36
38 anchors.verticalCenter: parent.verticalCenter 37 onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}")
38 }
39 39
40 Custom.Chrono { 40 font.pointSize: 10
41 id: chrono 41 font.family: "Fira Sans"
42 format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}" 42 font.features: { "tnum": 1 }
43 }
43 } 44 }
44
45 text: chrono.date
46
47 font.pointSize: 10
48 font.family: "Fira Sans"
49 font.features: { "tnum": 1 }
50 } 45 }
51 46
52 PopupWindow { 47 Loader {
53 id: tooltip 48 id: tooltipLoader
54 49
55 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse 50 active: false
56 51
57 anchor { 52 Connections {
58 item: clockMouseArea 53 target: clockMouseArea
59 edges: Edges.Bottom | Edges.Left 54 function onContainsMouseChanged() {
55 if (clockMouseArea.containsMouse)
56 tooltipLoader.active = true;
57 }
60 } 58 }
61 visible: false
62 59
63 onNextVisibleChanged: hangTimer.restart() 60 sourceComponent: PopupWindow {
61 id: tooltip
64 62
65 Timer { 63 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse
66 id: hangTimer
67 interval: 100
68 onTriggered: tooltip.visible = tooltip.nextVisible
69 }
70 64
71 implicitWidth: clockTooltipContent.width 65 anchor {
72 implicitHeight: clockTooltipContent.height 66 item: clockMouseArea
73 color: "black" 67 edges: Edges.Bottom | Edges.Left
68 }
69 visible: false
74 70
75 onVisibleChanged: { 71 onNextVisibleChanged: hangTimer.restart()
76 const d = new Date();
77 yearCalendar.year = d.getFullYear();
78 clockMouseArea.angleRem = 0;
79 }
80 72
81 WrapperMouseArea { 73 Timer {
82 id: tooltipMouseArea 74 id: hangTimer
75 interval: 100
76 onTriggered: tooltip.visible = tooltip.nextVisible
77 }
83 78
84 hoverEnabled: true 79 implicitWidth: clockTooltipContent.width
85 enabled: true 80 implicitHeight: clockTooltipContent.height
81 color: "black"
86 82
87 onWheel: event => clockMouseArea.scrollYear(event) 83 onVisibleChanged: {
84 yearCalendar.year = chrono.date.getFullYear();
85 yearCalendar.angleRem = 0;
86 }
88 87
89 anchors.fill: parent 88 WrapperMouseArea {
89 id: tooltipMouseArea
90 90
91 WrapperItem { 91 hoverEnabled: true
92 id: clockTooltipContent 92 enabled: true
93 93
94 margin: 8 94 onWheel: event => yearCalendar.scrollYear(event)
95 leftMargin: 0
96 95
97 ColumnLayout { 96 anchors.fill: parent
98 Text {
99 id: yearLabel
100 97
101 horizontalAlignment: Text.AlignHCenter 98 WrapperItem {
99 id: clockTooltipContent
102 100
103 font.pointSize: 14 101 margin: 8
104 font.family: "Fira Sans"
105 font.features: { "tnum": 1 }
106 color: "white"
107 102
108 text: yearCalendar.year 103 ColumnLayout {
104 anchors.centerIn: parent
109 105
110 Layout.fillWidth: true 106 Text {
111 Layout.bottomMargin: 8 107 id: yearLabel
112 } 108
109 horizontalAlignment: Text.AlignHCenter
113 110
114 GridLayout { 111 font.pointSize: 14
115 property int year: { const d = new Date(); return d.getFullYear(); } 112 font.family: "Fira Sans"
113 font.features: { "tnum": 1 }
114 color: "white"
116 115
117 id: yearCalendar 116 text: yearCalendar.year
118 117
119 columns: 3 118 Layout.fillWidth: true
120 columnSpacing: 16 119 Layout.bottomMargin: 8
121 rowSpacing: 16 120 }
122 121
123 Layout.alignment: Qt.AlignHCenter 122 GridLayout {
124 Layout.fillWidth: false 123 property int year: chrono.date.getFullYear()
125 124
126 Repeater { 125 id: yearCalendar
127 model: 12
128 126
129 GridLayout { 127 columns: 3
130 columns: 2 128 columnSpacing: 16
129 rowSpacing: 16
131 130
132 required property int index 131 Layout.alignment: Qt.AlignHCenter
133 property int month: index 132 Layout.fillWidth: false
134 133
135 id: monthCalendar 134 property real angleRem: 0
135 property real sensitivity: 1 / 120
136 136
137 Layout.alignment: Qt.AlignTop | Qt.AlignRight 137 function scrollYear(event) {
138 Layout.fillWidth: false 138 angleRem += event.angleDelta.y;
139 const d = Math.round(angleRem * sensitivity);
140 yearCalendar.year += d;
141 angleRem -= d / sensitivity;
142 }
139 143
140 Text { 144 Connections {
141 Layout.column: 1 145 target: clockMouseArea
142 Layout.fillWidth: true 146 function onWheel(event) { yearCalendar.scrollYear(event); }
147 }
143 148
144 horizontalAlignment: Text.AlignHCenter 149 Repeater {
150 model: 12
145 151
146 font.pointSize: 10 152 GridLayout {
147 font.family: "Fira Sans" 153 columns: 2
148 154
149 text: { 155 required property int index
150 const date = Date.fromLocaleDateString(Qt.locale(), `${yearCalendar.year}-${monthCalendar.month + 1}-01`, "yyyy-M-dd"); 156 property int month: index
151 return date.toLocaleString(Qt.locale("en_DK"), "MMMM")
152 }
153 157
154 color: "#ffead3" 158 id: monthCalendar
155 }
156 159
157 DayOfWeekRow { 160 Layout.alignment: Qt.AlignTop | Qt.AlignRight
158 locale: grid.locale 161 Layout.fillWidth: false
159 162
160 Layout.row: 1 163 Text {
161 Layout.column: 1 164 Layout.column: 1
162 Layout.fillWidth: true 165 Layout.fillWidth: true
163 166
164 delegate: Text { 167 horizontalAlignment: Text.AlignHCenter
165 required property string shortName
166 168
167 font.pointSize: 10 169 font.pointSize: 10
168 font.family: "Fira Mono" 170 font.family: "Fira Sans"
169 171
170 text: shortName 172 text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM")
171 color: "#ffcc66"
172 173
173 horizontalAlignment: Text.AlignRight 174 color: "#ffead3"
174 verticalAlignment: Text.AlignVCenter
175 } 175 }
176 }
177 176
178 WeekNumberColumn { 177 DayOfWeekRow {
179 month: grid.month 178 locale: grid.locale
180 year: grid.year
181 locale: grid.locale
182 179
183 Layout.fillHeight: true 180 Layout.row: 1
181 Layout.column: 1
182 Layout.fillWidth: true
184 183
185 delegate: Text { 184 delegate: WrapperItem {
186 required property int weekNumber 185 required property string shortName
187 186
188 opacity: { 187 width: dowLabel.contentWidth + 6
189 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
190 const dayOfWeek = simple.getDay();
191 const isoWeekStart = simple;
192 188
193 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); 189 Text {
194 if (dayOfWeek > 4) { 190 id: dowLabel
195 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
196 }
197
198 for (let i = 0; i < 7; i++) {
199 const dayInWeek = new Date(isoWeekStart);
200 dayInWeek.setDate(dayInWeek.getDate() + i);
201 if (dayInWeek.getMonth() == monthCalendar.month)
202 return 1;
203 }
204 191
205 return 0; 192 anchors.fill: parent
206 }
207 193
208 font.pointSize: 10 194 font.pointSize: 10
209 font.family: "Fira Sans" 195 font.family: "Fira Sans"
210 font.features: { "tnum": 1 }
211 196
212 text: weekNumber 197 text: parent.shortName
213 color: "#99ffdd" 198 color: "#ffcc66"
214 199
215 horizontalAlignment: Text.AlignRight 200 horizontalAlignment: Text.AlignHCenter
216 verticalAlignment: Text.AlignVCenter 201 verticalAlignment: Text.AlignVCenter
202 }
203 }
217 } 204 }
218 }
219 205
220 MonthGrid { 206 WeekNumberColumn {
221 id: grid 207 month: grid.month
208 year: grid.year
209 locale: grid.locale
222 210
223 year: yearCalendar.year 211 Layout.fillHeight: true
224 month: monthCalendar.month
225 locale: Qt.locale("en_DK")
226 212
227 Layout.fillWidth: true 213 delegate: Text {
228 Layout.fillHeight: true 214 required property int weekNumber
229 215
230 delegate: Text { 216 opacity: {
231 required property var model 217 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
218 const dayOfWeek = simple.getDay();
219 const isoWeekStart = simple;
232 220
233 opacity: model.month === monthCalendar.month ? 1 : 0 221 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1);
222 if (dayOfWeek > 4) {
223 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
224 }
225
226 for (let i = 0; i < 7; i++) {
227 const dayInWeek = new Date(isoWeekStart);
228 dayInWeek.setDate(dayInWeek.getDate() + i);
229 if (dayInWeek.getMonth() == monthCalendar.month)
230 return 1;
231 }
232
233 return 0;
234 }
234 235
235 font.pointSize: 10 236 font.pointSize: 10
236 font.family: "Fira Sans" 237 font.family: "Fira Sans"
237 font.features: { "tnum": 1 } 238 font.features: { "tnum": 1 }
238 239
239 text: model.day 240 text: weekNumber
240 color: model.today ? "#ff6699" : "white" 241 color: "#99ffdd"
241 242
242 horizontalAlignment: Text.AlignRight 243 horizontalAlignment: Text.AlignRight
243 verticalAlignment: Text.AlignVCenter 244 verticalAlignment: Text.AlignVCenter
245 }
246 }
247
248 MonthGrid {
249 id: grid
250
251 year: yearCalendar.year
252 month: monthCalendar.month
253 locale: Qt.locale("en_DK")
254
255 Layout.fillWidth: true
256 Layout.fillHeight: true
257
258 delegate: Text {
259 required property var model
260
261 opacity: model.month === monthCalendar.month ? 1 : 0
262
263 font.pointSize: 10
264 font.family: "Fira Sans"
265 font.features: { "tnum": 1 }
266
267 property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day
268
269 text: model.day
270 color: today ? "#ff6699" : "white"
271
272 horizontalAlignment: Text.AlignRight
273 verticalAlignment: Text.AlignVCenter
274 }
244 } 275 }
245 } 276 }
246 } 277 }
@@ -250,4 +281,4 @@ Item {
250 } 281 }
251 } 282 }
252 } 283 }
253} \ No newline at end of file 284}
diff --git a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
index b9f91580..bc3750f9 100644
--- a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
+++ b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
@@ -3,25 +3,16 @@ import QtQuick
3import qs.Services 3import qs.Services
4import Quickshell.Widgets 4import Quickshell.Widgets
5 5
6Rectangle { 6Item {
7 id: kbdWidget
8
9 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" }
10
11 width: kbdLabel.contentWidth + 8 7 width: kbdLabel.contentWidth + 8
12 color: {
13 if (kbdMouseArea.containsMouse) {
14 return "#33808080";
15 }
16 return "transparent";
17 }
18 height: parent.height 8 height: parent.height
19 anchors.verticalCenter: parent.verticalCenter 9 anchors.verticalCenter: parent.verticalCenter
20 10
21 MouseArea { 11 WrapperMouseArea {
22 id: kbdMouseArea 12 id: kbdMouseArea
23 13
24 anchors.fill: parent 14 anchors.fill: parent
15
25 hoverEnabled: true 16 hoverEnabled: true
26 cursorShape: Qt.PointingHandCursor 17 cursorShape: Qt.PointingHandCursor
27 enabled: true 18 enabled: true
@@ -31,25 +22,39 @@ Rectangle {
31 onWheel: event => { 22 onWheel: event => {
32 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {}) 23 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {})
33 } 24 }
34 }
35 25
36 Text { 26 Rectangle {
37 id: kbdLabel 27 id: kbdWidget
38 28
39 font.pointSize: 10 29 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" }
40 font.family: "Fira Sans" 30
41 color: { 31 anchors.fill: parent
42 if (NiriService.keyboardLayouts?.current_idx === 0) 32 color: {
43 return "#555"; 33 if (kbdMouseArea.containsMouse) {
44 return "white"; 34 return "#33808080";
45 } 35 }
46 anchors.centerIn: parent 36 return "transparent";
37 }
47 38
48 text: { 39 Text {
49 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; 40 id: kbdLabel
50 if (!currentLayout) 41
51 return ""; 42 font.pointSize: 10
52 return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; 43 font.family: "Fira Sans"
44 color: {
45 if (NiriService.keyboardLayouts?.current_idx === 0)
46 return "#555";
47 return "white";
48 }
49 anchors.centerIn: parent
50
51 text: {
52 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx];
53 if (!currentLayout)
54 return "";
55 return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout;
56 }
57 }
53 } 58 }
54 } 59 }
55 60
diff --git a/accounts/gkleen@sif/shell/quickshell/LockSurface.qml b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml
new file mode 100644
index 00000000..18698725
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml
@@ -0,0 +1,223 @@
1import Quickshell.Widgets
2import QtQuick.Effects
3import QtQuick.Layouts
4import QtQuick
5import QtQuick.Controls
6import QtQuick.Controls.Fusion
7import qs.Services
8import QtQml
9
10Item {
11 id: lockSurface
12
13 property var screen
14 property list<var> messages: []
15 property bool responseRequired: false
16 property bool responseVisible: false
17 property string currentText: ""
18 property bool authRunning: false
19
20 signal response(string responseText)
21
22 anchors.fill: parent
23
24 Item {
25 id: background
26
27 anchors.fill: parent
28
29 property Img current: one
30 property string source: selector.selected
31
32 WallpaperSelector {
33 id: selector
34 seed: lockSurface.screen?.name || ""
35 }
36
37 onSourceChanged: {
38 if (!source)
39 current = null;
40 else if (current === one)
41 two.update()
42 else
43 one.update()
44 }
45
46 Img { id: one }
47 Img { id: two }
48
49 component Img: Item {
50 id: img
51
52 property string source
53
54 function update() {
55 source = background.source || ""
56 }
57
58 anchors.fill: parent
59
60 Image {
61 id: imageSource
62
63 source: img.source
64 sourceSize: Qt.size(parent.width, parent.height)
65 fillMode: Image.PreserveAspectCrop
66 smooth: true
67 visible: false
68 asynchronous: true
69 cache: false
70
71 onStatusChanged: {
72 if (status === Image.Ready) {
73 background.current = img
74 }
75 }
76 }
77
78 MultiEffect {
79 id: imageEffect
80
81 source: imageSource
82 anchors.fill: parent
83 blurEnabled: true
84 blur: 1
85 blurMax: 64
86 blurMultiplier: 2
87
88 opacity: 0
89
90 states: State {
91 name: "visible"
92 when: background.current === img
93
94 PropertyChanges {
95 imageEffect.opacity: 1
96 }
97 StateChangeScript {
98 name: "unloadOther"
99 script: {
100 if (img === one)
101 two.source = ""
102 if (img === two)
103 one.source = ""
104 }
105 }
106 }
107
108 transitions: Transition {
109 SequentialAnimation {
110 NumberAnimation {
111 target: imageEffect
112 properties: "opacity"
113 duration: 5000
114 easing.type: Easing.OutCubic
115 }
116 ScriptAction {
117 scriptName: "unloadOther"
118 }
119 }
120 }
121 }
122 }
123 }
124
125 Item {
126 anchors {
127 top: lockSurface.top
128 left: lockSurface.left
129 right: lockSurface.right
130 }
131
132 implicitWidth: lockSurface.width
133 implicitHeight: 21
134
135 Rectangle {
136 anchors.fill: parent
137 color: Qt.rgba(0, 0, 0, 0.75)
138 }
139
140 Clock {
141 anchors.centerIn: parent
142 calendarPopup: false
143 }
144 }
145
146 WrapperRectangle {
147 id: unlockUi
148
149 Keys.onPressed: event => {
150 if (!lockSurface.authRunning) {
151 event.accepted = true;
152 lockSurface.authRunning = true;
153 }
154 }
155 focus: !passwordBox.visible
156
157 visible: lockSurface.authRunning
158
159 color: Qt.rgba(0, 0, 0, 0.75)
160 margin: 8
161
162 anchors.centerIn: parent
163
164 ColumnLayout {
165 spacing: 4
166
167 BusyIndicator {
168 visible: running
169 running: !Array.from(lockSurface.messages).length && !lockSurface.responseRequired
170 }
171
172 Repeater {
173 model: lockSurface.messages
174
175 Text {
176 required property var modelData
177
178 font.pointSize: 10
179 font.family: "Fira Sans"
180 color: modelData.error ? "#f28a21" : "#ffffff"
181
182 text: String(modelData.text).trim()
183
184 Layout.fillWidth: true
185 horizontalAlignment: Text.AlignHCenter
186 }
187 }
188
189 TextField {
190 id: passwordBox
191
192 visible: lockSurface.responseRequired
193 echoMode: lockSurface.responseVisible ? TextInput.Normal : TextInput.Password
194 inputMethodHints: Qt.ImhSensitiveData
195
196 onTextChanged: lockSurface.currentText = passwordBox.text
197 onAccepted: {
198 passwordBox.readOnly = true;
199 lockSurface.response(lockSurface.currentText);
200 }
201
202 Connections {
203 target: lockSurface
204 function onCurrentTextChanged() {
205 passwordBox.text = lockSurface.currentText
206 }
207 }
208 Connections {
209 target: lockSurface
210 function onResponseRequiredChanged() {
211 if (lockSurface.responseRequired)
212 passwordBox.readOnly = false;
213 passwordBox.focus = true;
214 passwordBox.selectAll();
215 }
216 }
217
218 Layout.topMargin: 4
219 Layout.fillWidth: true
220 }
221 }
222 }
223}
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
new file mode 100644
index 00000000..8e739359
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -0,0 +1,90 @@
1import Quickshell
2import Quickshell.Wayland
3import Quickshell.Io
4import Quickshell.Services.Pam
5import Quickshell.Services.Mpris
6import Custom as Custom
7import qs.Services
8import QtQml
9
10Scope {
11 id: lockscreen
12
13 property string currentText: ""
14
15 PamContext {
16 id: pam
17
18 property list<var> messages: []
19
20 config: "quickshell"
21 onCompleted: result => {
22 if (result === PamResult.Success) {
23 lock.locked = false;
24 }
25 }
26 onPamMessage: {
27 messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }])
28 }
29 onActiveChanged: {
30 messages = [];
31 }
32 }
33
34 IpcHandler {
35 target: "Lockscreen"
36
37 function setLocked(locked: bool): void { lock.locked = locked; }
38 function getLocked(): bool { return lock.locked; }
39 }
40
41 WlSessionLock {
42 id: lock
43
44 onLockStateChanged: {
45 if (!locked && pam.active)
46 pam.abort();
47
48 if (locked) {
49 Custom.KeePassXC.lockAllDatabases();
50 Array.from(Mpris.players.values).forEach(player => {
51 if (player.canPause && player.isPlaying)
52 player.pause();
53 });
54 // Custom.Systemd.stopUserUnit("gpg-agent.service", "replace");
55 GpgAgent.reloadAgent();
56 }
57 }
58
59 WlSessionLockSurface {
60 id: lockSurface
61
62 color: "black"
63
64 LockSurface {
65 id: surfaceContent
66
67 onResponse: responseText => pam.respond(responseText)
68 onAuthRunningChanged: {
69 if (authRunning)
70 pam.start();
71 }
72 Connections {
73 target: pam
74 function onMessagesChanged() { surfaceContent.messages = pam.messages; }
75 function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; }
76 function onActiveChanged() { surfaceContent.authRunning = pam.active; }
77 }
78 onCurrentTextChanged: lockscreen.currentText = currentText
79 Connections {
80 target: lockscreen
81 function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; }
82 }
83 Connections {
84 target: lockSurface
85 function onScreenChanged() { surfaceContent.screen = lockSurface.screen; }
86 }
87 }
88 }
89 }
90}
diff --git a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
new file mode 100644
index 00000000..387dcc8b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
@@ -0,0 +1,24 @@
1import QtQuick
2import QtQuick.Effects
3
4Item {
5 id: icon
6
7 required property string icon
8 property color color: "white"
9
10 Image {
11 id: sourceImage
12 source: "file://" + @mdi@ + "/svg/" + icon.icon + ".svg"
13 anchors.fill: parent
14
15 layer.enabled: true
16 layer.effect: MultiEffect {
17 id: effect
18
19 brightness: 1
20 colorization: 1
21 colorizationColor: icon.color
22 }
23 }
24}
diff --git a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
new file mode 100644
index 00000000..7fdb5006
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -0,0 +1,354 @@
1import QtQuick
2import QtQuick.Layouts
3import QtQuick.Controls.Fusion
4import Quickshell
5import Quickshell.Services.Pipewire
6import Quickshell.Widgets
7
8Item {
9 height: parent.height
10 width: volumeIcon.width + 8
11 anchors.verticalCenter: parent.verticalCenter
12
13 PwObjectTracker {
14 objects: [Pipewire.defaultAudioSink]
15 }
16
17 WrapperMouseArea {
18 id: widgetMouseArea
19
20 anchors.fill: parent
21 hoverEnabled: true
22 cursorShape: Qt.PointingHandCursor
23
24 onClicked: {
25 if (!Pipewire.defaultAudioSink)
26 return;
27 Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
28 }
29
30 property real sensitivity: (1 / 40) / 120
31 onWheel: event => {
32 if (!Pipewire.defaultAudioSink)
33 return;
34 Pipewire.defaultAudioSink.audio.volume += event.angleDelta.y * sensitivity;
35 }
36
37 Rectangle {
38 id: volumeWidget
39
40 anchors.fill: parent
41 color: {
42 if (widgetMouseArea.containsMouse)
43 return "#33808080";
44 return "transparent";
45 }
46
47 Item {
48 anchors.fill: parent
49
50 MaterialDesignIcon {
51 id: volumeIcon
52
53 width: 16
54 height: 16
55 anchors.centerIn: parent
56
57 icon: {
58 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
59 return "volume-off";
60 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
61 return "volume-low";
62 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
63 return "volume-medium";
64 return "volume-high";
65 }
66 color: "#555"
67 }
68 }
69 }
70 }
71
72 Loader {
73 id: tooltipLoader
74
75 active: false
76
77 Connections {
78 target: widgetMouseArea
79 function onContainsMouseChanged() {
80 if (widgetMouseArea.containsMouse)
81 tooltipLoader.active = true;
82 }
83 }
84
85 PwObjectTracker {
86 objects: Pipewire.devices
87 }
88 PwObjectTracker {
89 objects: Pipewire.nodes
90 }
91
92 sourceComponent: PopupWindow {
93 id: tooltip
94
95 property bool openPopup: false
96 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup
97
98 anchor {
99 item: widgetMouseArea
100 edges: Edges.Bottom | Edges.Left
101 }
102 visible: false
103
104 onNextVisibleChanged: hangTimer.restart()
105
106 Timer {
107 id: hangTimer
108 interval: 100
109 onTriggered: {
110 tooltip.visible = tooltip.nextVisible;
111 if (!tooltip.visible)
112 tooltipLoader.active = false;
113 }
114 }
115
116 implicitWidth: tooltipContent.width
117 implicitHeight: tooltipContent.height
118 color: "black"
119
120 WrapperMouseArea {
121 id: tooltipMouseArea
122
123 hoverEnabled: true
124 enabled: true
125
126 anchors.fill: parent
127
128 WrapperItem {
129 id: tooltipContent
130
131 margin: 8
132 bottomMargin: 8 + Math.max(0, 200 - tooltipLayout.implicitHeight)
133
134 GridLayout {
135 id: tooltipLayout
136
137 columns: 4
138
139 Repeater {
140 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
141
142 Item {
143 id: descItem
144
145 required property var modelData
146 required property int index
147
148 Layout.column: 0
149 Layout.row: index
150
151 implicitWidth: descText.contentWidth
152 implicitHeight: descText.contentHeight
153
154 Text {
155 id: descText
156
157 color: "white"
158 font.pointSize: 10
159 font.family: "Fira Sans"
160
161 text: descItem.modelData.description
162 }
163 }
164 }
165
166 Repeater {
167 id: defaultSinkRepeater
168
169 model: {
170 Array.from(Pipewire.devices.values)
171 .filter(dev => dev.type == "Audio/Device")
172 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
173 }
174
175 Item {
176 id: defaultSinkItem
177
178 required property var modelData
179 required property int index
180
181 PwObjectTracker {
182 objects: [defaultSinkItem.modelData]
183 }
184
185 Layout.column: 1
186 Layout.row: index
187
188 Layout.fillHeight: true
189
190 implicitWidth: 16 + 8
191
192 WrapperMouseArea {
193 id: defaultSinkMouseArea
194
195 anchors.fill: parent
196 hoverEnabled: true
197 cursorShape: Qt.PointingHandCursor
198
199 onClicked: {
200 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
201 }
202
203 Rectangle {
204 id: defaultSinkWidget
205
206 anchors.fill: parent
207 color: {
208 if (defaultSinkMouseArea.containsMouse)
209 return "#33808080";
210 return "transparent";
211 }
212
213 MaterialDesignIcon {
214 width: 16
215 height: 16
216 anchors.centerIn: parent
217
218 icon: {
219 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id)
220 return "speaker";
221 return "speaker-off";
222 }
223 color: icon == "speaker" ? "white" : "#555"
224 }
225 }
226 }
227 }
228 }
229
230 Repeater {
231 id: defaultSourceRepeater
232
233 model: {
234 Array.from(Pipewire.devices.values)
235 .filter(dev => dev.type == "Audio/Device")
236 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
237 }
238
239 Item {
240 id: defaultSourceItem
241
242 required property var modelData
243 required property int index
244
245 PwObjectTracker {
246 objects: [defaultSourceItem.modelData]
247 }
248
249 Layout.column: 2
250 Layout.row: index
251
252 Layout.fillHeight: true
253
254 implicitWidth: 16 + 8
255
256 WrapperMouseArea {
257 id: defaultSourceMouseArea
258
259 anchors.fill: parent
260 hoverEnabled: true
261 cursorShape: Qt.PointingHandCursor
262
263 onClicked: {
264 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
265 }
266
267 Rectangle {
268 id: defaultSourceWidget
269
270 anchors.fill: parent
271 color: {
272 if (defaultSourceMouseArea.containsMouse)
273 return "#33808080";
274 return "transparent";
275 }
276
277 MaterialDesignIcon {
278 width: 16
279 height: 16
280 anchors.centerIn: parent
281
282 icon: {
283 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
284 return "microphone";
285 return "microphone-off";
286 }
287 color: icon == "microphone" ? "white" : "#555"
288 }
289 }
290 }
291 }
292 }
293
294 Repeater {
295 id: profileRepeater
296
297 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
298
299 Item {
300 id: profileItem
301
302 required property var modelData
303 required property int index
304
305 PwObjectTracker {
306 objects: [profileItem.modelData]
307 }
308
309 Layout.column: 3
310 Layout.row: index
311
312 Layout.fillWidth: true
313
314 implicitWidth: Math.max(profileBox.implicitWidth, 300)
315 implicitHeight: profileBox.height
316
317 ComboBox {
318 id: profileBox
319
320 model: profileItem.modelData.profiles
321
322 textRole: "description"
323 valueRole: "index"
324 onActivated: profileItem.modelData.setProfile(currentValue)
325
326 anchors.fill: parent
327
328 implicitContentWidthPolicy: ComboBox.WidestText
329
330 Connections {
331 target: profileItem.modelData
332 function onCurrentProfileChanged() {
333 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
334 }
335 }
336 Component.onCompleted: {
337 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
338 }
339
340 Connections {
341 target: profileBox.popup
342 function onVisibleChanged() {
343 tooltip.openPopup = profileBox.popup.visible
344 }
345 }
346 }
347 }
348 }
349 }
350 }
351 }
352 }
353 }
354}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
new file mode 100644
index 00000000..3de69535
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
@@ -0,0 +1,18 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5
6Singleton {
7 id: root
8
9 Socket {
10 id: agentSocket
11 connected: true
12 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent`
13 }
14
15 function reloadAgent() {
16 agentSocket.write("RELOADAGENT\n")
17 }
18}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
index c82caaa6..af522ec4 100644
--- a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
@@ -28,9 +28,12 @@ Singleton {
28 path: root.socketPath 28 path: root.socketPath
29 connected: true 29 connected: true
30 30
31 property bool acked: false
32
31 onConnectionStateChanged: { 33 onConnectionStateChanged: {
32 if (connected) { 34 if (connected) {
33 write('"EventStream"\n') 35 acked = false;
36 write('"EventStream"\n');
34 } 37 }
35 } 38 }
36 39
@@ -66,6 +69,9 @@ Singleton {
66 eventWindowUrgencyChanged(event.WindowUrgencyChanged); 69 eventWindowUrgencyChanged(event.WindowUrgencyChanged);
67 else if (event.WindowLayoutsChanged) 70 else if (event.WindowLayoutsChanged)
68 eventWindowLayoutsChanged(event.WindowLayoutsChanged); 71 eventWindowLayoutsChanged(event.WindowLayoutsChanged);
72 else if (event.Ok && !eventStreamSocket.acked) { eventStreamSocket.acked = true; }
73 else if (event.OverviewOpenedOrClosed) {}
74 else if (event.ConfigLoaded) {}
69 else 75 else
70 console.log(JSON.stringify(event)); 76 console.log(JSON.stringify(event));
71 } catch (e) { 77 } catch (e) {
@@ -154,7 +160,11 @@ Singleton {
154 }); 160 });
155 } 161 }
156 function eventWindowOpenedOrChanged(data) { 162 function eventWindowOpenedOrChanged(data) {
157 root.windows = Array.from(root.windows).filter(win => win.id !== data.window.id).concat([data.window]); 163 root.windows = Array.from(root.windows).map(win => {
164 if (data.window.is_focused)
165 win.is_focused = false;
166 return win;
167 }).filter(win => win.id !== data.window.id).concat([data.window]);
158 } 168 }
159 function eventWindowClosed(data) { 169 function eventWindowClosed(data) {
160 root.windows = Array.from(root.windows).filter(win => win.id !== data.id); 170 root.windows = Array.from(root.windows).filter(win => win.id !== data.id);
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
new file mode 100644
index 00000000..3c524955
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
@@ -0,0 +1,8 @@
1import Custom as Custom
2
3Custom.FileSelector {
4 id: root
5
6 directory: @wallpapers@
7 epoch: 72000000
8}
diff --git a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
index ba678138..55b1690e 100644
--- a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
+++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
@@ -17,13 +17,17 @@ Item {
17 spacing: 0 17 spacing: 0
18 18
19 Repeater { 19 Repeater {
20 model: { 20 model: ScriptModel {
21 var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive); 21 values: {
22 trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id)) 22 var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive);
23 return trayItems; 23 trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id))
24 return trayItems;
25 }
24 } 26 }
25 27
26 delegate: Item { 28 delegate: Item {
29 id: trayItemWrapper
30
27 property var trayItem: modelData 31 property var trayItem: modelData
28 property string iconSource: { 32 property string iconSource: {
29 let icon = trayItem && trayItem.icon 33 let icon = trayItem && trayItem.icon
@@ -47,17 +51,7 @@ Item {
47 height: parent.height 51 height: parent.height
48 anchors.verticalCenter: parent.verticalCenter 52 anchors.verticalCenter: parent.verticalCenter
49 53
50 IconImage { 54 WrapperMouseArea {
51 anchors.centerIn: parent
52 width: parent.width
53 height: parent.width
54 source: parent.iconSource
55 asynchronous: true
56 smooth: true
57 mipmap: true
58 }
59
60 MouseArea {
61 id: trayItemArea 55 id: trayItemArea
62 56
63 anchors.fill: parent 57 anchors.fill: parent
@@ -88,6 +82,16 @@ Item {
88 menuAnchor.open() 82 menuAnchor.open()
89 } 83 }
90 } 84 }
85
86 IconImage {
87 anchors.centerIn: parent
88 width: parent.width
89 height: parent.width
90 source: trayItemWrapper.iconSource
91 asynchronous: true
92 smooth: true
93 mipmap: true
94 }
91 } 95 }
92 96
93 PopupWindow { 97 PopupWindow {
diff --git a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
new file mode 100644
index 00000000..02dcf227
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
@@ -0,0 +1,134 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Services.Pipewire
5import Quickshell.Widgets
6
7Scope {
8 id: root
9
10 property bool show: false
11 property bool inhibited: true
12
13 PwObjectTracker {
14 objects: [ Pipewire.defaultAudioSink ]
15 }
16
17 Connections {
18 enabled: Pipewire.defaultAudioSink
19 target: Pipewire.defaultAudioSink?.audio
20
21 function onVolumeChanged() {
22 root.show = true;
23 hideTimer.restart();
24 }
25 function onMutedChanged() {
26 root.show = true;
27 hideTimer.restart();
28 }
29 }
30
31 onShowChanged: {
32 if (show)
33 hideTimer.restart();
34 }
35
36 Timer {
37 id: hideTimer
38 interval: 1000
39 onTriggered: root.show = false
40 }
41
42 Timer {
43 id: startInhibit
44 interval: 100
45 running: true
46 onTriggered: {
47 root.show = false;
48 root.inhibited = false;
49 }
50 }
51
52 LazyLoader {
53 active: root.show && !root.inhibited
54
55 Variants {
56 model: Quickshell.screens
57
58 delegate: Scope {
59 id: screenScope
60
61 required property var modelData
62
63 PanelWindow {
64 id: window
65
66 screen: screenScope.modelData
67
68 anchors.top: true
69 margins.top: screen.height / 2 - 50 + 3.5
70 exclusiveZone: 0
71 exclusionMode: ExclusionMode.Ignore
72
73 implicitWidth: 400
74 implicitHeight: 50
75
76 mask: Region {}
77
78 color: "transparent"
79
80 Rectangle {
81 anchors.fill: parent
82 color: Qt.rgba(0, 0, 0, 0.75)
83 }
84
85 RowLayout {
86 id: layout
87
88 anchors.centerIn: parent
89
90 height: 50 - 8*2
91 width: 400 - 8*2
92
93 MaterialDesignIcon {
94 id: volumeIcon
95
96 implicitWidth: parent.height
97 implicitHeight: parent.height
98
99 icon: {
100 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
101 return "volume-off";
102 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
103 return "volume-low";
104 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
105 return "volume-medium";
106 return "volume-high";
107 }
108 }
109
110 Rectangle {
111 Layout.fillWidth: true
112
113 implicitHeight: 10
114
115 color: "#50ffffff"
116
117 Rectangle {
118 anchors {
119 left: parent.left
120 top: parent.top
121 bottom: parent.bottom
122 }
123
124 color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white"
125
126 implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0)
127 }
128 }
129 }
130 }
131 }
132 }
133 }
134}
diff --git a/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml
new file mode 100644
index 00000000..de31915f
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml
@@ -0,0 +1,85 @@
1import QtQuick
2import Quickshell
3import qs.Services
4
5Item {
6 id: root
7
8 anchors.fill: parent
9
10 required property string screen
11
12 property Img current: one
13 property string source: selector.selected
14
15 WallpaperSelector {
16 id: selector
17 seed: screen
18 }
19
20 onSourceChanged: {
21 if (!source)
22 current = null;
23 else if (current === one)
24 two.update()
25 else
26 one.update()
27 }
28
29 Img { id: one }
30 Img { id: two }
31
32 component Img: Image {
33 id: img
34
35 function update() {
36 source = root.source || ""
37 }
38
39 anchors.fill: parent
40 fillMode: Image.PreserveAspectCrop
41 smooth: true
42 asynchronous: true
43 cache: false
44
45 opacity: 0
46
47 onStatusChanged: {
48 if (status === Image.Ready) {
49 root.current = this
50 }
51 }
52
53 states: State {
54 name: "visible"
55 when: root.current === img
56
57 PropertyChanges {
58 img.opacity: 1
59 }
60 StateChangeScript {
61 name: "unloadOther"
62 script: {
63 if (img === one)
64 two.source = ""
65 if (img === two)
66 one.source = ""
67 }
68 }
69 }
70
71 transitions: Transition {
72 SequentialAnimation {
73 NumberAnimation {
74 target: img
75 properties: "opacity"
76 duration: 5000
77 easing.type: Easing.OutCubic
78 }
79 ScriptAction {
80 scriptName: "unloadOther"
81 }
82 }
83 }
84 }
85}
diff --git a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
index 153c56bb..c8c017c3 100644
--- a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
+++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
@@ -1,5 +1,8 @@
1import Quickshell
1import QtQuick 2import QtQuick
2import qs.Services 3import qs.Services
4import Quickshell.Widgets
5import QtQuick.Layouts
3 6
4Row { 7Row {
5 id: workspaces 8 id: workspaces
@@ -11,61 +14,188 @@ Row {
11 spacing: 0 14 spacing: 0
12 15
13 Repeater { 16 Repeater {
14 model: { 17 model: ScriptModel {
15 let currWorkspaces = NiriService.workspaces; 18 values: {
16 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); 19 let currWorkspaces = NiriService.workspaces;
17 currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); 20 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces);
18 currWorkspaces.sort((a, b) => { 21 currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name));
19 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) 22 currWorkspaces.sort((a, b) => {
20 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x 23 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x)
21 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) 24 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x
22 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y 25 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y)
23 return a.idx - b.idx; 26 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y
24 }); 27 return a.idx - b.idx;
25 return currWorkspaces; 28 });
29 return currWorkspaces;
30 }
26 } 31 }
27 32
28 Rectangle { 33 Item {
34 id: wsItem
35
29 property var workspaceData: modelData 36 property var workspaceData: modelData
30 37
31 width: wsLabel.contentWidth + 8 38 width: wsLabel.contentWidth + 8
32 color: {
33 if (mouseArea.containsMouse) {
34 return "#33808080";
35 }
36 return "transparent";
37 }
38 height: parent.height 39 height: parent.height
39 anchors.verticalCenter: parent.verticalCenter 40 anchors.verticalCenter: parent.verticalCenter
40 41
41 MouseArea { 42 WrapperMouseArea {
42 id: mouseArea 43 id: mouseArea
43 44
44 anchors.fill: parent 45 anchors.fill: parent
46
45 hoverEnabled: true 47 hoverEnabled: true
46 cursorShape: Qt.PointingHandCursor 48 cursorShape: Qt.PointingHandCursor
47 enabled: true 49 enabled: true
48 onClicked: { 50 onClicked: {
49 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}) 51 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {})
50 } 52 }
53
54 Rectangle {
55 anchors.fill: parent
56
57 color: {
58 if (mouseArea.containsMouse) {
59 return "#33808080";
60 }
61 return "transparent";
62 }
63
64 Text {
65 id: wsLabel
66
67 anchors.centerIn: parent
68
69 font.pointSize: 10
70 font.family: "Fira Sans"
71 color: {
72 if (workspaceData.is_active)
73 return "#23fd00";
74 if (workspaceData.active_window_id === null)
75 return "#555";
76 return "white";
77 }
78
79 text: workspaceData.name ? workspaceData.name : workspaceData.idx
80 }
81 }
51 } 82 }
52 83
53 Text { 84 PopupWindow {
54 id: wsLabel 85 id: tooltip
55 86
56 font.pointSize: 10 87 property bool nextVisible: (mouseArea.containsMouse || tooltipMouseArea.containsMouse) && [...windowsModel.values].length > 0
57 font.family: "Fira Sans" 88
58 color: { 89 anchor {
59 if (workspaceData.is_active) 90 item: mouseArea
60 return "#23fd00"; 91 edges: Edges.Bottom | Edges.Left
61 if (workspaceData.active_window_id === null) 92 }
62 return "#555"; 93 visible: false
63 return "white"; 94
95 onNextVisibleChanged: hangTimer.restart()
96
97 Timer {
98 id: hangTimer
99 interval: 100
100 onTriggered: tooltip.visible = tooltip.nextVisible
64 } 101 }
65 anchors.centerIn: parent
66 102
67 text: workspaceData.name ? workspaceData.name : workspaceData.idx 103 implicitWidth: tooltipContent.implicitWidth
104 implicitHeight: tooltipContent.implicitHeight
105 color: "black"
106
107 WrapperMouseArea {
108 id: tooltipMouseArea
109
110 hoverEnabled: true
111 enabled: true
112
113 anchors.fill: parent
114
115 WrapperItem {
116 id: tooltipContent
117
118 margin: 0
119
120 ColumnLayout {
121 spacing: 0
122
123 Repeater {
124 model: ScriptModel {
125 id: windowsModel
126
127 values: {
128 let currWindows = Array.from(NiriService.windows).filter(win => win.workspace_id == wsItem.workspaceData.id);
129 currWindows.sort((a, b) => {
130 if (a.is_floating !== b.is_floating)
131 return b.is_floating - a.is_floating;
132 if (a.layout.tile_pos_in_workspace_view?.[0] !== b.layout.tile_pos_in_workspace_view?.[0])
133 return a.layout.tile_pos_in_workspace_view?.[0] - b.layout.tile_pos_in_workspace_view?.[0]
134 if (a.layout.tile_pos_in_workspace_view?.[1] !== b.layout.tile_pos_in_workspace_view?.[1])
135 return a.layout.tile_pos_in_workspace_view?.[1] - b.layout.tile_pos_in_workspace_view?.[1]
136 if (a.layout.pos_in_scrolling_layout?.[0] !== b.layout.pos_in_scrolling_layout?.[0])
137 return a.layout.pos_in_scrolling_layout?.[0] - b.layout.pos_in_scrolling_layout?.[0]
138 if (a.layout.pos_in_scrolling_layout?.[1] !== b.layout.pos_in_scrolling_layout?.[1])
139 return a.layout.pos_in_scrolling_layout?.[1] - b.layout.pos_in_scrolling_layout?.[1]
140 if (a.app_id !== b.app_id)
141 return a.app_id.localeCompare(b.app_id);
142
143 return a.title.localeCompare(b.title);
144 });
145 return currWindows;
146 }
147 }
148
149 WrapperMouseArea {
150 id: windowMouseArea
151
152 property var windowData: modelData
153
154 hoverEnabled: true
155 cursorShape: Qt.PointingHandCursor
156 enabled: true
157
158 Layout.fillWidth: true
159
160 onClicked: {
161 NiriService.sendCommand({ "Action": { "FocusWindow": { "id": windowData.id } } }, _ => {})
162 }
163
164 WrapperRectangle {
165 color: windowMouseArea.containsMouse ? "#33808080" : "transparent";
166
167 anchors.fill: parent
168
169 WrapperItem {
170 anchors.fill: parent
171
172 margin: 4
173
174 Text {
175 id: windowLabel
176
177 font.pointSize: 10
178 font.family: "Fira Sans"
179 color: {
180 if (windowData.is_focused)
181 return "#23fd00";
182 if (NiriService.workspaces.find(ws => ws.id == windowData.workspace_id)?.active_window_id == windowData.id)
183 return "white";
184 return "#555";
185 }
186
187 text: windowData.title
188
189 horizontalAlignment: Text.AlignLeft
190 }
191 }
192 }
193 }
194 }
195 }
196 }
197 }
68 } 198 }
69 } 199 }
70 } 200 }
71} \ No newline at end of file 201}
diff --git a/accounts/gkleen@sif/shell/quickshell/displaymanager.qml b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml
new file mode 100644
index 00000000..b452c03d
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml
@@ -0,0 +1,115 @@
1//@ pragma UseQApplication
2
3import Quickshell
4import Quickshell.Wayland
5import Quickshell.Io
6import Quickshell.Services.Greetd
7import QtQml
8
9
10ShellRoot {
11 id: displaymanager
12
13 settings.watchFiles: false
14
15 property string currentText: ""
16 property string username: @username@
17 property list<string> command: @niri_session@
18 property list<var> messages: []
19 property bool responseRequired: false
20 property bool responseVisible: false
21
22 signal startAuth()
23
24 onStartAuth: {
25 if (Greetd.state !== GreetdState.Inactive)
26 Greetd.cancelSession();
27 displaymanager.messages = [];
28 Greetd.createSession(displaymanager.username);
29 }
30
31 Connections {
32 target: Greetd
33 function onStateChanged() {
34 console.log("greetd state: ", GreetdState.toString(Greetd.state));
35 if (Greetd.state === GreetdState.ReadyToLaunch)
36 Greetd.launch(displaymanager.command);
37 }
38 function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) {
39 displaymanager.responseVisible = echoResponse;
40 displaymanager.responseRequired = responseRequired;
41 displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": error }]);
42 }
43 function onAuthFailure(message: string) {
44 displaymanager.responseRequired = false;
45 displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": true }]);
46 }
47 }
48
49 Component.onCompleted: {
50 if (Greetd.state !== GreetdState.Inactive)
51 Greetd.cancelSession();
52 }
53
54 Variants {
55 model: Quickshell.screens
56
57 delegate: Scope {
58 id: screenScope
59
60 required property var modelData
61
62 PanelWindow {
63 color: "black"
64
65 screen: screenScope.modelData
66
67 WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
68
69 anchors.top: true
70 anchors.bottom: true
71 anchors.left: true
72 anchors.right: true
73
74 LockSurface {
75 id: surfaceContent
76
77 screen: screenScope.modelData
78
79 onCurrentTextChanged: displaymanager.currentText = currentText
80 Connections {
81 target: displaymanager
82 function onCurrentTextChanged() { surfaceContent.currentText = displaymanager.currentText; }
83 function onMessagesChanged() { surfaceContent.messages = Array.from(displaymanager.messages); }
84 function onResponseRequiredChanged() { surfaceContent.responseRequired = displaymanager.responseRequired; }
85 function onResponseVisibleChanged() { surfaceContent.responseVisible = displaymanager.responseVisible; }
86 }
87
88 onResponse: responseText => Greetd.respond(responseText);
89 Connections {
90 target: Greetd
91 function onStateChanged() {
92 if (Greetd.state === GreetdState.Authenticating) {
93 surfaceContent.authRunning = true;
94 } else {
95 surfaceContent.authRunning = false;
96 }
97 }
98 }
99
100 onAuthRunningChanged: {
101 if (surfaceContent.authRunning && Greetd.state !== GreetdState.Authenticating)
102 displaymanager.startAuth();
103 }
104 Component.onCompleted: {
105 surfaceContent.authRunning = Greetd.state === GreetdState.Authenticating
106 surfaceContent.messages = Array.from(displaymanager.messages);
107 surfaceContent.responseVisible = displaymanager.responseVisible;
108 surfaceContent.responseRequired = displaymanager.responseRequired;
109 surfaceContent.currentText = displaymanager.currentText;
110 }
111 }
112 }
113 }
114 }
115}
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
index 4934cd4d..3657f77f 100644
--- a/accounts/gkleen@sif/shell/quickshell/shell.qml
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -1,6 +1,7 @@
1//@ pragma UseQApplication 1//@ pragma UseQApplication
2 2
3import Quickshell 3import Quickshell
4import Quickshell.Wayland
4 5
5ShellRoot { 6ShellRoot {
6 settings.watchFiles: false 7 settings.watchFiles: false
@@ -13,11 +14,33 @@ ShellRoot {
13 14
14 required property var modelData 15 required property var modelData
15 16
16 Bar { 17 PanelWindow {
17 id: bar 18 id: bgWindow
18 19
19 screen: screenScope.modelData 20 screen: screenScope.modelData
21
22 WlrLayershell.layer: WlrLayer.Background
23 WlrLayershell.exclusionMode: ExclusionMode.Ignore
24
25 anchors.top: true
26 anchors.bottom: true
27 anchors.left: true
28 anchors.right: true
29
30 color: "black"
31
32 WallpaperBackground {
33 screen: bgWindow.screen.name
34 }
35 }
36
37 Bar {
38 screen: screenScope.modelData
20 } 39 }
21 } 40 }
22 } 41 }
42
43 Lockscreen {}
44
45 VolumeOSD {}
23} 46}
diff --git a/accounts/gkleen@sif/systemd.nix b/accounts/gkleen@sif/systemd.nix
index 1539126c..4543103f 100644
--- a/accounts/gkleen@sif/systemd.nix
+++ b/accounts/gkleen@sif/systemd.nix
@@ -351,8 +351,6 @@ in {
351 xembed-sni-proxy = { 351 xembed-sni-proxy = {
352 Unit = { 352 Unit = {
353 PartOf = lib.mkForce ["tray.target"]; 353 PartOf = lib.mkForce ["tray.target"];
354 BindsTo = ["xwayland-satellite.service"];
355 After = ["xwayland-satellite.service"];
356 }; 354 };
357 }; 355 };
358 poweralertd = { 356 poweralertd = {
diff --git a/hosts/sif/default.nix b/hosts/sif/default.nix
index ed85ca17..952b95f9 100644
--- a/hosts/sif/default.nix
+++ b/hosts/sif/default.nix
@@ -655,7 +655,7 @@ in {
655 "org.freedesktop.impl.portal.OpenFile" = ["gtk"]; 655 "org.freedesktop.impl.portal.OpenFile" = ["gtk"];
656 "org.freedesktop.impl.portal.Access" = ["gtk"]; 656 "org.freedesktop.impl.portal.Access" = ["gtk"];
657 "org.freedesktop.impl.portal.Notification" = ["gtk"]; 657 "org.freedesktop.impl.portal.Notification" = ["gtk"];
658 "org.freedesktop.impl.portal.Secret" = ["gnome-keyring"]; 658 "org.freedesktop.impl.portal.Secret" = ["none"];
659 "org.freedesktop.impl.portal.Inhibit" = ["none"]; 659 "org.freedesktop.impl.portal.Inhibit" = ["none"];
660 }; 660 };
661 }; 661 };
@@ -684,7 +684,7 @@ in {
684 timezone = true; 684 timezone = true;
685 }; 685 };
686 686
687 security.pam.services.gtklock = {}; 687 security.pam.services.quickshell = {};
688 688
689 home-manager.sharedModules = [ flakeInputs.nixVirt.homeModules.default ]; 689 home-manager.sharedModules = [ flakeInputs.nixVirt.homeModules.default ];
690 690
diff --git a/hosts/sif/greetd/default.nix b/hosts/sif/greetd/default.nix
index 37ca13c5..081b6346 100644
--- a/hosts/sif/greetd/default.nix
+++ b/hosts/sif/greetd/default.nix
@@ -1,49 +1,92 @@
1{ pkgs, ... }: 1{ config, pkgs, lib, flakeInputs, ... }:
2{ 2
3let
4 gkleenConfig = config.home-manager.users."gkleen";
5 toIni = lib.generators.toINI {
6 mkKeyValue =
7 key: value:
8 let
9 value' = if lib.isBool value then lib.boolToString value else toString value;
10 in
11 "${lib.escape [ "=" ] key}=${value'}";
12 };
13 toDconfIni = let
14 gvariant = import (flakeInputs.home-manager + "/modules/lib/gvariant.nix") { inherit lib; };
15 mkIniKeyValue = key: value: "${key}=${toString (gvariant.mkValue value)}";
16 in lib.generators.toINI { mkKeyValue = mkIniKeyValue; };
17in {
3 config = { 18 config = {
4 services.greetd = { 19 services.greetd = {
5 enable = true; 20 enable = true;
6 # settings.default_session.command = let 21 settings.default_session.command = lib.getExe (pkgs.writeShellApplication {
7 # cfg = config.programs.regreet; 22 name = "sway";
8 # in pkgs.writeShellScript "greeter" '' 23 runtimeInputs = [ pkgs.sway pkgs.fontconfig ];
9 # modprobe -r nvidia_drm 24 runtimeEnv = {
25 XDG_DATA_DIRS = lib.makeSearchPath "share" [
26 pkgs.equilux-theme pkgs.paper-icon-theme pkgs.fira
27 ];
28 QT_PLUGIN_PATH = lib.makeSearchPath (pkgs.qt6.qtbase.qtPluginPrefix) [
29 pkgs.qt6Packages.qtbase
30 ];
31 QML2_IMPORT_PATH = lib.makeSearchPath (pkgs.qt6.qtbase.qtQmlPrefix) [
32 pkgs.qt6Packages.qtbase
33 ];
34 QT_QPA_PLATFORMTHEME = "gtk3";
35 XDG_CONFIG_DIR = pkgs.symlinkJoin {
36 name = "config";
37 paths = [
38 (pkgs.writeTextDir "gtk-3.0/settings.ini" (toIni {
39 Settings = {
40 gtk-font-name = "Fira Sans 10";
41 gtk-theme-name = "Equilux-compact";
42 gtk-icon-theme-name = "Paper-Mono-Dark";
43 };
44 }))
45 ];
46 };
47 # XDG_CACHE_HOME = "/var/cache/greetd/greeter";
48 # XDG_CONFIG_HOME = "/var/cache/greetd/greeter/config";
49 };
50 text = ''
51 exec &>/tmp/sway-$$.log
52
53 unset MANAGERPID SYSTEMD_EXEC_PID
54
55 # ${lib.getExe' pkgs.coreutils "mkdir"} -p ''${XDG_CONFIG_HOME}/dconf
56 ${lib.getExe pkgs.dconf} load / < ${pkgs.writeText "dconf.ini" (toDconfIni {
57 "org/gnome/desktop/interface" = {
58 "color-scheme" = "prefer-dark";
59 "font-name" = "Fira Sans 10";
60 "gtk-theme" = "Equilux-compact";
61 "icon-theme" = "Paper-Mono-Dark";
62 };
63 })}
64
65 exec sway --unsupported-gpu --config ${pkgs.writeText "sway-config" ''
66 exec "${lib.getExe' config.systemd.package "systemctl"} --user import-environment {,WAYLAND_}DISPLAY SWAYSOCK; ${lib.getExe gkleenConfig.programs.quickshell.package} --path ${gkleenConfig.xdg.configFile."quickshell".source}/displaymanager.qml; swaymsg exit"
10 67
11 # exec ${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} ${lib.escapeShellArgs cfg.cageArgs} -- ${lib.getExe cfg.package} 68 input type:keyboard {
12 # ''; 69 xkb_layout "us,us"
70 xkb_variant "dvp,"
71 xkb_options "compose:caps,grp:win_space_toggle"
72 }
73
74 output eDP-1 scale 1.5
75 ''}
76 '';
77 });
13 }; 78 };
14 systemd.services.greetd.environment = { 79
15 XKB_DEFAULT_LAYOUT = "us,us"; 80 # security.pam.services.greetd.fprintAuth = false;
16 XKB_DEFAULT_VARIANT = "dvp,"; 81
17 XKB_DEFAULT_OPTIONS = "compose:caps,grp:win_space_toggle"; 82 systemd.services.greetd.serviceConfig = {
83 ExecStartPre = ''${lib.getExe' pkgs.coreutils "install"} -d -o greeter -g greeter -m 0700 ''${CACHE_DIRECTORY}/greeter'';
84 # CacheDirectory = "greetd";
18 }; 85 };
19 programs.regreet = { 86
20 enable = true; 87 users.users.greeter = {
21 theme = { 88 home = "/var/lib/greeter";
22 package = pkgs.equilux-theme; 89 createHome = true;
23 name = "Equilux-compact";
24 };
25 iconTheme = {
26 package = pkgs.paper-icon-theme;
27 name = "Paper-Mono-Dark";
28 };
29 font = {
30 package = pkgs.fira;
31 name = "Fira Sans";
32 # size = 6;
33 };
34 cageArgs = [ "-s" "-m" "last" ];
35 settings = {
36 GTK.application_prefer_dark_theme = true;
37 widget.clock.format = "%F %H:%M:%S%:z";
38 background = {
39 path = pkgs.runCommand "wallpaper.png" {
40 buildInputs = with pkgs; [ imagemagick ];
41 } ''
42 magick ${./wallpaper.png} -filter Gaussian -resize 6.25% -define filter:sigma=2.5 -resize 1600% "$out"
43 '';
44 fit = "Cover";
45 };
46 };
47 }; 90 };
48 }; 91 };
49} 92}
diff --git a/hosts/sif/greetd/wallpaper.png b/hosts/sif/greetd/wallpaper.png
deleted file mode 100644
index 20fc761a..00000000
--- a/hosts/sif/greetd/wallpaper.png
+++ /dev/null
Binary files differ
diff --git a/overlays/quickshell/default.nix b/overlays/quickshell/default.nix
new file mode 100644
index 00000000..622d69a3
--- /dev/null
+++ b/overlays/quickshell/default.nix
@@ -0,0 +1,10 @@
1{ final, prev, ... }:
2{
3 quickshell = prev.quickshell.overrideAttrs (oldAttrs: {
4 patches = (oldAttrs.patches or []) ++ [
5 ./greetd-response.patch
6 ./lock-state-changed.patch
7 ./pipewire.patch
8 ];
9 });
10}
diff --git a/overlays/quickshell/greetd-response.patch b/overlays/quickshell/greetd-response.patch
new file mode 100644
index 00000000..a0efb562
--- /dev/null
+++ b/overlays/quickshell/greetd-response.patch
@@ -0,0 +1,16 @@
1diff --git c/src/services/greetd/connection.cpp w/src/services/greetd/connection.cpp
2index bf0d1fd..a790ab7 100644
3--- c/src/services/greetd/connection.cpp
4+++ w/src/services/greetd/connection.cpp
5@@ -225,6 +225,11 @@ void GreetdConnection::onSocketReady() {
6
7 this->mResponseRequired = responseRequired;
8 emit this->authMessage(message, error, responseRequired, echoResponse);
9+
10+ if (!responseRequired)
11+ this->sendRequest({
12+ {"type", "post_auth_message_response"}
13+ });
14 } else goto unexpected;
15
16 return;
diff --git a/overlays/quickshell/lock-state-changed.patch b/overlays/quickshell/lock-state-changed.patch
new file mode 100644
index 00000000..4be273fa
--- /dev/null
+++ b/overlays/quickshell/lock-state-changed.patch
@@ -0,0 +1,12 @@
1diff --git i/src/wayland/session_lock.cpp w/src/wayland/session_lock.cpp
2index 0ecf9ec..3dbd19b 100644
3--- i/src/wayland/session_lock.cpp
4+++ w/src/wayland/session_lock.cpp
5@@ -127,6 +127,7 @@ void WlSessionLock::realizeLockTarget(WlSessionLock* old) {
6 this->updateSurfaces(false);
7
8 if (!this->manager->lock()) this->lockTarget = false;
9+ emit this->lockStateChanged();
10
11 this->updateSurfaces(true, old);
12 } else {
diff --git a/overlays/quickshell/pipewire.patch b/overlays/quickshell/pipewire.patch
new file mode 100644
index 00000000..33025d8b
--- /dev/null
+++ b/overlays/quickshell/pipewire.patch
@@ -0,0 +1,460 @@
1diff --git i/src/services/pipewire/device.cpp w/src/services/pipewire/device.cpp
2index 616e7d0..0c55008 100644
3--- i/src/services/pipewire/device.cpp
4+++ w/src/services/pipewire/device.cpp
5@@ -3,6 +3,7 @@
6 #include <cstdint>
7 #include <functional>
8 #include <utility>
9+#include <algorithm>
10
11 #include <pipewire/device.h>
12 #include <qcontainerfwd.h>
13@@ -19,6 +20,8 @@
14 #include <spa/pod/pod.h>
15 #include <spa/pod/vararg.h>
16 #include <spa/utils/type.h>
17+#include <spa/monitor/device.h>
18+#include <spa/utils/keys.h>
19
20 #include "../../core/logcat.hpp"
21 #include "core.hpp"
22@@ -46,6 +49,25 @@ void PwDevice::unbindHooks() {
23 this->mWaitingForDevice = false;
24 }
25
26+void PwDevice::initProps(const spa_dict* props) {
27+ if (const auto* deviceName = spa_dict_lookup(props, SPA_KEY_DEVICE_NAME)) {
28+ this->name = deviceName;
29+ }
30+
31+ if (const auto* deviceDesc = spa_dict_lookup(props, SPA_KEY_DEVICE_DESCRIPTION)) {
32+ this->description = deviceDesc;
33+ }
34+
35+ if (const auto* deviceNick = spa_dict_lookup(props, SPA_KEY_DEVICE_NICK)) {
36+ this->nick = deviceNick;
37+ }
38+
39+ if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) {
40+ this->type = mediaClass;
41+ }
42+}
43+
44+
45 const pw_device_events PwDevice::EVENTS = {
46 .version = PW_VERSION_DEVICE_EVENTS,
47 .info = &PwDevice::onInfo,
48@@ -71,6 +93,11 @@ void PwDevice::onInfo(void* data, const pw_device_info* info) {
49 }
50
51 break;
52+ } else if (param.id == SPA_PARAM_EnumProfile && param.flags & SPA_PARAM_INFO_READ) {
53+ self->validProfiles.clear();
54+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
55+ } else if (param.id == SPA_PARAM_Profile && param.flags & SPA_PARAM_INFO_READ) {
56+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
57 }
58 }
59 }
60@@ -97,6 +124,15 @@ void PwDevice::onParam(
61 }
62
63 self->addDeviceIndexPairs(param);
64+ } else if (id == SPA_PARAM_EnumProfile) {
65+ PwProfile profile = PwProfile::parseSpaPod(param);
66+ self->profilesUpdated = true;
67+ self->profiles.insertOrAssign(profile.index, profile);
68+ self->validProfiles.insert(profile.index);
69+ } else if (id == SPA_PARAM_Profile) {
70+ PwProfile profile = PwProfile::parseSpaPod(param);
71+ self->currentProfileUpdated = true;
72+ self->currentProfile = profile;
73 }
74 }
75
76@@ -145,6 +181,21 @@ void PwDevice::polled() {
77 return false;
78 });
79 }
80+ if (this->profilesUpdated) {
81+ this->profiles.removeIf([&](const std::pair<qint32, PwProfile>& entry) {
82+ return !this->validProfiles.contains(entry.first);
83+ });
84+ this->profilesUpdated = false;
85+ QList<PwProfile> profiles = this->profiles.values();
86+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
87+ emit this->profilesChanged(profiles);
88+ }
89+ if (this->currentProfileUpdated) {
90+ this->currentProfileUpdated = false;
91+ if (this->currentProfile) {
92+ emit this->currentProfileChanged(*this->currentProfile);
93+ }
94+ }
95 }
96
97 bool PwDevice::setVolumes(qint32 routeDevice, const QVector<float>& volumes) {
98@@ -182,6 +233,15 @@ bool PwDevice::setMuted(qint32 routeDevice, bool muted) {
99 });
100 }
101
102+void PwDevice::setProfile(qint32 profileIndex) {
103+ auto buffer = std::array<uint8_t, 1024>();
104+ auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
105+ auto* pod = spa_pod_builder_add_object(&builder,
106+ SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
107+ SPA_PARAM_PROFILE_index, SPA_POD_Int(profileIndex));
108+ pw_device_set_param(this->proxy(), SPA_PARAM_Profile, 0, static_cast<spa_pod*>(pod));
109+}
110+
111 void PwDevice::waitForDevice() { this->mWaitingForDevice = true; }
112 bool PwDevice::waitingForDevice() const { return this->mWaitingForDevice; }
113
114@@ -222,4 +282,24 @@ bool PwDevice::setRouteProps(
115 return true;
116 }
117
118+PwProfile PwProfile::parseSpaPod(const spa_pod* param) {
119+ PwProfile profile;
120+
121+ const auto* indexProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_index);
122+ const auto* descProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_description);
123+ const auto* nameProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_name);
124+
125+ spa_pod_get_int(&indexProp->value, &profile.index);
126+
127+ const char* desc_cstr = nullptr;
128+ spa_pod_get_string(&descProp->value, &desc_cstr);
129+ profile.description = QString(desc_cstr);
130+
131+ const char* name_cstr = nullptr;
132+ spa_pod_get_string(&nameProp->value, &name_cstr);
133+ profile.name = QString(name_cstr);
134+
135+ return profile;
136+}
137+
138 } // namespace qs::service::pipewire
139diff --git i/src/services/pipewire/device.hpp w/src/services/pipewire/device.hpp
140index 1a1f705..ee64858 100644
141--- i/src/services/pipewire/device.hpp
142+++ w/src/services/pipewire/device.hpp
143@@ -1,6 +1,7 @@
144 #pragma once
145
146 #include <functional>
147+#include <optional>
148
149 #include <pipewire/core.h>
150 #include <pipewire/device.h>
151@@ -17,6 +18,20 @@
152
153 namespace qs::service::pipewire {
154
155+struct PwProfile {
156+ Q_GADGET;
157+ Q_PROPERTY(qint32 index MEMBER index)
158+ Q_PROPERTY(QString description MEMBER description)
159+ Q_PROPERTY(QString name MEMBER name)
160+
161+public:
162+ qint32 index;
163+ QString description;
164+ QString name;
165+
166+ static PwProfile parseSpaPod(const spa_pod* param);
167+};
168+
169 class PwDevice;
170
171 class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSION_DEVICE> {
172@@ -25,6 +40,12 @@ class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSIO
173 public:
174 void bindHooks() override;
175 void unbindHooks() override;
176+ void initProps(const spa_dict* props) override;
177+
178+ QString name;
179+ QString description;
180+ QString nick;
181+ QString type;
182
183 bool setVolumes(qint32 routeDevice, const QVector<float>& volumes);
184 bool setMuted(qint32 routeDevice, bool muted);
185@@ -32,9 +53,16 @@ public:
186 void waitForDevice();
187 [[nodiscard]] bool waitingForDevice() const;
188
189+ void setProfile(qint32 profileIndex);
190+
191+ QHash<qint32, PwProfile> profiles;
192+ std::optional<PwProfile> currentProfile;
193+
194 signals:
195 void deviceReady();
196 void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps);
197+ void profilesChanged(QList<PwProfile> profiles);
198+ void currentProfileChanged(PwProfile profile);
199
200 private slots:
201 void polled();
202@@ -49,6 +77,11 @@ private:
203 QList<qint32> stagingIndexes;
204 void addDeviceIndexPairs(const spa_pod* param);
205
206+ bool profilesUpdated = false;
207+ QSet<qint32> validProfiles;
208+
209+ bool currentProfileUpdated = false;
210+
211 bool
212 setRouteProps(qint32 routeDevice, const std::function<void*(spa_pod_builder*)>& propsCallback);
213
214diff --git i/src/services/pipewire/qml.cpp w/src/services/pipewire/qml.cpp
215index 9efb17e..921d12a 100644
216--- i/src/services/pipewire/qml.cpp
217+++ w/src/services/pipewire/qml.cpp
218@@ -9,6 +9,9 @@
219 #include <qtypes.h>
220 #include <qvariant.h>
221
222+#include <cstdint>
223+#include <algorithm>
224+
225 #include "../../core/model.hpp"
226 #include "connection.hpp"
227 #include "defaults.hpp"
228@@ -54,6 +57,12 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
229
230 QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
231
232+ for (auto* device: connection->registry.devices.values()) {
233+ this->onDeviceAdded(device);
234+ }
235+
236+ QObject::connect(&connection->registry, &PwRegistry::deviceAdded, this, &Pipewire::onDeviceAdded);
237+
238 for (auto* link: connection->registry.links.values()) {
239 this->onLinkAdded(link);
240 }
241@@ -123,6 +132,19 @@ void Pipewire::onNodeRemoved(QObject* object) {
242 this->mNodes.removeObject(iface);
243 }
244
245+ObjectModel<PwDeviceIface>* Pipewire::devices() { return &this->mDevices; }
246+
247+void Pipewire::onDeviceAdded(PwDevice* device) {
248+ auto* iface = PwDeviceIface::instance(device);
249+ QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onDeviceRemoved);
250+ this->mDevices.insertObject(iface);
251+}
252+
253+void Pipewire::onDeviceRemoved(QObject* object) {
254+ auto* iface = static_cast<PwDeviceIface*>(object); // NOLINT
255+ this->mDevices.removeObject(iface);
256+}
257+
258 ObjectModel<PwLinkIface>* Pipewire::links() { return &this->mLinks; }
259
260 void Pipewire::onLinkAdded(PwLink* link) {
261@@ -357,6 +379,8 @@ QVariantMap PwNodeIface::properties() const {
262
263 PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
264
265+PwDeviceIface* PwNodeIface::device() const { return PwDeviceIface::instance(this->mNode->device); }
266+
267 PwNodeIface* PwNodeIface::instance(PwNode* node) {
268 if (node == nullptr) return nullptr;
269
270@@ -481,4 +505,42 @@ void PwObjectTracker::objectDestroyed(QObject* object) {
271 emit this->objectsChanged();
272 }
273
274+PwDeviceIface::PwDeviceIface(PwDevice* device): PwObjectIface(device), mDevice(device) {
275+ QObject::connect(device, &PwDevice::profilesChanged, this, &PwDeviceIface::deviceProfilesChanged);
276+ QObject::connect(device, &PwDevice::currentProfileChanged, this, &PwDeviceIface::deviceCurrentProfileChanged);
277+}
278+
279+void PwDeviceIface::deviceProfilesChanged(QList<PwProfile>) { emit this->profilesChanged(); }
280+void PwDeviceIface::deviceCurrentProfileChanged(PwProfile) { emit this->currentProfileChanged(); }
281+
282+quint32 PwDeviceIface::id() const { return this->mDevice->id; }
283+QString PwDeviceIface::name() const { return this->mDevice->name; }
284+QString PwDeviceIface::description() const { return this->mDevice->description; }
285+QString PwDeviceIface::nickname() const { return this->mDevice->nick; }
286+QString PwDeviceIface::type() const { return this->mDevice->type; }
287+QList<PwProfile> PwDeviceIface::profiles() const {
288+ QList<PwProfile> profiles = this->mDevice->profiles.values();
289+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
290+ return profiles;
291+}
292+qint32 PwDeviceIface::currentProfile() const { return this->mDevice->currentProfile->index; }
293+
294+PwDeviceIface* PwDeviceIface::instance(PwDevice* device) {
295+ if (device == nullptr) return nullptr;
296+
297+ auto v = device->property("iface");
298+ if (v.canConvert<PwDeviceIface*>()) {
299+ return v.value<PwDeviceIface*>();
300+ }
301+
302+ auto* instance = new PwDeviceIface(device);
303+ device->setProperty("iface", QVariant::fromValue(instance));
304+
305+ return instance;
306+}
307+
308+void PwDeviceIface::setProfile(qint32 profileIndex) {
309+ this->mDevice->setProfile(profileIndex);
310+}
311+
312 } // namespace qs::service::pipewire
313diff --git i/src/services/pipewire/qml.hpp w/src/services/pipewire/qml.hpp
314index e3489a1..e5e1891 100644
315--- i/src/services/pipewire/qml.hpp
316+++ w/src/services/pipewire/qml.hpp
317@@ -12,11 +12,13 @@
318 #include "../../core/model.hpp"
319 #include "link.hpp"
320 #include "node.hpp"
321+#include "device.hpp"
322 #include "registry.hpp"
323
324 namespace qs::service::pipewire {
325
326 class PwNodeIface;
327+class PwDeviceIface;
328 class PwLinkIface;
329 class PwLinkGroupIface;
330
331@@ -65,6 +67,8 @@ class Pipewire: public QObject {
332 /// - @@PwNode.audio - if non null the node is an audio node.
333 QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwNodeIface>*);
334 Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT);
335+ QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwDeviceIface>*);
336+ Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
337 /// All links present in pipewire.
338 ///
339 /// Links connect pipewire nodes to each other, and can be used to determine
340@@ -134,6 +138,7 @@ public:
341 explicit Pipewire(QObject* parent = nullptr);
342
343 [[nodiscard]] ObjectModel<PwNodeIface>* nodes();
344+ [[nodiscard]] ObjectModel<PwDeviceIface>* devices();
345 [[nodiscard]] ObjectModel<PwLinkIface>* links();
346 [[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
347
348@@ -159,7 +164,9 @@ signals:
349
350 private slots:
351 void onNodeAdded(PwNode* node);
352+ void onDeviceAdded(PwDevice* node);
353 void onNodeRemoved(QObject* object);
354+ void onDeviceRemoved(QObject* object);
355 void onLinkAdded(PwLink* link);
356 void onLinkRemoved(QObject* object);
357 void onLinkGroupAdded(PwLinkGroup* group);
358@@ -167,6 +174,7 @@ private slots:
359
360 private:
361 ObjectModel<PwNodeIface> mNodes {this};
362+ ObjectModel<PwDeviceIface> mDevices {this};
363 ObjectModel<PwLinkIface> mLinks {this};
364 ObjectModel<PwLinkGroupIface> mLinkGroups {this};
365 };
366@@ -315,6 +323,7 @@ class PwNodeIface: public PwObjectIface {
367 /// > [!NOTE] The node may be used before it is fully bound, but some data
368 /// > may be missing or incorrect.
369 Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
370+ Q_PROPERTY(qs::service::pipewire::PwDeviceIface* device READ device CONSTANT);
371 QML_NAMED_ELEMENT(PwNode);
372 QML_UNCREATABLE("PwNodes cannot be created directly");
373
374@@ -332,6 +341,7 @@ public:
375 [[nodiscard]] PwNodeType::Flags type() const;
376 [[nodiscard]] QVariantMap properties() const;
377 [[nodiscard]] PwNodeAudioIface* audio() const;
378+ [[nodiscard]] PwDeviceIface* device() const;
379
380 static PwNodeIface* instance(PwNode* node);
381
382@@ -344,6 +354,44 @@ private:
383 PwNodeAudioIface* audioIface = nullptr;
384 };
385
386+class PwDeviceIface: public PwObjectIface {
387+ Q_OBJECT;
388+ Q_PROPERTY(quint32 id READ id CONSTANT);
389+ Q_PROPERTY(QString name READ name CONSTANT);
390+ Q_PROPERTY(QString description READ description CONSTANT);
391+ Q_PROPERTY(QString nickname READ nickname CONSTANT);
392+ Q_PROPERTY(QString type READ type CONSTANT);
393+ Q_PROPERTY(QList<PwProfile> profiles READ profiles NOTIFY profilesChanged);
394+ Q_PROPERTY(qint32 currentProfile READ currentProfile NOTIFY currentProfileChanged);
395+
396+ QML_NAMED_ELEMENT(PwDevice);
397+ QML_UNCREATABLE("PwDevices cannot be created directly");
398+
399+signals:
400+ void profilesChanged();
401+ void currentProfileChanged();
402+
403+public:
404+ explicit PwDeviceIface(PwDevice* node);
405+
406+ [[nodiscard]] quint32 id() const;
407+ [[nodiscard]] QString name() const;
408+ [[nodiscard]] QString description() const;
409+ [[nodiscard]] QString nickname() const;
410+ [[nodiscard]] QString type() const;
411+ QList<PwProfile> profiles() const;
412+ qint32 currentProfile() const;
413+
414+ Q_INVOKABLE void setProfile(qint32 profileIndex);
415+
416+ static PwDeviceIface* instance(PwDevice* node);
417+private:
418+ PwDevice* mDevice;
419+
420+ void deviceProfilesChanged(QList<PwProfile> profiles);
421+ void deviceCurrentProfileChanged(PwProfile profile);
422+};
423+
424 ///! A connection between pipewire nodes.
425 /// Note that there is one link per *channel* of a connection between nodes.
426 /// You usually want @@PwLinkGroup.
427diff --git i/src/services/pipewire/registry.cpp w/src/services/pipewire/registry.cpp
428index c08fc1d..50c6d7a 100644
429--- i/src/services/pipewire/registry.cpp
430+++ w/src/services/pipewire/registry.cpp
431@@ -196,6 +196,7 @@ void PwRegistry::onGlobal(
432 device->initProps(props);
433
434 self->devices.emplace(id, device);
435+ emit self->deviceAdded(device);
436 }
437 }
438
439@@ -211,6 +212,9 @@ void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
440 } else if (auto* node = self->nodes.value(id)) {
441 self->nodes.remove(id);
442 node->safeDestroy();
443+ } else if (auto* device = self->devices.value(id)) {
444+ self->devices.remove(id);
445+ device->safeDestroy();
446 }
447 }
448
449diff --git i/src/services/pipewire/registry.hpp w/src/services/pipewire/registry.hpp
450index 8473f04..87e0766 100644
451--- i/src/services/pipewire/registry.hpp
452+++ w/src/services/pipewire/registry.hpp
453@@ -132,6 +132,7 @@ public:
454
455 signals:
456 void nodeAdded(PwNode* node);
457+ void deviceAdded(PwDevice* node);
458 void linkAdded(PwLink* link);
459 void linkGroupAdded(PwLinkGroup* group);
460 void metadataAdded(PwMetadata* metadata);
diff --git a/user-profiles/mpv/default.nix b/user-profiles/mpv/default.nix
index 94f241c8..48893f49 100644
--- a/user-profiles/mpv/default.nix
+++ b/user-profiles/mpv/default.nix
@@ -105,6 +105,7 @@
105 }; 105 };
106 config = { 106 config = {
107 ytdl = true; 107 ytdl = true;
108 ytdl-format = "ytdl";
108 ytdl-raw-options = "sub-langs=\"${config.programs.yt-dlp.settings.sub-langs}\""; 109 ytdl-raw-options = "sub-langs=\"${config.programs.yt-dlp.settings.sub-langs}\"";
109 subs-with-matching-audio = false; 110 subs-with-matching-audio = false;
110 audio-display = false; 111 audio-display = false;
diff --git a/user-profiles/yt-dlp.nix b/user-profiles/yt-dlp.nix
index 5c9858a6..9e30bba8 100644
--- a/user-profiles/yt-dlp.nix
+++ b/user-profiles/yt-dlp.nix
@@ -7,6 +7,10 @@
7 cookies-from-browser = "firefox::none"; 7 cookies-from-browser = "firefox::none";
8 mark-watched = true; 8 mark-watched = true;
9 format = lib.concatStringsSep "/" [ 9 format = lib.concatStringsSep "/" [
10 "bestvideo*[width<=2560][height<=1440][fps<=60][vcodec!*=av01][vcodec!*=avc1]+bestaudio"
11 "best[width<=2560][height<=1440][fps<=60][vcodec!*=av01][vcodec!*=avc1]"
12 "bestvideo*[vcodec!*=av01][vcodec!*=avc1]+bestaudio"
13 "best[vcodec!*=av01][vcodec!*=avc1]"
10 "bestvideo*[width<=2560][height<=1440][fps<=60]+bestaudio" 14 "bestvideo*[width<=2560][height<=1440][fps<=60]+bestaudio"
11 "best[width<=2560][height<=1440][fps<=60]" 15 "best[width<=2560][height<=1440][fps<=60]"
12 "bestvideo*+bestaudio" 16 "bestvideo*+bestaudio"