Compare commits

...

31 commits

Author SHA1 Message Date
4f2610dece
io/ipchandler: add prop get 2025-01-26 03:57:07 -08:00
9417d6fa57
core/command: deprecate qs msg 2025-01-25 01:00:42 -08:00
420529362f
core/clock: expose date as a QDateTime 2025-01-24 23:53:31 -08:00
325be8857c
core/command: add option to select newest matching instance 2025-01-24 16:30:32 -08:00
b289bfa504
hyprland/surface: add visibleMask 2025-01-23 14:00:16 -08:00
cdaff2967f
core/icon: stop reusing image ids (dbusmenu, notifications)
Fixes issues caused by the QML engine caching old pixmaps using the
same IDs as new ones, notably dbusmenu icons.
2025-01-22 23:10:49 -08:00
c6791cf1f2
core/window: fix screen assignments being completely broken 2025-01-22 20:13:29 -08:00
b73eff0e47
core/screen: add model and serial number properties 2025-01-22 19:38:18 -08:00
Richard Bainesly
6a017d63d6
fix single quote parsing 2025-01-22 19:25:45 -08:00
3c7dfcb220
hyprland/ipc: handle renameworkspace 2025-01-22 04:16:08 -08:00
b336129c34
core/window: add QsWindow.devicePixelRatio 2025-01-22 03:33:46 -08:00
bc73d35d03
wayland/screencopy: fix ScreencopyContext leak in ScreencopyView
Also caused an FD leak.
2025-01-20 15:53:04 -08:00
6464ead0f1
core/window: move input mask handling + commit scheduling to polish 2025-01-20 01:14:28 -08:00
d6b58521e9
core!: fix typo in ShellScreen.primaryOrientation 2025-01-19 01:00:03 -08:00
d195ca7680
wayland/screencopy: fix UAF in dmabuf modifier collection
The QList optimization the code was for no longer exists.
2025-01-15 03:24:19 -08:00
ca79715cce
wayland/screencopy: log more information during buffer creation 2025-01-15 02:52:08 -08:00
c2ed5bf559
core/stacklist: add tests 2025-01-15 02:47:14 -08:00
6024c37492
core/scriptmodel: improve docs 2025-01-14 15:30:48 -08:00
6d8022b709
service/pipewire: add registry and node ready properties 2025-01-14 15:30:47 -08:00
Richard Bainesly
8b6aa624a2
fix fd leaks in scanPath
use auto
2025-01-14 13:05:15 -08:00
cd429142a4
wayland/screencopy: add screencopy 2025-01-14 05:08:07 -08:00
918dd2392d
build/wayland: do not link to a target in wl_proto 2025-01-11 23:59:19 -08:00
2c411fce5a
all: fix new lints 2025-01-07 03:11:19 -08:00
26d443aa50
ci: add 6.8.1 2025-01-06 22:21:32 -08:00
af86d5fd19
hyprland/surface: remove debug print 2025-01-05 23:53:03 -08:00
761d99d644
service/mpris: reset position timestamps on seek
Moving the onPositionUpdated callback to a bpPosition binding caused
it not to fire when Position was changed to the same value, which can
happen when quickly changing tracks before the player has sent a new
position.

This reverts the above change while still updating position on seek.
2025-01-05 01:55:33 -08:00
fca058e66c
service/upower: add device model property 2025-01-04 04:38:03 -08:00
eaf854935b
service/upower: correctly deserialize UPowerDeviceState::Discharging
???
2025-01-04 03:37:53 -08:00
f3b7171b25
core/window: allow explicit surface format selection 2025-01-04 03:04:41 -08:00
dc3a79600d
core/command: avoid running when cli11 forces returning 0
Fixes running when --help is passed.
2025-01-03 02:42:32 -08:00
47bcf8ee61
service/upower: add power-profiles support 2025-01-02 21:54:36 -08:00
144 changed files with 5507 additions and 834 deletions

View file

@ -7,6 +7,7 @@ Checks: >
-bugprone-easily-swappable-parameters, -bugprone-easily-swappable-parameters,
-bugprone-forward-declararion-namespace, -bugprone-forward-declararion-namespace,
-bugprone-forward-declararion-namespace, -bugprone-forward-declararion-namespace,
-bugprone-return-const-ref-from-parameter,
concurrency-*, concurrency-*,
cppcoreguidelines-*, cppcoreguidelines-*,
-cppcoreguidelines-owning-memory, -cppcoreguidelines-owning-memory,
@ -44,6 +45,7 @@ Checks: >
-readability-container-data-pointer, -readability-container-data-pointer,
-readability-implicit-bool-conversion, -readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator, -readability-avoid-nested-conditional-operator,
-readability-math-missing-parentheses,
tidyfox-*, tidyfox-*,
CheckOptions: CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true performance-for-range-copy.WarnOnAllAutoCopies: true

View file

@ -6,7 +6,7 @@ jobs:
name: Nix name: Nix
strategy: strategy:
matrix: matrix:
qtver: [qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] qtver: [qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
compiler: [clang, gcc] compiler: [clang, gcc]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -43,6 +43,7 @@ jobs:
qt6-shadertools \ qt6-shadertools \
wayland-protocols \ wayland-protocols \
wayland \ wayland \
libdrm \
libxcb \ libxcb \
libpipewire \ libpipewire \
cli11 \ cli11 \

View file

@ -130,6 +130,24 @@ which allows quickshell to be used as a session lock under compatible wayland co
To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF`
#### Screencopy
Enables streaming video from monitors and toplevel windows through various protocols.
To disable: `-DSCREENCOPY=OFF`
Dependencies:
- `libdrm`
- `libgbm`
Specific protocols can also be disabled:
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
- `DSCREENCOPY_WLR=OFF` - Disable screencopy via [zwlr-screencopy-v1]
- `DSCREENCOPY_HYPRLAND_TOPLEVEL=OFF` - Disable screencopy via [hyprland-toplevel-export-v1]
[ext-image-copy-capture-v1]:https://wayland.app/protocols/ext-image-copy-capture-v1
[zwlr-screencopy-v1]: https://wayland.app/protocols/wlr-screencopy-unstable-v1
[hyprland-toplevel-export-v1]: https://wayland.app/protocols/hyprland-toplevel-export-v1
### X11 ### X11
This feature enables x11 support. Currently this implements panel windows for X11 similarly This feature enables x11 support. Currently this implements panel windows for X11 similarly
to the wlroots layershell above. to the wlroots layershell above.

View file

@ -56,6 +56,10 @@ boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND)
boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND) boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND)
boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND) boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND)
boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND) boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND)
boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND)
boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND)
boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND)
boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND)
boption(X11 "X11" ON) boption(X11 "X11" ON)
boption(I3 "I3/Sway" ON) boption(I3 "I3/Sway" ON)
boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3)
@ -70,7 +74,7 @@ boption(SERVICE_NOTIFICATIONS "Notifications" ON)
include(cmake/install-qml-module.cmake) include(cmake/install-qml-module.cmake)
include(cmake/util.cmake) include(cmake/util.cmake)
add_compile_options(-Wall -Wextra) add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
# pipewire defines this, breaking PCH # pipewire defines this, breaking PCH
add_compile_definitions(_REENTRANT) add_compile_definitions(_REENTRANT)

View file

@ -4,13 +4,13 @@ fmt:
find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i
lint: lint:
find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-ci: lint-ci:
find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }} find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }}
lint-changed: lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='': configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \ cmake -GNinja -B {{builddir}} \

View file

@ -11,9 +11,14 @@ in {
# For old qt versions, grab the commit before the version bump that has all the patches # For old qt versions, grab the commit before the version bump that has all the patches
# instead of the bumped version. # instead of the bumped version.
qt6_8_1 = byCommit {
commit = "3df3c47c19dc90fec35359e89ffb52b34d2b0e94";
sha256 = "1lhlm7czhwwys5ak6ngb5li6bxddilb9479k9nkss502kw8hwjyz";
};
qt6_8_0 = byCommit { qt6_8_0 = byCommit {
commit = "23e89b7da85c3640bbc2173fe04f4bd114342367"; commit = "352f462ad9d2aa2cde75fdd8f1734e86402a3ff6";
sha256 = "1b2v6y3bja4br5ribh9lj6xzz2k81dggz708b2mib83rwb509wyb"; sha256 = "02zfgkr9fpd6iwfh6dcr3m6fnx61jppm3v081f3brvkqwmmz7zq1";
}; };
qt6_7_3 = byCommit { qt6_7_3 = byCommit {

View file

@ -14,6 +14,8 @@
jemalloc, jemalloc,
wayland, wayland,
wayland-protocols, wayland-protocols,
libdrm,
libgbm ? null,
xorg, xorg,
pipewire, pipewire,
pam, pam,
@ -64,7 +66,7 @@
++ lib.optional withCrashReporter breakpad ++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc ++ lib.optional withJemalloc jemalloc
++ lib.optional withQtSvg qt6.qtsvg ++ lib.optional withQtSvg qt6.qtsvg
++ lib.optionals withWayland [ qt6.qtwayland wayland ] ++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else []))
++ lib.optional withX11 xorg.libxcb ++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam ++ lib.optional withPam pam
++ lib.optional withPipewire pipewire; ++ lib.optional withPipewire pipewire;
@ -79,6 +81,7 @@
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter) (lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc) (lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland) (lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "HYPRLAND" withHyprland) (lib.cmakeBool "HYPRLAND" withHyprland)

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1732014248, "lastModified": 1736012469,
"narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -6,8 +6,6 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include "util.hpp"
SystemClock::SystemClock(QObject* parent): QObject(parent) { SystemClock::SystemClock(QObject* parent): QObject(parent) {
QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout); QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout);
this->update(); this->update();
@ -48,19 +46,16 @@ void SystemClock::update() {
void SystemClock::setTime(const QDateTime& targetTime) { void SystemClock::setTime(const QDateTime& targetTime) {
auto currentTime = QDateTime::currentDateTime(); auto currentTime = QDateTime::currentDateTime();
auto offset = currentTime.msecsTo(targetTime); auto offset = currentTime.msecsTo(targetTime);
auto dtime = offset > -500 && offset < 500 ? targetTime : currentTime; this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime;
auto time = dtime.time();
auto secondPrecision = this->mPrecision >= SystemClock::Seconds; auto time = this->currentTime.time();
auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0); this->currentTime.setTime(QTime(
this->mPrecision >= SystemClock::Hours ? time.hour() : 0,
this->mPrecision >= SystemClock::Minutes ? time.minute() : 0,
this->mPrecision >= SystemClock::Seconds ? time.second() : 0
));
auto minutePrecision = this->mPrecision >= SystemClock::Minutes; emit this->dateChanged();
auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0);
auto hourPrecision = this->mPrecision >= SystemClock::Hours;
auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0);
DropEmitter::call(secondChanged, minuteChanged, hourChanged);
} }
void SystemClock::schedule(const QDateTime& targetTime) { void SystemClock::schedule(const QDateTime& targetTime) {
@ -76,11 +71,11 @@ void SystemClock::schedule(const QDateTime& targetTime) {
auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime; auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime;
auto baseTimeT = nextTime.time(); auto baseTimeT = nextTime.time();
nextTime.setTime( nextTime.setTime(QTime(
{hourPrecision ? baseTimeT.hour() : 0, hourPrecision ? baseTimeT.hour() : 0,
minutePrecision ? baseTimeT.minute() : 0, minutePrecision ? baseTimeT.minute() : 0,
secondPrecision ? baseTimeT.second() : 0} secondPrecision ? baseTimeT.second() : 0
); ));
if (secondPrecision) nextTime = nextTime.addSecs(1); if (secondPrecision) nextTime = nextTime.addSecs(1);
else if (minutePrecision) nextTime = nextTime.addSecs(60); else if (minutePrecision) nextTime = nextTime.addSecs(60);
@ -91,7 +86,3 @@ void SystemClock::schedule(const QDateTime& targetTime) {
this->timer.start(static_cast<qint32>(delay)); this->timer.start(static_cast<qint32>(delay));
this->targetTime = nextTime; this->targetTime = nextTime;
} }
DEFINE_MEMBER_GETSET(SystemClock, hours, setHours);
DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes);
DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds);

View file

@ -7,9 +7,26 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include "util.hpp"
///! System clock accessor. ///! System clock accessor.
/// SystemClock is a view into the system's clock.
/// It updates at hour, minute, or second intervals depending on @@precision.
///
/// # Examples
/// ```qml
/// SystemClock {
/// id: clock
/// precision: SystemClock.Seconds
/// }
///
/// @@QtQuick.Text {
/// text: Qt.formatDateTime(clock.date, "hh:mm:ss - yyyy-MM-dd")
/// }
/// ```
///
/// > [!WARNING] Clock updates will trigger within 50ms of the system clock changing,
/// > however this can be either before or after the clock changes (+-50ms). If you
/// > need a date object, use @@date instead of constructing a new one, or the time
/// > of the constructed object could be off by up to a second.
class SystemClock: public QObject { class SystemClock: public QObject {
Q_OBJECT; Q_OBJECT;
/// If the clock should update. Defaults to true. /// If the clock should update. Defaults to true.
@ -18,12 +35,17 @@ class SystemClock: public QObject {
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
/// The precision the clock should measure at. Defaults to `SystemClock.Seconds`. /// The precision the clock should measure at. Defaults to `SystemClock.Seconds`.
Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged); Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged);
/// The current date and time.
///
/// > [!TIP] You can use @@QtQml.Qt.formatDateTime() to get the time as a string in
/// > your format of choice.
Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged);
/// The current hour. /// The current hour.
Q_PROPERTY(quint32 hours READ hours NOTIFY hoursChanged); Q_PROPERTY(quint32 hours READ hours NOTIFY dateChanged);
/// The current minute, or 0 if @@precision is `SystemClock.Hours`. /// The current minute, or 0 if @@precision is `SystemClock.Hours`.
Q_PROPERTY(quint32 minutes READ minutes NOTIFY minutesChanged); Q_PROPERTY(quint32 minutes READ minutes NOTIFY dateChanged);
/// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`. /// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`.
Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged); Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged);
QML_ELEMENT; QML_ELEMENT;
public: public:
@ -43,12 +65,15 @@ public:
[[nodiscard]] SystemClock::Enum precision() const; [[nodiscard]] SystemClock::Enum precision() const;
void setPrecision(SystemClock::Enum precision); void setPrecision(SystemClock::Enum precision);
[[nodiscard]] QDateTime date() const { return this->currentTime; }
[[nodiscard]] quint32 hours() const { return this->currentTime.time().hour(); }
[[nodiscard]] quint32 minutes() const { return this->currentTime.time().minute(); }
[[nodiscard]] quint32 seconds() const { return this->currentTime.time().second(); }
signals: signals:
void enabledChanged(); void enabledChanged();
void precisionChanged(); void precisionChanged();
void hoursChanged(); void dateChanged();
void minutesChanged();
void secondsChanged();
private slots: private slots:
void onTimeout(); void onTimeout();
@ -56,17 +81,11 @@ private slots:
private: private:
bool mEnabled = true; bool mEnabled = true;
SystemClock::Enum mPrecision = SystemClock::Seconds; SystemClock::Enum mPrecision = SystemClock::Seconds;
quint32 mHours = 0;
quint32 mMinutes = 0;
quint32 mSeconds = 0;
QTimer timer; QTimer timer;
QDateTime currentTime;
QDateTime targetTime; QDateTime targetTime;
void update(); void update();
void setTime(const QDateTime& targetTime); void setTime(const QDateTime& targetTime);
void schedule(const QDateTime& targetTime); void schedule(const QDateTime& targetTime);
DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged);
DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged);
DECLARE_PRIVATE_MEMBER(SystemClock, seconds, setSeconds, mSeconds, secondsChanged);
}; };

View file

@ -18,7 +18,9 @@
#include "model.hpp" #include "model.hpp"
namespace {
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
}
struct Locale { struct Locale {
explicit Locale() = default; explicit Locale() = default;
@ -78,6 +80,7 @@ struct Locale {
QString modifier; QString modifier;
}; };
// NOLINTNEXTLINE(misc-use-internal-linkage)
QDebug operator<<(QDebug debug, const Locale& locale) { QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug); auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
@ -210,7 +213,7 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
currentArgument += c; currentArgument += c;
escape = 0; escape = 0;
} else if (c == u'"') { } else if (c == u'"' || c == u'\'') {
parsingString = false; parsingString = false;
} else { } else {
currentArgument += c; currentArgument += c;
@ -226,7 +229,7 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
percent = false; percent = false;
} else if (c == '%') { } else if (c == '%') {
percent = true; percent = true;
} else if (c == u'"') { } else if (c == u'"' || c == u'\'') {
parsingString = true; parsingString = true;
} else if (c == u' ') { } else if (c == u' ') {
if (!currentArgument.isEmpty()) { if (!currentArgument.isEmpty()) {
@ -306,7 +309,7 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (auto& entry: entries) { for (auto& entry: entries) {
if (entry.isDir()) this->scanPath(entry.path(), prefix + dir.dirName() + "-"); if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
else if (entry.isFile()) { else if (entry.isFile()) {
auto path = entry.filePath(); auto path = entry.filePath();
if (!path.endsWith(".desktop")) { if (!path.endsWith(".desktop")) {
@ -314,9 +317,8 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue; continue;
} }
auto* file = new QFile(path); auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) {
if (!file->open(QFile::ReadOnly)) {
qCDebug(logDesktopEntry) << "Could not open file" << path; qCDebug(logDesktopEntry) << "Could not open file" << path;
continue; continue;
} }
@ -324,7 +326,7 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
auto lowerId = id.toLower(); auto lowerId = id.toLower();
auto text = QString::fromUtf8(file->readAll()); auto text = QString::fromUtf8(file.readAll());
auto* dentry = new DesktopEntry(id, this); auto* dentry = new DesktopEntry(id, this);
dentry->parseEntry(text); dentry->parseEntry(text);

View file

@ -1,4 +1,5 @@
#include "iconimageprovider.hpp" #include "iconimageprovider.hpp"
#include <algorithm>
#include <qcolor.h> #include <qcolor.h>
#include <qicon.h> #include <qicon.h>
@ -49,8 +50,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
QPixmap IconImageProvider::missingPixmap(const QSize& size) { QPixmap IconImageProvider::missingPixmap(const QSize& size) {
auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1; auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1;
auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1; auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1;
if (width < 2) width = 2; width = std::max(width, 2);
if (height < 2) height = 2; height = std::max(height, 2);
auto pixmap = QPixmap(width, height); auto pixmap = QPixmap(width, height);
pixmap.fill(QColorConstants::Black); pixmap.fill(QColorConstants::Black);

View file

@ -1,5 +1,6 @@
#include "imageprovider.hpp" #include "imageprovider.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h> #include <qdebug.h>
#include <qimage.h> #include <qimage.h>
#include <qlogging.h> #include <qlogging.h>
@ -7,17 +8,30 @@
#include <qobject.h> #include <qobject.h>
#include <qpixmap.h> #include <qpixmap.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qtypes.h>
static QMap<QString, QsImageHandle*> liveImages; // NOLINT namespace {
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent) namespace {
: QObject(parent) QMap<QString, QsImageHandle*> liveImages; // NOLINT
, type(type) { quint32 handleIndex = 0; // NOLINT
{ } // namespace
auto dbg = QDebug(&this->id);
dbg.nospace() << static_cast<void*>(this); void parseReq(const QString& req, QString& target, QString& param) {
auto splitIdx = req.indexOf('/');
if (splitIdx != -1) {
target = req.sliced(0, splitIdx);
param = req.sliced(splitIdx + 1);
} else {
target = req;
}
} }
} // namespace
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type)
: type(type)
, id(QString::number(++handleIndex)) {
liveImages.insert(this->id, this); liveImages.insert(this->id, this);
} }
@ -43,16 +57,6 @@ QPixmap QsImageHandle::
return QPixmap(); return QPixmap();
} }
void parseReq(const QString& req, QString& target, QString& param) {
auto splitIdx = req.indexOf('/');
if (splitIdx != -1) {
target = req.sliced(0, splitIdx);
param = req.sliced(splitIdx + 1);
} else {
target = req;
}
}
QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) { QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) {
QString target; QString target;
QString param; QString param;
@ -81,3 +85,9 @@ QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& req
return QPixmap(); return QPixmap();
} }
} }
QString QsIndexedImageHandle::url() const {
return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex);
}
void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; }

View file

@ -20,15 +20,13 @@ public:
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
}; };
class QsImageHandle: public QObject { class QsImageHandle {
Q_OBJECT;
public: public:
explicit QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent = nullptr); explicit QsImageHandle(QQmlImageProviderBase::ImageType type);
~QsImageHandle() override; virtual ~QsImageHandle();
Q_DISABLE_COPY_MOVE(QsImageHandle); Q_DISABLE_COPY_MOVE(QsImageHandle);
[[nodiscard]] QString url() const; [[nodiscard]] virtual QString url() const;
virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize); virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize);
virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize); virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize);
@ -37,3 +35,14 @@ private:
QQmlImageProviderBase::ImageType type; QQmlImageProviderBase::ImageType type;
QString id; QString id;
}; };
class QsIndexedImageHandle: public QsImageHandle {
public:
explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {}
[[nodiscard]] QString url() const override;
void imageChanged();
private:
quint32 changeIndex = 0;
};

View file

@ -28,5 +28,6 @@ headers = [
"types.hpp", "types.hpp",
"qsmenuanchor.hpp", "qsmenuanchor.hpp",
"clock.hpp", "clock.hpp",
"scriptmodel.hpp",
] ]
----- -----

View file

@ -15,7 +15,9 @@
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
namespace {
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
}
QsPaths* QsPaths::instance() { QsPaths* QsPaths::instance() {
static auto* instance = new QsPaths(); // NOLINT static auto* instance = new QsPaths(); // NOLINT

View file

@ -10,16 +10,9 @@ static QVector<QsEnginePlugin*> plugins; // NOLINT
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); } void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
void QsEnginePlugin::initPlugins() { void QsEnginePlugin::initPlugins() {
plugins.erase( plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
std::remove_if(
plugins.begin(),
plugins.end(),
[](QsEnginePlugin* plugin) { return !plugin->applies(); }
),
plugins.end()
);
std::sort(plugins.begin(), plugins.end(), [](QsEnginePlugin* a, QsEnginePlugin* b) { std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) {
return b->dependencies().contains(a->name()); return b->dependencies().contains(a->name());
}); });

View file

@ -1,4 +1,5 @@
#include "popupanchor.hpp" #include "popupanchor.hpp"
#include <algorithm>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qlogging.h> #include <qlogging.h>
@ -276,9 +277,7 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only
effectiveX = screenGeometry.right() - windowGeometry.width() + 1; effectiveX = screenGeometry.right() - windowGeometry.width() + 1;
} }
if (effectiveX < screenGeometry.left()) { effectiveX = std::max(effectiveX, screenGeometry.left());
effectiveX = screenGeometry.left();
}
} }
if (adjustment.testFlag(PopupAdjustment::SlideY)) { if (adjustment.testFlag(PopupAdjustment::SlideY)) {
@ -286,9 +285,7 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only
effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1; effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1;
} }
if (effectiveY < screenGeometry.top()) { effectiveY = std::max(effectiveY, screenGeometry.top());
effectiveY = screenGeometry.top();
}
} }
if (adjustment.testFlag(PopupAdjustment::ResizeX)) { if (adjustment.testFlag(PopupAdjustment::ResizeX)) {

View file

@ -42,6 +42,24 @@ QString QuickshellScreenInfo::name() const {
return this->screen->name(); return this->screen->name();
} }
QString QuickshellScreenInfo::model() const {
if (this->screen == nullptr) {
this->warnDangling();
return "{ NULL SCREEN }";
}
return this->screen->model();
}
QString QuickshellScreenInfo::serialNumber() const {
if (this->screen == nullptr) {
this->warnDangling();
return "{ NULL SCREEN }";
}
return this->screen->serialNumber();
}
qint32 QuickshellScreenInfo::x() const { qint32 QuickshellScreenInfo::x() const {
if (this->screen == nullptr) { if (this->screen == nullptr) {
this->warnDangling(); this->warnDangling();

View file

@ -29,6 +29,10 @@ class QuickshellScreenInfo: public QObject {
/// ///
/// Usually something like `DP-1`, `HDMI-1`, `eDP-1`. /// Usually something like `DP-1`, `HDMI-1`, `eDP-1`.
Q_PROPERTY(QString name READ name CONSTANT); Q_PROPERTY(QString name READ name CONSTANT);
/// The model of the screen as seen by the operating system.
Q_PROPERTY(QString model READ model CONSTANT);
/// The serial number of the screen as seen by the operating system.
Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT);
Q_PROPERTY(qint32 x READ x NOTIFY geometryChanged); Q_PROPERTY(qint32 x READ x NOTIFY geometryChanged);
Q_PROPERTY(qint32 y READ y NOTIFY geometryChanged); Q_PROPERTY(qint32 y READ y NOTIFY geometryChanged);
Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged); Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged);
@ -40,7 +44,7 @@ class QuickshellScreenInfo: public QObject {
/// The ratio between physical pixels and device-independent (scaled) pixels. /// The ratio between physical pixels and device-independent (scaled) pixels.
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged); Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged);
Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged); Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged);
Q_PROPERTY(Qt::ScreenOrientation primatyOrientation READ primaryOrientation NOTIFY primaryOrientationChanged); Q_PROPERTY(Qt::ScreenOrientation primaryOrientation READ primaryOrientation NOTIFY primaryOrientationChanged);
// clang-format on // clang-format on
public: public:
@ -49,6 +53,8 @@ public:
bool operator==(QuickshellScreenInfo& other) const; bool operator==(QuickshellScreenInfo& other) const;
[[nodiscard]] QString name() const; [[nodiscard]] QString name() const;
[[nodiscard]] QString model() const;
[[nodiscard]] QString serialNumber() const;
[[nodiscard]] qint32 x() const; [[nodiscard]] qint32 x() const;
[[nodiscard]] qint32 y() const; [[nodiscard]] qint32 y() const;
[[nodiscard]] qint32 width() const; [[nodiscard]] qint32 width() const;

View file

@ -8,6 +8,7 @@
#include <qregion.h> #include <qregion.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include <qvectornd.h>
PendingRegion::PendingRegion(QObject* parent): QObject(parent) { PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed);
@ -105,8 +106,19 @@ QRegion PendingRegion::applyTo(QRegion& region) const {
return region; return region;
} }
QRegion PendingRegion::applyTo(const QRect& rect) const {
// if left as the default, dont combine it with the whole rect area, leave it as is.
if (this->mIntersection == Intersection::Combine) {
return this->build();
} else {
auto baseRegion = QRegion(rect);
return this->applyTo(baseRegion);
}
}
void PendingRegion::regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region) { void PendingRegion::regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
if (!region) return;
QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed); QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed);
QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged); QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged);

View file

@ -96,6 +96,7 @@ public:
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
[[nodiscard]] QRegion build() const; [[nodiscard]] QRegion build() const;
[[nodiscard]] QRegion applyTo(QRegion& region) const; [[nodiscard]] QRegion applyTo(QRegion& region) const;
[[nodiscard]] QRegion applyTo(const QRect& rect) const;
RegionShape::Enum mShape = RegionShape::Rect; RegionShape::Enum mShape = RegionShape::Rect;
Intersection::Enum mIntersection = Intersection::Combine; Intersection::Enum mIntersection = Intersection::Combine;
@ -109,6 +110,11 @@ signals:
void widthChanged(); void widthChanged();
void heightChanged(); void heightChanged();
void childrenChanged(); void childrenChanged();
/// Triggered when the region's geometry changes.
///
/// In some cases the region does not update automatically.
/// In those cases you can emit this signal manually.
void changed(); void changed();
private slots: private slots:

View file

@ -36,6 +36,8 @@
/// delegate: // ... /// delegate: // ...
/// } /// }
/// ``` /// ```
/// [QAbstractItemModel]: https://doc.qt.io/qt-6/qabstractitemmodel.html
/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models
class ScriptModel: public QAbstractListModel { class ScriptModel: public QAbstractListModel {
Q_OBJECT; Q_OBJECT;
/// The list of values to reflect in the model. /// The list of values to reflect in the model.
@ -51,8 +53,19 @@ class ScriptModel: public QAbstractListModel {
/// > } /// > }
/// > ``` /// > ```
/// > /// >
/// > Note that we are using @@DesktopEntries.values because it will cause @@ScriptModel.values /// > Note that we are using @@ObjectModel.values because it will cause @@ScriptModel.values
/// > to receive an update on change. /// > to receive an update on change.
///
/// > [!TIP] Most lists exposed by Quickshell are read-only. Some operations like `sort()`
/// > act on a list in-place and cannot be used directly on a list exposed by Quickshell.
/// > You can copy a list using spread syntax: `[...variable]` instead of `variable`.
/// >
/// > For example:
/// > ```qml
/// > ScriptModel {
/// > values: [...DesktopEntries.applications.values].sort(...)
/// > }
/// > ```
Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged); Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged);
QML_ELEMENT; QML_ELEMENT;

173
src/core/stacklist.hpp Normal file
View file

@ -0,0 +1,173 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <vector>
#include <qlist.h>
#include <qtypes.h>
template <class T, size_t N>
class StackList {
public:
T& operator[](size_t i) {
if (i < N) {
return this->array[i];
} else {
return this->vec[i - N];
}
}
const T& operator[](size_t i) const {
return const_cast<StackList<T, N>*>(this)->operator[](i); // NOLINT
}
void push(const T& value) {
if (this->size < N) {
this->array[this->size] = value;
} else {
this->vec.push_back(value);
}
++this->size;
}
[[nodiscard]] size_t length() const { return this->size; }
[[nodiscard]] bool isEmpty() const { return this->size == 0; }
[[nodiscard]] bool operator==(const StackList<T, N>& other) const {
if (other.size != this->size) return false;
for (size_t i = 0; i != this->size; ++i) {
if (this->operator[](i) != other[i]) return false;
}
return true;
}
[[nodiscard]] QList<T> toList() const {
QList<T> list;
list.reserve(this->size);
for (const auto& entry: *this) {
list.push_back(entry);
}
return list;
}
template <typename Self, typename ListPtr, typename IT>
struct BaseIterator {
using iterator_category = std::bidirectional_iterator_tag;
using difference_type = int64_t;
using value_type = IT;
using pointer = IT*;
using reference = IT&;
BaseIterator() = default;
explicit BaseIterator(ListPtr list, size_t i): list(list), i(i) {}
reference operator*() const { return this->list->operator[](this->i); }
pointer operator->() const { return &**this; }
Self& operator++() {
++this->i;
return *static_cast<Self*>(this);
}
Self& operator--() {
--this->i;
return *static_cast<Self*>(this);
}
Self operator++(int) {
auto v = *this;
this->operator++();
return v;
}
Self operator--(int) {
auto v = *this;
this->operator--();
return v;
}
difference_type operator-(const Self& other) {
return static_cast<int64_t>(this->i) - static_cast<int64_t>(other.i);
}
Self& operator+(difference_type offset) {
return Self(this->list, static_cast<int64_t>(this->i) + offset);
}
[[nodiscard]] bool operator==(const Self& other) const {
return this->list == other.list && this->i == other.i;
}
[[nodiscard]] bool operator!=(const Self& other) const { return !(*this == other); }
private:
ListPtr list = nullptr;
size_t i = 0;
};
struct Iterator: public BaseIterator<Iterator, StackList<T, N>*, T> {
Iterator() = default;
Iterator(StackList<T, N>* list, size_t i)
: BaseIterator<Iterator, StackList<T, N>*, T>(list, i) {}
};
struct ConstIterator: public BaseIterator<ConstIterator, const StackList<T, N>*, const T> {
ConstIterator() = default;
ConstIterator(const StackList<T, N>* list, size_t i)
: BaseIterator<ConstIterator, const StackList<T, N>*, const T>(list, i) {}
};
[[nodiscard]] Iterator begin() { return Iterator(this, 0); }
[[nodiscard]] Iterator end() { return Iterator(this, this->size); }
[[nodiscard]] ConstIterator begin() const { return ConstIterator(this, 0); }
[[nodiscard]] ConstIterator end() const { return ConstIterator(this, this->size); }
[[nodiscard]] bool isContiguous() const { return this->vec.empty(); }
[[nodiscard]] const T* pArray() const { return this->array.data(); }
[[nodiscard]] size_t dataLength() const { return this->size * sizeof(T); }
const T* populateAlloc(void* alloc) const {
auto arraylen = std::min(this->size, N) * sizeof(T);
memcpy(alloc, this->array.data(), arraylen);
if (!this->vec.empty()) {
memcpy(
static_cast<char*>(alloc) + arraylen, // NOLINT
this->vec.data(),
this->vec.size() * sizeof(T)
);
}
return static_cast<T*>(alloc);
}
private:
std::array<T, N> array {};
std::vector<T> vec;
size_t size = 0;
};
// might be incorrectly aligned depending on type
// #define STACKLIST_ALLOCA_VIEW(list) ((list).isContiguous() ? (list).pArray() : (list).populateAlloc(alloca((list).dataLength())))
// NOLINTBEGIN
#define STACKLIST_VLA_VIEW(type, list, var) \
const type* var; \
type var##Data[(list).length()]; \
if ((list).isContiguous()) { \
(var) = (list).pArray(); \
} else { \
(list).populateAlloc(var##Data); \
(var) = var##Data; \
}
// NOLINTEND

View file

@ -7,3 +7,4 @@ endfunction()
qs_test(transformwatcher transformwatcher.cpp) qs_test(transformwatcher transformwatcher.cpp)
qs_test(ringbuffer ringbuf.cpp) qs_test(ringbuffer ringbuf.cpp)
qs_test(scriptmodel scriptmodel.cpp) qs_test(scriptmodel scriptmodel.cpp)
qs_test(stacklist stacklist.cpp)

View file

@ -22,6 +22,7 @@ bool ModelOperation::operator==(const ModelOperation& other) const {
&& other.length == this->length && other.destIndex == this->destIndex; && other.length == this->length && other.destIndex == this->destIndex;
} }
// NOLINTNEXTLINE(misc-use-internal-linkage)
QDebug& operator<<(QDebug& debug, const ModelOperation& op) { QDebug& operator<<(QDebug& debug, const ModelOperation& op) {
auto saver = QDebugStateSaver(debug); auto saver = QDebugStateSaver(debug);
debug.nospace(); debug.nospace();
@ -43,6 +44,7 @@ QDebug& operator<<(QDebug& debug, const ModelOperation& op) {
return debug; return debug;
} }
// NOLINTNEXTLINE(misc-use-internal-linkage)
QDebug& operator<<(QDebug& debug, const QVariantList& list) { QDebug& operator<<(QDebug& debug, const QVariantList& list) {
auto str = QString(); auto str = QString();

View file

@ -0,0 +1,92 @@
#include "stacklist.hpp"
#include <cstddef>
#include <qlist.h>
#include <qtest.h>
#include <qtestcase.h>
#include "../stacklist.hpp"
void TestStackList::push() {
StackList<int, 2> list;
list.push(1);
list.push(2);
QCOMPARE_EQ(list.toList(), QList({1, 2}));
QCOMPARE_EQ(list.length(), 2);
}
void TestStackList::pushAndGrow() {
StackList<int, 2> list;
list.push(1);
list.push(2);
list.push(3);
list.push(4);
QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4}));
QCOMPARE_EQ(list.length(), 4);
}
void TestStackList::copy() {
StackList<int, 2> list;
list.push(1);
list.push(2);
list.push(3);
list.push(4);
QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4}));
QCOMPARE_EQ(list.length(), 4);
auto list2 = list;
QCOMPARE_EQ(list2.toList(), QList({1, 2, 3, 4}));
QCOMPARE_EQ(list2.length(), 4);
QCOMPARE_EQ(list2, list);
}
void TestStackList::viewVla() {
StackList<int, 2> list;
list.push(1);
list.push(2);
QCOMPARE_EQ(list.toList(), QList({1, 2}));
QCOMPARE_EQ(list.length(), 2);
STACKLIST_VLA_VIEW(int, list, listView);
QList<int> ql;
for (size_t i = 0; i != list.length(); ++i) {
ql.push_back(listView[i]); // NOLINT
}
QCOMPARE_EQ(ql, list.toList());
}
void TestStackList::viewVlaGrown() {
StackList<int, 2> list;
list.push(1);
list.push(2);
list.push(3);
list.push(4);
QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4}));
QCOMPARE_EQ(list.length(), 4);
STACKLIST_VLA_VIEW(int, list, listView);
QList<int> ql;
for (size_t i = 0; i != list.length(); ++i) {
ql.push_back(listView[i]); // NOLINT
}
QCOMPARE_EQ(ql, list.toList());
}
QTEST_MAIN(TestStackList);

View file

@ -0,0 +1,15 @@
#pragma once
#include <qobject.h>
#include <qtmetamacros.h>
class TestStackList: public QObject {
Q_OBJECT;
private slots:
static void push();
static void pushAndGrow();
static void copy();
static void viewVla();
static void viewVlaGrown();
};

View file

@ -196,7 +196,7 @@ V* AwfulMap<K, V>::get(const K& key) {
} }
template <typename K, typename V> template <typename K, typename V>
void AwfulMap<K, V>::insert(K key, V value) { void AwfulMap<K, V>::insert(const K& key, V value) {
this->values.push_back(QPair<K, V>(key, value)); this->values.push_back(QPair<K, V>(key, value));
} }

View file

@ -20,7 +20,7 @@ class AwfulMap {
public: public:
[[nodiscard]] bool contains(const K& key) const; [[nodiscard]] bool contains(const K& key) const;
[[nodiscard]] V* get(const K& key); [[nodiscard]] V* get(const K& key);
void insert(K key, V value); // assumes no duplicates void insert(const K& key, V value); // assumes no duplicates
bool remove(const K& key); // returns true if anything was removed bool remove(const K& key); // returns true if anything was removed
QList<QPair<K, V>> values; QList<QPair<K, V>> values;
}; };

View file

@ -22,7 +22,9 @@ using namespace google_breakpad;
namespace qs::crash { namespace qs::crash {
namespace {
Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
}
struct CrashHandlerPrivate { struct CrashHandlerPrivate {
ExceptionHandler* exceptionHandler = nullptr; ExceptionHandler* exceptionHandler = nullptr;

View file

@ -22,49 +22,10 @@
#include "build.hpp" #include "build.hpp"
#include "interface.hpp" #include "interface.hpp"
namespace {
Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg);
void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance);
void qsCheckCrash(int argc, char** argv) {
auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD");
if (fd.isEmpty()) return;
auto app = QApplication(argc, argv);
RelaunchInfo info;
auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
{
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
QFile file;
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
file.seek(0);
auto ds = QDataStream(&file);
ds >> info;
}
LogManager::init(
!info.noColor,
info.timestamp,
info.sparseLogsOnly,
info.defaultLogLevel,
info.logRules
);
auto crashDir = QsPaths::crashDir(info.instance.instanceId);
qCInfo(logCrashReporter) << "Starting crash reporter...";
recordCrashInfo(crashDir, info.instance);
auto gui = CrashReporterGui(crashDir.path(), crashProc);
gui.show();
exit(QApplication::exec()); // NOLINT
}
int tryDup(int fd, const QString& path) { int tryDup(int fd, const QString& path) {
QFile sourceFile; QFile sourceFile;
if (!sourceFile.open(fd, QFile::ReadOnly, QFile::AutoCloseHandle)) { if (!sourceFile.open(fd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
@ -184,3 +145,44 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
qCDebug(logCrashReporter) << "Recorded crash information."; qCDebug(logCrashReporter) << "Recorded crash information.";
} }
} // namespace
void qsCheckCrash(int argc, char** argv) {
auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD");
if (fd.isEmpty()) return;
auto app = QApplication(argc, argv);
RelaunchInfo info;
auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
{
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
QFile file;
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
file.seek(0);
auto ds = QDataStream(&file);
ds >> info;
}
LogManager::init(
!info.noColor,
info.timestamp,
info.sparseLogsOnly,
info.defaultLogLevel,
info.logRules
);
auto crashDir = QsPaths::crashDir(info.instance.instanceId);
qCInfo(logCrashReporter) << "Starting crash reporter...";
recordCrashInfo(crashDir, info.instance);
auto gui = CrashReporterGui(crashDir.path(), crashProc);
gui.show();
exit(QApplication::exec()); // NOLINT
}

View file

@ -14,7 +14,9 @@
namespace qs::dbus { namespace qs::dbus {
namespace {
Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg); Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
}
void tryLaunchService( void tryLaunchService(
QObject* parent, QObject* parent,

View file

@ -59,8 +59,8 @@ QString DBusMenuItem::icon() const {
this->iconName, this->iconName,
this->menu->iconThemePath.value().join(':') this->menu->iconThemePath.value().join(':')
); );
} else if (this->image != nullptr) { } else if (this->image.hasData()) {
return this->image->url(); return this->image.url();
} else return nullptr; } else return nullptr;
} }
@ -113,7 +113,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
auto originalEnabled = this->mEnabled; auto originalEnabled = this->mEnabled;
auto originalVisible = this->visible; auto originalVisible = this->visible;
auto originalIconName = this->iconName; auto originalIconName = this->iconName;
auto* originalImage = this->image; auto imageChanged = false;
auto originalIsSeparator = this->mSeparator; auto originalIsSeparator = this->mSeparator;
auto originalButtonType = this->mButtonType; auto originalButtonType = this->mButtonType;
auto originalToggleState = this->mCheckState; auto originalToggleState = this->mCheckState;
@ -173,12 +173,16 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
if (iconData.canConvert<QByteArray>()) { if (iconData.canConvert<QByteArray>()) {
auto data = iconData.value<QByteArray>(); auto data = iconData.value<QByteArray>();
if (data.isEmpty()) { if (data.isEmpty()) {
this->image = nullptr; imageChanged = this->image.hasData();
} else if (this->image == nullptr || this->image->data != data) { this->image.data.clear();
this->image = new DBusMenuPngImage(data, this); } else if (!this->image.hasData() || this->image.data != data) {
imageChanged = true;
this->image.data = data;
this->image.imageChanged();
} }
} else if (removed.isEmpty() || removed.contains("icon-data")) { } else if (removed.isEmpty() || removed.contains("icon-data")) {
this->image = nullptr; imageChanged = this->image.hasData();
image.data.clear();
} }
auto type = properties.value("type"); auto type = properties.value("type");
@ -239,17 +243,13 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged(); if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged();
if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged(); if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged();
if (this->iconName != originalIconName || this->image != originalImage) { if (this->iconName != originalIconName || imageChanged) {
if (this->image != originalImage) {
delete originalImage;
}
emit this->iconChanged(); emit this->iconChanged();
} }
qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText
<< ", enabled=" << this->mEnabled << ", visible=" << this->visible << ", enabled=" << this->mEnabled << ", visible=" << this->visible
<< ", iconName=" << this->iconName << ", iconData=" << this->image << ", iconName=" << this->iconName << ", iconData=" << &this->image
<< ", separator=" << this->mSeparator << ", separator=" << this->mSeparator
<< ", toggleType=" << this->mButtonType << ", toggleType=" << this->mButtonType
<< ", toggleState=" << this->mCheckState << ", toggleState=" << this->mCheckState
@ -368,11 +368,9 @@ void DBusMenu::updateLayoutRecursive(
auto childrenChanged = false; auto childrenChanged = false;
auto iter = item->mChildren.begin(); auto iter = item->mChildren.begin();
while (iter != item->mChildren.end()) { while (iter != item->mChildren.end()) {
auto existing = std::find_if( auto existing = std::ranges::find_if(layout.children, [&](const DBusMenuLayout& layout) {
layout.children.begin(), return layout.id == *iter;
layout.children.end(), });
[&](const DBusMenuLayout& layout) { return layout.id == *iter; }
);
if (!item->mShowChildren || existing == layout.children.end()) { if (!item->mShowChildren || existing == layout.children.end()) {
qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from" qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from"

View file

@ -30,7 +30,17 @@ namespace qs::dbus::dbusmenu {
using menu::QsMenuEntry; using menu::QsMenuEntry;
class DBusMenu; class DBusMenu;
class DBusMenuPngImage; class DBusMenuItem;
class DBusMenuPngImage: public QsIndexedImageHandle {
public:
explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {}
[[nodiscard]] bool hasData() const { return !data.isEmpty(); }
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
QByteArray data;
};
///! Menu item shared by an external program. ///! Menu item shared by an external program.
/// Menu item shared by an external program via the /// Menu item shared by an external program via the
@ -93,7 +103,7 @@ private:
bool visible = true; bool visible = true;
bool mSeparator = false; bool mSeparator = false;
QString iconName; QString iconName;
DBusMenuPngImage* image = nullptr; DBusMenuPngImage image;
menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None; menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None;
Qt::CheckState mCheckState = Qt::Unchecked; Qt::CheckState mCheckState = Qt::Unchecked;
bool displayChildren = false; bool displayChildren = false;
@ -156,17 +166,6 @@ private:
QDebug operator<<(QDebug debug, DBusMenu* menu); QDebug operator<<(QDebug debug, DBusMenu* menu);
class DBusMenuPngImage: public QsImageHandle {
public:
explicit DBusMenuPngImage(QByteArray data, DBusMenuItem* parent)
: QsImageHandle(QQuickImageProvider::Image, parent)
, data(std::move(data)) {}
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
QByteArray data;
};
class DBusMenuHandle; class DBusMenuHandle;
QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); QDebug operator<<(QDebug debug, const DBusMenuHandle* handle);

View file

@ -190,11 +190,9 @@ void DBusPropertyGroup::updateAllViaGetAll() {
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) { void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) {
for (const auto [name, value]: properties.asKeyValueRange()) { for (const auto [name, value]: properties.asKeyValueRange()) {
auto prop = std::find_if( auto prop = std::ranges::find_if(this->properties, [&name](DBusPropertyCore* prop) {
this->properties.begin(), return prop->nameRef() == name;
this->properties.end(), });
[&name](DBusPropertyCore* prop) { return prop->nameRef() == name; }
);
if (prop == this->properties.end()) { if (prop == this->properties.end()) {
qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for"
@ -312,11 +310,9 @@ void DBusPropertyGroup::onPropertiesChanged(
<< "Received property change set and invalidations for" << this->toString(); << "Received property change set and invalidations for" << this->toString();
for (const auto& name: invalidatedProperties) { for (const auto& name: invalidatedProperties) {
auto prop = std::find_if( auto prop = std::ranges::find_if(this->properties, [&name](DBusPropertyCore* prop) {
this->properties.begin(), return prop->nameRef() == name;
this->properties.end(), });
[&name](DBusPropertyCore* prop) { return prop->nameRef() == name; }
);
if (prop == this->properties.end()) { if (prop == this->properties.end()) {
qCDebug(logDbusProperties) << "Ignoring untracked property invalidation" << name << "for" qCDebug(logDbusProperties) << "Ignoring untracked property invalidation" << name << "for"

View file

@ -36,8 +36,8 @@ template <typename T>
class DBusResult { class DBusResult {
public: public:
explicit DBusResult() = default; explicit DBusResult() = default;
explicit DBusResult(T value): value(std::move(value)) {} DBusResult(T value): value(std::move(value)) {}
explicit DBusResult(QDBusError error): error(std::move(error)) {} DBusResult(QDBusError error): error(std::move(error)) {}
explicit DBusResult(T value, QDBusError error) explicit DBusResult(T value, QDBusError error)
: value(std::move(value)) : value(std::move(value))
, error(std::move(error)) {} , error(std::move(error)) {}
@ -66,7 +66,7 @@ template <typename T>
void asyncReadProperty( void asyncReadProperty(
QDBusAbstractInterface& interface, QDBusAbstractInterface& interface,
const QString& property, const QString& property,
std::function<void(T, QDBusError)> callback const std::function<void(T, QDBusError)>& callback
) { ) {
asyncReadPropertyInternal( asyncReadPropertyInternal(
QMetaType::fromType<T>(), QMetaType::fromType<T>(),

View file

@ -13,10 +13,12 @@
namespace qs::debug { namespace qs::debug {
namespace {
Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg); Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg);
void lintZeroSized(QQuickItem* item); void lintZeroSized(QQuickItem* item);
bool isRenderable(QQuickItem* item); bool isRenderable(QQuickItem* item);
} // namespace
void lintObjectTree(QObject* object) { void lintObjectTree(QObject* object) {
if (!logLint().isWarningEnabled()) return; if (!logLint().isWarningEnabled()) return;
@ -41,6 +43,8 @@ void lintItemTree(QQuickItem* item) {
} }
} }
namespace {
void lintZeroSized(QQuickItem* item) { void lintZeroSized(QQuickItem* item) {
if (!item->isEnabled() || !item->isVisible()) return; if (!item->isEnabled() || !item->isVisible()) return;
if (item->childItems().isEmpty()) return; if (item->childItems().isEmpty()) return;
@ -71,4 +75,6 @@ bool isRenderable(QQuickItem* item) {
return std::ranges::any_of(item->childItems(), [](auto* item) { return isRenderable(item); }); return std::ranges::any_of(item->childItems(), [](auto* item) { return isRenderable(item); });
} }
} // namespace
} // namespace qs::debug } // namespace qs::debug

View file

@ -24,7 +24,9 @@
namespace qs::io { namespace qs::io {
namespace {
Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg); Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg);
}
QString FileViewError::toString(FileViewError::Enum value) { QString FileViewError::toString(FileViewError::Enum value) {
switch (value) { switch (value) {

View file

@ -1,9 +1,12 @@
#include "ipc.hpp" #include "ipc.hpp"
#include <cstring>
#include <utility> #include <utility>
#include <qcolor.h> #include <qcolor.h>
#include <qmetatype.h> #include <qmetatype.h>
#include <qobjectdefs.h> #include <qobjectdefs.h>
#include <qtypes.h>
#include <qvariant.h>
namespace qs::io::ipc { namespace qs::io::ipc {
@ -14,6 +17,12 @@ const BoolIpcType BoolIpcType::INSTANCE {};
const DoubleIpcType DoubleIpcType::INSTANCE {}; const DoubleIpcType DoubleIpcType::INSTANCE {};
const ColorIpcType ColorIpcType::INSTANCE {}; const ColorIpcType ColorIpcType::INSTANCE {};
void* IpcType::copyStorage(const void* data) const {
auto* storage = this->createStorage();
memcpy(storage, data, this->size());
return storage;
}
const IpcType* IpcType::ipcType(const QMetaType& metaType) { const IpcType* IpcType::ipcType(const QMetaType& metaType) {
if (metaType.id() == QMetaType::Void) return &VoidIpcType::INSTANCE; if (metaType.id() == QMetaType::Void) return &VoidIpcType::INSTANCE;
if (metaType.id() == QMetaType::QString) return &StringIpcType::INSTANCE; if (metaType.id() == QMetaType::QString) return &StringIpcType::INSTANCE;
@ -70,12 +79,18 @@ void IpcTypeSlot::replace(void* value) {
this->storage = value; this->storage = value;
} }
void IpcTypeSlot::replace(const QVariant& value) {
this->replace(this->mType->copyStorage(value.constData()));
}
const char* VoidIpcType::name() const { return "void"; } const char* VoidIpcType::name() const { return "void"; }
const char* VoidIpcType::genericArgumentName() const { return "void"; } const char* VoidIpcType::genericArgumentName() const { return "void"; }
qsizetype VoidIpcType::size() const { return 0; }
// string // string
const char* StringIpcType::name() const { return "string"; } const char* StringIpcType::name() const { return "string"; }
const char* StringIpcType::genericArgumentName() const { return "QString"; } const char* StringIpcType::genericArgumentName() const { return "QString"; }
qsizetype StringIpcType::size() const { return sizeof(QString); }
void* StringIpcType::fromString(const QString& string) const { return new QString(string); } void* StringIpcType::fromString(const QString& string) const { return new QString(string); }
QString StringIpcType::toString(void* slot) const { return *static_cast<QString*>(slot); } QString StringIpcType::toString(void* slot) const { return *static_cast<QString*>(slot); }
void* StringIpcType::createStorage() const { return new QString(); } void* StringIpcType::createStorage() const { return new QString(); }
@ -84,6 +99,7 @@ void StringIpcType::destroyStorage(void* slot) const { delete static_cast<QStrin
// int // int
const char* IntIpcType::name() const { return "int"; } const char* IntIpcType::name() const { return "int"; }
const char* IntIpcType::genericArgumentName() const { return "int"; } const char* IntIpcType::genericArgumentName() const { return "int"; }
qsizetype IntIpcType::size() const { return sizeof(int); }
void* IntIpcType::fromString(const QString& string) const { void* IntIpcType::fromString(const QString& string) const {
auto ok = false; auto ok = false;
@ -100,6 +116,7 @@ void IntIpcType::destroyStorage(void* slot) const { delete static_cast<int*>(slo
// bool // bool
const char* BoolIpcType::name() const { return "bool"; } const char* BoolIpcType::name() const { return "bool"; }
const char* BoolIpcType::genericArgumentName() const { return "bool"; } const char* BoolIpcType::genericArgumentName() const { return "bool"; }
qsizetype BoolIpcType::size() const { return sizeof(bool); }
void* BoolIpcType::fromString(const QString& string) const { void* BoolIpcType::fromString(const QString& string) const {
if (string == "true") return new bool(true); if (string == "true") return new bool(true);
@ -121,6 +138,7 @@ void BoolIpcType::destroyStorage(void* slot) const { delete static_cast<bool*>(s
// double // double
const char* DoubleIpcType::name() const { return "real"; } const char* DoubleIpcType::name() const { return "real"; }
const char* DoubleIpcType::genericArgumentName() const { return "double"; } const char* DoubleIpcType::genericArgumentName() const { return "double"; }
qsizetype DoubleIpcType::size() const { return sizeof(double); }
void* DoubleIpcType::fromString(const QString& string) const { void* DoubleIpcType::fromString(const QString& string) const {
auto ok = false; auto ok = false;
@ -139,6 +157,7 @@ void DoubleIpcType::destroyStorage(void* slot) const { delete static_cast<double
// color // color
const char* ColorIpcType::name() const { return "color"; } const char* ColorIpcType::name() const { return "color"; }
const char* ColorIpcType::genericArgumentName() const { return "QColor"; } const char* ColorIpcType::genericArgumentName() const { return "QColor"; }
qsizetype ColorIpcType::size() const { return sizeof(QColor); }
void* ColorIpcType::fromString(const QString& string) const { void* ColorIpcType::fromString(const QString& string) const {
auto color = QColor::fromString(string); auto color = QColor::fromString(string);
@ -167,6 +186,10 @@ QString WireFunctionDefinition::toString() const {
return "function " % this->name % '(' % paramString % "): " % this->returnType; return "function " % this->name % '(' % paramString % "): " % this->returnType;
} }
QString WirePropertyDefinition::toString() const {
return "property " % this->name % ": " % this->type;
}
QString WireTargetDefinition::toString() const { QString WireTargetDefinition::toString() const {
QString accum = "target " % this->name; QString accum = "target " % this->name;
@ -174,6 +197,10 @@ QString WireTargetDefinition::toString() const {
accum += "\n " % func.toString(); accum += "\n " % func.toString();
} }
for (const auto& prop: this->properties) {
accum += "\n " % prop.toString();
}
return accum; return accum;
} }

View file

@ -3,6 +3,7 @@
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qobjectdefs.h> #include <qobjectdefs.h>
#include <qtclasshelpermacros.h> #include <qtclasshelpermacros.h>
#include <qtypes.h>
#include "../ipc/ipc.hpp" #include "../ipc/ipc.hpp"
@ -21,10 +22,12 @@ public:
[[nodiscard]] virtual const char* name() const = 0; [[nodiscard]] virtual const char* name() const = 0;
[[nodiscard]] virtual const char* genericArgumentName() const = 0; [[nodiscard]] virtual const char* genericArgumentName() const = 0;
[[nodiscard]] virtual qsizetype size() const = 0;
[[nodiscard]] virtual void* fromString(const QString& /*string*/) const { return nullptr; } [[nodiscard]] virtual void* fromString(const QString& /*string*/) const { return nullptr; }
[[nodiscard]] virtual QString toString(void* /*slot*/) const { return ""; } [[nodiscard]] virtual QString toString(void* /*slot*/) const { return ""; }
[[nodiscard]] virtual void* createStorage() const { return nullptr; } [[nodiscard]] virtual void* createStorage() const { return nullptr; }
virtual void destroyStorage(void* /*slot*/) const {} virtual void destroyStorage(void* /*slot*/) const {}
void* copyStorage(const void* data) const;
static const IpcType* ipcType(const QMetaType& metaType); static const IpcType* ipcType(const QMetaType& metaType);
}; };
@ -43,6 +46,7 @@ public:
[[nodiscard]] QGenericReturnArgument asGenericReturnArgument(); [[nodiscard]] QGenericReturnArgument asGenericReturnArgument();
void replace(void* value); void replace(void* value);
void replace(const QVariant& value);
private: private:
const IpcType* mType = nullptr; const IpcType* mType = nullptr;
@ -53,6 +57,7 @@ class VoidIpcType: public IpcType {
public: public:
[[nodiscard]] const char* name() const override; [[nodiscard]] const char* name() const override;
[[nodiscard]] const char* genericArgumentName() const override; [[nodiscard]] const char* genericArgumentName() const override;
[[nodiscard]] qsizetype size() const override;
static const VoidIpcType INSTANCE; static const VoidIpcType INSTANCE;
}; };
@ -61,6 +66,7 @@ class StringIpcType: public IpcType {
public: public:
[[nodiscard]] const char* name() const override; [[nodiscard]] const char* name() const override;
[[nodiscard]] const char* genericArgumentName() const override; [[nodiscard]] const char* genericArgumentName() const override;
[[nodiscard]] qsizetype size() const override;
[[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] void* fromString(const QString& string) const override;
[[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] QString toString(void* slot) const override;
[[nodiscard]] void* createStorage() const override; [[nodiscard]] void* createStorage() const override;
@ -73,6 +79,7 @@ class IntIpcType: public IpcType {
public: public:
[[nodiscard]] const char* name() const override; [[nodiscard]] const char* name() const override;
[[nodiscard]] const char* genericArgumentName() const override; [[nodiscard]] const char* genericArgumentName() const override;
[[nodiscard]] qsizetype size() const override;
[[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] void* fromString(const QString& string) const override;
[[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] QString toString(void* slot) const override;
[[nodiscard]] void* createStorage() const override; [[nodiscard]] void* createStorage() const override;
@ -85,6 +92,7 @@ class BoolIpcType: public IpcType {
public: public:
[[nodiscard]] const char* name() const override; [[nodiscard]] const char* name() const override;
[[nodiscard]] const char* genericArgumentName() const override; [[nodiscard]] const char* genericArgumentName() const override;
[[nodiscard]] qsizetype size() const override;
[[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] void* fromString(const QString& string) const override;
[[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] QString toString(void* slot) const override;
[[nodiscard]] void* createStorage() const override; [[nodiscard]] void* createStorage() const override;
@ -97,6 +105,7 @@ class DoubleIpcType: public IpcType {
public: public:
[[nodiscard]] const char* name() const override; [[nodiscard]] const char* name() const override;
[[nodiscard]] const char* genericArgumentName() const override; [[nodiscard]] const char* genericArgumentName() const override;
[[nodiscard]] qsizetype size() const override;
[[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] void* fromString(const QString& string) const override;
[[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] QString toString(void* slot) const override;
[[nodiscard]] void* createStorage() const override; [[nodiscard]] void* createStorage() const override;
@ -109,6 +118,7 @@ class ColorIpcType: public IpcType {
public: public:
[[nodiscard]] const char* name() const override; [[nodiscard]] const char* name() const override;
[[nodiscard]] const char* genericArgumentName() const override; [[nodiscard]] const char* genericArgumentName() const override;
[[nodiscard]] qsizetype size() const override;
[[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] void* fromString(const QString& string) const override;
[[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] QString toString(void* slot) const override;
[[nodiscard]] void* createStorage() const override; [[nodiscard]] void* createStorage() const override;
@ -127,13 +137,23 @@ struct WireFunctionDefinition {
DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments); DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments);
struct WireTargetDefinition { struct WirePropertyDefinition {
QString name; QString name;
QVector<WireFunctionDefinition> functions; QString type;
[[nodiscard]] QString toString() const; [[nodiscard]] QString toString() const;
}; };
DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions); DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type);
struct WireTargetDefinition {
QString name;
QVector<WireFunctionDefinition> functions;
QVector<WirePropertyDefinition> properties;
[[nodiscard]] QString toString() const;
};
DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties);
} // namespace qs::io::ipc } // namespace qs::io::ipc

View file

@ -21,16 +21,17 @@ namespace qs::io::ipc::comm {
struct NoCurrentGeneration: std::monostate {}; struct NoCurrentGeneration: std::monostate {};
struct TargetNotFound: std::monostate {}; struct TargetNotFound: std::monostate {};
struct FunctionNotFound: std::monostate {}; struct EntryNotFound: std::monostate {};
using QueryResponse = std::variant< using QueryResponse = std::variant<
std::monostate, std::monostate,
NoCurrentGeneration, NoCurrentGeneration,
TargetNotFound, TargetNotFound,
FunctionNotFound, EntryNotFound,
QVector<WireTargetDefinition>, QVector<WireTargetDefinition>,
WireTargetDefinition, WireTargetDefinition,
WireFunctionDefinition>; WireFunctionDefinition,
WirePropertyDefinition>;
void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto resp = conn->responseStream<QueryResponse>(); auto resp = conn->responseStream<QueryResponse>();
@ -44,16 +45,24 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto* handler = registry->findHandler(this->target); auto* handler = registry->findHandler(this->target);
if (handler) { if (handler) {
if (this->function.isEmpty()) { if (this->name.isEmpty()) {
resp << handler->wireDef(); resp << handler->wireDef();
} else { } else {
auto* func = handler->findFunction(this->function); auto* func = handler->findFunction(this->name);
if (func) { if (func) {
resp << func->wireDef(); resp << func->wireDef();
} else { return;
resp << FunctionNotFound();
} }
auto* prop = handler->findProperty(this->name);
if (prop) {
resp << prop->wireDef();
return;
}
resp << EntryNotFound();
} }
} else { } else {
resp << TargetNotFound(); resp << TargetNotFound();
@ -64,8 +73,8 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
} }
} }
int queryMetadata(IpcClient* client, const QString& target, const QString& function) { int queryMetadata(IpcClient* client, const QString& target, const QString& name) {
client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .function = function})); client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .name = name}));
QueryResponse slot; QueryResponse slot;
if (!client->waitForResponse(slot)) return -1; if (!client->waitForResponse(slot)) return -1;
@ -82,9 +91,11 @@ int queryMetadata(IpcClient* client, const QString& target, const QString& funct
qCInfo(logBare).noquote() << std::get<WireTargetDefinition>(slot).toString(); qCInfo(logBare).noquote() << std::get<WireTargetDefinition>(slot).toString();
} else if (std::holds_alternative<WireFunctionDefinition>(slot)) { } else if (std::holds_alternative<WireFunctionDefinition>(slot)) {
qCInfo(logBare).noquote() << std::get<WireFunctionDefinition>(slot).toString(); qCInfo(logBare).noquote() << std::get<WireFunctionDefinition>(slot).toString();
} else if (std::holds_alternative<WirePropertyDefinition>(slot)) {
qCInfo(logBare).noquote() << std::get<WirePropertyDefinition>(slot).toString();
} else if (std::holds_alternative<TargetNotFound>(slot)) { } else if (std::holds_alternative<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found."; qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<FunctionNotFound>(slot)) { } else if (std::holds_alternative<EntryNotFound>(slot)) {
qCCritical(logBare) << "Function not found."; qCCritical(logBare) << "Function not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(slot)) { } else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
qCCritical(logBare) << "Not ready to accept queries yet."; qCCritical(logBare) << "Not ready to accept queries yet.";
@ -119,7 +130,7 @@ using StringCallResponse = std::variant<
std::monostate, std::monostate,
NoCurrentGeneration, NoCurrentGeneration,
TargetNotFound, TargetNotFound,
FunctionNotFound, EntryNotFound,
ArgParseFailed, ArgParseFailed,
Completed>; Completed>;
@ -137,7 +148,7 @@ void StringCallCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto* func = handler->findFunction(this->function); auto* func = handler->findFunction(this->function);
if (!func) { if (!func) {
resp << FunctionNotFound(); resp << EntryNotFound();
return; return;
} }
@ -223,7 +234,7 @@ int callFunction(
qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString(); qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString();
} else if (std::holds_alternative<TargetNotFound>(slot)) { } else if (std::holds_alternative<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found."; qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<FunctionNotFound>(slot)) { } else if (std::holds_alternative<EntryNotFound>(slot)) {
qCCritical(logBare) << "Function not found."; qCCritical(logBare) << "Function not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(slot)) { } else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
qCCritical(logBare) << "Not ready to accept queries yet."; qCCritical(logBare) << "Not ready to accept queries yet.";
@ -233,4 +244,74 @@ int callFunction(
return -1; return -1;
} }
struct PropertyValue {
QString value;
};
DEFINE_SIMPLE_DATASTREAM_OPS(PropertyValue, data.value);
using StringPropReadResponse =
std::variant<std::monostate, NoCurrentGeneration, TargetNotFound, EntryNotFound, PropertyValue>;
void StringPropReadCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto resp = conn->responseStream<StringPropReadResponse>();
if (auto* generation = EngineGeneration::currentGeneration()) {
auto* registry = IpcHandlerRegistry::forGeneration(generation);
auto* handler = registry->findHandler(this->target);
if (!handler) {
resp << TargetNotFound();
return;
}
auto* prop = handler->findProperty(this->property);
if (!prop) {
resp << EntryNotFound();
return;
}
auto slot = IpcTypeSlot(prop->type);
prop->read(handler, slot);
resp << PropertyValue {
.value = slot.type()->toString(slot.get()),
};
} else {
conn->respond(StringCallResponse(NoCurrentGeneration()));
}
}
int getProperty(IpcClient* client, const QString& target, const QString& property) {
if (target.isEmpty()) {
qCCritical(logBare) << "Target required to send message.";
return -1;
} else if (property.isEmpty()) {
qCCritical(logBare) << "Property required to send message.";
return -1;
}
client->sendMessage(IpcCommand(StringPropReadCommand {.target = target, .property = property}));
StringPropReadResponse slot;
if (!client->waitForResponse(slot)) return -1;
if (std::holds_alternative<PropertyValue>(slot)) {
auto& result = std::get<PropertyValue>(slot);
QTextStream(stdout) << result.value << Qt::endl;
return 0;
} else if (std::holds_alternative<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<EntryNotFound>(slot)) {
qCCritical(logBare) << "Property not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
qCCritical(logBare) << "Not ready to accept queries yet.";
} else {
qCCritical(logIpc) << "Received invalid IPC response from" << client;
}
return -1;
}
} // namespace qs::io::ipc::comm } // namespace qs::io::ipc::comm

View file

@ -2,6 +2,7 @@
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qflags.h> #include <qflags.h>
#include <qtypes.h>
#include "../ipc/ipc.hpp" #include "../ipc/ipc.hpp"
@ -9,12 +10,12 @@ namespace qs::io::ipc::comm {
struct QueryMetadataCommand { struct QueryMetadataCommand {
QString target; QString target;
QString function; QString name;
void exec(qs::ipc::IpcServerConnection* conn) const; void exec(qs::ipc::IpcServerConnection* conn) const;
}; };
DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.function); DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.name);
struct StringCallCommand { struct StringCallCommand {
QString target; QString target;
@ -27,7 +28,7 @@ struct StringCallCommand {
DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments); DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments);
void handleMsg(qs::ipc::IpcServerConnection* conn); void handleMsg(qs::ipc::IpcServerConnection* conn);
int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& function); int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& name);
int callFunction( int callFunction(
qs::ipc::IpcClient* client, qs::ipc::IpcClient* client,
@ -36,4 +37,15 @@ int callFunction(
const QVector<QString>& arguments const QVector<QString>& arguments
); );
struct StringPropReadCommand {
QString target;
QString property;
void exec(qs::ipc::IpcServerConnection* conn) const;
};
DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property);
int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property);
} // namespace qs::io::ipc::comm } // namespace qs::io::ipc::comm

View file

@ -107,6 +107,32 @@ WireFunctionDefinition IpcFunction::wireDef() const {
return wire; return wire;
} }
bool IpcProperty::resolve(QString& error) {
this->type = IpcType::ipcType(this->property.metaType());
if (!this->type) {
error = QString("Type %1 cannot be used across IPC.").arg(this->property.metaType().name());
return false;
}
return true;
}
void IpcProperty::read(QObject* target, IpcTypeSlot& slot) const {
slot.replace(this->property.read(target));
}
QString IpcProperty::toString() const {
return QString("property ") % this->property.name() % ": " % this->type->name();
}
WirePropertyDefinition IpcProperty::wireDef() const {
WirePropertyDefinition wire;
wire.name = this->property.name();
wire.type = this->type->name();
return wire;
}
IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) {
for (const auto& arg: function.argumentTypes) { for (const auto& arg: function.argumentTypes) {
this->argumentSlots.emplace_back(arg); this->argumentSlots.emplace_back(arg);
@ -153,6 +179,21 @@ void IpcHandler::onPostReload() {
} }
} }
for (auto i = smeta.propertyCount(); i != meta->propertyCount(); i++) {
const auto& property = meta->property(i);
if (!property.isReadable() || !property.hasNotifySignal()) continue;
auto ipcProp = IpcProperty(property);
QString error;
if (!ipcProp.resolve(error)) {
qmlWarning(this).nospace().noquote()
<< "Error parsing property \"" << property.name() << "\": " << error;
} else {
this->propertyMap.insert(property.name(), ipcProp);
}
}
this->complete = true; this->complete = true;
this->updateRegistration(); this->updateRegistration();
@ -270,6 +311,10 @@ WireTargetDefinition IpcHandler::wireDef() const {
wire.functions += func.wireDef(); wire.functions += func.wireDef();
} }
for (const auto& prop: this->propertyMap.values()) {
wire.properties += prop.wireDef();
}
return wire; return wire;
} }
@ -307,6 +352,13 @@ IpcFunction* IpcHandler::findFunction(const QString& name) {
else return &*itr; else return &*itr;
} }
IpcProperty* IpcHandler::findProperty(const QString& name) {
auto itr = this->propertyMap.find(name);
if (itr == this->propertyMap.end()) return nullptr;
else return &*itr;
}
IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) {
return this->handlers.value(target); return this->handlers.value(target);
} }

View file

@ -53,14 +53,28 @@ private:
friend class IpcFunction; friend class IpcFunction;
}; };
class IpcProperty {
public:
explicit IpcProperty(QMetaProperty property): property(property) {}
bool resolve(QString& error);
void read(QObject* target, IpcTypeSlot& slot) const;
[[nodiscard]] QString toString() const;
[[nodiscard]] WirePropertyDefinition wireDef() const;
QMetaProperty property;
const IpcType* type = nullptr;
};
class IpcHandlerRegistry; class IpcHandlerRegistry;
///! Handler for IPC message calls. ///! Handler for IPC message calls.
/// Each IpcHandler is registered into a per-instance map by its unique @@target. /// Each IpcHandler is registered into a per-instance map by its unique @@target.
/// Functions defined on the IpcHandler can be called by `qs msg`. /// Functions and properties defined on the IpcHandler can be accessed via `qs ipc`.
/// ///
/// #### Handler Functions /// #### Handler Functions
/// IPC handler functions can be called by `qs msg` as long as they have at most 10 /// IPC handler functions can be called by `qs ipc call` as long as they have at most 10
/// arguments, and all argument types along with the return type are listed below. /// arguments, and all argument types along with the return type are listed below.
/// ///
/// **Argument and return types must be explicitly specified or they will not /// **Argument and return types must be explicitly specified or they will not
@ -112,9 +126,9 @@ class IpcHandlerRegistry;
/// } /// }
/// } /// }
/// ``` /// ```
/// The list of registered targets can be inspected using `qs msg -s`. /// The list of registered targets can be inspected using `qs ipc show`.
/// ```sh /// ```sh
/// $ qs msg -s /// $ qs ipc show
/// target rect /// target rect
/// function setColor(color: color): void /// function setColor(color: color): void
/// function getColor(): color /// function getColor(): color
@ -124,18 +138,22 @@ class IpcHandlerRegistry;
/// function getRadius(): int /// function getRadius(): int
/// ``` /// ```
/// ///
/// and then invoked using `qs msg`. /// and then invoked using `qs ipc call`.
/// ```sh /// ```sh
/// $ qs msg rect setColor orange /// $ qs ipc call rect setColor orange
/// $ qs msg rect setAngle 40.5 /// $ qs ipc call rect setAngle 40.5
/// $ qs msg rect setRadius 30 /// $ qs ipc call rect setRadius 30
/// $ qs msg rect getColor /// $ qs ipc call rect getColor
/// #ffffa500 /// #ffffa500
/// $ qs msg rect getAngle /// $ qs ipc call rect getAngle
/// 40.5 /// 40.5
/// $ qs msg rect getRadius /// $ qs ipc call rect getRadius
/// 30 /// 30
/// ``` /// ```
///
/// #### Properties
/// Properties of an IpcHanlder can be read using `qs ipc prop get` as long as they are
/// of an IPC compatible type. See the table above for compatible types.
class IpcHandler class IpcHandler
: public QObject : public QObject
, public PostReloadHook { , public PostReloadHook {
@ -162,12 +180,16 @@ public:
QString listMembers(qsizetype indent); QString listMembers(qsizetype indent);
[[nodiscard]] IpcFunction* findFunction(const QString& name); [[nodiscard]] IpcFunction* findFunction(const QString& name);
[[nodiscard]] IpcProperty* findProperty(const QString& name);
[[nodiscard]] WireTargetDefinition wireDef() const; [[nodiscard]] WireTargetDefinition wireDef() const;
signals: signals:
void enabledChanged(); void enabledChanged();
void targetChanged(); void targetChanged();
private slots:
//void handleIpcPropertyChange();
private: private:
void updateRegistration(bool destroying = false); void updateRegistration(bool destroying = false);
@ -183,6 +205,7 @@ private:
bool complete = false; bool complete = false;
QHash<QString, IpcFunction> functionMap; QHash<QString, IpcFunction> functionMap;
QHash<QString, IpcProperty> propertyMap;
friend class IpcHandlerRegistry; friend class IpcHandlerRegistry;
}; };

View file

@ -15,6 +15,7 @@ using IpcCommand = std::variant<
std::monostate, std::monostate,
IpcKillCommand, IpcKillCommand,
qs::io::ipc::comm::QueryMetadataCommand, qs::io::ipc::comm::QueryMetadataCommand,
qs::io::ipc::comm::StringCallCommand>; qs::io::ipc::comm::StringCallCommand,
qs::io::ipc::comm::StringPropReadCommand>;
} // namespace qs::ipc } // namespace qs::ipc

View file

@ -34,16 +34,310 @@ namespace qs::launch {
using qs::ipc::IpcClient; using qs::ipc::IpcClient;
int readLogFile(CommandState& cmd); namespace {
int listInstances(CommandState& cmd);
int killInstances(CommandState& cmd); int locateConfigFile(CommandState& cmd, QString& path) {
int msgInstance(CommandState& cmd); if (!cmd.config.path->isEmpty()) {
int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); path = *cmd.config.path;
int locateConfigFile(CommandState& cmd, QString& path); } else {
auto manifestPath = *cmd.config.manifest;
if (manifestPath.isEmpty()) {
auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
auto path = configDir.filePath("manifest.conf");
if (QFileInfo(path).isFile()) manifestPath = path;
}
if (!manifestPath.isEmpty()) {
auto file = QFile(manifestPath);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine();
if (line.trimmed().startsWith("#")) continue;
if (line.trimmed().isEmpty()) continue;
auto split = line.split('=');
if (split.length() != 2) {
qCritical() << "Manifest line not in expected format 'name = relativepath':" << line;
return -1;
}
if (split[0].trimmed() == *cmd.config.name) {
path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
break;
}
}
if (path.isEmpty()) {
qCCritical(logBare) << "Configuration" << *cmd.config.name
<< "not found when searching manifest" << manifestPath;
return -1;
}
} else {
qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest;
return -1;
}
} else {
auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
if (cmd.config.name->isEmpty()) {
path = configDir.path();
} else {
path = configDir.filePath(*cmd.config.name);
}
}
}
if (QFileInfo(path).isDir()) {
path = QDir(path).filePath("shell.qml");
}
if (!QFileInfo(path).isFile()) {
qCCritical(logBare) << "Could not open config file at" << path;
return -1;
}
path = QFileInfo(path).canonicalFilePath();
return 0;
}
void sortInstances(QVector<InstanceLockInfo>& list, bool newestFirst) {
std::ranges::sort(list, [=](const InstanceLockInfo& a, const InstanceLockInfo& b) {
auto r = a.instance.launchTime < b.instance.launchTime;
return newestFirst ? !r : r;
});
};
int selectInstance(CommandState& cmd, InstanceLockInfo* instance) {
auto* basePath = QsPaths::instance()->baseRunDir();
if (!basePath) return -1;
QString path;
if (cmd.instance.pid != -1) {
path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid));
if (!QsPaths::checkLock(path, instance)) {
qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid;
return -1;
}
} else if (!cmd.instance.id->isEmpty()) {
path = basePath->filePath("by-pid");
auto instances = QsPaths::collectInstances(path);
instances.removeIf([&](const InstanceLockInfo& info) {
return !info.instance.instanceId.startsWith(*cmd.instance.id);
});
if (instances.isEmpty()) {
qCInfo(logBare) << "No running instances start with" << *cmd.instance.id;
return -1;
} else if (instances.length() != 1) {
qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id;
for (auto& instance: instances) {
qCInfo(logBare).noquote() << " -" << instance.instance.instanceId;
}
return -1;
} else {
*instance = instances.value(0);
}
} else {
QString configFilePath;
auto r = locateConfigFile(cmd, configFilePath);
if (r != 0) return r;
auto pathId =
QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
path = QDir(basePath->filePath("by-path")).filePath(pathId);
auto instances = QsPaths::collectInstances(path);
sortInstances(instances, cmd.config.newest);
if (instances.isEmpty()) {
qCInfo(logBare) << "No running instances for" << configFilePath;
return -1;
}
*instance = instances.value(0);
}
return 0;
}
int readLogFile(CommandState& cmd) {
auto path = *cmd.log.file;
if (path.isEmpty()) {
InstanceLockInfo instance;
auto r = selectInstance(cmd, &instance);
if (r != 0) return r;
path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog");
}
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) {
qCCritical(logBare) << "Failed to open log file" << path;
return -1;
}
return qs::log::readEncodedLogs(
&file,
path,
cmd.log.timestamp,
cmd.log.tail,
cmd.log.follow,
*cmd.log.readoutRules
)
? 0
: -1;
}
int listInstances(CommandState& cmd) {
auto* basePath = QsPaths::instance()->baseRunDir();
if (!basePath) return -1; // NOLINT
QString path;
QString configFilePath;
if (cmd.instance.all) {
path = basePath->filePath("by-pid");
} else {
auto r = locateConfigFile(cmd, configFilePath);
if (r != 0) {
qCInfo(logBare) << "Use --all to list all instances.";
return r;
}
auto pathId =
QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
path = QDir(basePath->filePath("by-path")).filePath(pathId);
}
auto instances = QsPaths::collectInstances(path);
if (instances.isEmpty()) {
if (cmd.instance.all) {
qCInfo(logBare) << "No running instances.";
} else {
qCInfo(logBare) << "No running instances for" << configFilePath;
qCInfo(logBare) << "Use --all to list all instances.";
}
} else {
sortInstances(instances, cmd.config.newest);
if (cmd.output.json) {
auto array = QJsonArray();
for (auto& instance: instances) {
auto json = QJsonObject();
json["id"] = instance.instance.instanceId;
json["pid"] = instance.pid;
json["shell_id"] = instance.instance.shellId;
json["config_path"] = instance.instance.configPath;
json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate);
array.push_back(json);
}
auto document = QJsonDocument(array);
QTextStream(stdout) << document.toJson(QJsonDocument::Indented);
} else {
for (auto& instance: instances) {
auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss");
auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime());
auto remSeconds = runSeconds % 60;
auto runMinutes = (runSeconds - remSeconds) / 60;
auto remMinutes = runMinutes % 60;
auto runHours = (runMinutes - remMinutes) / 60;
auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds")
.arg(runHours)
.arg(remMinutes)
.arg(remSeconds);
qCInfo(logBare).noquote().nospace()
<< "Instance " << instance.instance.instanceId << ":\n"
<< " Process ID: " << instance.pid << '\n'
<< " Shell ID: " << instance.instance.shellId << '\n'
<< " Config path: " << instance.instance.configPath << '\n'
<< " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n";
}
}
}
return 0;
}
int killInstances(CommandState& cmd) {
InstanceLockInfo instance;
auto r = selectInstance(cmd, &instance);
if (r != 0) return r;
return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
client.kill();
qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId;
});
}
int ipcCommand(CommandState& cmd) {
InstanceLockInfo instance;
auto r = selectInstance(cmd, &instance);
if (r != 0) return r;
return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
if (*cmd.ipc.show || cmd.ipc.showOld) {
return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name);
} else if (*cmd.ipc.getprop) {
return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name);
} else {
QVector<QString> arguments;
for (auto& arg: cmd.ipc.arguments) {
arguments += *arg;
}
return qs::io::ipc::comm::callFunction(&client, *cmd.ipc.target, *cmd.ipc.name, arguments);
}
return -1;
});
}
int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) {
QString configPath;
auto r = locateConfigFile(cmd, configPath);
if (r != 0) return r;
{
InstanceLockInfo info;
if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) {
qCInfo(logBare) << "An instance of this configuration is already running.";
return 0;
}
}
return launch(
{
.configPath = configPath,
.debugPort = cmd.debug.port,
.waitForDebug = cmd.debug.wait,
},
cmd.exec.argv,
coreApplication
);
}
} // namespace
int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
auto state = CommandState(); auto state = CommandState();
if (auto ret = parseCommand(argc, argv, state); ret != 0) return ret; if (auto ret = parseCommand(argc, argv, state); ret != 65535) return ret;
if (state.misc.checkCompat) { if (state.misc.checkCompat) {
if (strcmp(qVersion(), QT_VERSION_STR) != 0) { if (strcmp(qVersion(), QT_VERSION_STR) != 0) {
@ -126,8 +420,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
return listInstances(state); return listInstances(state);
} else if (*state.subcommand.kill) { } else if (*state.subcommand.kill) {
return killInstances(state); return killInstances(state);
} else if (*state.subcommand.msg) { } else if (*state.subcommand.msg || *state.ipc.ipc) {
return msgInstance(state); return ipcCommand(state);
} else { } else {
if (strcmp(qVersion(), QT_VERSION_STR) != 0) { if (strcmp(qVersion(), QT_VERSION_STR) != 0) {
qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR
@ -142,306 +436,4 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
return 0; return 0;
} }
int locateConfigFile(CommandState& cmd, QString& path) {
if (!cmd.config.path->isEmpty()) {
path = *cmd.config.path;
} else {
auto manifestPath = *cmd.config.manifest;
if (manifestPath.isEmpty()) {
auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
auto path = configDir.filePath("manifest.conf");
if (QFileInfo(path).isFile()) manifestPath = path;
}
if (!manifestPath.isEmpty()) {
auto file = QFile(manifestPath);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine();
if (line.trimmed().startsWith("#")) continue;
if (line.trimmed().isEmpty()) continue;
auto split = line.split('=');
if (split.length() != 2) {
qCritical() << "Manifest line not in expected format 'name = relativepath':" << line;
return -1;
}
if (split[0].trimmed() == *cmd.config.name) {
path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
break;
}
}
if (path.isEmpty()) {
qCCritical(logBare) << "Configuration" << *cmd.config.name
<< "not found when searching manifest" << manifestPath;
return -1;
}
} else {
qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest;
return -1;
}
} else {
auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
if (cmd.config.name->isEmpty()) {
path = configDir.path();
} else {
path = configDir.filePath(*cmd.config.name);
}
}
}
if (QFileInfo(path).isDir()) {
path = QDir(path).filePath("shell.qml");
}
if (!QFileInfo(path).isFile()) {
qCCritical(logBare) << "Could not open config file at" << path;
return -1;
}
path = QFileInfo(path).canonicalFilePath();
return 0;
}
void sortInstances(QVector<InstanceLockInfo>& list) {
std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) {
return a.instance.launchTime < b.instance.launchTime;
});
};
int selectInstance(CommandState& cmd, InstanceLockInfo* instance) {
auto* basePath = QsPaths::instance()->baseRunDir();
if (!basePath) return -1;
QString path;
if (cmd.instance.pid != -1) {
path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid));
if (!QsPaths::checkLock(path, instance)) {
qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid;
return -1;
}
} else if (!cmd.instance.id->isEmpty()) {
path = basePath->filePath("by-pid");
auto instances = QsPaths::collectInstances(path);
auto itr =
std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) {
return !info.instance.instanceId.startsWith(*cmd.instance.id);
});
instances.erase(itr, instances.end());
if (instances.isEmpty()) {
qCInfo(logBare) << "No running instances start with" << *cmd.instance.id;
return -1;
} else if (instances.length() != 1) {
qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id;
for (auto& instance: instances) {
qCInfo(logBare).noquote() << " -" << instance.instance.instanceId;
}
return -1;
} else {
*instance = instances.value(0);
}
} else {
QString configFilePath;
auto r = locateConfigFile(cmd, configFilePath);
if (r != 0) return r;
auto pathId =
QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
path = QDir(basePath->filePath("by-path")).filePath(pathId);
auto instances = QsPaths::collectInstances(path);
sortInstances(instances);
if (instances.isEmpty()) {
qCInfo(logBare) << "No running instances for" << configFilePath;
return -1;
}
*instance = instances.value(0);
}
return 0;
}
int readLogFile(CommandState& cmd) {
auto path = *cmd.log.file;
if (path.isEmpty()) {
InstanceLockInfo instance;
auto r = selectInstance(cmd, &instance);
if (r != 0) return r;
path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog");
}
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) {
qCCritical(logBare) << "Failed to open log file" << path;
return -1;
}
return qs::log::readEncodedLogs(
&file,
path,
cmd.log.timestamp,
cmd.log.tail,
cmd.log.follow,
*cmd.log.readoutRules
)
? 0
: -1;
}
int listInstances(CommandState& cmd) {
auto* basePath = QsPaths::instance()->baseRunDir();
if (!basePath) return -1; // NOLINT
QString path;
QString configFilePath;
if (cmd.instance.all) {
path = basePath->filePath("by-pid");
} else {
auto r = locateConfigFile(cmd, configFilePath);
if (r != 0) {
qCInfo(logBare) << "Use --all to list all instances.";
return r;
}
auto pathId =
QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
path = QDir(basePath->filePath("by-path")).filePath(pathId);
}
auto instances = QsPaths::collectInstances(path);
if (instances.isEmpty()) {
if (cmd.instance.all) {
qCInfo(logBare) << "No running instances.";
} else {
qCInfo(logBare) << "No running instances for" << configFilePath;
qCInfo(logBare) << "Use --all to list all instances.";
}
} else {
sortInstances(instances);
if (cmd.output.json) {
auto array = QJsonArray();
for (auto& instance: instances) {
auto json = QJsonObject();
json["id"] = instance.instance.instanceId;
json["pid"] = instance.pid;
json["shell_id"] = instance.instance.shellId;
json["config_path"] = instance.instance.configPath;
json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate);
array.push_back(json);
}
auto document = QJsonDocument(array);
QTextStream(stdout) << document.toJson(QJsonDocument::Indented);
} else {
for (auto& instance: instances) {
auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss");
auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime());
auto remSeconds = runSeconds % 60;
auto runMinutes = (runSeconds - remSeconds) / 60;
auto remMinutes = runMinutes % 60;
auto runHours = (runMinutes - remMinutes) / 60;
auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds")
.arg(runHours)
.arg(remMinutes)
.arg(remSeconds);
qCInfo(logBare).noquote().nospace()
<< "Instance " << instance.instance.instanceId << ":\n"
<< " Process ID: " << instance.pid << '\n'
<< " Shell ID: " << instance.instance.shellId << '\n'
<< " Config path: " << instance.instance.configPath << '\n'
<< " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n";
}
}
}
return 0;
}
int killInstances(CommandState& cmd) {
InstanceLockInfo instance;
auto r = selectInstance(cmd, &instance);
if (r != 0) return r;
return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
client.kill();
qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId;
});
}
int msgInstance(CommandState& cmd) {
InstanceLockInfo instance;
auto r = selectInstance(cmd, &instance);
if (r != 0) return r;
return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
if (cmd.ipc.info) {
return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function);
} else {
QVector<QString> arguments;
for (auto& arg: cmd.ipc.arguments) {
arguments += *arg;
}
return qs::io::ipc::comm::callFunction(
&client,
*cmd.ipc.target,
*cmd.ipc.function,
arguments
);
}
return -1;
});
}
int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) {
QString configPath;
auto r = locateConfigFile(cmd, configPath);
if (r != 0) return r;
{
InstanceLockInfo info;
if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) {
qCInfo(logBare) << "An instance of this configuration is already running.";
return 0;
}
}
return launch(
{
.configPath = configPath,
.debugPort = cmd.debug.port,
.waitForDebug = cmd.debug.wait,
},
cmd.exec.argv,
coreApplication
);
}
} // namespace qs::launch } // namespace qs::launch

View file

@ -32,6 +32,8 @@
namespace qs::launch { namespace qs::launch {
namespace {
template <typename T> template <typename T>
QString base36Encode(T number) { QString base36Encode(T number) {
const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz";
@ -52,6 +54,8 @@ QString base36Encode(T number) {
return result; return result;
} }
} // namespace
int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) {
auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex();
auto shellId = QString(pathId); auto shellId = QString(pathId);

View file

@ -49,6 +49,7 @@ struct CommandState {
QStringOption path; QStringOption path;
QStringOption manifest; QStringOption manifest;
QStringOption name; QStringOption name;
bool newest = false;
} config; } config;
struct { struct {
@ -67,9 +68,13 @@ struct CommandState {
} output; } output;
struct { struct {
bool info = false; CLI::App* ipc = nullptr;
CLI::App* show = nullptr;
CLI::App* call = nullptr;
CLI::App* getprop = nullptr;
bool showOld = false;
QStringOption target; QStringOption target;
QStringOption function; QStringOption name;
std::vector<QStringOption> arguments; std::vector<QStringOption> arguments;
} ipc; } ipc;

View file

@ -22,36 +22,7 @@
namespace qs::launch { namespace qs::launch {
void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); namespace {
int DAEMON_PIPE = -1; // NOLINT
void exitDaemon(int code) {
if (DAEMON_PIPE == -1) return;
if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) {
qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": "
<< qt_error_string();
}
close(DAEMON_PIPE);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT
qFatal() << "Failed to open /dev/null on stdin";
}
if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT
qFatal() << "Failed to open /dev/null on stdout";
}
if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT
qFatal() << "Failed to open /dev/null on stderr";
}
}
void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
#if CRASH_REPORTER #if CRASH_REPORTER
@ -96,6 +67,37 @@ void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
#endif #endif
} }
} // namespace
int DAEMON_PIPE = -1; // NOLINT
void exitDaemon(int code) {
if (DAEMON_PIPE == -1) return;
if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) {
qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": "
<< qt_error_string();
}
close(DAEMON_PIPE);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT
qFatal() << "Failed to open /dev/null on stdin";
}
if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT
qFatal() << "Failed to open /dev/null on stdout";
}
if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT
qFatal() << "Failed to open /dev/null on stderr";
}
}
int main(int argc, char** argv) { int main(int argc, char** argv) {
QCoreApplication::setApplicationName("quickshell"); QCoreApplication::setApplicationName("quickshell");

View file

@ -16,7 +16,7 @@ int parseCommand(int argc, char** argv, CommandState& state) {
.argv = argv, .argv = argv,
}; };
auto addConfigSelection = [&](CLI::App* cmd) { auto addConfigSelection = [&](CLI::App* cmd, bool withNewestOption = false) {
auto* group = cmd->add_option_group("Config Selection") auto* group = cmd->add_option_group("Config Selection")
->description("If no options in this group are specified,\n" ->description("If no options in this group are specified,\n"
"$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); "$XDG_CONFIG_HOME/quickshell/shell.qml will be used.");
@ -37,6 +37,11 @@ int parseCommand(int argc, char** argv, CommandState& state) {
"otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.")
->envname("QS_CONFIG_NAME"); ->envname("QS_CONFIG_NAME");
if (withNewestOption) {
group->add_flag("-n,--newest", state.config.newest)
->description("Operate on the most recently launched instance instead of the oldest");
}
return group; return group;
}; };
@ -130,7 +135,7 @@ int parseCommand(int argc, char** argv, CommandState& state) {
->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES.");
auto* instance = addInstanceSelection(sub)->excludes(file); auto* instance = addInstanceSelection(sub)->excludes(file);
addConfigSelection(sub)->excludes(instance)->excludes(file); addConfigSelection(sub, true)->excludes(instance)->excludes(file);
addLoggingOptions(sub, false); addLoggingOptions(sub, false);
state.subcommand.log = sub; state.subcommand.log = sub;
@ -146,7 +151,7 @@ int parseCommand(int argc, char** argv, CommandState& state) {
sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); sub->add_flag("-j,--json", state.output.json, "Output the list as a json.");
addConfigSelection(sub)->excludes(all); addConfigSelection(sub, true)->excludes(all);
addLoggingOptions(sub, false, true); addLoggingOptions(sub, false, true);
state.subcommand.list = sub; state.subcommand.list = sub;
@ -156,43 +161,78 @@ int parseCommand(int argc, char** argv, CommandState& state) {
auto* sub = cli->add_subcommand("kill", "Kill quickshell instances."); auto* sub = cli->add_subcommand("kill", "Kill quickshell instances.");
//sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); //sub->add_flag("-a,--all", "Kill all matching instances instead of just one.");
auto* instance = addInstanceSelection(sub); auto* instance = addInstanceSelection(sub);
addConfigSelection(sub)->excludes(instance); addConfigSelection(sub, true)->excludes(instance);
addLoggingOptions(sub, false, true); addLoggingOptions(sub, false, true);
state.subcommand.kill = sub; state.subcommand.kill = sub;
} }
{ {
auto* sub = cli->add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); auto* sub = cli->add_subcommand("ipc", "Communicate with other Quickshell instances.")
->require_subcommand();
auto* target = sub->add_option("target", state.ipc.target, "The target to message."); state.ipc.ipc = sub;
auto* function = sub->add_option("function", state.ipc.function)
->description("The function to call in the target.")
->needs(target);
auto* arguments = sub->add_option("arguments", state.ipc.arguments)
->description("Arguments to the called function.")
->needs(function)
->allow_extra_args();
sub->add_flag("-s,--show", state.ipc.info)
->description("Print information about a function or target if given, or all available "
"targets if not.")
->excludes(arguments);
auto* instance = addInstanceSelection(sub); auto* instance = addInstanceSelection(sub);
addConfigSelection(sub)->excludes(instance); addConfigSelection(sub, true)->excludes(instance);
addLoggingOptions(sub, false, true); addLoggingOptions(sub, false, true);
sub->require_option(); {
auto* show = sub->add_subcommand("show", "Print information about available IPC targets.");
state.ipc.show = show;
}
{
auto* call = sub->add_subcommand("call", "Call an IpcHandler function.");
state.ipc.call = call;
call->add_option("target", state.ipc.target, "The target to message.");
call->add_option("function", state.ipc.name)
->description("The function to call in the target.");
call->add_option("arguments", state.ipc.arguments)
->description("Arguments to the called function.")
->allow_extra_args();
}
{
auto* prop =
sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand();
{
auto* get = prop->add_subcommand("get", "Read the value of a property.");
state.ipc.getprop = get;
get->add_option("target", state.ipc.target, "The target to read the property of.");
get->add_option("property", state.ipc.name)->description("The property to read.");
}
}
}
{
auto* sub = cli->add_subcommand("msg", "[DEPRECATED] Moved to `ipc call`.")->require_option();
sub->add_option("target", state.ipc.target, "The target to message.");
sub->add_option("function", state.ipc.name)->description("The function to call in the target.");
sub->add_option("arguments", state.ipc.arguments)
->description("Arguments to the called function.")
->allow_extra_args();
sub->add_flag("-s,--show", state.ipc.showOld)
->description("Print information about a function or target if given, or all available "
"targets if not.");
auto* instance = addInstanceSelection(sub);
addConfigSelection(sub, true)->excludes(instance);
addLoggingOptions(sub, false, true);
state.subcommand.msg = sub; state.subcommand.msg = sub;
} }
CLI11_PARSE(*cli, argc, argv); CLI11_PARSE(*cli, argc, argv);
return 0; return 65535;
} }
} // namespace qs::launch } // namespace qs::launch

View file

@ -15,7 +15,9 @@
#include "../../core/generation.hpp" #include "../../core/generation.hpp"
namespace {
Q_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd"); Q_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd");
}
QString GreetdState::toString(GreetdState::Enum value) { QString GreetdState::toString(GreetdState::Enum value) {
switch (value) { switch (value) {

View file

@ -21,7 +21,9 @@ using namespace qs::dbus;
namespace qs::service::mpris { namespace qs::service::mpris {
namespace {
Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg);
}
QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) { QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) {
switch (status) { switch (status) {
@ -237,7 +239,7 @@ void MprisPlayer::setPosition(qreal position) {
this->player->Seek(target - pos); this->player->Seek(target - pos);
} }
this->bpPosition = target; this->setPosition(target);
} }
void MprisPlayer::onPositionUpdated() { void MprisPlayer::onPositionUpdated() {
@ -248,11 +250,16 @@ void MprisPlayer::onPositionUpdated() {
if (firstChange) emit this->positionSupportedChanged(); if (firstChange) emit this->positionSupportedChanged();
} }
void MprisPlayer::setPosition(qlonglong position) {
this->bpPosition = position;
this->onPositionUpdated();
}
void MprisPlayer::onExportedPositionChanged() { void MprisPlayer::onExportedPositionChanged() {
if (!this->lengthSupported()) emit this->lengthChanged(); if (!this->lengthSupported()) emit this->lengthChanged();
} }
void MprisPlayer::onSeek(qlonglong time) { this->bpPosition = time; } void MprisPlayer::onSeek(qlonglong time) { this->setPosition(time); }
qreal MprisPlayer::length() const { qreal MprisPlayer::length() const {
if (this->bInternalLength == -1) { if (this->bInternalLength == -1) {

View file

@ -391,6 +391,8 @@ private slots:
private: private:
void onMetadataChanged(); void onMetadataChanged();
void onPositionUpdated(); void onPositionUpdated();
// call instead of setting bpPosition
void setPosition(qlonglong position);
void requestPositionUpdate() { this->pPosition.requestUpdate(); }; void requestPositionUpdate() { this->pPosition.requestUpdate(); };
// clang-format off // clang-format off
@ -457,7 +459,7 @@ private:
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanSeek, bpCanSeek, playerProperties, "CanSeek"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanSeek, bpCanSeek, playerProperties, "CanSeek");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanGoNext, bpCanGoNext, playerProperties, "CanGoNext"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanGoNext, bpCanGoNext, playerProperties, "CanGoNext");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanGoPrevious, bpCanGoPrevious, playerProperties, "CanGoPrevious"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanGoPrevious, bpCanGoPrevious, playerProperties, "CanGoPrevious");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, qlonglong, pPosition, bpPosition, playerProperties, "Position", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, qlonglong, pPosition, bpPosition, onPositionUpdated, playerProperties, "Position", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pVolume, bVolume, playerProperties, "Volume", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pVolume, bVolume, playerProperties, "Volume", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMetadata, bpMetadata, playerProperties, "Metadata"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMetadata, bpMetadata, playerProperties, "Metadata");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pPlaybackStatus, bpPlaybackStatus, playerProperties, "PlaybackStatus"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pPlaybackStatus, bpPlaybackStatus, playerProperties, "PlaybackStatus");
@ -466,8 +468,6 @@ private:
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMinRate, bMinRate, playerProperties, "MinimumRate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMinRate, bMinRate, playerProperties, "MinimumRate", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMaxRate, bMaxRate, playerProperties, "MaximumRate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMaxRate, bMaxRate, playerProperties, "MaximumRate", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pShuffle, bShuffle, playerProperties, "Shuffle", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pShuffle, bShuffle, playerProperties, "Shuffle", false);
QS_BINDING_SUBSCRIBE_METHOD(MprisPlayer, bpPosition, onPositionUpdated, onValueChanged);
// clang-format on // clang-format on
QDateTime lastPositionTimestamp; QDateTime lastPositionTimestamp;

View file

@ -14,7 +14,9 @@
namespace qs::service::mpris { namespace qs::service::mpris {
namespace {
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
}
MprisWatcher::MprisWatcher() { MprisWatcher::MprisWatcher() {
qCDebug(logMprisWatcher) << "Starting MprisWatcher"; qCDebug(logMprisWatcher) << "Starting MprisWatcher";

View file

@ -9,6 +9,7 @@
namespace qs::service::notifications { namespace qs::service::notifications {
// NOLINTNEXTLINE(misc-use-internal-linkage)
Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp
QImage DBusNotificationImage::createImage() const { QImage DBusNotificationImage::createImage() const {

View file

@ -1,7 +1,5 @@
#pragma once #pragma once
#include <utility>
#include <qdbusargument.h> #include <qdbusargument.h>
#include <qimage.h> #include <qimage.h>
#include <qobject.h> #include <qobject.h>
@ -23,14 +21,22 @@ struct DBusNotificationImage {
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap); const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap); const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap);
class NotificationImage: public QsImageHandle { class NotificationImage: public QsIndexedImageHandle {
public: public:
explicit NotificationImage(DBusNotificationImage image, QObject* parent) explicit NotificationImage(): QsIndexedImageHandle(QQuickAsyncImageProvider::Image) {}
: QsImageHandle(QQuickAsyncImageProvider::Image, parent)
, image(std::move(image)) {} [[nodiscard]] bool hasData() const { return !this->image.data.isEmpty(); }
void clear() { this->image.data.clear(); }
[[nodiscard]] DBusNotificationImage& writeImage() {
this->imageChanged();
return this->image;
}
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
private:
DBusNotificationImage image; DBusNotificationImage image;
}; };
} // namespace qs::service::notifications } // namespace qs::service::notifications

View file

@ -1,5 +1,4 @@
#include "notification.hpp" #include "notification.hpp"
#include <utility>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdbusargument.h> #include <qdbusargument.h>
@ -18,6 +17,7 @@
namespace qs::service::notifications { namespace qs::service::notifications {
// NOLINTNEXTLINE(misc-use-internal-linkage)
Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp
QString NotificationUrgency::toString(NotificationUrgency::Enum value) { QString NotificationUrgency::toString(NotificationUrgency::Enum value) {
@ -116,13 +116,12 @@ void Notification::updateProperties(
QString imagePath; QString imagePath;
if (!imageDataName.isEmpty()) { if (imageDataName.isEmpty()) {
this->mImagePixmap.clear();
} else {
auto value = hints.value(imageDataName).value<QDBusArgument>(); auto value = hints.value(imageDataName).value<QDBusArgument>();
DBusNotificationImage image; value >> this->mImagePixmap.writeImage();
value >> image; imagePath = this->mImagePixmap.url();
if (this->mImagePixmap) this->mImagePixmap->deleteLater();
this->mImagePixmap = new NotificationImage(std::move(image), this);
imagePath = this->mImagePixmap->url();
} }
// don't store giant byte arrays longer than necessary // don't store giant byte arrays longer than necessary
@ -130,7 +129,7 @@ void Notification::updateProperties(
hints.remove("image_data"); hints.remove("image_data");
hints.remove("icon_data"); hints.remove("icon_data");
if (!this->mImagePixmap) { if (!this->mImagePixmap.hasData()) {
QString imagePathName; QString imagePathName;
if (hints.contains("image-path")) imagePathName = "image-path"; if (hints.contains("image-path")) imagePathName = "image-path";
else if (hints.contains("image_path")) imagePathName = "image_path"; else if (hints.contains("image_path")) imagePathName = "image_path";

View file

@ -12,11 +12,10 @@
#include "../../core/retainable.hpp" #include "../../core/retainable.hpp"
#include "../../core/util.hpp" #include "../../core/util.hpp"
#include "dbusimage.hpp"
namespace qs::service::notifications { namespace qs::service::notifications {
class NotificationImage;
///! The urgency level of a Notification. ///! The urgency level of a Notification.
/// See @@Notification.urgency. /// See @@Notification.urgency.
class NotificationUrgency: public QObject { class NotificationUrgency: public QObject {
@ -187,7 +186,7 @@ private:
quint32 mId; quint32 mId;
NotificationCloseReason::Enum mCloseReason = NotificationCloseReason::Dismissed; NotificationCloseReason::Enum mCloseReason = NotificationCloseReason::Dismissed;
bool mLastGeneration = false; bool mLastGeneration = false;
NotificationImage* mImagePixmap = nullptr; NotificationImage mImagePixmap;
QList<NotificationAction*> mActions; QList<NotificationAction*> mActions;
// clang-format off // clang-format off

View file

@ -19,6 +19,7 @@
namespace qs::service::notifications { namespace qs::service::notifications {
// NOLINTNEXTLINE(misc-use-internal-linkage)
Q_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications"); Q_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications");
NotificationServer::NotificationServer() { NotificationServer::NotificationServer() {

View file

@ -10,12 +10,28 @@
#include <qobject.h> #include <qobject.h>
#include <qsocketnotifier.h> #include <qsocketnotifier.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h>
#include <spa/utils/defs.h> #include <spa/utils/defs.h>
#include <spa/utils/hook.h> #include <spa/utils/hook.h>
namespace qs::service::pipewire { namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg);
}
const pw_core_events PwCore::EVENTS = {
.version = PW_VERSION_CORE_EVENTS,
.info = nullptr,
.done = &PwCore::onSync,
.ping = nullptr,
.error = nullptr,
.remove_id = nullptr,
.bound_id = nullptr,
.add_mem = nullptr,
.remove_mem = nullptr,
.bound_props = nullptr,
};
PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) { PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) {
qCInfo(logLoop) << "Creating pipewire event loop."; qCInfo(logLoop) << "Creating pipewire event loop.";
@ -40,6 +56,8 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read
return; return;
} }
pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this);
qCInfo(logLoop) << "Linking pipewire event loop."; qCInfo(logLoop) << "Linking pipewire event loop.";
// Tie the pw event loop into qt. // Tie the pw event loop into qt.
auto fd = pw_loop_get_fd(this->loop); auto fd = pw_loop_get_fd(this->loop);
@ -77,6 +95,16 @@ void PwCore::poll() {
emit this->polled(); emit this->polled();
} }
qint32 PwCore::sync(quint32 id) const {
// Seq param doesn't seem to do anything. Seq is instead the returned value.
return pw_core_sync(this->core, id, 0);
}
void PwCore::onSync(void* data, quint32 id, qint32 seq) {
auto* self = static_cast<PwCore*>(data);
emit self->synced(id, seq);
}
SpaHook::SpaHook() { // NOLINT SpaHook::SpaHook() { // NOLINT
spa_zero(this->hook); spa_zero(this->hook);
} }

View file

@ -14,6 +14,14 @@
namespace qs::service::pipewire { namespace qs::service::pipewire {
class SpaHook {
public:
explicit SpaHook();
void remove();
spa_hook hook;
};
class PwCore: public QObject { class PwCore: public QObject {
Q_OBJECT; Q_OBJECT;
@ -23,6 +31,7 @@ public:
Q_DISABLE_COPY_MOVE(PwCore); Q_DISABLE_COPY_MOVE(PwCore);
[[nodiscard]] bool isValid() const; [[nodiscard]] bool isValid() const;
[[nodiscard]] qint32 sync(quint32 id) const;
pw_loop* loop = nullptr; pw_loop* loop = nullptr;
pw_context* context = nullptr; pw_context* context = nullptr;
@ -30,12 +39,18 @@ public:
signals: signals:
void polled(); void polled();
void synced(quint32 id, qint32 seq);
private slots: private slots:
void poll(); void poll();
private: private:
static const pw_core_events EVENTS;
static void onSync(void* data, quint32 id, qint32 seq);
QSocketNotifier notifier; QSocketNotifier notifier;
SpaHook listener;
}; };
template <typename T> template <typename T>
@ -49,12 +64,4 @@ public:
T* object; T* object;
}; };
class SpaHook {
public:
explicit SpaHook();
void remove();
spa_hook hook;
};
} // namespace qs::service::pipewire } // namespace qs::service::pipewire

View file

@ -18,7 +18,9 @@
namespace qs::service::pipewire { namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logDefaults, "quickshell.service.pipewire.defaults", QtWarningMsg); Q_LOGGING_CATEGORY(logDefaults, "quickshell.service.pipewire.defaults", QtWarningMsg);
}
PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) { PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) {
QObject::connect(registry, &PwRegistry::metadataAdded, this, &PwDefaultTracker::onMetadataAdded); QObject::connect(registry, &PwRegistry::metadataAdded, this, &PwDefaultTracker::onMetadataAdded);

View file

@ -23,7 +23,9 @@
namespace qs::service::pipewire { namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logDevice, "quickshell.service.pipewire.device", QtWarningMsg); Q_LOGGING_CATEGORY(logDevice, "quickshell.service.pipewire.device", QtWarningMsg);
}
// https://github.com/PipeWire/wireplumber/blob/895c1c7286e8809fad869059179e53ab39c807e9/modules/module-mixer-api.c#L397 // https://github.com/PipeWire/wireplumber/blob/895c1c7286e8809fad869059179e53ab39c807e9/modules/module-mixer-api.c#L397
// https://github.com/PipeWire/pipewire/blob/48c2e9516585ccc791335bc7baf4af6952ec54a0/src/modules/module-protocol-pulse/pulse-server.c#L2743-L2743 // https://github.com/PipeWire/pipewire/blob/48c2e9516585ccc791335bc7baf4af6952ec54a0/src/modules/module-protocol-pulse/pulse-server.c#L2743-L2743

View file

@ -14,7 +14,9 @@
namespace qs::service::pipewire { namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg); Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg);
}
QString PwLinkState::toString(Enum value) { QString PwLinkState::toString(Enum value) {
return QString(pw_link_state_as_string(static_cast<pw_link_state>(value))); return QString(pw_link_state_as_string(static_cast<pw_link_state>(value)));

View file

@ -15,7 +15,9 @@
namespace qs::service::pipewire { namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg); Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg);
}
void PwMetadata::bindHooks() { void PwMetadata::bindHooks() {
pw_metadata_add_listener(this->proxy(), &this->listener.hook, &PwMetadata::EVENTS, this); pw_metadata_add_listener(this->proxy(), &this->listener.hook, &PwMetadata::EVENTS, this);

View file

@ -24,11 +24,15 @@
#include <spa/utils/keys.h> #include <spa/utils/keys.h>
#include <spa/utils/type.h> #include <spa/utils/type.h>
#include "connection.hpp"
#include "core.hpp"
#include "device.hpp" #include "device.hpp"
namespace qs::service::pipewire { namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg); Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg);
}
QString PwAudioChannel::toString(Enum value) { QString PwAudioChannel::toString(Enum value) {
switch (value) { switch (value) {
@ -90,6 +94,12 @@ void PwNode::bindHooks() {
} }
void PwNode::unbindHooks() { void PwNode::unbindHooks() {
if (this->ready) {
this->ready = false;
emit this->readyChanged();
}
this->syncSeq = 0;
this->listener.remove(); this->listener.remove();
this->routeDevice = -1; this->routeDevice = -1;
this->properties.clear(); this->properties.clear();
@ -199,6 +209,20 @@ void PwNode::onInfo(void* data, const pw_node_info* info) {
if (self->boundData != nullptr) { if (self->boundData != nullptr) {
self->boundData->onInfo(info); self->boundData->onInfo(info);
} }
if (!self->ready && !self->syncSeq) {
auto* core = PwConnection::instance()->registry.core;
QObject::connect(core, &PwCore::synced, self, &PwNode::onCoreSync);
self->syncSeq = core->sync(self->id);
}
}
void PwNode::onCoreSync(quint32 id, qint32 seq) {
if (id != this->id || seq != this->syncSeq) return;
qCInfo(logNode) << "Completed initial sync for" << this;
this->ready = true;
this->syncSeq = 0;
emit this->readyChanged();
} }
void PwNode::onParam( void PwNode::onParam(

View file

@ -172,6 +172,7 @@ public:
PwNodeType type = PwNodeType::Untracked; PwNodeType type = PwNodeType::Untracked;
bool isSink = false; bool isSink = false;
bool isStream = false; bool isStream = false;
bool ready = false;
PwNodeBoundData* boundData = nullptr; PwNodeBoundData* boundData = nullptr;
@ -180,6 +181,10 @@ public:
signals: signals:
void propertiesChanged(); void propertiesChanged();
void readyChanged();
private slots:
void onCoreSync(quint32 id, qint32 seq);
private: private:
static const pw_node_events EVENTS; static const pw_node_events EVENTS;
@ -187,6 +192,7 @@ private:
static void static void
onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param);
qint32 syncSeq = 0;
SpaHook listener; SpaHook listener;
}; };

View file

@ -2,6 +2,7 @@
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qlist.h> #include <qlist.h>
#include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
@ -87,6 +88,16 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
this, this,
&Pipewire::defaultConfiguredAudioSourceChanged &Pipewire::defaultConfiguredAudioSourceChanged
); );
if (!connection->registry.isInitialized()) {
QObject::connect(
&connection->registry,
&PwRegistry::initialized,
this,
&Pipewire::readyChanged,
Qt::SingleShotConnection
);
}
} }
ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; } ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; }
@ -156,6 +167,8 @@ void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) {
PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr); PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr);
} }
bool Pipewire::isReady() { return PwConnection::instance()->registry.isInitialized(); }
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
void PwNodeLinkTracker::setNode(PwNodeIface* node) { void PwNodeLinkTracker::setNode(PwNodeIface* node) {
@ -298,6 +311,7 @@ void PwNodeAudioIface::setVolumes(const QVector<float>& volumes) {
PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) { PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) {
QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged); QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged);
QObject::connect(node, &PwNode::readyChanged, this, &PwNodeIface::readyChanged);
if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) { if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) {
this->audioIface = new PwNodeAudioIface(audioBoundData, this); this->audioIface = new PwNodeAudioIface(audioBoundData, this);
@ -318,6 +332,8 @@ bool PwNodeIface::isSink() const { return this->mNode->isSink; }
bool PwNodeIface::isStream() const { return this->mNode->isStream; } bool PwNodeIface::isStream() const { return this->mNode->isStream; }
bool PwNodeIface::isReady() const { return this->mNode->ready; }
QVariantMap PwNodeIface::properties() const { QVariantMap PwNodeIface::properties() const {
auto map = QVariantMap(); auto map = QVariantMap();
for (auto [k, v]: this->mNode->properties.asKeyValueRange()) { for (auto [k, v]: this->mNode->properties.asKeyValueRange()) {

View file

@ -116,6 +116,13 @@ class Pipewire: public QObject {
/// ///
/// See @@defaultAudioSource for the current default source, regardless of preference. /// See @@defaultAudioSource for the current default source, regardless of preference.
Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged);
/// This property is true if quickshell has completed its initial sync with
/// the pipewire server. If true, nodes, links and sync/source preferences will be
/// in a good state.
///
/// > [!NOTE] You can use the pipewire object before it is ready, but some nodes/links
/// > may be missing, and preference metadata may be null.
Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
// clang-format on // clang-format on
QML_ELEMENT; QML_ELEMENT;
QML_SINGLETON; QML_SINGLETON;
@ -136,6 +143,8 @@ public:
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const; [[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const;
static void setDefaultConfiguredAudioSource(PwNodeIface* node); static void setDefaultConfiguredAudioSource(PwNodeIface* node);
[[nodiscard]] static bool isReady();
signals: signals:
void defaultAudioSinkChanged(); void defaultAudioSinkChanged();
void defaultAudioSourceChanged(); void defaultAudioSourceChanged();
@ -143,6 +152,8 @@ signals:
void defaultConfiguredAudioSinkChanged(); void defaultConfiguredAudioSinkChanged();
void defaultConfiguredAudioSourceChanged(); void defaultConfiguredAudioSourceChanged();
void readyChanged();
private slots: private slots:
void onNodeAdded(PwNode* node); void onNodeAdded(PwNode* node);
void onNodeRemoved(QObject* object); void onNodeRemoved(QObject* object);
@ -294,6 +305,11 @@ class PwNodeIface: public PwObjectIface {
/// The presence or absence of this property can be used to determine if a node /// The presence or absence of this property can be used to determine if a node
/// manages audio, regardless of if it is bound. If non null, the node is an audio node. /// manages audio, regardless of if it is bound. If non null, the node is an audio node.
Q_PROPERTY(qs::service::pipewire::PwNodeAudioIface* audio READ audio CONSTANT); Q_PROPERTY(qs::service::pipewire::PwNodeAudioIface* audio READ audio CONSTANT);
/// True if the node is fully bound and ready to use.
///
/// > [!NOTE] The node may be used before it is fully bound, but some data
/// > may be missing or incorrect.
Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
QML_NAMED_ELEMENT(PwNode); QML_NAMED_ELEMENT(PwNode);
QML_UNCREATABLE("PwNodes cannot be created directly"); QML_UNCREATABLE("PwNodes cannot be created directly");
@ -307,6 +323,7 @@ public:
[[nodiscard]] QString nickname() const; [[nodiscard]] QString nickname() const;
[[nodiscard]] bool isSink() const; [[nodiscard]] bool isSink() const;
[[nodiscard]] bool isStream() const; [[nodiscard]] bool isStream() const;
[[nodiscard]] bool isReady() const;
[[nodiscard]] QVariantMap properties() const; [[nodiscard]] QVariantMap properties() const;
[[nodiscard]] PwNodeAudioIface* audio() const; [[nodiscard]] PwNodeAudioIface* audio() const;
@ -314,6 +331,7 @@ public:
signals: signals:
void propertiesChanged(); void propertiesChanged();
void readyChanged();
private: private:
PwNode* mNode; PwNode* mNode;

View file

@ -126,6 +126,29 @@ void PwRegistry::init(PwCore& core) {
this->core = &core; this->core = &core;
this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0); this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0);
pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this); pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this);
QObject::connect(this->core, &PwCore::synced, this, &PwRegistry::onCoreSync);
qCDebug(logRegistry) << "Registry created. Sending core sync for initial object tracking.";
this->coreSyncSeq = this->core->sync(PW_ID_CORE);
}
void PwRegistry::onCoreSync(quint32 id, qint32 seq) {
if (id != PW_ID_CORE || seq != this->coreSyncSeq) return;
switch (this->initState) {
case InitState::SendingObjects:
qCDebug(logRegistry) << "Initial sync for objects received. Syncing for metadata binding.";
this->coreSyncSeq = this->core->sync(PW_ID_CORE);
this->initState = InitState::Binding;
break;
case InitState::Binding:
qCInfo(logRegistry) << "Initial state sync complete.";
this->initState = InitState::Done;
emit this->initialized();
break;
default: break;
}
} }
const pw_registry_events PwRegistry::EVENTS = { const pw_registry_events PwRegistry::EVENTS = {

View file

@ -116,6 +116,8 @@ class PwRegistry
public: public:
void init(PwCore& core); void init(PwCore& core);
[[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; }
//QHash<quint32, PwClient*> clients; //QHash<quint32, PwClient*> clients;
QHash<quint32, PwMetadata*> metadata; QHash<quint32, PwMetadata*> metadata;
QHash<quint32, PwNode*> nodes; QHash<quint32, PwNode*> nodes;
@ -132,9 +134,11 @@ signals:
void linkAdded(PwLink* link); void linkAdded(PwLink* link);
void linkGroupAdded(PwLinkGroup* group); void linkGroupAdded(PwLinkGroup* group);
void metadataAdded(PwMetadata* metadata); void metadataAdded(PwMetadata* metadata);
void initialized();
private slots: private slots:
void onLinkGroupDestroyed(QObject* object); void onLinkGroupDestroyed(QObject* object);
void onCoreSync(quint32 id, qint32 seq);
private: private:
static const pw_registry_events EVENTS; static const pw_registry_events EVENTS;
@ -152,6 +156,13 @@ private:
void addLinkToGroup(PwLink* link); void addLinkToGroup(PwLink* link);
enum class InitState : quint8 {
SendingObjects,
Binding,
Done
} initState = InitState::SendingObjects;
qint32 coreSyncSeq = 0;
SpaHook listener; SpaHook listener;
}; };

View file

@ -35,7 +35,6 @@ using namespace qs::dbus::dbusmenu;
using namespace qs::menu::platform; using namespace qs::menu::platform;
Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg); Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg);
Q_LOGGING_CATEGORY(logSniMenu, "quickshell.service.sni.item.menu", QtWarningMsg);
namespace qs::service::sni { namespace qs::service::sni {
@ -283,7 +282,7 @@ void StatusNotifierItem::onGetAllFailed() const {
} }
TrayImageHandle::TrayImageHandle(StatusNotifierItem* item) TrayImageHandle::TrayImageHandle(StatusNotifierItem* item)
: QsImageHandle(QQmlImageProviderBase::Pixmap, item) : QsImageHandle(QQmlImageProviderBase::Pixmap)
, item(item) {} , item(item) {}
QPixmap QPixmap

View file

@ -21,6 +21,7 @@ qt_add_dbus_interface(DBUS_INTERFACES
qt_add_library(quickshell-service-upower STATIC qt_add_library(quickshell-service-upower STATIC
core.cpp core.cpp
device.cpp device.cpp
powerprofiles.cpp
${DBUS_INTERFACES} ${DBUS_INTERFACES}
) )

View file

@ -20,7 +20,9 @@
namespace qs::service::upower { namespace qs::service::upower {
namespace {
Q_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg); Q_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg);
}
UPower::UPower() { UPower::UPower() {
qCDebug(logUPower) << "Starting UPower Service"; qCDebug(logUPower) << "Starting UPower Service";

View file

@ -54,6 +54,14 @@ private:
DBusUPowerService* service = nullptr; DBusUPowerService* service = nullptr;
}; };
///! Provides access to the UPower service.
/// An interface to the [UPower daemon], which can be used to
/// view battery and power statistics for your computer and
/// connected devices.
///
/// > [!NOTE] The UPower daemon must be installed to use this service.
///
/// [UPower daemon]: https://upower.freedesktop.org
class UPowerQml: public QObject { class UPowerQml: public QObject {
Q_OBJECT; Q_OBJECT;
QML_NAMED_ELEMENT(UPower); QML_NAMED_ELEMENT(UPower);

View file

@ -15,7 +15,9 @@ using namespace qs::dbus;
namespace qs::service::upower { namespace qs::service::upower {
namespace {
Q_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg); Q_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg);
}
QString UPowerDeviceState::toString(UPowerDeviceState::Enum status) { QString UPowerDeviceState::toString(UPowerDeviceState::Enum status) {
switch (status) { switch (status) {
@ -114,9 +116,7 @@ DBusResult<qreal> DBusDataTransform<PowerPercentage>::fromWire(qreal wire) {
DBusResult<UPowerDeviceState::Enum> DBusResult<UPowerDeviceState::Enum>
DBusDataTransform<UPowerDeviceState::Enum>::fromWire(quint32 wire) { DBusDataTransform<UPowerDeviceState::Enum>::fromWire(quint32 wire) {
if (wire != UPowerDeviceType::Battery && wire >= UPowerDeviceState::Unknown if (wire >= UPowerDeviceState::Unknown && wire <= UPowerDeviceState::PendingDischarge) {
&& wire <= UPowerDeviceState::PendingDischarge)
{
return DBusResult(static_cast<UPowerDeviceState::Enum>(wire)); return DBusResult(static_cast<UPowerDeviceState::Enum>(wire));
} }

View file

@ -154,6 +154,8 @@ class UPowerDevice: public QObject {
Q_PROPERTY(bool isLaptopBattery READ isLaptopBattery NOTIFY isLaptopBatteryChanged BINDABLE bindableIsLaptopBattery); Q_PROPERTY(bool isLaptopBattery READ isLaptopBattery NOTIFY isLaptopBatteryChanged BINDABLE bindableIsLaptopBattery);
/// Native path of the device specific to your OS. /// Native path of the device specific to your OS.
Q_PROPERTY(QString nativePath READ nativePath NOTIFY nativePathChanged BINDABLE bindableNativePath); Q_PROPERTY(QString nativePath READ nativePath NOTIFY nativePathChanged BINDABLE bindableNativePath);
/// Model name of the device. Unlikely to be useful for internal devices.
Q_PROPERTY(QString model READ model NOTIFY modelChanged BINDABLE bindableModel);
/// If device statistics have been queried for this device yet. /// If device statistics have been queried for this device yet.
/// This will be true for all devices returned from @@UPower.devices, but not the default /// This will be true for all devices returned from @@UPower.devices, but not the default
/// device, which may be returned before it is ready to avoid returning null. /// device, which may be returned before it is ready to avoid returning null.
@ -186,6 +188,7 @@ public:
QS_BINDABLE_GETTER(QString, bIconName, iconName, bindableIconName); QS_BINDABLE_GETTER(QString, bIconName, iconName, bindableIconName);
QS_BINDABLE_GETTER(bool, bIsLaptopBattery, isLaptopBattery, bindableIsLaptopBattery); QS_BINDABLE_GETTER(bool, bIsLaptopBattery, isLaptopBattery, bindableIsLaptopBattery);
QS_BINDABLE_GETTER(QString, bNativePath, nativePath, bindableNativePath); QS_BINDABLE_GETTER(QString, bNativePath, nativePath, bindableNativePath);
QS_BINDABLE_GETTER(QString, bModel, model, bindableModel);
QS_BINDABLE_GETTER(bool, bReady, ready, bindableReady); QS_BINDABLE_GETTER(bool, bReady, ready, bindableReady);
signals: signals:
@ -206,6 +209,7 @@ signals:
void iconNameChanged(); void iconNameChanged();
void isLaptopBatteryChanged(); void isLaptopBatteryChanged();
void nativePathChanged(); void nativePathChanged();
void modelChanged();
private slots: private slots:
void onGetAllFinished(); void onGetAllFinished();
@ -227,6 +231,7 @@ private:
Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bIconName, &UPowerDevice::iconNameChanged); Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bIconName, &UPowerDevice::iconNameChanged);
Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, bool, bIsLaptopBattery, &UPowerDevice::isLaptopBatteryChanged); Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, bool, bIsLaptopBattery, &UPowerDevice::isLaptopBatteryChanged);
Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bNativePath, &UPowerDevice::nativePathChanged); Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bNativePath, &UPowerDevice::nativePathChanged);
Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bModel, &UPowerDevice::modelChanged);
Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, bool, bReady, &UPowerDevice::readyChanged); Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, bool, bReady, &UPowerDevice::readyChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(UPowerDevice, deviceProperties); QS_DBUS_BINDABLE_PROPERTY_GROUP(UPowerDevice, deviceProperties);
@ -243,6 +248,7 @@ private:
QS_DBUS_PROPERTY_BINDING(UPowerDevice, pHealthPercentage, bHealthPercentage, deviceProperties, "Capacity"); QS_DBUS_PROPERTY_BINDING(UPowerDevice, pHealthPercentage, bHealthPercentage, deviceProperties, "Capacity");
QS_DBUS_PROPERTY_BINDING(UPowerDevice, pIconName, bIconName, deviceProperties, "IconName"); QS_DBUS_PROPERTY_BINDING(UPowerDevice, pIconName, bIconName, deviceProperties, "IconName");
QS_DBUS_PROPERTY_BINDING(UPowerDevice, pNativePath, bNativePath, deviceProperties, "NativePath"); QS_DBUS_PROPERTY_BINDING(UPowerDevice, pNativePath, bNativePath, deviceProperties, "NativePath");
QS_DBUS_PROPERTY_BINDING(UPowerDevice, pModel, bModel, deviceProperties, "Model");
// clang-format on // clang-format on
DBusUPowerDevice* device = nullptr; DBusUPowerDevice* device = nullptr;

View file

@ -3,5 +3,6 @@ description = "UPower Service"
headers = [ headers = [
"core.hpp", "core.hpp",
"device.hpp", "device.hpp",
"powerprofiles.hpp",
] ]
----- -----

View file

@ -0,0 +1,213 @@
#include "powerprofiles.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbuserror.h>
#include <qdbusinterface.h>
#include <qdbusmetatype.h>
#include <qdebug.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qstringliteral.h>
#include "../../dbus/bus.hpp"
#include "../../dbus/properties.hpp"
namespace qs::service::upower {
namespace {
Q_LOGGING_CATEGORY(logPowerProfiles, "quickshell.service.powerprofiles", QtWarningMsg);
}
QString PowerProfile::toString(PowerProfile::Enum profile) {
switch (profile) {
case PowerProfile::PowerSaver: return QStringLiteral("PowerSaver");
case PowerProfile::Balanced: return QStringLiteral("Balanced");
case PowerProfile::Performance: return QStringLiteral("Performance");
default: return QStringLiteral("Invalid");
}
}
QString PerformanceDegradationReason::toString(PerformanceDegradationReason::Enum reason) {
switch (reason) {
case PerformanceDegradationReason::LapDetected: return QStringLiteral("LapDetected");
case PerformanceDegradationReason::HighTemperature: return QStringLiteral("HighTemperature");
default: return QStringLiteral("Invalid");
}
}
bool PowerProfileHold::operator==(const PowerProfileHold& other) const {
return other.profile == this->profile && other.applicationId == this->applicationId
&& other.reason == this->reason;
}
QDebug& operator<<(QDebug& debug, const PowerProfileHold& hold) {
auto saver = QDebugStateSaver(debug);
debug.nospace();
debug << "PowerProfileHold(profile=" << hold.profile << ", applicationId=" << hold.applicationId
<< ", reason=" << hold.reason << ')';
return debug;
}
PowerProfiles::PowerProfiles() {
qDBusRegisterMetaType<QList<QVariantMap>>();
this->bHasPerformanceProfile.setBinding([this]() {
return this->bProfiles.value().contains(PowerProfile::Performance);
});
qCDebug(logPowerProfiles) << "Starting PowerProfiles Service.";
auto bus = QDBusConnection::systemBus();
if (!bus.isConnected()) {
qCWarning(logPowerProfiles
) << "Could not connect to DBus. PowerProfiles services will not work.";
}
this->service = new QDBusInterface(
"org.freedesktop.UPower.PowerProfiles",
"/org/freedesktop/UPower/PowerProfiles",
"org.freedesktop.UPower.PowerProfiles",
bus,
this
);
if (!this->service->isValid()) {
qCDebug(logPowerProfiles
) << "PowerProfilesDaemon is not currently running, attempting to start it.";
dbus::tryLaunchService(this, bus, "org.freedesktop.UPower.PowerProfiles", [this](bool success) {
if (success) {
qCDebug(logPowerProfiles) << "Successfully launched PowerProfiles service.";
this->init();
} else {
qCWarning(logPowerProfiles)
<< "Could not start PowerProfilesDaemon. The PowerProfiles service will not work.";
}
});
} else {
this->init();
}
}
void PowerProfiles::init() {
this->properties.setInterface(this->service);
this->properties.updateAllViaGetAll();
}
void PowerProfiles::setProfile(PowerProfile::Enum profile) {
if (profile == PowerProfile::Performance && !this->bHasPerformanceProfile) {
qCCritical(logPowerProfiles
) << "Cannot request performance profile as it is not present for this device.";
return;
} else if (profile < PowerProfile::PowerSaver || profile > PowerProfile::Performance) {
qCCritical(logPowerProfiles) << "Tried to request invalid power profile" << profile;
return;
}
this->bProfile = profile;
this->pProfile.write();
}
PowerProfiles* PowerProfiles::instance() {
static auto* instance = new PowerProfiles(); // NOLINT
return instance;
}
PowerProfilesQml::PowerProfilesQml(QObject* parent): QObject(parent) {
auto* instance = PowerProfiles::instance();
this->bProfile.setBinding([instance]() { return instance->bProfile.value(); });
this->bHasPerformanceProfile.setBinding([instance]() {
return instance->bHasPerformanceProfile.value();
});
this->bDegradationReason.setBinding([instance]() { return instance->bDegradationReason.value(); }
);
this->bHolds.setBinding([instance]() { return instance->bHolds.value(); });
}
} // namespace qs::service::upower
namespace qs::dbus {
using namespace qs::service::upower;
DBusResult<PowerProfile::Enum> DBusDataTransform<PowerProfile::Enum>::fromWire(const Wire& wire) {
if (wire == QStringLiteral("power-saver")) {
return PowerProfile::PowerSaver;
} else if (wire == QStringLiteral("balanced")) {
return PowerProfile::Balanced;
} else if (wire == QStringLiteral("performance")) {
return PowerProfile::Performance;
} else {
return QDBusError(QDBusError::InvalidArgs, QString("Invalid PowerProfile: %1").arg(wire));
}
}
QString DBusDataTransform<PowerProfile::Enum>::toWire(Data data) {
switch (data) {
case PowerProfile::PowerSaver: return QStringLiteral("power-saver");
case PowerProfile::Balanced: return QStringLiteral("balanced");
case PowerProfile::Performance: return QStringLiteral("performance");
}
}
DBusResult<QList<PowerProfile::Enum>>
DBusDataTransform<QList<PowerProfile::Enum>>::fromWire(const Wire& wire) {
QList<PowerProfile::Enum> profiles;
for (const auto& entry: wire) {
auto profile =
DBusDataTransform<PowerProfile::Enum>::fromWire(entry.value("Profile").value<QString>());
if (!profile.isValid()) return profile.error;
profiles.append(profile.value);
}
return profiles;
}
DBusResult<PerformanceDegradationReason::Enum>
DBusDataTransform<PerformanceDegradationReason::Enum>::fromWire(const Wire& wire) {
if (wire.isEmpty()) {
return PerformanceDegradationReason::None;
} else if (wire == QStringLiteral("lap-detected")) {
return PerformanceDegradationReason::LapDetected;
} else if (wire == QStringLiteral("high-operating-temperature")) {
return PerformanceDegradationReason::HighTemperature;
} else {
return QDBusError(
QDBusError::InvalidArgs,
QString("Invalid PerformanceDegradationReason: %1").arg(wire)
);
}
}
DBusResult<QList<PowerProfileHold>>
DBusDataTransform<QList<PowerProfileHold>>::fromWire(const Wire& wire) {
QList<PowerProfileHold> holds;
for (const auto& entry: wire) {
auto profile =
DBusDataTransform<PowerProfile::Enum>::fromWire(entry.value("Profile").value<QString>());
if (!profile.isValid()) return profile.error;
auto applicationId = entry.value("ApplicationId").value<QString>();
auto reason = entry.value("Reason").value<QString>();
holds.append(PowerProfileHold(profile.value, applicationId, reason));
}
return holds;
}
} // namespace qs::dbus

View file

@ -0,0 +1,237 @@
#pragma once
#include <utility>
#include <qcontainerfwd.h>
#include <qdbusinterface.h>
#include <qdebug.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
namespace qs::service::upower {
///! Power profile exposed by the PowerProfiles service.
/// See @@PowerProfiles.
class PowerProfile: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// This profile will limit system performance in order to save power.
PowerSaver = 0,
/// This profile is the default, and will attempt to strike a balance
/// between performance and power consumption.
Balanced = 1,
/// This profile will maximize performance at the cost of power consumption.
Performance = 2,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(qs::service::upower::PowerProfile::Enum profile);
};
///! Reason for performance degradation exposed by the PowerProfiles service.
/// See @@PowerProfiles.degradationReason for more information.
class PerformanceDegradationReason: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// Performance has not been degraded in a way power-profiles-daemon can detect.
None = 0,
/// Performance has been reduced due to the computer's lap detection function,
/// which attempts to keep the computer from getting too hot while on your lap.
LapDetected = 1,
/// Performance has been reduced due to high system temperatures.
HighTemperature = 2,
};
Q_ENUM(Enum);
// clang-format off
Q_INVOKABLE static QString toString(qs::service::upower::PerformanceDegradationReason::Enum reason);
// clang-format on
};
class PowerProfileHold;
} // namespace qs::service::upower
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::service::upower::PowerProfile::Enum> {
using Wire = QString;
using Data = qs::service::upower::PowerProfile::Enum;
static DBusResult<Data> fromWire(const Wire& wire);
static Wire toWire(Data data);
};
template <>
struct DBusDataTransform<QList<qs::service::upower::PowerProfile::Enum>> {
using Wire = QList<QVariantMap>;
using Data = QList<qs::service::upower::PowerProfile::Enum>;
static DBusResult<Data> fromWire(const Wire& wire);
};
template <>
struct DBusDataTransform<qs::service::upower::PerformanceDegradationReason::Enum> {
using Wire = QString;
using Data = qs::service::upower::PerformanceDegradationReason::Enum;
static DBusResult<Data> fromWire(const Wire& wire);
};
template <>
struct DBusDataTransform<QList<qs::service::upower::PowerProfileHold>> {
using Wire = QList<QVariantMap>;
using Data = QList<qs::service::upower::PowerProfileHold>;
static DBusResult<Data> fromWire(const Wire& wire);
};
} // namespace qs::dbus
namespace qs::service::upower {
// docgen can't hit gadgets yet
class PowerProfileHold {
Q_GADGET;
QML_VALUE_TYPE(powerProfileHold);
Q_PROPERTY(qs::service::upower::PowerProfile::Enum profile MEMBER profile CONSTANT);
Q_PROPERTY(QString applicationId MEMBER applicationId CONSTANT);
Q_PROPERTY(QString reason MEMBER reason CONSTANT);
public:
explicit PowerProfileHold() = default;
explicit PowerProfileHold(PowerProfile::Enum profile, QString applicationId, QString reason)
: profile(profile)
, applicationId(std::move(applicationId))
, reason(std::move(reason)) {}
PowerProfile::Enum profile = PowerProfile::Balanced;
QString applicationId;
QString reason;
[[nodiscard]] bool operator==(const PowerProfileHold& other) const;
};
QDebug& operator<<(QDebug& debug, const PowerProfileHold& hold);
class PowerProfiles: public QObject {
Q_OBJECT;
public:
void setProfile(PowerProfile::Enum profile);
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(PowerProfiles, PowerProfile::Enum, bProfile, PowerProfile::Balanced);
Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, bool, bHasPerformanceProfile);
Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, PerformanceDegradationReason::Enum, bDegradationReason);
Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, QList<PowerProfileHold>, bHolds);
// clang-format on
static PowerProfiles* instance();
private:
explicit PowerProfiles();
void init();
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, QList<PowerProfile::Enum>, bProfiles);
QS_DBUS_BINDABLE_PROPERTY_GROUP(PowerProfiles, properties);
QS_DBUS_PROPERTY_BINDING(PowerProfiles, pProfile, bProfile, properties, "ActiveProfile");
QS_DBUS_PROPERTY_BINDING(PowerProfiles, pProfiles, bProfiles, properties, "Profiles");
QS_DBUS_PROPERTY_BINDING(PowerProfiles, pPerformanceDegraded, bDegradationReason, properties, "PerformanceDegraded");
QS_DBUS_PROPERTY_BINDING(PowerProfiles, pHolds, bHolds, properties, "ActiveProfileHolds");
// clang-format on
QDBusInterface* service = nullptr;
};
///! Provides access to the Power Profiles service.
/// An interface to the UPower [power profiles daemon], which can be
/// used to view and manage power profiles.
///
/// > [!NOTE] The power profiles daemon must be installed to use this service.
/// > Installing UPower does not necessarily install the power profiles daemon.
///
/// [power profiles daemon]: https://gitlab.freedesktop.org/upower/power-profiles-daemon
class PowerProfilesQml: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(PowerProfiles);
QML_SINGLETON;
// clang-format off
/// The current power profile.
///
/// This property may be set to change the system's power profile, however
/// it cannot be set to `Performance` unless @@hasPerformanceProfile is true.
Q_PROPERTY(qs::service::upower::PowerProfile::Enum profile READ default WRITE setProfile NOTIFY profileChanged BINDABLE bindableProfile);
/// If the system has a performance profile.
///
/// If this property is false, your system does not have a performance
/// profile known to power-profiles-daemon.
Q_PROPERTY(bool hasPerformanceProfile READ default NOTIFY hasPerformanceProfileChanged BINDABLE bindableHasPerformanceProfile);
/// If power-profiles-daemon detects degraded system performance, the reason
/// for the degradation will be present here.
Q_PROPERTY(qs::service::upower::PerformanceDegradationReason::Enum degradationReason READ default NOTIFY degradationReasonChanged BINDABLE bindableDegradationReason);
/// Power profile holds created by other applications.
///
/// This property returns a `powerProfileHold` object, which has the following properties.
/// - `profile` - The @@PowerProfile held by the application.
/// - `applicationId` - A string identifying the application
/// - `reason` - The reason the application has given for holding the profile.
///
/// Applications may "hold" a power profile in place for their lifetime, such
/// as a game holding Performance mode or a system daemon holding Power Saver mode
/// when reaching a battery threshold. If the user selects a different profile explicitly
/// (e.g. by setting @@profile$) all holds will be removed.
///
/// Multiple applications may hold a power profile, however if multiple applications request
/// profiles than `PowerSaver` will win over `Performance`. Only `Performance` and `PowerSaver`
/// profiles may be held.
Q_PROPERTY(QList<qs::service::upower::PowerProfileHold> holds READ default NOTIFY holdsChanged BINDABLE bindableHolds);
// clang-format on
signals:
void profileChanged();
void hasPerformanceProfileChanged();
void degradationReasonChanged();
void holdsChanged();
public:
explicit PowerProfilesQml(QObject* parent = nullptr);
[[nodiscard]] QBindable<PowerProfile::Enum> bindableProfile() const { return &this->bProfile; }
static void setProfile(PowerProfile::Enum profile) {
PowerProfiles::instance()->setProfile(profile);
}
[[nodiscard]] QBindable<bool> bindableHasPerformanceProfile() const {
return &this->bHasPerformanceProfile;
}
[[nodiscard]] QBindable<PerformanceDegradationReason::Enum> bindableDegradationReason() const {
return &this->bDegradationReason;
}
[[nodiscard]] QBindable<QList<PowerProfileHold>> bindableHolds() const { return &this->bHolds; }
private:
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, PowerProfile::Enum, bProfile, &PowerProfilesQml::profileChanged);
Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, bool, bHasPerformanceProfile, &PowerProfilesQml::hasPerformanceProfileChanged);
Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, PerformanceDegradationReason::Enum, bDegradationReason, &PowerProfilesQml::degradationReasonChanged);
Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, QList<PowerProfileHold>, bHolds, &PowerProfilesQml::holdsChanged);
// clang-format on
};
} // namespace qs::service::upower

View file

@ -28,7 +28,7 @@ qs_add_pchset(wayland-protocol
<qstring.h> <qstring.h>
) )
function (wl_proto target name path) function (wl_proto target name dir)
set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto/${name}) set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto/${name})
make_directory(${PROTO_BUILD_PATH}) make_directory(${PROTO_BUILD_PATH})
@ -36,39 +36,38 @@ function (wl_proto target name path)
set(WS_CLIENT_CODE "${PROTO_BUILD_PATH}/wayland-${name}.c") set(WS_CLIENT_CODE "${PROTO_BUILD_PATH}/wayland-${name}.c")
set(QWS_CLIENT_HEADER "${PROTO_BUILD_PATH}/qwayland-${name}.h") set(QWS_CLIENT_HEADER "${PROTO_BUILD_PATH}/qwayland-${name}.h")
set(QWS_CLIENT_CODE "${PROTO_BUILD_PATH}/qwayland-${name}.cpp") set(QWS_CLIENT_CODE "${PROTO_BUILD_PATH}/qwayland-${name}.cpp")
set(PATH "${dir}/${name}.xml")
add_custom_command( add_custom_command(
OUTPUT "${WS_CLIENT_HEADER}" OUTPUT "${WS_CLIENT_HEADER}"
COMMAND Wayland::Scanner client-header "${path}" "${WS_CLIENT_HEADER}" COMMAND Wayland::Scanner client-header "${PATH}" "${WS_CLIENT_HEADER}"
DEPENDS Wayland::Scanner "${path}" DEPENDS Wayland::Scanner "${PATH}"
) )
add_custom_command( add_custom_command(
OUTPUT "${WS_CLIENT_CODE}" OUTPUT "${WS_CLIENT_CODE}"
COMMAND Wayland::Scanner private-code "${path}" "${WS_CLIENT_CODE}" COMMAND Wayland::Scanner private-code "${PATH}" "${WS_CLIENT_CODE}"
DEPENDS Wayland::Scanner "${path}" DEPENDS Wayland::Scanner "${PATH}"
) )
add_custom_command( add_custom_command(
OUTPUT "${QWS_CLIENT_HEADER}" OUTPUT "${QWS_CLIENT_HEADER}"
COMMAND Qt6::qtwaylandscanner client-header "${path}" > "${QWS_CLIENT_HEADER}" COMMAND Qt6::qtwaylandscanner client-header "${PATH}" > "${QWS_CLIENT_HEADER}"
DEPENDS Qt6::qtwaylandscanner "${path}" DEPENDS Qt6::qtwaylandscanner "${PATH}"
) )
add_custom_command( add_custom_command(
OUTPUT "${QWS_CLIENT_CODE}" OUTPUT "${QWS_CLIENT_CODE}"
COMMAND Qt6::qtwaylandscanner client-code "${path}" > "${QWS_CLIENT_CODE}" COMMAND Qt6::qtwaylandscanner client-code "${PATH}" > "${QWS_CLIENT_CODE}"
DEPENDS Qt6::qtwaylandscanner "${path}" DEPENDS Qt6::qtwaylandscanner "${PATH}"
) )
add_library(wl-proto-${name}-wl STATIC ${WS_CLIENT_HEADER} ${WS_CLIENT_CODE}) add_library(wl-proto-${name}-wl STATIC ${WS_CLIENT_HEADER} ${WS_CLIENT_CODE})
add_library(wl-proto-${name} STATIC ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE}) add_library(${target} STATIC ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE})
target_include_directories(wl-proto-${name} INTERFACE ${PROTO_BUILD_PATH}) target_include_directories(${target} INTERFACE ${PROTO_BUILD_PATH})
target_link_libraries(wl-proto-${name} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate) target_link_libraries(${target} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate)
qs_pch(wl-proto-${name} SET wayland-protocol) qs_pch(${target} SET wayland-protocol)
target_link_libraries(${target} PRIVATE wl-proto-${name})
endfunction() endfunction()
# ----- # -----
@ -104,6 +103,13 @@ if (WAYLAND_TOPLEVEL_MANAGEMENT)
list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement)
endif() endif()
if (SCREENCOPY)
add_subdirectory(buffer)
add_subdirectory(screencopy)
list(APPEND WAYLAND_MODULES Quickshell.Wayland._Screencopy)
endif()
if (HYPRLAND) if (HYPRLAND)
add_subdirectory(hyprland) add_subdirectory(hyprland)
endif() endif()

View file

@ -0,0 +1,18 @@
find_package(PkgConfig REQUIRED)
pkg_check_modules(dmabuf-deps REQUIRED IMPORTED_TARGET libdrm gbm egl)
qt_add_library(quickshell-wayland-buffer STATIC
manager.cpp
dmabuf.cpp
shm.cpp
)
wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf")
target_link_libraries(quickshell-wayland-buffer PRIVATE
Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
PkgConfig::dmabuf-deps
wlp-linux-dmabuf
)
qs_pch(quickshell-wayland-buffer SET large)

View file

@ -0,0 +1,609 @@
#include "dmabuf.hpp"
#include <algorithm>
#include <array>
#include <csignal>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/gl.h>
#include <GLES3/gl32.h>
#include <fcntl.h>
#include <gbm.h>
#include <libdrm/drm_fourcc.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qopenglcontext.h>
#include <qopenglcontext_platform.h>
#include <qpair.h>
#include <qquickwindow.h>
#include <qscopedpointer.h>
#include <qsgtexture_platform.h>
#include <qwayland-linux-dmabuf-v1.h>
#include <qwaylandclientextension.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <wayland-client-protocol.h>
#include <wayland-linux-dmabuf-v1-client-protocol.h>
#include <wayland-util.h>
#include <xf86drm.h>
#include "../../core/stacklist.hpp"
#include "manager.hpp"
#include "manager_p.hpp"
namespace qs::wayland::buffer::dmabuf {
namespace {
Q_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg);
LinuxDmabufManager* MANAGER = nullptr; // NOLINT
} // namespace
QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) {
debug << fourcc.cStr();
return debug;
}
QDebug& operator<<(QDebug& debug, const FourCCModStr& fourcc) {
debug << fourcc.cStr();
return debug;
}
QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) {
auto saver = QDebugStateSaver(debug);
debug.nospace();
if (buffer) {
debug << "WlDmaBuffer(" << static_cast<const void*>(buffer) << ", size=" << buffer->width << 'x'
<< buffer->height << ", format=" << FourCCStr(buffer->format) << ", modifier=`"
<< FourCCModStr(buffer->modifier) << "`)";
} else {
debug << "WlDmaBuffer(0x0)";
}
return debug;
}
GbmDeviceHandle::~GbmDeviceHandle() {
if (device) {
MANAGER->unrefGbmDevice(this->device);
}
}
// This will definitely backfire later
void LinuxDmabufFormatSelection::ensureSorted() {
if (this->sorted) return;
auto beginIter = this->formats.begin();
auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) {
return format.first == DRM_FORMAT_XRGB8888;
});
if (xrgbIter != this->formats.end()) {
std::swap(*beginIter, *xrgbIter);
++beginIter;
}
auto argbIter = std::ranges::find_if(this->formats, [](const auto& format) {
return format.first == DRM_FORMAT_ARGB8888;
});
if (argbIter != this->formats.end()) std::swap(*beginIter, *argbIter);
this->sorted = true;
}
LinuxDmabufFeedback::LinuxDmabufFeedback(::zwp_linux_dmabuf_feedback_v1* feedback)
: zwp_linux_dmabuf_feedback_v1(feedback) {}
LinuxDmabufFeedback::~LinuxDmabufFeedback() { this->destroy(); }
void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_format_table(int32_t fd, uint32_t size) {
this->formatTableSize = size;
this->formatTable = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (this->formatTable == MAP_FAILED) {
this->formatTable = nullptr;
qCFatal(logDmabuf) << "Failed to mmap format table.";
}
qCDebug(logDmabuf) << "Got format table";
}
void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_main_device(wl_array* device) {
if (device->size != sizeof(dev_t)) {
qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. "
"Try recompiling both.";
}
this->mainDevice = *reinterpret_cast<dev_t*>(device->data);
qCDebug(logDmabuf) << "Got main device id" << this->mainDevice;
}
void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_target_device(wl_array* device) {
if (device->size != sizeof(dev_t)) {
qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. "
"Try recompiling both.";
}
auto& tranche = this->tranches.emplaceBack();
tranche.device = *reinterpret_cast<dev_t*>(device->data);
qCDebug(logDmabuf) << "Got target device id" << this->mainDevice;
}
void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_flags(uint32_t flags) {
this->tranches.back().flags = flags;
qCDebug(logDmabuf) << "Got target device flags" << flags;
}
void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* indices) {
struct FormatTableEntry {
uint32_t format;
uint32_t padding;
uint64_t modifier;
};
static_assert(sizeof(FormatTableEntry) == 16, "Format table entry was not packed to 16 bytes.");
if (this->formatTable == nullptr) {
qCFatal(logDmabuf) << "Received tranche formats before format table.";
}
auto& tranche = this->tranches.back();
auto* table = reinterpret_cast<FormatTableEntry*>(this->formatTable);
auto* indexTable = reinterpret_cast<uint16_t*>(indices->data);
auto indexTableLength = indices->size / sizeof(uint16_t);
uint32_t lastFormat = 0;
LinuxDmabufModifiers* modifiers = nullptr;
for (uint16_t ti = 0; ti != indexTableLength; ++ti) {
auto i = indexTable[ti]; // NOLINT
const auto& entry = table[i]; // NOLINT
// Compositors usually send a single format's modifiers as a block.
if (!modifiers || entry.format != lastFormat) {
lastFormat = entry.format;
auto modifiersIter = std::ranges::find_if(tranche.formats.formats, [&](const auto& pair) {
return pair.first == entry.format;
});
if (modifiersIter == tranche.formats.formats.end()) {
tranche.formats.formats.push(qMakePair(entry.format, LinuxDmabufModifiers()));
modifiers = &(--tranche.formats.formats.end())->second;
} else {
modifiers = &modifiersIter->second;
}
}
if (entry.modifier == DRM_FORMAT_MOD_INVALID) {
modifiers->implicit = true;
} else {
modifiers->modifiers.push(entry.modifier);
}
}
}
void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_done() {
qCDebug(logDmabuf) << "Got tranche end.";
}
void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_done() {
qCDebug(logDmabuf) << "Got feedback done.";
if (this->formatTable) {
munmap(this->formatTable, this->formatTableSize);
this->formatTable = nullptr;
}
if (logDmabuf().isDebugEnabled()) {
qCDebug(logDmabuf) << "Dmabuf tranches:";
for (auto& tranche: this->tranches) {
qCDebug(logDmabuf) << " Tranche on device" << tranche.device;
// will be sorted on first use otherwise
tranche.formats.ensureSorted();
for (auto& [format, modifiers]: tranche.formats.formats) {
qCDebug(logDmabuf) << " Format" << FourCCStr(format);
if (modifiers.implicit) {
qCDebug(logDmabuf) << " Implicit Modifier";
}
for (const auto& modifier: modifiers.modifiers) {
qCDebug(logDmabuf) << " Explicit Modifier" << FourCCModStr(modifier);
}
}
}
}
// Copy tranches to the manager. If the compositor ever updates
// our tranches, we'll start from a clean slate.
MANAGER->tranches = this->tranches;
this->tranches.clear();
MANAGER->feedbackDone();
}
LinuxDmabufManager::LinuxDmabufManager(WlBufferManagerPrivate* manager)
: QWaylandClientExtensionTemplate(5)
, manager(manager) {
MANAGER = this;
this->initialize();
if (this->isActive()) {
qCDebug(logDmabuf) << "Requesting default dmabuf feedback...";
new LinuxDmabufFeedback(this->get_default_feedback());
}
}
void LinuxDmabufManager::feedbackDone() { this->manager->dmabufReady(); }
GbmDeviceHandle LinuxDmabufManager::getGbmDevice(dev_t handle) {
struct DrmFree {
static void cleanup(drmDevice* d) { drmFreeDevice(&d); }
};
std::string renderNodeStorage;
std::string* renderNode = nullptr;
auto sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) {
return d.handle == handle;
});
if (sharedDevice != this->gbmDevices.end()) {
renderNode = &sharedDevice->renderNode;
} else {
drmDevice* drmDevPtr = nullptr;
if (auto error = drmGetDeviceFromDevId(handle, 0, &drmDevPtr); error != 0) {
qCWarning(logDmabuf) << "Failed to get drm device information from handle:"
<< qt_error_string(error);
return nullptr;
}
auto drmDev = QScopedPointer<drmDevice, DrmFree>(drmDevPtr);
if (!(drmDev->available_nodes & (1 << DRM_NODE_RENDER))) {
qCDebug(logDmabuf) << "Cannot create GBM device: DRM device does not have render node.";
return nullptr;
}
renderNodeStorage = drmDev->nodes[DRM_NODE_RENDER]; // NOLINT
renderNode = &renderNodeStorage;
sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) {
return d.renderNode == renderNodeStorage;
});
}
if (sharedDevice != this->gbmDevices.end()) {
qCDebug(logDmabuf) << "Used existing GBM device on render node" << *renderNode;
++sharedDevice->refcount;
return sharedDevice->device;
} else {
auto fd = open(renderNode->c_str(), O_RDWR | O_CLOEXEC);
if (fd < 0) {
qCDebug(logDmabuf) << "Could not open render node" << *renderNode << ":"
<< qt_error_string(fd);
return nullptr;
}
auto* device = gbm_create_device(fd);
if (!device) {
qCDebug(logDmabuf) << "Failed to create GBM device from render node" << *renderNode;
close(fd);
return nullptr;
}
qCDebug(logDmabuf) << "Created GBM device on render node" << *renderNode;
this->gbmDevices.push_back({
.handle = handle,
.renderNode = std::move(renderNodeStorage),
.device = device,
.refcount = 1,
});
return device;
}
}
void LinuxDmabufManager::unrefGbmDevice(gbm_device* device) {
auto iter = std::ranges::find_if(this->gbmDevices, [device](const SharedGbmDevice& d) {
return d.device == device;
});
if (iter == this->gbmDevices.end()) return;
qCDebug(logDmabuf) << "Lost reference to GBM device" << device;
if (--iter->refcount == 0) {
auto fd = gbm_device_get_fd(iter->device);
gbm_device_destroy(iter->device);
close(fd);
this->gbmDevices.erase(iter);
qCDebug(logDmabuf) << "Destroyed GBM device" << device;
}
}
GbmDeviceHandle LinuxDmabufManager::dupHandle(const GbmDeviceHandle& handle) {
if (!handle) return GbmDeviceHandle();
auto iter = std::ranges::find_if(this->gbmDevices, [&handle](const SharedGbmDevice& d) {
return d.device == *handle;
});
if (iter == this->gbmDevices.end()) return GbmDeviceHandle();
qCDebug(logDmabuf) << "Duplicated GBM device handle" << *handle;
++iter->refcount;
return GbmDeviceHandle(*handle);
}
WlBuffer* LinuxDmabufManager::createDmabuf(const WlBufferRequest& request) {
for (auto& tranche: this->tranches) {
if (request.dmabuf.device != 0 && tranche.device != request.dmabuf.device) {
continue;
}
LinuxDmabufFormatSelection formats;
for (const auto& format: request.dmabuf.formats) {
if (!format.modifiers.isEmpty()) {
formats.formats.push(
qMakePair(format.format, LinuxDmabufModifiers {.modifiers = format.modifiers})
);
} else {
for (const auto& trancheFormat: tranche.formats.formats) {
if (trancheFormat.first == format.format) {
formats.formats.push(trancheFormat);
}
}
}
}
if (formats.formats.isEmpty()) continue;
formats.ensureSorted();
auto gbmDevice = this->getGbmDevice(tranche.device);
if (!gbmDevice) {
qCWarning(logDmabuf) << "Hit unusable tranche device while trying to create dmabuf.";
continue;
}
for (const auto& [format, modifiers]: formats.formats) {
if (auto* buf =
this->createDmabuf(gbmDevice, format, modifiers, request.width, request.height))
{
return buf;
}
}
}
qCWarning(logDmabuf) << "Unable to create dmabuf for request: No matching formats.";
return nullptr;
}
WlBuffer* LinuxDmabufManager::createDmabuf(
GbmDeviceHandle& device,
uint32_t format,
const LinuxDmabufModifiers& modifiers,
uint32_t width,
uint32_t height
) {
auto buffer = std::unique_ptr<WlDmaBuffer>(new WlDmaBuffer());
auto& bo = buffer->bo;
const uint32_t flags = GBM_BO_USE_RENDERING;
if (modifiers.modifiers.isEmpty()) {
if (!modifiers.implicit) {
qCritical(logDmabuf
) << "Failed to create gbm_bo: format supports no implicit OR explicit modifiers.";
return nullptr;
}
qCDebug(logDmabuf) << "Creating gbm_bo without modifiers...";
bo = gbm_bo_create(*device, width, height, format, flags);
} else {
qCDebug(logDmabuf) << "Creating gbm_bo with modifiers...";
STACKLIST_VLA_VIEW(uint64_t, modifiers.modifiers, modifiersData);
bo = gbm_bo_create_with_modifiers2(
*device,
width,
height,
format,
modifiersData,
modifiers.modifiers.length(),
flags
);
}
if (!bo) {
qCritical(logDmabuf) << "Failed to create gbm_bo.";
return nullptr;
}
buffer->planeCount = gbm_bo_get_plane_count(bo);
buffer->planes = new WlDmaBuffer::Plane[buffer->planeCount]();
buffer->modifier = gbm_bo_get_modifier(bo);
auto params = QtWayland::zwp_linux_buffer_params_v1(this->create_params());
for (auto i = 0; i < buffer->planeCount; ++i) {
auto& plane = buffer->planes[i]; // NOLINT
plane.fd = gbm_bo_get_fd_for_plane(bo, i);
if (plane.fd < 0) {
qCritical(logDmabuf) << "Failed to get gbm_bo fd for plane" << i << qt_error_string(plane.fd);
params.destroy();
gbm_bo_destroy(bo);
return nullptr;
}
plane.stride = gbm_bo_get_stride_for_plane(bo, i);
plane.offset = gbm_bo_get_offset(bo, i);
params.add(
plane.fd,
i,
plane.offset,
plane.stride,
buffer->modifier >> 32,
buffer->modifier & 0xffffffff
);
}
buffer->mBuffer =
params.create_immed(static_cast<int32_t>(width), static_cast<int32_t>(height), format, 0);
params.destroy();
buffer->device = this->dupHandle(device);
buffer->width = width;
buffer->height = height;
buffer->format = format;
qCDebug(logDmabuf) << "Created dmabuf" << buffer.get();
return buffer.release();
}
WlDmaBuffer::WlDmaBuffer(WlDmaBuffer&& other) noexcept
: device(std::move(other.device))
, bo(other.bo)
, mBuffer(other.mBuffer)
, planes(other.planes) {
other.mBuffer = nullptr;
other.bo = nullptr;
other.planeCount = 0;
}
WlDmaBuffer& WlDmaBuffer::operator=(WlDmaBuffer&& other) noexcept {
this->~WlDmaBuffer();
new (this) WlDmaBuffer(std::move(other));
return *this;
}
WlDmaBuffer::~WlDmaBuffer() {
if (this->mBuffer) {
wl_buffer_destroy(this->mBuffer);
}
if (this->bo) {
gbm_bo_destroy(this->bo);
qCDebug(logDmabuf) << "Destroyed" << this << "freeing bo" << this->bo;
}
for (auto i = 0; i < this->planeCount; ++i) {
const auto& plane = this->planes[i]; // NOLINT
if (plane.fd) close(plane.fd);
}
delete[] this->planes;
}
bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const {
if (request.width != this->width || request.height != this->height) return false;
auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [&](const auto& format) {
return format.format == this->format
&& (format.modifiers.isEmpty()
|| std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end());
});
return matchingFormat != request.dmabuf.formats.end();
}
WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const {
static auto* glEGLImageTargetTexture2DOES = []() {
auto* fn = reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(
eglGetProcAddress("glEGLImageTargetTexture2DOES")
);
if (!fn) {
qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: "
"glEGLImageTargetTexture2DOES is missing.";
}
return fn;
}();
auto* context = QOpenGLContext::currentContext();
if (!context) {
qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No GL context.";
}
auto* qEglContext = context->nativeInterface<QNativeInterface::QEGLContext>();
if (!qEglContext) {
qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No EGL context.";
}
auto* display = qEglContext->display();
// clang-format off
auto attribs = std::array<EGLAttrib, 6 * 2 + 1> {
EGL_WIDTH, this->width,
EGL_HEIGHT, this->height,
EGL_LINUX_DRM_FOURCC_EXT, this->format,
EGL_DMA_BUF_PLANE0_FD_EXT, this->planes[0].fd, // NOLINT
EGL_DMA_BUF_PLANE0_OFFSET_EXT, this->planes[0].offset, // NOLINT
EGL_DMA_BUF_PLANE0_PITCH_EXT, this->planes[0].stride, // NOLINT
EGL_NONE
};
// clang-format on
auto* eglImage =
eglCreateImage(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data());
if (eglImage == EGL_NO_IMAGE) {
qFatal() << "failed to make egl image" << eglGetError();
return nullptr;
}
window->beginExternalCommands();
GLuint glTexture = 0;
glGenTextures(1, &glTexture);
glBindTexture(GL_TEXTURE_2D, glTexture);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);
glBindTexture(GL_TEXTURE_2D, 0);
window->endExternalCommands();
auto* qsgTexture = QNativeInterface::QSGOpenGLTexture::fromNative(
glTexture,
window,
QSize(static_cast<int>(this->width), static_cast<int>(this->height))
);
auto* tex = new WlDmaBufferQSGTexture(eglImage, glTexture, qsgTexture);
qCDebug(logDmabuf) << "Created WlDmaBufferQSGTexture" << tex << "from" << this;
return tex;
}
WlDmaBufferQSGTexture::~WlDmaBufferQSGTexture() {
auto* context = QOpenGLContext::currentContext();
auto* display = context->nativeInterface<QNativeInterface::QEGLContext>()->display();
if (this->glTexture) glDeleteTextures(1, &this->glTexture);
if (this->eglImage) eglDestroyImage(display, this->eglImage);
delete this->qsgTexture;
qCDebug(logDmabuf) << "WlDmaBufferQSGTexture" << this << "destroyed.";
}
} // namespace qs::wayland::buffer::dmabuf

View file

@ -0,0 +1,235 @@
#pragma once
#include <cstdint>
#include <EGL/egl.h>
#include <gbm.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlist.h>
#include <qquickwindow.h>
#include <qsgtexture.h>
#include <qsize.h>
#include <qtclasshelpermacros.h>
#include <qtypes.h>
#include <qwayland-linux-dmabuf-v1.h>
#include <qwaylandclientextension.h>
#include <sys/types.h>
#include <wayland-linux-dmabuf-v1-client-protocol.h>
#include <wayland-util.h>
#include <xf86drm.h>
#include "manager.hpp"
#include "qsg.hpp"
namespace qs::wayland::buffer {
class WlBufferManagerPrivate;
}
namespace qs::wayland::buffer::dmabuf {
class LinuxDmabufManager;
class FourCCStr {
public:
explicit FourCCStr(uint32_t code)
: chars(
{static_cast<char>(code >> 0 & 0xff),
static_cast<char>(code >> 8 & 0xff),
static_cast<char>(code >> 16 & 0xff),
static_cast<char>(code >> 24 & 0xff),
'\0'}
) {
for (auto i = 3; i != 0; i--) {
if (chars[i] == ' ') chars[i] = '\0';
else break;
}
}
[[nodiscard]] const char* cStr() const { return this->chars.data(); }
private:
std::array<char, 5> chars {};
};
class FourCCModStr {
public:
explicit FourCCModStr(uint64_t code): drmStr(drmGetFormatModifierName(code)) {}
~FourCCModStr() {
if (this->drmStr) drmFree(this->drmStr);
}
Q_DISABLE_COPY_MOVE(FourCCModStr);
[[nodiscard]] const char* cStr() const { return this->drmStr; }
private:
char* drmStr;
};
QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc);
QDebug& operator<<(QDebug& debug, const FourCCModStr& fourcc);
class GbmDeviceHandle {
public:
GbmDeviceHandle() = default;
GbmDeviceHandle(gbm_device* device): device(device) {}
GbmDeviceHandle(GbmDeviceHandle&& other) noexcept: device(other.device) {
other.device = nullptr;
}
~GbmDeviceHandle();
Q_DISABLE_COPY(GbmDeviceHandle);
GbmDeviceHandle& operator=(GbmDeviceHandle&& other) noexcept {
this->device = other.device;
other.device = nullptr;
return *this;
}
[[nodiscard]] gbm_device* operator*() const { return this->device; }
[[nodiscard]] operator bool() const { return this->device; }
private:
gbm_device* device = nullptr;
};
class WlDmaBufferQSGTexture: public WlBufferQSGTexture {
public:
~WlDmaBufferQSGTexture() override;
Q_DISABLE_COPY_MOVE(WlDmaBufferQSGTexture);
[[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture; }
private:
WlDmaBufferQSGTexture(EGLImage eglImage, GLuint glTexture, QSGTexture* qsgTexture)
: eglImage(eglImage)
, glTexture(glTexture)
, qsgTexture(qsgTexture) {}
EGLImage eglImage;
GLuint glTexture;
QSGTexture* qsgTexture;
friend class WlDmaBuffer;
};
class WlDmaBuffer: public WlBuffer {
public:
~WlDmaBuffer() override;
Q_DISABLE_COPY(WlDmaBuffer);
WlDmaBuffer(WlDmaBuffer&& other) noexcept;
WlDmaBuffer& operator=(WlDmaBuffer&& other) noexcept;
[[nodiscard]] wl_buffer* buffer() const override { return this->mBuffer; }
[[nodiscard]] QSize size() const override {
return QSize(static_cast<int>(this->width), static_cast<int>(this->height));
}
[[nodiscard]] bool isCompatible(const WlBufferRequest& request) const override;
[[nodiscard]] WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const override;
private:
WlDmaBuffer() noexcept = default;
struct Plane {
int fd = 0;
uint32_t offset = 0;
uint32_t stride = 0;
};
GbmDeviceHandle device;
gbm_bo* bo = nullptr;
wl_buffer* mBuffer = nullptr;
int planeCount = 0;
Plane* planes = nullptr;
uint32_t format = 0;
uint64_t modifier = 0;
uint32_t width = 0;
uint32_t height = 0;
friend class LinuxDmabufManager;
friend QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer);
};
QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer);
struct LinuxDmabufModifiers {
StackList<uint64_t, 10> modifiers;
bool implicit = false;
};
struct LinuxDmabufFormatSelection {
bool sorted = false;
StackList<QPair<uint32_t, LinuxDmabufModifiers>, 2> formats;
void ensureSorted();
};
struct LinuxDmabufTranche {
dev_t device = 0;
uint32_t flags = 0;
LinuxDmabufFormatSelection formats;
};
class LinuxDmabufFeedback: public QtWayland::zwp_linux_dmabuf_feedback_v1 {
public:
explicit LinuxDmabufFeedback(::zwp_linux_dmabuf_feedback_v1* feedback);
~LinuxDmabufFeedback() override;
Q_DISABLE_COPY_MOVE(LinuxDmabufFeedback);
protected:
void zwp_linux_dmabuf_feedback_v1_main_device(wl_array* device) override;
void zwp_linux_dmabuf_feedback_v1_format_table(int32_t fd, uint32_t size) override;
void zwp_linux_dmabuf_feedback_v1_tranche_target_device(wl_array* device) override;
void zwp_linux_dmabuf_feedback_v1_tranche_flags(uint32_t flags) override;
void zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* indices) override;
void zwp_linux_dmabuf_feedback_v1_tranche_done() override;
void zwp_linux_dmabuf_feedback_v1_done() override;
private:
dev_t mainDevice = 0;
QList<LinuxDmabufTranche> tranches;
void* formatTable = nullptr;
uint32_t formatTableSize = 0;
};
class LinuxDmabufManager
: public QWaylandClientExtensionTemplate<LinuxDmabufManager>
, public QtWayland::zwp_linux_dmabuf_v1 {
public:
explicit LinuxDmabufManager(WlBufferManagerPrivate* manager);
[[nodiscard]] WlBuffer* createDmabuf(const WlBufferRequest& request);
[[nodiscard]] WlBuffer* createDmabuf(
GbmDeviceHandle& device,
uint32_t format,
const LinuxDmabufModifiers& modifiers,
uint32_t width,
uint32_t height
);
private:
struct SharedGbmDevice {
dev_t handle = 0;
std::string renderNode;
gbm_device* device = nullptr;
qsizetype refcount = 0;
};
void feedbackDone();
GbmDeviceHandle getGbmDevice(dev_t handle);
void unrefGbmDevice(gbm_device* device);
GbmDeviceHandle dupHandle(const GbmDeviceHandle& handle);
QList<LinuxDmabufTranche> tranches;
QList<SharedGbmDevice> gbmDevices;
WlBufferManagerPrivate* manager;
friend class LinuxDmabufFeedback;
friend class GbmDeviceHandle;
};
} // namespace qs::wayland::buffer::dmabuf

View file

@ -0,0 +1,137 @@
#include "manager.hpp"
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qmatrix4x4.h>
#include <qnamespace.h>
#include <qquickwindow.h>
#include <qtenvironmentvariables.h>
#include <qtmetamacros.h>
#include <qvectornd.h>
#include "dmabuf.hpp"
#include "manager_p.hpp"
#include "qsg.hpp"
#include "shm.hpp"
namespace qs::wayland::buffer {
namespace {
Q_LOGGING_CATEGORY(logBuffer, "quickshell.wayland.buffer", QtWarningMsg);
}
WlBuffer* WlBufferSwapchain::createBackbuffer(const WlBufferRequest& request, bool* newBuffer) {
auto& buffer = this->presentSecondBuffer ? this->buffer1 : this->buffer2;
if (!buffer || !buffer->isCompatible(request)) {
buffer.reset(WlBufferManager::instance()->createBuffer(request));
if (newBuffer) *newBuffer = true;
}
return buffer.get();
}
WlBufferManager::WlBufferManager(): p(new WlBufferManagerPrivate(this)) {}
WlBufferManager::~WlBufferManager() { delete this->p; }
WlBufferManager* WlBufferManager::instance() {
static auto* instance = new WlBufferManager();
return instance;
}
bool WlBufferManager::isReady() const { return this->p->mReady; }
[[nodiscard]] WlBuffer* WlBufferManager::createBuffer(const WlBufferRequest& request) {
static const bool dmabufDisabled = qEnvironmentVariableIsSet("QS_DISABLE_DMABUF");
qCDebug(logBuffer).nospace() << "Creating buffer from request at " << request.width << 'x'
<< request.height;
qCDebug(logBuffer).nospace() << " Dmabuf requests on device " << request.dmabuf.device
<< " (disabled: " << dmabufDisabled << ')';
for (const auto& [format, modifiers]: request.dmabuf.formats) {
qCDebug(logBuffer) << " Format" << dmabuf::FourCCStr(format);
for (const auto& modifier: modifiers) {
qCDebug(logBuffer) << " Explicit Modifier" << dmabuf::FourCCModStr(modifier);
}
}
qCDebug(logBuffer).nospace() << " Shm requests";
for (const auto& format: request.shm.formats) {
qCDebug(logBuffer) << " Format" << format;
}
if (!dmabufDisabled) {
if (auto* buf = this->p->dmabuf.createDmabuf(request)) return buf;
qCWarning(logBuffer) << "DMA buffer creation failed, falling back to SHM.";
}
return shm::ShmbufManager::createShmbuf(request);
}
WlBufferManagerPrivate::WlBufferManagerPrivate(WlBufferManager* manager)
: manager(manager)
, dmabuf(this) {}
void WlBufferManagerPrivate::dmabufReady() {
this->mReady = true;
emit this->manager->ready();
}
WlBufferQSGDisplayNode::WlBufferQSGDisplayNode(QQuickWindow* window)
: window(window)
, imageNode(window->createImageNode()) {
this->appendChildNode(this->imageNode);
}
void WlBufferQSGDisplayNode::setRect(const QRectF& rect) {
const auto* buffer = (this->presentSecondBuffer ? this->buffer2 : this->buffer1).first;
if (!buffer) return;
auto matrix = QMatrix4x4();
auto center = rect.center();
auto centerX = static_cast<float>(center.x());
auto centerY = static_cast<float>(center.y());
matrix.translate(centerX, centerY);
buffer->transform.apply(matrix);
matrix.translate(-centerX, -centerY);
auto viewRect = matrix.mapRect(rect);
auto bufferSize = buffer->size().toSizeF();
bufferSize.scale(viewRect.width(), viewRect.height(), Qt::KeepAspectRatio);
this->imageNode->setRect(
viewRect.x() + viewRect.width() / 2 - bufferSize.width() / 2,
viewRect.y() + viewRect.height() / 2 - bufferSize.height() / 2,
bufferSize.width(),
bufferSize.height()
);
this->setMatrix(matrix);
}
void WlBufferQSGDisplayNode::syncSwapchain(const WlBufferSwapchain& swapchain) {
auto* buffer = swapchain.frontbuffer();
auto& texture = swapchain.presentSecondBuffer ? this->buffer2 : this->buffer1;
if (swapchain.presentSecondBuffer == this->presentSecondBuffer && texture.first == buffer) {
return;
}
this->presentSecondBuffer = swapchain.presentSecondBuffer;
if (texture.first == buffer) {
texture.second->sync(texture.first, this->window);
} else {
texture.first = buffer;
texture.second.reset(buffer->createQsgTexture(this->window));
}
this->imageNode->setTexture(texture.second->texture());
}
} // namespace qs::wayland::buffer

View file

@ -0,0 +1,134 @@
#pragma once
#include <cstdint>
#include <memory>
#include <qhash.h>
#include <qlist.h>
#include <qmatrix4x4.h>
#include <qobject.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qvariant.h>
#include <wayland-client-protocol.h>
#include <wayland-client.h>
#include "../../core/stacklist.hpp"
class QQuickWindow;
namespace qs::wayland::buffer {
class WlBufferManagerPrivate;
class WlBufferQSGTexture;
struct WlBufferTransform {
enum Transform : uint8_t {
Normal0 = 0,
Normal90 = 1,
Normal180 = 2,
Normal270 = 3,
Flipped0 = 4,
Flipped90 = 5,
Flipped180 = 6,
Flipped270 = 7,
} transform = Normal0;
WlBufferTransform() = default;
WlBufferTransform(uint8_t transform): transform(static_cast<Transform>(transform)) {}
[[nodiscard]] int degrees() const { return 90 * (this->transform & 0b11111011); }
[[nodiscard]] bool flip() const { return this->transform & 0b00000100; }
void apply(QMatrix4x4& matrix) const {
matrix.rotate(this->flip() ? 180 : 0, 0, 1, 0);
matrix.rotate(static_cast<float>(this->degrees()), 0, 0, 1);
}
};
struct WlBufferRequest {
uint32_t width = 0;
uint32_t height = 0;
struct DmaFormat {
DmaFormat() = default;
DmaFormat(uint32_t format): format(format) {}
uint32_t format = 0;
StackList<uint64_t, 10> modifiers;
};
struct {
StackList<uint32_t, 1> formats;
} shm;
struct {
dev_t device = 0;
StackList<DmaFormat, 1> formats;
} dmabuf;
};
class WlBuffer {
public:
virtual ~WlBuffer() = default;
Q_DISABLE_COPY_MOVE(WlBuffer);
[[nodiscard]] virtual wl_buffer* buffer() const = 0;
[[nodiscard]] virtual QSize size() const = 0;
[[nodiscard]] virtual bool isCompatible(const WlBufferRequest& request) const = 0;
[[nodiscard]] operator bool() const { return this->buffer(); }
// Must be called from render thread.
[[nodiscard]] virtual WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const = 0;
WlBufferTransform transform;
protected:
explicit WlBuffer() = default;
};
class WlBufferSwapchain {
public:
[[nodiscard]] WlBuffer*
createBackbuffer(const WlBufferRequest& request, bool* newBuffer = nullptr);
void swapBuffers() { this->presentSecondBuffer = !this->presentSecondBuffer; }
[[nodiscard]] WlBuffer* backbuffer() const {
return this->presentSecondBuffer ? this->buffer1.get() : this->buffer2.get();
}
[[nodiscard]] WlBuffer* frontbuffer() const {
return this->presentSecondBuffer ? this->buffer2.get() : this->buffer1.get();
}
private:
std::unique_ptr<WlBuffer> buffer1;
std::unique_ptr<WlBuffer> buffer2;
bool presentSecondBuffer = false;
friend class WlBufferQSGDisplayNode;
};
class WlBufferManager: public QObject {
Q_OBJECT;
public:
~WlBufferManager() override;
Q_DISABLE_COPY_MOVE(WlBufferManager);
static WlBufferManager* instance();
[[nodiscard]] bool isReady() const;
[[nodiscard]] WlBuffer* createBuffer(const WlBufferRequest& request);
signals:
void ready();
private:
explicit WlBufferManager();
WlBufferManagerPrivate* p;
};
} // namespace qs::wayland::buffer

View file

@ -0,0 +1,20 @@
#pragma once
#include "dmabuf.hpp"
#include "manager.hpp"
namespace qs::wayland::buffer {
class WlBufferManagerPrivate {
public:
explicit WlBufferManagerPrivate(WlBufferManager* manager);
void dmabufReady();
WlBufferManager* manager;
dmabuf::LinuxDmabufManager dmabuf;
bool mReady = false;
};
} // namespace qs::wayland::buffer

View file

@ -0,0 +1,45 @@
#pragma once
#include <memory>
#include <qcontainerfwd.h>
#include <qquickwindow.h>
#include <qsgimagenode.h>
#include <qsgnode.h>
#include <qsgtexture.h>
#include <qvectornd.h>
#include "manager.hpp"
namespace qs::wayland::buffer {
// Interact only from QSG thread.
class WlBufferQSGTexture {
public:
virtual ~WlBufferQSGTexture() = default;
Q_DISABLE_COPY_MOVE(WlBufferQSGTexture);
[[nodiscard]] virtual QSGTexture* texture() const = 0;
virtual void sync(const WlBuffer* /*buffer*/, QQuickWindow* /*window*/) {}
protected:
WlBufferQSGTexture() = default;
};
// Interact only from QSG thread.
class WlBufferQSGDisplayNode: public QSGTransformNode {
public:
explicit WlBufferQSGDisplayNode(QQuickWindow* window);
void syncSwapchain(const WlBufferSwapchain& swapchain);
void setRect(const QRectF& rect);
private:
QQuickWindow* window;
QSGImageNode* imageNode;
QPair<WlBuffer*, std::unique_ptr<WlBufferQSGTexture>> buffer1;
QPair<WlBuffer*, std::unique_ptr<WlBufferQSGTexture>> buffer2;
bool presentSecondBuffer = false;
};
} // namespace qs::wayland::buffer

View file

@ -0,0 +1,93 @@
#include "shm.hpp"
#include <algorithm>
#include <memory>
#include <private/qwaylanddisplay_p.h>
#include <private/qwaylandintegration_p.h>
#include <private/qwaylandshm_p.h>
#include <private/qwaylandshmbackingstore_p.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qquickwindow.h>
#include <qsize.h>
#include <wayland-client-protocol.h>
#include "manager.hpp"
namespace qs::wayland::buffer::shm {
namespace {
Q_LOGGING_CATEGORY(logShm, "quickshell.wayland.buffer.shm", QtWarningMsg);
}
bool WlShmBuffer::isCompatible(const WlBufferRequest& request) const {
if (QSize(static_cast<int>(request.width), static_cast<int>(request.height)) != this->size()) {
return false;
}
auto matchingFormat = std::ranges::find(request.shm.formats, this->format);
return matchingFormat != request.shm.formats.end();
}
QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer) {
auto saver = QDebugStateSaver(debug);
debug.nospace();
if (buffer) {
auto fmt = QtWaylandClient::QWaylandShm::formatFrom(
static_cast<::wl_shm_format>(buffer->format) // NOLINT
);
debug << "WlShmBuffer(" << static_cast<const void*>(buffer) << ", size=" << buffer->size()
<< ", format=" << fmt << ')';
} else {
debug << "WlShmBuffer(0x0)";
}
return debug;
}
WlShmBuffer::~WlShmBuffer() { qCDebug(logShm) << "Destroyed" << this; }
WlBufferQSGTexture* WlShmBuffer::createQsgTexture(QQuickWindow* window) const {
auto* texture = new WlShmBufferQSGTexture();
// If the QWaylandShmBuffer is destroyed before the QSGTexture, we'll hit a UAF
// in the render thread.
texture->shmBuffer = this->shmBuffer;
texture->qsgTexture.reset(window->createTextureFromImage(*this->shmBuffer->image()));
texture->sync(this, window);
return texture;
}
void WlShmBufferQSGTexture::sync(const WlBuffer* /*unused*/, QQuickWindow* window) {
// This is both dumb and expensive. We should use an RHI texture and render images into
// it more intelligently, but shm buffers are already a horribly slow fallback path,
// to the point where it barely matters.
this->qsgTexture.reset(window->createTextureFromImage(*this->shmBuffer->image()));
}
WlBuffer* ShmbufManager::createShmbuf(const WlBufferRequest& request) {
if (request.shm.formats.isEmpty()) return nullptr;
static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance();
auto* display = waylandIntegration->display();
// Its probably fine...
auto format = request.shm.formats[0];
auto* buffer = new WlShmBuffer(
new QtWaylandClient::QWaylandShmBuffer(
display,
QSize(static_cast<int>(request.width), static_cast<int>(request.height)),
QtWaylandClient::QWaylandShm::formatFrom(static_cast<::wl_shm_format>(format)) // NOLINT
),
format
);
qCDebug(logShm) << "Created shmbuf" << buffer;
return buffer;
}
} // namespace qs::wayland::buffer::shm

View file

@ -0,0 +1,61 @@
#pragma once
#include <memory>
#include <private/qwaylandshmbackingstore_p.h>
#include <qquickwindow.h>
#include <qsgtexture.h>
#include <qsize.h>
#include <qtclasshelpermacros.h>
#include <wayland-client-protocol.h>
#include "manager.hpp"
#include "qsg.hpp"
namespace qs::wayland::buffer::shm {
class WlShmBuffer: public WlBuffer {
public:
~WlShmBuffer() override;
Q_DISABLE_COPY_MOVE(WlShmBuffer);
[[nodiscard]] wl_buffer* buffer() const override { return this->shmBuffer->buffer(); }
[[nodiscard]] QSize size() const override { return this->shmBuffer->size(); }
[[nodiscard]] bool isCompatible(const WlBufferRequest& request) const override;
[[nodiscard]] WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const override;
private:
WlShmBuffer(QtWaylandClient::QWaylandShmBuffer* shmBuffer, uint32_t format)
: shmBuffer(shmBuffer)
, format(format) {}
std::shared_ptr<QtWaylandClient::QWaylandShmBuffer> shmBuffer;
uint32_t format;
friend class WlShmBufferQSGTexture;
friend class ShmbufManager;
friend QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer);
};
QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer);
class WlShmBufferQSGTexture: public WlBufferQSGTexture {
public:
[[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture.get(); }
void sync(const WlBuffer* buffer, QQuickWindow* window) override;
private:
WlShmBufferQSGTexture() = default;
std::shared_ptr<QtWaylandClient::QWaylandShmBuffer> shmBuffer;
std::unique_ptr<QSGTexture> qsgTexture;
friend class WlShmBuffer;
};
class ShmbufManager {
public:
[[nodiscard]] static WlBuffer* createShmbuf(const WlBufferRequest& request);
};
} // namespace qs::wayland::buffer::shm

View file

@ -14,13 +14,11 @@ qs_add_module_deps_light(quickshell-hyprland-focus-grab Quickshell)
install_qml_module(quickshell-hyprland-focus-grab) install_qml_module(quickshell-hyprland-focus-grab)
wl_proto(quickshell-hyprland-focus-grab wl_proto(wlp-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}")
hyprland-focus-grab-v1
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml"
)
target_link_libraries(quickshell-hyprland-focus-grab PRIVATE target_link_libraries(quickshell-hyprland-focus-grab PRIVATE
Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
wlp-hyprland-focus-grab
) )
qs_module_pch(quickshell-hyprland-focus-grab SET large) qs_module_pch(quickshell-hyprland-focus-grab SET large)

View file

@ -12,14 +12,12 @@ qt_add_qml_module(quickshell-hyprland-global-shortcuts
install_qml_module(quickshell-hyprland-global-shortcuts) install_qml_module(quickshell-hyprland-global-shortcuts)
wl_proto(quickshell-hyprland-global-shortcuts wl_proto(wlp-hyprland-shortcuts hyprland-global-shortcuts-v1 "${CMAKE_CURRENT_SOURCE_DIR}")
hyprland-global-shortcuts-v1
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml"
)
target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE
Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
Qt::Quick # pch Qt::Quick # pch
wlp-hyprland-shortcuts
) )
qs_module_pch(quickshell-hyprland-global-shortcuts) qs_module_pch(quickshell-hyprland-global-shortcuts)

View file

@ -26,8 +26,10 @@
namespace qs::hyprland::ipc { namespace qs::hyprland::ipc {
namespace {
Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg);
Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg);
} // namespace
HyprlandIpc::HyprlandIpc() { HyprlandIpc::HyprlandIpc() {
auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
@ -241,9 +243,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
const auto& mList = this->mMonitors.valueList(); const auto& mList = this->mMonitors.valueList();
auto name = QString::fromUtf8(event->data); auto name = QString::fromUtf8(event->data);
auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { auto monitorIter =
return m->name() == name; std::ranges::find_if(mList, [name](const HyprlandMonitor* m) { return m->name() == name; });
});
if (monitorIter == mList.end()) { if (monitorIter == mList.end()) {
qCWarning(logHyprlandIpc) << "Got removal for monitor" << name qCWarning(logHyprlandIpc) << "Got removal for monitor" << name
@ -292,9 +293,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
const auto& mList = this->mWorkspaces.valueList(); const auto& mList = this->mWorkspaces.valueList();
auto workspaceIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandWorkspace* m) { auto workspaceIter =
return m->id() == id; std::ranges::find_if(mList, [id](const HyprlandWorkspace* m) { return m->id() == id; });
});
if (workspaceIter == mList.end()) { if (workspaceIter == mList.end()) {
qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name
@ -352,6 +352,22 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
auto* monitor = this->findMonitorByName(monitorName, true); auto* monitor = this->findMonitorByName(monitorName, true);
workspace->setMonitor(monitor); workspace->setMonitor(monitor);
} else if (event->name == "renameworkspace") {
auto args = event->parseView(2);
auto id = args.at(0).toInt();
auto name = QString::fromUtf8(args.at(1));
const auto& mList = this->mWorkspaces.valueList();
auto workspaceIter =
std::ranges::find_if(mList, [id](const HyprlandWorkspace* m) { return m->id() == id; });
if (workspaceIter == mList.end()) return;
qCDebug(logHyprlandIpc) << "Workspace with id" << id << "renamed from"
<< (*workspaceIter)->name() << "to" << name;
(*workspaceIter)->setName(name);
} }
} }
@ -359,9 +375,8 @@ HyprlandWorkspace*
HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) { HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) {
const auto& mList = this->mWorkspaces.valueList(); const auto& mList = this->mWorkspaces.valueList();
auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { auto workspaceIter =
return m->name() == name; std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) { return m->name() == name; });
});
if (workspaceIter != mList.end()) { if (workspaceIter != mList.end()) {
return *workspaceIter; return *workspaceIter;
@ -395,8 +410,7 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) {
auto object = entry.toObject().toVariantMap(); auto object = entry.toObject().toVariantMap();
auto name = object.value("name").toString(); auto name = object.value("name").toString();
auto workspaceIter = auto workspaceIter = std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) {
std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) {
return m->name() == name; return m->name() == name;
}); });
@ -436,9 +450,8 @@ HyprlandMonitor*
HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) {
const auto& mList = this->mMonitors.valueList(); const auto& mList = this->mMonitors.valueList();
auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { auto monitorIter =
return m->name() == name; std::ranges::find_if(mList, [name](const HyprlandMonitor* m) { return m->name() == name; });
});
if (monitorIter != mList.end()) { if (monitorIter != mList.end()) {
return *monitorIter; return *monitorIter;
@ -506,7 +519,7 @@ void HyprlandIpc::refreshMonitors(bool canCreate) {
auto object = entry.toObject().toVariantMap(); auto object = entry.toObject().toVariantMap();
auto name = object.value("name").toString(); auto name = object.value("name").toString();
auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { auto monitorIter = std::ranges::find_if(mList, [name](const HyprlandMonitor* m) {
return m->name() == name; return m->name() == name;
}); });

View file

@ -11,7 +11,15 @@
namespace qs::hyprland::ipc { namespace qs::hyprland::ipc {
qint32 HyprlandWorkspace::id() const { return this->mId; } qint32 HyprlandWorkspace::id() const { return this->mId; }
QString HyprlandWorkspace::name() const { return this->mName; } QString HyprlandWorkspace::name() const { return this->mName; }
void HyprlandWorkspace::setName(QString name) {
if (name == this->mName) return;
this->mName = std::move(name);
emit this->nameChanged();
}
QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObject; } QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObject; }
void HyprlandWorkspace::updateInitial(qint32 id, QString name) { void HyprlandWorkspace::updateInitial(qint32 id, QString name) {

View file

@ -32,11 +32,14 @@ public:
void updateFromObject(QVariantMap object); void updateFromObject(QVariantMap object);
[[nodiscard]] qint32 id() const; [[nodiscard]] qint32 id() const;
[[nodiscard]] QString name() const; [[nodiscard]] QString name() const;
void setName(QString name);
[[nodiscard]] QVariantMap lastIpcObject() const; [[nodiscard]] QVariantMap lastIpcObject() const;
void setMonitor(HyprlandMonitor* monitor);
[[nodiscard]] HyprlandMonitor* monitor() const; [[nodiscard]] HyprlandMonitor* monitor() const;
void setMonitor(HyprlandMonitor* monitor);
signals: signals:
void idChanged(); void idChanged();

View file

@ -12,13 +12,11 @@ qt_add_qml_module(quickshell-hyprland-surface-extensions
install_qml_module(quickshell-hyprland-surface-extensions) install_qml_module(quickshell-hyprland-surface-extensions)
wl_proto(quickshell-hyprland-surface-extensions wl_proto(wlp-hyprland-surface hyprland-surface-v1 "${CMAKE_CURRENT_SOURCE_DIR}")
hyprland-surface-v1
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-surface-v1.xml"
)
target_link_libraries(quickshell-hyprland-surface-extensions PRIVATE target_link_libraries(quickshell-hyprland-surface-extensions PRIVATE
Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
wlp-hyprland-surface
) )
qs_module_pch(quickshell-hyprland-surface-extensions) qs_module_pch(quickshell-hyprland-surface-extensions)

View file

@ -34,7 +34,7 @@
This protocol exposes hyprland-specific wl_surface properties. This protocol exposes hyprland-specific wl_surface properties.
</description> </description>
<interface name="hyprland_surface_manager_v1" version="1"> <interface name="hyprland_surface_manager_v1" version="2">
<description summary="manager for hyprland surface objects"> <description summary="manager for hyprland surface objects">
This interface allows a client to create hyprland surface objects. This interface allows a client to create hyprland surface objects.
</description> </description>
@ -63,7 +63,7 @@
</enum> </enum>
</interface> </interface>
<interface name="hyprland_surface_v1" version="1"> <interface name="hyprland_surface_v1" version="2">
<description summary="hyprland-specific wl_surface properties"> <description summary="hyprland-specific wl_surface properties">
This interface allows access to hyprland-specific properties of a wl_surface. This interface allows access to hyprland-specific properties of a wl_surface.
@ -96,5 +96,31 @@
<entry name="no_surface" value="0" summary="wl_surface was destroyed"/> <entry name="no_surface" value="0" summary="wl_surface was destroyed"/>
<entry name="out_of_range" value="1" summary="given opacity was not in the range 0.0 - 1.0 (inclusive)"/> <entry name="out_of_range" value="1" summary="given opacity was not in the range 0.0 - 1.0 (inclusive)"/>
</enum> </enum>
<request name="set_visible_region" since="2">
<description summary="set the visible region of the surface">
This request sets the region of the surface that contains visible content.
Visible content refers to content that has an alpha value greater than zero.
The visible region is an optimization hint for the compositor that lets it
avoid drawing parts of the surface that are not visible. Setting a visible region
that does not contain all content in the surface may result in missing content
not being drawn.
The visible region is specified in buffer-local coordinates.
The compositor ignores the parts of the visible region that fall outside of the surface.
When all parts of the region fall outside of the buffer geometry, the compositor may
avoid rendering the surface entirely.
The initial value for the visible region is empty. Setting the
visible region has copy semantics, and the wl_region object can be destroyed immediately.
A NULL wl_region causes the visible region to be set to empty.
Does not take effect until wl_surface.commit is called.
</description>
<arg name="region" type="object" interface="wl_region" allow-null="true"/>
</request>
</interface> </interface>
</protocol> </protocol>

View file

@ -7,13 +7,13 @@
namespace qs::hyprland::surface::impl { namespace qs::hyprland::surface::impl {
HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(1) { HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(2) {
this->initialize(); this->initialize();
} }
HyprlandSurface* HyprlandSurface*
HyprlandSurfaceManager::createHyprlandExtension(QtWaylandClient::QWaylandWindow* surface) { HyprlandSurfaceManager::createHyprlandExtension(QtWaylandClient::QWaylandWindow* surface) {
return new HyprlandSurface(this->get_hyprland_surface(surface->surface())); return new HyprlandSurface(this->get_hyprland_surface(surface->surface()), surface);
} }
HyprlandSurfaceManager* HyprlandSurfaceManager::instance() { HyprlandSurfaceManager* HyprlandSurfaceManager::instance() {

View file

@ -1,17 +1,20 @@
#include "qml.hpp" #include "qml.hpp"
#include <memory> #include <memory>
#include <private/qhighdpiscaling_p.h>
#include <private/qwaylandwindow_p.h> #include <private/qwaylandwindow_p.h>
#include <qlogging.h> #include <qlogging.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlinfo.h> #include <qqmlinfo.h>
#include <qregion.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include <qvariant.h>
#include <qwindow.h> #include <qwindow.h>
#include "../../../core/region.hpp"
#include "../../../window/proxywindow.hpp" #include "../../../window/proxywindow.hpp"
#include "../../../window/windowinterface.hpp" #include "../../../window/windowinterface.hpp"
#include "../../util.hpp"
#include "manager.hpp" #include "manager.hpp"
#include "surface.hpp" #include "surface.hpp"
@ -28,7 +31,6 @@ HyprlandWindow* HyprlandWindow::qmlAttachedProperties(QObject* object) {
} }
} }
qDebug() << "hlwindow for" << proxyWindow;
if (!proxyWindow) return nullptr; if (!proxyWindow) return nullptr;
return new HyprlandWindow(proxyWindow); return new HyprlandWindow(proxyWindow);
} }
@ -41,6 +43,15 @@ HyprlandWindow::HyprlandWindow(ProxyWindowBase* window): QObject(nullptr), proxy
&HyprlandWindow::onWindowConnected &HyprlandWindow::onWindowConnected
); );
QObject::connect(window, &ProxyWindowBase::polished, this, &HyprlandWindow::onWindowPolished);
QObject::connect(
window,
&ProxyWindowBase::devicePixelRatioChanged,
this,
&HyprlandWindow::updateVisibleMask
);
QObject::connect(window, &QObject::destroyed, this, &HyprlandWindow::onProxyWindowDestroyed); QObject::connect(window, &QObject::destroyed, this, &HyprlandWindow::onProxyWindowDestroyed);
if (window->backingWindow()) { if (window->backingWindow()) {
@ -61,14 +72,76 @@ void HyprlandWindow::setOpacity(qreal opacity) {
this->mOpacity = opacity; this->mOpacity = opacity;
if (this->surface) { if (this->surface && this->proxyWindow) {
this->surface->setOpacity(opacity); this->pendingPolish.opacity = true;
qs::wayland::util::scheduleCommit(this->mWaylandWindow); this->proxyWindow->schedulePolish();
} }
emit this->opacityChanged(); emit this->opacityChanged();
} }
PendingRegion* HyprlandWindow::visibleMask() const { return this->mVisibleMask; }
void HyprlandWindow::setVisibleMask(PendingRegion* mask) {
if (mask == this->mVisibleMask) return;
if (this->mVisibleMask) {
QObject::disconnect(this->mVisibleMask, nullptr, this, nullptr);
}
this->mVisibleMask = mask;
if (mask) {
QObject::connect(mask, &QObject::destroyed, this, &HyprlandWindow::onVisibleMaskDestroyed);
QObject::connect(mask, &PendingRegion::changed, this, &HyprlandWindow::updateVisibleMask);
}
this->updateVisibleMask();
emit this->visibleMaskChanged();
}
void HyprlandWindow::onVisibleMaskDestroyed() {
this->mVisibleMask = nullptr;
this->updateVisibleMask();
emit this->visibleMaskChanged();
}
void HyprlandWindow::updateVisibleMask() {
if (!this->surface || !this->proxyWindow) return;
this->pendingPolish.visibleMask = true;
this->proxyWindow->schedulePolish();
}
void HyprlandWindow::onWindowPolished() {
if (!this->surface) return;
if (this->pendingPolish.opacity) {
this->surface->setOpacity(this->mOpacity);
this->pendingPolish.opacity = false;
}
if (this->pendingPolish.visibleMask) {
QRegion mask;
if (this->mVisibleMask != nullptr) {
mask =
this->mVisibleMask->applyTo(QRect(0, 0, this->mWindow->width(), this->mWindow->height()));
}
auto dpr = this->proxyWindow->devicePixelRatio();
if (dpr != 1.0) {
mask = QHighDpi::scale(mask, dpr);
}
if (mask.isEmpty() && this->mVisibleMask) {
mask = QRect(-1, -1, 1, 1);
}
this->surface->setVisibleRegion(mask);
this->pendingPolish.visibleMask = false;
}
}
void HyprlandWindow::onWindowConnected() { void HyprlandWindow::onWindowConnected() {
this->mWindow = this->proxyWindow->backingWindow(); this->mWindow = this->proxyWindow->backingWindow();
// disconnected by destructor // disconnected by destructor
@ -87,11 +160,24 @@ void HyprlandWindow::onWindowVisibleChanged() {
if (!this->mWindow->handle()) { if (!this->mWindow->handle()) {
this->mWindow->create(); this->mWindow->create();
} }
}
this->mWaylandWindow = dynamic_cast<QWaylandWindow*>(this->mWindow->handle()); auto* window = dynamic_cast<QWaylandWindow*>(this->mWindow->handle());
if (window == this->mWaylandWindow) return;
if (this->mWaylandWindow) { if (this->mWaylandWindow) {
// disconnected by destructor QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr);
}
this->mWaylandWindow = window;
if (!window) return;
QObject::connect(
this->mWaylandWindow,
&QObject::destroyed,
this,
&HyprlandWindow::onWaylandWindowDestroyed
);
QObject::connect( QObject::connect(
this->mWaylandWindow, this->mWaylandWindow,
@ -111,8 +197,8 @@ void HyprlandWindow::onWindowVisibleChanged() {
this->onWaylandSurfaceCreated(); this->onWaylandSurfaceCreated();
} }
} }
}
} void HyprlandWindow::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; }
void HyprlandWindow::onWaylandSurfaceCreated() { void HyprlandWindow::onWaylandSurfaceCreated() {
auto* manager = impl::HyprlandSurfaceManager::instance(); auto* manager = impl::HyprlandSurfaceManager::instance();
@ -123,12 +209,26 @@ void HyprlandWindow::onWaylandSurfaceCreated() {
return; return;
} }
auto v = this->mWaylandWindow->property("hyprland_window_ext");
if (v.canConvert<HyprlandWindow*>()) {
auto* windowExt = v.value<HyprlandWindow*>();
if (windowExt != this && windowExt->surface) {
this->surface.swap(windowExt->surface);
}
}
if (!this->surface) {
auto* ext = manager->createHyprlandExtension(this->mWaylandWindow); auto* ext = manager->createHyprlandExtension(this->mWaylandWindow);
this->surface = std::unique_ptr<impl::HyprlandSurface>(ext); this->surface = std::unique_ptr<impl::HyprlandSurface>(ext);
}
if (this->mOpacity != 1.0) { this->mWaylandWindow->setProperty("hyprland_window_ext", QVariant::fromValue(this));
this->surface->setOpacity(this->mOpacity);
qs::wayland::util::scheduleCommit(this->mWaylandWindow); this->pendingPolish.opacity = this->mOpacity != 1.0;
this->pendingPolish.visibleMask = this->mVisibleMask;
if (this->pendingPolish.opacity || this->pendingPolish.visibleMask) {
this->proxyWindow->schedulePolish();
} }
} }
@ -145,8 +245,9 @@ void HyprlandWindow::onProxyWindowDestroyed() {
// Deleting it when the proxy window is deleted will cause a full opacity frame between the destruction of the // Deleting it when the proxy window is deleted will cause a full opacity frame between the destruction of the
// hyprland_surface_v1 and wl_surface objects. // hyprland_surface_v1 and wl_surface objects.
if (this->surface == nullptr) {
this->proxyWindow = nullptr; this->proxyWindow = nullptr;
if (this->surface == nullptr) {
this->deleteLater(); this->deleteLater();
} }
} }

Some files were not shown because too many files have changed in this diff Show more