diff options
Diffstat (limited to 'source/lib/src')
| -rw-r--r-- | source/lib/src/control/commander.cpp | 80 | ||||
| -rw-r--r-- | source/lib/src/control/connection.cpp | 130 | ||||
| -rw-r--r-- | source/lib/src/control/interface.cpp | 160 | ||||
| -rw-r--r-- | source/lib/src/proto/command.cpp | 47 | ||||
| -rw-r--r-- | source/lib/src/proto/message.cpp | 82 | ||||
| -rw-r--r-- | source/lib/src/system/environment.cpp | 71 | ||||
| -rw-r--r-- | source/lib/src/system/filesystem.cpp | 31 | ||||
| -rw-r--r-- | source/lib/src/system/logging.cpp | 21 | ||||
| -rw-r--r-- | source/lib/src/system/setting.cpp | 102 | ||||
| -rw-r--r-- | source/lib/src/system/wallpaper.cpp | 91 | ||||
| -rw-r--r-- | source/lib/src/system/xdg.cpp | 46 |
11 files changed, 861 insertions, 0 deletions
diff --git a/source/lib/src/control/commander.cpp b/source/lib/src/control/commander.cpp new file mode 100644 index 0000000..4490bb7 --- /dev/null +++ b/source/lib/src/control/commander.cpp @@ -0,0 +1,80 @@ +#include "wanda/control/commander.hpp" + +#include "wanda/proto/message.hpp" +#include "wanda/proto/version.hpp" +#include "wanda/std_ext/optional.hpp" +#include "wanda/system/logging.hpp" + +#include <boost/asio/io_context.hpp> +#include <spdlog/fmt/ostr.h> + +namespace wanda::control +{ + commander::commander(boost::asio::io_context & 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) + { + system::get_logger()->error("error while connecting to control interface: '{}'", error.message()); + } + else + { + system::get_logger()->info("establishing connection to wanda deamon"); + m_connection = wanda::control::make_connection(std::move(m_socket)); + m_connection->add(this); + m_connection->start(); + m_connection->send({proto::message_source_controller, proto::message_command_hello, proto::version}); + } + }); + } + + void commander::stop() + { + system::get_logger()->info("closing control connection"); + m_connection->close(); + } + + void commander::send(proto::command command) + { + using namespace wanda::std_ext; + + if (!m_connection || m_connection->current_state() != connection::state::established) + { + system::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); }) || + [&] { system::get_logger()->error("unknown command"); }; + } + + void commander::on_error(connection::pointer connection, std::error_code error) + { + system::get_logger()->error("control interface communication error: '{}'", error.message()); + } + + void commander::on_received(connection::pointer connection, proto::message message) + { + if (auto state = connection->current_state(); message.command == "HELLO" && state == connection::state::fresh) + { + system::get_logger()->info("connection to wanda deamon successfully established"); + connection->update(connection::state::established); + m_listener.on_connected(*this); + } + else + { + system::get_logger()->error("unexpected message: '{}'", message); + m_listener.on_error(*this, "unexpected message '" + static_cast<std::string>(message) + '\''); + } + } + +} // namespace wanda::control
\ No newline at end of file diff --git a/source/lib/src/control/connection.cpp b/source/lib/src/control/connection.cpp new file mode 100644 index 0000000..97f41dd --- /dev/null +++ b/source/lib/src/control/connection.cpp @@ -0,0 +1,130 @@ +#include "wanda/control/connection.hpp" + +#include "wanda/proto/message.hpp" + +#include <boost/asio/completion_condition.hpp> +#include <boost/asio/read_until.hpp> +#include <boost/asio/write.hpp> +#include <boost/system/detail/error_code.hpp> +#include <boost/system/error_code.hpp> + +#include <limits> + +namespace wanda::control +{ + connection::pointer make_connection(connection::protocol::socket && socket) + { + return std::make_shared<connection>(connection::key{}, std::move(socket)); + } + + connection::connection(connection::key key, connection::protocol::socket socket) + : keyed{key} + , m_socket{std::move(socket)} + { + } + + bool connection::add(listener * listener) + { + auto [_, inserted] = m_listeners.insert(listener); + return inserted; + } + + bool connection::remove(listener * listener) + { + return m_listeners.erase(listener); + } + + void connection::start() + { + if (m_state == state::unknown) + { + m_state = state::fresh; + perform_read(); + } + } + + void connection::send(proto::message message) + { + m_output << message << '\n'; + boost::asio::async_write(m_socket, m_out, boost::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 connection::close() + { + auto error = boost::system::error_code{}; + + if (m_socket.cancel(error), error) + { + for (auto & listener : m_listeners) + { + listener->on_error(shared_from_this(), error); + } + } + + if (m_socket.close(error), 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 connection::update(state state) + { + m_state = state; + } + + connection::state connection::current_state() const + { + return m_state; + } + + void connection::perform_read() + { + boost::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 = proto::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::control
\ No newline at end of file diff --git a/source/lib/src/control/interface.cpp b/source/lib/src/control/interface.cpp new file mode 100644 index 0000000..3ebc55a --- /dev/null +++ b/source/lib/src/control/interface.cpp @@ -0,0 +1,160 @@ +#include "wanda/control/interface.hpp" + +#include "wanda/proto/version.hpp" +#include "wanda/std_ext/optional.hpp" +#include "wanda/system/logging.hpp" + +#include <spdlog/fmt/ostr.h> +#include <boost/system/error_code.hpp> +#include <boost/asio/error.hpp> +#include <unistd.h> + +#include <algorithm> +#include <iterator> +#include <string> +#include <system_error> +#include <utility> + +namespace wanda::control +{ + // 'socket_deleter' implementation + + socket_deleter::~socket_deleter() + { + if (std::filesystem::exists(path)) + { + std::filesystem::remove(path); + } + } + + // 'interface' implementation + + interface::interface(interface::key key, boost::asio::io_context & service, 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 interface::start() + { + if (auto error = boost::system::error_code{}; m_acceptor.open(m_endpoint.protocol(), error), error) + { + return error; + } + + if (auto error = boost::system::error_code{}; m_acceptor.bind(m_endpoint, error), error) + { + return error; + } + + if (auto error = boost::system::error_code{}; m_acceptor.listen(128, error), error) + { + return error; + } + else + { + perform_accept(); + return error; + } + } + + std::error_code interface::shutdown() + { + for (auto & connection : m_connections) + { + connection->close(); + } + + auto error = boost::system::error_code{}; + return m_acceptor.close(error), error; + } + + void interface::perform_accept() + { + m_acceptor.async_accept(m_socket, [that = shared_from_this(), this](auto const & error) { + if (error && error != boost::asio::error::operation_aborted) + { + system::get_logger()->error("failed to accept connection because '{}'", error.message()); + } + else + { + system::get_logger()->info("new incoming controller connection"); + auto [connection, inserted] = m_connections.insert(make_connection(std::move(m_socket))); + if (inserted) + { + (*connection)->add(this); + (*connection)->start(); + } + perform_accept(); + } + }); + } + + void interface::on_close(connection::pointer connection) + { + if (static_cast<char>(connection->current_state()) >= static_cast<char>(connection::state::established)) + { + system::get_logger()->info("controller connection closed"); + } + else + { + system::get_logger()->info("controller connection aborted before it could be established"); + } + m_connections.erase(connection); + } + + void interface::on_received(connection::pointer connection, proto::message message) + { + using namespace wanda::std_ext; + + if (m_connections.find(connection) == m_connections.cend()) + { + system::get_logger()->error("received message from an unknown connection"); + return; + } + + if (message.source != proto::message_source_controller) + { + system::get_logger()->error("received a deamon message"); + return; + } + + if (auto state = connection->current_state(); message.command == proto::message_command_hello && state == connection::state::fresh) + { + system::get_logger()->info("controller connection established"); + if (message.argument.has_value()) + { + system::get_logger()->info("remote controller version '{}'", *message.argument); + } + connection->send({proto::message_source_daemon, proto::message_command_hello, proto::version}); + connection->update(connection::state::established); + } + else + { + with(make_command(message), [&](auto const & command) { + m_listener.on_received(*this, command); + }) || + [&] { system::get_logger()->warn("ignoring unknown message '{}'", message); }; + } + } + + interface::pointer make_interface(boost::asio::io_context & service, std::filesystem::path socket, interface::listener & listener) + { + if (std::filesystem::exists(socket)) + { + system::get_logger()->error("socket '{}' exists", socket.native()); + return {}; + } + + interface::protocol::endpoint endpoint + { + socket.string() + }; + return std::make_shared<interface>(interface::key{}, service, std::move(endpoint), listener); + } + +} // namespace wanda::control
\ No newline at end of file diff --git a/source/lib/src/proto/command.cpp b/source/lib/src/proto/command.cpp new file mode 100644 index 0000000..5a669f5 --- /dev/null +++ b/source/lib/src/proto/command.cpp @@ -0,0 +1,47 @@ +#include "wanda/proto/command.hpp" + +namespace wanda::proto +{ + 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::proto::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::proto
\ No newline at end of file diff --git a/source/lib/src/proto/message.cpp b/source/lib/src/proto/message.cpp new file mode 100644 index 0000000..f44ca06 --- /dev/null +++ b/source/lib/src/proto/message.cpp @@ -0,0 +1,82 @@ +#include "wanda/proto/message.hpp" + +#include <spdlog/common.h> + +#include <ios> +#include <iterator> +#include <sstream> + +namespace wanda::proto +{ + 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::proto + +auto spdlog::fmt_lib::formatter<wanda::proto::message>::format(wanda::proto::message const & message, format_context & context) const -> decltype(context.out()) +{ + return formatter<std::string>::format(static_cast<std::string>(message), context); +} diff --git a/source/lib/src/system/environment.cpp b/source/lib/src/system/environment.cpp new file mode 100644 index 0000000..8f79531 --- /dev/null +++ b/source/lib/src/system/environment.cpp @@ -0,0 +1,71 @@ +#include "wanda/system/environment.hpp" + +#include <string> + +namespace wanda::system +{ + 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::system
\ No newline at end of file diff --git a/source/lib/src/system/filesystem.cpp b/source/lib/src/system/filesystem.cpp new file mode 100644 index 0000000..04dfff3 --- /dev/null +++ b/source/lib/src/system/filesystem.cpp @@ -0,0 +1,31 @@ +#include "wanda/system/filesystem.hpp" + +#include <random> +#include <ranges> + +namespace wanda::system +{ + 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::system
\ No newline at end of file diff --git a/source/lib/src/system/logging.cpp b/source/lib/src/system/logging.cpp new file mode 100644 index 0000000..83dc9f6 --- /dev/null +++ b/source/lib/src/system/logging.cpp @@ -0,0 +1,21 @@ +#include "wanda/system/logging.hpp" + +namespace wanda::system +{ + 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::system
\ No newline at end of file diff --git a/source/lib/src/system/setting.cpp b/source/lib/src/system/setting.cpp new file mode 100644 index 0000000..1553eae --- /dev/null +++ b/source/lib/src/system/setting.cpp @@ -0,0 +1,102 @@ +#include "wanda/system/setting.hpp" + +#include <algorithm> +#include <type_traits> + +namespace wanda::system +{ + // 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::system
\ No newline at end of file diff --git a/source/lib/src/system/wallpaper.cpp b/source/lib/src/system/wallpaper.cpp new file mode 100644 index 0000000..11a6402 --- /dev/null +++ b/source/lib/src/system/wallpaper.cpp @@ -0,0 +1,91 @@ +#include "wanda/system/wallpaper.hpp" + +#include "wanda/std_ext/optional.hpp" +#include "wanda/system/logging.hpp" +#include "wanda/system/magic.hpp" +#include "wanda/system/setting.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 <spdlog/fmt/fmt.h> + +#include <algorithm> +#include <cmath> +#include <memory> + +namespace wanda::system +{ + 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 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::system::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 = spdlog::fmt_lib::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"); }; + with(setting["picture-uri-dark"_key], [&](auto & value) { + value = "file://" + wallpaper.native(); + }) || + [&] { get_logger()->error("invalid settings key"); }; + }) || + [&] { get_logger()->error("invalid setting"); }; + } + +} // namespace wanda::system diff --git a/source/lib/src/system/xdg.cpp b/source/lib/src/system/xdg.cpp new file mode 100644 index 0000000..109beb1 --- /dev/null +++ b/source/lib/src/system/xdg.cpp @@ -0,0 +1,46 @@ +#include "wanda/system/xdg.hpp" + +#include <unistd.h> + +namespace wanda::system +{ + 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::system
\ No newline at end of file |
