summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--accounts/gkleen@sif/default.nix8
-rw-r--r--accounts/gkleen@sif/niri/default.nix78
-rw-r--r--accounts/gkleen@sif/niri/swayosd.nix66
-rw-r--r--accounts/gkleen@sif/shell/default.nix28
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt24
-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.cpp24
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp14
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/default.nix8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml32
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml61
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml117
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml33
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml310
-rw-r--r--accounts/gkleen@sif/shell/quickshell/LockSurface.qml223
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml235
-rw-r--r--accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml35
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml353
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml49
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml68
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml18
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml63
-rw-r--r--accounts/gkleen@sif/shell/quickshell/SystemTray.qml34
-rw-r--r--accounts/gkleen@sif/shell/quickshell/UnixIPC.qml59
-rw-r--r--accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml161
-rw-r--r--accounts/gkleen@sif/shell/quickshell/displaymanager.qml115
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml5
-rw-r--r--accounts/gkleen@sif/systemd.nix2
-rw-r--r--flake.lock45
-rw-r--r--flake.nix7
-rw-r--r--hosts/sif/default.nix2
-rw-r--r--hosts/sif/greetd/default.nix123
-rw-r--r--hosts/sif/greetd/wallpaper.pngbin6073128 -> 0 bytes
-rw-r--r--overlays/niri.nix1
-rw-r--r--overlays/quickshell/default.nix11
-rw-r--r--overlays/quickshell/greetd-response.patch16
-rw-r--r--overlays/quickshell/io.patch13
-rw-r--r--overlays/quickshell/lock-state-changed.patch12
-rw-r--r--overlays/quickshell/pipewire.patch488
-rw-r--r--overlays/swayosd/default.nix13
-rw-r--r--overlays/swayosd/exponential.patch57
-rw-r--r--user-profiles/yt-dlp.nix4
43 files changed, 2431 insertions, 623 deletions
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index 92163dca..c786c629 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -595,10 +595,10 @@ in {
595 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass) 595 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass)
596 ]; 596 ];
597 }; 597 };
598 "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 = ''
599 [Unit] 599 # [Unit]
600 After=graphical-session.target 600 # After=graphical-session.target
601 ''; 601 # '';
602 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = '' 602 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = ''
603 [Unit] 603 [Unit]
604 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..10b85169 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -10,7 +10,6 @@ let
10 makoctl = lib.getExe' config.services.mako.package "makoctl"; 10 makoctl = lib.getExe' config.services.mako.package "makoctl";
11 loginctl = lib.getExe' hostConfig.systemd.package "loginctl"; 11 loginctl = lib.getExe' hostConfig.systemd.package "loginctl";
12 systemctl = lib.getExe' hostConfig.systemd.package "systemctl"; 12 systemctl = lib.getExe' hostConfig.systemd.package "systemctl";
13 swayosd-client = lib.getExe' config.services.swayosd.package "swayosd-client";
14 13
15 focus_or_spawn = pkgs.writeShellApplication { 14 focus_or_spawn = pkgs.writeShellApplication {
16 name = "focus-or-spawn"; 15 name = "focus-or-spawn";
@@ -168,7 +167,6 @@ in {
168 imports = [ 167 imports = [
169 ./waybar.nix 168 ./waybar.nix
170 ./mako.nix 169 ./mako.nix
171 ./swayosd.nix
172 ]; 170 ];
173 171
174 options = { 172 options = {
@@ -231,25 +229,7 @@ in {
231 }; 229 };
232 230
233 config = { 231 config = {
234 systemd.user.services.xwayland-satellite = { 232 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 233
254 services.swayidle = { 234 services.swayidle = {
255 events = [ 235 events = [
@@ -957,31 +937,6 @@ in {
957 allow-when-locked = true; 937 allow-when-locked = true;
958 }; 938 };
959 939
960 "XF86MonBrightnessUp" = {
961 action = spawn swayosd-client "--brightness" "raise";
962 allow-when-locked = true;
963 };
964 "XF86MonBrightnessDown" = {
965 action = spawn swayosd-client "--brightness" "lower";
966 allow-when-locked = true;
967 };
968 "XF86AudioRaiseVolume" = {
969 action = spawn swayosd-client "--output-volume" "raise";
970 allow-when-locked = true;
971 };
972 "XF86AudioLowerVolume" = {
973 action = spawn swayosd-client "--output-volume" "lower";
974 allow-when-locked = true;
975 };
976 "XF86AudioMute" = {
977 action = spawn swayosd-client "--output-volume" "mute-toggle";
978 allow-when-locked = true;
979 };
980 "XF86AudioMicMute" = {
981 action = spawn swayosd-client "--input-volume" "mute-toggle";
982 allow-when-locked = true;
983 };
984
985 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group"; 940 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
986 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all"; 941 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
987 "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu"; 942 "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
@@ -1000,6 +955,37 @@ in {
1000 "Mod+K".action = spawn (lib.getExe' pkgs.worktime "worktime-ui"); 955 "Mod+K".action = spawn (lib.getExe' pkgs.worktime "worktime-ui");
1001 "Mod+Shift+K".action = spawn (lib.getExe' pkgs.worktime "worktime-stop"); 956 "Mod+Shift+K".action = spawn (lib.getExe' pkgs.worktime "worktime-stop");
1002 })) 957 }))
958 (lib.mapAttrsToList (name: cfg: node name [(lib.removeAttrs cfg ["action"])] [cfg.action]) (let
959 shell = obj: leaf "send-unix" [
960 { path = ''''${XDG_RUNTIME_DIR}/shell.sock''; }
961 (builtins.toJSON obj + "\n")
962 ];
963 in {
964 "XF86AudioRaiseVolume" = {
965 allow-when-locked = true;
966 action = shell { Volume.volume = "up"; };
967 };
968 "XF86AudioLowerVolume" = {
969 allow-when-locked = true;
970 action = shell { Volume.volume = "down"; };
971 };
972 "XF86AudioMute" = {
973 allow-when-locked = true;
974 action = shell { Volume.muted = "toggle"; };
975 };
976 "XF86AudioMicMute" = {
977 allow-when-locked = true;
978 action = shell { Volume."mic-muted" = "toggle"; };
979 };
980 "XF86MonBrightnessUp" = {
981 action = shell { Brightness = "up"; };
982 allow-when-locked = true;
983 };
984 "XF86MonBrightnessDown" = {
985 action = shell { Brightness = "down"; };
986 allow-when-locked = true;
987 };
988 }))
1003 (map ({ name, selector, spawn, key, ...}: if key != null && selector != null && spawn != null then bind key { action = focus-or-spawn-action selector name spawn; } else null) cfg.scratchspaces) 989 (map ({ name, selector, spawn, key, ...}: if key != null && selector != null && spawn != null then bind key { action = focus-or-spawn-action selector name spawn; } else null) cfg.scratchspaces)
1004 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces) 990 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces)
1005 ] 991 ]
diff --git a/accounts/gkleen@sif/niri/swayosd.nix b/accounts/gkleen@sif/niri/swayosd.nix
deleted file mode 100644
index 54ebb302..00000000
--- a/accounts/gkleen@sif/niri/swayosd.nix
+++ /dev/null
@@ -1,66 +0,0 @@
1{ pkgs, ... }:
2{
3 config = {
4 services.swayosd = {
5 enable = true;
6 topMargin = 0.4769706078;
7 stylePath = pkgs.runCommand "style.css" {
8 passAsFile = [ "src" ];
9 src = ''
10 window#osd {
11 padding: 12px 20px;
12 border-radius: 999px;
13 border: none;
14 background: rgba(0, 0, 0, 0.87);
15
16 #container {
17 margin: 16px;
18 }
19
20 image,
21 label {
22 color: rgb(255, 255, 255);
23
24 &:disabled {
25 opacity: 1;
26 color: rgb(84, 84, 84);
27 }
28 }
29
30 progressbar {
31 min-height: 6px;
32 border-radius: 999px;
33 background: transparent;
34 border: none;
35
36 trough, progress {
37 min-height: inherit;
38 border-radius: inherit;
39 border: none;
40 }
41
42 trough {
43 background: rgb(127, 127, 127);
44 }
45 progress {
46 background: rgb(255, 255, 255);
47 }
48
49 &:disabled {
50 opacity: 1;
51
52 trough {
53 background: rgb(19, 19, 19);
54 }
55 progress {
56 background: rgb(38, 38, 38);
57 }
58 }
59 }
60 }
61 '';
62 buildInputs = with pkgs; [sass];
63 } "scss -C --sourcemap=none --style=compact $srcPath $out";
64 };
65 };
66}
diff --git a/accounts/gkleen@sif/shell/default.nix b/accounts/gkleen@sif/shell/default.nix
index 84140072..5025dd90 100644
--- a/accounts/gkleen@sif/shell/default.nix
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -4,6 +4,18 @@
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 = {
@@ -76,12 +88,26 @@
76 runHook postBuild 88 runHook postBuild
77 ''; 89 '';
78 }); 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 });
79 }; 105 };
80 }; 106 };
81 }; 107 };
82 systemd.user.services.quickshell = { 108 systemd.user.services.quickshell = {
83 Service = { 109 Service = {
84 Environment = "QML_IMPORT_PATH=${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix}"; 110 RuntimeDirectory = "quickshell";
85 }; 111 };
86 }; 112 };
87 }; 113 };
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
index 2123ed35..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,16 +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 FileSelector.cpp FileSelector.hpp 107 NO_NAMESPACE TRUE
108) 108)
109 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
110target_compile_features(customplugin PUBLIC cxx_std_26) 117target_compile_features(customplugin PUBLIC cxx_std_26)
111 118
112target_link_libraries(customplugin PRIVATE 119target_link_libraries(customplugin PRIVATE
113 Qt6::Core 120 Qt6::Core
114 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}
115) 131)
116 132
117install_qml_module(customplugin) 133install_qml_module(customplugin)
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..5e607709
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
@@ -0,0 +1,24 @@
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 ) << unit << mode;
13 QDBusConnection::sessionBus().send(m);
14}
15
16void Systemd::setBrightness(const QString& subsystem, const QString& name, quint32 brightness) {
17 QDBusMessage m = QDBusMessage::createMethodCall(
18 "org.freedesktop.login1",
19 "/org/freedesktop/login1/session/auto",
20 "org.freedesktop.login1.Session",
21 "SetBrightness"
22 ) << subsystem << name << brightness;
23 QDBusConnection::systemBus().send(m);
24}
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..f8841518
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp
@@ -0,0 +1,14 @@
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 Q_INVOKABLE void setBrightness(const QString& subsystem, const QString& name, quint32 brightness);
14};
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..d52740b2 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -1,14 +1,11 @@
1import Quickshell 1import Quickshell
2import QtQuick 2import QtQuick
3 3
4
5PanelWindow { 4PanelWindow {
6 id: bar 5 id: bar
7 6
8 required property var screen 7 required property var screen
9 8
10 property var calendarMouseArea: clock.calendarMouseArea
11
12 anchors { 9 anchors {
13 top: true 10 top: true
14 left: true 11 left: true
@@ -66,22 +63,41 @@ PanelWindow {
66 anchors.verticalCenter: parent.verticalCenter 63 anchors.verticalCenter: parent.verticalCenter
67 spacing: 0 64 spacing: 0
68 65
69 SystemTray {} 66 PrivacyWidget {
67 id: privacy
68 }
70 69
71 Item { 70 Item {
71 enabled: privacy.active
72 height: parent.height 72 height: parent.height
73 width: 4 73 width: 8
74 } 74 }
75 75
76 KeyboardLayout {} 76 BatteryWidget {}
77
78 Item {
79 height: parent.height
80 width: 8
81 }
82
83 BrightnessWidget {}
77 84
78 Item { 85 Item {
79 height: parent.height 86 height: parent.height
80 width: 4 87 width: 4
81 } 88 }
82 89
83 Clock { 90 PipewireWidget {}
84 id: clock 91
92 SystemTray {}
93
94 KeyboardLayout {}
95
96 Item {
97 height: parent.height
98 width: 8 - 4
85 } 99 }
100
101 Clock {}
86 } 102 }
87} \ No newline at end of file 103} \ No newline at end of file
diff --git a/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
new file mode 100644
index 00000000..896440f1
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
@@ -0,0 +1,61 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import Quickshell.Services.UPower
5
6Item {
7 id: root
8
9 height: parent.height
10 width: batteryIcon.width
11 anchors.verticalCenter: parent.verticalCenter
12
13 property var batteryDevice: Array.from(UPower.devices.values).find(dev => dev.isLaptopBattery)
14
15 WrapperMouseArea {
16 id: widgetMouseArea
17
18 anchors.fill: parent
19
20 hoverEnabled: true
21
22 Item {
23 anchors.fill: parent
24
25 MaterialDesignIcon {
26 id: batteryIcon
27
28 implicitSize: 14
29 anchors.centerIn: parent
30
31 icon: {
32 if (!root.batteryDevice?.ready)
33 return "battery-unknown";
34
35 if (root.batteryDevice.state == UPowerDeviceState.FullyCharged)
36 return "power-plug-battery";
37
38 const perdec = 10 * Math.max(1, Math.ceil(root.batteryDevice.percentage * 10));
39 if (root.batteryDevice.state == UPowerDeviceState.Charging)
40 return `battery-charging-${perdec}`;
41 if (perdec == 100)
42 return "battery";
43 return `battery-${perdec}`;
44 }
45 color: {
46 if (!root.batteryDevice?.ready)
47 return "#555";
48
49 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 20 * 60)
50 return "#f2201f";
51 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 40 * 60)
52 return "#f28a21";
53 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged)
54 return "#fff";
55 return "#555";
56 }
57 }
58
59 }
60 }
61}
diff --git a/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml b/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml
new file mode 100644
index 00000000..a432179e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml
@@ -0,0 +1,117 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Widgets
5import qs.Services
6
7Scope {
8 id: root
9
10 property bool show: false
11 property bool inhibited: true
12
13 Connections {
14 target: Brightness
15
16 function onCurrBrightnessChanged() {
17 root.show = true;
18 hideTimer.restart();
19 }
20 }
21
22 onShowChanged: {
23 if (show)
24 hideTimer.restart();
25 }
26
27 Timer {
28 id: hideTimer
29 interval: 1000
30 onTriggered: root.show = false
31 }
32
33 Timer {
34 id: startInhibit
35 interval: 100
36 running: true
37 onTriggered: {
38 root.show = false;
39 root.inhibited = false;
40 }
41 }
42
43 LazyLoader {
44 active: root.show && !root.inhibited
45
46 Variants {
47 model: Quickshell.screens
48
49 delegate: Scope {
50 id: screenScope
51
52 required property var modelData
53
54 PanelWindow {
55 id: window
56
57 screen: screenScope.modelData
58
59 anchors.top: true
60 margins.top: screen.height / 2 - 50 + 3.5
61 exclusiveZone: 0
62 exclusionMode: ExclusionMode.Ignore
63
64 implicitWidth: 400
65 implicitHeight: 50
66
67 mask: Region {}
68
69 color: "transparent"
70
71 Rectangle {
72 anchors.fill: parent
73 color: Qt.rgba(0, 0, 0, 0.75)
74 }
75
76 RowLayout {
77 id: layout
78
79 anchors.centerIn: parent
80
81 height: 50 - 8*2
82 width: 400 - 8*2
83
84 MaterialDesignIcon {
85 id: volumeIcon
86
87 implicitWidth: parent.height
88 implicitHeight: parent.height
89
90 icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}`
91 }
92
93 Rectangle {
94 Layout.fillWidth: true
95
96 implicitHeight: 10
97
98 color: "#50ffffff"
99
100 Rectangle {
101 anchors {
102 left: parent.left
103 top: parent.top
104 bottom: parent.bottom
105 }
106
107 color: "white"
108
109 implicitWidth: parent.width * Brightness.currBrightness
110 }
111 }
112 }
113 }
114 }
115 }
116 }
117}
diff --git a/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
new file mode 100644
index 00000000..664b28e2
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
@@ -0,0 +1,33 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import qs.Services
5
6Item {
7 height: parent.height
8 width: brightnessIcon.width
9 anchors.verticalCenter: parent.verticalCenter
10
11 WrapperMouseArea {
12 id: widgetMouseArea
13
14 anchors.fill: parent
15
16 property real sensitivity: (1 / 50) / 120
17 onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity
18
19 Item {
20 anchors.fill: parent
21
22 MaterialDesignIcon {
23 id: brightnessIcon
24
25 implicitSize: 14
26 anchors.centerIn: parent
27
28 icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}`
29 color: "#555"
30 }
31 }
32 }
33}
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
index d0c9178b..4644d5e7 100644
--- a/accounts/gkleen@sif/shell/quickshell/Clock.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -22,18 +22,6 @@ Item {
22 hoverEnabled: true 22 hoverEnabled: true
23 enabled: clockItem.calendarPopup 23 enabled: clockItem.calendarPopup
24 24
25 property real angleRem: 0
26 property real sensitivity: 1 / 120
27
28 function scrollYear(event) {
29 angleRem += event.angleDelta.y;
30 const d = Math.round(angleRem * sensitivity);
31 yearCalendar.year += d;
32 angleRem -= d / sensitivity;
33 }
34
35 onWheel: event => scrollYear(event)
36
37 Item { 25 Item {
38 anchors.fill: parent 26 anchors.fill: parent
39 27
@@ -56,204 +44,234 @@ Item {
56 } 44 }
57 } 45 }
58 46
59 PopupWindow { 47 Loader {
60 id: tooltip 48 id: tooltipLoader
61 49
62 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse 50 active: false
63 51
64 anchor { 52 Connections {
65 item: clockMouseArea 53 target: clockMouseArea
66 edges: Edges.Bottom | Edges.Left 54 function onContainsMouseChanged() {
55 if (clockMouseArea.containsMouse)
56 tooltipLoader.active = true;
57 }
67 } 58 }
68 visible: false
69 59
70 onNextVisibleChanged: hangTimer.restart() 60 sourceComponent: PopupWindow {
61 id: tooltip
71 62
72 Timer { 63 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse
73 id: hangTimer
74 interval: 100
75 onTriggered: tooltip.visible = tooltip.nextVisible
76 }
77 64
78 implicitWidth: clockTooltipContent.width 65 anchor {
79 implicitHeight: clockTooltipContent.height 66 item: clockMouseArea
80 color: "black" 67 edges: Edges.Bottom | Edges.Left
68 }
69 visible: false
81 70
82 onVisibleChanged: { 71 onNextVisibleChanged: hangTimer.restart()
83 yearCalendar.year = chrono.date.getFullYear();
84 clockMouseArea.angleRem = 0;
85 }
86 72
87 WrapperMouseArea { 73 Timer {
88 id: tooltipMouseArea 74 id: hangTimer
75 interval: 100
76 onTriggered: tooltip.visible = tooltip.nextVisible
77 }
89 78
90 hoverEnabled: true 79 implicitWidth: clockTooltipContent.width
91 enabled: true 80 implicitHeight: clockTooltipContent.height
81 color: "black"
92 82
93 onWheel: event => clockMouseArea.scrollYear(event) 83 onVisibleChanged: {
84 yearCalendar.year = chrono.date.getFullYear();
85 yearCalendar.angleRem = 0;
86 }
94 87
95 anchors.fill: parent 88 WrapperMouseArea {
89 id: tooltipMouseArea
96 90
97 WrapperItem { 91 hoverEnabled: true
98 id: clockTooltipContent 92 enabled: true
99 93
100 margin: 8 94 onWheel: event => yearCalendar.scrollYear(event)
101 leftMargin: 0
102 95
103 ColumnLayout { 96 anchors.fill: parent
104 Text {
105 id: yearLabel
106 97
107 horizontalAlignment: Text.AlignHCenter 98 WrapperItem {
99 id: clockTooltipContent
108 100
109 font.pointSize: 14 101 margin: 8
110 font.family: "Fira Sans"
111 font.features: { "tnum": 1 }
112 color: "white"
113 102
114 text: yearCalendar.year 103 ColumnLayout {
104 anchors.centerIn: parent
115 105
116 Layout.fillWidth: true 106 Text {
117 Layout.bottomMargin: 8 107 id: yearLabel
118 }
119 108
120 GridLayout { 109 horizontalAlignment: Text.AlignHCenter
121 property int year: chrono.date.getFullYear()
122 110
123 id: yearCalendar 111 font.pointSize: 14
112 font.family: "Fira Sans"
113 font.features: { "tnum": 1 }
114 color: "white"
124 115
125 columns: 3 116 text: yearCalendar.year
126 columnSpacing: 16
127 rowSpacing: 16
128 117
129 Layout.alignment: Qt.AlignHCenter 118 Layout.fillWidth: true
130 Layout.fillWidth: false 119 Layout.bottomMargin: 8
120 }
131 121
132 Repeater { 122 GridLayout {
133 model: 12 123 property int year: chrono.date.getFullYear()
134 124
135 GridLayout { 125 id: yearCalendar
136 columns: 2
137 126
138 required property int index 127 columns: 3
139 property int month: index 128 columnSpacing: 16
129 rowSpacing: 16
140 130
141 id: monthCalendar 131 Layout.alignment: Qt.AlignHCenter
132 Layout.fillWidth: false
142 133
143 Layout.alignment: Qt.AlignTop | Qt.AlignRight 134 property real angleRem: 0
144 Layout.fillWidth: false 135 property real sensitivity: 1 / 120
145 136
146 Text { 137 function scrollYear(event) {
147 Layout.column: 1 138 angleRem += event.angleDelta.y;
148 Layout.fillWidth: true 139 const d = Math.round(angleRem * sensitivity);
140 yearCalendar.year += d;
141 angleRem -= d / sensitivity;
142 }
149 143
150 horizontalAlignment: Text.AlignHCenter 144 Connections {
145 target: clockMouseArea
146 function onWheel(event) { yearCalendar.scrollYear(event); }
147 }
151 148
152 font.pointSize: 10 149 Repeater {
153 font.family: "Fira Sans" 150 model: 12
154 151
155 text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM") 152 GridLayout {
153 columns: 2
156 154
157 color: "#ffead3" 155 required property int index
158 } 156 property int month: index
159 157
160 DayOfWeekRow { 158 id: monthCalendar
161 locale: grid.locale
162 159
163 Layout.row: 1 160 Layout.alignment: Qt.AlignTop | Qt.AlignRight
164 Layout.column: 1 161 Layout.fillWidth: false
165 Layout.fillWidth: true
166 162
167 delegate: WrapperItem { 163 Text {
168 required property string shortName 164 Layout.column: 1
165 Layout.fillWidth: true
169 166
170 width: dowLabel.contentWidth + 6 167 horizontalAlignment: Text.AlignHCenter
171 168
172 Text { 169 font.pointSize: 10
173 id: dowLabel 170 font.family: "Fira Sans"
174 171
175 anchors.fill: parent 172 text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM")
176 173
177 font.pointSize: 10 174 color: "#ffead3"
178 font.family: "Fira Sans" 175 }
179 176
180 text: parent.shortName 177 DayOfWeekRow {
181 color: "#ffcc66" 178 locale: grid.locale
182 179
183 horizontalAlignment: Text.AlignHCenter 180 Layout.row: 1
184 verticalAlignment: Text.AlignVCenter 181 Layout.column: 1
185 } 182 Layout.fillWidth: true
186 }
187 }
188 183
189 WeekNumberColumn { 184 delegate: WrapperItem {
190 month: grid.month 185 required property string shortName
191 year: grid.year
192 locale: grid.locale
193 186
194 Layout.fillHeight: true 187 width: dowLabel.contentWidth + 6
195 188
196 delegate: Text { 189 Text {
197 required property int weekNumber 190 id: dowLabel
198 191
199 opacity: { 192 anchors.fill: parent
200 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
201 const dayOfWeek = simple.getDay();
202 const isoWeekStart = simple;
203 193
204 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); 194 font.pointSize: 10
205 if (dayOfWeek > 4) { 195 font.family: "Fira Sans"
206 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
207 }
208 196
209 for (let i = 0; i < 7; i++) { 197 text: parent.shortName
210 const dayInWeek = new Date(isoWeekStart); 198 color: "#ffcc66"
211 dayInWeek.setDate(dayInWeek.getDate() + i);
212 if (dayInWeek.getMonth() == monthCalendar.month)
213 return 1;
214 }
215 199
216 return 0; 200 horizontalAlignment: Text.AlignHCenter
201 verticalAlignment: Text.AlignVCenter
202 }
217 } 203 }
204 }
218 205
219 font.pointSize: 10 206 WeekNumberColumn {
220 font.family: "Fira Sans" 207 month: grid.month
221 font.features: { "tnum": 1 } 208 year: grid.year
222 209 locale: grid.locale
223 text: weekNumber
224 color: "#99ffdd"
225 210
226 horizontalAlignment: Text.AlignRight 211 Layout.fillHeight: true
227 verticalAlignment: Text.AlignVCenter
228 }
229 }
230 212
231 MonthGrid { 213 delegate: Text {
232 id: grid 214 required property int weekNumber
233 215
234 year: yearCalendar.year 216 opacity: {
235 month: monthCalendar.month 217 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
236 locale: Qt.locale("en_DK") 218 const dayOfWeek = simple.getDay();
219 const isoWeekStart = simple;
237 220
238 Layout.fillWidth: true 221 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1);
239 Layout.fillHeight: true 222 if (dayOfWeek > 4) {
223 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
224 }
240 225
241 delegate: Text { 226 for (let i = 0; i < 7; i++) {
242 required property var model 227 const dayInWeek = new Date(isoWeekStart);
228 dayInWeek.setDate(dayInWeek.getDate() + i);
229 if (dayInWeek.getMonth() == monthCalendar.month)
230 return 1;
231 }
243 232
244 opacity: model.month === monthCalendar.month ? 1 : 0 233 return 0;
234 }
245 235
246 font.pointSize: 10 236 font.pointSize: 10
247 font.family: "Fira Sans" 237 font.family: "Fira Sans"
248 font.features: { "tnum": 1 } 238 font.features: { "tnum": 1 }
249 239
250 property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day 240 text: weekNumber
251 241 color: "#99ffdd"
252 text: model.day
253 color: today ? "#ff6699" : "white"
254 242
255 horizontalAlignment: Text.AlignRight 243 horizontalAlignment: Text.AlignRight
256 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 }
257 } 275 }
258 } 276 }
259 } 277 }
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
index 7cb1cc67..8e739359 100644
--- a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -2,14 +2,10 @@ import Quickshell
2import Quickshell.Wayland 2import Quickshell.Wayland
3import Quickshell.Io 3import Quickshell.Io
4import Quickshell.Services.Pam 4import Quickshell.Services.Pam
5import Quickshell.Widgets 5import Quickshell.Services.Mpris
6import QtQuick.Effects 6import Custom as Custom
7import QtQuick.Layouts
8import QtQuick
9import QtQuick.Controls
10import QtQuick.Controls.Fusion
11import qs.Services 7import qs.Services
12// import QtQml.Models 8import QtQml
13 9
14Scope { 10Scope {
15 id: lockscreen 11 id: lockscreen
@@ -45,213 +41,48 @@ Scope {
45 WlSessionLock { 41 WlSessionLock {
46 id: lock 42 id: lock
47 43
48 onLockedChanged: { 44 onLockStateChanged: {
49 if (!locked && pam.active) 45 if (!locked && pam.active)
50 pam.abort(); 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 }
51 } 57 }
52 58
53 WlSessionLockSurface { 59 WlSessionLockSurface {
54 id: lockSurface 60 id: lockSurface
55 61
56 color: "#000000" 62 color: "black"
57
58 Item {
59 id: background
60
61 anchors.fill: parent
62
63 property Img current: one
64 property string source: selector.selected
65
66 WallpaperSelector {
67 id: selector
68 seed: lockSurface.screen?.name || ""
69 }
70
71 onSourceChanged: {
72 if (!source)
73 current = null;
74 else if (current === one)
75 two.update()
76 else
77 one.update()
78 }
79
80 Img { id: one }
81 Img { id: two }
82
83 component Img: Item {
84 id: img
85
86 property string source
87
88 function update() {
89 source = background.source || ""
90 }
91
92 anchors.fill: parent
93
94 Image {
95 id: imageSource
96
97 source: img.source
98 sourceSize: Qt.size(parent.width, parent.height)
99 fillMode: Image.PreserveAspectCrop
100 smooth: true
101 visible: false
102 asynchronous: true
103 cache: false
104
105 onStatusChanged: {
106 if (status === Image.Ready) {
107 background.current = img
108 }
109 }
110 }
111
112 MultiEffect {
113 id: imageEffect
114
115 source: imageSource
116 anchors.fill: parent
117 blurEnabled: true
118 blur: 1
119 blurMax: 64
120 blurMultiplier: 2
121
122 opacity: 0
123
124 states: State {
125 name: "visible"
126 when: background.current === img
127
128 PropertyChanges {
129 imageEffect.opacity: 1
130 }
131 StateChangeScript {
132 name: "unloadOther"
133 script: {
134 if (img === one)
135 two.source = ""
136 if (img === two)
137 one.source = ""
138 }
139 }
140 }
141
142 transitions: Transition {
143 SequentialAnimation {
144 NumberAnimation {
145 target: imageEffect
146 properties: "opacity"
147 duration: 5000
148 easing.type: Easing.OutCubic
149 }
150 ScriptAction {
151 scriptName: "unloadOther"
152 }
153 }
154 }
155 }
156 }
157 }
158
159 Item {
160 anchors {
161 top: lockSurface.top
162 left: lockSurface.left
163 right: lockSurface.right
164 }
165 63
166 implicitWidth: lockSurface.width 64 LockSurface {
167 implicitHeight: 21 65 id: surfaceContent
168 66
169 Rectangle { 67 onResponse: responseText => pam.respond(responseText)
170 anchors.fill: parent 68 onAuthRunningChanged: {
171 color: Qt.rgba(0, 0, 0, 0.75) 69 if (authRunning)
70 pam.start();
172 } 71 }
173 72 Connections {
174 Clock { 73 target: pam
175 anchors.centerIn: parent 74 function onMessagesChanged() { surfaceContent.messages = pam.messages; }
176 calendarPopup: false 75 function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; }
76 function onActiveChanged() { surfaceContent.authRunning = pam.active; }
177 } 77 }
178 } 78 onCurrentTextChanged: lockscreen.currentText = currentText
179 79 Connections {
180 WrapperRectangle { 80 target: lockscreen
181 id: unlockUi 81 function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; }
182
183 Keys.onPressed: event => {
184 if (!pam.active) {
185 event.accepted = true;
186 pam.start();
187 }
188 } 82 }
189 focus: !passwordBox.visible 83 Connections {
190 84 target: lockSurface
191 visible: pam.active 85 function onScreenChanged() { surfaceContent.screen = lockSurface.screen; }
192
193 color: Qt.rgba(0, 0, 0, 0.75)
194 margin: 8
195
196 anchors.centerIn: parent
197
198 ColumnLayout {
199 spacing: 4
200
201 BusyIndicator {
202 visible: running
203 running: !Array.from(pam.messages).length && !pam.responseRequired
204 }
205
206 Repeater {
207 model: pam.messages
208
209 Text {
210 required property var modelData
211
212 font.pointSize: 10
213 font.family: "Fira Sans"
214 color: modelData.error ? "#f28a21" : "#ffffff"
215
216 text: modelData.text
217
218 Layout.fillWidth: true
219 horizontalAlignment: Text.AlignHCenter
220 }
221 }
222
223 TextField {
224 id: passwordBox
225
226 visible: pam.responseRequired
227 echoMode: pam.responseVisible ? TextInput.Normal : TextInput.Password
228 inputMethodHints: Qt.ImhSensitiveData
229
230 onTextChanged: lockscreen.currentText = passwordBox.text
231 onAccepted: {
232 passwordBox.readOnly = true;
233 pam.respond(lockscreen.currentText);
234 }
235
236 Connections {
237 target: lockscreen
238 function onCurrentTextChanged() {
239 passwordBox.text = lockscreen.currentText
240 }
241 }
242 Connections {
243 target: pam
244 function onResponseRequiredChanged() {
245 if (pam.responseRequired)
246 passwordBox.readOnly = false;
247 passwordBox.focus = true;
248 passwordBox.selectAll();
249 }
250 }
251
252 Layout.topMargin: 4
253 Layout.fillWidth: true
254 }
255 } 86 }
256 } 87 }
257 } 88 }
diff --git a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
new file mode 100644
index 00000000..155a009e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
@@ -0,0 +1,35 @@
1import QtQuick
2import QtQuick.Effects
3
4Item {
5 id: root
6
7 required property string icon
8 property color color: "white"
9
10 property real implicitSize: 0
11
12 readonly property real actualSize: Math.min(root.width, root.height)
13
14 implicitWidth: root.implicitSize
15 implicitHeight: root.implicitSize
16
17 Image {
18 id: sourceImage
19 source: "file://" + @mdi@ + "/svg/" + root.icon + ".svg"
20 anchors.fill: parent
21 fillMode: Image.PreserveAspectFit
22
23 sourceSize.width: root.actualSize
24 sourceSize.height: root.actualSize
25
26 layer.enabled: true
27 layer.effect: MultiEffect {
28 id: effect
29
30 brightness: 1
31 colorization: 1
32 colorizationColor: root.color
33 }
34 }
35}
diff --git a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
new file mode 100644
index 00000000..007ce100
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -0,0 +1,353 @@
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 implicitSize: 14
54 anchors.centerIn: parent
55
56 icon: {
57 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
58 return "volume-off";
59 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
60 return "volume-low";
61 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
62 return "volume-medium";
63 return "volume-high";
64 }
65 color: "#555"
66 }
67 }
68 }
69 }
70
71 Loader {
72 id: tooltipLoader
73
74 active: false
75
76 Connections {
77 target: widgetMouseArea
78 function onContainsMouseChanged() {
79 if (widgetMouseArea.containsMouse)
80 tooltipLoader.active = true;
81 }
82 }
83
84 PwObjectTracker {
85 objects: Pipewire.devices
86 }
87 PwObjectTracker {
88 objects: Pipewire.nodes
89 }
90
91 sourceComponent: PopupWindow {
92 id: tooltip
93
94 property bool openPopup: false
95 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup
96
97 anchor {
98 item: widgetMouseArea
99 edges: Edges.Bottom | Edges.Left
100 }
101 visible: false
102
103 onNextVisibleChanged: hangTimer.restart()
104
105 Timer {
106 id: hangTimer
107 interval: 100
108 onTriggered: {
109 tooltip.visible = tooltip.nextVisible;
110 if (!tooltip.visible)
111 tooltipLoader.active = false;
112 }
113 }
114
115 implicitWidth: tooltipContent.width
116 implicitHeight: tooltipContent.height
117 color: "black"
118
119 WrapperMouseArea {
120 id: tooltipMouseArea
121
122 hoverEnabled: true
123 enabled: true
124
125 anchors.fill: parent
126
127 WrapperItem {
128 id: tooltipContent
129
130 margin: 8
131 bottomMargin: 8 + Math.max(0, 200 - tooltipLayout.implicitHeight)
132
133 GridLayout {
134 id: tooltipLayout
135
136 columns: 4
137
138 Repeater {
139 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
140
141 Item {
142 id: descItem
143
144 required property var modelData
145 required property int index
146
147 Layout.column: 0
148 Layout.row: index
149
150 implicitWidth: descText.contentWidth
151 implicitHeight: descText.contentHeight
152
153 Text {
154 id: descText
155
156 color: "white"
157 font.pointSize: 10
158 font.family: "Fira Sans"
159
160 text: descItem.modelData.description
161 }
162 }
163 }
164
165 Repeater {
166 id: defaultSinkRepeater
167
168 model: {
169 Array.from(Pipewire.devices.values)
170 .filter(dev => dev.type == "Audio/Device")
171 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
172 }
173
174 Item {
175 id: defaultSinkItem
176
177 required property var modelData
178 required property int index
179
180 PwObjectTracker {
181 objects: [defaultSinkItem.modelData]
182 }
183
184 Layout.column: 1
185 Layout.row: index
186
187 Layout.fillHeight: true
188
189 implicitWidth: 16 + 8
190
191 WrapperMouseArea {
192 id: defaultSinkMouseArea
193
194 anchors.fill: parent
195 hoverEnabled: true
196 cursorShape: Qt.PointingHandCursor
197
198 onClicked: {
199 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
200 }
201
202 Rectangle {
203 id: defaultSinkWidget
204
205 anchors.fill: parent
206 color: {
207 if (defaultSinkMouseArea.containsMouse)
208 return "#33808080";
209 return "transparent";
210 }
211
212 MaterialDesignIcon {
213 width: 16
214 height: 16
215 anchors.centerIn: parent
216
217 icon: {
218 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id)
219 return "speaker";
220 return "speaker-off";
221 }
222 color: icon == "speaker" ? "white" : "#555"
223 }
224 }
225 }
226 }
227 }
228
229 Repeater {
230 id: defaultSourceRepeater
231
232 model: {
233 Array.from(Pipewire.devices.values)
234 .filter(dev => dev.type == "Audio/Device")
235 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
236 }
237
238 Item {
239 id: defaultSourceItem
240
241 required property var modelData
242 required property int index
243
244 PwObjectTracker {
245 objects: [defaultSourceItem.modelData]
246 }
247
248 Layout.column: 2
249 Layout.row: index
250
251 Layout.fillHeight: true
252
253 implicitWidth: 16 + 8
254
255 WrapperMouseArea {
256 id: defaultSourceMouseArea
257
258 anchors.fill: parent
259 hoverEnabled: true
260 cursorShape: Qt.PointingHandCursor
261
262 onClicked: {
263 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
264 }
265
266 Rectangle {
267 id: defaultSourceWidget
268
269 anchors.fill: parent
270 color: {
271 if (defaultSourceMouseArea.containsMouse)
272 return "#33808080";
273 return "transparent";
274 }
275
276 MaterialDesignIcon {
277 width: 16
278 height: 16
279 anchors.centerIn: parent
280
281 icon: {
282 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
283 return "microphone";
284 return "microphone-off";
285 }
286 color: icon == "microphone" ? "white" : "#555"
287 }
288 }
289 }
290 }
291 }
292
293 Repeater {
294 id: profileRepeater
295
296 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
297
298 Item {
299 id: profileItem
300
301 required property var modelData
302 required property int index
303
304 PwObjectTracker {
305 objects: [profileItem.modelData]
306 }
307
308 Layout.column: 3
309 Layout.row: index
310
311 Layout.fillWidth: true
312
313 implicitWidth: Math.max(profileBox.implicitWidth, 300)
314 implicitHeight: profileBox.height
315
316 ComboBox {
317 id: profileBox
318
319 model: profileItem.modelData.profiles
320
321 textRole: "description"
322 valueRole: "index"
323 onActivated: profileItem.modelData.setProfile(currentValue)
324
325 anchors.fill: parent
326
327 implicitContentWidthPolicy: ComboBox.WidestText
328
329 Connections {
330 target: profileItem.modelData
331 function onCurrentProfileChanged() {
332 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
333 }
334 }
335 Component.onCompleted: {
336 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
337 }
338
339 Connections {
340 target: profileBox.popup
341 function onVisibleChanged() {
342 tooltip.openPopup = profileBox.popup.visible
343 }
344 }
345 }
346 }
347 }
348 }
349 }
350 }
351 }
352 }
353}
diff --git a/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml b/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml
new file mode 100644
index 00000000..bb02528b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml
@@ -0,0 +1,49 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Widgets
5import qs.Services
6
7Item {
8 height: parent.height
9 width: layout.childrenRect.width
10 anchors.verticalCenter: parent.verticalCenter
11
12 readonly property bool active: Boolean(Privacy.activeItems)
13
14 RowLayout {
15 id: layout
16
17 anchors.fill: parent
18
19 spacing: 8
20
21 Repeater {
22 model: Privacy.activeItems
23
24 Item {
25 id: privacyItem
26
27 required property var modelData;
28
29 height: parent.height
30 width: icon.width
31
32 MaterialDesignIcon {
33 id: icon
34
35 implicitSize: 14
36 anchors.centerIn: parent
37
38 icon: {
39 if (privacyItem.modelData == Privacy.Item.Microphone)
40 return "microphone";
41 if (privacyItem.modelData == Privacy.Item.Screensharing)
42 return "monitor-share";
43 }
44 color: "#f2201f"
45 }
46 }
47 }
48 }
49}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
new file mode 100644
index 00000000..87c7c05b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
@@ -0,0 +1,68 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Io
6import Custom as Custom
7
8Singleton {
9 id: root
10
11 property string subsystem: "backlight"
12 property string device: "intel_backlight"
13
14 property real currBrightness
15 property real exponent: 4
16
17 function calcCurrBrightness() {
18 if (!currFile.loaded || !maxFile.loaded)
19 return undefined;
20 const curr = Number(currFile.text());
21 const max = Number(maxFile.text());
22 const val = Math.pow(curr / max, 1 / root.exponent);
23 return val;
24 }
25
26 Component.onCompleted: root.currBrightness = root.calcCurrBrightness()
27 Connections {
28 target: currFile
29 function onLoaded() { root.currBrightness = root.calcCurrBrightness(); }
30 }
31 Connections {
32 target: maxFile
33 function onLoaded() { root.currBrightness = root.calcCurrBrightness(); }
34 }
35
36 onCurrBrightnessChanged: {
37 root.currBrightness = Math.max(0, Math.min(1, root.currBrightness));
38
39 const prev = root.calcCurrBrightness();
40 if (typeof prev === 'undefined' || Math.abs(root.currBrightness - prev) < 0.01)
41 return;
42
43 const max = Number(maxFile.text());
44 const actual = Number(currFile.text());
45 let curr = Math.max(0, Math.min(max, Math.pow(root.currBrightness, root.exponent) * max));
46 if (Math.round(curr) == actual && curr < actual)
47 curr = Math.max(0, actual - 1);
48 else if (Math.round(curr) == actual && curr > actual)
49 curr = Math.min(max, actual + 1);
50 // root.currBrightness = Math.pow(curr / max, 1 / root.exponent);
51 Custom.Systemd.setBrightness(root.subsystem, root.device, Math.round(curr));
52 }
53
54 FileView {
55 id: currFile
56 path: `/sys/class/${root.subsystem}/${root.device}/brightness`
57 blockAllReads: true
58 watchChanges: true
59 onFileChanged: reload()
60 }
61 FileView {
62 id: maxFile
63 path: `/sys/class/${root.subsystem}/${root.device}/max_brightness`
64 blockAllReads: true
65 watchChanges: true
66 onFileChanged: reload()
67 }
68}
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/Privacy.qml b/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml
new file mode 100644
index 00000000..9c813e49
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml
@@ -0,0 +1,63 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Services.Pipewire
6
7Singleton {
8 id: root
9
10 PwObjectTracker {
11 objects: Pipewire.nodes.values
12 }
13
14 enum Item {
15 Microphone,
16 Screensharing
17 }
18
19 readonly property list<var> activeItems: {
20 var items = [];
21 if (microphoneActive)
22 items.push(Privacy.Item.Microphone);
23 if (screensharingActive)
24 items.push(Privacy.Item.Screensharing);
25 return items;
26 }
27
28 readonly property bool microphoneActive: {
29 if (!Pipewire.ready || !Pipewire.nodes?.values) {
30 return false
31 }
32
33 for (const node of Pipewire.nodes.values) {
34 if (!node || (node.type & PwNodeType.AudioInStream) != PwNodeType.AudioInStream)
35 continue;
36
37 if (node.properties?.["stream.monitor"] === "true")
38 continue;
39
40 if (node.audio?.muted)
41 continue;
42
43 return true;
44 }
45
46 return false;
47 }
48
49 readonly property bool screensharingActive: {
50 if (!Pipewire.ready || !Pipewire.nodes?.values) {
51 return false
52 }
53
54 for (const node of Pipewire.nodes.values) {
55 if (!node || (node.type & PwNodeType.VideoInStream) != PwNodeType.VideoInStream)
56 continue;
57
58 return true;
59 }
60
61 return false;
62 }
63}
diff --git a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
index 55b1690e..6f70be29 100644
--- a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
+++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
@@ -47,7 +47,7 @@ Item {
47 return "" 47 return ""
48 } 48 }
49 49
50 width: 16 50 width: icon.width + 6
51 height: parent.height 51 height: parent.height
52 anchors.verticalCenter: parent.verticalCenter 52 anchors.verticalCenter: parent.verticalCenter
53 53
@@ -83,14 +83,24 @@ Item {
83 } 83 }
84 } 84 }
85 85
86 IconImage { 86 Rectangle {
87 anchors.centerIn: parent 87 anchors.fill: parent
88 width: parent.width 88 color: {
89 height: parent.width 89 if (trayItemArea.containsMouse)
90 source: trayItemWrapper.iconSource 90 return "#33808080";
91 asynchronous: true 91 return "transparent";
92 smooth: true 92 }
93 mipmap: true 93
94 IconImage {
95 id: icon
96
97 anchors.centerIn: parent
98 implicitSize: 16
99 source: trayItemWrapper.iconSource
100 asynchronous: true
101 smooth: true
102 mipmap: true
103 }
94 } 104 }
95 } 105 }
96 106
@@ -116,7 +126,7 @@ Item {
116 color: "black" 126 color: "black"
117 127
118 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16 128 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16
119 implicitHeight: tooltipTitle.contentHeight + tooltipDescription.contentHeight + 16 129 implicitHeight: (trayItem.tooltipTitle ? tooltipTitle.contentHeight : 0) + (trayItem.tooltipDescription ? tooltipDescription.contentHeight : 0) + 16
120 130
121 WrapperMouseArea { 131 WrapperMouseArea {
122 id: tooltipMouseArea 132 id: tooltipMouseArea
@@ -130,6 +140,8 @@ Item {
130 Text { 140 Text {
131 id: tooltipTitle 141 id: tooltipTitle
132 142
143 enabled: trayItem.tooltipTitle
144
133 font.pointSize: 10 145 font.pointSize: 10
134 font.family: "Fira Sans" 146 font.family: "Fira Sans"
135 font.bold: true 147 font.bold: true
@@ -141,6 +153,8 @@ Item {
141 Text { 153 Text {
142 id: tooltipDescription 154 id: tooltipDescription
143 155
156 enabled: trayItem.tooltipDescription
157
144 font.pointSize: 10 158 font.pointSize: 10
145 font.family: "Fira Sans" 159 font.family: "Fira Sans"
146 color: "white" 160 color: "white"
diff --git a/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
new file mode 100644
index 00000000..742ef4f5
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
@@ -0,0 +1,59 @@
1import Quickshell
2import Quickshell.Io
3import Quickshell.Services.Pipewire
4import qs.Services
5
6Scope {
7 id: root
8
9 SocketServer {
10 active: true
11 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/shell.sock`
12 handler: Socket {
13 parser: SplitParser {
14 onRead: line => {
15 try {
16 const command = JSON.parse(line);
17
18 if (command.Volume)
19 root.onCommandVolume(command.Volume);
20 else if (command.Brightness)
21 root.onCommandBrightness(command.Brightness);
22 else
23 console.warn("UnixIPC: Command not handled:", JSON.stringify(command));
24 } catch (e) {
25 console.warn("UnixIPC: Failed to parse command:", line, e);
26 }
27 }
28 }
29
30 onError: e => {
31 if (e == 1)
32 return;
33 console.warn("QLocalSocket::LocalSocketError", e);
34 }
35 }
36 }
37
38 PwObjectTracker {
39 objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ]
40 }
41 function onCommandVolume(command) {
42 if (command.muted === "toggle")
43 Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
44 if (command.volume === "up")
45 Pipewire.defaultAudioSink.audio.volume += 0.02;
46 if (command.volume === "down")
47 Pipewire.defaultAudioSink.audio.volume -= 0.02;
48
49 if (command["mic-muted"] === "toggle")
50 Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted;
51 }
52
53 function onCommandBrightness(command) {
54 if (command === "up")
55 Brightness.currBrightness += 0.02
56 if (command === "down")
57 Brightness.currBrightness -= 0.02
58 }
59}
diff --git a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
new file mode 100644
index 00000000..16fb5dea
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
@@ -0,0 +1,161 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Services.Pipewire
5import Quickshell.Widgets
6
7Scope {
8 id: root
9
10 property string show: ""
11 property bool inhibited: true
12
13 PwObjectTracker {
14 objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ]
15 }
16
17 Connections {
18 enabled: Pipewire.defaultAudioSink
19 target: Pipewire.defaultAudioSink?.audio
20
21 function onVolumeChanged() {
22 root.show = "sink";
23 hideTimer.restart();
24 }
25 function onMutedChanged() {
26 root.show = "sink";
27 hideTimer.restart();
28 }
29 }
30
31 Connections {
32 enabled: Pipewire.defaultAudioSource
33 target: Pipewire.defaultAudioSource?.audio
34
35 function onVolumeChanged() {
36 root.show = "source";
37 hideTimer.restart();
38 }
39 function onMutedChanged() {
40 root.show = "source";
41 hideTimer.restart();
42 }
43 }
44
45 onShowChanged: {
46 if (show)
47 hideTimer.restart();
48 }
49
50 Timer {
51 id: hideTimer
52 interval: 1000
53 onTriggered: root.show = ""
54 }
55
56 Timer {
57 id: startInhibit
58 interval: 100
59 running: true
60 onTriggered: {
61 root.show = "";
62 root.inhibited = false;
63 }
64 }
65
66 LazyLoader {
67 active: root.show && !root.inhibited
68
69 Variants {
70 model: Quickshell.screens
71
72 delegate: Scope {
73 id: screenScope
74
75 required property var modelData
76
77 PanelWindow {
78 id: window
79
80 screen: screenScope.modelData
81
82 anchors.top: true
83 margins.top: screen.height / 2 - 50 + 3.5
84 exclusiveZone: 0
85 exclusionMode: ExclusionMode.Ignore
86
87 implicitWidth: 400
88 implicitHeight: 50
89
90 mask: Region {}
91
92 color: "transparent"
93
94 Rectangle {
95 anchors.fill: parent
96 color: Qt.rgba(0, 0, 0, 0.75)
97 }
98
99 RowLayout {
100 id: layout
101
102 anchors.centerIn: parent
103
104 height: 50 - 8*2
105 width: 400 - 8*2
106
107 MaterialDesignIcon {
108 id: volumeIcon
109
110 implicitWidth: parent.height
111 implicitHeight: parent.height
112
113 icon: {
114 if (root.show == "sink") {
115 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
116 return "volume-off";
117 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
118 return "volume-low";
119 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
120 return "volume-medium";
121 return "volume-high";
122 } else if (root.show == "source") {
123 if (!Pipewire.defaultAudioSource || Pipewire.defaultAudioSource.audio.muted)
124 return "microphone-off";
125 return "microphone";
126 }
127 return "volume-high";
128 }
129 }
130
131 Rectangle {
132 Layout.fillWidth: true
133
134 implicitHeight: 10
135
136 color: "#50ffffff"
137
138 Rectangle {
139 anchors {
140 left: parent.left
141 top: parent.top
142 bottom: parent.bottom
143 }
144
145 color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white"
146
147 implicitWidth: {
148 if (root.show == "sink")
149 return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0);
150 else if (root.show == "source")
151 return parent.width * (Pipewire.defaultAudioSource?.audio.volume ?? 0);
152 return 0;
153 }
154 }
155 }
156 }
157 }
158 }
159 }
160 }
161}
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 1da9457d..0fa45f79 100644
--- a/accounts/gkleen@sif/shell/quickshell/shell.qml
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -41,4 +41,9 @@ ShellRoot {
41 } 41 }
42 42
43 Lockscreen {} 43 Lockscreen {}
44
45 VolumeOSD {}
46 BrightnessOSD {}
47
48 UnixIPC {}
44} 49}
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/flake.lock b/flake.lock
index 6046d92f..05385ecd 100644
--- a/flake.lock
+++ b/flake.lock
@@ -507,11 +507,11 @@
507 "xwayland-satellite-unstable": "xwayland-satellite-unstable" 507 "xwayland-satellite-unstable": "xwayland-satellite-unstable"
508 }, 508 },
509 "locked": { 509 "locked": {
510 "lastModified": 1755424351, 510 "lastModified": 1757437545,
511 "narHash": "sha256-xcorYLNdtLpb0wH5CPlUcpmYQUxeK95j1X855xQw+DY=", 511 "narHash": "sha256-7ssbrFnmSrqtCtOySiu5ncyOBxPrR6p2nhNHrg6D+fo=",
512 "owner": "sodiboo", 512 "owner": "sodiboo",
513 "repo": "niri-flake", 513 "repo": "niri-flake",
514 "rev": "9aa137af01f05386e5bb5050e983750017007a66", 514 "rev": "ef694b996daeeb8684c0adfaa9b7067a6e709054",
515 "type": "github" 515 "type": "github"
516 }, 516 },
517 "original": { 517 "original": {
@@ -524,16 +524,16 @@
524 "niri-stable": { 524 "niri-stable": {
525 "flake": false, 525 "flake": false,
526 "locked": { 526 "locked": {
527 "lastModified": 1748151941, 527 "lastModified": 1756556321,
528 "narHash": "sha256-z4viQZLgC2bIJ3VrzQnR+q2F3gAOEQpU1H5xHtX/2fs=", 528 "narHash": "sha256-RLD89dfjN0RVO86C/Mot0T7aduCygPGaYbog566F0Qo=",
529 "owner": "YaLTeR", 529 "owner": "YaLTeR",
530 "repo": "niri", 530 "repo": "niri",
531 "rev": "8ba57fcf25d2fc9565131684a839d58703f1dae7", 531 "rev": "01be0e65f4eb91a9cd624ac0b76aaeab765c7294",
532 "type": "github" 532 "type": "github"
533 }, 533 },
534 "original": { 534 "original": {
535 "owner": "YaLTeR", 535 "owner": "YaLTeR",
536 "ref": "v25.05.1", 536 "ref": "v25.08",
537 "repo": "niri", 537 "repo": "niri",
538 "type": "github" 538 "type": "github"
539 } 539 }
@@ -541,15 +541,16 @@
541 "niri-unstable": { 541 "niri-unstable": {
542 "flake": false, 542 "flake": false,
543 "locked": { 543 "locked": {
544 "lastModified": 1755419373, 544 "lastModified": 1757438446,
545 "narHash": "sha256-EFH3zbpyLYjEboNV2Lmkxf9joEuFCmeYX+MMLRPStpg=", 545 "narHash": "sha256-4NN+weI4isQCFB+A36J+CU8tb251maVlBO9usuzgMQ8=",
546 "owner": "YaLTeR", 546 "owner": "gkleen",
547 "repo": "niri", 547 "repo": "niri",
548 "rev": "a6febb86aa5af0df7bf2792ca027ef95a503d599", 548 "rev": "2c7f7053ce360279160a9e2366d54980def848ad",
549 "type": "github" 549 "type": "github"
550 }, 550 },
551 "original": { 551 "original": {
552 "owner": "YaLTeR", 552 "owner": "gkleen",
553 "ref": "feat/unix-sockets",
553 "repo": "niri", 554 "repo": "niri",
554 "type": "github" 555 "type": "github"
555 } 556 }
@@ -780,11 +781,11 @@
780 }, 781 },
781 "nixpkgs-stable_3": { 782 "nixpkgs-stable_3": {
782 "locked": { 783 "locked": {
783 "lastModified": 1755274400, 784 "lastModified": 1757408970,
784 "narHash": "sha256-rTInmnp/xYrfcMZyFMH3kc8oko5zYfxsowaLv1LVobY=", 785 "narHash": "sha256-aSgK4BLNFFGvDTNKPeB28lVXYqVn8RdyXDNAvgGq+k0=",
785 "owner": "NixOS", 786 "owner": "NixOS",
786 "repo": "nixpkgs", 787 "repo": "nixpkgs",
787 "rev": "ad7196ae55c295f53a7d1ec39e4a06d922f3b899", 788 "rev": "d179d77c139e0a3f5c416477f7747e9d6b7ec315",
788 "type": "github" 789 "type": "github"
789 }, 790 },
790 "original": { 791 "original": {
@@ -1280,16 +1281,16 @@
1280 "xwayland-satellite-stable": { 1281 "xwayland-satellite-stable": {
1281 "flake": false, 1282 "flake": false,
1282 "locked": { 1283 "locked": {
1283 "lastModified": 1748488455, 1284 "lastModified": 1755491097,
1284 "narHash": "sha256-IiLr1alzKFIy5tGGpDlabQbe6LV1c9ABvkH6T5WmyRI=", 1285 "narHash": "sha256-m+9tUfsmBeF2Gn4HWa6vSITZ4Gz1eA1F5Kh62B0N4oE=",
1285 "owner": "Supreeeme", 1286 "owner": "Supreeeme",
1286 "repo": "xwayland-satellite", 1287 "repo": "xwayland-satellite",
1287 "rev": "3ba30b149f9eb2bbf42cf4758d2158ca8cceef73", 1288 "rev": "388d291e82ffbc73be18169d39470f340707edaa",
1288 "type": "github" 1289 "type": "github"
1289 }, 1290 },
1290 "original": { 1291 "original": {
1291 "owner": "Supreeeme", 1292 "owner": "Supreeeme",
1292 "ref": "v0.6", 1293 "ref": "v0.7",
1293 "repo": "xwayland-satellite", 1294 "repo": "xwayland-satellite",
1294 "type": "github" 1295 "type": "github"
1295 } 1296 }
@@ -1297,11 +1298,11 @@
1297 "xwayland-satellite-unstable": { 1298 "xwayland-satellite-unstable": {
1298 "flake": false, 1299 "flake": false,
1299 "locked": { 1300 "locked": {
1300 "lastModified": 1755219541, 1301 "lastModified": 1757179758,
1301 "narHash": "sha256-yKV6xHaPbEbh5RPxAJnb9yTs1wypr7do86hFFGQm1w8=", 1302 "narHash": "sha256-TIvyWzRt1miQj6Cf5Wy8Qz43XIZX7c4vTVwRLAT5S4Y=",
1302 "owner": "Supreeeme", 1303 "owner": "Supreeeme",
1303 "repo": "xwayland-satellite", 1304 "repo": "xwayland-satellite",
1304 "rev": "5a184d435927c3423f0ad189ea2b490578450fb7", 1305 "rev": "970728d0d9d1eada342bb8860af214b601139e58",
1305 "type": "github" 1306 "type": "github"
1306 }, 1307 },
1307 "original": { 1308 "original": {
diff --git a/flake.nix b/flake.nix
index b9382c6f..0b2ce0a8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -209,7 +209,12 @@
209 ref = "main"; 209 ref = "main";
210 inputs = { 210 inputs = {
211 nixpkgs.follows = "nixpkgs"; 211 nixpkgs.follows = "nixpkgs";
212 # niri-unstable.url = "github:gkleen/niri"; 212 niri-unstable = {
213 type = "github";
214 owner = "gkleen";
215 repo = "niri";
216 ref = "feat/unix-sockets";
217 };
213 }; 218 };
214 }; 219 };
215 nix-monitored = { 220 nix-monitored = {
diff --git a/hosts/sif/default.nix b/hosts/sif/default.nix
index bd85e472..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 };
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/niri.nix b/overlays/niri.nix
index 9188ed7d..95a918b0 100644
--- a/overlays/niri.nix
+++ b/overlays/niri.nix
@@ -3,6 +3,7 @@
3 (final: prev: { 3 (final: prev: {
4 niri-unstable = prev.niri-unstable.overrideAttrs (oldAttrs: { 4 niri-unstable = prev.niri-unstable.overrideAttrs (oldAttrs: {
5 buildInputs = (oldAttrs.buildInputs or []) ++ [ final.libgbm ]; 5 buildInputs = (oldAttrs.buildInputs or []) ++ [ final.libgbm ];
6 doCheck = false;
6 }); 7 });
7 }) 8 })
8 final prev 9 final prev
diff --git a/overlays/quickshell/default.nix b/overlays/quickshell/default.nix
new file mode 100644
index 00000000..d806753f
--- /dev/null
+++ b/overlays/quickshell/default.nix
@@ -0,0 +1,11 @@
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 ./io.patch
9 ];
10 });
11}
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/io.patch b/overlays/quickshell/io.patch
new file mode 100644
index 00000000..961bdcaf
--- /dev/null
+++ b/overlays/quickshell/io.patch
@@ -0,0 +1,13 @@
1diff --git i/src/io/socket.cpp w/src/io/socket.cpp
2index 371f687..d12eaeb 100644
3--- i/src/io/socket.cpp
4+++ w/src/io/socket.cpp
5@@ -66,7 +66,7 @@ void Socket::onSocketDisconnected() {
6 }
7
8 void Socket::onSocketError(QLocalSocket::LocalSocketError error) {
9- qCWarning(logSocket) << "Socket error for" << this << error;
10+ // qCWarning(logSocket) << "Socket error for" << this << error;
11 emit this->error(error);
12 }
13
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..2d98eefc
--- /dev/null
+++ b/overlays/quickshell/pipewire.patch
@@ -0,0 +1,488 @@
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/node.cpp w/src/services/pipewire/node.cpp
215index 3e68149..4721a58 100644
216--- i/src/services/pipewire/node.cpp
217+++ w/src/services/pipewire/node.cpp
218@@ -145,6 +145,10 @@ void PwNode::initProps(const spa_dict* props) {
219 this->type = PwNodeType::VideoSink;
220 } else if (strcmp(mediaClass, "Video/Source") == 0) {
221 this->type = PwNodeType::VideoSource;
222+ } else if (strcmp(mediaClass, "Stream/Output/Video") == 0) {
223+ this->type = PwNodeType::VideoOutStream;
224+ } else if (strcmp(mediaClass, "Stream/Input/Video") == 0) {
225+ this->type = PwNodeType::VideoInStream;
226 }
227 }
228
229diff --git i/src/services/pipewire/node.hpp w/src/services/pipewire/node.hpp
230index 0d4c92e..ee6f223 100644
231--- i/src/services/pipewire/node.hpp
232+++ w/src/services/pipewire/node.hpp
233@@ -144,6 +144,8 @@ public:
234 // This is equivalent to the media class `Video/Sink` and is composed of the
235 // @@PwNodeType.Video and @@PwNodeType.Sink flags.
236 VideoSink = Video | Sink,
237+ VideoOutStream = Video | Sink | Stream,
238+ VideoInStream = Video | Source | Stream,
239 };
240 Q_ENUM(Flag);
241 Q_DECLARE_FLAGS(Flags, Flag);
242diff --git i/src/services/pipewire/qml.cpp w/src/services/pipewire/qml.cpp
243index 9efb17e..921d12a 100644
244--- i/src/services/pipewire/qml.cpp
245+++ w/src/services/pipewire/qml.cpp
246@@ -9,6 +9,9 @@
247 #include <qtypes.h>
248 #include <qvariant.h>
249
250+#include <cstdint>
251+#include <algorithm>
252+
253 #include "../../core/model.hpp"
254 #include "connection.hpp"
255 #include "defaults.hpp"
256@@ -54,6 +57,12 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
257
258 QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
259
260+ for (auto* device: connection->registry.devices.values()) {
261+ this->onDeviceAdded(device);
262+ }
263+
264+ QObject::connect(&connection->registry, &PwRegistry::deviceAdded, this, &Pipewire::onDeviceAdded);
265+
266 for (auto* link: connection->registry.links.values()) {
267 this->onLinkAdded(link);
268 }
269@@ -123,6 +132,19 @@ void Pipewire::onNodeRemoved(QObject* object) {
270 this->mNodes.removeObject(iface);
271 }
272
273+ObjectModel<PwDeviceIface>* Pipewire::devices() { return &this->mDevices; }
274+
275+void Pipewire::onDeviceAdded(PwDevice* device) {
276+ auto* iface = PwDeviceIface::instance(device);
277+ QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onDeviceRemoved);
278+ this->mDevices.insertObject(iface);
279+}
280+
281+void Pipewire::onDeviceRemoved(QObject* object) {
282+ auto* iface = static_cast<PwDeviceIface*>(object); // NOLINT
283+ this->mDevices.removeObject(iface);
284+}
285+
286 ObjectModel<PwLinkIface>* Pipewire::links() { return &this->mLinks; }
287
288 void Pipewire::onLinkAdded(PwLink* link) {
289@@ -357,6 +379,8 @@ QVariantMap PwNodeIface::properties() const {
290
291 PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
292
293+PwDeviceIface* PwNodeIface::device() const { return PwDeviceIface::instance(this->mNode->device); }
294+
295 PwNodeIface* PwNodeIface::instance(PwNode* node) {
296 if (node == nullptr) return nullptr;
297
298@@ -481,4 +505,42 @@ void PwObjectTracker::objectDestroyed(QObject* object) {
299 emit this->objectsChanged();
300 }
301
302+PwDeviceIface::PwDeviceIface(PwDevice* device): PwObjectIface(device), mDevice(device) {
303+ QObject::connect(device, &PwDevice::profilesChanged, this, &PwDeviceIface::deviceProfilesChanged);
304+ QObject::connect(device, &PwDevice::currentProfileChanged, this, &PwDeviceIface::deviceCurrentProfileChanged);
305+}
306+
307+void PwDeviceIface::deviceProfilesChanged(QList<PwProfile>) { emit this->profilesChanged(); }
308+void PwDeviceIface::deviceCurrentProfileChanged(PwProfile) { emit this->currentProfileChanged(); }
309+
310+quint32 PwDeviceIface::id() const { return this->mDevice->id; }
311+QString PwDeviceIface::name() const { return this->mDevice->name; }
312+QString PwDeviceIface::description() const { return this->mDevice->description; }
313+QString PwDeviceIface::nickname() const { return this->mDevice->nick; }
314+QString PwDeviceIface::type() const { return this->mDevice->type; }
315+QList<PwProfile> PwDeviceIface::profiles() const {
316+ QList<PwProfile> profiles = this->mDevice->profiles.values();
317+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
318+ return profiles;
319+}
320+qint32 PwDeviceIface::currentProfile() const { return this->mDevice->currentProfile->index; }
321+
322+PwDeviceIface* PwDeviceIface::instance(PwDevice* device) {
323+ if (device == nullptr) return nullptr;
324+
325+ auto v = device->property("iface");
326+ if (v.canConvert<PwDeviceIface*>()) {
327+ return v.value<PwDeviceIface*>();
328+ }
329+
330+ auto* instance = new PwDeviceIface(device);
331+ device->setProperty("iface", QVariant::fromValue(instance));
332+
333+ return instance;
334+}
335+
336+void PwDeviceIface::setProfile(qint32 profileIndex) {
337+ this->mDevice->setProfile(profileIndex);
338+}
339+
340 } // namespace qs::service::pipewire
341diff --git i/src/services/pipewire/qml.hpp w/src/services/pipewire/qml.hpp
342index e3489a1..e5e1891 100644
343--- i/src/services/pipewire/qml.hpp
344+++ w/src/services/pipewire/qml.hpp
345@@ -12,11 +12,13 @@
346 #include "../../core/model.hpp"
347 #include "link.hpp"
348 #include "node.hpp"
349+#include "device.hpp"
350 #include "registry.hpp"
351
352 namespace qs::service::pipewire {
353
354 class PwNodeIface;
355+class PwDeviceIface;
356 class PwLinkIface;
357 class PwLinkGroupIface;
358
359@@ -65,6 +67,8 @@ class Pipewire: public QObject {
360 /// - @@PwNode.audio - if non null the node is an audio node.
361 QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwNodeIface>*);
362 Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT);
363+ QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwDeviceIface>*);
364+ Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
365 /// All links present in pipewire.
366 ///
367 /// Links connect pipewire nodes to each other, and can be used to determine
368@@ -134,6 +138,7 @@ public:
369 explicit Pipewire(QObject* parent = nullptr);
370
371 [[nodiscard]] ObjectModel<PwNodeIface>* nodes();
372+ [[nodiscard]] ObjectModel<PwDeviceIface>* devices();
373 [[nodiscard]] ObjectModel<PwLinkIface>* links();
374 [[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
375
376@@ -159,7 +164,9 @@ signals:
377
378 private slots:
379 void onNodeAdded(PwNode* node);
380+ void onDeviceAdded(PwDevice* node);
381 void onNodeRemoved(QObject* object);
382+ void onDeviceRemoved(QObject* object);
383 void onLinkAdded(PwLink* link);
384 void onLinkRemoved(QObject* object);
385 void onLinkGroupAdded(PwLinkGroup* group);
386@@ -167,6 +174,7 @@ private slots:
387
388 private:
389 ObjectModel<PwNodeIface> mNodes {this};
390+ ObjectModel<PwDeviceIface> mDevices {this};
391 ObjectModel<PwLinkIface> mLinks {this};
392 ObjectModel<PwLinkGroupIface> mLinkGroups {this};
393 };
394@@ -315,6 +323,7 @@ class PwNodeIface: public PwObjectIface {
395 /// > [!NOTE] The node may be used before it is fully bound, but some data
396 /// > may be missing or incorrect.
397 Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
398+ Q_PROPERTY(qs::service::pipewire::PwDeviceIface* device READ device CONSTANT);
399 QML_NAMED_ELEMENT(PwNode);
400 QML_UNCREATABLE("PwNodes cannot be created directly");
401
402@@ -332,6 +341,7 @@ public:
403 [[nodiscard]] PwNodeType::Flags type() const;
404 [[nodiscard]] QVariantMap properties() const;
405 [[nodiscard]] PwNodeAudioIface* audio() const;
406+ [[nodiscard]] PwDeviceIface* device() const;
407
408 static PwNodeIface* instance(PwNode* node);
409
410@@ -344,6 +354,44 @@ private:
411 PwNodeAudioIface* audioIface = nullptr;
412 };
413
414+class PwDeviceIface: public PwObjectIface {
415+ Q_OBJECT;
416+ Q_PROPERTY(quint32 id READ id CONSTANT);
417+ Q_PROPERTY(QString name READ name CONSTANT);
418+ Q_PROPERTY(QString description READ description CONSTANT);
419+ Q_PROPERTY(QString nickname READ nickname CONSTANT);
420+ Q_PROPERTY(QString type READ type CONSTANT);
421+ Q_PROPERTY(QList<PwProfile> profiles READ profiles NOTIFY profilesChanged);
422+ Q_PROPERTY(qint32 currentProfile READ currentProfile NOTIFY currentProfileChanged);
423+
424+ QML_NAMED_ELEMENT(PwDevice);
425+ QML_UNCREATABLE("PwDevices cannot be created directly");
426+
427+signals:
428+ void profilesChanged();
429+ void currentProfileChanged();
430+
431+public:
432+ explicit PwDeviceIface(PwDevice* node);
433+
434+ [[nodiscard]] quint32 id() const;
435+ [[nodiscard]] QString name() const;
436+ [[nodiscard]] QString description() const;
437+ [[nodiscard]] QString nickname() const;
438+ [[nodiscard]] QString type() const;
439+ QList<PwProfile> profiles() const;
440+ qint32 currentProfile() const;
441+
442+ Q_INVOKABLE void setProfile(qint32 profileIndex);
443+
444+ static PwDeviceIface* instance(PwDevice* node);
445+private:
446+ PwDevice* mDevice;
447+
448+ void deviceProfilesChanged(QList<PwProfile> profiles);
449+ void deviceCurrentProfileChanged(PwProfile profile);
450+};
451+
452 ///! A connection between pipewire nodes.
453 /// Note that there is one link per *channel* of a connection between nodes.
454 /// You usually want @@PwLinkGroup.
455diff --git i/src/services/pipewire/registry.cpp w/src/services/pipewire/registry.cpp
456index c08fc1d..50c6d7a 100644
457--- i/src/services/pipewire/registry.cpp
458+++ w/src/services/pipewire/registry.cpp
459@@ -196,6 +196,7 @@ void PwRegistry::onGlobal(
460 device->initProps(props);
461
462 self->devices.emplace(id, device);
463+ emit self->deviceAdded(device);
464 }
465 }
466
467@@ -211,6 +212,9 @@ void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
468 } else if (auto* node = self->nodes.value(id)) {
469 self->nodes.remove(id);
470 node->safeDestroy();
471+ } else if (auto* device = self->devices.value(id)) {
472+ self->devices.remove(id);
473+ device->safeDestroy();
474 }
475 }
476
477diff --git i/src/services/pipewire/registry.hpp w/src/services/pipewire/registry.hpp
478index 8473f04..87e0766 100644
479--- i/src/services/pipewire/registry.hpp
480+++ w/src/services/pipewire/registry.hpp
481@@ -132,6 +132,7 @@ public:
482
483 signals:
484 void nodeAdded(PwNode* node);
485+ void deviceAdded(PwDevice* node);
486 void linkAdded(PwLink* link);
487 void linkGroupAdded(PwLinkGroup* group);
488 void metadataAdded(PwMetadata* metadata);
diff --git a/overlays/swayosd/default.nix b/overlays/swayosd/default.nix
deleted file mode 100644
index 5e715dae..00000000
--- a/overlays/swayosd/default.nix
+++ /dev/null
@@ -1,13 +0,0 @@
1{ final, prev, sources, ... }: {
2 swayosd = prev.swayosd.overrideAttrs (oldAttrs: rec {
3 inherit (sources.swayosd) version src;
4 cargoDeps = prev.rustPlatform.fetchCargoVendor {
5 inherit (oldAttrs) pname;
6 inherit version src;
7 hash = "sha256-J2sl6/4+bRWlkvaTJtFsMqvvOxYtWLRjJcYWcu0loRE=";
8 };
9 patches = (oldAttrs.patches or []) ++ [
10 ./exponential.patch
11 ];
12 });
13}
diff --git a/overlays/swayosd/exponential.patch b/overlays/swayosd/exponential.patch
deleted file mode 100644
index eb90d739..00000000
--- a/overlays/swayosd/exponential.patch
+++ /dev/null
@@ -1,57 +0,0 @@
1diff --git a/src/brightness_backend/brightnessctl.rs b/src/brightness_backend/brightnessctl.rs
2index ccb0e11..740fdb6 100644
3--- a/src/brightness_backend/brightnessctl.rs
4+++ b/src/brightness_backend/brightnessctl.rs
5@@ -107,10 +107,21 @@ impl VirtualDevice {
6 }
7 }
8
9- fn set_percent(&mut self, mut val: u32) -> anyhow::Result<()> {
10- val = val.clamp(0, 100);
11- self.current = self.max.map(|max| val * max / 100);
12- let _: String = self.run(("set", &*format!("{val}%")))?;
13+ fn val_to_percent(&mut self, val: u32) -> u32 {
14+ return ((val as f64 / self.get_max() as f64).powf(0.25) * 100_f64).round() as u32;
15+ }
16+ fn percent_to_val(&mut self, perc: u32) -> u32 {
17+ return ((perc as f64 / 100_f64).powf(4_f64) * self.get_max() as f64).round() as u32;
18+ }
19+
20+ fn set_percent(&mut self, val: u32) -> anyhow::Result<()> {
21+ let new = self.percent_to_val(val);
22+ self.set_val(new)
23+ }
24+ fn set_val(&mut self, val: u32) -> anyhow::Result<()> {
25+ let curr = val.clamp(0, self.get_max());
26+ self.current = Some(curr);
27+ let _: String = self.run(("set", &*format!("{curr}")))?;
28 Ok(())
29 }
30 }
31@@ -134,20 +145,18 @@ impl BrightnessBackend for BrightnessCtl {
32
33 fn lower(&mut self, by: u32) -> anyhow::Result<()> {
34 let curr = self.get_current();
35- let max = self.get_max();
36-
37- let curr = curr * 100 / max;
38+ let mut new = self.device.val_to_percent(curr).saturating_sub(by);
39+ new = self.device.percent_to_val(new).min(curr.saturating_sub(1));
40
41- self.device.set_percent(curr.saturating_sub(by))
42+ self.device.set_val(new)
43 }
44
45 fn raise(&mut self, by: u32) -> anyhow::Result<()> {
46 let curr = self.get_current();
47- let max = self.get_max();
48-
49- let curr = curr * 100 / max;
50+ let mut new = self.device.val_to_percent(curr) + by;
51+ new = self.device.percent_to_val(new).max(curr + 1);
52
53- self.device.set_percent(curr + by)
54+ self.device.set_val(new)
55 }
56
57 fn set(&mut self, val: u32) -> anyhow::Result<()> {
diff --git a/user-profiles/yt-dlp.nix b/user-profiles/yt-dlp.nix
index f59a71b9..9e30bba8 100644
--- a/user-profiles/yt-dlp.nix
+++ b/user-profiles/yt-dlp.nix
@@ -11,6 +11,10 @@
11 "best[width<=2560][height<=1440][fps<=60][vcodec!*=av01][vcodec!*=avc1]" 11 "best[width<=2560][height<=1440][fps<=60][vcodec!*=av01][vcodec!*=avc1]"
12 "bestvideo*[vcodec!*=av01][vcodec!*=avc1]+bestaudio" 12 "bestvideo*[vcodec!*=av01][vcodec!*=avc1]+bestaudio"
13 "best[vcodec!*=av01][vcodec!*=avc1]" 13 "best[vcodec!*=av01][vcodec!*=avc1]"
14 "bestvideo*[width<=2560][height<=1440][fps<=60]+bestaudio"
15 "best[width<=2560][height<=1440][fps<=60]"
16 "bestvideo*+bestaudio"
17 "best"
14 ]; 18 ];
15 embed-subs = true; 19 embed-subs = true;
16 embed-thumbnail = true; 20 embed-thumbnail = true;