From 0ec0359c9f6b494089566d658133f420f25ed8b3 Mon Sep 17 00:00:00 2001 From: pants Date: Sun, 28 Dec 2025 00:36:34 -0800 Subject: [PATCH] init --- CMakeLists.txt | 280 ++++++++ CODE_OF_CONDUCT.md | 53 ++ LICENSE | 674 ++++++++++++++++++++ README.md | 247 +++++++ debian/changelog | 5 + debian/changelog.in | 5 + debian/control | 37 ++ debian/copyright | 36 ++ debian/rules | 10 + debian/source/format | 1 + docs/ueberzugpp.1.in | 169 +++++ include/application.hpp | 66 ++ include/canvas.hpp | 38 ++ include/dimensions.hpp | 51 ++ include/flags.hpp | 68 ++ include/image.hpp | 54 ++ include/os.hpp | 41 ++ include/process.hpp | 38 ++ include/terminal.hpp | 89 +++ include/tmux.hpp | 46 ++ include/util.hpp | 67 ++ include/util/dbus.hpp | 33 + include/util/egl.hpp | 54 ++ include/util/ptr.hpp | 53 ++ include/util/socket.hpp | 46 ++ include/util/x11.hpp | 49 ++ include/version.hpp.in | 19 + include/window.hpp | 36 ++ install | 18 + plugins/wayfire/.gitignore | 1 + plugins/wayfire/CMakeLists.txt | 66 ++ plugins/wayfire/ueberzugpp.cpp | 150 +++++ scripts/fifo/fzf-fifo | 45 ++ scripts/fifo/img-fifo | 31 + scripts/fzfub | 25 + scripts/img | 30 + scripts/lf/cleaner | 3 + scripts/lf/lfub | 34 + scripts/lf/preview | 65 ++ scripts/sockets.py | 32 + src/application.cpp | 281 ++++++++ src/canvas.cpp | 64 ++ src/canvas/chafa.cpp | 104 +++ src/canvas/chafa.hpp | 52 ++ src/canvas/iterm2/chunk.cpp | 56 ++ src/canvas/iterm2/chunk.hpp | 45 ++ src/canvas/iterm2/iterm2.cpp | 108 ++++ src/canvas/iterm2/iterm2.hpp | 51 ++ src/canvas/kitty/chunk.cpp | 52 ++ src/canvas/kitty/chunk.hpp | 42 ++ src/canvas/kitty/kitty.cpp | 106 +++ src/canvas/kitty/kitty.hpp | 48 ++ src/canvas/sixel.cpp | 96 +++ src/canvas/sixel.hpp | 57 ++ src/canvas/stdout.hpp | 64 ++ src/canvas/wayland/config.cpp | 43 ++ src/canvas/wayland/config.hpp | 45 ++ src/canvas/wayland/config/dummy.cpp | 29 + src/canvas/wayland/config/dummy.hpp | 35 + src/canvas/wayland/config/hyprland.cpp | 151 +++++ src/canvas/wayland/config/hyprland.hpp | 53 ++ src/canvas/wayland/config/sway.cpp | 183 ++++++ src/canvas/wayland/config/sway.hpp | 62 ++ src/canvas/wayland/config/wayfire.cpp | 73 +++ src/canvas/wayland/config/wayfire.hpp | 44 ++ src/canvas/wayland/wayland.cpp | 222 +++++++ src/canvas/wayland/wayland.hpp | 84 +++ src/canvas/wayland/window/shm.cpp | 75 +++ src/canvas/wayland/window/shm.hpp | 47 ++ src/canvas/wayland/window/waylandegl.cpp | 235 +++++++ src/canvas/wayland/window/waylandegl.hpp | 88 +++ src/canvas/wayland/window/waylandshm.cpp | 186 ++++++ src/canvas/wayland/window/waylandshm.hpp | 77 +++ src/canvas/wayland/window/waylandwindow.hpp | 46 ++ src/canvas/x11/window/x11.cpp | 119 ++++ src/canvas/x11/window/x11.hpp | 60 ++ src/canvas/x11/window/x11egl.cpp | 138 ++++ src/canvas/x11/window/x11egl.hpp | 68 ++ src/canvas/x11/x11.cpp | 257 ++++++++ src/canvas/x11/x11.hpp | 93 +++ src/dimensions.cpp | 62 ++ src/flags.cpp | 52 ++ src/image.cpp | 185 ++++++ src/image/libvips.cpp | 216 +++++++ src/image/libvips.hpp | 71 +++ src/image/opencv.cpp | 249 ++++++++ src/image/opencv.hpp | 68 ++ src/main.cpp | 151 +++++ src/os.cpp | 126 ++++ src/process/apple.cpp | 43 ++ src/process/linux.cpp | 39 ++ src/terminal.cpp | 335 ++++++++++ src/tmux.cpp | 142 +++++ src/util/dbus.cpp | 15 + src/util/egl.cpp | 228 +++++++ src/util/socket.cpp | 169 +++++ src/util/util.cpp | 262 ++++++++ src/util/x11.cpp | 171 +++++ 98 files changed, 9188 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 debian/changelog create mode 100644 debian/changelog.in create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 docs/ueberzugpp.1.in create mode 100644 include/application.hpp create mode 100644 include/canvas.hpp create mode 100644 include/dimensions.hpp create mode 100644 include/flags.hpp create mode 100644 include/image.hpp create mode 100644 include/os.hpp create mode 100644 include/process.hpp create mode 100644 include/terminal.hpp create mode 100644 include/tmux.hpp create mode 100644 include/util.hpp create mode 100644 include/util/dbus.hpp create mode 100644 include/util/egl.hpp create mode 100644 include/util/ptr.hpp create mode 100644 include/util/socket.hpp create mode 100644 include/util/x11.hpp create mode 100644 include/version.hpp.in create mode 100644 include/window.hpp create mode 100755 install create mode 100644 plugins/wayfire/.gitignore create mode 100644 plugins/wayfire/CMakeLists.txt create mode 100644 plugins/wayfire/ueberzugpp.cpp create mode 100755 scripts/fifo/fzf-fifo create mode 100755 scripts/fifo/img-fifo create mode 100755 scripts/fzfub create mode 100755 scripts/img create mode 100755 scripts/lf/cleaner create mode 100755 scripts/lf/lfub create mode 100755 scripts/lf/preview create mode 100644 scripts/sockets.py create mode 100644 src/application.cpp create mode 100644 src/canvas.cpp create mode 100644 src/canvas/chafa.cpp create mode 100644 src/canvas/chafa.hpp create mode 100644 src/canvas/iterm2/chunk.cpp create mode 100644 src/canvas/iterm2/chunk.hpp create mode 100644 src/canvas/iterm2/iterm2.cpp create mode 100644 src/canvas/iterm2/iterm2.hpp create mode 100644 src/canvas/kitty/chunk.cpp create mode 100644 src/canvas/kitty/chunk.hpp create mode 100644 src/canvas/kitty/kitty.cpp create mode 100644 src/canvas/kitty/kitty.hpp create mode 100644 src/canvas/sixel.cpp create mode 100644 src/canvas/sixel.hpp create mode 100644 src/canvas/stdout.hpp create mode 100644 src/canvas/wayland/config.cpp create mode 100644 src/canvas/wayland/config.hpp create mode 100644 src/canvas/wayland/config/dummy.cpp create mode 100644 src/canvas/wayland/config/dummy.hpp create mode 100644 src/canvas/wayland/config/hyprland.cpp create mode 100644 src/canvas/wayland/config/hyprland.hpp create mode 100644 src/canvas/wayland/config/sway.cpp create mode 100644 src/canvas/wayland/config/sway.hpp create mode 100644 src/canvas/wayland/config/wayfire.cpp create mode 100644 src/canvas/wayland/config/wayfire.hpp create mode 100644 src/canvas/wayland/wayland.cpp create mode 100644 src/canvas/wayland/wayland.hpp create mode 100644 src/canvas/wayland/window/shm.cpp create mode 100644 src/canvas/wayland/window/shm.hpp create mode 100644 src/canvas/wayland/window/waylandegl.cpp create mode 100644 src/canvas/wayland/window/waylandegl.hpp create mode 100644 src/canvas/wayland/window/waylandshm.cpp create mode 100644 src/canvas/wayland/window/waylandshm.hpp create mode 100644 src/canvas/wayland/window/waylandwindow.hpp create mode 100644 src/canvas/x11/window/x11.cpp create mode 100644 src/canvas/x11/window/x11.hpp create mode 100644 src/canvas/x11/window/x11egl.cpp create mode 100644 src/canvas/x11/window/x11egl.hpp create mode 100644 src/canvas/x11/x11.cpp create mode 100644 src/canvas/x11/x11.hpp create mode 100644 src/dimensions.cpp create mode 100644 src/flags.cpp create mode 100644 src/image.cpp create mode 100644 src/image/libvips.cpp create mode 100644 src/image/libvips.hpp create mode 100644 src/image/opencv.cpp create mode 100644 src/image/opencv.hpp create mode 100644 src/main.cpp create mode 100644 src/os.cpp create mode 100644 src/process/apple.cpp create mode 100644 src/process/linux.cpp create mode 100644 src/terminal.cpp create mode 100644 src/tmux.cpp create mode 100644 src/util/dbus.cpp create mode 100644 src/util/egl.cpp create mode 100644 src/util/socket.cpp create mode 100644 src/util/util.cpp create mode 100644 src/util/x11.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4f0e54e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,280 @@ +# 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 . + +cmake_minimum_required(VERSION 3.21...3.28 FATAL_ERROR) + +set(UEBERZUGPP_VERSION 2.9.8) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_BUILD_TYPE + Debug + CACHE STRING "Build type.") + +project( + ueberzugpp + LANGUAGES CXX C + VERSION ${UEBERZUGPP_VERSION}) +add_executable(ueberzug) + +option(ENABLE_X11 "Enable X11 canvas." ON) +option(ENABLE_XCB_ERRORS "Enable useful logging of XCB errors." OFF) +option(ENABLE_WAYLAND "Enable wayland canvas" OFF) +option(ENABLE_DBUS "Enable dbus support" OFF) +option(ENABLE_OPENCV "Enable OpenCV image processing." ON) +option(ENABLE_TURBOBASE64 "Enable Turbo-Base64 for base64 encoding." OFF) +option(ENABLE_OPENGL "Enable canvas rendering with OpenGL." OFF) + +include(FetchContent) +include(GNUInstallDirs) +include(CheckCXXSymbolExists) + +find_package(PkgConfig REQUIRED) +find_package(Threads REQUIRED) +find_package(OpenSSL REQUIRED) +find_package(TBB REQUIRED) + +# check if is available +set(CMAKE_REQUIRED_LIBRARIES TBB::tbb) +check_cxx_symbol_exists(std::execution::par_unseq execution + HAVE_STD_EXECUTION_H) +if(HAVE_STD_EXECUTION_H) + target_compile_definitions(ueberzug PRIVATE HAVE_STD_EXECUTION_H) +endif() + +find_package(CLI11 QUIET) +if(NOT CLI11_FOUND) + if(FETCHCONTENT_FULLY_DISCONNECTED) + add_subdirectory("${CMAKE_SOURCE_DIR}/third_party/CLI11") + else() + FetchContent_Declare( + cli11 + URL https://github.com/CLIUtils/CLI11/archive/refs/tags/v2.4.2.tar.gz) + list(APPEND FC_LIBS cli11) + endif() +endif() + +find_package(nlohmann_json QUIET) +if(NOT nlohmann_json_FOUND) + FetchContent_Declare( + nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz) + list(APPEND FC_LIBS nlohmann_json) +endif() + +find_package(fmt QUIET) +if(NOT fmt_FOUND) + FetchContent_Declare( + fmt URL https://github.com/fmtlib/fmt/archive/refs/tags/10.2.1.tar.gz) + list(APPEND FC_LIBS fmt) +endif() + +find_package(spdlog QUIET) +if(NOT spdlog_FOUND) + set(SPDLOG_FMT_EXTERNAL ON) + FetchContent_Declare( + spdlog + URL https://github.com/gabime/spdlog/archive/refs/tags/v1.14.1.tar.gz) + list(APPEND FC_LIBS spdlog) +endif() + +if(FC_LIBS) + FetchContent_MakeAvailable(${FC_LIBS}) +endif() + +find_package(range-v3 QUIET) +if(NOT range-v3_FOUND) + FetchContent_Declare( + range-v3 + URL https://github.com/ericniebler/range-v3/archive/refs/tags/0.12.0.tar.gz) + FetchContent_Populate(range-v3) + add_subdirectory(${range-v3_SOURCE_DIR} ${range-v3_BINARY_DIR} + EXCLUDE_FROM_ALL) +endif() + +if(ENABLE_OPENGL) + target_compile_definitions(ueberzug PRIVATE ENABLE_OPENGL) + find_package(OpenGL REQUIRED) + list(APPEND UEBERZUG_SOURCES "src/util/egl.cpp") + list(APPEND UEBERZUG_LIBRARIES OpenGL::OpenGL OpenGL::EGL) +endif() + +if(ENABLE_X11) + target_compile_definitions(ueberzug PRIVATE ENABLE_X11) + pkg_check_modules(XCB REQUIRED IMPORTED_TARGET xcb) + pkg_check_modules(XCBIMAGE REQUIRED IMPORTED_TARGET xcb-image) + pkg_check_modules(XCBRES REQUIRED IMPORTED_TARGET xcb-res) + list(APPEND UEBERZUG_SOURCES "src/util/x11.cpp" "src/canvas/x11/x11.cpp" + "src/canvas/x11/window/x11.cpp") + list(APPEND UEBERZUG_LIBRARIES PkgConfig::XCB PkgConfig::XCBIMAGE + PkgConfig::XCBRES) + + if(ENABLE_OPENGL) + list(APPEND UEBERZUG_SOURCES "src/canvas/x11/window/x11egl.cpp") + endif() + + if(ENABLE_XCB_ERRORS) + target_compile_definitions(ueberzug PRIVATE ENABLE_XCB_ERRORS) + pkg_check_modules(XCBERRORS REQUIRED IMPORTED_TARGET xcb-errors) + list(APPEND UEBERZUG_LIBRARIES PkgConfig::XCBERRORS) + endif() +endif() + +if(ENABLE_WAYLAND) + target_compile_definitions(ueberzug PRIVATE ENABLE_WAYLAND) + find_package(ECM REQUIRED NO_MODULE) + list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + + find_library(LIBRT rt REQUIRED) + if(ENABLE_OPENGL) + find_package(Wayland REQUIRED COMPONENTS Client Egl) + list(APPEND UEBERZUG_LIBRARIES Wayland::Egl) + list(APPEND UEBERZUG_SOURCES "src/canvas/wayland/window/waylandegl.cpp") + else() + find_package(Wayland REQUIRED COMPONENTS Client) + endif() + find_package(WaylandProtocols REQUIRED) + find_package(WaylandScanner REQUIRED) + + ecm_add_wayland_client_protocol( + UEBERZUG_SOURCES PROTOCOL + "${WaylandProtocols_DATADIR}/stable/xdg-shell/xdg-shell.xml" BASENAME + "xdg-shell") + + list( + APPEND + UEBERZUG_SOURCES + "src/canvas/wayland/wayland.cpp" + "src/canvas/wayland/config.cpp" + "src/canvas/wayland/window/shm.cpp" + "src/canvas/wayland/window/waylandshm.cpp" + "src/canvas/wayland/config/sway.cpp" + "src/canvas/wayland/config/hyprland.cpp" + "src/canvas/wayland/config/wayfire.cpp" + "src/canvas/wayland/config/dummy.cpp") + + list(APPEND UEBERZUG_LIBRARIES Wayland::Client ${LIBRT}) +endif() + +if(ENABLE_OPENCV) + target_compile_definitions(ueberzug PRIVATE ENABLE_OPENCV) + find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs videoio) + + list(APPEND UEBERZUG_SOURCES "src/image/opencv.cpp") + list(APPEND UEBERZUG_LIBRARIES opencv_core opencv_imgproc opencv_imgcodecs + opencv_videoio) +endif() + +if(ENABLE_TURBOBASE64) + target_compile_definitions(ueberzug PRIVATE ENABLE_TURBOBASE64) + find_package(turbobase64 QUIET) + if(NOT turbobase64_FOUND) + FetchContent_Declare( + turbobase64 + URL https://github.com/powturbo/Turbo-Base64/archive/refs/tags/2023.08.tar.gz + ) + FetchContent_Populate(turbobase64) + add_subdirectory(${turbobase64_SOURCE_DIR} ${turbobase64_BINARY_DIR} + EXCLUDE_FROM_ALL) + list(APPEND UEBERZUG_LIBRARIES base64) + else() + target_compile_definitions(ueberzug PRIVATE WITH_SYSTEM_TURBOBASE64) + list(APPEND UEBERZUG_LIBRARIES turbo::base64) + endif() +endif() + +if(ENABLE_DBUS) + pkg_check_modules(DBUS REQUIRED IMPORTED_TARGET dbus-1) + list(APPEND UEBERZUG_LIBRARIES PkgConfig::DBUS) + list(APPEND UEBERZUG_SOURCES "src/util/dbus.cpp") +endif() + +set(PROJECT_WARNINGS_CXX -Wall -Wextra -Wpedantic -Werror) + +target_compile_options( + ueberzug PRIVATE $<$: + $<$:${PROJECT_WARNINGS_CXX}> >) + +target_compile_definitions(ueberzug PRIVATE $<$: DEBUG >) + +pkg_check_modules(VIPS REQUIRED IMPORTED_TARGET vips-cpp) +pkg_check_modules(SIXEL REQUIRED IMPORTED_TARGET libsixel) +pkg_check_modules(CHAFA REQUIRED IMPORTED_TARGET chafa>=1.6) + +if(CMAKE_HOST_SYSTEM_VERSION MATCHES "^.*microsoft.*$") + target_compile_definitions(ueberzug PRIVATE WSL) +endif() + +if(APPLE) + list(APPEND UEBERZUG_SOURCES src/process/apple.cpp) +else() + list(APPEND UEBERZUG_SOURCES src/process/linux.cpp) +endif() + +configure_file("include/version.hpp.in" version.hpp) +configure_file("docs/ueberzugpp.1.in" ueberzugpp.1) + +list( + APPEND + UEBERZUG_SOURCES + "src/main.cpp" + "src/application.cpp" + "src/os.cpp" + "src/tmux.cpp" + "src/terminal.cpp" + "src/dimensions.cpp" + "src/flags.cpp" + "src/util/util.cpp" + "src/util/socket.cpp" + "src/canvas.cpp" + "src/canvas/chafa.cpp" + "src/canvas/sixel.cpp" + "src/canvas/kitty/kitty.cpp" + "src/canvas/kitty/chunk.cpp" + "src/canvas/iterm2/iterm2.cpp" + "src/canvas/iterm2/chunk.cpp" + "src/image.cpp" + "src/image/libvips.cpp") + +list( + APPEND + UEBERZUG_LIBRARIES + nlohmann_json::nlohmann_json + CLI11::CLI11 + Threads::Threads + fmt::fmt + spdlog::spdlog + range-v3 + OpenSSL::Crypto + TBB::tbb + PkgConfig::VIPS + PkgConfig::SIXEL + PkgConfig::CHAFA) + +target_include_directories(ueberzug PRIVATE "${CMAKE_SOURCE_DIR}/include" + "${PROJECT_BINARY_DIR}") +target_sources(ueberzug PRIVATE ${UEBERZUG_SOURCES}) +target_link_libraries(ueberzug PRIVATE ${UEBERZUG_LIBRARIES}) +file(CREATE_LINK ueberzug "${PROJECT_BINARY_DIR}/ueberzugpp" SYMBOLIC) + +install(TARGETS ueberzug RUNTIME) +install(FILES "${PROJECT_BINARY_DIR}/ueberzugpp" TYPE BIN) +install(FILES "${PROJECT_BINARY_DIR}/ueberzugpp.1" + DESTINATION "${CMAKE_INSTALL_MANDIR}/man1") +install( + FILES "${PROJECT_BINARY_DIR}/ueberzugpp.1" + DESTINATION "${CMAKE_INSTALL_MANDIR}/man1" + RENAME ueberzug.1) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ea94b86 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,53 @@ +# Code of Merit + +1. The project creators, lead developers, core team, constitute +the managing members of the project and have final say in every decision +of the project, technical or otherwise, including overruling previous decisions. +There are no limitations to this decisional power. + +2. Contributions are an expected result of your membership on the project. +Don't expect others to do your work or help you with your work forever. + +3. All members have the same opportunities to seek any challenge they want +within the project. + +4. Authority or position in the project will be proportional +to the accrued contribution. Seniority must be earned. + +5. Software is evolutive: the better implementations must supersede lesser +implementations. Technical advantage is the primary evaluation metric. + +6. This is a space for technical prowess; topics outside of the project +will not be tolerated. + +7. Non technical conflicts will be discussed in a separate space. Disruption +of the project will not be allowed. + +8. Individual characteristics, including but not limited to, +body, sex, sexual preference, race, language, religion, nationality, +or political preferences are irrelevant in the scope of the project and +will not be taken into account concerning your value or that of your contribution +to the project. + +9. Discuss or debate the idea, not the person. + +10. There is no room for ambiguity: Ambiguity will be met with questioning; +further ambiguity will be met with silence. It is the responsibility +of the originator to provide requested context. + +11. If something is illegal outside the scope of the project, it is illegal +in the scope of the project. This Code of Merit does not take precedence over +governing law. + +12. This Code of Merit governs the technical procedures of the project not the +activities outside of it. + +13. Participation on the project equates to agreement of this Code of Merit. + +14. No objectives beyond the stated objectives of this project are relevant +to the project. Any intent to deviate the project from its original purpose +of existence will constitute grounds for remedial action which may include +expulsion from the project. + +This document is adapted from the Code of Merit, version 1.0. +See: https://codeofmerit.org/. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..76a8cf2 --- /dev/null +++ b/README.md @@ -0,0 +1,247 @@ +# pantsnet frozen version with install script + +## Überzug++ + +Überzug++ is a command line utility written in C++ which allows to draw images on terminals by using X11/wayland child +windows, sixels, kitty and iterm2.. + +This project intends to be a drop-in replacement for the now defunct [ueberzug](https://github.com/seebye/ueberzug) +project. If some tool doesn't work, +feel free to open an issue. + +Advantages over w3mimgdisplay and ueberzug: + +- support for wayland: sway, hyprland and wayfire +- support for MacOS +- no race conditions as a new window is created to display images +- expose events will be processed, so images will be redrawn on switch workspaces +- tmux support on X11, sway and hyprland +- terminals without the WINDOWID environment variable are supported +- chars are used as position - and size unit +- No memory leak (usage of smart pointers) +- A lot of image formats supported (through opencv and libvips). +- GIF and animated WEBP support on X11, Sixel, Sway and hyprland +- Fast image downscaling (through opencv and opencl) +- Cache resized images for faster viewing + +# Applications that support Überzug++ + +- [ytfzf](https://github.com/pystardust/ytfzf) +- [lobster](https://github.com/justchokingaround/lobster) +- [vifm](https://github.com/vifm/vifm) +- [rnvimr](https://github.com/kevinhwang91/rnvimr) +- [image.nvim](https://github.com/3rd/image.nvim) +- [yazi](https://github.com/sxyazi/yazi) +- [twitchez](https://github.com/WANDEX/twitchez) +- ÜberzugPP is a drop in replacement for Ueberzug, so applications that worked with ueberzug should work out of the box + with this project. + +# Integration scripts + +- [lf](https://github.com/jstkdng/ueberzugpp/blob/master/scripts/lf/lfub) +- [fzf](https://github.com/jstkdng/ueberzugpp/blob/master/scripts/fzfub) + +# Install + + + +### Homebrew (MacOS/Linux) + +`brew install jstkdng/programs/ueberzugpp` + +### Debian/Ubuntu/Fedora + +Packages for x86_64, aarch64 and ppc64le are available in the following repository. + +https://software.opensuse.org/download.html?project=home%3Ajustkidding&package=ueberzugpp + +# Usage + +1. Ueberzugpp provides two commands, `layer` and `tmux`. `layer` is used to send + commands to ueberzugpp, `tmux` is used internally. + +- Layer accepts the following options + +```bash +$ ueberzug layer -h +Display images on the terminal. +Usage: ueberzug layer [OPTIONS] + +Options: + -h,--help Print this help message and exit + -s,--silent Print stderr to /dev/null. + --use-escape-codes [0] Use escape codes to get terminal capabilities. + --pid-file TEXT Output file where to write the daemon PID. + --no-stdin Needs: --pid-file + Do not listen on stdin for commands. + --no-cache Disable caching of resized images. + --no-opencv Do not use OpenCV, use Libvips instead. + -o,--output TEXT:{x11,wayland,sixel,kitty,iterm2,chafa} + Image output method + -p,--parser **UNUSED**, only present for backwards compatibility. + -l,--loader **UNUSED**, only present for backwards compatibility. +``` + +2. You can also configure ueberzugpp with a json file. The file should be located + on `$XDG_CONFIG_HOME/ueberzugpp/config.json`, in case XDG_CONFIG_HOME isn't set, + ueberzugpp will look for the configuration at `~/.config/ueberzugpp/config.json` + +Application flags have precedence over the configuration file. +The configuration file should have this format. + +```json +{ + "layer": { + "silent": true, + "use-escape-codes": false, + "no-stdin": false, + "output": "sixel" + } +} +``` + +The most helpful is the `output` variable as that can be used to force +ueberzugpp to output images with a particular method. + +3. You can configure ueberzug++ directory for temporary files (logs, sockets) + with `${UEBERZUGPP_TMPDIR}` environment variable (by default it is system temporary directory) + +```sh +export UEBERZUGPP_TMPDIR="${TMPDIR}/ueberzugpp" +``` + +4. By default, commands are sent to ueberzug++ through stdin, this is enough in + some cases. In some terminals and application combinations (e.g. ranger + wezterm + zellij) + using stdin to send commands doesn't work properly or ueberzug++ could fail to + start altogether. In those cases, the user may send commands to ueberzug++ through + a unix socket. By default, ueberzug++ will listen to commands on `${UEBERZUGPP_TMPDIR}/ueberzugpp-${PID}.socket`. + +New software is encouraged to use sockets instead of stdin as they cover more cases. + +5. You can then feed Ueberzug with json objects to display an image or make it disappear. + +- json object to display the image: + + ```json + {"action":"add","identifier":"preview","max_height":0,"max_width":0,"path":"/path/image.ext","x":0,"y":0} + ``` + +The number values are COLUMNS and LINES of your terminal window, in TMUX it's relative to the size of the panels. + +- Don't display the image anymore: + + ```json + {"action":"remove","identifier":"preview"} + ``` + +# Build from source + +This project uses C++20 features so you must use a recent compiler. GCC 10.1 is +the minimum supported version. + +## Required dependencies + +Must be installed in order to build. + +- cmake ≥ 3.22 +- libvips +- libsixel +- chafa ≥ 1.6 +- openssl +- tbb + +### Install dependencies on Ubuntu + +``` +apt-get install libssl-dev libvips-dev libsixel-dev libchafa-dev libtbb-dev +``` + +## Downloadable dependencies + +Required for building, if they are not installed, they will be downloaded +and included in the binary. + +- nlohmann-json +- cli11 +- spdlog +- fmt +- range-v3 + +## Optional dependencies + +Not required for building, can be disabled/enabled using flags. + +- opencv +- xcb-util-image +- turbo-base64 +- wayland (libwayland) +- wayland-protocols +- extra-cmake-modules + +## Build instructions + +1. Download and extract the latest release. + +2. Choose feature flags + +The following feature flags are available: + +ENABLE_OPENCV (ON by default) + +ENABLE_X11 (ON by default) + +ENABLE_TURBOBASE64 (OFF by default) + +ENABLE_WAYLAND (OFF by default) + +You may use any of them when building the project, for example: + +- Compile with default options + +```sh +git clone https://github.com/jstkdng/ueberzugpp.git +cd ueberzugpp +mkdir build && cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +cmake --build . +``` + +- Disable X11 and OpenCV support + +```sh +git clone https://github.com/jstkdng/ueberzugpp.git +cd ueberzugpp +mkdir build && cd build +cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_X11=OFF -DENABLE_OPENCV=OFF .. +cmake --build . +``` + +- Enable support for Turbo-Base64 + +```sh +git clone https://github.com/jstkdng/ueberzugpp.git +cd ueberzugpp +mkdir build && cd build +cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_TURBOBASE64=ON .. +cmake --build . +``` + +after running these commands the resulting binary is ready to be used. + +- Install the resulting build directory to the default installation path (Optional) + +```sh +cmake --install build +``` + +# Donate + +If you like my work you can send some monero my way. + +XMR Address: `8BRt2qYXjyR9Bb2CXtjVWSYNCepqgcZkheoMWTXTNmwLLU3ZEscuxtYFGaytSMNn1FETLdbdhXimCTTLSkN5r5j7SEBLMho` + +# Thanks + +Thank you jetbrains for providing licenses for this project. + +[](https://jb.gg/OpenSourceSupport) diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..da8d5a5 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +ueberzugpp (2.8.6) unstable; urgency=medium + + * Initial release. + + -- JustKidding Wed, 31 May 2023 07:35:31 -0400 diff --git a/debian/changelog.in b/debian/changelog.in new file mode 100644 index 0000000..39191dd --- /dev/null +++ b/debian/changelog.in @@ -0,0 +1,5 @@ +ueberzugpp (@@VERSION@@) unstable; urgency=medium + + * Initial release. + + -- JustKidding @@DATETIME@@ diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..c9fb936 --- /dev/null +++ b/debian/control @@ -0,0 +1,37 @@ +Source: ueberzugpp +Maintainer: JustKidding +Homepage: https://github.com/jstkdng/ueberzugpp +Section: misc +Priority: optional +Standards-Version: 4.5.1 +Conflicts: ueberzug +Build-Depends: + debhelper (>= 13), + debhelper-compat (= 13), + cmake (>= 3.18), + libchafa-dev, + libvips-dev, + nlohmann-json3-dev, + libspdlog-dev, + libfmt-dev, + libopencv-videoio-dev, + libopencv-dev, + libopencv-imgproc-dev, + libopencv-imgcodecs-dev, + libopencv-core-dev, + libsixel-dev, + libssl-dev, + libxcb-image0-dev, + libxcb-res0-dev, + wayland-protocols, + libwayland-dev, + librange-v3-dev, + extra-cmake-modules + +Package: ueberzugpp +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Display images inside the terminal. + Überzug++ is a command line utility written in C++ which + allows to draw images on terminals by using X11/wayland + child windows, sixels, kitty and iterm2. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..a395e17 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,36 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: ueberzugpp +Upstream-Contact: JustKidding +Source: https://github.com/jstkdng/ueberzugpp + +Files: * +Copyright: JustKidding +License: GPL-3+ with OpenSSL exception + 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. + . + In addition, as a special exception, the author of this program gives + permission to link the code of its release with the OpenSSL project's + "OpenSSL" library (or with modified versions of it that use the same + license as the "OpenSSL" library), and distribute the linked executables. + You must obey the GNU General Public License in all respects for all of + the code used other than "OpenSSL". If you modify this file, you may + extend this exception to your version of the file, but you are not + obligated to do so. If you do not wish to do so, delete this exception + statement from your 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 . +Comment: + On Debian systems, the full text of the GNU General Public License + version 3 can be found in the file '/usr/share/common-licenses/GPL-3'. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..14bcdd3 --- /dev/null +++ b/debian/rules @@ -0,0 +1,10 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +export DH_VERBOSE := 1 +%: + dh $@ --buildsystem=cmake + +override_dh_auto_configure: + dh_auto_configure -- \ + -DENABLE_WAYLAND=ON \ + -DFETCHCONTENT_FULLY_DISCONNECTED=ON diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/docs/ueberzugpp.1.in b/docs/ueberzugpp.1.in new file mode 100644 index 0000000..f95296a --- /dev/null +++ b/docs/ueberzugpp.1.in @@ -0,0 +1,169 @@ +.TH UEBERZUGPP 1 "2023 May" "Ueberzugpp @ueberzugpp_VERSION@" + +.SH NAME +ueberzugpp \- display images in a terminal + +.SH SYNOPSIS +.SY ueberzugpp +.RI [ options ] + +.SH DESCRIPTION +.PP +.B ueberzugpp +is a program meant to display images in a terminal in a with an IPC. + +.SH OPTIONS + +.TP +.BR \-h ", " \-\-help +Show help text. + +.TP +.BR \-\-version +Show version + +.TP +.BR \-\-use\-escape\-codes +Use escape codes to get terminal capabilities + +.TP +.BR \-\-no\-stdin +Do not listen on stdin for commands + +.TP +.BR \-\-no\-cache +Disable caching of resized images + +.TP +.BR \-\-no\-opencv +Do not use OpenCV, use Libvips instead + +.TP +.BR \-o ", " \-\-output +Image output method, valid values for this include: +.PP +.RS +.I x11 " (May not be available if disabled in compilation)" +.br +.I sixel +.br +.I kitty +.br +.I iterm2 +.br +.I wayland " (May not be available if disabled in compilation)" +.br +.I chafa +.RE + +.TP +.BR \-p ", " \-\-parser +.B UNUSED ", " +only present for backwards compatibility + +.TP +.BR \-l ", " \-\-loader +.B UNUSED ", " +only present for backwards compatibility + +.SH STDIN + +.PP +Ueberzugpp reads commands through stdin. Or through the unix socket located at /tmp/ueberzug_${USER}.sock +.PP +Commands should be in JSON form, as described in the JSON IPC section + +.SH JSON IPC + +.PP +There are two actions, +.I add +and +.I remove +.PP + +.SS +.B add +action json schema +.PP +Requried Keys + +.RE +.TP +.B action " (string)" +should be +.I add + +.TP +.B path " (string)" +the path to the image to use + +.TP +.B identifier " (string)" +an identifier for the image, so that it can be removed with the remove action + +.TP +.B One of, width/height, or max_width/max_height + +.TP +.B width " (integer)" +width of the image + +.TP +.B max_width " (integer)" +maximum width of the image + +.TP +.B height " (integer)" +height of the image + +.TP +.B max_height " (integer)" +maximum height of the image + +.TP +.B x " (integer)" +the column position in the terminal + +.TP +.B y " (integer)" +the row position in the terminal + +.PP +Optional keys + +.TP +.B scaler " (string)" +can be fit_contain or forced_cover. +.br +Both base the scale on whichever is larger, the width, or height of the image + +.RE + +.SS +.B remove +action json schema +.PP +Requried Keys + +.RS +.TP +.B action " (string)" +should be remove + +.TP +.B identifier " (string)" +The identifier of the image to remove + +.RE + +.SH EXAMPLE + +.PP +Create a fifo file named fifo, and have an image in the current folder named image.png for this example to work + +.PP +ueberzugpp layer -o sixel < fifo & + +.PP +echo '{"path": "./image.png", "action": "add", "identifier": "image", "x": 0, "y": 0, "width": 20, "height": 20}' > fifo diff --git a/include/application.hpp b/include/application.hpp new file mode 100644 index 0000000..24291ac --- /dev/null +++ b/include/application.hpp @@ -0,0 +1,66 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef APPLICATION_H +#define APPLICATION_H + +#include "canvas.hpp" +#include "flags.hpp" +#include "os.hpp" +#include "terminal.hpp" +#include "util/ptr.hpp" + +#include +#include +#include +#include +#include + +#include + +class Application +{ + public: + explicit Application(const char *executable); + ~Application(); + + void execute(std::string_view cmd); + void command_loop(); + void handle_tmux_hook(std::string_view hook); + + inline static std::atomic stop_flag = false; // NOLINT + inline static const int parent_pid = os::get_ppid(); + + static void print_version(); + static void print_header(); + + private: + std::unique_ptr terminal; + std::unique_ptr canvas; + + std::shared_ptr flags; + std::shared_ptr logger; + + cn_unique_ptr f_stderr; + std::thread socket_thread; + + void setup_logger(); + void set_silent(); + void socket_loop(); + void daemonize(); +}; + +#endif diff --git a/include/canvas.hpp b/include/canvas.hpp new file mode 100644 index 0000000..8e0c15e --- /dev/null +++ b/include/canvas.hpp @@ -0,0 +1,38 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef CANVAS_H +#define CANVAS_H + +#include "image.hpp" + +#include + +class Canvas +{ + public: + static auto create() -> std::unique_ptr; + virtual ~Canvas() = default; + + virtual void add_image(const std::string &identifier, std::unique_ptr new_image) = 0; + virtual void remove_image(const std::string &identifier) = 0; + + virtual void show() {} + virtual void hide() {} + virtual void toggle() {} +}; + +#endif diff --git a/include/dimensions.hpp b/include/dimensions.hpp new file mode 100644 index 0000000..a69ee27 --- /dev/null +++ b/include/dimensions.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 . + +#ifndef DIMENSIONS_H +#define DIMENSIONS_H + +#include +#include + +#include "terminal.hpp" + +class Dimensions +{ + public: + Dimensions(const Terminal *terminal, uint16_t xcoord, uint16_t ycoord, int max_w, int max_h, std::string scaler); + + [[nodiscard]] auto xpixels() const -> int; + [[nodiscard]] auto ypixels() const -> int; + [[nodiscard]] auto max_wpixels() const -> int; + [[nodiscard]] auto max_hpixels() const -> int; + + uint16_t x; + uint16_t y; + uint16_t max_w; + uint16_t max_h; + uint16_t padding_horizontal; + uint16_t padding_vertical; + std::string scaler; + const Terminal *terminal; + + private: + uint16_t orig_x; + uint16_t orig_y; + + void read_offsets(); +}; + +#endif diff --git a/include/flags.hpp b/include/flags.hpp new file mode 100644 index 0000000..bf2c0cb --- /dev/null +++ b/include/flags.hpp @@ -0,0 +1,68 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef FLAGS_H +#define FLAGS_H + +#include +#include +#include + +// singleton +class Flags +{ + public: + static auto instance() -> std::shared_ptr + { + static std::shared_ptr instance{new Flags}; + return instance; + } + + Flags(const Flags &) = delete; + Flags(Flags &) = delete; + auto operator=(const Flags &) -> Flags & = delete; + auto operator=(Flags &) -> Flags & = delete; + + bool no_stdin = false; + bool silent = false; + bool use_escape_codes = false; + bool print_version = false; + bool no_cache = false; + bool no_opencv = false; + bool use_opengl = false; + std::string output; + std::string pid_file; + bool origin_center = false; + int32_t scale_factor = 1; + bool needs_scaling = false; + + std::string cmd_id; + std::string cmd_action; + std::string cmd_socket; + std::string cmd_x; + std::string cmd_y; + std::string cmd_max_width; + std::string cmd_max_height; + std::string cmd_file_path; + + private: + Flags(); + std::filesystem::path config_file; + + void read_config_file(); +}; + +#endif diff --git a/include/image.hpp b/include/image.hpp new file mode 100644 index 0000000..3124b58 --- /dev/null +++ b/include/image.hpp @@ -0,0 +1,54 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef IMAGE_H +#define IMAGE_H + +#include +#include +#include +#include + +#include "dimensions.hpp" +#include "terminal.hpp" + +class Image +{ + public: + static auto load(const nlohmann::json &command, const Terminal *terminal) -> std::unique_ptr; + static auto check_cache(const Dimensions &dimensions, const std::filesystem::path &orig_path) -> std::string; + static auto get_dimensions(const nlohmann::json &json, const Terminal *terminal) -> std::shared_ptr; + + virtual ~Image() = default; + + [[nodiscard]] virtual auto dimensions() const -> const Dimensions & = 0; + [[nodiscard]] virtual auto width() const -> int = 0; + [[nodiscard]] virtual auto height() const -> int = 0; + [[nodiscard]] virtual auto size() const -> size_t = 0; + [[nodiscard]] virtual auto data() const -> const unsigned char * = 0; + [[nodiscard]] virtual auto channels() const -> int = 0; + + [[nodiscard]] virtual auto frame_delay() const -> int { return -1; } + [[nodiscard]] virtual auto is_animated() const -> bool { return false; } + [[nodiscard]] virtual auto filename() const -> std::string = 0; + virtual auto next_frame() -> void {} + + protected: + [[nodiscard]] auto get_new_sizes(double max_width, double max_height, std::string_view scaler, + int scale_factor = 0) const -> std::pair; +}; + +#endif diff --git a/include/os.hpp b/include/os.hpp new file mode 100644 index 0000000..2268d94 --- /dev/null +++ b/include/os.hpp @@ -0,0 +1,41 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef NAMESPACE_OS_H +#define NAMESPACE_OS_H + +#include +#include + +namespace os +{ + +auto exec(const std::string &cmd) -> std::string; +auto getenv(const std::string &var) -> std::optional; +auto read_data_from_fd(int filde, char sep = '\n') -> std::string; +auto read_data_from_stdin(char sep = '\n') -> std::string; +auto wait_for_data_on_fd(int filde, int waitms) -> bool; +auto wait_for_data_on_stdin(int waitms) -> bool; + +auto get_pid() -> int; +auto get_ppid() -> int; + +void get_process_info(int pid); +void daemonize(); + +} // namespace os + +#endif diff --git a/include/process.hpp b/include/process.hpp new file mode 100644 index 0000000..3817f1e --- /dev/null +++ b/include/process.hpp @@ -0,0 +1,38 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef PROCESS_INFO_H +#define PROCESS_INFO_H + +#include + +class Process +{ + public: + explicit Process(int pid); + ~Process() = default; + + int pid; + char state; + int ppid; + int process_group_id; + int session_id; + int tty_nr; + int minor_dev; + std::string pty_path; +}; + +#endif diff --git a/include/terminal.hpp b/include/terminal.hpp new file mode 100644 index 0000000..8aec512 --- /dev/null +++ b/include/terminal.hpp @@ -0,0 +1,89 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef TERMINAL_H +#define TERMINAL_H + +#include +#include +#include + +#include +#include + +#include "flags.hpp" +#include "os.hpp" + +class Terminal +{ + public: + Terminal(); + ~Terminal(); + + uint16_t font_width; + uint16_t font_height; + uint16_t padding_horizontal; + uint16_t padding_vertical; + uint16_t rows; + uint16_t cols; + int pid = os::get_pid(); + int terminal_pid; + unsigned int x11_wid; + std::string term; + std::string term_program; + std::string detected_output; + + private: + auto get_terminal_size() -> void; + static auto guess_padding(uint16_t chars, double pixels) -> double; + static auto guess_font_size(uint16_t chars, float pixels, float padding) -> float; + static auto read_raw_str(std::string_view esc) -> std::string; + + void init_termios(); + void reset_termios() const; + + void check_sixel_support(); + void check_kitty_support(); + void check_iterm2_support(); + + void get_terminal_size_escape_code(); + void get_terminal_size_xtsm(); + void get_fallback_x11_terminal_sizes(); + void get_fallback_wayland_terminal_sizes(); + + void open_first_pty(); + void set_detected_output(); + + int pty_fd; + int xpixel = 0; + int ypixel = 0; + uint16_t fallback_xpixel = 0; + uint16_t fallback_ypixel = 0; + + bool supports_sixel = false; + bool supports_kitty = false; + bool supports_x11 = false; + bool supports_iterm2 = false; + bool supports_wayland = false; + + std::shared_ptr flags; + std::shared_ptr logger; + + struct termios old_term; + struct termios new_term; +}; + +#endif diff --git a/include/tmux.hpp b/include/tmux.hpp new file mode 100644 index 0000000..5652660 --- /dev/null +++ b/include/tmux.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 . + +#ifndef NAMESPACE_TMUX_H +#define NAMESPACE_TMUX_H + +#include +#include +#include +#include + +namespace tmux +{ + auto get_session_id() -> std::string; + auto get_pane() -> std::string; + + auto is_used() -> bool; + auto is_window_focused() -> bool; + + auto get_client_pids() -> std::optional>; + + auto get_offset() -> std::pair; + + auto get_pane_offset() -> std::pair; + + auto get_statusbar_offset() -> int; + + void handle_hook(std::string_view hook, int pid); + void register_hooks(); + void unregister_hooks(); +} // namespace tmux + +#endif diff --git a/include/util.hpp b/include/util.hpp new file mode 100644 index 0000000..9a0d3a7 --- /dev/null +++ b/include/util.hpp @@ -0,0 +1,67 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef NAMESPACE_UTIL_H +#define NAMESPACE_UTIL_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "flags.hpp" +#include "os.hpp" +#include "process.hpp" + +namespace util +{ +auto str_split(std::string_view str, std::string_view delim) -> std::vector; +auto get_process_tree(int pid) -> std::vector; +auto get_process_tree_v2(int pid) -> std::vector; +auto get_b2_hash_ssl(std::string_view str) -> std::string; +auto get_cache_path() -> std::string; +auto get_cache_file_save_location(const std::filesystem::path &path) -> std::string; +auto get_log_filename() -> std::string; +auto get_socket_path(int pid = os::get_pid()) -> std::string; +void send_socket_message(std::string_view msg, std::string_view endpoint); +auto base64_encode(const unsigned char *input, size_t length) -> std::string; +void base64_encode_v2(const unsigned char *input, size_t length, unsigned char *out); +void move_cursor(int row, int col); +void save_cursor_position(); +void restore_cursor_position(); +void benchmark(const std::function &func); +void send_command(const Flags &flags); +void clear_terminal_area(int xcoord, int ycoord, int width, int height); +auto generate_random_string(std::size_t length) -> std::string; +auto round_up(int num_to_round, int multiple) -> int; +auto temp_directory_path() -> std::filesystem::path; + +auto read_exif_rotation(const std::filesystem::path &path) -> std::optional; +template +auto generate_random_number(T min, T max = std::numeric_limits::max()) -> T +{ + std::random_device dev; + std::mt19937 rng(dev()); + std::uniform_int_distribution dist(min, max); + return dist(rng); +} +} // namespace util + +#endif diff --git a/include/util/dbus.hpp b/include/util/dbus.hpp new file mode 100644 index 0000000..ae10773 --- /dev/null +++ b/include/util/dbus.hpp @@ -0,0 +1,33 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef UEBERZUGPP_DBUS_HPP +#define UEBERZUGPP_DBUS_HPP + +#include +#include + +class DbusUtil +{ + public: + explicit DbusUtil(const std::string &address); + ~DbusUtil(); + + private: + DBusConnection *connection = nullptr; +}; + +#endif // UEBERZUGPP_DBUS_HPP diff --git a/include/util/egl.hpp b/include/util/egl.hpp new file mode 100644 index 0000000..05a1373 --- /dev/null +++ b/include/util/egl.hpp @@ -0,0 +1,54 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef UTIL_EGL_H +#define UTIL_EGL_H + +#include "image.hpp" + +#include +#include + +#include + +#include +#define GL_GLEXT_PROTOTYPES +#include + +template +class EGLUtil +{ + public: + EGLUtil(EGLenum platform, T *native_display, const EGLAttrib *attrib = nullptr); + ~EGLUtil(); + + void get_texture_from_image(const Image &image, GLuint texture) const; + void run_contained(EGLSurface surface, EGLContext context, const std::function &func) const; + void make_current(EGLSurface surface, EGLContext context) const; + void restore() const; + [[nodiscard]] auto create_surface(V *native_window) const -> EGLSurface; + [[nodiscard]] auto create_context(EGLSurface surface) const -> EGLContext; + + EGLDisplay display; + + private: + EGLConfig config; + std::shared_ptr logger; + + [[nodiscard]] auto error_to_string() const -> std::string; +}; + +#endif diff --git a/include/util/ptr.hpp b/include/util/ptr.hpp new file mode 100644 index 0000000..0e8be3f --- /dev/null +++ b/include/util/ptr.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 . + +#ifndef UTIL_PTR_H +#define UTIL_PTR_H + +#include + +template +struct deleter_from_fn { + template + constexpr void operator()(T *arg) const + { + fn(const_cast *>(arg)); + } +}; + +template +struct deleter_from_fn_null { + template + constexpr void operator()(T *arg) const + { + if (arg != nullptr) { + fn(const_cast *>(arg)); + } + } +}; + +// custom unique pointer +template +using c_unique_ptr = std::unique_ptr>; + +// custom unique pointer that checks for null before deleting +template +using cn_unique_ptr = std::unique_ptr>; + +template +using unique_C_ptr = std::unique_ptr>; + +#endif diff --git a/include/util/socket.hpp b/include/util/socket.hpp new file mode 100644 index 0000000..14859ae --- /dev/null +++ b/include/util/socket.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 . + +#ifndef UTIL_SOCKET_H +#define UTIL_SOCKET_H + +#include +#include +#include +#include + +class UnixSocket +{ + public: + UnixSocket(); + explicit UnixSocket(std::string_view endpoint); + ~UnixSocket(); + + void connect_to_endpoint(std::string_view endpoint); + void bind_to_endpoint(std::string_view endpoint) const; + [[nodiscard]] auto wait_for_connections(int waitms) const -> int; + [[nodiscard]] auto read_data_from_connection(int filde) -> std::vector; + void write(const void *data, std::size_t len) const; + void read(void *data, std::size_t len) const; + [[nodiscard]] auto read_until_empty() const -> std::string; + + private: + int fd; + bool connected = true; + std::string buffer; +}; + +#endif diff --git a/include/util/x11.hpp b/include/util/x11.hpp new file mode 100644 index 0000000..ee3542b --- /dev/null +++ b/include/util/x11.hpp @@ -0,0 +1,49 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef X11_UTIL_H +#define X11_UTIL_H + +#include +#include +#include +#include + +#include + +class X11Util +{ + public: + X11Util(); + explicit X11Util(xcb_connection_t *connection); + ~X11Util(); + + [[nodiscard]] auto get_server_window_ids() const -> std::vector; + [[nodiscard]] auto get_pid_window_map() const -> std::unordered_map; + [[nodiscard]] auto get_window_dimensions(xcb_window_t window) const -> std::pair; + [[nodiscard]] auto get_parent_window(int pid) const -> xcb_window_t; + [[nodiscard]] auto window_has_properties(xcb_window_t window, std::initializer_list properties) const + -> bool; + + bool connected = false; + + private: + xcb_connection_t *connection; + xcb_screen_t *screen = nullptr; + bool owns_connection = true; +}; + +#endif diff --git a/include/version.hpp.in b/include/version.hpp.in new file mode 100644 index 0000000..aa79608 --- /dev/null +++ b/include/version.hpp.in @@ -0,0 +1,19 @@ +// 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 . + +#define ueberzugpp_VERSION_MAJOR @ueberzugpp_VERSION_MAJOR@ +#define ueberzugpp_VERSION_MINOR @ueberzugpp_VERSION_MINOR@ +#define ueberzugpp_VERSION_PATCH @ueberzugpp_VERSION_PATCH@ diff --git a/include/window.hpp b/include/window.hpp new file mode 100644 index 0000000..44e3eac --- /dev/null +++ b/include/window.hpp @@ -0,0 +1,36 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef WINDOW_H +#define WINDOW_H + +#include +#include + +class Window +{ +public: + virtual ~Window() = default; + virtual void draw() = 0; + virtual void generate_frame() = 0; + virtual void show() {}; + virtual void hide() {}; +}; + +template +concept WindowType = std::is_base_of::value; + +#endif diff --git a/install b/install new file mode 100755 index 0000000..0cc1257 --- /dev/null +++ b/install @@ -0,0 +1,18 @@ +#!/bin/sh +# install script for debian-like systems + +echo "just installing dependencies..." +sudo apt install libxcb-* libopencv-dev make cmake + +echo "building" + +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +cmake --build . + +echo "done building, installing" +sudo install -Dm755 ueberzug /usr/local/bin/ueberzug +sudo ln -sf /usr/local/bin/ueberzug /usr/local/bin/ueberzugpp + +echo "done" diff --git a/plugins/wayfire/.gitignore b/plugins/wayfire/.gitignore new file mode 100644 index 0000000..ceddaa3 --- /dev/null +++ b/plugins/wayfire/.gitignore @@ -0,0 +1 @@ +.cache/ diff --git a/plugins/wayfire/CMakeLists.txt b/plugins/wayfire/CMakeLists.txt new file mode 100644 index 0000000..8044b8f --- /dev/null +++ b/plugins/wayfire/CMakeLists.txt @@ -0,0 +1,66 @@ +# 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 . + +cmake_minimum_required(VERSION 3.18) + +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type.") + +cmake_policy(SET CMP0065 NEW) +set(CMAKE_ENABLE_EXPORTS ON) + +project(wayfire-ueberzugpp LANGUAGES CXX C VERSION 0.0.1) +add_library(ueberzugpp SHARED) + +list(APPEND PLUGIN_SOURCES "ueberzugpp.cpp") + +find_package(PkgConfig REQUIRED) + +find_package(ECM REQUIRED NO_MODULE) +list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) +find_package(WaylandProtocols REQUIRED) +find_package(WaylandScanner REQUIRED) + +ecm_add_wayland_server_protocol(PLUGIN_SOURCES + PROTOCOL "${WaylandProtocols_DATADIR}/stable/xdg-shell/xdg-shell.xml" + BASENAME "xdg-shell") +file(CREATE_LINK + "wayland-xdg-shell-server-protocol.h" + "${PROJECT_BINARY_DIR}/xdg-shell-protocol.h" SYMBOLIC) + +pkg_check_modules(WAYFIRE REQUIRED IMPORTED_TARGET wayfire) +pkg_check_modules(WLROOTS REQUIRED IMPORTED_TARGET wlroots) +pkg_check_modules(WFCONFIG REQUIRED IMPORTED_TARGET wf-config) + +add_compile_definitions(WLR_USE_UNSTABLE WAYFIRE_PLUGIN) +target_compile_options(ueberzugpp PRIVATE + $<$: + -Wall -Wextra -Wpedantic -Werror + > +) + +target_include_directories(ueberzugpp PRIVATE "${PROJECT_BINARY_DIR}") +target_sources(ueberzugpp PRIVATE ${PLUGIN_SOURCES}) +target_link_libraries(ueberzugpp + PkgConfig::WAYFIRE + PkgConfig::WLROOTS + PkgConfig::WFCONFIG) diff --git a/plugins/wayfire/ueberzugpp.cpp b/plugins/wayfire/ueberzugpp.cpp new file mode 100644 index 0000000..cc70a16 --- /dev/null +++ b/plugins/wayfire/ueberzugpp.cpp @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wf +{ +class ueberzugpp_surface_t +{ + std::weak_ptr terminal; + std::shared_ptr surface; + std::shared_ptr translation; + wlr_xdg_toplevel *toplevel; + + wf::wl_listener_wrapper on_toplevel_destroy; + + public: + + ueberzugpp_surface_t(const std::weak_ptr& _terminal, + wlr_xdg_toplevel *_toplevel): + terminal(_terminal), + toplevel(_toplevel) + { + // We create a custom surface node for the ueberzugpp window and wrap it in a translation node, so that + // we can control its position. + // We add the translation node as a subsurface of the terminal. + surface = std::make_shared(_toplevel->base->surface, true); + translation = std::make_shared(); + + translation->set_children_list({surface}); + + auto term = _terminal.lock(); + wf::scene::add_front(term->get_surface_root_node(), translation); + + // Finally, wait for the toplevel to be destroyed + on_toplevel_destroy.set_callback([this] (auto) + { + destroy_callback(); + }); + on_toplevel_destroy.connect(&toplevel->base->events.destroy); + } + + std::function destroy_callback; + + ~ueberzugpp_surface_t() + { + wf::scene::remove_child(translation); + } + + void set_offset(int xcoord, int ycoord) + { + translation->set_offset({xcoord, ycoord}); + const auto term = terminal.lock(); + if (!term) { + return; + } + term->damage(); + } +}; + + +class ueberzugpp_mapper : public wf::plugin_interface_t +{ + public: + void init() override + { + ipc_repo->register_method("ueberzugpp/set_offset", set_offset); + wf::get_core().connect(&on_new_xdg_surface); + } + + void fini() override + { + ipc_repo->unregister_method("ueberzugpp/set_offset"); + } + + ipc::method_callback set_offset = [this] (nlohmann::json json) + { + // NOLINTBEGIN + WFJSON_EXPECT_FIELD(json, "app-id", string); + WFJSON_EXPECT_FIELD(json, "x", number_integer); + WFJSON_EXPECT_FIELD(json, "y", number_integer); + // NOLINTEND + + const std::string app_id = json["app-id"]; + const int xcoord = json["x"]; + const int ycoord = json["y"]; + + const auto search = surfaces.find(app_id); + if (search == surfaces.end()) + { + return ipc::json_error("Unknown ueberzugpp window with appid " + app_id); + } + + search->second->set_offset(xcoord, ycoord); + return ipc::json_ok(); + }; + + shared_data::ref_ptr_t ipc_repo; + + // When a new xdg_toplevel is created, we need to check whether it is an ueberzugpp window by looking at + // its app-id. If it is indeed an ueberzugpp window, we take over the toplevel (by setting + // use_default_implementation=false) and create our own ueberzugpp_surface. In addition, we directly map + // the surface to the currently focused view, if it exists. + wf::signal::connection_t on_new_xdg_surface = [this] (new_xdg_surface_signal *event) + { + if (!event->use_default_implementation) { + return; + } + if (event->surface->role != WLR_XDG_SURFACE_ROLE_TOPLEVEL) { + return; + } + + const auto* toplevel_title = event->surface->toplevel->title; + const std::string appid = toplevel_title != nullptr ? toplevel_title : "null"; + const std::string search_for = "ueberzugpp_"; + if (appid.find(search_for) != std::string::npos) + { + auto terminal = wf::get_core().get_active_output()->get_active_view(); + if (!terminal) + { + LOGE("Which window to map ueberzugpp to????"); + return; + } + + event->use_default_implementation = false; + const auto [iter, was_inserted] = surfaces.insert_or_assign(appid, + std::make_unique(terminal->weak_from_this(), event->surface->toplevel)); + iter->second->destroy_callback = [this, appid] + { + surfaces.erase(appid); + }; + } + }; + + std::unordered_map> surfaces; +}; +} // end namespace wf + +// NOLINTNEXTLINE +DECLARE_WAYFIRE_PLUGIN(wf::ueberzugpp_mapper); diff --git a/scripts/fifo/fzf-fifo b/scripts/fifo/fzf-fifo new file mode 100755 index 0000000..a9d5d20 --- /dev/null +++ b/scripts/fifo/fzf-fifo @@ -0,0 +1,45 @@ +#!/bin/sh + +# This is a simple script that allows you to have image preview +# in fzf, using ueberzugpp with fifo. For this script to work +# you must add it to path, because it requires to call itself +# during the preview process in fzf (line 42 -> $0). +# Example usage: +# ls | fzf-fifo +# find $HOME/pix/ -type f -iname "*.jpg" | fzf-fifo + +FIFO="/tmp/fzf_preview_fifo" +[ -p "$FIFO" ] || mkfifo "$FIFO" + +start_ueberzugpp() { + ueberzugpp layer --silent <"$FIFO" & + exec 3>"${FIFO}" +} + +cleanup() { + exec 3>&- +} +trap cleanup HUP INT QUIT TERM EXIT + +preview_image() { + echo '{"path": "'"$1"'", "action": "add", ''"identifier": "fzfpreview", '\ + '"x": "'"$FZF_PREVIEW_LEFT"'", "y": "'"$FZF_PREVIEW_TOP"'", '\ + '"width": "'"$FZF_PREVIEW_COLUMNS"'", "height": "'"$FZF_PREVIEW_LINES"'"}' \ + >"$FIFO" +} + +case "$1" in + -W) + shift + preview_image "$@" + exit + ;; +esac + +main() { + start_ueberzugpp + selected_image=$(fzf --preview "$0 -W {}" --preview-window=up:60%:wrap) + [ -n "$selected_image" ] && echo "Selected image: $selected_image" + rm "$FIFO" +} +main diff --git a/scripts/fifo/img-fifo b/scripts/fifo/img-fifo new file mode 100755 index 0000000..78aeb9d --- /dev/null +++ b/scripts/fifo/img-fifo @@ -0,0 +1,31 @@ +#!/bin/sh + +# This is a simple script that allows you to use ueberzugpp to +# preview images in the terminal by providing the x and y +# coordinates of the image, the width and height of the image, +# and the path to the image file, with $1, $2, $3, $4 and $5 +# as arguments, respectively. +# Example usage: +# ./img-fifo 0 0 30 30 image.jpg +# Use Ctrl+C to exit. + +FIFO="/tmp/preview_fifo" +[ -p "$FIFO" ] || mkfifo "$FIFO" + +start_ueberzugpp() { + ueberzugpp layer --silent <"$FIFO" & + exec 3>"${FIFO}" +} + +cleanup() { + rm -f "$FIFO" +} +trap cleanup HUP INT QUIT TERM EXIT + +preview_image() { + echo '{"path": "'"$5"'", "action": "add", "identifier": "img-fifo", "x": "'"$1"'", "y": "'"$2"'", "width": "'"$3"'", "height": "'"$4"'"}' >"$FIFO" +} + +start_ueberzugpp +preview_image "$1" "$2" "$3" "$4" "$5" +wait diff --git a/scripts/fzfub b/scripts/fzfub new file mode 100755 index 0000000..48d8b67 --- /dev/null +++ b/scripts/fzfub @@ -0,0 +1,25 @@ +#!/bin/sh + +case "$(uname -a)" in + *Darwin*) UEBERZUG_TMP_DIR="$TMPDIR" ;; + *) UEBERZUG_TMP_DIR="/tmp" ;; +esac + +cleanup() { + ueberzugpp cmd -s "$SOCKET" -a exit +} +trap cleanup HUP INT QUIT TERM EXIT + +UB_PID_FILE="$UEBERZUG_TMP_DIR/.$(uuidgen)" +ueberzugpp layer --no-stdin --silent --use-escape-codes --pid-file "$UB_PID_FILE" +UB_PID=$(cat "$UB_PID_FILE") + +export SOCKET="$UEBERZUG_TMP_DIR"/ueberzugpp-"$UB_PID".socket + +# run fzf with preview +fzf --reverse --preview="ueberzugpp cmd -s $SOCKET -i fzfpreview -a add \ + -x \$FZF_PREVIEW_LEFT -y \$FZF_PREVIEW_TOP \ + --max-width \$FZF_PREVIEW_COLUMNS --max-height \$FZF_PREVIEW_LINES \ + -f {}" + +ueberzugpp cmd -s "$SOCKET" -a exit diff --git a/scripts/img b/scripts/img new file mode 100755 index 0000000..fef7262 --- /dev/null +++ b/scripts/img @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +# This is a simple script that allows you to use ueberzugpp to +# preview images in the terminal by providing the x and y +# coordinates of the image, the width and height of the image, +# and the path to the image file, with $1, $2, $3, $4 and $5 +# as arguments, respectively. +# Example usage: +# ./img 0 0 30 30 image.jpg + +UB_PID=0 +UB_SOCKET="" + +case "$(uname -a)" in + *Darwin*) UEBERZUG_TMP_DIR="$TMPDIR" ;; + *) UEBERZUG_TMP_DIR="/tmp" ;; +esac + +cleanup() { + ueberzugpp cmd -s "$UB_SOCKET" -a exit +} +trap cleanup HUP INT QUIT TERM EXIT + +UB_PID_FILE="$UEBERZUG_TMP_DIR/.$(uuidgen)" +ueberzugpp layer --no-stdin --silent --use-escape-codes --pid-file "$UB_PID_FILE" +UB_PID="$(cat "$UB_PID_FILE")" +export UB_SOCKET="$UEBERZUG_TMP_DIR"/ueberzugpp-"$UB_PID".socket +ueberzugpp cmd -s "$UB_SOCKET" -a add -i PREVIEW -x "$1" -y "$2" --max-width "$3" --max-height "$4" -f "$5" +sleep 2 diff --git a/scripts/lf/cleaner b/scripts/lf/cleaner new file mode 100755 index 0000000..0159d5a --- /dev/null +++ b/scripts/lf/cleaner @@ -0,0 +1,3 @@ +#!/bin/sh + +ueberzugpp cmd -s $UB_SOCKET -a remove -i PREVIEW diff --git a/scripts/lf/lfub b/scripts/lf/lfub new file mode 100755 index 0000000..45864e5 --- /dev/null +++ b/scripts/lf/lfub @@ -0,0 +1,34 @@ +#!/bin/sh + +# This is a wrapper script for lf that allows it to create image previews with +# ueberzug. This works in concert with the lf configuration file and the +# lf-cleaner script. + +set -e + +UB_PID=0 +UB_SOCKET="" + +case "$(uname -a)" in + *Darwin*) UEBERZUG_TMP_DIR="$TMPDIR" ;; + *) UEBERZUG_TMP_DIR="/tmp" ;; +esac + +cleanup() { + exec 3>&- + ueberzugpp cmd -s "$UB_SOCKET" -a exit +} + +if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then + lf "$@" +else + [ ! -d "$HOME/.cache/lf" ] && mkdir -p "$HOME/.cache/lf" + UB_PID_FILE="$UEBERZUG_TMP_DIR/.$(uuidgen)" + ueberzugpp layer --silent --no-stdin --use-escape-codes --pid-file "$UB_PID_FILE" + UB_PID=$(cat "$UB_PID_FILE") + rm "$UB_PID_FILE" + UB_SOCKET="$UEBERZUG_TMP_DIR/ueberzugpp-${UB_PID}.socket" + export UB_PID UB_SOCKET + trap cleanup HUP INT QUIT TERM EXIT + lf "$@" 3>&- +fi diff --git a/scripts/lf/preview b/scripts/lf/preview new file mode 100755 index 0000000..ab41c61 --- /dev/null +++ b/scripts/lf/preview @@ -0,0 +1,65 @@ +#!/bin/sh + +image() { + FILE_PATH="$1" + X=$4 + Y=$5 + MW=$(($2 - 1)) + MH=$3 + ueberzugpp cmd -s "$UB_SOCKET" -a add -i PREVIEW -x "$X" -y "$Y" --max-width "$MW" --max-height "$MH" -f "$FILE_PATH" + exit 1 +} + +batorcat() { + file="$1" + shift + if command -v bat >/dev/null 2>&1; then + bat --color=always --style=plain --pager=never "$file" "$@" + else + cat "$file" + fi +} + +CACHE="$HOME/.cache/lf/thumbnail.$(stat --printf '%n\0%i\0%F\0%s\0%W\0%Y' -- "$(readlink -f "$1")" | sha256sum | awk '{print $1}'))" + +case "$(printf "%s\n" "$(readlink -f "$1")" | tr '[:upper:]' '[:lower:]')" in + *.tgz | *.tar.gz) tar tzf "$1" ;; + *.tar.bz2 | *.tbz2) tar tjf "$1" ;; + *.tar.txz | *.txz) xz --list "$1" ;; + *.tar) tar tf "$1" ;; + *.zip | *.jar | *.war | *.ear | *.oxt) unzip -l "$1" ;; + *.rar) unrar l "$1" ;; + *.7z) 7z l "$1" ;; + *.[1-8]) man "$1" | col -b ;; + *.o) nm "$1" ;; + *.torrent) transmission-show "$1" ;; + *.iso) iso-info --no-header -l "$1" ;; + *.odt | *.ods | *.odp | *.sxw) odt2txt "$1" ;; + *.doc) catdoc "$1" ;; + *.docx) docx2txt "$1" - ;; + *.xls | *.xlsx) + ssconvert --export-type=Gnumeric_stf:stf_csv "$1" "fd://1" | batorcat --language=csv + ;; + *.wav | *.mp3 | *.flac | *.m4a | *.wma | *.ape | *.ac3 | *.og[agx] | *.spx | *.opus | *.as[fx] | *.mka) + exiftool "$1" + ;; + *.pdf) + [ ! -f "${CACHE}.jpg" ] && pdftoppm -jpeg -f 1 -singlefile "$1" "$CACHE" + image "${CACHE}.jpg" "$2" "$3" "$4" "$5" + ;; + *.avi | *.mp4 | *.wmv | *.dat | *.3gp | *.ogv | *.mkv | *.mpg | *.mpeg | *.vob | *.fl[icv] | *.m2v | *.mov | *.webm | *.ts | *.mts | *.m4v | *.r[am] | *.qt | *.divx) + [ ! -f "${CACHE}.jpg" ] && ffmpegthumbnailer -i "$1" -o "${CACHE}.jpg" -s 0 -q 5 + image "${CACHE}.jpg" "$2" "$3" "$4" "$5" + ;; + *.bmp | *.jpg | *.jpeg | *.png | *.xpm | *.webp | *.gif | *.jfif) + image "$1" "$2" "$3" "$4" "$5" + ;; + *.svg) + [ ! -f "${CACHE}.jpg" ] && convert "$1" "${CACHE}.jpg" + image "${CACHE}.jpg" "$2" "$3" "$4" "$5" + ;; + *) + batorcat "$1" + ;; +esac +exit 0 diff --git a/scripts/sockets.py b/scripts/sockets.py new file mode 100644 index 0000000..802d5e6 --- /dev/null +++ b/scripts/sockets.py @@ -0,0 +1,32 @@ +import socket +import time +import os + +cmd1 = """ +{"action":"add","identifier":"preview","max_height":21,"max_width":118,"path":"/tmp/a.png","x":10,"y":15} +""" + +cmd2 = """ +{"action":"add","identifier":"preview","max_height":47,"max_width":118,"path":"/tmp/b.png","x":10,"y":15} +""" + +cmd_exit = """ +{"action":"remove","identifier":"preview"} +""" + +s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +connected = False +while not connected: + try: + s.connect(f"/tmp/ueberzugpp-30468.socket") + connected = True + except Exception: + time.sleep(0.05) + +s.sendall(str.encode(cmd1 + "\n")) +time.sleep(2) +s.sendall(str.encode(cmd_exit + "\n")) +s.sendall(str.encode(cmd2 + "\n")) +time.sleep(2) +s.sendall(str.encode(cmd_exit + "\n")) +s.close() diff --git a/src/application.cpp b/src/application.cpp new file mode 100644 index 0000000..2aa06f5 --- /dev/null +++ b/src/application.cpp @@ -0,0 +1,281 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "application.hpp" +#include "image.hpp" +#include "tmux.hpp" +#include "util.hpp" +#include "util/socket.hpp" +#include "version.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +using njson = nlohmann::json; +namespace fs = std::filesystem; + +Application::Application(const char *executable) +{ + flags = Flags::instance(); + print_header(); + setup_logger(); + set_silent(); + terminal = std::make_unique(); + if (flags->no_stdin) { + daemonize(); + } + canvas = Canvas::create(); + const auto cache_path = util::get_cache_path(); + if (!fs::exists(cache_path) && !flags->no_cache) { + fs::create_directories(cache_path); + } + tmux::register_hooks(); + socket_thread = std::thread([this] { + const auto sock_path = util::get_socket_path(); + logger->info("Listening for commands on socket {}", sock_path); + socket_loop(); + }); + if (flags->no_cache) { + logger->info("Image caching is disabled"); + } + if (VIPS_INIT(executable)) { + vips_error_exit(nullptr); + } + vips_cache_set_max(1); +} + +Application::~Application() +{ + if (socket_thread.joinable()) { + socket_thread.join(); + } + logger->info("Exiting ueberzugpp"); + canvas.reset(); + vips_shutdown(); + tmux::unregister_hooks(); + fs::remove(util::get_socket_path()); +} + +void Application::execute(const std::string_view cmd) +{ + if (!canvas) { + return; + } + njson json; + try { + json = njson::parse(cmd); + } catch (const njson::parse_error &e) { + logger->error("Command received is not valid json"); + return; + } + const auto json_str = json.dump(); + logger->info("Command received: {}", json_str); + + const std::string &action = json.at("action"); + if (action == "tmux") { + const std::string &hook = json.at("hook"); + handle_tmux_hook(hook); + return; + } + + const std::string &identifier = json.at("identifier"); + if (action == "add") { + if (!json.at("path").is_string()) { + logger->error("Path received is not valid"); + return; + } + auto image = Image::load(json, terminal.get()); + if (!image) { + logger->error("Unable to load image file"); + return; + } + canvas->add_image(identifier, std::move(image)); + } else if (action == "remove") { + canvas->remove_image(identifier); + } else { + logger->warn("Command not supported"); + } +} + +void Application::handle_tmux_hook(const std::string_view hook) +{ + const std::unordered_map> hook_fns{ + {"client-session-changed", + [this] { + if (tmux::is_window_focused()) { + canvas->show(); + } else { + canvas->hide(); + } + }}, + {"session-window-changed", + [this] { + if (tmux::is_window_focused()) { + canvas->show(); + } else { + canvas->hide(); + } + }}, + {"client-detached", [this] { canvas->hide(); }}, + {"window-layout-changed", + [this] { + if (tmux::is_window_focused()) { + canvas->hide(); + } + }}, + }; + + try { + hook_fns.at(hook)(); + } catch (const std::out_of_range &oor) { + logger->warn("TMUX hook not recognized"); + } +} + +void Application::setup_logger() +{ + const auto log_tmp = util::get_log_filename(); + const auto log_path = util::temp_directory_path() / log_tmp; + try { + spdlog::flush_on(spdlog::level::debug); + const auto sink = std::make_shared(log_path); + + const auto main_logger = std::make_shared("main", sink); + const auto terminal_logger = std::make_shared("terminal", sink); + const auto cv_logger = std::make_shared("opencv", sink); + const auto vips_logger = std::make_shared("vips", sink); + const auto x11_logger = std::make_shared("X11", sink); + const auto sixel_logger = std::make_shared("sixel", sink); + const auto kitty_logger = std::make_shared("kitty", sink); + const auto iterm2_logger = std::make_shared("iterm2", sink); + const auto chafa_logger = std::make_shared("chafa", sink); + const auto wayland_logger = std::make_shared("wayland", sink); + const auto opengl_logger = std::make_shared("opengl", sink); + + spdlog::initialize_logger(main_logger); + spdlog::initialize_logger(terminal_logger); + spdlog::initialize_logger(cv_logger); + spdlog::initialize_logger(vips_logger); + spdlog::initialize_logger(x11_logger); + spdlog::initialize_logger(sixel_logger); + spdlog::initialize_logger(kitty_logger); + spdlog::initialize_logger(iterm2_logger); + spdlog::initialize_logger(chafa_logger); + spdlog::initialize_logger(wayland_logger); + spdlog::initialize_logger(opengl_logger); + + logger = spdlog::get("main"); + } catch (const spdlog::spdlog_ex &ex) { + std::cout << "Log init failed: " << ex.what() << '\n'; + } +} + +void Application::command_loop() +{ + if (flags->no_stdin) { + return; + } + while (!stop_flag) { + try { + const auto in_event = os::wait_for_data_on_stdin(100); + if (!in_event) { + continue; + } + const auto cmd = os::read_data_from_stdin(); + execute(cmd); + } catch (const std::system_error &err) { + stop_flag = true; + break; + } + } +} + +void Application::socket_loop() +{ + UnixSocket socket; + socket.bind_to_endpoint(util::get_socket_path()); + + const int waitms = 100; + int conn = -1; + while (!stop_flag) { + try { + conn = socket.wait_for_connections(waitms); + } catch (const std::system_error &err) { + stop_flag = true; + break; + } + + if (conn == -1) { + continue; + } + + const auto data = socket.read_data_from_connection(conn); + for (const auto &cmd : data) { + if (cmd == "EXIT") { + stop_flag = true; + return; + } + execute(cmd); + } + } +} + +void Application::print_header() +{ + const auto log_tmp = util::get_log_filename(); + const auto log_path = util::temp_directory_path() / log_tmp; + const auto art = fmt::format(R"( + _ _ _ +| | | | | | _ _ +| | | | ___| |__ ___ _ __ _____ _ __ _ _| |_ _| |_ +| | | |/ _ \ '_ \ / _ \ '__|_ / | | |/ _` |_ _|_ _| +| |_| | __/ |_) | __/ | / /| |_| | (_| | |_| |_| + \___/ \___|_.__/ \___|_| /___|\__,_|\__, | + __/ | + |___/ v{}.{}.{})", + ueberzugpp_VERSION_MAJOR, ueberzugpp_VERSION_MINOR, ueberzugpp_VERSION_PATCH); + std::ofstream ofs(log_path, std::ios::out | std::ios::app); + ofs << art << '\n' << std::flush; +} + +void Application::set_silent() +{ + if (!flags->silent) { + return; + } + f_stderr.reset(std::freopen("/dev/null", "w", stderr)); +} + +void Application::print_version() +{ + const auto ver_str = fmt::format("ueberzugpp {}.{}.{}", ueberzugpp_VERSION_MAJOR, ueberzugpp_VERSION_MINOR, + ueberzugpp_VERSION_PATCH); + std::cout << ver_str << '\n'; +} + +void Application::daemonize() +{ + os::daemonize(); + std::ofstream ofs(flags->pid_file); + ofs << os::get_pid() << std::flush; +} diff --git a/src/canvas.cpp b/src/canvas.cpp new file mode 100644 index 0000000..08fd0a3 --- /dev/null +++ b/src/canvas.cpp @@ -0,0 +1,64 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "canvas/chafa.hpp" +#include "canvas/iterm2/iterm2.hpp" +#include "canvas/kitty/kitty.hpp" +#include "canvas/sixel.hpp" +#include "canvas/stdout.hpp" +#ifdef ENABLE_X11 +# include "canvas/x11/x11.hpp" +#endif +#ifdef ENABLE_WAYLAND +# include "canvas/wayland/wayland.hpp" +#endif +#include "flags.hpp" +#include + +auto Canvas::create() -> std::unique_ptr +{ + const auto flags = Flags::instance(); + const auto logger = spdlog::get("main"); + +#ifdef ENABLE_WAYLAND + if (flags->output == "wayland") { + return std::make_unique(); + } +#else + logger->debug("Wayland support not compiled in the binary"); +#endif +#ifdef ENABLE_X11 + if (flags->output == "x11") { + return std::make_unique(); + } +#else + logger->debug("X11 support not compiled in the binary"); +#endif + + if (flags->output == "kitty") { + return std::make_unique>(flags->output); + } + if (flags->output == "iterm2") { + return std::make_unique>(flags->output); + } + if (flags->output == "sixel") { + return std::make_unique>(flags->output); + } + if (flags->output == "chafa") { + return std::make_unique>(flags->output); + } + throw std::runtime_error(fmt::format("output backend not supported (backend is {})", flags->output)); +} diff --git a/src/canvas/chafa.cpp b/src/canvas/chafa.cpp new file mode 100644 index 0000000..dc3ece5 --- /dev/null +++ b/src/canvas/chafa.cpp @@ -0,0 +1,104 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "chafa.hpp" +#include "dimensions.hpp" +#include "terminal.hpp" +#include "util.hpp" +#include "util/ptr.hpp" + +#include +#include +#include + +#include +#include + +void gstring_delete(GString *str) +{ + g_string_free(str, true); +} + +Chafa::Chafa(std::unique_ptr 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{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(image->width()) / dims.terminal->font_width); + vertical_cells = std::ceil(static_cast(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{lines[i]}; + util::move_cursor(ycoord++, x); + std::cout << line->str; + } + g_free(lines); +#else + const auto result = c_unique_ptr{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 . + +#ifndef CHAFA_WINDOW_H +#define CHAFA_WINDOW_H + +#include "image.hpp" +#include "window.hpp" + +#include +#include + +#include + +class Chafa : public Window +{ + public: + Chafa(std::unique_ptr 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; + 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 . + +#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& chunk) +{ + util::base64_encode_v2(reinterpret_cast(chunk->get_buffer()), + chunk->get_size(), reinterpret_cast(chunk->get_result())); +} + +void Iterm2Chunk::operator()(std::unique_ptr& 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 . + +#ifndef ITERM2_CHUNK_H +#define ITERM2_CHUNK_H + +#include +#include +#include + +class Iterm2Chunk +{ +public: + Iterm2Chunk() = default; + explicit Iterm2Chunk(uint64_t size); + + void operator()(std::unique_ptr& chunk) const; + void static process_chunk(std::unique_ptr& 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 buffer; + std::vector 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 . + +#include "iterm2.hpp" +#include "chunk.hpp" +#include "dimensions.hpp" +#include "image.hpp" +#include "terminal.hpp" +#include "util.hpp" + +#include +#include +#include +#include + +#include +#include + +#ifdef HAVE_STD_EXECUTION_H +# include +#else +# include +#endif + +#include + +namespace fs = std::filesystem; + +Iterm2::Iterm2(std::unique_ptr 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(image->width()) / dims.terminal->font_width); + vertical_cells = std::ceil(static_cast(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(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(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 &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> +{ + const int num_chunks = std::ceil(static_cast(num_bytes) / chunk_size); + std::vector> chunks; + chunks.reserve(num_chunks + 2); + + std::ifstream ifs(filename); + while (ifs.good()) { + auto chunk = std::make_unique(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 . + +#ifndef ITERM2_CANVAS_H +#define ITERM2_CANVAS_H + +#include "chunk.hpp" +#include "image.hpp" +#include "window.hpp" + +#include +#include +#include + +class Iterm2 : public Window +{ + public: + Iterm2(std::unique_ptr new_image, std::mutex *stdout_mutex); + ~Iterm2() override; + + void draw() override; + void generate_frame() override{}; + + private: + std::unique_ptr 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>; +}; + +#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 . + +#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(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 . + +#ifndef KITTY_CHUNK_H +#define KITTY_CHUNK_H + +#include +#include + +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 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 . + +#include "kitty.hpp" +#include "dimensions.hpp" +#include "util.hpp" + +#include + +#include + +#ifdef HAVE_STD_EXECUTION_H +# include +#else +# include +#endif + +Kitty::Kitty(std::unique_ptr new_image, std::mutex *stdout_mutex) + : image(std::move(new_image)), + stdout_mutex(stdout_mutex), + id(util::generate_random_number(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 +{ + 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 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 . + +#ifndef KITTY_WINDOW_H +#define KITTY_WINDOW_H + +#include "chunk.hpp" +#include "image.hpp" +#include "window.hpp" + +#include +#include +#include + +class Kitty : public Window +{ + public: + Kitty(std::unique_ptr new_image, std::mutex *stdout_mutex); + ~Kitty() override; + + void draw() override; + void generate_frame() override; + + private: + std::string str; + std::unique_ptr image; + std::mutex *stdout_mutex; + uint32_t id; + int x; + int y; + + auto process_chunks() -> std::vector; +}; + +#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 . + +#include "sixel.hpp" +#include "dimensions.hpp" +#include "terminal.hpp" +#include "util.hpp" + +#include +#include + +namespace fs = std::filesystem; + +Sixel::Sixel(std::unique_ptr 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(image->width()) / dims.terminal->font_width); + vertical_cells = std::ceil(static_cast(image->height()) / dims.terminal->font_height); + + const auto draw_callback = [](char *data, int size, void *priv) -> int { + auto *str = static_cast(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(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(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 . + +#ifndef SIXEL_WINDOW_H +#define SIXEL_WINDOW_H + +#include "image.hpp" +#include "window.hpp" + +#include +#include +#include +#include +#include + +#include + +class Sixel : public Window +{ + public: + Sixel(std::unique_ptr new_image, std::mutex *stdout_mutex); + ~Sixel() override; + + void draw() override; + void generate_frame() override; + + private: + std::unique_ptr image; + std::mutex *stdout_mutex; + + std::string str; + std::thread draw_thread; + std::atomic 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 . + +#ifndef STDOUT_CANVAS_H +#define STDOUT_CANVAS_H + +#include "canvas.hpp" +#include "image.hpp" +#include "window.hpp" + +#include + +#include +#include +#include +#include + +template +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 new_image) override + { + logger->info("Displaying image with id {}", identifier); + images.erase(identifier); + const auto [entry, success] = + images.emplace(identifier, std::make_unique(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 logger; + std::unordered_map> 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 . + +#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 +{ + const auto sway_sock = os::getenv("SWAYSOCK"); + if (sway_sock.has_value()) { + return std::make_unique(sway_sock.value()); + } + + const auto hypr_sig = os::getenv("HYPRLAND_INSTANCE_SIGNATURE"); + if (hypr_sig.has_value()) { + return std::make_unique(hypr_sig.value()); + } + + const auto wayfire_sock = os::getenv("WAYFIRE_SOCKET"); + if (wayfire_sock.has_value()) { + return std::make_unique(wayfire_sock.value()); + } + + return std::make_unique(); +} + 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 . + +#ifndef WAYLAND_CONFIG_H +#define WAYLAND_CONFIG_H + +#include +#include +#include + +struct WaylandWindowGeometry { + int width; + int height; + int x; + int y; +}; + +class WaylandConfig +{ + public: + static auto get() -> std::unique_ptr; + + 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 . + +#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 . + +#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 . + +#include "hyprland.hpp" +#include "os.hpp" +#include "tmux.hpp" +#include "util/socket.hpp" + +#include +#include +#include +#include + +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 . + +#ifndef HYPRLAND_SOCKET_H +#define HYPRLAND_SOCKET_H + +#include "../config.hpp" + +#include +#include + +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 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 . + +#include "sway.hpp" +#include "application.hpp" +#include "os.hpp" +#include "tmux.hpp" +#include "util.hpp" + +#include +#include +#include +#include + +#include +#include + +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 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() - output_info.x, + .y = rect.at("y").get() - output_info.y}; +} + +auto SwaySocket::get_active_window(const std::vector& nodes) -> nlohmann::json +{ + const auto pids = tmux::get_client_pids().value_or(std::vector{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()) != 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 +{ + logger->debug("Obtaining sway tree"); + const auto tree = ipc_message(IPC_GET_TREE); + std::stack nodes_st; + std::vector 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 . + +#ifndef SWAY_SOCKET_H +#define SWAY_SOCKET_H + +#include "../config.hpp" +#include "util/socket.hpp" + +#include +#include + +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 &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; + [[nodiscard]] auto ipc_message(ipc_message_type type, std::string_view payload = "") const -> nlohmann::json; + + UnixSocket socket; + std::shared_ptr 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 . + +#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() - 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 . + +#ifndef WAYFIRE_SOCKET_H +#define WAYFIRE_SOCKET_H + +#include "../config.hpp" +#include "util/socket.hpp" + +#include +#include + +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 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 . + +#include "wayland.hpp" +#include "application.hpp" +#include "image.hpp" +#include "os.hpp" +#include "util.hpp" + +#ifdef ENABLE_OPENGL +# include "window/waylandegl.hpp" +# include +#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(data); + if (interface_str == wl_compositor_interface.name) { + canvas->compositor = static_cast( + wl_registry_bind(registry, name, &wl_compositor_interface, compositor_ver)); + } else if (interface_str == wl_shm_interface.name) { + canvas->wl_shm = static_cast(wl_registry_bind(registry, name, &wl_shm_interface, shm_ver)); + } else if (interface_str == xdg_wm_base_interface.name) { + canvas->xdg_base = + static_cast(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(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(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(data); + canvas->output_pair.second = scale; +} + +void WaylandCanvas::output_done(void *data, [[maybe_unused]] struct wl_output *output) +{ + auto *canvas = static_cast(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>(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 new_image) +{ + std::shared_ptr window; +#ifdef ENABLE_OPENGL + if (egl_available) { + try { + window = std::make_shared(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(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 . + +#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 +#include +#include + +#include +#include + +#ifdef ENABLE_OPENGL +# include "util/egl.hpp" +# include +#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 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 output_pair; + std::unordered_map output_info; + + private: + struct wl_display *display = nullptr; + struct wl_registry *registry = nullptr; + std::thread event_handler; + + std::shared_ptr logger; + std::unique_ptr config; + std::shared_ptr flags; + std::unordered_map> windows; + +#ifdef ENABLE_OPENGL + std::unique_ptr> 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 . + +#include "shm.hpp" +#include "util.hpp" +#include "util/ptr.hpp" + +#include +#include +#include +#include + +#include + +#include +#include + +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(pool_ptr); +} + +void WaylandShm::allocate_pool_buffers() +{ + const auto pool = c_unique_ptr{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 . + +#ifndef WAYLAND_SHM_H +#define WAYLAND_SHM_H + +#include +#include + +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 . + +#include "waylandegl.hpp" +#include "dimensions.hpp" +#include "util.hpp" + +#include +#include + +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 *egl, + std::unique_ptr 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(); + 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(data); + const auto window = tmp->ptr.lock(); + if (!window) { + return; + } + auto *egl_window = dynamic_cast(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(data); + const auto window = tmp->ptr.lock(); + if (!window) { + return; + } + auto *egl_window = dynamic_cast(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 . + +#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 +#include + +#include +#include + +class WaylandEglWindow : public WaylandWindow +{ + public: + WaylandEglWindow(struct wl_compositor *compositor, struct xdg_wm_base *xdg_base, + const EGLUtil *egl, std::unique_ptr 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; + WaylandConfig *config; + + EGLSurface egl_surface; + EGLContext egl_context; + + struct wl_egl_window *egl_window = nullptr; + const EGLUtil *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 . + +#include "waylandshm.hpp" +#include "dimensions.hpp" +#include "shm.hpp" +#include "util.hpp" + +#include + +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(data); + const auto window = tmp->ptr.lock(); + if (!window) { + return; + } + auto *shm_window = dynamic_cast(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(data); + const auto window = tmp->ptr.lock(); + if (!window) { + return; + } + auto *shm_window = dynamic_cast(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 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(image->width(), image->height(), output_scale, canvas->wl_shm); +} + +void WaylandShmWindow::finish_init() +{ + auto xdg = std::make_unique(); + 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 . + +#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 +#include +#include +#include +#include + +class WaylandShmWindow : public WaylandWindow +{ + public: + WaylandShmWindow(WaylandCanvas *canvas, std::unique_ptr 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 visible{false}; + std::unique_ptr 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; + 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 . + +#ifndef WAYLAND_WINDOW_H +#define WAYLAND_WINDOW_H + +#include "window.hpp" + +#include +#include + +class WaylandWindow: + public Window, + public std::enable_shared_from_this +{ +public: + ~WaylandWindow() override = default; + + virtual void wl_draw([[maybe_unused]] int32_t scale_factor) {}; + virtual void finish_init() = 0; +}; + +struct XdgStruct +{ + std::weak_ptr ptr; +}; + +struct XdgStructAgg +{ + std::vector> 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 . + +#include "x11.hpp" +#include "dimensions.hpp" + +#include + +#include + +constexpr std::string_view win_name = "ueberzugpp"; + +X11Window::X11Window(xcb_connection_t *connection, xcb_screen_t *screen, xcb_window_t window, xcb_window_t parent, + std::shared_ptr image) + : connection(connection), + screen(screen), + window(window), + parent(parent), + gc(xcb_generate_id(connection)), + image(std::move(image)) +{ + logger = spdlog::get("X11"); + create(); + change_title(); +} + +void X11Window::create() +{ + const uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP; + struct xcb_create_window_value_list_t value_list; + value_list.background_pixel = screen->black_pixel; + value_list.border_pixel = screen->black_pixel; + value_list.event_mask = XCB_EVENT_MASK_EXPOSURE; + value_list.colormap = screen->default_colormap; + + const auto dimensions = image->dimensions(); + const auto xcoord = static_cast(dimensions.xpixels() + dimensions.padding_horizontal); + const auto ycoord = static_cast(dimensions.ypixels() + dimensions.padding_vertical); + xcb_create_window_aux(connection, screen->root_depth, window, this->parent, xcoord, ycoord, image->width(), + image->height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, value_mask, + &value_list); + + xcb_create_gc(connection, gc, window, 0, nullptr); + logger->debug("Created child window {} at ({},{}) with parent {}", window, xcoord, ycoord, parent); +} + +void X11Window::change_title() +{ + const int bits_in_char = 8; + xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, bits_in_char, + win_name.size(), win_name.data()); +} + +void X11Window::show() +{ + if (visible) { + return; + } + visible = true; + xcb_map_window(connection, window); + xcb_flush(connection); +} + +void X11Window::hide() +{ + if (!visible) { + return; + } + visible = false; + xcb_unmap_window(connection, window); + xcb_flush(connection); +} + +void X11Window::draw() +{ + if (!xcb_image) { + return; + } + xcb_image_put(connection, window, gc, xcb_image.get(), 0, 0, 0); +} + +void X11Window::generate_frame() +{ + xcb_image.reset(xcb_image_create_native(connection, image->width(), image->height(), XCB_IMAGE_FORMAT_Z_PIXMAP, + screen->root_depth, nullptr, 0, nullptr)); + xcb_image->data = const_cast(image->data()); + send_expose_event(); +} + +X11Window::~X11Window() +{ + xcb_destroy_window(connection, window); + xcb_free_gc(connection, gc); + xcb_flush(connection); +} + +void X11Window::send_expose_event() +{ + const int event_size = 32; + std::array buffer; + auto *event = reinterpret_cast(buffer.data()); + event->response_type = XCB_EXPOSE; + event->window = window; + xcb_send_event(connection, 0, window, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast(event)); + xcb_flush(connection); +} diff --git a/src/canvas/x11/window/x11.hpp b/src/canvas/x11/window/x11.hpp new file mode 100644 index 0000000..5230b1d --- /dev/null +++ b/src/canvas/x11/window/x11.hpp @@ -0,0 +1,60 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef X11_WINDOW_H +#define X11_WINDOW_H + +#include "image.hpp" +#include "util/ptr.hpp" +#include "window.hpp" + +#include +#include + +class Dimensions; + +class X11Window : public Window +{ +public: + X11Window(xcb_connection_t* connection, xcb_screen_t *screen, + xcb_window_t window, xcb_window_t parent, std::shared_ptr image); + ~X11Window() override; + + void draw() override; + void generate_frame() override; + void show() override; + void hide() override; + +private: + xcb_connection_t *connection; + xcb_screen_t *screen; + + xcb_window_t window; + xcb_window_t parent; + xcb_gcontext_t gc; + + c_unique_ptr xcb_image; + std::shared_ptr logger; + std::shared_ptr image; + + bool visible = false; + + void send_expose_event(); + void create(); + void change_title(); +}; + +#endif diff --git a/src/canvas/x11/window/x11egl.cpp b/src/canvas/x11/window/x11egl.cpp new file mode 100644 index 0000000..c494957 --- /dev/null +++ b/src/canvas/x11/window/x11egl.cpp @@ -0,0 +1,138 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "x11egl.hpp" +#include "dimensions.hpp" +#include "image.hpp" +#include "util.hpp" + +#include + +X11EGLWindow::X11EGLWindow(xcb_connection_t *connection, xcb_screen_t *screen, xcb_window_t windowid, + xcb_window_t parentid, const EGLUtil *egl, + std::shared_ptr new_image) + : connection(connection), + screen(screen), + windowid(windowid), + parentid(parentid), + image(std::move(new_image)), + egl(egl) +{ + logger = spdlog::get("x11"); + create(); + opengl_setup(); +} + +X11EGLWindow::~X11EGLWindow() +{ + egl->run_contained(egl_surface, egl_context, [this] { + glDeleteTextures(1, &texture); + glDeleteFramebuffers(1, &fbo); + }); + eglDestroySurface(egl->display, egl_surface); + eglDestroyContext(egl->display, egl_context); + + xcb_destroy_window(connection, windowid); + xcb_flush(connection); +} + +void X11EGLWindow::create() +{ + const uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP; + struct xcb_create_window_value_list_t value_list; + value_list.background_pixel = screen->black_pixel; + value_list.border_pixel = screen->black_pixel; + value_list.event_mask = XCB_EVENT_MASK_EXPOSURE; + value_list.colormap = screen->default_colormap; + + const auto dimensions = image->dimensions(); + const auto xcoord = static_cast(dimensions.xpixels() + dimensions.padding_horizontal); + const auto ycoord = static_cast(dimensions.ypixels() + dimensions.padding_vertical); + xcb_create_window_aux(connection, screen->root_depth, windowid, parentid, xcoord, ycoord, image->width(), + image->height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, value_mask, + &value_list); +} + +void X11EGLWindow::opengl_setup() +{ + egl_surface = egl->create_surface(&windowid); + if (egl_surface == EGL_NO_SURFACE) { + throw std::runtime_error(""); + } + + egl_context = egl->create_context(egl_surface); + if (egl_context == EGL_NO_CONTEXT) { + throw std::runtime_error(""); + } + + egl->run_contained(egl_surface, egl_context, [this] { + glGenFramebuffers(1, &fbo); + glGenTextures(1, &texture); + }); +} + +void X11EGLWindow::draw() +{ + const std::scoped_lock lock{egl_mutex}; + egl->run_contained(egl_surface, egl_context, [this] { + glBlitFramebuffer(0, 0, image->width(), image->height(), 0, 0, image->width(), image->height(), + GL_COLOR_BUFFER_BIT, GL_NEAREST); + eglSwapBuffers(egl->display, egl_surface); + }); +} + +void X11EGLWindow::generate_frame() +{ + const std::scoped_lock lock{egl_mutex}; + egl->run_contained(egl_surface, egl_context, [this] { + egl->get_texture_from_image(*image, texture); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + }); + + send_expose_event(); +} + +void X11EGLWindow::show() +{ + if (visible) { + return; + } + visible = true; + xcb_map_window(connection, windowid); + xcb_flush(connection); +} + +void X11EGLWindow::hide() +{ + if (!visible) { + return; + } + visible = false; + xcb_unmap_window(connection, windowid); + xcb_flush(connection); +} + +void X11EGLWindow::send_expose_event() +{ + const int event_size = 32; + std::array buffer; + auto *event = reinterpret_cast(buffer.data()); + event->response_type = XCB_EXPOSE; + event->window = windowid; + xcb_send_event(connection, 0, windowid, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast(event)); + xcb_flush(connection); +} diff --git a/src/canvas/x11/window/x11egl.hpp b/src/canvas/x11/window/x11egl.hpp new file mode 100644 index 0000000..09fdf82 --- /dev/null +++ b/src/canvas/x11/window/x11egl.hpp @@ -0,0 +1,68 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef X11_EGL_WINDOW_H +#define X11_EGL_WINDOW_H + +#include "window.hpp" +#include "util/egl.hpp" + +#include +#include + +#include +#include + +class Image; + +class X11EGLWindow : public Window +{ +public: + X11EGLWindow(xcb_connection_t* connection, xcb_screen_t* screen, + xcb_window_t windowid, xcb_window_t parentid, const EGLUtil* egl, + std::shared_ptr new_image); + ~X11EGLWindow() override; + + void draw() override; + void generate_frame() override; + void show() override; + void hide() override; + +private: + xcb_connection_t* connection; + xcb_screen_t* screen; + xcb_window_t windowid; + xcb_window_t parentid; + std::shared_ptr image; + const EGLUtil* egl; + + GLuint texture; + GLuint fbo; + EGLContext egl_context; + EGLSurface egl_surface; + + std::mutex egl_mutex; + std::shared_ptr logger; + + bool visible = false; + + void send_expose_event(); + void create(); + void change_title(); + void opengl_setup(); +}; + +#endif diff --git a/src/canvas/x11/x11.cpp b/src/canvas/x11/x11.cpp new file mode 100644 index 0000000..5f1a04c --- /dev/null +++ b/src/canvas/x11/x11.cpp @@ -0,0 +1,257 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "x11.hpp" +#include "application.hpp" +#include "flags.hpp" +#include "os.hpp" +#include "tmux.hpp" +#include "util.hpp" + +#include + +#include + +#ifdef ENABLE_OPENGL +# include "window/x11egl.hpp" +# include +#endif +#include "window/x11.hpp" + +X11Canvas::X11Canvas() + : connection(xcb_connect(nullptr, nullptr)) +{ + if (xcb_connection_has_error(connection) > 0) { + throw std::runtime_error("Can't connect to X11 server"); + } + screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data; + +#ifdef ENABLE_XCB_ERRORS + xcb_errors_context_new(connection, &err_ctx); +#endif + + flags = Flags::instance(); + +#ifdef ENABLE_OPENGL + if (flags->use_opengl) { + try { + egl = std::make_unique>(EGL_PLATFORM_XCB_EXT, connection); + } catch (const std::runtime_error &err) { + egl_available = false; + } + } else { + egl_available = false; + } +#endif + + xutil = std::make_unique(connection); + logger = spdlog::get("X11"); + event_handler = std::thread(&X11Canvas::handle_events, this); + logger->info("Canvas created"); +} + +X11Canvas::~X11Canvas() +{ + draw_threads.clear(); + windows.clear(); + image_windows.clear(); + + if (event_handler.joinable()) { + event_handler.join(); + } + +#ifdef ENABLE_XCB_ERRORS + xcb_errors_context_free(err_ctx); +#endif + + xcb_disconnect(connection); +} + +void X11Canvas::draw(const std::string &identifier) +{ + if (!images.at(identifier)->is_animated()) { + for (const auto &[wid, window] : image_windows.at(identifier)) { + window->generate_frame(); + } + return; + } + + draw_threads.insert_or_assign(identifier, std::jthread([this, identifier](const std::stop_token &stoken) { + const auto image = images.at(identifier); + const auto wins = image_windows.at(identifier); + while (!stoken.stop_requested()) { + for (const auto &[wid, window] : wins) { + window->generate_frame(); + } + image->next_frame(); + std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay())); + } + })); +} + +void X11Canvas::show() +{ + const std::scoped_lock lock{windows_mutex}; + for (const auto &[wid, window] : windows) { + window->show(); + } +} + +void X11Canvas::hide() +{ + const std::scoped_lock lock{windows_mutex}; + for (const auto &[wid, window] : windows) { + window->hide(); + } +} + +void X11Canvas::handle_events() +{ + const int event_mask = 0x80; + const int waitms = 100; + const int connfd = xcb_get_file_descriptor(connection); + bool status = false; + + while (!Application::stop_flag) { + try { + status = os::wait_for_data_on_fd(connfd, waitms); + } catch (const std::system_error &err) { + Application::stop_flag = true; + break; + } + + if (!status) { + continue; + } + + const std::scoped_lock lock{windows_mutex}; + auto event = unique_C_ptr{xcb_poll_for_event(connection)}; + while (event) { + const int real_event = event->response_type & ~event_mask; + switch (real_event) { + case 0: { + const auto *err = reinterpret_cast(event.get()); + print_xcb_error(err); + break; + } + case XCB_EXPOSE: { + const auto *expose = reinterpret_cast(event.get()); + try { + logger->debug("Received expose event for window {}", expose->window); + const auto window = windows.at(expose->window); + window->draw(); + } catch (const std::out_of_range &oor) { + logger->debug("Discarding expose event for window {}", expose->window); + } + break; + } + default: { + logger->debug("Received unknown event {}", real_event); + break; + } + } + event.reset(xcb_poll_for_event(connection)); + } + } +} + +void X11Canvas::add_image(const std::string &identifier, std::unique_ptr new_image) +{ + remove_image(identifier); + + logger->debug("Initializing canvas"); + images.insert({identifier, std::move(new_image)}); + image_windows.insert({identifier, {}}); + + const auto image = images.at(identifier); + const auto dims = image->dimensions(); + std::unordered_set parent_ids{dims.terminal->x11_wid}; + get_tmux_window_ids(parent_ids); + + ranges::for_each(parent_ids, [this, &identifier, &image](xcb_window_t parent) { + const auto window_id = xcb_generate_id(connection); + std::shared_ptr window; +#ifdef ENABLE_OPENGL + if (egl_available) { + try { + window = std::make_shared(connection, screen, window_id, parent, egl.get(), image); + } catch (const std::runtime_error &err) { + return; + } + } +#endif + if (window == nullptr) { + window = std::make_shared(connection, screen, window_id, parent, image); + } + windows.insert({window_id, window}); + image_windows.at(identifier).insert({window_id, window}); + window->show(); + }); + + draw(identifier); +} + +void X11Canvas::get_tmux_window_ids(std::unordered_set &windows) +{ + const auto pids = tmux::get_client_pids(); + if (!pids.has_value()) { + return; + } + const auto pid_window_map = xutil->get_pid_window_map(); + for (const auto pid : pids.value()) { + const auto ppids = util::get_process_tree(pid); + for (const auto ppid : ppids) { + const auto win = pid_window_map.find(ppid); + if (win == pid_window_map.end()) { + continue; + } + windows.insert(win->second); + break; // prevent multiple windows being created + } + } +} + +void X11Canvas::print_xcb_error(const xcb_generic_error_t *err) +{ +#ifdef ENABLE_XCB_ERRORS + const char *extension = nullptr; + const char *major = xcb_errors_get_name_for_major_code(err_ctx, err->major_code); + const char *minor = xcb_errors_get_name_for_minor_code(err_ctx, err->major_code, err->minor_code); + const char *error = xcb_errors_get_name_for_error(err_ctx, err->error_code, &extension); + + const std::string_view ext_str = extension != nullptr ? extension : "no_extension"; + const std::string_view minor_str = minor != nullptr ? minor : "no_minor"; + logger->error("XCB: {}:{}, {}:{}, resource {} sequence {}", error, ext_str, major, minor_str, err->resource_id, + err->sequence); +#else + logger->error("XCB: resource {} sequence {}", err->resource_id, err->sequence); +#endif +} + +void X11Canvas::remove_image(const std::string &identifier) +{ + draw_threads.erase(identifier); + images.erase(identifier); + + const std::scoped_lock lock{windows_mutex}; + const auto old_windows = image_windows.extract(identifier); + if (old_windows.empty()) { + return; + } + for (const auto &[key, value] : old_windows.mapped()) { + windows.erase(key); + } +} diff --git a/src/canvas/x11/x11.hpp b/src/canvas/x11/x11.hpp new file mode 100644 index 0000000..09d99a3 --- /dev/null +++ b/src/canvas/x11/x11.hpp @@ -0,0 +1,93 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef X11_CANVAS_H +#define X11_CANVAS_H + +#include "canvas.hpp" +#include "image.hpp" +#include "window.hpp" +#include "dimensions.hpp" +#include "util/x11.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#ifdef ENABLE_XCB_ERRORS +# include +#endif + +#ifdef ENABLE_OPENGL +# include "util/egl.hpp" +#endif + +class Flags; + +class X11Canvas : public Canvas +{ +public: + explicit X11Canvas(); + ~X11Canvas() override; + + void add_image(const std::string& identifier, std::unique_ptr new_image) override; + void remove_image(const std::string& identifier) override; + void hide() override; + void show() override; + +private: + xcb_connection_t *connection; + xcb_screen_t *screen; + +#ifdef ENABLE_XCB_ERRORS + xcb_errors_context_t *err_ctx; +#endif + + std::unique_ptr xutil; + + // map for event handler + std::unordered_map> windows; + + // windows per image + std::unordered_map>> image_windows; + + std::unordered_map> images; + std::unordered_map draw_threads; + + std::thread event_handler; + std::mutex windows_mutex; + + std::shared_ptr logger; + std::shared_ptr flags; + +#ifdef ENABLE_OPENGL + std::unique_ptr> egl; + bool egl_available = true; +#endif + + void draw(const std::string& identifier); + void handle_events(); + void get_tmux_window_ids(std::unordered_set& windows); + void print_xcb_error(const xcb_generic_error_t* err); +}; + +#endif diff --git a/src/dimensions.cpp b/src/dimensions.cpp new file mode 100644 index 0000000..ecbe851 --- /dev/null +++ b/src/dimensions.cpp @@ -0,0 +1,62 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "dimensions.hpp" +#include "tmux.hpp" +#include "terminal.hpp" + +#include + +Dimensions::Dimensions(const Terminal* terminal, uint16_t xcoord, + uint16_t ycoord, int max_w, int max_h, std::string scaler): +max_w(max_w), +max_h(max_h), +padding_horizontal(terminal->padding_horizontal), +padding_vertical(terminal->padding_vertical), +scaler(std::move(scaler)), +terminal(terminal), +orig_x(xcoord), +orig_y(ycoord) +{ + read_offsets(); +} + +void Dimensions::read_offsets() +{ + const auto [offset_x, offset_y] = tmux::get_offset(); + x = orig_x + offset_x; + y = orig_y + offset_y; +} + +auto Dimensions::xpixels() const -> int +{ + return x * terminal->font_width; +} + +auto Dimensions::ypixels() const -> int +{ + return y * terminal->font_height; +} + +auto Dimensions::max_wpixels() const -> int +{ + return max_w * terminal->font_width; +} + +auto Dimensions::max_hpixels() const -> int +{ + return max_h * terminal->font_height; +} diff --git a/src/flags.cpp b/src/flags.cpp new file mode 100644 index 0000000..4369794 --- /dev/null +++ b/src/flags.cpp @@ -0,0 +1,52 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "flags.hpp" +#include "os.hpp" +#include "util.hpp" + +#include +#include +#include + +namespace fs = std::filesystem; +using json = nlohmann::json; + +// read configuration file +Flags::Flags() +{ + const auto home = os::getenv("HOME").value_or(util::temp_directory_path()); + const auto config_home = os::getenv("XDG_CONFIG_HOME").value_or(fmt::format("{}/.config", home)); + config_file = fmt::format("{}/ueberzugpp/config.json", config_home); + if (fs::exists(config_file)) { + read_config_file(); + } +} + +void Flags::read_config_file() +{ + std::ifstream ifs(config_file); + const auto data = json::parse(ifs); + if (!data.contains("layer")) { + return; + } + const auto &layer = data.at("layer"); + silent = layer.value("silent", false); + output = layer.value("output", ""); + no_cache = layer.value("no-cache", false); + no_opencv = layer.value("no-opencv", false); + use_opengl = layer.value("opengl", false); +} diff --git a/src/image.cpp b/src/image.cpp new file mode 100644 index 0000000..fae3ef6 --- /dev/null +++ b/src/image.cpp @@ -0,0 +1,185 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "image.hpp" +#ifdef ENABLE_OPENCV +# include "image/opencv.hpp" +#endif +#include "dimensions.hpp" +#include "flags.hpp" +#include "image/libvips.hpp" +#include "util.hpp" + +#ifdef ENABLE_OPENCV +# include +#endif +#include +#include + +namespace fs = std::filesystem; +using njson = nlohmann::json; + +auto Image::load(const njson &command, const Terminal *terminal) -> std::unique_ptr +{ + const fs::path &filename = command.at("path"); + if (!fs::exists(filename)) { + return nullptr; + } + const auto flags = Flags::instance(); + const auto logger = spdlog::get("main"); + std::shared_ptr dimensions; + try { + dimensions = get_dimensions(command, terminal); + } catch (const std::exception &) { + logger->error("Could not parse dimensions from command"); + return nullptr; + } + std::string image_path = filename; + bool in_cache = false; + if (!flags->no_cache) { + image_path = check_cache(*dimensions, filename); + in_cache = image_path != filename; + } + +#ifdef ENABLE_OPENCV + if (cv::haveImageReader(image_path) && !flags->no_opencv) { + try { + return std::make_unique(dimensions, image_path, in_cache); + } catch (const std::runtime_error &) { + return nullptr; + } + } +#endif + const auto *vips_loader = vips_foreign_find_load(image_path.c_str()); + if (vips_loader != nullptr) { + try { + return std::make_unique(dimensions, image_path, in_cache); + } catch (const vips::VError &) { + return nullptr; + } + } + return nullptr; +} + +auto Image::check_cache(const Dimensions &dimensions, const fs::path &orig_path) -> std::string +{ + const fs::path cache_path = util::get_cache_file_save_location(orig_path); + if (!fs::exists(cache_path)) { + return orig_path; + } + + vips::VImage cache_img; + try { + cache_img = vips::VImage::new_from_file(cache_path.c_str()); + } catch (const vips::VError &) { + return orig_path; + } + const uint32_t img_width = cache_img.width(); + const uint32_t img_height = cache_img.height(); + const uint32_t dim_width = dimensions.max_wpixels(); + const uint32_t dim_height = dimensions.max_hpixels(); + const int delta = 10; + + if ((dim_width >= img_width && dim_height >= img_height) && + ((dim_width - img_width) <= delta || (dim_height - img_height) <= delta)) { + return cache_path; + } + return orig_path; +} + +auto Image::get_new_sizes(double max_width, double max_height, std::string_view scaler, int scale_factor) const + -> std::pair +{ + int img_width = width(); + int img_height = height(); + int new_width = img_width; + int new_height = img_height; + double new_scale = 0; + double width_scale = 0; + double height_scale = 0; + double min_scale = 0; + double max_scale = 0; + + if (scaler == "fit_contain" || scaler == "forced_cover") { + // I believe these should work the same + new_scale = max_height / img_height; + if (img_width >= img_height) { + new_scale = max_width / img_width; + } + new_width = static_cast(img_width * new_scale); + new_height = static_cast(img_height * new_scale); + new_scale = 1; + } + + if (new_height > max_height) { + if (new_width > max_width) { + width_scale = max_width / new_width; + height_scale = max_height / new_height; + min_scale = std::min(width_scale, height_scale); + max_scale = std::max(width_scale, height_scale); + if (new_width * max_scale <= max_width && new_height * max_scale <= max_height) { + new_scale = max_scale; + } else { + new_scale = min_scale; + } + } else { + new_scale = max_height / new_height; + } + } else if (new_width > max_width) { + new_scale = max_width / new_width; + } + if (new_scale != 1) { + new_width = static_cast(new_width * new_scale); + new_height = static_cast(new_height * new_scale); + } + + return std::make_pair(util::round_up(new_width, scale_factor), util::round_up(new_height, scale_factor)); +} + +auto Image::get_dimensions(const njson &json, const Terminal *terminal) -> std::shared_ptr +{ + using std::string; + int xcoord = 0; + int ycoord = 0; + int max_width = 0; + int max_height = 0; + string width_key = "max_width"; + string height_key = "max_height"; + const string scaler = json.value("scaler", "contain"); + if (json.contains("width")) { + width_key = "width"; + height_key = "height"; + } + if (json.at(width_key).is_string()) { + const string &width = json.at(width_key); + const string &height = json.at(height_key); + max_width = std::stoi(width); + max_height = std::stoi(height); + } else { + max_width = json.at(width_key); + max_height = json.at(height_key); + } + if (json.at("x").is_string()) { + const string &xcoords = json.at("x"); + const string &ycoords = json.at("y"); + xcoord = std::stoi(xcoords); + ycoord = std::stoi(ycoords); + } else { + xcoord = json.at("x"); + ycoord = json.at("y"); + } + return std::make_shared(terminal, xcoord, ycoord, max_width, max_height, scaler); +} diff --git a/src/image/libvips.cpp b/src/image/libvips.cpp new file mode 100644 index 0000000..c7ecffc --- /dev/null +++ b/src/image/libvips.cpp @@ -0,0 +1,216 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "libvips.hpp" +#include "dimensions.hpp" +#include "flags.hpp" +#include "terminal.hpp" +#include "util.hpp" + +#include +#include + +#ifdef ENABLE_OPENCV +# include +#endif + +using vips::VError; +using vips::VImage; + +LibvipsImage::LibvipsImage(std::shared_ptr new_dims, const std::string &filename, bool in_cache) + : path(filename), + dims(std::move(new_dims)), + max_width(dims->max_wpixels()), + max_height(dims->max_hpixels()), + in_cache(in_cache) +{ + image = VImage::new_from_file(path.c_str()).colourspace(VIPS_INTERPRETATION_sRGB); + flags = Flags::instance(); + logger = spdlog::get("vips"); + logger->info("loading file {}", filename); + + try { + // animated images should have both n-pages and delay + npages = image.get_int("n-pages"); + std::ignore = image.get_array_int("delay"); + is_anim = true; + logger->info("file is an animated image"); + auto *opts = VImage::option()->set("n", -1); + backup = VImage::new_from_file(filename.c_str(), opts).colourspace(VIPS_INTERPRETATION_sRGB); + orig_height = backup.height() / npages; + image = backup.crop(0, 0, backup.width(), orig_height); + } catch (const VError &err) { + logger->debug("Failed to process image animation"); + } + + if (!is_anim) { + image = image.autorot(); + } + process_image(); +} + +auto LibvipsImage::dimensions() const -> const Dimensions & +{ + return *dims; +} + +auto LibvipsImage::filename() const -> std::string +{ + return path.string(); +} + +auto LibvipsImage::width() const -> int +{ + return image.width(); +} + +auto LibvipsImage::height() const -> int +{ + return image.height(); +} + +auto LibvipsImage::size() const -> size_t +{ + return _size; +} + +auto LibvipsImage::data() const -> const unsigned char * +{ + return _data.get(); +} + +auto LibvipsImage::channels() const -> int +{ + return image.bands(); +} + +auto LibvipsImage::is_animated() const -> bool +{ + return is_anim; +} + +auto LibvipsImage::next_frame() -> void +{ + if (!is_anim) { + return; + } + top += orig_height; + if (top == backup.height()) { + top = 0; + } + image = backup.crop(0, top, backup.width(), orig_height); + process_image(); +} + +auto LibvipsImage::frame_delay() const -> int +{ + if (!is_anim) { + return -1; + } + try { + const auto delays = backup.get_array_int("delay"); + const int ms_per_sec = 1000; + if (delays.at(0) == 0) { +#ifdef ENABLE_OPENCV + const cv::VideoCapture video(path); + if (video.isOpened()) { + return static_cast((1.0 / video.get(cv::CAP_PROP_FPS)) * ms_per_sec); + } +#endif + return static_cast((1.0 / npages) * ms_per_sec); + } + return delays.at(0); + } catch (const VError &err) { + return -1; + } +} + +auto LibvipsImage::resize_image() -> void +{ + if (in_cache) { + return; + } + const auto [new_width, new_height] = get_new_sizes(max_width, max_height, dims->scaler, flags->scale_factor); + if (new_width <= 0 && new_height <= 0) { + // ensure width and height are pair + if (flags->needs_scaling) { + const auto curw = width(); + const auto curh = height(); + if ((curw % 2) != 0 || (curh % 2) != 0) { + auto *opts = VImage::option() + ->set("height", util::round_up(curh, flags->scale_factor)) + ->set("size", VIPS_SIZE_FORCE); + image = image.thumbnail_image(util::round_up(curw, flags->scale_factor), opts); + } + } + return; + } + + logger->debug("Resizing image"); + + auto *opts = VImage::option()->set("height", new_height)->set("size", VIPS_SIZE_FORCE); + image = image.thumbnail_image(new_width, opts); + + if (is_anim || flags->no_cache) { + return; + } + + const auto save_location = util::get_cache_file_save_location(path); + try { + image.write_to_file(save_location.c_str()); + logger->debug("Saved resized image"); + } catch (const VError &err) { + logger->debug("Could not save resized image"); + } +} + +auto LibvipsImage::process_image() -> void +{ + resize_image(); + if (flags->origin_center) { + const double img_width = static_cast(width()) / dims->terminal->font_width; + const double img_height = static_cast(height()) / dims->terminal->font_height; + dims->x -= std::floor(img_width / 2); + dims->y -= std::floor(img_height / 2); + } + + const std::unordered_set bgra_trifecta = {"x11", "chafa", "wayland"}; + +#ifdef ENABLE_OPENGL + if (flags->use_opengl) { + image = image.flipver(); + } +#endif + + if (bgra_trifecta.contains(flags->output)) { + // alpha channel required + if (!image.has_alpha()) { + const int alpha_value = 255; + image = image.bandjoin(alpha_value); + } + // convert from RGB to BGR + auto bands = image.bandsplit(); + std::swap(bands[0], bands[2]); + image = VImage::bandjoin(bands); + } else if (flags->output == "sixel") { + // sixel expects RGB888 + if (image.has_alpha()) { + image = image.flatten(); + } + } + _size = VIPS_IMAGE_SIZEOF_IMAGE(image.get_image()); + _data.reset(static_cast(image.write_to_memory(&_size))); +} diff --git a/src/image/libvips.hpp b/src/image/libvips.hpp new file mode 100644 index 0000000..76cc731 --- /dev/null +++ b/src/image/libvips.hpp @@ -0,0 +1,71 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef LIBVIPS_IMAGE_H +#define LIBVIPS_IMAGE_H + +#include "image.hpp" +#include "util/ptr.hpp" + +#include +#include +#include +#include + +class LibvipsImage : public Image +{ + public: + LibvipsImage(std::shared_ptr new_dims, const std::string &filename, bool in_cache); + + [[nodiscard]] auto dimensions() const -> const Dimensions & override; + [[nodiscard]] auto width() const -> int override; + [[nodiscard]] auto height() const -> int override; + [[nodiscard]] auto size() const -> size_t override; + [[nodiscard]] auto data() const -> const unsigned char * override; + [[nodiscard]] auto channels() const -> int override; + + void next_frame() override; + [[nodiscard]] auto frame_delay() const -> int override; + [[nodiscard]] auto is_animated() const -> bool override; + [[nodiscard]] auto filename() const -> std::string override; + + private: + vips::VImage image; + vips::VImage backup; + + c_unique_ptr _data; + std::filesystem::path path; + std::shared_ptr dims; + + std::shared_ptr flags; + std::shared_ptr logger; + + uint32_t max_width; + uint32_t max_height; + size_t _size = 0; + + // for animated pictures + int top = 0; + int orig_height; + int npages = 0; + bool is_anim = false; + bool in_cache; + + void process_image(); + void resize_image(); +}; + +#endif diff --git a/src/image/opencv.cpp b/src/image/opencv.cpp new file mode 100644 index 0000000..60c2425 --- /dev/null +++ b/src/image/opencv.cpp @@ -0,0 +1,249 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "opencv.hpp" +#include "dimensions.hpp" +#include "flags.hpp" +#include "terminal.hpp" +#include "util.hpp" + +#include +#include + +#include +#include +#include + +enum { + EXIF_ORIENTATION_2 = 2, + EXIF_ORIENTATION_3, + EXIF_ORIENTATION_4, + EXIF_ORIENTATION_5, + EXIF_ORIENTATION_6, + EXIF_ORIENTATION_7, + EXIF_ORIENTATION_8, +}; + +OpencvImage::OpencvImage(std::shared_ptr new_dims, const std::string &filename, bool in_cache) + : path(filename), + dims(std::move(new_dims)), + max_width(dims->max_wpixels()), + max_height(dims->max_hpixels()), + in_cache(in_cache) +{ + logger = spdlog::get("opencv"); + image = cv::imread(filename, cv::IMREAD_UNCHANGED); + + if (image.empty()) { + logger->warn("unable to read image"); + throw std::runtime_error(""); + } + logger->info("loading file {}", filename); + flags = Flags::instance(); + + rotate_image(); + process_image(); +} + +auto OpencvImage::filename() const -> std::string +{ + return path.string(); +} + +auto OpencvImage::dimensions() const -> const Dimensions & +{ + return *dims; +} + +auto OpencvImage::width() const -> int +{ + return image.cols; +} + +auto OpencvImage::height() const -> int +{ + return image.rows; +} + +auto OpencvImage::size() const -> size_t +{ + return _size; +} + +auto OpencvImage::data() const -> const unsigned char * +{ + return image.data; +} + +auto OpencvImage::channels() const -> int +{ + return image.channels(); +} + +void OpencvImage::wayland_processing() +{ + if (flags->output != "wayland") { + return; + } +} + +void OpencvImage::rotate_image() +{ + const auto rotation = util::read_exif_rotation(path); + if (!rotation.has_value()) { + return; + } + const auto value = rotation.value(); + + // kudos https://jdhao.github.io/2019/07/31/image_rotation_exif_info/ + switch (value) { + case EXIF_ORIENTATION_2: + cv::flip(image, image, 1); + break; + case EXIF_ORIENTATION_3: + cv::flip(image, image, -1); + break; + case EXIF_ORIENTATION_4: + cv::flip(image, image, 0); + break; + case EXIF_ORIENTATION_5: + cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE); + cv::flip(image, image, 1); + break; + case EXIF_ORIENTATION_6: + cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE); + break; + case EXIF_ORIENTATION_7: + cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); + cv::flip(image, image, 1); + break; + case EXIF_ORIENTATION_8: + cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); + break; + default: + break; + } +} + +// only use opencl if required +auto OpencvImage::resize_image() -> void +{ + if (in_cache) { + return; + } + const auto [new_width, new_height] = get_new_sizes(max_width, max_height, dims->scaler, flags->scale_factor); + if (new_width <= 0 && new_height <= 0) { + // ensure width and height are pair + if (flags->needs_scaling) { + const auto curw = width(); + const auto curh = height(); + if ((curw % 2) != 0 || (curh % 2) != 0) { + resize_image_helper(image, util::round_up(curw, flags->scale_factor), + util::round_up(curh, flags->scale_factor)); + } + } + return; + } + + const auto opencl_ctx = cv::ocl::Context::getDefault(); + opencl_available = opencl_ctx.ptr() != nullptr; + + if (opencl_available) { + logger->debug("OpenCL is available"); + image.copyTo(uimage); + resize_image_helper(uimage, new_width, new_height); + uimage.copyTo(image); + } else { + resize_image_helper(image, new_width, new_height); + } +} + +void OpencvImage::resize_image_helper(cv::InputOutputArray &mat, int new_width, int new_height) +{ + logger->debug("Resizing image"); + cv::resize(mat, mat, cv::Size(new_width, new_height), 0, 0, cv::INTER_AREA); + + if (flags->no_cache) { + logger->debug("Caching is disabled"); + return; + } + + const auto save_location = util::get_cache_file_save_location(path); + try { + cv::imwrite(save_location, mat); + logger->debug("Saved resized image"); + } catch (const cv::Exception &ex) { + logger->error("Could not save image"); + } +} + +void OpencvImage::process_image() +{ + resize_image(); + if (flags->origin_center) { + const double img_width = static_cast(width()) / dims->terminal->font_width; + const double img_height = static_cast(height()) / dims->terminal->font_height; + dims->x -= std::floor(img_width / 2); + dims->y -= std::floor(img_height / 2); + } + + const std::unordered_set bgra_trifecta = {"x11", "chafa", "wayland"}; + + if (image.depth() == CV_16U) { + const float alpha = 0.00390625; // 1 / 256 + image.convertTo(image, CV_8U, alpha); + } + + if (image.channels() == 4) { + // premultiply alpha + image.forEach([](cv::Vec4b &pix, const int *) { + const uint8_t alpha = pix[3]; + const uint8_t div = 255; + pix[0] = (pix[0] * alpha) / div; + pix[1] = (pix[1] * alpha) / div; + pix[2] = (pix[2] * alpha) / div; + }); + } + +#ifdef ENABLE_OPENGL + if (flags->use_opengl) { + cv::flip(image, image, 0); + } +#endif + + if (image.channels() == 1) { + cv::cvtColor(image, image, cv::COLOR_GRAY2BGRA); + } + + if (bgra_trifecta.contains(flags->output)) { + if (image.channels() == 3) { + cv::cvtColor(image, image, cv::COLOR_BGR2BGRA); + } + } else if (flags->output == "kitty") { + if (image.channels() == 4) { + cv::cvtColor(image, image, cv::COLOR_BGRA2RGBA); + } else { + cv::cvtColor(image, image, cv::COLOR_BGR2RGB); + } + } else if (flags->output == "sixel") { + if (image.channels() == 4) { + cv::cvtColor(image, image, cv::COLOR_BGRA2RGB); + } else { + cv::cvtColor(image, image, cv::COLOR_BGR2RGB); + } + } + _size = image.total() * image.elemSize(); +} diff --git a/src/image/opencv.hpp b/src/image/opencv.hpp new file mode 100644 index 0000000..6214abc --- /dev/null +++ b/src/image/opencv.hpp @@ -0,0 +1,68 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef OPENCV_IMAGE_H +#define OPENCV_IMAGE_H + +#include "image.hpp" + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +class OpencvImage : public Image +{ + public: + OpencvImage(std::shared_ptr new_dims, const std::string &filename, bool in_cache); + ~OpencvImage() override = default; + + [[nodiscard]] auto dimensions() const -> const Dimensions & override; + [[nodiscard]] auto width() const -> int override; + [[nodiscard]] auto height() const -> int override; + [[nodiscard]] auto size() const -> size_t override; + [[nodiscard]] auto data() const -> const unsigned char * override; + [[nodiscard]] auto channels() const -> int override; + + [[nodiscard]] auto filename() const -> std::string override; + + private: + cv::Mat image; + cv::UMat uimage; + + fs::path path; + std::shared_ptr dims; + + uint64_t _size = 0; + uint32_t max_width; + uint32_t max_height; + bool in_cache; + bool opencl_available = false; + + std::shared_ptr logger; + std::shared_ptr flags; + + void process_image(); + void resize_image(); + void resize_image_helper(cv::InputOutputArray &mat, int new_width, int new_height); + + void rotate_image(); + void wayland_processing(); +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..dbc83a8 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,151 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#include +#include +#include +#include +#include + +#include +#if (CLI11_VERSION_MAJOR >= 3) || (CLI11_VERSION_MAJOR == 2 && CLI11_VERSION_MINOR >= 6) +// CLI11 2.6.0 and beyond requires this header file for the CLI::IsMember validator. +# include +#endif + +#include "application.hpp" +#include "flags.hpp" +#include "tmux.hpp" +#include "util.hpp" + +void signal_handler(const int signal) +{ + Application::stop_flag = true; + + const auto logger = spdlog::get("main"); + if (!logger) { + return; + } + switch (signal) { + case SIGINT: + logger->error("SIGINT received, exiting..."); + break; + case SIGTERM: + logger->error("SIGTERM received, exiting..."); + break; + case SIGHUP: + logger->error("SIGHUP received, exiting..."); + break; + default: + logger->error("UNKNOWN({}) signal received, exiting...", signal); + break; + } +} + +auto main(int argc, char *argv[]) -> int +{ + // handle signals + struct sigaction sga; + sga.sa_handler = signal_handler; + sigemptyset(&sga.sa_mask); + sga.sa_flags = 0; + sigaction(SIGINT, &sga, nullptr); + sigaction(SIGTERM, &sga, nullptr); + sigaction(SIGHUP, &sga, nullptr); + sigaction(SIGCHLD, nullptr, nullptr); + + spdlog::cfg::load_env_levels(); + + std::shared_ptr flags; + try { + flags = Flags::instance(); + } catch (const std::exception &e) { + std::cerr << "Could not parse config file: " << e.what() << std::endl; + return 1; + } + + CLI::App program("Display images in the terminal", "ueberzug"); + program.add_flag("-V,--version", flags->print_version, "Print version information."); + + auto *layer_command = program.add_subcommand("layer", "Display images on the terminal."); + layer_command->add_flag("-s,--silent", flags->silent, "Print stderr to /dev/null."); + layer_command + ->add_flag("--use-escape-codes", flags->use_escape_codes, "Use escape codes to get terminal capabilities.") + ->default_val(false); + layer_command->add_option("--pid-file", flags->pid_file, "Output file where to write the daemon PID."); + layer_command->add_flag("--no-stdin", flags->no_stdin, "Do not listen on stdin for commands.")->needs("--pid-file"); + layer_command->add_flag("--no-cache", flags->no_cache, "Disable caching of resized images."); + layer_command->add_flag("--no-opencv", flags->no_opencv, "Do not use OpenCV, use Libvips instead."); + layer_command->add_option("-o,--output", flags->output, "Image output method") + ->check(CLI::IsMember({"x11", "wayland", "sixel", "kitty", "iterm2", "chafa"})); + layer_command->add_flag("--origin-center", flags->origin_center, "Location of the origin wrt the image"); + layer_command->add_option("-p,--parser", nullptr, "**UNUSED**, only present for backwards compatibility."); + layer_command->add_option("-l,--loader", nullptr, "**UNUSED**, only present for backwards compatibility."); + + auto *cmd_comand = program.add_subcommand("cmd", "Send a command to a running ueberzugpp instance."); + cmd_comand->add_option("-s,--socket", flags->cmd_socket, "UNIX socket of running instance"); + cmd_comand->add_option("-i,--identifier", flags->cmd_id, "Preview identifier"); + cmd_comand->add_option("-a,--action", flags->cmd_action, "Action to send"); + cmd_comand->add_option("-f,--file", flags->cmd_file_path, "Path of image file"); + cmd_comand->add_option("-x,--xpos", flags->cmd_x, "X position of preview"); + cmd_comand->add_option("-y,--ypos", flags->cmd_y, "Y position of preview"); + cmd_comand->add_option("--max-width", flags->cmd_max_width, "Max width of preview"); + cmd_comand->add_option("--max-height", flags->cmd_max_height, "Max height of preview"); + + auto *tmux_command = program.add_subcommand("tmux", "Handle tmux hooks. Used internaly."); + tmux_command->allow_extras(); + + auto *query_win_command = + program.add_subcommand("query_windows", "**UNUSED**, only present for backwards compatibility."); + query_win_command->allow_extras(); + + CLI11_PARSE(program, argc, argv); + + if (query_win_command->parsed()) { + return 0; + } + + if (flags->print_version) { + Application::print_version(); + return 0; + } + + if (!layer_command->parsed() && !tmux_command->parsed() && !cmd_comand->parsed()) { + program.exit(CLI::CallForHelp()); + return 1; + } + + if (layer_command->parsed()) { + Application application(argv[0]); + application.command_loop(); + } + + if (tmux_command->parsed()) { + try { + const auto positionals = tmux_command->remaining(); + tmux::handle_hook(positionals.at(0), std::stoi(positionals.at(1))); + } catch (const std::out_of_range &oor) { + } + } + + if (cmd_comand->parsed()) { + util::send_command(*flags); + } + + return 0; +} diff --git a/src/os.cpp b/src/os.cpp new file mode 100644 index 0000000..af37a0e --- /dev/null +++ b/src/os.cpp @@ -0,0 +1,126 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "os.hpp" +#include "util/ptr.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +auto os::exec(const std::string &cmd) -> std::string +{ + const int bufsize = 128; + std::array buffer{}; + std::string result; + const c_unique_ptr pipe{popen(cmd.c_str(), "r")}; + if (!pipe) { + throw std::system_error(errno, std::generic_category()); + } + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + result.append(buffer.data()); + } + if (!result.empty()) { + result.erase(result.length() - 1); + } + return result; +} + +auto os::read_data_from_fd(int filde, char sep) -> std::string +{ + using std::errc; + std::string response; + char readch = 0; + + while (true) { + const auto status = read(filde, &readch, 1); + if (status == -1) { + throw std::system_error(errno, std::system_category()); + } + if (status == 0 || readch == sep) { + if (response.empty()) { + throw std::system_error(EIO, std::system_category()); + } + break; + } + response.push_back(readch); + } + return response; +} + +auto os::read_data_from_stdin(char sep) -> std::string +{ + return read_data_from_fd(STDIN_FILENO, sep); +} + +auto os::wait_for_data_on_fd(int filde, int waitms) -> bool +{ + struct pollfd fds; + fds.fd = filde; + fds.events = POLLIN; + + poll(&fds, 1, waitms); + + if ((fds.revents & (POLLERR | POLLNVAL | POLLHUP)) != 0) { + throw std::system_error(EIO, std::generic_category()); + } + + return (fds.revents & POLLIN) != 0; +} + +auto os::wait_for_data_on_stdin(int waitms) -> bool +{ + return wait_for_data_on_fd(STDIN_FILENO, waitms); +} + +auto os::getenv(const std::string &var) -> std::optional +{ + const char *env_p = std::getenv(var.c_str()); // NOLINT + if (env_p == nullptr) { + return {}; + } + return env_p; +} + +auto os::get_pid() -> int +{ + return getpid(); +} + +auto os::get_ppid() -> int +{ + return getppid(); +} + +void os::daemonize() +{ + const int pid = fork(); + if (pid < 0) { + std::exit(EXIT_FAILURE); // NOLINT + } + if (pid > 0) { + std::exit(EXIT_SUCCESS); // NOLINT + } + const int status = setsid(); + if (status < 0) { + std::exit(EXIT_FAILURE); // NOLINT + } +} diff --git a/src/process/apple.cpp b/src/process/apple.cpp new file mode 100644 index 0000000..bf9f831 --- /dev/null +++ b/src/process/apple.cpp @@ -0,0 +1,43 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "os.hpp" +#include "process.hpp" +#include "tmux.hpp" +#include "util.hpp" + +#include +#include +#include + +Process::Process(int pid) + : pid(pid) +{ + struct proc_bsdshortinfo sproc; + struct proc_bsdinfo proc; + + int status = proc_pidinfo(pid, PROC_PIDT_SHORTBSDINFO, 0, &sproc, PROC_PIDT_SHORTBSDINFO_SIZE); + if (status == PROC_PIDT_SHORTBSDINFO_SIZE) { + ppid = static_cast(sproc.pbsi_ppid); + } + + status = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &proc, PROC_PIDTBSDINFO_SIZE); + if (status == PROC_PIDTBSDINFO_SIZE) { + tty_nr = static_cast(proc.e_tdev); + minor_dev = minor(tty_nr); + pty_path = fmt::format("/dev/ttys{:0>3}", minor_dev); + } +} diff --git a/src/process/linux.cpp b/src/process/linux.cpp new file mode 100644 index 0000000..9dbe05c --- /dev/null +++ b/src/process/linux.cpp @@ -0,0 +1,39 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "process.hpp" + +#include +#include +#include +#if defined(__FreeBSD__) || defined(__NetBSD__) +# include +#else +# include +#endif + +constexpr auto max_size = std::numeric_limits::max(); + +Process::Process(int pid) + : pid(pid) +{ + const auto stat = fmt::format("/proc/{}/stat", pid); + std::ifstream ifs(stat); + ifs.ignore(max_size, ')'); // skip pid and executable name + ifs >> state >> ppid >> process_group_id >> session_id >> tty_nr; + minor_dev = minor(tty_nr); // NOLINT + pty_path = fmt::format("/dev/pts/{}", minor_dev); +} diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..e093238 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,335 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "terminal.hpp" +#include "flags.hpp" +#include "os.hpp" +#include "process.hpp" +#include "tmux.hpp" +#include "util.hpp" +#ifdef ENABLE_X11 +# include "util/x11.hpp" +#endif +#ifdef ENABLE_WAYLAND +# include "canvas/wayland/config.hpp" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +Terminal::Terminal() + : terminal_pid(pid) +{ + flags = Flags::instance(); + logger = spdlog::get("terminal"); + term = os::getenv("TERM").value_or("xterm-256color"); + term_program = os::getenv("TERM_PROGRAM").value_or(""); + logger->info("TERM = {}", term); + if (!term_program.empty()) { + logger->info("TERM_PROGRAM = {}", term_program); + } + open_first_pty(); + get_terminal_size(); + set_detected_output(); +} + +Terminal::~Terminal() +{ + if (pty_fd > 0) { + close(pty_fd); + } +} + +void Terminal::get_terminal_size() +{ + struct winsize size; + ioctl(pty_fd, TIOCGWINSZ, &size); + cols = size.ws_col; + rows = size.ws_row; + xpixel = size.ws_xpixel; + ypixel = size.ws_ypixel; + logger->debug("ioctl sizes: COLS={} ROWS={} XPIXEL={} YPIXEL={}", cols, rows, xpixel, ypixel); + + check_iterm2_support(); + if (flags->use_escape_codes) { + init_termios(); + if (xpixel == 0 || ypixel == 0) { + get_terminal_size_escape_code(); + } + if (xpixel == 0 || ypixel == 0) { + get_terminal_size_xtsm(); + } + check_sixel_support(); + check_kitty_support(); + reset_termios(); + } + + get_fallback_x11_terminal_sizes(); + get_fallback_wayland_terminal_sizes(); + + if (xpixel == 0 || ypixel == 0) { + xpixel = fallback_xpixel; + ypixel = fallback_ypixel; + } + if (xpixel == 0 || ypixel == 0) { + throw std::runtime_error("Unable to calculate terminal sizes"); + } + + const double padding_horiz = guess_padding(cols, xpixel); + const double padding_vert = guess_padding(rows, ypixel); + + padding_horizontal = static_cast(std::max(padding_horiz, padding_vert)); + padding_vertical = padding_horizontal; + font_width = static_cast( + std::floor(guess_font_size(cols, static_cast(xpixel), static_cast(padding_horizontal)))); + font_height = static_cast( + std::floor(guess_font_size(rows, static_cast(ypixel), static_cast(padding_vertical)))); + + if (xpixel < fallback_xpixel && ypixel < fallback_ypixel) { + padding_horizontal = static_cast((fallback_xpixel - xpixel) / 2); + padding_vertical = static_cast((fallback_ypixel - ypixel) / 2); + font_width = static_cast(xpixel / cols); + font_height = static_cast(ypixel / rows); + } + + logger->debug("padding_horiz={} padding_vert={}", padding_horizontal, padding_vertical); + logger->debug("font_width={} font_height={}", font_width, font_height); +} + +void Terminal::set_detected_output() +{ + if (supports_sixel) { + detected_output = "sixel"; + } + if (supports_kitty) { + detected_output = "kitty"; + } + if (supports_iterm2) { + detected_output = "iterm2"; + } + if (supports_x11) { + detected_output = "x11"; + } + if (supports_wayland) { + detected_output = "wayland"; + } + if (flags->output.empty()) { + if (detected_output.empty()) { + flags->output = "chafa"; + } else { + flags->output = detected_output; + } + } +} + +auto Terminal::guess_padding(uint16_t chars, double pixels) -> double +{ + const double font_size = std::floor(pixels / chars); + return (pixels - font_size * chars) / 2; +} + +auto Terminal::guess_font_size(uint16_t chars, float pixels, float padding) -> float +{ + return (pixels - 2 * padding) / static_cast(chars); +} + +void Terminal::get_terminal_size_escape_code() +{ + const auto resp = read_raw_str("\033[14t").erase(0, 4); + if (resp.empty()) { + return; + } + const auto sizes = util::str_split(resp, ";"); + try { + ypixel = std::stoi(sizes[0]); + xpixel = std::stoi(sizes[1]); + } catch (const std::invalid_argument &) { + logger->debug("Got unexpected values in get_terminal_size_escape_code"); + } + // some old vte terminals respond to these values in a different order + // assume everything older than 7000 is broken + const auto vte_ver_str = os::getenv("VTE_VERSION").value_or(""); + if (!vte_ver_str.empty()) { + const auto vte_ver = std::stoi(vte_ver_str); + const auto working_ver = 7000; + if (vte_ver <= working_ver) { + std::swap(ypixel, xpixel); + } + } + logger->debug("ESC sizes XPIXEL={} YPIXEL={}", xpixel, ypixel); +} + +void Terminal::get_terminal_size_xtsm() +{ + const auto resp = read_raw_str("\033[?2;1;0S").erase(0, 3); + if (resp.empty()) { + return; + } + const auto sizes = util::str_split(resp, ";"); + if (sizes.size() != 4) { + return; + } + try { + ypixel = std::stoi(sizes[3]); + xpixel = std::stoi(sizes[2]); + } catch (const std::invalid_argument &) { + logger->debug("Got unexpected values in get_terminal_size_xtsm"); + } + logger->debug("XTSM sizes XPIXEL={} YPIXEL={}", xpixel, ypixel); +} + +void Terminal::check_sixel_support() +{ + // some terminals support sixel but don't respond to escape sequences + const auto supported_terms = std::unordered_set{"yaft-256color", "iTerm.app"}; + const auto resp = read_raw_str("\033[?1;1;0S").erase(0, 3); + const auto vals = util::str_split(resp, ";"); + if (vals.size() > 2 || supported_terms.contains(term) || supported_terms.contains(term_program)) { + supports_sixel = true; + logger->debug("sixel is supported"); + } else { + logger->debug("sixel is not supported"); + } +} + +void Terminal::check_kitty_support() +{ + const auto resp = read_raw_str("\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\\033[c"); + if (resp.find("OK") != std::string::npos) { + supports_kitty = true; + logger->debug("kitty is supported"); + } else { + logger->debug("kitty is not supported"); + } +} + +void Terminal::check_iterm2_support() +{ + const auto supported_terms = std::unordered_set{"WezTerm", "iTerm.app"}; + if (supported_terms.contains(term_program)) { + supports_iterm2 = true; + logger->debug("iterm2 is supported"); + } else { + logger->debug("iterm2 is not supported"); + } +} + +auto Terminal::read_raw_str(const std::string_view esc) -> std::string +{ + const auto waitms = 100; + std::cout << esc << std::flush; + try { + const auto in_event = os::wait_for_data_on_stdin(waitms); + if (!in_event) { + return ""; + } + return os::read_data_from_stdin(esc.back()); + } catch (const std::system_error &) { + return ""; + } +} + +void Terminal::init_termios() +{ + tcgetattr(0, &old_term); /* grab old terminal i/o settings */ + new_term = old_term; /* make new settings same as old settings */ + new_term.c_lflag &= ~ICANON; /* disable buffered i/o */ + new_term.c_lflag &= ~ECHO; /* set echo mode */ + tcsetattr(0, TCSANOW, &new_term); /* use these new terminal i/o settings now */ +} + +void Terminal::reset_termios() const +{ + tcsetattr(0, TCSANOW, &old_term); +} + +void Terminal::get_fallback_x11_terminal_sizes() +{ +#ifdef ENABLE_X11 + const auto xutil = X11Util(); + if (xutil.connected) { + supports_x11 = true; + logger->debug("X11 is supported"); + } else { + logger->debug("x11 is not supported"); + return; + } + x11_wid = xutil.get_parent_window(terminal_pid); + logger->debug("Using fallback X11 window id {}", x11_wid); + const auto [xpix, ypix] = xutil.get_window_dimensions(x11_wid); + fallback_xpixel = xpix; + fallback_ypixel = ypix; + logger->debug("X11 sizes: XPIXEL={} YPIXEL={}", fallback_xpixel, fallback_ypixel); +#endif +} + +void Terminal::get_fallback_wayland_terminal_sizes() +{ +#ifdef ENABLE_WAYLAND + const auto config = WaylandConfig::get(); + if (!config->is_dummy()) { + supports_wayland = true; + logger->debug("Wayland is supported."); + } else { + logger->debug("Wayland is not supported"); + return; + } + const auto window = config->get_window_info(); + fallback_xpixel = static_cast(window.width); + fallback_ypixel = static_cast(window.height); +#endif +} + +void Terminal::open_first_pty() +{ + const auto pids = tmux::get_client_pids().value_or(std::vector{pid}); + + struct stat stat_info; + for (const auto spid : pids) { + auto tree = util::get_process_tree_v2(spid); + ranges::reverse(tree); + for (const auto &proc : tree) { + const int stat_res = stat(proc.pty_path.c_str(), &stat_info); + if (stat_res == -1) { + const auto err = std::error_code(errno, std::generic_category()); + logger->debug("stat failed ({}) for pty {}, pid {}, ignoring", err.message(), proc.pty_path, proc.pid); + continue; + } + if (proc.tty_nr == static_cast(stat_info.st_rdev)) { + pty_fd = open(proc.pty_path.c_str(), O_RDONLY | O_NOCTTY); + if (pty_fd == -1) { + const auto err = std::error_code(errno, std::generic_category()); + logger->debug("could not open pty {}, {}, ignoring", proc.pty_path, err.message()); + continue; + } + terminal_pid = proc.pid; + logger->info("PTY = {}", proc.pty_path); + return; + } + } + } + logger->warn("Could not open pty, using stdout as fallback"); + pty_fd = STDOUT_FILENO; +} diff --git a/src/tmux.cpp b/src/tmux.cpp new file mode 100644 index 0000000..b919d7d --- /dev/null +++ b/src/tmux.cpp @@ -0,0 +1,142 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "tmux.hpp" +#include "os.hpp" +#include "util.hpp" + +#include +#include +#include +#include + +constexpr auto session_hooks = std::to_array( + {"session-window-changed", "client-detached", "window-layout-changed"}); + +constexpr auto global_hooks = std::to_array( + {"client-session-changed"}); + +auto tmux::get_session_id() -> std::string +{ + const auto cmd = fmt::format("tmux display -p -F '#{{session_id}}' -t {}", tmux::get_pane()); + return os::exec(cmd); +} + +auto tmux::is_used() -> bool +{ + return !tmux::get_pane().empty(); +} + +auto tmux::is_window_focused() -> bool +{ + const auto cmd = fmt::format("tmux display -p -F '#{{session_attached}},#{{window_active}},#{{pane_in_mode}}' -t {}", tmux::get_pane()); + return os::exec(cmd) == "1,1,0"; +} + +auto tmux::get_pane() -> std::string +{ + return os::getenv("TMUX_PANE").value_or(""); +} + +auto tmux::get_client_pids() -> std::optional> +{ + if (!tmux::is_used()) { + return {}; + } + if (!tmux::is_window_focused()) { + return {}; + } + + std::vector pids; + const auto cmd = fmt::format("tmux list-clients -F '#{{client_pid}}' -t {}", tmux::get_pane()); + const auto output = os::exec(cmd); + + for (const auto &line : util::str_split(output, "\n")) { + pids.push_back(std::stoi(line)); + } + + return pids; +} + +auto tmux::get_offset() -> std::pair +{ + if (!tmux::is_used()) { + return std::make_pair(0, 0); + } + const auto [p_x, p_y] = tmux::get_pane_offset(); + const auto s_y = tmux::get_statusbar_offset(); + return std::make_pair(p_x, p_y + s_y); +} + +auto tmux::get_pane_offset() -> std::pair +{ + const auto cmd = fmt::format(R"(tmux display -p -F '#{{pane_top}},#{{pane_left}}, + #{{pane_bottom}},#{{pane_right}}, + #{{window_height}},#{{window_width}}' -t {})", + tmux::get_pane()); + const auto output = util::str_split(os::exec(cmd), ","); + return std::make_pair(std::stoi(output.at(1)), std::stoi(output.at(0))); +} + +auto tmux::get_statusbar_offset() -> int +{ + const std::string cmd = "tmux display -p '#{status},#{status-position}'"; + const auto output = util::str_split(os::exec(cmd), ","); + if (output.at(1) != "top" || output.at(0) == "off") { + return 0; + } + if (output.at(0) == "on") { + return 1; + } + return std::stoi(output.at(0)); +} + +void tmux::handle_hook(const std::string_view hook, int pid) +{ + const auto msg = fmt::format(R"({{"action":"tmux","hook":"{}"}})", hook); + const auto endpoint = util::get_socket_path(pid); + util::send_socket_message(msg, endpoint); +} + +void tmux::register_hooks() +{ + if (!tmux::is_used()) { + return; + } + for (const auto &hook : session_hooks) { + std::string cmd = fmt::format(R"(tmux set-hook -t {0} {1} "run-shell 'ueberzugpp tmux {1} {2}'")", tmux::get_pane(), hook, os::get_pid()); + os::exec(cmd); + } + for (const auto &hook : global_hooks) { + std::string cmd = fmt::format(R"(tmux set-hook -g {0} "run-shell 'ueberzugpp tmux {0} {1}'")", hook, os::get_pid()); + os::exec(cmd); + } +} + +void tmux::unregister_hooks() +{ + if (!tmux::is_used()) { + return; + } + for (const auto &hook : session_hooks) { + const auto cmd = fmt::format("tmux set-hook -u -t {} {}", tmux::get_pane(), hook); + os::exec(cmd); + } + for (const auto &hook : global_hooks) { + const auto cmd = fmt::format("tmux set-hook -gu {}", hook); + os::exec(cmd); + } +} diff --git a/src/util/dbus.cpp b/src/util/dbus.cpp new file mode 100644 index 0000000..1cc78e5 --- /dev/null +++ b/src/util/dbus.cpp @@ -0,0 +1,15 @@ +#include "util/dbus.hpp" +#include + +DbusUtil::DbusUtil(const std::string &address) + : connection(dbus_connection_open(address.c_str(), nullptr)) +{ + if (connection == nullptr) { + throw std::invalid_argument("Could not connect to dbus address"); + } +} + +DbusUtil::~DbusUtil() +{ + dbus_connection_unref(connection); +} diff --git a/src/util/egl.cpp b/src/util/egl.cpp new file mode 100644 index 0000000..5b64459 --- /dev/null +++ b/src/util/egl.cpp @@ -0,0 +1,228 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "util/egl.hpp" +#include "image.hpp" + +#include +#include + +#include +#include + +constexpr EGLint egl_major_version = 1; +constexpr EGLint egl_minor_version = 5; +constexpr EGLint opengl_major_version = 4; +constexpr EGLint opengl_minor_version = 6; + +constexpr auto config_attrs = std::to_array({EGL_SURFACE_TYPE, + EGL_WINDOW_BIT, + EGL_CONFORMANT, + EGL_OPENGL_BIT, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_BIT, + EGL_COLOR_BUFFER_TYPE, + EGL_RGB_BUFFER, + + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_BLUE_SIZE, + 8, + EGL_ALPHA_SIZE, + 8, + + EGL_DEPTH_SIZE, + 24, + EGL_STENCIL_SIZE, + 8, + + EGL_NONE}); + +const auto context_attrs = std::to_array( + {EGL_CONTEXT_MAJOR_VERSION, opengl_major_version, EGL_CONTEXT_MINOR_VERSION, opengl_minor_version, +#ifdef DEBUG + EGL_CONTEXT_OPENGL_DEBUG, EGL_TRUE, +#endif + EGL_CONTEXT_OPENGL_ROBUST_ACCESS, EGL_TRUE, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_NONE}); + +void GLAPIENTRY debug_callback([[maybe_unused]] GLenum source, [[maybe_unused]] GLenum type, + [[maybe_unused]] GLuint gl_id, GLenum severity, [[maybe_unused]] GLsizei length, + const GLchar *message, [[maybe_unused]] const void *user) +{ + if (type != GL_DEBUG_TYPE_ERROR) { + return; + } + const auto logger = spdlog::get("opengl"); + logger->error("Type: {:#X}, Severity: {:#X}, Message: {}", type, severity, message); +} + +template +EGLUtil::EGLUtil(EGLenum platform, T *native_display, const EGLAttrib *attrib) + : display(eglGetPlatformDisplay(platform, native_display, attrib)) +{ + logger = spdlog::get("opengl"); + + if (display == EGL_NO_DISPLAY) { + const auto err = error_to_string(); + logger->error("Could not obtain display, error {}", err); + throw std::runtime_error(""); + } + + EGLint egl_major = 0; + EGLint egl_minor = 0; + EGLBoolean eglres = eglInitialize(display, &egl_major, &egl_minor); + if (eglres != EGL_TRUE) { + const auto err = error_to_string(); + logger->error("Could not initialize display, error {}", err); + throw std::runtime_error(""); + } + if (egl_major != egl_major_version && egl_minor != egl_minor_version) { + logger->error("EGL {}.{} is not available", egl_major_version, egl_minor_version); + throw std::runtime_error(""); + } + + eglres = eglBindAPI(EGL_OPENGL_API); + if (eglres != EGL_TRUE) { + const auto err = error_to_string(); + logger->error("Could not bind to OpenGL API, error {}", err); + throw std::runtime_error(""); + } + + int num_config = 0; + eglres = eglChooseConfig(display, config_attrs.data(), &config, 1, &num_config); + if (eglres != EGL_TRUE || num_config != 1) { + const auto err = error_to_string(); + logger->error("Could not create config, error {}", err); + throw std::runtime_error(""); + } + + logger->info("Using EGL {}.{} and OpenGL {}.{}", egl_major_version, egl_minor_version, opengl_major_version, + opengl_minor_version); +} + +template +EGLUtil::~EGLUtil() +{ + eglTerminate(display); +} + +template +auto EGLUtil::error_to_string() const -> std::string +{ + using pair_type = std::pair; + constexpr auto error_codes = std::to_array({ + {EGL_SUCCESS, "EGL_SUCCESS"}, + {EGL_BAD_ACCESS, "EGL_BAD_ACCESS"}, + {EGL_BAD_ALLOC, "EGL_BAD_ALLOC"}, + {EGL_BAD_ATTRIBUTE, "EGL_BAD_ATTRIBUTE"}, + {EGL_BAD_CONFIG, "EGL_BAD_CONFIG"}, + {EGL_BAD_CONTEXT, "EGL_BAD_CONTEXT"}, + {EGL_BAD_CURRENT_SURFACE, "EGL_BAD_CURRENT_SURFACE"}, + {EGL_BAD_DISPLAY, "EGL_BAD_DISPLAY"}, + {EGL_BAD_MATCH, "EGL_BAD_MATCH"}, + {EGL_BAD_NATIVE_PIXMAP, "EGL_BAD_NATIVE_PIXMAP"}, + {EGL_BAD_NATIVE_WINDOW, "EGL_BAD_NATIVE_WINDOW"}, + {EGL_BAD_PARAMETER, "EGL_BAD_PARAMETER"}, + {EGL_BAD_SURFACE, "EGL_BAD_SURFACE"}, + }); + + const auto current_error = eglGetError(); + + const auto found = + ranges::find_if(error_codes, [current_error](const pair_type &pair) { return pair.first == current_error; }); + + if (found == error_codes.end()) { + return "EGL_UNKNOWN_ERROR"; + } + + return std::string(found->second); +} + +template +auto EGLUtil::create_context(EGLSurface surface) const -> EGLContext +{ + EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attrs.data()); + if (context == EGL_NO_CONTEXT) { + const auto err = error_to_string(); + logger->error("Could not create context, error {}", err); + return context; + } + +#ifdef DEBUG + run_contained(surface, context, [] { + glDebugMessageCallback(debug_callback, nullptr); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + }); +#endif + + return context; +} + +template +auto EGLUtil::create_surface(V *native_window) const -> EGLSurface +{ + EGLSurface surface = eglCreatePlatformWindowSurface(display, config, native_window, nullptr); + if (surface == EGL_NO_SURFACE) { + const auto err = error_to_string(); + logger->error("Could not create surface, error {}", err); + } + return surface; +} + +template +void EGLUtil::make_current(EGLSurface surface, EGLContext context) const +{ + eglMakeCurrent(display, surface, surface, context); +} + +template +void EGLUtil::restore() const +{ + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +template +void EGLUtil::run_contained(EGLSurface surface, EGLContext context, const std::function &func) const +{ + make_current(surface, context); + func(); + restore(); +} + +template +void EGLUtil::get_texture_from_image(const Image &image, GLuint texture) const +{ + glBindTexture(GL_TEXTURE_2D, texture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width(), image.height(), 0, GL_BGRA, GL_UNSIGNED_BYTE, image.data()); +} + +#ifdef ENABLE_X11 +# include +template class EGLUtil; +#endif + +#ifdef ENABLE_WAYLAND +# include +# include +template class EGLUtil; +#endif diff --git a/src/util/socket.cpp b/src/util/socket.cpp new file mode 100644 index 0000000..d770270 --- /dev/null +++ b/src/util/socket.cpp @@ -0,0 +1,169 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "util/socket.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "os.hpp" +#include "util.hpp" + +namespace fs = std::filesystem; + +UnixSocket::UnixSocket(const std::string_view endpoint) + : fd(socket(AF_UNIX, SOCK_STREAM, 0)) +{ + if (fd == -1) { + throw std::system_error(errno, std::generic_category()); + } + connect_to_endpoint(endpoint); +} + +UnixSocket::UnixSocket() + : fd(socket(AF_UNIX, SOCK_STREAM, 0)) +{ + if (fd == -1) { + throw std::system_error(errno, std::generic_category()); + } + const int bufsize = 8192; + buffer.reserve(bufsize); +} + +void UnixSocket::connect_to_endpoint(const std::string_view endpoint) +{ + if (!fs::exists(endpoint)) { + connected = false; + return; + } + struct sockaddr_un sock; + std::memset(&sock, 0, sizeof(sockaddr_un)); + sock.sun_family = AF_UNIX; + endpoint.copy(sock.sun_path, endpoint.size()); + + int res = connect(fd, reinterpret_cast(&sock), sizeof(struct sockaddr_un)); + if (res == -1) { + throw std::system_error(errno, std::generic_category()); + } +} + +void UnixSocket::bind_to_endpoint(const std::string_view endpoint) const +{ + struct sockaddr_un sock; + std::memset(&sock, 0, sizeof(sockaddr_un)); + sock.sun_family = AF_UNIX; + endpoint.copy(sock.sun_path, endpoint.size()); + + int res = bind(fd, reinterpret_cast(&sock), sizeof(struct sockaddr_un)); + if (res == -1) { + throw std::system_error(errno, std::generic_category()); + } + res = listen(fd, SOMAXCONN); + if (res == -1) { + throw std::system_error(errno, std::generic_category()); + } +} + +auto UnixSocket::wait_for_connections(int waitms) const -> int +{ + const auto in_event = os::wait_for_data_on_fd(fd, waitms); + if (in_event) { + return accept(fd, nullptr, nullptr); + } + return -1; +} + +auto UnixSocket::read_data_from_connection(int filde) -> std::vector +{ + // a single connection could send multiples commands at once + // each command should end with a '\n' character + const int read_buffer_size = 4096; + std::array read_buffer; + while (true) { + const auto status = recv(filde, read_buffer.data(), read_buffer_size, 0); + if (status <= 0) { + break; + } + buffer.append(read_buffer.data(), status); + } + auto cmds = util::str_split(buffer, "\n"); + buffer.clear(); + close(filde); + return cmds; +} + +void UnixSocket::write(const void *data, std::size_t len) const +{ + if (!connected) { + return; + } + const auto *runner = static_cast(data); + while (len != 0) { + const auto status = send(fd, runner, len, MSG_NOSIGNAL); + if (status == -1) { + throw std::system_error(errno, std::generic_category()); + } + len -= status; + runner += status; + } +} + +void UnixSocket::read(void *data, std::size_t len) const +{ + if (!connected) { + return; + } + auto *runner = static_cast(data); + while (len != 0) { + const auto status = recv(fd, runner, len, 0); + if (status == 0) { + return; // no data + } + if (status == -1) { + throw std::system_error(errno, std::generic_category()); + } + len -= status; + runner += status; + } +} + +auto UnixSocket::read_until_empty() const -> std::string +{ + std::string result; + const int read_buffer_size = 4096; + std::array read_buffer; + result.reserve(read_buffer_size); + while (true) { + const auto status = recv(fd, read_buffer.data(), read_buffer_size, 0); + if (status <= 0) { + break; + } + result.append(read_buffer.data(), status); + } + return result; +} + +UnixSocket::~UnixSocket() +{ + close(fd); +} diff --git a/src/util/util.cpp b/src/util/util.cpp new file mode 100644 index 0000000..2d3e99f --- /dev/null +++ b/src/util/util.cpp @@ -0,0 +1,262 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "util.hpp" +#include "flags.hpp" +#include "os.hpp" +#include "process.hpp" +#include "util/ptr.hpp" +#include "util/socket.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#if OPENSSL_VERSION_NUMBER < 0x10100000L +# define EVP_MD_CTX_new EVP_MD_CTX_create +# define EVP_MD_CTX_free EVP_MD_CTX_destroy +#endif +#ifdef ENABLE_TURBOBASE64 +# ifdef WITH_SYSTEM_TURBOBASE64 +# include +# else +# include "turbob64.h" +# endif +#endif +#include + +#include + +namespace fs = std::filesystem; +using njson = nlohmann::json; + +auto util::str_split(std::string_view str, std::string_view delim) -> std::vector +{ + std::vector result; + for (const auto word : ranges::views::split(str, delim)) { + result.emplace_back(ranges::to(word)); + } + return result; +} + +auto util::get_process_tree(int pid) -> std::vector +{ + std::vector res; + Process runner(pid); + while (runner.pid > 1) { + res.push_back(runner.pid); + runner = Process(runner.ppid); + } + return res; +} + +auto util::get_process_tree_v2(int pid) -> std::vector +{ + std::vector tree; + Process runner(pid); + while (runner.pid > 1) { + tree.push_back(runner); + runner = Process(runner.ppid); + } + return tree; +} + +auto util::get_cache_path() -> std::string +{ + const auto home = os::getenv("HOME").value_or(util::temp_directory_path()); + const auto cache_home = os::getenv("XDG_CACHE_HOME").value_or(fmt::format("{}/.cache", home)); + return fmt::format("{}/ueberzugpp/", cache_home); +} + +auto util::get_log_filename() -> std::string +{ + const auto user = os::getenv("USER").value_or("NOUSER"); + return fmt::format("ueberzugpp-{}.log", user); +} + +auto util::get_socket_path(int pid) -> std::string +{ + return fmt::format("{}/ueberzugpp-{}.socket", util::temp_directory_path().string(), pid); +} + +void util::send_socket_message(const std::string_view msg, const std::string_view endpoint) +{ + try { + UnixSocket socket; + socket.connect_to_endpoint(endpoint); + socket.write(msg.data(), msg.size()); + } catch (const std::system_error &err) { + return; + } +} + +auto util::base64_encode(const unsigned char *input, size_t length) -> std::string +{ + const size_t bufsize = 4 * ((length + 2) / 3) + 1; + std::vector res(bufsize, 0); + base64_encode_v2(input, length, reinterpret_cast(res.data())); + return {res.data()}; +} + +void util::base64_encode_v2(const unsigned char *input, size_t length, unsigned char *out) +{ +#ifdef ENABLE_TURBOBASE64 + tb64enc(input, length, out); +#else + EVP_EncodeBlock(out, input, static_cast(length)); +#endif +} + +auto util::get_b2_hash_ssl(const std::string_view str) -> std::string +{ + std::stringstream sstream; + const auto mdctx = c_unique_ptr{EVP_MD_CTX_new()}; +#ifdef LIBRESSL_VERSION_NUMBER + const auto *evp = EVP_sha256(); +#else + const auto *evp = EVP_blake2b512(); +#endif + auto digest = std::array(); + + EVP_DigestInit_ex(mdctx.get(), evp, nullptr); + EVP_DigestUpdate(mdctx.get(), str.data(), str.size()); + unsigned int digest_len = 0; + EVP_DigestFinal_ex(mdctx.get(), digest.data(), &digest_len); + + sstream << std::hex << std::setw(2) << std::setfill('0'); + for (unsigned int i = 0; i < digest_len; ++i) { + sstream << static_cast(digest[i]); + } + return sstream.str(); +} + +void util::move_cursor(int row, int col) +{ + std::cout << "\033[" << row << ";" << col << "f" << std::flush; +} + +void util::save_cursor_position() +{ + std::cout << "\0337" << std::flush; +} + +void util::restore_cursor_position() +{ + std::cout << "\0338" << std::flush; +} + +auto util::get_cache_file_save_location(const fs::path &path) -> std::string +{ + return fmt::format("{}{}{}", get_cache_path(), get_b2_hash_ssl(path.string()), path.extension().string()); +} + +void util::benchmark(const std::function &func) +{ + using std::chrono::duration; + using std::chrono::duration_cast; + using std::chrono::high_resolution_clock; + using std::chrono::milliseconds; + + const auto ti1 = high_resolution_clock::now(); + func(); + const auto ti2 = high_resolution_clock::now(); + const duration ms_double = ti2 - ti1; + + std::cout << ms_double.count() << "ms\n"; +} + +void util::send_command(const Flags &flags) +{ + if (flags.cmd_action == "exit") { + util::send_socket_message("EXIT", flags.cmd_socket); + return; + } + + if (flags.cmd_action == "remove") { + const njson json = {{"action", "remove"}, {"identifier", flags.cmd_id}}; + util::send_socket_message(json.dump(), flags.cmd_socket); + return; + } + + const njson json = {{"action", flags.cmd_action}, + {"identifier", flags.cmd_id}, + {"max_width", std::stoi(flags.cmd_max_width)}, + {"max_height", std::stoi(flags.cmd_max_height)}, + {"x", std::stoi(flags.cmd_x)}, + {"y", std::stoi(flags.cmd_y)}, + {"path", flags.cmd_file_path}}; + util::send_socket_message(json.dump(), flags.cmd_socket); +} + +void util::clear_terminal_area(int xcoord, int ycoord, int width, int height) +{ + save_cursor_position(); + const auto line_clear = std::string(width, ' '); + for (int i = ycoord; i <= height + 2; ++i) { + util::move_cursor(i, xcoord); + std::cout << line_clear; + } + std::cout << std::flush; + restore_cursor_position(); +} + +auto util::generate_random_string(size_t length) -> std::string +{ + constexpr auto chars = + std::to_array({'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}); + auto rng_dev = std::random_device(); + auto rng = std::mt19937(rng_dev()); + auto dist = std::uniform_int_distribution{{}, chars.size() - 1}; + std::string result(length, 0); + std::generate_n(std::begin(result), length, [&chars, &dist, &rng] { return chars[dist(rng)]; }); + return result; +} + +auto util::read_exif_rotation(const fs::path &path) -> std::optional +{ + try { + auto image = vips::VImage::new_from_file(path.c_str()); + return image.get_int("orientation"); + } catch (const vips::VError &) { + return {}; + } +} + +auto util::round_up(int num_to_round, int multiple) -> int +{ + if (multiple == 0) { + return num_to_round; + } + + int remainder = num_to_round % multiple; + if (remainder == 0) { + return num_to_round; + } + + return num_to_round + multiple - remainder; +} + +auto util::temp_directory_path() -> fs::path +{ + return os::getenv("UEBERZUGPP_TMPDIR").value_or(fs::temp_directory_path()); +} diff --git a/src/util/x11.cpp b/src/util/x11.cpp new file mode 100644 index 0000000..5250997 --- /dev/null +++ b/src/util/x11.cpp @@ -0,0 +1,171 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "util/x11.hpp" +#include "flags.hpp" +#include "os.hpp" +#include "util.hpp" +#include "util/ptr.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +X11Util::X11Util() + : connection(xcb_connect(nullptr, nullptr)) +{ + const auto flags = Flags::instance(); + const auto xdg_session = os::getenv("XDG_SESSION_TYPE").value_or(""); + if (xcb_connection_has_error(connection) == 0) { + screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data; + if (xdg_session != "wayland" || flags->output == "x11") { + connected = true; + } + } +} + +X11Util::X11Util(xcb_connection_t *connection) + : connection(connection), + screen(xcb_setup_roots_iterator(xcb_get_setup(connection)).data), + owns_connection(false) +{ +} + +X11Util::~X11Util() +{ + if (owns_connection) { + xcb_disconnect(connection); + } +} + +auto X11Util::get_server_window_ids() const -> std::vector +{ + const int num_clients = 256; + std::vector windows; + std::stack cookies_st; + windows.reserve(num_clients); + + cookies_st.push(xcb_query_tree_unchecked(connection, screen->root)); + + while (!cookies_st.empty()) { + const auto cookie = cookies_st.top(); + cookies_st.pop(); + + const auto reply = unique_C_ptr{xcb_query_tree_reply(connection, cookie, nullptr)}; + if (!reply) { + continue; + } + + const auto num_children = xcb_query_tree_children_length(reply.get()); + if (num_children == 0) { + continue; + } + const auto *children = xcb_query_tree_children(reply.get()); + for (int i = 0; i < num_children; ++i) { + const auto child = children[i]; + const bool is_complete_window = window_has_properties(child, {XCB_ATOM_WM_CLASS, XCB_ATOM_WM_NAME}); + if (is_complete_window) { + windows.push_back(child); + } + cookies_st.push(xcb_query_tree_unchecked(connection, child)); + } + } + return windows; +} + +auto X11Util::get_pid_window_map() const -> std::unordered_map +{ + const auto windows = get_server_window_ids(); + std::unordered_map res; + std::vector cookies; + res.reserve(windows.size()); + cookies.reserve(windows.size()); + + struct xcb_res_client_id_spec_t spec; + spec.mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID; + + // bulk request pids + for (const auto window : windows) { + spec.client = window; + cookies.push_back(xcb_res_query_client_ids_unchecked(connection, 1, &spec)); + } + + // process replies + auto win_iter = windows.cbegin(); + for (const auto cookie : cookies) { + const auto reply = + unique_C_ptr{xcb_res_query_client_ids_reply(connection, cookie, nullptr)}; + if (!reply) { + continue; + } + const auto iter = xcb_res_query_client_ids_ids_iterator(reply.get()); + const auto pid = *xcb_res_client_id_value_value(iter.data); + res.insert_or_assign(pid, *win_iter); + std::advance(win_iter, 1); + } + return res; +} + +auto X11Util::window_has_properties(xcb_window_t window, std::initializer_list properties) const -> bool +{ + std::vector cookies; + cookies.reserve(properties.size()); + for (const auto prop : properties) { + cookies.push_back(xcb_get_property_unchecked(connection, 0, window, prop, XCB_ATOM_ANY, 0, 4)); + } + return ranges::any_of(cookies, [this](xcb_get_property_cookie_t cookie) -> bool { + const auto reply = unique_C_ptr{xcb_get_property_reply(connection, cookie, nullptr)}; + return reply && xcb_get_property_value_length(reply.get()) != 0; + }); +} + +auto X11Util::get_window_dimensions(xcb_window_t window) const -> std::pair +{ + const auto cookie = xcb_get_geometry_unchecked(connection, window); + const auto reply = unique_C_ptr{xcb_get_geometry_reply(connection, cookie, nullptr)}; + if (!reply) { + return std::make_pair(0, 0); + } + return std::make_pair(reply->width, reply->height); +} + +auto X11Util::get_parent_window(int pid) const -> xcb_window_t +{ + const auto wid = os::getenv("WINDOWID"); + if (wid.has_value()) { + try { + return std::stoi(wid.value()); + } catch (const std::out_of_range &oor) { + return 0; + } + } + + const auto pid_window_map = get_pid_window_map(); + const auto tree = util::get_process_tree(pid); + for (const auto spid : tree) { + const auto win = pid_window_map.find(spid); + if (win != pid_window_map.end()) { + return win->second; + } + } + + return 0; +}