summaryrefslogtreecommitdiff
path: root/accounts/gkleen@sif
diff options
context:
space:
mode:
Diffstat (limited to 'accounts/gkleen@sif')
-rw-r--r--accounts/gkleen@sif/default.nix5
-rw-r--r--accounts/gkleen@sif/shell/default.nix69
-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/ActiveWindowDisplay.qml94
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml327
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml266
-rw-r--r--accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml106
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml259
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml20
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/SystemTray.qml159
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml85
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml201
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml33
18 files changed, 1481 insertions, 355 deletions
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index f2978b6e..92163dca 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";
diff --git a/accounts/gkleen@sif/shell/default.nix b/accounts/gkleen@sif/shell/default.nix
index 26c8bd98..84140072 100644
--- a/accounts/gkleen@sif/shell/default.nix
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -7,8 +7,75 @@
7 config = { 7 config = {
8 src = ./quickshell; 8 src = ./quickshell;
9 replacements = { 9 replacements = {
10 coreutils = toString pkgs.coreutils;
11 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces); 10 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces);
11 wallpapers = builtins.toJSON (pkgs.stdenvNoCC.mkDerivation {
12 name = "wallpapers";
13 srcs = [
14 (pkgs.fetchurl {
15 url = "https://esawebb.org/media/archives/images/publicationtiff10k/carinanebula3.tif";
16 hash = "sha256-YxZEweDKJfvfrdxb/QFmgJhcZDEJYxotoHrG+RRn1tw=";
17 })
18 (pkgs.fetchurl {
19 url = "https://esawebb.org/media/archives/images/original/pillarsofcreation_composite.tif";
20 hash = "sha256-qRiODxR0lZWdxgYXna0fNRFFDErpBJDwOJuQl6sNjRc=";
21 })
22 (pkgs.fetchurl {
23 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2212a.tif";
24 hash = "sha256-l2fqE/z//C1a0xkvZwsnwPbTSb+WuA11h+SUl3E1dhw=";
25 })
26 (pkgs.fetchurl {
27 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2415a.tif";
28 hash = "sha256-onBy7cPoUpDuzQStbY2E+qmlGgSLXPwFCLX53ukAb4c=";
29 })
30 (pkgs.fetchurl {
31 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2330a.tif";
32 hash = "sha256-nn0ZtjZIrPcpj3YcLTsrL7XiXvyh3QYgCSmdDMD+3OM=";
33 })
34 (pkgs.fetchurl {
35 url = "https://esawebb.org/media/archives/images/original/weic2426a.tif";
36 hash = "sha256-EDnfPn3GE9jt6XPqiGInP7E2F3Az7d25NqATSWltDv0=";
37 })
38 (pkgs.fetchurl {
39 url = "https://esawebb.org/media/archives/images/original/weic2503a.tif";
40 hash = "sha256-3/RX6RQp8naELcgReHPd5/zhJkoCjnA10w5BEnNo+qI=";
41 })
42 (pkgs.fetchurl {
43 url = "https://esawebb.org/media/archives/images/original/weic2506a.tif";
44 hash = "sha256-aDld0aoY1owRxDVf7Jcyw71TH42M1foYotxn2thyFYw=";
45 })
46 (pkgs.fetchurl {
47 url = "https://esawebb.org/media/archives/images/original/weic2514a.tif";
48 hash = "sha256-jTi1G1Ofo5xsF6ggrbtYJHxqLaCQ7edM5B3uORiVQtg=";
49 })
50 (pkgs.fetchurl {
51 url = "https://esawebb.org/media/archives/images/original/weic2425c.tif";
52 hash = "sha256-oaEOexSJHEGj090dJF3ct5HAoR+Y5gRiPrUlxdvnTRo=";
53 })
54 ];
55
56 dontUnpack = true;
57
58 buildInputs = [ pkgs.imagemagick ];
59 buildPhase = ''
60 runHook preBuild
61
62 typeset sources=($srcs)
63
64 mkdir -p $out
65 magick ''${sources[0]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/carinanebula3.jpeg
66 magick ''${sources[1]} -crop 6716x3778+329+80 +repage -define jpeg:extent=10MB $out/pillarsofcreation_composite.jpeg
67 magick ''${sources[2]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/weic2212a.jpeg
68 magick ''${sources[3]} -crop 7650x4302+1166+389 +repage -define jpeg:extent=10MB $out/weic2415a.jpeg
69 magick ''${sources[4]} -crop 8732x4912+0+434 +repage -define jpeg:extent=10MB $out/weic2330a.jpeg
70 magick ''${sources[5]} -crop 5302x2982+636+0 +repage -define jpeg:extent=10MB $out/weic2426a.jpeg
71 magick ''${sources[6]} -crop 4328x2434+0+906 +repage -define jpeg:extent=10MB $out/weic2503a.jpeg
72 magick ''${sources[7]} -crop 4152x2335+0+666 +repage -define jpeg:extent=10MB $out/weic2506a.jpeg
73 magick ''${sources[8]} -crop 4320x2430+0+0 +repage -define jpeg:extent=10MB $out/weic2514a.jpeg
74 magick ''${sources[9]} -crop 5863x3298+0+477 +repage -define jpeg:extent=10MB $out/weic2425c.jpeg
75
76 runHook postBuild
77 '';
78 });
12 }; 79 };
13 }; 80 };
14 }; 81 };
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/ActiveWindowDisplay.qml b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
new file mode 100644
index 00000000..57ade488
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
@@ -0,0 +1,94 @@
1import QtQuick
2import qs.Services
3import Quickshell
4import Quickshell.Widgets
5
6Item {
7 id: activeWindowDisplay
8
9 required property int maxWidth
10 required property var screen
11
12 property var activeWindow: {
13 let currWindowId = Array.from(NiriService.workspaces).find(ws => {
14 return ws.output === screen.name && ws.is_active;
15 })?.active_window_id;
16
17 return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null;
18 }
19 property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null
20
21 anchors.verticalCenter: parent.verticalCenter
22 width: activeWindowDisplayContent.width
23 height: parent.height
24
25 Row {
26 id: activeWindowDisplayContent
27
28 width: childrenRect.width
29 height: parent.height
30 anchors.verticalCenter: parent.verticalCenter
31 spacing: 8
32
33 IconImage {
34 id: activeWindowIcon
35
36 height: 14
37 width: 14
38
39 anchors.verticalCenter: parent.verticalCenter
40
41 source: {
42 let icon = activeWindowDisplay.windowEntry?.icon
43 if (typeof icon === 'string' || icon instanceof String) {
44 if (icon.includes("?path=")) {
45 const split = icon.split("?path=")
46 if (split.length !== 2)
47 return icon
48 const name = split[0]
49 const path = split[1]
50 const fileName = name.substring(
51 name.lastIndexOf("/") + 1)
52 return `file://${path}/${fileName}`
53 } else
54 icon = Quickshell.iconPath(icon);
55 return icon
56 }
57 return ""
58 }
59 asynchronous: true
60 smooth: true
61 mipmap: true
62 }
63
64 Text {
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 }
90 return title;
91 }
92 }
93 }
94}
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index 52f875da..aab1607f 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -1,16 +1,13 @@
1import Quickshell 1import Quickshell
2import Quickshell.Io
3import Quickshell.Services.SystemTray
4import Quickshell.Widgets
5import Custom as Custom
6import QtQuick 2import QtQuick
7import qs.Services
8 3
9 4
10PanelWindow { 5PanelWindow {
11 id: bar 6 id: bar
12 7
13 property var modelData 8 required property var screen
9
10 property var calendarMouseArea: clock.calendarMouseArea
14 11
15 anchors { 12 anchors {
16 top: true 13 top: true
@@ -22,12 +19,11 @@ PanelWindow {
22 right: 26 + 8 19 right: 26 + 8
23 } 20 }
24 21
25 screen: modelData
26 implicitHeight: 21 22 implicitHeight: 21
27 color: "transparent" 23 color: "transparent"
28 24
29 Rectangle { 25 Rectangle {
30 color: Qt.rgba(0, 0, 0, 0.66) 26 color: Qt.rgba(0, 0, 0, 0.75)
31 anchors.fill: parent 27 anchors.fill: parent
32 // bottomLeftRadius: 8 28 // bottomLeftRadius: 8
33 // bottomRightRadius: 8 29 // bottomRightRadius: 8
@@ -43,74 +39,7 @@ PanelWindow {
43 anchors.verticalCenter: parent.verticalCenter 39 anchors.verticalCenter: parent.verticalCenter
44 spacing: 8 40 spacing: 8
45 41
46 Row { 42 WorkspaceSwitcher {}
47 id: workspaces
48
49 property var ignoreWorkspaces: @ignore_workspaces@
50
51 height: parent.height
52 anchors.verticalCenter: parent.verticalCenter
53 spacing: 0
54
55 Repeater {
56 model: {
57 let currWorkspaces = NiriService.workspaces;
58 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces);
59 currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name));
60 currWorkspaces.sort((a, b) => {
61 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x)
62 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x
63 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y)
64 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y
65 return a.idx - b.idx;
66 });
67 return currWorkspaces;
68 }
69
70 Rectangle {
71 property var workspaceData: modelData
72
73 width: wsLabel.contentWidth + 8
74 color: {
75 if (mouseArea.containsMouse) {
76 return "#33808080";
77 }
78 return "transparent";
79 }
80 height: parent.height
81 anchors.verticalCenter: parent.verticalCenter
82
83 MouseArea {
84 id: mouseArea
85
86 anchors.fill: parent
87 hoverEnabled: true
88 cursorShape: Qt.PointingHandCursor
89 enabled: true
90 onClicked: {
91 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {})
92 }
93 }
94
95 Text {
96 id: wsLabel
97
98 font.pointSize: 10
99 font.family: "Fira Sans"
100 color: {
101 if (workspaceData.is_active)
102 return "#23fd00";
103 if (workspaceData.active_window_id === null)
104 return "#555";
105 return "white";
106 }
107 anchors.centerIn: parent
108
109 text: workspaceData.name ? workspaceData.name : workspaceData.idx
110 }
111 }
112 }
113 }
114 } 43 }
115 44
116 Row { 45 Row {
@@ -121,91 +50,9 @@ PanelWindow {
121 anchors.centerIn: parent 50 anchors.centerIn: parent
122 spacing: 5 51 spacing: 5
123 52
124 Item { 53 ActiveWindowDisplay {
125 id: activeWindowDisplay 54 screen: bar.screen
126 55 maxWidth: bar.width - 2*Math.max(left.width, right.width) - 2*8
127 property var activeWindow: {
128 let currWindowId = Array.from(NiriService.workspaces).find(ws => {
129 return ws.output === bar.screen.name && ws.is_active;
130 })?.active_window_id;
131
132 return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null;
133 }
134 property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null
135
136 anchors.verticalCenter: parent.verticalCenter
137 width: activeWindowDisplayContent.width
138 height: parent.height
139
140 Row {
141 id: activeWindowDisplayContent
142
143 width: childrenRect.width
144 height: parent.height
145 anchors.verticalCenter: parent.verticalCenter
146 spacing: 8
147
148 IconImage {
149 id: activeWindowIcon
150
151 height: 14
152 width: 14
153
154 anchors.verticalCenter: parent.verticalCenter
155
156 source: {
157 let icon = activeWindowDisplay.windowEntry?.icon
158 if (typeof icon === 'string' || icon instanceof String) {
159 if (icon.includes("?path=")) {
160 const split = icon.split("?path=")
161 if (split.length !== 2)
162 return icon
163 const name = split[0]
164 const path = split[1]
165 const fileName = name.substring(
166 name.lastIndexOf("/") + 1)
167 return `file://${path}/${fileName}`
168 } else
169 icon = Quickshell.iconPath(icon);
170 return icon
171 }
172 return ""
173 }
174 asynchronous: true
175 smooth: true
176 mipmap: true
177 }
178
179 Text {
180 id: windowTitle
181
182 width: Math.min(implicitWidth, bar.width - 2*Math.max(left.width, right.width) - 2*8 - activeWindowIcon.width - activeWindowDisplayContent.spacing)
183
184 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
185
186 elide: Text.ElideRight
187 maximumLineCount: 1
188 color: "white"
189 anchors.verticalCenter: parent.verticalCenter
190 text: {
191 if (!activeWindowDisplay.activeWindow)
192 return "";
193
194 var title = activeWindowDisplay.activeWindow.title;
195 var appName = activeWindowDisplay.windowEntry?.name;
196 if (appAliases[appName])
197 appName = appAliases[appName];
198 if (appName && title.endsWith(appName)) {
199 const oldTitle = title;
200 title = title.substring(0, title.length - appName.length);
201 title = title.replace(/\s*(—|-)\s*$/, "");
202 if (!title)
203 title = oldTitle;
204 }
205 return title;
206 }
207 }
208 }
209 } 56 }
210 } 57 }
211 58
@@ -219,170 +66,22 @@ PanelWindow {
219 anchors.verticalCenter: parent.verticalCenter 66 anchors.verticalCenter: parent.verticalCenter
220 spacing: 0 67 spacing: 0
221 68
222 Item { 69 SystemTray {}
223 anchors.verticalCenter: parent.verticalCenter
224 width: systemTrayRow.childrenRect.width
225 height: parent.height
226 clip: true
227
228 Row {
229 id: systemTrayRow
230 anchors.centerIn: parent
231 width: childrenRect.width
232 height: parent.height
233 spacing: 0
234
235 Repeater {
236 model: SystemTray.items.values
237
238 delegate: Item {
239 property var trayItem: modelData
240 property string iconSource: {
241 let icon = trayItem && trayItem.icon
242 if (typeof icon === 'string' || icon instanceof String) {
243 if (icon.includes("?path=")) {
244 const split = icon.split("?path=")
245 if (split.length !== 2)
246 return icon
247 const name = split[0]
248 const path = split[1]
249 const fileName = name.substring(
250 name.lastIndexOf("/") + 1)
251 return `file://${path}/${fileName}`
252 }
253 return icon
254 }
255 return ""
256 }
257
258 width: 16
259 height: parent.height
260 anchors.verticalCenter: parent.verticalCenter
261 70
262 IconImage { 71 Item {
263 anchors.centerIn: parent
264 width: parent.width
265 height: parent.width
266 source: parent.iconSource
267 asynchronous: true
268 smooth: true
269 mipmap: true
270 }
271
272 MouseArea {
273 id: trayItemArea
274
275 anchors.fill: parent
276 acceptedButtons: Qt.LeftButton | Qt.RightButton
277 hoverEnabled: true
278 cursorShape: Qt.PointingHandCursor
279 onClicked: mouse => {
280 if (!trayItem)
281 return
282
283 if (mouse.button === Qt.LeftButton
284 && !trayItem.onlyMenu) {
285 trayItem.activate()
286 return
287 }
288
289 if (trayItem.hasMenu) {
290 var globalPos = mapToGlobal(0, 0)
291 var currentScreen = screen || Screen
292 var screenX = currentScreen.x || 0
293 var relativeX = globalPos.x - screenX
294 menuAnchor.menu = trayItem.menu
295 menuAnchor.anchor.window = bar
296 menuAnchor.anchor.rect = Qt.rect(
297 relativeX,
298 21,
299 parent.width, 1)
300 menuAnchor.open()
301 }
302 }
303 }
304 }
305 }
306 }
307 QsMenuAnchor {
308 id: menuAnchor
309 }
310 }
311
312 Rectangle {
313 height: parent.height 72 height: parent.height
314 width: 4 73 width: 4
315 color: "transparent"
316 } 74 }
317 75
318 Rectangle { 76 KeyboardLayout {}
319 id: kbdWidget
320 77
321 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" } 78 Item {
322
323 width: kbdLabel.contentWidth + 8
324 color: {
325 if (kbdMouseArea.containsMouse) {
326 return "#33808080";
327 }
328 return "transparent";
329 }
330 height: parent.height
331 anchors.verticalCenter: parent.verticalCenter
332
333 MouseArea {
334 id: kbdMouseArea
335
336 anchors.fill: parent
337 hoverEnabled: true
338 cursorShape: Qt.PointingHandCursor
339 enabled: true
340 onClicked: {
341 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {})
342 }
343 }
344
345 Text {
346 id: kbdLabel
347
348 font.pointSize: 10
349 font.family: "Fira Sans"
350 color: {
351 if (NiriService.keyboardLayouts?.current_idx === 0)
352 return "#555";
353 return "white";
354 }
355 anchors.centerIn: parent
356
357 text: {
358 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx];
359 return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout;
360 }
361 }
362 }
363
364 Rectangle {
365 height: parent.height 79 height: parent.height
366 width: 4 80 width: 4
367 color: "transparent"
368 } 81 }
369 82
370 Text { 83 Clock {
371 id: clock 84 id: clock
372 color: "white"
373
374 anchors.verticalCenter: parent.verticalCenter
375
376 Custom.Chrono {
377 id: chrono
378 format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}"
379 }
380
381 text: chrono.date
382
383 font.pointSize: 10
384 font.family: "Fira Sans"
385 font.features: { "tnum": 1 }
386 } 85 }
387 } 86 }
388} \ No newline at end of file 87} \ No newline at end of file
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
new file mode 100644
index 00000000..d0c9178b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -0,0 +1,266 @@
1import QtQml
2import QtQuick
3import Quickshell
4import Custom as Custom
5import QtQuick.Controls
6import QtQuick.Layouts
7import Quickshell.Widgets
8
9Item {
10 id: clockItem
11
12 property bool calendarPopup: true
13
14 width: clock.contentWidth
15 height: parent.height
16 anchors.verticalCenter: parent.verticalCenter
17
18 WrapperMouseArea {
19 id: clockMouseArea
20
21 anchors.fill: parent
22 hoverEnabled: true
23 enabled: clockItem.calendarPopup
24
25 property real angleRem: 0
26 property real sensitivity: 1 / 120
27
28 function scrollYear(event) {
29 angleRem += event.angleDelta.y;
30 const d = Math.round(angleRem * sensitivity);
31 yearCalendar.year += d;
32 angleRem -= d / sensitivity;
33 }
34
35 onWheel: event => scrollYear(event)
36
37 Item {
38 anchors.fill: parent
39
40 Text {
41 id: clock
42 color: "white"
43
44 anchors.verticalCenter: parent.verticalCenter
45
46 Custom.Chrono {
47 id: chrono
48
49 onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}")
50 }
51
52 font.pointSize: 10
53 font.family: "Fira Sans"
54 font.features: { "tnum": 1 }
55 }
56 }
57 }
58
59 PopupWindow {
60 id: tooltip
61
62 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse
63
64 anchor {
65 item: clockMouseArea
66 edges: Edges.Bottom | Edges.Left
67 }
68 visible: false
69
70 onNextVisibleChanged: hangTimer.restart()
71
72 Timer {
73 id: hangTimer
74 interval: 100
75 onTriggered: tooltip.visible = tooltip.nextVisible
76 }
77
78 implicitWidth: clockTooltipContent.width
79 implicitHeight: clockTooltipContent.height
80 color: "black"
81
82 onVisibleChanged: {
83 yearCalendar.year = chrono.date.getFullYear();
84 clockMouseArea.angleRem = 0;
85 }
86
87 WrapperMouseArea {
88 id: tooltipMouseArea
89
90 hoverEnabled: true
91 enabled: true
92
93 onWheel: event => clockMouseArea.scrollYear(event)
94
95 anchors.fill: parent
96
97 WrapperItem {
98 id: clockTooltipContent
99
100 margin: 8
101 leftMargin: 0
102
103 ColumnLayout {
104 Text {
105 id: yearLabel
106
107 horizontalAlignment: Text.AlignHCenter
108
109 font.pointSize: 14
110 font.family: "Fira Sans"
111 font.features: { "tnum": 1 }
112 color: "white"
113
114 text: yearCalendar.year
115
116 Layout.fillWidth: true
117 Layout.bottomMargin: 8
118 }
119
120 GridLayout {
121 property int year: chrono.date.getFullYear()
122
123 id: yearCalendar
124
125 columns: 3
126 columnSpacing: 16
127 rowSpacing: 16
128
129 Layout.alignment: Qt.AlignHCenter
130 Layout.fillWidth: false
131
132 Repeater {
133 model: 12
134
135 GridLayout {
136 columns: 2
137
138 required property int index
139 property int month: index
140
141 id: monthCalendar
142
143 Layout.alignment: Qt.AlignTop | Qt.AlignRight
144 Layout.fillWidth: false
145
146 Text {
147 Layout.column: 1
148 Layout.fillWidth: true
149
150 horizontalAlignment: Text.AlignHCenter
151
152 font.pointSize: 10
153 font.family: "Fira Sans"
154
155 text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM")
156
157 color: "#ffead3"
158 }
159
160 DayOfWeekRow {
161 locale: grid.locale
162
163 Layout.row: 1
164 Layout.column: 1
165 Layout.fillWidth: true
166
167 delegate: WrapperItem {
168 required property string shortName
169
170 width: dowLabel.contentWidth + 6
171
172 Text {
173 id: dowLabel
174
175 anchors.fill: parent
176
177 font.pointSize: 10
178 font.family: "Fira Sans"
179
180 text: parent.shortName
181 color: "#ffcc66"
182
183 horizontalAlignment: Text.AlignHCenter
184 verticalAlignment: Text.AlignVCenter
185 }
186 }
187 }
188
189 WeekNumberColumn {
190 month: grid.month
191 year: grid.year
192 locale: grid.locale
193
194 Layout.fillHeight: true
195
196 delegate: Text {
197 required property int weekNumber
198
199 opacity: {
200 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
201 const dayOfWeek = simple.getDay();
202 const isoWeekStart = simple;
203
204 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1);
205 if (dayOfWeek > 4) {
206 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
207 }
208
209 for (let i = 0; i < 7; i++) {
210 const dayInWeek = new Date(isoWeekStart);
211 dayInWeek.setDate(dayInWeek.getDate() + i);
212 if (dayInWeek.getMonth() == monthCalendar.month)
213 return 1;
214 }
215
216 return 0;
217 }
218
219 font.pointSize: 10
220 font.family: "Fira Sans"
221 font.features: { "tnum": 1 }
222
223 text: weekNumber
224 color: "#99ffdd"
225
226 horizontalAlignment: Text.AlignRight
227 verticalAlignment: Text.AlignVCenter
228 }
229 }
230
231 MonthGrid {
232 id: grid
233
234 year: yearCalendar.year
235 month: monthCalendar.month
236 locale: Qt.locale("en_DK")
237
238 Layout.fillWidth: true
239 Layout.fillHeight: true
240
241 delegate: Text {
242 required property var model
243
244 opacity: model.month === monthCalendar.month ? 1 : 0
245
246 font.pointSize: 10
247 font.family: "Fira Sans"
248 font.features: { "tnum": 1 }
249
250 property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day
251
252 text: model.day
253 color: today ? "#ff6699" : "white"
254
255 horizontalAlignment: Text.AlignRight
256 verticalAlignment: Text.AlignVCenter
257 }
258 }
259 }
260 }
261 }
262 }
263 }
264 }
265 }
266}
diff --git a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
new file mode 100644
index 00000000..bc3750f9
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
@@ -0,0 +1,106 @@
1import Quickshell
2import QtQuick
3import qs.Services
4import Quickshell.Widgets
5
6Item {
7 width: kbdLabel.contentWidth + 8
8 height: parent.height
9 anchors.verticalCenter: parent.verticalCenter
10
11 WrapperMouseArea {
12 id: kbdMouseArea
13
14 anchors.fill: parent
15
16 hoverEnabled: true
17 cursorShape: Qt.PointingHandCursor
18 enabled: true
19 onClicked: {
20 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {})
21 }
22 onWheel: event => {
23 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {})
24 }
25
26 Rectangle {
27 id: kbdWidget
28
29 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" }
30
31 anchors.fill: parent
32 color: {
33 if (kbdMouseArea.containsMouse) {
34 return "#33808080";
35 }
36 return "transparent";
37 }
38
39 Text {
40 id: kbdLabel
41
42 font.pointSize: 10
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 }
58 }
59 }
60
61 PopupWindow {
62 id: tooltip
63
64 property bool nextVisible: kbdMouseArea.containsMouse || tooltipMouseArea.containsMouse
65
66 anchor {
67 item: kbdMouseArea
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: kbdTooltipText.contentWidth + 16
81 implicitHeight: kbdTooltipText.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: kbdTooltipText
94
95 font.pointSize: 10
96 font.family: "Fira Sans"
97 color: "white"
98
99 text: {
100 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx];
101 return currentLayout || "";
102 }
103 }
104 }
105 }
106}
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
new file mode 100644
index 00000000..7cb1cc67
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -0,0 +1,259 @@
1import Quickshell
2import Quickshell.Wayland
3import Quickshell.Io
4import Quickshell.Services.Pam
5import Quickshell.Widgets
6import QtQuick.Effects
7import QtQuick.Layouts
8import QtQuick
9import QtQuick.Controls
10import QtQuick.Controls.Fusion
11import qs.Services
12// import QtQml.Models
13
14Scope {
15 id: lockscreen
16
17 property string currentText: ""
18
19 PamContext {
20 id: pam
21
22 property list<var> messages: []
23
24 config: "quickshell"
25 onCompleted: result => {
26 if (result === PamResult.Success) {
27 lock.locked = false;
28 }
29 }
30 onPamMessage: {
31 messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }])
32 }
33 onActiveChanged: {
34 messages = [];
35 }
36 }
37
38 IpcHandler {
39 target: "Lockscreen"
40
41 function setLocked(locked: bool): void { lock.locked = locked; }
42 function getLocked(): bool { return lock.locked; }
43 }
44
45 WlSessionLock {
46 id: lock
47
48 onLockedChanged: {
49 if (!locked && pam.active)
50 pam.abort();
51 }
52
53 WlSessionLockSurface {
54 id: lockSurface
55
56 color: "#000000"
57
58 Item {
59 id: background
60
61 anchors.fill: parent
62
63 property Img current: one
64 property string source: selector.selected
65
66 WallpaperSelector {
67 id: selector
68 seed: lockSurface.screen?.name || ""
69 }
70
71 onSourceChanged: {
72 if (!source)
73 current = null;
74 else if (current === one)
75 two.update()
76 else
77 one.update()
78 }
79
80 Img { id: one }
81 Img { id: two }
82
83 component Img: Item {
84 id: img
85
86 property string source
87
88 function update() {
89 source = background.source || ""
90 }
91
92 anchors.fill: parent
93
94 Image {
95 id: imageSource
96
97 source: img.source
98 sourceSize: Qt.size(parent.width, parent.height)
99 fillMode: Image.PreserveAspectCrop
100 smooth: true
101 visible: false
102 asynchronous: true
103 cache: false
104
105 onStatusChanged: {
106 if (status === Image.Ready) {
107 background.current = img
108 }
109 }
110 }
111
112 MultiEffect {
113 id: imageEffect
114
115 source: imageSource
116 anchors.fill: parent
117 blurEnabled: true
118 blur: 1
119 blurMax: 64
120 blurMultiplier: 2
121
122 opacity: 0
123
124 states: State {
125 name: "visible"
126 when: background.current === img
127
128 PropertyChanges {
129 imageEffect.opacity: 1
130 }
131 StateChangeScript {
132 name: "unloadOther"
133 script: {
134 if (img === one)
135 two.source = ""
136 if (img === two)
137 one.source = ""
138 }
139 }
140 }
141
142 transitions: Transition {
143 SequentialAnimation {
144 NumberAnimation {
145 target: imageEffect
146 properties: "opacity"
147 duration: 5000
148 easing.type: Easing.OutCubic
149 }
150 ScriptAction {
151 scriptName: "unloadOther"
152 }
153 }
154 }
155 }
156 }
157 }
158
159 Item {
160 anchors {
161 top: lockSurface.top
162 left: lockSurface.left
163 right: lockSurface.right
164 }
165
166 implicitWidth: lockSurface.width
167 implicitHeight: 21
168
169 Rectangle {
170 anchors.fill: parent
171 color: Qt.rgba(0, 0, 0, 0.75)
172 }
173
174 Clock {
175 anchors.centerIn: parent
176 calendarPopup: false
177 }
178 }
179
180 WrapperRectangle {
181 id: unlockUi
182
183 Keys.onPressed: event => {
184 if (!pam.active) {
185 event.accepted = true;
186 pam.start();
187 }
188 }
189 focus: !passwordBox.visible
190
191 visible: pam.active
192
193 color: Qt.rgba(0, 0, 0, 0.75)
194 margin: 8
195
196 anchors.centerIn: parent
197
198 ColumnLayout {
199 spacing: 4
200
201 BusyIndicator {
202 visible: running
203 running: !Array.from(pam.messages).length && !pam.responseRequired
204 }
205
206 Repeater {
207 model: pam.messages
208
209 Text {
210 required property var modelData
211
212 font.pointSize: 10
213 font.family: "Fira Sans"
214 color: modelData.error ? "#f28a21" : "#ffffff"
215
216 text: modelData.text
217
218 Layout.fillWidth: true
219 horizontalAlignment: Text.AlignHCenter
220 }
221 }
222
223 TextField {
224 id: passwordBox
225
226 visible: pam.responseRequired
227 echoMode: pam.responseVisible ? TextInput.Normal : TextInput.Password
228 inputMethodHints: Qt.ImhSensitiveData
229
230 onTextChanged: lockscreen.currentText = passwordBox.text
231 onAccepted: {
232 passwordBox.readOnly = true;
233 pam.respond(lockscreen.currentText);
234 }
235
236 Connections {
237 target: lockscreen
238 function onCurrentTextChanged() {
239 passwordBox.text = lockscreen.currentText
240 }
241 }
242 Connections {
243 target: pam
244 function onResponseRequiredChanged() {
245 if (pam.responseRequired)
246 passwordBox.readOnly = false;
247 passwordBox.focus = true;
248 passwordBox.selectAll();
249 }
250 }
251
252 Layout.topMargin: 4
253 Layout.fillWidth: true
254 }
255 }
256 }
257 }
258 }
259}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
index 914152e1..af522ec4 100644
--- a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
@@ -13,10 +13,6 @@ Singleton {
13 property var windows: [] 13 property var windows: []
14 readonly property string socketPath: Quickshell.env("NIRI_SOCKET") 14 readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
15 15
16 onKeyboardLayoutsChanged: {
17 console.log(JSON.stringify(keyboardLayouts));
18 }
19
20 function refreshOutputs() { 16 function refreshOutputs() {
21 commandSocket.sendCommand("Outputs", data => { 17 commandSocket.sendCommand("Outputs", data => {
22 outputs = data.Ok.Outputs; 18 outputs = data.Ok.Outputs;
@@ -32,9 +28,12 @@ Singleton {
32 path: root.socketPath 28 path: root.socketPath
33 connected: true 29 connected: true
34 30
31 property bool acked: false
32
35 onConnectionStateChanged: { 33 onConnectionStateChanged: {
36 if (connected) { 34 if (connected) {
37 write('"EventStream"\n') 35 acked = false;
36 write('"EventStream"\n');
38 } 37 }
39 } 38 }
40 39
@@ -70,6 +69,11 @@ Singleton {
70 eventWindowUrgencyChanged(event.WindowUrgencyChanged); 69 eventWindowUrgencyChanged(event.WindowUrgencyChanged);
71 else if (event.WindowLayoutsChanged) 70 else if (event.WindowLayoutsChanged)
72 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) {}
75 else
76 console.log(JSON.stringify(event));
73 } catch (e) { 77 } catch (e) {
74 console.warn("NiriService: Failed to parse event:", line, e) 78 console.warn("NiriService: Failed to parse event:", line, e)
75 } 79 }
@@ -157,10 +161,10 @@ Singleton {
157 } 161 }
158 function eventWindowOpenedOrChanged(data) { 162 function eventWindowOpenedOrChanged(data) {
159 root.windows = Array.from(root.windows).map(win => { 163 root.windows = Array.from(root.windows).map(win => {
160 if (win.id === data.window.id) 164 if (data.window.is_focused)
161 return data.window; 165 win.is_focused = false;
162 return win; 166 return win;
163 }); 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/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
new file mode 100644
index 00000000..55b1690e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
@@ -0,0 +1,159 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import Quickshell.Services.SystemTray
5
6Item {
7 anchors.verticalCenter: parent.verticalCenter
8 width: systemTrayRow.childrenRect.width
9 height: parent.height
10 clip: true
11
12 Row {
13 id: systemTrayRow
14 anchors.centerIn: parent
15 width: childrenRect.width
16 height: parent.height
17 spacing: 0
18
19 Repeater {
20 model: ScriptModel {
21 values: {
22 var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive);
23 trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id))
24 return trayItems;
25 }
26 }
27
28 delegate: Item {
29 id: trayItemWrapper
30
31 property var trayItem: modelData
32 property string iconSource: {
33 let icon = trayItem && trayItem.icon
34 if (typeof icon === 'string' || icon instanceof String) {
35 if (icon.includes("?path=")) {
36 const split = icon.split("?path=")
37 if (split.length !== 2)
38 return icon
39 const name = split[0]
40 const path = split[1]
41 const fileName = name.substring(
42 name.lastIndexOf("/") + 1)
43 return `file://${path}/${fileName}`
44 }
45 return icon
46 }
47 return ""
48 }
49
50 width: 16
51 height: parent.height
52 anchors.verticalCenter: parent.verticalCenter
53
54 WrapperMouseArea {
55 id: trayItemArea
56
57 anchors.fill: parent
58 acceptedButtons: Qt.LeftButton | Qt.RightButton
59 hoverEnabled: true
60 cursorShape: trayItem.onlyMenu ? Qt.ArrowCursor : Qt.PointingHandCursor
61 onClicked: mouse => {
62 if (!trayItem)
63 return
64
65 if (mouse.button === Qt.LeftButton
66 && !trayItem.onlyMenu) {
67 trayItem.activate()
68 return
69 }
70
71 if (trayItem.hasMenu) {
72 var globalPos = mapToGlobal(0, 0)
73 var currentScreen = screen || Screen
74 var screenX = currentScreen.x || 0
75 var relativeX = globalPos.x - screenX
76 menuAnchor.menu = trayItem.menu
77 menuAnchor.anchor.window = bar
78 menuAnchor.anchor.rect = Qt.rect(
79 relativeX,
80 21,
81 parent.width, 1)
82 menuAnchor.open()
83 }
84 }
85
86 IconImage {
87 anchors.centerIn: parent
88 width: parent.width
89 height: parent.width
90 source: trayItemWrapper.iconSource
91 asynchronous: true
92 smooth: true
93 mipmap: true
94 }
95 }
96
97 PopupWindow {
98 id: tooltip
99
100 property bool nextVisible: (trayItem.tooltipTitle || trayItem.tooltipDescription) && (trayItemArea.containsMouse || tooltipMouseArea.containsMouse) && !menuAnchor.visible
101
102 anchor {
103 item: trayItemArea
104 edges: Edges.Bottom
105 }
106
107 visible: false
108 onNextVisibleChanged: hangTimer.restart()
109
110 Timer {
111 id: hangTimer
112 interval: 100
113 onTriggered: tooltip.visible = tooltip.nextVisible
114 }
115
116 color: "black"
117
118 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16
119 implicitHeight: tooltipTitle.contentHeight + tooltipDescription.contentHeight + 16
120
121 WrapperMouseArea {
122 id: tooltipMouseArea
123
124 hoverEnabled: true
125 enabled: true
126
127 margin: 8
128
129 Column {
130 Text {
131 id: tooltipTitle
132
133 font.pointSize: 10
134 font.family: "Fira Sans"
135 font.bold: true
136 color: "white"
137
138 text: trayItem.tooltipTitle
139 }
140
141 Text {
142 id: tooltipDescription
143
144 font.pointSize: 10
145 font.family: "Fira Sans"
146 color: "white"
147
148 text: trayItem.tooltipDescription
149 }
150 }
151 }
152 }
153 }
154 }
155 }
156 QsMenuAnchor {
157 id: menuAnchor
158 }
159}
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
new file mode 100644
index 00000000..c8c017c3
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
@@ -0,0 +1,201 @@
1import Quickshell
2import QtQuick
3import qs.Services
4import Quickshell.Widgets
5import QtQuick.Layouts
6
7Row {
8 id: workspaces
9
10 property var ignoreWorkspaces: @ignore_workspaces@
11
12 height: parent.height
13 anchors.verticalCenter: parent.verticalCenter
14 spacing: 0
15
16 Repeater {
17 model: ScriptModel {
18 values: {
19 let currWorkspaces = NiriService.workspaces;
20 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces);
21 currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name));
22 currWorkspaces.sort((a, b) => {
23 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x)
24 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x
25 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y)
26 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y
27 return a.idx - b.idx;
28 });
29 return currWorkspaces;
30 }
31 }
32
33 Item {
34 id: wsItem
35
36 property var workspaceData: modelData
37
38 width: wsLabel.contentWidth + 8
39 height: parent.height
40 anchors.verticalCenter: parent.verticalCenter
41
42 WrapperMouseArea {
43 id: mouseArea
44
45 anchors.fill: parent
46
47 hoverEnabled: true
48 cursorShape: Qt.PointingHandCursor
49 enabled: true
50 onClicked: {
51 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {})
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 }
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 }
199 }
200 }
201}
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
index 2abd1fef..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
@@ -8,8 +9,36 @@ ShellRoot {
8 Variants { 9 Variants {
9 model: Quickshell.screens 10 model: Quickshell.screens
10 11
11 delegate: Bar { 12 delegate: Scope {
12 modelData: item 13 id: screenScope
14
15 required property var modelData
16
17 PanelWindow {
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"
31
32 WallpaperBackground {
33 screen: bgWindow.screen.name
34 }
35 }
36
37 Bar {
38 screen: screenScope.modelData
39 }
13 } 40 }
14 } 41 }
42
43 Lockscreen {}
15} 44}