summaryrefslogtreecommitdiff
path: root/src
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
Diffstat (limited to 'src')
-rw-r--r--src/application.cpp281
-rw-r--r--src/canvas.cpp64
-rw-r--r--src/canvas/chafa.cpp104
-rw-r--r--src/canvas/chafa.hpp52
-rw-r--r--src/canvas/iterm2/chunk.cpp56
-rw-r--r--src/canvas/iterm2/chunk.hpp45
-rw-r--r--src/canvas/iterm2/iterm2.cpp108
-rw-r--r--src/canvas/iterm2/iterm2.hpp51
-rw-r--r--src/canvas/kitty/chunk.cpp52
-rw-r--r--src/canvas/kitty/chunk.hpp42
-rw-r--r--src/canvas/kitty/kitty.cpp106
-rw-r--r--src/canvas/kitty/kitty.hpp48
-rw-r--r--src/canvas/sixel.cpp96
-rw-r--r--src/canvas/sixel.hpp57
-rw-r--r--src/canvas/stdout.hpp64
-rw-r--r--src/canvas/wayland/config.cpp43
-rw-r--r--src/canvas/wayland/config.hpp45
-rw-r--r--src/canvas/wayland/config/dummy.cpp29
-rw-r--r--src/canvas/wayland/config/dummy.hpp35
-rw-r--r--src/canvas/wayland/config/hyprland.cpp151
-rw-r--r--src/canvas/wayland/config/hyprland.hpp53
-rw-r--r--src/canvas/wayland/config/sway.cpp183
-rw-r--r--src/canvas/wayland/config/sway.hpp62
-rw-r--r--src/canvas/wayland/config/wayfire.cpp73
-rw-r--r--src/canvas/wayland/config/wayfire.hpp44
-rw-r--r--src/canvas/wayland/wayland.cpp222
-rw-r--r--src/canvas/wayland/wayland.hpp84
-rw-r--r--src/canvas/wayland/window/shm.cpp75
-rw-r--r--src/canvas/wayland/window/shm.hpp47
-rw-r--r--src/canvas/wayland/window/waylandegl.cpp235
-rw-r--r--src/canvas/wayland/window/waylandegl.hpp88
-rw-r--r--src/canvas/wayland/window/waylandshm.cpp186
-rw-r--r--src/canvas/wayland/window/waylandshm.hpp77
-rw-r--r--src/canvas/wayland/window/waylandwindow.hpp46
-rw-r--r--src/canvas/x11/window/x11.cpp119
-rw-r--r--src/canvas/x11/window/x11.hpp60
-rw-r--r--src/canvas/x11/window/x11egl.cpp138
-rw-r--r--src/canvas/x11/window/x11egl.hpp68
-rw-r--r--src/canvas/x11/x11.cpp257
-rw-r--r--src/canvas/x11/x11.hpp93
-rw-r--r--src/dimensions.cpp62
-rw-r--r--src/flags.cpp52
-rw-r--r--src/image.cpp185
-rw-r--r--src/image/libvips.cpp216
-rw-r--r--src/image/libvips.hpp71
-rw-r--r--src/image/opencv.cpp249
-rw-r--r--src/image/opencv.hpp68
-rw-r--r--src/main.cpp151
-rw-r--r--src/os.cpp126
-rw-r--r--src/process/apple.cpp43
-rw-r--r--src/process/linux.cpp39
-rw-r--r--src/terminal.cpp335
-rw-r--r--src/tmux.cpp142
-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
58 files changed, 6323 insertions, 0 deletions
diff --git a/src/application.cpp b/src/application.cpp
new file mode 100644
index 0000000..2aa06f5
--- /dev/null
+++ b/src/application.cpp
@@ -0,0 +1,281 @@
+// 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 "application.hpp"
+#include "image.hpp"
+#include "tmux.hpp"
+#include "util.hpp"
+#include "util/socket.hpp"
+#include "version.hpp"
+
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <unordered_map>
+
+#include <fmt/format.h>
+#include <nlohmann/json.hpp>
+#include <spdlog/sinks/basic_file_sink.h>
+#include <vips/vips8>
+
+using njson = nlohmann::json;
+namespace fs = std::filesystem;
+
+Application::Application(const char *executable)
+{
+ flags = Flags::instance();
+ print_header();
+ setup_logger();
+ set_silent();
+ terminal = std::make_unique<Terminal>();
+ if (flags->no_stdin) {
+ daemonize();
+ }
+ canvas = Canvas::create();
+ const auto cache_path = util::get_cache_path();
+ if (!fs::exists(cache_path) && !flags->no_cache) {
+ fs::create_directories(cache_path);
+ }
+ tmux::register_hooks();
+ socket_thread = std::thread([this] {
+ const auto sock_path = util::get_socket_path();
+ logger->info("Listening for commands on socket {}", sock_path);
+ socket_loop();
+ });
+ if (flags->no_cache) {
+ logger->info("Image caching is disabled");
+ }
+ if (VIPS_INIT(executable)) {
+ vips_error_exit(nullptr);
+ }
+ vips_cache_set_max(1);
+}
+
+Application::~Application()
+{
+ if (socket_thread.joinable()) {
+ socket_thread.join();
+ }
+ logger->info("Exiting ueberzugpp");
+ canvas.reset();
+ vips_shutdown();
+ tmux::unregister_hooks();
+ fs::remove(util::get_socket_path());
+}
+
+void Application::execute(const std::string_view cmd)
+{
+ if (!canvas) {
+ return;
+ }
+ njson json;
+ try {
+ json = njson::parse(cmd);
+ } catch (const njson::parse_error &e) {
+ logger->error("Command received is not valid json");
+ return;
+ }
+ const auto json_str = json.dump();
+ logger->info("Command received: {}", json_str);
+
+ const std::string &action = json.at("action");
+ if (action == "tmux") {
+ const std::string &hook = json.at("hook");
+ handle_tmux_hook(hook);
+ return;
+ }
+
+ const std::string &identifier = json.at("identifier");
+ if (action == "add") {
+ if (!json.at("path").is_string()) {
+ logger->error("Path received is not valid");
+ return;
+ }
+ auto image = Image::load(json, terminal.get());
+ if (!image) {
+ logger->error("Unable to load image file");
+ return;
+ }
+ canvas->add_image(identifier, std::move(image));
+ } else if (action == "remove") {
+ canvas->remove_image(identifier);
+ } else {
+ logger->warn("Command not supported");
+ }
+}
+
+void Application::handle_tmux_hook(const std::string_view hook)
+{
+ const std::unordered_map<std::string_view, std::function<void()>> hook_fns{
+ {"client-session-changed",
+ [this] {
+ if (tmux::is_window_focused()) {
+ canvas->show();
+ } else {
+ canvas->hide();
+ }
+ }},
+ {"session-window-changed",
+ [this] {
+ if (tmux::is_window_focused()) {
+ canvas->show();
+ } else {
+ canvas->hide();
+ }
+ }},
+ {"client-detached", [this] { canvas->hide(); }},
+ {"window-layout-changed",
+ [this] {
+ if (tmux::is_window_focused()) {
+ canvas->hide();
+ }
+ }},
+ };
+
+ try {
+ hook_fns.at(hook)();
+ } catch (const std::out_of_range &oor) {
+ logger->warn("TMUX hook not recognized");
+ }
+}
+
+void Application::setup_logger()
+{
+ const auto log_tmp = util::get_log_filename();
+ const auto log_path = util::temp_directory_path() / log_tmp;
+ try {
+ spdlog::flush_on(spdlog::level::debug);
+ const auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(log_path);
+
+ const auto main_logger = std::make_shared<spdlog::logger>("main", sink);
+ const auto terminal_logger = std::make_shared<spdlog::logger>("terminal", sink);
+ const auto cv_logger = std::make_shared<spdlog::logger>("opencv", sink);
+ const auto vips_logger = std::make_shared<spdlog::logger>("vips", sink);
+ const auto x11_logger = std::make_shared<spdlog::logger>("X11", sink);
+ const auto sixel_logger = std::make_shared<spdlog::logger>("sixel", sink);
+ const auto kitty_logger = std::make_shared<spdlog::logger>("kitty", sink);
+ const auto iterm2_logger = std::make_shared<spdlog::logger>("iterm2", sink);
+ const auto chafa_logger = std::make_shared<spdlog::logger>("chafa", sink);
+ const auto wayland_logger = std::make_shared<spdlog::logger>("wayland", sink);
+ const auto opengl_logger = std::make_shared<spdlog::logger>("opengl", sink);
+
+ spdlog::initialize_logger(main_logger);
+ spdlog::initialize_logger(terminal_logger);
+ spdlog::initialize_logger(cv_logger);
+ spdlog::initialize_logger(vips_logger);
+ spdlog::initialize_logger(x11_logger);
+ spdlog::initialize_logger(sixel_logger);
+ spdlog::initialize_logger(kitty_logger);
+ spdlog::initialize_logger(iterm2_logger);
+ spdlog::initialize_logger(chafa_logger);
+ spdlog::initialize_logger(wayland_logger);
+ spdlog::initialize_logger(opengl_logger);
+
+ logger = spdlog::get("main");
+ } catch (const spdlog::spdlog_ex &ex) {
+ std::cout << "Log init failed: " << ex.what() << '\n';
+ }
+}
+
+void Application::command_loop()
+{
+ if (flags->no_stdin) {
+ return;
+ }
+ while (!stop_flag) {
+ try {
+ const auto in_event = os::wait_for_data_on_stdin(100);
+ if (!in_event) {
+ continue;
+ }
+ const auto cmd = os::read_data_from_stdin();
+ execute(cmd);
+ } catch (const std::system_error &err) {
+ stop_flag = true;
+ break;
+ }
+ }
+}
+
+void Application::socket_loop()
+{
+ UnixSocket socket;
+ socket.bind_to_endpoint(util::get_socket_path());
+
+ const int waitms = 100;
+ int conn = -1;
+ while (!stop_flag) {
+ try {
+ conn = socket.wait_for_connections(waitms);
+ } catch (const std::system_error &err) {
+ stop_flag = true;
+ break;
+ }
+
+ if (conn == -1) {
+ continue;
+ }
+
+ const auto data = socket.read_data_from_connection(conn);
+ for (const auto &cmd : data) {
+ if (cmd == "EXIT") {
+ stop_flag = true;
+ return;
+ }
+ execute(cmd);
+ }
+ }
+}
+
+void Application::print_header()
+{
+ const auto log_tmp = util::get_log_filename();
+ const auto log_path = util::temp_directory_path() / log_tmp;
+ const auto art = fmt::format(R"(
+ _ _ _
+| | | | | | _ _
+| | | | ___| |__ ___ _ __ _____ _ __ _ _| |_ _| |_
+| | | |/ _ \ '_ \ / _ \ '__|_ / | | |/ _` |_ _|_ _|
+| |_| | __/ |_) | __/ | / /| |_| | (_| | |_| |_|
+ \___/ \___|_.__/ \___|_| /___|\__,_|\__, |
+ __/ |
+ |___/ v{}.{}.{})",
+ ueberzugpp_VERSION_MAJOR, ueberzugpp_VERSION_MINOR, ueberzugpp_VERSION_PATCH);
+ std::ofstream ofs(log_path, std::ios::out | std::ios::app);
+ ofs << art << '\n' << std::flush;
+}
+
+void Application::set_silent()
+{
+ if (!flags->silent) {
+ return;
+ }
+ f_stderr.reset(std::freopen("/dev/null", "w", stderr));
+}
+
+void Application::print_version()
+{
+ const auto ver_str = fmt::format("ueberzugpp {}.{}.{}", ueberzugpp_VERSION_MAJOR, ueberzugpp_VERSION_MINOR,
+ ueberzugpp_VERSION_PATCH);
+ std::cout << ver_str << '\n';
+}
+
+void Application::daemonize()
+{
+ os::daemonize();
+ std::ofstream ofs(flags->pid_file);
+ ofs << os::get_pid() << std::flush;
+}
diff --git a/src/canvas.cpp b/src/canvas.cpp
new file mode 100644
index 0000000..08fd0a3
--- /dev/null
+++ b/src/canvas.cpp
@@ -0,0 +1,64 @@
+// 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 "canvas/chafa.hpp"
+#include "canvas/iterm2/iterm2.hpp"
+#include "canvas/kitty/kitty.hpp"
+#include "canvas/sixel.hpp"
+#include "canvas/stdout.hpp"
+#ifdef ENABLE_X11
+# include "canvas/x11/x11.hpp"
+#endif
+#ifdef ENABLE_WAYLAND
+# include "canvas/wayland/wayland.hpp"
+#endif
+#include "flags.hpp"
+#include <fmt/format.h>
+
+auto Canvas::create() -> std::unique_ptr<Canvas>
+{
+ const auto flags = Flags::instance();
+ const auto logger = spdlog::get("main");
+
+#ifdef ENABLE_WAYLAND
+ if (flags->output == "wayland") {
+ return std::make_unique<WaylandCanvas>();
+ }
+#else
+ logger->debug("Wayland support not compiled in the binary");
+#endif
+#ifdef ENABLE_X11
+ if (flags->output == "x11") {
+ return std::make_unique<X11Canvas>();
+ }
+#else
+ logger->debug("X11 support not compiled in the binary");
+#endif
+
+ if (flags->output == "kitty") {
+ return std::make_unique<StdoutCanvas<Kitty>>(flags->output);
+ }
+ if (flags->output == "iterm2") {
+ return std::make_unique<StdoutCanvas<Iterm2>>(flags->output);
+ }
+ if (flags->output == "sixel") {
+ return std::make_unique<StdoutCanvas<Sixel>>(flags->output);
+ }
+ if (flags->output == "chafa") {
+ return std::make_unique<StdoutCanvas<Chafa>>(flags->output);
+ }
+ throw std::runtime_error(fmt::format("output backend not supported (backend is {})", flags->output));
+}
diff --git a/src/canvas/chafa.cpp b/src/canvas/chafa.cpp
new file mode 100644
index 0000000..dc3ece5
--- /dev/null
+++ b/src/canvas/chafa.cpp
@@ -0,0 +1,104 @@
+// 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 "chafa.hpp"
+#include "dimensions.hpp"
+#include "terminal.hpp"
+#include "util.hpp"
+#include "util/ptr.hpp"
+
+#include <algorithm>
+#include <cmath>
+#include <iostream>
+
+#include <range/v3/all.hpp>
+#include <spdlog/spdlog.h>
+
+void gstring_delete(GString *str)
+{
+ g_string_free(str, true);
+}
+
+Chafa::Chafa(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
+ : symbol_map(chafa_symbol_map_new()),
+ config(chafa_canvas_config_new()),
+ image(std::move(new_image)),
+ stdout_mutex(stdout_mutex)
+{
+ const auto envp = c_unique_ptr<gchar *, g_strfreev>{g_get_environ()};
+ term_info = chafa_term_db_detect(chafa_term_db_get_default(), envp.get());
+
+ const auto dims = image->dimensions();
+ x = dims.x + 1;
+ y = dims.y + 1;
+ horizontal_cells = std::ceil(static_cast<double>(image->width()) / dims.terminal->font_width);
+ vertical_cells = std::ceil(static_cast<double>(image->height()) / dims.terminal->font_height);
+
+ chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_BLOCK);
+ chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_BORDER);
+ chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_SPACE);
+ chafa_symbol_map_remove_by_tags(symbol_map, CHAFA_SYMBOL_TAG_WIDE);
+ chafa_canvas_config_set_symbol_map(config, symbol_map);
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_SYMBOLS);
+ chafa_canvas_config_set_geometry(config, horizontal_cells, vertical_cells);
+}
+
+Chafa::~Chafa()
+{
+ chafa_canvas_unref(canvas);
+ chafa_canvas_config_unref(config);
+ chafa_symbol_map_unref(symbol_map);
+ chafa_term_info_unref(term_info);
+
+ const std::scoped_lock lock{*stdout_mutex};
+ util::clear_terminal_area(x, y, horizontal_cells, vertical_cells);
+}
+
+void Chafa::draw()
+{
+ canvas = chafa_canvas_new(config);
+ chafa_canvas_draw_all_pixels(canvas, CHAFA_PIXEL_BGRA8_UNASSOCIATED, image->data(), image->width(), image->height(),
+ image->width() * 4);
+
+#ifdef CHAFA_VERSION_1_14
+ GString **lines = nullptr;
+ gint lines_length = 0;
+
+ chafa_canvas_print_rows(canvas, term_info, &lines, &lines_length);
+ auto ycoord = y;
+ const std::scoped_lock lock{*stdout_mutex};
+ util::save_cursor_position();
+ for (int i = 0; i < lines_length; ++i) {
+ const auto line = c_unique_ptr<GString, gstring_delete>{lines[i]};
+ util::move_cursor(ycoord++, x);
+ std::cout << line->str;
+ }
+ g_free(lines);
+#else
+ const auto result = c_unique_ptr<GString, gstring_delete>{chafa_canvas_print(canvas, term_info)};
+ auto ycoord = y;
+ const auto lines = util::str_split(result->str, "\n");
+
+ const std::scoped_lock lock{*stdout_mutex};
+ util::save_cursor_position();
+ ranges::for_each(lines, [this, &ycoord](const std::string &line) {
+ util::move_cursor(ycoord++, x);
+ std::cout << line;
+ });
+#endif
+ std::cout << std::flush;
+ util::restore_cursor_position();
+}
diff --git a/src/canvas/chafa.hpp b/src/canvas/chafa.hpp
new file mode 100644
index 0000000..608aa13
--- /dev/null
+++ b/src/canvas/chafa.hpp
@@ -0,0 +1,52 @@
+// 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/>.
+
+#ifndef CHAFA_WINDOW_H
+#define CHAFA_WINDOW_H
+
+#include "image.hpp"
+#include "window.hpp"
+
+#include <memory>
+#include <mutex>
+
+#include <chafa.h>
+
+class Chafa : public Window
+{
+ public:
+ Chafa(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
+ ~Chafa() override;
+
+ void draw() override;
+ void generate_frame() override{};
+
+ private:
+ ChafaTermInfo *term_info = nullptr;
+ ChafaSymbolMap *symbol_map = nullptr;
+ ChafaCanvasConfig *config = nullptr;
+ ChafaCanvas *canvas = nullptr;
+
+ std::unique_ptr<Image> image;
+ std::mutex *stdout_mutex;
+
+ int x;
+ int y;
+ int horizontal_cells = 0;
+ int vertical_cells = 0;
+};
+
+#endif
diff --git a/src/canvas/iterm2/chunk.cpp b/src/canvas/iterm2/chunk.cpp
new file mode 100644
index 0000000..950606f
--- /dev/null
+++ b/src/canvas/iterm2/chunk.cpp
@@ -0,0 +1,56 @@
+// 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 "chunk.hpp"
+#include "util.hpp"
+
+Iterm2Chunk::Iterm2Chunk(uint64_t size)
+{
+ auto bufsize = 4*((size+2)/3);
+ buffer.resize(size, 0);
+ result.resize(bufsize + 1, 0);
+}
+
+void Iterm2Chunk::set_size(uint64_t size)
+{
+ this->size = size;
+}
+
+auto Iterm2Chunk::get_size() const -> uint64_t
+{
+ return size;
+}
+
+auto Iterm2Chunk::get_buffer() -> char*
+{
+ return buffer.data();
+}
+
+auto Iterm2Chunk::get_result() -> char*
+{
+ return result.data();
+}
+
+void Iterm2Chunk::process_chunk(std::unique_ptr<Iterm2Chunk>& chunk)
+{
+ util::base64_encode_v2(reinterpret_cast<unsigned char*>(chunk->get_buffer()),
+ chunk->get_size(), reinterpret_cast<unsigned char*>(chunk->get_result()));
+}
+
+void Iterm2Chunk::operator()(std::unique_ptr<Iterm2Chunk>& chunk) const
+{
+ process_chunk(chunk);
+}
diff --git a/src/canvas/iterm2/chunk.hpp b/src/canvas/iterm2/chunk.hpp
new file mode 100644
index 0000000..4abcb21
--- /dev/null
+++ b/src/canvas/iterm2/chunk.hpp
@@ -0,0 +1,45 @@
+// 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/>.
+
+#ifndef ITERM2_CHUNK_H
+#define ITERM2_CHUNK_H
+
+#include <vector>
+#include <memory>
+#include <cstdint>
+
+class Iterm2Chunk
+{
+public:
+ Iterm2Chunk() = default;
+ explicit Iterm2Chunk(uint64_t size);
+
+ void operator()(std::unique_ptr<Iterm2Chunk>& chunk) const;
+ void static process_chunk(std::unique_ptr<Iterm2Chunk>& chunk);
+
+ [[nodiscard]] auto get_size() const -> uint64_t;
+ void set_size(uint64_t size);
+ auto get_buffer() -> char*;
+ auto get_result() -> char*;
+
+private:
+ uint64_t size;
+ std::vector<char> buffer;
+ std::vector<char> result;
+
+};
+
+#endif
diff --git a/src/canvas/iterm2/iterm2.cpp b/src/canvas/iterm2/iterm2.cpp
new file mode 100644
index 0000000..2818aab
--- /dev/null
+++ b/src/canvas/iterm2/iterm2.cpp
@@ -0,0 +1,108 @@
+// 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 "iterm2.hpp"
+#include "chunk.hpp"
+#include "dimensions.hpp"
+#include "image.hpp"
+#include "terminal.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+
+#include <fmt/format.h>
+#include <spdlog/spdlog.h>
+
+#ifdef HAVE_STD_EXECUTION_H
+# include <execution>
+#else
+# include <oneapi/tbb.h>
+#endif
+
+#include <range/v3/all.hpp>
+
+namespace fs = std::filesystem;
+
+Iterm2::Iterm2(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
+ : image(std::move(new_image)),
+ stdout_mutex(stdout_mutex)
+{
+ const auto dims = image->dimensions();
+ x = dims.x + 1;
+ y = dims.y + 1;
+ horizontal_cells = std::ceil(static_cast<double>(image->width()) / dims.terminal->font_width);
+ vertical_cells = std::ceil(static_cast<double>(image->height()) / dims.terminal->font_height);
+}
+
+Iterm2::~Iterm2()
+{
+ const std::scoped_lock lock{*stdout_mutex};
+ util::clear_terminal_area(x, y, horizontal_cells, vertical_cells);
+}
+
+void Iterm2::draw()
+{
+ str.append("\033]1337;File=inline=1;");
+ const auto filename = image->filename();
+ const auto num_bytes = fs::file_size(filename);
+ const auto encoded_filename =
+ util::base64_encode(reinterpret_cast<const unsigned char *>(filename.c_str()), filename.size());
+ str.append(fmt::format("size={};name={};width={}px;height={}px:", num_bytes, encoded_filename, image->width(),
+ image->height()));
+
+ const int chunk_size = 1023;
+ const auto chunks = process_chunks(filename, chunk_size, num_bytes);
+ const int num_chunks = std::ceil(static_cast<double>(num_bytes) / chunk_size);
+ const uint64_t bytes_per_chunk = 4 * ((chunk_size + 2) / 3) + 100;
+ str.reserve((num_chunks + 2) * bytes_per_chunk);
+
+ ranges::for_each(chunks, [this](const std::unique_ptr<Iterm2Chunk> &chunk) { str.append(chunk->get_result()); });
+ str.append("\a");
+
+ const std::scoped_lock lock{*stdout_mutex};
+ util::save_cursor_position();
+ util::move_cursor(y, x);
+ std::cout << str << std::flush;
+ util::restore_cursor_position();
+ str.clear();
+}
+
+auto Iterm2::process_chunks(const std::string &filename, int chunk_size, size_t num_bytes)
+ -> std::vector<std::unique_ptr<Iterm2Chunk>>
+{
+ const int num_chunks = std::ceil(static_cast<double>(num_bytes) / chunk_size);
+ std::vector<std::unique_ptr<Iterm2Chunk>> chunks;
+ chunks.reserve(num_chunks + 2);
+
+ std::ifstream ifs(filename);
+ while (ifs.good()) {
+ auto chunk = std::make_unique<Iterm2Chunk>(chunk_size);
+ ifs.read(chunk->get_buffer(), chunk_size);
+ chunk->set_size(ifs.gcount());
+ chunks.push_back(std::move(chunk));
+ }
+
+#ifdef HAVE_STD_EXECUTION_H
+ std::for_each(std::execution::par_unseq, chunks.begin(), chunks.end(), Iterm2Chunk::process_chunk);
+#else
+ oneapi::tbb::parallel_for_each(chunks.begin(), chunks.end(), Iterm2Chunk());
+#endif
+
+ return chunks;
+}
diff --git a/src/canvas/iterm2/iterm2.hpp b/src/canvas/iterm2/iterm2.hpp
new file mode 100644
index 0000000..0912809
--- /dev/null
+++ b/src/canvas/iterm2/iterm2.hpp
@@ -0,0 +1,51 @@
+// 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/>.
+
+#ifndef ITERM2_CANVAS_H
+#define ITERM2_CANVAS_H
+
+#include "chunk.hpp"
+#include "image.hpp"
+#include "window.hpp"
+
+#include <mutex>
+#include <string>
+#include <vector>
+
+class Iterm2 : public Window
+{
+ public:
+ Iterm2(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
+ ~Iterm2() override;
+
+ void draw() override;
+ void generate_frame() override{};
+
+ private:
+ std::unique_ptr<Image> image;
+ std::mutex *stdout_mutex;
+ std::string str;
+
+ int x;
+ int y;
+ int horizontal_cells = 0;
+ int vertical_cells = 0;
+
+ static auto process_chunks(const std::string &filename, int chunk_size, size_t num_bytes)
+ -> std::vector<std::unique_ptr<Iterm2Chunk>>;
+};
+
+#endif
diff --git a/src/canvas/kitty/chunk.cpp b/src/canvas/kitty/chunk.cpp
new file mode 100644
index 0000000..a54dc94
--- /dev/null
+++ b/src/canvas/kitty/chunk.cpp
@@ -0,0 +1,52 @@
+// 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 "chunk.hpp"
+#include "util.hpp"
+
+KittyChunk::KittyChunk(const unsigned char* ptr, uint64_t size):
+ptr(ptr),
+size(size)
+{
+ uint64_t bufsize = 4*((size+2)/3);
+ result.resize(bufsize + 1, 0);
+}
+
+void KittyChunk::process_chunk(KittyChunk& chunk)
+{
+ util::base64_encode_v2(chunk.get_ptr(), chunk.get_size(),
+ reinterpret_cast<unsigned char*>(chunk.get_result()));
+}
+
+void KittyChunk::operator()(KittyChunk& chunk) const
+{
+ process_chunk(chunk);
+}
+
+auto KittyChunk::get_result() -> char*
+{
+ return result.data();
+}
+
+auto KittyChunk::get_ptr() const -> const unsigned char*
+{
+ return ptr;
+}
+
+auto KittyChunk::get_size() const -> uint64_t
+{
+ return size;
+}
diff --git a/src/canvas/kitty/chunk.hpp b/src/canvas/kitty/chunk.hpp
new file mode 100644
index 0000000..ac611e8
--- /dev/null
+++ b/src/canvas/kitty/chunk.hpp
@@ -0,0 +1,42 @@
+// 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/>.
+
+#ifndef KITTY_CHUNK_H
+#define KITTY_CHUNK_H
+
+#include <vector>
+#include <cstdint>
+
+class KittyChunk
+{
+public:
+ KittyChunk() = default;
+ KittyChunk(const unsigned char* ptr, uint64_t size);
+
+ void operator()(KittyChunk& chunk) const;
+
+ auto get_result() -> char*;
+ [[nodiscard]] auto get_ptr() const -> const unsigned char*;
+ [[nodiscard]] auto get_size() const -> uint64_t;
+
+ static void process_chunk(KittyChunk& chunk);
+private:
+ const unsigned char* ptr;
+ uint64_t size;
+ std::vector<char> result;
+};
+
+#endif
diff --git a/src/canvas/kitty/kitty.cpp b/src/canvas/kitty/kitty.cpp
new file mode 100644
index 0000000..ed89948
--- /dev/null
+++ b/src/canvas/kitty/kitty.cpp
@@ -0,0 +1,106 @@
+// 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 "kitty.hpp"
+#include "dimensions.hpp"
+#include "util.hpp"
+
+#include <fmt/format.h>
+
+#include <iostream>
+
+#ifdef HAVE_STD_EXECUTION_H
+# include <execution>
+#else
+# include <oneapi/tbb.h>
+#endif
+
+Kitty::Kitty(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
+ : image(std::move(new_image)),
+ stdout_mutex(stdout_mutex),
+ id(util::generate_random_number<uint32_t>(1))
+{
+ const auto dims = image->dimensions();
+ x = dims.x + 1;
+ y = dims.y + 1;
+}
+
+Kitty::~Kitty()
+{
+ const std::scoped_lock lock{*stdout_mutex};
+ std::cout << fmt::format("\033_Ga=d,d=i,i={}\033\\", id) << std::flush;
+}
+
+void Kitty::draw()
+{
+ generate_frame();
+}
+
+void Kitty::generate_frame()
+{
+ const int bits_per_channel = 8;
+ auto chunks = process_chunks();
+ str.append(fmt::format("\033_Ga=T,m=1,i={},q=2,f={},s={},v={};{}\033\\", id, image->channels() * bits_per_channel,
+ image->width(), image->height(), chunks.front().get_result()));
+
+ for (auto chunk = std::next(std::begin(chunks)); chunk != std::prev(std::end(chunks)); std::advance(chunk, 1)) {
+ str.append("\033_Gm=1,q=2;");
+ str.append(chunk->get_result());
+ str.append("\033\\");
+ }
+
+ str.append("\033_Gm=0,q=2;");
+ str.append(chunks.back().get_result());
+ str.append("\033\\");
+
+ const std::scoped_lock lock{*stdout_mutex};
+ util::save_cursor_position();
+ util::move_cursor(y, x);
+ std::cout << str << std::flush;
+ util::restore_cursor_position();
+ str.clear();
+}
+
+auto Kitty::process_chunks() -> std::vector<KittyChunk>
+{
+ const uint64_t chunk_size = 3068;
+ uint64_t num_chunks = image->size() / chunk_size;
+ uint64_t last_chunk_size = image->size() % chunk_size;
+ if (last_chunk_size == 0) {
+ last_chunk_size = chunk_size;
+ num_chunks--;
+ }
+ const uint64_t bytes_per_chunk = 4 * ((chunk_size + 2) / 3) + 100;
+ str.reserve((num_chunks + 2) * bytes_per_chunk);
+
+ std::vector<KittyChunk> chunks;
+ chunks.reserve(num_chunks + 2);
+ const auto *ptr = image->data();
+
+ uint64_t idx = 0;
+ for (; idx < num_chunks; idx++) {
+ chunks.emplace_back(ptr + idx * chunk_size, chunk_size);
+ }
+ chunks.emplace_back(ptr + idx * chunk_size, last_chunk_size);
+
+#ifdef HAVE_STD_EXECUTION_H
+ std::for_each(std::execution::par_unseq, std::begin(chunks), std::end(chunks), KittyChunk::process_chunk);
+#else
+ oneapi::tbb::parallel_for_each(std::begin(chunks), std::end(chunks), KittyChunk());
+#endif
+
+ return chunks;
+}
diff --git a/src/canvas/kitty/kitty.hpp b/src/canvas/kitty/kitty.hpp
new file mode 100644
index 0000000..9889434
--- /dev/null
+++ b/src/canvas/kitty/kitty.hpp
@@ -0,0 +1,48 @@
+// 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/>.
+
+#ifndef KITTY_WINDOW_H
+#define KITTY_WINDOW_H
+
+#include "chunk.hpp"
+#include "image.hpp"
+#include "window.hpp"
+
+#include <memory>
+#include <mutex>
+#include <vector>
+
+class Kitty : public Window
+{
+ public:
+ Kitty(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
+ ~Kitty() override;
+
+ void draw() override;
+ void generate_frame() override;
+
+ private:
+ std::string str;
+ std::unique_ptr<Image> image;
+ std::mutex *stdout_mutex;
+ uint32_t id;
+ int x;
+ int y;
+
+ auto process_chunks() -> std::vector<KittyChunk>;
+};
+
+#endif
diff --git a/src/canvas/sixel.cpp b/src/canvas/sixel.cpp
new file mode 100644
index 0000000..17bf1d4
--- /dev/null
+++ b/src/canvas/sixel.cpp
@@ -0,0 +1,96 @@
+// 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 "sixel.hpp"
+#include "dimensions.hpp"
+#include "terminal.hpp"
+#include "util.hpp"
+
+#include <filesystem>
+#include <iostream>
+
+namespace fs = std::filesystem;
+
+Sixel::Sixel(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
+ : image(std::move(new_image)),
+ stdout_mutex(stdout_mutex)
+{
+ const auto dims = image->dimensions();
+ x = dims.x + 1;
+ y = dims.y + 1;
+ horizontal_cells = std::ceil(static_cast<double>(image->width()) / dims.terminal->font_width);
+ vertical_cells = std::ceil(static_cast<double>(image->height()) / dims.terminal->font_height);
+
+ const auto draw_callback = [](char *data, int size, void *priv) -> int {
+ auto *str = static_cast<std::string *>(priv);
+ str->append(data, size);
+ return size;
+ };
+ sixel_output_new(&output, draw_callback, &str, nullptr);
+
+ const auto file_size = fs::file_size(image->filename());
+ constexpr auto reserve_ratio = 50;
+ str.reserve(file_size * reserve_ratio);
+
+ // create dither and palette from image
+ sixel_dither_new(&dither, -1, nullptr);
+ sixel_dither_initialize(dither, const_cast<unsigned char *>(image->data()), image->width(), image->height(),
+ SIXEL_PIXELFORMAT_RGB888, SIXEL_LARGE_LUM, SIXEL_REP_CENTER_BOX, SIXEL_QUALITY_HIGH);
+}
+
+Sixel::~Sixel()
+{
+ can_draw.store(false);
+ if (draw_thread.joinable()) {
+ draw_thread.join();
+ }
+ sixel_dither_destroy(dither);
+ sixel_output_destroy(output);
+
+ const std::scoped_lock lock{*stdout_mutex};
+ util::clear_terminal_area(x, y, horizontal_cells, vertical_cells);
+}
+
+void Sixel::draw()
+{
+ if (!image->is_animated()) {
+ generate_frame();
+ return;
+ }
+
+ // start drawing loop
+ draw_thread = std::thread([this] {
+ while (can_draw.load()) {
+ generate_frame();
+ image->next_frame();
+ std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
+ }
+ });
+}
+
+void Sixel::generate_frame()
+{
+ // output sixel content to stream
+ sixel_encode(const_cast<unsigned char *>(image->data()), image->width(), image->height(), 3 /*unused*/, dither,
+ output);
+
+ const std::scoped_lock lock{*stdout_mutex};
+ util::save_cursor_position();
+ util::move_cursor(y, x);
+ std::cout << str << std::flush;
+ util::restore_cursor_position();
+ str.clear();
+}
diff --git a/src/canvas/sixel.hpp b/src/canvas/sixel.hpp
new file mode 100644
index 0000000..a675f91
--- /dev/null
+++ b/src/canvas/sixel.hpp
@@ -0,0 +1,57 @@
+// 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/>.
+
+#ifndef SIXEL_WINDOW_H
+#define SIXEL_WINDOW_H
+
+#include "image.hpp"
+#include "window.hpp"
+
+#include <atomic>
+#include <memory>
+#include <mutex>
+#include <thread>
+#include <vector>
+
+#include <sixel.h>
+
+class Sixel : public Window
+{
+ public:
+ Sixel(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
+ ~Sixel() override;
+
+ void draw() override;
+ void generate_frame() override;
+
+ private:
+ std::unique_ptr<Image> image;
+ std::mutex *stdout_mutex;
+
+ std::string str;
+ std::thread draw_thread;
+ std::atomic<bool> can_draw{true};
+
+ int x;
+ int y;
+ int horizontal_cells = 0;
+ int vertical_cells = 0;
+
+ sixel_dither_t *dither = nullptr;
+ sixel_output_t *output = nullptr;
+};
+
+#endif
diff --git a/src/canvas/stdout.hpp b/src/canvas/stdout.hpp
new file mode 100644
index 0000000..11a8ebb
--- /dev/null
+++ b/src/canvas/stdout.hpp
@@ -0,0 +1,64 @@
+// 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/>.
+
+#ifndef STDOUT_CANVAS_H
+#define STDOUT_CANVAS_H
+
+#include "canvas.hpp"
+#include "image.hpp"
+#include "window.hpp"
+
+#include <spdlog/spdlog.h>
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <unordered_map>
+
+template <WindowType T>
+class StdoutCanvas : public Canvas
+{
+ public:
+ explicit StdoutCanvas(const std::string &output)
+ {
+ logger = spdlog::get(output);
+ logger->info("Canvas created");
+ }
+
+ ~StdoutCanvas() override = default;
+
+ void add_image(const std::string &identifier, std::unique_ptr<Image> new_image) override
+ {
+ logger->info("Displaying image with id {}", identifier);
+ images.erase(identifier);
+ const auto [entry, success] =
+ images.emplace(identifier, std::make_unique<T>(std::move(new_image), &stdout_mutex));
+ entry->second->draw();
+ }
+
+ void remove_image(const std::string &identifier) override
+ {
+ logger->info("Removing image with id {}", identifier);
+ images.erase(identifier);
+ }
+
+ private:
+ std::mutex stdout_mutex;
+ std::shared_ptr<spdlog::logger> logger;
+ std::unordered_map<std::string, std::unique_ptr<T>> images;
+};
+
+#endif
diff --git a/src/canvas/wayland/config.cpp b/src/canvas/wayland/config.cpp
new file mode 100644
index 0000000..4cd19e9
--- /dev/null
+++ b/src/canvas/wayland/config.cpp
@@ -0,0 +1,43 @@
+// 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 "config.hpp"
+#include "os.hpp"
+#include "config/sway.hpp"
+#include "config/hyprland.hpp"
+#include "config/wayfire.hpp"
+#include "config/dummy.hpp"
+
+auto WaylandConfig::get() -> std::unique_ptr<WaylandConfig>
+{
+ const auto sway_sock = os::getenv("SWAYSOCK");
+ if (sway_sock.has_value()) {
+ return std::make_unique<SwaySocket>(sway_sock.value());
+ }
+
+ const auto hypr_sig = os::getenv("HYPRLAND_INSTANCE_SIGNATURE");
+ if (hypr_sig.has_value()) {
+ return std::make_unique<HyprlandSocket>(hypr_sig.value());
+ }
+
+ const auto wayfire_sock = os::getenv("WAYFIRE_SOCKET");
+ if (wayfire_sock.has_value()) {
+ return std::make_unique<WayfireSocket>(wayfire_sock.value());
+ }
+
+ return std::make_unique<DummyWaylandConfig>();
+}
+
diff --git a/src/canvas/wayland/config.hpp b/src/canvas/wayland/config.hpp
new file mode 100644
index 0000000..c84e669
--- /dev/null
+++ b/src/canvas/wayland/config.hpp
@@ -0,0 +1,45 @@
+// 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/>.
+
+#ifndef WAYLAND_CONFIG_H
+#define WAYLAND_CONFIG_H
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+struct WaylandWindowGeometry {
+ int width;
+ int height;
+ int x;
+ int y;
+};
+
+class WaylandConfig
+{
+ public:
+ static auto get() -> std::unique_ptr<WaylandConfig>;
+
+ virtual ~WaylandConfig() = default;
+
+ virtual auto get_focused_output_name() -> std::string = 0;
+ virtual auto get_window_info() -> struct WaylandWindowGeometry = 0;
+ virtual auto is_dummy() -> bool { return false; }
+ virtual void initial_setup(std::string_view appid) = 0;
+ virtual void move_window(std::string_view appid, int xcoord, int ycoord) = 0;
+};
+
+#endif
diff --git a/src/canvas/wayland/config/dummy.cpp b/src/canvas/wayland/config/dummy.cpp
new file mode 100644
index 0000000..d4b4297
--- /dev/null
+++ b/src/canvas/wayland/config/dummy.cpp
@@ -0,0 +1,29 @@
+// 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 "dummy.hpp"
+
+auto DummyWaylandConfig::get_window_info() -> struct WaylandWindowGeometry
+{
+ return {};
+}
+
+void DummyWaylandConfig::initial_setup([[maybe_unused]] std::string_view appid)
+{}
+
+void DummyWaylandConfig::move_window([[maybe_unused]] std::string_view appid,
+ [[maybe_unused]] int xcoord, [[maybe_unused]] int ycoord)
+{}
diff --git a/src/canvas/wayland/config/dummy.hpp b/src/canvas/wayland/config/dummy.hpp
new file mode 100644
index 0000000..60b35c1
--- /dev/null
+++ b/src/canvas/wayland/config/dummy.hpp
@@ -0,0 +1,35 @@
+// 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/>.
+
+#ifndef DUMMY_WAYLAND_CONFIG_H
+#define DUMMY_WAYLAND_CONFIG_H
+
+#include "../config.hpp"
+
+class DummyWaylandConfig : public WaylandConfig
+{
+ public:
+ DummyWaylandConfig() = default;
+ ~DummyWaylandConfig() override = default;
+
+ auto get_focused_output_name() -> std::string override { return {}; };
+ auto get_window_info() -> struct WaylandWindowGeometry override;
+ auto is_dummy() -> bool override { return true; }
+ void initial_setup(std::string_view appid) override;
+ void move_window(std::string_view appid, int xcoord, int ycoord) override;
+};
+
+#endif
diff --git a/src/canvas/wayland/config/hyprland.cpp b/src/canvas/wayland/config/hyprland.cpp
new file mode 100644
index 0000000..42387fb
--- /dev/null
+++ b/src/canvas/wayland/config/hyprland.cpp
@@ -0,0 +1,151 @@
+// 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 "hyprland.hpp"
+#include "os.hpp"
+#include "tmux.hpp"
+#include "util/socket.hpp"
+
+#include <filesystem>
+#include <fmt/format.h>
+#include <nlohmann/json.hpp>
+#include <range/v3/all.hpp>
+
+using njson = nlohmann::json;
+namespace fs = std::filesystem;
+
+HyprlandSocket::HyprlandSocket(const std::string_view signature)
+ : logger(spdlog::get("wayland"))
+{
+ const auto socket_base_dir = os::getenv("XDG_RUNTIME_DIR").value_or("/tmp");
+ const auto socket_rel_path = fmt::format("hypr/{}/.socket.sock", signature);
+ socket_path = fmt::format("{}/{}", socket_base_dir, socket_rel_path);
+ // XDG_RUNTIME_DIR set but hyprland < 0.40
+ if (!fs::exists(socket_path)) {
+ socket_path = fmt::format("/tmp/{}", socket_rel_path);
+ }
+
+ logger->info("Using hyprland socket {}", socket_path);
+ const auto active = request_result("j/activewindow");
+ address = active.at("address");
+ set_active_monitor();
+}
+
+void HyprlandSocket::set_active_monitor()
+{
+ const auto monitors = request_result("j/monitors");
+ for (const auto &monitor : monitors) {
+ bool focused = monitor.at("focused");
+ if (focused) {
+ output_name = monitor.at("name");
+ output_scale = monitor.at("scale");
+ break;
+ }
+ }
+}
+
+auto HyprlandSocket::get_focused_output_name() -> std::string
+{
+ return output_name;
+}
+
+auto HyprlandSocket::request_result(const std::string_view payload) -> nlohmann::json
+{
+ const UnixSocket socket{socket_path};
+ socket.write(payload.data(), payload.size());
+ const std::string result = socket.read_until_empty();
+ return njson::parse(result);
+}
+
+void HyprlandSocket::request(const std::string_view payload)
+{
+ const UnixSocket socket{socket_path};
+ logger->debug("Running socket command {}", payload);
+ socket.write(payload.data(), payload.length());
+}
+
+auto HyprlandSocket::get_active_window() -> nlohmann::json
+{
+ // recalculate address in case it changed
+ if (tmux::is_used()) {
+ const auto active = request_result("j/activewindow");
+ address = active.at("address");
+ }
+ const auto clients = request_result("j/clients");
+ const auto client = ranges::find_if(clients, [this](const njson &json) { return json.at("address") == address; });
+ if (client == clients.end()) {
+ throw std::runtime_error("Active window not found");
+ }
+ return *client;
+}
+
+auto HyprlandSocket::get_window_info() -> struct WaylandWindowGeometry {
+ const auto terminal = get_active_window();
+ const auto &sizes = terminal.at("size");
+ const auto &coords = terminal.at("at");
+
+ return {
+ .width = sizes.at(0),
+ .height = sizes.at(1),
+ .x = coords.at(0),
+ .y = coords.at(1),
+ };
+}
+
+void HyprlandSocket::initial_setup(const std::string_view appid)
+{
+ disable_focus(appid);
+ enable_floating(appid);
+ remove_borders(appid);
+ remove_rounding(appid);
+}
+
+void HyprlandSocket::remove_rounding(const std::string_view appid)
+{
+ const auto payload = fmt::format("/keyword windowrulev2 rounding 0,title:{}", appid);
+ request(payload);
+}
+
+void HyprlandSocket::disable_focus(const std::string_view appid)
+{
+ const auto payload = fmt::format("/keyword windowrulev2 nofocus,title:{}", appid);
+ request(payload);
+}
+
+void HyprlandSocket::enable_floating(const std::string_view appid)
+{
+ const auto payload = fmt::format("/keyword windowrulev2 float,title:{}", appid);
+ request(payload);
+}
+
+void HyprlandSocket::remove_borders(const std::string_view appid)
+{
+ const auto payload = fmt::format("/keyword windowrulev2 noborder,title:{}", appid);
+ request(payload);
+}
+
+void HyprlandSocket::move_window(const std::string_view appid, int xcoord, int ycoord)
+{
+ int res_x = xcoord;
+ int res_y = ycoord;
+ if (output_scale > 1.0F) {
+ const int offset = 10;
+ res_x = res_x / 2 + offset;
+ res_y = res_y / 2 + offset;
+ }
+ const auto payload = fmt::format("/dispatch movewindowpixel exact {} {},title:{}", res_x, res_y, appid);
+ request(payload);
+}
diff --git a/src/canvas/wayland/config/hyprland.hpp b/src/canvas/wayland/config/hyprland.hpp
new file mode 100644
index 0000000..4353d98
--- /dev/null
+++ b/src/canvas/wayland/config/hyprland.hpp
@@ -0,0 +1,53 @@
+// 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/>.
+
+#ifndef HYPRLAND_SOCKET_H
+#define HYPRLAND_SOCKET_H
+
+#include "../config.hpp"
+
+#include <nlohmann/json.hpp>
+#include <spdlog/spdlog.h>
+
+class HyprlandSocket : public WaylandConfig
+{
+ public:
+ explicit HyprlandSocket(std::string_view signature);
+ ~HyprlandSocket() override = default;
+
+ auto get_window_info() -> struct WaylandWindowGeometry override;
+ auto get_focused_output_name() -> std::string override;
+ void initial_setup(std::string_view appid) override;
+ void move_window(std::string_view appid, int xcoord, int ycoord) override;
+
+ private:
+ void disable_focus(std::string_view appid);
+ void enable_floating(std::string_view appid);
+ void remove_borders(std::string_view appid);
+ void remove_rounding(std::string_view appid);
+ void request(std::string_view payload);
+ auto request_result(std::string_view payload) -> nlohmann::json;
+ auto get_active_window() -> nlohmann::json;
+ void set_active_monitor();
+
+ std::shared_ptr<spdlog::logger> logger;
+ std::string socket_path;
+ std::string address;
+ std::string output_name;
+ float output_scale = 1.0F;
+};
+
+#endif
diff --git a/src/canvas/wayland/config/sway.cpp b/src/canvas/wayland/config/sway.cpp
new file mode 100644
index 0000000..85bff0c
--- /dev/null
+++ b/src/canvas/wayland/config/sway.cpp
@@ -0,0 +1,183 @@
+// 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 "sway.hpp"
+#include "application.hpp"
+#include "os.hpp"
+#include "tmux.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <array>
+#include <stack>
+#include <string>
+
+#include <fmt/format.h>
+#include <range/v3/all.hpp>
+
+using njson = nlohmann::json;
+
+constexpr auto ipc_magic = std::string_view{"i3-ipc"};
+constexpr auto ipc_header_size = ipc_magic.size() + 8;
+
+SwaySocket::SwaySocket(const std::string_view endpoint)
+ : socket(endpoint),
+ logger(spdlog::get("wayland"))
+{
+ logger->info("Using sway socket {}", endpoint);
+ set_active_output_info();
+}
+
+struct __attribute__((packed)) ipc_header {
+ std::array<char, ipc_magic.size()> magic;
+ uint32_t len;
+ uint32_t type;
+};
+
+auto SwaySocket::get_window_info() -> struct WaylandWindowGeometry {
+ const auto nodes = get_nodes();
+ const auto window = get_active_window(nodes);
+ const auto &rect = window.at("rect");
+ return {.width = rect.at("width"),
+ .height = rect.at("height"),
+ .x = rect.at("x").get<int>() - output_info.x,
+ .y = rect.at("y").get<int>() - output_info.y};
+}
+
+auto SwaySocket::get_active_window(const std::vector<nlohmann::json>& nodes) -> nlohmann::json
+{
+ const auto pids = tmux::get_client_pids().value_or(std::vector<int>{Application::parent_pid});
+
+ for (const auto pid : pids) {
+ const auto tree = util::get_process_tree(pid);
+ const auto found = ranges::find_if(nodes, [&tree](const njson &json) -> bool {
+ try {
+ return ranges::find(tree, json.at("pid").get<int>()) != tree.end();
+ } catch (const njson::out_of_range &err) {
+ return false;
+ }
+ });
+ if (found != nodes.end()) {
+ return *found;
+ }
+ }
+ return nullptr;
+}
+
+auto SwaySocket::get_focused_output_name() -> std::string
+{
+ return output_info.name;
+};
+
+void SwaySocket::set_active_output_info()
+{
+ const auto outputs = ipc_message(IPC_GET_OUTPUTS);
+ for (const auto &node : outputs) {
+ bool focused = node.at("focused");
+ if (focused) {
+ const auto &rect = node.at("rect");
+ output_info = {
+ .x = rect.at("x"),
+ .y = rect.at("y"),
+ .scale = node.at("scale"),
+ .name = node.at("name"),
+ };
+ break;
+ }
+ }
+}
+
+void SwaySocket::initial_setup(const std::string_view appid)
+{
+ disable_focus(appid);
+ enable_floating(appid);
+}
+
+void SwaySocket::disable_focus(const std::string_view appid)
+{
+ const auto payload = fmt::format(R"(no_focus [app_id="{}"])", appid);
+ ipc_command(payload);
+}
+
+void SwaySocket::enable_floating(const std::string_view appid)
+{
+ ipc_command(appid, "floating enable");
+}
+
+void SwaySocket::move_window(const std::string_view appid, int xcoord, int ycoord)
+{
+ int res_x = xcoord;
+ int res_y = ycoord;
+ if (output_info.scale > 1.0F) {
+ res_x = res_x / 2;
+ res_y = res_y / 2;
+ }
+ const auto payload = fmt::format(R"([app_id="{}"] move position {} {})", appid, res_x, res_y);
+ ipc_command(payload);
+}
+
+auto SwaySocket::ipc_message(ipc_message_type type, const std::string_view payload) const -> nlohmann::json
+{
+ struct ipc_header header;
+ header.len = payload.size();
+ header.type = type;
+ ipc_magic.copy(header.magic.data(), ipc_magic.size());
+
+ if (!payload.empty()) {
+ logger->debug("Running socket command {}", payload);
+ }
+ socket.write(&header, ipc_header_size);
+ socket.write(payload.data(), payload.size());
+
+ socket.read(&header, ipc_header_size);
+ std::string buff(header.len, 0);
+ socket.read(buff.data(), buff.size());
+ return njson::parse(buff);
+}
+
+auto SwaySocket::get_nodes() const -> std::vector<nlohmann::json>
+{
+ logger->debug("Obtaining sway tree");
+ const auto tree = ipc_message(IPC_GET_TREE);
+ std::stack<njson> nodes_st;
+ std::vector<njson> nodes_vec;
+
+ nodes_st.push(tree);
+
+ while (!nodes_st.empty()) {
+ const auto top = nodes_st.top();
+ nodes_st.pop();
+ nodes_vec.push_back(top);
+ for (const auto &node : top.at("nodes")) {
+ nodes_st.push(node);
+ }
+ for (const auto &node : top.at("floating_nodes")) {
+ nodes_st.push(node);
+ }
+ }
+ return nodes_vec;
+}
+
+void SwaySocket::ipc_command(const std::string_view appid, const std::string_view command) const
+{
+ const auto payload = fmt::format(R"(for_window [app_id="{}"] {})", appid, command);
+ ipc_command(payload);
+}
+
+void SwaySocket::ipc_command(const std::string_view payload) const
+{
+ std::ignore = ipc_message(IPC_COMMAND, payload);
+}
diff --git a/src/canvas/wayland/config/sway.hpp b/src/canvas/wayland/config/sway.hpp
new file mode 100644
index 0000000..9ac4d69
--- /dev/null
+++ b/src/canvas/wayland/config/sway.hpp
@@ -0,0 +1,62 @@
+// 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/>.
+
+#ifndef SWAY_SOCKET_H
+#define SWAY_SOCKET_H
+
+#include "../config.hpp"
+#include "util/socket.hpp"
+
+#include <nlohmann/json.hpp>
+#include <spdlog/spdlog.h>
+
+enum ipc_message_type { IPC_COMMAND = 0, IPC_GET_WORKSPACES = 1, IPC_GET_OUTPUTS = 3, IPC_GET_TREE = 4 };
+
+struct SwayOutputInfo {
+ int x;
+ int y;
+ float scale;
+ std::string name;
+};
+
+class SwaySocket : public WaylandConfig
+{
+ public:
+ explicit SwaySocket(std::string_view endpoint);
+ ~SwaySocket() override = default;
+
+ auto get_focused_output_name() -> std::string override;
+ auto get_window_info() -> struct WaylandWindowGeometry override;
+ void initial_setup(std::string_view appid) override;
+ void move_window(std::string_view appid, int xcoord, int ycoord) override;
+ static auto get_active_window(const std::vector<nlohmann::json> &nodes) -> nlohmann::json;
+
+ private:
+ void disable_focus(std::string_view appid);
+ void enable_floating(std::string_view appid);
+ void ipc_command(std::string_view appid, std::string_view command) const;
+ void ipc_command(std::string_view payload) const;
+ void set_active_output_info();
+
+ [[nodiscard]] auto get_nodes() const -> std::vector<nlohmann::json>;
+ [[nodiscard]] auto ipc_message(ipc_message_type type, std::string_view payload = "") const -> nlohmann::json;
+
+ UnixSocket socket;
+ std::shared_ptr<spdlog::logger> logger;
+ struct SwayOutputInfo output_info;
+};
+
+#endif
diff --git a/src/canvas/wayland/config/wayfire.cpp b/src/canvas/wayland/config/wayfire.cpp
new file mode 100644
index 0000000..efde546
--- /dev/null
+++ b/src/canvas/wayland/config/wayfire.cpp
@@ -0,0 +1,73 @@
+// 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 "wayfire.hpp"
+
+using njson = nlohmann::json;
+
+WayfireSocket::WayfireSocket(const std::string_view endpoint):
+socket(endpoint),
+logger(spdlog::get("wayland"))
+{
+ logger->info("Using wayfire socket {}", endpoint);
+}
+
+auto WayfireSocket::get_window_info() -> struct WaylandWindowGeometry
+{
+ const auto response = request("window-rules/get-focused-view");
+ const auto& info = response.at("info");
+ const auto& geometry = info.at("geometry");
+ const int decoration_height = 25;
+ return {
+ .width = geometry.at("width"),
+ .height = geometry.at("height").get<int>() - decoration_height,
+ .x = 0,
+ .y = 0
+ };
+}
+
+void WayfireSocket::initial_setup([[maybe_unused]] const std::string_view appid)
+{
+ // all is handled by the ueberzug plugin
+}
+
+void WayfireSocket::move_window(const std::string_view appid, int xcoord, int ycoord)
+{
+ const njson payload_data = {
+ {"app-id", appid},
+ {"x", xcoord},
+ {"y", ycoord}
+ };
+ std::ignore = request("ueberzugpp/set_offset", payload_data);
+}
+
+auto WayfireSocket::request(const std::string_view method, const njson& data) const -> njson
+{
+ const njson json = {
+ {"method", method},
+ {"data", data}
+ };
+ const auto payload = json.dump();
+ const uint32_t payload_size = payload.length();
+ socket.write(&payload_size, sizeof(uint32_t));
+ socket.write(payload.c_str(), payload_size);
+
+ uint32_t response_size = 0;
+ socket.read(&response_size, sizeof(uint32_t));
+ std::string buffer (response_size, 0);
+ socket.read(buffer.data(), response_size);
+ return njson::parse(buffer);
+}
diff --git a/src/canvas/wayland/config/wayfire.hpp b/src/canvas/wayland/config/wayfire.hpp
new file mode 100644
index 0000000..e1e83eb
--- /dev/null
+++ b/src/canvas/wayland/config/wayfire.hpp
@@ -0,0 +1,44 @@
+// 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/>.
+
+#ifndef WAYFIRE_SOCKET_H
+#define WAYFIRE_SOCKET_H
+
+#include "../config.hpp"
+#include "util/socket.hpp"
+
+#include <nlohmann/json.hpp>
+#include <spdlog/spdlog.h>
+
+class WayfireSocket : public WaylandConfig
+{
+ public:
+ explicit WayfireSocket(std::string_view endpoint);
+ ~WayfireSocket() override = default;
+
+ auto get_window_info() -> struct WaylandWindowGeometry override;
+ auto get_focused_output_name() -> std::string override { return {}; };
+ void initial_setup(std::string_view appid) override;
+ void move_window(std::string_view appid, int xcoord, int ycoord) override;
+
+ private:
+ [[nodiscard]] auto request(std::string_view method, const nlohmann::json &data = {}) const -> nlohmann::json;
+
+ UnixSocket socket;
+ std::shared_ptr<spdlog::logger> logger;
+};
+
+#endif
diff --git a/src/canvas/wayland/wayland.cpp b/src/canvas/wayland/wayland.cpp
new file mode 100644
index 0000000..9e2a642
--- /dev/null
+++ b/src/canvas/wayland/wayland.cpp
@@ -0,0 +1,222 @@
+// 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 "wayland.hpp"
+#include "application.hpp"
+#include "image.hpp"
+#include "os.hpp"
+#include "util.hpp"
+
+#ifdef ENABLE_OPENGL
+# include "window/waylandegl.hpp"
+# include <EGL/eglext.h>
+#endif
+#include "window/waylandshm.hpp"
+
+constexpr struct wl_registry_listener registry_listener = {.global = WaylandCanvas::registry_handle_global,
+ .global_remove = [](auto...) { /*unused*/ }};
+
+constexpr struct xdg_wm_base_listener xdg_wm_base_listener = {.ping = WaylandCanvas::xdg_wm_base_ping};
+
+constexpr struct wl_output_listener wl_output_listener = {.geometry = [](auto...) { /*unused*/ },
+ .mode = [](auto...) { /*unused*/ },
+ .done = WaylandCanvas::output_done,
+ .scale = WaylandCanvas::output_scale,
+ .name = WaylandCanvas::output_name,
+ .description = [](auto...) { /*unused*/ }};
+
+void WaylandCanvas::registry_handle_global(void *data, wl_registry *registry, uint32_t name, const char *interface,
+ [[maybe_unused]] uint32_t version)
+{
+ const std::string_view interface_str{interface};
+#ifdef WSL
+ const uint32_t compositor_ver = 4;
+ const uint32_t shm_ver = 1;
+ const uint32_t xdg_base_ver = 1;
+ const uint32_t output_ver = 3;
+#else
+ const uint32_t compositor_ver = 5;
+ const uint32_t shm_ver = 1;
+ const uint32_t xdg_base_ver = 2;
+ const uint32_t output_ver = 4;
+#endif
+
+ auto *canvas = static_cast<WaylandCanvas *>(data);
+ if (interface_str == wl_compositor_interface.name) {
+ canvas->compositor = static_cast<struct wl_compositor *>(
+ wl_registry_bind(registry, name, &wl_compositor_interface, compositor_ver));
+ } else if (interface_str == wl_shm_interface.name) {
+ canvas->wl_shm = static_cast<struct wl_shm *>(wl_registry_bind(registry, name, &wl_shm_interface, shm_ver));
+ } else if (interface_str == xdg_wm_base_interface.name) {
+ canvas->xdg_base =
+ static_cast<struct xdg_wm_base *>(wl_registry_bind(registry, name, &xdg_wm_base_interface, xdg_base_ver));
+ xdg_wm_base_add_listener(canvas->xdg_base, &xdg_wm_base_listener, canvas);
+ } else if (interface_str == wl_output_interface.name) {
+ auto *output =
+ static_cast<struct wl_output *>(wl_registry_bind(registry, name, &wl_output_interface, output_ver));
+ wl_output_add_listener(output, &wl_output_listener, canvas);
+ }
+}
+
+void WaylandCanvas::xdg_wm_base_ping([[maybe_unused]] void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial)
+{
+ xdg_wm_base_pong(xdg_wm_base, serial);
+}
+
+void WaylandCanvas::output_name(void *data, [[maybe_unused]] struct wl_output *output, const char *name)
+{
+ auto *canvas = static_cast<WaylandCanvas *>(data);
+ canvas->output_pair.first = name;
+}
+
+void WaylandCanvas::output_scale(void *data, [[maybe_unused]] struct wl_output *output, int32_t scale)
+{
+ auto *canvas = static_cast<WaylandCanvas *>(data);
+ canvas->output_pair.second = scale;
+}
+
+void WaylandCanvas::output_done(void *data, [[maybe_unused]] struct wl_output *output)
+{
+ auto *canvas = static_cast<WaylandCanvas *>(data);
+ const auto active_output = canvas->config->get_focused_output_name();
+ if (active_output == canvas->output_pair.first) {
+ canvas->flags->scale_factor = canvas->output_pair.second;
+ canvas->flags->needs_scaling = canvas->output_pair.second > 1;
+ }
+ canvas->output_info.insert(canvas->output_pair);
+}
+
+WaylandCanvas::WaylandCanvas()
+ : display(wl_display_connect(nullptr))
+{
+ if (display == nullptr) {
+ throw std::runtime_error("Failed to connect to wayland display.");
+ }
+ logger = spdlog::get("wayland");
+ flags = Flags::instance();
+ config = WaylandConfig::get();
+ registry = wl_display_get_registry(display);
+ wl_registry_add_listener(registry, &registry_listener, this);
+ wl_display_roundtrip(display);
+ event_handler = std::thread(&WaylandCanvas::handle_events, this);
+
+#ifdef ENABLE_OPENGL
+ if (flags->use_opengl) {
+ try {
+ egl = std::make_unique<EGLUtil<struct wl_display, struct wl_egl_window>>(EGL_PLATFORM_WAYLAND_EXT, display);
+ } catch (const std::runtime_error &) {
+ egl_available = false;
+ }
+ } else {
+ egl_available = false;
+ }
+#endif
+
+ logger->info("Canvas created");
+}
+
+void WaylandCanvas::show()
+{
+ for (const auto &[key, value] : windows) {
+ value->show();
+ }
+}
+
+void WaylandCanvas::hide()
+{
+ for (const auto &[key, value] : windows) {
+ value->hide();
+ }
+}
+
+WaylandCanvas::~WaylandCanvas()
+{
+ windows.clear();
+ if (event_handler.joinable()) {
+ event_handler.join();
+ }
+
+#ifdef ENABLE_OPENGL
+ egl.reset();
+#endif
+
+ if (wl_shm != nullptr) {
+ wl_shm_destroy(wl_shm);
+ }
+ if (compositor != nullptr) {
+ wl_compositor_destroy(compositor);
+ }
+ if (registry != nullptr) {
+ wl_registry_destroy(registry);
+ }
+ wl_display_disconnect(display);
+}
+
+void WaylandCanvas::add_image(const std::string &identifier, std::unique_ptr<Image> new_image)
+{
+ std::shared_ptr<WaylandWindow> window;
+#ifdef ENABLE_OPENGL
+ if (egl_available) {
+ try {
+ window = std::make_shared<WaylandEglWindow>(compositor, xdg_base, egl.get(), std::move(new_image),
+ config.get(), &xdg_agg);
+ } catch (const std::runtime_error &err) {
+ return;
+ }
+ }
+#endif
+ if (window == nullptr) {
+ window = std::make_shared<WaylandShmWindow>(this, std::move(new_image), &xdg_agg, config.get());
+ }
+
+ window->finish_init();
+ windows.insert_or_assign(identifier, std::move(window));
+}
+
+void WaylandCanvas::handle_events()
+{
+ const auto wl_fd = wl_display_get_fd(display);
+ bool in_event = false;
+
+ while (!Application::stop_flag) {
+ // prepare to read wayland events
+ while (wl_display_prepare_read(display) != 0) {
+ wl_display_dispatch_pending(display);
+ }
+ wl_display_flush(display);
+
+ try {
+ constexpr int waitms = 100;
+ in_event = os::wait_for_data_on_fd(wl_fd, waitms);
+ } catch (const std::system_error &err) {
+ Application::stop_flag = true;
+ break;
+ }
+
+ if (in_event) {
+ wl_display_read_events(display);
+ wl_display_dispatch_pending(display);
+ } else {
+ wl_display_cancel_read(display);
+ }
+ }
+}
+
+void WaylandCanvas::remove_image(const std::string &identifier)
+{
+ windows.erase(identifier);
+ wl_display_flush(display);
+}
diff --git a/src/canvas/wayland/wayland.hpp b/src/canvas/wayland/wayland.hpp
new file mode 100644
index 0000000..e2016df
--- /dev/null
+++ b/src/canvas/wayland/wayland.hpp
@@ -0,0 +1,84 @@
+// 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/>.
+
+#ifndef WAYLAND_CANVAS_H
+#define WAYLAND_CANVAS_H
+
+#include "canvas.hpp"
+#include "config.hpp"
+#include "flags.hpp"
+#include "wayland-xdg-shell-client-protocol.h"
+#include "window/waylandwindow.hpp"
+
+#include <memory>
+#include <thread>
+#include <unordered_map>
+
+#include <spdlog/spdlog.h>
+#include <wayland-client.h>
+
+#ifdef ENABLE_OPENGL
+# include "util/egl.hpp"
+# include <wayland-egl.h>
+#endif
+
+class WaylandCanvas : public Canvas
+{
+ public:
+ explicit WaylandCanvas();
+ ~WaylandCanvas() override;
+
+ static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface,
+ uint32_t version);
+ static void xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial);
+
+ static void output_scale(void *data, struct wl_output *output, int32_t scale);
+ static void output_name(void *data, struct wl_output *output, const char *name);
+ static void output_done(void *data, struct wl_output *output);
+
+ void add_image(const std::string &identifier, std::unique_ptr<Image> new_image) override;
+ void remove_image(const std::string &identifier) override;
+ void show() override;
+ void hide() override;
+
+ struct wl_compositor *compositor = nullptr;
+ struct wl_shm *wl_shm = nullptr;
+ struct xdg_wm_base *xdg_base = nullptr;
+
+ std::pair<std::string, int32_t> output_pair;
+ std::unordered_map<std::string, int32_t> output_info;
+
+ private:
+ struct wl_display *display = nullptr;
+ struct wl_registry *registry = nullptr;
+ std::thread event_handler;
+
+ std::shared_ptr<spdlog::logger> logger;
+ std::unique_ptr<WaylandConfig> config;
+ std::shared_ptr<Flags> flags;
+ std::unordered_map<std::string, std::shared_ptr<WaylandWindow>> windows;
+
+#ifdef ENABLE_OPENGL
+ std::unique_ptr<EGLUtil<struct wl_display, struct wl_egl_window>> egl;
+ bool egl_available = true;
+#endif
+
+ struct XdgStructAgg xdg_agg;
+
+ void handle_events();
+};
+
+#endif
diff --git a/src/canvas/wayland/window/shm.cpp b/src/canvas/wayland/window/shm.cpp
new file mode 100644
index 0000000..461267d
--- /dev/null
+++ b/src/canvas/wayland/window/shm.cpp
@@ -0,0 +1,75 @@
+// 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 "shm.hpp"
+#include "util.hpp"
+#include "util/ptr.hpp"
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <fmt/format.h>
+
+#include <cerrno>
+#include <system_error>
+
+WaylandShm::WaylandShm(int width, int height, int scale_factor, struct wl_shm *shm)
+ : shm(shm),
+ width(width),
+ height(height),
+ stride(width * 4),
+ pool_size(height * stride * scale_factor)
+{
+ const int path_size = 32;
+ shm_path = fmt::format("/{}", util::generate_random_string(path_size));
+ create_shm_file();
+ allocate_pool_buffers();
+}
+
+void WaylandShm::create_shm_file()
+{
+ fd = memfd_create("ueberzugpp-shm", 0);
+ if (fd == -1) {
+ throw std::system_error(errno, std::system_category());
+ }
+ int res = ftruncate(fd, pool_size);
+ if (res == -1) {
+ throw std::system_error(errno, std::system_category());
+ }
+ auto *pool_ptr = mmap(nullptr, pool_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (pool_ptr == MAP_FAILED) {
+ throw std::system_error(errno, std::system_category());
+ }
+ pool_data = static_cast<uint8_t *>(pool_ptr);
+}
+
+void WaylandShm::allocate_pool_buffers()
+{
+ const auto pool = c_unique_ptr<struct wl_shm_pool, wl_shm_pool_destroy>{wl_shm_create_pool(shm, fd, pool_size)};
+ buffer = wl_shm_pool_create_buffer(pool.get(), 0, width, height, stride, WL_SHM_FORMAT_ARGB8888);
+}
+
+WaylandShm::~WaylandShm()
+{
+ shm_unlink(shm_path.c_str());
+ close(fd);
+ munmap(pool_data, pool_size);
+ if (buffer != nullptr) {
+ wl_buffer_destroy(buffer);
+ }
+}
diff --git a/src/canvas/wayland/window/shm.hpp b/src/canvas/wayland/window/shm.hpp
new file mode 100644
index 0000000..fca1f88
--- /dev/null
+++ b/src/canvas/wayland/window/shm.hpp
@@ -0,0 +1,47 @@
+// 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/>.
+
+#ifndef WAYLAND_SHM_H
+#define WAYLAND_SHM_H
+
+#include <string>
+#include <wayland-client.h>
+
+class WaylandShm
+{
+ public:
+ WaylandShm(int width, int height, int scale_factor, struct wl_shm *shm);
+ ~WaylandShm();
+
+ struct wl_buffer *buffer = nullptr;
+ uint8_t *pool_data;
+
+ private:
+ void create_shm_file();
+ void allocate_pool_buffers();
+
+ struct wl_shm *shm = nullptr;
+
+ int fd = 0;
+ std::string shm_path;
+
+ int width = 0;
+ int height = 0;
+ int stride = 0;
+ int pool_size = 0;
+};
+
+#endif
diff --git a/src/canvas/wayland/window/waylandegl.cpp b/src/canvas/wayland/window/waylandegl.cpp
new file mode 100644
index 0000000..3545d20
--- /dev/null
+++ b/src/canvas/wayland/window/waylandegl.cpp
@@ -0,0 +1,235 @@
+// 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 "waylandegl.hpp"
+#include "dimensions.hpp"
+#include "util.hpp"
+
+#include <fmt/format.h>
+#include <thread>
+
+constexpr int id_len = 10;
+
+constexpr struct xdg_surface_listener xdg_surface_listener_egl = {
+ .configure = WaylandEglWindow::xdg_surface_configure,
+};
+
+constexpr struct wl_callback_listener frame_listener_egl = {.done = WaylandEglWindow::wl_surface_frame_done};
+
+WaylandEglWindow::WaylandEglWindow(struct wl_compositor *compositor, struct xdg_wm_base *xdg_base,
+ const EGLUtil<struct wl_display, struct wl_egl_window> *egl,
+ std::unique_ptr<Image> new_image, WaylandConfig *new_config,
+ struct XdgStructAgg *xdg_agg)
+ : compositor(compositor),
+ xdg_base(xdg_base),
+ surface(wl_compositor_create_surface(compositor)),
+ xdg_surface(xdg_wm_base_get_xdg_surface(xdg_base, surface)),
+ xdg_toplevel(xdg_surface_get_toplevel(xdg_surface)),
+ image(std::move(new_image)),
+ config(new_config),
+ egl_window(wl_egl_window_create(surface, image->width(), image->height())),
+ egl(egl),
+ appid(fmt::format("ueberzugpp_{}", util::generate_random_string(id_len))),
+ xdg_agg(xdg_agg)
+{
+ config->initial_setup(appid);
+ opengl_setup();
+ xdg_setup();
+}
+
+WaylandEglWindow::~WaylandEglWindow()
+{
+ opengl_cleanup();
+ delete_xdg_structs();
+ delete_wayland_structs();
+}
+
+void WaylandEglWindow::opengl_cleanup()
+{
+ egl->run_contained(egl_context, egl_surface, [this] {
+ glDeleteTextures(1, &texture);
+ glDeleteFramebuffers(1, &fbo);
+ });
+ eglDestroySurface(egl->display, egl_surface);
+ eglDestroyContext(egl->display, egl_context);
+}
+
+void WaylandEglWindow::finish_init()
+{
+ auto xdg = std::make_unique<XdgStruct>();
+ xdg->ptr = weak_from_this();
+ this_ptr = xdg.get();
+ xdg_agg->ptrs.push_back(std::move(xdg));
+ setup_listeners();
+ visible = true;
+}
+
+void WaylandEglWindow::opengl_setup()
+{
+ egl_surface = egl->create_surface(egl_window);
+ if (egl_surface == EGL_NO_SURFACE) {
+ throw std::runtime_error("");
+ }
+
+ egl_context = egl->create_context(egl_surface);
+ if (egl_context == EGL_NO_CONTEXT) {
+ throw std::runtime_error("");
+ }
+
+ egl->run_contained(egl_surface, egl_context, [this] {
+ eglSwapInterval(egl->display, 0);
+ glGenFramebuffers(1, &fbo);
+ glGenTextures(1, &texture);
+ });
+}
+
+void WaylandEglWindow::setup_listeners()
+{
+ xdg_surface_add_listener(xdg_surface, &xdg_surface_listener_egl, this_ptr);
+ wl_surface_commit(surface);
+
+ if (image->is_animated()) {
+ callback = wl_surface_frame(surface);
+ wl_callback_add_listener(callback, &frame_listener_egl, this_ptr);
+ }
+}
+
+void WaylandEglWindow::xdg_setup()
+{
+ xdg_toplevel_set_app_id(xdg_toplevel, appid.c_str());
+ xdg_toplevel_set_title(xdg_toplevel, appid.c_str());
+}
+
+void WaylandEglWindow::delete_xdg_structs()
+{
+ if (xdg_toplevel != nullptr) {
+ xdg_toplevel_destroy(xdg_toplevel);
+ xdg_toplevel = nullptr;
+ }
+ if (xdg_surface != nullptr) {
+ xdg_surface_destroy(xdg_surface);
+ xdg_surface = nullptr;
+ }
+}
+
+void WaylandEglWindow::delete_wayland_structs()
+{
+ if (egl_window != nullptr) {
+ wl_egl_window_destroy(egl_window);
+ egl_window = nullptr;
+ }
+ if (surface != nullptr) {
+ wl_surface_destroy(surface);
+ surface = nullptr;
+ }
+}
+
+void WaylandEglWindow::draw()
+{
+ load_framebuffer();
+
+ wl_surface_commit(surface);
+ move_window();
+}
+
+void WaylandEglWindow::load_framebuffer()
+{
+ std::scoped_lock lock{egl_mutex};
+ egl->run_contained(egl_surface, egl_context, [this] {
+ egl->get_texture_from_image(*image, texture);
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+ glBlitFramebuffer(0, 0, image->width(), image->height(), 0, 0, image->width(), image->height(),
+ GL_COLOR_BUFFER_BIT, GL_NEAREST);
+
+ eglSwapBuffers(egl->display, egl_surface);
+ });
+}
+
+void WaylandEglWindow::move_window()
+{
+ const auto dims = image->dimensions();
+ const auto cur_window = config->get_window_info();
+ const int wayland_x = dims.xpixels() + dims.padding_horizontal;
+ const int wayland_y = dims.ypixels() + dims.padding_vertical;
+ const int xcoord = cur_window.x + wayland_x;
+ const int ycoord = cur_window.y + wayland_y;
+ config->move_window(appid, xcoord, ycoord);
+}
+
+void WaylandEglWindow::generate_frame()
+{
+ std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
+ callback = wl_surface_frame(surface);
+ wl_callback_add_listener(callback, &frame_listener_egl, this_ptr);
+
+ image->next_frame();
+ load_framebuffer();
+
+ wl_surface_commit(surface);
+}
+
+void WaylandEglWindow::show()
+{
+ if (visible) {
+ return;
+ }
+ visible = true;
+ xdg_surface = xdg_wm_base_get_xdg_surface(xdg_base, surface);
+ xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
+ xdg_setup();
+ setup_listeners();
+}
+
+void WaylandEglWindow::hide()
+{
+ if (!visible) {
+ return;
+ }
+ visible = false;
+ const std::scoped_lock lock{draw_mutex};
+ delete_xdg_structs();
+ wl_surface_attach(surface, nullptr, 0, 0);
+ wl_surface_commit(surface);
+}
+
+void WaylandEglWindow::xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
+{
+ xdg_surface_ack_configure(xdg_surface, serial);
+ const auto *tmp = static_cast<struct XdgStruct *>(data);
+ const auto window = tmp->ptr.lock();
+ if (!window) {
+ return;
+ }
+ auto *egl_window = dynamic_cast<WaylandEglWindow *>(window.get());
+ egl_window->draw();
+}
+
+void WaylandEglWindow::wl_surface_frame_done(void *data, struct wl_callback *callback, [[maybe_unused]] uint32_t time)
+{
+ wl_callback_destroy(callback);
+ const auto *tmp = static_cast<struct XdgStruct *>(data);
+ const auto window = tmp->ptr.lock();
+ if (!window) {
+ return;
+ }
+ auto *egl_window = dynamic_cast<WaylandEglWindow *>(window.get());
+ const std::scoped_lock lock{egl_window->draw_mutex};
+ if (!egl_window->visible) {
+ return;
+ }
+ egl_window->generate_frame();
+}
diff --git a/src/canvas/wayland/window/waylandegl.hpp b/src/canvas/wayland/window/waylandegl.hpp
new file mode 100644
index 0000000..768e57f
--- /dev/null
+++ b/src/canvas/wayland/window/waylandegl.hpp
@@ -0,0 +1,88 @@
+// 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/>.
+
+#ifndef WAYLAND_EGL_WINDOW_H
+#define WAYLAND_EGL_WINDOW_H
+
+#include "../config.hpp"
+#include "image.hpp"
+#include "util/egl.hpp"
+#include "wayland-xdg-shell-client-protocol.h"
+#include "waylandwindow.hpp"
+
+#include <wayland-client.h>
+#include <wayland-egl.h>
+
+#include <memory>
+#include <mutex>
+
+class WaylandEglWindow : public WaylandWindow
+{
+ public:
+ WaylandEglWindow(struct wl_compositor *compositor, struct xdg_wm_base *xdg_base,
+ const EGLUtil<struct wl_display, struct wl_egl_window> *egl, std::unique_ptr<Image> new_image,
+ WaylandConfig *new_config, struct XdgStructAgg *xdg_agg);
+ ~WaylandEglWindow() override;
+ static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial);
+ static void wl_surface_frame_done(void *data, struct wl_callback *callback, uint32_t time);
+
+ void draw() override;
+ void generate_frame() override;
+ void show() override;
+ void hide() override;
+
+ void finish_init() override;
+
+ private:
+ struct wl_compositor *compositor;
+ struct xdg_wm_base *xdg_base;
+
+ struct wl_surface *surface = nullptr;
+ struct xdg_surface *xdg_surface = nullptr;
+ struct xdg_toplevel *xdg_toplevel = nullptr;
+ struct wl_callback *callback;
+
+ std::unique_ptr<Image> image;
+ WaylandConfig *config;
+
+ EGLSurface egl_surface;
+ EGLContext egl_context;
+
+ struct wl_egl_window *egl_window = nullptr;
+ const EGLUtil<struct wl_display, struct wl_egl_window> *egl;
+
+ GLuint texture;
+ GLuint fbo;
+
+ std::mutex draw_mutex;
+ std::mutex egl_mutex;
+
+ std::string appid;
+ void *this_ptr;
+ struct XdgStructAgg *xdg_agg;
+ bool visible = false;
+
+ void move_window();
+ void delete_wayland_structs();
+ void delete_xdg_structs();
+
+ void opengl_cleanup();
+ void xdg_setup();
+ void setup_listeners();
+ void opengl_setup();
+ void load_framebuffer();
+};
+#endif
diff --git a/src/canvas/wayland/window/waylandshm.cpp b/src/canvas/wayland/window/waylandshm.cpp
new file mode 100644
index 0000000..1ee5994
--- /dev/null
+++ b/src/canvas/wayland/window/waylandshm.cpp
@@ -0,0 +1,186 @@
+// 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 "waylandshm.hpp"
+#include "dimensions.hpp"
+#include "shm.hpp"
+#include "util.hpp"
+
+#include <fmt/format.h>
+
+constexpr int id_len = 10;
+
+constexpr struct xdg_surface_listener xdg_surface_listener = {
+ .configure = WaylandShmWindow::xdg_surface_configure,
+};
+
+constexpr struct wl_callback_listener frame_listener = {.done = WaylandShmWindow::wl_surface_frame_done};
+
+void WaylandShmWindow::xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
+{
+ xdg_surface_ack_configure(xdg_surface, serial);
+ const auto *tmp = static_cast<struct XdgStruct *>(data);
+ const auto window = tmp->ptr.lock();
+ if (!window) {
+ return;
+ }
+ auto *shm_window = dynamic_cast<WaylandShmWindow *>(window.get());
+ shm_window->wl_draw(shm_window->output_scale);
+}
+
+void WaylandShmWindow::wl_surface_frame_done(void *data, struct wl_callback *callback, [[maybe_unused]] uint32_t time)
+{
+ wl_callback_destroy(callback);
+ const auto *tmp = static_cast<struct XdgStruct *>(data);
+ const auto window = tmp->ptr.lock();
+ if (!window) {
+ return;
+ }
+ auto *shm_window = dynamic_cast<WaylandShmWindow *>(window.get());
+ const std::scoped_lock lock{shm_window->draw_mutex};
+ if (!shm_window->visible) {
+ return;
+ }
+ shm_window->generate_frame();
+}
+
+WaylandShmWindow::WaylandShmWindow(WaylandCanvas *canvas, std::unique_ptr<Image> new_image,
+ struct XdgStructAgg *xdg_agg, WaylandConfig *config)
+ : config(config),
+ xdg_base(canvas->xdg_base),
+ surface(wl_compositor_create_surface(canvas->compositor)),
+ xdg_surface(xdg_wm_base_get_xdg_surface(xdg_base, surface)),
+ xdg_toplevel(xdg_surface_get_toplevel(xdg_surface)),
+ image(std::move(new_image)),
+ appid(fmt::format("ueberzugpp_{}", util::generate_random_string(id_len))),
+ xdg_agg(xdg_agg)
+{
+ config->initial_setup(appid);
+ xdg_setup();
+ output_scale = canvas->output_info.at(config->get_focused_output_name());
+ shm = std::make_unique<WaylandShm>(image->width(), image->height(), output_scale, canvas->wl_shm);
+}
+
+void WaylandShmWindow::finish_init()
+{
+ auto xdg = std::make_unique<XdgStruct>();
+ xdg->ptr = weak_from_this();
+ this_ptr = xdg.get();
+ xdg_agg->ptrs.push_back(std::move(xdg));
+
+ setup_listeners();
+ visible = true;
+}
+
+void WaylandShmWindow::setup_listeners()
+{
+ xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, this_ptr);
+ wl_surface_commit(surface);
+
+ if (image->is_animated()) {
+ callback = wl_surface_frame(surface);
+ wl_callback_add_listener(callback, &frame_listener, this_ptr);
+ }
+}
+
+void WaylandShmWindow::xdg_setup()
+{
+ xdg_toplevel_set_app_id(xdg_toplevel, appid.c_str());
+ xdg_toplevel_set_title(xdg_toplevel, appid.c_str());
+}
+
+WaylandShmWindow::~WaylandShmWindow()
+{
+ delete_xdg_structs();
+ delete_wayland_structs();
+}
+
+void WaylandShmWindow::wl_draw(int32_t scale_factor)
+{
+ std::memcpy(shm->pool_data, image->data(), image->size());
+ wl_surface_attach(surface, shm->buffer, 0, 0);
+ wl_surface_set_buffer_scale(surface, scale_factor);
+ wl_surface_commit(surface);
+ move_window();
+}
+
+void WaylandShmWindow::show()
+{
+ if (visible) {
+ return;
+ }
+ visible = true;
+ xdg_surface = xdg_wm_base_get_xdg_surface(xdg_base, surface);
+ xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
+ xdg_setup();
+ setup_listeners();
+}
+
+void WaylandShmWindow::hide()
+{
+ if (!visible) {
+ return;
+ }
+ visible = false;
+ const std::scoped_lock lock{draw_mutex};
+ delete_xdg_structs();
+ wl_surface_attach(surface, nullptr, 0, 0);
+ wl_surface_commit(surface);
+}
+
+void WaylandShmWindow::delete_xdg_structs()
+{
+ if (xdg_toplevel != nullptr) {
+ xdg_toplevel_destroy(xdg_toplevel);
+ xdg_toplevel = nullptr;
+ }
+ if (xdg_surface != nullptr) {
+ xdg_surface_destroy(xdg_surface);
+ xdg_surface = nullptr;
+ }
+}
+
+void WaylandShmWindow::delete_wayland_structs()
+{
+ if (surface != nullptr) {
+ wl_surface_destroy(surface);
+ surface = nullptr;
+ }
+}
+
+void WaylandShmWindow::move_window()
+{
+ const auto dims = image->dimensions();
+ const auto cur_window = config->get_window_info();
+ const int wayland_x = dims.xpixels() + dims.padding_horizontal;
+ const int wayland_y = dims.ypixels() + dims.padding_vertical;
+ const int xcoord = cur_window.x + wayland_x;
+ const int ycoord = cur_window.y + wayland_y;
+ config->move_window(appid, xcoord, ycoord);
+}
+
+void WaylandShmWindow::generate_frame()
+{
+ std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
+ callback = wl_surface_frame(surface);
+ wl_callback_add_listener(callback, &frame_listener, this_ptr);
+
+ image->next_frame();
+ std::memcpy(shm->pool_data, image->data(), image->size());
+ wl_surface_attach(surface, shm->buffer, 0, 0);
+ wl_surface_damage_buffer(surface, 0, 0, image->width(), image->height());
+ wl_surface_commit(surface);
+}
diff --git a/src/canvas/wayland/window/waylandshm.hpp b/src/canvas/wayland/window/waylandshm.hpp
new file mode 100644
index 0000000..f275c4c
--- /dev/null
+++ b/src/canvas/wayland/window/waylandshm.hpp
@@ -0,0 +1,77 @@
+// 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/>.
+
+#ifndef WAYLAND_SHM_WINDOW_H
+#define WAYLAND_SHM_WINDOW_H
+
+#include "../wayland.hpp"
+#include "image.hpp"
+#include "shm.hpp"
+#include "wayland-xdg-shell-client-protocol.h"
+#include "waylandwindow.hpp"
+
+#include <atomic>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <wayland-client.h>
+
+class WaylandShmWindow : public WaylandWindow
+{
+ public:
+ WaylandShmWindow(WaylandCanvas *canvas, std::unique_ptr<Image> new_image, struct XdgStructAgg *xdg_agg,
+ WaylandConfig *config);
+ ~WaylandShmWindow() override;
+ static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial);
+ static void wl_surface_frame_done(void *data, struct wl_callback *callback, uint32_t time);
+
+ void draw() override {}
+ void wl_draw(int32_t scale_factor) override;
+ void generate_frame() override;
+ void show() override;
+ void hide() override;
+
+ void finish_init() override;
+
+ std::mutex draw_mutex;
+ std::atomic<bool> visible{false};
+ std::unique_ptr<WaylandShm> shm;
+ int32_t output_scale;
+
+ private:
+ WaylandConfig *config;
+
+ struct xdg_wm_base *xdg_base = nullptr;
+ struct wl_surface *surface = nullptr;
+ struct xdg_surface *xdg_surface = nullptr;
+ struct xdg_toplevel *xdg_toplevel = nullptr;
+ struct wl_callback *callback;
+
+ std::unique_ptr<Image> image;
+ std::string appid;
+
+ struct XdgStructAgg *xdg_agg;
+ void *this_ptr;
+
+ void move_window();
+ void xdg_setup();
+
+ void setup_listeners();
+ void delete_wayland_structs();
+ void delete_xdg_structs();
+};
+
+#endif
diff --git a/src/canvas/wayland/window/waylandwindow.hpp b/src/canvas/wayland/window/waylandwindow.hpp
new file mode 100644
index 0000000..0a1a1e8
--- /dev/null
+++ b/src/canvas/wayland/window/waylandwindow.hpp
@@ -0,0 +1,46 @@
+// 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/>.
+
+#ifndef WAYLAND_WINDOW_H
+#define WAYLAND_WINDOW_H
+
+#include "window.hpp"
+
+#include <memory>
+#include <vector>
+
+class WaylandWindow:
+ public Window,
+ public std::enable_shared_from_this<WaylandWindow>
+{
+public:
+ ~WaylandWindow() override = default;
+
+ virtual void wl_draw([[maybe_unused]] int32_t scale_factor) {};
+ virtual void finish_init() = 0;
+};
+
+struct XdgStruct
+{
+ std::weak_ptr<WaylandWindow> ptr;
+};
+
+struct XdgStructAgg
+{
+ std::vector<std::unique_ptr<XdgStruct>> ptrs;
+};
+
+#endif
diff --git a/src/canvas/x11/window/x11.cpp b/src/canvas/x11/window/x11.cpp
new file mode 100644
index 0000000..c32c715
--- /dev/null
+++ b/src/canvas/x11/window/x11.cpp
@@ -0,0 +1,119 @@
+// 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 "x11.hpp"
+#include "dimensions.hpp"
+
+#include <string_view>
+
+#include <xcb/xcb.h>
+
+constexpr std::string_view win_name = "ueberzugpp";
+
+X11Window::X11Window(xcb_connection_t *connection, xcb_screen_t *screen, xcb_window_t window, xcb_window_t parent,
+ std::shared_ptr<Image> image)
+ : connection(connection),
+ screen(screen),
+ window(window),
+ parent(parent),
+ gc(xcb_generate_id(connection)),
+ image(std::move(image))
+{
+ logger = spdlog::get("X11");
+ create();
+ change_title();
+}
+
+void X11Window::create()
+{
+ const uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP;
+ struct xcb_create_window_value_list_t value_list;
+ value_list.background_pixel = screen->black_pixel;
+ value_list.border_pixel = screen->black_pixel;
+ value_list.event_mask = XCB_EVENT_MASK_EXPOSURE;
+ value_list.colormap = screen->default_colormap;
+
+ const auto dimensions = image->dimensions();
+ const auto xcoord = static_cast<int16_t>(dimensions.xpixels() + dimensions.padding_horizontal);
+ const auto ycoord = static_cast<int16_t>(dimensions.ypixels() + dimensions.padding_vertical);
+ xcb_create_window_aux(connection, screen->root_depth, window, this->parent, xcoord, ycoord, image->width(),
+ image->height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, value_mask,
+ &value_list);
+
+ xcb_create_gc(connection, gc, window, 0, nullptr);
+ logger->debug("Created child window {} at ({},{}) with parent {}", window, xcoord, ycoord, parent);
+}
+
+void X11Window::change_title()
+{
+ const int bits_in_char = 8;
+ xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, bits_in_char,
+ win_name.size(), win_name.data());
+}
+
+void X11Window::show()
+{
+ if (visible) {
+ return;
+ }
+ visible = true;
+ xcb_map_window(connection, window);
+ xcb_flush(connection);
+}
+
+void X11Window::hide()
+{
+ if (!visible) {
+ return;
+ }
+ visible = false;
+ xcb_unmap_window(connection, window);
+ xcb_flush(connection);
+}
+
+void X11Window::draw()
+{
+ if (!xcb_image) {
+ return;
+ }
+ xcb_image_put(connection, window, gc, xcb_image.get(), 0, 0, 0);
+}
+
+void X11Window::generate_frame()
+{
+ xcb_image.reset(xcb_image_create_native(connection, image->width(), image->height(), XCB_IMAGE_FORMAT_Z_PIXMAP,
+ screen->root_depth, nullptr, 0, nullptr));
+ xcb_image->data = const_cast<unsigned char *>(image->data());
+ send_expose_event();
+}
+
+X11Window::~X11Window()
+{
+ xcb_destroy_window(connection, window);
+ xcb_free_gc(connection, gc);
+ xcb_flush(connection);
+}
+
+void X11Window::send_expose_event()
+{
+ const int event_size = 32;
+ std::array<char, event_size> buffer;
+ auto *event = reinterpret_cast<xcb_expose_event_t *>(buffer.data());
+ event->response_type = XCB_EXPOSE;
+ event->window = window;
+ xcb_send_event(connection, 0, window, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast<char *>(event));
+ xcb_flush(connection);
+}
diff --git a/src/canvas/x11/window/x11.hpp b/src/canvas/x11/window/x11.hpp
new file mode 100644
index 0000000..5230b1d
--- /dev/null
+++ b/src/canvas/x11/window/x11.hpp
@@ -0,0 +1,60 @@
+// 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/>.
+
+#ifndef X11_WINDOW_H
+#define X11_WINDOW_H
+
+#include "image.hpp"
+#include "util/ptr.hpp"
+#include "window.hpp"
+
+#include <xcb/xcb_image.h>
+#include <spdlog/spdlog.h>
+
+class Dimensions;
+
+class X11Window : public Window
+{
+public:
+ X11Window(xcb_connection_t* connection, xcb_screen_t *screen,
+ xcb_window_t window, xcb_window_t parent, std::shared_ptr<Image> image);
+ ~X11Window() override;
+
+ void draw() override;
+ void generate_frame() override;
+ void show() override;
+ void hide() override;
+
+private:
+ xcb_connection_t *connection;
+ xcb_screen_t *screen;
+
+ xcb_window_t window;
+ xcb_window_t parent;
+ xcb_gcontext_t gc;
+
+ c_unique_ptr<xcb_image_t, xcb_image_destroy> xcb_image;
+ std::shared_ptr<spdlog::logger> logger;
+ std::shared_ptr<Image> image;
+
+ bool visible = false;
+
+ void send_expose_event();
+ void create();
+ void change_title();
+};
+
+#endif
diff --git a/src/canvas/x11/window/x11egl.cpp b/src/canvas/x11/window/x11egl.cpp
new file mode 100644
index 0000000..c494957
--- /dev/null
+++ b/src/canvas/x11/window/x11egl.cpp
@@ -0,0 +1,138 @@
+// 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 "x11egl.hpp"
+#include "dimensions.hpp"
+#include "image.hpp"
+#include "util.hpp"
+
+#include <array>
+
+X11EGLWindow::X11EGLWindow(xcb_connection_t *connection, xcb_screen_t *screen, xcb_window_t windowid,
+ xcb_window_t parentid, const EGLUtil<xcb_connection_t, xcb_window_t> *egl,
+ std::shared_ptr<Image> new_image)
+ : connection(connection),
+ screen(screen),
+ windowid(windowid),
+ parentid(parentid),
+ image(std::move(new_image)),
+ egl(egl)
+{
+ logger = spdlog::get("x11");
+ create();
+ opengl_setup();
+}
+
+X11EGLWindow::~X11EGLWindow()
+{
+ egl->run_contained(egl_surface, egl_context, [this] {
+ glDeleteTextures(1, &texture);
+ glDeleteFramebuffers(1, &fbo);
+ });
+ eglDestroySurface(egl->display, egl_surface);
+ eglDestroyContext(egl->display, egl_context);
+
+ xcb_destroy_window(connection, windowid);
+ xcb_flush(connection);
+}
+
+void X11EGLWindow::create()
+{
+ const uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP;
+ struct xcb_create_window_value_list_t value_list;
+ value_list.background_pixel = screen->black_pixel;
+ value_list.border_pixel = screen->black_pixel;
+ value_list.event_mask = XCB_EVENT_MASK_EXPOSURE;
+ value_list.colormap = screen->default_colormap;
+
+ const auto dimensions = image->dimensions();
+ const auto xcoord = static_cast<int16_t>(dimensions.xpixels() + dimensions.padding_horizontal);
+ const auto ycoord = static_cast<int16_t>(dimensions.ypixels() + dimensions.padding_vertical);
+ xcb_create_window_aux(connection, screen->root_depth, windowid, parentid, xcoord, ycoord, image->width(),
+ image->height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, value_mask,
+ &value_list);
+}
+
+void X11EGLWindow::opengl_setup()
+{
+ egl_surface = egl->create_surface(&windowid);
+ if (egl_surface == EGL_NO_SURFACE) {
+ throw std::runtime_error("");
+ }
+
+ egl_context = egl->create_context(egl_surface);
+ if (egl_context == EGL_NO_CONTEXT) {
+ throw std::runtime_error("");
+ }
+
+ egl->run_contained(egl_surface, egl_context, [this] {
+ glGenFramebuffers(1, &fbo);
+ glGenTextures(1, &texture);
+ });
+}
+
+void X11EGLWindow::draw()
+{
+ const std::scoped_lock lock{egl_mutex};
+ egl->run_contained(egl_surface, egl_context, [this] {
+ glBlitFramebuffer(0, 0, image->width(), image->height(), 0, 0, image->width(), image->height(),
+ GL_COLOR_BUFFER_BIT, GL_NEAREST);
+ eglSwapBuffers(egl->display, egl_surface);
+ });
+}
+
+void X11EGLWindow::generate_frame()
+{
+ const std::scoped_lock lock{egl_mutex};
+ egl->run_contained(egl_surface, egl_context, [this] {
+ egl->get_texture_from_image(*image, texture);
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+ });
+
+ send_expose_event();
+}
+
+void X11EGLWindow::show()
+{
+ if (visible) {
+ return;
+ }
+ visible = true;
+ xcb_map_window(connection, windowid);
+ xcb_flush(connection);
+}
+
+void X11EGLWindow::hide()
+{
+ if (!visible) {
+ return;
+ }
+ visible = false;
+ xcb_unmap_window(connection, windowid);
+ xcb_flush(connection);
+}
+
+void X11EGLWindow::send_expose_event()
+{
+ const int event_size = 32;
+ std::array<char, event_size> buffer;
+ auto *event = reinterpret_cast<xcb_expose_event_t *>(buffer.data());
+ event->response_type = XCB_EXPOSE;
+ event->window = windowid;
+ xcb_send_event(connection, 0, windowid, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast<char *>(event));
+ xcb_flush(connection);
+}
diff --git a/src/canvas/x11/window/x11egl.hpp b/src/canvas/x11/window/x11egl.hpp
new file mode 100644
index 0000000..09fdf82
--- /dev/null
+++ b/src/canvas/x11/window/x11egl.hpp
@@ -0,0 +1,68 @@
+// 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/>.
+
+#ifndef X11_EGL_WINDOW_H
+#define X11_EGL_WINDOW_H
+
+#include "window.hpp"
+#include "util/egl.hpp"
+
+#include <xcb/xcb.h>
+#include <spdlog/spdlog.h>
+
+#include <memory>
+#include <mutex>
+
+class Image;
+
+class X11EGLWindow : public Window
+{
+public:
+ X11EGLWindow(xcb_connection_t* connection, xcb_screen_t* screen,
+ xcb_window_t windowid, xcb_window_t parentid, const EGLUtil<xcb_connection_t, xcb_window_t>* egl,
+ std::shared_ptr<Image> new_image);
+ ~X11EGLWindow() override;
+
+ void draw() override;
+ void generate_frame() override;
+ void show() override;
+ void hide() override;
+
+private:
+ xcb_connection_t* connection;
+ xcb_screen_t* screen;
+ xcb_window_t windowid;
+ xcb_window_t parentid;
+ std::shared_ptr<Image> image;
+ const EGLUtil<xcb_connection_t, xcb_window_t>* egl;
+
+ GLuint texture;
+ GLuint fbo;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+
+ std::mutex egl_mutex;
+ std::shared_ptr<spdlog::logger> logger;
+
+ bool visible = false;
+
+ void send_expose_event();
+ void create();
+ void change_title();
+ void opengl_setup();
+};
+
+#endif
diff --git a/src/canvas/x11/x11.cpp b/src/canvas/x11/x11.cpp
new file mode 100644
index 0000000..5f1a04c
--- /dev/null
+++ b/src/canvas/x11/x11.cpp
@@ -0,0 +1,257 @@
+// 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 "x11.hpp"
+#include "application.hpp"
+#include "flags.hpp"
+#include "os.hpp"
+#include "tmux.hpp"
+#include "util.hpp"
+
+#include <string_view>
+
+#include <range/v3/all.hpp>
+
+#ifdef ENABLE_OPENGL
+# include "window/x11egl.hpp"
+# include <EGL/eglext.h>
+#endif
+#include "window/x11.hpp"
+
+X11Canvas::X11Canvas()
+ : connection(xcb_connect(nullptr, nullptr))
+{
+ if (xcb_connection_has_error(connection) > 0) {
+ throw std::runtime_error("Can't connect to X11 server");
+ }
+ screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data;
+
+#ifdef ENABLE_XCB_ERRORS
+ xcb_errors_context_new(connection, &err_ctx);
+#endif
+
+ flags = Flags::instance();
+
+#ifdef ENABLE_OPENGL
+ if (flags->use_opengl) {
+ try {
+ egl = std::make_unique<EGLUtil<xcb_connection_t, xcb_window_t>>(EGL_PLATFORM_XCB_EXT, connection);
+ } catch (const std::runtime_error &err) {
+ egl_available = false;
+ }
+ } else {
+ egl_available = false;
+ }
+#endif
+
+ xutil = std::make_unique<X11Util>(connection);
+ logger = spdlog::get("X11");
+ event_handler = std::thread(&X11Canvas::handle_events, this);
+ logger->info("Canvas created");
+}
+
+X11Canvas::~X11Canvas()
+{
+ draw_threads.clear();
+ windows.clear();
+ image_windows.clear();
+
+ if (event_handler.joinable()) {
+ event_handler.join();
+ }
+
+#ifdef ENABLE_XCB_ERRORS
+ xcb_errors_context_free(err_ctx);
+#endif
+
+ xcb_disconnect(connection);
+}
+
+void X11Canvas::draw(const std::string &identifier)
+{
+ if (!images.at(identifier)->is_animated()) {
+ for (const auto &[wid, window] : image_windows.at(identifier)) {
+ window->generate_frame();
+ }
+ return;
+ }
+
+ draw_threads.insert_or_assign(identifier, std::jthread([this, identifier](const std::stop_token &stoken) {
+ const auto image = images.at(identifier);
+ const auto wins = image_windows.at(identifier);
+ while (!stoken.stop_requested()) {
+ for (const auto &[wid, window] : wins) {
+ window->generate_frame();
+ }
+ image->next_frame();
+ std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
+ }
+ }));
+}
+
+void X11Canvas::show()
+{
+ const std::scoped_lock lock{windows_mutex};
+ for (const auto &[wid, window] : windows) {
+ window->show();
+ }
+}
+
+void X11Canvas::hide()
+{
+ const std::scoped_lock lock{windows_mutex};
+ for (const auto &[wid, window] : windows) {
+ window->hide();
+ }
+}
+
+void X11Canvas::handle_events()
+{
+ const int event_mask = 0x80;
+ const int waitms = 100;
+ const int connfd = xcb_get_file_descriptor(connection);
+ bool status = false;
+
+ while (!Application::stop_flag) {
+ try {
+ status = os::wait_for_data_on_fd(connfd, waitms);
+ } catch (const std::system_error &err) {
+ Application::stop_flag = true;
+ break;
+ }
+
+ if (!status) {
+ continue;
+ }
+
+ const std::scoped_lock lock{windows_mutex};
+ auto event = unique_C_ptr<xcb_generic_event_t>{xcb_poll_for_event(connection)};
+ while (event) {
+ const int real_event = event->response_type & ~event_mask;
+ switch (real_event) {
+ case 0: {
+ const auto *err = reinterpret_cast<xcb_generic_error_t *>(event.get());
+ print_xcb_error(err);
+ break;
+ }
+ case XCB_EXPOSE: {
+ const auto *expose = reinterpret_cast<xcb_expose_event_t *>(event.get());
+ try {
+ logger->debug("Received expose event for window {}", expose->window);
+ const auto window = windows.at(expose->window);
+ window->draw();
+ } catch (const std::out_of_range &oor) {
+ logger->debug("Discarding expose event for window {}", expose->window);
+ }
+ break;
+ }
+ default: {
+ logger->debug("Received unknown event {}", real_event);
+ break;
+ }
+ }
+ event.reset(xcb_poll_for_event(connection));
+ }
+ }
+}
+
+void X11Canvas::add_image(const std::string &identifier, std::unique_ptr<Image> new_image)
+{
+ remove_image(identifier);
+
+ logger->debug("Initializing canvas");
+ images.insert({identifier, std::move(new_image)});
+ image_windows.insert({identifier, {}});
+
+ const auto image = images.at(identifier);
+ const auto dims = image->dimensions();
+ std::unordered_set<xcb_window_t> parent_ids{dims.terminal->x11_wid};
+ get_tmux_window_ids(parent_ids);
+
+ ranges::for_each(parent_ids, [this, &identifier, &image](xcb_window_t parent) {
+ const auto window_id = xcb_generate_id(connection);
+ std::shared_ptr<Window> window;
+#ifdef ENABLE_OPENGL
+ if (egl_available) {
+ try {
+ window = std::make_shared<X11EGLWindow>(connection, screen, window_id, parent, egl.get(), image);
+ } catch (const std::runtime_error &err) {
+ return;
+ }
+ }
+#endif
+ if (window == nullptr) {
+ window = std::make_shared<X11Window>(connection, screen, window_id, parent, image);
+ }
+ windows.insert({window_id, window});
+ image_windows.at(identifier).insert({window_id, window});
+ window->show();
+ });
+
+ draw(identifier);
+}
+
+void X11Canvas::get_tmux_window_ids(std::unordered_set<xcb_window_t> &windows)
+{
+ const auto pids = tmux::get_client_pids();
+ if (!pids.has_value()) {
+ return;
+ }
+ const auto pid_window_map = xutil->get_pid_window_map();
+ for (const auto pid : pids.value()) {
+ const auto ppids = util::get_process_tree(pid);
+ for (const auto ppid : ppids) {
+ const auto win = pid_window_map.find(ppid);
+ if (win == pid_window_map.end()) {
+ continue;
+ }
+ windows.insert(win->second);
+ break; // prevent multiple windows being created
+ }
+ }
+}
+
+void X11Canvas::print_xcb_error(const xcb_generic_error_t *err)
+{
+#ifdef ENABLE_XCB_ERRORS
+ const char *extension = nullptr;
+ const char *major = xcb_errors_get_name_for_major_code(err_ctx, err->major_code);
+ const char *minor = xcb_errors_get_name_for_minor_code(err_ctx, err->major_code, err->minor_code);
+ const char *error = xcb_errors_get_name_for_error(err_ctx, err->error_code, &extension);
+
+ const std::string_view ext_str = extension != nullptr ? extension : "no_extension";
+ const std::string_view minor_str = minor != nullptr ? minor : "no_minor";
+ logger->error("XCB: {}:{}, {}:{}, resource {} sequence {}", error, ext_str, major, minor_str, err->resource_id,
+ err->sequence);
+#else
+ logger->error("XCB: resource {} sequence {}", err->resource_id, err->sequence);
+#endif
+}
+
+void X11Canvas::remove_image(const std::string &identifier)
+{
+ draw_threads.erase(identifier);
+ images.erase(identifier);
+
+ const std::scoped_lock lock{windows_mutex};
+ const auto old_windows = image_windows.extract(identifier);
+ if (old_windows.empty()) {
+ return;
+ }
+ for (const auto &[key, value] : old_windows.mapped()) {
+ windows.erase(key);
+ }
+}
diff --git a/src/canvas/x11/x11.hpp b/src/canvas/x11/x11.hpp
new file mode 100644
index 0000000..09d99a3
--- /dev/null
+++ b/src/canvas/x11/x11.hpp
@@ -0,0 +1,93 @@
+// 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/>.
+
+#ifndef X11_CANVAS_H
+#define X11_CANVAS_H
+
+#include "canvas.hpp"
+#include "image.hpp"
+#include "window.hpp"
+#include "dimensions.hpp"
+#include "util/x11.hpp"
+
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+#include <thread>
+#include <mutex>
+
+#include <xcb/xcb.h>
+#include <spdlog/spdlog.h>
+
+#ifdef ENABLE_XCB_ERRORS
+# include <xcb/xcb_errors.h>
+#endif
+
+#ifdef ENABLE_OPENGL
+# include "util/egl.hpp"
+#endif
+
+class Flags;
+
+class X11Canvas : public Canvas
+{
+public:
+ explicit X11Canvas();
+ ~X11Canvas() override;
+
+ void add_image(const std::string& identifier, std::unique_ptr<Image> new_image) override;
+ void remove_image(const std::string& identifier) override;
+ void hide() override;
+ void show() override;
+
+private:
+ xcb_connection_t *connection;
+ xcb_screen_t *screen;
+
+#ifdef ENABLE_XCB_ERRORS
+ xcb_errors_context_t *err_ctx;
+#endif
+
+ std::unique_ptr<X11Util> xutil;
+
+ // map for event handler
+ std::unordered_map<xcb_window_t, std::shared_ptr<Window>> windows;
+
+ // windows per image
+ std::unordered_map<std::string,
+ std::unordered_map<xcb_window_t, std::shared_ptr<Window>>> image_windows;
+
+ std::unordered_map<std::string, std::shared_ptr<Image>> images;
+ std::unordered_map<std::string, std::jthread> draw_threads;
+
+ std::thread event_handler;
+ std::mutex windows_mutex;
+
+ std::shared_ptr<spdlog::logger> logger;
+ std::shared_ptr<Flags> flags;
+
+#ifdef ENABLE_OPENGL
+ std::unique_ptr<EGLUtil<xcb_connection_t, xcb_window_t>> egl;
+ bool egl_available = true;
+#endif
+
+ void draw(const std::string& identifier);
+ void handle_events();
+ void get_tmux_window_ids(std::unordered_set<xcb_window_t>& windows);
+ void print_xcb_error(const xcb_generic_error_t* err);
+};
+
+#endif
diff --git a/src/dimensions.cpp b/src/dimensions.cpp
new file mode 100644
index 0000000..ecbe851
--- /dev/null
+++ b/src/dimensions.cpp
@@ -0,0 +1,62 @@
+// 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 "dimensions.hpp"
+#include "tmux.hpp"
+#include "terminal.hpp"
+
+#include <utility>
+
+Dimensions::Dimensions(const Terminal* terminal, uint16_t xcoord,
+ uint16_t ycoord, int max_w, int max_h, std::string scaler):
+max_w(max_w),
+max_h(max_h),
+padding_horizontal(terminal->padding_horizontal),
+padding_vertical(terminal->padding_vertical),
+scaler(std::move(scaler)),
+terminal(terminal),
+orig_x(xcoord),
+orig_y(ycoord)
+{
+ read_offsets();
+}
+
+void Dimensions::read_offsets()
+{
+ const auto [offset_x, offset_y] = tmux::get_offset();
+ x = orig_x + offset_x;
+ y = orig_y + offset_y;
+}
+
+auto Dimensions::xpixels() const -> int
+{
+ return x * terminal->font_width;
+}
+
+auto Dimensions::ypixels() const -> int
+{
+ return y * terminal->font_height;
+}
+
+auto Dimensions::max_wpixels() const -> int
+{
+ return max_w * terminal->font_width;
+}
+
+auto Dimensions::max_hpixels() const -> int
+{
+ return max_h * terminal->font_height;
+}
diff --git a/src/flags.cpp b/src/flags.cpp
new file mode 100644
index 0000000..4369794
--- /dev/null
+++ b/src/flags.cpp
@@ -0,0 +1,52 @@
+// 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 "flags.hpp"
+#include "os.hpp"
+#include "util.hpp"
+
+#include <fmt/format.h>
+#include <fstream>
+#include <nlohmann/json.hpp>
+
+namespace fs = std::filesystem;
+using json = nlohmann::json;
+
+// read configuration file
+Flags::Flags()
+{
+ const auto home = os::getenv("HOME").value_or(util::temp_directory_path());
+ const auto config_home = os::getenv("XDG_CONFIG_HOME").value_or(fmt::format("{}/.config", home));
+ config_file = fmt::format("{}/ueberzugpp/config.json", config_home);
+ if (fs::exists(config_file)) {
+ read_config_file();
+ }
+}
+
+void Flags::read_config_file()
+{
+ std::ifstream ifs(config_file);
+ const auto data = json::parse(ifs);
+ if (!data.contains("layer")) {
+ return;
+ }
+ const auto &layer = data.at("layer");
+ silent = layer.value("silent", false);
+ output = layer.value("output", "");
+ no_cache = layer.value("no-cache", false);
+ no_opencv = layer.value("no-opencv", false);
+ use_opengl = layer.value("opengl", false);
+}
diff --git a/src/image.cpp b/src/image.cpp
new file mode 100644
index 0000000..fae3ef6
--- /dev/null
+++ b/src/image.cpp
@@ -0,0 +1,185 @@
+// 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 "image.hpp"
+#ifdef ENABLE_OPENCV
+# include "image/opencv.hpp"
+#endif
+#include "dimensions.hpp"
+#include "flags.hpp"
+#include "image/libvips.hpp"
+#include "util.hpp"
+
+#ifdef ENABLE_OPENCV
+# include <opencv2/imgcodecs.hpp>
+#endif
+#include <spdlog/spdlog.h>
+#include <vips/vips.h>
+
+namespace fs = std::filesystem;
+using njson = nlohmann::json;
+
+auto Image::load(const njson &command, const Terminal *terminal) -> std::unique_ptr<Image>
+{
+ const fs::path &filename = command.at("path");
+ if (!fs::exists(filename)) {
+ return nullptr;
+ }
+ const auto flags = Flags::instance();
+ const auto logger = spdlog::get("main");
+ std::shared_ptr<Dimensions> dimensions;
+ try {
+ dimensions = get_dimensions(command, terminal);
+ } catch (const std::exception &) {
+ logger->error("Could not parse dimensions from command");
+ return nullptr;
+ }
+ std::string image_path = filename;
+ bool in_cache = false;
+ if (!flags->no_cache) {
+ image_path = check_cache(*dimensions, filename);
+ in_cache = image_path != filename;
+ }
+
+#ifdef ENABLE_OPENCV
+ if (cv::haveImageReader(image_path) && !flags->no_opencv) {
+ try {
+ return std::make_unique<OpencvImage>(dimensions, image_path, in_cache);
+ } catch (const std::runtime_error &) {
+ return nullptr;
+ }
+ }
+#endif
+ const auto *vips_loader = vips_foreign_find_load(image_path.c_str());
+ if (vips_loader != nullptr) {
+ try {
+ return std::make_unique<LibvipsImage>(dimensions, image_path, in_cache);
+ } catch (const vips::VError &) {
+ return nullptr;
+ }
+ }
+ return nullptr;
+}
+
+auto Image::check_cache(const Dimensions &dimensions, const fs::path &orig_path) -> std::string
+{
+ const fs::path cache_path = util::get_cache_file_save_location(orig_path);
+ if (!fs::exists(cache_path)) {
+ return orig_path;
+ }
+
+ vips::VImage cache_img;
+ try {
+ cache_img = vips::VImage::new_from_file(cache_path.c_str());
+ } catch (const vips::VError &) {
+ return orig_path;
+ }
+ const uint32_t img_width = cache_img.width();
+ const uint32_t img_height = cache_img.height();
+ const uint32_t dim_width = dimensions.max_wpixels();
+ const uint32_t dim_height = dimensions.max_hpixels();
+ const int delta = 10;
+
+ if ((dim_width >= img_width && dim_height >= img_height) &&
+ ((dim_width - img_width) <= delta || (dim_height - img_height) <= delta)) {
+ return cache_path;
+ }
+ return orig_path;
+}
+
+auto Image::get_new_sizes(double max_width, double max_height, std::string_view scaler, int scale_factor) const
+ -> std::pair<int, int>
+{
+ int img_width = width();
+ int img_height = height();
+ int new_width = img_width;
+ int new_height = img_height;
+ double new_scale = 0;
+ double width_scale = 0;
+ double height_scale = 0;
+ double min_scale = 0;
+ double max_scale = 0;
+
+ if (scaler == "fit_contain" || scaler == "forced_cover") {
+ // I believe these should work the same
+ new_scale = max_height / img_height;
+ if (img_width >= img_height) {
+ new_scale = max_width / img_width;
+ }
+ new_width = static_cast<int>(img_width * new_scale);
+ new_height = static_cast<int>(img_height * new_scale);
+ new_scale = 1;
+ }
+
+ if (new_height > max_height) {
+ if (new_width > max_width) {
+ width_scale = max_width / new_width;
+ height_scale = max_height / new_height;
+ min_scale = std::min(width_scale, height_scale);
+ max_scale = std::max(width_scale, height_scale);
+ if (new_width * max_scale <= max_width && new_height * max_scale <= max_height) {
+ new_scale = max_scale;
+ } else {
+ new_scale = min_scale;
+ }
+ } else {
+ new_scale = max_height / new_height;
+ }
+ } else if (new_width > max_width) {
+ new_scale = max_width / new_width;
+ }
+ if (new_scale != 1) {
+ new_width = static_cast<int>(new_width * new_scale);
+ new_height = static_cast<int>(new_height * new_scale);
+ }
+
+ return std::make_pair(util::round_up(new_width, scale_factor), util::round_up(new_height, scale_factor));
+}
+
+auto Image::get_dimensions(const njson &json, const Terminal *terminal) -> std::shared_ptr<Dimensions>
+{
+ using std::string;
+ int xcoord = 0;
+ int ycoord = 0;
+ int max_width = 0;
+ int max_height = 0;
+ string width_key = "max_width";
+ string height_key = "max_height";
+ const string scaler = json.value("scaler", "contain");
+ if (json.contains("width")) {
+ width_key = "width";
+ height_key = "height";
+ }
+ if (json.at(width_key).is_string()) {
+ const string &width = json.at(width_key);
+ const string &height = json.at(height_key);
+ max_width = std::stoi(width);
+ max_height = std::stoi(height);
+ } else {
+ max_width = json.at(width_key);
+ max_height = json.at(height_key);
+ }
+ if (json.at("x").is_string()) {
+ const string &xcoords = json.at("x");
+ const string &ycoords = json.at("y");
+ xcoord = std::stoi(xcoords);
+ ycoord = std::stoi(ycoords);
+ } else {
+ xcoord = json.at("x");
+ ycoord = json.at("y");
+ }
+ return std::make_shared<Dimensions>(terminal, xcoord, ycoord, max_width, max_height, scaler);
+}
diff --git a/src/image/libvips.cpp b/src/image/libvips.cpp
new file mode 100644
index 0000000..c7ecffc
--- /dev/null
+++ b/src/image/libvips.cpp
@@ -0,0 +1,216 @@
+// 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 "libvips.hpp"
+#include "dimensions.hpp"
+#include "flags.hpp"
+#include "terminal.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <unordered_set>
+
+#ifdef ENABLE_OPENCV
+# include <opencv2/videoio.hpp>
+#endif
+
+using vips::VError;
+using vips::VImage;
+
+LibvipsImage::LibvipsImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache)
+ : path(filename),
+ dims(std::move(new_dims)),
+ max_width(dims->max_wpixels()),
+ max_height(dims->max_hpixels()),
+ in_cache(in_cache)
+{
+ image = VImage::new_from_file(path.c_str()).colourspace(VIPS_INTERPRETATION_sRGB);
+ flags = Flags::instance();
+ logger = spdlog::get("vips");
+ logger->info("loading file {}", filename);
+
+ try {
+ // animated images should have both n-pages and delay
+ npages = image.get_int("n-pages");
+ std::ignore = image.get_array_int("delay");
+ is_anim = true;
+ logger->info("file is an animated image");
+ auto *opts = VImage::option()->set("n", -1);
+ backup = VImage::new_from_file(filename.c_str(), opts).colourspace(VIPS_INTERPRETATION_sRGB);
+ orig_height = backup.height() / npages;
+ image = backup.crop(0, 0, backup.width(), orig_height);
+ } catch (const VError &err) {
+ logger->debug("Failed to process image animation");
+ }
+
+ if (!is_anim) {
+ image = image.autorot();
+ }
+ process_image();
+}
+
+auto LibvipsImage::dimensions() const -> const Dimensions &
+{
+ return *dims;
+}
+
+auto LibvipsImage::filename() const -> std::string
+{
+ return path.string();
+}
+
+auto LibvipsImage::width() const -> int
+{
+ return image.width();
+}
+
+auto LibvipsImage::height() const -> int
+{
+ return image.height();
+}
+
+auto LibvipsImage::size() const -> size_t
+{
+ return _size;
+}
+
+auto LibvipsImage::data() const -> const unsigned char *
+{
+ return _data.get();
+}
+
+auto LibvipsImage::channels() const -> int
+{
+ return image.bands();
+}
+
+auto LibvipsImage::is_animated() const -> bool
+{
+ return is_anim;
+}
+
+auto LibvipsImage::next_frame() -> void
+{
+ if (!is_anim) {
+ return;
+ }
+ top += orig_height;
+ if (top == backup.height()) {
+ top = 0;
+ }
+ image = backup.crop(0, top, backup.width(), orig_height);
+ process_image();
+}
+
+auto LibvipsImage::frame_delay() const -> int
+{
+ if (!is_anim) {
+ return -1;
+ }
+ try {
+ const auto delays = backup.get_array_int("delay");
+ const int ms_per_sec = 1000;
+ if (delays.at(0) == 0) {
+#ifdef ENABLE_OPENCV
+ const cv::VideoCapture video(path);
+ if (video.isOpened()) {
+ return static_cast<int>((1.0 / video.get(cv::CAP_PROP_FPS)) * ms_per_sec);
+ }
+#endif
+ return static_cast<int>((1.0 / npages) * ms_per_sec);
+ }
+ return delays.at(0);
+ } catch (const VError &err) {
+ return -1;
+ }
+}
+
+auto LibvipsImage::resize_image() -> void
+{
+ if (in_cache) {
+ return;
+ }
+ const auto [new_width, new_height] = get_new_sizes(max_width, max_height, dims->scaler, flags->scale_factor);
+ if (new_width <= 0 && new_height <= 0) {
+ // ensure width and height are pair
+ if (flags->needs_scaling) {
+ const auto curw = width();
+ const auto curh = height();
+ if ((curw % 2) != 0 || (curh % 2) != 0) {
+ auto *opts = VImage::option()
+ ->set("height", util::round_up(curh, flags->scale_factor))
+ ->set("size", VIPS_SIZE_FORCE);
+ image = image.thumbnail_image(util::round_up(curw, flags->scale_factor), opts);
+ }
+ }
+ return;
+ }
+
+ logger->debug("Resizing image");
+
+ auto *opts = VImage::option()->set("height", new_height)->set("size", VIPS_SIZE_FORCE);
+ image = image.thumbnail_image(new_width, opts);
+
+ if (is_anim || flags->no_cache) {
+ return;
+ }
+
+ const auto save_location = util::get_cache_file_save_location(path);
+ try {
+ image.write_to_file(save_location.c_str());
+ logger->debug("Saved resized image");
+ } catch (const VError &err) {
+ logger->debug("Could not save resized image");
+ }
+}
+
+auto LibvipsImage::process_image() -> void
+{
+ resize_image();
+ if (flags->origin_center) {
+ const double img_width = static_cast<double>(width()) / dims->terminal->font_width;
+ const double img_height = static_cast<double>(height()) / dims->terminal->font_height;
+ dims->x -= std::floor(img_width / 2);
+ dims->y -= std::floor(img_height / 2);
+ }
+
+ const std::unordered_set<std::string_view> bgra_trifecta = {"x11", "chafa", "wayland"};
+
+#ifdef ENABLE_OPENGL
+ if (flags->use_opengl) {
+ image = image.flipver();
+ }
+#endif
+
+ if (bgra_trifecta.contains(flags->output)) {
+ // alpha channel required
+ if (!image.has_alpha()) {
+ const int alpha_value = 255;
+ image = image.bandjoin(alpha_value);
+ }
+ // convert from RGB to BGR
+ auto bands = image.bandsplit();
+ std::swap(bands[0], bands[2]);
+ image = VImage::bandjoin(bands);
+ } else if (flags->output == "sixel") {
+ // sixel expects RGB888
+ if (image.has_alpha()) {
+ image = image.flatten();
+ }
+ }
+ _size = VIPS_IMAGE_SIZEOF_IMAGE(image.get_image());
+ _data.reset(static_cast<unsigned char *>(image.write_to_memory(&_size)));
+}
diff --git a/src/image/libvips.hpp b/src/image/libvips.hpp
new file mode 100644
index 0000000..76cc731
--- /dev/null
+++ b/src/image/libvips.hpp
@@ -0,0 +1,71 @@
+// 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/>.
+
+#ifndef LIBVIPS_IMAGE_H
+#define LIBVIPS_IMAGE_H
+
+#include "image.hpp"
+#include "util/ptr.hpp"
+
+#include <filesystem>
+#include <spdlog/spdlog.h>
+#include <string>
+#include <vips/vips8>
+
+class LibvipsImage : public Image
+{
+ public:
+ LibvipsImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache);
+
+ [[nodiscard]] auto dimensions() const -> const Dimensions & override;
+ [[nodiscard]] auto width() const -> int override;
+ [[nodiscard]] auto height() const -> int override;
+ [[nodiscard]] auto size() const -> size_t override;
+ [[nodiscard]] auto data() const -> const unsigned char * override;
+ [[nodiscard]] auto channels() const -> int override;
+
+ void next_frame() override;
+ [[nodiscard]] auto frame_delay() const -> int override;
+ [[nodiscard]] auto is_animated() const -> bool override;
+ [[nodiscard]] auto filename() const -> std::string override;
+
+ private:
+ vips::VImage image;
+ vips::VImage backup;
+
+ c_unique_ptr<unsigned char, g_free> _data;
+ std::filesystem::path path;
+ std::shared_ptr<Dimensions> dims;
+
+ std::shared_ptr<Flags> flags;
+ std::shared_ptr<spdlog::logger> logger;
+
+ uint32_t max_width;
+ uint32_t max_height;
+ size_t _size = 0;
+
+ // for animated pictures
+ int top = 0;
+ int orig_height;
+ int npages = 0;
+ bool is_anim = false;
+ bool in_cache;
+
+ void process_image();
+ void resize_image();
+};
+
+#endif
diff --git a/src/image/opencv.cpp b/src/image/opencv.cpp
new file mode 100644
index 0000000..60c2425
--- /dev/null
+++ b/src/image/opencv.cpp
@@ -0,0 +1,249 @@
+// 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 "opencv.hpp"
+#include "dimensions.hpp"
+#include "flags.hpp"
+#include "terminal.hpp"
+#include "util.hpp"
+
+#include <string_view>
+#include <unordered_set>
+
+#include <opencv2/core/ocl.hpp>
+#include <opencv2/imgcodecs.hpp>
+#include <opencv2/imgproc.hpp>
+
+enum {
+ EXIF_ORIENTATION_2 = 2,
+ EXIF_ORIENTATION_3,
+ EXIF_ORIENTATION_4,
+ EXIF_ORIENTATION_5,
+ EXIF_ORIENTATION_6,
+ EXIF_ORIENTATION_7,
+ EXIF_ORIENTATION_8,
+};
+
+OpencvImage::OpencvImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache)
+ : path(filename),
+ dims(std::move(new_dims)),
+ max_width(dims->max_wpixels()),
+ max_height(dims->max_hpixels()),
+ in_cache(in_cache)
+{
+ logger = spdlog::get("opencv");
+ image = cv::imread(filename, cv::IMREAD_UNCHANGED);
+
+ if (image.empty()) {
+ logger->warn("unable to read image");
+ throw std::runtime_error("");
+ }
+ logger->info("loading file {}", filename);
+ flags = Flags::instance();
+
+ rotate_image();
+ process_image();
+}
+
+auto OpencvImage::filename() const -> std::string
+{
+ return path.string();
+}
+
+auto OpencvImage::dimensions() const -> const Dimensions &
+{
+ return *dims;
+}
+
+auto OpencvImage::width() const -> int
+{
+ return image.cols;
+}
+
+auto OpencvImage::height() const -> int
+{
+ return image.rows;
+}
+
+auto OpencvImage::size() const -> size_t
+{
+ return _size;
+}
+
+auto OpencvImage::data() const -> const unsigned char *
+{
+ return image.data;
+}
+
+auto OpencvImage::channels() const -> int
+{
+ return image.channels();
+}
+
+void OpencvImage::wayland_processing()
+{
+ if (flags->output != "wayland") {
+ return;
+ }
+}
+
+void OpencvImage::rotate_image()
+{
+ const auto rotation = util::read_exif_rotation(path);
+ if (!rotation.has_value()) {
+ return;
+ }
+ const auto value = rotation.value();
+
+ // kudos https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
+ switch (value) {
+ case EXIF_ORIENTATION_2:
+ cv::flip(image, image, 1);
+ break;
+ case EXIF_ORIENTATION_3:
+ cv::flip(image, image, -1);
+ break;
+ case EXIF_ORIENTATION_4:
+ cv::flip(image, image, 0);
+ break;
+ case EXIF_ORIENTATION_5:
+ cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE);
+ cv::flip(image, image, 1);
+ break;
+ case EXIF_ORIENTATION_6:
+ cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE);
+ break;
+ case EXIF_ORIENTATION_7:
+ cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE);
+ cv::flip(image, image, 1);
+ break;
+ case EXIF_ORIENTATION_8:
+ cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE);
+ break;
+ default:
+ break;
+ }
+}
+
+// only use opencl if required
+auto OpencvImage::resize_image() -> void
+{
+ if (in_cache) {
+ return;
+ }
+ const auto [new_width, new_height] = get_new_sizes(max_width, max_height, dims->scaler, flags->scale_factor);
+ if (new_width <= 0 && new_height <= 0) {
+ // ensure width and height are pair
+ if (flags->needs_scaling) {
+ const auto curw = width();
+ const auto curh = height();
+ if ((curw % 2) != 0 || (curh % 2) != 0) {
+ resize_image_helper(image, util::round_up(curw, flags->scale_factor),
+ util::round_up(curh, flags->scale_factor));
+ }
+ }
+ return;
+ }
+
+ const auto opencl_ctx = cv::ocl::Context::getDefault();
+ opencl_available = opencl_ctx.ptr() != nullptr;
+
+ if (opencl_available) {
+ logger->debug("OpenCL is available");
+ image.copyTo(uimage);
+ resize_image_helper(uimage, new_width, new_height);
+ uimage.copyTo(image);
+ } else {
+ resize_image_helper(image, new_width, new_height);
+ }
+}
+
+void OpencvImage::resize_image_helper(cv::InputOutputArray &mat, int new_width, int new_height)
+{
+ logger->debug("Resizing image");
+ cv::resize(mat, mat, cv::Size(new_width, new_height), 0, 0, cv::INTER_AREA);
+
+ if (flags->no_cache) {
+ logger->debug("Caching is disabled");
+ return;
+ }
+
+ const auto save_location = util::get_cache_file_save_location(path);
+ try {
+ cv::imwrite(save_location, mat);
+ logger->debug("Saved resized image");
+ } catch (const cv::Exception &ex) {
+ logger->error("Could not save image");
+ }
+}
+
+void OpencvImage::process_image()
+{
+ resize_image();
+ if (flags->origin_center) {
+ const double img_width = static_cast<double>(width()) / dims->terminal->font_width;
+ const double img_height = static_cast<double>(height()) / dims->terminal->font_height;
+ dims->x -= std::floor(img_width / 2);
+ dims->y -= std::floor(img_height / 2);
+ }
+
+ const std::unordered_set<std::string_view> bgra_trifecta = {"x11", "chafa", "wayland"};
+
+ if (image.depth() == CV_16U) {
+ const float alpha = 0.00390625; // 1 / 256
+ image.convertTo(image, CV_8U, alpha);
+ }
+
+ if (image.channels() == 4) {
+ // premultiply alpha
+ image.forEach<cv::Vec4b>([](cv::Vec4b &pix, const int *) {
+ const uint8_t alpha = pix[3];
+ const uint8_t div = 255;
+ pix[0] = (pix[0] * alpha) / div;
+ pix[1] = (pix[1] * alpha) / div;
+ pix[2] = (pix[2] * alpha) / div;
+ });
+ }
+
+#ifdef ENABLE_OPENGL
+ if (flags->use_opengl) {
+ cv::flip(image, image, 0);
+ }
+#endif
+
+ if (image.channels() == 1) {
+ cv::cvtColor(image, image, cv::COLOR_GRAY2BGRA);
+ }
+
+ if (bgra_trifecta.contains(flags->output)) {
+ if (image.channels() == 3) {
+ cv::cvtColor(image, image, cv::COLOR_BGR2BGRA);
+ }
+ } else if (flags->output == "kitty") {
+ if (image.channels() == 4) {
+ cv::cvtColor(image, image, cv::COLOR_BGRA2RGBA);
+ } else {
+ cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
+ }
+ } else if (flags->output == "sixel") {
+ if (image.channels() == 4) {
+ cv::cvtColor(image, image, cv::COLOR_BGRA2RGB);
+ } else {
+ cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
+ }
+ }
+ _size = image.total() * image.elemSize();
+}
diff --git a/src/image/opencv.hpp b/src/image/opencv.hpp
new file mode 100644
index 0000000..6214abc
--- /dev/null
+++ b/src/image/opencv.hpp
@@ -0,0 +1,68 @@
+// 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/>.
+
+#ifndef OPENCV_IMAGE_H
+#define OPENCV_IMAGE_H
+
+#include "image.hpp"
+
+#include <filesystem>
+#include <opencv2/core.hpp>
+#include <spdlog/spdlog.h>
+#include <string>
+
+namespace fs = std::filesystem;
+
+class OpencvImage : public Image
+{
+ public:
+ OpencvImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache);
+ ~OpencvImage() override = default;
+
+ [[nodiscard]] auto dimensions() const -> const Dimensions & override;
+ [[nodiscard]] auto width() const -> int override;
+ [[nodiscard]] auto height() const -> int override;
+ [[nodiscard]] auto size() const -> size_t override;
+ [[nodiscard]] auto data() const -> const unsigned char * override;
+ [[nodiscard]] auto channels() const -> int override;
+
+ [[nodiscard]] auto filename() const -> std::string override;
+
+ private:
+ cv::Mat image;
+ cv::UMat uimage;
+
+ fs::path path;
+ std::shared_ptr<Dimensions> dims;
+
+ uint64_t _size = 0;
+ uint32_t max_width;
+ uint32_t max_height;
+ bool in_cache;
+ bool opencl_available = false;
+
+ std::shared_ptr<spdlog::logger> logger;
+ std::shared_ptr<Flags> flags;
+
+ void process_image();
+ void resize_image();
+ void resize_image_helper(cv::InputOutputArray &mat, int new_width, int new_height);
+
+ void rotate_image();
+ void wayland_processing();
+};
+
+#endif
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..dbc83a8
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,151 @@
+// 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 <csignal>
+
+#include <CLI/App.hpp>
+#include <CLI/Config.hpp>
+#include <CLI/Formatter.hpp>
+#include <spdlog/cfg/env.h>
+#include <spdlog/spdlog.h>
+
+#include <CLI/Version.hpp>
+#if (CLI11_VERSION_MAJOR >= 3) || (CLI11_VERSION_MAJOR == 2 && CLI11_VERSION_MINOR >= 6)
+// CLI11 2.6.0 and beyond requires this header file for the CLI::IsMember validator.
+# include <CLI/ExtraValidators.hpp>
+#endif
+
+#include "application.hpp"
+#include "flags.hpp"
+#include "tmux.hpp"
+#include "util.hpp"
+
+void signal_handler(const int signal)
+{
+ Application::stop_flag = true;
+
+ const auto logger = spdlog::get("main");
+ if (!logger) {
+ return;
+ }
+ switch (signal) {
+ case SIGINT:
+ logger->error("SIGINT received, exiting...");
+ break;
+ case SIGTERM:
+ logger->error("SIGTERM received, exiting...");
+ break;
+ case SIGHUP:
+ logger->error("SIGHUP received, exiting...");
+ break;
+ default:
+ logger->error("UNKNOWN({}) signal received, exiting...", signal);
+ break;
+ }
+}
+
+auto main(int argc, char *argv[]) -> int
+{
+ // handle signals
+ struct sigaction sga;
+ sga.sa_handler = signal_handler;
+ sigemptyset(&sga.sa_mask);
+ sga.sa_flags = 0;
+ sigaction(SIGINT, &sga, nullptr);
+ sigaction(SIGTERM, &sga, nullptr);
+ sigaction(SIGHUP, &sga, nullptr);
+ sigaction(SIGCHLD, nullptr, nullptr);
+
+ spdlog::cfg::load_env_levels();
+
+ std::shared_ptr<Flags> flags;
+ try {
+ flags = Flags::instance();
+ } catch (const std::exception &e) {
+ std::cerr << "Could not parse config file: " << e.what() << std::endl;
+ return 1;
+ }
+
+ CLI::App program("Display images in the terminal", "ueberzug");
+ program.add_flag("-V,--version", flags->print_version, "Print version information.");
+
+ auto *layer_command = program.add_subcommand("layer", "Display images on the terminal.");
+ layer_command->add_flag("-s,--silent", flags->silent, "Print stderr to /dev/null.");
+ layer_command
+ ->add_flag("--use-escape-codes", flags->use_escape_codes, "Use escape codes to get terminal capabilities.")
+ ->default_val(false);
+ layer_command->add_option("--pid-file", flags->pid_file, "Output file where to write the daemon PID.");
+ layer_command->add_flag("--no-stdin", flags->no_stdin, "Do not listen on stdin for commands.")->needs("--pid-file");
+ layer_command->add_flag("--no-cache", flags->no_cache, "Disable caching of resized images.");
+ layer_command->add_flag("--no-opencv", flags->no_opencv, "Do not use OpenCV, use Libvips instead.");
+ layer_command->add_option("-o,--output", flags->output, "Image output method")
+ ->check(CLI::IsMember({"x11", "wayland", "sixel", "kitty", "iterm2", "chafa"}));
+ layer_command->add_flag("--origin-center", flags->origin_center, "Location of the origin wrt the image");
+ layer_command->add_option("-p,--parser", nullptr, "**UNUSED**, only present for backwards compatibility.");
+ layer_command->add_option("-l,--loader", nullptr, "**UNUSED**, only present for backwards compatibility.");
+
+ auto *cmd_comand = program.add_subcommand("cmd", "Send a command to a running ueberzugpp instance.");
+ cmd_comand->add_option("-s,--socket", flags->cmd_socket, "UNIX socket of running instance");
+ cmd_comand->add_option("-i,--identifier", flags->cmd_id, "Preview identifier");
+ cmd_comand->add_option("-a,--action", flags->cmd_action, "Action to send");
+ cmd_comand->add_option("-f,--file", flags->cmd_file_path, "Path of image file");
+ cmd_comand->add_option("-x,--xpos", flags->cmd_x, "X position of preview");
+ cmd_comand->add_option("-y,--ypos", flags->cmd_y, "Y position of preview");
+ cmd_comand->add_option("--max-width", flags->cmd_max_width, "Max width of preview");
+ cmd_comand->add_option("--max-height", flags->cmd_max_height, "Max height of preview");
+
+ auto *tmux_command = program.add_subcommand("tmux", "Handle tmux hooks. Used internaly.");
+ tmux_command->allow_extras();
+
+ auto *query_win_command =
+ program.add_subcommand("query_windows", "**UNUSED**, only present for backwards compatibility.");
+ query_win_command->allow_extras();
+
+ CLI11_PARSE(program, argc, argv);
+
+ if (query_win_command->parsed()) {
+ return 0;
+ }
+
+ if (flags->print_version) {
+ Application::print_version();
+ return 0;
+ }
+
+ if (!layer_command->parsed() && !tmux_command->parsed() && !cmd_comand->parsed()) {
+ program.exit(CLI::CallForHelp());
+ return 1;
+ }
+
+ if (layer_command->parsed()) {
+ Application application(argv[0]);
+ application.command_loop();
+ }
+
+ if (tmux_command->parsed()) {
+ try {
+ const auto positionals = tmux_command->remaining();
+ tmux::handle_hook(positionals.at(0), std::stoi(positionals.at(1)));
+ } catch (const std::out_of_range &oor) {
+ }
+ }
+
+ if (cmd_comand->parsed()) {
+ util::send_command(*flags);
+ }
+
+ return 0;
+}
diff --git a/src/os.cpp b/src/os.cpp
new file mode 100644
index 0000000..af37a0e
--- /dev/null
+++ b/src/os.cpp
@@ -0,0 +1,126 @@
+// 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 "os.hpp"
+#include "util/ptr.hpp"
+
+#include <array>
+#include <cstdlib>
+#include <system_error>
+
+#include <cerrno>
+#include <cstdio>
+#include <poll.h>
+#include <unistd.h>
+
+auto os::exec(const std::string &cmd) -> std::string
+{
+ const int bufsize = 128;
+ std::array<char, bufsize> buffer{};
+ std::string result;
+ const c_unique_ptr<FILE, &pclose> pipe{popen(cmd.c_str(), "r")};
+ if (!pipe) {
+ throw std::system_error(errno, std::generic_category());
+ }
+ while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
+ result.append(buffer.data());
+ }
+ if (!result.empty()) {
+ result.erase(result.length() - 1);
+ }
+ return result;
+}
+
+auto os::read_data_from_fd(int filde, char sep) -> std::string
+{
+ using std::errc;
+ std::string response;
+ char readch = 0;
+
+ while (true) {
+ const auto status = read(filde, &readch, 1);
+ if (status == -1) {
+ throw std::system_error(errno, std::system_category());
+ }
+ if (status == 0 || readch == sep) {
+ if (response.empty()) {
+ throw std::system_error(EIO, std::system_category());
+ }
+ break;
+ }
+ response.push_back(readch);
+ }
+ return response;
+}
+
+auto os::read_data_from_stdin(char sep) -> std::string
+{
+ return read_data_from_fd(STDIN_FILENO, sep);
+}
+
+auto os::wait_for_data_on_fd(int filde, int waitms) -> bool
+{
+ struct pollfd fds;
+ fds.fd = filde;
+ fds.events = POLLIN;
+
+ poll(&fds, 1, waitms);
+
+ if ((fds.revents & (POLLERR | POLLNVAL | POLLHUP)) != 0) {
+ throw std::system_error(EIO, std::generic_category());
+ }
+
+ return (fds.revents & POLLIN) != 0;
+}
+
+auto os::wait_for_data_on_stdin(int waitms) -> bool
+{
+ return wait_for_data_on_fd(STDIN_FILENO, waitms);
+}
+
+auto os::getenv(const std::string &var) -> std::optional<std::string>
+{
+ const char *env_p = std::getenv(var.c_str()); // NOLINT
+ if (env_p == nullptr) {
+ return {};
+ }
+ return env_p;
+}
+
+auto os::get_pid() -> int
+{
+ return getpid();
+}
+
+auto os::get_ppid() -> int
+{
+ return getppid();
+}
+
+void os::daemonize()
+{
+ const int pid = fork();
+ if (pid < 0) {
+ std::exit(EXIT_FAILURE); // NOLINT
+ }
+ if (pid > 0) {
+ std::exit(EXIT_SUCCESS); // NOLINT
+ }
+ const int status = setsid();
+ if (status < 0) {
+ std::exit(EXIT_FAILURE); // NOLINT
+ }
+}
diff --git a/src/process/apple.cpp b/src/process/apple.cpp
new file mode 100644
index 0000000..bf9f831
--- /dev/null
+++ b/src/process/apple.cpp
@@ -0,0 +1,43 @@
+// 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 "os.hpp"
+#include "process.hpp"
+#include "tmux.hpp"
+#include "util.hpp"
+
+#include <fmt/format.h>
+#include <libproc.h>
+#include <sys/types.h>
+
+Process::Process(int pid)
+ : pid(pid)
+{
+ struct proc_bsdshortinfo sproc;
+ struct proc_bsdinfo proc;
+
+ int status = proc_pidinfo(pid, PROC_PIDT_SHORTBSDINFO, 0, &sproc, PROC_PIDT_SHORTBSDINFO_SIZE);
+ if (status == PROC_PIDT_SHORTBSDINFO_SIZE) {
+ ppid = static_cast<int>(sproc.pbsi_ppid);
+ }
+
+ status = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &proc, PROC_PIDTBSDINFO_SIZE);
+ if (status == PROC_PIDTBSDINFO_SIZE) {
+ tty_nr = static_cast<int>(proc.e_tdev);
+ minor_dev = minor(tty_nr);
+ pty_path = fmt::format("/dev/ttys{:0>3}", minor_dev);
+ }
+}
diff --git a/src/process/linux.cpp b/src/process/linux.cpp
new file mode 100644
index 0000000..9dbe05c
--- /dev/null
+++ b/src/process/linux.cpp
@@ -0,0 +1,39 @@
+// 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 "process.hpp"
+
+#include <fmt/format.h>
+#include <fstream>
+#include <limits>
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+# include <sys/types.h>
+#else
+# include <sys/sysmacros.h>
+#endif
+
+constexpr auto max_size = std::numeric_limits<std::streamsize>::max();
+
+Process::Process(int pid)
+ : pid(pid)
+{
+ const auto stat = fmt::format("/proc/{}/stat", pid);
+ std::ifstream ifs(stat);
+ ifs.ignore(max_size, ')'); // skip pid and executable name
+ ifs >> state >> ppid >> process_group_id >> session_id >> tty_nr;
+ minor_dev = minor(tty_nr); // NOLINT
+ pty_path = fmt::format("/dev/pts/{}", minor_dev);
+}
diff --git a/src/terminal.cpp b/src/terminal.cpp
new file mode 100644
index 0000000..e093238
--- /dev/null
+++ b/src/terminal.cpp
@@ -0,0 +1,335 @@
+// 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 "terminal.hpp"
+#include "flags.hpp"
+#include "os.hpp"
+#include "process.hpp"
+#include "tmux.hpp"
+#include "util.hpp"
+#ifdef ENABLE_X11
+# include "util/x11.hpp"
+#endif
+#ifdef ENABLE_WAYLAND
+# include "canvas/wayland/config.hpp"
+#endif
+
+#include <cmath>
+#include <iostream>
+#include <unordered_set>
+
+#include <fcntl.h>
+#include <range/v3/all.hpp>
+#include <spdlog/spdlog.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <system_error>
+#include <unistd.h>
+
+Terminal::Terminal()
+ : terminal_pid(pid)
+{
+ flags = Flags::instance();
+ logger = spdlog::get("terminal");
+ term = os::getenv("TERM").value_or("xterm-256color");
+ term_program = os::getenv("TERM_PROGRAM").value_or("");
+ logger->info("TERM = {}", term);
+ if (!term_program.empty()) {
+ logger->info("TERM_PROGRAM = {}", term_program);
+ }
+ open_first_pty();
+ get_terminal_size();
+ set_detected_output();
+}
+
+Terminal::~Terminal()
+{
+ if (pty_fd > 0) {
+ close(pty_fd);
+ }
+}
+
+void Terminal::get_terminal_size()
+{
+ struct winsize size;
+ ioctl(pty_fd, TIOCGWINSZ, &size);
+ cols = size.ws_col;
+ rows = size.ws_row;
+ xpixel = size.ws_xpixel;
+ ypixel = size.ws_ypixel;
+ logger->debug("ioctl sizes: COLS={} ROWS={} XPIXEL={} YPIXEL={}", cols, rows, xpixel, ypixel);
+
+ check_iterm2_support();
+ if (flags->use_escape_codes) {
+ init_termios();
+ if (xpixel == 0 || ypixel == 0) {
+ get_terminal_size_escape_code();
+ }
+ if (xpixel == 0 || ypixel == 0) {
+ get_terminal_size_xtsm();
+ }
+ check_sixel_support();
+ check_kitty_support();
+ reset_termios();
+ }
+
+ get_fallback_x11_terminal_sizes();
+ get_fallback_wayland_terminal_sizes();
+
+ if (xpixel == 0 || ypixel == 0) {
+ xpixel = fallback_xpixel;
+ ypixel = fallback_ypixel;
+ }
+ if (xpixel == 0 || ypixel == 0) {
+ throw std::runtime_error("Unable to calculate terminal sizes");
+ }
+
+ const double padding_horiz = guess_padding(cols, xpixel);
+ const double padding_vert = guess_padding(rows, ypixel);
+
+ padding_horizontal = static_cast<uint16_t>(std::max(padding_horiz, padding_vert));
+ padding_vertical = padding_horizontal;
+ font_width = static_cast<uint16_t>(
+ std::floor(guess_font_size(cols, static_cast<float>(xpixel), static_cast<float>(padding_horizontal))));
+ font_height = static_cast<uint16_t>(
+ std::floor(guess_font_size(rows, static_cast<float>(ypixel), static_cast<float>(padding_vertical))));
+
+ if (xpixel < fallback_xpixel && ypixel < fallback_ypixel) {
+ padding_horizontal = static_cast<uint16_t>((fallback_xpixel - xpixel) / 2);
+ padding_vertical = static_cast<uint16_t>((fallback_ypixel - ypixel) / 2);
+ font_width = static_cast<uint16_t>(xpixel / cols);
+ font_height = static_cast<uint16_t>(ypixel / rows);
+ }
+
+ logger->debug("padding_horiz={} padding_vert={}", padding_horizontal, padding_vertical);
+ logger->debug("font_width={} font_height={}", font_width, font_height);
+}
+
+void Terminal::set_detected_output()
+{
+ if (supports_sixel) {
+ detected_output = "sixel";
+ }
+ if (supports_kitty) {
+ detected_output = "kitty";
+ }
+ if (supports_iterm2) {
+ detected_output = "iterm2";
+ }
+ if (supports_x11) {
+ detected_output = "x11";
+ }
+ if (supports_wayland) {
+ detected_output = "wayland";
+ }
+ if (flags->output.empty()) {
+ if (detected_output.empty()) {
+ flags->output = "chafa";
+ } else {
+ flags->output = detected_output;
+ }
+ }
+}
+
+auto Terminal::guess_padding(uint16_t chars, double pixels) -> double
+{
+ const double font_size = std::floor(pixels / chars);
+ return (pixels - font_size * chars) / 2;
+}
+
+auto Terminal::guess_font_size(uint16_t chars, float pixels, float padding) -> float
+{
+ return (pixels - 2 * padding) / static_cast<float>(chars);
+}
+
+void Terminal::get_terminal_size_escape_code()
+{
+ const auto resp = read_raw_str("\033[14t").erase(0, 4);
+ if (resp.empty()) {
+ return;
+ }
+ const auto sizes = util::str_split(resp, ";");
+ try {
+ ypixel = std::stoi(sizes[0]);
+ xpixel = std::stoi(sizes[1]);
+ } catch (const std::invalid_argument &) {
+ logger->debug("Got unexpected values in get_terminal_size_escape_code");
+ }
+ // some old vte terminals respond to these values in a different order
+ // assume everything older than 7000 is broken
+ const auto vte_ver_str = os::getenv("VTE_VERSION").value_or("");
+ if (!vte_ver_str.empty()) {
+ const auto vte_ver = std::stoi(vte_ver_str);
+ const auto working_ver = 7000;
+ if (vte_ver <= working_ver) {
+ std::swap(ypixel, xpixel);
+ }
+ }
+ logger->debug("ESC sizes XPIXEL={} YPIXEL={}", xpixel, ypixel);
+}
+
+void Terminal::get_terminal_size_xtsm()
+{
+ const auto resp = read_raw_str("\033[?2;1;0S").erase(0, 3);
+ if (resp.empty()) {
+ return;
+ }
+ const auto sizes = util::str_split(resp, ";");
+ if (sizes.size() != 4) {
+ return;
+ }
+ try {
+ ypixel = std::stoi(sizes[3]);
+ xpixel = std::stoi(sizes[2]);
+ } catch (const std::invalid_argument &) {
+ logger->debug("Got unexpected values in get_terminal_size_xtsm");
+ }
+ logger->debug("XTSM sizes XPIXEL={} YPIXEL={}", xpixel, ypixel);
+}
+
+void Terminal::check_sixel_support()
+{
+ // some terminals support sixel but don't respond to escape sequences
+ const auto supported_terms = std::unordered_set<std::string_view>{"yaft-256color", "iTerm.app"};
+ const auto resp = read_raw_str("\033[?1;1;0S").erase(0, 3);
+ const auto vals = util::str_split(resp, ";");
+ if (vals.size() > 2 || supported_terms.contains(term) || supported_terms.contains(term_program)) {
+ supports_sixel = true;
+ logger->debug("sixel is supported");
+ } else {
+ logger->debug("sixel is not supported");
+ }
+}
+
+void Terminal::check_kitty_support()
+{
+ const auto resp = read_raw_str("\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\\033[c");
+ if (resp.find("OK") != std::string::npos) {
+ supports_kitty = true;
+ logger->debug("kitty is supported");
+ } else {
+ logger->debug("kitty is not supported");
+ }
+}
+
+void Terminal::check_iterm2_support()
+{
+ const auto supported_terms = std::unordered_set<std::string_view>{"WezTerm", "iTerm.app"};
+ if (supported_terms.contains(term_program)) {
+ supports_iterm2 = true;
+ logger->debug("iterm2 is supported");
+ } else {
+ logger->debug("iterm2 is not supported");
+ }
+}
+
+auto Terminal::read_raw_str(const std::string_view esc) -> std::string
+{
+ const auto waitms = 100;
+ std::cout << esc << std::flush;
+ try {
+ const auto in_event = os::wait_for_data_on_stdin(waitms);
+ if (!in_event) {
+ return "";
+ }
+ return os::read_data_from_stdin(esc.back());
+ } catch (const std::system_error &) {
+ return "";
+ }
+}
+
+void Terminal::init_termios()
+{
+ tcgetattr(0, &old_term); /* grab old terminal i/o settings */
+ new_term = old_term; /* make new settings same as old settings */
+ new_term.c_lflag &= ~ICANON; /* disable buffered i/o */
+ new_term.c_lflag &= ~ECHO; /* set echo mode */
+ tcsetattr(0, TCSANOW, &new_term); /* use these new terminal i/o settings now */
+}
+
+void Terminal::reset_termios() const
+{
+ tcsetattr(0, TCSANOW, &old_term);
+}
+
+void Terminal::get_fallback_x11_terminal_sizes()
+{
+#ifdef ENABLE_X11
+ const auto xutil = X11Util();
+ if (xutil.connected) {
+ supports_x11 = true;
+ logger->debug("X11 is supported");
+ } else {
+ logger->debug("x11 is not supported");
+ return;
+ }
+ x11_wid = xutil.get_parent_window(terminal_pid);
+ logger->debug("Using fallback X11 window id {}", x11_wid);
+ const auto [xpix, ypix] = xutil.get_window_dimensions(x11_wid);
+ fallback_xpixel = xpix;
+ fallback_ypixel = ypix;
+ logger->debug("X11 sizes: XPIXEL={} YPIXEL={}", fallback_xpixel, fallback_ypixel);
+#endif
+}
+
+void Terminal::get_fallback_wayland_terminal_sizes()
+{
+#ifdef ENABLE_WAYLAND
+ const auto config = WaylandConfig::get();
+ if (!config->is_dummy()) {
+ supports_wayland = true;
+ logger->debug("Wayland is supported.");
+ } else {
+ logger->debug("Wayland is not supported");
+ return;
+ }
+ const auto window = config->get_window_info();
+ fallback_xpixel = static_cast<uint16_t>(window.width);
+ fallback_ypixel = static_cast<uint16_t>(window.height);
+#endif
+}
+
+void Terminal::open_first_pty()
+{
+ const auto pids = tmux::get_client_pids().value_or(std::vector<int>{pid});
+
+ struct stat stat_info;
+ for (const auto spid : pids) {
+ auto tree = util::get_process_tree_v2(spid);
+ ranges::reverse(tree);
+ for (const auto &proc : tree) {
+ const int stat_res = stat(proc.pty_path.c_str(), &stat_info);
+ if (stat_res == -1) {
+ const auto err = std::error_code(errno, std::generic_category());
+ logger->debug("stat failed ({}) for pty {}, pid {}, ignoring", err.message(), proc.pty_path, proc.pid);
+ continue;
+ }
+ if (proc.tty_nr == static_cast<int>(stat_info.st_rdev)) {
+ pty_fd = open(proc.pty_path.c_str(), O_RDONLY | O_NOCTTY);
+ if (pty_fd == -1) {
+ const auto err = std::error_code(errno, std::generic_category());
+ logger->debug("could not open pty {}, {}, ignoring", proc.pty_path, err.message());
+ continue;
+ }
+ terminal_pid = proc.pid;
+ logger->info("PTY = {}", proc.pty_path);
+ return;
+ }
+ }
+ }
+ logger->warn("Could not open pty, using stdout as fallback");
+ pty_fd = STDOUT_FILENO;
+}
diff --git a/src/tmux.cpp b/src/tmux.cpp
new file mode 100644
index 0000000..b919d7d
--- /dev/null
+++ b/src/tmux.cpp
@@ -0,0 +1,142 @@
+// 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 "tmux.hpp"
+#include "os.hpp"
+#include "util.hpp"
+
+#include <array>
+#include <fmt/format.h>
+#include <string>
+#include <utility>
+
+constexpr auto session_hooks = std::to_array<std::string_view>(
+ {"session-window-changed", "client-detached", "window-layout-changed"});
+
+constexpr auto global_hooks = std::to_array<std::string_view>(
+ {"client-session-changed"});
+
+auto tmux::get_session_id() -> std::string
+{
+ const auto cmd = fmt::format("tmux display -p -F '#{{session_id}}' -t {}", tmux::get_pane());
+ return os::exec(cmd);
+}
+
+auto tmux::is_used() -> bool
+{
+ return !tmux::get_pane().empty();
+}
+
+auto tmux::is_window_focused() -> bool
+{
+ const auto cmd = fmt::format("tmux display -p -F '#{{session_attached}},#{{window_active}},#{{pane_in_mode}}' -t {}", tmux::get_pane());
+ return os::exec(cmd) == "1,1,0";
+}
+
+auto tmux::get_pane() -> std::string
+{
+ return os::getenv("TMUX_PANE").value_or("");
+}
+
+auto tmux::get_client_pids() -> std::optional<std::vector<int>>
+{
+ if (!tmux::is_used()) {
+ return {};
+ }
+ if (!tmux::is_window_focused()) {
+ return {};
+ }
+
+ std::vector<int> pids;
+ const auto cmd = fmt::format("tmux list-clients -F '#{{client_pid}}' -t {}", tmux::get_pane());
+ const auto output = os::exec(cmd);
+
+ for (const auto &line : util::str_split(output, "\n")) {
+ pids.push_back(std::stoi(line));
+ }
+
+ return pids;
+}
+
+auto tmux::get_offset() -> std::pair<int, int>
+{
+ if (!tmux::is_used()) {
+ return std::make_pair(0, 0);
+ }
+ const auto [p_x, p_y] = tmux::get_pane_offset();
+ const auto s_y = tmux::get_statusbar_offset();
+ return std::make_pair(p_x, p_y + s_y);
+}
+
+auto tmux::get_pane_offset() -> std::pair<int, int>
+{
+ const auto cmd = fmt::format(R"(tmux display -p -F '#{{pane_top}},#{{pane_left}},
+ #{{pane_bottom}},#{{pane_right}},
+ #{{window_height}},#{{window_width}}' -t {})",
+ tmux::get_pane());
+ const auto output = util::str_split(os::exec(cmd), ",");
+ return std::make_pair(std::stoi(output.at(1)), std::stoi(output.at(0)));
+}
+
+auto tmux::get_statusbar_offset() -> int
+{
+ const std::string cmd = "tmux display -p '#{status},#{status-position}'";
+ const auto output = util::str_split(os::exec(cmd), ",");
+ if (output.at(1) != "top" || output.at(0) == "off") {
+ return 0;
+ }
+ if (output.at(0) == "on") {
+ return 1;
+ }
+ return std::stoi(output.at(0));
+}
+
+void tmux::handle_hook(const std::string_view hook, int pid)
+{
+ const auto msg = fmt::format(R"({{"action":"tmux","hook":"{}"}})", hook);
+ const auto endpoint = util::get_socket_path(pid);
+ util::send_socket_message(msg, endpoint);
+}
+
+void tmux::register_hooks()
+{
+ if (!tmux::is_used()) {
+ return;
+ }
+ for (const auto &hook : session_hooks) {
+ std::string cmd = fmt::format(R"(tmux set-hook -t {0} {1} "run-shell 'ueberzugpp tmux {1} {2}'")", tmux::get_pane(), hook, os::get_pid());
+ os::exec(cmd);
+ }
+ for (const auto &hook : global_hooks) {
+ std::string cmd = fmt::format(R"(tmux set-hook -g {0} "run-shell 'ueberzugpp tmux {0} {1}'")", hook, os::get_pid());
+ os::exec(cmd);
+ }
+}
+
+void tmux::unregister_hooks()
+{
+ if (!tmux::is_used()) {
+ return;
+ }
+ for (const auto &hook : session_hooks) {
+ const auto cmd = fmt::format("tmux set-hook -u -t {} {}", tmux::get_pane(), hook);
+ os::exec(cmd);
+ }
+ for (const auto &hook : global_hooks) {
+ const auto cmd = fmt::format("tmux set-hook -gu {}", hook);
+ os::exec(cmd);
+ }
+}
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;
+}