Compare commits

...

23 commits

Author SHA1 Message Date
outfoxxed
a1a150fab0
version: bump to 0.2.1 2025-10-11 17:14:14 -07:00
Gregor Kleen
40bc09b800
service/greetd: always send responses 2025-10-11 17:06:06 -07:00
bbedward
a94418f45d
core/desktopentry: don't match keys with wrong modifier or country 2025-10-11 17:06:06 -07:00
bbedward
8d2a2d3dd2
core/desktopentry: mask entries with priority less than hidden entry 2025-10-11 17:06:06 -07:00
outfoxxed
e10747addd
all: fix gcc warnings and lints 2025-10-11 17:06:06 -07:00
outfoxxed
b254f6dabc
nix: remove qtwayland dependency when qt >= 6.10
QtWaylandClient was moved into QtBase in 6.10. The QtWayland packages
is now only the wayland server code which Quickshell does not need.
2025-10-11 17:06:06 -07:00
outfoxxed
7fc6635ca8
wayland/lock: support Qt 6.10 2025-10-11 17:06:06 -07:00
outfoxxed
522d126d1b
services/pipewire: consider device volume step when sending updates
Previously a hardcoded 0.0001 offset was used to determine if a volume
change was significant enough to send to a device, however some
devices have a much more granular step size, which caused future
volume updates to be blocked.

This change replaces the hardcoded offset with the volumeStep device
route property which should be large enough for the device to work with.

Fixes #279
2025-10-11 17:06:06 -07:00
outfoxxed
f5ca8453c0
docs: start tracking qs-next changelog 2025-10-11 17:06:04 -07:00
outfoxxed
e81e6da08f
ci: fix magic-nix-cache write permissions 2025-10-11 17:05:30 -07:00
outfoxxed
f73729754d
build: explicitly depend on private qt modules
In Qt 6.10, private Qt modules must be depended on explicitly.
2025-10-11 17:05:30 -07:00
outfoxxed
344ca340ba
all: fix lints 2025-10-11 17:05:26 -07:00
outfoxxed
6cc1b6f36a
nix: update flake + tidyfox 2025-10-11 17:04:23 -07:00
outfoxxed
354afeaa35
ci: add detsys nix cache 2025-10-11 17:04:23 -07:00
outfoxxed
62df8d917f
ci: use unwrapped package for dependencies derivation
Since adding the wrapper, CI built qs as it was a dependency of the
wrapper instead of dependencies of qs itself.
2025-10-11 17:04:23 -07:00
outfoxxed
2f0f2d1201
ci: add qt 6.9.2 and 6.9.1 checkouts 2025-10-11 17:04:23 -07:00
outfoxxed
4b825e7051
nix: add overlay 2025-10-11 17:04:23 -07:00
outfoxxed
d4b19e4a30
build: fix cross compilation 2025-10-11 17:04:23 -07:00
outfoxxed
b9cce25061
core: derive incubation controllers from tracked windows list
Replaces the attempts to track incubation controllers directly with a
list of all known windows, then pulls the first usable incubation
controller when an assignment is requested.

This should finally fix incubation controller related use after free crashes.
2025-10-11 17:04:23 -07:00
bbedward
996efc93b7
core/desktopentry: watch for changes and rescan entries 2025-10-11 17:04:22 -07:00
outfoxxed
2115f31416
ci: use latest wayland-protocol for all test cases
Fixes missing protocols on old nixpkgs versions
2025-10-11 17:04:22 -07:00
kossLAN
e4d33fa52f
hyprland/ipc: fix focusedWorkspaceChanged connection 2025-10-11 17:04:22 -07:00
Derock
83f5af522d
core/log: fix nullptr crash in ThreadLogging 2025-10-11 17:04:00 -07:00
54 changed files with 863 additions and 418 deletions

View file

@ -6,17 +6,23 @@ jobs:
name: Nix
strategy:
matrix:
qtver: [qt6.9.0, qt6.8.3, qt6.8.2, 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]
qtver: [qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, 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
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- name: Download Dependencies
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation'
- name: Build
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'

View file

@ -5,11 +5,17 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- uses: nicknovitski/nix-develop@v1
- name: Check formatting

View file

@ -55,7 +55,7 @@ On some distros, private Qt headers are in separate packages which you may have
We currently require private headers for the following libraries:
- `qt6declarative`
- `qt6wayland`
- `qt6wayland` (for Qt versions prior to 6.10)
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
svg icons will not work, including system ones.
@ -104,7 +104,7 @@ Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF`
Dependencies:
- `qt6wayland`
- `qt6wayland` (for Qt versions prior to 6.10)
- `wayland` (libwayland-client)
- `wayland-scanner` (build time)
- `wayland-protocols` (static library)

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.2.0" LANGUAGES CXX C)
project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
@ -100,6 +100,7 @@ if (NOT CMAKE_BUILD_TYPE)
endif()
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
set(QT_PRIVDEPS QuickPrivate)
include(cmake/pch.cmake)
@ -115,6 +116,7 @@ endif()
if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
list(APPEND QT_PRIVDEPS WaylandClientPrivate)
endif()
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH)
@ -127,6 +129,13 @@ endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
# In Qt 6.10, private dependencies are required to be explicit,
# but they could not be explicitly depended on prior to 6.9.
if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0")
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt6 REQUIRED COMPONENTS ${QT_PRIVDEPS})
endif()
set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)

View file

@ -12,6 +12,9 @@ lint-ci:
lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-staged:
git diff --staged --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}} \
-DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \

17
changelog/v0.2.1.md Normal file
View file

@ -0,0 +1,17 @@
## New Features
- Changes to desktop entries are now tracked in real time.
## Other Changes
- Added support for Qt 6.10
## Bug Fixes
- Fixed volumes getting stuck on change for pipewire devices with few volume steps.
- Fixed a crash when running out of disk space to write log files.
- Fixed a rare crash when disconnecting a monitor.
- Fixed build issues preventing cross compilation from working.
- Fixed dekstop entries with lower priority than a hidden entry not being hidden.
- Fixed desktop entry keys with mismatched modifier or country not being discarded.
- Fixed greetd hanging when authenticating with a fingerprint.

View file

@ -2,7 +2,10 @@
qtver,
compiler,
}: let
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
checkouts = import ./nix-checkouts.nix;
nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver};
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
pkg = (nixpkgs.callPackage ../default.nix {}).override (compilerOverride // {
wayland-protocols = checkouts.latest.wayland-protocols;
});
in pkg

View file

@ -7,9 +7,18 @@ let
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
inherit sha256;
}) {};
in {
# For old qt versions, grab the commit before the version bump that has all the patches
# instead of the bumped version.
in rec {
latest = qt6_9_0;
qt6_9_2 = byCommit {
commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj";
};
qt6_9_1 = byCommit {
commit = "4c202d26483c5ccf3cb95e0053163facde9f047e";
sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di";
};
qt6_9_0 = byCommit {
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";

View file

@ -2,6 +2,6 @@
clangStdenv,
gccStdenv,
}: {
clang = { buildStdenv = clangStdenv; };
gcc = { buildStdenv = gccStdenv; };
clang = { stdenv = clangStdenv; };
gcc = { stdenv = gccStdenv; };
}

View file

@ -2,8 +2,8 @@
lib,
nix-gitignore,
pkgs,
stdenv,
keepDebugInfo,
buildStdenv ? pkgs.clangStdenv,
pkg-config,
cmake,
@ -44,7 +44,7 @@
withHyprland ? true,
withI3 ? true,
}: let
unwrapped = buildStdenv.mkDerivation {
unwrapped = stdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.2.0";
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
@ -54,11 +54,14 @@
nativeBuildInputs = [
cmake
ninja
qt6.qtshadertools
spirv-tools
pkg-config
]
++ lib.optional withWayland wayland-scanner;
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [
qt6.qtwayland # qtwaylandscanner required at build time
wayland-scanner
];
buildInputs = [
qt6.qtbase
@ -68,7 +71,8 @@
++ lib.optional withQtSvg qt6.qtsvg
++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc
++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ]
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [ wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1749285348,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
"lastModified": 1758690382,
"narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
"rev": "e643668fd71b949c53f8626614b21ff71a07379d",
"type": "github"
},
"original": {

View file

@ -4,23 +4,28 @@
};
outputs = { self, nixpkgs }: let
overlayPkgs = p: p.appendOverlays [ self.overlays.default ];
forEachSystem = fn:
nixpkgs.lib.genAttrs
nixpkgs.lib.platforms.linux
(system: fn system nixpkgs.legacyPackages.${system});
(system: fn system (overlayPkgs nixpkgs.legacyPackages.${system}));
in {
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.callPackage ./default.nix {
gitRev = self.rev or self.dirtyRev;
overlays.default = import ./overlay.nix {
rev = self.rev or self.dirtyRev;
};
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.quickshell;
default = quickshell;
});
devShells = forEachSystem (system: pkgs: rec {
default = import ./shell.nix {
inherit pkgs;
inherit (self.packages.${system}) quickshell;
quickshell = self.packages.${system}.quickshell.override {
stdenv = pkgs.clangStdenv;
};
};
});
};

5
overlay.nix Normal file
View file

@ -0,0 +1,5 @@
{ rev ? null }: (final: prev: {
quickshell = final.callPackage ./default.nix {
gitRev = rev;
};
})

View file

@ -1,14 +1,15 @@
{
pkgs ? import <nixpkgs> {},
quickshell ? pkgs.callPackage ./default.nix {},
stdenv ? pkgs.clangStdenv, # faster compiles than gcc
quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; },
...
}: let
tidyfox = import (pkgs.fetchFromGitea {
domain = "git.outfoxxed.me";
owner = "outfoxxed";
repo = "tidyfox";
rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b";
sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I=";
rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f";
hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w=";
}) { inherit pkgs; };
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
inputsFrom = [ quickshell ];

View file

@ -23,6 +23,7 @@ qt_add_library(quickshell-core STATIC
model.cpp
elapsedtimer.cpp
desktopentry.cpp
desktopentrymonitor.cpp
objectrepeater.cpp
platformmenu.cpp
qsmenu.cpp

View file

@ -28,26 +28,28 @@ ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qrea
: source(source)
, maxDepth(depth)
, rescaleSize(rescaleSize) {
setAutoDelete(false);
this->setAutoDelete(false);
}
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
if (shouldCancel.loadAcquire() || source->isEmpty()) return;
if (shouldCancel.loadAcquire() || this->source->isEmpty()) return;
colors.clear();
this->colors.clear();
auto image = QImage(source->toLocalFile());
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
auto image = QImage(this->source->toLocalFile());
if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
&& this->rescaleSize > 0)
{
image = image.scaled(
static_cast<int>(rescaleSize),
static_cast<int>(rescaleSize),
static_cast<int>(this->rescaleSize),
static_cast<int>(this->rescaleSize),
Qt::KeepAspectRatio,
Qt::SmoothTransformation
);
}
if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString();
qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString();
return;
}
@ -63,7 +65,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCa
auto startTime = QDateTime::currentDateTime();
colors = quantization(pixels, 0);
this->colors = this->quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime);
@ -77,7 +79,7 @@ QList<QColor> ColorQuantizerOperation::quantization(
) {
if (shouldCancel.loadAcquire()) return QList<QColor>();
if (depth >= maxDepth || rgbValues.isEmpty()) {
if (depth >= this->maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList<QColor>();
auto totalR = 0;
@ -114,8 +116,8 @@ QList<QColor> ColorQuantizerOperation::quantization(
auto rightHalf = rgbValues.mid(mid);
QList<QColor> result;
result.append(quantization(leftHalf, depth + 1));
result.append(quantization(rightHalf, depth + 1));
result.append(this->quantization(leftHalf, depth + 1));
result.append(this->quantization(rightHalf, depth + 1));
return result;
}
@ -159,7 +161,7 @@ void ColorQuantizerOperation::finishRun() {
}
void ColorQuantizerOperation::finished() {
emit this->done(colors);
emit this->done(this->colors);
delete this;
}
@ -178,39 +180,39 @@ void ColorQuantizerOperation::run() {
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() {
componentCompleted = true;
if (!mSource.isEmpty()) quantizeAsync();
this->componentCompleted = true;
if (!this->mSource.isEmpty()) this->quantizeAsync();
}
void ColorQuantizer::setSource(const QUrl& source) {
if (mSource != source) {
mSource = source;
if (this->mSource != source) {
this->mSource = source;
emit this->sourceChanged();
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
}
}
void ColorQuantizer::setDepth(qreal depth) {
if (mDepth != depth) {
mDepth = depth;
if (this->mDepth != depth) {
this->mDepth = depth;
emit this->depthChanged();
if (this->componentCompleted) quantizeAsync();
if (this->componentCompleted) this->quantizeAsync();
}
}
void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (mRescaleSize != rescaleSize) {
mRescaleSize = rescaleSize;
if (this->mRescaleSize != rescaleSize) {
this->mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged();
if (this->componentCompleted) quantizeAsync();
if (this->componentCompleted) this->quantizeAsync();
}
}
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
bColors = result;
this->bColors = result;
this->liveOperation = nullptr;
emit this->colorsChanged();
}
@ -219,7 +221,8 @@ void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync();
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
this->liveOperation =
new ColorQuantizerOperation(&this->mSource, this->mDepth, this->mRescaleSize);
QObject::connect(
this->liveOperation,

View file

@ -91,13 +91,13 @@ public:
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
[[nodiscard]] QUrl source() const { return mSource; }
[[nodiscard]] QUrl source() const { return this->mSource; }
void setSource(const QUrl& source);
[[nodiscard]] qreal depth() const { return mDepth; }
[[nodiscard]] qreal depth() const { return this->mDepth; }
void setDepth(qreal depth);
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
[[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
void setRescaleSize(int rescaleSize);
signals:

View file

@ -1,22 +1,27 @@
#include "desktopentry.hpp"
#include <algorithm>
#include <utility>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qhash.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h>
#include <qstringview.h>
#include <qproperty.h>
#include <qscopeguard.h>
#include <qtenvironmentvariables.h>
#include <qthreadpool.h>
#include <qtmetamacros.h>
#include <ranges>
#include "../io/processcore.hpp"
#include "desktopentrymonitor.hpp"
#include "logcat.hpp"
#include "model.hpp"
#include "qmlglobal.hpp"
@ -56,12 +61,14 @@ struct Locale {
[[nodiscard]] int matchScore(const Locale& other) const {
if (this->language != other.language) return 0;
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0;
if (!other.territory.isEmpty() && this->territory != other.territory) return 0;
auto score = 1;
if (territoryMatches) score += 2;
if (modifierMatches) score += 1;
if (!other.territory.isEmpty()) score += 2;
if (!other.modifier.isEmpty()) score += 1;
return score;
}
@ -87,57 +94,60 @@ struct Locale {
QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
<< ", modifier" << locale.modifier << ')';
<< ", modifier=" << locale.modifier << ')';
return debug;
}
void DesktopEntry::parseEntry(const QString& text) {
ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
ParsedDesktopEntryData data;
data.id = id;
const auto& system = Locale::system();
auto groupName = QString();
auto entries = QHash<QString, QPair<Locale, QString>>();
auto finishCategory = [this, &groupName, &entries]() {
auto finishCategory = [&data, &groupName, &entries]() {
if (groupName == "Desktop Entry") {
if (entries["Type"].second != "Application") return;
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
if (entries.value("Type").second != "Application") return;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
auto& [_, value] = pair;
this->mEntries.insert(key, value);
data.entries.insert(key, value);
if (key == "Name") this->mName = value;
else if (key == "GenericName") this->mGenericName = value;
else if (key == "StartupWMClass") this->mStartupClass = value;
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
else if (key == "Comment") this->mComment = value;
else if (key == "Icon") this->mIcon = value;
if (key == "Name") data.name = value;
else if (key == "GenericName") data.genericName = value;
else if (key == "StartupWMClass") data.startupClass = value;
else if (key == "NoDisplay") data.noDisplay = value == "true";
else if (key == "Hidden") data.hidden = value == "true";
else if (key == "Comment") data.comment = value;
else if (key == "Icon") data.icon = value;
else if (key == "Exec") {
this->mExecString = value;
this->mCommand = DesktopEntry::parseExecString(value);
} else if (key == "Path") this->mWorkingDirectory = value;
else if (key == "Terminal") this->mTerminal = value == "true";
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
data.execString = value;
data.command = DesktopEntry::parseExecString(value);
} else if (key == "Path") data.workingDirectory = value;
else if (key == "Terminal") data.terminal = value == "true";
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
}
} else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(16);
auto* action = new DesktopAction(actionName, this);
DesktopActionData action;
action.id = actionName;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
const auto& [_, value] = pair;
action->mEntries.insert(key, value);
action.entries.insert(key, value);
if (key == "Name") action->mName = value;
else if (key == "Icon") action->mIcon = value;
if (key == "Name") action.name = value;
else if (key == "Icon") action.icon = value;
else if (key == "Exec") {
action->mExecString = value;
action->mCommand = DesktopEntry::parseExecString(value);
action.execString = value;
action.command = DesktopEntry::parseExecString(value);
}
}
this->mActions.insert(actionName, action);
data.actions.insert(actionName, action);
}
entries.clear();
@ -183,14 +193,62 @@ void DesktopEntry::parseEntry(const QString& text) {
}
finishCategory();
return data;
}
void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
Qt::beginPropertyUpdateGroup();
this->bName = newState.name;
this->bGenericName = newState.genericName;
this->bStartupClass = newState.startupClass;
this->bNoDisplay = newState.noDisplay;
this->bComment = newState.comment;
this->bIcon = newState.icon;
this->bExecString = newState.execString;
this->bCommand = newState.command;
this->bWorkingDirectory = newState.workingDirectory;
this->bRunInTerminal = newState.terminal;
this->bCategories = newState.categories;
this->bKeywords = newState.keywords;
Qt::endPropertyUpdateGroup();
this->state = newState;
this->updateActions(newState.actions);
}
void DesktopEntry::updateActions(const QHash<QString, DesktopActionData>& newActions) {
auto old = this->mActions;
for (const auto& [key, d]: newActions.asKeyValueRange()) {
DesktopAction* act = nullptr;
if (auto found = old.find(key); found != old.end()) {
act = found.value();
old.erase(found);
} else {
act = new DesktopAction(d.id, this);
this->mActions.insert(key, act);
}
Qt::beginPropertyUpdateGroup();
act->bName = d.name;
act->bIcon = d.icon;
act->bExecString = d.execString;
act->bCommand = d.command;
Qt::endPropertyUpdateGroup();
act->mEntries = d.entries;
}
for (auto* leftover: old) {
leftover->deleteLater();
}
}
void DesktopEntry::execute() const {
DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory);
DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value());
}
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
@ -266,59 +324,44 @@ void DesktopEntry::doExec(const QList<QString>& execString, const QString& worki
}
void DesktopAction::execute() const {
DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory);
DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value());
}
DesktopEntryManager::DesktopEntryManager() {
this->scanDesktopEntries();
this->populateApplications();
DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) {
this->setAutoDelete(true);
}
void DesktopEntryManager::scanDesktopEntries() {
QList<QString> dataPaths;
void DesktopEntryScanner::run() {
const auto& desktopPaths = DesktopEntryManager::desktopPaths();
auto scanResults = QList<ParsedDesktopEntryData>();
if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
} else if (qEnvironmentVariableIsSet("HOME")) {
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
for (const auto& path: desktopPaths | std::views::reverse) {
auto file = QFileInfo(path);
if (!file.isDir()) continue;
this->scanDirectory(QDir(path), QString(), scanResults);
}
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
dataPaths += var.split(u':', Qt::SkipEmptyParts);
} else {
dataPaths.push_back("/usr/local/share");
dataPaths.push_back("/usr/share");
QMetaObject::invokeMethod(
this->manager,
"onScanCompleted",
Qt::QueuedConnection,
Q_ARG(QList<ParsedDesktopEntryData>, scanResults)
);
}
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
void DesktopEntryScanner::scanDirectory(
const QDir& dir,
const QString& idPrefix,
QList<ParsedDesktopEntryData>& entries
) {
auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (auto& path: std::ranges::reverse_view(dataPaths)) {
auto p = QDir(path).filePath("applications");
auto file = QFileInfo(p);
if (!file.isDir()) {
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
continue;
}
qCDebug(logDesktopEntry) << "Scanning path" << p;
this->scanPath(p);
}
}
void DesktopEntryManager::populateApplications() {
for (auto& entry: this->desktopEntries.values()) {
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
}
}
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.absoluteFilePath(), prefix + dir.dirName() + "-");
else if (entry.isFile()) {
for (auto& entry: dirEntries) {
if (entry.isDir()) {
auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName();
this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
} else if (entry.isFile()) {
auto path = entry.filePath();
if (!path.endsWith(".desktop")) {
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
@ -331,46 +374,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue;
}
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
auto lowerId = id.toLower();
auto basename = QFileInfo(entry.fileName()).completeBaseName();
auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename;
auto content = QString::fromUtf8(file.readAll());
auto text = QString::fromUtf8(file.readAll());
auto* dentry = new DesktopEntry(id, this);
dentry->parseEntry(text);
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
delete dentry;
continue;
}
qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
auto conflictingId = this->desktopEntries.contains(id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
delete this->desktopEntries.value(id);
this->desktopEntries.remove(id);
this->lowercaseDesktopEntries.remove(lowerId);
}
this->desktopEntries.insert(id, dentry);
if (this->lowercaseDesktopEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
this->lowercaseDesktopEntries.remove(lowerId);
}
this->lowercaseDesktopEntries.insert(lowerId, dentry);
auto data = DesktopEntry::parseText(id, content);
entries.append(std::move(data));
}
}
}
DesktopEntryManager::DesktopEntryManager(): monitor(new DesktopEntryMonitor(this)) {
QObject::connect(
this->monitor,
&DesktopEntryMonitor::desktopEntriesChanged,
this,
&DesktopEntryManager::handleFileChanges
);
DesktopEntryScanner(this).run();
}
void DesktopEntryManager::scanDesktopEntries() {
qCDebug(logDesktopEntry) << "Starting desktop entry scan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
DesktopEntryManager* DesktopEntryManager::instance() {
static auto* instance = new DesktopEntryManager(); // NOLINT
return instance;
@ -391,14 +430,14 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
auto list = this->desktopEntries.values();
auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) {
return name == entry->mStartupClass;
auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name == entry->bStartupClass.value();
});
if (iter != list.end()) return *iter;
iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) {
return name.toLower() == entry->mStartupClass.toLower();
iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name.toLower() == entry->bStartupClass.value().toLower();
});
if (iter != list.end()) return *iter;
@ -407,7 +446,137 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
void DesktopEntryManager::handleFileChanges() {
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
const QStringList& DesktopEntryManager::desktopPaths() {
static const auto paths = []() {
auto dataPaths = QStringList();
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME"))
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications");
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share";
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
dataPaths.append(dir + "/applications");
}
return dataPaths;
}();
return paths;
}
void DesktopEntryManager::onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults) {
auto guard = qScopeGuard([this] {
this->scanInProgress = false;
if (this->scanQueued) {
this->scanQueued = false;
this->scanDesktopEntries();
}
});
auto oldEntries = this->desktopEntries;
auto newEntries = QHash<QString, DesktopEntry*>();
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
for (const auto& data: scanResults) {
auto lowerId = data.id.toLower();
if (data.hidden) {
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
it.value()->deleteLater();
oldEntries.erase(it);
}
qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id;
continue;
}
DesktopEntry* dentry = nullptr;
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
dentry = it.value();
oldEntries.erase(it);
dentry->updateState(data);
} else {
dentry = new DesktopEntry(data.id, this);
dentry->updateState(data);
}
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
if (!oldEntries.contains(data.id)) {
dentry->deleteLater();
}
continue;
}
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
auto conflictingId = newEntries.contains(data.id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
}
newEntries.insert(data.id, dentry);
if (newLowercaseEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
newLowercaseEntries.remove(lowerId);
}
newLowercaseEntries.insert(lowerId, dentry);
}
this->desktopEntries = newEntries;
this->lowercaseDesktopEntries = newLowercaseEntries;
auto newApplications = QVector<DesktopEntry*>();
for (auto* entry: this->desktopEntries.values())
if (!entry->bNoDisplay) newApplications.append(entry);
this->mApplications.diffUpdate(newApplications);
emit this->applicationsChanged();
for (auto* e: oldEntries) e->deleteLater();
}
DesktopEntries::DesktopEntries() {
QObject::connect(
DesktopEntryManager::instance(),
&DesktopEntryManager::applicationsChanged,
this,
&DesktopEntries::applicationsChanged
);
}
DesktopEntry* DesktopEntries::byId(const QString& id) {
return DesktopEntryManager::instance()->byId(id);

View file

@ -6,35 +6,68 @@
#include <qdir.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qrunnable.h>
#include <qtmetamacros.h>
#include "desktopentrymonitor.hpp"
#include "doc.hpp"
#include "model.hpp"
class DesktopAction;
class DesktopEntryMonitor;
struct DesktopActionData {
QString id;
QString name;
QString icon;
QString execString;
QVector<QString> command;
QHash<QString, QString> entries;
};
struct ParsedDesktopEntryData {
QString id;
QString name;
QString genericName;
QString startupClass;
bool noDisplay = false;
bool hidden = false;
QString comment;
QString icon;
QString execString;
QVector<QString> command;
QString workingDirectory;
bool terminal = false;
QVector<QString> categories;
QVector<QString> keywords;
QHash<QString, QString> entries;
QHash<QString, DesktopActionData> actions;
};
/// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
/// Name of the specific application, such as "Firefox".
Q_PROPERTY(QString name MEMBER mName CONSTANT);
// clang-format off
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
/// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName);
/// Initial class or app id the app intends to use. May be useful for matching running apps
/// to desktop entries.
Q_PROPERTY(QString startupClass MEMBER mStartupClass CONSTANT);
Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass);
/// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay);
/// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment);
/// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the desktop entry.
///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the desktop entry.
///
/// The entry can be run with @@execute(), or by using this command in
@ -43,13 +76,14 @@ class DesktopEntry: public QObject {
/// the invoked process. See @@execute() for details.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT);
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
/// The working directory to execute from.
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory);
/// If the application should run in a terminal.
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
// clang-format on
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
@ -57,7 +91,8 @@ class DesktopEntry: public QObject {
public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
void parseEntry(const QString& text);
static ParsedDesktopEntryData parseText(const QString& id, const QString& text);
void updateState(const ParsedDesktopEntryData& newState);
/// Run the application. Currently ignores @@runInTerminal and field codes.
///
@ -73,30 +108,65 @@ public:
Q_INVOKABLE void execute() const;
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector<DesktopAction*> actions() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableGenericName() const { return &this->bGenericName; }
[[nodiscard]] QBindable<QString> bindableStartupClass() const { return &this->bStartupClass; }
[[nodiscard]] QBindable<bool> bindableNoDisplay() const { return &this->bNoDisplay; }
[[nodiscard]] QBindable<QString> bindableComment() const { return &this->bComment; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
[[nodiscard]] QBindable<QString> bindableWorkingDirectory() const {
return &this->bWorkingDirectory;
}
[[nodiscard]] QBindable<bool> bindableRunInTerminal() const { return &this->bRunInTerminal; }
[[nodiscard]] QBindable<QVector<QString>> bindableCategories() const {
return &this->bCategories;
}
[[nodiscard]] QBindable<QVector<QString>> bindableKeywords() const { return &this->bKeywords; }
// currently ignores all field codes.
static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QList<QString>& execString, const QString& workingDirectory);
signals:
void nameChanged();
void genericNameChanged();
void startupClassChanged();
void noDisplayChanged();
void commentChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
void workingDirectoryChanged();
void runInTerminalChanged();
void categoriesChanged();
void keywordsChanged();
public:
QString mId;
QString mName;
QString mGenericName;
QString mStartupClass;
bool mNoDisplay = false;
QString mComment;
QString mIcon;
QString mExecString;
QVector<QString> mCommand;
QString mWorkingDirectory;
bool mTerminal = false;
QVector<QString> mCategories;
QVector<QString> mKeywords;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCommand, &DesktopEntry::commandChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCategories, &DesktopEntry::categoriesChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bKeywords, &DesktopEntry::keywordsChanged);
// clang-format on
private:
QHash<QString, QString> mEntries;
void updateActions(const QHash<QString, DesktopActionData>& newActions);
ParsedDesktopEntryData state;
QHash<QString, DesktopAction*> mActions;
friend class DesktopAction;
@ -106,12 +176,13 @@ private:
class DesktopAction: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
Q_PROPERTY(QString name MEMBER mName CONSTANT);
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
// clang-format off
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the action.
///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the action.
///
/// The entry can be run with @@execute(), or by using this command in
@ -120,7 +191,8 @@ class DesktopAction: public QObject {
/// the invoked process.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT);
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
@ -136,18 +208,47 @@ public:
/// and @@DesktopEntry.workingDirectory.
Q_INVOKABLE void execute() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
signals:
void nameChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
private:
DesktopEntry* entry;
QString mId;
QString mName;
QString mIcon;
QString mExecString;
QVector<QString> mCommand;
QHash<QString, QString> mEntries;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector<QString>, bCommand, &DesktopAction::commandChanged);
// clang-format on
friend class DesktopEntry;
};
class DesktopEntryManager;
class DesktopEntryScanner: public QRunnable {
public:
explicit DesktopEntryScanner(DesktopEntryManager* manager);
void run() override;
// clang-format off
void scanDirectory(const QDir& dir, const QString& idPrefix, QList<ParsedDesktopEntryData>& entries);
// clang-format on
private:
DesktopEntryManager* manager;
};
class DesktopEntryManager: public QObject {
Q_OBJECT;
@ -161,15 +262,26 @@ public:
static DesktopEntryManager* instance();
static const QStringList& desktopPaths();
signals:
void applicationsChanged();
private slots:
void handleFileChanges();
void onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults);
private:
explicit DesktopEntryManager();
void populateApplications();
void scanPath(const QDir& dir, const QString& prefix = QString());
QHash<QString, DesktopEntry*> desktopEntries;
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
ObjectModel<DesktopEntry> mApplications {this};
DesktopEntryMonitor* monitor = nullptr;
bool scanInProgress = false;
bool scanQueued = false;
friend class DesktopEntryScanner;
};
///! Desktop entry index.
@ -201,4 +313,7 @@ public:
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
signals:
void applicationsChanged();
};

View file

@ -0,0 +1,68 @@
#include "desktopentrymonitor.hpp"
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include "desktopentry.hpp"
namespace {
void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) {
watcher.addPath(path);
auto p = QFileInfo(path).absolutePath();
while (!p.isEmpty()) {
watcher.addPath(p);
const auto parent = QFileInfo(p).dir().absolutePath();
if (parent == p) break;
p = parent;
}
}
} // namespace
DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
this->debounceTimer.setSingleShot(true);
this->debounceTimer.setInterval(100);
QObject::connect(
&this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&DesktopEntryMonitor::onDirectoryChanged
);
QObject::connect(
&this->debounceTimer,
&QTimer::timeout,
this,
&DesktopEntryMonitor::processChanges
);
this->startMonitoring();
}
void DesktopEntryMonitor::startMonitoring() {
for (const auto& path: DesktopEntryManager::desktopPaths()) {
if (!QDir(path).exists()) continue;
addPathAndParents(this->watcher, path);
this->scanAndWatch(path);
}
}
void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
auto dir = QDir(dirPath);
if (!dir.exists()) return;
this->watcher.addPath(dirPath);
auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath());
}
void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) {
this->debounceTimer.start();
}
void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); }

View file

@ -0,0 +1,32 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstringlist.h>
#include <qtimer.h>
class DesktopEntryMonitor: public QObject {
Q_OBJECT
public:
explicit DesktopEntryMonitor(QObject* parent = nullptr);
~DesktopEntryMonitor() override = default;
DesktopEntryMonitor(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor(DesktopEntryMonitor&&) = delete;
DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete;
signals:
void desktopEntriesChanged();
private slots:
void onDirectoryChanged(const QString& path);
void processChanges();
private:
void startMonitoring();
void scanAndWatch(const QString& dirPath);
QFileSystemWatcher watcher;
QTimer debounceTimer;
};

View file

@ -11,12 +11,12 @@
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlerror.h>
#include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include "iconimageprovider.hpp"
@ -242,90 +242,6 @@ void EngineGeneration::onDirectoryChanged() {
}
}
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
// We only want controllers that we can swap out if destroyed.
// This happens if the window owning the active controller dies.
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
return;
}
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed,
Qt::UniqueConnection
);
this->incubationControllers.push_back(obj);
qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this;
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == &this->delayedIncubationController) {
this->assignIncubationController();
}
}
// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working
// with any controllers. The QQmlIncubationController destructor will already have run by the
// point QObject::destroyed is called, so we can't cast to that.
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
"however only QObject controllers should be registered.";
}
QObject::disconnect(obj, nullptr, this, nullptr);
if (this->incubationControllers.removeOne(obj)) {
qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this;
} else {
qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "from"
<< this << "as it was not registered to begin with";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
if (this->incubationControllers.removeAll(sender) != 0) {
qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from"
<< this;
} else {
qCCritical(logIncubator) << "Destroyed incubation controller" << sender
<< "was not registered, but its destruction was observed by" << this;
return;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (dynamic_cast<QObject*>(this->engine->incubationController()) == sender) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
for (const auto& error: warnings) {
const auto& url = error.url();
@ -367,13 +283,27 @@ void EngineGeneration::exit(int code) {
this->destroy();
}
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = nullptr;
void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
if (this->trackedWindows.contains(window)) return;
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
controller = &this->delayedIncubationController;
} else {
controller = dynamic_cast<QQmlIncubationController*>(this->incubationControllers.first());
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
this->trackedWindows.append(window);
this->assignIncubationController();
}
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
this->assignIncubationController();
}
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = &this->delayedIncubationController;
for (auto* window: this->trackedWindows) {
if (auto* wctl = window->incubationController()) {
controller = wctl;
break;
}
}
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"

View file

@ -9,6 +9,7 @@
#include <qqmlengine.h>
#include <qqmlerror.h>
#include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtclasshelpermacros.h>
#include "incubator.hpp"
@ -40,8 +41,7 @@ public:
void setWatchingFiles(bool watching);
bool setExtraWatchedFiles(const QVector<QString>& files);
void registerIncubationController(QQmlIncubationController* controller);
void deregisterIncubationController(QQmlIncubationController* controller);
void trackWindowIncubationController(QQuickWindow* window);
// takes ownership
void registerExtension(const void* key, EngineGenerationExt* extension);
@ -84,13 +84,13 @@ public slots:
private slots:
void onFileChanged(const QString& name);
void onDirectoryChanged();
void incubationControllerDestroyed();
void onTrackedWindowDestroyed(QObject* object);
static void onEngineWarnings(const QList<QQmlError>& warnings);
private:
void postReload();
void assignIncubationController();
QVector<QObject*> incubationControllers;
QVector<QQuickWindow*> trackedWindows;
bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions;

View file

@ -313,8 +313,12 @@ void ThreadLogging::init() {
if (logMfd != -1) {
this->file = new QFile();
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) {
this->fileStream.setDevice(this->file);
} else {
qCCritical(logLogging) << "Failed to open early logging memfd.";
}
}
if (dlogMfd != -1) {
@ -322,7 +326,9 @@ void ThreadLogging::init() {
this->detailedFile = new QFile();
// buffered by WriteBuffer
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
if (this->detailedFile
->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle))
{
this->detailedWriter.setDevice(this->detailedFile);
if (!this->detailedWriter.writeHeader()) {
@ -331,6 +337,9 @@ void ThreadLogging::init() {
delete this->detailedFile;
this->detailedFile = nullptr;
}
} else {
qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
}
}
// This connection is direct so it works while the event loop is destroyed between
@ -458,13 +467,13 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
}
if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
if (this->detailedFile) {
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
this->detailedWriter.setDevice(nullptr);
if (this->detailedFile) {
this->detailedFile->close();
this->detailedFile = nullptr;
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
}
}

View file

@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
auto newIter = newValues.begin();
// TODO: cache this
auto getCmpKey = [&](const QVariant& v) {
auto getCmpKey = [this](const QVariant& v) {
if (v.canConvert<QVariantMap>()) {
auto vMap = v.value<QVariantMap>();
if (vMap.contains(this->cmpKey)) {
@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
return v;
};
auto variantCmp = [&](const QVariant& a, const QVariant& b) {
auto variantCmp = [&, this](const QVariant& a, const QVariant& b) {
if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b);
else return a == b;
};

View file

@ -77,7 +77,11 @@ void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
}
QFile file;
file.open(this->d->infoFd, QFile::ReadWrite);
if (!file.open(this->d->infoFd, QFile::ReadWrite)) {
qCCritical(logCrashHandler
) << "Failed to open instance info memfd, crash recovery will not work.";
}
QDataStream ds(&file);
ds << info;

View file

@ -161,7 +161,10 @@ void qsCheckCrash(int argc, char** argv) {
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
QFile file;
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
if (!file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qFatal() << "Failed to open instance info fd.";
}
file.seek(0);
auto ds = QDataStream(&file);

View file

@ -183,7 +183,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
}
} else if (removed.isEmpty() || removed.contains("icon-data")) {
imageChanged = this->image.hasData();
image.data.clear();
this->image.data.clear();
}
auto type = properties.value("type");

View file

@ -36,7 +36,7 @@ class DBusMenuPngImage: public QsIndexedImageHandle {
public:
explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {}
[[nodiscard]] bool hasData() const { return !data.isEmpty(); }
[[nodiscard]] bool hasData() const { return !this->data.isEmpty(); }
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
QByteArray data;

View file

@ -93,7 +93,8 @@ void FileViewReader::run() {
FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel);
if (this->shouldCancel.loadAcquire()) {
qCDebug(logFileView) << "Read" << this << "of" << state.path << "canceled for" << this->owner;
qCDebug(logFileView) << "Read" << this << "of" << this->state.path << "canceled for"
<< this->owner;
}
}
@ -206,7 +207,7 @@ void FileViewWriter::run() {
FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel);
if (this->shouldCancel.loadAcquire()) {
qCDebug(logFileView) << "Write" << this << "of" << state.path << "canceled for"
qCDebug(logFileView) << "Write" << this << "of" << this->state.path << "canceled for"
<< this->owner;
}
}

View file

@ -44,7 +44,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject);
for (auto* object: oldCreatedObjects) {
for (auto* object: this->oldCreatedObjects) {
delete object; // FIXME: QMetaType::destroy?
}
@ -56,7 +56,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
void JsonAdapter::connectNotifiers() {
auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()");
connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject);
this->connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject);
}
void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) {
@ -71,7 +71,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
auto val = prop.read(obj);
if (val.canView<JsonObject*>()) {
auto* pobj = prop.read(obj).view<JsonObject*>();
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
} else if (val.canConvert<QQmlListProperty<JsonObject>>()) {
auto listVal = val.value<QQmlListProperty<JsonObject>>();
@ -79,7 +79,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
for (auto i = 0; i != len; i++) {
auto* pobj = listVal.at(&listVal, i);
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
}
}
}
@ -111,7 +111,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
auto* pobj = val.view<JsonObject*>();
if (pobj) {
json.insert(prop.name(), serializeRec(pobj, &JsonObject::staticMetaObject));
json.insert(prop.name(), this->serializeRec(pobj, &JsonObject::staticMetaObject));
} else {
json.insert(prop.name(), QJsonValue::Null);
}
@ -124,7 +124,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
auto* pobj = listVal.at(&listVal, i);
if (pobj) {
array.push_back(serializeRec(pobj, &JsonObject::staticMetaObject));
array.push_back(this->serializeRec(pobj, &JsonObject::staticMetaObject));
} else {
array.push_back(QJsonValue::Null);
}
@ -178,8 +178,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
currentValue->setParent(this);
this->createdObjects.push_back(currentValue);
} else if (oldCreatedObjects.removeOne(currentValue)) {
createdObjects.push_back(currentValue);
} else if (this->oldCreatedObjects.removeOne(currentValue)) {
this->createdObjects.push_back(currentValue);
}
this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject);
@ -212,8 +212,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
if (jsonValue.isObject()) {
if (isNew) {
currentValue = lp.at(&lp, i);
if (oldCreatedObjects.removeOne(currentValue)) {
createdObjects.push_back(currentValue);
if (this->oldCreatedObjects.removeOne(currentValue)) {
this->createdObjects.push_back(currentValue);
}
} else {
// FIXME: should be the type inside the QQmlListProperty but how can we get that?

View file

@ -509,7 +509,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
if (state.misc.printVersion) {
qCInfo(logBare).noquote().nospace()
<< "quickshell 0.2.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
<< "quickshell 0.2.1, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
if (state.log.verbosity > 1) {
qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;

View file

@ -32,7 +32,10 @@ void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
auto lastInfoFd = lastInfoFdStr.toInt();
QFile file;
file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
if (!file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qFatal() << "Failed to open crash info fd. Cannot restart.";
}
file.seek(0);
auto ds = QDataStream(&file);

View file

@ -225,6 +225,10 @@ void GreetdConnection::onSocketReady() {
this->mResponseRequired = responseRequired;
emit this->authMessage(message, error, responseRequired, echoResponse);
if (!responseRequired) {
this->sendRequest({{"type", "post_auth_message_response"}});
}
} else goto unexpected;
return;

View file

@ -100,7 +100,7 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren
} else return static_cast<qlonglong>(-1);
});
this->bLengthSupported.setBinding([this]() { return this->bInternalLength != -1; });
this->bLengthSupported.setBinding([this]() { return this->bInternalLength.value() != -1; });
this->bIsPlaying.setBinding([this]() {
return this->bPlaybackState == MprisPlaybackState::Playing;
@ -378,7 +378,7 @@ void MprisPlayer::onPlaybackStatusUpdated() {
// For exceptionally bad players that update playback timestamps at an indeterminate time AFTER
// updating playback state. (Youtube)
QTimer::singleShot(100, this, [&]() { this->pPosition.requestUpdate(); });
QTimer::singleShot(100, this, [this]() { this->pPosition.requestUpdate(); });
// For exceptionally bad players that don't update length (or other metadata) until a new track actually
// starts playing, and then don't trigger a metadata update when they do. (Jellyfin)

View file

@ -127,7 +127,7 @@ void Notification::updateProperties(
if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) {
if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) {
appIcon = entry->mIcon;
appIcon = entry->bIcon.value();
}
}

View file

@ -135,8 +135,8 @@ void PwDevice::polled() {
// It is far more likely that the list content has not come in yet than it having no entries,
// and there isn't a way to check in the case that there *aren't* actually any entries.
if (!this->stagingIndexes.isEmpty()) {
this->routeDeviceIndexes.removeIf([&](const std::pair<qint32, qint32>& entry) {
if (!stagingIndexes.contains(entry.first)) {
this->routeDeviceIndexes.removeIf([&, this](const std::pair<qint32, qint32>& entry) {
if (!this->stagingIndexes.contains(entry.first)) {
qCDebug(logDevice).nospace() << "Removed device/index pair [device: " << entry.first
<< ", index: " << entry.second << "] for" << this;
return true;

View file

@ -304,6 +304,8 @@ void PwNodeBoundAudio::updateVolumeProps(const PwVolumeProps& volumeProps) {
return;
}
this->volumeStep = volumeProps.volumeStep;
// It is important that the lengths of channels and volumes stay in sync whenever you read them.
auto channelsChanged = false;
auto volumesChanged = false;
@ -435,11 +437,12 @@ void PwNodeBoundAudio::setVolumes(const QVector<float>& volumes) {
<< "via device";
this->waitingVolumes = realVolumes;
} else {
if (this->volumeStep != -1) {
auto significantChange = this->mServerVolumes.isEmpty();
for (auto i = 0; i < this->mServerVolumes.length(); i++) {
auto serverVolume = this->mServerVolumes.value(i);
auto targetVolume = realVolumes.value(i);
if (targetVolume == 0 || abs(targetVolume - serverVolume) >= 0.0001) {
if (targetVolume == 0 || abs(targetVolume - serverVolume) >= this->volumeStep) {
significantChange = true;
break;
}
@ -457,9 +460,12 @@ void PwNodeBoundAudio::setVolumes(const QVector<float>& volumes) {
} else {
// Insignificant changes won't cause an info event on the device, leaving qs hung in the
// "waiting for acknowledgement" state forever.
qCInfo(logNode) << "Ignoring volume change for" << this->node << "to" << realVolumes
<< "from" << this->mServerVolumes
<< "as it is a device node and the change is too small.";
qCInfo(logNode).nospace()
<< "Ignoring volume change for " << this->node << " to " << realVolumes << " from "
<< this->mServerVolumes
<< " as it is a device node and the change is too small (min step: "
<< this->volumeStep << ").";
}
}
}
} else {
@ -519,6 +525,7 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) {
const auto* volumesProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes);
const auto* channelsProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelMap);
const auto* muteProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute);
const auto* volumeStepProp = spa_pod_find_prop(param, nullptr, SPA_PROP_volumeStep);
const auto* volumes = reinterpret_cast<const spa_pod_array*>(&volumesProp->value);
const auto* channels = reinterpret_cast<const spa_pod_array*>(&channelsProp->value);
@ -537,6 +544,12 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) {
spa_pod_get_bool(&muteProp->value, &props.mute);
if (volumeStepProp) {
spa_pod_get_float(&volumeStepProp->value, &props.volumeStep);
} else {
props.volumeStep = -1;
}
return props;
}

View file

@ -158,6 +158,7 @@ struct PwVolumeProps {
QVector<PwAudioChannel::Enum> channels;
QVector<float> volumes;
bool mute = false;
float volumeStep = -1;
static PwVolumeProps parseSpaPod(const spa_pod* param);
};
@ -214,6 +215,7 @@ private:
QVector<float> mServerVolumes;
QVector<float> mDeviceVolumes;
QVector<float> waitingVolumes;
float volumeStep = -1;
PwNode* node;
};

View file

@ -73,7 +73,7 @@ UPowerDevice::UPowerDevice(QObject* parent): QObject(parent) {
return this->bType == UPowerDeviceType::Battery && this->bPowerSupply;
});
this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage != 0; });
this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage.value() != 0; });
}
void UPowerDevice::init(const QString& path) {
@ -101,7 +101,7 @@ QString UPowerDevice::address() const { return this->device ? this->device->serv
QString UPowerDevice::path() const { return this->device ? this->device->path() : QString(); }
void UPowerDevice::onGetAllFinished() {
qCDebug(logUPowerDevice) << "UPowerDevice" << device->path() << "ready.";
qCDebug(logUPowerDevice) << "UPowerDevice" << this->device->path() << "ready.";
this->bReady = true;
}

View file

@ -164,6 +164,7 @@ QString DBusDataTransform<PowerProfile::Enum>::toWire(Data data) {
case PowerProfile::PowerSaver: return QStringLiteral("power-saver");
case PowerProfile::Balanced: return QStringLiteral("balanced");
case PowerProfile::Performance: return QStringLiteral("performance");
default: qFatal() << "Attempted to convert invalid power profile" << data << "to wire format.";
}
}

View file

@ -25,7 +25,7 @@ ReloadPopup::ReloadPopup(QString instanceId, bool failed, QString errorString)
this->popup = component.createWithInitialProperties({{"reloadInfo", QVariant::fromValue(this)}});
if (!popup) {
if (!this->popup) {
qCritical() << "Failed to open reload popup:" << component.errorString();
}

View file

@ -1,6 +1,6 @@
find_package(PkgConfig REQUIRED)
find_package(WaylandScanner REQUIRED)
pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols)
pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols>=1.41)
# wayland protocols
@ -12,13 +12,13 @@ if(NOT TARGET Qt6::qtwaylandscanner)
message(FATAL_ERROR "qtwaylandscanner executable not found. Most likely there is an issue with your Qt installation.")
endif()
execute_process(
COMMAND pkg-config --variable=pkgdatadir wayland-protocols
OUTPUT_VARIABLE WAYLAND_PROTOCOLS
OUTPUT_STRIP_TRAILING_WHITESPACE
)
pkg_get_variable(WAYLAND_PROTOCOLS wayland-protocols pkgdatadir)
message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}")
if(WAYLAND_PROTOCOLS)
message(STATUS "Found wayland protocols at ${WAYLAND_PROTOCOLS}")
else()
message(FATAL_ERROR "Could not find wayland protocols")
endif()
qs_add_pchset(wayland-protocol
DEPENDENCIES Qt::Core Qt::WaylandClient Qt::WaylandClientPrivate

View file

@ -77,7 +77,7 @@ QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) {
}
GbmDeviceHandle::~GbmDeviceHandle() {
if (device) {
if (this->device) {
MANAGER->unrefGbmDevice(this->device);
}
}
@ -522,7 +522,7 @@ WlDmaBuffer::~WlDmaBuffer() {
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) {
auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [this](const auto& format) {
return format.format == this->format
&& (format.modifiers.isEmpty()
|| std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end());

View file

@ -40,7 +40,7 @@ public:
'\0'}
) {
for (auto i = 3; i != 0; i--) {
if (chars[i] == ' ') chars[i] = '\0';
if (this->chars[i] == ' ') this->chars[i] = '\0';
else break;
}
}

View file

@ -24,9 +24,9 @@ HyprlandIpcQml::HyprlandIpcQml() {
QObject::connect(
instance,
&HyprlandIpc::focusedMonitorChanged,
&HyprlandIpc::focusedWorkspaceChanged,
this,
&HyprlandIpcQml::focusedMonitorChanged
&HyprlandIpcQml::focusedWorkspaceChanged
);
QObject::connect(

View file

@ -68,11 +68,11 @@ void WlrScreencopyContext::captureFrame() {
this->request.reset();
if (this->region.isEmpty()) {
this->init(manager->capture_output(this->paintCursors ? 1 : 0, screen->output()));
this->init(this->manager->capture_output(this->paintCursors ? 1 : 0, this->screen->output()));
} else {
this->init(manager->capture_output_region(
this->init(this->manager->capture_output_region(
this->paintCursors ? 1 : 0,
screen->output(),
this->screen->output(),
this->region.x(),
this->region.y(),
this->region.width(),

View file

@ -10,10 +10,11 @@
QtWaylandClient::QWaylandShellSurface*
QSWaylandSessionLockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) {
auto* lock = LockWindowExtension::get(window->window());
if (lock == nullptr || lock->surface == nullptr || !lock->surface->isExposed()) {
if (lock == nullptr || lock->surface == nullptr) {
qFatal() << "Visibility canary failed. A window with a LockWindowExtension MUST be set to "
"visible via LockWindowExtension::setVisible";
}
return lock->surface;
QSWaylandSessionLockSurface* surface = lock->surface; // shut up the unused include linter
return surface;
}

View file

@ -48,16 +48,6 @@ void QSWaylandSessionLockSurface::applyConfigure() {
this->window()->resizeFromApplyConfigure(this->size);
}
bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) {
if (this->initBuf != nullptr) {
// at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one.
delete this->initBuf;
this->initBuf = nullptr;
}
return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region);
}
void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) {
if (ext == nullptr) {
if (this->window() != nullptr) this->window()->window()->close();
@ -71,11 +61,6 @@ void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) {
}
}
void QSWaylandSessionLockSurface::setVisible() {
if (this->configured && !this->visible) this->initVisible();
this->visible = true;
}
void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure(
quint32 serial,
quint32 width,
@ -97,13 +82,41 @@ void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure(
#else
this->window()->updateExposure();
#endif
#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
if (this->visible) this->initVisible();
#endif
} else {
// applyConfigureWhenPossible runs too late and causes a protocol error on reconfigure.
this->window()->resizeFromApplyConfigure(this->size);
}
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
bool QSWaylandSessionLockSurface::commitSurfaceRole() const { return false; }
void QSWaylandSessionLockSurface::setVisible() { this->window()->window()->setVisible(true); }
#else
bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) {
if (this->initBuf != nullptr) {
// at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one.
delete this->initBuf;
this->initBuf = nullptr;
}
return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region);
}
void QSWaylandSessionLockSurface::setVisible() {
if (this->configured && !this->visible) this->initVisible();
this->visible = true;
}
#endif
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
#include <private/qwaylandshmbackingstore_p.h>
@ -123,7 +136,7 @@ void QSWaylandSessionLockSurface::initVisible() {
this->window()->window()->setVisible(true);
}
#else
#elif QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
#include <cmath>

View file

@ -5,6 +5,7 @@
#include <private/qwaylandwindow_p.h>
#include <qregion.h>
#include <qtclasshelpermacros.h>
#include <qtversionchecks.h>
#include <qtypes.h>
#include <qwayland-ext-session-lock-v1.h>
@ -20,7 +21,12 @@ public:
[[nodiscard]] bool isExposed() const override;
void applyConfigure() override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
[[nodiscard]] bool commitSurfaceRole() const override;
#else
bool handleExpose(const QRegion& region) override;
#endif
void setExtension(LockWindowExtension* ext);
void setVisible();
@ -29,11 +35,13 @@ private:
void
ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override;
#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
void initVisible();
bool visible = false;
QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr;
#endif
LockWindowExtension* ext = nullptr;
QSize size;
bool configured = false;
bool visible = false;
QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr;
};

View file

@ -28,9 +28,10 @@ WlrLayershell::WlrLayershell(QObject* parent): ProxyWindowBase(parent) {
case Qt::BottomEdge: return this->bImplicitHeight + margins.top;
case Qt::LeftEdge: return this->bImplicitWidth + margins.right;
case Qt::RightEdge: return this->bImplicitWidth + margins.left;
default: return 0;
}
}
return 0;
});
this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); });

View file

@ -153,13 +153,7 @@ void ProxyWindowBase::createWindow() {
void ProxyWindowBase::deleteWindow(bool keepItemOwnership) {
if (this->window != nullptr) emit this->windowDestroyed();
if (auto* window = this->disownWindow(keepItemOwnership)) {
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
generation->deregisterIncubationController(window->incubationController());
}
window->deleteLater();
}
if (auto* window = this->disownWindow(keepItemOwnership)) window->deleteLater();
}
ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) {
@ -185,7 +179,7 @@ void ProxyWindowBase::connectWindow() {
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
// All windows have effectively the same incubation controller so it dosen't matter
// which window it belongs to. We do want to replace the delay one though.
generation->registerIncubationController(this->window->incubationController());
generation->trackWindowIncubationController(this->window);
}
this->window->setProxy(this);

View file

@ -532,7 +532,7 @@ QString I3IpcEvent::eventToString(EventCode event) {
case EventCode::BarStateUpdate: return "bar_state_update"; break;
case EventCode::Input: return "input"; break;
case EventCode::Unknown: return "unknown"; break;
default: return "unknown"; break;
}
}

View file

@ -115,6 +115,8 @@ XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) {
return 0;
}
}
return 0;
});
this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); });