28 Commits

Author SHA1 Message Date
Uyanide cf73b12996 feat: quitOnSelected default to true
Release / Build ArchLinux Package (push) Successful in 1m0s
Release / Publish to Gitea Release (push) Successful in 3s
Release / Publish to AUR (push) Successful in 9s
2026-04-03 09:09:40 +02:00
Uyanide 9a6fa483a5 ⬆️ bump to v2.1.0
Release / Build ArchLinux Package (push) Successful in 1m3s
Release / Publish to Gitea Release (push) Successful in 3s
Release / Publish to AUR (push) Successful in 8s
2026-04-03 08:54:39 +02:00
Uyanide 5db3650184 🐛 fix: remove unused config 'printPreview' 2026-04-03 08:52:56 +02:00
Uyanide 0524f26f97 🐛 fix: forgot to implement 'printSelected' 😒 2026-04-03 08:48:49 +02:00
Uyanide 740411f194 🐛 fix: correct behaviour of --disable-actions
Release / Build ArchLinux Package (push) Successful in 1m0s
Release / Publish to Gitea Release (push) Successful in 3s
Release / Publish to AUR (push) Successful in 9s
2026-03-24 12:48:01 +01:00
Uyanide 1a2daec165 🚀 CD: refactor release workflow 2026-03-24 11:42:41 +01:00
Uyanide 524b53b7b2 CD: rename workflow to Release 2026-03-24 11:23:10 +01:00
Uyanide b5ea96bb8b 🚀 CD: add MAKEFLAGS to optimize local build process 2026-03-24 11:14:08 +01:00
Uyanide 470bb1620a 🚀 CD: 😡
CI/CD / Package (push) Successful in 2m26s
CI/CD / Publish (push) Successful in 10s
2026-03-24 11:10:24 +01:00
Uyanide b1372cacd7 🚀 CD: change license to 0BSD
CI/CD / Package (push) Successful in 59s
CI/CD / Publish (push) Successful in 10s
2026-03-24 10:40:22 +01:00
Uyanide e59fba0689 🚀 CD
CI/CD / Package (push) Successful in 1m0s
CI/CD / Publish (push) Successful in 10s
2026-03-24 09:54:32 +01:00
Uyanide fe174ba2e0 🚀 CD
CI/CD / Package (push) Successful in 1m9s
CI/CD / Publish (push) Failing after 15s
2026-03-24 09:47:55 +01:00
Uyanide d8ab530fa8 🚀 CD
CI/CD / Package (push) Failing after 1m3s
CI/CD / Publish (push) Has been skipped
2026-03-24 09:27:31 +01:00
Uyanide 07142eb19e 🚀 CD
CI/CD / Package (push) Failing after 24s
CI/CD / Publish (push) Has been skipped
2026-03-24 09:16:08 +01:00
Uyanide da3c0d6896 🚀 CD
CI/CD / Package (push) Failing after 25s
CI/CD / Publish (push) Has been skipped
2026-03-24 09:09:35 +01:00
Uyanide 07d281d9f1 🚀 CD
CI/CD / Package (push) Failing after 23s
CI/CD / Publish (push) Has been skipped
2026-03-24 09:06:22 +01:00
Uyanide 7fb0de38c9 🚀 CD
CI/CD / Package (push) Failing after 1m8s
CI/CD / Publish (push) Has been skipped
2026-03-24 08:56:09 +01:00
Uyanide 5d4b50ebad 🔧 chore: add man pages 2026-03-24 07:30:48 +01:00
Uyanide 3156e46c62 🔧 chore: rename assets & add "apply" desktop entry 2026-03-24 06:02:30 +01:00
Uyanide 23c80d30e9 feat: add option to disable actions via command line 2026-03-24 05:36:41 +01:00
Uyanide 11c9c2f88d feat: implement signal handling for graceful shutdown using signalfd and QSocketNotifier 2026-03-12 02:42:52 +01:00
Uyanide b06d27cecf feat: add apply option to set wallpaper from command line and enhance process completion signals 2026-03-11 05:44:58 +01:00
Uyanide 5df0b53df0 feat: implement image cache management with max entries limit 2026-03-01 06:28:08 +01:00
Uyanide bf2f3d57c7 feat: defer preview command until states are captured 2026-03-01 05:08:58 +01:00
Uyanide 1e9c175dd5 feat: add settings persist store, remove sort config items 2026-03-01 04:08:12 +01:00
Uyanide da515566cb 🐛 fix: correct screenshot URL in README 2026-03-01 02:44:10 +01:00
Uyanide a6caa0c950 🔧 chore: update screenshot 2026-03-01 02:43:21 +01:00
Uyanide 807278d748 🐛 fix: this is why we need CI, again 2026-03-01 00:59:29 +01:00
37 changed files with 1911 additions and 447 deletions
+162
View File
@@ -0,0 +1,162 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-arch:
name: Build ArchLinux Package
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Extract Version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Generate PKGBUILD
run: |
TAR_URL="https://git.uyani.de/Uyanide/WallReel/archive/v${{ env.VERSION }}.tar.gz"
wget -qO source.tar.gz "$TAR_URL"
SHA256=$(sha256sum source.tar.gz | awk '{print $1}')
cat << 'EOF' > PKGBUILD
# Maintainer: Uyanide <me@uyani.de>
pkgname=wallreel
pkgver=${{ env.VERSION }}
pkgrel=1
pkgdesc="Choose and set desktop wallpapers with customizable themes and actions"
arch=('x86_64')
url="https://git.uyani.de/Uyanide/WallReel"
license=('MIT')
depends=('qt6-base' 'qt6-declarative' 'gcc-libs' 'glibc')
makedepends=('cmake')
options=('!debug')
source=("${pkgname}-${pkgver}.tar.gz::https://git.uyani.de/Uyanide/WallReel/archive/v${pkgver}.tar.gz")
sha256sums=('INSERT_SHA256_HERE')
build() {
cd "wallreel"
cmake -B build -S . \
-DCMAKE_BUILD_TYPE='Release' \
-DCMAKE_INSTALL_PREFIX='/usr' \
-Wno-dev
cmake --build build
}
package() {
cd "wallreel"
DESTDIR="$pkgdir" cmake --install build
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
EOF
sed -i "s/INSERT_SHA256_HERE/$SHA256/" PKGBUILD
- name: Build and Generate AUR Meta
run: |
tar -cf - . | docker run --rm -i archlinux:latest /bin/bash -e -c "
mkdir -p /workspace && cd /workspace
tar -xf -
exec 3>&1 1>&2
pacman-key --init && pacman-key --populate
pacman -Sy --noconfirm archlinux-keyring
pacman -Su --noconfirm base-devel cmake qt6-base qt6-declarative sudo
echo 'MAKEFLAGS="-j$(nproc)"' >> /etc/makepkg.conf
useradd -m builduser
chown -R builduser:builduser /workspace
echo 'builduser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
su - builduser -c 'cd /workspace && makepkg -sf --noconfirm'
su - builduser -c 'cd /workspace && makepkg --printsrcinfo' > SRCINFO.txt
tar -cf - *.pkg.tar.zst PKGBUILD SRCINFO.txt >&3
" | tar -xf -
- name: Upload Arch Artifacts
uses: actions/upload-artifact@v3
with:
name: arch-artifacts
path: |
*.pkg.tar.zst
PKGBUILD
SRCINFO.txt
publish-gitea:
name: Publish to Gitea Release
needs: [build-arch]
runs-on: ubuntu-latest
steps:
- name: Extract Version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download Arch Artifacts
uses: actions/download-artifact@v3
with:
name: arch-artifacts
path: .
- name: Publish to Gitea Release
uses: softprops/action-gh-release@v1
with:
name: WallReel ${{ env.VERSION }}
draft: false
prerelease: false
files: "*.pkg.tar.zst"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-aur:
name: Publish to AUR
needs: [build-arch]
runs-on: ubuntu-latest
steps:
- name: Extract Version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download Arch Artifacts
uses: actions/download-artifact@v3
with:
name: arch-artifacts
path: .
- name: Publish to AUR
env:
AUR_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$AUR_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
cat <<EOF > ~/.ssh/config
Host aur.archlinux.org
IdentityFile ~/.ssh/aur
User aur
EOF
git config --global user.name "Uyanide"
git config --global user.email "me@uyani.de"
git clone ssh://aur@aur.archlinux.org/wallreel.git aur-repo
cp PKGBUILD aur-repo/
cp SRCINFO.txt aur-repo/.SRCINFO
cd aur-repo
cat << 'EOF' > LICENSE
Copyright (C) 2026 by Uyanide me@uyani.de
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
EOF
git add PKGBUILD LICENSE .SRCINFO
git commit -m "Release v${{ env.VERSION }}"
git push origin master
+2
View File
@@ -81,3 +81,5 @@ CMakeLists.txt.user*
.uic/ .uic/
/build*/ /build*/
.cache .cache
.vscode
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(WallReel VERSION 2.0.0 LANGUAGES CXX) project(WallReel VERSION 2.1.0 LANGUAGES CXX)
set(EXECUTABLE_NAME "wallreel") set(EXECUTABLE_NAME "wallreel")
set(CORELIB_NAME "wallreel-core") set(CORELIB_NAME "wallreel-core")
+80 -49
View File
@@ -1,20 +1,20 @@
## What this is ## What this is
It might not be that worthy to build a Qt application from ground for such a small feature, but I kind of enjoy the pain... So here it is. It might be a bit overkill to build a Qt application from ground for such a small feature, but I kind of enjoy the pain... So here it is.
<img src="https://io.uyani.de/s/s6t5JDMEfqZmADB/preview"/> ![WallReel screenshot](misc/screenshot.webp)
## How to build ## How to build
1. Make sure you have Qt6 libraries, CMake and a C++ compiler installed. 1. Make sure Qt6 libraries, CMake, and a C++ compiler are installed.
e.g. On Arch-based systems: On Arch-based systems:
```bash ```bash
sudo pacman -S --needed qt6-base qt6-declarative cmake gcc sudo pacman -S --needed qt6-base qt6-declarative cmake gcc
``` ```
on Debian-based systems: On Debian-based systems:
```bash ```bash
sudo apt install --no-install-recommends qt6-base-dev qt6-declarative-dev qml6-module-qtquick qml6-module-qtquick-controls2 qml6-module-qtquick-layouts qml6-module-qtquick-templates qml6-qtqml-workerscript cmake g++ sudo apt install --no-install-recommends qt6-base-dev qt6-declarative-dev qml6-module-qtquick qml6-module-qtquick-controls2 qml6-module-qtquick-layouts qml6-module-qtquick-templates qml6-qtqml-workerscript cmake g++
@@ -29,35 +29,63 @@ It might not be that worthy to build a Qt application from ground for such a sma
3. Build and install: 3. Build and install:
This is a standard CMake managed project, so the build process is pretty normal and straightforward. First, configure the project: This is a standard CMake project. First, configure it. Adjust the install prefix as needed.
```bash ```bash
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local
``` ```
Adjust install prefix to your needs. Start building: Then build.
```bash ```bash
cmake --build build -- -j$(nproc) cmake --build build -- -j$(nproc)
``` ```
The binary will be located at `build/wallreel` and can be run directly for testing: The binary will be located at `build/wallreel` and can be run directly for testing.
```bash ```bash
build/wallreel build/wallreel
``` ```
Install it to the previously specified prefix. This step may require root permissions if the install prefix is set to a system directory like `/usr/local`. Install to the configured prefix. This step may require root permissions if the prefix is set to a system path such as `/usr/local`.
```bash ```bash
cmake --install build cmake --install build --strip
``` ```
`--strip` reduces binary size by removing symbol information that is usually unnecessary for normal usage.
## Man Pages
This project ships man pages and installs them through `cmake --install`.
- `wallreel(1)` for CLI usage
- `wallreel(5)` for configuration
The source files are maintained in Markdown for easier editing:
- `docs/man/man.1.md`
- `docs/man/man.5.md`
Generated man files are committed for packaging and normal installation without extra tool dependencies:
- `WallReel/Assets/man/man.1`
- `WallReel/Assets/man/man.5`
To regenerate them, run:
```bash
for src in docs/man/man.*.md; do
dst="WallReel/Assets/man/$(basename "${src%.md}")"
pandoc --from gfm --to man --standalone -o "$dst" "$src"
done
```
## Configuration Reference ## Configuration Reference
Refer to [config.schema.json](config.schema.json) for a complete reference of the configuration file schema. Below is a summary of the available options. Refer to [config.schema.json](config.schema.json) for a complete reference of the configuration file schema. Below is a summary of the available options.
The configuration file is divided into five main sections: `wallpaper`, `theme`, `action`, `style`, and `sort`. The configuration file is divided into five main sections: `wallpaper`, `theme`, `action`, `style`, and `cache`.
### Wallpaper (`wallpaper`) ### Wallpaper (`wallpaper`)
@@ -73,30 +101,28 @@ Defines where WallReel looks for images and what to exclude. If none of the `pat
Configures the color palettes. Configures the color palettes.
By default, a **dominant color** will be extracted from each wallpaper. If a palette is **selected**, the color that matches the dominant color the best will be selected as the **primary color**. This might be convinient if you prefer to set your desktop theme to match the wallpaper using a predefined palette (e.g. Catppuccin, Tokyo Night) instead of generating a custom one (e.g. using matugen). By default, a **dominant color** is extracted from each wallpaper. If a palette is **selected**, the closest palette color is used as the **primary color**. This is useful when you want your desktop theme to follow a predefined palette (for example Catppuccin or Tokyo Night) instead of generating a custom one (for example with matugen).
There are a few embeded palettes available in the application, including "Catppuccin Frappe", "Catppuccin Latte", "Catppuccin Macchiato", and "Catppuccin Mocha". You can also define your own palettes or override the embeded ones by providing a custom configuration. Several embedded palettes are available, including "Catppuccin Frappe", "Catppuccin Latte", "Catppuccin Macchiato", and "Catppuccin Mocha". You can also define custom palettes or override embedded ones via configuration.
| Property | Type | Default | Description | | Property | Type | Default | Description |
| :--------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ | | :--------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
| `defaultPalette` | String | `""` | Name of the default palette to use. | | `palettes` | Array of Objects | `[]` | List of defined palettes. Each contains a `name` (string) and an array of `colors` (each with a `name` and a hex `value` like `"#ff0000"`). |
| `palettes` | Array of Objects | `[]` | List of defined palettes. Each contains a `name` (string) and an array of `colors` (each with a `name` and a hex `value` like `"#ff0000"`). |
### Action (`action`) ### Action (`action`)
Configures system commands to execute on specific events mapping to your window manager or wallpaper utility (e.g., `swaybg`, `feh`). Configures system commands to execute on specific events mapping to your window manager or wallpaper utility (e.g., `swaybg`, `feh`).
| Property | Type | Default | Description | | Property | Type | Default | Description |
| :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `previewDebounceTime` | Integer | `300` | Debounce time (ms) for triggering the preview action. | | `previewDebounceTime` | Integer | `300` | Debounce time (ms) for triggering the preview action. |
| `printSelected` | Boolean | `true` | Print selected wallpaper path to stdout on confirm. | | `printSelected` | Boolean | `true` | Print selected wallpaper path to stdout on confirm. |
| `printPreview` | Boolean | `false` | Print previewed wallpaper path to stdout on preview. | | `onSelected` | String | `""` | Command to execute when a wallpaper is confirmed. |
| `onSelected` | String | `""` | Command to execute when a wallpaper is confirmed. | | `onPreview` | String | `""` | Command to execute when a wallpaper is previewed. |
| `onPreview` | String | `""` | Command to execute when a wallpaper is previewed. | | `saveState` | Array of Objects | `[]` | Commands to fetch system states before changing wallpapers. Each object defines: `key`, `fallback` (fallback value), `command` (stdout mapping), and `timeout` (ms). |
| `saveState` | Array of Objects | `[]` | Commands to fetch system states before changing wallpapers. Each object defines: `key`, `default` (fallback value), `command` (stdout mapping), and `timeout` (ms). | | `onRestore` | String | `""` | Command to execute on restore. Extracted states from `saveState` can be injected using `{{ key }}`. |
| `onRestore` | String | `""` | Command to execute on restore. Extracted states from `saveState` can be injected using `{{ key }}`. | | `quitOnSelected` | Boolean | `true` | Quit the application after a selection is made. |
| `quitOnSelected` | Boolean | `false` | Quit the application after a selection is made. | | `restoreOnClose` | Boolean | `true` | Run `onRestore` command if the application is closed without making a final selection. |
| `restoreOnClose` | Boolean | `true` | Run `onRestore` command if the application is closed without making a final selection. |
Available placeholders for `onSelected`, `onPreview` commands: Available placeholders for `onSelected`, `onPreview` commands:
@@ -109,7 +135,7 @@ Available placeholders for `onSelected`, `onPreview` commands:
| `{{ colorName }}` | Name of the currently determined primary color. ("null" if none) | | `{{ colorName }}` | Name of the currently determined primary color. ("null" if none) |
| `{{ colorHex }}` | Hex code (starting with "#") of the currently determined primary color. ("null" if none) | | `{{ colorHex }}` | Hex code (starting with "#") of the currently determined primary color. ("null" if none) |
| `{{ domColorHex }}` | Hex code (starting with "#") of the dominant color in the selected or previewed wallpaper. | | `{{ domColorHex }}` | Hex code (starting with "#") of the dominant color in the selected or previewed wallpaper. |
| `{{ key }}` | Value of the saved state with the specified key. | | `{{ <key> }}` | Value of the saved state with the specified key. |
### Style (`style`) ### Style (`style`)
@@ -123,14 +149,15 @@ Controls the layout and dimensions of the application window and image items.
| `window_width` | Integer | `750` | Initial application window width. | | `window_width` | Integer | `750` | Initial application window width. |
| `window_height` | Integer | `500` | Initial application window height. | | `window_height` | Integer | `500` | Initial application window height. |
### Sort (`sort`) ### Cache (`cache`)
Initial sorting behavior for loaded images. Controls what UI state is persisted between sessions.
| Property | Type | Default | Description | | Property | Type | Default | Description |
| :----------- | :------ | :------- | :------------------------------------------------------------------------------- | | :---------------- | :------ | :------ | :---------------------------------------------------------------------------- |
| `type` | String | `"date"` | Defines sorting criteria. Acceptable values: `"name"`, `"date"`, `"size"`. | | `saveSortMethod` | Boolean | `true` | Whether to persist the sort type and order. |
| `descending` | Boolean | `true` | If true, sorts in descending order (e.g. newer dates first, larger files first). | | `savePalette` | Boolean | `true` | Whether to persist the selected palette. |
| `maxImageEntries` | Integer | `1000` | Maximum number of entries in the image cache (older entries will be evicted). |
--- ---
@@ -150,7 +177,6 @@ Initial sorting behavior for loaded images.
"excludes": ["\\.gif$"] "excludes": ["\\.gif$"]
}, },
"theme": { "theme": {
"defaultPalette": "Dark",
"palettes": [ "palettes": [
{ {
"name": "Dark", "name": "Dark",
@@ -169,7 +195,7 @@ Initial sorting behavior for loaded images.
"saveState": [ "saveState": [
{ {
"key": "current_wp", "key": "current_wp",
"default": "/home/user/Pictures/default.jpg", "fallback": "/home/user/Pictures/default.jpg",
"command": "find ~/.config/wallpaper/current -type f | head -n 1", "command": "find ~/.config/wallpaper/current -type f | head -n 1",
"timeout": 1000 "timeout": 1000
} }
@@ -183,32 +209,37 @@ Initial sorting behavior for loaded images.
"window_width": 1280, "window_width": 1280,
"window_height": 720 "window_height": 720
}, },
"sort": { "cache": {
"type": "date", "saveSortMethod": true,
"descending": true "savePalette": true,
"maxImageEntries": 300
} }
} }
``` ```
## CLI ## CLI
``` ```text
Usage: wallreel [options] Usage: wallreel [options]
Options: Options:
-h, --help Displays help on commandline options. -h, --help Displays help on commandline options.
-v, --version Displays version information. -v, --version Displays version information.
-V, --verbose Set log level to DEBUG (default is INFO) -V, --verbose Set log level to DEBUG (default is INFO)
-C, --clear-cache Clear the cache and exit -C, --clear-cache Clear the image cache and exit
-q, --quiet Suppress all log output -q, --quiet Suppress all log output
-d, --append-dir <dir> Append an additional wallpaper search directory -d, --append-dir <dir> Append an additional wallpaper search directory
-c, --config-file <file> Specify a custom configuration file -c, --config-file <file> Specify a custom configuration file
-D, --disable-actions Disable actions set in configuration file
-a, --apply <file> Apply the specified image as wallpaper and exit
``` ```
A few things to notice: A few things to notice:
- It's generally not necessary to provide any CLI arguments, I would recommend using the config file to customize the behavior instead. However, it is still possible to control some essential options via CLI. - In most cases you do not need CLI arguments; configuration is usually the better place to customize behavior. CLI flags are still useful for quick overrides and one-shot runs though.
- The `--append-dir` option can be used multiple times to add multiple directories. - The `--append-dir` option can be used multiple times to add multiple directories.
- It is quite obvious that some options conflicts with each other (e.g. `--verbose` and `--quiet`). Case mutually exclusive options are provided together, the behavior is un.. just please, don't do that. - It is quite obvious that some options conflicts with each other (e.g. `--verbose` and `--quiet`). Case mutually exclusive options are provided together, the behavior is un.. just please, don't do that.
- With `--apply`, WallReel still parses the configuration (default path or `--config-file`) and executes `onSelected` with placeholders resolved from the specified image. If `savePalette` is enabled and a palette was selected in the last session, `palette`, `colorName`, and `colorHex` placeholders are also available. `saveState` commands are executed as well. The application exits immediately after executing the action, without opening the UI. This mode allows WallReel to be used as a command-line wallpaper setter with palette-aware theming and state placeholders.
+21 -5
View File
@@ -1,13 +1,29 @@
qt_add_resources(${EXECUTABLE_NAME} "app_icons" qt_add_resources(${EXECUTABLE_NAME} "app_icons"
PREFIX "/" PREFIX "/"
FILES FILES icon.svg
${EXECUTABLE_NAME}.svg
) )
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.desktop install(FILES ${CMAKE_CURRENT_LIST_DIR}/app.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
RENAME ${EXECUTABLE_NAME}.desktop
) )
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.svg install(FILES ${CMAKE_CURRENT_LIST_DIR}/apply.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
RENAME ${EXECUTABLE_NAME}-apply.desktop
)
install(FILES ${CMAKE_CURRENT_LIST_DIR}/icon.svg
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps
RENAME ${EXECUTABLE_NAME}.svg
)
install(FILES ${CMAKE_CURRENT_LIST_DIR}/man/man.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1
RENAME ${EXECUTABLE_NAME}.1
)
install(FILES ${CMAKE_CURRENT_LIST_DIR}/man/man.5
DESTINATION ${CMAKE_INSTALL_MANDIR}/man5
RENAME ${EXECUTABLE_NAME}.5
) )
@@ -3,10 +3,10 @@ Version=1.0
Type=Application Type=Application
Name=WallReel Name=WallReel
Icon=wallreel Icon=wallreel
GenericName=Animated wallpaper selector GenericName=Wallpaper Selector
TryExec=wallreel TryExec=wallreel
Exec=wallreel Exec=wallreel
Comment=A small wallpaper utility made with Qt Comment=Choose and set desktop wallpapers with customizable themes and actions
Terminal=false Terminal=false
Categories=Application;Utility;DesktopSettings; Categories=Application;Utility;DesktopSettings;
StartupNotify=true StartupNotify=true
+15
View File
@@ -0,0 +1,15 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Apply with WallReel
Icon=wallreel
NoDisplay=true
GenericName=Wallpaper Selector
TryExec=wallreel
Exec=wallreel -a %f
Comment=Choose and set desktop wallpapers with customizable themes and actions
Terminal=false
Categories=Application;Utility;DesktopSettings;
StartupNotify=true
Keywords=wallpaper;animated;utility;qt;
MimeType=image/jpeg;image/png;image/webp;image/bmp;image/gif;image/heic;image/heif;

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

+89
View File
@@ -0,0 +1,89 @@
.\" Automatically generated by Pandoc 3.9.0.2
.\"
.TH "WALLREEL" "1" "2026\-03\-24" "WallReel 2.0.2" "User Commands"
.SH NAME
wallreel \- Choose and set desktop wallpapers with customizable themes
and actions
.SH SYNOPSIS
\f[B]wallreel\f[R] [\f[I]options\f[R]]
.SH DESCRIPTION
\f[B]wallreel\f[R] is a Qt6 application for browsing wallpaper images,
previewing candidates, and applying a selected image.
.PP
Configuration is loaded from a JSON file.
CLI options are available for logging, one\-shot operations, and runtime
overrides.
.SH OPTIONS
\f[B]\-h, \-\-help\f[R] : Display help for command\-line options.
.PP
\f[B]\-v, \-\-version\f[R] : Display version information.
.PP
\f[B]\-V, \-\-verbose\f[R] : Set log level to DEBUG (default is INFO).
.PP
\f[B]\-C, \-\-clear\-cache\f[R] : Clear image cache and exit.
.PP
\f[B]\-q, \-\-quiet\f[R] : Suppress log output.
.PP
\f[B]\-d, \-\-append\-dir\f[R] \f[I]dir\f[R] : Append an additional
wallpaper search directory.
.PP
This option can be provided multiple times.
.PP
\f[B]\-c, \-\-config\-file\f[R] \f[I]file\f[R] : Use a custom
configuration file.
.PP
\f[B]\-D, \-\-disable\-actions\f[R] : Disable actions defined in the
configuration file.
.PP
\f[B]\-a, \-\-apply\f[R] \f[I]file\f[R] : Apply the specified image as
wallpaper and exit.
.PP
In this mode, the configuration is still parsed.
Action placeholders are resolved from the selected image and any
captured state values.
.SH BEHAVIOR NOTES
.IP \(bu 2
CLI options are generally optional; configuration is the preferred
customization path.
.IP \(bu 2
Some options are mutually exclusive (for example \f[CR]\-\-verbose\f[R]
and \f[CR]\-\-quiet\f[R]).
.IP \(bu 2
With \f[CR]\-\-apply\f[R], WallReel executes configured selection
actions without opening the UI.
.SH FILES
\f[CR]\(ti/.config/wallreel/config.json\f[R] : Default configuration
file location.
.PP
\f[CR]\(ti/.cache/wallreel/\f[R] : Runtime cache location.
.SH EXAMPLES
Run with default configuration:
.IP
.EX
wallreel
.EE
.PP
Use a custom configuration file:
.IP
.EX
wallreel \-\-config\-file \(ti/.config/wallreel/config.json
.EE
.PP
Append additional search directories:
.IP
.EX
wallreel \-\-append\-dir \(ti/Pictures/Wallpapers \-\-append\-dir \(ti/Art
.EE
.PP
Apply a wallpaper and exit:
.IP
.EX
wallreel \-\-apply \(ti/Pictures/wallpaper.jpg
.EE
.SH EXIT STATUS
Returns \f[CR]0\f[R] on success.
Returns a non\-zero value on failure.
.SH SEE ALSO
\f[B]wallreel\f[R](5)
.SH AUTHOR
Uyanide <github.com/Uyanide>
+216
View File
@@ -0,0 +1,216 @@
.\" Automatically generated by Pandoc 3.9.0.2
.\"
.TH "WALLREEL" "5" "2026\-03\-24" "WallReel 2.0.2" "File Formats Manual"
.SH NAME
wallreel\-config \- configuration format for wallreel
.SH SYNOPSIS
\f[CR]\(ti/.config/wallreel/config.json\f[R]
.SH DESCRIPTION
WallReel reads configuration from a JSON document.
The root object is divided into five sections:
.IP \(bu 2
\f[CR]wallpaper\f[R]
.IP \(bu 2
\f[CR]theme\f[R]
.IP \(bu 2
\f[CR]action\f[R]
.IP \(bu 2
\f[CR]style\f[R]
.IP \(bu 2
\f[CR]cache\f[R]
.PP
For complete machine\-readable validation details, refer to
\f[CR]config.schema.json\f[R].
.SH WALLPAPER SECTION
Defines where WallReel looks for images and what to exclude.
.PP
If both \f[CR]paths\f[R] and \f[CR]dirs\f[R] are empty or omitted,
WallReel defaults to recursively scanning the user\(aqs Pictures
directory and treating all supported image files as wallpaper
candidates.
.PP
\f[CR]paths\f[R] (array of string, default: \f[CR][]\f[R]) : Exact paths
to specific image files.
.PP
\f[CR]dirs\f[R] (array of object, default: \f[CR][]\f[R]) : Directories
to scan for images.
.PP
Each item has:
.IP \(bu 2
\f[CR]path\f[R] (string)
.IP \(bu 2
\f[CR]recursive\f[R] (boolean)
.PP
\f[CR]excludes\f[R] (array of string, default: \f[CR][]\f[R]) : Exclude
patterns as regular expressions.
.SH THEME SECTION
Configures color palettes.
.PP
A dominant color is extracted from each wallpaper.
If a palette is selected, WallReel picks the closest palette color as
the primary color.
.PP
\f[CR]palettes\f[R] (array of object, default: \f[CR][]\f[R]) : Custom
palette definitions.
.PP
Each palette has:
.IP \(bu 2
\f[CR]name\f[R] (string)
.IP \(bu 2
\f[CR]colors\f[R] (array)
.PP
Each color item has:
.IP \(bu 2
\f[CR]name\f[R] (string)
.IP \(bu 2
\f[CR]value\f[R] (hex string, for example \f[CR]\(dq#89b4fa\(dq\f[R])
.SH ACTION SECTION
Configures commands executed for preview, selection, and restore
behavior.
.PP
\f[CR]previewDebounceTime\f[R] (integer, default: \f[CR]300\f[R]) :
Debounce interval in milliseconds for preview actions.
.PP
\f[CR]printSelected\f[R] (boolean, default: \f[CR]true\f[R]) : Print
selected wallpaper path to stdout on confirmation.
.PP
\f[CR]onSelected\f[R] (string, default: \f[CR]\(dq\(dq\f[R]) : Command
executed when a wallpaper is confirmed.
.PP
\f[CR]onPreview\f[R] (string, default: \f[CR]\(dq\(dq\f[R]) : Command
executed when a wallpaper is previewed.
.PP
\f[CR]saveState\f[R] (array of object, default: \f[CR][]\f[R]) :
Commands for capturing system values before changing wallpaper.
.PP
Each item has:
.IP \(bu 2
\f[CR]key\f[R] (placeholder key)
.IP \(bu 2
\f[CR]fallback\f[R] (default value)
.IP \(bu 2
\f[CR]command\f[R] (stdout\-mapped command)
.IP \(bu 2
\f[CR]timeout\f[R] (milliseconds)
.PP
\f[CR]onRestore\f[R] (string, default: \f[CR]\(dq\(dq\f[R]) : Command
executed on restore.
Saved state keys are usable as placeholders.
.PP
\f[CR]quitOnSelected\f[R] (boolean, default: \f[CR]true\f[R]) : Exit
application immediately after confirming a selection.
.PP
\f[CR]restoreOnClose\f[R] (boolean, default: \f[CR]true\f[R]) : Run
\f[CR]onRestore\f[R] when application closes without a final selection.
.SS ACTION PLACEHOLDERS
The following placeholders are available in \f[CR]onSelected\f[R],
\f[CR]onPreview\f[R], and \f[CR]onRestore\f[R] (where applicable):
.PP
\f[CR]{{ path }}\f[R] : Full path of selected or previewed wallpaper.
.PP
\f[CR]{{ name }}\f[R] : File name of selected or previewed wallpaper.
.PP
\f[CR]{{ size }}\f[R] : Size in bytes of selected or previewed
wallpaper.
.PP
\f[CR]{{ palette }}\f[R] : Selected palette name
(\f[CR]\(dqnull\(dq\f[R] if none).
.PP
\f[CR]{{ colorName }}\f[R] : Chosen primary color name
(\f[CR]\(dqnull\(dq\f[R] if none).
.PP
\f[CR]{{ colorHex }}\f[R] : Chosen primary color hex
(\f[CR]\(dqnull\(dq\f[R] if none).
.PP
\f[CR]{{ domColorHex }}\f[R] : Dominant color hex extracted from the
wallpaper.
.PP
\f[CR]{{ <key> }}\f[R] : Value of a saved state item with matching key.
.SH STYLE SECTION
Controls window layout and thumbnail dimensions.
.PP
\f[CR]image_width\f[R] (integer, default: \f[CR]320\f[R]) : Width of
each thumbnail.
.PP
\f[CR]image_height\f[R] (integer, default: \f[CR]180\f[R]) : Height of
each thumbnail.
.PP
\f[CR]image_focus_scale\f[R] (number, default: \f[CR]1.5\f[R]) : Focus
scale multiplier for highlighted thumbnail.
.PP
\f[CR]window_width\f[R] (integer, default: \f[CR]750\f[R]) : Initial
window width.
.PP
\f[CR]window_height\f[R] (integer, default: \f[CR]500\f[R]) : Initial
window height.
.SH CACHE SECTION
Controls persisted UI state.
.PP
\f[CR]saveSortMethod\f[R] (boolean, default: \f[CR]true\f[R]) : Persist
sort method and direction.
.PP
\f[CR]savePalette\f[R] (boolean, default: \f[CR]true\f[R]) : Persist
selected palette.
.PP
\f[CR]maxImageEntries\f[R] (integer, default: \f[CR]1000\f[R]) : Maximum
number of image cache entries.
Older entries are evicted.
.SH EXAMPLE
.IP
.EX
{
\(dq$schema\(dq: \(dqhttps://raw.githubusercontent.com/Uyanide/WallReel/refs/heads/master/config.schema.json\(dq,
\(dqwallpaper\(dq: {
\(dqpaths\(dq: [\(dq/home/user/Pictures/favorite.jpg\(dq],
\(dqdirs\(dq: [
{
\(dqpath\(dq: \(dq/home/user/Pictures/Wallpapers\(dq,
\(dqrecursive\(dq: \f[B]true\f[R]
}
],
\(dqexcludes\(dq: [\(dq\(rs\(rs.gif$\(dq]
},
\(dqtheme\(dq: {
\(dqpalettes\(dq: [
{
\(dqname\(dq: \(dqDark\(dq,
\(dqcolors\(dq: [
{ \(dqname\(dq: \(dqblue\(dq, \(dqvalue\(dq: \(dq#89b4fa\(dq },
{ \(dqname\(dq: \(dqred\(dq, \(dqvalue\(dq: \(dq#f38ba8\(dq }
]
}
]
},
\(dqaction\(dq: {
\(dqpreviewDebounceTime\(dq: 500,
\(dqquitOnSelected\(dq: \f[B]true\f[R],
\(dqonPreview\(dq: \(dqswww img {{ path }}\(dq,
\(dqonSelected\(dq: \(dqcp {{ path }} \(ti/.config/wallpaper/current/ && swww img {{ path }}\(dq,
\(dqsaveState\(dq: [
{
\(dqkey\(dq: \(dqcurrent_wp\(dq,
\(dqfallback\(dq: \(dq/home/user/Pictures/default.jpg\(dq,
\(dqcommand\(dq: \(dqfind \(ti/.config/wallpaper/current \-type f | head \-n 1\(dq,
\(dqtimeout\(dq: 1000
}
],
\(dqonRestore\(dq: \(dqswww img {{ current_wp }}\(dq
},
\(dqstyle\(dq: {
\(dqimage_width\(dq: 640,
\(dqimage_height\(dq: 400,
\(dqimage_focus_scale\(dq: 1.2,
\(dqwindow_width\(dq: 1280,
\(dqwindow_height\(dq: 720
},
\(dqcache\(dq: {
\(dqsaveSortMethod\(dq: \f[B]true\f[R],
\(dqsavePalette\(dq: \f[B]true\f[R],
\(dqmaxImageEntries\(dq: 300
}
}
.EE
.SH SEE ALSO
\f[B]wallreel\f[R](1)
.SH AUTHOR
Uyanide <github.com/Uyanide>
+1 -1
View File
@@ -15,7 +15,7 @@ qt_add_qml_module(${CORELIB_NAME}
Config/data.hpp Config/data.hpp
Config/manager.hpp Config/manager.cpp Config/manager.hpp Config/manager.cpp
logger.hpp logger.cpp logger.hpp logger.cpp
Service/manager.hpp Service/manager.hpp Service/manager.cpp
Service/wallpaper.hpp Service/wallpaper.cpp Service/wallpaper.hpp Service/wallpaper.cpp
appoptions.hpp appoptions.cpp appoptions.hpp appoptions.cpp
) )
+294 -33
View File
@@ -7,6 +7,7 @@
#include <QSqlError> #include <QSqlError>
#include <QSqlQuery> #include <QSqlQuery>
#include <QThread> #include <QThread>
#include <QtConcurrent>
#include "logger.hpp" #include "logger.hpp"
@@ -16,21 +17,51 @@ using namespace Qt::StringLiterals;
namespace WallReel::Core::Cache { namespace WallReel::Core::Cache {
static QLatin1StringView settingKey(SettingsType type) {
switch (type) {
case SettingsType::LastSelectedPalette: return "last_selected_palette"_L1;
case SettingsType::LastSortType: return "last_sort_type"_L1;
case SettingsType::LastSortDescending: return "last_sort_descending"_L1;
}
Q_UNREACHABLE();
}
QString Manager::cacheKey(const QFileInfo& fileInfo, const QSize& imageSize) { QString Manager::cacheKey(const QFileInfo& fileInfo, const QSize& imageSize) {
const QString raw = fileInfo.absoluteFilePath() + QString::number(fileInfo.lastModified().toMSecsSinceEpoch()) + u'x' + QString::number(imageSize.width()) + u'x' + QString::number(imageSize.height()); const QString raw = fileInfo.absoluteFilePath() +
QString::number(fileInfo.lastModified().toMSecsSinceEpoch()) +
u'x' + QString::number(imageSize.width()) +
u'x' + QString::number(imageSize.height());
return QString::fromLatin1( return QString::fromLatin1(
QCryptographicHash::hash(raw.toUtf8(), QCryptographicHash::Sha256).toHex()); QCryptographicHash::hash(raw.toUtf8(), QCryptographicHash::Sha256).toHex());
} }
Manager::Manager(const QDir& cacheDir) Manager::Manager(const QDir& cacheDir, int maxEntries)
: m_cacheDir(cacheDir), m_dbPath(cacheDir.filePath(u"cache.db"_s)), m_connectionPrefix(u"WallReelCache:"_s + QString::fromLatin1(QCryptographicHash::hash(m_dbPath.toUtf8(), QCryptographicHash::Md5).toHex())) { : m_cacheDir(cacheDir),
m_maxEntries(maxEntries),
m_dbPath(cacheDir.filePath(u"cache.db"_s)),
m_connectionPrefix(u"WallReelCache:"_s +
QString::fromLatin1(QCryptographicHash::hash(
m_dbPath.toUtf8(),
QCryptographicHash::Md5)
.toHex())) {
WR_DEBUG(u"Initializing cache db: %1"_s.arg(m_dbPath)); WR_DEBUG(u"Initializing cache db: %1"_s.arg(m_dbPath));
// Open a connection on the constructing thread so the schema is // Open a connection on the constructing thread so the schema is
// guaranteed to exist before any worker thread first calls _db(). // guaranteed to exist before any worker thread first calls _db().
_db(); _db();
} }
void Manager::evictOldEntries() {
if (m_maxEntries > 0)
m_cleanupFuture = QtConcurrent::run([this] { _runCleanup(); });
}
Manager::~Manager() { Manager::~Manager() {
// Wait for the background cleanup to finish before tearing down DB connections.
if (m_cleanupFuture.isValid() && !m_cleanupFuture.isFinished()) {
WR_DEBUG(u"Waiting for cache cleanup to finish..."_s);
m_cleanupFuture.waitForFinished();
}
QSet<QString> names; QSet<QString> names;
{ {
QMutexLocker lock(&m_connectionsMutex); QMutexLocker lock(&m_connectionsMutex);
@@ -50,41 +81,59 @@ void Manager::clearCache(Type type) {
if ((type & Type::Image) != Type::None) { if ((type & Type::Image) != Type::None) {
int removed = 0; int removed = 0;
QSqlQuery selectQuery(db); QSqlQuery selectQuery(db);
if (selectQuery.exec(QStringLiteral("SELECT file_name FROM image_cache"))) { if (selectQuery.exec(u"SELECT file_name FROM image_cache"_s)) {
while (selectQuery.next()) { while (selectQuery.next()) {
QFile::remove(m_cacheDir.filePath(selectQuery.value(0).toString())); QFile::remove(m_cacheDir.filePath(selectQuery.value(0).toString()));
++removed; ++removed;
} }
} }
QSqlQuery(db).exec(QStringLiteral("DELETE FROM image_cache")); QSqlQuery(db).exec(u"DELETE FROM image_cache"_s);
WR_INFO(u"Cleared %1 image cache file(s)"_s.arg(removed)); WR_INFO(u"Cleared %1 image cache file(s)"_s.arg(removed));
} }
if ((type & Type::Color) != Type::None) { if ((type & Type::Color) != Type::None) {
QSqlQuery(db).exec(QStringLiteral("DELETE FROM color_cache")); QSqlQuery(db).exec(u"DELETE FROM color_cache"_s);
WR_INFO(u"Cleared color cache"_s); WR_INFO(u"Cleared color cache"_s);
} }
if ((type & Type::Settings) != Type::None) {
QSqlQuery(db).exec(u"DELETE FROM settings_cache"_s);
WR_INFO(u"Cleared settings cache"_s);
}
} }
QColor Manager::getColor(const QString& key, const std::function<QColor()>& computeFunc) { QColor Manager::getColor(const QString& key, const std::function<QColor()>& computeFunc) {
QSqlDatabase db = _db(); QSqlDatabase db = _db();
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery query(db); QSqlQuery query(db);
query.prepare(QStringLiteral( query.prepare(u"SELECT r, g, b, a FROM color_cache WHERE key = :key"_s);
"SELECT r, g, b, a FROM color_cache WHERE key = :key"));
query.bindValue(u":key"_s, key); query.bindValue(u":key"_s, key);
if (query.exec() && query.next()) { if (query.exec() && query.next()) {
WR_DEBUG(u"Color cache hit [%1]"_s.arg(key)); WR_DEBUG(u"Color cache hit [%1]"_s.arg(key));
return QColor( QColor result(
query.value(0).toInt(), query.value(0).toInt(),
query.value(1).toInt(), query.value(1).toInt(),
query.value(2).toInt(), query.value(2).toInt(),
query.value(3).toInt()); query.value(3).toInt());
{
QMutexLocker lk(&m_hotKeysMutex);
m_hotColorKeys.insert(key);
}
QSqlQuery touchQuery(db);
touchQuery.prepare(u"UPDATE color_cache SET last_accessed = CURRENT_TIMESTAMP WHERE key = :key"_s);
touchQuery.bindValue(u":key"_s, key);
touchQuery.exec();
return result;
} }
} }
WR_DEBUG(u"Color cache miss [%1], computing"_s.arg(key)); WR_DEBUG(u"Color cache miss [%1], computing"_s.arg(key));
if (!computeFunc) {
WR_WARN(u"No compute function provided for color cache miss [%1]"_s.arg(key));
return QColor();
}
const QColor color = computeFunc(); const QColor color = computeFunc();
if (!color.isValid()) { if (!color.isValid()) {
@@ -94,9 +143,9 @@ QColor Manager::getColor(const QString& key, const std::function<QColor()>& comp
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery insertQuery(db); QSqlQuery insertQuery(db);
insertQuery.prepare(QStringLiteral( insertQuery.prepare(
"INSERT OR REPLACE INTO color_cache (key, r, g, b, a) " u"INSERT OR REPLACE INTO color_cache (key, r, g, b, a, last_accessed) "
"VALUES (:key, :r, :g, :b, :a)")); "VALUES (:key, :r, :g, :b, :a, CURRENT_TIMESTAMP)"_s);
insertQuery.bindValue(u":key"_s, key); insertQuery.bindValue(u":key"_s, key);
insertQuery.bindValue(u":r"_s, color.red()); insertQuery.bindValue(u":r"_s, color.red());
insertQuery.bindValue(u":g"_s, color.green()); insertQuery.bindValue(u":g"_s, color.green());
@@ -116,8 +165,7 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
QSqlDatabase db = _db(); QSqlDatabase db = _db();
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery query(db); QSqlQuery query(db);
query.prepare(QStringLiteral( query.prepare(u"SELECT file_name FROM image_cache WHERE key = :key"_s);
"SELECT file_name FROM image_cache WHERE key = :key"));
query.bindValue(u":key"_s, key); query.bindValue(u":key"_s, key);
if (query.exec() && query.next()) { if (query.exec() && query.next()) {
@@ -125,29 +173,42 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
if (cached.exists()) { if (cached.exists()) {
WR_DEBUG(u"Image cache hit [%1] -> %2"_s WR_DEBUG(u"Image cache hit [%1] -> %2"_s
.arg(key, cached.absoluteFilePath())); .arg(key, cached.absoluteFilePath()));
{
QMutexLocker lk(&m_hotKeysMutex);
m_hotImageKeys.insert(key);
}
QSqlQuery touchQuery(db);
touchQuery.prepare(u"UPDATE image_cache SET last_accessed = CURRENT_TIMESTAMP WHERE key = :key"_s);
touchQuery.bindValue(u":key"_s, key);
touchQuery.exec();
return cached; return cached;
} }
// File was deleted externally — evict the stale DB record. // File was deleted externally — evict the stale DB record.
WR_WARN(u"Image cache stale, file missing [%1], evicting"_s.arg(key)); WR_WARN(u"Image cache stale, file missing [%1], evicting"_s.arg(key));
QSqlQuery evict(db); QSqlQuery evict(db);
evict.prepare(QStringLiteral("DELETE FROM image_cache WHERE key = :key")); evict.prepare(u"DELETE FROM image_cache WHERE key = :key"_s);
evict.bindValue(u":key"_s, key); evict.bindValue(u":key"_s, key);
evict.exec(); evict.exec();
} }
} }
WR_DEBUG(u"Image cache miss [%1], computing"_s.arg(key)); WR_DEBUG(u"Image cache miss [%1], computing"_s.arg(key));
if (!computeFunc) {
WR_WARN(u"No compute function provided for image cache miss [%1]"_s.arg(key));
return QFileInfo{};
}
const QImage image = computeFunc(); const QImage image = computeFunc();
if (image.isNull()) { if (image.isNull()) {
WR_WARN(u"ComputeFunc returned null image for key [%1]"_s.arg(key)); WR_WARN(u"ComputeFunc returned null image for key [%1]"_s.arg(key));
return QFileInfo{}; return QFileInfo{};
} }
const QString fileName = key + u".png"_s; const QString fileName = key + u".jpg"_s;
const QString filePath = m_cacheDir.filePath(fileName); const QString filePath = m_cacheDir.filePath(fileName);
if (!image.save(filePath, "PNG")) { if (!image.save(filePath, "JPEG", 85)) {
WR_WARN(u"Failed to save image to %1"_s.arg(filePath)); WR_WARN(u"Failed to save image to %1"_s.arg(filePath));
return QFileInfo{}; return QFileInfo{};
} }
@@ -155,19 +216,93 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery insertQuery(db); QSqlQuery insertQuery(db);
insertQuery.prepare(QStringLiteral( insertQuery.prepare(
"INSERT OR REPLACE INTO image_cache (key, file_name) " u"INSERT OR REPLACE INTO image_cache (key, file_name, last_accessed) "
"VALUES (:key, :file_name)")); "VALUES (:key, :file_name, CURRENT_TIMESTAMP)"_s);
insertQuery.bindValue(u":key"_s, key); insertQuery.bindValue(u":key"_s, key);
insertQuery.bindValue(u":file_name"_s, fileName); insertQuery.bindValue(u":file_name"_s, fileName);
if (!insertQuery.exec()) if (!insertQuery.exec())
WR_WARN(u"Failed to record image in db [%1]: %2"_s WR_WARN(u"Failed to record image in db [%1]: %2"_s
.arg(key, insertQuery.lastError().text())); .arg(key, insertQuery.lastError().text()));
else {
QMutexLocker lock(&m_hotKeysMutex);
m_hotImageKeys.insert(key);
}
} }
return QFileInfo(filePath); return QFileInfo(filePath);
} }
QString Manager::getSetting(SettingsType key, const std::function<QString()>& computeFunc) {
QSqlDatabase db = _db();
const QLatin1StringView keyStr = settingKey(key);
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(u"SELECT value FROM settings_cache WHERE key = :key"_s);
query.bindValue(u":key"_s, keyStr);
if (query.exec() && query.next()) {
WR_DEBUG(u"Settings cache hit [%1]"_s.arg(keyStr));
return query.value(0).toString();
}
}
WR_DEBUG(u"Settings cache miss [%1], computing"_s.arg(keyStr));
if (!computeFunc) {
WR_WARN(u"No compute function provided for settings cache miss [%1]"_s.arg(keyStr));
return QString{};
}
const QString value = computeFunc();
if (db.isOpen() && !value.isNull()) {
QSqlQuery insertQuery(db);
insertQuery.prepare(
u"INSERT OR REPLACE INTO settings_cache (key, value) "
"VALUES (:key, :value)"_s);
insertQuery.bindValue(u":key"_s, keyStr);
insertQuery.bindValue(u":value"_s, value);
if (!insertQuery.exec())
WR_WARN(u"Failed to cache setting [%1]: %2"_s
.arg(keyStr, insertQuery.lastError().text()));
else
WR_DEBUG(u"Setting cached [%1]"_s.arg(keyStr));
}
return value;
}
void Manager::storeSetting(SettingsType key, const QString& value) {
QSqlDatabase db = _db();
const QLatin1StringView keyStr = settingKey(key);
if (db.isOpen()) {
if (value.isNull()) {
QSqlQuery deleteQuery(db);
deleteQuery.prepare(u"DELETE FROM settings_cache WHERE key = :key"_s);
deleteQuery.bindValue(u":key"_s, keyStr);
if (!deleteQuery.exec())
WR_WARN(u"Failed to delete setting [%1]: %2"_s
.arg(keyStr, deleteQuery.lastError().text()));
else
WR_DEBUG(u"Setting deleted [%1]"_s.arg(keyStr));
} else {
QSqlQuery insertQuery(db);
insertQuery.prepare(
u"INSERT OR REPLACE INTO settings_cache (key, value) "
"VALUES (:key, :value)"_s);
insertQuery.bindValue(u":key"_s, keyStr);
insertQuery.bindValue(u":value"_s, value);
if (!insertQuery.exec())
WR_WARN(u"Failed to store setting [%1]: %2"_s
.arg(keyStr, insertQuery.lastError().text()));
else
WR_DEBUG(u"Setting stored [%1]"_s.arg(keyStr));
}
}
}
/// Returns an open QSqlDatabase for the calling thread, creating it on first use. /// Returns an open QSqlDatabase for the calling thread, creating it on first use.
QSqlDatabase Manager::_db() const { QSqlDatabase Manager::_db() const {
// thread_local: one slot per OS thread, initialized on first call in that thread. // thread_local: one slot per OS thread, initialized on first call in that thread.
@@ -225,19 +360,145 @@ QSqlDatabase Manager::_db() const {
void Manager::_setupTables(QSqlDatabase& db) const { void Manager::_setupTables(QSqlDatabase& db) const {
QSqlQuery q(db); QSqlQuery q(db);
q.exec(QStringLiteral( q.exec(
"CREATE TABLE IF NOT EXISTS color_cache (" u"CREATE TABLE IF NOT EXISTS color_cache ("
" key TEXT PRIMARY KEY NOT NULL," " key TEXT PRIMARY KEY NOT NULL,"
" r INTEGER NOT NULL," " r INTEGER NOT NULL,"
" g INTEGER NOT NULL," " g INTEGER NOT NULL,"
" b INTEGER NOT NULL," " b INTEGER NOT NULL,"
" a INTEGER NOT NULL" " a INTEGER NOT NULL,"
")")); " last_accessed TEXT"
q.exec(QStringLiteral( ")"_s);
"CREATE TABLE IF NOT EXISTS image_cache (" q.exec(
" key TEXT PRIMARY KEY NOT NULL," u"CREATE TABLE IF NOT EXISTS image_cache ("
" file_name TEXT NOT NULL" " key TEXT PRIMARY KEY NOT NULL,"
")")); " file_name TEXT NOT NULL,"
" last_accessed TEXT"
")"_s);
q.exec(
u"CREATE TABLE IF NOT EXISTS settings_cache ("
" key TEXT PRIMARY KEY NOT NULL,"
" value TEXT NOT NULL"
");"_s);
// Migrate existing databases that predate the last_accessed column.
q.exec(u"ALTER TABLE color_cache ADD COLUMN last_accessed TEXT"_s);
q.exec(u"ALTER TABLE image_cache ADD COLUMN last_accessed TEXT"_s);
}
void Manager::_runCleanup() {
WR_DEBUG(u"Cache cleanup started (maxEntries=%1)"_s.arg(m_maxEntries));
QSqlDatabase db = _db();
if (!db.isOpen())
return;
// Evict image_cache rows whose backing file no longer exists
{
QSqlQuery sel(db);
if (sel.exec(u"SELECT key, file_name FROM image_cache"_s)) {
struct Stale {
QString key, fileName;
};
QList<Stale> stale;
while (sel.next()) {
const QString k = sel.value(0).toString();
const QString file = sel.value(1).toString();
if (!QFileInfo::exists(m_cacheDir.filePath(file)))
stale.push_back({k, file});
}
int evicted = 0;
for (const auto& s : std::as_const(stale)) {
{
QMutexLocker lk(&m_hotKeysMutex);
if (m_hotImageKeys.contains(s.key))
continue;
}
QSqlQuery del(db);
del.prepare(u"DELETE FROM image_cache WHERE key = :key"_s);
del.bindValue(u":key"_s, s.key);
if (del.exec())
++evicted;
}
if (evicted)
WR_INFO(u"Cleanup evicted %1 stale image cache row(s)"_s.arg(evicted));
}
}
// Trim image_cache to m_maxEntries (oldest last_accessed first)
{
QSqlQuery countQ(db);
if (countQ.exec(u"SELECT COUNT(*) FROM image_cache"_s) && countQ.next()) {
int excess = countQ.value(0).toInt() - m_maxEntries;
if (excess > 0) {
QSqlQuery sel(db);
sel.exec(u"SELECT key, file_name FROM image_cache ORDER BY last_accessed ASC"_s);
QList<QPair<QString, QString>> toDelete;
while (sel.next() && excess > 0) {
const QString k = sel.value(0).toString();
QMutexLocker lk(&m_hotKeysMutex);
if (!m_hotImageKeys.contains(k)) {
toDelete.push_back({k, sel.value(1).toString()});
--excess;
}
}
int removed = 0;
for (const auto& [k, fileName] : std::as_const(toDelete)) {
{
QMutexLocker lk(&m_hotKeysMutex);
if (m_hotImageKeys.contains(k))
continue;
}
QFile::remove(m_cacheDir.filePath(fileName));
QSqlQuery del(db);
del.prepare(u"DELETE FROM image_cache WHERE key = :key"_s);
del.bindValue(u":key"_s, k);
if (del.exec())
++removed;
}
if (removed)
WR_INFO(u"Cleanup trimmed %1 image cache entry(ies)"_s.arg(removed));
}
}
}
// Trim color_cache to m_maxEntries (oldest last_accessed first)
{
QSqlQuery countQ(db);
if (countQ.exec(u"SELECT COUNT(*) FROM color_cache"_s) && countQ.next()) {
int excess = countQ.value(0).toInt() - m_maxEntries;
if (excess > 0) {
QSqlQuery sel(db);
sel.exec(u"SELECT key FROM color_cache ORDER BY last_accessed ASC"_s);
QStringList toDelete;
while (sel.next() && excess > 0) {
const QString k = sel.value(0).toString();
QMutexLocker lk(&m_hotKeysMutex);
if (!m_hotColorKeys.contains(k)) {
toDelete << k;
--excess;
}
}
int removed = 0;
for (const QString& k : std::as_const(toDelete)) {
{
QMutexLocker lk(&m_hotKeysMutex);
if (m_hotColorKeys.contains(k))
continue;
}
QSqlQuery del(db);
del.prepare(u"DELETE FROM color_cache WHERE key = :key"_s);
del.bindValue(u":key"_s, k);
if (del.exec())
++removed;
}
if (removed)
WR_INFO(u"Cleanup trimmed %1 color cache entry(ies)"_s.arg(removed));
}
}
}
WR_DEBUG(u"Cache cleanup complete"_s);
} }
} // namespace WallReel::Core::Cache } // namespace WallReel::Core::Cache
+18 -3
View File
@@ -3,6 +3,7 @@
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QFuture>
#include <QMutex> #include <QMutex>
#include <QSet> #include <QSet>
#include <QtSql> #include <QtSql>
@@ -15,26 +16,40 @@ class Manager {
public: public:
static QString cacheKey(const QFileInfo& fileInfo, const QSize& imageSize); static QString cacheKey(const QFileInfo& fileInfo, const QSize& imageSize);
Manager(const QDir& cacheDir); Manager(const QDir& cacheDir, int maxEntries = 1000);
~Manager(); ~Manager();
void evictOldEntries();
void clearCache(Type type = Type::Image | Type::Color); void clearCache(Type type = Type::Image | Type::Color);
QColor getColor(const QString& key, const std::function<QColor()>& computeFunc); QColor getColor(const QString& key, const std::function<QColor()>& computeFunc = nullptr);
QFileInfo getImage(const QString& key, const std::function<QImage()>& computeFunc); QFileInfo getImage(const QString& key, const std::function<QImage()>& computeFunc = nullptr);
QString getSetting(SettingsType key, const std::function<QString()>& computeFunc = nullptr);
void storeSetting(SettingsType key, const QString& value);
private: private:
QDir m_cacheDir; QDir m_cacheDir;
int m_maxEntries;
QString m_dbPath; QString m_dbPath;
QString m_connectionPrefix; QString m_connectionPrefix;
mutable QMutex m_connectionsMutex; mutable QMutex m_connectionsMutex;
mutable QSet<QString> m_connectionNames; mutable QSet<QString> m_connectionNames;
mutable QMutex m_hotKeysMutex;
mutable QSet<QString> m_hotColorKeys;
mutable QSet<QString> m_hotImageKeys;
QFuture<void> m_cleanupFuture;
QSqlDatabase _db() const; QSqlDatabase _db() const;
void _setupTables(QSqlDatabase& db) const; void _setupTables(QSqlDatabase& db) const;
void _runCleanup();
}; };
} // namespace WallReel::Core::Cache } // namespace WallReel::Core::Cache
+11 -4
View File
@@ -10,10 +10,11 @@
namespace WallReel::Core::Cache { namespace WallReel::Core::Cache {
enum class Type : uint32_t { enum class Type : uint32_t {
None = 0, None = 0,
Image = 1, ///< Cache for processed images Image = 1, ///< Cache for processed images
Color = 1 << 1, ///< Cache for palette color matching results Color = 1 << 1, ///< Cache for dominant colors
All = ~0u Settings = 1 << 2, ///< Cache for settings (simple key-value pairs)
All = ~0u
}; };
inline constexpr Type operator|(Type a, Type b) { inline constexpr Type operator|(Type a, Type b) {
@@ -28,6 +29,12 @@ inline constexpr Type operator&(Type a, Type b) {
using Data = std::variant<std::monostate, QFileInfo, QColor>; using Data = std::variant<std::monostate, QFileInfo, QColor>;
enum class SettingsType : uint32_t {
LastSelectedPalette = 0,
LastSortType,
LastSortDescending,
};
} // namespace WallReel::Core::Cache } // namespace WallReel::Core::Cache
#endif // WALLREEL_CACHE_TYPES_HPP #endif // WALLREEL_CACHE_TYPES_HPP
+16 -17
View File
@@ -19,7 +19,6 @@
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively. // wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
// wallpaper.excludes array [] Exclude patterns (regex) // wallpaper.excludes array [] Exclude patterns (regex)
// //
// theme.defaultPalette string "" Name of the default palette to use
// theme.palettes array [] // theme.palettes array []
// theme.palettes[].name string "" Name of the palette // theme.palettes[].name string "" Name of the palette
// theme.palettes[].colors array [] List of colors in the palette // theme.palettes[].colors array [] List of colors in the palette
@@ -28,16 +27,15 @@
// //
// action.previewDebounceTime number 300 Debounce time for preview action in milliseconds // action.previewDebounceTime number 300 Debounce time for preview action in milliseconds
// action.printSelected boolean true Whether to print the selected wallpaper path to stdout on confirm // action.printSelected boolean true Whether to print the selected wallpaper path to stdout on confirm
// action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview
// action.onSelected string "" Command to execute on confirmation // action.onSelected string "" Command to execute on confirmation
// action.onPreview string "" Command to execute on preview // action.onPreview string "" Command to execute on preview
// action.saveState array [] Useful for restore command // action.saveState array [] Useful for restore command
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command // action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command
// action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty // action.saveState[].fallback string "" Value to save, used when "command" is not set or command execution fails or output is empty
// action.saveState[].command string "" Command that outputs(to stdout) the value to save when executed // action.saveState[].command string "" Command that outputs(to stdout) the value to save when executed
// action.saveState[].timeout number 3000 Timeout for executing "cmd" in milliseconds. 0 or negative means no timeout // action.saveState[].timeout number 3000 Timeout for executing "command" in milliseconds. 0 or negative means no timeout
// action.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState) // action.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState)
// action.quitOnSelected boolean false Whether to quit the application after confirming a wallpaper // action.quitOnSelected boolean true Whether to quit the application after confirming a wallpaper
// action.restoreOnClose boolean true Whether to run the restore command after closing the application without confirming a wallpaper // action.restoreOnClose boolean true Whether to run the restore command after closing the application without confirming a wallpaper
// //
// style.image_width number 320 Width of each image // style.image_width number 320 Width of each image
@@ -46,11 +44,9 @@
// style.window_width number 750 Initial window width // style.window_width number 750 Initial window width
// style.window_height number 500 Initial window height // style.window_height number 500 Initial window height
// //
// sort.type string "date" Initial sorting type: "name", "date", "size" // cache.saveSortMethod boolean true Whether to persist the sort type and order
// sort.descending boolean true Initial sorting order // cache.savePalette bool true Whether to persist the selected palette
// Ascending: name: lexicographical, e.g. "a.jpg" before "b.jpg" // cache.maxImageEntries number 1000 Maximum number of entries in the image cache (older entries will be evicted)
// date: older before newer
// size: smaller before larger
namespace WallReel::Core::Config { namespace WallReel::Core::Config {
@@ -64,7 +60,7 @@ enum class SortType : int {
inline const QStringList s_availableSortTypes = {"Name", "Date", "Size"}; inline const QStringList s_availableSortTypes = {"Name", "Date", "Size"};
inline QString sortTypeToString(SortType type) { inline QString sortTypeToString(const SortType& type) {
switch (type) { switch (type) {
case SortType::Name: case SortType::Name:
return "Name"; return "Name";
@@ -112,7 +108,6 @@ struct ThemeConfigItems {
}; };
QList<PaletteConfigItem> palettes; QList<PaletteConfigItem> palettes;
QString defaultPalette;
}; };
struct ActionConfigItems { struct ActionConfigItems {
@@ -130,8 +125,7 @@ struct ActionConfigItems {
QString onRestore; QString onRestore;
int previewDebounceTime = 300; // milliseconds int previewDebounceTime = 300; // milliseconds
bool printSelected = true; bool printSelected = true;
bool printPreview = false; bool quitOnSelected = true;
bool quitOnSelected = false;
bool restoreOnClose = true; bool restoreOnClose = true;
}; };
@@ -143,9 +137,14 @@ struct StyleConfigItems {
int windowHeight = 500; int windowHeight = 500;
}; };
struct SortConfigItems { struct CacheConfigItems {
SortType type = SortType::Date; bool saveSortMethod = true;
bool descending = true; bool savePalette = true;
int maxImageEntries = 1000;
static const QString defaultSortType;
static const QString defaultSortDescending;
static const QString defaultSelectedPalette;
}; };
} // namespace WallReel::Core::Config } // namespace WallReel::Core::Config
+56 -42
View File
@@ -15,13 +15,25 @@
WALLREEL_DECLARE_SENDER("ConfigManager") WALLREEL_DECLARE_SENDER("ConfigManager")
WallReel::Core::Config::Manager::Manager( namespace WallReel::Core::Config {
const QString CacheConfigItems::defaultSortType = "Date";
const QString CacheConfigItems::defaultSortDescending = "true";
const QString CacheConfigItems::defaultSelectedPalette = "";
Manager::Manager(
const QDir& configDir, const QDir& configDir,
const QDir& picturesDir, const QDir& picturesDir,
const QStringList& searchDirs, const QStringList& searchDirs,
const QString& configPath, const QString& configPath,
bool disableActions,
QObject* parent) QObject* parent)
: QObject(parent), m_configDir(configDir) { : QObject(parent), m_configDir(configDir), m_disableActions(disableActions) {
connect(this, &Manager::stateCaptured, this, [this]() {
m_stateCaptured = true;
WR_INFO("State capture completed");
});
// Load configPath if not empty, otherwise load from default location (configDir + s_DefaultConfigFileName) // Load configPath if not empty, otherwise load from default location (configDir + s_DefaultConfigFileName)
if (configPath.isEmpty()) { if (configPath.isEmpty()) {
WR_INFO(QString("Configuration directory: %1").arg(m_configDir.absolutePath())); WR_INFO(QString("Configuration directory: %1").arg(m_configDir.absolutePath()));
@@ -47,10 +59,10 @@ WallReel::Core::Config::Manager::Manager(
_loadWallpapers(); _loadWallpapers();
} }
WallReel::Core::Config::Manager::~Manager() { Manager::~Manager() {
} }
void WallReel::Core::Config::Manager::_loadConfig(const QString& configPath) { void Manager::_loadConfig(const QString& configPath) {
WR_INFO(QString("Loading configuration from: %1").arg(configPath)); WR_INFO(QString("Loading configuration from: %1").arg(configPath));
QFile configFile(configPath); QFile configFile(configPath);
if (!configFile.open(QIODevice::ReadOnly)) { if (!configFile.open(QIODevice::ReadOnly)) {
@@ -72,10 +84,10 @@ void WallReel::Core::Config::Manager::_loadConfig(const QString& configPath) {
_loadThemeConfig(jsonObj); _loadThemeConfig(jsonObj);
_loadActionConfig(jsonObj); _loadActionConfig(jsonObj);
_loadStyleConfig(jsonObj); _loadStyleConfig(jsonObj);
_loadSortConfig(jsonObj); _loadCacheConfig(jsonObj);
} }
void WallReel::Core::Config::Manager::_loadWallpaperConfig(const QJsonObject& root) { void Manager::_loadWallpaperConfig(const QJsonObject& root) {
if (!root.contains("wallpaper") || !root["wallpaper"].isObject()) { if (!root.contains("wallpaper") || !root["wallpaper"].isObject()) {
return; return;
} }
@@ -121,14 +133,11 @@ void WallReel::Core::Config::Manager::_loadWallpaperConfig(const QJsonObject& ro
} }
} }
void WallReel::Core::Config::Manager::_loadThemeConfig(const QJsonObject& root) { void Manager::_loadThemeConfig(const QJsonObject& root) {
if (!root.contains("theme") || !root["theme"].isObject()) { if (!root.contains("theme") || !root["theme"].isObject()) {
return; return;
} }
const QJsonObject& theme = root["theme"].toObject(); const QJsonObject& theme = root["theme"].toObject();
if (theme.contains("defaultPalette") && theme["defaultPalette"].isString()) {
m_themeConfig.defaultPalette = theme["defaultPalette"].toString();
}
if (!theme.contains("palettes") || !theme["palettes"].isArray()) { if (!theme.contains("palettes") || !theme["palettes"].isArray()) {
return; return;
@@ -176,7 +185,7 @@ void WallReel::Core::Config::Manager::_loadThemeConfig(const QJsonObject& root)
} }
} }
void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root) { void Manager::_loadActionConfig(const QJsonObject& root) {
if (!root.contains("action") || !root["action"].isObject()) { if (!root.contains("action") || !root["action"].isObject()) {
return; return;
} }
@@ -194,12 +203,6 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
m_actionConfig.printSelected = val.toBool(); m_actionConfig.printSelected = val.toBool();
} }
} }
if (config.contains("printPreview")) {
const auto& val = config["printPreview"];
if (val.isBool()) {
m_actionConfig.printPreview = val.toBool();
}
}
if (config.contains("saveState") && config["saveState"].isArray()) { if (config.contains("saveState") && config["saveState"].isArray()) {
const QJsonArray& arr = config["saveState"].toArray(); const QJsonArray& arr = config["saveState"].toArray();
for (const auto& item : arr) { for (const auto& item : arr) {
@@ -209,8 +212,8 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
if (obj.contains("key") && obj["key"].isString()) { if (obj.contains("key") && obj["key"].isString()) {
sItem.key = obj["key"].toString(); sItem.key = obj["key"].toString();
} }
if (obj.contains("default") && obj["default"].isString()) { if (obj.contains("fallback") && obj["fallback"].isString()) {
sItem.defaultVal = obj["default"].toString(); sItem.defaultVal = obj["fallback"].toString();
} }
if (obj.contains("command") && obj["command"].isString()) { if (obj.contains("command") && obj["command"].isString()) {
sItem.command = obj["command"].toString(); sItem.command = obj["command"].toString();
@@ -257,7 +260,7 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
} }
} }
void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root) { void Manager::_loadStyleConfig(const QJsonObject& root) {
if (!root.contains("style") || !root["style"].isObject()) { if (!root.contains("style") || !root["style"].isObject()) {
return; return;
} }
@@ -295,36 +298,33 @@ void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root)
} }
} }
void WallReel::Core::Config::Manager::_loadSortConfig(const QJsonObject& root) { void Manager::_loadCacheConfig(const QJsonObject& root) {
if (!root.contains("sort") || !root["sort"].isObject()) { if (!root.contains("cache") || !root["cache"].isObject()) {
return; return;
} }
const QJsonObject& config = root["sort"].toObject(); const QJsonObject& config = root["cache"].toObject();
if (config.contains("type")) { if (config.contains("saveSortMethod")) {
const auto& val = config["type"]; const auto& val = config["saveSortMethod"];
if (val.isString()) { if (val.isBool()) {
QString type = val.toString().toLower(); m_cacheConfig.saveSortMethod = val.toBool();
if (type == "name") {
m_sortConfig.type = SortType::Name;
} else if (type == "date") {
m_sortConfig.type = SortType::Date;
} else if (type == "size") {
m_sortConfig.type = SortType::Size;
} else {
WR_WARN(QString("Unknown sort type: %1").arg(type));
}
} }
} }
if (config.contains("descending")) { if (config.contains("savePalette")) {
const auto& val = config["descending"]; const auto& val = config["savePalette"];
if (val.isBool()) { if (val.isBool()) {
m_sortConfig.descending = val.toBool(); m_cacheConfig.savePalette = val.toBool();
}
}
if (config.contains("maxImageEntries")) {
const auto& val = config["maxImageEntries"];
if (val.isDouble() && val.toDouble() > 0) {
m_cacheConfig.maxImageEntries = val.toInt();
} }
} }
} }
void WallReel::Core::Config::Manager::_loadWallpapers() { void Manager::_loadWallpapers() {
m_wallpapers.clear(); m_wallpapers.clear();
// Add paths first using a set to avoid duplicates // Add paths first using a set to avoid duplicates
@@ -389,7 +389,19 @@ void WallReel::Core::Config::Manager::_loadWallpapers() {
WR_INFO(QString("Found %1 images").arg(paths.size())); WR_INFO(QString("Found %1 images").arg(paths.size()));
} }
void WallReel::Core::Config::Manager::captureState() { void Manager::captureState() {
if (m_stateCaptured) {
WR_DEBUG("State already captured, skipping capture");
emit stateCaptured();
return;
}
if (m_disableActions) {
WR_DEBUG("Actions are disabled, skipping state capture");
emit stateCaptured();
return;
}
if (m_pendingCaptures > 0) { if (m_pendingCaptures > 0) {
WR_WARN("State capture already in progress, ignoring new capture request"); WR_WARN("State capture already in progress, ignoring new capture request");
return; return;
@@ -481,7 +493,7 @@ void WallReel::Core::Config::Manager::captureState() {
} }
} }
void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const QString& value) { void Manager::_onCaptureResult(const QString& key, const QString& value) {
// This is all in main thread, so no lock needed // This is all in main thread, so no lock needed
m_actionConfig.savedState[key] = value; m_actionConfig.savedState[key] = value;
m_pendingCaptures--; m_pendingCaptures--;
@@ -489,3 +501,5 @@ void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const
emit stateCaptured(); emit stateCaptured();
} }
} }
} // namespace WallReel::Core::Config
+10 -3
View File
@@ -27,6 +27,7 @@ class Manager : public QObject {
* @param searchDirs Additional directories to search for wallpapers (not recursive) * @param searchDirs Additional directories to search for wallpapers (not recursive)
* @param configPath Optional path to a specific configuration file (overrides the default config path) * @param configPath Optional path to a specific configuration file (overrides the default config path)
* @param picturesDir The pictures directory (default location for user wallpapers) * @param picturesDir The pictures directory (default location for user wallpapers)
* @param disableActions Whether to disable actions
* @param parent QObject parent * @param parent QObject parent
* *
* @note The constructor will load the configuration and scan for wallpapers immediately. * @note The constructor will load the configuration and scan for wallpapers immediately.
@@ -36,6 +37,7 @@ class Manager : public QObject {
const QDir& picturesDir, const QDir& picturesDir,
const QStringList& searchDirs = {}, const QStringList& searchDirs = {},
const QString& configPath = "", const QString& configPath = "",
bool disableActions = false,
QObject* parent = nullptr); QObject* parent = nullptr);
~Manager(); ~Manager();
@@ -57,7 +59,9 @@ class Manager : public QObject {
const StyleConfigItems& getStyleConfig() const { return m_styleConfig; } const StyleConfigItems& getStyleConfig() const { return m_styleConfig; }
const SortConfigItems& getSortConfig() const { return m_sortConfig; } const CacheConfigItems& getCacheConfig() const { return m_cacheConfig; }
bool isStateCaptured() const { return m_stateCaptured; }
QSize getFocusImageSize() const { QSize getFocusImageSize() const {
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale; return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
@@ -78,7 +82,7 @@ class Manager : public QObject {
void _loadThemeConfig(const QJsonObject& config); void _loadThemeConfig(const QJsonObject& config);
void _loadActionConfig(const QJsonObject& config); void _loadActionConfig(const QJsonObject& config);
void _loadStyleConfig(const QJsonObject& config); void _loadStyleConfig(const QJsonObject& config);
void _loadSortConfig(const QJsonObject& config); void _loadCacheConfig(const QJsonObject& config);
// Load wallpapers // Load wallpapers
void _loadWallpapers(); void _loadWallpapers();
// Callback for state capture results // Callback for state capture results
@@ -86,15 +90,18 @@ class Manager : public QObject {
private: private:
const QDir m_configDir; const QDir m_configDir;
bool m_disableActions = false;
WallpaperConfigItems m_wallpaperConfig; WallpaperConfigItems m_wallpaperConfig;
ThemeConfigItems m_themeConfig; ThemeConfigItems m_themeConfig;
ActionConfigItems m_actionConfig; ActionConfigItems m_actionConfig;
StyleConfigItems m_styleConfig; StyleConfigItems m_styleConfig;
SortConfigItems m_sortConfig; CacheConfigItems m_cacheConfig;
QStringList m_wallpapers; QStringList m_wallpapers;
int m_pendingCaptures = 0; int m_pendingCaptures = 0;
bool m_stateCaptured = false; // changed and accessed in main thread, no lock needed
}; };
} // namespace WallReel::Core::Config } // namespace WallReel::Core::Config
+4 -2
View File
@@ -24,11 +24,13 @@ WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize,
: m_cacheMgr(cacheMgr), m_file(path), m_targetSize(targetSize) { : m_cacheMgr(cacheMgr), m_file(path), m_targetSize(targetSize) {
m_id = cacheMgr.cacheKey(m_file, m_targetSize); m_id = cacheMgr.cacheKey(m_file, m_targetSize);
m_cachedFile = cacheMgr.getImage(m_id, [this]() { return computeImage(); }); m_cachedFile = cacheMgr.getImage(m_id, [this]() { return computeImage(); });
m_dominantColor = cacheMgr.getColor(m_id, [this]() { return computeDominantColor(loadImage()); }); m_dominantColor = cacheMgr.getColor(m_id, [this]() { return computeDominantColor(loadImageFromCache()); });
m_isValid = m_cachedFile.isFile() && m_dominantColor.isValid();
} }
QImage WallReel::Core::Image::Data::loadImage() const { QImage WallReel::Core::Image::Data::loadImageFromCache() const {
QImageReader reader(m_cachedFile.absoluteFilePath()); QImageReader reader(m_cachedFile.absoluteFilePath());
if (!reader.canRead()) { if (!reader.canRead()) {
WR_WARN("Cannot read cached image: " + m_cachedFile.absoluteFilePath()); WR_WARN("Cannot read cached image: " + m_cachedFile.absoluteFilePath());
return QImage(); return QImage();
+4 -3
View File
@@ -54,8 +54,11 @@ class Data {
QColor m_dominantColor; ///< Dominant color of the image, used for palette matching QColor m_dominantColor; ///< Dominant color of the image, used for palette matching
QHash<QString, QString> m_colorCache; ///< Cache for palette color matching results, key is palette name, value is matched color name QHash<QString, QString> m_colorCache; ///< Cache for palette color matching results, key is palette name, value is matched color name
bool m_isValid = false;
QImage computeImage() const; QImage computeImage() const;
QColor computeDominantColor(const QImage& image) const; QColor computeDominantColor(const QImage& image) const;
QImage loadImageFromCache() const;
Data(const QString& path, const QSize& size, Cache::Manager& cacheMgr); Data(const QString& path, const QSize& size, Cache::Manager& cacheMgr);
@@ -75,7 +78,7 @@ class Data {
QUrl getUrl() const { return QUrl::fromLocalFile(m_cachedFile.absoluteFilePath()); } QUrl getUrl() const { return QUrl::fromLocalFile(m_cachedFile.absoluteFilePath()); }
bool isValid() const { return m_cachedFile.exists(); } bool isValid() const { return m_isValid; }
QString getFullPath() const { return m_file.absoluteFilePath(); } QString getFullPath() const { return m_file.absoluteFilePath(); }
@@ -87,8 +90,6 @@ class Data {
const QFileInfo& getFileInfo() const { return m_file; } const QFileInfo& getFileInfo() const { return m_file; }
QImage loadImage() const;
const QColor& getDominantColor() const { return m_dominantColor; } const QColor& getDominantColor() const { return m_dominantColor; }
std::optional<QString> getCachedColor(const QString& paletteName) const { std::optional<QString> getCachedColor(const QString& paletteName) const {
-13
View File
@@ -49,19 +49,6 @@ WallReel::Core::Palette::Manager::Manager(
m_palettes.append(newP); m_palettes.append(newP);
} }
} }
// Set default palette if specified
if (!config.defaultPalette.isEmpty()) {
for (const auto& p : m_palettes) {
if (p.name == config.defaultPalette) {
m_selectedColor = std::nullopt;
m_selectedPalette = p;
emit selectedColorChanged();
emit selectedPaletteChanged();
break;
}
}
}
} }
void WallReel::Core::Palette::Manager::updateColor(const QString& imageId) { void WallReel::Core::Palette::Manager::updateColor(const QString& imageId) {
+16 -1
View File
@@ -42,8 +42,23 @@ class Manager : public QObject {
void setSelectedPalette(const QVariant& paletteVar) { void setSelectedPalette(const QVariant& paletteVar) {
if (paletteVar.isNull() || !paletteVar.isValid()) { if (paletteVar.isNull() || !paletteVar.isValid()) {
m_selectedPalette = std::nullopt; m_selectedPalette = std::nullopt;
} else { } else if (paletteVar.canConvert<PaletteItem>()) {
m_selectedPalette = paletteVar.value<PaletteItem>(); m_selectedPalette = paletteVar.value<PaletteItem>();
} else if (paletteVar.canConvert<QString>()) {
QString paletteName = paletteVar.toString();
auto it =
std::find_if(m_palettes.begin(),
m_palettes.end(),
[&paletteName](const PaletteItem& item) {
return item.name == paletteName;
});
if (it != m_palettes.end()) {
m_selectedPalette = *it;
} else {
m_selectedPalette = std::nullopt;
}
} else {
m_selectedPalette = std::nullopt;
} }
m_selectedColor = std::nullopt; m_selectedColor = std::nullopt;
emit selectedPaletteChanged(); emit selectedPaletteChanged();
+1 -1
View File
@@ -10,7 +10,7 @@ WALLREEL_DECLARE_SENDER("PaletteMatchColor")
namespace WallReel::Core::Palette { namespace WallReel::Core::Palette {
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates) { ColorItem bestMatch(const QColor& target, const QList<ColorItem>& candidates) {
if (candidates.isEmpty() || !target.isValid()) { if (candidates.isEmpty() || !target.isValid()) {
WR_WARN("No candidates or invalid target color for palette matching"); WR_WARN("No candidates or invalid target color for palette matching");
static ColorItem emptyItem; static ColorItem emptyItem;
+2 -2
View File
@@ -10,9 +10,9 @@ namespace WallReel::Core::Palette {
* *
* @param target * @param target
* @param candidates * @param candidates
* @return const ColorItem& The best matching color item, or an empty ColorItem if no candidates are provided * @return ColorItem The best matching color item, or an empty ColorItem if no candidates are provided
*/ */
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates); ColorItem bestMatch(const QColor& target, const QList<ColorItem>& candidates);
} // namespace WallReel::Core::Palette } // namespace WallReel::Core::Palette
+80 -11
View File
@@ -10,6 +10,7 @@
#include "Service/manager.hpp" #include "Service/manager.hpp"
#include "Utils/misc.hpp" #include "Utils/misc.hpp"
#include "appoptions.hpp" #include "appoptions.hpp"
#include "logger.hpp"
namespace WallReel::Core::Provider { namespace WallReel::Core::Provider {
@@ -17,18 +18,22 @@ class Bootstrap {
friend class Carousel; friend class Carousel;
public: public:
Bootstrap(const AppOptions& options) { Bootstrap(const AppOptions& options) : options(options) {
cacheMgr = new Cache::Manager(Utils::getCacheDir()); configMgr = new Config::Manager(
Utils::getConfigDir(),
Utils::getPicturesDir(),
options.appendDirs,
options.configPath,
options.disableActions);
cacheMgr = new Cache::Manager(
Utils::getCacheDir(),
configMgr->getCacheConfig().maxImageEntries);
if (options.clearCache) { if (options.clearCache) {
cacheMgr->clearCache(); cacheMgr->clearCache();
return; return;
} }
configMgr = new Config::Manager(
Utils::getConfigDir(),
Utils::getPicturesDir(),
options.appendDirs,
options.configPath);
imageMgr = new Image::Manager( imageMgr = new Image::Manager(
*cacheMgr, *cacheMgr,
@@ -40,19 +45,82 @@ class Bootstrap {
qRegisterMetaType<Palette::PaletteItem>("PaletteItem"); qRegisterMetaType<Palette::PaletteItem>("PaletteItem");
qRegisterMetaType<Palette::ColorItem>("ColorItem"); qRegisterMetaType<Palette::ColorItem>("ColorItem");
ServiceMgr = new Service::Manager( serviceMgr = new Service::Manager(
configMgr->getActionConfig(), configMgr->getActionConfig(),
*imageMgr, *imageMgr,
*paletteMgr); *paletteMgr,
options.disableActions);
} }
void start() { void start() {
cacheMgr->evictOldEntries();
configMgr->captureState(); configMgr->captureState();
imageMgr->loadAndProcess(configMgr->getWallpapers()); imageMgr->loadAndProcess(configMgr->getWallpapers());
} }
bool apply(const QString& path) {
if (options.disableActions) {
Logger::warn("Bootstrap", "Actions are disabled, cannot apply wallpaper");
return false;
}
QEventLoop loop;
bool successFlag = false;
paletteMgr->setSelectedPalette(cacheMgr->getSetting(
Cache::SettingsType::LastSelectedPalette,
[]() { return Config::CacheConfigItems::defaultSelectedPalette; }));
QObject::connect(
configMgr,
&Config::Manager::stateCaptured,
&loop,
[&]() {
loop.quit();
},
Qt::SingleShotConnection);
configMgr->captureState();
loop.exec();
QMetaObject::Connection connection;
connection = QObject::connect(
imageMgr,
&Image::Manager::isLoadingChanged,
&loop,
[&]() {
if (!imageMgr->isLoading()) {
QObject::disconnect(connection);
QVariant idVar = imageMgr->model()->data(
imageMgr->model()->index(0, 0),
Image::Model::IdRole);
if (idVar.isValid()) {
auto id = idVar.toString();
paletteMgr->updateColor(id);
QObject::connect(
serviceMgr,
&Service::Manager::selectCompleted,
&loop,
[&](bool success) {
successFlag = success;
loop.quit();
},
Qt::SingleShotConnection);
serviceMgr->selectWallpaper(id);
} else {
Logger::critical("Bootstrap", "No images loaded, cannot apply wallpaper");
loop.quit();
}
}
});
imageMgr->loadAndProcess({Utils::expandPath(path)});
loop.exec();
return successFlag;
}
~Bootstrap() { ~Bootstrap() {
delete ServiceMgr; delete serviceMgr;
delete paletteMgr; delete paletteMgr;
delete imageMgr; delete imageMgr;
delete configMgr; delete configMgr;
@@ -60,11 +128,12 @@ class Bootstrap {
} }
private: private:
const AppOptions& options;
Cache::Manager* cacheMgr{}; Cache::Manager* cacheMgr{};
Config::Manager* configMgr{}; Config::Manager* configMgr{};
Image::Manager* imageMgr{}; Image::Manager* imageMgr{};
Palette::Manager* paletteMgr{}; Palette::Manager* paletteMgr{};
Service::Manager* ServiceMgr{}; Service::Manager* serviceMgr{};
}; };
} // namespace WallReel::Core::Provider } // namespace WallReel::Core::Provider
+46 -18
View File
@@ -1,10 +1,10 @@
#ifndef WALLREEL_PROVIDER_CAROUSEL_HPP #ifndef WALLREEL_PROVIDER_CAROUSEL_HPP
#define WALLREEL_PROVIDER_CAROUSEL_HPP #define WALLREEL_PROVIDER_CAROUSEL_HPP
#include <qapplication.h>
#include <QApplication> #include <QApplication>
#include "Cache/manager.hpp"
#include "Cache/types.hpp"
#include "Config/data.hpp" #include "Config/data.hpp"
#include "Config/manager.hpp" #include "Config/manager.hpp"
#include "Image/manager.hpp" #include "Image/manager.hpp"
@@ -146,10 +146,6 @@ class Carousel : public QObject {
signals: signals:
void isProcessingChanged(); void isProcessingChanged();
void selectCompleted();
void previewCompleted();
void restoreCompleted();
void cancelCompleted();
// Other states // Other states
@@ -190,10 +186,11 @@ class Carousel : public QObject {
Bootstrap& bootstrap, Bootstrap& bootstrap,
QObject* parent = nullptr) QObject* parent = nullptr)
: QObject(parent), : QObject(parent),
m_cacheMgr(bootstrap.cacheMgr),
m_configMgr(bootstrap.configMgr), m_configMgr(bootstrap.configMgr),
m_imageMgr(bootstrap.imageMgr), m_imageMgr(bootstrap.imageMgr),
m_paletteMgr(bootstrap.paletteMgr), m_paletteMgr(bootstrap.paletteMgr),
m_serviceMgr(bootstrap.ServiceMgr) { m_serviceMgr(bootstrap.serviceMgr) {
// Simply forward signals // Simply forward signals
connect(m_imageMgr, &Image::Manager::isLoadingChanged, this, &Carousel::isLoadingChanged); connect(m_imageMgr, &Image::Manager::isLoadingChanged, this, &Carousel::isLoadingChanged);
connect(m_imageMgr, &Image::Manager::processedCountChanged, this, &Carousel::processedCountChanged); connect(m_imageMgr, &Image::Manager::processedCountChanged, this, &Carousel::processedCountChanged);
@@ -203,10 +200,6 @@ class Carousel : public QObject {
connect(m_paletteMgr, &Palette::Manager::colorChanged, this, &Carousel::colorChanged); connect(m_paletteMgr, &Palette::Manager::colorChanged, this, &Carousel::colorChanged);
connect(m_paletteMgr, &Palette::Manager::colorNameChanged, this, &Carousel::colorNameChanged); connect(m_paletteMgr, &Palette::Manager::colorNameChanged, this, &Carousel::colorNameChanged);
connect(m_serviceMgr, &Service::Manager::isProcessingChanged, this, &Carousel::isProcessingChanged); connect(m_serviceMgr, &Service::Manager::isProcessingChanged, this, &Carousel::isProcessingChanged);
connect(m_serviceMgr, &Service::Manager::selectCompleted, this, &Carousel::selectCompleted);
connect(m_serviceMgr, &Service::Manager::previewCompleted, this, &Carousel::previewCompleted);
connect(m_serviceMgr, &Service::Manager::restoreCompleted, this, &Carousel::restoreCompleted);
connect(m_serviceMgr, &Service::Manager::cancelCompleted, this, &Carousel::cancelCompleted);
// "Preview" is costly, but is (usually) protected by a debounce timer, so it seems fine // "Preview" is costly, but is (usually) protected by a debounce timer, so it seems fine
// to call it multiple times in a short period, and it simplifies the code a lot :) // to call it multiple times in a short period, and it simplifies the code a lot :)
@@ -248,19 +241,25 @@ class Carousel : public QObject {
} }
}); });
// Defer preview until state captured
connect(m_configMgr,
&Config::Manager::stateCaptured,
m_serviceMgr,
&Service::Manager::onStateCaptured);
// Quit on selected // Quit on selected
if (m_configMgr->getActionConfig().quitOnSelected) { if (m_configMgr->getActionConfig().quitOnSelected) {
QObject::connect( QObject::connect(
this, m_serviceMgr,
&Provider::Carousel::selectCompleted, &Service::Manager::selectCompleted,
app, app,
&QApplication::quit, &QApplication::quit,
Qt::QueuedConnection); Qt::QueuedConnection);
} }
// Quit on cancel // Quit on cancel
QObject::connect( QObject::connect(
this, m_serviceMgr,
&Provider::Carousel::cancelCompleted, &Service::Manager::cancelCompleted,
app, app,
&QApplication::quit, &QApplication::quit,
Qt::QueuedConnection); Qt::QueuedConnection);
@@ -273,12 +272,41 @@ class Carousel : public QObject {
&Service::Manager::restoreOnQuit); &Service::Manager::restoreOnQuit);
} }
// Initial value of sort method // Restore last state if configured
setSortType(m_configMgr->getSortConfig().type); // and store state on change if configured
setSortDescending(m_configMgr->getSortConfig().descending); // Note: connect after restoring state to avoid storing the restored state again
if (m_configMgr->getCacheConfig().saveSortMethod) {
setSortType(m_cacheMgr->getSetting(
Cache::SettingsType::LastSortType,
[]() { return Config::CacheConfigItems::defaultSortType; }));
setSortDescending(m_cacheMgr->getSetting(
Cache::SettingsType::LastSortDescending,
[]() { return Config::CacheConfigItems::defaultSortDescending; }) == "true");
connect(app, &QApplication::aboutToQuit, this, [this]() {
m_cacheMgr->storeSetting(
Cache::SettingsType::LastSortType,
Config::sortTypeToString(m_imageMgr->sortType()));
});
connect(app, &QApplication::aboutToQuit, this, [this]() {
m_cacheMgr->storeSetting(
Cache::SettingsType::LastSortDescending,
m_imageMgr->sortDescending() ? "true" : "false");
});
}
if (m_configMgr->getCacheConfig().savePalette) {
requestSelectPalette(m_cacheMgr->getSetting(
Cache::SettingsType::LastSelectedPalette,
[]() { return Config::CacheConfigItems::defaultSelectedPalette; }));
connect(app, &QApplication::aboutToQuit, this, [this]() {
m_cacheMgr->storeSetting(
Cache::SettingsType::LastSelectedPalette,
m_paletteMgr->getSelectedPaletteName());
});
}
} }
private: private:
Cache::Manager* m_cacheMgr;
Config::Manager* m_configMgr; Config::Manager* m_configMgr;
Image::Manager* m_imageMgr; Image::Manager* m_imageMgr;
Palette::Manager* m_paletteMgr; Palette::Manager* m_paletteMgr;
+197
View File
@@ -0,0 +1,197 @@
#include "manager.hpp"
#include "Utils/misc.hpp"
#include "Utils/texttemplate.hpp"
#include "logger.hpp"
WALLREEL_DECLARE_SENDER("ServiceManager")
namespace WallReel::Core::Service {
Manager::Manager(
const Config::ActionConfigItems& actionConfig,
Image::Manager& imageManager,
Palette::Manager& paletteManager,
bool disableActions,
QObject* parent)
: m_actionConfig(actionConfig),
m_imageManager(imageManager),
m_paletteManager(paletteManager),
m_disableActions(disableActions) {
m_wallpaperService = new WallpaperService(m_actionConfig.previewDebounceTime, this);
// Forward signals
// Direct signal 2 signal connection
connect(m_wallpaperService, &WallpaperService::previewCompleted, this, &Manager::previewCompleted);
// Signal 2 slot connection to handle processing state
connect(m_wallpaperService, &WallpaperService::selectCompleted, this, &Manager::_onSelectCompleted);
connect(m_wallpaperService, &WallpaperService::restoreCompleted, this, &Manager::_onRestoreCompleted);
}
void Manager::onStateCaptured() {
m_stateCaptured = true;
if (!m_pendingPreviewId.isEmpty()) {
WR_DEBUG("State captured, executing pending preview for id " + m_pendingPreviewId);
const QString pending = m_pendingPreviewId;
m_pendingPreviewId.clear();
previewWallpaper(pending);
}
}
void Manager::selectWallpaper(const QString& id) {
WR_DEBUG("Select action triggered for id " + id);
if (m_disableActions) {
WR_DEBUG("Actions are disabled, skipping select for id " + id);
emit selectCompleted(true);
return;
}
if (m_isProcessing) {
WR_DEBUG("Already processing an select action, ignoring new request");
return;
}
m_isProcessing = true;
emit isProcessingChanged();
const auto* data = m_imageManager.imageAt(id);
if (!data || !data->isValid()) {
WR_WARN(QString("No valid image data at id %1. Skipping select action.").arg(id));
m_isProcessing = false;
emit isProcessingChanged();
emit selectCompleted(false);
return;
}
Utils::printPath(data->getFullPath());
const auto command = _renderCommand(m_actionConfig.onSelected, _generateVariables(*data));
m_wallpaperService->select(command);
}
void Manager::restore() {
WR_DEBUG("Restore action triggered");
if (m_disableActions) {
WR_DEBUG("Actions are disabled, skipping restore");
emit restoreCompleted(true);
return;
}
if (m_isProcessing) {
WR_DEBUG("Already processing an restore action, ignoring new request");
return;
}
if (!m_stateCaptured) {
WR_DEBUG("State not captured yet, skipping restore action");
emit restoreCompleted(false);
return;
}
m_isProcessing = true;
emit isProcessingChanged();
m_wallpaperService->restore(_renderCommand(m_actionConfig.onRestore, m_actionConfig.savedState));
}
void Manager::cancel() {
WR_DEBUG("Cancel action triggered");
if (m_disableActions) {
WR_DEBUG("Actions are disabled, skipping cancel");
emit cancelCompleted();
return;
}
m_wallpaperService->stopAll();
emit cancelCompleted();
}
void Manager::previewWallpaper(const QString& id) {
if (m_disableActions) {
WR_DEBUG("Actions are disabled, skipping preview for id " + id);
emit previewCompleted(true);
return;
}
if (!m_stateCaptured) {
WR_DEBUG("State not captured yet, deferring preview for id " + id);
m_pendingPreviewId = id;
emit previewCompleted(false);
return;
}
WR_DEBUG("Preview action triggered for id " + id);
const auto* data = m_imageManager.imageAt(id);
if (!data || !data->isValid()) {
WR_WARN(QString("No valid image data at id %1. Skipping preview action.").arg(id));
emit previewCompleted(false);
return;
}
m_wallpaperService->preview(_renderCommand(m_actionConfig.onPreview, _generateVariables(*data)));
}
void Manager::restoreOnQuit() {
if (m_hasSelected) {
WR_DEBUG("Quit with selected wallpaper, no need to restore");
return;
}
WR_DEBUG("Restore on quit");
m_wallpaperService->stopAll();
if (m_disableActions) {
WR_DEBUG("Actions are disabled, skipping restore on quit");
return;
}
QEventLoop loop;
connect(this, &Manager::restoreCompleted, &loop, &QEventLoop::quit);
// Call restore after the event loop starts
QTimer::singleShot(0, this, &Manager::restore);
loop.exec();
}
void Manager::_onSelectCompleted(bool success) {
WR_DEBUG("Select completed");
_onProcessCompleted();
m_hasSelected = m_hasSelected || success;
emit selectCompleted(success);
}
void Manager::_onRestoreCompleted(bool success) {
WR_DEBUG("Restore completed");
_onProcessCompleted();
emit restoreCompleted(success);
}
void Manager::_onProcessCompleted() {
m_isProcessing = false;
emit isProcessingChanged();
}
QString Manager::_renderCommand(const QString& templateStr, const QHash<QString, QString>& variables) const {
return Utils::renderTemplate(templateStr, variables);
}
QHash<QString, QString> Manager::_generateVariables(const Image::Data& imageData) const {
auto palette = m_paletteManager.getSelectedPaletteName();
if (palette.isEmpty()) {
palette = "null";
}
auto color = m_paletteManager.getCurrentColorName();
if (color.isEmpty()) {
color = "null";
}
auto hex = m_paletteManager.getCurrentColorHex();
if (hex.isEmpty()) {
hex = "null";
}
QHash<QString, QString> ret{
{"path", imageData.getFullPath()},
{"name", imageData.getFileName()},
{"size", QString::number(imageData.getSize())},
{"palette", palette},
{"colorName", color},
{"colorHex", hex},
{"domColorHex", imageData.getDominantColor().name()},
};
ret.insert(m_actionConfig.savedState);
return ret;
}
} // namespace WallReel::Core::Service
+23 -85
View File
@@ -8,7 +8,6 @@
#include "Image/manager.hpp" #include "Image/manager.hpp"
#include "Palette/manager.hpp" #include "Palette/manager.hpp"
#include "Service/wallpaper.hpp" #include "Service/wallpaper.hpp"
#include "logger.hpp"
namespace WallReel::Core::Service { namespace WallReel::Core::Service {
@@ -22,16 +21,8 @@ class Manager : public QObject {
const Config::ActionConfigItems& actionConfig, const Config::ActionConfigItems& actionConfig,
Image::Manager& imageManager, Image::Manager& imageManager,
Palette::Manager& paletteManager, Palette::Manager& paletteManager,
QObject* parent = nullptr) : m_actionConfig(actionConfig), m_imageManager(imageManager), m_paletteManager(paletteManager) { bool disableActions = false,
m_wallpaperService = new WallpaperService(m_actionConfig, m_paletteManager, this); QObject* parent = nullptr);
// Forward signals
// Direct signal 2 signal connection
connect(m_wallpaperService, &WallpaperService::previewCompleted, this, &Manager::previewCompleted);
// Signal 2 slot connection to handle processing state
connect(m_wallpaperService, &WallpaperService::selectCompleted, this, &Manager::_onSelectCompleted);
connect(m_wallpaperService, &WallpaperService::restoreCompleted, this, &Manager::_onRestoreCompleted);
}
bool isProcessing() const { return m_isProcessing; } bool isProcessing() const { return m_isProcessing; }
@@ -39,102 +30,49 @@ class Manager : public QObject {
public slots: public slots:
void selectWallpaper(const QString& id) { void onStateCaptured();
Logger::debug("ServiceManager", QString("Select wallpaper with id %1").arg(id));
if (m_isProcessing) {
Logger::debug("ServiceManager", "Already processing an select action, ignoring new request");
return;
}
m_isProcessing = true;
emit isProcessingChanged();
const auto* data = m_imageManager.imageAt(id);
if (data) {
m_wallpaperService->select(*data);
} else {
Logger::warn("ServiceManager", QString("No image data at id %1. Skipping select action.").arg(id));
m_isProcessing = false;
emit isProcessingChanged();
emit selectCompleted();
}
}
void restore() { void selectWallpaper(const QString& id);
Logger::debug("ServiceManager", "Restore states");
if (m_isProcessing) {
Logger::debug("ServiceManager", "Already processing an restore action, ignoring new request");
return;
}
m_isProcessing = true;
emit isProcessingChanged();
m_wallpaperService->restore();
}
void cancel() { void restore();
Logger::debug("ServiceManager", "Cancel action");
m_wallpaperService->stopAll();
emit cancelCompleted();
}
void previewWallpaper(const QString& id) { void cancel();
Logger::debug("ServiceManager", "Preview wallpaper");
const auto* data = m_imageManager.imageAt(id);
if (data) {
m_wallpaperService->preview(*data);
} else {
Logger::warn("ServiceManager", "No image data at id " + id + ". Skipping preview action.");
emit previewCompleted();
}
}
void restoreOnQuit() { void previewWallpaper(const QString& id);
if (m_hasSelected) {
Logger::debug("ServiceManager", "Quit with selected wallpaper, no need to restore"); void restoreOnQuit();
return;
}
Logger::debug("ServiceManager", "Restore on quit");
m_wallpaperService->stopAll();
QEventLoop loop;
connect(m_wallpaperService, &WallpaperService::restoreCompleted, &loop, &QEventLoop::quit);
// Call restore after the event loop starts
QTimer::singleShot(0, m_wallpaperService, &WallpaperService::restore);
loop.exec();
}
private slots: private slots:
void _onSelectCompleted() { void _onSelectCompleted(bool success);
Logger::debug("ServiceManager", "Select completed");
_onProcessCompleted();
m_hasSelected = true;
emit selectCompleted();
}
void _onRestoreCompleted() { void _onRestoreCompleted(bool success);
Logger::debug("ServiceManager", "Restore completed");
_onProcessCompleted();
emit restoreCompleted();
}
void _onProcessCompleted() { void _onProcessCompleted();
m_isProcessing = false;
emit isProcessingChanged();
}
signals: signals:
void isProcessingChanged(); void isProcessingChanged();
void selectCompleted(); void selectCompleted(bool success);
void previewCompleted(); void previewCompleted(bool success);
void restoreCompleted(); void restoreCompleted(bool success);
void cancelCompleted(); void cancelCompleted();
private:
QString _renderCommand(const QString& templateStr, const QHash<QString, QString>& variables) const;
QHash<QString, QString> _generateVariables(const Image::Data& imageData) const;
private: private:
WallpaperService* m_wallpaperService; WallpaperService* m_wallpaperService;
const Config::ActionConfigItems& m_actionConfig; const Config::ActionConfigItems& m_actionConfig;
Image::Manager& m_imageManager; Image::Manager& m_imageManager;
Palette::Manager& m_paletteManager; Palette::Manager& m_paletteManager;
bool m_disableActions;
bool m_isProcessing = false; bool m_isProcessing = false;
bool m_hasSelected = false; bool m_hasSelected = false;
bool m_stateCaptured = false;
QString m_pendingPreviewId;
}; };
} // namespace WallReel::Core::Service } // namespace WallReel::Core::Service
+81 -93
View File
@@ -1,54 +1,103 @@
#include "Service/wallpaper.hpp" #include "Service/wallpaper.hpp"
#include <QColor> #include <qprocess.h>
#include <iostream>
#include <QColor>
#include "Utils/texttemplate.hpp"
#include "logger.hpp" #include "logger.hpp"
WALLREEL_DECLARE_SENDER("WallpaperService") WALLREEL_DECLARE_SENDER("WallpaperService")
WallReel::Core::Service::WallpaperService::WallpaperService( namespace WallReel::Core::Service {
const Config::ActionConfigItems& actionConfig,
const Palette::Manager& paletteManager, WallpaperService::WallpaperService(int previewDebounceTime, QObject* parent)
QObject* parent) : QObject(parent) {
: QObject(parent), m_actionConfig(actionConfig), m_paletteManager(paletteManager) {
m_previewDebounceTimer = new QTimer(this); m_previewDebounceTimer = new QTimer(this);
m_previewDebounceTimer->setSingleShot(true); m_previewDebounceTimer->setSingleShot(true);
m_previewDebounceTimer->setInterval(m_actionConfig.previewDebounceTime); m_previewDebounceTimer->setInterval(previewDebounceTime);
connect(m_previewDebounceTimer, &QTimer::timeout, this, [this]() { connect(m_previewDebounceTimer, &QTimer::timeout, this, [this]() {
_doPreview(*m_pendingImageData); _doPreview(m_pendingPreviewCommand);
}); });
// There is a chance that a QProcess fails to start, changing its state from Starting to NotRunning without emitting finished signal,
// so we need to handle errorOccurred signal to catch that case and emit previewCompleted/selectCompleted/restoreCompleted with
// false to indicate failure.
// However, this is probably impossible since we use "sh" "-c" to execute commands and "sh" should always be available.
m_previewProcess = new QProcess(this); m_previewProcess = new QProcess(this);
connect(m_previewProcess,
&QProcess::errorOccurred,
this,
[this](QProcess::ProcessError error) {
WR_WARN(QString("Preview command process error: %1").arg(error));
if (error == QProcess::FailedToStart) {
WR_WARN("Failed to start preview command process.");
emit previewCompleted(false);
}
});
connect(m_previewProcess, connect(m_previewProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) { [this](int exitCode, QProcess::ExitStatus exitStatus) {
WR_DEBUG(QString("Preview process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus)); bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
emit previewCompleted(); if (!success) {
WR_WARN(QString("Preview command failed with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
} else {
WR_DEBUG("Preview command executed successfully");
}
emit previewCompleted(success);
}); });
m_selectProcess = new QProcess(this); m_selectProcess = new QProcess(this);
connect(m_selectProcess,
&QProcess::errorOccurred,
this,
[this](QProcess::ProcessError error) {
WR_WARN(QString("Select command process error: %1").arg(error));
if (error == QProcess::FailedToStart) {
WR_WARN("Failed to start select command process.");
emit selectCompleted(false);
}
});
connect(m_selectProcess, connect(m_selectProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) { [this](int exitCode, QProcess::ExitStatus exitStatus) {
WR_DEBUG(QString("Select process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus)); bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
emit selectCompleted(); if (!success) {
WR_WARN(QString("Select command failed with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
} else {
WR_DEBUG("Select command executed successfully");
}
emit selectCompleted(success);
}); });
m_restoreProcess = new QProcess(this); m_restoreProcess = new QProcess(this);
connect(m_restoreProcess,
&QProcess::errorOccurred,
this,
[this](QProcess::ProcessError error) {
WR_WARN(QString("Restore command process error: %1").arg(error));
if (error == QProcess::FailedToStart) {
WR_WARN("Failed to start restore command process.");
emit restoreCompleted(false);
}
});
connect(m_restoreProcess, connect(m_restoreProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) { [this](int exitCode, QProcess::ExitStatus exitStatus) {
WR_DEBUG(QString("Restore process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus)); bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
emit restoreCompleted(); if (!success) {
WR_WARN(QString("Restore command failed with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
} else {
WR_DEBUG("Restore command executed successfully");
}
emit restoreCompleted(success);
}); });
} }
void WallReel::Core::Service::WallpaperService::stopAll() { void WallpaperService::stopAll() {
WR_DEBUG("Stopping all wallpaper service processes"); WR_DEBUG("Stopping all wallpaper service processes");
if (m_previewProcess->state() != QProcess::NotRunning) { if (m_previewProcess->state() != QProcess::NotRunning) {
m_previewProcess->kill(); m_previewProcess->kill();
@@ -65,74 +114,32 @@ void WallReel::Core::Service::WallpaperService::stopAll() {
m_previewDebounceTimer->stop(); m_previewDebounceTimer->stop();
} }
void WallReel::Core::Service::WallpaperService::preview(const Image::Data& imageData) { void WallpaperService::preview(const QString& command) {
m_pendingImageData = &imageData; m_pendingPreviewCommand = command;
m_previewDebounceTimer->start(); m_previewDebounceTimer->start();
} }
void WallReel::Core::Service::WallpaperService::select(const Image::Data& imageData) { void WallpaperService::select(const QString& command) {
if (m_selectProcess->state() != QProcess::NotRunning) { if (m_selectProcess->state() != QProcess::NotRunning) {
WR_WARN("Previous select command is still running. Ignoring new command."); WR_WARN("Previous select command is still running. Ignoring new command.");
return; return;
} }
WR_DEBUG(QString("Select wallpaper: %1").arg(imageData.getFullPath())); _doSelect(command);
_doSelect(imageData);
} }
void WallReel::Core::Service::WallpaperService::restore() { void WallpaperService::restore(const QString& command) {
if (m_restoreProcess->state() != QProcess::NotRunning) { if (m_restoreProcess->state() != QProcess::NotRunning) {
WR_WARN("Previous restore command is still running. Ignoring new command."); WR_WARN("Previous restore command is still running. Ignoring new command.");
return; return;
} }
WR_DEBUG("Restore state"); WR_DEBUG("Restore state");
_doRestore(); _doRestore(command);
} }
QHash<QString, QString> WallReel::Core::Service::WallpaperService::_generateVariables(const Image::Data& imageData) { void WallpaperService::_doPreview(const QString& command) {
auto palette = m_paletteManager.getSelectedPaletteName();
if (palette.isEmpty()) {
palette = "null";
}
auto color = m_paletteManager.getCurrentColorName();
if (color.isEmpty()) {
color = "null";
}
auto hex = m_paletteManager.getCurrentColorHex();
if (hex.isEmpty()) {
hex = "null";
}
QHash<QString, QString> ret{
{"path", imageData.getFullPath()},
{"name", imageData.getFileName()},
{"size", QString::number(imageData.getSize())},
{"palette", palette},
{"colorName", color},
{"colorHex", hex},
{"domColorHex", imageData.getDominantColor().name()},
};
ret.insert(m_actionConfig.savedState);
return ret;
}
void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& imageData) {
QString path = imageData.getFullPath();
if (path.isEmpty()) {
WR_WARN("No valid image path for preview. Skipping preview action.");
emit previewCompleted();
return;
}
if (m_actionConfig.printPreview) {
std::cout << path.toStdString() << std::endl;
}
const auto variables = _generateVariables(imageData);
auto command = Utils::renderTemplate(m_actionConfig.onPreview, variables);
if (command.isEmpty()) { if (command.isEmpty()) {
WR_DEBUG("No preview command configured. Skipping preview action."); WR_DEBUG("No preview command configured. Skipping preview action.");
emit previewCompleted(); emit previewCompleted(true);
return; return;
} }
WR_DEBUG(QString("Executing preview command: %1").arg(command)); WR_DEBUG(QString("Executing preview command: %1").arg(command));
@@ -144,43 +151,24 @@ void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& im
m_previewProcess->start("sh", QStringList() << "-c" << command); m_previewProcess->start("sh", QStringList() << "-c" << command);
} }
void WallReel::Core::Service::WallpaperService::_doSelect(const Image::Data& imageData) { void WallpaperService::_doSelect(const QString& command) {
QString path = imageData.getFullPath();
if (path.isEmpty()) {
WR_WARN("No valid image path for select. Skipping select action.");
emit selectCompleted();
return;
}
if (m_actionConfig.printSelected) {
std::cout << path.toStdString() << std::endl;
}
const auto variables = _generateVariables(imageData);
auto command = Utils::renderTemplate(m_actionConfig.onSelected, variables);
if (command.isEmpty()) { if (command.isEmpty()) {
WR_DEBUG("No select command configured. Skipping select action."); WR_DEBUG("No select command configured. Skipping select action.");
emit selectCompleted(); emit selectCompleted(true);
return; return;
} }
WR_DEBUG(QString("Executing select command: %1").arg(command)); WR_DEBUG(QString("Executing select command: %1").arg(command));
m_selectProcess->start("sh", QStringList() << "-c" << command); m_selectProcess->start("sh", QStringList() << "-c" << command);
} }
void WallReel::Core::Service::WallpaperService::_doRestore() { void WallpaperService::_doRestore(const QString& command) {
if (m_actionConfig.onRestore.isEmpty()) {
WR_DEBUG("No restore command configured. Skipping restore action.");
emit restoreCompleted();
return;
}
const QString command = Utils::renderTemplate(m_actionConfig.onRestore, m_actionConfig.savedState);
if (command.isEmpty()) { if (command.isEmpty()) {
WR_DEBUG("Restore command is empty after rendering. Skipping restore action."); WR_DEBUG("Restore command is empty. Skipping restore action.");
emit restoreCompleted(); emit restoreCompleted(true);
return; return;
} }
WR_DEBUG(QString("Executing restore command: %1").arg(command)); WR_DEBUG(QString("Executing restore command: %1").arg(command));
m_restoreProcess->start("sh", QStringList() << "-c" << command); m_restoreProcess->start("sh", QStringList() << "-c" << command);
} }
} // namespace WallReel::Core::Service
+11 -21
View File
@@ -4,43 +4,33 @@
#include <QProcess> #include <QProcess>
#include <QTimer> #include <QTimer>
#include "Config/data.hpp"
#include "Image/data.hpp"
#include "Palette/manager.hpp"
namespace WallReel::Core::Service { namespace WallReel::Core::Service {
class WallpaperService : public QObject { class WallpaperService : public QObject {
Q_OBJECT Q_OBJECT
public: public:
WallpaperService( WallpaperService(int previewDebounceTime, QObject* parent = nullptr);
const Config::ActionConfigItems& actionConfig,
const Palette::Manager& paletteManager,
QObject* parent = nullptr);
void stopAll(); void stopAll();
public slots: public slots:
void preview(const Image::Data& imageData); // execute after 500ms of inactivity void preview(const QString& command); // execute after 500ms of inactivity
void select(const Image::Data& imageData); // execute immediately, ignore if already running void select(const QString& command); // execute immediately, ignore if already running
void restore(); // execute immediately, ignore if already running void restore(const QString& command); // execute immediately, ignore if already running
signals: signals:
void previewCompleted(); void previewCompleted(bool success);
void selectCompleted(); void selectCompleted(bool success);
void restoreCompleted(); void restoreCompleted(bool success);
private: private:
void _doPreview(const Image::Data& imageData); void _doPreview(const QString& command);
void _doSelect(const Image::Data& imageData); void _doSelect(const QString& command);
void _doRestore(); void _doRestore(const QString& command);
QHash<QString, QString> _generateVariables(const Image::Data& imageData);
const Config::ActionConfigItems& m_actionConfig;
const Palette::Manager& m_paletteManager;
QTimer* m_previewDebounceTimer; QTimer* m_previewDebounceTimer;
const Image::Data* m_pendingImageData; QString m_pendingPreviewCommand;
QProcess* m_previewProcess; QProcess* m_previewProcess;
QProcess* m_selectProcess; QProcess* m_selectProcess;
QProcess* m_restoreProcess; QProcess* m_restoreProcess;
+18
View File
@@ -179,6 +179,24 @@ inline QDir getPicturesDir() {
return QDir(picturesDir); return QDir(picturesDir);
} }
inline void printPath(const QString& path, std::FILE* out = stdout) {
if (path.isEmpty()) {
return;
}
const QByteArray bytes = QFile::encodeName(path);
const size_t n = static_cast<size_t>(bytes.size());
if (std::fwrite(bytes.constData(), 1, n, out) != n) {
return;
}
if (std::fputc('\n', out) == EOF) {
return;
}
std::fflush(out);
return;
}
} // namespace WallReel::Core::Utils } // namespace WallReel::Core::Utils
#endif // WALLREEL_MISC_HPP #endif // WALLREEL_MISC_HPP
+22 -1
View File
@@ -67,6 +67,12 @@ void AppOptions::parseArgs(QApplication& app) {
QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file"); QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file");
parser.addOption(configFileOption); parser.addOption(configFileOption);
QCommandLineOption disableActionsOption(QStringList() << "D" << "disable-actions", "Disable actions set in configuration file");
parser.addOption(disableActionsOption);
QCommandLineOption applyOption(QStringList() << "a" << "apply", "Apply the specified image as wallpaper and exit", "file");
parser.addOption(applyOption);
// Not parser.process(a->arguments()) because we want to handle exit logics ourselves. // Not parser.process(a->arguments()) because we want to handle exit logics ourselves.
// parser.process(...) will do something like exit(...) that will terminate // parser.process(...) will do something like exit(...) that will terminate
// the application brutally and produce unwanted warnings. // the application brutally and produce unwanted warnings.
@@ -111,7 +117,7 @@ void AppOptions::parseArgs(QApplication& app) {
} }
if (parser.isSet(configFileOption)) { if (parser.isSet(configFileOption)) {
QString path = parser.value(configFileOption); QString path = Utils::expandPath(parser.value(configFileOption));
if (Utils::checkFile(path)) { if (Utils::checkFile(path)) {
configPath = path; configPath = path;
} else { } else {
@@ -120,6 +126,21 @@ void AppOptions::parseArgs(QApplication& app) {
return; return;
} }
} }
if (parser.isSet(disableActionsOption)) {
disableActions = true;
}
if (parser.isSet(applyOption)) {
QString path = Utils::expandPath(parser.value(applyOption));
if (Utils::checkImageFile(path)) {
applyPath = path;
} else {
errorText = QString("Error: Image file does not exist, is not accessible, or has an unsupported format: %1").arg(path);
printError();
return;
}
}
} }
} // namespace WallReel::Core } // namespace WallReel::Core
+4 -2
View File
@@ -26,8 +26,10 @@ class AppOptions {
QString configPath; QString configPath;
QStringList appendDirs; QStringList appendDirs;
QString errorText; QString errorText;
bool clearCache = false; // -C --clear-cache QString applyPath; // -a --apply
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments. bool clearCache = false; // -C --clear-cache
bool disableActions = false; // -D --disable-actions
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
AppOptions(); AppOptions();
void parseArgs(QApplication& app); void parseArgs(QApplication& app);
+52 -8
View File
@@ -1,9 +1,15 @@
#include <qapplication.h>
#include <qobject.h> #include <qobject.h>
#include <QApplication> #include <QApplication>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include <QSocketNotifier>
extern "C" {
#include <signal.h>
#include <sys/signalfd.h>
#include <unistd.h>
}
#include "Core/Provider/bootstrap.hpp" #include "Core/Provider/bootstrap.hpp"
#include "Core/Provider/carousel.hpp" #include "Core/Provider/carousel.hpp"
@@ -20,14 +26,29 @@ int main(int argc, char* argv[]) {
// 1. QQmlApplicationEngine (with all QML objects) // 1. QQmlApplicationEngine (with all QML objects)
// 2. provider (manages states and connections) // 2. provider (manages states and connections)
// 3. bootstrap (manages lifecycle of all managers) // 3. bootstrap (manages lifecycle of all managers)
// 4. QApplication // 4. QSocketNotifier (receives signals for graceful shutdown)
// 5. QApplication
// Mask signals for graceful shutdown
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGHUP);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGUSR1);
sigaddset(&mask, SIGUSR2);
if (pthread_sigmask(SIG_BLOCK, &mask, nullptr) == -1) {
// Logger is yet to be initialized, but is still usable with default behavior
WR_CRITICAL(QString("Failed to block signals: %1").arg(strerror(errno)));
return 1;
}
QApplication a(argc, argv); QApplication a(argc, argv);
a.setApplicationName(APP_NAME); a.setApplicationName(APP_NAME);
a.setApplicationVersion(APP_VERSION); a.setApplicationVersion(APP_VERSION);
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
a.setWindowIcon(QIcon(u":/%1.svg"_s.arg(APP_NAME))); a.setWindowIcon(QIcon(u":/icon.svg"_s));
#else #else
a.setWindowIcon(QIcon(u":/%1.svg"_qs.arg(APP_NAME))); a.setWindowIcon(QIcon(u":/%1.svg"_qs.arg(APP_NAME)));
#endif #endif
@@ -35,6 +56,27 @@ int main(int argc, char* argv[]) {
{ {
Logger::init(); Logger::init();
// Create signalfd to receive signals in the Qt event loop
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
if (sfd == -1) {
WR_CRITICAL(QString("Failed to create signalfd: %1").arg(strerror(errno)));
return 1;
}
QSocketNotifier notifier(sfd, QSocketNotifier::Read, &a);
QObject::connect(
&notifier,
&QSocketNotifier::activated,
&a,
[sfd, &a]() {
struct signalfd_siginfo fdsi;
ssize_t s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
if (s == sizeof(struct signalfd_siginfo)) {
WR_DEBUG(QString("Received signal: %1").arg(fdsi.ssi_signo));
a.quit();
}
});
AppOptions options; AppOptions options;
options.parseArgs(a); options.parseArgs(a);
@@ -48,6 +90,10 @@ int main(int argc, char* argv[]) {
return 0; return 0;
} }
if (!options.applyPath.isEmpty()) {
return bootstrap.apply(options.applyPath) ? 0 : 1;
}
{ {
Provider::Carousel provider(&a, bootstrap); Provider::Carousel provider(&a, bootstrap);
qmlRegisterSingletonInstance( qmlRegisterSingletonInstance(
@@ -66,13 +112,11 @@ int main(int argc, char* argv[]) {
[]() { QCoreApplication::exit(-1); }, []() { QCoreApplication::exit(-1); },
Qt::QueuedConnection); Qt::QueuedConnection);
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
engine.loadFromModule(UIMODULE_URI, u"Main"_s); engine.loadFromModule(UIMODULE_URI, u"Main"_s);
#elif QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
engine.loadFromModule(UIMODULE_URI, u"Main"_qs);
#else #else
engine.addImportPath(u"qrc:/"_qs)); engine.addImportPath(u"qrc:/"_qs);
engine.load(QUrl(u"qrc:/WallReel/UI/Main.qml"_qs)); engine.load(QUrl(u"qrc:/WallReel/UI/Main.qml"_qs));
#endif #endif
+17 -26
View File
@@ -47,11 +47,6 @@
"theme": { "theme": {
"type": "object", "type": "object",
"properties": { "properties": {
"defaultPalette": {
"type": "string",
"default": "",
"description": "Name of the default palette to use"
},
"palettes": { "palettes": {
"type": "array", "type": "array",
"items": { "items": {
@@ -101,11 +96,6 @@
"default": true, "default": true,
"description": "Whether to print the selected wallpaper path to stdout on confirm" "description": "Whether to print the selected wallpaper path to stdout on confirm"
}, },
"printPreview": {
"type": "boolean",
"default": false,
"description": "Whether to print the previewed wallpaper path to stdout on preview"
},
"onSelected": { "onSelected": {
"type": "string", "type": "string",
"default": "", "default": "",
@@ -126,10 +116,10 @@
"default": "", "default": "",
"description": "Key of value to save, used as {{ key }} in onRestore command" "description": "Key of value to save, used as {{ key }} in onRestore command"
}, },
"default": { "fallback": {
"type": "string", "type": "string",
"default": "", "default": "",
"description": "Value to save, used when \"cmd\" is not set or command execution fails or output is empty" "description": "Value to save, used when \"command\" is not set or command execution fails or output is empty"
}, },
"command": { "command": {
"type": "string", "type": "string",
@@ -153,7 +143,7 @@
}, },
"quitOnSelected": { "quitOnSelected": {
"type": "boolean", "type": "boolean",
"default": false, "default": true,
"description": "Whether to quit the application after confirming a wallpaper" "description": "Whether to quit the application after confirming a wallpaper"
}, },
"restoreOnClose": { "restoreOnClose": {
@@ -179,6 +169,7 @@
"image_focus_scale": { "image_focus_scale": {
"type": "number", "type": "number",
"default": 1.5, "default": 1.5,
"minimum": 1.0,
"description": "Scale of the focused image (relative to unfocused image)" "description": "Scale of the focused image (relative to unfocused image)"
}, },
"window_width": { "window_width": {
@@ -193,23 +184,23 @@
} }
} }
}, },
"sort": { "cache": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "saveSortMethod": {
"type": "string",
"enum": [
"name",
"date",
"size"
],
"default": "date",
"description": "Initial sorting type"
},
"descending": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
"description": "Initial sorting order. Ascending: name: lexicographical, date: older before newer, size: smaller before larger" "description": "Whether to persist the sort type and order"
},
"savePalette": {
"type": "boolean",
"default": true,
"description": "Whether to persist the selected palette"
},
"maxImageEntries": {
"type": "integer",
"default": 1000,
"description": "Maximum number of entries in the image cache (older entries will be evicted)"
} }
} }
} }
+109
View File
@@ -0,0 +1,109 @@
---
title: WALLREEL
section: 1
header: User Commands
footer: WallReel 2.0.2
date: 2026-03-24
---
# NAME
wallreel - Choose and set desktop wallpapers with customizable themes and actions
# SYNOPSIS
**wallreel** [*options*]
# DESCRIPTION
**wallreel** is a Qt6 application for browsing wallpaper images, previewing candidates,
and applying a selected image.
Configuration is loaded from a JSON file. CLI options are available for logging,
one-shot operations, and runtime overrides.
# OPTIONS
**-h, --help**
: Display help for command-line options.
**-v, --version**
: Display version information.
**-V, --verbose**
: Set log level to DEBUG (default is INFO).
**-C, --clear-cache**
: Clear image cache and exit.
**-q, --quiet**
: Suppress log output.
**-d, --append-dir** _dir_
: Append an additional wallpaper search directory.
This option can be provided multiple times.
**-c, --config-file** _file_
: Use a custom configuration file.
**-D, --disable-actions**
: Disable actions defined in the configuration file.
**-a, --apply** _file_
: Apply the specified image as wallpaper and exit.
In this mode, the configuration is still parsed. Action placeholders are resolved
from the selected image and any captured state values.
# BEHAVIOR NOTES
- CLI options are generally optional; configuration is the preferred customization path.
- Some options are mutually exclusive (for example `--verbose` and `--quiet`).
- With `--apply`, WallReel executes configured selection actions without opening the UI.
# FILES
`~/.config/wallreel/config.json`
: Default configuration file location.
`~/.cache/wallreel/`
: Runtime cache location.
# EXAMPLES
Run with default configuration:
```bash
wallreel
```
Use a custom configuration file:
```bash
wallreel --config-file ~/.config/wallreel/config.json
```
Append additional search directories:
```bash
wallreel --append-dir ~/Pictures/Wallpapers --append-dir ~/Art
```
Apply a wallpaper and exit:
```bash
wallreel --apply ~/Pictures/wallpaper.jpg
```
# EXIT STATUS
Returns `0` on success. Returns a non-zero value on failure.
# SEE ALSO
**wallreel**(5)
# AUTHOR
Uyanide <github.com/Uyanide>
+230
View File
@@ -0,0 +1,230 @@
---
title: WALLREEL
section: 5
header: File Formats Manual
footer: WallReel 2.0.2
date: 2026-03-24
---
# NAME
wallreel-config - configuration format for wallreel
# SYNOPSIS
`~/.config/wallreel/config.json`
# DESCRIPTION
WallReel reads configuration from a JSON document. The root object is divided into
five sections:
- `wallpaper`
- `theme`
- `action`
- `style`
- `cache`
For complete machine-readable validation details, refer to `config.schema.json`.
# WALLPAPER SECTION
Defines where WallReel looks for images and what to exclude.
If both `paths` and `dirs` are empty or omitted, WallReel defaults to recursively
scanning the user's Pictures directory and treating all supported image files as
wallpaper candidates.
`paths` (array of string, default: `[]`)
: Exact paths to specific image files.
`dirs` (array of object, default: `[]`)
: Directories to scan for images.
Each item has:
- `path` (string)
- `recursive` (boolean)
`excludes` (array of string, default: `[]`)
: Exclude patterns as regular expressions.
# THEME SECTION
Configures color palettes.
A dominant color is extracted from each wallpaper. If a palette is selected,
WallReel picks the closest palette color as the primary color.
`palettes` (array of object, default: `[]`)
: Custom palette definitions.
Each palette has:
- `name` (string)
- `colors` (array)
Each color item has:
- `name` (string)
- `value` (hex string, for example `"#89b4fa"`)
# ACTION SECTION
Configures commands executed for preview, selection, and restore behavior.
`previewDebounceTime` (integer, default: `300`)
: Debounce interval in milliseconds for preview actions.
`printSelected` (boolean, default: `true`)
: Print selected wallpaper path to stdout on confirmation.
`onSelected` (string, default: `""`)
: Command executed when a wallpaper is confirmed.
`onPreview` (string, default: `""`)
: Command executed when a wallpaper is previewed.
`saveState` (array of object, default: `[]`)
: Commands for capturing system values before changing wallpaper.
Each item has:
- `key` (placeholder key)
- `fallback` (default value)
- `command` (stdout-mapped command)
- `timeout` (milliseconds)
`onRestore` (string, default: `""`)
: Command executed on restore. Saved state keys are usable as placeholders.
`quitOnSelected` (boolean, default: `true`)
: Exit application immediately after confirming a selection.
`restoreOnClose` (boolean, default: `true`)
: Run `onRestore` when application closes without a final selection.
## ACTION PLACEHOLDERS
The following placeholders are available in `onSelected`, `onPreview`, and
`onRestore` (where applicable):
`{{ path }}`
: Full path of selected or previewed wallpaper.
`{{ name }}`
: File name of selected or previewed wallpaper.
`{{ size }}`
: Size in bytes of selected or previewed wallpaper.
`{{ palette }}`
: Selected palette name (`"null"` if none).
`{{ colorName }}`
: Chosen primary color name (`"null"` if none).
`{{ colorHex }}`
: Chosen primary color hex (`"null"` if none).
`{{ domColorHex }}`
: Dominant color hex extracted from the wallpaper.
`{{ <key> }}`
: Value of a saved state item with matching key.
# STYLE SECTION
Controls window layout and thumbnail dimensions.
`image_width` (integer, default: `320`)
: Width of each thumbnail.
`image_height` (integer, default: `180`)
: Height of each thumbnail.
`image_focus_scale` (number, default: `1.5`)
: Focus scale multiplier for highlighted thumbnail.
`window_width` (integer, default: `750`)
: Initial window width.
`window_height` (integer, default: `500`)
: Initial window height.
# CACHE SECTION
Controls persisted UI state.
`saveSortMethod` (boolean, default: `true`)
: Persist sort method and direction.
`savePalette` (boolean, default: `true`)
: Persist selected palette.
`maxImageEntries` (integer, default: `1000`)
: Maximum number of image cache entries. Older entries are evicted.
# EXAMPLE
```json
{
"$schema": "https://raw.githubusercontent.com/Uyanide/WallReel/refs/heads/master/config.schema.json",
"wallpaper": {
"paths": ["/home/user/Pictures/favorite.jpg"],
"dirs": [
{
"path": "/home/user/Pictures/Wallpapers",
"recursive": true
}
],
"excludes": ["\\.gif$"]
},
"theme": {
"palettes": [
{
"name": "Dark",
"colors": [
{ "name": "blue", "value": "#89b4fa" },
{ "name": "red", "value": "#f38ba8" }
]
}
]
},
"action": {
"previewDebounceTime": 500,
"quitOnSelected": true,
"onPreview": "swww img {{ path }}",
"onSelected": "cp {{ path }} ~/.config/wallpaper/current/ && swww img {{ path }}",
"saveState": [
{
"key": "current_wp",
"fallback": "/home/user/Pictures/default.jpg",
"command": "find ~/.config/wallpaper/current -type f | head -n 1",
"timeout": 1000
}
],
"onRestore": "swww img {{ current_wp }}"
},
"style": {
"image_width": 640,
"image_height": 400,
"image_focus_scale": 1.2,
"window_width": 1280,
"window_height": 720
},
"cache": {
"saveSortMethod": true,
"savePalette": true,
"maxImageEntries": 300
}
}
```
# SEE ALSO
**wallreel**(1)
# AUTHOR
Uyanide <github.com/Uyanide>
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB