diff options
| author | pants <niklasjo2004@gmail.com> | 2026-09-21 14:18:06 -0700 |
|---|---|---|
| committer | pants <niklasjo2004@gmail.com> | 2026-09-21 14:18:06 -0700 |
| commit | 83d5ba42f8a98fc0df177efef4333f74a0481f61 (patch) | |
| tree | 6f1fcff64ec9adb2fc51a7cfcc451eb22a103fb3 /src/canvas | |
Diffstat (limited to 'src/canvas')
38 files changed, 3394 insertions, 0 deletions
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, ®istry_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 |
