// 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); } }