29 Commits

Author SHA1 Message Date
Uyanide d39e36e096 feat: add reload button to force 'reload from disk'
Release / Build ArchLinux Package (push) Successful in 1m3s
Release / Publish to Gitea Release (push) Successful in 4s
Release / Publish to AUR (push) Successful in 9s
2026-04-05 19:22:12 +02:00
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
43 changed files with 1981 additions and 453 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/
/build*/
.cache
.vscode
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16)
project(WallReel VERSION 2.0.0 LANGUAGES CXX)
project(WallReel VERSION 2.2.0 LANGUAGES CXX)
set(EXECUTABLE_NAME "wallreel")
set(CORELIB_NAME "wallreel-core")
+64 -33
View File
@@ -1,20 +1,20 @@
## 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
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
sudo pacman -S --needed qt6-base qt6-declarative cmake gcc
```
on Debian-based systems:
On Debian-based systems:
```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++
@@ -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:
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
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local
```
Adjust install prefix to your needs. Start building:
Then build.
```bash
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
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
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
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`)
@@ -73,13 +101,12 @@ Defines where WallReel looks for images and what to exclude. If none of the `pat
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 |
| :--------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
| `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"`). |
### Action (`action`)
@@ -87,15 +114,14 @@ There are a few embeded palettes available in the application, including "Catppu
Configures system commands to execute on specific events mapping to your window manager or wallpaper utility (e.g., `swaybg`, `feh`).
| Property | Type | Default | Description |
| :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `previewDebounceTime` | Integer | `300` | Debounce time (ms) for triggering the preview action. |
| `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. |
| `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`, `default` (fallback value), `command` (stdout mapping), and `timeout` (ms). |
| `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). |
| `onRestore` | String | `""` | Command to execute on restore. Extracted states from `saveState` can be injected using `{{ key }}`. |
| `quitOnSelected` | Boolean | `false` | Quit the application after a selection is made. |
| `quitOnSelected` | Boolean | `true` | Quit the application after a selection is made. |
| `restoreOnClose` | Boolean | `true` | Run `onRestore` command if the application is closed without making a final selection. |
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) |
| `{{ 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. |
| `{{ key }}` | Value of the saved state with the specified key. |
| `{{ <key> }}` | Value of the saved state with the specified key. |
### 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_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 |
| :----------- | :------ | :------- | :------------------------------------------------------------------------------- |
| `type` | String | `"date"` | Defines sorting criteria. Acceptable values: `"name"`, `"date"`, `"size"`. |
| `descending` | Boolean | `true` | If true, sorts in descending order (e.g. newer dates first, larger files first). |
| :---------------- | :------ | :------ | :---------------------------------------------------------------------------- |
| `saveSortMethod` | Boolean | `true` | Whether to persist the sort type and order. |
| `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$"]
},
"theme": {
"defaultPalette": "Dark",
"palettes": [
{
"name": "Dark",
@@ -169,7 +195,7 @@ Initial sorting behavior for loaded images.
"saveState": [
{
"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",
"timeout": 1000
}
@@ -183,32 +209,37 @@ Initial sorting behavior for loaded images.
"window_width": 1280,
"window_height": 720
},
"sort": {
"type": "date",
"descending": true
"cache": {
"saveSortMethod": true,
"savePalette": true,
"maxImageEntries": 300
}
}
```
## CLI
```
```text
Usage: wallreel [options]
Options:
-h, --help Displays help on commandline options.
-v, --version Displays version information.
-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
-d, --append-dir <dir> Append an additional wallpaper search directory
-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:
- 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.
- 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"
PREFIX "/"
FILES
${EXECUTABLE_NAME}.svg
FILES icon.svg
)
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.desktop
install(FILES ${CMAKE_CURRENT_LIST_DIR}/app.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
RENAME ${EXECUTABLE_NAME}.desktop
)
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.svg
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps
install(FILES ${CMAKE_CURRENT_LIST_DIR}/apply.desktop
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
Name=WallReel
Icon=wallreel
GenericName=Animated wallpaper selector
GenericName=Wallpaper Selector
TryExec=wallreel
Exec=wallreel
Comment=A small wallpaper utility made with Qt
Comment=Choose and set desktop wallpapers with customizable themes and actions
Terminal=false
Categories=Application;Utility;DesktopSettings;
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/manager.hpp Config/manager.cpp
logger.hpp logger.cpp
Service/manager.hpp
Service/manager.hpp Service/manager.cpp
Service/wallpaper.hpp Service/wallpaper.cpp
appoptions.hpp appoptions.cpp
)
+289 -28
View File
@@ -7,6 +7,7 @@
#include <QSqlError>
#include <QSqlQuery>
#include <QThread>
#include <QtConcurrent>
#include "logger.hpp"
@@ -16,21 +17,51 @@ using namespace Qt::StringLiterals;
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) {
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(
QCryptographicHash::hash(raw.toUtf8(), QCryptographicHash::Sha256).toHex());
}
Manager::Manager(const QDir& cacheDir)
: 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())) {
Manager::Manager(const QDir& cacheDir, int maxEntries)
: 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));
// Open a connection on the constructing thread so the schema is
// guaranteed to exist before any worker thread first calls _db().
_db();
}
void Manager::evictOldEntries() {
if (m_maxEntries > 0)
m_cleanupFuture = QtConcurrent::run([this] { _runCleanup(); });
}
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;
{
QMutexLocker lock(&m_connectionsMutex);
@@ -50,41 +81,59 @@ void Manager::clearCache(Type type) {
if ((type & Type::Image) != Type::None) {
int removed = 0;
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()) {
QFile::remove(m_cacheDir.filePath(selectQuery.value(0).toString()));
++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));
}
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);
}
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) {
QSqlDatabase db = _db();
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(QStringLiteral(
"SELECT r, g, b, a FROM color_cache WHERE key = :key"));
query.prepare(u"SELECT r, g, b, a FROM color_cache WHERE key = :key"_s);
query.bindValue(u":key"_s, key);
if (query.exec() && query.next()) {
WR_DEBUG(u"Color cache hit [%1]"_s.arg(key));
return QColor(
QColor result(
query.value(0).toInt(),
query.value(1).toInt(),
query.value(2).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));
if (!computeFunc) {
WR_WARN(u"No compute function provided for color cache miss [%1]"_s.arg(key));
return QColor();
}
const QColor color = computeFunc();
if (!color.isValid()) {
@@ -94,9 +143,9 @@ QColor Manager::getColor(const QString& key, const std::function<QColor()>& comp
if (db.isOpen()) {
QSqlQuery insertQuery(db);
insertQuery.prepare(QStringLiteral(
"INSERT OR REPLACE INTO color_cache (key, r, g, b, a) "
"VALUES (:key, :r, :g, :b, :a)"));
insertQuery.prepare(
u"INSERT OR REPLACE INTO color_cache (key, r, g, b, a, last_accessed) "
"VALUES (:key, :r, :g, :b, :a, CURRENT_TIMESTAMP)"_s);
insertQuery.bindValue(u":key"_s, key);
insertQuery.bindValue(u":r"_s, color.red());
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();
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(QStringLiteral(
"SELECT file_name FROM image_cache WHERE key = :key"));
query.prepare(u"SELECT file_name FROM image_cache WHERE key = :key"_s);
query.bindValue(u":key"_s, key);
if (query.exec() && query.next()) {
@@ -125,29 +173,42 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
if (cached.exists()) {
WR_DEBUG(u"Image cache hit [%1] -> %2"_s
.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;
}
// File was deleted externally — evict the stale DB record.
WR_WARN(u"Image cache stale, file missing [%1], evicting"_s.arg(key));
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.exec();
}
}
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();
if (image.isNull()) {
WR_WARN(u"ComputeFunc returned null image for key [%1]"_s.arg(key));
return QFileInfo{};
}
const QString fileName = key + u".png"_s;
const QString fileName = key + u".jpg"_s;
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));
return QFileInfo{};
}
@@ -155,19 +216,93 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
if (db.isOpen()) {
QSqlQuery insertQuery(db);
insertQuery.prepare(QStringLiteral(
"INSERT OR REPLACE INTO image_cache (key, file_name) "
"VALUES (:key, :file_name)"));
insertQuery.prepare(
u"INSERT OR REPLACE INTO image_cache (key, file_name, last_accessed) "
"VALUES (:key, :file_name, CURRENT_TIMESTAMP)"_s);
insertQuery.bindValue(u":key"_s, key);
insertQuery.bindValue(u":file_name"_s, fileName);
if (!insertQuery.exec())
WR_WARN(u"Failed to record image in db [%1]: %2"_s
.arg(key, insertQuery.lastError().text()));
else {
QMutexLocker lock(&m_hotKeysMutex);
m_hotImageKeys.insert(key);
}
}
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.
QSqlDatabase Manager::_db() const {
// 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 {
QSqlQuery q(db);
q.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS color_cache ("
q.exec(
u"CREATE TABLE IF NOT EXISTS color_cache ("
" key TEXT PRIMARY KEY NOT NULL,"
" r INTEGER NOT NULL,"
" g INTEGER NOT NULL,"
" b INTEGER NOT NULL,"
" a INTEGER NOT NULL"
")"));
q.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS image_cache ("
" a INTEGER NOT NULL,"
" last_accessed TEXT"
")"_s);
q.exec(
u"CREATE TABLE IF NOT EXISTS image_cache ("
" key TEXT PRIMARY KEY NOT NULL,"
" file_name TEXT 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
+18 -3
View File
@@ -3,6 +3,7 @@
#include <QDir>
#include <QFileInfo>
#include <QFuture>
#include <QMutex>
#include <QSet>
#include <QtSql>
@@ -15,26 +16,40 @@ class Manager {
public:
static QString cacheKey(const QFileInfo& fileInfo, const QSize& imageSize);
Manager(const QDir& cacheDir);
Manager(const QDir& cacheDir, int maxEntries = 1000);
~Manager();
void evictOldEntries();
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:
QDir m_cacheDir;
int m_maxEntries;
QString m_dbPath;
QString m_connectionPrefix;
mutable QMutex m_connectionsMutex;
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;
void _setupTables(QSqlDatabase& db) const;
void _runCleanup();
};
} // namespace WallReel::Core::Cache
+8 -1
View File
@@ -12,7 +12,8 @@ namespace WallReel::Core::Cache {
enum class Type : uint32_t {
None = 0,
Image = 1, ///< Cache for processed images
Color = 1 << 1, ///< Cache for palette color matching results
Color = 1 << 1, ///< Cache for dominant colors
Settings = 1 << 2, ///< Cache for settings (simple key-value pairs)
All = ~0u
};
@@ -28,6 +29,12 @@ inline constexpr Type operator&(Type a, Type b) {
using Data = std::variant<std::monostate, QFileInfo, QColor>;
enum class SettingsType : uint32_t {
LastSelectedPalette = 0,
LastSortType,
LastSortDescending,
};
} // namespace WallReel::Core::Cache
#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.excludes array [] Exclude patterns (regex)
//
// theme.defaultPalette string "" Name of the default palette to use
// theme.palettes array []
// theme.palettes[].name string "" Name of 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.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.onPreview string "" Command to execute on preview
// action.saveState array [] Useful for restore 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[].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.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
//
// style.image_width number 320 Width of each image
@@ -46,11 +44,9 @@
// style.window_width number 750 Initial window width
// style.window_height number 500 Initial window height
//
// sort.type string "date" Initial sorting type: "name", "date", "size"
// sort.descending boolean true Initial sorting order
// Ascending: name: lexicographical, e.g. "a.jpg" before "b.jpg"
// date: older before newer
// size: smaller before larger
// cache.saveSortMethod boolean true Whether to persist the sort type and order
// cache.savePalette bool true Whether to persist the selected palette
// cache.maxImageEntries number 1000 Maximum number of entries in the image cache (older entries will be evicted)
namespace WallReel::Core::Config {
@@ -64,7 +60,7 @@ enum class SortType : int {
inline const QStringList s_availableSortTypes = {"Name", "Date", "Size"};
inline QString sortTypeToString(SortType type) {
inline QString sortTypeToString(const SortType& type) {
switch (type) {
case SortType::Name:
return "Name";
@@ -112,7 +108,6 @@ struct ThemeConfigItems {
};
QList<PaletteConfigItem> palettes;
QString defaultPalette;
};
struct ActionConfigItems {
@@ -130,8 +125,7 @@ struct ActionConfigItems {
QString onRestore;
int previewDebounceTime = 300; // milliseconds
bool printSelected = true;
bool printPreview = false;
bool quitOnSelected = false;
bool quitOnSelected = true;
bool restoreOnClose = true;
};
@@ -143,9 +137,14 @@ struct StyleConfigItems {
int windowHeight = 500;
};
struct SortConfigItems {
SortType type = SortType::Date;
bool descending = true;
struct CacheConfigItems {
bool saveSortMethod = true;
bool savePalette = true;
int maxImageEntries = 1000;
static const QString defaultSortType;
static const QString defaultSortDescending;
static const QString defaultSelectedPalette;
};
} // namespace WallReel::Core::Config
+58 -47
View File
@@ -15,13 +15,25 @@
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& picturesDir,
const QStringList& searchDirs,
const QString& configPath,
bool disableActions,
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)
if (configPath.isEmpty()) {
WR_INFO(QString("Configuration directory: %1").arg(m_configDir.absolutePath()));
@@ -42,15 +54,12 @@ WallReel::Core::Config::Manager::Manager(
WR_INFO(QString("No search directories specified, using Pictures directory: %1").arg(picturesPath));
m_wallpaperConfig.dirs.append({picturesPath, true});
}
WR_DEBUG("Loading wallpapers ...");
_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));
QFile configFile(configPath);
if (!configFile.open(QIODevice::ReadOnly)) {
@@ -72,10 +81,10 @@ void WallReel::Core::Config::Manager::_loadConfig(const QString& configPath) {
_loadThemeConfig(jsonObj);
_loadActionConfig(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()) {
return;
}
@@ -121,14 +130,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()) {
return;
}
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()) {
return;
@@ -176,7 +182,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()) {
return;
}
@@ -194,12 +200,6 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
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()) {
const QJsonArray& arr = config["saveState"].toArray();
for (const auto& item : arr) {
@@ -209,8 +209,8 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
if (obj.contains("key") && obj["key"].isString()) {
sItem.key = obj["key"].toString();
}
if (obj.contains("default") && obj["default"].isString()) {
sItem.defaultVal = obj["default"].toString();
if (obj.contains("fallback") && obj["fallback"].isString()) {
sItem.defaultVal = obj["fallback"].toString();
}
if (obj.contains("command") && obj["command"].isString()) {
sItem.command = obj["command"].toString();
@@ -257,7 +257,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()) {
return;
}
@@ -295,36 +295,33 @@ void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root)
}
}
void WallReel::Core::Config::Manager::_loadSortConfig(const QJsonObject& root) {
if (!root.contains("sort") || !root["sort"].isObject()) {
void Manager::_loadCacheConfig(const QJsonObject& root) {
if (!root.contains("cache") || !root["cache"].isObject()) {
return;
}
const QJsonObject& config = root["sort"].toObject();
const QJsonObject& config = root["cache"].toObject();
if (config.contains("type")) {
const auto& val = config["type"];
if (val.isString()) {
QString type = val.toString().toLower();
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")) {
const auto& val = config["descending"];
if (config.contains("saveSortMethod")) {
const auto& val = config["saveSortMethod"];
if (val.isBool()) {
m_sortConfig.descending = val.toBool();
m_cacheConfig.saveSortMethod = val.toBool();
}
}
if (config.contains("savePalette")) {
const auto& val = config["savePalette"];
if (val.isBool()) {
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::scanWallpapers() {
m_wallpapers.clear();
// Add paths first using a set to avoid duplicates
@@ -389,7 +386,19 @@ void WallReel::Core::Config::Manager::_loadWallpapers() {
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) {
WR_WARN("State capture already in progress, ignoring new capture request");
return;
@@ -481,7 +490,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
m_actionConfig.savedState[key] = value;
m_pendingCaptures--;
@@ -489,3 +498,5 @@ void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const
emit stateCaptured();
}
}
} // namespace WallReel::Core::Config
+12 -5
View File
@@ -27,6 +27,7 @@ class Manager : public QObject {
* @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 picturesDir The pictures directory (default location for user wallpapers)
* @param disableActions Whether to disable actions
* @param parent QObject parent
*
* @note The constructor will load the configuration and scan for wallpapers immediately.
@@ -36,6 +37,7 @@ class Manager : public QObject {
const QDir& picturesDir,
const QStringList& searchDirs = {},
const QString& configPath = "",
bool disableActions = false,
QObject* parent = nullptr);
~Manager();
@@ -57,7 +59,9 @@ class Manager : public QObject {
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 {
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
@@ -68,6 +72,8 @@ class Manager : public QObject {
*/
Q_INVOKABLE void captureState();
void scanWallpapers();
signals:
void stateCaptured();
@@ -78,23 +84,24 @@ class Manager : public QObject {
void _loadThemeConfig(const QJsonObject& config);
void _loadActionConfig(const QJsonObject& config);
void _loadStyleConfig(const QJsonObject& config);
void _loadSortConfig(const QJsonObject& config);
// Load wallpapers
void _loadWallpapers();
void _loadCacheConfig(const QJsonObject& config);
// Callback for state capture results
void _onCaptureResult(const QString& key, const QString& value);
private:
const QDir m_configDir;
bool m_disableActions = false;
WallpaperConfigItems m_wallpaperConfig;
ThemeConfigItems m_themeConfig;
ActionConfigItems m_actionConfig;
StyleConfigItems m_styleConfig;
SortConfigItems m_sortConfig;
CacheConfigItems m_cacheConfig;
QStringList m_wallpapers;
int m_pendingCaptures = 0;
bool m_stateCaptured = false; // changed and accessed in main thread, no lock needed
};
} // 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_id = cacheMgr.cacheKey(m_file, m_targetSize);
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());
if (!reader.canRead()) {
WR_WARN("Cannot read cached image: " + m_cachedFile.absoluteFilePath());
return QImage();
+4 -3
View File
@@ -54,8 +54,11 @@ class Data {
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
bool m_isValid = false;
QImage computeImage() const;
QColor computeDominantColor(const QImage& image) const;
QImage loadImageFromCache() const;
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()); }
bool isValid() const { return m_cachedFile.exists(); }
bool isValid() const { return m_isValid; }
QString getFullPath() const { return m_file.absoluteFilePath(); }
@@ -87,8 +90,6 @@ class Data {
const QFileInfo& getFileInfo() const { return m_file; }
QImage loadImage() const;
const QColor& getDominantColor() const { return m_dominantColor; }
std::optional<QString> getCachedColor(const QString& paletteName) const {
+22
View File
@@ -9,10 +9,12 @@
WALLREEL_DECLARE_SENDER("ImageManager")
WallReel::Core::Image::Manager::Manager(
Config::Manager& configMgr,
Cache::Manager& cacheMgr,
const QSize& thumbnailSize,
QObject* parent)
: QObject(parent),
m_configMgr(configMgr),
m_cacheMgr(cacheMgr),
m_thumbnailSize(thumbnailSize) {
m_dataModel = new Model(this);
@@ -38,6 +40,21 @@ WallReel::Core::Image::Manager::~Manager() {
m_watcher.waitForFinished();
}
void WallReel::Core::Image::Manager::loadAndProcess() {
if (m_isLoading) {
WR_WARN("Already loading images. Ignoring new load request.");
return;
}
m_isLoading = true;
emit isLoadingChanged();
_clearData();
m_configMgr.scanWallpapers();
const auto paths = m_configMgr.getWallpapers();
return _process(paths);
}
void WallReel::Core::Image::Manager::loadAndProcess(const QStringList& paths) {
if (m_isLoading) {
WR_WARN("Already loading images. Ignoring new load request.");
@@ -48,6 +65,10 @@ void WallReel::Core::Image::Manager::loadAndProcess(const QStringList& paths) {
_clearData();
return _process(paths);
}
void WallReel::Core::Image::Manager::_process(const QStringList& paths) {
m_processedCount = 0;
m_progressUpdateTimer.start(s_ProgressUpdateIntervalMs);
// These are all small objects so capturing by value should be fine
@@ -75,6 +96,7 @@ void WallReel::Core::Image::Manager::stop() {
void WallReel::Core::Image::Manager::_clearData() {
m_dataModel->clearData();
m_dataMap.clear();
}
void WallReel::Core::Image::Manager::_onProgressValueChanged(int value) {
+8
View File
@@ -8,6 +8,7 @@
#include <atomic>
#include "Cache/manager.hpp"
#include "Config/manager.hpp"
#include "data.hpp"
#include "model.hpp"
@@ -20,6 +21,7 @@ class Manager : public QObject {
// Constructor / Destructor
Manager(
Config::Manager& configMgr,
Cache::Manager& cacheMgr,
const QSize& thumbnailSize,
QObject* parent = nullptr);
@@ -32,6 +34,8 @@ class Manager : public QObject {
int processedCount() const { return m_processedCount.load(std::memory_order_relaxed); }
// Total count of processing items, NOT the count of items in the model
// (Why did I name this method like this? idk)
int totalCount() const { return m_watcher.progressMaximum(); }
void setSortType(Config::SortType type) { m_proxyModel->setSortType(type); }
@@ -46,6 +50,8 @@ class Manager : public QObject {
QString searchText() const { return m_proxyModel->getSearchText(); }
void loadAndProcess();
void loadAndProcess(const QStringList& paths);
void stop();
@@ -59,6 +65,7 @@ class Manager : public QObject {
private:
void _clearData();
void _process(const QStringList& paths);
signals:
// Properties
@@ -75,6 +82,7 @@ class Manager : public QObject {
ProxyModel* m_proxyModel;
QHash<QString, Data*> m_dataMap;
Config::Manager& m_configMgr;
Cache::Manager& m_cacheMgr;
QSize m_thumbnailSize;
-13
View File
@@ -49,19 +49,6 @@ WallReel::Core::Palette::Manager::Manager(
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) {
+16 -1
View File
@@ -42,8 +42,23 @@ class Manager : public QObject {
void setSelectedPalette(const QVariant& paletteVar) {
if (paletteVar.isNull() || !paletteVar.isValid()) {
m_selectedPalette = std::nullopt;
} else {
} else if (paletteVar.canConvert<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;
emit selectedPaletteChanged();
+1 -1
View File
@@ -10,7 +10,7 @@ WALLREEL_DECLARE_SENDER("PaletteMatchColor")
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()) {
WR_WARN("No candidates or invalid target color for palette matching");
static ColorItem emptyItem;
+2 -2
View File
@@ -10,9 +10,9 @@ namespace WallReel::Core::Palette {
*
* @param target
* @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
+82 -12
View File
@@ -10,6 +10,7 @@
#include "Service/manager.hpp"
#include "Utils/misc.hpp"
#include "appoptions.hpp"
#include "logger.hpp"
namespace WallReel::Core::Provider {
@@ -17,20 +18,25 @@ class Bootstrap {
friend class Carousel;
public:
Bootstrap(const AppOptions& options) {
cacheMgr = new Cache::Manager(Utils::getCacheDir());
Bootstrap(const AppOptions& options) : options(options) {
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) {
cacheMgr->clearCache();
return;
}
configMgr = new Config::Manager(
Utils::getConfigDir(),
Utils::getPicturesDir(),
options.appendDirs,
options.configPath);
imageMgr = new Image::Manager(
*configMgr,
*cacheMgr,
configMgr->getFocusImageSize());
@@ -40,19 +46,82 @@ class Bootstrap {
qRegisterMetaType<Palette::PaletteItem>("PaletteItem");
qRegisterMetaType<Palette::ColorItem>("ColorItem");
ServiceMgr = new Service::Manager(
serviceMgr = new Service::Manager(
configMgr->getActionConfig(),
*imageMgr,
*paletteMgr);
*paletteMgr,
options.disableActions);
}
void start() {
cacheMgr->evictOldEntries();
configMgr->captureState();
imageMgr->loadAndProcess(configMgr->getWallpapers());
imageMgr->loadAndProcess();
}
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() {
delete ServiceMgr;
delete serviceMgr;
delete paletteMgr;
delete imageMgr;
delete configMgr;
@@ -60,11 +129,12 @@ class Bootstrap {
}
private:
const AppOptions& options;
Cache::Manager* cacheMgr{};
Config::Manager* configMgr{};
Image::Manager* imageMgr{};
Palette::Manager* paletteMgr{};
Service::Manager* ServiceMgr{};
Service::Manager* serviceMgr{};
};
} // namespace WallReel::Core::Provider
+50 -18
View File
@@ -1,10 +1,10 @@
#ifndef WALLREEL_PROVIDER_CAROUSEL_HPP
#define WALLREEL_PROVIDER_CAROUSEL_HPP
#include <qapplication.h>
#include <QApplication>
#include "Cache/manager.hpp"
#include "Cache/types.hpp"
#include "Config/data.hpp"
#include "Config/manager.hpp"
#include "Image/manager.hpp"
@@ -146,10 +146,6 @@ class Carousel : public QObject {
signals:
void isProcessingChanged();
void selectCompleted();
void previewCompleted();
void restoreCompleted();
void cancelCompleted();
// Other states
@@ -175,6 +171,10 @@ class Carousel : public QObject {
}
}
Q_INVOKABLE void requestReload() {
m_imageMgr->loadAndProcess();
}
signals:
void currentImageIdChanged();
void currentIndexChanged();
@@ -190,10 +190,11 @@ class Carousel : public QObject {
Bootstrap& bootstrap,
QObject* parent = nullptr)
: QObject(parent),
m_cacheMgr(bootstrap.cacheMgr),
m_configMgr(bootstrap.configMgr),
m_imageMgr(bootstrap.imageMgr),
m_paletteMgr(bootstrap.paletteMgr),
m_serviceMgr(bootstrap.ServiceMgr) {
m_serviceMgr(bootstrap.serviceMgr) {
// Simply forward signals
connect(m_imageMgr, &Image::Manager::isLoadingChanged, this, &Carousel::isLoadingChanged);
connect(m_imageMgr, &Image::Manager::processedCountChanged, this, &Carousel::processedCountChanged);
@@ -203,10 +204,6 @@ class Carousel : public QObject {
connect(m_paletteMgr, &Palette::Manager::colorChanged, this, &Carousel::colorChanged);
connect(m_paletteMgr, &Palette::Manager::colorNameChanged, this, &Carousel::colorNameChanged);
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
// to call it multiple times in a short period, and it simplifies the code a lot :)
@@ -248,19 +245,25 @@ class Carousel : public QObject {
}
});
// Defer preview until state captured
connect(m_configMgr,
&Config::Manager::stateCaptured,
m_serviceMgr,
&Service::Manager::onStateCaptured);
// Quit on selected
if (m_configMgr->getActionConfig().quitOnSelected) {
QObject::connect(
this,
&Provider::Carousel::selectCompleted,
m_serviceMgr,
&Service::Manager::selectCompleted,
app,
&QApplication::quit,
Qt::QueuedConnection);
}
// Quit on cancel
QObject::connect(
this,
&Provider::Carousel::cancelCompleted,
m_serviceMgr,
&Service::Manager::cancelCompleted,
app,
&QApplication::quit,
Qt::QueuedConnection);
@@ -273,12 +276,41 @@ class Carousel : public QObject {
&Service::Manager::restoreOnQuit);
}
// Initial value of sort method
setSortType(m_configMgr->getSortConfig().type);
setSortDescending(m_configMgr->getSortConfig().descending);
// Restore last state if configured
// and store state on change if configured
// 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:
Cache::Manager* m_cacheMgr;
Config::Manager* m_configMgr;
Image::Manager* m_imageMgr;
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 "Palette/manager.hpp"
#include "Service/wallpaper.hpp"
#include "logger.hpp"
namespace WallReel::Core::Service {
@@ -22,16 +21,8 @@ class Manager : public QObject {
const Config::ActionConfigItems& actionConfig,
Image::Manager& imageManager,
Palette::Manager& paletteManager,
QObject* parent = nullptr) : m_actionConfig(actionConfig), m_imageManager(imageManager), m_paletteManager(paletteManager) {
m_wallpaperService = new WallpaperService(m_actionConfig, m_paletteManager, 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);
}
bool disableActions = false,
QObject* parent = nullptr);
bool isProcessing() const { return m_isProcessing; }
@@ -39,102 +30,49 @@ class Manager : public QObject {
public slots:
void selectWallpaper(const QString& id) {
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 onStateCaptured();
void restore() {
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 selectWallpaper(const QString& id);
void cancel() {
Logger::debug("ServiceManager", "Cancel action");
m_wallpaperService->stopAll();
emit cancelCompleted();
}
void restore();
void previewWallpaper(const QString& id) {
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 cancel();
void restoreOnQuit() {
if (m_hasSelected) {
Logger::debug("ServiceManager", "Quit with selected wallpaper, no need to restore");
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();
}
void previewWallpaper(const QString& id);
void restoreOnQuit();
private slots:
void _onSelectCompleted() {
Logger::debug("ServiceManager", "Select completed");
_onProcessCompleted();
m_hasSelected = true;
emit selectCompleted();
}
void _onSelectCompleted(bool success);
void _onRestoreCompleted() {
Logger::debug("ServiceManager", "Restore completed");
_onProcessCompleted();
emit restoreCompleted();
}
void _onRestoreCompleted(bool success);
void _onProcessCompleted() {
m_isProcessing = false;
emit isProcessingChanged();
}
void _onProcessCompleted();
signals:
void isProcessingChanged();
void selectCompleted();
void previewCompleted();
void restoreCompleted();
void selectCompleted(bool success);
void previewCompleted(bool success);
void restoreCompleted(bool success);
void cancelCompleted();
private:
QString _renderCommand(const QString& templateStr, const QHash<QString, QString>& variables) const;
QHash<QString, QString> _generateVariables(const Image::Data& imageData) const;
private:
WallpaperService* m_wallpaperService;
const Config::ActionConfigItems& m_actionConfig;
Image::Manager& m_imageManager;
Palette::Manager& m_paletteManager;
bool m_disableActions;
bool m_isProcessing = false;
bool m_hasSelected = false;
bool m_stateCaptured = false;
QString m_pendingPreviewId;
};
} // namespace WallReel::Core::Service
+81 -93
View File
@@ -1,54 +1,103 @@
#include "Service/wallpaper.hpp"
#include <QColor>
#include <iostream>
#include <qprocess.h>
#include <QColor>
#include "Utils/texttemplate.hpp"
#include "logger.hpp"
WALLREEL_DECLARE_SENDER("WallpaperService")
WallReel::Core::Service::WallpaperService::WallpaperService(
const Config::ActionConfigItems& actionConfig,
const Palette::Manager& paletteManager,
QObject* parent)
: QObject(parent), m_actionConfig(actionConfig), m_paletteManager(paletteManager) {
namespace WallReel::Core::Service {
WallpaperService::WallpaperService(int previewDebounceTime, QObject* parent)
: QObject(parent) {
m_previewDebounceTimer = new QTimer(this);
m_previewDebounceTimer->setSingleShot(true);
m_previewDebounceTimer->setInterval(m_actionConfig.previewDebounceTime);
m_previewDebounceTimer->setInterval(previewDebounceTime);
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);
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,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
WR_DEBUG(QString("Preview process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
emit previewCompleted();
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
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);
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,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
WR_DEBUG(QString("Select process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
emit selectCompleted();
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
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);
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,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
WR_DEBUG(QString("Restore process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
emit restoreCompleted();
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
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");
if (m_previewProcess->state() != QProcess::NotRunning) {
m_previewProcess->kill();
@@ -65,74 +114,32 @@ void WallReel::Core::Service::WallpaperService::stopAll() {
m_previewDebounceTimer->stop();
}
void WallReel::Core::Service::WallpaperService::preview(const Image::Data& imageData) {
m_pendingImageData = &imageData;
void WallpaperService::preview(const QString& command) {
m_pendingPreviewCommand = command;
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) {
WR_WARN("Previous select command is still running. Ignoring new command.");
return;
}
WR_DEBUG(QString("Select wallpaper: %1").arg(imageData.getFullPath()));
_doSelect(imageData);
_doSelect(command);
}
void WallReel::Core::Service::WallpaperService::restore() {
void WallpaperService::restore(const QString& command) {
if (m_restoreProcess->state() != QProcess::NotRunning) {
WR_WARN("Previous restore command is still running. Ignoring new command.");
return;
}
WR_DEBUG("Restore state");
_doRestore();
_doRestore(command);
}
QHash<QString, QString> WallReel::Core::Service::WallpaperService::_generateVariables(const Image::Data& imageData) {
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);
void WallpaperService::_doPreview(const QString& command) {
if (command.isEmpty()) {
WR_DEBUG("No preview command configured. Skipping preview action.");
emit previewCompleted();
emit previewCompleted(true);
return;
}
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);
}
void WallReel::Core::Service::WallpaperService::_doSelect(const Image::Data& imageData) {
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);
void WallpaperService::_doSelect(const QString& command) {
if (command.isEmpty()) {
WR_DEBUG("No select command configured. Skipping select action.");
emit selectCompleted();
emit selectCompleted(true);
return;
}
WR_DEBUG(QString("Executing select command: %1").arg(command));
m_selectProcess->start("sh", QStringList() << "-c" << command);
}
void WallReel::Core::Service::WallpaperService::_doRestore() {
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);
void WallpaperService::_doRestore(const QString& command) {
if (command.isEmpty()) {
WR_DEBUG("Restore command is empty after rendering. Skipping restore action.");
emit restoreCompleted();
WR_DEBUG("Restore command is empty. Skipping restore action.");
emit restoreCompleted(true);
return;
}
WR_DEBUG(QString("Executing restore command: %1").arg(command));
m_restoreProcess->start("sh", QStringList() << "-c" << command);
}
} // namespace WallReel::Core::Service
+11 -21
View File
@@ -4,43 +4,33 @@
#include <QProcess>
#include <QTimer>
#include "Config/data.hpp"
#include "Image/data.hpp"
#include "Palette/manager.hpp"
namespace WallReel::Core::Service {
class WallpaperService : public QObject {
Q_OBJECT
public:
WallpaperService(
const Config::ActionConfigItems& actionConfig,
const Palette::Manager& paletteManager,
QObject* parent = nullptr);
WallpaperService(int previewDebounceTime, QObject* parent = nullptr);
void stopAll();
public slots:
void preview(const Image::Data& imageData); // execute after 500ms of inactivity
void select(const Image::Data& imageData); // execute immediately, ignore if already running
void restore(); // execute immediately, ignore if already running
void preview(const QString& command); // execute after 500ms of inactivity
void select(const QString& command); // execute immediately, ignore if already running
void restore(const QString& command); // execute immediately, ignore if already running
signals:
void previewCompleted();
void selectCompleted();
void restoreCompleted();
void previewCompleted(bool success);
void selectCompleted(bool success);
void restoreCompleted(bool success);
private:
void _doPreview(const Image::Data& imageData);
void _doSelect(const Image::Data& imageData);
void _doRestore();
QHash<QString, QString> _generateVariables(const Image::Data& imageData);
void _doPreview(const QString& command);
void _doSelect(const QString& command);
void _doRestore(const QString& command);
const Config::ActionConfigItems& m_actionConfig;
const Palette::Manager& m_paletteManager;
QTimer* m_previewDebounceTimer;
const Image::Data* m_pendingImageData;
QString m_pendingPreviewCommand;
QProcess* m_previewProcess;
QProcess* m_selectProcess;
QProcess* m_restoreProcess;
+18
View File
@@ -179,6 +179,24 @@ inline QDir getPicturesDir() {
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
#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");
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.
// parser.process(...) will do something like exit(...) that will terminate
// the application brutally and produce unwanted warnings.
@@ -111,7 +117,7 @@ void AppOptions::parseArgs(QApplication& app) {
}
if (parser.isSet(configFileOption)) {
QString path = parser.value(configFileOption);
QString path = Utils::expandPath(parser.value(configFileOption));
if (Utils::checkFile(path)) {
configPath = path;
} else {
@@ -120,6 +126,21 @@ void AppOptions::parseArgs(QApplication& app) {
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
+2
View File
@@ -26,7 +26,9 @@ class AppOptions {
QString configPath;
QStringList appendDirs;
QString errorText;
QString applyPath; // -a --apply
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();
+1
View File
@@ -28,6 +28,7 @@ qt_add_qml_module(${UILIB_NAME}_Modules
Modules/ColorControl.qml
Modules/TopBar.qml
Modules/BottomBar.qml
Modules/ReloadButton.qml
)
qt_add_qml_module(${UILIB_NAME}_Components
STATIC
+17
View File
@@ -0,0 +1,17 @@
import QtQuick
import QtQuick.Controls
ToolButton {
id: reloadBtn
property bool isLoading: false
icon.name: "view-refresh"
icon.width: 16
icon.height: 16
focusPolicy: Qt.NoFocus
ToolTip.visible: hovered
ToolTip.delay: 600
ToolTip.text: "Reload from disk"
enabled: !isLoading
}
+9
View File
@@ -14,10 +14,12 @@ Item {
property alias availableSortTypes: sortCtrl.availableSortTypes
property alias selectedSortType: sortCtrl.selectedSortType
property alias isSortDescending: sortCtrl.isDescending
property alias isLoading: reloadBtn.isLoading
signal sortTypeSelected(string sortType)
signal sortDescendingToggled(bool descending)
signal searchDismissed()
signal reloadRequested()
function requestSearchFocus() {
searchBar.requestFocus();
@@ -63,6 +65,13 @@ Item {
}
}
ReloadButton {
id: reloadBtn
Layout.alignment: Qt.AlignVCenter
onClicked: root.reloadRequested()
}
}
}
+5
View File
@@ -32,6 +32,10 @@ Item {
root.forceActiveFocus();
}
function onReloadRequested() {
CarouselProvider.requestReload();
}
target: topBar
}
@@ -48,6 +52,7 @@ Item {
title: carousel.currentImageName
availableSortTypes: CarouselProvider.availableSortTypes
isSortDescending: CarouselProvider.sortDescending
isLoading: CarouselProvider.isLoading
onSortTypeSelected: (t) => {
return CarouselProvider.setSortType(t);
}
+52 -8
View File
@@ -1,9 +1,15 @@
#include <qapplication.h>
#include <qobject.h>
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QSocketNotifier>
extern "C" {
#include <signal.h>
#include <sys/signalfd.h>
#include <unistd.h>
}
#include "Core/Provider/bootstrap.hpp"
#include "Core/Provider/carousel.hpp"
@@ -20,14 +26,29 @@ int main(int argc, char* argv[]) {
// 1. QQmlApplicationEngine (with all QML objects)
// 2. provider (manages states and connections)
// 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);
a.setApplicationName(APP_NAME);
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;
a.setWindowIcon(QIcon(u":/%1.svg"_s.arg(APP_NAME)));
a.setWindowIcon(QIcon(u":/icon.svg"_s));
#else
a.setWindowIcon(QIcon(u":/%1.svg"_qs.arg(APP_NAME)));
#endif
@@ -35,6 +56,27 @@ int main(int argc, char* argv[]) {
{
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;
options.parseArgs(a);
@@ -48,6 +90,10 @@ int main(int argc, char* argv[]) {
return 0;
}
if (!options.applyPath.isEmpty()) {
return bootstrap.apply(options.applyPath) ? 0 : 1;
}
{
Provider::Carousel provider(&a, bootstrap);
qmlRegisterSingletonInstance(
@@ -66,13 +112,11 @@ int main(int argc, char* argv[]) {
[]() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
using namespace Qt::StringLiterals;
engine.loadFromModule(UIMODULE_URI, u"Main"_s);
#elif QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
engine.loadFromModule(UIMODULE_URI, u"Main"_qs);
#else
engine.addImportPath(u"qrc:/"_qs));
engine.addImportPath(u"qrc:/"_qs);
engine.load(QUrl(u"qrc:/WallReel/UI/Main.qml"_qs));
#endif
+17 -26
View File
@@ -47,11 +47,6 @@
"theme": {
"type": "object",
"properties": {
"defaultPalette": {
"type": "string",
"default": "",
"description": "Name of the default palette to use"
},
"palettes": {
"type": "array",
"items": {
@@ -101,11 +96,6 @@
"default": true,
"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": {
"type": "string",
"default": "",
@@ -126,10 +116,10 @@
"default": "",
"description": "Key of value to save, used as {{ key }} in onRestore command"
},
"default": {
"fallback": {
"type": "string",
"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": {
"type": "string",
@@ -153,7 +143,7 @@
},
"quitOnSelected": {
"type": "boolean",
"default": false,
"default": true,
"description": "Whether to quit the application after confirming a wallpaper"
},
"restoreOnClose": {
@@ -179,6 +169,7 @@
"image_focus_scale": {
"type": "number",
"default": 1.5,
"minimum": 1.0,
"description": "Scale of the focused image (relative to unfocused image)"
},
"window_width": {
@@ -193,23 +184,23 @@
}
}
},
"sort": {
"cache": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"name",
"date",
"size"
],
"default": "date",
"description": "Initial sorting type"
},
"descending": {
"saveSortMethod": {
"type": "boolean",
"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