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.nix20
-rw-r--r--accounts/gkleen@sif/shell/default.nix91
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt1
-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/Bar.qml6
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml314
-rw-r--r--accounts/gkleen@sif/shell/quickshell/LockSurface.qml223
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml77
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml6
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml85
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml117
-rw-r--r--accounts/gkleen@sif/shell/quickshell/displaymanager.qml115
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml25
-rw-r--r--accounts/gkleen@sif/systemd.nix2
19 files changed, 1096 insertions, 210 deletions
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index f2978b6e..c786c629 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -49,7 +49,8 @@ let
49 ]; 49 ];
50 }; 50 };
51 51
52 lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service"; 52 # lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service";
53 lockCommand = "${lib.getExe' cfg.programs.quickshell.package "qs"} ipc call Lockscreen setLocked true";
53 54
54 editor = pkgs.symlinkJoin { 55 editor = pkgs.symlinkJoin {
55 inherit (cfg.services.emacs.package) name; 56 inherit (cfg.services.emacs.package) name;
@@ -374,7 +375,7 @@ in {
374 375
375 services = { 376 services = {
376 wpaperd = { 377 wpaperd = {
377 enable = true; 378 enable = false;
378 settings.default = { 379 settings.default = {
379 path = "~/.wallpapers"; 380 path = "~/.wallpapers";
380 duration = "15m"; 381 duration = "15m";
@@ -594,10 +595,10 @@ in {
594 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass) 595 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass)
595 ]; 596 ];
596 }; 597 };
597 "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = '' 598 # "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = ''
598 [Unit] 599 # [Unit]
599 After=graphical-session.target 600 # After=graphical-session.target
600 ''; 601 # '';
601 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = '' 602 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = ''
602 [Unit] 603 [Unit]
603 Before=graphical-session-pre.target 604 Before=graphical-session-pre.target
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 35a3d799..e1eca4c4 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -231,25 +231,7 @@ in {
231 }; 231 };
232 232
233 config = { 233 config = {
234 systemd.user.services.xwayland-satellite = { 234 home.packages = [ pkgs.xwayland-satellite-unstable ];
235 Unit = {
236 BindsTo = [ "graphical-session.target" ];
237 PartOf = [ "graphical-session.target" ];
238 After = [ "graphical-session.target" ];
239 Requisite = [ "graphical-session.target" ];
240 };
241 Service = {
242 Type = "notify";
243 NotifyAccess = "all";
244 Environment = [ "DISPLAY=:0" ];
245 ExecStart = ''${lib.getExe pkgs.xwayland-satellite-unstable} ''${DISPLAY}'';
246 ExecStartPre = "${systemctl} --user import-environment DISPLAY";
247 StandardOutput = "journal";
248 };
249 Install = {
250 WantedBy = [ "graphical-session.target" ];
251 };
252 };
253 235
254 services.swayidle = { 236 services.swayidle = {
255 events = [ 237 events = [
diff --git a/accounts/gkleen@sif/shell/default.nix b/accounts/gkleen@sif/shell/default.nix
index 26c8bd98..85e034d6 100644
--- a/accounts/gkleen@sif/shell/default.nix
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -4,17 +4,104 @@
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;
12 }; 99 };
13 }; 100 };
14 }; 101 };
15 systemd.user.services.quickshell = { 102 systemd.user.services.quickshell = {
16 Service = { 103 Service = {
17 Environment = "QML_IMPORT_PATH=${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix}"; 104 RuntimeDirectory = "quickshell";
18 }; 105 };
19 }; 106 };
20 }; 107 };
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
index aa363c4c..2123ed35 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
@@ -104,6 +104,7 @@ qt6_add_qml_module(customplugin
104 104
105target_sources(customplugin PRIVATE 105target_sources(customplugin PRIVATE
106 Chrono.cpp Chrono.hpp 106 Chrono.cpp Chrono.hpp
107 FileSelector.cpp FileSelector.hpp
107) 108)
108 109
109target_compile_features(customplugin PUBLIC cxx_std_26) 110target_compile_features(customplugin PUBLIC cxx_std_26)
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/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index aab1607f..38225d74 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -7,8 +7,6 @@ PanelWindow {
7 7
8 required property var screen 8 required property var screen
9 9
10 property var calendarMouseArea: clock.calendarMouseArea
11
12 anchors { 10 anchors {
13 top: true 11 top: true
14 left: true 12 left: true
@@ -80,8 +78,6 @@ PanelWindow {
80 width: 4 78 width: 4
81 } 79 }
82 80
83 Clock { 81 Clock {}
84 id: clock
85 }
86 } 82 }
87} \ No newline at end of file 83} \ No newline at end of file
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
index d645cfa7..382af168 100644
--- a/accounts/gkleen@sif/shell/quickshell/Clock.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -9,6 +9,8 @@ import Quickshell.Widgets
9Item { 9Item {
10 id: clockItem 10 id: clockItem
11 11
12 property bool calendarPopup: true
13
12 width: clock.contentWidth 14 width: clock.contentWidth
13 height: parent.height 15 height: parent.height
14 anchors.verticalCenter: parent.verticalCenter 16 anchors.verticalCenter: parent.verticalCenter
@@ -18,19 +20,7 @@ Item {
18 20
19 anchors.fill: parent 21 anchors.fill: parent
20 hoverEnabled: true 22 hoverEnabled: true
21 enabled: true 23 enabled: clockItem.calendarPopup
22
23 property real angleRem: 0
24 property real sensitivity: 1 / 120
25
26 function scrollYear(event) {
27 angleRem += event.angleDelta.y;
28 const d = Math.round(angleRem * sensitivity);
29 yearCalendar.year += d;
30 angleRem -= d / sensitivity;
31 }
32
33 onWheel: event => scrollYear(event)
34 24
35 Item { 25 Item {
36 anchors.fill: parent 26 anchors.fill: parent
@@ -43,10 +33,9 @@ Item {
43 33
44 Custom.Chrono { 34 Custom.Chrono {
45 id: chrono 35 id: chrono
46 format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}"
47 }
48 36
49 text: chrono.date 37 onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}")
38 }
50 39
51 font.pointSize: 10 40 font.pointSize: 10
52 font.family: "Fira Sans" 41 font.family: "Fira Sans"
@@ -55,198 +44,233 @@ Item {
55 } 44 }
56 } 45 }
57 46
58 PopupWindow { 47 Loader {
59 id: tooltip 48 id: tooltipLoader
60 49
61 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse 50 active: false
62 51
63 anchor { 52 Connections {
64 item: clockMouseArea 53 target: clockMouseArea
65 edges: Edges.Bottom | Edges.Left 54 function onContainsMouseChanged() {
55 if (clockMouseArea.containsMouse)
56 tooltipLoader.active = true;
57 }
66 } 58 }
67 visible: false
68 59
69 onNextVisibleChanged: hangTimer.restart() 60 sourceComponent: PopupWindow {
61 id: tooltip
70 62
71 Timer { 63 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse
72 id: hangTimer
73 interval: 100
74 onTriggered: tooltip.visible = tooltip.nextVisible
75 }
76 64
77 implicitWidth: clockTooltipContent.width 65 anchor {
78 implicitHeight: clockTooltipContent.height 66 item: clockMouseArea
79 color: "black" 67 edges: Edges.Bottom | Edges.Left
68 }
69 visible: false
80 70
81 onVisibleChanged: { 71 onNextVisibleChanged: hangTimer.restart()
82 const d = new Date();
83 yearCalendar.year = d.getFullYear();
84 clockMouseArea.angleRem = 0;
85 }
86 72
87 WrapperMouseArea { 73 Timer {
88 id: tooltipMouseArea 74 id: hangTimer
75 interval: 100
76 onTriggered: tooltip.visible = tooltip.nextVisible
77 }
89 78
90 hoverEnabled: true 79 implicitWidth: clockTooltipContent.width
91 enabled: true 80 implicitHeight: clockTooltipContent.height
81 color: "black"
92 82
93 onWheel: event => clockMouseArea.scrollYear(event) 83 onVisibleChanged: {
84 yearCalendar.year = chrono.date.getFullYear();
85 yearCalendar.angleRem = 0;
86 }
94 87
95 anchors.fill: parent 88 WrapperMouseArea {
89 id: tooltipMouseArea
96 90
97 WrapperItem { 91 hoverEnabled: true
98 id: clockTooltipContent 92 enabled: true
99 93
100 margin: 8 94 onWheel: event => yearCalendar.scrollYear(event)
101 leftMargin: 0
102 95
103 ColumnLayout { 96 anchors.fill: parent
104 Text {
105 id: yearLabel
106 97
107 horizontalAlignment: Text.AlignHCenter 98 WrapperItem {
99 id: clockTooltipContent
108 100
109 font.pointSize: 14 101 margin: 8
110 font.family: "Fira Sans" 102 leftMargin: 0
111 font.features: { "tnum": 1 }
112 color: "white"
113 103
114 text: yearCalendar.year 104 ColumnLayout {
105 Text {
106 id: yearLabel
115 107
116 Layout.fillWidth: true 108 horizontalAlignment: Text.AlignHCenter
117 Layout.bottomMargin: 8
118 }
119 109
120 GridLayout { 110 font.pointSize: 14
121 property int year: { const d = new Date(); return d.getFullYear(); } 111 font.family: "Fira Sans"
112 font.features: { "tnum": 1 }
113 color: "white"
122 114
123 id: yearCalendar 115 text: yearCalendar.year
124 116
125 columns: 3 117 Layout.fillWidth: true
126 columnSpacing: 16 118 Layout.bottomMargin: 8
127 rowSpacing: 16 119 }
128 120
129 Layout.alignment: Qt.AlignHCenter 121 GridLayout {
130 Layout.fillWidth: false 122 property int year: chrono.date.getFullYear()
131 123
132 Repeater { 124 id: yearCalendar
133 model: 12
134 125
135 GridLayout { 126 columns: 3
136 columns: 2 127 columnSpacing: 16
128 rowSpacing: 16
137 129
138 required property int index 130 Layout.alignment: Qt.AlignHCenter
139 property int month: index 131 Layout.fillWidth: false
140 132
141 id: monthCalendar 133 property real angleRem: 0
134 property real sensitivity: 1 / 120
142 135
143 Layout.alignment: Qt.AlignTop | Qt.AlignRight 136 function scrollYear(event) {
144 Layout.fillWidth: false 137 angleRem += event.angleDelta.y;
138 const d = Math.round(angleRem * sensitivity);
139 yearCalendar.year += d;
140 angleRem -= d / sensitivity;
141 }
145 142
146 Text { 143 Connections {
147 Layout.column: 1 144 target: clockMouseArea
148 Layout.fillWidth: true 145 function onWheel(event) { yearCalendar.scrollYear(event); }
146 }
149 147
150 horizontalAlignment: Text.AlignHCenter 148 Repeater {
149 model: 12
151 150
152 font.pointSize: 10 151 GridLayout {
153 font.family: "Fira Sans" 152 columns: 2
154 153
155 text: { 154 required property int index
156 const date = Date.fromLocaleDateString(Qt.locale(), `${yearCalendar.year}-${monthCalendar.month + 1}-01`, "yyyy-M-dd"); 155 property int month: index
157 return date.toLocaleString(Qt.locale("en_DK"), "MMMM")
158 }
159 156
160 color: "#ffead3" 157 id: monthCalendar
161 }
162 158
163 DayOfWeekRow { 159 Layout.alignment: Qt.AlignTop | Qt.AlignRight
164 locale: grid.locale 160 Layout.fillWidth: false
165 161
166 Layout.row: 1 162 Text {
167 Layout.column: 1 163 Layout.column: 1
168 Layout.fillWidth: true 164 Layout.fillWidth: true
169 165
170 delegate: Text { 166 horizontalAlignment: Text.AlignHCenter
171 required property string shortName
172 167
173 font.pointSize: 10 168 font.pointSize: 10
174 font.family: "Fira Mono" 169 font.family: "Fira Sans"
175 170
176 text: shortName 171 text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM")
177 color: "#ffcc66"
178 172
179 horizontalAlignment: Text.AlignRight 173 color: "#ffead3"
180 verticalAlignment: Text.AlignVCenter
181 } 174 }
182 }
183
184 WeekNumberColumn {
185 month: grid.month
186 year: grid.year
187 locale: grid.locale
188 175
189 Layout.fillHeight: true 176 DayOfWeekRow {
177 locale: grid.locale
190 178
191 delegate: Text { 179 Layout.row: 1
192 required property int weekNumber 180 Layout.column: 1
181 Layout.fillWidth: true
193 182
194 opacity: { 183 delegate: WrapperItem {
195 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); 184 required property string shortName
196 const dayOfWeek = simple.getDay();
197 const isoWeekStart = simple;
198 185
199 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); 186 width: dowLabel.contentWidth + 6
200 if (dayOfWeek > 4) {
201 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
202 }
203 187
204 for (let i = 0; i < 7; i++) { 188 Text {
205 const dayInWeek = new Date(isoWeekStart); 189 id: dowLabel
206 dayInWeek.setDate(dayInWeek.getDate() + i);
207 if (dayInWeek.getMonth() == monthCalendar.month)
208 return 1;
209 }
210 190
211 return 0; 191 anchors.fill: parent
212 }
213 192
214 font.pointSize: 10 193 font.pointSize: 10
215 font.family: "Fira Sans" 194 font.family: "Fira Sans"
216 font.features: { "tnum": 1 }
217 195
218 text: weekNumber 196 text: parent.shortName
219 color: "#99ffdd" 197 color: "#ffcc66"
220 198
221 horizontalAlignment: Text.AlignRight 199 horizontalAlignment: Text.AlignHCenter
222 verticalAlignment: Text.AlignVCenter 200 verticalAlignment: Text.AlignVCenter
201 }
202 }
223 } 203 }
224 }
225 204
226 MonthGrid { 205 WeekNumberColumn {
227 id: grid 206 month: grid.month
207 year: grid.year
208 locale: grid.locale
209
210 Layout.fillHeight: true
228 211
229 year: yearCalendar.year 212 delegate: Text {
230 month: monthCalendar.month 213 required property int weekNumber
231 locale: Qt.locale("en_DK")
232 214
233 Layout.fillWidth: true 215 opacity: {
234 Layout.fillHeight: true 216 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
217 const dayOfWeek = simple.getDay();
218 const isoWeekStart = simple;
235 219
236 delegate: Text { 220 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1);
237 required property var model 221 if (dayOfWeek > 4) {
222 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
223 }
238 224
239 opacity: model.month === monthCalendar.month ? 1 : 0 225 for (let i = 0; i < 7; i++) {
226 const dayInWeek = new Date(isoWeekStart);
227 dayInWeek.setDate(dayInWeek.getDate() + i);
228 if (dayInWeek.getMonth() == monthCalendar.month)
229 return 1;
230 }
231
232 return 0;
233 }
240 234
241 font.pointSize: 10 235 font.pointSize: 10
242 font.family: "Fira Sans" 236 font.family: "Fira Sans"
243 font.features: { "tnum": 1 } 237 font.features: { "tnum": 1 }
244 238
245 text: model.day 239 text: weekNumber
246 color: model.today ? "#ff6699" : "white" 240 color: "#99ffdd"
247 241
248 horizontalAlignment: Text.AlignRight 242 horizontalAlignment: Text.AlignRight
249 verticalAlignment: Text.AlignVCenter 243 verticalAlignment: Text.AlignVCenter
244 }
245 }
246
247 MonthGrid {
248 id: grid
249
250 year: yearCalendar.year
251 month: monthCalendar.month
252 locale: Qt.locale("en_DK")
253
254 Layout.fillWidth: true
255 Layout.fillHeight: true
256
257 delegate: Text {
258 required property var model
259
260 opacity: model.month === monthCalendar.month ? 1 : 0
261
262 font.pointSize: 10
263 font.family: "Fira Sans"
264 font.features: { "tnum": 1 }
265
266 property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day
267
268 text: model.day
269 color: today ? "#ff6699" : "white"
270
271 horizontalAlignment: Text.AlignRight
272 verticalAlignment: Text.AlignVCenter
273 }
250 } 274 }
251 } 275 }
252 } 276 }
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..cc82a275
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -0,0 +1,77 @@
1import Quickshell
2import Quickshell.Wayland
3import Quickshell.Io
4import Quickshell.Services.Pam
5import QtQml
6
7Scope {
8 id: lockscreen
9
10 property string currentText: ""
11
12 PamContext {
13 id: pam
14
15 property list<var> messages: []
16
17 config: "quickshell"
18 onCompleted: result => {
19 if (result === PamResult.Success) {
20 lock.locked = false;
21 }
22 }
23 onPamMessage: {
24 messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }])
25 }
26 onActiveChanged: {
27 messages = [];
28 }
29 }
30
31 IpcHandler {
32 target: "Lockscreen"
33
34 function setLocked(locked: bool): void { lock.locked = locked; }
35 function getLocked(): bool { return lock.locked; }
36 }
37
38 WlSessionLock {
39 id: lock
40
41 onLockedChanged: {
42 if (!locked && pam.active)
43 pam.abort();
44 }
45
46 WlSessionLockSurface {
47 id: lockSurface
48
49 color: "black"
50
51 LockSurface {
52 id: surfaceContent
53
54 onResponse: responseText => pam.respond(responseText)
55 onAuthRunningChanged: {
56 if (authRunning)
57 pam.start();
58 }
59 Connections {
60 target: pam
61 function onMessagesChanged() { surfaceContent.messages = pam.messages; }
62 function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; }
63 function onActiveChanged() { surfaceContent.authRunning = pam.active; }
64 }
65 onCurrentTextChanged: lockscreen.currentText = currentText
66 Connections {
67 target: lockscreen
68 function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; }
69 }
70 Connections {
71 target: lockSurface
72 function onScreenChanged() { surfaceContent.screen = lockSurface.screen; }
73 }
74 }
75 }
76 }
77}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
index 179b55e0..af522ec4 100644
--- a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
@@ -160,7 +160,11 @@ Singleton {
160 }); 160 });
161 } 161 }
162 function eventWindowOpenedOrChanged(data) { 162 function eventWindowOpenedOrChanged(data) {
163 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]);
164 } 168 }
165 function eventWindowClosed(data) { 169 function eventWindowClosed(data) {
166 root.windows = Array.from(root.windows).filter(win => win.id !== data.id); 170 root.windows = Array.from(root.windows).filter(win => win.id !== data.id);
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
new file mode 100644
index 00000000..3c524955
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
@@ -0,0 +1,8 @@
1import Custom as Custom
2
3Custom.FileSelector {
4 id: root
5
6 directory: @wallpapers@
7 epoch: 72000000
8}
diff --git a/accounts/gkleen@sif/shell/quickshell/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 9546abb4..c8c017c3 100644
--- a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
+++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
@@ -2,6 +2,7 @@ import Quickshell
2import QtQuick 2import QtQuick
3import qs.Services 3import qs.Services
4import Quickshell.Widgets 4import Quickshell.Widgets
5import QtQuick.Layouts
5 6
6Row { 7Row {
7 id: workspaces 8 id: workspaces
@@ -79,6 +80,122 @@ Row {
79 } 80 }
80 } 81 }
81 } 82 }
83
84 PopupWindow {
85 id: tooltip
86
87 property bool nextVisible: (mouseArea.containsMouse || tooltipMouseArea.containsMouse) && [...windowsModel.values].length > 0
88
89 anchor {
90 item: mouseArea
91 edges: Edges.Bottom | Edges.Left
92 }
93 visible: false
94
95 onNextVisibleChanged: hangTimer.restart()
96
97 Timer {
98 id: hangTimer
99 interval: 100
100 onTriggered: tooltip.visible = tooltip.nextVisible
101 }
102
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 }
198 }
82 } 199 }
83 } 200 }
84} 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..1da9457d 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,31 @@ 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
18 29
30 color: "black"
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 {}
23} 44}
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 = {