diff options
Diffstat (limited to 'source')
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 |
