aboutsummaryrefslogtreecommitdiff
path: root/source/lib/src
diff options
context:
space:
mode:
Diffstat (limited to 'source/lib/src')
-rw-r--r--source/lib/src/command.cpp47
-rw-r--r--source/lib/src/commander.cpp77
-rw-r--r--source/lib/src/control_connection.cpp120
-rw-r--r--source/lib/src/control_interface.cpp154
-rw-r--r--source/lib/src/environment.cpp71
-rw-r--r--source/lib/src/filesystem.cpp31
-rw-r--r--source/lib/src/logging.cpp21
-rw-r--r--source/lib/src/message.cpp75
-rw-r--r--source/lib/src/setting.cpp102
-rw-r--r--source/lib/src/wallpaper.cpp87
-rw-r--r--source/lib/src/xdg.cpp46
11 files changed, 831 insertions, 0 deletions
diff --git a/source/lib/src/command.cpp b/source/lib/src/command.cpp
new file mode 100644
index 0000000..83c6b73
--- /dev/null
+++ b/source/lib/src/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/lib/src/commander.cpp b/source/lib/src/commander.cpp
new file mode 100644
index 0000000..5122b62
--- /dev/null
+++ b/source/lib/src/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/lib/src/control_connection.cpp b/source/lib/src/control_connection.cpp
new file mode 100644
index 0000000..ace13d2
--- /dev/null
+++ b/source/lib/src/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/lib/src/control_interface.cpp b/source/lib/src/control_interface.cpp
new file mode 100644
index 0000000..5f4cf26
--- /dev/null
+++ b/source/lib/src/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/lib/src/environment.cpp b/source/lib/src/environment.cpp
new file mode 100644
index 0000000..2f1af0a
--- /dev/null
+++ b/source/lib/src/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/lib/src/filesystem.cpp b/source/lib/src/filesystem.cpp
new file mode 100644
index 0000000..35b6b40
--- /dev/null
+++ b/source/lib/src/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/lib/src/logging.cpp b/source/lib/src/logging.cpp
new file mode 100644
index 0000000..0aa7e40
--- /dev/null
+++ b/source/lib/src/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/lib/src/message.cpp b/source/lib/src/message.cpp
new file mode 100644
index 0000000..34930bc
--- /dev/null
+++ b/source/lib/src/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/lib/src/setting.cpp b/source/lib/src/setting.cpp
new file mode 100644
index 0000000..b3f661e
--- /dev/null
+++ b/source/lib/src/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/lib/src/wallpaper.cpp b/source/lib/src/wallpaper.cpp
new file mode 100644
index 0000000..25cb493
--- /dev/null
+++ b/source/lib/src/wallpaper.cpp
@@ -0,0 +1,87 @@
+#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 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/lib/src/xdg.cpp b/source/lib/src/xdg.cpp
new file mode 100644
index 0000000..cfd1719
--- /dev/null
+++ b/source/lib/src/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