summaryrefslogtreecommitdiff
path: root/src/util
diff options
context:
space:
mode:
authorpants <niklasjo2004@gmail.com>2026-09-21 14:18:06 -0700
committerpants <niklasjo2004@gmail.com>2026-09-21 14:18:06 -0700
commit83d5ba42f8a98fc0df177efef4333f74a0481f61 (patch)
tree6f1fcff64ec9adb2fc51a7cfcc451eb22a103fb3 /src/util
Diffstat (limited to 'src/util')
-rw-r--r--src/util/dbus.cpp15
-rw-r--r--src/util/egl.cpp228
-rw-r--r--src/util/socket.cpp169
-rw-r--r--src/util/util.cpp262
-rw-r--r--src/util/x11.cpp171
5 files changed, 845 insertions, 0 deletions
diff --git a/src/util/dbus.cpp b/src/util/dbus.cpp
new file mode 100644
index 0000000..1cc78e5
--- /dev/null
+++ b/src/util/dbus.cpp
@@ -0,0 +1,15 @@
+#include "util/dbus.hpp"
+#include <stdexcept>
+
+DbusUtil::DbusUtil(const std::string &address)
+ : connection(dbus_connection_open(address.c_str(), nullptr))
+{
+ if (connection == nullptr) {
+ throw std::invalid_argument("Could not connect to dbus address");
+ }
+}
+
+DbusUtil::~DbusUtil()
+{
+ dbus_connection_unref(connection);
+}
diff --git a/src/util/egl.cpp b/src/util/egl.cpp
new file mode 100644
index 0000000..5b64459
--- /dev/null
+++ b/src/util/egl.cpp
@@ -0,0 +1,228 @@
+// Display images inside a terminal
+// Copyright (C) 2023 JustKidding
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+#include "util/egl.hpp"
+#include "image.hpp"
+
+#include <range/v3/all.hpp>
+#include <spdlog/spdlog.h>
+
+#include <string_view>
+#include <unordered_map>
+
+constexpr EGLint egl_major_version = 1;
+constexpr EGLint egl_minor_version = 5;
+constexpr EGLint opengl_major_version = 4;
+constexpr EGLint opengl_minor_version = 6;
+
+constexpr auto config_attrs = std::to_array<EGLint>({EGL_SURFACE_TYPE,
+ EGL_WINDOW_BIT,
+ EGL_CONFORMANT,
+ EGL_OPENGL_BIT,
+ EGL_RENDERABLE_TYPE,
+ EGL_OPENGL_BIT,
+ EGL_COLOR_BUFFER_TYPE,
+ EGL_RGB_BUFFER,
+
+ EGL_RED_SIZE,
+ 8,
+ EGL_GREEN_SIZE,
+ 8,
+ EGL_BLUE_SIZE,
+ 8,
+ EGL_ALPHA_SIZE,
+ 8,
+
+ EGL_DEPTH_SIZE,
+ 24,
+ EGL_STENCIL_SIZE,
+ 8,
+
+ EGL_NONE});
+
+const auto context_attrs = std::to_array<EGLint>(
+ {EGL_CONTEXT_MAJOR_VERSION, opengl_major_version, EGL_CONTEXT_MINOR_VERSION, opengl_minor_version,
+#ifdef DEBUG
+ EGL_CONTEXT_OPENGL_DEBUG, EGL_TRUE,
+#endif
+ EGL_CONTEXT_OPENGL_ROBUST_ACCESS, EGL_TRUE, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
+ EGL_NONE});
+
+void GLAPIENTRY debug_callback([[maybe_unused]] GLenum source, [[maybe_unused]] GLenum type,
+ [[maybe_unused]] GLuint gl_id, GLenum severity, [[maybe_unused]] GLsizei length,
+ const GLchar *message, [[maybe_unused]] const void *user)
+{
+ if (type != GL_DEBUG_TYPE_ERROR) {
+ return;
+ }
+ const auto logger = spdlog::get("opengl");
+ logger->error("Type: {:#X}, Severity: {:#X}, Message: {}", type, severity, message);
+}
+
+template <class T, class V>
+EGLUtil<T, V>::EGLUtil(EGLenum platform, T *native_display, const EGLAttrib *attrib)
+ : display(eglGetPlatformDisplay(platform, native_display, attrib))
+{
+ logger = spdlog::get("opengl");
+
+ if (display == EGL_NO_DISPLAY) {
+ const auto err = error_to_string();
+ logger->error("Could not obtain display, error {}", err);
+ throw std::runtime_error("");
+ }
+
+ EGLint egl_major = 0;
+ EGLint egl_minor = 0;
+ EGLBoolean eglres = eglInitialize(display, &egl_major, &egl_minor);
+ if (eglres != EGL_TRUE) {
+ const auto err = error_to_string();
+ logger->error("Could not initialize display, error {}", err);
+ throw std::runtime_error("");
+ }
+ if (egl_major != egl_major_version && egl_minor != egl_minor_version) {
+ logger->error("EGL {}.{} is not available", egl_major_version, egl_minor_version);
+ throw std::runtime_error("");
+ }
+
+ eglres = eglBindAPI(EGL_OPENGL_API);
+ if (eglres != EGL_TRUE) {
+ const auto err = error_to_string();
+ logger->error("Could not bind to OpenGL API, error {}", err);
+ throw std::runtime_error("");
+ }
+
+ int num_config = 0;
+ eglres = eglChooseConfig(display, config_attrs.data(), &config, 1, &num_config);
+ if (eglres != EGL_TRUE || num_config != 1) {
+ const auto err = error_to_string();
+ logger->error("Could not create config, error {}", err);
+ throw std::runtime_error("");
+ }
+
+ logger->info("Using EGL {}.{} and OpenGL {}.{}", egl_major_version, egl_minor_version, opengl_major_version,
+ opengl_minor_version);
+}
+
+template <class T, class V>
+EGLUtil<T, V>::~EGLUtil()
+{
+ eglTerminate(display);
+}
+
+template <class T, class V>
+auto EGLUtil<T, V>::error_to_string() const -> std::string
+{
+ using pair_type = std::pair<EGLint, std::string_view>;
+ constexpr auto error_codes = std::to_array<pair_type>({
+ {EGL_SUCCESS, "EGL_SUCCESS"},
+ {EGL_BAD_ACCESS, "EGL_BAD_ACCESS"},
+ {EGL_BAD_ALLOC, "EGL_BAD_ALLOC"},
+ {EGL_BAD_ATTRIBUTE, "EGL_BAD_ATTRIBUTE"},
+ {EGL_BAD_CONFIG, "EGL_BAD_CONFIG"},
+ {EGL_BAD_CONTEXT, "EGL_BAD_CONTEXT"},
+ {EGL_BAD_CURRENT_SURFACE, "EGL_BAD_CURRENT_SURFACE"},
+ {EGL_BAD_DISPLAY, "EGL_BAD_DISPLAY"},
+ {EGL_BAD_MATCH, "EGL_BAD_MATCH"},
+ {EGL_BAD_NATIVE_PIXMAP, "EGL_BAD_NATIVE_PIXMAP"},
+ {EGL_BAD_NATIVE_WINDOW, "EGL_BAD_NATIVE_WINDOW"},
+ {EGL_BAD_PARAMETER, "EGL_BAD_PARAMETER"},
+ {EGL_BAD_SURFACE, "EGL_BAD_SURFACE"},
+ });
+
+ const auto current_error = eglGetError();
+
+ const auto found =
+ ranges::find_if(error_codes, [current_error](const pair_type &pair) { return pair.first == current_error; });
+
+ if (found == error_codes.end()) {
+ return "EGL_UNKNOWN_ERROR";
+ }
+
+ return std::string(found->second);
+}
+
+template <class T, class V>
+auto EGLUtil<T, V>::create_context(EGLSurface surface) const -> EGLContext
+{
+ EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attrs.data());
+ if (context == EGL_NO_CONTEXT) {
+ const auto err = error_to_string();
+ logger->error("Could not create context, error {}", err);
+ return context;
+ }
+
+#ifdef DEBUG
+ run_contained(surface, context, [] {
+ glDebugMessageCallback(debug_callback, nullptr);
+ glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
+ });
+#endif
+
+ return context;
+}
+
+template <class T, class V>
+auto EGLUtil<T, V>::create_surface(V *native_window) const -> EGLSurface
+{
+ EGLSurface surface = eglCreatePlatformWindowSurface(display, config, native_window, nullptr);
+ if (surface == EGL_NO_SURFACE) {
+ const auto err = error_to_string();
+ logger->error("Could not create surface, error {}", err);
+ }
+ return surface;
+}
+
+template <class T, class V>
+void EGLUtil<T, V>::make_current(EGLSurface surface, EGLContext context) const
+{
+ eglMakeCurrent(display, surface, surface, context);
+}
+
+template <class T, class V>
+void EGLUtil<T, V>::restore() const
+{
+ eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+}
+
+template <class T, class V>
+void EGLUtil<T, V>::run_contained(EGLSurface surface, EGLContext context, const std::function<void()> &func) const
+{
+ make_current(surface, context);
+ func();
+ restore();
+}
+
+template <class T, class V>
+void EGLUtil<T, V>::get_texture_from_image(const Image &image, GLuint texture) const
+{
+ glBindTexture(GL_TEXTURE_2D, texture);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width(), image.height(), 0, GL_BGRA, GL_UNSIGNED_BYTE, image.data());
+}
+
+#ifdef ENABLE_X11
+# include <xcb/xcb.h>
+template class EGLUtil<xcb_connection_t, xcb_window_t>;
+#endif
+
+#ifdef ENABLE_WAYLAND
+# include <wayland-client.h>
+# include <wayland-egl.h>
+template class EGLUtil<struct wl_display, struct wl_egl_window>;
+#endif
diff --git a/src/util/socket.cpp b/src/util/socket.cpp
new file mode 100644
index 0000000..d770270
--- /dev/null
+++ b/src/util/socket.cpp
@@ -0,0 +1,169 @@
+// Display images inside a terminal
+// Copyright (C) 2023 JustKidding
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+#include "util/socket.hpp"
+
+#include <array>
+#include <cerrno>
+#include <cstring>
+#include <filesystem>
+#include <system_error>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "os.hpp"
+#include "util.hpp"
+
+namespace fs = std::filesystem;
+
+UnixSocket::UnixSocket(const std::string_view endpoint)
+ : fd(socket(AF_UNIX, SOCK_STREAM, 0))
+{
+ if (fd == -1) {
+ throw std::system_error(errno, std::generic_category());
+ }
+ connect_to_endpoint(endpoint);
+}
+
+UnixSocket::UnixSocket()
+ : fd(socket(AF_UNIX, SOCK_STREAM, 0))
+{
+ if (fd == -1) {
+ throw std::system_error(errno, std::generic_category());
+ }
+ const int bufsize = 8192;
+ buffer.reserve(bufsize);
+}
+
+void UnixSocket::connect_to_endpoint(const std::string_view endpoint)
+{
+ if (!fs::exists(endpoint)) {
+ connected = false;
+ return;
+ }
+ struct sockaddr_un sock;
+ std::memset(&sock, 0, sizeof(sockaddr_un));
+ sock.sun_family = AF_UNIX;
+ endpoint.copy(sock.sun_path, endpoint.size());
+
+ int res = connect(fd, reinterpret_cast<const struct sockaddr *>(&sock), sizeof(struct sockaddr_un));
+ if (res == -1) {
+ throw std::system_error(errno, std::generic_category());
+ }
+}
+
+void UnixSocket::bind_to_endpoint(const std::string_view endpoint) const
+{
+ struct sockaddr_un sock;
+ std::memset(&sock, 0, sizeof(sockaddr_un));
+ sock.sun_family = AF_UNIX;
+ endpoint.copy(sock.sun_path, endpoint.size());
+
+ int res = bind(fd, reinterpret_cast<const struct sockaddr *>(&sock), sizeof(struct sockaddr_un));
+ if (res == -1) {
+ throw std::system_error(errno, std::generic_category());
+ }
+ res = listen(fd, SOMAXCONN);
+ if (res == -1) {
+ throw std::system_error(errno, std::generic_category());
+ }
+}
+
+auto UnixSocket::wait_for_connections(int waitms) const -> int
+{
+ const auto in_event = os::wait_for_data_on_fd(fd, waitms);
+ if (in_event) {
+ return accept(fd, nullptr, nullptr);
+ }
+ return -1;
+}
+
+auto UnixSocket::read_data_from_connection(int filde) -> std::vector<std::string>
+{
+ // a single connection could send multiples commands at once
+ // each command should end with a '\n' character
+ const int read_buffer_size = 4096;
+ std::array<char, read_buffer_size> read_buffer;
+ while (true) {
+ const auto status = recv(filde, read_buffer.data(), read_buffer_size, 0);
+ if (status <= 0) {
+ break;
+ }
+ buffer.append(read_buffer.data(), status);
+ }
+ auto cmds = util::str_split(buffer, "\n");
+ buffer.clear();
+ close(filde);
+ return cmds;
+}
+
+void UnixSocket::write(const void *data, std::size_t len) const
+{
+ if (!connected) {
+ return;
+ }
+ const auto *runner = static_cast<const uint8_t *>(data);
+ while (len != 0) {
+ const auto status = send(fd, runner, len, MSG_NOSIGNAL);
+ if (status == -1) {
+ throw std::system_error(errno, std::generic_category());
+ }
+ len -= status;
+ runner += status;
+ }
+}
+
+void UnixSocket::read(void *data, std::size_t len) const
+{
+ if (!connected) {
+ return;
+ }
+ auto *runner = static_cast<uint8_t *>(data);
+ while (len != 0) {
+ const auto status = recv(fd, runner, len, 0);
+ if (status == 0) {
+ return; // no data
+ }
+ if (status == -1) {
+ throw std::system_error(errno, std::generic_category());
+ }
+ len -= status;
+ runner += status;
+ }
+}
+
+auto UnixSocket::read_until_empty() const -> std::string
+{
+ std::string result;
+ const int read_buffer_size = 4096;
+ std::array<char, read_buffer_size> read_buffer;
+ result.reserve(read_buffer_size);
+ while (true) {
+ const auto status = recv(fd, read_buffer.data(), read_buffer_size, 0);
+ if (status <= 0) {
+ break;
+ }
+ result.append(read_buffer.data(), status);
+ }
+ return result;
+}
+
+UnixSocket::~UnixSocket()
+{
+ close(fd);
+}
diff --git a/src/util/util.cpp b/src/util/util.cpp
new file mode 100644
index 0000000..2d3e99f
--- /dev/null
+++ b/src/util/util.cpp
@@ -0,0 +1,262 @@
+// Display images inside a terminal
+// Copyright (C) 2023 JustKidding
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+#include "util.hpp"
+#include "flags.hpp"
+#include "os.hpp"
+#include "process.hpp"
+#include "util/ptr.hpp"
+#include "util/socket.hpp"
+
+#include <array>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <random>
+#include <sstream>
+
+#include <fmt/format.h>
+#include <nlohmann/json.hpp>
+#include <openssl/evp.h>
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+# define EVP_MD_CTX_new EVP_MD_CTX_create
+# define EVP_MD_CTX_free EVP_MD_CTX_destroy
+#endif
+#ifdef ENABLE_TURBOBASE64
+# ifdef WITH_SYSTEM_TURBOBASE64
+# include <turbobase64/turbob64.h>
+# else
+# include "turbob64.h"
+# endif
+#endif
+#include <range/v3/all.hpp>
+
+#include <vips/vips8>
+
+namespace fs = std::filesystem;
+using njson = nlohmann::json;
+
+auto util::str_split(std::string_view str, std::string_view delim) -> std::vector<std::string>
+{
+ std::vector<std::string> result;
+ for (const auto word : ranges::views::split(str, delim)) {
+ result.emplace_back(ranges::to<std::string>(word));
+ }
+ return result;
+}
+
+auto util::get_process_tree(int pid) -> std::vector<int>
+{
+ std::vector<int> res;
+ Process runner(pid);
+ while (runner.pid > 1) {
+ res.push_back(runner.pid);
+ runner = Process(runner.ppid);
+ }
+ return res;
+}
+
+auto util::get_process_tree_v2(int pid) -> std::vector<Process>
+{
+ std::vector<Process> tree;
+ Process runner(pid);
+ while (runner.pid > 1) {
+ tree.push_back(runner);
+ runner = Process(runner.ppid);
+ }
+ return tree;
+}
+
+auto util::get_cache_path() -> std::string
+{
+ const auto home = os::getenv("HOME").value_or(util::temp_directory_path());
+ const auto cache_home = os::getenv("XDG_CACHE_HOME").value_or(fmt::format("{}/.cache", home));
+ return fmt::format("{}/ueberzugpp/", cache_home);
+}
+
+auto util::get_log_filename() -> std::string
+{
+ const auto user = os::getenv("USER").value_or("NOUSER");
+ return fmt::format("ueberzugpp-{}.log", user);
+}
+
+auto util::get_socket_path(int pid) -> std::string
+{
+ return fmt::format("{}/ueberzugpp-{}.socket", util::temp_directory_path().string(), pid);
+}
+
+void util::send_socket_message(const std::string_view msg, const std::string_view endpoint)
+{
+ try {
+ UnixSocket socket;
+ socket.connect_to_endpoint(endpoint);
+ socket.write(msg.data(), msg.size());
+ } catch (const std::system_error &err) {
+ return;
+ }
+}
+
+auto util::base64_encode(const unsigned char *input, size_t length) -> std::string
+{
+ const size_t bufsize = 4 * ((length + 2) / 3) + 1;
+ std::vector<char> res(bufsize, 0);
+ base64_encode_v2(input, length, reinterpret_cast<unsigned char *>(res.data()));
+ return {res.data()};
+}
+
+void util::base64_encode_v2(const unsigned char *input, size_t length, unsigned char *out)
+{
+#ifdef ENABLE_TURBOBASE64
+ tb64enc(input, length, out);
+#else
+ EVP_EncodeBlock(out, input, static_cast<int>(length));
+#endif
+}
+
+auto util::get_b2_hash_ssl(const std::string_view str) -> std::string
+{
+ std::stringstream sstream;
+ const auto mdctx = c_unique_ptr<EVP_MD_CTX, EVP_MD_CTX_free>{EVP_MD_CTX_new()};
+#ifdef LIBRESSL_VERSION_NUMBER
+ const auto *evp = EVP_sha256();
+#else
+ const auto *evp = EVP_blake2b512();
+#endif
+ auto digest = std::array<unsigned char, EVP_MAX_MD_SIZE>();
+
+ EVP_DigestInit_ex(mdctx.get(), evp, nullptr);
+ EVP_DigestUpdate(mdctx.get(), str.data(), str.size());
+ unsigned int digest_len = 0;
+ EVP_DigestFinal_ex(mdctx.get(), digest.data(), &digest_len);
+
+ sstream << std::hex << std::setw(2) << std::setfill('0');
+ for (unsigned int i = 0; i < digest_len; ++i) {
+ sstream << static_cast<int>(digest[i]);
+ }
+ return sstream.str();
+}
+
+void util::move_cursor(int row, int col)
+{
+ std::cout << "\033[" << row << ";" << col << "f" << std::flush;
+}
+
+void util::save_cursor_position()
+{
+ std::cout << "\0337" << std::flush;
+}
+
+void util::restore_cursor_position()
+{
+ std::cout << "\0338" << std::flush;
+}
+
+auto util::get_cache_file_save_location(const fs::path &path) -> std::string
+{
+ return fmt::format("{}{}{}", get_cache_path(), get_b2_hash_ssl(path.string()), path.extension().string());
+}
+
+void util::benchmark(const std::function<void(void)> &func)
+{
+ using std::chrono::duration;
+ using std::chrono::duration_cast;
+ using std::chrono::high_resolution_clock;
+ using std::chrono::milliseconds;
+
+ const auto ti1 = high_resolution_clock::now();
+ func();
+ const auto ti2 = high_resolution_clock::now();
+ const duration<double, std::milli> ms_double = ti2 - ti1;
+
+ std::cout << ms_double.count() << "ms\n";
+}
+
+void util::send_command(const Flags &flags)
+{
+ if (flags.cmd_action == "exit") {
+ util::send_socket_message("EXIT", flags.cmd_socket);
+ return;
+ }
+
+ if (flags.cmd_action == "remove") {
+ const njson json = {{"action", "remove"}, {"identifier", flags.cmd_id}};
+ util::send_socket_message(json.dump(), flags.cmd_socket);
+ return;
+ }
+
+ const njson json = {{"action", flags.cmd_action},
+ {"identifier", flags.cmd_id},
+ {"max_width", std::stoi(flags.cmd_max_width)},
+ {"max_height", std::stoi(flags.cmd_max_height)},
+ {"x", std::stoi(flags.cmd_x)},
+ {"y", std::stoi(flags.cmd_y)},
+ {"path", flags.cmd_file_path}};
+ util::send_socket_message(json.dump(), flags.cmd_socket);
+}
+
+void util::clear_terminal_area(int xcoord, int ycoord, int width, int height)
+{
+ save_cursor_position();
+ const auto line_clear = std::string(width, ' ');
+ for (int i = ycoord; i <= height + 2; ++i) {
+ util::move_cursor(i, xcoord);
+ std::cout << line_clear;
+ }
+ std::cout << std::flush;
+ restore_cursor_position();
+}
+
+auto util::generate_random_string(size_t length) -> std::string
+{
+ constexpr auto chars =
+ std::to_array({'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
+ 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'});
+ auto rng_dev = std::random_device();
+ auto rng = std::mt19937(rng_dev());
+ auto dist = std::uniform_int_distribution{{}, chars.size() - 1};
+ std::string result(length, 0);
+ std::generate_n(std::begin(result), length, [&chars, &dist, &rng] { return chars[dist(rng)]; });
+ return result;
+}
+
+auto util::read_exif_rotation(const fs::path &path) -> std::optional<std::uint16_t>
+{
+ try {
+ auto image = vips::VImage::new_from_file(path.c_str());
+ return image.get_int("orientation");
+ } catch (const vips::VError &) {
+ return {};
+ }
+}
+
+auto util::round_up(int num_to_round, int multiple) -> int
+{
+ if (multiple == 0) {
+ return num_to_round;
+ }
+
+ int remainder = num_to_round % multiple;
+ if (remainder == 0) {
+ return num_to_round;
+ }
+
+ return num_to_round + multiple - remainder;
+}
+
+auto util::temp_directory_path() -> fs::path
+{
+ return os::getenv("UEBERZUGPP_TMPDIR").value_or(fs::temp_directory_path());
+}
diff --git a/src/util/x11.cpp b/src/util/x11.cpp
new file mode 100644
index 0000000..5250997
--- /dev/null
+++ b/src/util/x11.cpp
@@ -0,0 +1,171 @@
+// Display images inside a terminal
+// Copyright (C) 2023 JustKidding
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+#include "util/x11.hpp"
+#include "flags.hpp"
+#include "os.hpp"
+#include "util.hpp"
+#include "util/ptr.hpp"
+
+#include <range/v3/all.hpp>
+#include <xcb/res.h>
+#include <xcb/xcb.h>
+
+#include <algorithm>
+#include <stack>
+#include <string>
+#include <utility>
+
+X11Util::X11Util()
+ : connection(xcb_connect(nullptr, nullptr))
+{
+ const auto flags = Flags::instance();
+ const auto xdg_session = os::getenv("XDG_SESSION_TYPE").value_or("");
+ if (xcb_connection_has_error(connection) == 0) {
+ screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data;
+ if (xdg_session != "wayland" || flags->output == "x11") {
+ connected = true;
+ }
+ }
+}
+
+X11Util::X11Util(xcb_connection_t *connection)
+ : connection(connection),
+ screen(xcb_setup_roots_iterator(xcb_get_setup(connection)).data),
+ owns_connection(false)
+{
+}
+
+X11Util::~X11Util()
+{
+ if (owns_connection) {
+ xcb_disconnect(connection);
+ }
+}
+
+auto X11Util::get_server_window_ids() const -> std::vector<xcb_window_t>
+{
+ const int num_clients = 256;
+ std::vector<xcb_window_t> windows;
+ std::stack<xcb_query_tree_cookie_t> cookies_st;
+ windows.reserve(num_clients);
+
+ cookies_st.push(xcb_query_tree_unchecked(connection, screen->root));
+
+ while (!cookies_st.empty()) {
+ const auto cookie = cookies_st.top();
+ cookies_st.pop();
+
+ const auto reply = unique_C_ptr<xcb_query_tree_reply_t>{xcb_query_tree_reply(connection, cookie, nullptr)};
+ if (!reply) {
+ continue;
+ }
+
+ const auto num_children = xcb_query_tree_children_length(reply.get());
+ if (num_children == 0) {
+ continue;
+ }
+ const auto *children = xcb_query_tree_children(reply.get());
+ for (int i = 0; i < num_children; ++i) {
+ const auto child = children[i];
+ const bool is_complete_window = window_has_properties(child, {XCB_ATOM_WM_CLASS, XCB_ATOM_WM_NAME});
+ if (is_complete_window) {
+ windows.push_back(child);
+ }
+ cookies_st.push(xcb_query_tree_unchecked(connection, child));
+ }
+ }
+ return windows;
+}
+
+auto X11Util::get_pid_window_map() const -> std::unordered_map<uint32_t, xcb_window_t>
+{
+ const auto windows = get_server_window_ids();
+ std::unordered_map<uint32_t, xcb_window_t> res;
+ std::vector<xcb_res_query_client_ids_cookie_t> cookies;
+ res.reserve(windows.size());
+ cookies.reserve(windows.size());
+
+ struct xcb_res_client_id_spec_t spec;
+ spec.mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID;
+
+ // bulk request pids
+ for (const auto window : windows) {
+ spec.client = window;
+ cookies.push_back(xcb_res_query_client_ids_unchecked(connection, 1, &spec));
+ }
+
+ // process replies
+ auto win_iter = windows.cbegin();
+ for (const auto cookie : cookies) {
+ const auto reply =
+ unique_C_ptr<xcb_res_query_client_ids_reply_t>{xcb_res_query_client_ids_reply(connection, cookie, nullptr)};
+ if (!reply) {
+ continue;
+ }
+ const auto iter = xcb_res_query_client_ids_ids_iterator(reply.get());
+ const auto pid = *xcb_res_client_id_value_value(iter.data);
+ res.insert_or_assign(pid, *win_iter);
+ std::advance(win_iter, 1);
+ }
+ return res;
+}
+
+auto X11Util::window_has_properties(xcb_window_t window, std::initializer_list<xcb_atom_t> properties) const -> bool
+{
+ std::vector<xcb_get_property_cookie_t> cookies;
+ cookies.reserve(properties.size());
+ for (const auto prop : properties) {
+ cookies.push_back(xcb_get_property_unchecked(connection, 0, window, prop, XCB_ATOM_ANY, 0, 4));
+ }
+ return ranges::any_of(cookies, [this](xcb_get_property_cookie_t cookie) -> bool {
+ const auto reply = unique_C_ptr<xcb_get_property_reply_t>{xcb_get_property_reply(connection, cookie, nullptr)};
+ return reply && xcb_get_property_value_length(reply.get()) != 0;
+ });
+}
+
+auto X11Util::get_window_dimensions(xcb_window_t window) const -> std::pair<uint16_t, uint16_t>
+{
+ const auto cookie = xcb_get_geometry_unchecked(connection, window);
+ const auto reply = unique_C_ptr<xcb_get_geometry_reply_t>{xcb_get_geometry_reply(connection, cookie, nullptr)};
+ if (!reply) {
+ return std::make_pair(0, 0);
+ }
+ return std::make_pair(reply->width, reply->height);
+}
+
+auto X11Util::get_parent_window(int pid) const -> xcb_window_t
+{
+ const auto wid = os::getenv("WINDOWID");
+ if (wid.has_value()) {
+ try {
+ return std::stoi(wid.value());
+ } catch (const std::out_of_range &oor) {
+ return 0;
+ }
+ }
+
+ const auto pid_window_map = get_pid_window_map();
+ const auto tree = util::get_process_tree(pid);
+ for (const auto spid : tree) {
+ const auto win = pid_window_map.find(spid);
+ if (win != pid_window_map.end()) {
+ return win->second;
+ }
+ }
+
+ return 0;
+}