Compare commits
24 Commits
66db6a9d5b
...
v2.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
740411f194
|
|||
| 1a2daec165 | |||
| 524b53b7b2 | |||
| b5ea96bb8b | |||
| 470bb1620a | |||
| b1372cacd7 | |||
| e59fba0689 | |||
| fe174ba2e0 | |||
| d8ab530fa8 | |||
| 07142eb19e | |||
| da3c0d6896 | |||
| 07d281d9f1 | |||
| 7fb0de38c9 | |||
|
5d4b50ebad
|
|||
|
3156e46c62
|
|||
|
23c80d30e9
|
|||
|
11c9c2f88d
|
|||
|
b06d27cecf
|
|||
|
5df0b53df0
|
|||
|
bf2f3d57c7
|
|||
|
1e9c175dd5
|
|||
|
da515566cb
|
|||
|
a6caa0c950
|
|||
|
807278d748
|
@@ -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
|
||||
@@ -81,3 +81,5 @@ CMakeLists.txt.user*
|
||||
.uic/
|
||||
/build*/
|
||||
.cache
|
||||
|
||||
.vscode
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(WallReel VERSION 2.0.0 LANGUAGES CXX)
|
||||
project(WallReel VERSION 2.0.2 LANGUAGES CXX)
|
||||
|
||||
set(EXECUTABLE_NAME "wallreel")
|
||||
set(CORELIB_NAME "wallreel-core")
|
||||
|
||||
@@ -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"/>
|
||||

|
||||
|
||||
## 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,30 +101,29 @@ 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"`). |
|
||||
| Property | Type | Default | Description |
|
||||
| :--------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `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`)
|
||||
|
||||
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). |
|
||||
| `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. |
|
||||
| `restoreOnClose` | Boolean | `true` | Run `onRestore` command if the application is closed without making a final selection. |
|
||||
| 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`, `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. |
|
||||
| `restoreOnClose` | Boolean | `true` | Run `onRestore` command if the application is closed without making a final selection. |
|
||||
|
||||
Available placeholders for `onSelected`, `onPreview` commands:
|
||||
|
||||
@@ -109,7 +136,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 +150,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). |
|
||||
| Property | Type | Default | Description |
|
||||
| :---------------- | :------ | :------ | :---------------------------------------------------------------------------- |
|
||||
| `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 +178,6 @@ Initial sorting behavior for loaded images.
|
||||
"excludes": ["\\.gif$"]
|
||||
},
|
||||
"theme": {
|
||||
"defaultPalette": "Dark",
|
||||
"palettes": [
|
||||
{
|
||||
"name": "Dark",
|
||||
@@ -169,7 +196,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 +210,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
|
||||
-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
|
||||
-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 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.
|
||||
|
||||
@@ -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
|
||||
@@ -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 |
@@ -0,0 +1,89 @@
|
||||
.\" Automatically generated by Pandoc 3.5
|
||||
.\"
|
||||
.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>
|
||||
@@ -0,0 +1,219 @@
|
||||
.\" Automatically generated by Pandoc 3.5
|
||||
.\"
|
||||
.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\[aq]s 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]printPreview\f[R] (boolean, default: \f[CR]false\f[R]) : Print
|
||||
previewed wallpaper path to stdout on preview.
|
||||
.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]false\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]\[dq]null\[dq]\f[R] if none).
|
||||
.PP
|
||||
\f[CR]{{ colorName }}\f[R] : Chosen primary color name
|
||||
(\f[CR]\[dq]null\[dq]\f[R] if none).
|
||||
.PP
|
||||
\f[CR]{{ colorHex }}\f[R] : Chosen primary color hex
|
||||
(\f[CR]\[dq]null\[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]: \[dq]https://raw.githubusercontent.com/Uyanide/WallReel/refs/heads/master/config.schema.json\[dq],
|
||||
\[dq]wallpaper\[dq]: {
|
||||
\[dq]paths\[dq]: [\[dq]/home/user/Pictures/favorite.jpg\[dq]],
|
||||
\[dq]dirs\[dq]: [
|
||||
{
|
||||
\[dq]path\[dq]: \[dq]/home/user/Pictures/Wallpapers\[dq],
|
||||
\[dq]recursive\[dq]: \f[B]true\f[R]
|
||||
}
|
||||
],
|
||||
\[dq]excludes\[dq]: [\[dq]\[rs]\[rs].gif$\[dq]]
|
||||
},
|
||||
\[dq]theme\[dq]: {
|
||||
\[dq]palettes\[dq]: [
|
||||
{
|
||||
\[dq]name\[dq]: \[dq]Dark\[dq],
|
||||
\[dq]colors\[dq]: [
|
||||
{ \[dq]name\[dq]: \[dq]blue\[dq], \[dq]value\[dq]: \[dq]#89b4fa\[dq] },
|
||||
{ \[dq]name\[dq]: \[dq]red\[dq], \[dq]value\[dq]: \[dq]#f38ba8\[dq] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
\[dq]action\[dq]: {
|
||||
\[dq]previewDebounceTime\[dq]: 500,
|
||||
\[dq]quitOnSelected\[dq]: \f[B]true\f[R],
|
||||
\[dq]onPreview\[dq]: \[dq]swww img {{ path }}\[dq],
|
||||
\[dq]onSelected\[dq]: \[dq]cp {{ path }} \[ti]/.config/wallpaper/current/ && swww img {{ path }}\[dq],
|
||||
\[dq]saveState\[dq]: [
|
||||
{
|
||||
\[dq]key\[dq]: \[dq]current_wp\[dq],
|
||||
\[dq]fallback\[dq]: \[dq]/home/user/Pictures/default.jpg\[dq],
|
||||
\[dq]command\[dq]: \[dq]find \[ti]/.config/wallpaper/current \-type f | head \-n 1\[dq],
|
||||
\[dq]timeout\[dq]: 1000
|
||||
}
|
||||
],
|
||||
\[dq]onRestore\[dq]: \[dq]swww img {{ current_wp }}\[dq]
|
||||
},
|
||||
\[dq]style\[dq]: {
|
||||
\[dq]image_width\[dq]: 640,
|
||||
\[dq]image_height\[dq]: 400,
|
||||
\[dq]image_focus_scale\[dq]: 1.2,
|
||||
\[dq]window_width\[dq]: 1280,
|
||||
\[dq]window_height\[dq]: 720
|
||||
},
|
||||
\[dq]cache\[dq]: {
|
||||
\[dq]saveSortMethod\[dq]: \f[B]true\f[R],
|
||||
\[dq]savePalette\[dq]: \f[B]true\f[R],
|
||||
\[dq]maxImageEntries\[dq]: 300
|
||||
}
|
||||
}
|
||||
.EE
|
||||
.SH SEE ALSO
|
||||
\f[B]wallreel\f[R](1)
|
||||
.SH AUTHOR
|
||||
Uyanide <github.com/Uyanide>
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+294
-33
@@ -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 ("
|
||||
" 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 ("
|
||||
" key TEXT PRIMARY KEY NOT NULL,"
|
||||
" file_name TEXT NOT NULL"
|
||||
")"));
|
||||
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,"
|
||||
" 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,"
|
||||
" 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
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
|
||||
All = ~0u
|
||||
None = 0,
|
||||
Image = 1, ///< Cache for processed images
|
||||
Color = 1 << 1, ///< Cache for dominant colors
|
||||
Settings = 1 << 2, ///< Cache for settings (simple key-value pairs)
|
||||
All = ~0u
|
||||
};
|
||||
|
||||
inline constexpr Type operator|(Type a, Type b) {
|
||||
@@ -28,6 +29,12 @@ inline constexpr Type operator&(Type a, Type b) {
|
||||
|
||||
using Data = std::variant<std::monostate, QFileInfo, QColor>;
|
||||
|
||||
enum class SettingsType : uint32_t {
|
||||
LastSelectedPalette = 0,
|
||||
LastSortType,
|
||||
LastSortDescending,
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Cache
|
||||
|
||||
#endif // WALLREEL_CACHE_TYPES_HPP
|
||||
|
||||
@@ -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
|
||||
@@ -33,9 +32,9 @@
|
||||
// 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.restoreOnClose boolean true Whether to run the restore command after closing the application without confirming a wallpaper
|
||||
@@ -46,11 +45,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 +61,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 +109,6 @@ struct ThemeConfigItems {
|
||||
};
|
||||
|
||||
QList<PaletteConfigItem> palettes;
|
||||
QString defaultPalette;
|
||||
};
|
||||
|
||||
struct ActionConfigItems {
|
||||
@@ -143,9 +139,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
|
||||
|
||||
@@ -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()));
|
||||
@@ -47,10 +59,10 @@ WallReel::Core::Config::Manager::Manager(
|
||||
_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 +84,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 +133,11 @@ void WallReel::Core::Config::Manager::_loadWallpaperConfig(const QJsonObject& ro
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadThemeConfig(const QJsonObject& root) {
|
||||
void Manager::_loadThemeConfig(const QJsonObject& root) {
|
||||
if (!root.contains("theme") || !root["theme"].isObject()) {
|
||||
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 +185,7 @@ void WallReel::Core::Config::Manager::_loadThemeConfig(const QJsonObject& root)
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root) {
|
||||
void Manager::_loadActionConfig(const QJsonObject& root) {
|
||||
if (!root.contains("action") || !root["action"].isObject()) {
|
||||
return;
|
||||
}
|
||||
@@ -209,8 +218,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 +266,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 +304,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("saveSortMethod")) {
|
||||
const auto& val = config["saveSortMethod"];
|
||||
if (val.isBool()) {
|
||||
m_cacheConfig.saveSortMethod = val.toBool();
|
||||
}
|
||||
}
|
||||
if (config.contains("descending")) {
|
||||
const auto& val = config["descending"];
|
||||
if (config.contains("savePalette")) {
|
||||
const auto& val = config["savePalette"];
|
||||
if (val.isBool()) {
|
||||
m_sortConfig.descending = val.toBool();
|
||||
m_cacheConfig.savePalette = val.toBool();
|
||||
}
|
||||
}
|
||||
if (config.contains("maxImageEntries")) {
|
||||
const auto& val = config["maxImageEntries"];
|
||||
if (val.isDouble() && val.toDouble() > 0) {
|
||||
m_cacheConfig.maxImageEntries = val.toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadWallpapers() {
|
||||
void Manager::_loadWallpapers() {
|
||||
m_wallpapers.clear();
|
||||
|
||||
// Add paths first using a set to avoid duplicates
|
||||
@@ -389,7 +395,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 +499,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 +507,5 @@ void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const
|
||||
emit stateCaptured();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace WallReel::Core::Config
|
||||
|
||||
@@ -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;
|
||||
@@ -78,7 +82,7 @@ class Manager : public QObject {
|
||||
void _loadThemeConfig(const QJsonObject& config);
|
||||
void _loadActionConfig(const QJsonObject& config);
|
||||
void _loadStyleConfig(const QJsonObject& config);
|
||||
void _loadSortConfig(const QJsonObject& config);
|
||||
void _loadCacheConfig(const QJsonObject& config);
|
||||
// Load wallpapers
|
||||
void _loadWallpapers();
|
||||
// Callback for state capture results
|
||||
@@ -86,15 +90,18 @@ class Manager : public QObject {
|
||||
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Service/manager.hpp"
|
||||
#include "Utils/misc.hpp"
|
||||
#include "appoptions.hpp"
|
||||
#include "logger.hpp"
|
||||
|
||||
namespace WallReel::Core::Provider {
|
||||
|
||||
@@ -17,18 +18,22 @@ 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(
|
||||
*cacheMgr,
|
||||
@@ -40,19 +45,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());
|
||||
}
|
||||
|
||||
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 +128,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -190,10 +186,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 +200,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 +241,25 @@ class Carousel : public QObject {
|
||||
}
|
||||
});
|
||||
|
||||
// Defer preview until state captured
|
||||
connect(m_configMgr,
|
||||
&Config::Manager::stateCaptured,
|
||||
m_serviceMgr,
|
||||
&Service::Manager::onStateCaptured);
|
||||
|
||||
// Quit on selected
|
||||
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 +272,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;
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "manager.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;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,8 +26,10 @@ class AppOptions {
|
||||
QString configPath;
|
||||
QStringList appendDirs;
|
||||
QString errorText;
|
||||
bool clearCache = false; // -C --clear-cache
|
||||
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
|
||||
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();
|
||||
void parseArgs(QApplication& app);
|
||||
|
||||
+52
-8
@@ -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(
|
||||
¬ifier,
|
||||
&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
|
||||
|
||||
|
||||
+16
-20
@@ -47,11 +47,6 @@
|
||||
"theme": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defaultPalette": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Name of the default palette to use"
|
||||
},
|
||||
"palettes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -126,10 +121,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",
|
||||
@@ -179,6 +174,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 +189,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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,233 @@
|
||||
---
|
||||
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.
|
||||
|
||||
`printPreview` (boolean, default: `false`)
|
||||
: Print previewed wallpaper path to stdout on preview.
|
||||
|
||||
`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: `false`)
|
||||
: 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 |
Reference in New Issue
Block a user