summaryrefslogtreecommitdiff
path: root/src/canvas/x11
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/canvas/x11
Diffstat (limited to 'src/canvas/x11')
-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
6 files changed, 735 insertions, 0 deletions
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