diff --git a/.clang-tidy b/.clang-tidy
index 6362e662..ca6c9549 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -5,6 +5,8 @@ Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
+ -bugprone-forward-declararion-namespace,
+ -bugprone-forward-declararion-namespace,
concurrency-*,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,
@@ -13,8 +15,10 @@ Checks: >
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-goto,
- google-build-using-namespace.
- google-explicit-constructor,
+ -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
+ -cppcoreguidelines-avoid-do-while,
+ -cppcoreguidelines-pro-type-reinterpret-cast,
+ -cppcoreguidelines-pro-type-vararg,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
@@ -26,6 +30,7 @@ Checks: >
-modernize-return-braced-init-list,
-modernize-use-trailing-return-type,
performance-*,
+ -performance-avoid-endl,
portability-std-allocator-const,
readability-*,
-readability-function-cognitive-complexity,
@@ -37,6 +42,8 @@ Checks: >
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
+ -readability-implicit-bool-conversion,
+ -readability-avoid-nested-conditional-operator,
tidyfox-*,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true
diff --git a/.editorconfig b/.editorconfig
index 6b1b58df..439ba6b7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,3 +9,7 @@ indent_style = tab
[*.nix]
indent_style = space
indent_size = 2
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..0086358d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml
new file mode 100644
index 00000000..c8b4804e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash.yml
@@ -0,0 +1,82 @@
+name: Crash Report
+description: Quickshell has crashed
+labels: ["bug", "crash"]
+body:
+ - type: textarea
+ id: crashinfo
+ attributes:
+ label: General crash information
+ description: |
+ Paste the contents of the `info.txt` file in your crash folder here.
+ value: " General information
+
+
+ ```
+
+
+
+ ```
+
+
+ "
+ validations:
+ required: true
+ - type: textarea
+ id: userinfo
+ attributes:
+ label: What caused the crash
+ description: |
+ Any information likely to help debug the crash. What were you doing when the crash occurred,
+ what changes did you make, can you get it to happen again?
+ - type: textarea
+ id: dump
+ attributes:
+ label: Minidump
+ description: |
+ Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
+
+ You may skip this step if quickshell crashed while processing a password
+ or other sensitive information. If you skipped it write why instead.
+ validations:
+ required: true
+ - type: textarea
+ id: logs
+ attributes:
+ label: Log file
+ description: |
+ Attach `log.qslog.log` here. If it is too big to upload, compress it.
+
+ You can preview the log if you'd like using `quickshell read-log `.
+ validations:
+ required: true
+ - type: textarea
+ id: config
+ attributes:
+ label: Configuration
+ description: |
+ Attach your configuration here, preferrably in full (not just one file).
+ Compress it into a zip, tar, etc.
+
+ This will help us reproduce the crash ourselves.
+ - type: textarea
+ id: bt
+ attributes:
+ label: Backtrace
+ description: |
+ If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
+ we would appreciate one. (You may have gdb installed without knowing it)
+
+ 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID"
+ in the crash reporter.
+ 2. Once it loads, type `bt -full` (then enter)
+ 3. Copy the output and attach it as a file or in a spoiler.
+ - type: textarea
+ id: exe
+ attributes:
+ label: Executable
+ description: |
+ If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field.
+ If it is too big to upload, compress it.
+
+ Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on
+ filetypes.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..b176e982
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,55 @@
+name: Build
+on: [push, pull_request, workflow_dispatch]
+
+jobs:
+ nix:
+ 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]
+ compiler: [clang, gcc]
+ runs-on: ubuntu-latest
+ 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
+
+ - name: Download Dependencies
+ run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
+
+ - name: Build
+ run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
+
+ archlinux:
+ name: Archlinux
+ runs-on: ubuntu-latest
+ container: archlinux
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Download Dependencies
+ run: |
+ pacman --noconfirm --noprogressbar -Syyu
+ pacman --noconfirm --noprogressbar -Sy \
+ base-devel \
+ cmake \
+ ninja \
+ pkgconf \
+ qt6-base \
+ qt6-declarative \
+ qt6-svg \
+ qt6-wayland \
+ qt6-shadertools \
+ wayland-protocols \
+ wayland \
+ libxcb \
+ libpipewire \
+ cli11 \
+ jemalloc
+
+ - name: Build
+ # breakpad is annoying to build in ci due to makepkg not running as root
+ run: |
+ cmake -GNinja -B build -DCRASH_REPORTER=OFF
+ cmake --build build
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000..a53221cb
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,25 @@
+name: Lint
+on: [push, pull_request, workflow_dispatch]
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ 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: nicknovitski/nix-develop@v1
+
+ - name: Check formatting
+ run: clang-format -Werror --dry-run src/**/*.{cpp,hpp}
+
+ # required for lint
+ - name: Build
+ run: |
+ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
+ just build
+
+ - name: Run lints
+ run: just lint-ci
diff --git a/.gitignore b/.gitignore
index 1933837e..dcdefe39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# related repos
+/docs
+/examples
+
# build files
/result
/build/
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 74013769..00000000
--- a/.gitmodules
+++ /dev/null
@@ -1,6 +0,0 @@
-[submodule "docs"]
- path = docs
- url = https://git.outfoxxed.me/outfoxxed/quickshell-docs
-[submodule "examples"]
- path = examples
- url = https://git.outfoxxed.me/outfoxxed/quickshell-examples
diff --git a/BUILD.md b/BUILD.md
new file mode 100644
index 00000000..cf6b3a03
--- /dev/null
+++ b/BUILD.md
@@ -0,0 +1,225 @@
+# Build instructions
+Instructions for building from source and distro packagers. We highly recommend
+distro packagers read through this page fully.
+
+## Packaging
+If you are packaging quickshell for official or unofficial distribution channels,
+such as a distro package repository, user repository, or other shared build location,
+please set the following CMake flags.
+
+`-DDISTRIBUTOR="your distribution platform"`
+
+Please make this descriptive enough to identify your specific package, for example:
+- `Official Nix Flake`
+- `AUR (quickshell-git)`
+- `Nixpkgs`
+- `Fedora COPR (errornointernet/quickshell)`
+
+`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO`
+
+If we can retrieve binaries and debug information for the package without actually running your
+distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`.
+
+If we cannot retrieve debug information, please set this to `NO` and
+**ensure you aren't distributing stripped (non debuggable) binaries**.
+
+In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo).
+
+### QML Module dir
+Currently all QML modules are statically linked to quickshell, but this is where
+tooling information will go.
+
+`-DINSTALL_QML_PREFIX="path/to/qml"`
+
+`-DINSTALL_QMLDIR="/full/path/to/qml"`
+
+`INSTALL_QML_PREFIX` works the same as `INSTALL_QMLDIR`, except it prepends `CMAKE_INSTALL_PREFIX`. You usually want this.
+
+## Dependencies
+Quickshell has a set of base dependencies you will always need, names vary by distro:
+
+- `cmake`
+- `qt6base`
+- `qt6declarative`
+- `qtshadertools` (build-time only)
+- `spirv-tools` (build-time only)
+- `pkg-config` (build-time only)
+- `cli11`
+
+On some distros, private Qt headers are in separate packages which you may have to install.
+We currently require private headers for the following libraries:
+
+- `qt6declarative`
+- `qt6wayland`
+
+We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
+svg icons will not work, including system ones.
+
+At least Qt 6.6 is required.
+
+All features are enabled by default and some have their own dependencies.
+
+### Crash Reporter
+The crash reporter catches crashes, restarts quickshell when it crashes,
+and collects useful crash information in one place. Leaving this enabled will
+enable us to fix bugs far more easily.
+
+To disable: `-DCRASH_REPORTER=OFF`
+
+Dependencies: `google-breakpad`
+
+### Jemalloc
+We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
+by the QML engine, which results in much lower memory usage. Without this you
+will get a perceived memory leak.
+
+To disable: `-DUSE_JEMALLOC=OFF`
+
+Dependencies: `jemalloc`
+
+### Unix Sockets
+This feature allows interaction with unix sockets and creating socket servers
+which is useful for IPC and has no additional dependencies.
+
+WARNING: Disabling unix sockets will NOT make it safe to run arbitrary code using quickshell.
+There are many vectors which mallicious code can use to escape into your system.
+
+To disable: `-DSOCKETS=OFF`
+
+### Wayland
+This feature enables wayland support. Subfeatures exist for each particular wayland integration.
+
+WARNING: Wayland integration relies on features that are not part of the public Qt API and which
+may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring
+that the current Qt version is supported WILL result in quickshell failing to build or misbehaving
+at runtime.
+
+Currently supported Qt versions: `6.6`, `6.7`.
+
+To disable: `-DWAYLAND=OFF`
+
+Dependencies:
+ - `qt6wayland`
+ - `wayland` (libwayland-client)
+ - `wayland-scanner` (may be part of your distro's wayland package)
+ - `wayland-protocols`
+
+#### Wlroots Layershell
+Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
+enabling use cases such as bars overlays and backgrounds.
+This feature has no extra dependencies.
+
+To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF`
+
+[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1
+
+#### Session Lock
+Enables session lock support through the [ext-session-lock-v1] protocol,
+which allows quickshell to be used as a session lock under compatible wayland compositors.
+
+To disable: `-DWAYLAND_SESSION_LOCK=OFF`
+
+[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1
+
+
+#### Foreign Toplevel Management
+Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol,
+which allows quickshell to be used as a session lock under compatible wayland compositors.
+
+[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1
+
+To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF`
+
+### X11
+This feature enables x11 support. Currently this implements panel windows for X11 similarly
+to the wlroots layershell above.
+
+To disable: `-DX11=OFF`
+
+Dependencies: `libxcb`
+
+### Pipewire
+This features enables viewing and management of pipewire nodes.
+
+To disable: `-DSERVICE_PIPEWIRE=OFF`
+
+Dependencies: `libpipewire`
+
+### StatusNotifier / System Tray
+This feature enables system tray support using the status notifier dbus protocol.
+
+To disable: `-DSERVICE_STATUS_NOTIFIER=OFF`
+
+Dependencies: `qt6dbus` (usually part of qt6base)
+
+### MPRIS
+This feature enables access to MPRIS compatible media players using its dbus protocol.
+
+To disable: `-DSERVICE_MPRIS=OFF`
+
+Dependencies: `qt6dbus` (usually part of qt6base)
+
+### PAM
+This feature enables PAM integration for user authentication.
+
+To disable: `-DSERVICE_PAM=OFF`
+
+Dependencies: `pam`
+
+### Hyprland
+This feature enables hyprland specific integrations. It requires wayland support
+but has no extra dependencies.
+
+To disable: `-DHYPRLAND=OFF`
+
+#### Hyprland Global Shortcuts
+Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1]
+protocol. Generally a much nicer alternative to using unix sockets to implement the same thing.
+This feature has no extra dependencies.
+
+To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF`
+
+[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml
+
+#### Hyprland Focus Grab
+Enables windows to grab focus similarly to a context menu under hyprland through the
+[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies.
+
+To disable: `-DHYPRLAND_FOCUS_GRAB=OFF`
+
+[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml
+
+### i3/Sway
+Enables i3 and Sway specific features, does not have any dependency on Wayland or x11.
+
+To disable: `-DI3=OFF`
+
+#### i3/Sway IPC
+Enables interfacing with i3 and Sway's IPC.
+
+To disable: `-DI3_IPC=OFF`
+
+## Building
+*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).*
+
+We highly recommend using `ninja` to run the build, but you can use makefiles if you must.
+
+#### Configuring the build
+```sh
+$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
+```
+
+Note that features you do not supply dependencies for MUST be disabled with their associated flags
+or quickshell will fail to build.
+
+Additionally, note that clang builds much faster than gcc if you care.
+
+#### Building
+```sh
+$ cmake --build build
+```
+
+#### Installing
+```sh
+$ cmake --install build
+```
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 159acd49..a4919952 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,50 +5,76 @@ set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
-option(BUILD_TESTING "Build tests" OFF)
-option(ASAN "Enable ASAN" OFF)
-option(FRAME_POINTERS "Always keep frame pointers" ${ASAN})
+set(QS_BUILD_OPTIONS "")
-option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF)
-option(SOCKETS "Enable unix socket support" ON)
-option(WAYLAND "Enable wayland support" ON)
-option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
-option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
-option(HYPRLAND "Support hyprland specific features" ON)
-option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
-option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
-option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
-option(SERVICE_PIPEWIRE "PipeWire service" ON)
+function(boption VAR NAME DEFAULT)
+ cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
+
+ option(${VAR} ${NAME} ${DEFAULT})
+
+ set(STATUS "${VAR}_status")
+ set(EFFECTIVE "${VAR}_effective")
+ set(${STATUS} ${${VAR}})
+ set(${EFFECTIVE} ${${VAR}})
+
+ if (${${VAR}} AND DEFINED arg_REQUIRES)
+ set(REQUIRED_EFFECTIVE "${arg_REQUIRES}_effective")
+ if (NOT ${${REQUIRED_EFFECTIVE}})
+ set(${STATUS} "OFF (Requires ${arg_REQUIRES})")
+ set(${EFFECTIVE} OFF)
+ endif()
+ endif()
+
+ set(${EFFECTIVE} "${${EFFECTIVE}}" PARENT_SCOPE)
+
+ message(STATUS " ${NAME}: ${${STATUS}}")
+
+ string(APPEND QS_BUILD_OPTIONS "\\n ${NAME}: ${${STATUS}}")
+ set(QS_BUILD_OPTIONS "${QS_BUILD_OPTIONS}" PARENT_SCOPE)
+endfunction()
+
+set(DISTRIBUTOR "Unset" CACHE STRING "Distributor")
+string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
message(STATUS "Quickshell configuration")
-message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
-message(STATUS " Build tests: ${BUILD_TESTING}")
-message(STATUS " Sockets: ${SOCKETS}")
-message(STATUS " Wayland: ${WAYLAND}")
-if (WAYLAND)
- message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
- message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
-endif ()
-message(STATUS " Services")
-message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
-message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
-message(STATUS " Hyprland: ${HYPRLAND}")
-if (HYPRLAND)
- message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
- message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}")
-endif()
+message(STATUS " Distributor: ${DISTRIBUTOR}")
+boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
+boption(NO_PCH "Disable precompild headers (dev)" OFF)
+boption(BUILD_TESTING "Build tests (dev)" OFF)
+boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
+boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
-if (NOT DEFINED GIT_REVISION)
- execute_process(
- COMMAND git rev-parse HEAD
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
- OUTPUT_VARIABLE GIT_REVISION
- OUTPUT_STRIP_TRAILING_WHITESPACE
- )
-endif()
+boption(CRASH_REPORTER "Crash Handling" ON)
+boption(USE_JEMALLOC "Use jemalloc" ON)
+boption(SOCKETS "Unix Sockets" ON)
+boption(WAYLAND "Wayland" ON)
+boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
+boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND)
+boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND)
+boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND)
+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(X11 "X11" ON)
+boption(I3 "I3/Sway" ON)
+boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3)
+boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
+boption(SERVICE_PIPEWIRE "PipeWire" ON)
+boption(SERVICE_MPRIS "Mpris" ON)
+boption(SERVICE_PAM "Pam" ON)
+boption(SERVICE_GREETD "Greetd" ON)
+boption(SERVICE_UPOWER "UPower" ON)
+boption(SERVICE_NOTIFICATIONS "Notifications" ON)
+
+include(cmake/install-qml-module.cmake)
+include(cmake/util.cmake)
add_compile_options(-Wall -Wextra)
+# pipewire defines this, breaking PCH
+add_compile_definitions(_REENTRANT)
+
if (FRAME_POINTERS)
add_compile_options(-fno-omit-frame-pointer)
endif()
@@ -68,8 +94,9 @@ if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
-set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
-set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets)
+set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
+
+include(cmake/pch.cmake)
if (BUILD_TESTING)
enable_testing()
@@ -78,58 +105,39 @@ if (BUILD_TESTING)
endif()
if (SOCKETS)
- list(APPEND QT_DEPS Qt6::Network)
list(APPEND QT_FPDEPS Network)
endif()
if (WAYLAND)
- list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
list(APPEND QT_FPDEPS WaylandClient)
endif()
-if (SERVICE_STATUS_NOTIFIER)
+if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
set(DBUS ON)
endif()
if (DBUS)
- list(APPEND QT_DEPS Qt6::DBus)
list(APPEND QT_FPDEPS DBus)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
+set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
-# pch breaks clang-tidy..... somehow
-if (NOT NO_PCH)
- file(GENERATE
- OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp
- CONTENT "// intentionally empty"
- )
-
- add_library(qt-pch ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp)
- target_link_libraries(qt-pch PRIVATE ${QT_DEPS})
- target_precompile_headers(qt-pch PUBLIC
-
-
-
-
-
-
-
- )
-endif()
-
-function (qs_pch target)
- if (NOT NO_PCH)
- target_precompile_headers(${target} REUSE_FROM qt-pch)
- target_link_libraries(${target} PRIVATE ${QT_DEPS}) # required for gcc to accept the pch on plugin targets
- endif()
-endfunction()
-
-if (NVIDIA_COMPAT)
- add_compile_definitions(NVIDIA_COMPAT)
-endif()
-
add_subdirectory(src)
+
+if (USE_JEMALLOC)
+ find_package(PkgConfig REQUIRED)
+ # IMPORTED_TARGET not working for some reason
+ pkg_check_modules(JEMALLOC REQUIRED jemalloc)
+ target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES})
+endif()
+
+install(CODE "
+ execute_process(
+ COMMAND ${CMAKE_COMMAND} -E create_symlink \
+ ${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
+ )
+")
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..feeb746b
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,102 @@
+# Contributing / Development
+Instructions for development setup and upstreaming patches.
+
+If you just want to build or package quickshell see [BUILD.md](BUILD.md).
+
+## Development
+
+Install the dependencies listed in [BUILD.md](BUILD.md).
+You probably want all of them even if you don't use all of them
+to ensure tests work correctly and avoid passing a bunch of configure
+flags when you need to wipe the build directory.
+
+Quickshell also uses `just` for common development command aliases.
+
+The dependencies are also available as a nix shell or nix flake which we recommend
+using with nix-direnv.
+
+Common aliases:
+- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args)
+- `just build` - runs the build, configuring if not configured already.
+- `just run [args]` - runs quickshell with the given arguments
+- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
+
+### Formatting
+All contributions should be formatted similarly to what already exists.
+Group related functionality together.
+
+Run the formatter using `just fmt`.
+If the results look stupid, fix the clang-format file if possible,
+or disable clang-format in the affected area
+using `// clang-format off` and `// clang-format on`.
+
+### Linter
+All contributions should pass the linter.
+
+Note that running the linter requires disabling precompiled
+headers and including the test codepaths:
+```sh
+$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
+$ just lint
+```
+
+If the linter is complaining about something that you think it should not,
+please disable the lint in your MR and explain your reasoning.
+
+### Tests
+If you feel like the feature you are working on is very complex or likely to break,
+please write some tests. We will ask you to directly if you send in an MR for an
+overly complex or breakable feature.
+
+At least all tests that passed before your changes should still be passing
+by the time your contribution is ready.
+
+You can run the tests using `just test` but you must enable them first
+using `-DBUILD_TESTING=ON`.
+
+### Documentation
+Most of quickshell's documentation is automatically generated from the source code.
+You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
+cannot handle random line breaks and will usually require you to disable clang-format if the
+lines are too long.
+
+Before submitting an MR, if adding new features please make sure the documentation is generated
+reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
+
+Doc comments take the form `///` or `///!` (summary) and work with markdown.
+You can reference other types using the `@@[Module.][Type.][member]` shorthand
+where all parts are optional. If module or type are not specified they will
+be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
+Look at existing code for how it works.
+
+Quickshell modules additionally have a `module.md` file which contains a summary, description,
+and list of headers to scan for documentation.
+
+## Contributing
+
+### Commits
+Please structure your commit messages as `scope[!]: commit` where
+the scope is something like `core` or `service/mpris`. (pick what has been
+used historically or what makes sense if new.) Add `!` for changes that break
+existing APIs or functionality.
+
+Commit descriptions should contain a summary of the changes if they are not
+sufficiently addressed in the commit message.
+
+Please squash/rebase additions or edits to previous changes and follow the
+commit style to keep the history easily searchable at a glance.
+Depending on the change, it is often reasonable to squash it into just
+a single commit. (If you do not follow this we will squash your changes
+for you.)
+
+### Sending patches
+You may contribute by submitting a pull request on github, asking for
+an account on our git server, or emailing patches / git bundles
+directly to `outfoxxed@outfoxxed.me`.
+
+### Getting help
+If you're getting stuck, you can come talk to us in the
+[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
+for help on implementation, conventions, etc.
+Feel free to ask for advice early in your implementation if you are
+unsure.
diff --git a/Justfile b/Justfile
index 69fdff70..b4fe87ec 100644
--- a/Justfile
+++ b/Justfile
@@ -4,7 +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 --eta clang-tidy --load={{ env_var("TIDYFOX") }}
+ find src -type f -name "*.cpp" -print0 | parallel -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") }}
+
+lint-changed:
+ git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \
diff --git a/README.md b/README.md
index d05e3347..82f912fd 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# quickshell
-Simple and flexbile QtQuick based desktop shell toolkit.
+Flexbile QtQuick based desktop shell toolkit.
Hosted on: [outfoxxed's gitea], [github]
@@ -11,21 +11,14 @@ Hosted on: [outfoxxed's gitea], [github]
Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or
can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo.
-Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
+Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
repo.
-Both the documentation and examples are included as submodules with revisions that work with the current
-version of quickshell.
+# Breaking Changes
+Quickshell is still in alpha and there will be breaking changes.
-You can clone everything with
-```
-$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git
-```
-
-Or clone missing submodules later with
-```
-$ git submodule update --init --recursive
-```
+Commits with breaking qml api changes will contain a `!` at the end of the scope
+(`thing!: foo`) and the commit description will contain details about the broken api.
# Installation
@@ -39,6 +32,9 @@ This repo has a nix flake you can use to install the package directly:
quickshell = {
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
+
+ # THIS IS IMPORTANT
+ # Mismatched system dependencies will lead to crashes and other issues.
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -48,75 +44,45 @@ This repo has a nix flake you can use to install the package directly:
Quickshell's binary is available at `quickshell.packages..default` to be added to
lists such as `environment.systemPackages` or `home.packages`.
-`quickshell.packages..nvidia` is also available for nvidia users which fixes some
-common crashes.
+The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled
+or disabled with overrides:
+
+```nix
+quickshell.packages..default.override {
+ withJemalloc = true;
+ withQtSvg = true;
+ withWayland = true;
+ withX11 = true;
+ withPipewire = true;
+ withPam = true;
+ withHyprland = true;
+}
+```
Note: by default this package is built with clang as it is significantly faster.
-## Manual
+## Arch (AUR)
+Quickshell has a third party [AUR package] available under the same name.
+It is not managed by us and should be looked over before use.
-If not using nix, you'll have to build from source.
+[AUR package]: https://aur.archlinux.org/packages/quickshell
-### Dependencies
-To build quickshell at all, you will need the following packages (names may vary by distro)
+> [!CAUTION]
+> The AUR provides no way to force the quickshell package to rebuild when the Qt version changes.
+> If you experience crashes after updating Qt, please try rebuilding Quickshell against the
+> current Qt version before opening an issue.
-- just
-- cmake
-- pkg-config
-- ninja
-- Qt6 [ QtBase, QtDeclarative ]
+## Fedora (COPR)
+Quickshell has a third party [Fedora COPR package] available under the same name.
+It is not managed by us and should be looked over before use.
-To build with wayland support you will additionally need:
-- wayland
-- wayland-scanner (may be part of wayland on some distros)
-- wayland-protocols
-- Qt6 [ QtWayland ]
+[Fedora COPR package]: https://copr.fedorainfracloud.org/coprs/errornointernet/quickshell
-### Building
+## Anything else
+See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell.
-To make a release build of quickshell run:
-```sh
-$ just release
-```
-
-If running an nvidia GPU, instead run:
-```sh
-$ just configure release -DNVIDIA_COMPAT=ON
-$ just build
-```
-
-(These commands are just aliases for cmake commands you can run directly,
-see the Justfile for more information.)
-
-If you have all the dependencies installed and they are in expected
-locations this will build correctly.
-
-To install to /usr/local/bin run as root (usually `sudo`) in the same folder:
-```
-$ just install
-```
-
-### Building (Nix)
-
-You can build directly using the provided nix flake or nix package.
-```
-nix build
-nix build -f package.nix # calls default.nix with a basic callPackage expression
-```
-
-# Development
-
-For nix there is a devshell available from `shell.nix` and as a devShell
-output from the flake.
-
-The Justfile contains various useful aliases:
-- `just configure [ [extra cmake args]]`
-- `just build` (runs configure for debug mode)
-- `just run [args]`
-- `just clean`
-- `just test [args]` (configure with `-DBUILD_TESTING=ON` first)
-- `just fmt`
-- `just lint`
+# Contributing / Development
+See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
#### License
diff --git a/ci/matrix.nix b/ci/matrix.nix
new file mode 100644
index 00000000..be2da616
--- /dev/null
+++ b/ci/matrix.nix
@@ -0,0 +1,8 @@
+{
+ qtver,
+ compiler,
+}: let
+ nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
+ compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
+ pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
+in pkg
diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix
new file mode 100644
index 00000000..3c99ab11
--- /dev/null
+++ b/ci/nix-checkouts.nix
@@ -0,0 +1,58 @@
+let
+ byCommit = {
+ commit,
+ sha256,
+ }: import (builtins.fetchTarball {
+ name = "nixpkgs-${commit}";
+ 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.
+
+ qt6_8_0 = byCommit {
+ commit = "23e89b7da85c3640bbc2173fe04f4bd114342367";
+ sha256 = "1b2v6y3bja4br5ribh9lj6xzz2k81dggz708b2mib83rwb509wyb";
+ };
+
+ qt6_7_3 = byCommit {
+ commit = "273673e839189c26130d48993d849a84199523e6";
+ sha256 = "0aca369hdxb8j0vx9791anyzy4m65zckx0lriicqhp95kv9q6m7z";
+ };
+
+ qt6_7_2 = byCommit {
+ commit = "841f166ff96fc2f3ecd1c0cc08072633033d41bf";
+ sha256 = "0d7p0cp7zjiadhpa6sdafxvrpw4lnmb1h673w17q615vm1yaasvy";
+ };
+
+ qt6_7_1 = byCommit {
+ commit = "69bee9866a4e2708b3153fdb61c1425e7857d6b8";
+ sha256 = "1an4sha4jsa29dvc4n9mqxbq8jjwg7frl0rhy085g73m7l1yx0lj";
+ };
+
+ qt6_7_0 = byCommit {
+ commit = "4fbbc17ccf11bc80002b19b31387c9c80276f076";
+ sha256 = "09lhgdqlx8j9a7vpdcf8sddlhbzjq0s208spfmxfjdn14fvx8k0j";
+ };
+
+ qt6_6_3 = byCommit {
+ commit = "8f1a3fbaa92f1d59b09f2d24af6a607b5a280071";
+ sha256 = "0322zwxvmg8v2wkm03xpk6mqmmbfjgrhc9prcx0zd36vjl6jmi18";
+ };
+
+ qt6_6_2 = byCommit {
+ commit = "0bb9cfbd69459488576a0ef3c0e0477bedc3a29e";
+ sha256 = "172ww486jm1mczk9id78s32p7ps9m9qgisml286flc8jffb6yad8";
+ };
+
+ qt6_6_1 = byCommit {
+ commit = "8eecc3342103c38eea666309a7c0d90d403a039a";
+ sha256 = "1lakc0immsgrpz3basaysdvd0sx01r0mcbyymx6id12fk0404z5r";
+ };
+
+ qt6_6_0 = byCommit {
+ commit = "1ded005f95a43953112ffc54b39593ea2f16409f";
+ sha256 = "1xvyd3lj81hak9j53mrhdsqx78x5v2ppv8m2s54qa2099anqgm0f";
+ };
+}
diff --git a/ci/variations.nix b/ci/variations.nix
new file mode 100644
index 00000000..b0889be6
--- /dev/null
+++ b/ci/variations.nix
@@ -0,0 +1,7 @@
+{
+ clangStdenv,
+ gccStdenv,
+}: {
+ clang = { buildStdenv = clangStdenv; };
+ gcc = { buildStdenv = gccStdenv; };
+}
diff --git a/cmake/install-qml-module.cmake b/cmake/install-qml-module.cmake
new file mode 100644
index 00000000..5c95531c
--- /dev/null
+++ b/cmake/install-qml-module.cmake
@@ -0,0 +1,89 @@
+set(INSTALL_QMLDIR "" CACHE STRING "QML install dir")
+set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix")
+
+# There doesn't seem to be a standard cross-distro qml install path.
+if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "")
+ message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.")
+else()
+ if ("${INSTALL_QMLDIR}" STREQUAL "")
+ set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}")
+ else()
+ set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}")
+ endif()
+
+ message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}")
+endif()
+
+# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem
+# to be an official way to do it.
+# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160
+function(install_qml_module arg_TARGET)
+ if (NOT DEFINED QML_FULL_INSTALLDIR)
+ return()
+ endif()
+
+ qt_query_qml_module(${arg_TARGET}
+ URI module_uri
+ VERSION module_version
+ PLUGIN_TARGET module_plugin_target
+ TARGET_PATH module_target_path
+ QMLDIR module_qmldir
+ TYPEINFO module_typeinfo
+ QML_FILES module_qml_files
+ RESOURCES module_resources
+ )
+
+ set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}")
+
+ if (NOT TARGET "${module_plugin_target}")
+ message(FATAL_ERROR "install_qml_modules called for a target without a plugin")
+ endif()
+
+ get_target_property(target_type "${arg_TARGET}" TYPE)
+ if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY")
+ install(
+ TARGETS "${arg_TARGET}"
+ LIBRARY DESTINATION "${module_dir}"
+ RUNTIME DESTINATION "${module_dir}"
+ )
+
+ install(
+ TARGETS "${module_plugin_target}"
+ LIBRARY DESTINATION "${module_dir}"
+ RUNTIME DESTINATION "${module_dir}"
+ )
+ endif()
+
+ install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
+ install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
+
+ # Install QML files
+ list(LENGTH module_qml_files num_files)
+ if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
+ qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths)
+
+ math(EXPR last_index "${num_files} - 1")
+ foreach(i RANGE 0 ${last_index})
+ list(GET module_qml_files ${i} src_file)
+ list(GET qml_files_deploy_paths ${i} deploy_path)
+ get_filename_component(dst_name "${deploy_path}" NAME)
+ get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
+ install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
+ endforeach()
+ endif()
+
+ # Install resources
+ list(LENGTH module_resources num_files)
+ if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
+ qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths)
+
+ math(EXPR last_index "${num_files} - 1")
+ foreach(i RANGE 0 ${last_index})
+ list(GET module_resources ${i} src_file)
+ list(GET resources_deploy_paths ${i} deploy_path)
+ get_filename_component(dst_name "${deploy_path}" NAME)
+ get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
+ install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
+ endforeach()
+ endif()
+endfunction()
diff --git a/cmake/pch.cmake b/cmake/pch.cmake
new file mode 100644
index 00000000..e136015e
--- /dev/null
+++ b/cmake/pch.cmake
@@ -0,0 +1,85 @@
+# pch breaks clang-tidy..... somehow
+if (NOT NO_PCH)
+ file(GENERATE
+ OUTPUT ${CMAKE_BINARY_DIR}/pchstub.cpp
+ CONTENT "// intentionally empty"
+ )
+endif()
+
+function (qs_pch target)
+ if (NO_PCH)
+ return()
+ endif()
+
+ cmake_parse_arguments(PARSE_ARGV 1 arg "" "SET" "")
+
+ if ("${arg_SET}" STREQUAL "")
+ set(arg_SET "common")
+ endif()
+
+ target_precompile_headers(${target} REUSE_FROM "qs-pchset-${arg_SET}")
+endfunction()
+
+function (qs_module_pch target)
+ qs_pch(${target} ${ARGN})
+ qs_pch("${target}plugin" SET plugin)
+ qs_pch("${target}plugin_init" SET plugin)
+endfunction()
+
+function (qs_add_pchset SETNAME)
+ if (NO_PCH)
+ return()
+ endif()
+
+ cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "HEADERS;DEPENDENCIES")
+
+ set(LIBNAME "qs-pchset-${SETNAME}")
+
+ add_library(${LIBNAME} ${CMAKE_BINARY_DIR}/pchstub.cpp)
+ target_link_libraries(${LIBNAME} ${arg_DEPENDENCIES})
+ target_precompile_headers(${LIBNAME} PUBLIC ${arg_HEADERS})
+endfunction()
+
+set(COMMON_PCH_SET
+
+
+
+
+
+
+
+
+
+
+)
+
+qs_add_pchset(common
+ DEPENDENCIES Qt::Quick
+ HEADERS ${COMMON_PCH_SET}
+)
+
+qs_add_pchset(large
+ DEPENDENCIES Qt::Quick
+ HEADERS
+ ${COMMON_PCH_SET}
+
+
+
+
+
+
+
+
+
+
+)
+
+
+# including qplugin.h directly will cause required symbols to disappear
+qs_add_pchset(plugin
+ DEPENDENCIES Qt::Qml
+ HEADERS
+
+
+
+)
diff --git a/cmake/util.cmake b/cmake/util.cmake
new file mode 100644
index 00000000..14fa7c2d
--- /dev/null
+++ b/cmake/util.cmake
@@ -0,0 +1,29 @@
+# Adds a dependency hint to the link order, but does not block build on the dependency.
+function (qs_add_link_dependencies target)
+ set_property(
+ TARGET ${target}
+ APPEND PROPERTY INTERFACE_LINK_LIBRARIES
+ ${ARGN}
+ )
+endfunction()
+
+function (qs_append_qmldir target text)
+ get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content)
+
+ if ("${qmldir_content}" STREQUAL "")
+ message(WARNING "qs_append_qmldir depends on private Qt cmake code, which has broken.")
+ return()
+ endif()
+
+ set_property(TARGET ${target} APPEND_STRING PROPERTY _qt_internal_qmldir_content ${text})
+endfunction()
+
+# DEPENDENCIES introduces a cmake dependency which we don't need with static modules.
+# This greatly improves comp speed by not introducing those dependencies.
+function (qs_add_module_deps_light target)
+ foreach (dep IN LISTS ARGN)
+ string(APPEND qmldir_extra "depends ${dep}\n")
+ endforeach()
+
+ qs_append_qmldir(${target} "${qmldir_extra}")
+endfunction()
diff --git a/default.nix b/default.nix
index 514c7946..fab038a7 100644
--- a/default.nix
+++ b/default.nix
@@ -3,13 +3,20 @@
nix-gitignore,
pkgs,
keepDebugInfo,
- buildStdenv ? pkgs.clang17Stdenv,
+ buildStdenv ? pkgs.clangStdenv,
cmake,
ninja,
qt6,
+ spirv-tools,
+ cli11,
+ breakpad,
+ jemalloc,
wayland,
wayland-protocols,
+ xorg,
+ pipewire,
+ pam,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@@ -23,10 +30,15 @@
else "unknown"),
debug ? false,
- enableWayland ? true,
- enablePipewire ? true,
- nvidiaCompat ? false,
- svgSupport ? true, # you almost always want this
+ withCrashReporter ? true,
+ withJemalloc ? true, # masks heap fragmentation
+ withQtSvg ? true,
+ withWayland ? true,
+ withX11 ? true,
+ withPipewire ? true,
+ withPam ? true,
+ withHyprland ? true,
+ withI3 ? true,
}: buildStdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.1.0";
@@ -35,45 +47,54 @@
nativeBuildInputs = with pkgs; [
cmake
ninja
+ qt6.qtshadertools
+ spirv-tools
qt6.wrapQtAppsHook
- ] ++ (lib.optionals enableWayland [
pkg-config
+ ] ++ (lib.optionals withWayland [
wayland-protocols
wayland-scanner
]);
- buildInputs = with pkgs; [
+ buildInputs = [
qt6.qtbase
qt6.qtdeclarative
+ cli11
]
- ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
- ++ (lib.optionals svgSupport [ qt6.qtsvg ])
- ++ (lib.optionals enablePipewire [ pipewire ]);
+ ++ lib.optional withCrashReporter breakpad
+ ++ lib.optional withJemalloc jemalloc
+ ++ lib.optional withQtSvg qt6.qtsvg
+ ++ lib.optionals withWayland [ qt6.qtwayland wayland ]
+ ++ lib.optional withX11 xorg.libxcb
+ ++ lib.optional withPam pam
+ ++ lib.optional withPipewire pipewire;
- QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
-
- configurePhase = let
- cmakeBuildType = if debug
- then "Debug"
- else "RelWithDebInfo";
- in ''
- cmakeBuildType=${cmakeBuildType} # qt6 setup hook resets this for some godforsaken reason
- cmakeConfigurePhase
- '';
+ cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
cmakeFlags = [
- "-DGIT_REVISION=${gitRev}"
- ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
- ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"
- ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF";
+ (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
+ (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
+ (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
+ (lib.cmakeFeature "GIT_REVISION" gitRev)
+ (lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
+ (lib.cmakeBool "USE_JEMALLOC" withJemalloc)
+ (lib.cmakeBool "WAYLAND" withWayland)
+ (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
+ (lib.cmakeBool "SERVICE_PAM" withPam)
+ (lib.cmakeBool "HYPRLAND" withHyprland)
+ (lib.cmakeBool "I3" withI3)
+ ];
- buildPhase = "ninjaBuildPhase";
- enableParallelBuilding = true;
- dontStrip = true;
+ # How to get debuginfo in gdb from a release build:
+ # 1. build `quickshell.debug`
+ # 2. set NIX_DEBUG_INFO_DIRS="/lib/debug"
+ # 3. launch gdb / coredumpctl and debuginfo will work
+ separateDebugInfo = !debug;
+ dontStrip = debug;
meta = with lib; {
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
- description = "Simple and flexbile QtQuick based desktop shell toolkit";
+ description = "Flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
};
diff --git a/docs b/docs
deleted file mode 160000
index ff5da84a..00000000
--- a/docs
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903
diff --git a/examples b/examples
deleted file mode 160000
index b9e744b5..00000000
--- a/examples
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96
diff --git a/flake.lock b/flake.lock
index 1527f635..ed928826 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1709237383,
- "narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
+ "lastModified": 1732014248,
+ "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
+ "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 5bb5069e..a0bc18d4 100644
--- a/flake.nix
+++ b/flake.nix
@@ -12,10 +12,8 @@
quickshell = pkgs.callPackage ./default.nix {
gitRev = self.rev or self.dirtyRev;
};
- quickshell-nvidia = quickshell.override { nvidiaCompat = true; };
default = quickshell;
- nvidia = quickshell-nvidia;
});
devShells = forEachSystem (system: pkgs: rec {
diff --git a/shell.nix b/shell.nix
index 07b5b57d..82382f90 100644
--- a/shell.nix
+++ b/shell.nix
@@ -15,13 +15,12 @@ in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
nativeBuildInputs = with pkgs; [
just
- clang-tools_17
+ clang-tools
parallel
makeWrapper
];
TIDYFOX = "${tidyfox}/lib/libtidyfox.so";
- QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER;
shellHook = ''
export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8fe9c651..882d2bae 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -2,8 +2,18 @@ qt_add_executable(quickshell main.cpp)
install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+add_subdirectory(build)
+add_subdirectory(launch)
add_subdirectory(core)
+add_subdirectory(debug)
+add_subdirectory(ipc)
+add_subdirectory(window)
add_subdirectory(io)
+add_subdirectory(widgets)
+
+if (CRASH_REPORTER)
+ add_subdirectory(crash)
+endif()
if (DBUS)
add_subdirectory(dbus)
@@ -11,6 +21,10 @@ endif()
if (WAYLAND)
add_subdirectory(wayland)
-endif ()
+endif()
+
+if (X11)
+ add_subdirectory(x11)
+endif()
add_subdirectory(services)
diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt
new file mode 100644
index 00000000..bb35da99
--- /dev/null
+++ b/src/build/CMakeLists.txt
@@ -0,0 +1,26 @@
+add_library(quickshell-build INTERFACE)
+
+if (NOT DEFINED GIT_REVISION)
+ execute_process(
+ COMMAND git rev-parse HEAD
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ OUTPUT_VARIABLE GIT_REVISION
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+endif()
+
+if (CRASH_REPORTER)
+ set(CRASH_REPORTER_DEF 1)
+else()
+ set(CRASH_REPORTER_DEF 0)
+endif()
+
+if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
+ set(DEBUGINFO_AVAILABLE 1)
+else()
+ set(DEBUGINFO_AVAILABLE 0)
+endif()
+
+configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
+
+target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in
new file mode 100644
index 00000000..075abd17
--- /dev/null
+++ b/src/build/build.hpp.in
@@ -0,0 +1,12 @@
+#pragma once
+
+// NOLINTBEGIN
+#define GIT_REVISION "@GIT_REVISION@"
+#define DISTRIBUTOR "@DISTRIBUTOR@"
+#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
+#define CRASH_REPORTER @CRASH_REPORTER_DEF@
+#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
+#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
+#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
+#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
+// NOLINTEND
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index b40b807f..6778e984 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1,20 +1,14 @@
qt_add_library(quickshell-core STATIC
- main.cpp
plugin.cpp
shell.cpp
variants.cpp
rootwrapper.cpp
- proxywindow.cpp
reload.cpp
rootwrapper.cpp
qmlglobal.cpp
qmlscreen.cpp
region.cpp
persistentprops.cpp
- windowinterface.cpp
- floatingwindow.cpp
- panelinterface.cpp
- popupwindow.cpp
singleton.cpp
generation.cpp
scan.cpp
@@ -26,13 +20,38 @@ qt_add_library(quickshell-core STATIC
imageprovider.cpp
transformwatcher.cpp
boundcomponent.cpp
+ model.cpp
+ elapsedtimer.cpp
+ desktopentry.cpp
+ objectrepeater.cpp
+ platformmenu.cpp
+ qsmenu.cpp
+ retainable.cpp
+ popupanchor.cpp
+ types.cpp
+ qsmenuanchor.cpp
+ clock.cpp
+ logging.cpp
+ paths.cpp
+ instanceinfo.cpp
+ common.cpp
+ iconprovider.cpp
+ scriptmodel.cpp
)
-set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
-qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1)
+qt_add_qml_module(quickshell-core
+ URI Quickshell
+ VERSION 0.1
+ DEPENDENCIES QtQuick
+ OPTIONAL_IMPORTS Quickshell._Window
+ DEFAULT_IMPORTS Quickshell._Window
+)
-target_link_libraries(quickshell-core PRIVATE ${QT_DEPS})
-qs_pch(quickshell-core)
+install_qml_module(quickshell-core)
+
+target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
+
+qs_module_pch(quickshell-core SET large)
target_link_libraries(quickshell PRIVATE quickshell-coreplugin)
diff --git a/src/core/clock.cpp b/src/core/clock.cpp
new file mode 100644
index 00000000..ebb7e92a
--- /dev/null
+++ b/src/core/clock.cpp
@@ -0,0 +1,97 @@
+#include "clock.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "util.hpp"
+
+SystemClock::SystemClock(QObject* parent): QObject(parent) {
+ QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout);
+ this->update();
+}
+
+bool SystemClock::enabled() const { return this->mEnabled; }
+
+void SystemClock::setEnabled(bool enabled) {
+ if (enabled == this->mEnabled) return;
+ this->mEnabled = enabled;
+ emit this->enabledChanged();
+ this->update();
+}
+
+SystemClock::Enum SystemClock::precision() const { return this->mPrecision; }
+
+void SystemClock::setPrecision(SystemClock::Enum precision) {
+ if (precision == this->mPrecision) return;
+ this->mPrecision = precision;
+ emit this->precisionChanged();
+ this->update();
+}
+
+void SystemClock::onTimeout() {
+ this->setTime(this->targetTime);
+ this->schedule(this->targetTime);
+}
+
+void SystemClock::update() {
+ if (this->mEnabled) {
+ this->setTime(QDateTime::fromMSecsSinceEpoch(0));
+ this->schedule(QDateTime::fromMSecsSinceEpoch(0));
+ } else {
+ this->timer.stop();
+ }
+}
+
+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();
+
+ auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
+ auto secondChanged = this->setSeconds(secondPrecision ? 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);
+}
+
+void SystemClock::schedule(const QDateTime& targetTime) {
+ auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
+ auto minutePrecision = this->mPrecision >= SystemClock::Minutes;
+ auto hourPrecision = this->mPrecision >= SystemClock::Hours;
+
+ auto currentTime = QDateTime::currentDateTime();
+
+ auto offset = currentTime.msecsTo(targetTime);
+
+ // timer skew
+ 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}
+ );
+
+ if (secondPrecision) nextTime = nextTime.addSecs(1);
+ else if (minutePrecision) nextTime = nextTime.addSecs(60);
+ else if (hourPrecision) nextTime = nextTime.addSecs(3600);
+
+ auto delay = currentTime.msecsTo(nextTime);
+
+ this->timer.start(static_cast(delay));
+ this->targetTime = nextTime;
+}
+
+DEFINE_MEMBER_GETSET(SystemClock, hours, setHours);
+DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes);
+DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds);
diff --git a/src/core/clock.hpp b/src/core/clock.hpp
new file mode 100644
index 00000000..3e669589
--- /dev/null
+++ b/src/core/clock.hpp
@@ -0,0 +1,72 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "util.hpp"
+
+///! System clock accessor.
+class SystemClock: public QObject {
+ Q_OBJECT;
+ /// If the clock should update. Defaults to true.
+ ///
+ /// Setting enabled to false pauses the clock.
+ 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 hour.
+ Q_PROPERTY(quint32 hours READ hours NOTIFY hoursChanged);
+ /// The current minute, or 0 if @@precision is `SystemClock.Hours`.
+ Q_PROPERTY(quint32 minutes READ minutes NOTIFY minutesChanged);
+ /// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`.
+ Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged);
+ QML_ELEMENT;
+
+public:
+ // must be named enum until docgen is ready to handle member enums better
+ enum Enum : quint8 {
+ Hours = 1,
+ Minutes = 2,
+ Seconds = 3,
+ };
+ Q_ENUM(Enum);
+
+ explicit SystemClock(QObject* parent = nullptr);
+
+ [[nodiscard]] bool enabled() const;
+ void setEnabled(bool enabled);
+
+ [[nodiscard]] SystemClock::Enum precision() const;
+ void setPrecision(SystemClock::Enum precision);
+
+signals:
+ void enabledChanged();
+ void precisionChanged();
+ void hoursChanged();
+ void minutesChanged();
+ void secondsChanged();
+
+private slots:
+ void onTimeout();
+
+private:
+ bool mEnabled = true;
+ SystemClock::Enum mPrecision = SystemClock::Seconds;
+ quint32 mHours = 0;
+ quint32 mMinutes = 0;
+ quint32 mSeconds = 0;
+ QTimer timer;
+ 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);
+};
diff --git a/src/core/common.cpp b/src/core/common.cpp
new file mode 100644
index 00000000..d09661f1
--- /dev/null
+++ b/src/core/common.cpp
@@ -0,0 +1,9 @@
+#include "common.hpp"
+
+#include
+
+namespace qs {
+
+const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
+
+}
diff --git a/src/core/common.hpp b/src/core/common.hpp
new file mode 100644
index 00000000..36094f89
--- /dev/null
+++ b/src/core/common.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include
+
+namespace qs {
+
+struct Common {
+ static const QDateTime LAUNCH_TIME;
+};
+
+} // namespace qs
diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp
new file mode 100644
index 00000000..3714df01
--- /dev/null
+++ b/src/core/desktopentry.cpp
@@ -0,0 +1,389 @@
+#include "desktopentry.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "model.hpp"
+
+Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
+
+struct Locale {
+ explicit Locale() = default;
+
+ explicit Locale(const QString& string) {
+ auto territoryIdx = string.indexOf('_');
+ auto codesetIdx = string.indexOf('.');
+ auto modifierIdx = string.indexOf('@');
+
+ auto parseEnd = string.length();
+
+ if (modifierIdx != -1) {
+ this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1);
+ parseEnd = modifierIdx;
+ }
+
+ if (codesetIdx != -1) {
+ parseEnd = codesetIdx;
+ }
+
+ if (territoryIdx != -1) {
+ this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1);
+ parseEnd = territoryIdx;
+ }
+
+ this->language = string.sliced(0, parseEnd);
+ }
+
+ [[nodiscard]] bool isValid() const { return !this->language.isEmpty(); }
+
+ [[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;
+
+ auto score = 1;
+ if (territoryMatches) score += 2;
+ if (modifierMatches) score += 1;
+
+ return score;
+ }
+
+ static const Locale& system() {
+ static Locale* locale = nullptr; // NOLINT
+
+ if (locale == nullptr) {
+ auto lstr = qEnvironmentVariable("LC_MESSAGES");
+ if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG");
+ locale = new Locale(lstr);
+ }
+
+ return *locale;
+ }
+
+ QString language;
+ QString territory;
+ QString modifier;
+};
+
+QDebug operator<<(QDebug debug, const Locale& locale) {
+ auto saver = QDebugStateSaver(debug);
+ debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
+ << ", modifier" << locale.modifier << ')';
+
+ return debug;
+}
+
+void DesktopEntry::parseEntry(const QString& text) {
+ const auto& system = Locale::system();
+
+ auto groupName = QString();
+ auto entries = QHash>();
+
+ auto finishCategory = [this, &groupName, &entries]() {
+ if (groupName == "Desktop Entry") {
+ if (entries["Type"].second != "Application") return;
+ if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
+
+ for (const auto& [key, pair]: entries.asKeyValueRange()) {
+ auto& [_, value] = pair;
+ this->mEntries.insert(key, value);
+
+ if (key == "Name") this->mName = value;
+ else if (key == "GenericName") this->mGenericName = value;
+ else if (key == "NoDisplay") this->mNoDisplay = value == "true";
+ else if (key == "Comment") this->mComment = value;
+ else if (key == "Icon") this->mIcon = value;
+ else if (key == "Exec") this->mExecString = 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);
+ }
+ } else if (groupName.startsWith("Desktop Action ")) {
+ auto actionName = groupName.sliced(16);
+ auto* action = new DesktopAction(actionName, this);
+
+ for (const auto& [key, pair]: entries.asKeyValueRange()) {
+ const auto& [_, value] = pair;
+ action->mEntries.insert(key, value);
+
+ if (key == "Name") action->mName = value;
+ else if (key == "Icon") action->mIcon = value;
+ else if (key == "Exec") action->mExecString = value;
+ }
+
+ this->mActions.insert(actionName, action);
+ }
+
+ entries.clear();
+ };
+
+ for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) {
+ if (line.startsWith(u'#')) continue;
+
+ if (line.startsWith(u'[') && line.endsWith(u']')) {
+ finishCategory();
+ groupName = line.sliced(1, line.length() - 2);
+ continue;
+ }
+
+ auto splitIdx = line.indexOf(u'=');
+ if (splitIdx == -1) {
+ qCWarning(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line;
+ continue;
+ }
+
+ auto key = line.sliced(0, splitIdx);
+ const auto& value = line.sliced(splitIdx + 1);
+
+ auto localeIdx = key.indexOf('[');
+ Locale locale;
+ if (localeIdx != -1 && localeIdx != key.length() - 1) {
+ locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2));
+ key = key.sliced(0, localeIdx);
+ }
+
+ if (entries.contains(key)) {
+ const auto& old = entries.value(key);
+
+ auto oldScore = system.matchScore(old.first);
+ auto newScore = system.matchScore(locale);
+
+ if (newScore > oldScore || (oldScore == 0 && !locale.isValid())) {
+ entries.insert(key, qMakePair(locale, value));
+ }
+ } else {
+ entries.insert(key, qMakePair(locale, value));
+ }
+ }
+
+ finishCategory();
+}
+
+void DesktopEntry::execute() const {
+ DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
+}
+
+bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
+bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
+
+QVector DesktopEntry::actions() const { return this->mActions.values(); }
+
+QVector DesktopEntry::parseExecString(const QString& execString) {
+ QVector arguments;
+ QString currentArgument;
+ auto parsingString = false;
+ auto escape = 0;
+ auto percent = false;
+
+ for (auto c: execString) {
+ if (escape == 0 && c == u'\\') {
+ escape = 1;
+ } else if (parsingString) {
+ if (c == '\\') {
+ escape++;
+ if (escape == 4) {
+ currentArgument += '\\';
+ escape = 0;
+ }
+ } else if (escape != 0) {
+ if (escape != 2) {
+ // Technically this is an illegal state, but the spec has a terrible double escape
+ // rule in strings for no discernable reason. Assuming someone might understandably
+ // misunderstand it, treat it as a normal escape and log it.
+ qCWarning(logDesktopEntry).noquote()
+ << "Illegal escape sequence in desktop entry exec string:" << execString;
+ }
+
+ currentArgument += c;
+ escape = 0;
+ } else if (c == u'"') {
+ parsingString = false;
+ } else {
+ currentArgument += c;
+ }
+ } else if (escape != 0) {
+ currentArgument += c;
+ escape = 0;
+ } else if (percent) {
+ if (c == '%') {
+ currentArgument += '%';
+ } // else discard
+
+ percent = false;
+ } else if (c == '%') {
+ percent = true;
+ } else if (c == u'"') {
+ parsingString = true;
+ } else if (c == u' ') {
+ if (!currentArgument.isEmpty()) {
+ arguments.push_back(currentArgument);
+ currentArgument.clear();
+ }
+ } else {
+ currentArgument += c;
+ }
+ }
+
+ if (!currentArgument.isEmpty()) {
+ arguments.push_back(currentArgument);
+ currentArgument.clear();
+ }
+
+ return arguments;
+}
+
+void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
+ auto args = DesktopEntry::parseExecString(execString);
+ if (args.isEmpty()) {
+ qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
+ return;
+ }
+
+ auto process = QProcess();
+ process.setProgram(args.at(0));
+ process.setArguments(args.sliced(1));
+ if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
+ process.startDetached();
+}
+
+void DesktopAction::execute() const {
+ DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
+}
+
+DesktopEntryManager::DesktopEntryManager() {
+ this->scanDesktopEntries();
+ this->populateApplications();
+}
+
+void DesktopEntryManager::scanDesktopEntries() {
+ QList dataPaths;
+
+ 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");
+ }
+
+ qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
+
+ 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.path(), prefix + dir.dirName() + "-");
+ else if (entry.isFile()) {
+ auto path = entry.filePath();
+ if (!path.endsWith(".desktop")) {
+ qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
+ continue;
+ }
+
+ auto* file = new QFile(path);
+
+ if (!file->open(QFile::ReadOnly)) {
+ qCDebug(logDesktopEntry) << "Could not open file" << path;
+ continue;
+ }
+
+ auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
+ auto lowerId = id.toLower();
+
+ 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);
+ }
+ }
+}
+
+DesktopEntryManager* DesktopEntryManager::instance() {
+ static auto* instance = new DesktopEntryManager(); // NOLINT
+ return instance;
+}
+
+DesktopEntry* DesktopEntryManager::byId(const QString& id) {
+ if (auto* entry = this->desktopEntries.value(id)) {
+ return entry;
+ } else if (auto* entry = this->lowercaseDesktopEntries.value(id.toLower())) {
+ return entry;
+ } else {
+ return nullptr;
+ }
+}
+
+ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; }
+
+DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
+
+DesktopEntry* DesktopEntries::byId(const QString& id) {
+ return DesktopEntryManager::instance()->byId(id);
+}
+
+ObjectModel* DesktopEntries::applications() {
+ return DesktopEntryManager::instance()->applications();
+}
diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp
new file mode 100644
index 00000000..3871181b
--- /dev/null
+++ b/src/core/desktopentry.hpp
@@ -0,0 +1,155 @@
+#pragma once
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "doc.hpp"
+#include "model.hpp"
+
+class DesktopAction;
+
+/// 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);
+ /// Short description of the application, such as "Web Browser". May be empty.
+ Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
+ /// If true, this application should not be displayed in menus and launchers.
+ Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
+ /// Long description of the application, such as "View websites on the internet". May be empty.
+ Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
+ /// Name of the icon associated with this application. May be empty.
+ Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
+ /// The raw `Exec` string from the desktop entry. You probably want @@execute().
+ Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
+ /// The working directory to execute from.
+ Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
+ /// If the application should run in a terminal.
+ Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
+ Q_PROPERTY(QVector categories MEMBER mCategories CONSTANT);
+ Q_PROPERTY(QVector keywords MEMBER mKeywords CONSTANT);
+ Q_PROPERTY(QVector actions READ actions CONSTANT);
+ QML_ELEMENT;
+ QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
+
+public:
+ explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
+
+ void parseEntry(const QString& text);
+
+ /// Run the application. Currently ignores @@runInTerminal and field codes.
+ Q_INVOKABLE void execute() const;
+
+ [[nodiscard]] bool isValid() const;
+ [[nodiscard]] bool noDisplay() const;
+ [[nodiscard]] QVector actions() const;
+
+ // currently ignores all field codes.
+ static QVector parseExecString(const QString& execString);
+ static void doExec(const QString& execString, const QString& workingDirectory);
+
+public:
+ QString mId;
+ QString mName;
+ QString mGenericName;
+ bool mNoDisplay = false;
+ QString mComment;
+ QString mIcon;
+ QString mExecString;
+ QString mWorkingDirectory;
+ bool mTerminal = false;
+ QVector mCategories;
+ QVector mKeywords;
+
+private:
+ QHash mEntries;
+ QHash mActions;
+
+ friend class DesktopAction;
+};
+
+/// An action of a @@DesktopEntry$.
+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);
+ /// The raw `Exec` string from the desktop entry. You probably want @@execute().
+ Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
+ QML_ELEMENT;
+ QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
+
+public:
+ explicit DesktopAction(QString id, DesktopEntry* entry)
+ : QObject(entry)
+ , entry(entry)
+ , mId(std::move(id)) {}
+
+ /// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
+ Q_INVOKABLE void execute() const;
+
+private:
+ DesktopEntry* entry;
+ QString mId;
+ QString mName;
+ QString mIcon;
+ QString mExecString;
+ QHash mEntries;
+
+ friend class DesktopEntry;
+};
+
+class DesktopEntryManager: public QObject {
+ Q_OBJECT;
+
+public:
+ void scanDesktopEntries();
+
+ [[nodiscard]] DesktopEntry* byId(const QString& id);
+
+ [[nodiscard]] ObjectModel* applications();
+
+ static DesktopEntryManager* instance();
+
+private:
+ explicit DesktopEntryManager();
+
+ void populateApplications();
+ void scanPath(const QDir& dir, const QString& prefix = QString());
+
+ QHash desktopEntries;
+ QHash lowercaseDesktopEntries;
+ ObjectModel mApplications {this};
+};
+
+///! Desktop entry index.
+/// Index of desktop entries according to the [desktop entry specification].
+///
+/// Primarily useful for looking up icons and metadata from an id, as there is
+/// currently no mechanism for usage based sorting of entries and other launcher niceties.
+///
+/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/
+class DesktopEntries: public QObject {
+ Q_OBJECT;
+ /// All desktop entries of type Application that are not Hidden or NoDisplay.
+ QSDOC_TYPE_OVERRIDE(ObjectModel*);
+ Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT);
+ QML_ELEMENT;
+ QML_SINGLETON;
+
+public:
+ explicit DesktopEntries();
+
+ /// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
+ Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
+
+ [[nodiscard]] static ObjectModel* applications();
+};
diff --git a/src/core/doc.hpp b/src/core/doc.hpp
index b619b0a6..fbb21400 100644
--- a/src/core/doc.hpp
+++ b/src/core/doc.hpp
@@ -10,5 +10,14 @@
#define QSDOC_ELEMENT
#define QSDOC_NAMED_ELEMENT(name)
+// unmark uncreatable (will be overlayed by other types)
+#define QSDOC_CREATABLE
+
+// change the cname used for this type
+#define QSDOC_CNAME(name)
+
// overridden properties
#define QSDOC_PROPERTY_OVERRIDE(...)
+
+// override types of properties for docs
+#define QSDOC_TYPE_OVERRIDE(type)
diff --git a/src/core/elapsedtimer.cpp b/src/core/elapsedtimer.cpp
new file mode 100644
index 00000000..91321122
--- /dev/null
+++ b/src/core/elapsedtimer.cpp
@@ -0,0 +1,22 @@
+#include "elapsedtimer.hpp"
+
+#include
+
+ElapsedTimer::ElapsedTimer() { this->timer.start(); }
+
+qreal ElapsedTimer::elapsed() { return static_cast(this->elapsedNs()) / 1000000000.0; }
+
+qreal ElapsedTimer::restart() { return static_cast(this->restartNs()) / 1000000000.0; }
+
+qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); }
+
+qint64 ElapsedTimer::restartMs() { return this->timer.restart(); }
+
+qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); }
+
+qint64 ElapsedTimer::restartNs() {
+ // see qelapsedtimer.cpp
+ auto old = this->timer;
+ this->timer.start();
+ return old.durationTo(this->timer).count();
+}
diff --git a/src/core/elapsedtimer.hpp b/src/core/elapsedtimer.hpp
new file mode 100644
index 00000000..85850963
--- /dev/null
+++ b/src/core/elapsedtimer.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+///! Measures time between events
+/// The ElapsedTimer measures time since its last restart, and is useful
+/// for determining the time between events that don't supply it.
+class ElapsedTimer: public QObject {
+ Q_OBJECT;
+ QML_ELEMENT;
+
+public:
+ explicit ElapsedTimer();
+
+ /// Return the number of seconds since the timer was last
+ /// started or restarted, with nanosecond precision.
+ Q_INVOKABLE qreal elapsed();
+
+ /// Restart the timer, returning the number of seconds since
+ /// the timer was last started or restarted, with nanosecond precision.
+ Q_INVOKABLE qreal restart();
+
+ /// Return the number of milliseconds since the timer was last
+ /// started or restarted.
+ Q_INVOKABLE qint64 elapsedMs();
+
+ /// Restart the timer, returning the number of milliseconds since
+ /// the timer was last started or restarted.
+ Q_INVOKABLE qint64 restartMs();
+
+ /// Return the number of nanoseconds since the timer was last
+ /// started or restarted.
+ Q_INVOKABLE qint64 elapsedNs();
+
+ /// Restart the timer, returning the number of nanoseconds since
+ /// the timer was last started or restarted.
+ Q_INVOKABLE qint64 restartNs();
+
+private:
+ QElapsedTimer timer;
+};
diff --git a/src/core/generation.cpp b/src/core/generation.cpp
index 77e4a9cb..ef4449b3 100644
--- a/src/core/generation.cpp
+++ b/src/core/generation.cpp
@@ -4,6 +4,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -12,7 +14,6 @@
#include
#include
#include
-#include
#include
#include "iconimageprovider.hpp"
@@ -23,10 +24,12 @@
#include "reload.hpp"
#include "scan.hpp"
-static QHash g_generations; // NOLINT
+static QHash g_generations; // NOLINT
-EngineGeneration::EngineGeneration(QmlScanner scanner)
- : scanner(std::move(scanner))
+EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
+ : rootPath(rootPath)
+ , scanner(std::move(scanner))
+ , urlInterceptor(this->rootPath)
, interceptNetFactory(this->scanner.qmldirIntercepts)
, engine(new QQmlEngine()) {
g_generations.insert(this->engine, this);
@@ -39,56 +42,93 @@ EngineGeneration::EngineGeneration(QmlScanner scanner)
this->engine->addImageProvider("qsimage", new QsImageProvider());
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
- QuickshellPlugin::runConstructGeneration(*this);
+ QsEnginePlugin::runConstructGeneration(*this);
}
EngineGeneration::~EngineGeneration() {
- g_generations.remove(this->engine);
- delete this->engine;
+ if (this->engine != nullptr) {
+ qFatal() << this << "destroyed without calling destroy()";
+ }
}
void EngineGeneration::destroy() {
- // Multiple generations can detect a reload at the same time.
- delete this->watcher;
- this->watcher = nullptr;
+ if (this->destroying) return;
+ this->destroying = true;
- // Yes all of this is actually necessary.
- if (this->engine != nullptr && this->root != nullptr) {
+ if (this->watcher != nullptr) {
+ // Multiple generations can detect a reload at the same time.
+ QObject::disconnect(this->watcher, nullptr, this, nullptr);
+ this->watcher->deleteLater();
+ this->watcher = nullptr;
+ }
+
+ for (auto* extension: this->extensions.values()) {
+ delete extension;
+ }
+
+ if (this->root != nullptr) {
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
- // The timer seems to fix *one* of the possible qml item destructor crashes.
- QTimer::singleShot(0, [this]() {
- // Garbage is not collected during engine destruction.
- this->engine->collectGarbage();
+ // prevent further js execution between garbage collection and engine destruction.
+ this->engine->setInterrupted(true);
- QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; });
+ g_generations.remove(this->engine);
- // Even after all of that there's still multiple failing assertions and segfaults.
- // Pray you don't hit one.
- // Note: it appeats *some* of the crashes are related to values owned by the generation.
- // Test by commenting the connect() above.
- this->engine->deleteLater();
- this->engine = nullptr;
- });
+ // Garbage is not collected during engine destruction.
+ this->engine->collectGarbage();
+
+ delete this->engine;
+ this->engine = nullptr;
+
+ auto terminate = this->shouldTerminate;
+ auto code = this->exitCode;
+ delete this;
+
+ if (terminate) QCoreApplication::exit(code);
});
this->root->deleteLater();
this->root = nullptr;
+ } else {
+ g_generations.remove(this->engine);
+
+ // the engine has never been used, no need to clean up
+ delete this->engine;
+ this->engine = nullptr;
+
+ auto terminate = this->shouldTerminate;
+ auto code = this->exitCode;
+ delete this;
+
+ if (terminate) QCoreApplication::exit(code);
}
}
+void EngineGeneration::shutdown() {
+ if (this->destroying) return;
+
+ delete this->root;
+ this->root = nullptr;
+ delete this->engine;
+ this->engine = nullptr;
+ delete this;
+}
+
void EngineGeneration::onReload(EngineGeneration* old) {
if (old != nullptr) {
// if the old generation holds the window incubation controller as the
// new generation acquires it then incubators will hang intermittently
- old->incubationControllers.clear();
+ qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
+ old->incubationControllersLocked = true;
old->assignIncubationController();
}
- auto* app = QCoreApplication::instance();
- QObject::connect(this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit);
- QObject::connect(this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit);
+ QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
+ QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
+
+ if (auto* reloadable = qobject_cast(this->root)) {
+ reloadable->reload(old ? old->root : nullptr);
+ }
- this->root->reload(old == nullptr ? nullptr : old->root);
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
this->reloadComplete = true;
emit this->reloadFinished();
@@ -105,7 +145,7 @@ void EngineGeneration::postReload() {
// This can be called on a generation during its destruction.
if (this->engine == nullptr || this->root == nullptr) return;
- QuickshellPlugin::runOnReload();
+ QsEnginePlugin::runOnReload();
PostReloadHook::postReloadTree(this->root);
this->singletonRegistry.onPostReload();
}
@@ -117,13 +157,21 @@ void EngineGeneration::setWatchingFiles(bool watching) {
for (auto& file: this->scanner.scannedFiles) {
this->watcher->addPath(file);
+ this->watcher->addPath(QFileInfo(file).dir().absolutePath());
}
QObject::connect(
this->watcher,
&QFileSystemWatcher::fileChanged,
this,
- &EngineGeneration::filesChanged
+ &EngineGeneration::onFileChanged
+ );
+
+ QObject::connect(
+ this->watcher,
+ &QFileSystemWatcher::directoryChanged,
+ this,
+ &EngineGeneration::onDirectoryChanged
);
}
} else {
@@ -134,28 +182,44 @@ void EngineGeneration::setWatchingFiles(bool watching) {
}
}
-void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
- auto* obj = dynamic_cast(controller);
+void EngineGeneration::onFileChanged(const QString& name) {
+ if (!this->watcher->files().contains(name)) {
+ this->deletedWatchedFiles.push_back(name);
+ } else {
+ emit this->filesChanged();
+ }
+}
+void EngineGeneration::onDirectoryChanged() {
+ // try to find any files that were just deleted from a replace operation
+ for (auto& file: this->deletedWatchedFiles) {
+ if (QFileInfo(file).exists()) {
+ emit this->filesChanged();
+ break;
+ }
+ }
+}
+
+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.
- if (obj == nullptr) {
- qCDebug(logIncubator) << "Could not register incubation controller as it is not a QObject"
- << controller;
+ if (auto* obj = dynamic_cast(controller)) {
+ QObject::connect(
+ obj,
+ &QObject::destroyed,
+ this,
+ &EngineGeneration::incubationControllerDestroyed
+ );
+ } else {
+ qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
+ << controller;
return;
}
- this->incubationControllers.push_back({controller, obj});
-
- QObject::connect(
- obj,
- &QObject::destroyed,
- this,
- &EngineGeneration::incubationControllerDestroyed
- );
-
- qCDebug(logIncubator) << "Registered incubation controller" << controller;
+ this->incubationControllers.push_back(controller);
+ qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
+ << this;
// This function can run during destruction.
if (this->engine == nullptr) return;
@@ -166,22 +230,20 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co
}
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
- QObject* obj = nullptr;
- this->incubationControllers.removeIf([&](QPair other) {
- if (controller == other.first) {
- obj = other.second;
- return true;
- } else return false;
- });
-
- if (obj == nullptr) {
- qCWarning(logIncubator) << "Failed to deregister incubation controller" << controller
- << "as it was not registered to begin with";
- qCWarning(logIncubator) << "Current registered incuabation controllers"
- << this->incubationControllers;
- } else {
+ if (auto* obj = dynamic_cast(controller)) {
QObject::disconnect(obj, nullptr, this, nullptr);
- qCDebug(logIncubator) << "Deregistered incubation controller" << controller;
+ } else {
+ qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
+ "however only QObject controllers should be registered.";
+ }
+
+ if (!this->incubationControllers.removeOne(controller)) {
+ qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from"
+ << this << "as it was not registered to begin with";
+ qCCritical(logIncubator) << "Current registered incuabation controllers"
+ << this->incubationControllers;
+ } else {
+ qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
}
// This function can run during destruction.
@@ -196,22 +258,25 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController*
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
- QQmlIncubationController* controller = nullptr;
-
- this->incubationControllers.removeIf([&](QPair other) {
- if (sender == other.second) {
- controller = other.first;
- return true;
- } else return false;
- });
+ auto* controller = dynamic_cast(sender);
if (controller == nullptr) {
- qCCritical(logIncubator) << "Destroyed incubation controller" << this->sender()
- << "could not be identified, this may cause memory corruption";
+ qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
+ << this << ", this may cause memory corruption";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
+
+ return;
+ }
+
+ if (this->incubationControllers.removeOne(controller)) {
+ qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from"
+ << this;
} else {
- qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered";
+ qCCritical(logIncubator) << "Destroyed incubation controller" << controller
+ << "was not registered, but its destruction was observed by" << this;
+
+ return;
}
// This function can run during destruction.
@@ -224,23 +289,64 @@ void EngineGeneration::incubationControllerDestroyed() {
}
}
+void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) {
+ if (this->extensions.contains(key)) {
+ delete this->extensions.value(key);
+ }
+
+ this->extensions.insert(key, extension);
+}
+
+EngineGenerationExt* EngineGeneration::findExtension(const void* key) {
+ return this->extensions.value(key);
+}
+
+void EngineGeneration::quit() {
+ this->shouldTerminate = true;
+ this->destroy();
+}
+
+void EngineGeneration::exit(int code) {
+ this->shouldTerminate = true;
+ this->exitCode = code;
+ this->destroy();
+}
+
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = nullptr;
- if (this->incubationControllers.isEmpty()) controller = &this->delayedIncubationController;
- else controller = this->incubationControllers.first().first;
- qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller
+ if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
+ controller = &this->delayedIncubationController;
+ } else {
+ controller = this->incubationControllers.first();
+ }
+
+ qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
+ << this
<< "fallback:" << (controller == &this->delayedIncubationController);
this->engine->setIncubationController(controller);
}
-EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
+EngineGeneration* EngineGeneration::currentGeneration() {
+ if (g_generations.size() == 1) {
+ return *g_generations.begin();
+ } else return nullptr;
+}
+
+EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
+ return g_generations.value(engine);
+}
+
+EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
+ // Objects can still attempt to find their generation after it has been destroyed.
+ // if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
+
while (object != nullptr) {
auto* context = QQmlEngine::contextForObject(object);
if (context != nullptr) {
- if (auto* generation = g_generations.value(context->engine())) {
+ if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
return generation;
}
}
diff --git a/src/core/generation.hpp b/src/core/generation.hpp
index 11ebf0be..632bd8a5 100644
--- a/src/core/generation.hpp
+++ b/src/core/generation.hpp
@@ -1,25 +1,34 @@
#pragma once
#include
+#include
#include
+#include
#include
-#include
+#include
#include
#include
#include "incubator.hpp"
#include "qsintercept.hpp"
#include "scan.hpp"
-#include "shell.hpp"
#include "singleton.hpp"
class RootWrapper;
+class QuickshellGlobal;
+
+class EngineGenerationExt {
+public:
+ EngineGenerationExt() = default;
+ virtual ~EngineGenerationExt() = default;
+ Q_DISABLE_COPY_MOVE(EngineGenerationExt);
+};
class EngineGeneration: public QObject {
Q_OBJECT;
public:
- explicit EngineGeneration(QmlScanner scanner);
+ explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner);
~EngineGeneration() override;
Q_DISABLE_COPY_MOVE(EngineGeneration);
@@ -30,30 +39,55 @@ public:
void registerIncubationController(QQmlIncubationController* controller);
void deregisterIncubationController(QQmlIncubationController* controller);
- static EngineGeneration* findObjectGeneration(QObject* object);
+ // takes ownership
+ void registerExtension(const void* key, EngineGenerationExt* extension);
+ EngineGenerationExt* findExtension(const void* key);
+
+ static EngineGeneration* findEngineGeneration(const QQmlEngine* engine);
+ static EngineGeneration* findObjectGeneration(const QObject* object);
+
+ // Returns the current generation if there is only one generation,
+ // otherwise null.
+ static EngineGeneration* currentGeneration();
RootWrapper* wrapper = nullptr;
+ QDir rootPath;
QmlScanner scanner;
QsUrlInterceptor urlInterceptor;
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
QQmlEngine* engine = nullptr;
- ShellRoot* root = nullptr;
+ QObject* root = nullptr;
SingletonRegistry singletonRegistry;
QFileSystemWatcher* watcher = nullptr;
+ QVector deletedWatchedFiles;
DelayedQmlIncubationController delayedIncubationController;
bool reloadComplete = false;
+ QuickshellGlobal* qsgInstance = nullptr;
void destroy();
+ void shutdown();
signals:
void filesChanged();
void reloadFinished();
+public slots:
+ void quit();
+ void exit(int code);
+
private slots:
+ void onFileChanged(const QString& name);
+ void onDirectoryChanged();
void incubationControllerDestroyed();
private:
void postReload();
void assignIncubationController();
- QVector> incubationControllers;
+ QVector incubationControllers;
+ bool incubationControllersLocked = false;
+ QHash extensions;
+
+ bool destroying = false;
+ bool shouldTerminate = false;
+ int exitCode = 0;
};
diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp
index f4710fb8..cf24d37d 100644
--- a/src/core/iconimageprovider.cpp
+++ b/src/core/iconimageprovider.cpp
@@ -11,7 +11,9 @@
QPixmap
IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
QString iconName;
+ QString fallbackName;
QString path;
+
auto splitIdx = id.indexOf("?path=");
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
@@ -19,10 +21,17 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
<< id;
} else {
- iconName = id;
+ splitIdx = id.indexOf("?fallback=");
+ if (splitIdx != -1) {
+ iconName = id.sliced(0, splitIdx);
+ fallbackName = id.sliced(splitIdx + 10);
+ } else {
+ iconName = id;
+ }
}
auto icon = QIcon::fromTheme(iconName);
+ if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
@@ -55,12 +64,20 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) {
return pixmap;
}
-QString IconImageProvider::requestString(const QString& icon, const QString& path) {
+QString IconImageProvider::requestString(
+ const QString& icon,
+ const QString& path,
+ const QString& fallback
+) {
auto req = "image://icon/" + icon;
if (!path.isEmpty()) {
req += "?path=" + path;
}
+ if (!fallback.isEmpty()) {
+ req += "?fallback=" + fallback;
+ }
+
return req;
}
diff --git a/src/core/iconimageprovider.hpp b/src/core/iconimageprovider.hpp
index 167d93bd..57e26049 100644
--- a/src/core/iconimageprovider.hpp
+++ b/src/core/iconimageprovider.hpp
@@ -10,5 +10,10 @@ public:
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
static QPixmap missingPixmap(const QSize& size);
- static QString requestString(const QString& icon, const QString& path);
+
+ static QString requestString(
+ const QString& icon,
+ const QString& path = QString(),
+ const QString& fallback = QString()
+ );
};
diff --git a/src/core/iconprovider.cpp b/src/core/iconprovider.cpp
new file mode 100644
index 00000000..99b423ed
--- /dev/null
+++ b/src/core/iconprovider.cpp
@@ -0,0 +1,105 @@
+#include "iconprovider.hpp"
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "generation.hpp"
+
+// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
+class PixmapCacheIconEngine: public QIconEngine {
+ void paint(
+ QPainter* /*unused*/,
+ const QRect& /*unused*/,
+ QIcon::Mode /*unused*/,
+ QIcon::State /*unused*/
+ ) override {
+ qFatal(
+ ) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
+ }
+
+ QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
+ if (this->lastPixmap.isNull() || size != this->lastSize) {
+ this->lastPixmap = this->createPixmap(size);
+ this->lastSize = size;
+ }
+
+ return this->lastPixmap;
+ }
+
+ virtual QPixmap createPixmap(const QSize& size) = 0;
+
+private:
+ QSize lastSize;
+ QPixmap lastPixmap;
+};
+
+class ImageProviderIconEngine: public PixmapCacheIconEngine {
+public:
+ explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
+ : provider(provider)
+ , id(std::move(id)) {}
+
+ QPixmap createPixmap(const QSize& size) override {
+ if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
+ return this->provider->requestPixmap(this->id, nullptr, size);
+ } else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
+ auto image = this->provider->requestImage(this->id, nullptr, size);
+ return QPixmap::fromImage(image);
+ } else {
+ qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
+ return QPixmap(); // never reached, satisfies lint
+ }
+ }
+
+ [[nodiscard]] QIconEngine* clone() const override {
+ return new ImageProviderIconEngine(this->provider, this->id);
+ }
+
+private:
+ QQuickImageProvider* provider;
+ QString id;
+};
+
+QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url) {
+ if (!engine || url.isEmpty()) return QIcon();
+
+ auto scheme = url.scheme();
+ if (scheme == "image") {
+ auto providerName = url.authority();
+ auto path = url.path();
+ if (!path.isEmpty()) path = path.sliced(1);
+
+ auto* provider = qobject_cast(engine->imageProvider(providerName));
+
+ if (provider == nullptr) {
+ qWarning() << "iconByUrl failed: no provider found for" << url;
+ return QIcon();
+ }
+
+ if (provider->imageType() == QQmlImageProviderBase::Pixmap
+ || provider->imageType() == QQmlImageProviderBase::Image)
+ {
+ return QIcon(new ImageProviderIconEngine(provider, path));
+ }
+
+ } else {
+ qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
+ }
+
+ return QIcon();
+}
+
+QIcon getCurrentEngineImageAsIcon(const QUrl& url) {
+ auto* generation = EngineGeneration::currentGeneration();
+ if (!generation) return QIcon();
+ return getEngineImageAsIcon(generation->engine, url);
+}
diff --git a/src/core/iconprovider.hpp b/src/core/iconprovider.hpp
new file mode 100644
index 00000000..173d20e6
--- /dev/null
+++ b/src/core/iconprovider.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include
+#include
+#include
+
+QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url);
+QIcon getCurrentEngineImageAsIcon(const QUrl& url);
diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp
new file mode 100644
index 00000000..96097c76
--- /dev/null
+++ b/src/core/instanceinfo.cpp
@@ -0,0 +1,35 @@
+#include "instanceinfo.hpp"
+
+#include
+
+QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
+ stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
+ return stream;
+}
+
+QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
+ stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
+ return stream;
+}
+
+QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) {
+ stream << info.instance << info.noColor << info.timestamp << info.sparseLogsOnly
+ << info.defaultLogLevel << info.logRules;
+
+ return stream;
+}
+
+QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) {
+ stream >> info.instance >> info.noColor >> info.timestamp >> info.sparseLogsOnly
+ >> info.defaultLogLevel >> info.logRules;
+
+ return stream;
+}
+
+InstanceInfo InstanceInfo::CURRENT = {}; // NOLINT
+
+namespace qs::crash {
+
+CrashInfo CrashInfo::INSTANCE = {}; // NOLINT
+
+}
diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp
new file mode 100644
index 00000000..f0fc02a0
--- /dev/null
+++ b/src/core/instanceinfo.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include
+#include
+#include
+
+struct InstanceInfo {
+ QString instanceId;
+ QString configPath;
+ QString shellId;
+ QDateTime launchTime;
+
+ static InstanceInfo CURRENT; // NOLINT
+};
+
+struct RelaunchInfo {
+ InstanceInfo instance;
+ bool noColor = false;
+ bool timestamp = false;
+ bool sparseLogsOnly = false;
+ QtMsgType defaultLogLevel = QtWarningMsg;
+ QString logRules;
+};
+
+QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info);
+QDataStream& operator>>(QDataStream& stream, InstanceInfo& info);
+
+QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info);
+QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info);
+
+namespace qs::crash {
+
+struct CrashInfo {
+ int logFd = -1;
+
+ static CrashInfo INSTANCE; // NOLINT
+};
+
+} // namespace qs::crash
diff --git a/src/core/lazyloader.cpp b/src/core/lazyloader.cpp
index 76317223..be0eb78b 100644
--- a/src/core/lazyloader.cpp
+++ b/src/core/lazyloader.cpp
@@ -179,7 +179,9 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) {
void LazyLoader::onIncubationCompleted() {
this->setItem(this->incubator->object());
- delete this->incubator;
+ // The incubator is not necessarily inert at the time of this callback,
+ // so deleteLater is required.
+ this->incubator->deleteLater();
this->incubator = nullptr;
this->targetLoading = false;
emit this->loadingChanged();
diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp
index 8ef935f6..dbaad4b5 100644
--- a/src/core/lazyloader.hpp
+++ b/src/core/lazyloader.hpp
@@ -79,7 +79,7 @@
/// > [!WARNING] Components that internally load other components must explicitly
/// > support asynchronous loading to avoid blocking.
/// >
-/// > Notably, [Variants](../variants) does not corrently support asynchronous
+/// > Notably, @@Variants does not corrently support asynchronous
/// > loading, meaning using it inside a LazyLoader will block similarly to not
/// > having a loader to start with.
///
@@ -87,8 +87,8 @@
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
class LazyLoader: public Reloadable {
Q_OBJECT;
- /// The fully loaded item if the loader is `loading` or `active`, or `null`
- /// if neither `loading` or `active`.
+ /// The fully loaded item if the loader is @@loading or @@active, or `null`
+ /// if neither @@loading nor @@active.
///
/// Note that the item is owned by the LazyLoader, and destroying the LazyLoader
/// will destroy the item.
@@ -96,7 +96,7 @@ class LazyLoader: public Reloadable {
/// > [!WARNING] If you access the `item` of a loader that is currently loading,
/// > it will block as if you had set `active` to true immediately beforehand.
/// >
- /// > You can instead set `loading` and listen to the `activeChanged` signal to
+ /// > You can instead set @@loading and listen to @@activeChanged(s) signal to
/// > ensure loading happens asynchronously.
Q_PROPERTY(QObject* item READ item NOTIFY itemChanged);
/// If the loader is actively loading.
@@ -105,7 +105,7 @@ class LazyLoader: public Reloadable {
/// loading it asynchronously. If the component is already loaded, setting
/// this property has no effect.
///
- /// See also: [activeAsync](#prop.activeAsync).
+ /// See also: @@activeAsync.
Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged);
/// If the component is fully loaded.
///
@@ -113,17 +113,17 @@ class LazyLoader: public Reloadable {
/// blocking the UI, and setting it to `false` will destroy the component, requiring
/// it to be loaded again.
///
- /// See also: [activeAsync](#prop.activeAsync).
+ /// See also: @@activeAsync.
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged);
/// If the component is fully loaded.
///
/// Setting this property to true will asynchronously load the component similarly to
- /// [loading](#prop.loading). Reading it or setting it to false will behanve
- /// the same as [active](#prop.active).
+ /// @@loading. Reading it or setting it to false will behanve
+ /// the same as @@active.
Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged);
- /// The component to load. Mutually exclusive to `source`.
+ /// The component to load. Mutually exclusive to @@source.
Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged);
- /// The URI to load the component from. Mutually exclusive to `component`.
+ /// The URI to load the component from. Mutually exclusive to @@component.
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
Q_CLASSINFO("DefaultProperty", "component");
QML_ELEMENT;
diff --git a/src/core/logging.cpp b/src/core/logging.cpp
new file mode 100644
index 00000000..57b63e18
--- /dev/null
+++ b/src/core/logging.cpp
@@ -0,0 +1,937 @@
+#include "logging.hpp"
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "instanceinfo.hpp"
+#include "logging_p.hpp"
+#include "logging_qtprivate.cpp" // NOLINT
+#include "paths.hpp"
+#include "ringbuf.hpp"
+
+Q_LOGGING_CATEGORY(logBare, "quickshell.bare");
+
+namespace qs::log {
+using namespace qt_logging_registry;
+
+Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
+
+bool LogMessage::operator==(const LogMessage& other) const {
+ // note: not including time
+ return this->type == other.type && this->category == other.category && this->body == other.body;
+}
+
+size_t qHash(const LogMessage& message) {
+ return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body);
+}
+
+void LogMessage::formatMessage(
+ QTextStream& stream,
+ const LogMessage& msg,
+ bool color,
+ bool timestamp,
+ const QString& prefix
+) {
+ if (!prefix.isEmpty()) {
+ if (color) stream << "\033[90m";
+ stream << '[' << prefix << ']';
+ if (timestamp) stream << ' ';
+ if (color) stream << "\033[0m";
+ }
+
+ if (timestamp) {
+ if (color) stream << "\033[90m";
+ stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz");
+ }
+
+ if (msg.category == "quickshell.bare") {
+ if (!prefix.isEmpty()) stream << ' ';
+ stream << msg.body;
+ } else {
+ if (color) {
+ switch (msg.type) {
+ case QtDebugMsg: stream << "\033[34m DEBUG"; break;
+ case QtInfoMsg: stream << "\033[32m INFO"; break;
+ case QtWarningMsg: stream << "\033[33m WARN"; break;
+ case QtCriticalMsg: stream << "\033[31m ERROR"; break;
+ case QtFatalMsg: stream << "\033[31m FATAL"; break;
+ }
+ } else {
+ switch (msg.type) {
+ case QtDebugMsg: stream << " DEBUG"; break;
+ case QtInfoMsg: stream << " INFO"; break;
+ case QtWarningMsg: stream << " WARN"; break;
+ case QtCriticalMsg: stream << " ERROR"; break;
+ case QtFatalMsg: stream << " FATAL"; break;
+ }
+ }
+
+ const auto isDefault = msg.category == "default";
+
+ if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m";
+
+ if (!isDefault) {
+ stream << ' ' << msg.category;
+ }
+
+ if (color && msg.type != QtFatalMsg) stream << "\033[0m";
+
+ stream << ": " << msg.body;
+
+ if (color && msg.type == QtFatalMsg) stream << "\033[0m";
+ }
+}
+
+bool CategoryFilter::shouldDisplay(QtMsgType type) const {
+ switch (type) {
+ case QtDebugMsg: return this->debug;
+ case QtInfoMsg: return this->info;
+ case QtWarningMsg: return this->warn;
+ case QtCriticalMsg: return this->critical;
+ default: return true;
+ }
+}
+
+void CategoryFilter::apply(QLoggingCategory* category) const {
+ category->setEnabled(QtDebugMsg, this->debug);
+ category->setEnabled(QtInfoMsg, this->info);
+ category->setEnabled(QtWarningMsg, this->warn);
+ category->setEnabled(QtCriticalMsg, this->critical);
+}
+
+void CategoryFilter::applyRule(
+ QLatin1StringView category,
+ const qt_logging_registry::QLoggingRule& rule
+) {
+ auto filterpass = rule.pass(category, QtDebugMsg);
+ if (filterpass != 0) this->debug = filterpass > 0;
+
+ filterpass = rule.pass(category, QtInfoMsg);
+ if (filterpass != 0) this->info = filterpass > 0;
+
+ filterpass = rule.pass(category, QtWarningMsg);
+ if (filterpass != 0) this->warn = filterpass > 0;
+
+ filterpass = rule.pass(category, QtCriticalMsg);
+ if (filterpass != 0) this->critical = filterpass > 0;
+}
+
+LogManager::LogManager(): stdoutStream(stdout) {}
+
+void LogManager::messageHandler(
+ QtMsgType type,
+ const QMessageLogContext& context,
+ const QString& msg
+) {
+ auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8());
+
+ auto* self = LogManager::instance();
+
+ auto display = true;
+
+ const auto* key = static_cast(context.category);
+
+ if (self->sparseFilters.contains(key)) {
+ display = self->sparseFilters.value(key).shouldDisplay(type);
+ }
+
+ if (display) {
+ LogMessage::formatMessage(
+ self->stdoutStream,
+ message,
+ self->colorLogs,
+ self->timestampLogs,
+ self->prefix
+ );
+
+ self->stdoutStream << Qt::endl;
+ }
+
+ emit self->logMessage(message, display);
+}
+
+void LogManager::filterCategory(QLoggingCategory* category) {
+ auto* instance = LogManager::instance();
+
+ auto categoryName = QLatin1StringView(category->categoryName());
+ auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
+
+ if (instance->lastCategoryFilter) {
+ instance->lastCategoryFilter(category);
+ }
+
+ auto filter = CategoryFilter(category);
+
+ if (isQs) {
+ filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg;
+ filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg;
+ filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg;
+ filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg;
+ }
+
+ for (const auto& rule: *instance->rules) {
+ filter.applyRule(categoryName, rule);
+ }
+
+ if (isQs && !instance->sparse) {
+ // We assume the category name pointer will always be the same and be comparable in the message handler.
+ instance->sparseFilters.insert(static_cast(category->categoryName()), filter);
+
+ // all enabled by default
+ CategoryFilter().apply(category);
+ } else {
+ filter.apply(category);
+ }
+
+ instance->allFilters.insert(categoryName, filter);
+}
+
+LogManager* LogManager::instance() {
+ static auto* instance = new LogManager(); // NOLINT
+ return instance;
+}
+
+void LogManager::init(
+ bool color,
+ bool timestamp,
+ bool sparseOnly,
+ QtMsgType defaultLevel,
+ const QString& rules,
+ const QString& prefix
+) {
+ auto* instance = LogManager::instance();
+ instance->colorLogs = color;
+ instance->timestampLogs = timestamp;
+ instance->sparse = sparseOnly;
+ instance->prefix = prefix;
+ instance->mDefaultLevel = defaultLevel;
+ instance->mRulesString = rules;
+
+ {
+ QLoggingSettingsParser parser;
+ parser.setContent(rules);
+ instance->rules = new QList(parser.rules());
+ }
+
+ qInstallMessageHandler(&LogManager::messageHandler);
+
+ instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
+
+ qCDebug(logLogging) << "Creating offthread logger...";
+ auto* thread = new QThread();
+ instance->threadProxy.moveToThread(thread);
+ thread->start();
+
+ QMetaObject::invokeMethod(
+ &instance->threadProxy,
+ &LoggingThreadProxy::initInThread,
+ Qt::BlockingQueuedConnection
+ );
+
+ qCDebug(logLogging) << "Logger initialized.";
+}
+
+void LogManager::initFs() {
+ QMetaObject::invokeMethod(
+ &LogManager::instance()->threadProxy,
+ "initFs",
+ Qt::BlockingQueuedConnection
+ );
+}
+
+QString LogManager::rulesString() const { return this->mRulesString; }
+QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; }
+bool LogManager::isSparse() const { return this->sparse; }
+
+CategoryFilter LogManager::getFilter(QLatin1StringView category) {
+ return this->allFilters.value(category);
+}
+
+void LoggingThreadProxy::initInThread() {
+ this->logging = new ThreadLogging(this);
+ this->logging->init();
+}
+
+void LoggingThreadProxy::initFs() { this->logging->initFs(); }
+
+void ThreadLogging::init() {
+ auto logMfd = memfd_create("quickshell:logs", 0);
+
+ if (logMfd == -1) {
+ qCCritical(logLogging) << "Failed to create memfd for initial log storage"
+ << qt_error_string(-1);
+ }
+
+ auto dlogMfd = memfd_create("quickshell:detailedlogs", 0);
+
+ if (dlogMfd == -1) {
+ qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage"
+ << qt_error_string(-1);
+ }
+
+ if (logMfd != -1) {
+ this->file = new QFile();
+ this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
+ this->fileStream.setDevice(this->file);
+ }
+
+ if (dlogMfd != -1) {
+ crash::CrashInfo::INSTANCE.logFd = dlogMfd;
+
+ this->detailedFile = new QFile();
+ // buffered by WriteBuffer
+ this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
+ this->detailedWriter.setDevice(this->detailedFile);
+
+ if (!this->detailedWriter.writeHeader()) {
+ qCCritical(logLogging) << "Could not write header for detailed logs.";
+ this->detailedWriter.setDevice(nullptr);
+ delete this->detailedFile;
+ this->detailedFile = nullptr;
+ }
+ }
+
+ // This connection is direct so it works while the event loop is destroyed between
+ // QCoreApplication delete and Q(Gui)Application launch.
+ QObject::connect(
+ LogManager::instance(),
+ &LogManager::logMessage,
+ this,
+ &ThreadLogging::onMessage,
+ Qt::DirectConnection
+ );
+
+ qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs.";
+ qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs.";
+}
+
+void ThreadLogging::initFs() {
+ qCDebug(logLogging) << "Starting filesystem logging...";
+ auto* runDir = QsPaths::instance()->instanceRunDir();
+
+ if (!runDir) {
+ qCCritical(logLogging
+ ) << "Could not start filesystem logging as the runtime directory could not be created.";
+ return;
+ }
+
+ auto path = runDir->filePath("log.log");
+ auto detailedPath = runDir->filePath("log.qslog");
+ auto* file = new QFile(path);
+ auto* detailedFile = new QFile(detailedPath);
+
+ if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
+ qCCritical(logLogging
+ ) << "Could not start filesystem logger as the log file could not be created:"
+ << path;
+ delete file;
+ file = nullptr;
+ } else {
+ qInfo() << "Saving logs to" << path;
+ }
+
+ // buffered by WriteBuffer
+ if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
+ qCCritical(logLogging
+ ) << "Could not start detailed filesystem logger as the log file could not be created:"
+ << detailedPath;
+ delete detailedFile;
+ detailedFile = nullptr;
+ } else {
+ auto lock = flock {
+ .l_type = F_WRLCK,
+ .l_whence = SEEK_SET,
+ .l_start = 0,
+ .l_len = 0,
+ .l_pid = 0,
+ };
+
+ if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT
+ qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from "
+ "other instances will not work.";
+ }
+
+ qCInfo(logLogging) << "Saving detailed logs to" << path;
+ }
+
+ qCDebug(logLogging) << "Copying memfd logs to log file...";
+
+ if (file) {
+ auto* oldFile = this->file;
+ if (oldFile) {
+ oldFile->seek(0);
+ sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
+ }
+
+ this->file = file;
+ this->fileStream.setDevice(file);
+ delete oldFile;
+ }
+
+ if (detailedFile) {
+ auto* oldFile = this->detailedFile;
+ if (oldFile) {
+ oldFile->seek(0);
+ sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
+ }
+
+ crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
+
+ this->detailedFile = detailedFile;
+ this->detailedWriter.setDevice(detailedFile);
+
+ if (!oldFile) {
+ if (!this->detailedWriter.writeHeader()) {
+ qCCritical(logLogging) << "Could not write header for detailed logs.";
+ this->detailedWriter.setDevice(nullptr);
+ delete this->detailedFile;
+ this->detailedFile = nullptr;
+ }
+ }
+
+ delete oldFile;
+ }
+
+ qCDebug(logLogging) << "Switched logging to disk logs.";
+
+ auto* logManager = LogManager::instance();
+ QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage);
+
+ QObject::connect(
+ logManager,
+ &LogManager::logMessage,
+ this,
+ &ThreadLogging::onMessage,
+ Qt::QueuedConnection
+ );
+
+ qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection.";
+}
+
+void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
+ if (showInSparse) {
+ if (this->fileStream.device() == nullptr) return;
+ LogMessage::formatMessage(this->fileStream, msg, false, true);
+ this->fileStream << Qt::endl;
+ }
+
+ if (this->detailedWriter.write(msg)) {
+ this->detailedFile->flush();
+ } else if (this->detailedFile != nullptr) {
+ qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
+ }
+}
+
+CompressedLogType compressedTypeOf(QtMsgType type) {
+ switch (type) {
+ case QtDebugMsg: return CompressedLogType::Debug;
+ case QtInfoMsg: return CompressedLogType::Info;
+ case QtWarningMsg: return CompressedLogType::Warn;
+ case QtCriticalMsg:
+ case QtFatalMsg: return CompressedLogType::Critical;
+ }
+
+ return CompressedLogType::Info; // unreachable under normal conditions
+}
+
+QtMsgType typeOfCompressed(CompressedLogType type) {
+ switch (type) {
+ case CompressedLogType::Debug: return QtDebugMsg;
+ case CompressedLogType::Info: return QtInfoMsg;
+ case CompressedLogType::Warn: return QtWarningMsg;
+ case CompressedLogType::Critical: return QtCriticalMsg;
+ }
+
+ return QtInfoMsg; // unreachable under normal conditions
+}
+
+void WriteBuffer::setDevice(QIODevice* device) { this->device = device; }
+bool WriteBuffer::hasDevice() const { return this->device; }
+
+bool WriteBuffer::flush() {
+ auto written = this->device->write(this->buffer);
+ auto success = written == this->buffer.length();
+ this->buffer.clear();
+ return success;
+}
+
+void WriteBuffer::writeBytes(const char* data, qsizetype length) {
+ this->buffer.append(data, length);
+}
+
+void WriteBuffer::writeU8(quint8 data) { this->writeBytes(reinterpret_cast(&data), 1); }
+
+void WriteBuffer::writeU16(quint16 data) {
+ data = qToLittleEndian(data);
+ this->writeBytes(reinterpret_cast(&data), 2);
+}
+
+void WriteBuffer::writeU32(quint32 data) {
+ data = qToLittleEndian(data);
+ this->writeBytes(reinterpret_cast(&data), 4);
+}
+
+void WriteBuffer::writeU64(quint64 data) {
+ data = qToLittleEndian(data);
+ this->writeBytes(reinterpret_cast(&data), 8);
+}
+
+void DeviceReader::setDevice(QIODevice* device) { this->device = device; }
+bool DeviceReader::hasDevice() const { return this->device; }
+
+bool DeviceReader::readBytes(char* data, qsizetype length) {
+ return this->device->read(data, length) == length;
+}
+
+qsizetype DeviceReader::peekBytes(char* data, qsizetype length) {
+ return this->device->peek(data, length);
+}
+
+bool DeviceReader::skip(qsizetype length) { return this->device->skip(length) == length; }
+
+bool DeviceReader::readU8(quint8* data) {
+ return this->readBytes(reinterpret_cast(data), 1);
+}
+
+bool DeviceReader::readU16(quint16* data) {
+ return this->readBytes(reinterpret_cast(data), 2);
+}
+
+bool DeviceReader::readU32(quint32* data) {
+ return this->readBytes(reinterpret_cast(data), 4);
+}
+
+bool DeviceReader::readU64(quint64* data) {
+ return this->readBytes(reinterpret_cast(data), 8);
+}
+
+void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); }
+void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); }
+
+constexpr quint8 LOG_VERSION = 2;
+
+bool EncodedLogWriter::writeHeader() {
+ this->buffer.writeU8(LOG_VERSION);
+ return this->buffer.flush();
+}
+
+bool EncodedLogReader::readHeader(bool* success, quint8* version, quint8* readerVersion) {
+ if (!this->reader.readU8(version)) return false;
+ *success = *version == LOG_VERSION;
+ *readerVersion = LOG_VERSION;
+ return true;
+}
+
+bool EncodedLogWriter::write(const LogMessage& message) {
+ if (!this->buffer.hasDevice()) return false;
+
+ LogMessage* prevMessage = nullptr;
+ auto index = this->recentMessages.indexOf(message, &prevMessage);
+
+ // If its a dupe, save memory by reusing the buffer of the first message and letting
+ // the new one be deallocated.
+ auto body = prevMessage ? prevMessage->body : message.body;
+ this->recentMessages.emplace(message.type, message.category, body, message.time);
+
+ if (index != -1) {
+ auto secondDelta = this->lastMessageTime.secsTo(message.time);
+
+ if (secondDelta < 16 && index < 16) {
+ this->writeOp(EncodedLogOpcode::RecentMessageShort);
+ this->buffer.writeU8(index | (secondDelta << 4));
+ } else {
+ this->writeOp(EncodedLogOpcode::RecentMessageLong);
+ this->buffer.writeU8(index);
+ this->writeVarInt(secondDelta);
+ }
+
+ goto finish;
+ } else {
+ auto categoryId = this->getOrCreateCategory(message.category);
+ this->writeVarInt(categoryId);
+
+ auto writeFullTimestamp = [this, &message]() {
+ this->buffer.writeU64(message.time.toSecsSinceEpoch());
+ };
+
+ if (message.type == QtFatalMsg) {
+ this->buffer.writeU8(0xff);
+ writeFullTimestamp();
+ } else {
+ quint8 field = compressedTypeOf(message.type);
+
+ auto secondDelta = this->lastMessageTime.secsTo(message.time);
+ if (secondDelta >= 0x1d) {
+ // 0x1d = followed by delta int
+ // 0x1e = followed by epoch delta int
+ field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3;
+ } else {
+ field |= secondDelta << 3;
+ }
+
+ this->buffer.writeU8(field);
+
+ if (secondDelta >= 0x1d) {
+ if (secondDelta > 0xffff) {
+ writeFullTimestamp();
+ } else {
+ this->writeVarInt(secondDelta);
+ }
+ }
+ }
+
+ this->writeString(message.body);
+ }
+
+finish:
+ // copy with second precision
+ this->lastMessageTime = QDateTime::fromSecsSinceEpoch(message.time.toSecsSinceEpoch());
+ return this->buffer.flush();
+}
+
+bool EncodedLogReader::read(LogMessage* slot) {
+start:
+ quint32 next = 0;
+ if (!this->readVarInt(&next)) return false;
+
+ if (next < EncodedLogOpcode::BeginCategories) {
+ if (next == EncodedLogOpcode::RegisterCategory) {
+ if (!this->registerCategory()) return false;
+ goto start;
+ } else if (next == EncodedLogOpcode::RecentMessageShort
+ || next == EncodedLogOpcode::RecentMessageLong)
+ {
+ quint8 index = 0;
+ quint32 secondDelta = 0;
+
+ if (next == EncodedLogOpcode::RecentMessageShort) {
+ quint8 field = 0;
+ if (!this->reader.readU8(&field)) return false;
+ index = field & 0xf;
+ secondDelta = field >> 4;
+ } else {
+ if (!this->reader.readU8(&index)) return false;
+ if (!this->readVarInt(&secondDelta)) return false;
+ }
+
+ if (index >= this->recentMessages.size()) return false;
+ *slot = this->recentMessages.at(index);
+ this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta));
+ slot->time = this->lastMessageTime;
+ }
+ } else {
+ auto categoryId = next - EncodedLogOpcode::BeginCategories;
+ auto category = this->categories.value(categoryId);
+
+ quint8 field = 0;
+ if (!this->reader.readU8(&field)) return false;
+
+ auto msgType = QtDebugMsg;
+ quint64 secondDelta = 0;
+ auto needsTimeRead = false;
+
+ if (field == 0xff) {
+ msgType = QtFatalMsg;
+ needsTimeRead = true;
+ } else {
+ msgType = typeOfCompressed(static_cast(field & 0x07));
+ secondDelta = field >> 3;
+
+ if (secondDelta == 0x1d) {
+ quint32 slot = 0;
+ if (!this->readVarInt(&slot)) return false;
+ secondDelta = slot;
+ } else if (secondDelta == 0x1e) {
+ needsTimeRead = true;
+ }
+ }
+
+ if (needsTimeRead) {
+ if (!this->reader.readU64(&secondDelta)) return false;
+ }
+
+ this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta));
+
+ QByteArray body;
+ if (!this->readString(&body)) return false;
+
+ *slot = LogMessage(msgType, QLatin1StringView(category.first), body, this->lastMessageTime);
+ slot->readCategoryId = categoryId;
+ }
+
+ this->recentMessages.emplace(*slot);
+ return true;
+}
+
+CategoryFilter EncodedLogReader::categoryFilterById(quint16 id) {
+ return this->categories.value(id).second;
+}
+
+void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); }
+
+void EncodedLogWriter::writeVarInt(quint32 n) {
+ if (n < 0xff) {
+ this->buffer.writeU8(n);
+ } else if (n < 0xffff) {
+ this->buffer.writeU8(0xff);
+ this->buffer.writeU16(n);
+ } else {
+ this->buffer.writeU8(0xff);
+ this->buffer.writeU16(0xffff);
+ this->buffer.writeU32(n);
+ }
+}
+
+bool EncodedLogReader::readVarInt(quint32* slot) {
+ auto bytes = std::array();
+ auto readLength = this->reader.peekBytes(reinterpret_cast(bytes.data()), 7);
+
+ if (bytes[0] != 0xff && readLength >= 1) {
+ auto n = *reinterpret_cast(bytes.data());
+ if (!this->reader.skip(1)) return false;
+ *slot = qFromLittleEndian(n);
+ } else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
+ auto n = *reinterpret_cast(bytes.data() + 1);
+ if (!this->reader.skip(3)) return false;
+ *slot = qFromLittleEndian(n);
+ } else if (readLength == 7) {
+ auto n = *reinterpret_cast(bytes.data() + 3);
+ if (!this->reader.skip(7)) return false;
+ *slot = qFromLittleEndian(n);
+ } else return false;
+
+ return true;
+}
+
+void EncodedLogWriter::writeString(QByteArrayView bytes) {
+ this->writeVarInt(bytes.length());
+ this->buffer.writeBytes(bytes.constData(), bytes.length());
+}
+
+bool EncodedLogReader::readString(QByteArray* slot) {
+ quint32 length = 0;
+ if (!this->readVarInt(&length)) return false;
+
+ *slot = QByteArray(length, Qt::Uninitialized);
+ auto r = this->reader.readBytes(slot->data(), slot->size());
+ return r;
+}
+
+quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) {
+ if (this->categories.contains(category)) {
+ return this->categories.value(category);
+ } else {
+ this->writeOp(EncodedLogOpcode::RegisterCategory);
+ // id is implicitly the next available id
+ this->writeString(category);
+
+ auto id = this->nextCategory++;
+ this->categories.insert(category, id);
+
+ auto filter = LogManager::instance()->getFilter(category);
+ quint8 flags = 0;
+ flags |= filter.debug << 0;
+ flags |= filter.info << 1;
+ flags |= filter.warn << 2;
+ flags |= filter.critical << 3;
+
+ this->buffer.writeU8(flags);
+ return id;
+ }
+}
+
+bool EncodedLogReader::registerCategory() {
+ QByteArray name;
+ quint8 flags = 0;
+ if (!this->readString(&name)) return false;
+ if (!this->reader.readU8(&flags)) return false;
+
+ CategoryFilter filter;
+ filter.debug = (flags >> 0) & 1;
+ filter.info = (flags >> 1) & 1;
+ filter.warn = (flags >> 2) & 1;
+ filter.critical = (flags >> 3) & 1;
+
+ this->categories.append(qMakePair(name, filter));
+ return true;
+}
+
+bool LogReader::initialize() {
+ this->reader.setDevice(this->file);
+
+ bool readable = false;
+ quint8 logVersion = 0;
+ quint8 readerVersion = 0;
+ if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) {
+ qCritical() << "Failed to read log header.";
+ return false;
+ }
+
+ if (!readable) {
+ qCritical() << "This log was encoded with version" << logVersion
+ << "of the quickshell log encoder, which cannot be decoded by the current "
+ "version of quickshell, with log version"
+ << readerVersion;
+ return false;
+ }
+
+ return true;
+}
+
+bool LogReader::continueReading() {
+ auto color = LogManager::instance()->colorLogs;
+ auto tailRing = RingBuffer(this->remainingTail);
+
+ LogMessage message;
+ auto stream = QTextStream(stdout);
+ auto readCursor = this->file->pos();
+ while (this->reader.read(&message)) {
+ readCursor = this->file->pos();
+
+ CategoryFilter filter;
+ if (this->filters.contains(message.readCategoryId)) {
+ filter = this->filters.value(message.readCategoryId);
+ } else {
+ filter = this->reader.categoryFilterById(message.readCategoryId);
+
+ for (const auto& rule: this->rules) {
+ filter.applyRule(message.category, rule);
+ }
+
+ this->filters.insert(message.readCategoryId, filter);
+ }
+
+ if (filter.shouldDisplay(message.type)) {
+ if (this->remainingTail == 0) {
+ LogMessage::formatMessage(stream, message, color, this->timestamps);
+ stream << '\n';
+ } else {
+ tailRing.emplace(message);
+ }
+ }
+ }
+
+ if (this->remainingTail != 0) {
+ for (auto i = tailRing.size() - 1; i != -1; i--) {
+ auto& message = tailRing.at(i);
+ LogMessage::formatMessage(stream, message, color, this->timestamps);
+ stream << '\n';
+ }
+ }
+
+ stream << Qt::flush;
+
+ if (this->file->pos() != readCursor) {
+ qCritical() << "An error occurred parsing the end of this log file.";
+ qCritical() << "Remaining data:" << this->file->readAll();
+ return false;
+ }
+
+ return true;
+}
+
+void LogFollower::FcntlWaitThread::run() {
+ auto lock = flock {
+ .l_type = F_RDLCK, // won't block other read locks when we take it
+ .l_whence = SEEK_SET,
+ .l_start = 0,
+ .l_len = 0,
+ .l_pid = 0,
+ };
+
+ auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT
+
+ if (r != 0) {
+ qCWarning(logLogging).nospace()
+ << "Failed to wait for write locks to be removed from log file with error code " << errno
+ << ": " << qt_error_string();
+ }
+}
+
+bool LogFollower::follow() {
+ QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked);
+
+ QObject::connect(
+ &this->fileWatcher,
+ &QFileSystemWatcher::fileChanged,
+ this,
+ &LogFollower::onFileChanged
+ );
+
+ this->fileWatcher.addPath(this->path);
+ this->waitThread.start();
+
+ auto r = QCoreApplication::exec();
+ return r == 0;
+}
+
+void LogFollower::onFileChanged() {
+ if (!this->reader->continueReading()) {
+ QCoreApplication::exit(1);
+ }
+}
+
+void LogFollower::onFileLocked() {
+ if (!this->reader->continueReading()) {
+ QCoreApplication::exit(1);
+ } else {
+ QCoreApplication::exit(0);
+ }
+}
+
+bool readEncodedLogs(
+ QFile* file,
+ const QString& path,
+ bool timestamps,
+ int tail,
+ bool follow,
+ const QString& rulespec
+) {
+ QList rules;
+
+ {
+ QLoggingSettingsParser parser;
+ parser.setContent(rulespec);
+ rules = parser.rules();
+ }
+
+ auto reader = LogReader(file, timestamps, tail, rules);
+
+ if (!reader.initialize()) return false;
+ if (!reader.continueReading()) return false;
+
+ if (follow) {
+ auto follower = LogFollower(&reader, path);
+ return follower.follow();
+ }
+
+ return true;
+}
+
+} // namespace qs::log
diff --git a/src/core/logging.hpp b/src/core/logging.hpp
new file mode 100644
index 00000000..7ff1b5e0
--- /dev/null
+++ b/src/core/logging.hpp
@@ -0,0 +1,148 @@
+#pragma once
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(logBare);
+
+namespace qs::log {
+
+struct LogMessage {
+ explicit LogMessage() = default;
+
+ explicit LogMessage(
+ QtMsgType type,
+ QLatin1StringView category,
+ QByteArray body,
+ QDateTime time = QDateTime::currentDateTime()
+ )
+ : type(type)
+ , time(std::move(time))
+ , category(category)
+ , body(std::move(body)) {}
+
+ bool operator==(const LogMessage& other) const;
+
+ QtMsgType type = QtDebugMsg;
+ QDateTime time;
+ QLatin1StringView category;
+ QByteArray body;
+ quint16 readCategoryId = 0;
+
+ static void formatMessage(
+ QTextStream& stream,
+ const LogMessage& msg,
+ bool color,
+ bool timestamp,
+ const QString& prefix = ""
+ );
+};
+
+size_t qHash(const LogMessage& message);
+
+class ThreadLogging;
+
+class LoggingThreadProxy: public QObject {
+ Q_OBJECT;
+
+public:
+ explicit LoggingThreadProxy() = default;
+
+public slots:
+ void initInThread();
+ void initFs();
+
+private:
+ ThreadLogging* logging = nullptr;
+};
+
+namespace qt_logging_registry {
+class QLoggingRule;
+}
+
+struct CategoryFilter {
+ explicit CategoryFilter() = default;
+ explicit CategoryFilter(QLoggingCategory* category)
+ : debug(category->isDebugEnabled())
+ , info(category->isInfoEnabled())
+ , warn(category->isWarningEnabled())
+ , critical(category->isCriticalEnabled()) {}
+
+ [[nodiscard]] bool shouldDisplay(QtMsgType type) const;
+ void apply(QLoggingCategory* category) const;
+ void applyRule(QLatin1StringView category, const qt_logging_registry::QLoggingRule& rule);
+
+ bool debug = true;
+ bool info = true;
+ bool warn = true;
+ bool critical = true;
+};
+
+class LogManager: public QObject {
+ Q_OBJECT;
+
+public:
+ static void init(
+ bool color,
+ bool timestamp,
+ bool sparseOnly,
+ QtMsgType defaultLevel,
+ const QString& rules,
+ const QString& prefix = ""
+ );
+
+ static void initFs();
+ static LogManager* instance();
+
+ bool colorLogs = true;
+ bool timestampLogs = false;
+
+ [[nodiscard]] QString rulesString() const;
+ [[nodiscard]] QtMsgType defaultLevel() const;
+ [[nodiscard]] bool isSparse() const;
+
+ [[nodiscard]] CategoryFilter getFilter(QLatin1StringView category);
+
+signals:
+ void logMessage(LogMessage msg, bool showInSparse);
+
+private:
+ explicit LogManager();
+ static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg);
+
+ static void filterCategory(QLoggingCategory* category);
+
+ QLoggingCategory::CategoryFilter lastCategoryFilter = nullptr;
+ bool sparse = false;
+ QString prefix;
+ QString mRulesString;
+ QList* rules = nullptr;
+ QtMsgType mDefaultLevel = QtWarningMsg;
+ QHash sparseFilters;
+ QHash allFilters;
+
+ QTextStream stdoutStream;
+ LoggingThreadProxy threadProxy;
+};
+
+bool readEncodedLogs(
+ QFile* file,
+ const QString& path,
+ bool timestamps,
+ int tail,
+ bool follow,
+ const QString& rulespec
+);
+
+} // namespace qs::log
+
+using LogManager = qs::log::LogManager;
diff --git a/src/core/logging_p.hpp b/src/core/logging_p.hpp
new file mode 100644
index 00000000..3297ea1b
--- /dev/null
+++ b/src/core/logging_p.hpp
@@ -0,0 +1,190 @@
+#pragma once
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "logging.hpp"
+#include "logging_qtprivate.hpp"
+#include "ringbuf.hpp"
+
+namespace qs::log {
+
+enum EncodedLogOpcode : quint8 {
+ RegisterCategory = 0,
+ RecentMessageShort,
+ RecentMessageLong,
+ BeginCategories,
+};
+
+enum CompressedLogType : quint8 {
+ Debug = 0,
+ Info = 1,
+ Warn = 2,
+ Critical = 3,
+};
+
+CompressedLogType compressedTypeOf(QtMsgType type);
+QtMsgType typeOfCompressed(CompressedLogType type);
+
+class WriteBuffer {
+public:
+ void setDevice(QIODevice* device);
+ [[nodiscard]] bool hasDevice() const;
+ [[nodiscard]] bool flush();
+ void writeBytes(const char* data, qsizetype length);
+ void writeU8(quint8 data);
+ void writeU16(quint16 data);
+ void writeU32(quint32 data);
+ void writeU64(quint64 data);
+
+private:
+ QIODevice* device = nullptr;
+ QByteArray buffer;
+};
+
+class DeviceReader {
+public:
+ void setDevice(QIODevice* device);
+ [[nodiscard]] bool hasDevice() const;
+ [[nodiscard]] bool readBytes(char* data, qsizetype length);
+ // peek UP TO length
+ [[nodiscard]] qsizetype peekBytes(char* data, qsizetype length);
+ [[nodiscard]] bool skip(qsizetype length);
+ [[nodiscard]] bool readU8(quint8* data);
+ [[nodiscard]] bool readU16(quint16* data);
+ [[nodiscard]] bool readU32(quint32* data);
+ [[nodiscard]] bool readU64(quint64* data);
+
+private:
+ QIODevice* device = nullptr;
+};
+
+class EncodedLogWriter {
+public:
+ void setDevice(QIODevice* target);
+ [[nodiscard]] bool writeHeader();
+ [[nodiscard]] bool write(const LogMessage& message);
+
+private:
+ void writeOp(EncodedLogOpcode opcode);
+ void writeVarInt(quint32 n);
+ void writeString(QByteArrayView bytes);
+ quint16 getOrCreateCategory(QLatin1StringView category);
+
+ WriteBuffer buffer;
+
+ QHash categories;
+ quint16 nextCategory = EncodedLogOpcode::BeginCategories;
+
+ QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
+ HashBuffer recentMessages {256};
+};
+
+class EncodedLogReader {
+public:
+ void setDevice(QIODevice* source);
+ [[nodiscard]] bool readHeader(bool* success, quint8* logVersion, quint8* readerVersion);
+ // WARNING: log messages written to the given slot are invalidated when the log reader is destroyed.
+ [[nodiscard]] bool read(LogMessage* slot);
+ [[nodiscard]] CategoryFilter categoryFilterById(quint16 id);
+
+private:
+ [[nodiscard]] bool readVarInt(quint32* slot);
+ [[nodiscard]] bool readString(QByteArray* slot);
+ [[nodiscard]] bool registerCategory();
+
+ DeviceReader reader;
+ QVector> categories;
+ QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
+ RingBuffer recentMessages {256};
+};
+
+class ThreadLogging: public QObject {
+ Q_OBJECT;
+
+public:
+ explicit ThreadLogging(QObject* parent): QObject(parent) {}
+
+ void init();
+ void initFs();
+ void setupFileLogging();
+
+private slots:
+ void onMessage(const LogMessage& msg, bool showInSparse);
+
+private:
+ QFile* file = nullptr;
+ QTextStream fileStream;
+ QFile* detailedFile = nullptr;
+ EncodedLogWriter detailedWriter;
+};
+
+class LogFollower;
+
+class LogReader {
+public:
+ explicit LogReader(
+ QFile* file,
+ bool timestamps,
+ int tail,
+ QList rules
+ )
+ : file(file)
+ , timestamps(timestamps)
+ , remainingTail(tail)
+ , rules(std::move(rules)) {}
+
+ bool initialize();
+ bool continueReading();
+
+private:
+ QFile* file;
+ EncodedLogReader reader;
+ bool timestamps;
+ int remainingTail;
+ QHash filters;
+ QList rules;
+
+ friend class LogFollower;
+};
+
+class LogFollower: public QObject {
+ Q_OBJECT;
+
+public:
+ explicit LogFollower(LogReader* reader, QString path): reader(reader), path(std::move(path)) {}
+
+ bool follow();
+
+private slots:
+ void onFileChanged();
+ void onFileLocked();
+
+private:
+ LogReader* reader;
+ QString path;
+ QFileSystemWatcher fileWatcher;
+
+ class FcntlWaitThread: public QThread {
+ public:
+ explicit FcntlWaitThread(LogFollower* follower): follower(follower) {}
+
+ protected:
+ void run() override;
+
+ private:
+ LogFollower* follower;
+ };
+
+ FcntlWaitThread waitThread {this};
+};
+
+} // namespace qs::log
diff --git a/src/core/logging_qtprivate.cpp b/src/core/logging_qtprivate.cpp
new file mode 100644
index 00000000..5078eeb4
--- /dev/null
+++ b/src/core/logging_qtprivate.cpp
@@ -0,0 +1,138 @@
+// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
+
+// Was unable to properly link the functions when directly using the headers (which we depend
+// on anyway), so below is a slightly stripped down copy. Making the originals link would
+// be preferable.
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "logging_qtprivate.hpp"
+
+namespace qs::log {
+Q_DECLARE_LOGGING_CATEGORY(logLogging);
+
+namespace qt_logging_registry {
+
+class QLoggingSettingsParser {
+public:
+ void setContent(QStringView content);
+
+ [[nodiscard]] QList rules() const { return this->mRules; }
+
+private:
+ void parseNextLine(QStringView line);
+
+private:
+ QList mRules;
+};
+
+void QLoggingSettingsParser::setContent(QStringView content) {
+ this->mRules.clear();
+ for (auto line: qTokenize(content, u';')) this->parseNextLine(line);
+}
+
+void QLoggingSettingsParser::parseNextLine(QStringView line) {
+ // Remove whitespace at start and end of line:
+ line = line.trimmed();
+
+ const qsizetype equalPos = line.indexOf(u'=');
+ if (equalPos != -1) {
+ if (line.lastIndexOf(u'=') == equalPos) {
+ const auto key = line.left(equalPos).trimmed();
+ const QStringView pattern = key;
+ const auto valueStr = line.mid(equalPos + 1).trimmed();
+ int value = -1;
+ if (valueStr == QString("true")) value = 1;
+ else if (valueStr == QString("false")) value = 0;
+ QLoggingRule rule(pattern, (value == 1));
+ if (rule.flags != 0 && (value != -1)) this->mRules.append(std::move(rule));
+ else
+ qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
+ } else {
+ qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
+ }
+ }
+}
+
+QLoggingRule::QLoggingRule(QStringView pattern, bool enabled): messageType(-1), enabled(enabled) {
+ this->parse(pattern);
+}
+
+void QLoggingRule::parse(QStringView pattern) {
+ QStringView p;
+
+ // strip trailing ".messagetype"
+ if (pattern.endsWith(QString(".debug"))) {
+ p = pattern.chopped(6); // strlen(".debug")
+ this->messageType = QtDebugMsg;
+ } else if (pattern.endsWith(QString(".info"))) {
+ p = pattern.chopped(5); // strlen(".info")
+ this->messageType = QtInfoMsg;
+ } else if (pattern.endsWith(QString(".warning"))) {
+ p = pattern.chopped(8); // strlen(".warning")
+ this->messageType = QtWarningMsg;
+ } else if (pattern.endsWith(QString(".critical"))) {
+ p = pattern.chopped(9); // strlen(".critical")
+ this->messageType = QtCriticalMsg;
+ } else {
+ p = pattern;
+ }
+
+ const QChar asterisk = u'*';
+ if (!p.contains(asterisk)) {
+ this->flags = FullText;
+ } else {
+ if (p.endsWith(asterisk)) {
+ this->flags |= LeftFilter;
+ p = p.chopped(1);
+ }
+ if (p.startsWith(asterisk)) {
+ this->flags |= RightFilter;
+ p = p.mid(1);
+ }
+ if (p.contains(asterisk)) // '*' only supported at start/end
+ this->flags = PatternFlags();
+ }
+
+ this->category = p.toString();
+}
+
+int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const {
+ // check message type
+ if (this->messageType > -1 && this->messageType != msgType) return 0;
+
+ if (this->flags == FullText) {
+ // full match
+ if (this->category == cat) return (this->enabled ? 1 : -1);
+ else return 0;
+ }
+
+ const qsizetype idx = cat.indexOf(this->category);
+ if (idx >= 0) {
+ if (this->flags == MidFilter) {
+ // matches somewhere
+ return (this->enabled ? 1 : -1);
+ } else if (this->flags == LeftFilter) {
+ // matches left
+ if (idx == 0) return (this->enabled ? 1 : -1);
+ } else if (this->flags == RightFilter) {
+ // matches right
+ if (idx == (cat.size() - this->category.size())) return (this->enabled ? 1 : -1);
+ }
+ }
+ return 0;
+}
+
+} // namespace qt_logging_registry
+
+} // namespace qs::log
diff --git a/src/core/logging_qtprivate.hpp b/src/core/logging_qtprivate.hpp
new file mode 100644
index 00000000..83c82585
--- /dev/null
+++ b/src/core/logging_qtprivate.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
+
+// Was unable to properly link the functions when directly using the headers (which we depend
+// on anyway), so below is a slightly stripped down copy. Making the originals link would
+// be preferable.
+
+#include
+#include
+#include
+#include
+#include
+
+namespace qs::log {
+Q_DECLARE_LOGGING_CATEGORY(logLogging);
+
+namespace qt_logging_registry {
+
+class QLoggingRule {
+public:
+ QLoggingRule();
+ QLoggingRule(QStringView pattern, bool enabled);
+ [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const;
+
+ enum PatternFlag : quint8 {
+ FullText = 0x1,
+ LeftFilter = 0x2,
+ RightFilter = 0x4,
+ MidFilter = LeftFilter | RightFilter
+ };
+ Q_DECLARE_FLAGS(PatternFlags, PatternFlag)
+
+ QString category;
+ int messageType;
+ PatternFlags flags;
+ bool enabled;
+
+private:
+ void parse(QStringView pattern);
+};
+
+} // namespace qt_logging_registry
+
+} // namespace qs::log
diff --git a/src/core/main.cpp b/src/core/main.cpp
deleted file mode 100644
index 2cfd4d9c..00000000
--- a/src/core/main.cpp
+++ /dev/null
@@ -1,331 +0,0 @@
-#include "main.hpp"
-#include