From 83d5ba42f8a98fc0df177efef4333f74a0481f61 Mon Sep 17 00:00:00 2001 From: pants Date: Mon, 21 Sep 2026 14:18:06 -0700 Subject: init --- src/canvas/x11/window/x11.cpp | 119 ++++++++++++++++++ src/canvas/x11/window/x11.hpp | 60 +++++++++ src/canvas/x11/window/x11egl.cpp | 138 +++++++++++++++++++++ src/canvas/x11/window/x11egl.hpp | 68 +++++++++++ src/canvas/x11/x11.cpp | 257 +++++++++++++++++++++++++++++++++++++++ src/canvas/x11/x11.hpp | 93 ++++++++++++++ 6 files changed, 735 insertions(+) create mode 100644 src/canvas/x11/window/x11.cpp create mode 100644 src/canvas/x11/window/x11.hpp create mode 100644 src/canvas/x11/window/x11egl.cpp create mode 100644 src/canvas/x11/window/x11egl.hpp create mode 100644 src/canvas/x11/x11.cpp create mode 100644 src/canvas/x11/x11.hpp (limited to 'src/canvas/x11') 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 . + +#include "x11.hpp" +#include "dimensions.hpp" + +#include + +#include + +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) + : 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(dimensions.xpixels() + dimensions.padding_horizontal); + const auto ycoord = static_cast(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(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 buffer; + auto *event = reinterpret_cast(buffer.data()); + event->response_type = XCB_EXPOSE; + event->window = window; + xcb_send_event(connection, 0, window, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast(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 . + +#ifndef X11_WINDOW_H +#define X11_WINDOW_H + +#include "image.hpp" +#include "util/ptr.hpp" +#include "window.hpp" + +#include +#include + +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); + ~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; + std::shared_ptr logger; + std::shared_ptr 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 . + +#include "x11egl.hpp" +#include "dimensions.hpp" +#include "image.hpp" +#include "util.hpp" + +#include + +X11EGLWindow::X11EGLWindow(xcb_connection_t *connection, xcb_screen_t *screen, xcb_window_t windowid, + xcb_window_t parentid, const EGLUtil *egl, + std::shared_ptr 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(dimensions.xpixels() + dimensions.padding_horizontal); + const auto ycoord = static_cast(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 buffer; + auto *event = reinterpret_cast(buffer.data()); + event->response_type = XCB_EXPOSE; + event->window = windowid; + xcb_send_event(connection, 0, windowid, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast(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 . + +#ifndef X11_EGL_WINDOW_H +#define X11_EGL_WINDOW_H + +#include "window.hpp" +#include "util/egl.hpp" + +#include +#include + +#include +#include + +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* egl, + std::shared_ptr 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; + const EGLUtil* egl; + + GLuint texture; + GLuint fbo; + EGLContext egl_context; + EGLSurface egl_surface; + + std::mutex egl_mutex; + std::shared_ptr 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 . + +#include "x11.hpp" +#include "application.hpp" +#include "flags.hpp" +#include "os.hpp" +#include "tmux.hpp" +#include "util.hpp" + +#include + +#include + +#ifdef ENABLE_OPENGL +# include "window/x11egl.hpp" +# include +#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>(EGL_PLATFORM_XCB_EXT, connection); + } catch (const std::runtime_error &err) { + egl_available = false; + } + } else { + egl_available = false; + } +#endif + + xutil = std::make_unique(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_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(event.get()); + print_xcb_error(err); + break; + } + case XCB_EXPOSE: { + const auto *expose = reinterpret_cast(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 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 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; +#ifdef ENABLE_OPENGL + if (egl_available) { + try { + window = std::make_shared(connection, screen, window_id, parent, egl.get(), image); + } catch (const std::runtime_error &err) { + return; + } + } +#endif + if (window == nullptr) { + window = std::make_shared(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 &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 . + +#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 +#include +#include +#include +#include + +#include +#include + +#ifdef ENABLE_XCB_ERRORS +# include +#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 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 xutil; + + // map for event handler + std::unordered_map> windows; + + // windows per image + std::unordered_map>> image_windows; + + std::unordered_map> images; + std::unordered_map draw_threads; + + std::thread event_handler; + std::mutex windows_mutex; + + std::shared_ptr logger; + std::shared_ptr flags; + +#ifdef ENABLE_OPENGL + std::unique_ptr> egl; + bool egl_available = true; +#endif + + void draw(const std::string& identifier); + void handle_events(); + void get_tmux_window_ids(std::unordered_set& windows); + void print_xcb_error(const xcb_generic_error_t* err); +}; + +#endif -- cgit v1.2.3