commit 0d840f0d50d528e42c3c2fa388b2ccbf15ccd045 Author: Vyn Date: Sun Jun 2 20:03:02 2024 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9785597 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +.cache diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..328950e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.29.3) + +project( + SwayWallpaper + VERSION 1.0 + LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_COMPILE_WARNING_AS_ERROR ON) + +add_executable(sway-wallpaper + src/main.cpp + src/Wallpapers.cpp + src/SwaybgProcess.cpp +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ed2fdc --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Sway Wallpaper + +Display wallpapers on Sway, this is essentially a wrapper for `swaybg`. + +## Features + +- Specify a directory containing all your wallpapers +- Specify for which display +- Set interval to automatically change wallpaper after some time +- Wallpapers can be chosen randomly +- Wallpapers can be chosen in alphabetical order +- No "grey" flash when switching wallpapers + +## Usage + +```sh +usage: sway-wallpaper [options] + +required: + output The outputs as shown with the command `swaymsg -t get_outputs` + path The path to the directory containing the wallpapers + +options: + --always-random if set, always randomize the next wallpaper + -h, --help Show help + -i, --interval Specify the interval between + --never-random if set, wallpapers order is based on the names of the files +``` + +## Installation + +First, clone this repository `git clone https://codeberg.org/vyn/sway-wallpaper.git` (or download it the way you prefer). + +You only need **gcc** and **cmake**, run these commands in the project directory: + +``` +mkdir build +cd build +cmake .. +make +``` + +The executable should be located in `build/sway-wallpaper` diff --git a/src/CliArguments.h b/src/CliArguments.h new file mode 100644 index 0000000..c2de5a9 --- /dev/null +++ b/src/CliArguments.h @@ -0,0 +1,225 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CliArguments { + + enum Type { + String, + Int, + Bool + }; + + struct Argument { + std::vector aliases; + std::string description; + Type type = String; + short order = 0; + bool required = false; + bool direct = false; + }; + + class CliArguments { + public: + + CliArguments(int argc, char** argv, std::map arguments) + : argc_(argc), argv_(argv), arguments_(arguments) { + registerArgs(argc, argv, arguments); + } + + std::string getString(const std::string& argName) const { + auto value = argumentsValue_.find(argName); + if (value == argumentsValue_.end()) { + throw std::logic_error("trying to access non existant argument '" + argName + "'"); + } + return value->second; + } + + int getInt(const std::string& argName) const { + auto valueStr = getString(argName); + try { + return std::stoi(valueStr); + } catch (std::exception& e) { + throw std::logic_error("trying to access argument '" + argName + "' (value: '" + valueStr + "')" + " as integer"); + } + } + + int isInt(const std::string& argName) const { + auto valueStr = getString(argName); + for (const auto& ch : valueStr) { + if (!isdigit(ch)) { + return false; + } + } + return true; + } + + bool getBool(const std::string& argName) const { + auto valueStr = getString(argName); + if (valueStr != "true" && valueStr != "false") { + throw std::logic_error("trying to access argument '" + argName + "' (value: '" + valueStr + "')" + " as bool"); + } + return valueStr == "true"; + } + + int isBool(const std::string& argName) const { + auto valueStr = getString(argName); + return valueStr == "true" || valueStr == "false"; + } + + bool exists(const std::string& argName) const { + auto value = argumentsValue_.find(argName); + return value != argumentsValue_.end(); + } + + void printDebug() const { + for (const auto& [key, arg] : arguments_) { + std::string value = "-"; + if (argumentsValue_.find(key) != argumentsValue_.end()) { + value = argumentsValue_.find(key)->second; + } + std::cout << key << ": " << value << std::endl; + } + } + + void printHelp() const { + int longestArgLength = 0; + for (const auto& [key, arg] : arguments_) { + if (key.length() > longestArgLength) { + longestArgLength = key.length(); + } + } + std::cout << "usage: sway-wallpaper [options]" << std::endl; + for (const auto& arg : directArguments_) { + std::cout << " <" << arg << ">"; + } + std::cout << std::endl; + std::cout << "required:" << std::endl; + + for (const auto& [key, arg] : arguments_) { + if (!arg.direct) { + continue; + } + std::cout << std::left << std::setw(longestArgLength + 10) << (" " + key) << arg.description << std::endl; + } + + std::cout << std::endl; + std::cout << "options:" << std::endl; + + for (const auto& [key, arg] : arguments_) { + if (arg.direct) { + continue; + } + std::string argWithAliases = " "; + for (const auto& alias : arg.aliases) { + argWithAliases += "-" + alias + ", "; + } + argWithAliases += "--" + key; + + std::cout << std::left << std::setw(longestArgLength + 10) << argWithAliases; + std::cout << arg.description << std::endl; + } + } + + bool isValid() const { + return error_ == ""; + } + + const std::string& error() const { + return error_; + } + + private: + + void registerArgs(int argc, char** argv, std::map arguments) { + for (auto& [key, arg] : arguments) { + if (arg.direct) { + directArguments_.push_back(key); + } + for (auto& alias : arg.aliases) { + aliases_[alias] = key; + } + } + std::ranges::sort(directArguments_, [&](const std::string& s1, const std::string& s2) { + return arguments_[s1].order < arguments_[s2].order; + }); + + int currentDirectFieldIndex = 0; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg.starts_with("-")) { + auto argName = extractArgName(arg); + if (!argName.has_value()) { + setError("'" + arg + "' is not a valid argument"); + return; + } + if (arguments_[argName.value()].type == Bool) { + argumentsValue_[argName.value()] = "true"; + return; + } + ++i; + if (i >= argc) { + setError("Missing value for argument '" + arg + "'"); + return; + } + std::string argValue = std::string(argv[i]); + argumentsValue_[argName.value()] = argValue; + } else if (currentDirectFieldIndex >= directArguments_.size()) { + setError("Too many argument, unknown '" + arg + "'"); + return; + } else { + std::string& argName = directArguments_[currentDirectFieldIndex]; + argumentsValue_[argName] = arg; + ++currentDirectFieldIndex; + } + } + + if (currentDirectFieldIndex != directArguments_.size()) { + setError("Missing required arguments"); + } + } + + std::optional extractArgName(const std::string& arg) const { + if (arg.length() > 2 && arg.starts_with("--")) { + std::string argName = arg.substr(2); + if (arguments_.find(argName) == arguments_.end()) { + return std::nullopt; + } + return argName; + } else if (arg.length() == 2 && arg.starts_with("-")) { + std::string argAlias = arg.substr(1); + const auto& alias = aliases_.find(argAlias); + if (alias == aliases_.end()) { + return std::nullopt; + } + std::string argName = alias->second; + return argName; + } + return std::nullopt; + } + + void setError(std::string error) { + error_ = error; + } + + std::string error_; + int argc_; + char** argv_; + std::vector directArguments_; + std::map arguments_; + std::map argumentsValue_; + std::map aliases_; + }; +} diff --git a/src/SwaybgProcess.cpp b/src/SwaybgProcess.cpp new file mode 100644 index 0000000..972a1f9 --- /dev/null +++ b/src/SwaybgProcess.cpp @@ -0,0 +1,57 @@ +#include "SwaybgProcess.h" +#include +#include + + +std::vector processes; + +void signalHandler(int sig) { + std::cout << "received signal: " << sig << std::endl; + for (const auto& process : processes) { + process->kill(); + } + exit(1); +} + +SwaybgProcess::SwaybgProcess(const std::string& wallpaperPath, const std::string& output) : wallpaperPath(wallpaperPath), output(output) { + static bool signalHandlerCreated = false; + if (!signalHandlerCreated) { + signalHandlerCreated = true; + signal(SIGTERM, signalHandler); + } + processes.push_back(this); +} + +SwaybgProcess::~SwaybgProcess() { processes.erase(std::ranges::find(processes, this)); } + +void SwaybgProcess::exec() { + if (pipe(fds) == -1) { + throw std::runtime_error("Can't create pipe"); + } + pid = fork(); + if (pid == 0) { + dup2(fds[1], STDOUT_FILENO); + close(fds[0]); + close(fds[1]); + execl("/bin/swaybg", "/bin/swaybg" , "-o", output.c_str(), "-i", wallpaperPath.c_str(), "-m", "fill", (char*)0) ; + } else if (pid > 0) { + killed = false; + close(fds[1]); + //int nbytes = read(fds[0], buf, sizeof(buf)); + //printf("Output: (%.*s)\n", nbytes, buf); + } else { + throw std::runtime_error("Can't fork"); + } +} + +void SwaybgProcess::kill() { + if (killed) { + return; + } + killed = true; + ::kill(pid, SIGKILL); + int status; + ::waitpid(pid, &status, 0); +} + + diff --git a/src/SwaybgProcess.h b/src/SwaybgProcess.h new file mode 100644 index 0000000..5af2c21 --- /dev/null +++ b/src/SwaybgProcess.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include +#include + +#define SWAYBGPROCESS_BUFFER_SIZE 4094 + +class SwaybgProcess { + public: + + SwaybgProcess(const std::string& wallpaperPath, const std::string& output); + + ~SwaybgProcess(); + + void exec(); + + void kill(); + + private: + + std::string wallpaperPath; + std::string output; + pid_t pid; + bool killed = true; + + int fds[2]; // file descriptors for pipe + char buf[SWAYBGPROCESS_BUFFER_SIZE]; + ssize_t nbytes; +}; + diff --git a/src/Wallpapers.cpp b/src/Wallpapers.cpp new file mode 100644 index 0000000..5471b22 --- /dev/null +++ b/src/Wallpapers.cpp @@ -0,0 +1,27 @@ +#include "Wallpapers.h" +#include +#include +#include + +Wallpapers::Wallpapers(const std::string& directoryPath) : directoryPath(directoryPath) { + + for (const auto& entry : std::filesystem::directory_iterator(directoryPath)) { + wallpapersPath.push_back(entry.path()); + } + for (const std::string& wallpaperPath : wallpapersPath) { + std::cout << wallpaperPath << std::endl; + } + e1 = std::default_random_engine(r()); + uniformDist = std::uniform_int_distribution(0, wallpapersPath.size()); +} + +void Wallpapers::shuffle() { + wallpapersPathQueue = wallpapersPath; + std::ranges::shuffle(wallpapersPathQueue, e1); +} + +const std::string& Wallpapers::next() { + const std::string& wallpaperPath = wallpapersPathQueue[currentWallpaperIndex]; + currentWallpaperIndex = (currentWallpaperIndex + 1) % wallpapersPathQueue.size(); + return wallpaperPath; +} diff --git a/src/Wallpapers.h b/src/Wallpapers.h new file mode 100644 index 0000000..3294f9e --- /dev/null +++ b/src/Wallpapers.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include + +class Wallpapers { + public: + + Wallpapers(const std::string& directoryPath); + + void shuffle(); + + const std::string& next(); + + private: + + std::string directoryPath; + std::vector wallpapersPath; + std::vector wallpapersPathQueue; + + std::random_device r; + std::default_random_engine e1; + std::uniform_int_distribution uniformDist; + int currentWallpaperIndex = 0; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..550a6f4 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,91 @@ +#include "CliArguments.h" +#include "Wallpapers.h" +#include "SwaybgProcess.h" +#include +#include +#include + +int main(int argc, char** argv, char** envp) { + + CliArguments::CliArguments arguments{ + argc, argv, + { + {"path", { + .description = "The path to the directory containing the wallpapers", + .type = CliArguments::String, + .order = 0, + .direct = true + }}, + {"output", { + .description = "The outputs as shown with the command `swaymsg -t get_outputs`", + .type = CliArguments::String, + .order = 1, + .direct = true + }}, + {"interval", { + .aliases = {"i"}, + .description = "Specify the interval between", + .type = CliArguments::Int, + }}, + {"never-random", { + .description = "if set, wallpapers order is based on the names of the files", + .type = CliArguments::Bool, + }}, + {"always-random", { + .description = "if set, always randomize the next wallpaper", + .type = CliArguments::Bool, + }}, + {"help", { + .aliases = {"h"}, + .description = "Show help", + .type = CliArguments::Bool, + }}, + } + }; + + if (!arguments.isValid()) { + std::cout << arguments.error() << std::endl; + arguments.printHelp(); + return 1; + } + + if (arguments.exists("help")) { + arguments.printHelp(); + return 0; + } + + const std::string path = arguments.getString("path"); + const std::string output = arguments.getString("output"); + + Wallpapers wallpapers(path); + + if (!arguments.exists("never-random")) { + wallpapers.shuffle(); + } + + SwaybgProcess swayBgProcess(wallpapers.next(), output); + swayBgProcess.exec(); + + if (arguments.exists("interval")) { + int interval = arguments.getInt("interval"); + while (true) { + sleep(interval); + if (arguments.exists("always-random")) { + wallpapers.shuffle(); + } + SwaybgProcess newSwayBgProcess(wallpapers.next(), output); + newSwayBgProcess.exec(); + sleep(1); + swayBgProcess.kill(); + swayBgProcess = newSwayBgProcess; + } + } else { + char ch; + while (ch != 'q') { + ch = std::cin.get(); + } + } + + swayBgProcess.kill(); + return 0; +}