summaryrefslogtreecommitdiff
path: root/accounts
diff options
context:
space:
mode:
Diffstat (limited to 'accounts')
-rw-r--r--accounts/gkleen@sif/default.nix13
-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.nix97
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt23
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp35
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp14
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp102
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp52
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp18
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp21
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.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/ActiveWindowDisplay.qml182
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml24
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml130
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml117
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml78
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml345
-rw-r--r--accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml61
-rw-r--r--accounts/gkleen@sif/shell/quickshell/LockSurface.qml223
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml90
-rw-r--r--accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml35
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml471
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml49
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml75
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml18
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml14
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml63
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/SystemTray.qml52
-rw-r--r--accounts/gkleen@sif/shell/quickshell/UnixIPC.qml59
-rw-r--r--accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml163
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml85
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml198
-rw-r--r--accounts/gkleen@sif/shell/quickshell/displaymanager.qml115
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml30
-rw-r--r--accounts/gkleen@sif/systemd.nix2
39 files changed, 2797 insertions, 455 deletions
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index f2978b6e..c786c629 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -49,7 +49,8 @@ let
49 ]; 49 ];
50 }; 50 };
51 51
52 lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service"; 52 # lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service";
53 lockCommand = "${lib.getExe' cfg.programs.quickshell.package "qs"} ipc call Lockscreen setLocked true";
53 54
54 editor = pkgs.symlinkJoin { 55 editor = pkgs.symlinkJoin {
55 inherit (cfg.services.emacs.package) name; 56 inherit (cfg.services.emacs.package) name;
@@ -374,7 +375,7 @@ in {
374 375
375 services = { 376 services = {
376 wpaperd = { 377 wpaperd = {
377 enable = true; 378 enable = false;
378 settings.default = { 379 settings.default = {
379 path = "~/.wallpapers"; 380 path = "~/.wallpapers";
380 duration = "15m"; 381 duration = "15m";
@@ -594,10 +595,10 @@ in {
594 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass) 595 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass)
595 ]; 596 ];
596 }; 597 };
597 "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = '' 598 # "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = ''
598 [Unit] 599 # [Unit]
599 After=graphical-session.target 600 # After=graphical-session.target
600 ''; 601 # '';
601 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = '' 602 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = ''
602 [Unit] 603 [Unit]
603 Before=graphical-session-pre.target 604 Before=graphical-session-pre.target
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 35a3d799..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 26c8bd98..5025dd90 100644
--- a/accounts/gkleen@sif/shell/default.nix
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -4,17 +4,110 @@
4 config = { 4 config = {
5 programs.quickshell = { 5 programs.quickshell = {
6 enable = true; 6 enable = true;
7 package = pkgs.symlinkJoin {
8 pname = pkgs.quickshell.pname + "-wrapped";
9 inherit (pkgs.quickshell) version meta;
10 paths = [ pkgs.quickshell ];
11 buildInputs = [ pkgs.makeWrapper ];
12 postBuild = ''
13 for binary in quickshell qs; do
14 wrapProgram $out/bin/$binary \
15 --prefix QML_IMPORT_PATH : ${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix}
16 done
17 '';
18 };
7 config = { 19 config = {
8 src = ./quickshell; 20 src = ./quickshell;
9 replacements = { 21 replacements = {
10 coreutils = toString pkgs.coreutils;
11 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces); 22 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces);
23 wallpapers = builtins.toJSON (pkgs.stdenvNoCC.mkDerivation {
24 name = "wallpapers";
25 srcs = [
26 (pkgs.fetchurl {
27 url = "https://esawebb.org/media/archives/images/publicationtiff10k/carinanebula3.tif";
28 hash = "sha256-YxZEweDKJfvfrdxb/QFmgJhcZDEJYxotoHrG+RRn1tw=";
29 })
30 (pkgs.fetchurl {
31 url = "https://esawebb.org/media/archives/images/original/pillarsofcreation_composite.tif";
32 hash = "sha256-qRiODxR0lZWdxgYXna0fNRFFDErpBJDwOJuQl6sNjRc=";
33 })
34 (pkgs.fetchurl {
35 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2212a.tif";
36 hash = "sha256-l2fqE/z//C1a0xkvZwsnwPbTSb+WuA11h+SUl3E1dhw=";
37 })
38 (pkgs.fetchurl {
39 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2415a.tif";
40 hash = "sha256-onBy7cPoUpDuzQStbY2E+qmlGgSLXPwFCLX53ukAb4c=";
41 })
42 (pkgs.fetchurl {
43 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2330a.tif";
44 hash = "sha256-nn0ZtjZIrPcpj3YcLTsrL7XiXvyh3QYgCSmdDMD+3OM=";
45 })
46 (pkgs.fetchurl {
47 url = "https://esawebb.org/media/archives/images/original/weic2426a.tif";
48 hash = "sha256-EDnfPn3GE9jt6XPqiGInP7E2F3Az7d25NqATSWltDv0=";
49 })
50 (pkgs.fetchurl {
51 url = "https://esawebb.org/media/archives/images/original/weic2503a.tif";
52 hash = "sha256-3/RX6RQp8naELcgReHPd5/zhJkoCjnA10w5BEnNo+qI=";
53 })
54 (pkgs.fetchurl {
55 url = "https://esawebb.org/media/archives/images/original/weic2506a.tif";
56 hash = "sha256-aDld0aoY1owRxDVf7Jcyw71TH42M1foYotxn2thyFYw=";
57 })
58 (pkgs.fetchurl {
59 url = "https://esawebb.org/media/archives/images/original/weic2514a.tif";
60 hash = "sha256-jTi1G1Ofo5xsF6ggrbtYJHxqLaCQ7edM5B3uORiVQtg=";
61 })
62 (pkgs.fetchurl {
63 url = "https://esawebb.org/media/archives/images/original/weic2425c.tif";
64 hash = "sha256-oaEOexSJHEGj090dJF3ct5HAoR+Y5gRiPrUlxdvnTRo=";
65 })
66 ];
67
68 dontUnpack = true;
69
70 buildInputs = [ pkgs.imagemagick ];
71 buildPhase = ''
72 runHook preBuild
73
74 typeset sources=($srcs)
75
76 mkdir -p $out
77 magick ''${sources[0]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/carinanebula3.jpeg
78 magick ''${sources[1]} -crop 6716x3778+329+80 +repage -define jpeg:extent=10MB $out/pillarsofcreation_composite.jpeg
79 magick ''${sources[2]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/weic2212a.jpeg
80 magick ''${sources[3]} -crop 7650x4302+1166+389 +repage -define jpeg:extent=10MB $out/weic2415a.jpeg
81 magick ''${sources[4]} -crop 8732x4912+0+434 +repage -define jpeg:extent=10MB $out/weic2330a.jpeg
82 magick ''${sources[5]} -crop 5302x2982+636+0 +repage -define jpeg:extent=10MB $out/weic2426a.jpeg
83 magick ''${sources[6]} -crop 4328x2434+0+906 +repage -define jpeg:extent=10MB $out/weic2503a.jpeg
84 magick ''${sources[7]} -crop 4152x2335+0+666 +repage -define jpeg:extent=10MB $out/weic2506a.jpeg
85 magick ''${sources[8]} -crop 4320x2430+0+0 +repage -define jpeg:extent=10MB $out/weic2514a.jpeg
86 magick ''${sources[9]} -crop 5863x3298+0+477 +repage -define jpeg:extent=10MB $out/weic2425c.jpeg
87
88 runHook postBuild
89 '';
90 });
91 niri_session = builtins.toJSON [
92 (pkgs.writeShellScript "niri-session" ''
93 exec ${lib.getExe pkgs.dex} -w ${config.programs.niri.package}/share/wayland-sessions/niri.desktop &>/tmp/niri-session-$$.log
94 '')
95 # (lib.getExe pkgs.dex)
96 # "${config.programs.niri.package}/share/wayland-sessions/niri.desktop"
97 ];
98 username = builtins.toJSON config.home.username;
99 mdi = builtins.toJSON (pkgs.fetchFromGitHub {
100 owner = "Templarian";
101 repo = "MaterialDesign";
102 rev = "2424e748e0cc63ab7b9c095a099b9fe239b737c0";
103 hash = "sha256-QMGl7soAhErrrnY3aKOZpt49yebkSNzy10p/v5OaqQ0=";
104 });
12 }; 105 };
13 }; 106 };
14 }; 107 };
15 systemd.user.services.quickshell = { 108 systemd.user.services.quickshell = {
16 Service = { 109 Service = {
17 Environment = "QML_IMPORT_PATH=${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix}"; 110 RuntimeDirectory = "quickshell";
18 }; 111 };
19 }; 112 };
20 }; 113 };
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
index aa363c4c..a7e88fa7 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
@@ -92,7 +92,7 @@ endfunction()
92cmake_minimum_required(VERSION 3.20) 92cmake_minimum_required(VERSION 3.20)
93project(custom LANGUAGES CXX) 93project(custom LANGUAGES CXX)
94 94
95find_package(Qt6 REQUIRED COMPONENTS Core Qml) 95find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus)
96 96
97qt_standard_project_setup(REQUIRES 6.6) 97qt_standard_project_setup(REQUIRES 6.6)
98 98
@@ -102,15 +102,32 @@ qt6_add_qml_module(customplugin
102 PLUGIN_TARGET customplugin 102 PLUGIN_TARGET customplugin
103) 103)
104 104
105target_sources(customplugin PRIVATE 105set_source_files_properties(org.keepassxc.KeePassXC.MainWindow.xml PROPERTIES
106 Chrono.cpp Chrono.hpp 106 CLASSNAME DBusKeePassXC
107 NO_NAMESPACE TRUE
107) 108)
108 109
110qt_add_dbus_interface(DBUS_INTERFACES
111 org.keepassxc.KeePassXC.MainWindow.xml
112 dbus_keepassxc
113)
114
115include_directories(${CMAKE_SOURCE_DIR}/build)
116
109target_compile_features(customplugin PUBLIC cxx_std_26) 117target_compile_features(customplugin PUBLIC cxx_std_26)
110 118
111target_link_libraries(customplugin PRIVATE 119target_link_libraries(customplugin PRIVATE
112 Qt6::Core 120 Qt6::Core
113 Qt6::Qml 121 Qt6::Qml
122 Qt6::DBus
123)
124
125target_sources(customplugin PRIVATE
126 Chrono.cpp Chrono.hpp
127 FileSelector.cpp FileSelector.hpp
128 KeePassXC.cpp KeePassXC.hpp
129 Systemd.cpp Systemd.hpp
130 ${DBUS_INTERFACES}
114) 131)
115 132
116install_qml_module(customplugin) 133install_qml_module(customplugin)
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
index 929b7be6..22b3469b 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
@@ -42,9 +42,11 @@ void Chrono::update() {
42} 42}
43 43
44void Chrono::setTime(const std::chrono::time_point<std::chrono::system_clock>& targetTime) { 44void Chrono::setTime(const std::chrono::time_point<std::chrono::system_clock>& targetTime) {
45 using namespace std::chrono_literals;
46
45 auto currentTime = std::chrono::system_clock::now(); 47 auto currentTime = std::chrono::system_clock::now();
46 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime); 48 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime);
47 this->currentTime = abs(offset.count()) < 500 ? targetTime : currentTime; 49 this->currentTime = abs(offset) < 500ms ? targetTime : currentTime;
48 50
49 switch (this->mPrecision) { 51 switch (this->mPrecision) {
50 case Chrono::Hours: this->currentTime = std::chrono::time_point_cast<std::chrono::hours>(this->currentTime); 52 case Chrono::Hours: this->currentTime = std::chrono::time_point_cast<std::chrono::hours>(this->currentTime);
@@ -56,33 +58,26 @@ void Chrono::setTime(const std::chrono::time_point<std::chrono::system_clock>& t
56} 58}
57 59
58void Chrono::schedule(const std::chrono::time_point<std::chrono::system_clock>& targetTime) { 60void Chrono::schedule(const std::chrono::time_point<std::chrono::system_clock>& targetTime) {
61 using namespace std::chrono_literals;
62
59 auto currentTime = std::chrono::system_clock::now(); 63 auto currentTime = std::chrono::system_clock::now();
60 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime); 64 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime);
61 auto nextTime = abs(offset.count()) < 500 ? targetTime : currentTime; 65 auto nextTime = abs(offset) < 500ms ? targetTime : currentTime;
62
63 {
64 using namespace std::chrono_literals;
65 66
66 switch (this->mPrecision) { 67 switch (this->mPrecision) {
67 case Chrono::Hours: nextTime = std::chrono::time_point_cast<std::chrono::hours>(nextTime) + 1h; 68 case Chrono::Hours: nextTime = std::chrono::time_point_cast<std::chrono::hours>(nextTime) + 1h;
68 case Chrono::Minutes: nextTime = std::chrono::time_point_cast<std::chrono::minutes>(nextTime) + 1min; 69 case Chrono::Minutes: nextTime = std::chrono::time_point_cast<std::chrono::minutes>(nextTime) + 1min;
69 case Chrono::Seconds: nextTime = std::chrono::time_point_cast<std::chrono::seconds>(nextTime) + 1s; 70 case Chrono::Seconds: nextTime = std::chrono::time_point_cast<std::chrono::seconds>(nextTime) + 1s;
70 }
71 } 71 }
72 72
73 this->targetTime = nextTime; 73 this->targetTime = nextTime;
74 auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime); 74 this->timer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime));
75 this->timer.start(delay);
76} 75}
77 76
78QString Chrono::format() const { return this->mFormat; } 77QString Chrono::format(const QString& fmt) const {
79void Chrono::setFormat(QString format) { 78 return QString::fromStdString(std::format(std::runtime_format(fmt.toStdString()), std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::time_point_cast<std::chrono::seconds>(this->currentTime))));
80 if (format == this->mFormat) return;
81 this->mFormat = format;
82 emit this->formatChanged();
83 this->update();
84} 79}
85 80
86QString Chrono::date() const { 81QDateTime Chrono::date() const {
87 return QString::fromStdString(std::format(std::runtime_format(this->mFormat.toStdString()), std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::time_point_cast<std::chrono::seconds>(this->currentTime)))); 82 return QDateTime::fromStdTimePoint(std::chrono::time_point_cast<std::chrono::milliseconds>(this->currentTime));
88} 83}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
index 788fa88e..04080187 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
@@ -1,17 +1,18 @@
1#pragma once 1#pragma once
2 2
3#include <chrono>
4
5#include <QDateTime>
3#include <QObject> 6#include <QObject>
4#include <QTimer> 7#include <QTimer>
5 8
6#include <qqmlintegration.h> 9#include <qqmlintegration.h>
7#include <chrono>
8 10
9class Chrono : public QObject { 11class Chrono : public QObject {
10 Q_OBJECT; 12 Q_OBJECT;
11 Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); 13 Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
12 Q_PROPERTY(Chrono::Precision precision READ precision WRITE setPrecision NOTIFY precisionChanged); 14 Q_PROPERTY(Chrono::Precision precision READ precision WRITE setPrecision NOTIFY precisionChanged);
13 Q_PROPERTY(QString format READ format WRITE setFormat NOTIFY formatChanged); 15 Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged)
14 Q_PROPERTY(QString date READ date NOTIFY dateChanged);
15 QML_ELEMENT; 16 QML_ELEMENT;
16 17
17public: 18public:
@@ -30,15 +31,13 @@ public:
30 Chrono::Precision precision() const; 31 Chrono::Precision precision() const;
31 void setPrecision(Chrono::Precision precision); 32 void setPrecision(Chrono::Precision precision);
32 33
33 QString format() const; 34 Q_INVOKABLE QString format(const QString& fmt) const;
34 void setFormat (QString format);
35 35
36 QString date() const; 36 QDateTime date() const;
37 37
38signals: 38signals:
39 void enabledChanged(); 39 void enabledChanged();
40 void precisionChanged(); 40 void precisionChanged();
41 void formatChanged();
42 void dateChanged(); 41 void dateChanged();
43 42
44private slots: 43private slots:
@@ -47,7 +46,6 @@ private slots:
47private: 46private:
48 bool mEnabled = true; 47 bool mEnabled = true;
49 Chrono::Precision mPrecision = Chrono::Seconds; 48 Chrono::Precision mPrecision = Chrono::Seconds;
50 QString mFormat = "{:%c}";
51 QTimer timer; 49 QTimer timer;
52 std::chrono::time_point<std::chrono::system_clock> currentTime, targetTime; 50 std::chrono::time_point<std::chrono::system_clock> currentTime, targetTime;
53 51
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp
new file mode 100644
index 00000000..d7051d2a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp
@@ -0,0 +1,102 @@
1#include "FileSelector.hpp"
2
3#include <sstream>
4#include <vector>
5#include <random>
6#include <algorithm>
7
8#include <iostream>
9#include <format>
10
11namespace fs = std::filesystem;
12
13FileSelector::FileSelector(QObject* parent): QObject(parent) {
14 QObject::connect(&this->timer, &QTimer::timeout, this, &FileSelector::onTimeout);
15 this->timer.setTimerType(Qt::PreciseTimer);
16}
17
18QString FileSelector::directory() const {
19 return QString::fromStdString(this->mDirectory->string());
20}
21void FileSelector::setDirectory(QString directory) {
22 this->mDirectory = directory.toStdString();
23 if (this->mDirectory && this->mEpoch)
24 this->update();
25 emit this->directoryChanged();
26}
27
28unsigned int FileSelector::epoch() const {
29 return std::chrono::duration_cast<std::chrono::milliseconds>(*this->mEpoch).count();
30}
31void FileSelector::setEpoch(unsigned int epoch) {
32 this->mEpoch = std::chrono::milliseconds{epoch};
33 if (this->mDirectory && this->mEpoch)
34 this->update();
35 emit this->epochChanged();
36}
37
38QString FileSelector::seed() const {
39 return this->mSeed;
40}
41void FileSelector::setSeed(QString seed) {
42 this->mSeed = seed;
43 emit this->seedChanged();
44 if (this->mDirectory && this->mEpoch)
45 emit this->selectedChanged();
46}
47
48QString FileSelector::selected() const {
49 if (!this->mDirectory || !this->mEpoch)
50 return QString();
51
52 std::vector<fs::path> shuffled(this->mFiles.begin(), this->mFiles.end());
53 std::sort(shuffled.begin(), shuffled.end());
54
55 auto currentTime = std::chrono::system_clock::now();
56 uint64_t currentEpoch = currentTime.time_since_epoch() / *this->mEpoch;
57 std::chrono::milliseconds timeInEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime.time_since_epoch()) % *this->mEpoch;
58
59 std::ostringstream seed;
60 seed << this->mSeed.size() << ";" << this->mSeed.toStdString() << ";";
61 seed << *this->mEpoch << ";";
62 seed << currentEpoch << ";";
63 seed << this->mDirectory->string().size() << ";" << *this->mDirectory << ";";
64 seed << this->mFiles.size() << ";";
65 for (const fs::path& p: this->mFiles)
66 seed << p.string().size() << ";" << p << ";";
67
68 std::vector<std::seed_seq::result_type> v;
69 v.reserve(seed.str().size());
70 for (const char& c: seed.str())
71 v.push_back(c);
72
73 std::seed_seq engine_seed(v.begin(), v.end());
74 std::mt19937 g(engine_seed);
75 std::shuffle(shuffled.begin(), shuffled.end(), g);
76
77 std::vector<fs::path>::size_type ix = shuffled.size() * timeInEpoch / *this->mEpoch;
78 return QString::fromStdString((*this->mDirectory / shuffled[ix]).string());
79}
80
81void FileSelector::onTimeout() {
82 if (!this->mFiles.size())
83 return;
84
85 auto currentTime = std::chrono::system_clock::now();
86 uint64_t currentMinorEpoch = currentTime.time_since_epoch() / (*this->mEpoch / this->mFiles.size());
87 auto nextTime = std::chrono::time_point<std::chrono::system_clock>((currentMinorEpoch + 1) * (*this->mEpoch / this->mFiles.size()));
88 this->timer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime));
89
90 emit this->selectedChanged();
91}
92
93void FileSelector::update() {
94 this->mFiles = std::set<fs::path>{};
95 for (const fs::directory_entry& entry:
96 fs::recursive_directory_iterator(*this->mDirectory, fs::directory_options::follow_directory_symlink))
97 {
98 this->mFiles.insert(fs::relative(entry, *this->mDirectory));
99 }
100
101 this->onTimeout();
102}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp
new file mode 100644
index 00000000..72c4f2a7
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp
@@ -0,0 +1,52 @@
1#pragma once
2
3#include <filesystem>
4#include <chrono>
5#include <set>
6#include <optional>
7
8#include <QObject>
9#include <QTimer>
10
11#include <qqmlintegration.h>
12
13class FileSelector : public QObject {
14 Q_OBJECT;
15 Q_PROPERTY(QString directory READ directory WRITE setDirectory NOTIFY directoryChanged REQUIRED);
16 Q_PROPERTY(unsigned int epoch READ epoch WRITE setEpoch NOTIFY epochChanged REQUIRED);
17 Q_PROPERTY(QString seed READ seed WRITE setSeed NOTIFY seedChanged);
18 Q_PROPERTY(QString selected READ selected NOTIFY selectedChanged);
19 QML_ELEMENT;
20
21public:
22 explicit FileSelector(QObject* parent = nullptr);
23
24 QString directory() const;
25 void setDirectory(QString directory);
26
27 unsigned int epoch() const;
28 void setEpoch(unsigned int epoch);
29
30 QString seed() const;
31 void setSeed(QString seed);
32
33 QString selected() const;
34
35signals:
36 void directoryChanged();
37 void epochChanged();
38 void seedChanged();
39 void selectedChanged();
40
41private slots:
42 void onTimeout();
43
44private:
45 std::optional<std::filesystem::path> mDirectory;
46 std::optional<std::chrono::milliseconds> mEpoch;
47 std::set<std::filesystem::path> mFiles;
48 QString mSeed;
49 QTimer timer;
50
51 void update();
52};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
new file mode 100644
index 00000000..f6e4dd6e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
@@ -0,0 +1,18 @@
1#include "KeePassXC.hpp"
2
3#include <QDBusConnection>
4
5KeePassXC::KeePassXC() {
6 this->service = new DBusKeePassXC(DBusKeePassXC::staticInterfaceName(), "/keepassxc", QDBusConnection::sessionBus(), this);
7}
8KeePassXC::~KeePassXC() {
9 if (this->service)
10 delete this->service;
11}
12
13void KeePassXC::lockAllDatabases() {
14 if (!this->service)
15 return;
16
17 this->service->lockAllDatabases();
18}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
new file mode 100644
index 00000000..c4cd71e0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
@@ -0,0 +1,21 @@
1#pragma once
2
3#include "dbus_keepassxc.h"
4
5#include <QObject>
6#include <QtQmlIntegration/qqmlintegration.h>
7
8class KeePassXC : public QObject {
9 Q_OBJECT;
10 QML_SINGLETON;
11 QML_ELEMENT;
12
13public:
14 explicit KeePassXC();
15 ~KeePassXC();
16
17 Q_INVOKABLE void lockAllDatabases();
18
19private:
20 DBusKeePassXC* service = nullptr;
21};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
new file mode 100644
index 00000000..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/ActiveWindowDisplay.qml b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
index 57ade488..883f9001 100644
--- a/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
+++ b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
@@ -22,72 +22,144 @@ Item {
22 width: activeWindowDisplayContent.width 22 width: activeWindowDisplayContent.width
23 height: parent.height 23 height: parent.height
24 24
25 Row { 25 WrapperMouseArea {
26 id: activeWindowDisplayContent 26 id: widgetMouseArea
27 27
28 width: childrenRect.width 28 anchors.fill: parent
29 height: parent.height
30 anchors.verticalCenter: parent.verticalCenter
31 spacing: 8
32 29
33 IconImage { 30 hoverEnabled: true
34 id: activeWindowIcon
35 31
36 height: 14 32 Item {
37 width: 14 33 anchors.fill: parent
38 34
39 anchors.verticalCenter: parent.verticalCenter 35 Row {
36 id: activeWindowDisplayContent
40 37
41 source: { 38 width: childrenRect.width
42 let icon = activeWindowDisplay.windowEntry?.icon 39 height: parent.height
43 if (typeof icon === 'string' || icon instanceof String) { 40 anchors.verticalCenter: parent.verticalCenter
44 if (icon.includes("?path=")) { 41 spacing: 8
45 const split = icon.split("?path=") 42
46 if (split.length !== 2) 43 IconImage {
44 id: activeWindowIcon
45
46 implicitSize: 14
47
48 anchors.verticalCenter: parent.verticalCenter
49
50 source: {
51 let icon = activeWindowDisplay.windowEntry?.icon
52 if (typeof icon === 'string' || icon instanceof String) {
53 if (icon.includes("?path=")) {
54 const split = icon.split("?path=")
55 if (split.length !== 2)
56 return icon
57 const name = split[0]
58 const path = split[1]
59 const fileName = name.substring(
60 name.lastIndexOf("/") + 1)
61 return `file://${path}/${fileName}`
62 } else
63 icon = Quickshell.iconPath(icon);
47 return icon 64 return icon
48 const name = split[0] 65 }
49 const path = split[1] 66 return ""
50 const fileName = name.substring( 67 }
51 name.lastIndexOf("/") + 1) 68 asynchronous: true
52 return `file://${path}/${fileName}` 69 smooth: true
53 } else 70 mipmap: true
54 icon = Quickshell.iconPath(icon); 71 }
55 return icon 72
73 Text {
74 id: windowTitle
75
76 width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing)
77
78 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
79
80 elide: Text.ElideRight
81 maximumLineCount: 1
82 color: "white"
83 anchors.verticalCenter: parent.verticalCenter
84 text: {
85 if (!activeWindowDisplay.activeWindow)
86 return "";
87
88 var title = activeWindowDisplay.activeWindow.title;
89 var appName = activeWindowDisplay.windowEntry?.name;
90 if (appAliases[appName])
91 appName = appAliases[appName];
92 if (appName && title.endsWith(appName)) {
93 const oldTitle = title;
94 title = title.substring(0, title.length - appName.length);
95 title = title.replace(/\s*(—|-)\s*$/, "");
96 if (!title)
97 title = oldTitle;
98 }
99 return title;
100 }
56 } 101 }
57 return ""
58 } 102 }
59 asynchronous: true
60 smooth: true
61 mipmap: true
62 } 103 }
104 }
105
106 Loader {
107 id: tooltipLoader
108
109 active: false
110
111 Connections {
112 target: widgetMouseArea
113 function onContainsMouseChanged() {
114 if (widgetMouseArea.containsMouse)
115 tooltipLoader.active = true;
116 }
117 }
118
119 PopupWindow {
120 id: tooltip
121
122 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
123
124 anchor {
125 item: widgetMouseArea
126 edges: Edges.Bottom | Edges.Left
127 }
128 visible: false
129
130 onNextVisibleChanged: hangTimer.restart()
131
132 Timer {
133 id: hangTimer
134 interval: tooltip.visible ? 100 : 500
135 onTriggered: {
136 tooltip.visible = tooltip.nextVisible;
137 if (!tooltip.visible)
138 tooltipLoader.active = false;
139 }
140 }
141
142 implicitWidth: widgetTooltipText.contentWidth + 16
143 implicitHeight: widgetTooltipText.contentHeight + 16
144 color: "black"
145
146 WrapperMouseArea {
147 id: tooltipMouseArea
148
149 hoverEnabled: true
150 enabled: true
151
152 anchors.centerIn: parent
153
154 Text {
155 id: widgetTooltipText
156
157 font.pointSize: 10
158 font.family: "Fira Mono"
159 color: "white"
63 160
64 Text { 161 text: JSON.stringify(Object.assign({}, activeWindowDisplay.activeWindow), null, 2)
65 id: windowTitle
66
67 width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing)
68
69 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
70
71 elide: Text.ElideRight
72 maximumLineCount: 1
73 color: "white"
74 anchors.verticalCenter: parent.verticalCenter
75 text: {
76 if (!activeWindowDisplay.activeWindow)
77 return "";
78
79 var title = activeWindowDisplay.activeWindow.title;
80 var appName = activeWindowDisplay.windowEntry?.name;
81 if (appAliases[appName])
82 appName = appAliases[appName];
83 if (appName && title.endsWith(appName)) {
84 const oldTitle = title;
85 title = title.substring(0, title.length - appName.length);
86 title = title.replace(/\s*(—|-)\s*$/, "");
87 if (!title)
88 title = oldTitle;
89 } 162 }
90 return title;
91 } 163 }
92 } 164 }
93 } 165 }
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index aab1607f..52b9b344 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,31 @@ 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 - 4
74 } 74 }
75 75
76 BatteryWidget {}
77
78 BrightnessWidget {}
79
80 PipewireWidget {}
81
82 SystemTray {}
83
76 KeyboardLayout {} 84 KeyboardLayout {}
77 85
78 Item { 86 Item {
79 height: parent.height 87 height: parent.height
80 width: 4 88 width: 8 - 4
81 } 89 }
82 90
83 Clock { 91 Clock {}
84 id: clock
85 }
86 } 92 }
87} \ No newline at end of file 93} \ 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..fd031627
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
@@ -0,0 +1,130 @@
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 + 8
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 PopupWindow {
62 id: tooltip
63
64 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
65
66 anchor {
67 item: widgetMouseArea
68 edges: Edges.Bottom | Edges.Left
69 }
70 visible: false
71
72 onNextVisibleChanged: hangTimer.restart()
73
74 Timer {
75 id: hangTimer
76 interval: 100
77 onTriggered: tooltip.visible = tooltip.nextVisible
78 }
79
80 implicitWidth: widgetTooltipText.contentWidth + 16
81 implicitHeight: widgetTooltipText.contentHeight + 16
82 color: "black"
83
84 WrapperMouseArea {
85 id: tooltipMouseArea
86
87 hoverEnabled: true
88 enabled: true
89
90 anchors.centerIn: parent
91
92 Text {
93 id: widgetTooltipText
94
95 font.pointSize: 10
96 font.family: "Fira Sans"
97 color: "white"
98
99 text: {
100 const stateStr = UPowerDeviceState.toString(root.batteryDevice.state);
101 var outStr = stateStr;
102 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged)
103 outStr += ` ${Math.round(root.batteryDevice.percentage * 100)}%`;
104
105 function formatTime(t) {
106 var res = "";
107 for (const unit of [{ "s": "h", "v": 3600 }, { "s": "m", "v": 60 }, { "s": "s", "v": 1 }]) {
108 if (t < unit.v)
109 continue;
110 res += Math.floor(t / unit.v) + unit.s;
111 t %= unit.v;
112 }
113 return res;
114 }
115 if (root.batteryDevice.timeToEmpty != 0) {
116 const tStr = formatTime(Math.floor(root.batteryDevice.timeToEmpty / 60) * 60);
117 if (tStr)
118 outStr += " " + tStr;
119 } else if (root.batteryDevice.timeToFull != 0) {
120 const tStr = formatTime(Math.ceil(root.batteryDevice.timeToFull / 60) * 60);
121 if (tStr)
122 outStr += " " + tStr;
123 }
124
125 return outStr;
126 }
127 }
128 }
129 }
130}
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..7f9c1ad0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
@@ -0,0 +1,78 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import qs.Services
5
6Item {
7 height: parent.height
8 width: brightnessIcon.width + 8
9 anchors.verticalCenter: parent.verticalCenter
10
11 WrapperMouseArea {
12 id: widgetMouseArea
13
14 anchors.fill: parent
15
16 hoverEnabled: true
17
18 property real sensitivity: (1 / 50) / 120
19 onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity
20
21 Item {
22 anchors.fill: parent
23
24 MaterialDesignIcon {
25 id: brightnessIcon
26
27 implicitSize: 14
28 anchors.centerIn: parent
29
30 icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}`
31 color: "#555"
32 }
33 }
34 }
35
36 PopupWindow {
37 id: tooltip
38
39 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
40
41 anchor {
42 item: widgetMouseArea
43 edges: Edges.Bottom | Edges.Left
44 }
45 visible: false
46
47 onNextVisibleChanged: hangTimer.restart()
48
49 Timer {
50 id: hangTimer
51 interval: 100
52 onTriggered: tooltip.visible = tooltip.nextVisible
53 }
54
55 implicitWidth: widgetTooltipText.contentWidth + 16
56 implicitHeight: widgetTooltipText.contentHeight + 16
57 color: "black"
58
59 WrapperMouseArea {
60 id: tooltipMouseArea
61
62 hoverEnabled: true
63 enabled: true
64
65 anchors.centerIn: parent
66
67 Text {
68 id: widgetTooltipText
69
70 font.pointSize: 10
71 font.family: "Fira Sans"
72 color: "white"
73
74 text: `${Math.round(Brightness.currBrightness * 100)}%`
75 }
76 }
77 }
78}
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
index 58600adb..bb618f6a 100644
--- a/accounts/gkleen@sif/shell/quickshell/Clock.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -7,240 +7,275 @@ import QtQuick.Layouts
7import Quickshell.Widgets 7import Quickshell.Widgets
8 8
9Item { 9Item {
10 id: clockItem
11
12 property bool calendarPopup: true
13
10 width: clock.contentWidth 14 width: clock.contentWidth
11 height: parent.height 15 height: parent.height
12 anchors.verticalCenter: parent.verticalCenter 16 anchors.verticalCenter: parent.verticalCenter
13 17
14 MouseArea { 18 WrapperMouseArea {
15 id: clockMouseArea 19 id: clockMouseArea
16 20
17 anchors.fill: parent 21 anchors.fill: parent
18 hoverEnabled: true 22 hoverEnabled: true
19 enabled: true 23 enabled: clockItem.calendarPopup
20 24
21 property real angleRem: 0 25 Item {
22 property real sensitivity: 1 / 120 26 anchors.fill: parent
23 27
24 function scrollYear(event) { 28 Text {
25 angleRem += event.angleDelta.y; 29 id: clock
26 const d = Math.round(angleRem * sensitivity); 30 color: "white"
27 yearCalendar.year += d;
28 angleRem -= d / sensitivity;
29 }
30 31
31 onWheel: event => scrollYear(event) 32 anchors.verticalCenter: parent.verticalCenter
32 }
33 33
34 Text { 34 Custom.Chrono {
35 id: clock 35 id: chrono
36 color: "white"
37 36
38 anchors.verticalCenter: parent.verticalCenter 37 onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}")
38 }
39 39
40 Custom.Chrono { 40 font.pointSize: 10
41 id: chrono 41 font.family: "Fira Sans"
42 format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}" 42 font.features: { "tnum": 1 }
43 }
43 } 44 }
44
45 text: chrono.date
46
47 font.pointSize: 10
48 font.family: "Fira Sans"
49 font.features: { "tnum": 1 }
50 } 45 }
51 46
52 PopupWindow { 47 Loader {
53 id: tooltip 48 id: tooltipLoader
54 49
55 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse 50 active: false
56 51
57 anchor { 52 Connections {
58 item: clockMouseArea 53 target: clockMouseArea
59 edges: Edges.Bottom | Edges.Left 54 function onContainsMouseChanged() {
55 if (clockMouseArea.containsMouse)
56 tooltipLoader.active = true;
57 }
60 } 58 }
61 visible: false
62 59
63 onNextVisibleChanged: hangTimer.restart() 60 sourceComponent: PopupWindow {
61 id: tooltip
64 62
65 Timer { 63 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse
66 id: hangTimer
67 interval: 100
68 onTriggered: tooltip.visible = tooltip.nextVisible
69 }
70 64
71 implicitWidth: clockTooltipContent.width 65 anchor {
72 implicitHeight: clockTooltipContent.height 66 item: clockMouseArea
73 color: "black" 67 edges: Edges.Bottom | Edges.Left
68 }
69 visible: false
74 70
75 onVisibleChanged: { 71 onNextVisibleChanged: hangTimer.restart()
76 const d = new Date();
77 yearCalendar.year = d.getFullYear();
78 clockMouseArea.angleRem = 0;
79 }
80 72
81 WrapperMouseArea { 73 Timer {
82 id: tooltipMouseArea 74 id: hangTimer
75 interval: 100
76 onTriggered: {
77 tooltip.visible = tooltip.nextVisible;
78 if (!tooltip.visible)
79 tooltipLoader.active = false;
80 }
81 }
82
83 implicitWidth: clockTooltipContent.width
84 implicitHeight: clockTooltipContent.height
85 color: "black"
83 86
84 hoverEnabled: true 87 onVisibleChanged: {
85 enabled: true 88 yearCalendar.year = chrono.date.getFullYear();
89 yearCalendar.angleRem = 0;
90 }
86 91
87 onWheel: event => clockMouseArea.scrollYear(event) 92 WrapperMouseArea {
93 id: tooltipMouseArea
88 94
89 anchors.fill: parent 95 hoverEnabled: true
96 enabled: true
90 97
91 WrapperItem { 98 onWheel: event => yearCalendar.scrollYear(event)
92 id: clockTooltipContent
93 99
94 margin: 8 100 anchors.fill: parent
95 leftMargin: 0
96 101
97 ColumnLayout { 102 WrapperItem {
98 Text { 103 id: clockTooltipContent
99 id: yearLabel
100 104
101 horizontalAlignment: Text.AlignHCenter 105 margin: 8
102 106
103 font.pointSize: 14 107 ColumnLayout {
104 font.family: "Fira Sans" 108 anchors.centerIn: parent
105 font.features: { "tnum": 1 }
106 color: "white"
107 109
108 text: yearCalendar.year 110 Text {
111 id: yearLabel
109 112
110 Layout.fillWidth: true 113 horizontalAlignment: Text.AlignHCenter
111 Layout.bottomMargin: 8
112 }
113 114
114 GridLayout { 115 font.pointSize: 14
115 property int year: { const d = new Date(); return d.getFullYear(); } 116 font.family: "Fira Sans"
117 font.features: { "tnum": 1 }
118 color: "white"
116 119
117 id: yearCalendar 120 text: yearCalendar.year
118 121
119 columns: 3 122 Layout.fillWidth: true
120 columnSpacing: 16 123 Layout.bottomMargin: 8
121 rowSpacing: 16 124 }
122 125
123 Layout.alignment: Qt.AlignHCenter 126 GridLayout {
124 Layout.fillWidth: false 127 property int year: chrono.date.getFullYear()
125 128
126 Repeater { 129 id: yearCalendar
127 model: 12
128 130
129 GridLayout { 131 columns: 3
130 columns: 2 132 columnSpacing: 16
133 rowSpacing: 16
131 134
132 required property int index 135 Layout.alignment: Qt.AlignHCenter
133 property int month: index 136 Layout.fillWidth: false
134 137
135 id: monthCalendar 138 property real angleRem: 0
139 property real sensitivity: 1 / 120
136 140
137 Layout.alignment: Qt.AlignTop | Qt.AlignRight 141 function scrollYear(event) {
138 Layout.fillWidth: false 142 angleRem += event.angleDelta.y;
143 const d = Math.round(angleRem * sensitivity);
144 yearCalendar.year += d;
145 angleRem -= d / sensitivity;
146 }
139 147
140 Text { 148 Connections {
141 Layout.column: 1 149 target: clockMouseArea
142 Layout.fillWidth: true 150 function onWheel(event) { yearCalendar.scrollYear(event); }
151 }
143 152
144 horizontalAlignment: Text.AlignHCenter 153 Repeater {
154 model: 12
145 155
146 font.pointSize: 10 156 GridLayout {
147 font.family: "Fira Sans" 157 columns: 2
148 158
149 text: { 159 required property int index
150 const date = Date.fromLocaleDateString(Qt.locale(), `${yearCalendar.year}-${monthCalendar.month + 1}-01`, "yyyy-M-dd"); 160 property int month: index
151 return date.toLocaleString(Qt.locale("en_DK"), "MMMM")
152 }
153 161
154 color: "#ffead3" 162 id: monthCalendar
155 }
156 163
157 DayOfWeekRow { 164 Layout.alignment: Qt.AlignTop | Qt.AlignRight
158 locale: grid.locale 165 Layout.fillWidth: false
159 166
160 Layout.row: 1 167 Text {
161 Layout.column: 1 168 Layout.column: 1
162 Layout.fillWidth: true 169 Layout.fillWidth: true
163 170
164 delegate: Text { 171 horizontalAlignment: Text.AlignHCenter
165 required property string shortName
166 172
167 font.pointSize: 10 173 font.pointSize: 10
168 font.family: "Fira Mono" 174 font.family: "Fira Sans"
169 175
170 text: shortName 176 text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM")
171 color: "#ffcc66"
172 177
173 horizontalAlignment: Text.AlignRight 178 color: "#ffead3"
174 verticalAlignment: Text.AlignVCenter
175 } 179 }
176 }
177 180
178 WeekNumberColumn { 181 DayOfWeekRow {
179 month: grid.month 182 locale: grid.locale
180 year: grid.year
181 locale: grid.locale
182 183
183 Layout.fillHeight: true 184 Layout.row: 1
185 Layout.column: 1
186 Layout.fillWidth: true
184 187
185 delegate: Text { 188 delegate: WrapperItem {
186 required property int weekNumber 189 required property string shortName
187 190
188 opacity: { 191 width: dowLabel.contentWidth + 6
189 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
190 const dayOfWeek = simple.getDay();
191 const isoWeekStart = simple;
192 192
193 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); 193 Text {
194 if (dayOfWeek > 4) { 194 id: dowLabel
195 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
196 }
197 195
198 for (let i = 0; i < 7; i++) { 196 anchors.fill: parent
199 const dayInWeek = new Date(isoWeekStart);
200 dayInWeek.setDate(dayInWeek.getDate() + i);
201 if (dayInWeek.getMonth() == monthCalendar.month)
202 return 1;
203 }
204 197
205 return 0; 198 font.pointSize: 10
206 } 199 font.family: "Fira Sans"
207
208 font.pointSize: 10
209 font.family: "Fira Sans"
210 font.features: { "tnum": 1 }
211 200
212 text: weekNumber 201 text: parent.shortName
213 color: "#99ffdd" 202 color: "#ffcc66"
214 203
215 horizontalAlignment: Text.AlignRight 204 horizontalAlignment: Text.AlignHCenter
216 verticalAlignment: Text.AlignVCenter 205 verticalAlignment: Text.AlignVCenter
206 }
207 }
217 } 208 }
218 }
219 209
220 MonthGrid { 210 WeekNumberColumn {
221 id: grid 211 month: grid.month
212 year: grid.year
213 locale: grid.locale
222 214
223 year: yearCalendar.year 215 Layout.fillHeight: true
224 month: monthCalendar.month
225 locale: Qt.locale("en_DK")
226 216
227 Layout.fillWidth: true 217 delegate: Text {
228 Layout.fillHeight: true 218 required property int weekNumber
229 219
230 delegate: Text { 220 opacity: {
231 required property var model 221 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
222 const dayOfWeek = simple.getDay();
223 const isoWeekStart = simple;
232 224
233 opacity: model.month === monthCalendar.month ? 1 : 0 225 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1);
226 if (dayOfWeek > 4) {
227 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
228 }
229
230 for (let i = 0; i < 7; i++) {
231 const dayInWeek = new Date(isoWeekStart);
232 dayInWeek.setDate(dayInWeek.getDate() + i);
233 if (dayInWeek.getMonth() == monthCalendar.month)
234 return 1;
235 }
236
237 return 0;
238 }
234 239
235 font.pointSize: 10 240 font.pointSize: 10
236 font.family: "Fira Sans" 241 font.family: "Fira Sans"
237 font.features: { "tnum": 1 } 242 font.features: { "tnum": 1 }
238 243
239 text: model.day 244 text: weekNumber
240 color: model.today ? "#ff6699" : "white" 245 color: "#99ffdd"
241 246
242 horizontalAlignment: Text.AlignRight 247 horizontalAlignment: Text.AlignRight
243 verticalAlignment: Text.AlignVCenter 248 verticalAlignment: Text.AlignVCenter
249 }
250 }
251
252 MonthGrid {
253 id: grid
254
255 year: yearCalendar.year
256 month: monthCalendar.month
257 locale: Qt.locale("en_DK")
258
259 Layout.fillWidth: true
260 Layout.fillHeight: true
261
262 delegate: Text {
263 required property var model
264
265 opacity: model.month === monthCalendar.month ? 1 : 0
266
267 font.pointSize: 10
268 font.family: "Fira Sans"
269 font.features: { "tnum": 1 }
270
271 property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day
272
273 text: model.day
274 color: today ? "#ff6699" : "white"
275
276 horizontalAlignment: Text.AlignRight
277 verticalAlignment: Text.AlignVCenter
278 }
244 } 279 }
245 } 280 }
246 } 281 }
@@ -250,4 +285,4 @@ Item {
250 } 285 }
251 } 286 }
252 } 287 }
253} \ No newline at end of file 288}
diff --git a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
index b9f91580..bc3750f9 100644
--- a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
+++ b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
@@ -3,25 +3,16 @@ import QtQuick
3import qs.Services 3import qs.Services
4import Quickshell.Widgets 4import Quickshell.Widgets
5 5
6Rectangle { 6Item {
7 id: kbdWidget
8
9 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" }
10
11 width: kbdLabel.contentWidth + 8 7 width: kbdLabel.contentWidth + 8
12 color: {
13 if (kbdMouseArea.containsMouse) {
14 return "#33808080";
15 }
16 return "transparent";
17 }
18 height: parent.height 8 height: parent.height
19 anchors.verticalCenter: parent.verticalCenter 9 anchors.verticalCenter: parent.verticalCenter
20 10
21 MouseArea { 11 WrapperMouseArea {
22 id: kbdMouseArea 12 id: kbdMouseArea
23 13
24 anchors.fill: parent 14 anchors.fill: parent
15
25 hoverEnabled: true 16 hoverEnabled: true
26 cursorShape: Qt.PointingHandCursor 17 cursorShape: Qt.PointingHandCursor
27 enabled: true 18 enabled: true
@@ -31,25 +22,39 @@ Rectangle {
31 onWheel: event => { 22 onWheel: event => {
32 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {}) 23 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {})
33 } 24 }
34 }
35 25
36 Text { 26 Rectangle {
37 id: kbdLabel 27 id: kbdWidget
38 28
39 font.pointSize: 10 29 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" }
40 font.family: "Fira Sans" 30
41 color: { 31 anchors.fill: parent
42 if (NiriService.keyboardLayouts?.current_idx === 0) 32 color: {
43 return "#555"; 33 if (kbdMouseArea.containsMouse) {
44 return "white"; 34 return "#33808080";
45 } 35 }
46 anchors.centerIn: parent 36 return "transparent";
37 }
47 38
48 text: { 39 Text {
49 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; 40 id: kbdLabel
50 if (!currentLayout) 41
51 return ""; 42 font.pointSize: 10
52 return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; 43 font.family: "Fira Sans"
44 color: {
45 if (NiriService.keyboardLayouts?.current_idx === 0)
46 return "#555";
47 return "white";
48 }
49 anchors.centerIn: parent
50
51 text: {
52 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx];
53 if (!currentLayout)
54 return "";
55 return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout;
56 }
57 }
53 } 58 }
54 } 59 }
55 60
diff --git a/accounts/gkleen@sif/shell/quickshell/LockSurface.qml b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml
new file mode 100644
index 00000000..18698725
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml
@@ -0,0 +1,223 @@
1import Quickshell.Widgets
2import QtQuick.Effects
3import QtQuick.Layouts
4import QtQuick
5import QtQuick.Controls
6import QtQuick.Controls.Fusion
7import qs.Services
8import QtQml
9
10Item {
11 id: lockSurface
12
13 property var screen
14 property list<var> messages: []
15 property bool responseRequired: false
16 property bool responseVisible: false
17 property string currentText: ""
18 property bool authRunning: false
19
20 signal response(string responseText)
21
22 anchors.fill: parent
23
24 Item {
25 id: background
26
27 anchors.fill: parent
28
29 property Img current: one
30 property string source: selector.selected
31
32 WallpaperSelector {
33 id: selector
34 seed: lockSurface.screen?.name || ""
35 }
36
37 onSourceChanged: {
38 if (!source)
39 current = null;
40 else if (current === one)
41 two.update()
42 else
43 one.update()
44 }
45
46 Img { id: one }
47 Img { id: two }
48
49 component Img: Item {
50 id: img
51
52 property string source
53
54 function update() {
55 source = background.source || ""
56 }
57
58 anchors.fill: parent
59
60 Image {
61 id: imageSource
62
63 source: img.source
64 sourceSize: Qt.size(parent.width, parent.height)
65 fillMode: Image.PreserveAspectCrop
66 smooth: true
67 visible: false
68 asynchronous: true
69 cache: false
70
71 onStatusChanged: {
72 if (status === Image.Ready) {
73 background.current = img
74 }
75 }
76 }
77
78 MultiEffect {
79 id: imageEffect
80
81 source: imageSource
82 anchors.fill: parent
83 blurEnabled: true
84 blur: 1
85 blurMax: 64
86 blurMultiplier: 2
87
88 opacity: 0
89
90 states: State {
91 name: "visible"
92 when: background.current === img
93
94 PropertyChanges {
95 imageEffect.opacity: 1
96 }
97 StateChangeScript {
98 name: "unloadOther"
99 script: {
100 if (img === one)
101 two.source = ""
102 if (img === two)
103 one.source = ""
104 }
105 }
106 }
107
108 transitions: Transition {
109 SequentialAnimation {
110 NumberAnimation {
111 target: imageEffect
112 properties: "opacity"
113 duration: 5000
114 easing.type: Easing.OutCubic
115 }
116 ScriptAction {
117 scriptName: "unloadOther"
118 }
119 }
120 }
121 }
122 }
123 }
124
125 Item {
126 anchors {
127 top: lockSurface.top
128 left: lockSurface.left
129 right: lockSurface.right
130 }
131
132 implicitWidth: lockSurface.width
133 implicitHeight: 21
134
135 Rectangle {
136 anchors.fill: parent
137 color: Qt.rgba(0, 0, 0, 0.75)
138 }
139
140 Clock {
141 anchors.centerIn: parent
142 calendarPopup: false
143 }
144 }
145
146 WrapperRectangle {
147 id: unlockUi
148
149 Keys.onPressed: event => {
150 if (!lockSurface.authRunning) {
151 event.accepted = true;
152 lockSurface.authRunning = true;
153 }
154 }
155 focus: !passwordBox.visible
156
157 visible: lockSurface.authRunning
158
159 color: Qt.rgba(0, 0, 0, 0.75)
160 margin: 8
161
162 anchors.centerIn: parent
163
164 ColumnLayout {
165 spacing: 4
166
167 BusyIndicator {
168 visible: running
169 running: !Array.from(lockSurface.messages).length && !lockSurface.responseRequired
170 }
171
172 Repeater {
173 model: lockSurface.messages
174
175 Text {
176 required property var modelData
177
178 font.pointSize: 10
179 font.family: "Fira Sans"
180 color: modelData.error ? "#f28a21" : "#ffffff"
181
182 text: String(modelData.text).trim()
183
184 Layout.fillWidth: true
185 horizontalAlignment: Text.AlignHCenter
186 }
187 }
188
189 TextField {
190 id: passwordBox
191
192 visible: lockSurface.responseRequired
193 echoMode: lockSurface.responseVisible ? TextInput.Normal : TextInput.Password
194 inputMethodHints: Qt.ImhSensitiveData
195
196 onTextChanged: lockSurface.currentText = passwordBox.text
197 onAccepted: {
198 passwordBox.readOnly = true;
199 lockSurface.response(lockSurface.currentText);
200 }
201
202 Connections {
203 target: lockSurface
204 function onCurrentTextChanged() {
205 passwordBox.text = lockSurface.currentText
206 }
207 }
208 Connections {
209 target: lockSurface
210 function onResponseRequiredChanged() {
211 if (lockSurface.responseRequired)
212 passwordBox.readOnly = false;
213 passwordBox.focus = true;
214 passwordBox.selectAll();
215 }
216 }
217
218 Layout.topMargin: 4
219 Layout.fillWidth: true
220 }
221 }
222 }
223}
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
new file mode 100644
index 00000000..8e739359
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -0,0 +1,90 @@
1import Quickshell
2import Quickshell.Wayland
3import Quickshell.Io
4import Quickshell.Services.Pam
5import Quickshell.Services.Mpris
6import Custom as Custom
7import qs.Services
8import QtQml
9
10Scope {
11 id: lockscreen
12
13 property string currentText: ""
14
15 PamContext {
16 id: pam
17
18 property list<var> messages: []
19
20 config: "quickshell"
21 onCompleted: result => {
22 if (result === PamResult.Success) {
23 lock.locked = false;
24 }
25 }
26 onPamMessage: {
27 messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }])
28 }
29 onActiveChanged: {
30 messages = [];
31 }
32 }
33
34 IpcHandler {
35 target: "Lockscreen"
36
37 function setLocked(locked: bool): void { lock.locked = locked; }
38 function getLocked(): bool { return lock.locked; }
39 }
40
41 WlSessionLock {
42 id: lock
43
44 onLockStateChanged: {
45 if (!locked && pam.active)
46 pam.abort();
47
48 if (locked) {
49 Custom.KeePassXC.lockAllDatabases();
50 Array.from(Mpris.players.values).forEach(player => {
51 if (player.canPause && player.isPlaying)
52 player.pause();
53 });
54 // Custom.Systemd.stopUserUnit("gpg-agent.service", "replace");
55 GpgAgent.reloadAgent();
56 }
57 }
58
59 WlSessionLockSurface {
60 id: lockSurface
61
62 color: "black"
63
64 LockSurface {
65 id: surfaceContent
66
67 onResponse: responseText => pam.respond(responseText)
68 onAuthRunningChanged: {
69 if (authRunning)
70 pam.start();
71 }
72 Connections {
73 target: pam
74 function onMessagesChanged() { surfaceContent.messages = pam.messages; }
75 function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; }
76 function onActiveChanged() { surfaceContent.authRunning = pam.active; }
77 }
78 onCurrentTextChanged: lockscreen.currentText = currentText
79 Connections {
80 target: lockscreen
81 function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; }
82 }
83 Connections {
84 target: lockSurface
85 function onScreenChanged() { surfaceContent.screen = lockSurface.screen; }
86 }
87 }
88 }
89 }
90}
diff --git a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
new file mode 100644
index 00000000..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..3e0b8fd9
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -0,0 +1,471 @@
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: "transparent"
118
119 Rectangle {
120 width: tooltip.width
121 height: tooltipLayout.childrenRect.height + 16
122 color: "black"
123 }
124
125 WrapperItem {
126 id: tooltipContent
127
128 bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight)
129
130 WrapperMouseArea {
131 id: tooltipMouseArea
132
133 hoverEnabled: true
134 enabled: true
135
136 WrapperItem {
137 margin: 8
138 bottomMargin: 8
139
140 GridLayout {
141 id: tooltipLayout
142
143 columns: 4
144
145 Repeater {
146 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
147
148 Item {
149 id: descItem
150
151 required property var modelData
152 required property int index
153
154 Layout.column: 0
155 Layout.row: index
156
157 implicitWidth: descText.contentWidth
158 implicitHeight: descText.contentHeight
159
160 Text {
161 id: descText
162
163 color: "white"
164 font.pointSize: 10
165 font.family: "Fira Sans"
166
167 text: descItem.modelData.description
168 }
169 }
170 }
171
172 Repeater {
173 id: defaultSinkRepeater
174
175 model: {
176 Array.from(Pipewire.devices.values)
177 .filter(dev => dev.type == "Audio/Device")
178 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
179 }
180
181 Item {
182 id: defaultSinkItem
183
184 required property var modelData
185 required property int index
186
187 visible: Boolean(modelData)
188
189 PwObjectTracker {
190 objects: [defaultSinkItem.modelData]
191 }
192
193 Layout.column: 1
194 Layout.row: index
195
196 Layout.fillHeight: true
197
198 implicitWidth: 16 + 8
199
200 WrapperMouseArea {
201 id: defaultSinkMouseArea
202
203 anchors.fill: parent
204 hoverEnabled: true
205 cursorShape: Qt.PointingHandCursor
206
207 onClicked: {
208 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
209 }
210
211 onWheel: event => scrollVolume(event);
212 property real sensitivity: (1 / 40) / 120
213 function scrollVolume(event) {
214 defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
215 }
216
217 Rectangle {
218 id: defaultSinkWidget
219
220 anchors.fill: parent
221 color: {
222 if (defaultSinkMouseArea.containsMouse)
223 return "#33808080";
224 return "transparent";
225 }
226
227 MaterialDesignIcon {
228 width: 16
229 height: 16
230 anchors.centerIn: parent
231
232 icon: {
233 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id)
234 return "speaker";
235 return "speaker-off";
236 }
237 color: icon == "speaker" ? "white" : "#555"
238 }
239 }
240 }
241
242 PopupWindow {
243 id: volumeTooltip
244
245 property bool nextVisible: defaultSinkMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
246
247 anchor {
248 item: defaultSinkMouseArea
249 edges: Edges.Bottom | Edges.Left
250 }
251 visible: false
252
253 onNextVisibleChanged: volumeHangTimer.restart()
254
255 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
256
257 Timer {
258 id: volumeHangTimer
259 interval: 100
260 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
261 }
262
263 implicitWidth: volumeTooltipText.contentWidth + 16
264 implicitHeight: volumeTooltipText.contentHeight + 16
265 color: "black"
266
267 WrapperMouseArea {
268 id: volumeTooltipMouseArea
269
270 hoverEnabled: true
271 enabled: true
272
273 onWheel: event => defaultSinkMouseArea.scrollVolume(event);
274
275 anchors.centerIn: parent
276
277 Text {
278 id: volumeTooltipText
279
280 font.pointSize: 10
281 font.family: "Fira Sans"
282 color: "white"
283
284 text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%`
285 }
286 }
287 }
288 }
289 }
290
291 Repeater {
292 id: defaultSourceRepeater
293
294 model: {
295 Array.from(Pipewire.devices.values)
296 .filter(dev => dev.type == "Audio/Device")
297 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
298 }
299
300 Item {
301 id: defaultSourceItem
302
303 required property var modelData
304 required property int index
305
306 visible: Boolean(modelData)
307
308 PwObjectTracker {
309 objects: [defaultSourceItem.modelData]
310 }
311
312 Layout.column: 2
313 Layout.row: index
314
315 Layout.fillHeight: true
316
317 implicitWidth: 16 + 8
318
319 WrapperMouseArea {
320 id: defaultSourceMouseArea
321
322 anchors.fill: parent
323 hoverEnabled: true
324 cursorShape: Qt.PointingHandCursor
325
326 onClicked: {
327 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
328 }
329
330 onWheel: event => scrollVolume(event);
331 property real sensitivity: (1 / 40) / 120
332 function scrollVolume(event) {
333 defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
334 }
335
336 Rectangle {
337 id: defaultSourceWidget
338
339 anchors.fill: parent
340 color: {
341 if (defaultSourceMouseArea.containsMouse)
342 return "#33808080";
343 return "transparent";
344 }
345
346 MaterialDesignIcon {
347 width: 16
348 height: 16
349 anchors.centerIn: parent
350
351 icon: {
352 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
353 return "microphone";
354 return "microphone-off";
355 }
356 color: icon == "microphone" ? "white" : "#555"
357 }
358 }
359 }
360
361 PopupWindow {
362 id: volumeTooltip
363
364 property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
365
366 anchor {
367 item: defaultSourceMouseArea
368 edges: Edges.Bottom | Edges.Left
369 }
370 visible: false
371
372 onNextVisibleChanged: volumeHangTimer.restart()
373
374 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
375
376 Timer {
377 id: volumeHangTimer
378 interval: 100
379 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
380 }
381
382 implicitWidth: volumeTooltipText.contentWidth + 16
383 implicitHeight: volumeTooltipText.contentHeight + 16
384 color: "black"
385
386 WrapperMouseArea {
387 id: volumeTooltipMouseArea
388
389 hoverEnabled: true
390 enabled: true
391
392 onWheel: event => defaultSourceMouseArea.scrollVolume(event);
393
394 anchors.centerIn: parent
395
396 Text {
397 id: volumeTooltipText
398
399 font.pointSize: 10
400 font.family: "Fira Sans"
401 color: "white"
402
403 text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%`
404 }
405 }
406 }
407 }
408 }
409
410 Repeater {
411 id: profileRepeater
412
413 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
414
415 Item {
416 id: profileItem
417
418 required property var modelData
419 required property int index
420
421 PwObjectTracker {
422 objects: [profileItem.modelData]
423 }
424
425 Layout.column: 3
426 Layout.row: index
427
428 Layout.fillWidth: true
429
430 implicitWidth: Math.max(profileBox.implicitWidth, 300)
431 implicitHeight: profileBox.height
432
433 ComboBox {
434 id: profileBox
435
436 model: profileItem.modelData.profiles
437
438 textRole: "description"
439 valueRole: "index"
440 onActivated: profileItem.modelData.setProfile(currentValue)
441
442 anchors.fill: parent
443
444 implicitContentWidthPolicy: ComboBox.WidestText
445
446 Connections {
447 target: profileItem.modelData
448 function onCurrentProfileChanged() {
449 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
450 }
451 }
452 Component.onCompleted: {
453 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
454 }
455
456 Connections {
457 target: profileBox.popup
458 function onVisibleChanged() {
459 tooltip.openPopup = profileBox.popup.visible
460 }
461 }
462 }
463 }
464 }
465 }
466 }
467 }
468 }
469 }
470 }
471}
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..8318df50
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
@@ -0,0 +1,75 @@
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 Connections {
27 target: currFile
28 function onLoaded() {
29 const b = root.calcCurrBrightness();
30 if (typeof b !== 'undefined')
31 root.currBrightness = b;
32 }
33 }
34 Connections {
35 target: maxFile
36 function onLoaded() {
37 const b = root.calcCurrBrightness();
38 if (typeof b !== 'undefined')
39 root.currBrightness = b;
40 }
41 }
42
43 onCurrBrightnessChanged: {
44 root.currBrightness = Math.max(0, Math.min(1, root.currBrightness));
45
46 const prev = root.calcCurrBrightness();
47 if (typeof prev === 'undefined' || Math.abs(root.currBrightness - prev) < 0.01)
48 return;
49
50 const max = Number(maxFile.text());
51 const actual = Number(currFile.text());
52 let curr = Math.max(0, Math.min(max, Math.pow(root.currBrightness, root.exponent) * max));
53 if (Math.round(curr) == actual && curr < actual)
54 curr = Math.max(0, actual - 1);
55 else if (Math.round(curr) == actual && curr > actual)
56 curr = Math.min(max, actual + 1);
57 // root.currBrightness = Math.pow(curr / max, 1 / root.exponent);
58 Custom.Systemd.setBrightness(root.subsystem, root.device, Math.round(curr));
59 }
60
61 FileView {
62 id: currFile
63 path: `/sys/class/${root.subsystem}/${root.device}/brightness`
64 blockAllReads: true
65 watchChanges: true
66 onFileChanged: reload()
67 }
68 FileView {
69 id: maxFile
70 path: `/sys/class/${root.subsystem}/${root.device}/max_brightness`
71 blockAllReads: true
72 watchChanges: true
73 onFileChanged: reload()
74 }
75}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
new file mode 100644
index 00000000..3de69535
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
@@ -0,0 +1,18 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5
6Singleton {
7 id: root
8
9 Socket {
10 id: agentSocket
11 connected: true
12 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent`
13 }
14
15 function reloadAgent() {
16 agentSocket.write("RELOADAGENT\n")
17 }
18}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
index c82caaa6..af522ec4 100644
--- a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
@@ -28,9 +28,12 @@ Singleton {
28 path: root.socketPath 28 path: root.socketPath
29 connected: true 29 connected: true
30 30
31 property bool acked: false
32
31 onConnectionStateChanged: { 33 onConnectionStateChanged: {
32 if (connected) { 34 if (connected) {
33 write('"EventStream"\n') 35 acked = false;
36 write('"EventStream"\n');
34 } 37 }
35 } 38 }
36 39
@@ -66,6 +69,9 @@ Singleton {
66 eventWindowUrgencyChanged(event.WindowUrgencyChanged); 69 eventWindowUrgencyChanged(event.WindowUrgencyChanged);
67 else if (event.WindowLayoutsChanged) 70 else if (event.WindowLayoutsChanged)
68 eventWindowLayoutsChanged(event.WindowLayoutsChanged); 71 eventWindowLayoutsChanged(event.WindowLayoutsChanged);
72 else if (event.Ok && !eventStreamSocket.acked) { eventStreamSocket.acked = true; }
73 else if (event.OverviewOpenedOrClosed) {}
74 else if (event.ConfigLoaded) {}
69 else 75 else
70 console.log(JSON.stringify(event)); 76 console.log(JSON.stringify(event));
71 } catch (e) { 77 } catch (e) {
@@ -154,7 +160,11 @@ Singleton {
154 }); 160 });
155 } 161 }
156 function eventWindowOpenedOrChanged(data) { 162 function eventWindowOpenedOrChanged(data) {
157 root.windows = Array.from(root.windows).filter(win => win.id !== data.window.id).concat([data.window]); 163 root.windows = Array.from(root.windows).map(win => {
164 if (data.window.is_focused)
165 win.is_focused = false;
166 return win;
167 }).filter(win => win.id !== data.window.id).concat([data.window]);
158 } 168 }
159 function eventWindowClosed(data) { 169 function eventWindowClosed(data) {
160 root.windows = Array.from(root.windows).filter(win => win.id !== data.id); 170 root.windows = Array.from(root.windows).filter(win => win.id !== data.id);
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/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/Services/WallpaperSelector.qml b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
new file mode 100644
index 00000000..3c524955
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
@@ -0,0 +1,8 @@
1import Custom as Custom
2
3Custom.FileSelector {
4 id: root
5
6 directory: @wallpapers@
7 epoch: 72000000
8}
diff --git a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
index ba678138..6f70be29 100644
--- a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
+++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
@@ -17,13 +17,17 @@ Item {
17 spacing: 0 17 spacing: 0
18 18
19 Repeater { 19 Repeater {
20 model: { 20 model: ScriptModel {
21 var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive); 21 values: {
22 trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id)) 22 var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive);
23 return trayItems; 23 trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id))
24 return trayItems;
25 }
24 } 26 }
25 27
26 delegate: Item { 28 delegate: Item {
29 id: trayItemWrapper
30
27 property var trayItem: modelData 31 property var trayItem: modelData
28 property string iconSource: { 32 property string iconSource: {
29 let icon = trayItem && trayItem.icon 33 let icon = trayItem && trayItem.icon
@@ -43,21 +47,11 @@ Item {
43 return "" 47 return ""
44 } 48 }
45 49
46 width: 16 50 width: icon.width + 6
47 height: parent.height 51 height: parent.height
48 anchors.verticalCenter: parent.verticalCenter 52 anchors.verticalCenter: parent.verticalCenter
49 53
50 IconImage { 54 WrapperMouseArea {
51 anchors.centerIn: parent
52 width: parent.width
53 height: parent.width
54 source: parent.iconSource
55 asynchronous: true
56 smooth: true
57 mipmap: true
58 }
59
60 MouseArea {
61 id: trayItemArea 55 id: trayItemArea
62 56
63 anchors.fill: parent 57 anchors.fill: parent
@@ -88,6 +82,26 @@ Item {
88 menuAnchor.open() 82 menuAnchor.open()
89 } 83 }
90 } 84 }
85
86 Rectangle {
87 anchors.fill: parent
88 color: {
89 if (trayItemArea.containsMouse)
90 return "#33808080";
91 return "transparent";
92 }
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 }
104 }
91 } 105 }
92 106
93 PopupWindow { 107 PopupWindow {
@@ -112,7 +126,7 @@ Item {
112 color: "black" 126 color: "black"
113 127
114 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16 128 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16
115 implicitHeight: tooltipTitle.contentHeight + tooltipDescription.contentHeight + 16 129 implicitHeight: (trayItem.tooltipTitle ? tooltipTitle.contentHeight : 0) + (trayItem.tooltipDescription ? tooltipDescription.contentHeight : 0) + 16
116 130
117 WrapperMouseArea { 131 WrapperMouseArea {
118 id: tooltipMouseArea 132 id: tooltipMouseArea
@@ -126,6 +140,8 @@ Item {
126 Text { 140 Text {
127 id: tooltipTitle 141 id: tooltipTitle
128 142
143 enabled: trayItem.tooltipTitle
144
129 font.pointSize: 10 145 font.pointSize: 10
130 font.family: "Fira Sans" 146 font.family: "Fira Sans"
131 font.bold: true 147 font.bold: true
@@ -137,6 +153,8 @@ Item {
137 Text { 153 Text {
138 id: tooltipDescription 154 id: tooltipDescription
139 155
156 enabled: trayItem.tooltipDescription
157
140 font.pointSize: 10 158 font.pointSize: 10
141 font.family: "Fira Sans" 159 font.family: "Fira Sans"
142 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..653f4763
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
@@ -0,0 +1,163 @@
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 if (Pipewire.defaultAudioSource.audio.volume > 1)
126 return "microphone-plus";
127 return "microphone";
128 }
129 return "volume-high";
130 }
131 }
132
133 Rectangle {
134 Layout.fillWidth: true
135
136 implicitHeight: 10
137
138 color: "#50ffffff"
139
140 Rectangle {
141 anchors {
142 left: parent.left
143 top: parent.top
144 bottom: parent.bottom
145 }
146
147 color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white"
148
149 implicitWidth: {
150 if (root.show == "sink")
151 return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0);
152 else if (root.show == "source")
153 return parent.width * Math.min(1, (Pipewire.defaultAudioSource?.audio.volume ?? 0));
154 return 0;
155 }
156 }
157 }
158 }
159 }
160 }
161 }
162 }
163}
diff --git a/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml
new file mode 100644
index 00000000..de31915f
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml
@@ -0,0 +1,85 @@
1import QtQuick
2import Quickshell
3import qs.Services
4
5Item {
6 id: root
7
8 anchors.fill: parent
9
10 required property string screen
11
12 property Img current: one
13 property string source: selector.selected
14
15 WallpaperSelector {
16 id: selector
17 seed: screen
18 }
19
20 onSourceChanged: {
21 if (!source)
22 current = null;
23 else if (current === one)
24 two.update()
25 else
26 one.update()
27 }
28
29 Img { id: one }
30 Img { id: two }
31
32 component Img: Image {
33 id: img
34
35 function update() {
36 source = root.source || ""
37 }
38
39 anchors.fill: parent
40 fillMode: Image.PreserveAspectCrop
41 smooth: true
42 asynchronous: true
43 cache: false
44
45 opacity: 0
46
47 onStatusChanged: {
48 if (status === Image.Ready) {
49 root.current = this
50 }
51 }
52
53 states: State {
54 name: "visible"
55 when: root.current === img
56
57 PropertyChanges {
58 img.opacity: 1
59 }
60 StateChangeScript {
61 name: "unloadOther"
62 script: {
63 if (img === one)
64 two.source = ""
65 if (img === two)
66 one.source = ""
67 }
68 }
69 }
70
71 transitions: Transition {
72 SequentialAnimation {
73 NumberAnimation {
74 target: img
75 properties: "opacity"
76 duration: 5000
77 easing.type: Easing.OutCubic
78 }
79 ScriptAction {
80 scriptName: "unloadOther"
81 }
82 }
83 }
84 }
85}
diff --git a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
index 153c56bb..c8c017c3 100644
--- a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
+++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
@@ -1,5 +1,8 @@
1import Quickshell
1import QtQuick 2import QtQuick
2import qs.Services 3import qs.Services
4import Quickshell.Widgets
5import QtQuick.Layouts
3 6
4Row { 7Row {
5 id: workspaces 8 id: workspaces
@@ -11,61 +14,188 @@ Row {
11 spacing: 0 14 spacing: 0
12 15
13 Repeater { 16 Repeater {
14 model: { 17 model: ScriptModel {
15 let currWorkspaces = NiriService.workspaces; 18 values: {
16 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); 19 let currWorkspaces = NiriService.workspaces;
17 currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); 20 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces);
18 currWorkspaces.sort((a, b) => { 21 currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name));
19 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) 22 currWorkspaces.sort((a, b) => {
20 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x 23 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x)
21 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) 24 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x
22 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y 25 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y)
23 return a.idx - b.idx; 26 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y
24 }); 27 return a.idx - b.idx;
25 return currWorkspaces; 28 });
29 return currWorkspaces;
30 }
26 } 31 }
27 32
28 Rectangle { 33 Item {
34 id: wsItem
35
29 property var workspaceData: modelData 36 property var workspaceData: modelData
30 37
31 width: wsLabel.contentWidth + 8 38 width: wsLabel.contentWidth + 8
32 color: {
33 if (mouseArea.containsMouse) {
34 return "#33808080";
35 }
36 return "transparent";
37 }
38 height: parent.height 39 height: parent.height
39 anchors.verticalCenter: parent.verticalCenter 40 anchors.verticalCenter: parent.verticalCenter
40 41
41 MouseArea { 42 WrapperMouseArea {
42 id: mouseArea 43 id: mouseArea
43 44
44 anchors.fill: parent 45 anchors.fill: parent
46
45 hoverEnabled: true 47 hoverEnabled: true
46 cursorShape: Qt.PointingHandCursor 48 cursorShape: Qt.PointingHandCursor
47 enabled: true 49 enabled: true
48 onClicked: { 50 onClicked: {
49 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}) 51 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {})
50 } 52 }
53
54 Rectangle {
55 anchors.fill: parent
56
57 color: {
58 if (mouseArea.containsMouse) {
59 return "#33808080";
60 }
61 return "transparent";
62 }
63
64 Text {
65 id: wsLabel
66
67 anchors.centerIn: parent
68
69 font.pointSize: 10
70 font.family: "Fira Sans"
71 color: {
72 if (workspaceData.is_active)
73 return "#23fd00";
74 if (workspaceData.active_window_id === null)
75 return "#555";
76 return "white";
77 }
78
79 text: workspaceData.name ? workspaceData.name : workspaceData.idx
80 }
81 }
51 } 82 }
52 83
53 Text { 84 PopupWindow {
54 id: wsLabel 85 id: tooltip
55 86
56 font.pointSize: 10 87 property bool nextVisible: (mouseArea.containsMouse || tooltipMouseArea.containsMouse) && [...windowsModel.values].length > 0
57 font.family: "Fira Sans" 88
58 color: { 89 anchor {
59 if (workspaceData.is_active) 90 item: mouseArea
60 return "#23fd00"; 91 edges: Edges.Bottom | Edges.Left
61 if (workspaceData.active_window_id === null) 92 }
62 return "#555"; 93 visible: false
63 return "white"; 94
95 onNextVisibleChanged: hangTimer.restart()
96
97 Timer {
98 id: hangTimer
99 interval: 100
100 onTriggered: tooltip.visible = tooltip.nextVisible
64 } 101 }
65 anchors.centerIn: parent
66 102
67 text: workspaceData.name ? workspaceData.name : workspaceData.idx 103 implicitWidth: tooltipContent.implicitWidth
104 implicitHeight: tooltipContent.implicitHeight
105 color: "black"
106
107 WrapperMouseArea {
108 id: tooltipMouseArea
109
110 hoverEnabled: true
111 enabled: true
112
113 anchors.fill: parent
114
115 WrapperItem {
116 id: tooltipContent
117
118 margin: 0
119
120 ColumnLayout {
121 spacing: 0
122
123 Repeater {
124 model: ScriptModel {
125 id: windowsModel
126
127 values: {
128 let currWindows = Array.from(NiriService.windows).filter(win => win.workspace_id == wsItem.workspaceData.id);
129 currWindows.sort((a, b) => {
130 if (a.is_floating !== b.is_floating)
131 return b.is_floating - a.is_floating;
132 if (a.layout.tile_pos_in_workspace_view?.[0] !== b.layout.tile_pos_in_workspace_view?.[0])
133 return a.layout.tile_pos_in_workspace_view?.[0] - b.layout.tile_pos_in_workspace_view?.[0]
134 if (a.layout.tile_pos_in_workspace_view?.[1] !== b.layout.tile_pos_in_workspace_view?.[1])
135 return a.layout.tile_pos_in_workspace_view?.[1] - b.layout.tile_pos_in_workspace_view?.[1]
136 if (a.layout.pos_in_scrolling_layout?.[0] !== b.layout.pos_in_scrolling_layout?.[0])
137 return a.layout.pos_in_scrolling_layout?.[0] - b.layout.pos_in_scrolling_layout?.[0]
138 if (a.layout.pos_in_scrolling_layout?.[1] !== b.layout.pos_in_scrolling_layout?.[1])
139 return a.layout.pos_in_scrolling_layout?.[1] - b.layout.pos_in_scrolling_layout?.[1]
140 if (a.app_id !== b.app_id)
141 return a.app_id.localeCompare(b.app_id);
142
143 return a.title.localeCompare(b.title);
144 });
145 return currWindows;
146 }
147 }
148
149 WrapperMouseArea {
150 id: windowMouseArea
151
152 property var windowData: modelData
153
154 hoverEnabled: true
155 cursorShape: Qt.PointingHandCursor
156 enabled: true
157
158 Layout.fillWidth: true
159
160 onClicked: {
161 NiriService.sendCommand({ "Action": { "FocusWindow": { "id": windowData.id } } }, _ => {})
162 }
163
164 WrapperRectangle {
165 color: windowMouseArea.containsMouse ? "#33808080" : "transparent";
166
167 anchors.fill: parent
168
169 WrapperItem {
170 anchors.fill: parent
171
172 margin: 4
173
174 Text {
175 id: windowLabel
176
177 font.pointSize: 10
178 font.family: "Fira Sans"
179 color: {
180 if (windowData.is_focused)
181 return "#23fd00";
182 if (NiriService.workspaces.find(ws => ws.id == windowData.workspace_id)?.active_window_id == windowData.id)
183 return "white";
184 return "#555";
185 }
186
187 text: windowData.title
188
189 horizontalAlignment: Text.AlignLeft
190 }
191 }
192 }
193 }
194 }
195 }
196 }
197 }
68 } 198 }
69 } 199 }
70 } 200 }
71} \ No newline at end of file 201}
diff --git a/accounts/gkleen@sif/shell/quickshell/displaymanager.qml b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml
new file mode 100644
index 00000000..b452c03d
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml
@@ -0,0 +1,115 @@
1//@ pragma UseQApplication
2
3import Quickshell
4import Quickshell.Wayland
5import Quickshell.Io
6import Quickshell.Services.Greetd
7import QtQml
8
9
10ShellRoot {
11 id: displaymanager
12
13 settings.watchFiles: false
14
15 property string currentText: ""
16 property string username: @username@
17 property list<string> command: @niri_session@
18 property list<var> messages: []
19 property bool responseRequired: false
20 property bool responseVisible: false
21
22 signal startAuth()
23
24 onStartAuth: {
25 if (Greetd.state !== GreetdState.Inactive)
26 Greetd.cancelSession();
27 displaymanager.messages = [];
28 Greetd.createSession(displaymanager.username);
29 }
30
31 Connections {
32 target: Greetd
33 function onStateChanged() {
34 console.log("greetd state: ", GreetdState.toString(Greetd.state));
35 if (Greetd.state === GreetdState.ReadyToLaunch)
36 Greetd.launch(displaymanager.command);
37 }
38 function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) {
39 displaymanager.responseVisible = echoResponse;
40 displaymanager.responseRequired = responseRequired;
41 displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": error }]);
42 }
43 function onAuthFailure(message: string) {
44 displaymanager.responseRequired = false;
45 displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": true }]);
46 }
47 }
48
49 Component.onCompleted: {
50 if (Greetd.state !== GreetdState.Inactive)
51 Greetd.cancelSession();
52 }
53
54 Variants {
55 model: Quickshell.screens
56
57 delegate: Scope {
58 id: screenScope
59
60 required property var modelData
61
62 PanelWindow {
63 color: "black"
64
65 screen: screenScope.modelData
66
67 WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
68
69 anchors.top: true
70 anchors.bottom: true
71 anchors.left: true
72 anchors.right: true
73
74 LockSurface {
75 id: surfaceContent
76
77 screen: screenScope.modelData
78
79 onCurrentTextChanged: displaymanager.currentText = currentText
80 Connections {
81 target: displaymanager
82 function onCurrentTextChanged() { surfaceContent.currentText = displaymanager.currentText; }
83 function onMessagesChanged() { surfaceContent.messages = Array.from(displaymanager.messages); }
84 function onResponseRequiredChanged() { surfaceContent.responseRequired = displaymanager.responseRequired; }
85 function onResponseVisibleChanged() { surfaceContent.responseVisible = displaymanager.responseVisible; }
86 }
87
88 onResponse: responseText => Greetd.respond(responseText);
89 Connections {
90 target: Greetd
91 function onStateChanged() {
92 if (Greetd.state === GreetdState.Authenticating) {
93 surfaceContent.authRunning = true;
94 } else {
95 surfaceContent.authRunning = false;
96 }
97 }
98 }
99
100 onAuthRunningChanged: {
101 if (surfaceContent.authRunning && Greetd.state !== GreetdState.Authenticating)
102 displaymanager.startAuth();
103 }
104 Component.onCompleted: {
105 surfaceContent.authRunning = Greetd.state === GreetdState.Authenticating
106 surfaceContent.messages = Array.from(displaymanager.messages);
107 surfaceContent.responseVisible = displaymanager.responseVisible;
108 surfaceContent.responseRequired = displaymanager.responseRequired;
109 surfaceContent.currentText = displaymanager.currentText;
110 }
111 }
112 }
113 }
114 }
115}
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
index 4934cd4d..0fa45f79 100644
--- a/accounts/gkleen@sif/shell/quickshell/shell.qml
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -1,6 +1,7 @@
1//@ pragma UseQApplication 1//@ pragma UseQApplication
2 2
3import Quickshell 3import Quickshell
4import Quickshell.Wayland
4 5
5ShellRoot { 6ShellRoot {
6 settings.watchFiles: false 7 settings.watchFiles: false
@@ -13,11 +14,36 @@ ShellRoot {
13 14
14 required property var modelData 15 required property var modelData
15 16
16 Bar { 17 PanelWindow {
17 id: bar 18 id: bgWindow
19
20 screen: screenScope.modelData
21
22 WlrLayershell.layer: WlrLayer.Background
23 WlrLayershell.exclusionMode: ExclusionMode.Ignore
24
25 anchors.top: true
26 anchors.bottom: true
27 anchors.left: true
28 anchors.right: true
29
30 color: "black"
18 31
32 WallpaperBackground {
33 screen: bgWindow.screen.name
34 }
35 }
36
37 Bar {
19 screen: screenScope.modelData 38 screen: screenScope.modelData
20 } 39 }
21 } 40 }
22 } 41 }
42
43 Lockscreen {}
44
45 VolumeOSD {}
46 BrightnessOSD {}
47
48 UnixIPC {}
23} 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 = {