diff --git a/README.md b/README.md
index 491a9d1..1d4e742 100644
--- a/README.md
+++ b/README.md
@@ -50,9 +50,11 @@ It might not be that worthy to build a Qt application from ground for such a sma
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`.
```bash
- cmake --install build
+ cmake --install build --strip
```
+ `--strip` option is used to reduce the binary size by removing symbol information, which is generally not needed for normal usage.
+
## 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.
@@ -203,6 +205,7 @@ Options:
-q, --quiet Suppress all log output
-d, --append-dir
Append an additional wallpaper search directory
-c, --config-file Specify a custom configuration file
+ -a, --apply Apply the specified image as wallpaper and exit
```
A few things to notice:
@@ -212,3 +215,5 @@ A few things to notice:
- 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.
+
+- Given `--apply`, the config file (default or specified with `--config-file`) will be parsed, and the `onSelected` action will be executed with the properties of the specified image available for placeholders. If `savePalette` is enabled and a palette is selected in the last session, `palette`, `colorName` and `colorHex` placeholders will also be available. `saveState` commands will also be executed to fetch states for placeholders. After the action is executed, the application will exit immediately without showing the UI. This allows you to use WallReel as a command-line wallpaper setter that also supports palette-based theming and state management.
diff --git a/WallReel/Core/Palette/matchcolor.cpp b/WallReel/Core/Palette/matchcolor.cpp
index f3227e1..c520c89 100644
--- a/WallReel/Core/Palette/matchcolor.cpp
+++ b/WallReel/Core/Palette/matchcolor.cpp
@@ -10,7 +10,7 @@ WALLREEL_DECLARE_SENDER("PaletteMatchColor")
namespace WallReel::Core::Palette {
-const ColorItem& bestMatch(const QColor& target, const QList& candidates) {
+ColorItem bestMatch(const QColor& target, const QList& candidates) {
if (candidates.isEmpty() || !target.isValid()) {
WR_WARN("No candidates or invalid target color for palette matching");
static ColorItem emptyItem;
diff --git a/WallReel/Core/Palette/matchcolor.hpp b/WallReel/Core/Palette/matchcolor.hpp
index 9229ef7..21ef4cd 100644
--- a/WallReel/Core/Palette/matchcolor.hpp
+++ b/WallReel/Core/Palette/matchcolor.hpp
@@ -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& candidates);
+ColorItem bestMatch(const QColor& target, const QList& candidates);
} // namespace WallReel::Core::Palette
diff --git a/WallReel/Core/Provider/bootstrap.hpp b/WallReel/Core/Provider/bootstrap.hpp
index ea9cc83..4f4b4c9 100644
--- a/WallReel/Core/Provider/bootstrap.hpp
+++ b/WallReel/Core/Provider/bootstrap.hpp
@@ -10,6 +10,7 @@
#include "Service/manager.hpp"
#include "Utils/misc.hpp"
#include "appoptions.hpp"
+#include "logger.hpp"
namespace WallReel::Core::Provider {
@@ -43,7 +44,7 @@ class Bootstrap {
qRegisterMetaType("PaletteItem");
qRegisterMetaType("ColorItem");
- ServiceMgr = new Service::Manager(
+ serviceMgr = new Service::Manager(
configMgr->getActionConfig(),
*imageMgr,
*paletteMgr);
@@ -55,8 +56,64 @@ class Bootstrap {
imageMgr->loadAndProcess(configMgr->getWallpapers());
}
+ bool apply(const QString& path) {
+ 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;
@@ -68,7 +125,7 @@ class Bootstrap {
Config::Manager* configMgr{};
Image::Manager* imageMgr{};
Palette::Manager* paletteMgr{};
- Service::Manager* ServiceMgr{};
+ Service::Manager* serviceMgr{};
};
} // namespace WallReel::Core::Provider
diff --git a/WallReel/Core/Provider/carousel.hpp b/WallReel/Core/Provider/carousel.hpp
index 47aff48..72ef3a3 100644
--- a/WallReel/Core/Provider/carousel.hpp
+++ b/WallReel/Core/Provider/carousel.hpp
@@ -1,8 +1,6 @@
#ifndef WALLREEL_PROVIDER_CAROUSEL_HPP
#define WALLREEL_PROVIDER_CAROUSEL_HPP
-#include
-
#include
#include "Cache/manager.hpp"
@@ -148,10 +146,6 @@ class Carousel : public QObject {
signals:
void isProcessingChanged();
- void selectCompleted();
- void previewCompleted();
- void restoreCompleted();
- void cancelCompleted();
// Other states
@@ -196,7 +190,7 @@ class Carousel : public QObject {
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);
@@ -206,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 :)
@@ -260,16 +250,16 @@ class Carousel : public QObject {
// 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);
diff --git a/WallReel/Core/Service/manager.cpp b/WallReel/Core/Service/manager.cpp
index 0da5c59..3e45000 100644
--- a/WallReel/Core/Service/manager.cpp
+++ b/WallReel/Core/Service/manager.cpp
@@ -47,7 +47,8 @@ void Manager::selectWallpaper(const QString& id) {
WR_WARN(QString("No valid image data at id %1. Skipping select action.").arg(id));
m_isProcessing = false;
emit isProcessingChanged();
- emit selectCompleted();
+ emit selectCompleted(false);
+ return;
}
const auto command = _renderCommand(m_actionConfig.onSelected, _generateVariables(*data));
@@ -62,7 +63,7 @@ void Manager::restore() {
}
if (!m_stateCaptured) {
WR_DEBUG("State not captured yet, skipping restore action");
- emit restoreCompleted();
+ emit restoreCompleted(false);
return;
}
m_isProcessing = true;
@@ -81,7 +82,7 @@ void Manager::previewWallpaper(const QString& id) {
if (!m_stateCaptured) {
WR_DEBUG("State not captured yet, deferring preview for id " + id);
m_pendingPreviewId = id;
- emit previewCompleted();
+ emit previewCompleted(false);
return;
}
@@ -91,7 +92,7 @@ void Manager::previewWallpaper(const QString& id) {
if (!data || !data->isValid()) {
WR_WARN(QString("No valid image data at id %1. Skipping preview action.").arg(id));
- emit previewCompleted();
+ emit previewCompleted(false);
return;
}
@@ -106,23 +107,23 @@ void Manager::restoreOnQuit() {
Logger::debug("ServiceManager", "Restore on quit");
m_wallpaperService->stopAll();
QEventLoop loop;
- connect(m_wallpaperService, &WallpaperService::restoreCompleted, &loop, &QEventLoop::quit);
+ 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() {
+void Manager::_onSelectCompleted(bool success) {
Logger::debug("ServiceManager", "Select completed");
_onProcessCompleted();
- m_hasSelected = true;
- emit selectCompleted();
+ m_hasSelected = m_hasSelected || success;
+ emit selectCompleted(success);
}
-void Manager::_onRestoreCompleted() {
+void Manager::_onRestoreCompleted(bool success) {
Logger::debug("ServiceManager", "Restore completed");
_onProcessCompleted();
- emit restoreCompleted();
+ emit restoreCompleted(success);
}
void Manager::_onProcessCompleted() {
diff --git a/WallReel/Core/Service/manager.hpp b/WallReel/Core/Service/manager.hpp
index cfd25a3..e87b6d5 100644
--- a/WallReel/Core/Service/manager.hpp
+++ b/WallReel/Core/Service/manager.hpp
@@ -43,17 +43,17 @@ class Manager : public QObject {
private slots:
- void _onSelectCompleted();
+ void _onSelectCompleted(bool success);
- void _onRestoreCompleted();
+ void _onRestoreCompleted(bool success);
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:
diff --git a/WallReel/Core/Service/wallpaper.cpp b/WallReel/Core/Service/wallpaper.cpp
index bf53fee..500ef41 100644
--- a/WallReel/Core/Service/wallpaper.cpp
+++ b/WallReel/Core/Service/wallpaper.cpp
@@ -1,5 +1,7 @@
#include "Service/wallpaper.hpp"
+#include
+
#include
#include "logger.hpp"
@@ -17,31 +19,81 @@ WallpaperService::WallpaperService(int previewDebounceTime, QObject* parent)
_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::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::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::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);
});
}
@@ -87,7 +139,7 @@ void WallpaperService::restore(const QString& command) {
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));
@@ -102,7 +154,7 @@ void WallpaperService::_doPreview(const QString& command) {
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));
@@ -112,7 +164,7 @@ void WallpaperService::_doSelect(const QString& command) {
void WallpaperService::_doRestore(const QString& command) {
if (command.isEmpty()) {
WR_DEBUG("Restore command is empty. Skipping restore action.");
- emit restoreCompleted();
+ emit restoreCompleted(true);
return;
}
WR_DEBUG(QString("Executing restore command: %1").arg(command));
diff --git a/WallReel/Core/Service/wallpaper.hpp b/WallReel/Core/Service/wallpaper.hpp
index ebd0bbe..3785a79 100644
--- a/WallReel/Core/Service/wallpaper.hpp
+++ b/WallReel/Core/Service/wallpaper.hpp
@@ -20,9 +20,9 @@ class WallpaperService : public QObject {
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 QString& command);
diff --git a/WallReel/Core/appoptions.cpp b/WallReel/Core/appoptions.cpp
index acd14fd..a6528ea 100644
--- a/WallReel/Core/appoptions.cpp
+++ b/WallReel/Core/appoptions.cpp
@@ -67,6 +67,9 @@ void AppOptions::parseArgs(QApplication& app) {
QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file");
parser.addOption(configFileOption);
+ 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 +114,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 +123,17 @@ void AppOptions::parseArgs(QApplication& app) {
return;
}
}
+
+ 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
diff --git a/WallReel/Core/appoptions.hpp b/WallReel/Core/appoptions.hpp
index 91ceeb1..bc1ac51 100644
--- a/WallReel/Core/appoptions.hpp
+++ b/WallReel/Core/appoptions.hpp
@@ -26,6 +26,7 @@ class AppOptions {
QString configPath;
QStringList appendDirs;
QString errorText;
+ QString applyPath; // -a --apply
bool clearCache = false; // -C --clear-cache
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
diff --git a/WallReel/main.cpp b/WallReel/main.cpp
index e92e710..f91bbc9 100644
--- a/WallReel/main.cpp
+++ b/WallReel/main.cpp
@@ -48,6 +48,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(