1
0
Fork 0

Compare commits

...

31 commits

Author SHA1 Message Date
outfoxxed 4f2610dece
io/ipchandler: add prop get 2025-01-26 03:57:07 -08:00
outfoxxed 9417d6fa57
core/command: deprecate qs msg 2025-01-25 01:00:42 -08:00
outfoxxed 420529362f
core/clock: expose date as a QDateTime 2025-01-24 23:53:31 -08:00
outfoxxed 325be8857c
core/command: add option to select newest matching instance 2025-01-24 16:30:32 -08:00
outfoxxed b289bfa504
hyprland/surface: add visibleMask 2025-01-23 14:00:16 -08:00
outfoxxed 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
outfoxxed c6791cf1f2
core/window: fix screen assignments being completely broken 2025-01-22 20:13:29 -08:00
outfoxxed 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
outfoxxed 3c7dfcb220
hyprland/ipc: handle renameworkspace 2025-01-22 04:16:08 -08:00
outfoxxed b336129c34
core/window: add QsWindow.devicePixelRatio 2025-01-22 03:33:46 -08:00
outfoxxed bc73d35d03
wayland/screencopy: fix ScreencopyContext leak in ScreencopyView
Also caused an FD leak.
2025-01-20 15:53:04 -08:00
outfoxxed 6464ead0f1
core/window: move input mask handling + commit scheduling to polish 2025-01-20 01:14:28 -08:00
outfoxxed d6b58521e9
core!: fix typo in ShellScreen.primaryOrientation 2025-01-19 01:00:03 -08:00
outfoxxed 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
outfoxxed ca79715cce
wayland/screencopy: log more information during buffer creation 2025-01-15 02:52:08 -08:00
outfoxxed c2ed5bf559
core/stacklist: add tests 2025-01-15 02:47:14 -08:00
outfoxxed 6024c37492
core/scriptmodel: improve docs 2025-01-14 15:30:48 -08:00
outfoxxed 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
outfoxxed cd429142a4
wayland/screencopy: add screencopy 2025-01-14 05:08:07 -08:00
outfoxxed 918dd2392d
build/wayland: do not link to a target in wl_proto 2025-01-11 23:59:19 -08:00
outfoxxed 2c411fce5a
all: fix new lints 2025-01-07 03:11:19 -08:00
outfoxxed 26d443aa50
ci: add 6.8.1 2025-01-06 22:21:32 -08:00
outfoxxed af86d5fd19
hyprland/surface: remove debug print 2025-01-05 23:53:03 -08:00
outfoxxed 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
outfoxxed fca058e66c
service/upower: add device model property 2025-01-04 04:38:03 -08:00
outfoxxed eaf854935b
service/upower: correctly deserialize UPowerDeviceState::Discharging
???
2025-01-04 03:37:53 -08:00
outfoxxed f3b7171b25
core/window: allow explicit surface format selection 2025-01-04 03:04:41 -08:00
outfoxxed dc3a79600d
core/command: avoid running when cli11 forces returning 0
Fixes running when --help is passed.
2025-01-03 02:42:32 -08:00
outfoxxed 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-forward-declararion-namespace,
-bugprone-forward-declararion-namespace,
-bugprone-return-const-ref-from-parameter,
concurrency-*,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,
@ -44,6 +45,7 @@ Checks: >
-readability-container-data-pointer,
-readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator,
-readability-math-missing-parentheses,
tidyfox-*,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true

View file

@ -6,7 +6,7 @@ jobs:
name: Nix
strategy:
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]
runs-on: ubuntu-latest
steps:
@ -43,6 +43,7 @@ jobs:
qt6-shadertools \
wayland-protocols \
wayland \
libdrm \
libxcb \
libpipewire \
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`
#### 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
This feature enables x11 support. Currently this implements panel windows for X11 similarly
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_FOCUS_GRAB " Hyprland Focus Grabbing" 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(I3 "I3/Sway" ON)
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/util.cmake)
add_compile_options(-Wall -Wextra)
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
# pipewire defines this, breaking PCH
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
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:
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:
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='':
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
# instead of the bumped version.
qt6_8_1 = byCommit {
commit = "3df3c47c19dc90fec35359e89ffb52b34d2b0e94";
sha256 = "1lhlm7czhwwys5ak6ngb5li6bxddilb9479k9nkss502kw8hwjyz";
};
qt6_8_0 = byCommit {
commit = "23e89b7da85c3640bbc2173fe04f4bd114342367";
sha256 = "1b2v6y3bja4br5ribh9lj6xzz2k81dggz708b2mib83rwb509wyb";
commit = "352f462ad9d2aa2cde75fdd8f1734e86402a3ff6";
sha256 = "02zfgkr9fpd6iwfh6dcr3m6fnx61jppm3v081f3brvkqwmmz7zq1";
};
qt6_7_3 = byCommit {

View file

@ -14,6 +14,8 @@
jemalloc,
wayland,
wayland-protocols,
libdrm,
libgbm ? null,
xorg,
pipewire,
pam,
@ -64,7 +66,7 @@
++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc
++ 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 withPam pam
++ lib.optional withPipewire pipewire;
@ -79,6 +81,7 @@
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "HYPRLAND" withHyprland)

View file

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

View file

@ -6,8 +6,6 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include "util.hpp"
SystemClock::SystemClock(QObject* parent): QObject(parent) {
QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout);
this->update();
@ -48,19 +46,16 @@ void SystemClock::update() {
void SystemClock::setTime(const QDateTime& targetTime) {
auto currentTime = QDateTime::currentDateTime();
auto offset = currentTime.msecsTo(targetTime);
auto dtime = offset > -500 && offset < 500 ? targetTime : currentTime;
auto time = dtime.time();
this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime;
auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0);
auto time = this->currentTime.time();
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;
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);
emit this->dateChanged();
}
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 baseTimeT = nextTime.time();
nextTime.setTime(
{hourPrecision ? baseTimeT.hour() : 0,
minutePrecision ? baseTimeT.minute() : 0,
secondPrecision ? baseTimeT.second() : 0}
);
nextTime.setTime(QTime(
hourPrecision ? baseTimeT.hour() : 0,
minutePrecision ? baseTimeT.minute() : 0,
secondPrecision ? baseTimeT.second() : 0
));
if (secondPrecision) nextTime = nextTime.addSecs(1);
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->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 <qtypes.h>
#include "util.hpp"
///! 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 {
Q_OBJECT;
/// 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);
/// The precision the clock should measure at. Defaults to `SystemClock.Seconds`.
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.
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`.
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`.
Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged);
Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged);
QML_ELEMENT;
public:
@ -43,12 +65,15 @@ public:
[[nodiscard]] SystemClock::Enum precision() const;
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:
void enabledChanged();
void precisionChanged();
void hoursChanged();
void minutesChanged();
void secondsChanged();
void dateChanged();
private slots:
void onTimeout();
@ -56,17 +81,11 @@ private slots:
private:
bool mEnabled = true;
SystemClock::Enum mPrecision = SystemClock::Seconds;
quint32 mHours = 0;
quint32 mMinutes = 0;
quint32 mSeconds = 0;
QTimer timer;
QDateTime currentTime;
QDateTime targetTime;
void update();
void setTime(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"
namespace {
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
}
struct Locale {
explicit Locale() = default;
@ -78,6 +80,7 @@ struct Locale {
QString modifier;
};
// NOLINTNEXTLINE(misc-use-internal-linkage)
QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
@ -210,7 +213,7 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
currentArgument += c;
escape = 0;
} else if (c == u'"') {
} else if (c == u'"' || c == u'\'') {
parsingString = false;
} else {
currentArgument += c;
@ -226,7 +229,7 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
percent = false;
} else if (c == '%') {
percent = true;
} else if (c == u'"') {
} else if (c == u'"' || c == u'\'') {
parsingString = true;
} else if (c == u' ') {
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);
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()) {
auto path = entry.filePath();
if (!path.endsWith(".desktop")) {
@ -314,9 +317,8 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue;
}
auto* file = new QFile(path);
if (!file->open(QFile::ReadOnly)) {
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) {
qCDebug(logDesktopEntry) << "Could not open file" << path;
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 lowerId = id.toLower();
auto text = QString::fromUtf8(file->readAll());
auto text = QString::fromUtf8(file.readAll());
auto* dentry = new DesktopEntry(id, this);
dentry->parseEntry(text);

View file

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

View file

@ -1,5 +1,6 @@
#include "imageprovider.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qimage.h>
#include <qlogging.h>
@ -7,17 +8,30 @@
#include <qobject.h>
#include <qpixmap.h>
#include <qqmlengine.h>
#include <qtypes.h>
static QMap<QString, QsImageHandle*> liveImages; // NOLINT
namespace {
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent)
: QObject(parent)
, type(type) {
{
auto dbg = QDebug(&this->id);
dbg.nospace() << static_cast<void*>(this);
namespace {
QMap<QString, QsImageHandle*> liveImages; // NOLINT
quint32 handleIndex = 0; // NOLINT
} // namespace
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);
}
@ -43,16 +57,6 @@ QPixmap QsImageHandle::
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) {
QString target;
QString param;
@ -81,3 +85,9 @@ QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& req
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;
};
class QsImageHandle: public QObject {
Q_OBJECT;
class QsImageHandle {
public:
explicit QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent = nullptr);
~QsImageHandle() override;
explicit QsImageHandle(QQmlImageProviderBase::ImageType type);
virtual ~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 QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize);
@ -37,3 +35,14 @@ private:
QQmlImageProviderBase::ImageType type;
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",
"qsmenuanchor.hpp",
"clock.hpp",
"scriptmodel.hpp",
]
-----

View file

@ -15,7 +15,9 @@
#include "instanceinfo.hpp"
namespace {
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
}
QsPaths* QsPaths::instance() {
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::initPlugins() {
plugins.erase(
std::remove_if(
plugins.begin(),
plugins.end(),
[](QsEnginePlugin* plugin) { return !plugin->applies(); }
),
plugins.end()
);
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
std::sort(plugins.begin(), plugins.end(), [](QsEnginePlugin* a, QsEnginePlugin* b) {
std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) {
return b->dependencies().contains(a->name());
});

View file

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

View file

@ -42,6 +42,24 @@ QString QuickshellScreenInfo::name() const {
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 {
if (this->screen == nullptr) {
this->warnDangling();

View file

@ -29,6 +29,10 @@ class QuickshellScreenInfo: public QObject {
///
/// Usually something like `DP-1`, `HDMI-1`, `eDP-1`.
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 y READ y 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.
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged);
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
public:
@ -49,6 +53,8 @@ public:
bool operator==(QuickshellScreenInfo& other) const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString model() const;
[[nodiscard]] QString serialNumber() const;
[[nodiscard]] qint32 x() const;
[[nodiscard]] qint32 y() const;
[[nodiscard]] qint32 width() const;

View file

@ -8,6 +8,7 @@
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvectornd.h>
PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed);
@ -105,8 +106,19 @@ QRegion PendingRegion::applyTo(QRegion& region) const {
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) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
if (!region) return;
QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed);
QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged);

View file

@ -96,6 +96,7 @@ public:
[[nodiscard]] bool empty() const;
[[nodiscard]] QRegion build() const;
[[nodiscard]] QRegion applyTo(QRegion& region) const;
[[nodiscard]] QRegion applyTo(const QRect& rect) const;
RegionShape::Enum mShape = RegionShape::Rect;
Intersection::Enum mIntersection = Intersection::Combine;
@ -109,6 +110,11 @@ signals:
void widthChanged();
void heightChanged();
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();
private slots:

View file

@ -36,6 +36,8 @@
/// 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 {
Q_OBJECT;
/// 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.
///
/// > [!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);
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(ringbuffer ringbuf.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;
}
// NOLINTNEXTLINE(misc-use-internal-linkage)
QDebug& operator<<(QDebug& debug, const ModelOperation& op) {
auto saver = QDebugStateSaver(debug);
debug.nospace();
@ -43,6 +44,7 @@ QDebug& operator<<(QDebug& debug, const ModelOperation& op) {
return debug;
}
// NOLINTNEXTLINE(misc-use-internal-linkage)
QDebug& operator<<(QDebug& debug, const QVariantList& list) {
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>
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));
}

View file

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

View file

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

View file

@ -22,49 +22,10 @@
#include "build.hpp"
#include "interface.hpp"
namespace {
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) {
QFile sourceFile;
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.";
}
} // 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 {
Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
}
void tryLaunchService(
QObject* parent,

View file

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

View file

@ -30,7 +30,17 @@ namespace qs::dbus::dbusmenu {
using menu::QsMenuEntry;
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 via the
@ -93,7 +103,7 @@ private:
bool visible = true;
bool mSeparator = false;
QString iconName;
DBusMenuPngImage* image = nullptr;
DBusMenuPngImage image;
menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None;
Qt::CheckState mCheckState = Qt::Unchecked;
bool displayChildren = false;
@ -156,17 +166,6 @@ private:
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;
QDebug operator<<(QDebug debug, const DBusMenuHandle* handle);

View file

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

View file

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

View file

@ -13,10 +13,12 @@
namespace qs::debug {
namespace {
Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg);
void lintZeroSized(QQuickItem* item);
bool isRenderable(QQuickItem* item);
} // namespace
void lintObjectTree(QObject* object) {
if (!logLint().isWarningEnabled()) return;
@ -41,6 +43,8 @@ void lintItemTree(QQuickItem* item) {
}
}
namespace {
void lintZeroSized(QQuickItem* item) {
if (!item->isEnabled() || !item->isVisible()) 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); });
}
} // namespace
} // namespace qs::debug

View file

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

View file

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

View file

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

View file

@ -21,16 +21,17 @@ namespace qs::io::ipc::comm {
struct NoCurrentGeneration: std::monostate {};
struct TargetNotFound: std::monostate {};
struct FunctionNotFound: std::monostate {};
struct EntryNotFound: std::monostate {};
using QueryResponse = std::variant<
std::monostate,
NoCurrentGeneration,
TargetNotFound,
FunctionNotFound,
EntryNotFound,
QVector<WireTargetDefinition>,
WireTargetDefinition,
WireFunctionDefinition>;
WireFunctionDefinition,
WirePropertyDefinition>;
void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto resp = conn->responseStream<QueryResponse>();
@ -44,16 +45,24 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto* handler = registry->findHandler(this->target);
if (handler) {
if (this->function.isEmpty()) {
if (this->name.isEmpty()) {
resp << handler->wireDef();
} else {
auto* func = handler->findFunction(this->function);
auto* func = handler->findFunction(this->name);
if (func) {
resp << func->wireDef();
} else {
resp << FunctionNotFound();
return;
}
auto* prop = handler->findProperty(this->name);
if (prop) {
resp << prop->wireDef();
return;
}
resp << EntryNotFound();
}
} else {
resp << TargetNotFound();
@ -64,8 +73,8 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
}
}
int queryMetadata(IpcClient* client, const QString& target, const QString& function) {
client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .function = function}));
int queryMetadata(IpcClient* client, const QString& target, const QString& name) {
client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .name = name}));
QueryResponse slot;
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();
} else if (std::holds_alternative<WireFunctionDefinition>(slot)) {
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)) {
qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<FunctionNotFound>(slot)) {
} else if (std::holds_alternative<EntryNotFound>(slot)) {
qCCritical(logBare) << "Function not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
qCCritical(logBare) << "Not ready to accept queries yet.";
@ -119,7 +130,7 @@ using StringCallResponse = std::variant<
std::monostate,
NoCurrentGeneration,
TargetNotFound,
FunctionNotFound,
EntryNotFound,
ArgParseFailed,
Completed>;
@ -137,7 +148,7 @@ void StringCallCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto* func = handler->findFunction(this->function);
if (!func) {
resp << FunctionNotFound();
resp << EntryNotFound();
return;
}
@ -223,7 +234,7 @@ int callFunction(
qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString();
} else if (std::holds_alternative<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<FunctionNotFound>(slot)) {
} else if (std::holds_alternative<EntryNotFound>(slot)) {
qCCritical(logBare) << "Function not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
qCCritical(logBare) << "Not ready to accept queries yet.";
@ -233,4 +244,74 @@ int callFunction(
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

View file

@ -2,6 +2,7 @@
#include <qcontainerfwd.h>
#include <qflags.h>
#include <qtypes.h>
#include "../ipc/ipc.hpp"
@ -9,12 +10,12 @@ namespace qs::io::ipc::comm {
struct QueryMetadataCommand {
QString target;
QString function;
QString name;
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 {
QString target;
@ -27,7 +28,7 @@ struct StringCallCommand {
DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments);
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(
qs::ipc::IpcClient* client,
@ -36,4 +37,15 @@ int callFunction(
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

View file

@ -107,6 +107,32 @@ WireFunctionDefinition IpcFunction::wireDef() const {
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) {
for (const auto& arg: function.argumentTypes) {
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->updateRegistration();
@ -270,6 +311,10 @@ WireTargetDefinition IpcHandler::wireDef() const {
wire.functions += func.wireDef();
}
for (const auto& prop: this->propertyMap.values()) {
wire.properties += prop.wireDef();
}
return wire;
}
@ -307,6 +352,13 @@ IpcFunction* IpcHandler::findFunction(const QString& name) {
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) {
return this->handlers.value(target);
}

View file

@ -53,14 +53,28 @@ private:
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;
///! Handler for IPC message calls.
/// 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
/// 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.
///
/// **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
/// $ qs msg -s
/// $ qs ipc show
/// target rect
/// function setColor(color: color): void
/// function getColor(): color
@ -124,18 +138,22 @@ class IpcHandlerRegistry;
/// function getRadius(): int
/// ```
///
/// and then invoked using `qs msg`.
/// and then invoked using `qs ipc call`.
/// ```sh
/// $ qs msg rect setColor orange
/// $ qs msg rect setAngle 40.5
/// $ qs msg rect setRadius 30
/// $ qs msg rect getColor
/// $ qs ipc call rect setColor orange
/// $ qs ipc call rect setAngle 40.5
/// $ qs ipc call rect setRadius 30
/// $ qs ipc call rect getColor
/// #ffffa500
/// $ qs msg rect getAngle
/// $ qs ipc call rect getAngle
/// 40.5
/// $ qs msg rect getRadius
/// $ qs ipc call rect getRadius
/// 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
: public QObject
, public PostReloadHook {
@ -162,12 +180,16 @@ public:
QString listMembers(qsizetype indent);
[[nodiscard]] IpcFunction* findFunction(const QString& name);
[[nodiscard]] IpcProperty* findProperty(const QString& name);
[[nodiscard]] WireTargetDefinition wireDef() const;
signals:
void enabledChanged();
void targetChanged();
private slots:
//void handleIpcPropertyChange();
private:
void updateRegistration(bool destroying = false);
@ -183,6 +205,7 @@ private:
bool complete = false;
QHash<QString, IpcFunction> functionMap;
QHash<QString, IpcProperty> propertyMap;
friend class IpcHandlerRegistry;
};

View file

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

View file

@ -34,16 +34,310 @@ namespace qs::launch {
using qs::ipc::IpcClient;
int readLogFile(CommandState& cmd);
int listInstances(CommandState& cmd);
int killInstances(CommandState& cmd);
int msgInstance(CommandState& cmd);
int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication);
int locateConfigFile(CommandState& cmd, QString& path);
namespace {
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, 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) {
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 (strcmp(qVersion(), QT_VERSION_STR) != 0) {
@ -126,8 +420,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
return listInstances(state);
} else if (*state.subcommand.kill) {
return killInstances(state);
} else if (*state.subcommand.msg) {
return msgInstance(state);
} else if (*state.subcommand.msg || *state.ipc.ipc) {
return ipcCommand(state);
} else {
if (strcmp(qVersion(), QT_VERSION_STR) != 0) {
qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR
@ -142,306 +436,4 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
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

View file

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

View file

@ -49,6 +49,7 @@ struct CommandState {
QStringOption path;
QStringOption manifest;
QStringOption name;
bool newest = false;
} config;
struct {
@ -67,9 +68,13 @@ struct CommandState {
} output;
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 function;
QStringOption name;
std::vector<QStringOption> arguments;
} ipc;

View file

@ -22,36 +22,7 @@
namespace qs::launch {
void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication);
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";
}
}
namespace {
void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
#if CRASH_REPORTER
@ -96,6 +67,37 @@ void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
#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) {
QCoreApplication::setApplicationName("quickshell");

View file

@ -16,7 +16,7 @@ int parseCommand(int argc, char** argv, CommandState& state) {
.argv = argv,
};
auto addConfigSelection = [&](CLI::App* cmd) {
auto addConfigSelection = [&](CLI::App* cmd, bool withNewestOption = false) {
auto* group = cmd->add_option_group("Config Selection")
->description("If no options in this group are specified,\n"
"$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.")
->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;
};
@ -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.");
auto* instance = addInstanceSelection(sub)->excludes(file);
addConfigSelection(sub)->excludes(instance)->excludes(file);
addConfigSelection(sub, true)->excludes(instance)->excludes(file);
addLoggingOptions(sub, false);
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.");
addConfigSelection(sub)->excludes(all);
addConfigSelection(sub, true)->excludes(all);
addLoggingOptions(sub, false, true);
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.");
//sub->add_flag("-a,--all", "Kill all matching instances instead of just one.");
auto* instance = addInstanceSelection(sub);
addConfigSelection(sub)->excludes(instance);
addConfigSelection(sub, true)->excludes(instance);
addLoggingOptions(sub, false, true);
state.subcommand.kill = sub;
}
{
auto* sub = cli->add_subcommand("msg", "Send messages to IpcHandlers.")->require_option();
auto* target = sub->add_option("target", state.ipc.target, "The target to message.");
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* sub = cli->add_subcommand("ipc", "Communicate with other Quickshell instances.")
->require_subcommand();
state.ipc.ipc = sub;
auto* instance = addInstanceSelection(sub);
addConfigSelection(sub)->excludes(instance);
addConfigSelection(sub, true)->excludes(instance);
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;
}
CLI11_PARSE(*cli, argc, argv);
return 0;
return 65535;
}
} // namespace qs::launch

View file

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

View file

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

View file

@ -391,6 +391,8 @@ private slots:
private:
void onMetadataChanged();
void onPositionUpdated();
// call instead of setting bpPosition
void setPosition(qlonglong position);
void requestPositionUpdate() { this->pPosition.requestUpdate(); };
// clang-format off
@ -457,7 +459,7 @@ private:
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanSeek, bpCanSeek, playerProperties, "CanSeek");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanGoNext, bpCanGoNext, playerProperties, "CanGoNext");
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, pMetadata, bpMetadata, playerProperties, "Metadata");
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, pMaxRate, bMaxRate, playerProperties, "MaximumRate", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pShuffle, bShuffle, playerProperties, "Shuffle", false);
QS_BINDING_SUBSCRIBE_METHOD(MprisPlayer, bpPosition, onPositionUpdated, onValueChanged);
// clang-format on
QDateTime lastPositionTimestamp;

View file

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

View file

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

View file

@ -1,7 +1,5 @@
#pragma once
#include <utility>
#include <qdbusargument.h>
#include <qimage.h>
#include <qobject.h>
@ -23,14 +21,22 @@ struct DBusNotificationImage {
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap);
class NotificationImage: public QsImageHandle {
class NotificationImage: public QsIndexedImageHandle {
public:
explicit NotificationImage(DBusNotificationImage image, QObject* parent)
: QsImageHandle(QQuickAsyncImageProvider::Image, parent)
, image(std::move(image)) {}
explicit NotificationImage(): QsIndexedImageHandle(QQuickAsyncImageProvider::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;
private:
DBusNotificationImage image;
};
} // namespace qs::service::notifications

View file

@ -1,5 +1,4 @@
#include "notification.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qdbusargument.h>
@ -18,6 +17,7 @@
namespace qs::service::notifications {
// NOLINTNEXTLINE(misc-use-internal-linkage)
Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp
QString NotificationUrgency::toString(NotificationUrgency::Enum value) {
@ -116,13 +116,12 @@ void Notification::updateProperties(
QString imagePath;
if (!imageDataName.isEmpty()) {
if (imageDataName.isEmpty()) {
this->mImagePixmap.clear();
} else {
auto value = hints.value(imageDataName).value<QDBusArgument>();
DBusNotificationImage image;
value >> image;
if (this->mImagePixmap) this->mImagePixmap->deleteLater();
this->mImagePixmap = new NotificationImage(std::move(image), this);
imagePath = this->mImagePixmap->url();
value >> this->mImagePixmap.writeImage();
imagePath = this->mImagePixmap.url();
}
// don't store giant byte arrays longer than necessary
@ -130,7 +129,7 @@ void Notification::updateProperties(
hints.remove("image_data");
hints.remove("icon_data");
if (!this->mImagePixmap) {
if (!this->mImagePixmap.hasData()) {
QString imagePathName;
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/util.hpp"
#include "dbusimage.hpp"
namespace qs::service::notifications {
class NotificationImage;
///! The urgency level of a Notification.
/// See @@Notification.urgency.
class NotificationUrgency: public QObject {
@ -187,7 +186,7 @@ private:
quint32 mId;
NotificationCloseReason::Enum mCloseReason = NotificationCloseReason::Dismissed;
bool mLastGeneration = false;
NotificationImage* mImagePixmap = nullptr;
NotificationImage mImagePixmap;
QList<NotificationAction*> mActions;
// clang-format off

View file

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

View file

@ -10,12 +10,28 @@
#include <qobject.h>
#include <qsocketnotifier.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <spa/utils/defs.h>
#include <spa/utils/hook.h>
namespace qs::service::pipewire {
namespace {
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) {
qCInfo(logLoop) << "Creating pipewire event loop.";
@ -40,6 +56,8 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read
return;
}
pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this);
qCInfo(logLoop) << "Linking pipewire event loop.";
// Tie the pw event loop into qt.
auto fd = pw_loop_get_fd(this->loop);
@ -77,6 +95,16 @@ void PwCore::poll() {
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
spa_zero(this->hook);
}

View file

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

View file

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

View file

@ -23,7 +23,9 @@
namespace qs::service::pipewire {
namespace {
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/pipewire/blob/48c2e9516585ccc791335bc7baf4af6952ec54a0/src/modules/module-protocol-pulse/pulse-server.c#L2743-L2743

View file

@ -14,7 +14,9 @@
namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg);
}
QString PwLinkState::toString(Enum 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 {
Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg);
}
void PwMetadata::bindHooks() {
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/type.h>
#include "connection.hpp"
#include "core.hpp"
#include "device.hpp"
namespace qs::service::pipewire {
namespace {
Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg);
}
QString PwAudioChannel::toString(Enum value) {
switch (value) {
@ -90,6 +94,12 @@ void PwNode::bindHooks() {
}
void PwNode::unbindHooks() {
if (this->ready) {
this->ready = false;
emit this->readyChanged();
}
this->syncSeq = 0;
this->listener.remove();
this->routeDevice = -1;
this->properties.clear();
@ -199,6 +209,20 @@ void PwNode::onInfo(void* data, const pw_node_info* info) {
if (self->boundData != nullptr) {
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(

View file

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

View file

@ -2,6 +2,7 @@
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
@ -87,6 +88,16 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
this,
&Pipewire::defaultConfiguredAudioSourceChanged
);
if (!connection->registry.isInitialized()) {
QObject::connect(
&connection->registry,
&PwRegistry::initialized,
this,
&Pipewire::readyChanged,
Qt::SingleShotConnection
);
}
}
ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; }
@ -156,6 +167,8 @@ void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) {
PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr);
}
bool Pipewire::isReady() { return PwConnection::instance()->registry.isInitialized(); }
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
void PwNodeLinkTracker::setNode(PwNodeIface* node) {
@ -298,6 +311,7 @@ void PwNodeAudioIface::setVolumes(const QVector<float>& volumes) {
PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) {
QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged);
QObject::connect(node, &PwNode::readyChanged, this, &PwNodeIface::readyChanged);
if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) {
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::isReady() const { return this->mNode->ready; }
QVariantMap PwNodeIface::properties() const {
auto map = QVariantMap();
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.
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
QML_ELEMENT;
QML_SINGLETON;
@ -136,6 +143,8 @@ public:
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const;
static void setDefaultConfiguredAudioSource(PwNodeIface* node);
[[nodiscard]] static bool isReady();
signals:
void defaultAudioSinkChanged();
void defaultAudioSourceChanged();
@ -143,6 +152,8 @@ signals:
void defaultConfiguredAudioSinkChanged();
void defaultConfiguredAudioSourceChanged();
void readyChanged();
private slots:
void onNodeAdded(PwNode* node);
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
/// 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);
/// 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_UNCREATABLE("PwNodes cannot be created directly");
@ -307,6 +323,7 @@ public:
[[nodiscard]] QString nickname() const;
[[nodiscard]] bool isSink() const;
[[nodiscard]] bool isStream() const;
[[nodiscard]] bool isReady() const;
[[nodiscard]] QVariantMap properties() const;
[[nodiscard]] PwNodeAudioIface* audio() const;
@ -314,6 +331,7 @@ public:
signals:
void propertiesChanged();
void readyChanged();
private:
PwNode* mNode;

View file

@ -126,6 +126,29 @@ void PwRegistry::init(PwCore& core) {
this->core = &core;
this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0);
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 = {

View file

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

View file

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

View file

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

View file

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

View file

@ -54,6 +54,14 @@ private:
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 {
Q_OBJECT;
QML_NAMED_ELEMENT(UPower);

View file

@ -15,7 +15,9 @@ using namespace qs::dbus;
namespace qs::service::upower {
namespace {
Q_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg);
}
QString UPowerDeviceState::toString(UPowerDeviceState::Enum status) {
switch (status) {
@ -114,9 +116,7 @@ DBusResult<qreal> DBusDataTransform<PowerPercentage>::fromWire(qreal wire) {
DBusResult<UPowerDeviceState::Enum>
DBusDataTransform<UPowerDeviceState::Enum>::fromWire(quint32 wire) {
if (wire != UPowerDeviceType::Battery && wire >= UPowerDeviceState::Unknown
&& wire <= UPowerDeviceState::PendingDischarge)
{
if (wire >= UPowerDeviceState::Unknown && wire <= UPowerDeviceState::PendingDischarge) {
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);
/// Native path of the device specific to your OS.
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.
/// 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.
@ -186,6 +188,7 @@ public:
QS_BINDABLE_GETTER(QString, bIconName, iconName, bindableIconName);
QS_BINDABLE_GETTER(bool, bIsLaptopBattery, isLaptopBattery, bindableIsLaptopBattery);
QS_BINDABLE_GETTER(QString, bNativePath, nativePath, bindableNativePath);
QS_BINDABLE_GETTER(QString, bModel, model, bindableModel);
QS_BINDABLE_GETTER(bool, bReady, ready, bindableReady);
signals:
@ -206,6 +209,7 @@ signals:
void iconNameChanged();
void isLaptopBatteryChanged();
void nativePathChanged();
void modelChanged();
private slots:
void onGetAllFinished();
@ -227,6 +231,7 @@ private:
Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bIconName, &UPowerDevice::iconNameChanged);
Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, bool, bIsLaptopBattery, &UPowerDevice::isLaptopBatteryChanged);
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);
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, pIconName, bIconName, deviceProperties, "IconName");
QS_DBUS_PROPERTY_BINDING(UPowerDevice, pNativePath, bNativePath, deviceProperties, "NativePath");
QS_DBUS_PROPERTY_BINDING(UPowerDevice, pModel, bModel, deviceProperties, "Model");
// clang-format on
DBusUPowerDevice* device = nullptr;

View file

@ -3,5 +3,6 @@ description = "UPower Service"
headers = [
"core.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>
)
function (wl_proto target name path)
function (wl_proto target name dir)
set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto/${name})
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(QWS_CLIENT_HEADER "${PROTO_BUILD_PATH}/qwayland-${name}.h")
set(QWS_CLIENT_CODE "${PROTO_BUILD_PATH}/qwayland-${name}.cpp")
set(PATH "${dir}/${name}.xml")
add_custom_command(
OUTPUT "${WS_CLIENT_HEADER}"
COMMAND Wayland::Scanner client-header "${path}" "${WS_CLIENT_HEADER}"
DEPENDS Wayland::Scanner "${path}"
COMMAND Wayland::Scanner client-header "${PATH}" "${WS_CLIENT_HEADER}"
DEPENDS Wayland::Scanner "${PATH}"
)
add_custom_command(
OUTPUT "${WS_CLIENT_CODE}"
COMMAND Wayland::Scanner private-code "${path}" "${WS_CLIENT_CODE}"
DEPENDS Wayland::Scanner "${path}"
COMMAND Wayland::Scanner private-code "${PATH}" "${WS_CLIENT_CODE}"
DEPENDS Wayland::Scanner "${PATH}"
)
add_custom_command(
OUTPUT "${QWS_CLIENT_HEADER}"
COMMAND Qt6::qtwaylandscanner client-header "${path}" > "${QWS_CLIENT_HEADER}"
DEPENDS Qt6::qtwaylandscanner "${path}"
COMMAND Qt6::qtwaylandscanner client-header "${PATH}" > "${QWS_CLIENT_HEADER}"
DEPENDS Qt6::qtwaylandscanner "${PATH}"
)
add_custom_command(
OUTPUT "${QWS_CLIENT_CODE}"
COMMAND Qt6::qtwaylandscanner client-code "${path}" > "${QWS_CLIENT_CODE}"
DEPENDS Qt6::qtwaylandscanner "${path}"
COMMAND Qt6::qtwaylandscanner client-code "${PATH}" > "${QWS_CLIENT_CODE}"
DEPENDS Qt6::qtwaylandscanner "${PATH}"
)
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_link_libraries(wl-proto-${name} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate)
qs_pch(wl-proto-${name} SET wayland-protocol)
target_link_libraries(${target} PRIVATE wl-proto-${name})
target_include_directories(${target} INTERFACE ${PROTO_BUILD_PATH})
target_link_libraries(${target} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate)
qs_pch(${target} SET wayland-protocol)
endfunction()
# -----
@ -104,6 +103,13 @@ if (WAYLAND_TOPLEVEL_MANAGEMENT)
list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement)
endif()
if (SCREENCOPY)
add_subdirectory(buffer)
add_subdirectory(screencopy)
list(APPEND WAYLAND_MODULES Quickshell.Wayland._Screencopy)
endif()
if (HYPRLAND)
add_subdirectory(hyprland)
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)
wl_proto(quickshell-hyprland-focus-grab
hyprland-focus-grab-v1
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml"
)
wl_proto(wlp-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(quickshell-hyprland-focus-grab PRIVATE
Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
wlp-hyprland-focus-grab
)
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)
wl_proto(quickshell-hyprland-global-shortcuts
hyprland-global-shortcuts-v1
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml"
)
wl_proto(wlp-hyprland-shortcuts hyprland-global-shortcuts-v1 "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE
Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
Qt::Quick # pch
wlp-hyprland-shortcuts
)
qs_module_pch(quickshell-hyprland-global-shortcuts)

View file

@ -26,8 +26,10 @@
namespace qs::hyprland::ipc {
namespace {
Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg);
Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg);
} // namespace
HyprlandIpc::HyprlandIpc() {
auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
@ -241,9 +243,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
const auto& mList = this->mMonitors.valueList();
auto name = QString::fromUtf8(event->data);
auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) {
return m->name() == name;
});
auto monitorIter =
std::ranges::find_if(mList, [name](const HyprlandMonitor* m) { return m->name() == name; });
if (monitorIter == mList.end()) {
qCWarning(logHyprlandIpc) << "Got removal for monitor" << name
@ -292,9 +293,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
const auto& mList = this->mWorkspaces.valueList();
auto workspaceIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandWorkspace* m) {
return m->id() == id;
});
auto workspaceIter =
std::ranges::find_if(mList, [id](const HyprlandWorkspace* m) { return m->id() == id; });
if (workspaceIter == mList.end()) {
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);
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) {
const auto& mList = this->mWorkspaces.valueList();
auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) {
return m->name() == name;
});
auto workspaceIter =
std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) { return m->name() == name; });
if (workspaceIter != mList.end()) {
return *workspaceIter;
@ -395,10 +410,9 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) {
auto object = entry.toObject().toVariantMap();
auto name = object.value("name").toString();
auto workspaceIter =
std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) {
return m->name() == name;
});
auto workspaceIter = std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) {
return m->name() == name;
});
auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter;
auto existed = workspace != nullptr;
@ -436,9 +450,8 @@ HyprlandMonitor*
HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) {
const auto& mList = this->mMonitors.valueList();
auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) {
return m->name() == name;
});
auto monitorIter =
std::ranges::find_if(mList, [name](const HyprlandMonitor* m) { return m->name() == name; });
if (monitorIter != mList.end()) {
return *monitorIter;
@ -506,7 +519,7 @@ void HyprlandIpc::refreshMonitors(bool canCreate) {
auto object = entry.toObject().toVariantMap();
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;
});

View file

@ -11,7 +11,15 @@
namespace qs::hyprland::ipc {
qint32 HyprlandWorkspace::id() const { return this->mId; }
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; }
void HyprlandWorkspace::updateInitial(qint32 id, QString name) {

View file

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

View file

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

View file

@ -34,7 +34,7 @@
This protocol exposes hyprland-specific wl_surface properties.
</description>
<interface name="hyprland_surface_manager_v1" version="1">
<interface name="hyprland_surface_manager_v1" version="2">
<description summary="manager for hyprland surface objects">
This interface allows a client to create hyprland surface objects.
</description>
@ -63,7 +63,7 @@
</enum>
</interface>
<interface name="hyprland_surface_v1" version="1">
<interface name="hyprland_surface_v1" version="2">
<description summary="hyprland-specific wl_surface properties">
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="out_of_range" value="1" summary="given opacity was not in the range 0.0 - 1.0 (inclusive)"/>
</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>
</protocol>

View file

@ -7,13 +7,13 @@
namespace qs::hyprland::surface::impl {
HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(1) {
HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(2) {
this->initialize();
}
HyprlandSurface*
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() {

View file

@ -1,17 +1,20 @@
#include "qml.hpp"
#include <memory>
#include <private/qhighdpiscaling_p.h>
#include <private/qwaylandwindow_p.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlinfo.h>
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include <qwindow.h>
#include "../../../core/region.hpp"
#include "../../../window/proxywindow.hpp"
#include "../../../window/windowinterface.hpp"
#include "../../util.hpp"
#include "manager.hpp"
#include "surface.hpp"
@ -28,7 +31,6 @@ HyprlandWindow* HyprlandWindow::qmlAttachedProperties(QObject* object) {
}
}
qDebug() << "hlwindow for" << proxyWindow;
if (!proxyWindow) return nullptr;
return new HyprlandWindow(proxyWindow);
}
@ -41,6 +43,15 @@ HyprlandWindow::HyprlandWindow(ProxyWindowBase* window): QObject(nullptr), proxy
&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);
if (window->backingWindow()) {
@ -61,14 +72,76 @@ void HyprlandWindow::setOpacity(qreal opacity) {
this->mOpacity = opacity;
if (this->surface) {
this->surface->setOpacity(opacity);
qs::wayland::util::scheduleCommit(this->mWaylandWindow);
if (this->surface && this->proxyWindow) {
this->pendingPolish.opacity = true;
this->proxyWindow->schedulePolish();
}
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() {
this->mWindow = this->proxyWindow->backingWindow();
// disconnected by destructor
@ -87,33 +160,46 @@ void HyprlandWindow::onWindowVisibleChanged() {
if (!this->mWindow->handle()) {
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) {
// disconnected by destructor
if (this->mWaylandWindow) {
QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr);
}
QObject::connect(
this->mWaylandWindow,
&QWaylandWindow::surfaceCreated,
this,
&HyprlandWindow::onWaylandSurfaceCreated
);
this->mWaylandWindow = window;
if (!window) return;
QObject::connect(
this->mWaylandWindow,
&QWaylandWindow::surfaceDestroyed,
this,
&HyprlandWindow::onWaylandSurfaceDestroyed
);
QObject::connect(
this->mWaylandWindow,
&QObject::destroyed,
this,
&HyprlandWindow::onWaylandWindowDestroyed
);
if (this->mWaylandWindow->surface()) {
this->onWaylandSurfaceCreated();
}
}
QObject::connect(
this->mWaylandWindow,
&QWaylandWindow::surfaceCreated,
this,
&HyprlandWindow::onWaylandSurfaceCreated
);
QObject::connect(
this->mWaylandWindow,
&QWaylandWindow::surfaceDestroyed,
this,
&HyprlandWindow::onWaylandSurfaceDestroyed
);
if (this->mWaylandWindow->surface()) {
this->onWaylandSurfaceCreated();
}
}
void HyprlandWindow::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; }
void HyprlandWindow::onWaylandSurfaceCreated() {
auto* manager = impl::HyprlandSurfaceManager::instance();
@ -123,12 +209,26 @@ void HyprlandWindow::onWaylandSurfaceCreated() {
return;
}
auto* ext = manager->createHyprlandExtension(this->mWaylandWindow);
this->surface = std::unique_ptr<impl::HyprlandSurface>(ext);
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->mOpacity != 1.0) {
this->surface->setOpacity(this->mOpacity);
qs::wayland::util::scheduleCommit(this->mWaylandWindow);
if (!this->surface) {
auto* ext = manager->createHyprlandExtension(this->mWaylandWindow);
this->surface = std::unique_ptr<impl::HyprlandSurface>(ext);
}
this->mWaylandWindow->setProperty("hyprland_window_ext", QVariant::fromValue(this));
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
// hyprland_surface_v1 and wl_surface objects.
this->proxyWindow = nullptr;
if (this->surface == nullptr) {
this->proxyWindow = nullptr;
this->deleteLater();
}
}

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