aboutsummaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@gmail.com>2022-09-16 21:36:12 +0200
committerFelix Morgner <felix.morgner@gmail.com>2022-09-16 21:36:12 +0200
commit64922e213ac731279cf3341253e67509adb2dfc8 (patch)
treeddcf4fd5b05ac426ecf032e43e8662c2f9ce0f57 /source
parentd22bc7b557d36da41fe88d3188a7cd335c3ccaa0 (diff)
downloadwanda-64922e213ac731279cf3341253e67509adb2dfc8.tar.xz
wanda-64922e213ac731279cf3341253e67509adb2dfc8.zip
wanda: restructure source directory
Diffstat (limited to 'source')
-rw-r--r--source/.clang-format68
-rw-r--r--source/.gitignore1
-rw-r--r--source/CMakeLists.txt130
-rw-r--r--source/include/wanda/command.hpp46
-rw-r--r--source/include/wanda/commander.hpp66
-rw-r--r--source/include/wanda/control_connection.hpp114
-rw-r--r--source/include/wanda/control_interface.hpp96
-rw-r--r--source/include/wanda/deferred_failure.hpp21
-rw-r--r--source/include/wanda/environment.hpp61
-rw-r--r--source/include/wanda/expected.hpp256
-rw-r--r--source/include/wanda/filesystem.hpp39
-rw-r--r--source/include/wanda/keyed.hpp28
-rw-r--r--source/include/wanda/logging.hpp35
-rw-r--r--source/include/wanda/magic.hpp58
-rw-r--r--source/include/wanda/message.hpp83
-rw-r--r--source/include/wanda/optional.hpp65
-rw-r--r--source/include/wanda/setting.hpp156
-rw-r--r--source/include/wanda/type_wrapper.hpp47
-rw-r--r--source/include/wanda/wallpaper.hpp24
-rw-r--r--source/include/wanda/xdg.hpp40
-rw-r--r--source/src/wanda/command.cpp47
-rw-r--r--source/src/wanda/commander.cpp77
-rw-r--r--source/src/wanda/control_connection.cpp120
-rw-r--r--source/src/wanda/control_interface.cpp154
-rw-r--r--source/src/wanda/environment.cpp71
-rw-r--r--source/src/wanda/filesystem.cpp31
-rw-r--r--source/src/wanda/logging.cpp21
-rw-r--r--source/src/wanda/message.cpp75
-rw-r--r--source/src/wanda/setting.cpp102
-rw-r--r--source/src/wanda/wallpaper.cpp95
-rw-r--r--source/src/wanda/wandac.cpp94
-rw-r--r--source/src/wanda/wandad.cpp146
-rw-r--r--source/src/wanda/xdg.cpp46
-rw-r--r--source/tests/wanda/driver.cpp23
-rw-r--r--source/tests/wanda/test_suite_xdg.cpp96
35 files changed, 2632 insertions, 0 deletions
diff --git a/source/.clang-format b/source/.clang-format
new file mode 100644
index 0000000..b99e359
--- /dev/null
+++ b/source/.clang-format
@@ -0,0 +1,68 @@
+---
+AccessModifierOffset: '-2'
+AlignAfterOpenBracket: Align
+AlignConsecutiveAssignments: 'false'
+AlignConsecutiveDeclarations: 'false'
+AlignEscapedNewlines: Left
+AlignOperands: 'true'
+AlignTrailingComments: 'false'
+AllowAllParametersOfDeclarationOnNextLine: 'true'
+AllowShortBlocksOnASingleLine: 'false'
+AllowShortCaseLabelsOnASingleLine: 'false'
+AllowShortFunctionsOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: 'false'
+AllowShortLoopsOnASingleLine: 'false'
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakTemplateDeclarations: 'true'
+BinPackArguments: 'true'
+BinPackParameters: 'true'
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterClass: 'true'
+ AfterControlStatement: 'true'
+ AfterEnum: 'true'
+ AfterFunction: 'true'
+ AfterNamespace: 'true'
+ AfterStruct: 'true'
+ AfterUnion: 'true'
+ AfterExternBlock: 'true'
+ BeforeCatch: 'true'
+ BeforeElse: 'true'
+ IndentBraces: 'false'
+BreakBeforeInheritanceComma: 'false'
+BreakConstructorInitializers: BeforeComma
+BreakStringLiterals: 'true'
+ColumnLimit: '0'
+CompactNamespaces: 'false'
+Cpp11BracedListStyle: 'true'
+DerivePointerAlignment: 'false'
+FixNamespaceComments: 'true'
+IncludeBlocks: Preserve
+IndentCaseLabels: 'true'
+IndentPPDirectives: None
+IndentWidth: '2'
+KeepEmptyLinesAtTheStartOfBlocks: 'false'
+Language: Cpp
+MaxEmptyLinesToKeep: '1'
+NamespaceIndentation: All
+PointerAlignment: Middle
+ReflowComments: 'true'
+SortIncludes: 'true'
+SortUsingDeclarations: 'true'
+SpaceAfterCStyleCast: 'false'
+SpaceAfterTemplateKeyword: 'false'
+SpaceBeforeAssignmentOperators: 'true'
+SpaceBeforeParens: ControlStatements
+SpaceInEmptyParentheses: 'false'
+SpacesBeforeTrailingComments: '2'
+SpacesInAngles: 'false'
+SpacesInCStyleCastParentheses: 'false'
+SpacesInContainerLiterals: 'false'
+SpacesInParentheses: 'false'
+SpacesInSquareBrackets: 'false'
+Standard: Cpp11
+TabWidth: '2'
+UseTab: Never
+
+...
diff --git a/source/.gitignore b/source/.gitignore
new file mode 100644
index 0000000..0c2999d
--- /dev/null
+++ b/source/.gitignore
@@ -0,0 +1 @@
+/CMakeUserPresets.json
diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt
new file mode 100644
index 0000000..6d1ec20
--- /dev/null
+++ b/source/CMakeLists.txt
@@ -0,0 +1,130 @@
+cmake_minimum_required(VERSION 3.24)
+
+project("wanda"
+ LANGUAGES CXX
+ VERSION "1.0.0"
+)
+
+set(CMAKE_THREAD_PREFER_PTHREAD ON)
+set(THREADS_PREFER_PTHREAD_FLAG ON)
+
+find_package("asio")
+find_package("Boost")
+find_package("JPEG")
+find_package("PNG")
+find_package("lyra")
+find_package("spdlog")
+find_package("Threads")
+
+find_package("PkgConfig" REQUIRED)
+
+pkg_check_modules("GIO"
+ REQUIRED
+ IMPORTED_TARGET
+ GLOBAL
+ "gio-2.0"
+)
+
+pkg_check_modules("libmagic"
+ REQUIRED
+ IMPORTED_TARGET
+ GLOBAL
+ "libmagic"
+)
+
+# Core Library
+
+add_library("${PROJECT_NAME}" STATIC
+ "${PROJECT_SOURCE_DIR}/src/wanda/command.cpp"
+ "${PROJECT_SOURCE_DIR}/src/wanda/control_connection.cpp"
+ "${PROJECT_SOURCE_DIR}/src/wanda/environment.cpp"
+ "${PROJECT_SOURCE_DIR}/src/wanda/logging.cpp"
+ "${PROJECT_SOURCE_DIR}/src/wanda/message.cpp"
+ "${PROJECT_SOURCE_DIR}/src/wanda/xdg.cpp"
+
+ "${PROJECT_SOURCE_DIR}/include/wanda/command.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/control_connection.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/deferred_failure.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/environment.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/expected.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/keyed.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/logging.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/message.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/optional.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/type_wrapper.hpp"
+ "${PROJECT_SOURCE_DIR}/include/wanda/xdg.hpp"
+)
+
+target_compile_features("${PROJECT_NAME}" PUBLIC
+ "cxx_std_20"
+)
+
+target_link_libraries("${PROJECT_NAME}" PUBLIC
+ "asio::asio"
+ "spdlog::spdlog"
+ "Threads::Threads"
+)
+
+target_include_directories("${PROJECT_NAME}" SYSTEM
+ PUBLIC "${PROJECT_SOURCE_DIR}/include"
+)
+
+set_target_properties("${PROJECT_NAME}" PROPERTIES
+ CXX_STANDARD_REQUIRED YES
+ CXX_EXTENSIONS OFF
+)
+
+# Core Executables
+
+add_executable("${PROJECT_NAME}d"
+ "${PROJECT_SOURCE_DIR}/src/${PROJECT_NAME}/control_interface.cpp"
+ "${PROJECT_SOURCE_DIR}/src/${PROJECT_NAME}/filesystem.cpp"
+ "${PROJECT_SOURCE_DIR}/src/${PROJECT_NAME}/setting.cpp"
+ "${PROJECT_SOURCE_DIR}/src/${PROJECT_NAME}/wallpaper.cpp"
+ "${PROJECT_SOURCE_DIR}/src/${PROJECT_NAME}/wandad.cpp"
+
+ "${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/control_interface.hpp"
+ "${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/filesystem.hpp"
+ "${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/setting.hpp"
+ "${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/wallpaper.hpp"
+)
+
+target_link_libraries("${PROJECT_NAME}d" PRIVATE
+ "${PROJECT_NAME}"
+ "bfg::lyra"
+ "boost::boost"
+ "JPEG::JPEG"
+ "PkgConfig::GIO"
+ "PkgConfig::libmagic"
+ "PNG::PNG"
+)
+
+set_target_properties("${PROJECT_NAME}d" PROPERTIES
+ CXX_STANDARD_REQUIRED YES
+ CXX_EXTENSIONS OFF
+)
+
+add_executable("${PROJECT_NAME}c"
+ "${PROJECT_SOURCE_DIR}/src/${PROJECT_NAME}/commander.cpp"
+ "${PROJECT_SOURCE_DIR}/src/${PROJECT_NAME}/wandac.cpp"
+
+ "${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/commander.hpp"
+)
+
+target_link_libraries("${PROJECT_NAME}c" PRIVATE
+ "${PROJECT_NAME}"
+ "bfg::lyra"
+)
+
+set_target_properties("${PROJECT_NAME}c" PROPERTIES
+ CXX_STANDARD_REQUIRED YES
+ CXX_EXTENSIONS OFF
+)
+
+# Install Targets
+
+install(TARGETS
+ "${PROJECT_NAME}"
+ "${PROJECT_NAME}c"
+ "${PROJECT_NAME}d"
+)
diff --git a/source/include/wanda/command.hpp b/source/include/wanda/command.hpp
new file mode 100644
index 0000000..5ea1a08
--- /dev/null
+++ b/source/include/wanda/command.hpp
@@ -0,0 +1,46 @@
+#ifndef WANDA_COMMAND_HPP
+#define WANDA_COMMAND_HPP
+
+#include <wanda/message.hpp>
+
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace wanda
+{
+ /**
+ * @brief An enum to describe different command IDs
+ */
+ enum struct command_id : char
+ {
+ change, //< Change the wallpaper
+ };
+
+ /**
+ * @brief A simple type to represent commands transported through the control connection
+ */
+ struct command
+ {
+ command_id const id;
+ std::vector<std::string> const arguments;
+
+ /**
+ * @brief Convert the command to a message for transmission to a remote endpoint
+ */
+ std::optional<wanda::message> message() const;
+ };
+
+ /**
+ * @brief Extract a command from a message
+ */
+ std::optional<command> make_command(message message);
+
+ /**
+ * @brief A simple factory to create a "Change wallpaper" command
+ */
+ command make_change_command();
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/commander.hpp b/source/include/wanda/commander.hpp
new file mode 100644
index 0000000..1c76c6d
--- /dev/null
+++ b/source/include/wanda/commander.hpp
@@ -0,0 +1,66 @@
+#ifndef WANDA_COMMANDER_HPP
+#define WANDA_COMMANDER_HPP
+
+#include <wanda/command.hpp>
+#include <wanda/control_connection.hpp>
+#include <wanda/message.hpp>
+
+#include <asio.hpp>
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace wanda
+{
+ /**
+ * @brief The remote control client
+ *
+ */
+ struct commander : wanda::control_connection::listener
+ {
+ /**
+ * @brief The interface to be implemented by remote control listeners
+ */
+ struct listener
+ {
+ virtual void on_connected(commander & commander){};
+ virtual void on_response(commander & commander, std::string response){};
+ virtual void on_error(commander & commander, std::string error){};
+ };
+
+ /**
+ * @brief Construct a new commander
+ */
+ commander(asio::io_service & service, std::filesystem::path socket, listener & listener);
+
+ /**
+ * @brief Start communication with the remote daemon endpoint
+ */
+ void start();
+
+ /**
+ * @brief Stop communication with the remote daemon endpoint
+ */
+ void stop();
+
+ /**
+ * @brief Send a command to the remote daemon endpoint
+ */
+ void send(command command);
+
+ void on_error(control_connection::pointer connection, std::error_code error) override;
+ void on_received(control_connection::pointer connection, message message) override;
+
+ private:
+ asio::io_service & m_service;
+ wanda::control_connection::protocol::endpoint m_endpoint;
+ wanda::control_connection::protocol::socket m_socket;
+ wanda::control_connection::pointer m_connection;
+ listener & m_listener;
+ };
+
+} // namespace wanda
+#endif \ No newline at end of file
diff --git a/source/include/wanda/control_connection.hpp b/source/include/wanda/control_connection.hpp
new file mode 100644
index 0000000..b692d37
--- /dev/null
+++ b/source/include/wanda/control_connection.hpp
@@ -0,0 +1,114 @@
+#ifndef WANDA_CONTROL_CONNECTION_HPP
+#define WANDA_CONTROL_CONNECTION_HPP
+
+#include <wanda/keyed.hpp>
+#include <wanda/message.hpp>
+
+#include <asio.hpp>
+
+#include <istream>
+#include <memory>
+#include <ostream>
+#include <set>
+#include <string>
+#include <system_error>
+
+namespace wanda
+{
+ /**
+ * @brief A connection to a remote control endpoint
+ */
+ struct control_connection : keyed<control_connection>, std::enable_shared_from_this<control_connection>
+ {
+ using protocol = asio::local::stream_protocol;
+ using pointer = std::shared_ptr<control_connection>;
+
+ /**
+ * @brief The interface to be implemented by the control interface listener
+ */
+ struct listener
+ {
+ virtual void on_close(pointer connection) {}
+ virtual void on_received(pointer connection, message message) {}
+ virtual void on_error(pointer connection, std::error_code) {}
+ };
+
+ /**
+ * @brief A enum to describe different connection states
+ */
+ enum struct state : std::underlying_type_t<std::byte>
+ {
+ unknown, //< Connection is in an unknown state
+ fresh, //< Connection is freshly created but not established
+ established, //< Connection has been established
+ };
+
+ /**
+ * @internal
+ * @brief Construct a new control connection object
+ *
+ * @note This constructor is keyed on a private key type so it can only be constructed using the #wanda::make_connection factory
+ */
+ control_connection(key, protocol::socket socket);
+
+ /**
+ * @brief Add the given listener to this control connection's listener set
+ *
+ * @returns <code>true</code> iff. the listener was not already in the listener set
+ */
+ bool add(listener * listener);
+
+ /**
+ * @brief Remove the given listener from this control connection's listener set
+ *
+ * @return <code>true</code> iff. the listener was previously registered with this control connection
+ */
+ bool remove(listener * listener);
+
+ /**
+ * @brief Start I/O processing for this control connection
+ */
+ void start();
+
+ /**
+ * @brief Close this control connection
+ */
+ void close();
+
+ /**
+ * @brief Send the given message to the remote endpoint
+ */
+ void send(message message);
+
+ /**
+ * @brief Set the connection state to the provided state
+ */
+ void update(state state);
+
+ /**
+ * @brief Get the current connection state
+ */
+ state current_state() const;
+
+ private:
+ friend pointer make_control_connection(protocol::socket && socket);
+
+ void perform_read();
+
+ protocol::socket m_socket;
+ asio::streambuf m_in{};
+ asio::streambuf m_out{};
+ std::istream m_input{&m_in};
+ std::ostream m_output{&m_out};
+ std::set<listener *> m_listeners{};
+ state m_state{};
+ };
+
+ /**
+ * @brief Create a new control connection
+ */
+ control_connection::pointer make_control_connection(control_connection::protocol::socket && socket);
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/control_interface.hpp b/source/include/wanda/control_interface.hpp
new file mode 100644
index 0000000..73ef2cf
--- /dev/null
+++ b/source/include/wanda/control_interface.hpp
@@ -0,0 +1,96 @@
+/**
+ * @file environment.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_CONTROL_INTERFACE_HPP
+#define WANDA_CONTROL_INTERFACE_HPP
+
+#include <wanda/command.hpp>
+#include <wanda/control_connection.hpp>
+#include <wanda/keyed.hpp>
+
+#include <asio.hpp>
+#include <spdlog/spdlog.h>
+
+#include <cstddef>
+#include <filesystem>
+#include <istream>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <type_traits>
+
+namespace wanda
+{
+ /**
+ * @brief An RAII type to delete a socket file upon destruction
+ */
+ struct socket_deleter
+ {
+ ~socket_deleter();
+
+ std::filesystem::path path;
+ };
+
+ /**
+ * @brief The daemon control interface
+ */
+ struct control_interface : control_connection::listener, keyed<control_interface>, std::enable_shared_from_this<control_interface>
+ {
+ using protocol = asio::local::stream_protocol;
+ using pointer = std::shared_ptr<control_interface>;
+
+ /**
+ * @brief The interface to be implemented by the control interface listener
+ */
+ struct listener
+ {
+ virtual void on_received(control_interface & interface, command command){};
+ };
+
+ /**
+ * @internal
+ * @brief Construct a new control interface object
+ *
+ * @note This constructor is keyed on a private key type so it can only be constructed using the #wanda::make_interface factory
+ */
+ control_interface(key, asio::io_service & service, protocol::endpoint endpoint, listener & listener);
+
+ /**
+ * @brief Start handling of controller connections
+ */
+ std::error_code start();
+
+ /**
+ * @brief Stop the control interface
+ */
+ std::error_code shutdown();
+
+ void on_close(control_connection::pointer connection) override;
+ void on_received(control_connection::pointer connection, message message) override;
+
+ private:
+ void perform_accept();
+
+ friend pointer make_interface(asio::io_service & service, std::filesystem::path file, control_interface::listener & listener);
+
+ asio::io_service & m_service;
+ protocol::endpoint m_endpoint;
+ protocol::socket m_socket;
+ protocol::acceptor m_acceptor;
+ listener & m_listener;
+ socket_deleter m_deleter{m_endpoint.path()};
+ std::set<control_connection::pointer> m_connections;
+ };
+
+ /**
+ * @brief A factory to create new #control_interface instances
+ */
+ control_interface::pointer make_interface(asio::io_service & service, std::filesystem::path socket, control_interface::listener & listener);
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/deferred_failure.hpp b/source/include/wanda/deferred_failure.hpp
new file mode 100644
index 0000000..5db26f6
--- /dev/null
+++ b/source/include/wanda/deferred_failure.hpp
@@ -0,0 +1,21 @@
+/**
+ * @file deferred_failure.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_DEFERRED_FAILURE_HPP
+#define WANDA_DEFERRED_FAILURE_HPP
+
+#include <type_traits>
+
+namespace
+{
+ /**
+ * @brief A helper type to defer static_assert failures
+ */
+ template<typename...>
+ using deferred_failure = std::false_type;
+} // namespace
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/environment.hpp b/source/include/wanda/environment.hpp
new file mode 100644
index 0000000..5a702a8
--- /dev/null
+++ b/source/include/wanda/environment.hpp
@@ -0,0 +1,61 @@
+/**
+ * @file environment.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_ENVIRONMENT_HPP
+#define WANDA_ENVIRONMENT_HPP
+
+#include <unistd.h>
+
+#include <map>
+#include <string>
+
+namespace wanda
+{
+ /**
+ * @brief A type to provide access to the runtime environment
+ */
+ struct environment
+ {
+ using map_type = std::map<std::string, std::string>;
+ using iterator = map_type::iterator;
+ using const_iterator = map_type::const_iterator;
+ using reference = map_type::reference;
+ using const_reference = map_type::const_reference;
+
+ /**
+ * @brief Construct a new environment from the given string array
+ */
+ explicit environment(char const * const * env = ::environ);
+
+ /**
+ * @brief Get the value of the given variable
+ *
+ * @return A mutable reference to the value of the given environment variable
+ */
+ std::string & operator[](std::string const & variable);
+
+ /**
+ * @brief Get the value of the given variable
+ *
+ * @return An immutable reference to the value of the given environment variable
+ */
+ std::string const & operator[](std::string const & variable) const;
+
+ iterator begin();
+ const_iterator begin() const;
+ const_iterator cbegin() const;
+
+ iterator end();
+ const_iterator end() const;
+ const_iterator cend() const;
+
+ private:
+ map_type m_cache{};
+ };
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/expected.hpp b/source/include/wanda/expected.hpp
new file mode 100644
index 0000000..fff0d81
--- /dev/null
+++ b/source/include/wanda/expected.hpp
@@ -0,0 +1,256 @@
+/**
+ * @file expected.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_EXPECTED_HPP
+#define WANDA_EXPECTED_HPP
+
+#include <initializer_list>
+#include <type_traits>
+#include <utility>
+
+namespace wanda
+{
+ /**
+ * @brief A type to represent the error case of a computation based on #wanda::expected
+ */
+ template<typename ErrorType>
+ struct unexpected
+ {
+ static_assert(!std::is_same_v<ErrorType, void>, "ErrorType can not be 'void'!");
+ static_assert(!std::is_array_v<ErrorType>, "ErrorType can not be an array type!");
+
+ /**
+ * @brief Copy construct a new @p unexpected from another @p unexpected
+ */
+ constexpr unexpected(unexpected const &) = default;
+
+ /**
+ * @brief Move construct a new @p unexpected from another @p unexpected
+ */
+ constexpr unexpected(unexpected &&) = default;
+
+ /**
+ * @brief Construct a new @p unexpected by direct initializing the error object from @p args
+ */
+ template<typename... Args>
+ constexpr explicit unexpected(std::in_place_t, Args &&... args)
+ : m_error(std::forward<Args>(args)...)
+ {
+ }
+
+ /**
+ * @brief Construct a new @p unexpected by direct initializing the error object from @p il and @p args
+ */
+ template<
+ typename U,
+ typename... Args,
+ std::enable_if_t<std::is_constructible_v<ErrorType, std::initializer_list<U>, Args...>> * = nullptr>
+ constexpr explicit unexpected(std::in_place_t, std::initializer_list<U> il, Args &&... args)
+ : m_error(il, std::forward<Args>(args)...)
+ {
+ }
+
+ /**
+ * @brief Construct a new @p unexpected by direct initializing the error object from @p error
+ */
+ template<
+ typename Err = ErrorType,
+ std::enable_if_t<std::is_constructible_v<ErrorType, Err> &&
+ !std::is_same_v<std::remove_cv_t<std::remove_reference_t<Err>>, std::in_place_t> &&
+ !std::is_same_v<std::remove_cv_t<std::remove_reference_t<Err>>, unexpected>> * = nullptr>
+ constexpr explicit unexpected(Err && error)
+ : m_error(std::forward<Err>(error))
+ {
+ }
+
+ /**
+ * @brief Construct a new @p unexpected by copying the value of another @p unexpected of different error type
+ */
+ template<
+ typename Err,
+ std::enable_if_t<!(
+ std::is_constructible_v<ErrorType, Err> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err>> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const> &&
+ !std::is_convertible_v<unexpected<Err> &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err>, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const, ErrorType>)> * = nullptr,
+ std::enable_if_t<!std::is_convertible_v<Err, ErrorType>> * = nullptr>
+ constexpr explicit unexpected(unexpected<Err> const & error)
+ : m_error(error.m_error)
+ {
+ }
+
+ /**
+ * @brief Construct a new @p unexpected by copying the value of another @p unexpected of different error type
+ */
+ template<
+ typename Err,
+ std::enable_if_t<!(
+ std::is_constructible_v<ErrorType, Err> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err>> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const> &&
+ !std::is_convertible_v<unexpected<Err> &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err>, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const, ErrorType>)> * = nullptr,
+ std::enable_if_t<std::is_convertible_v<Err, ErrorType>> * = nullptr>
+ constexpr unexpected(unexpected<Err> const & error)
+ : m_error(error.m_error)
+ {
+ }
+
+ /**
+ * @brief Construct a new @p unexpected by moving the value of another @p unexpected of different error type
+ */
+ template<
+ typename Err,
+ std::enable_if_t<!(
+ std::is_constructible_v<ErrorType, Err> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err>> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const> &&
+ !std::is_convertible_v<unexpected<Err> &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err>, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const, ErrorType>)> * = nullptr,
+ std::enable_if_t<!std::is_convertible_v<Err, ErrorType>> * = nullptr>
+ constexpr explicit unexpected(unexpected<Err> && error)
+ : m_error(std::move(error.m_error))
+ {
+ }
+
+ /**
+ * @brief Construct a new @p unexpected by moving the value of another @p unexpected of different error type
+ */
+ template<
+ typename Err,
+ std::enable_if_t<!(
+ std::is_constructible_v<ErrorType, Err> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err>> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const &> &&
+ !std::is_constructible_v<ErrorType, unexpected<Err> const> &&
+ !std::is_convertible_v<unexpected<Err> &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err>, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const &, ErrorType> &&
+ !std::is_convertible_v<unexpected<Err> const, ErrorType>)> * = nullptr,
+ std::enable_if_t<std::is_convertible_v<Err, ErrorType>> * = nullptr>
+ constexpr unexpected(unexpected<Err> && error)
+ : m_error(std::move(error.m_error))
+ {
+ }
+
+ /**
+ * @brief Get the error value contained in this @p unexpected instance
+ */
+ constexpr ErrorType const & value() const &
+ {
+ return m_error;
+ }
+
+ /**
+ * @brief Get the error value contained in this @p unexpected instance
+ */
+ constexpr ErrorType & value() &
+ {
+ return m_error;
+ }
+
+ /**
+ * @brief Get the error value contained in this @p unexpected instance
+ */
+ constexpr ErrorType && value() &&
+ {
+ return std::move(m_error);
+ }
+
+ /**
+ * @brief Get the error value contained in this @p unexpected instance
+ */
+ constexpr ErrorType const && value() const &&
+ {
+ return std::move(m_error);
+ }
+
+ /**
+ * @brief Swap the error value of this @p unexpected instance with the one of @p other
+ */
+ void swap(unexpected & other) noexcept(std::is_nothrow_swappable_v<ErrorType>)
+ {
+ using std::swap;
+ swap(m_error, other.m_error);
+ }
+
+ template<typename ErrorType1, typename ErrorType2>
+ friend constexpr bool operator==(unexpected<ErrorType1> const & lhs, unexpected<ErrorType2> const & rhs);
+
+ template<typename ErrorType1, typename ErrorType2>
+ friend constexpr bool operator!=(unexpected<ErrorType1> const & lhs, unexpected<ErrorType2> const & rhs);
+
+ template<
+ typename Err,
+ std::enable_if_t<std::is_swappable_v<Err>> *>
+ friend void swap(unexpected<Err> & lhs, unexpected<Err> & rhs);
+
+ private:
+ ErrorType m_error;
+ };
+
+ template<typename ErrorType>
+ unexpected(ErrorType)->unexpected<ErrorType>;
+
+ /**
+ * @brief Compare two @p unexpected instances for equality
+ */
+ template<typename ErrorType1, typename ErrorType2>
+ constexpr bool operator==(unexpected<ErrorType1> const & lhs, unexpected<ErrorType2> const & rhs)
+ {
+ return lhs.m_error == rhs.m_error;
+ }
+
+ /**
+ * @brief Compare two @p unexpected instances for inequality
+ */
+ template<typename ErrorType1, typename ErrorType2>
+ constexpr bool operator!=(unexpected<ErrorType1> const & lhs, unexpected<ErrorType2> const & rhs)
+ {
+ return lhs.m_error != rhs.m_error;
+ }
+
+ /**
+ * @brief Swap the error values of two @p unexpected instances
+ */
+ template<
+ typename Err,
+ std::enable_if_t<std::is_swappable_v<Err>> * = nullptr>
+ void swap(unexpected<Err> & lhs, unexpected<Err> & rhs)
+ {
+ lhs.swap(rhs);
+ }
+
+ /**
+ * @brief A tag type for @p unexpected
+ */
+ struct unexpect_t
+ {
+ explicit unexpect_t() = default;
+ };
+
+ /**
+ * @brief A tap for @p unexpected
+ */
+ inline constexpr unexpect_t unexpect{};
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/filesystem.hpp b/source/include/wanda/filesystem.hpp
new file mode 100644
index 0000000..1975bc5
--- /dev/null
+++ b/source/include/wanda/filesystem.hpp
@@ -0,0 +1,39 @@
+/**
+ * @file filesystem.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_FILESYSTEM_HPP
+#define WANDA_FILESYSTEM_HPP
+
+#include <filesystem>
+#include <optional>
+#include <vector>
+
+namespace wanda
+{
+ /**
+ * @brief Covenience alias for path lists
+ */
+ using path_list = std::vector<std::filesystem::path>;
+
+ /**
+ * @brief The default scan filter, allowing only regular files to pass
+ */
+ constexpr inline auto default_filter = [](std::filesystem::path const & path) {
+ return is_regular_file(path);
+ };
+
+ /**
+ * @brief Scan the given folder for files
+ */
+ std::optional<path_list> scan(std::filesystem::path folder, bool(filter)(std::filesystem::path const &) = default_filter);
+
+ /**
+ * @brief Pick a random path from the given list
+ */
+ std::filesystem::path random_pick(path_list const & paths);
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/keyed.hpp b/source/include/wanda/keyed.hpp
new file mode 100644
index 0000000..58f17ad
--- /dev/null
+++ b/source/include/wanda/keyed.hpp
@@ -0,0 +1,28 @@
+/**
+ * @file keyed.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_KEYED_HPP
+#define WANDA_KEYED_HPP
+
+namespace wanda
+{
+ /**
+ * @brief A tag type to prevent construction of a type without a factory
+ */
+ template<typename Derived>
+ struct keyed
+ {
+ protected:
+ struct key
+ {
+ };
+
+ explicit keyed(key) {}
+ };
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/logging.hpp b/source/include/wanda/logging.hpp
new file mode 100644
index 0000000..b3c1665
--- /dev/null
+++ b/source/include/wanda/logging.hpp
@@ -0,0 +1,35 @@
+/**
+ * @file logging.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_LOGGING_HPP
+#define WANDA_LOGGING_HPP
+
+#include <spdlog/sinks/null_sink.h>
+#include <spdlog/spdlog.h>
+
+#include <memory>
+
+namespace wanda
+{
+ /**
+ * @brief A covenience alias to represent a handle for a logger
+ */
+ using logger_ptr = std::shared_ptr<spdlog::logger>;
+
+ /**
+ * @brief Initialize the shared logger
+ *
+ * @note The logger will only ever be initialized once, even if this function is called multiple times
+ */
+ void initialize_logger(spdlog::sink_ptr sink = std::make_shared<spdlog::sinks::null_sink_st>());
+
+ /**
+ * @brief Get the shared logger
+ */
+ logger_ptr get_logger();
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/magic.hpp b/source/include/wanda/magic.hpp
new file mode 100644
index 0000000..fcb153e
--- /dev/null
+++ b/source/include/wanda/magic.hpp
@@ -0,0 +1,58 @@
+#ifndef WANDA_MAGIC_HPP
+#define WANDA_MAGIC_HPP
+
+#include <magic.h>
+
+#include <filesystem>
+#include <memory>
+#include <string>
+#include <type_traits>
+
+namespace wanda
+{
+ struct magic
+ {
+ struct closer
+ {
+ auto operator()(magic_t handle) const noexcept -> void
+ {
+ magic_close(handle);
+ }
+ };
+
+ enum struct mime_type
+ {
+ unknown,
+ image_jpeg,
+ image_png,
+ };
+
+ magic()
+ : m_handle{magic_open(MAGIC_MIME_TYPE)}
+ {
+ magic_load(m_handle.get(), nullptr);
+ }
+
+ auto type(std::filesystem::path path) -> mime_type
+ {
+ auto magic_type = std::string{magic_file(m_handle.get(), path.native().c_str())};
+
+ if (magic_type == "image/jpeg")
+ {
+ return mime_type::image_jpeg;
+ }
+ else if (magic_type == "image/png")
+ {
+ return mime_type::image_png;
+ }
+
+ return mime_type::unknown;
+ }
+
+ private:
+ std::unique_ptr<std::remove_pointer_t<magic_t>, closer> m_handle;
+ };
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/message.hpp b/source/include/wanda/message.hpp
new file mode 100644
index 0000000..866408f
--- /dev/null
+++ b/source/include/wanda/message.hpp
@@ -0,0 +1,83 @@
+/**
+ * @file message.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_MESSAGE_HPP
+#define WANDA_MESSAGE_HPP
+
+#include <cstddef>
+#include <istream>
+#include <optional>
+#include <string>
+
+namespace wanda
+{
+ inline namespace v1
+ {
+ /**
+ * @brief The version argument for the hello message reflecting the current version
+ */
+ auto constexpr message_argument_hello = "1.0.0";
+ } // namespace v1
+
+ /**
+ * @brief A tag to mark messages originating from the controller
+ */
+ auto constexpr message_source_controller = "C";
+
+ /**
+ * @brief A tag to mark messages originating from the daemon
+ */
+ auto constexpr message_source_daemon = "D";
+
+ /**
+ * @brief The command of the hello message
+ */
+ auto constexpr message_command_hello = "HELLO";
+
+ /**
+ * @brief A control protocol message, consisting of a @p source, @p command, and @p arguments
+ */
+ struct message
+ {
+ /**
+ * @brief Serialize this message into a string
+ */
+ explicit operator std::string() const;
+
+ /**
+ * @brief Get the size of the message as if it was serialized
+ */
+ std::size_t size() const;
+
+ /**
+ * @brief The source of the message
+ */
+ std::string source;
+
+ /**
+ * @brief The command of the message
+ */
+ std::string command;
+
+ /**
+ * @brief The arguments of the message command
+ */
+ std::optional<std::string> argument;
+ };
+
+ /**
+ * @brief Deserialize a message from the given stream
+ */
+ std::istream & operator>>(std::istream & in, message & message);
+
+ /**
+ * @brief Serialize a message to the given stream
+ */
+ std::ostream & operator<<(std::ostream & out, message const & message);
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/optional.hpp b/source/include/wanda/optional.hpp
new file mode 100644
index 0000000..da3774c
--- /dev/null
+++ b/source/include/wanda/optional.hpp
@@ -0,0 +1,65 @@
+/**
+ * @file optional.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_OPTIONAL_HPP
+#define WANDA_OPTIONAL_HPP
+
+#include <optional>
+
+namespace wanda::std_ext
+{
+ /**
+ * @brief A type to represent a computation that could fail
+ */
+ struct failable
+ {
+ /**
+ * @brief A factory to create a successful computation
+ */
+ constexpr static auto success() { return failable{false}; }
+
+ /**
+ * @brief A factory to create a failed computation
+ */
+ constexpr static auto failure() { return failable{true}; }
+
+ /**
+ * @brief Execute the given handler if the computation failed
+ */
+ template<typename Handler>
+ constexpr void operator||(Handler handler) const
+ {
+ if (m_failed)
+ {
+ handler();
+ }
+ }
+
+ private:
+ constexpr explicit failable(bool failed)
+ : m_failed{failed} {};
+ bool const m_failed;
+ };
+
+ /**
+ * @brief Unwrap the given optional object, if present, and pass it to the handler
+ *
+ * @return A successful computation iff. the object was present, a failed computation otherwise.
+ */
+ template<typename ObjectType, typename HandlerType>
+ auto with(std::optional<ObjectType> && object, HandlerType handler)
+ {
+ if (object)
+ {
+ handler(object.value());
+ return failable::success();
+ }
+ return failable::failure();
+ }
+
+} // namespace wanda::std_ext
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/setting.hpp b/source/include/wanda/setting.hpp
new file mode 100644
index 0000000..1721651
--- /dev/null
+++ b/source/include/wanda/setting.hpp
@@ -0,0 +1,156 @@
+/**
+ * @file setting.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_setting_HPP
+#define WANDA_setting_HPP
+
+#include <wanda/deferred_failure.hpp>
+#include <wanda/type_wrapper.hpp>
+
+#include <gio/gio.h>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace wanda
+{
+ struct setting;
+
+ /**
+ * @brief A convenience type to represent setting keys
+ */
+ using key = type_wrapper<std::string, struct KeyTag>;
+
+ namespace literals
+ {
+ /**
+ * @brief UDL to create setting keys
+ */
+ key operator""_key(char const * str, std::size_t len);
+
+ /**
+ * @brief UDL to create setting schemas
+ */
+ std::optional<setting> operator""_setting(char const * str, std::size_t lent);
+ } // namespace literals
+
+ /**
+ * @brief A simple wrapper for GSettings Schemas
+ */
+ struct setting
+ {
+ struct entry
+ {
+ using value_type = std::variant<std::monostate, bool, std::int32_t, std::int64_t, std::uint32_t, std::uint64_t, double, std::string, std::vector<std::string>>;
+
+ /**
+ * @brief Get the value of the settings entry
+ */
+ value_type operator*() const;
+
+ /**
+ * @brief Assign the given @p value to the settings entry
+ *
+ * @returns @p true iff. the value could be successfully assigned
+ */
+ template<typename Type>
+ bool operator=(Type value)
+ {
+ struct setting_applier
+ {
+ setting_applier(GSettings * setting, gchar const * key, Type value) noexcept
+ : m_result{[&] {
+ if constexpr (std::is_same_v<Type, bool>)
+ {
+ return g_settings_set_boolean(setting, key, value);
+ }
+ else if constexpr (std::is_same_v<Type, std::int32_t>)
+ {
+ return g_settings_set_int(setting, key, value);
+ }
+ else if constexpr (std::is_same_v<Type, std::int64_t>)
+ {
+ return g_settings_set_int64(setting, key, value);
+ }
+ else if constexpr (std::is_same_v<Type, std::uint32_t>)
+ {
+ return g_settings_set_uint(setting, key, value);
+ }
+ else if constexpr (std::is_same_v<Type, std::uint64_t>)
+ {
+ return g_settings_set_uint64(setting, key, value);
+ }
+ else if constexpr (std::is_same_v<Type, double>)
+ {
+ return g_settings_set_double(setting, key, value);
+ }
+ else if constexpr (std::is_same_v<Type, std::string>)
+ {
+ return g_settings_set_string(setting, key, value.c_str());
+ }
+ else if constexpr (std::is_same_v<Type, std::vector<std::string>>)
+ {
+ auto temp = std::vector<gchar const *>{value.size() + 1};
+ std::transform(value.begin(), value.end(), temp.begin(), [](auto const & str) { return str.c_str(); });
+ return g_settings_set_strv(setting, key, temp.data());
+ }
+ }()}
+ {
+ }
+
+ ~setting_applier()
+ {
+ g_settings_sync();
+ }
+
+ operator bool() const
+ {
+ return m_result;
+ }
+
+ private:
+ gboolean const m_result;
+ };
+
+ return setting_applier{m_settings.get(), m_key.get().c_str(), value};
+ }
+
+ private:
+ entry(setting const & schema, key key);
+
+ std::unique_ptr<GSettings, decltype(&g_object_unref)> m_settings;
+
+ key m_key;
+
+ friend setting;
+ };
+
+ /**
+ * @brief Get the entry for the given key
+ *
+ * @return An <code>std::optional</code> wrapping the entry associated with
+ * the given key, or an empty <code>std::optional</code> if the desired key
+ * does not exist in the setting's schema.
+ */
+ std::optional<entry> operator[](key key) const;
+
+ private:
+ explicit setting(GSettingsSchema * schema);
+
+ std::unique_ptr<GSettingsSchema, decltype(&g_settings_schema_unref)> m_schema;
+
+ friend std::optional<setting> literals::operator""_setting(char const *, std::size_t);
+ };
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/type_wrapper.hpp b/source/include/wanda/type_wrapper.hpp
new file mode 100644
index 0000000..12684cb
--- /dev/null
+++ b/source/include/wanda/type_wrapper.hpp
@@ -0,0 +1,47 @@
+/**
+ * @file type_wrapper.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_TYPE_WRAPPER_HPP
+#define WANDA_TYPE_WRAPPER_HPP
+
+#include <utility>
+
+namespace wanda
+{
+ /**
+ * @brief A type to create a distinct type based on an existing type
+ *
+ * @tparam InnerType The type to wrap
+ * @tparam TagType A tag type to identify the distinct type
+ */
+ template<typename InnerType, typename TagType>
+ struct type_wrapper
+ {
+ /**
+ * @brief Construct a new type wrapper object
+ */
+ explicit type_wrapper(InnerType value)
+ : m_value{std::move(value)}
+ {
+ }
+
+ /**
+ * @brief Retrieve the wrapped value with its original type
+ */
+ constexpr explicit operator InnerType const &() const { return get(); }
+
+ /**
+ * @brief Retrieve the wrapped value with its original type
+ */
+ constexpr InnerType const & get() const { return m_value; }
+
+ private:
+ InnerType m_value;
+ };
+
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/wallpaper.hpp b/source/include/wanda/wallpaper.hpp
new file mode 100644
index 0000000..0cad473
--- /dev/null
+++ b/source/include/wanda/wallpaper.hpp
@@ -0,0 +1,24 @@
+/**
+ * @file wallpaper.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_WALLPAPER_HPP
+#define WANDA_WALLPAPER_HPP
+
+#include <spdlog/sinks/null_sink.h>
+#include <spdlog/spdlog.h>
+
+#include <filesystem>
+#include <memory>
+
+namespace wanda
+{
+ /**
+ * @brief Set the wallpaper to the file specified by the given path
+ */
+ void set_wallpaper(std::filesystem::path wallpaper);
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/include/wanda/xdg.hpp b/source/include/wanda/xdg.hpp
new file mode 100644
index 0000000..bc138fa
--- /dev/null
+++ b/source/include/wanda/xdg.hpp
@@ -0,0 +1,40 @@
+/**
+ * @file xdg.hpp
+ * @author Felix Morgner (felix.morgner@gmail.com)
+ * @since 1.0.0
+ */
+
+#ifndef WANDA_XDG_HPP
+#define WANDA_XDG_HPP
+
+#include <wanda/environment.hpp>
+
+#include <cstddef>
+#include <filesystem>
+#include <type_traits>
+
+namespace wanda
+{
+ /**
+ * @brief An @p enum to represet the standardized XDG directories
+ */
+ enum struct xdg_directory : std::underlying_type_t<std::byte>
+ {
+ data_home,
+ config_home,
+ cache_home,
+ runtime_dir,
+ };
+
+ /**
+ * @brief Get the name of the environment variable associated with the given XDG directory
+ */
+ std::string xdg_variable(xdg_directory directory);
+
+ /**
+ * @brief Get the path to the given @p directory given the provided @p environment
+ */
+ std::filesystem::path xdg_path_for(xdg_directory directory, environment const & environment);
+} // namespace wanda
+
+#endif \ No newline at end of file
diff --git a/source/src/wanda/command.cpp b/source/src/wanda/command.cpp
new file mode 100644
index 0000000..960c52b
--- /dev/null
+++ b/source/src/wanda/command.cpp
@@ -0,0 +1,47 @@
+#include <wanda/command.hpp>
+
+namespace wanda
+{
+ std::optional<message> command::message() const
+ {
+ using namespace std::string_literals;
+ auto const command = [this] {
+ switch (id)
+ {
+ case command_id::change:
+ return "CHANGE"s;
+ default:
+ return ""s;
+ }
+ }();
+
+ auto argument_string = std::string{};
+ for (int index = 0ul; index < arguments.size(); ++index)
+ {
+ argument_string += (index) ? "," + arguments[index] : arguments[index];
+ }
+
+ if (command.empty())
+ {
+ return std::nullopt;
+ }
+
+ return wanda::message{"C", command, argument_string};
+ }
+
+ std::optional<command> make_command(message message)
+ {
+ if (message.command == "CHANGE")
+ {
+ return {{command_id::change}};
+ }
+
+ return std::nullopt;
+ }
+
+ command make_change_command()
+ {
+ return {command_id::change};
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/commander.cpp b/source/src/wanda/commander.cpp
new file mode 100644
index 0000000..85fc68a
--- /dev/null
+++ b/source/src/wanda/commander.cpp
@@ -0,0 +1,77 @@
+#include <wanda/commander.hpp>
+#include <wanda/logging.hpp>
+#include <wanda/message.hpp>
+#include <wanda/optional.hpp>
+
+#include <spdlog/fmt/ostr.h>
+
+namespace wanda
+{
+ commander::commander(asio::io_service & service, std::filesystem::path socket, listener & listener)
+ : m_service{service}
+ , m_endpoint{socket.string()}
+ , m_socket{service}
+ , m_listener{listener}
+ {
+ }
+
+ void commander::start()
+ {
+ m_socket.async_connect(m_endpoint, [&](auto const & error) {
+ if (error)
+ {
+ get_logger()->error("error while connecting to control interface: '{}'", error.message());
+ }
+ else
+ {
+ get_logger()->info("establishing connection to wanda deamon");
+ m_connection = wanda::make_control_connection(std::move(m_socket));
+ m_connection->add(this);
+ m_connection->start();
+ m_connection->send({message_source_controller, message_command_hello, message_argument_hello});
+ }
+ });
+ }
+
+ void commander::stop()
+ {
+ get_logger()->info("closing control connection");
+ m_connection->close();
+ }
+
+ void commander::send(command command)
+ {
+ using namespace wanda::std_ext;
+
+ if (!m_connection || m_connection->current_state() != control_connection::state::established)
+ {
+ get_logger()->error("tried to send command without an established connection");
+ m_listener.on_error(*this, "tried to send command without an established connection");
+ return;
+ }
+
+ with(command.message(), [&](auto const & message) { m_connection->send(message); }) ||
+ [&] { get_logger()->error("unknown command"); };
+ }
+
+ void commander::on_error(control_connection::pointer connection, std::error_code error)
+ {
+ get_logger()->error("control interface communication error: '{}'", error.message());
+ }
+
+ void commander::on_received(wanda::control_connection::pointer connection, message message)
+ {
+ if (auto state = connection->current_state(); message.command == "HELLO" && state == control_connection::state::fresh)
+ {
+ get_logger()->info("connection to wanda deamon successfully established");
+ connection->update(control_connection::state::established);
+ m_listener.on_connected(*this);
+ }
+ else
+ {
+ get_logger()->error("unexpected message: '{}'", message);
+ m_listener.on_error(*this, "unexpected message '" + static_cast<std::string>(message) + '\'');
+ }
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/control_connection.cpp b/source/src/wanda/control_connection.cpp
new file mode 100644
index 0000000..40e29f3
--- /dev/null
+++ b/source/src/wanda/control_connection.cpp
@@ -0,0 +1,120 @@
+#include <wanda/control_connection.hpp>
+
+#include <limits>
+
+namespace wanda
+{
+ control_connection::pointer make_control_connection(control_connection::protocol::socket && socket)
+ {
+ return std::make_shared<control_connection>(control_connection::key{}, std::move(socket));
+ }
+
+ control_connection::control_connection(control_connection::key key, control_connection::protocol::socket socket)
+ : keyed{key}
+ , m_socket{std::move(socket)}
+ {
+ }
+
+ bool control_connection::add(listener * listener)
+ {
+ auto [_, inserted] = m_listeners.insert(listener);
+ return inserted;
+ }
+
+ bool control_connection::remove(listener * listener)
+ {
+ return m_listeners.erase(listener);
+ }
+
+ void control_connection::start()
+ {
+ if (m_state == state::unknown)
+ {
+ m_state = state::fresh;
+ perform_read();
+ }
+ }
+
+ void control_connection::send(message message)
+ {
+ m_output << message << '\n';
+ asio::async_write(m_socket, m_out, asio::transfer_exactly(message.size() + 1), [that = shared_from_this(), this](auto const & error, auto const length) {
+ if (error)
+ {
+ // TODO: Handle error
+ }
+ else
+ {
+ m_out.consume(length);
+ }
+ });
+ }
+
+ void control_connection::close()
+ {
+ if (auto error = std::error_code{}; m_socket.cancel(error))
+ {
+ for (auto & listener : m_listeners)
+ {
+ listener->on_error(shared_from_this(), error);
+ }
+ }
+
+ if (auto error = std::error_code{}; m_socket.close(error))
+ {
+ for (auto & listener : m_listeners)
+ {
+ listener->on_error(shared_from_this(), error);
+ }
+ }
+
+ for (auto & listener : m_listeners)
+ {
+ listener->on_close(shared_from_this());
+ }
+ m_listeners.clear();
+ }
+
+ void control_connection::update(state state)
+ {
+ m_state = state;
+ }
+
+ control_connection::state control_connection::current_state() const
+ {
+ return m_state;
+ }
+
+ void control_connection::perform_read()
+ {
+ asio::async_read_until(m_socket, m_in, '\n', [that = shared_from_this(), this](auto const & error, auto const length) {
+ if (error)
+ {
+ for (auto & listener : m_listeners)
+ {
+ listener->on_error(shared_from_this(), error);
+ }
+ close();
+ }
+ else
+ {
+ auto msg = message{};
+ m_input >> msg;
+ if (!m_input)
+ {
+ m_input.ignore(std::numeric_limits<std::streamsize>::max());
+ m_input.clear();
+ }
+ else
+ {
+ for (auto & listener : m_listeners)
+ {
+ listener->on_received(shared_from_this(), msg);
+ }
+ }
+ perform_read();
+ }
+ });
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/control_interface.cpp b/source/src/wanda/control_interface.cpp
new file mode 100644
index 0000000..c008920
--- /dev/null
+++ b/source/src/wanda/control_interface.cpp
@@ -0,0 +1,154 @@
+#include <wanda/control_interface.hpp>
+#include <wanda/logging.hpp>
+#include <wanda/optional.hpp>
+
+#include <spdlog/fmt/ostr.h>
+
+#include <unistd.h>
+
+#include <algorithm>
+#include <iterator>
+#include <string>
+#include <system_error>
+#include <utility>
+
+namespace wanda
+{
+ // 'socket_deleter' implementation
+
+ socket_deleter::~socket_deleter()
+ {
+ if (std::filesystem::exists(path))
+ {
+ std::filesystem::remove(path);
+ }
+ }
+
+ // 'control_interface' implementation
+
+ control_interface::control_interface(control_interface::key key, asio::io_service & service, control_interface::protocol::endpoint endpoint, listener & listener)
+ : keyed{key}
+ , m_service{service}
+ , m_endpoint{std::move(endpoint)}
+ , m_socket{m_service}
+ , m_acceptor{m_service}
+ , m_listener{listener}
+ {
+ }
+
+ std::error_code control_interface::start()
+ {
+ if (auto error = std::error_code{}; m_acceptor.open(m_endpoint.protocol(), error))
+ {
+ return error;
+ }
+
+ if (auto error = std::error_code{}; m_acceptor.bind(m_endpoint, error))
+ {
+ return error;
+ }
+
+ if (auto error = std::error_code{}; m_acceptor.listen(128, error))
+ {
+ return error;
+ }
+ else
+ {
+ perform_accept();
+ return error;
+ }
+ }
+
+ std::error_code control_interface::shutdown()
+ {
+ for (auto & connection : m_connections)
+ {
+ connection->close();
+ }
+
+ auto error = std::error_code{};
+ return m_acceptor.close(error);
+ }
+
+ void control_interface::perform_accept()
+ {
+ m_acceptor.async_accept(m_socket, [that = shared_from_this(), this](auto const & error) {
+ if (error && error != asio::error::operation_aborted)
+ {
+ get_logger()->error("failed to accept connection because '{}'", error);
+ }
+ else
+ {
+ get_logger()->info("new incoming controller connection");
+ auto [connection, inserted] = m_connections.insert(make_control_connection(std::move(m_socket)));
+ if (inserted)
+ {
+ (*connection)->add(this);
+ (*connection)->start();
+ }
+ perform_accept();
+ }
+ });
+ }
+
+ void control_interface::on_close(control_connection::pointer connection)
+ {
+ if (static_cast<char>(connection->current_state()) >= static_cast<char>(control_connection::state::established))
+ {
+ get_logger()->info("controller connection closed");
+ }
+ else
+ {
+ get_logger()->info("controller connection aborted before it could be established");
+ }
+ m_connections.erase(connection);
+ }
+
+ void control_interface::on_received(control_connection::pointer connection, message message)
+ {
+ using namespace wanda::std_ext;
+
+ if (m_connections.find(connection) == m_connections.cend())
+ {
+ get_logger()->error("received message from an unknown connection");
+ return;
+ }
+
+ if (message.source != message_source_controller)
+ {
+ get_logger()->error("received a deamon message");
+ return;
+ }
+
+ if (auto state = connection->current_state(); message.command == message_command_hello && state == control_connection::state::fresh)
+ {
+ get_logger()->info("controller connection established");
+ if (message.argument.has_value())
+ {
+ get_logger()->info("remote controller version '{}'", *message.argument);
+ }
+ connection->send({message_source_daemon, message_command_hello, message_argument_hello});
+ connection->update(control_connection::state::established);
+ }
+ else
+ {
+ with(make_command(message), [&](auto const & command) {
+ m_listener.on_received(*this, command);
+ }) ||
+ [&] { get_logger()->warn("ignoring unknown message '{}'", message); };
+ }
+ }
+
+ control_interface::pointer make_interface(asio::io_service & service, std::filesystem::path socket, control_interface::listener & listener)
+ {
+ if (std::filesystem::exists(socket))
+ {
+ get_logger()->error("socket '{}' exists", socket.native());
+ return {};
+ }
+
+ control_interface::protocol::endpoint endpoint{socket.string()};
+ return std::make_shared<control_interface>(control_interface::key{}, service, std::move(endpoint), listener);
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/environment.cpp b/source/src/wanda/environment.cpp
new file mode 100644
index 0000000..533a5f5
--- /dev/null
+++ b/source/src/wanda/environment.cpp
@@ -0,0 +1,71 @@
+#include <wanda/environment.hpp>
+
+#include <string>
+
+namespace wanda
+{
+ environment::environment(char const * const * env)
+ {
+ if (!env)
+ {
+ return;
+ }
+
+ std::string buffer{};
+ for (; *env != nullptr; ++env)
+ {
+ buffer = *env;
+ int split_point = buffer.find('=');
+ if (split_point != std::string::npos)
+ {
+ m_cache[buffer.substr(0, split_point)] = buffer.substr(split_point + 1);
+ }
+ }
+ }
+
+ std::string & environment::operator[](std::string const & variable)
+ {
+ return m_cache[variable];
+ }
+
+ std::string const & environment::operator[](std::string const & variable) const
+ {
+ static std::string const empty{};
+ if (auto needle = m_cache.find(variable); needle != cend())
+ {
+ return needle->second;
+ }
+ return empty;
+ }
+
+ environment::iterator environment::begin()
+ {
+ return m_cache.begin();
+ }
+
+ environment::const_iterator environment::begin() const
+ {
+ return m_cache.begin();
+ }
+
+ environment::const_iterator environment::cbegin() const
+ {
+ return m_cache.cbegin();
+ }
+
+ environment::iterator environment::end()
+ {
+ return m_cache.end();
+ }
+
+ environment::const_iterator environment::end() const
+ {
+ return m_cache.end();
+ }
+
+ environment::const_iterator environment::cend() const
+ {
+ return m_cache.cend();
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/filesystem.cpp b/source/src/wanda/filesystem.cpp
new file mode 100644
index 0000000..4da30b1
--- /dev/null
+++ b/source/src/wanda/filesystem.cpp
@@ -0,0 +1,31 @@
+#include <wanda/filesystem.hpp>
+
+#include <random>
+#include <ranges>
+
+namespace wanda
+{
+ std::optional<path_list> scan(std::filesystem::path source, bool(filter)(std::filesystem::path const &))
+ {
+ if (!std::filesystem::is_directory(source))
+ {
+ return std::nullopt;
+ }
+ auto entries = std::filesystem::recursive_directory_iterator{source};
+ auto result = path_list{};
+ for (auto & entry : entries | std::views::filter(filter))
+ {
+ result.push_back(entry.path());
+ }
+ return result;
+ }
+
+ std::filesystem::path random_pick(path_list const & paths)
+ {
+ static auto generator = std::mt19937{std::random_device{}()};
+ auto distribution = std::uniform_int_distribution<std::size_t>{0, paths.size() - 1};
+
+ return paths[distribution(generator)];
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/logging.cpp b/source/src/wanda/logging.cpp
new file mode 100644
index 0000000..8c61953
--- /dev/null
+++ b/source/src/wanda/logging.cpp
@@ -0,0 +1,21 @@
+#include <wanda/logging.hpp>
+
+namespace wanda
+{
+ std::function<void(spdlog::sink_ptr)> initializer = [](spdlog::sink_ptr sink) {
+ spdlog::register_logger(std::make_shared<spdlog::logger>("wanda", sink));
+ initializer = [](auto) {};
+ };
+
+ void initialize_logger(spdlog::sink_ptr sink)
+ {
+ initializer(sink);
+ }
+
+ logger_ptr get_logger()
+ {
+ initialize_logger();
+ return spdlog::get("wanda");
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/message.cpp b/source/src/wanda/message.cpp
new file mode 100644
index 0000000..978b7c4
--- /dev/null
+++ b/source/src/wanda/message.cpp
@@ -0,0 +1,75 @@
+#include <wanda/message.hpp>
+
+#include <ios>
+#include <iterator>
+#include <sstream>
+
+namespace wanda
+{
+ message::operator std::string() const
+ {
+ std::ostringstream buffer{};
+ buffer << source
+ << ':'
+ << command;
+ if (argument.has_value())
+ {
+ buffer << ':' << *argument;
+ }
+ return buffer.str();
+ }
+
+ std::size_t message::size() const
+ {
+ return static_cast<std::string>(*this).size();
+ }
+
+ template<typename InputIt, typename OutputIt, typename UnaryPredicate>
+ OutputIt copy_until(InputIt first, InputIt last, OutputIt out, UnaryPredicate predicate)
+ {
+ while (first != last && !predicate(*first))
+ {
+ *out++ = *first++;
+ }
+ return out;
+ }
+
+ std::istream & operator>>(std::istream & in, message & message)
+ {
+ auto pos = std::istream_iterator<char>{in};
+ auto end = std::istream_iterator<char>{};
+ auto buffer = std::string{};
+
+ copy_until(pos, end, std::back_inserter(buffer), [](auto const & c) { return c == ':'; });
+ if (in.eof() || buffer.size() != 1)
+ {
+ in.setstate(std::ios_base::failbit);
+ return in;
+ }
+ message.source = buffer;
+
+ buffer.clear();
+ copy_until(++pos, end, std::back_inserter(buffer), [](auto const & c) { return c == ':'; });
+ if (in.eof())
+ {
+ in.setstate(std::ios_base::failbit);
+ }
+ message.command = buffer;
+
+ buffer.clear();
+ copy(++pos, end, std::back_inserter(buffer));
+ if (buffer.size())
+ {
+ message.argument = std::optional{std::move(buffer)};
+ }
+
+ in.clear(in.rdstate() ^ std::ios_base::failbit);
+ return in;
+ }
+
+ std::ostream & operator<<(std::ostream & out, message const & message)
+ {
+ return out << static_cast<std::string>(message);
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/setting.cpp b/source/src/wanda/setting.cpp
new file mode 100644
index 0000000..f0cf7a7
--- /dev/null
+++ b/source/src/wanda/setting.cpp
@@ -0,0 +1,102 @@
+#include <wanda/setting.hpp>
+
+#include <algorithm>
+#include <type_traits>
+
+namespace wanda
+{
+ // UDL implementations
+
+ key literals::operator""_key(char const * str, std::size_t len)
+ {
+ return key{{str, len}};
+ }
+
+ std::optional<setting> literals::operator""_setting(char const * str, std::size_t len)
+ {
+ auto source = g_settings_schema_source_get_default();
+ if (!source)
+ {
+ return std::nullopt;
+ }
+
+ auto schema = g_settings_schema_source_lookup(source, str, true);
+ if (!schema)
+ {
+ return std::nullopt;
+ }
+
+ return setting{schema};
+ }
+
+ // 'setting' implementation
+
+ setting::setting(GSettingsSchema * schema)
+ : m_schema{schema, &g_settings_schema_unref}
+ {
+ }
+
+ std::optional<setting::entry> setting::operator[](key key) const
+ {
+ if (!g_settings_schema_has_key(m_schema.get(), key.get().c_str()))
+ {
+ return std::nullopt;
+ }
+
+ return setting::entry{*this, std::move(key)};
+ }
+
+ // 'setting::entry' implementation
+
+ setting::entry::entry(setting const & setting, key key)
+ : m_settings{g_settings_new(g_settings_schema_get_id(setting.m_schema.get())), &g_object_unref}
+ , m_key{key.get()}
+ {
+ }
+
+ setting::entry::value_type setting::entry::operator*() const
+ {
+ auto value = std::unique_ptr<GVariant, decltype(&g_variant_unref)>{g_settings_get_value(m_settings.get(), m_key.get().c_str()), &g_variant_unref};
+ auto raw = value.get();
+
+ if (g_variant_is_of_type(raw, G_VARIANT_TYPE_BOOLEAN))
+ {
+ return static_cast<bool>(g_variant_get_boolean(raw));
+ }
+ else if (g_variant_is_of_type(raw, G_VARIANT_TYPE_INT32))
+ {
+ return static_cast<std::int32_t>(g_variant_get_int32(raw));
+ }
+ else if (g_variant_is_of_type(raw, G_VARIANT_TYPE_INT64))
+ {
+ return static_cast<std::int64_t>(g_variant_get_int64(raw));
+ }
+ else if (g_variant_is_of_type(raw, G_VARIANT_TYPE_UINT32))
+ {
+ return static_cast<std::uint32_t>(g_variant_get_uint32(raw));
+ }
+ else if (g_variant_is_of_type(raw, G_VARIANT_TYPE_UINT64))
+ {
+ return static_cast<std::uint64_t>(g_variant_get_uint64(raw));
+ }
+ else if (g_variant_is_of_type(raw, G_VARIANT_TYPE_DOUBLE))
+ {
+ return static_cast<double>(g_variant_get_double(raw));
+ }
+ else if (g_variant_is_of_type(raw, G_VARIANT_TYPE_STRING))
+ {
+ auto size = gsize{};
+ auto string = g_variant_get_string(raw, &size);
+ return std::string{string, size};
+ }
+ else if (g_variant_is_of_type(raw, G_VARIANT_TYPE_STRING_ARRAY))
+ {
+ auto length = gsize{};
+ auto data = g_variant_get_strv(raw, &length);
+ return std::vector<std::string>{data, data + length};
+ }
+
+ return {};
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/wallpaper.cpp b/source/src/wanda/wallpaper.cpp
new file mode 100644
index 0000000..7d4c7d5
--- /dev/null
+++ b/source/src/wanda/wallpaper.cpp
@@ -0,0 +1,95 @@
+#include <wanda/logging.hpp>
+#include <wanda/magic.hpp>
+#include <wanda/optional.hpp>
+#include <wanda/setting.hpp>
+#include <wanda/wallpaper.hpp>
+
+#include <boost/gil.hpp>
+#include <boost/gil/extension/io/jpeg.hpp>
+#include <boost/gil/extension/io/png.hpp>
+#include <boost/gil/extension/numeric/resample.hpp>
+#include <boost/gil/extension/numeric/sampler.hpp>
+
+#include <fmt/format.h>
+
+#include <algorithm>
+#include <cmath>
+#include <memory>
+
+namespace wanda
+{
+ namespace
+ {
+ auto magic_instance = magic{};
+
+ auto load_image(std::filesystem::path wallpaper)
+ {
+ auto image = boost::gil::rgb8_image_t{};
+
+ switch (magic_instance.type(wallpaper))
+ {
+ case magic::mime_type::image_jpeg:
+ boost::gil::read_and_convert_image(wallpaper.native(), image, boost::gil::jpeg_tag{});
+ break;
+ case magic::mime_type::image_png:
+ boost::gil::read_and_convert_image(wallpaper.native(), image, boost::gil::png_tag{});
+ break;
+ }
+
+ return image;
+
+ // // auto source_view = ;
+
+ // return fmt::format("#{:02X}{:02X}{:02X}",
+ // static_cast<uint8_t>(std::sqrt((at_c<0>(pixel64) / image.size()))),
+ // static_cast<uint8_t>(std::sqrt((at_c<1>(pixel64) / image.size()))),
+ // static_cast<uint8_t>(std::sqrt((at_c<2>(pixel64) / image.size()))));
+ }
+
+ auto average_colors(boost::gil::rgb8_image_t image)
+ {
+ auto accumulator = boost::gil::rgb64f_pixel_t{};
+ auto view = const_view(image);
+
+ std::ranges::for_each(view, [&](auto const & source_pixel) {
+ at_c<0>(accumulator) += std::pow(boost::gil::at_c<0>(source_pixel), 2);
+ at_c<1>(accumulator) += std::pow(boost::gil::at_c<1>(source_pixel), 2);
+ at_c<2>(accumulator) += std::pow(boost::gil::at_c<2>(source_pixel), 2);
+ });
+
+ at_c<0>(accumulator) = std::sqrt(at_c<0>(accumulator) / view.size());
+ at_c<1>(accumulator) = std::sqrt(at_c<1>(accumulator) / view.size());
+ at_c<2>(accumulator) = std::sqrt(at_c<2>(accumulator) / view.size());
+
+ return accumulator;
+ }
+
+ //
+ } // namespace
+
+ void set_wallpaper(std::filesystem::path wallpaper)
+ {
+ using namespace wanda::literals;
+ using namespace wanda::std_ext;
+ using namespace std::string_literals;
+
+ auto image = load_image(wallpaper);
+ auto color = average_colors(std::move(image));
+ auto hexstring = fmt::format("#{:02X}{:02X}{:02X}",
+ static_cast<std::uint8_t>(at_c<0>(color)),
+ static_cast<std::uint8_t>(at_c<1>(color)),
+ static_cast<std::uint8_t>(at_c<2>(color)));
+
+ with("org.gnome.desktop.background"_setting, [&](auto & setting) {
+ with(setting["primary-color"_key], [&](auto & value) {
+ value = hexstring;
+ });
+ with(setting["picture-uri"_key], [&](auto & value) {
+ value = "file://" + wallpaper.native();
+ }) ||
+ [&] { get_logger()->error("invalid settings key"); };
+ }) ||
+ [&] { get_logger()->error("invalid setting"); };
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/src/wanda/wandac.cpp b/source/src/wanda/wandac.cpp
new file mode 100644
index 0000000..1873ef4
--- /dev/null
+++ b/source/src/wanda/wandac.cpp
@@ -0,0 +1,94 @@
+#include <wanda/command.hpp>
+#include <wanda/commander.hpp>
+#include <wanda/environment.hpp>
+#include <wanda/logging.hpp>
+#include <wanda/xdg.hpp>
+
+#include <asio.hpp>
+#include <lyra/lyra.hpp>
+#include <spdlog/sinks/stdout_color_sinks.h>
+#include <spdlog/spdlog.h>
+
+#include <cstdlib>
+#include <filesystem>
+#include <iostream>
+#include <memory>
+
+struct cli
+{
+ std::string command{};
+ bool help{};
+
+ lyra::cli_parser parser{};
+
+ auto parse(int argc, char const * const * argv, std::ostream & error)
+ {
+ parser |= lyra::arg{command, "command"}("The command to send to the deamon").required() |
+ lyra::help(help);
+
+ auto result = parser.parse({argc, argv});
+
+ if (!result)
+ {
+ error << "Error while processing command line arguments: "
+ << result.message()
+ << '\n'
+ << parser
+ << '\n';
+ return false;
+ }
+
+ return true;
+ }
+};
+
+struct listener : wanda::commander::listener
+{
+ listener(::cli & cli, asio::io_service & service)
+ : m_cli{cli}
+ , m_service{service}
+ {
+ }
+
+ void on_connected(wanda::commander & commander) override
+ {
+ if (m_cli.command == "change")
+ {
+ commander.send(wanda::make_change_command());
+ m_service.post([&]{
+ commander.stop();
+ });
+ }
+ }
+
+private:
+ ::cli & m_cli;
+ asio::io_service & m_service;
+};
+
+int main(int argc, char const * const * argv)
+{
+ auto cli = ::cli{};
+ if (!cli.parse(argc, argv, std::cerr))
+ {
+ return EXIT_FAILURE;
+ }
+ else if (cli.help)
+ {
+ std::cout << cli.parser << '\n';
+ return EXIT_SUCCESS;
+ }
+
+ wanda::initialize_logger(std::make_shared<spdlog::sinks::stderr_color_sink_st>());
+
+ auto interface = wanda::xdg_path_for(wanda::xdg_directory::runtime_dir, wanda::environment{}) / ".wanda_interface";
+ auto service = asio::io_service{};
+ auto listener = ::listener{cli, service};
+
+ auto commander = wanda::commander{service, interface, listener};
+
+ wanda::get_logger()->info("trying to connect to wanda control interface on '{}'", interface.native());
+ commander.start();
+
+ service.run();
+}
diff --git a/source/src/wanda/wandad.cpp b/source/src/wanda/wandad.cpp
new file mode 100644
index 0000000..8579a83
--- /dev/null
+++ b/source/src/wanda/wandad.cpp
@@ -0,0 +1,146 @@
+#include <wanda/command.hpp>
+#include <wanda/control_interface.hpp>
+#include <wanda/environment.hpp>
+#include <wanda/filesystem.hpp>
+#include <wanda/logging.hpp>
+#include <wanda/optional.hpp>
+#include <wanda/setting.hpp>
+#include <wanda/wallpaper.hpp>
+#include <wanda/xdg.hpp>
+
+#include <asio.hpp>
+#include <lyra/lyra.hpp>
+#include <spdlog/sinks/stdout_color_sinks.h>
+
+#include <csignal>
+#include <cstdlib>
+#include <iostream>
+#include <set>
+#include <string>
+
+namespace
+{
+ constexpr auto image_filter = [](auto const & path) {
+ static auto const extensions = std::set<std::filesystem::path>{
+ std::filesystem::path{".jpg"},
+ std::filesystem::path{".png"},
+ };
+
+ if (!std::filesystem::is_regular_file(path))
+ {
+ return false;
+ }
+
+ return extensions.find(path.extension()) != extensions.cend();
+ };
+
+ struct cli
+ {
+ std::string wallpaper_directory{};
+ bool help{};
+
+ lyra::cli_parser parser{};
+
+ auto parse(int argc, char const * const * argv, std::ostream & error)
+ {
+ parser |= lyra::arg{wallpaper_directory, "directory"}("The wallpaper source directory").required() |
+ lyra::help(help);
+
+ auto result = parser.parse({argc, argv});
+
+ if (!result)
+ {
+ error << "Error while processing command line arguments: "
+ << result.message()
+ << '\n'
+ << parser
+ << '\n';
+ return false;
+ }
+
+ return true;
+ }
+ };
+
+ struct listener : wanda::control_interface::listener
+ {
+ listener(std::vector<std::filesystem::path> const & wallpapers)
+ : m_wallpapers{wallpapers}
+ {
+ }
+
+ void on_received(wanda::control_interface & interface, wanda::command command) override
+ {
+ switch (command.id)
+ {
+ case wanda::command_id::change: {
+ auto wallpaper = wanda::random_pick(m_wallpapers);
+ wanda::get_logger()->info("changing wallpaper to '{}'", wallpaper.native());
+ wanda::set_wallpaper(wallpaper);
+ break;
+ }
+ default:
+ wanda::get_logger()->error("received unknown command '{}'", static_cast<int>(command.id));
+ }
+ }
+
+ private:
+ std::vector<std::filesystem::path> const & m_wallpapers;
+ };
+
+} // namespace
+
+int main(int argc, char const * const * argv)
+{
+ using namespace wanda::std_ext;
+
+ auto cli = ::cli{};
+ if (!cli.parse(argc, argv, std::cerr))
+ {
+ return EXIT_FAILURE;
+ }
+ else if (cli.help)
+ {
+ std::cout << cli.parser << '\n';
+ return EXIT_SUCCESS;
+ }
+
+ wanda::initialize_logger(std::make_shared<spdlog::sinks::stdout_color_sink_st>());
+ wanda::get_logger()->info("wanda is starting up");
+
+ with(wanda::scan({cli.wallpaper_directory}, image_filter), [&](auto const & list) {
+ auto service = asio::io_service{};
+ auto socket_path = wanda::xdg_path_for(wanda::xdg_directory::runtime_dir, wanda::environment{}) / ".wanda_interface";
+
+ wanda::get_logger()->info("starting control interface on '{}'", socket_path.native());
+ auto listener = ::listener{list};
+ auto interface = wanda::make_interface(service, socket_path, listener);
+
+ if (!interface)
+ {
+ wanda::get_logger()->error("failed to start control interface");
+ return;
+ }
+
+ if (interface->start())
+ {
+ return;
+ }
+
+ auto signals = asio::signal_set{service, SIGINT, SIGTERM};
+ signals.async_wait([&](auto const & error, auto const signal) {
+ if (!error)
+ {
+ wanda::get_logger()->info("Received signal {}. terminating...", signal);
+ interface->shutdown();
+ service.stop();
+ }
+ });
+
+ auto wallpaper = wanda::random_pick(list);
+ wanda::set_wallpaper(wallpaper);
+
+ service.run();
+ }) ||
+ [&] { wanda::get_logger()->error("wallpaper directory does not exist"); };
+}
diff --git a/source/src/wanda/xdg.cpp b/source/src/wanda/xdg.cpp
new file mode 100644
index 0000000..d49e53f
--- /dev/null
+++ b/source/src/wanda/xdg.cpp
@@ -0,0 +1,46 @@
+#include <wanda/xdg.hpp>
+
+#include <unistd.h>
+
+namespace wanda
+{
+ std::string xdg_variable(xdg_directory directory)
+ {
+ switch (directory)
+ {
+ case xdg_directory::data_home:
+ return "XDG_DATA_HOME";
+ case xdg_directory::config_home:
+ return "XDG_CONFIG_HOME";
+ case xdg_directory::cache_home:
+ return "XDG_CACHE_HOME";
+ case xdg_directory::runtime_dir:
+ return "XDG_RUNTIME_DIR";
+ }
+ return "XDG_INVALID_PATH";
+ }
+
+ std::filesystem::path xdg_path_for(xdg_directory directory, environment const & environment)
+ {
+ if (auto path = environment[xdg_variable(directory)]; !path.empty())
+ {
+ return path;
+ }
+
+ auto home = std::filesystem::path{environment["HOME"]};
+ switch (directory)
+ {
+ case xdg_directory::data_home:
+ return home / ".local/share";
+ case xdg_directory::config_home:
+ return home / ".config";
+ case xdg_directory::cache_home:
+ return home / ".cache";
+ case xdg_directory::runtime_dir:
+ return std::filesystem::path{"/run/user"} / std::to_string(::getuid());
+ }
+
+ return "";
+ }
+
+} // namespace wanda \ No newline at end of file
diff --git a/source/tests/wanda/driver.cpp b/source/tests/wanda/driver.cpp
new file mode 100644
index 0000000..e0dd7c8
--- /dev/null
+++ b/source/tests/wanda/driver.cpp
@@ -0,0 +1,23 @@
+#include "cute/cute.h"
+#include "cute/cute_runner.h"
+#include "cute/tap_listener.h"
+
+#include <algorithm>
+#include <iostream>
+#include <iterator>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace wanda::test
+{
+ std::pair<cute::suite, std::string> suite();
+}
+
+int main(int argc, char const *const *argv)
+{
+ auto listener = cute::tap_listener<>{std::cout};
+ auto runner = cute::makeRunner(listener, argc, argv);
+ auto suite = wanda::test::suite();
+ return !runner(suite.first, suite.second.c_str());
+} \ No newline at end of file
diff --git a/source/tests/wanda/test_suite_xdg.cpp b/source/tests/wanda/test_suite_xdg.cpp
new file mode 100644
index 0000000..70597f7
--- /dev/null
+++ b/source/tests/wanda/test_suite_xdg.cpp
@@ -0,0 +1,96 @@
+#include <wanda/xdg.hpp>
+
+#include <cute/cute.h>
+
+#include <unistd.h>
+
+#include <filesystem>
+#include <string>
+#include <utility>
+
+namespace wanda::test
+{
+
+namespace
+{
+ char const * home_only_environment[2] = {"HOME=/home/cute"};
+ char const * xdg_data_home_environment[2] = {"XDG_DATA_HOME=/home/cute/xdg_data_home"};
+ char const * xdg_config_home_environment[2] = {"XDG_CONFIG_HOME=/home/cute/xdg_config_home"};
+ char const * xdg_cache_home_environment[2] = {"XDG_CACHE_HOME=/home/cute/xdg_cache_home"};
+ char const * xdg_runtime_dir_environment[2] = {"XDG_RUNTIME_DIR=/home/cute/xdg_runtime_dir"};
+}
+
+void test_xdg_variables()
+{
+ ASSERT_EQUAL("XDG_DATA_HOME", xdg_variable(xdg_directory::data_home));
+ ASSERT_EQUAL("XDG_CONFIG_HOME", xdg_variable(xdg_directory::config_home));
+ ASSERT_EQUAL("XDG_CACHE_HOME", xdg_variable(xdg_directory::cache_home));
+ ASSERT_EQUAL("XDG_RUNTIME_DIR", xdg_variable(xdg_directory::runtime_dir));
+}
+
+void test_xdg_path_for_data_home_without_xdg_data_home_in_environment()
+{
+ auto env = environment{home_only_environment};
+ ASSERT_EQUAL("/home/cute/.local/share", xdg_path_for(xdg_directory::data_home, env));
+}
+
+void test_xdg_path_for_data_home_with_xdg_data_home_in_environment()
+{
+ auto env = environment{xdg_data_home_environment};
+ ASSERT_EQUAL("/home/cute/xdg_data_home", xdg_path_for(xdg_directory::data_home, env));
+}
+
+void test_xdg_path_for_config_home_without_xdg_config_home_in_environment()
+{
+ auto env = environment{home_only_environment};
+ ASSERT_EQUAL("/home/cute/.config", xdg_path_for(xdg_directory::config_home, env));
+}
+
+void test_xdg_path_for_config_home_with_xdg_config_home_in_environment()
+{
+ auto env = environment{xdg_config_home_environment};
+ ASSERT_EQUAL("/home/cute/xdg_config_home", xdg_path_for(xdg_directory::config_home, env));
+}
+
+void test_xdg_path_for_cache_home_without_xdg_cache_home_in_environment()
+{
+ auto env = environment{home_only_environment};
+ ASSERT_EQUAL("/home/cute/.cache", xdg_path_for(xdg_directory::cache_home, env));
+}
+
+void test_xdg_path_for_cache_home_with_xdg_cache_home_in_environment()
+{
+ auto env = environment{xdg_cache_home_environment};
+ ASSERT_EQUAL("/home/cute/xdg_cache_home", xdg_path_for(xdg_directory::cache_home, env));
+}
+
+void test_xdg_path_for_runtime_dir_without_xdg_runtime_dir_in_environment()
+{
+ auto env = environment{home_only_environment};
+ auto expected = std::filesystem::path{"/run/user"} / std::to_string(::getuid());
+ ASSERT_EQUAL(expected, xdg_path_for(xdg_directory::runtime_dir, env));
+}
+
+void test_xdg_path_for_runtime_dir_with_xdg_runtime_dir_in_environment()
+{
+ auto env = environment{xdg_runtime_dir_environment};
+ ASSERT_EQUAL("/home/cute/xdg_runtime_dir", xdg_path_for(xdg_directory::runtime_dir, env));
+}
+
+std::pair<cute::suite, std::string> suite()
+{
+ return std::make_pair(cute::suite{
+ CUTE(test_xdg_variables),
+ CUTE(test_xdg_path_for_data_home_without_xdg_data_home_in_environment),
+ CUTE(test_xdg_path_for_data_home_with_xdg_data_home_in_environment),
+ CUTE(test_xdg_path_for_config_home_without_xdg_config_home_in_environment),
+ CUTE(test_xdg_path_for_config_home_with_xdg_config_home_in_environment),
+ CUTE(test_xdg_path_for_cache_home_without_xdg_cache_home_in_environment),
+ CUTE(test_xdg_path_for_cache_home_with_xdg_cache_home_in_environment),
+ CUTE(test_xdg_path_for_runtime_dir_without_xdg_runtime_dir_in_environment),
+ CUTE(test_xdg_path_for_runtime_dir_with_xdg_runtime_dir_in_environment),
+ },
+ "XDG Utilities");
+}
+
+} // namespace wanda \ No newline at end of file