This commit is contained in:
2025-12-28 00:36:34 -08:00
commit 0ec0359c9f
98 changed files with 9188 additions and 0 deletions

280
CMakeLists.txt Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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 <execution> 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 $<$<CONFIG:Debug>:
$<$<COMPILE_LANGUAGE:CXX>:${PROJECT_WARNINGS_CXX}> >)
target_compile_definitions(ueberzug PRIVATE $<$<CONFIG:Debug>: 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)

53
CODE_OF_CONDUCT.md Normal file
View File

@ -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/.

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

247
README.md Normal file
View File

@ -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
<a href="https://repology.org/project/ueberzugpp/versions"><img src="https://repology.org/badge/vertical-allrepos/ueberzugpp.svg" /></a>
### 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.
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg">](https://jb.gg/OpenSourceSupport)

5
debian/changelog vendored Normal file
View File

@ -0,0 +1,5 @@
ueberzugpp (2.8.6) unstable; urgency=medium
* Initial release.
-- JustKidding <jk@vin.ovh> Wed, 31 May 2023 07:35:31 -0400

5
debian/changelog.in vendored Normal file
View File

@ -0,0 +1,5 @@
ueberzugpp (@@VERSION@@) unstable; urgency=medium
* Initial release.
-- JustKidding <jk@vin.ovh> @@DATETIME@@

37
debian/control vendored Normal file
View File

@ -0,0 +1,37 @@
Source: ueberzugpp
Maintainer: JustKidding <jk@vin.ovh>
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.

36
debian/copyright vendored Normal file
View File

@ -0,0 +1,36 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ueberzugpp
Upstream-Contact: JustKidding <jk@vin.ovh>
Source: https://github.com/jstkdng/ueberzugpp
Files: *
Copyright: JustKidding <jk@vin.ovh>
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 <https://www.gnu.org/licenses/>.
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'.

10
debian/rules vendored Executable file
View File

@ -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

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (native)

169
docs/ueberzugpp.1.in Normal file
View File

@ -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

66
include/application.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef APPLICATION_H
#define APPLICATION_H
#include "canvas.hpp"
#include "flags.hpp"
#include "os.hpp"
#include "terminal.hpp"
#include "util/ptr.hpp"
#include <atomic>
#include <cstdlib>
#include <string>
#include <string_view>
#include <thread>
#include <spdlog/spdlog.h>
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<bool> 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> terminal;
std::unique_ptr<Canvas> canvas;
std::shared_ptr<Flags> flags;
std::shared_ptr<spdlog::logger> logger;
cn_unique_ptr<std::FILE, std::fclose> f_stderr;
std::thread socket_thread;
void setup_logger();
void set_silent();
void socket_loop();
void daemonize();
};
#endif

38
include/canvas.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef CANVAS_H
#define CANVAS_H
#include "image.hpp"
#include <memory>
class Canvas
{
public:
static auto create() -> std::unique_ptr<Canvas>;
virtual ~Canvas() = default;
virtual void add_image(const std::string &identifier, std::unique_ptr<Image> new_image) = 0;
virtual void remove_image(const std::string &identifier) = 0;
virtual void show() {}
virtual void hide() {}
virtual void toggle() {}
};
#endif

51
include/dimensions.hpp Normal file
View File

@ -0,0 +1,51 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef DIMENSIONS_H
#define DIMENSIONS_H
#include <cstdint>
#include <string>
#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

68
include/flags.hpp Normal file
View File

@ -0,0 +1,68 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef FLAGS_H
#define FLAGS_H
#include <filesystem>
#include <memory>
#include <string>
// singleton
class Flags
{
public:
static auto instance() -> std::shared_ptr<Flags>
{
static std::shared_ptr<Flags> 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

54
include/image.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef IMAGE_H
#define IMAGE_H
#include <filesystem>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include "dimensions.hpp"
#include "terminal.hpp"
class Image
{
public:
static auto load(const nlohmann::json &command, const Terminal *terminal) -> std::unique_ptr<Image>;
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<Dimensions>;
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<int, int>;
};
#endif

41
include/os.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef NAMESPACE_OS_H
#define NAMESPACE_OS_H
#include <optional>
#include <string>
namespace os
{
auto exec(const std::string &cmd) -> std::string;
auto getenv(const std::string &var) -> std::optional<std::string>;
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

38
include/process.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef PROCESS_INFO_H
#define PROCESS_INFO_H
#include <string>
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

89
include/terminal.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef TERMINAL_H
#define TERMINAL_H
#include <memory>
#include <string>
#include <string_view>
#include <spdlog/fwd.h>
#include <termios.h>
#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> flags;
std::shared_ptr<spdlog::logger> logger;
struct termios old_term;
struct termios new_term;
};
#endif

46
include/tmux.hpp Normal file
View File

@ -0,0 +1,46 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef NAMESPACE_TMUX_H
#define NAMESPACE_TMUX_H
#include <string>
#include <vector>
#include <optional>
#include <string_view>
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<std::vector<int>>;
auto get_offset() -> std::pair<int, int>;
auto get_pane_offset() -> std::pair<int, int>;
auto get_statusbar_offset() -> int;
void handle_hook(std::string_view hook, int pid);
void register_hooks();
void unregister_hooks();
} // namespace tmux
#endif

67
include/util.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef NAMESPACE_UTIL_H
#define NAMESPACE_UTIL_H
#include <filesystem>
#include <functional>
#include <limits>
#include <optional>
#include <random>
#include <string>
#include <string_view>
#include <vector>
#include "flags.hpp"
#include "os.hpp"
#include "process.hpp"
namespace util
{
auto str_split(std::string_view str, std::string_view delim) -> std::vector<std::string>;
auto get_process_tree(int pid) -> std::vector<int>;
auto get_process_tree_v2(int pid) -> std::vector<Process>;
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<void(void)> &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<std::uint16_t>;
template <typename T>
auto generate_random_number(T min, T max = std::numeric_limits<T>::max()) -> T
{
std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution<T> dist(min, max);
return dist(rng);
}
} // namespace util
#endif

33
include/util/dbus.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef UEBERZUGPP_DBUS_HPP
#define UEBERZUGPP_DBUS_HPP
#include <dbus/dbus.h>
#include <string>
class DbusUtil
{
public:
explicit DbusUtil(const std::string &address);
~DbusUtil();
private:
DBusConnection *connection = nullptr;
};
#endif // UEBERZUGPP_DBUS_HPP

54
include/util/egl.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef UTIL_EGL_H
#define UTIL_EGL_H
#include "image.hpp"
#include <functional>
#include <memory>
#include <spdlog/fwd.h>
#include <EGL/egl.h>
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
template <class T, class V>
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<void()> &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<spdlog::logger> logger;
[[nodiscard]] auto error_to_string() const -> std::string;
};
#endif

53
include/util/ptr.hpp Normal file
View File

@ -0,0 +1,53 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef UTIL_PTR_H
#define UTIL_PTR_H
#include <memory>
template <auto fn>
struct deleter_from_fn {
template <typename T>
constexpr void operator()(T *arg) const
{
fn(const_cast<std::remove_const_t<T> *>(arg));
}
};
template <auto fn>
struct deleter_from_fn_null {
template <typename T>
constexpr void operator()(T *arg) const
{
if (arg != nullptr) {
fn(const_cast<std::remove_const_t<T> *>(arg));
}
}
};
// custom unique pointer
template <typename T, auto fn>
using c_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
// custom unique pointer that checks for null before deleting
template <typename T, auto fn>
using cn_unique_ptr = std::unique_ptr<T, deleter_from_fn_null<fn>>;
template <typename T>
using unique_C_ptr = std::unique_ptr<T, deleter_from_fn<std::free>>;
#endif

46
include/util/socket.hpp Normal file
View File

@ -0,0 +1,46 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef UTIL_SOCKET_H
#define UTIL_SOCKET_H
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
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<std::string>;
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

49
include/util/x11.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef X11_UTIL_H
#define X11_UTIL_H
#include <initializer_list>
#include <memory>
#include <unordered_map>
#include <vector>
#include <xcb/xcb.h>
class X11Util
{
public:
X11Util();
explicit X11Util(xcb_connection_t *connection);
~X11Util();
[[nodiscard]] auto get_server_window_ids() const -> std::vector<xcb_window_t>;
[[nodiscard]] auto get_pid_window_map() const -> std::unordered_map<uint32_t, xcb_window_t>;
[[nodiscard]] auto get_window_dimensions(xcb_window_t window) const -> std::pair<uint16_t, uint16_t>;
[[nodiscard]] auto get_parent_window(int pid) const -> xcb_window_t;
[[nodiscard]] auto window_has_properties(xcb_window_t window, std::initializer_list<xcb_atom_t> properties) const
-> bool;
bool connected = false;
private:
xcb_connection_t *connection;
xcb_screen_t *screen = nullptr;
bool owns_connection = true;
};
#endif

19
include/version.hpp.in Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#define ueberzugpp_VERSION_MAJOR @ueberzugpp_VERSION_MAJOR@
#define ueberzugpp_VERSION_MINOR @ueberzugpp_VERSION_MINOR@
#define ueberzugpp_VERSION_PATCH @ueberzugpp_VERSION_PATCH@

36
include/window.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef WINDOW_H
#define WINDOW_H
#include <concepts>
#include <memory>
class Window
{
public:
virtual ~Window() = default;
virtual void draw() = 0;
virtual void generate_frame() = 0;
virtual void show() {};
virtual void hide() {};
};
template<class T>
concept WindowType = std::is_base_of<Window, T>::value;
#endif

18
install Executable file
View File

@ -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"

1
plugins/wayfire/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.cache/

View File

@ -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 <https://www.gnu.org/licenses/>.
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
$<$<CONFIG:Debug>:
-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)

View File

@ -0,0 +1,150 @@
#include <memory>
#include <unordered_map>
#include <wayfire/core.hpp>
#include <wayfire/scene-render.hpp>
#include <wayfire/util.hpp>
#include <wayfire/view.hpp>
#include <wayfire/output.hpp>
#include <wayfire/plugin.hpp>
#include <wayfire/scene-operations.hpp>
#include <wayfire/plugins/ipc/ipc-method-repository.hpp>
#include <wayfire/plugins/common/shared-core-data.hpp>
#include <wayfire/unstable/wlr-view-events.hpp>
#include <wayfire/unstable/wlr-surface-node.hpp>
#include <wayfire/unstable/translation-node.hpp>
namespace wf
{
class ueberzugpp_surface_t
{
std::weak_ptr<wf::view_interface_t> terminal;
std::shared_ptr<scene::wlr_surface_node_t> surface;
std::shared_ptr<scene::translation_node_t> translation;
wlr_xdg_toplevel *toplevel;
wf::wl_listener_wrapper on_toplevel_destroy;
public:
ueberzugpp_surface_t(const std::weak_ptr<wf::view_interface_t>& _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<scene::wlr_surface_node_t>(_toplevel->base->surface, true);
translation = std::make_shared<scene::translation_node_t>();
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<void()> 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::method_repository_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<new_xdg_surface_signal> 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<ueberzugpp_surface_t>(terminal->weak_from_this(), event->surface->toplevel));
iter->second->destroy_callback = [this, appid]
{
surfaces.erase(appid);
};
}
};
std::unordered_map<std::string, std::unique_ptr<ueberzugpp_surface_t>> surfaces;
};
} // end namespace wf
// NOLINTNEXTLINE
DECLARE_WAYFIRE_PLUGIN(wf::ueberzugpp_mapper);

45
scripts/fifo/fzf-fifo Executable file
View File

@ -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

31
scripts/fifo/img-fifo Executable file
View File

@ -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

25
scripts/fzfub Executable file
View File

@ -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

30
scripts/img Executable file
View File

@ -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

3
scripts/lf/cleaner Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
ueberzugpp cmd -s $UB_SOCKET -a remove -i PREVIEW

34
scripts/lf/lfub Executable file
View File

@ -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

65
scripts/lf/preview Executable file
View File

@ -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

32
scripts/sockets.py Normal file
View File

@ -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()

281
src/application.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "application.hpp"
#include "image.hpp"
#include "tmux.hpp"
#include "util.hpp"
#include "util/socket.hpp"
#include "version.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <unordered_map>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <spdlog/sinks/basic_file_sink.h>
#include <vips/vips8>
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<Terminal>();
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<std::string_view, std::function<void()>> 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<spdlog::sinks::basic_file_sink_mt>(log_path);
const auto main_logger = std::make_shared<spdlog::logger>("main", sink);
const auto terminal_logger = std::make_shared<spdlog::logger>("terminal", sink);
const auto cv_logger = std::make_shared<spdlog::logger>("opencv", sink);
const auto vips_logger = std::make_shared<spdlog::logger>("vips", sink);
const auto x11_logger = std::make_shared<spdlog::logger>("X11", sink);
const auto sixel_logger = std::make_shared<spdlog::logger>("sixel", sink);
const auto kitty_logger = std::make_shared<spdlog::logger>("kitty", sink);
const auto iterm2_logger = std::make_shared<spdlog::logger>("iterm2", sink);
const auto chafa_logger = std::make_shared<spdlog::logger>("chafa", sink);
const auto wayland_logger = std::make_shared<spdlog::logger>("wayland", sink);
const auto opengl_logger = std::make_shared<spdlog::logger>("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;
}

64
src/canvas.cpp Normal file
View File

@ -0,0 +1,64 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#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 <fmt/format.h>
auto Canvas::create() -> std::unique_ptr<Canvas>
{
const auto flags = Flags::instance();
const auto logger = spdlog::get("main");
#ifdef ENABLE_WAYLAND
if (flags->output == "wayland") {
return std::make_unique<WaylandCanvas>();
}
#else
logger->debug("Wayland support not compiled in the binary");
#endif
#ifdef ENABLE_X11
if (flags->output == "x11") {
return std::make_unique<X11Canvas>();
}
#else
logger->debug("X11 support not compiled in the binary");
#endif
if (flags->output == "kitty") {
return std::make_unique<StdoutCanvas<Kitty>>(flags->output);
}
if (flags->output == "iterm2") {
return std::make_unique<StdoutCanvas<Iterm2>>(flags->output);
}
if (flags->output == "sixel") {
return std::make_unique<StdoutCanvas<Sixel>>(flags->output);
}
if (flags->output == "chafa") {
return std::make_unique<StdoutCanvas<Chafa>>(flags->output);
}
throw std::runtime_error(fmt::format("output backend not supported (backend is {})", flags->output));
}

104
src/canvas/chafa.cpp Normal file
View File

@ -0,0 +1,104 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "chafa.hpp"
#include "dimensions.hpp"
#include "terminal.hpp"
#include "util.hpp"
#include "util/ptr.hpp"
#include <algorithm>
#include <cmath>
#include <iostream>
#include <range/v3/all.hpp>
#include <spdlog/spdlog.h>
void gstring_delete(GString *str)
{
g_string_free(str, true);
}
Chafa::Chafa(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
: symbol_map(chafa_symbol_map_new()),
config(chafa_canvas_config_new()),
image(std::move(new_image)),
stdout_mutex(stdout_mutex)
{
const auto envp = c_unique_ptr<gchar *, g_strfreev>{g_get_environ()};
term_info = chafa_term_db_detect(chafa_term_db_get_default(), envp.get());
const auto dims = image->dimensions();
x = dims.x + 1;
y = dims.y + 1;
horizontal_cells = std::ceil(static_cast<double>(image->width()) / dims.terminal->font_width);
vertical_cells = std::ceil(static_cast<double>(image->height()) / dims.terminal->font_height);
chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_BLOCK);
chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_BORDER);
chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_SPACE);
chafa_symbol_map_remove_by_tags(symbol_map, CHAFA_SYMBOL_TAG_WIDE);
chafa_canvas_config_set_symbol_map(config, symbol_map);
chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_SYMBOLS);
chafa_canvas_config_set_geometry(config, horizontal_cells, vertical_cells);
}
Chafa::~Chafa()
{
chafa_canvas_unref(canvas);
chafa_canvas_config_unref(config);
chafa_symbol_map_unref(symbol_map);
chafa_term_info_unref(term_info);
const std::scoped_lock lock{*stdout_mutex};
util::clear_terminal_area(x, y, horizontal_cells, vertical_cells);
}
void Chafa::draw()
{
canvas = chafa_canvas_new(config);
chafa_canvas_draw_all_pixels(canvas, CHAFA_PIXEL_BGRA8_UNASSOCIATED, image->data(), image->width(), image->height(),
image->width() * 4);
#ifdef CHAFA_VERSION_1_14
GString **lines = nullptr;
gint lines_length = 0;
chafa_canvas_print_rows(canvas, term_info, &lines, &lines_length);
auto ycoord = y;
const std::scoped_lock lock{*stdout_mutex};
util::save_cursor_position();
for (int i = 0; i < lines_length; ++i) {
const auto line = c_unique_ptr<GString, gstring_delete>{lines[i]};
util::move_cursor(ycoord++, x);
std::cout << line->str;
}
g_free(lines);
#else
const auto result = c_unique_ptr<GString, gstring_delete>{chafa_canvas_print(canvas, term_info)};
auto ycoord = y;
const auto lines = util::str_split(result->str, "\n");
const std::scoped_lock lock{*stdout_mutex};
util::save_cursor_position();
ranges::for_each(lines, [this, &ycoord](const std::string &line) {
util::move_cursor(ycoord++, x);
std::cout << line;
});
#endif
std::cout << std::flush;
util::restore_cursor_position();
}

52
src/canvas/chafa.hpp Normal file
View File

@ -0,0 +1,52 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef CHAFA_WINDOW_H
#define CHAFA_WINDOW_H
#include "image.hpp"
#include "window.hpp"
#include <memory>
#include <mutex>
#include <chafa.h>
class Chafa : public Window
{
public:
Chafa(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
~Chafa() override;
void draw() override;
void generate_frame() override{};
private:
ChafaTermInfo *term_info = nullptr;
ChafaSymbolMap *symbol_map = nullptr;
ChafaCanvasConfig *config = nullptr;
ChafaCanvas *canvas = nullptr;
std::unique_ptr<Image> image;
std::mutex *stdout_mutex;
int x;
int y;
int horizontal_cells = 0;
int vertical_cells = 0;
};
#endif

View File

@ -0,0 +1,56 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "chunk.hpp"
#include "util.hpp"
Iterm2Chunk::Iterm2Chunk(uint64_t size)
{
auto bufsize = 4*((size+2)/3);
buffer.resize(size, 0);
result.resize(bufsize + 1, 0);
}
void Iterm2Chunk::set_size(uint64_t size)
{
this->size = size;
}
auto Iterm2Chunk::get_size() const -> uint64_t
{
return size;
}
auto Iterm2Chunk::get_buffer() -> char*
{
return buffer.data();
}
auto Iterm2Chunk::get_result() -> char*
{
return result.data();
}
void Iterm2Chunk::process_chunk(std::unique_ptr<Iterm2Chunk>& chunk)
{
util::base64_encode_v2(reinterpret_cast<unsigned char*>(chunk->get_buffer()),
chunk->get_size(), reinterpret_cast<unsigned char*>(chunk->get_result()));
}
void Iterm2Chunk::operator()(std::unique_ptr<Iterm2Chunk>& chunk) const
{
process_chunk(chunk);
}

View File

@ -0,0 +1,45 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef ITERM2_CHUNK_H
#define ITERM2_CHUNK_H
#include <vector>
#include <memory>
#include <cstdint>
class Iterm2Chunk
{
public:
Iterm2Chunk() = default;
explicit Iterm2Chunk(uint64_t size);
void operator()(std::unique_ptr<Iterm2Chunk>& chunk) const;
void static process_chunk(std::unique_ptr<Iterm2Chunk>& chunk);
[[nodiscard]] auto get_size() const -> uint64_t;
void set_size(uint64_t size);
auto get_buffer() -> char*;
auto get_result() -> char*;
private:
uint64_t size;
std::vector<char> buffer;
std::vector<char> result;
};
#endif

View File

@ -0,0 +1,108 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "iterm2.hpp"
#include "chunk.hpp"
#include "dimensions.hpp"
#include "image.hpp"
#include "terminal.hpp"
#include "util.hpp"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <fmt/format.h>
#include <spdlog/spdlog.h>
#ifdef HAVE_STD_EXECUTION_H
# include <execution>
#else
# include <oneapi/tbb.h>
#endif
#include <range/v3/all.hpp>
namespace fs = std::filesystem;
Iterm2::Iterm2(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
: image(std::move(new_image)),
stdout_mutex(stdout_mutex)
{
const auto dims = image->dimensions();
x = dims.x + 1;
y = dims.y + 1;
horizontal_cells = std::ceil(static_cast<double>(image->width()) / dims.terminal->font_width);
vertical_cells = std::ceil(static_cast<double>(image->height()) / dims.terminal->font_height);
}
Iterm2::~Iterm2()
{
const std::scoped_lock lock{*stdout_mutex};
util::clear_terminal_area(x, y, horizontal_cells, vertical_cells);
}
void Iterm2::draw()
{
str.append("\033]1337;File=inline=1;");
const auto filename = image->filename();
const auto num_bytes = fs::file_size(filename);
const auto encoded_filename =
util::base64_encode(reinterpret_cast<const unsigned char *>(filename.c_str()), filename.size());
str.append(fmt::format("size={};name={};width={}px;height={}px:", num_bytes, encoded_filename, image->width(),
image->height()));
const int chunk_size = 1023;
const auto chunks = process_chunks(filename, chunk_size, num_bytes);
const int num_chunks = std::ceil(static_cast<double>(num_bytes) / chunk_size);
const uint64_t bytes_per_chunk = 4 * ((chunk_size + 2) / 3) + 100;
str.reserve((num_chunks + 2) * bytes_per_chunk);
ranges::for_each(chunks, [this](const std::unique_ptr<Iterm2Chunk> &chunk) { str.append(chunk->get_result()); });
str.append("\a");
const std::scoped_lock lock{*stdout_mutex};
util::save_cursor_position();
util::move_cursor(y, x);
std::cout << str << std::flush;
util::restore_cursor_position();
str.clear();
}
auto Iterm2::process_chunks(const std::string &filename, int chunk_size, size_t num_bytes)
-> std::vector<std::unique_ptr<Iterm2Chunk>>
{
const int num_chunks = std::ceil(static_cast<double>(num_bytes) / chunk_size);
std::vector<std::unique_ptr<Iterm2Chunk>> chunks;
chunks.reserve(num_chunks + 2);
std::ifstream ifs(filename);
while (ifs.good()) {
auto chunk = std::make_unique<Iterm2Chunk>(chunk_size);
ifs.read(chunk->get_buffer(), chunk_size);
chunk->set_size(ifs.gcount());
chunks.push_back(std::move(chunk));
}
#ifdef HAVE_STD_EXECUTION_H
std::for_each(std::execution::par_unseq, chunks.begin(), chunks.end(), Iterm2Chunk::process_chunk);
#else
oneapi::tbb::parallel_for_each(chunks.begin(), chunks.end(), Iterm2Chunk());
#endif
return chunks;
}

View File

@ -0,0 +1,51 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef ITERM2_CANVAS_H
#define ITERM2_CANVAS_H
#include "chunk.hpp"
#include "image.hpp"
#include "window.hpp"
#include <mutex>
#include <string>
#include <vector>
class Iterm2 : public Window
{
public:
Iterm2(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
~Iterm2() override;
void draw() override;
void generate_frame() override{};
private:
std::unique_ptr<Image> image;
std::mutex *stdout_mutex;
std::string str;
int x;
int y;
int horizontal_cells = 0;
int vertical_cells = 0;
static auto process_chunks(const std::string &filename, int chunk_size, size_t num_bytes)
-> std::vector<std::unique_ptr<Iterm2Chunk>>;
};
#endif

View File

@ -0,0 +1,52 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "chunk.hpp"
#include "util.hpp"
KittyChunk::KittyChunk(const unsigned char* ptr, uint64_t size):
ptr(ptr),
size(size)
{
uint64_t bufsize = 4*((size+2)/3);
result.resize(bufsize + 1, 0);
}
void KittyChunk::process_chunk(KittyChunk& chunk)
{
util::base64_encode_v2(chunk.get_ptr(), chunk.get_size(),
reinterpret_cast<unsigned char*>(chunk.get_result()));
}
void KittyChunk::operator()(KittyChunk& chunk) const
{
process_chunk(chunk);
}
auto KittyChunk::get_result() -> char*
{
return result.data();
}
auto KittyChunk::get_ptr() const -> const unsigned char*
{
return ptr;
}
auto KittyChunk::get_size() const -> uint64_t
{
return size;
}

View File

@ -0,0 +1,42 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef KITTY_CHUNK_H
#define KITTY_CHUNK_H
#include <vector>
#include <cstdint>
class KittyChunk
{
public:
KittyChunk() = default;
KittyChunk(const unsigned char* ptr, uint64_t size);
void operator()(KittyChunk& chunk) const;
auto get_result() -> char*;
[[nodiscard]] auto get_ptr() const -> const unsigned char*;
[[nodiscard]] auto get_size() const -> uint64_t;
static void process_chunk(KittyChunk& chunk);
private:
const unsigned char* ptr;
uint64_t size;
std::vector<char> result;
};
#endif

106
src/canvas/kitty/kitty.cpp Normal file
View File

@ -0,0 +1,106 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "kitty.hpp"
#include "dimensions.hpp"
#include "util.hpp"
#include <fmt/format.h>
#include <iostream>
#ifdef HAVE_STD_EXECUTION_H
# include <execution>
#else
# include <oneapi/tbb.h>
#endif
Kitty::Kitty(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
: image(std::move(new_image)),
stdout_mutex(stdout_mutex),
id(util::generate_random_number<uint32_t>(1))
{
const auto dims = image->dimensions();
x = dims.x + 1;
y = dims.y + 1;
}
Kitty::~Kitty()
{
const std::scoped_lock lock{*stdout_mutex};
std::cout << fmt::format("\033_Ga=d,d=i,i={}\033\\", id) << std::flush;
}
void Kitty::draw()
{
generate_frame();
}
void Kitty::generate_frame()
{
const int bits_per_channel = 8;
auto chunks = process_chunks();
str.append(fmt::format("\033_Ga=T,m=1,i={},q=2,f={},s={},v={};{}\033\\", id, image->channels() * bits_per_channel,
image->width(), image->height(), chunks.front().get_result()));
for (auto chunk = std::next(std::begin(chunks)); chunk != std::prev(std::end(chunks)); std::advance(chunk, 1)) {
str.append("\033_Gm=1,q=2;");
str.append(chunk->get_result());
str.append("\033\\");
}
str.append("\033_Gm=0,q=2;");
str.append(chunks.back().get_result());
str.append("\033\\");
const std::scoped_lock lock{*stdout_mutex};
util::save_cursor_position();
util::move_cursor(y, x);
std::cout << str << std::flush;
util::restore_cursor_position();
str.clear();
}
auto Kitty::process_chunks() -> std::vector<KittyChunk>
{
const uint64_t chunk_size = 3068;
uint64_t num_chunks = image->size() / chunk_size;
uint64_t last_chunk_size = image->size() % chunk_size;
if (last_chunk_size == 0) {
last_chunk_size = chunk_size;
num_chunks--;
}
const uint64_t bytes_per_chunk = 4 * ((chunk_size + 2) / 3) + 100;
str.reserve((num_chunks + 2) * bytes_per_chunk);
std::vector<KittyChunk> chunks;
chunks.reserve(num_chunks + 2);
const auto *ptr = image->data();
uint64_t idx = 0;
for (; idx < num_chunks; idx++) {
chunks.emplace_back(ptr + idx * chunk_size, chunk_size);
}
chunks.emplace_back(ptr + idx * chunk_size, last_chunk_size);
#ifdef HAVE_STD_EXECUTION_H
std::for_each(std::execution::par_unseq, std::begin(chunks), std::end(chunks), KittyChunk::process_chunk);
#else
oneapi::tbb::parallel_for_each(std::begin(chunks), std::end(chunks), KittyChunk());
#endif
return chunks;
}

View File

@ -0,0 +1,48 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef KITTY_WINDOW_H
#define KITTY_WINDOW_H
#include "chunk.hpp"
#include "image.hpp"
#include "window.hpp"
#include <memory>
#include <mutex>
#include <vector>
class Kitty : public Window
{
public:
Kitty(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
~Kitty() override;
void draw() override;
void generate_frame() override;
private:
std::string str;
std::unique_ptr<Image> image;
std::mutex *stdout_mutex;
uint32_t id;
int x;
int y;
auto process_chunks() -> std::vector<KittyChunk>;
};
#endif

96
src/canvas/sixel.cpp Normal file
View File

@ -0,0 +1,96 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "sixel.hpp"
#include "dimensions.hpp"
#include "terminal.hpp"
#include "util.hpp"
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
Sixel::Sixel(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex)
: image(std::move(new_image)),
stdout_mutex(stdout_mutex)
{
const auto dims = image->dimensions();
x = dims.x + 1;
y = dims.y + 1;
horizontal_cells = std::ceil(static_cast<double>(image->width()) / dims.terminal->font_width);
vertical_cells = std::ceil(static_cast<double>(image->height()) / dims.terminal->font_height);
const auto draw_callback = [](char *data, int size, void *priv) -> int {
auto *str = static_cast<std::string *>(priv);
str->append(data, size);
return size;
};
sixel_output_new(&output, draw_callback, &str, nullptr);
const auto file_size = fs::file_size(image->filename());
constexpr auto reserve_ratio = 50;
str.reserve(file_size * reserve_ratio);
// create dither and palette from image
sixel_dither_new(&dither, -1, nullptr);
sixel_dither_initialize(dither, const_cast<unsigned char *>(image->data()), image->width(), image->height(),
SIXEL_PIXELFORMAT_RGB888, SIXEL_LARGE_LUM, SIXEL_REP_CENTER_BOX, SIXEL_QUALITY_HIGH);
}
Sixel::~Sixel()
{
can_draw.store(false);
if (draw_thread.joinable()) {
draw_thread.join();
}
sixel_dither_destroy(dither);
sixel_output_destroy(output);
const std::scoped_lock lock{*stdout_mutex};
util::clear_terminal_area(x, y, horizontal_cells, vertical_cells);
}
void Sixel::draw()
{
if (!image->is_animated()) {
generate_frame();
return;
}
// start drawing loop
draw_thread = std::thread([this] {
while (can_draw.load()) {
generate_frame();
image->next_frame();
std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
}
});
}
void Sixel::generate_frame()
{
// output sixel content to stream
sixel_encode(const_cast<unsigned char *>(image->data()), image->width(), image->height(), 3 /*unused*/, dither,
output);
const std::scoped_lock lock{*stdout_mutex};
util::save_cursor_position();
util::move_cursor(y, x);
std::cout << str << std::flush;
util::restore_cursor_position();
str.clear();
}

57
src/canvas/sixel.hpp Normal file
View File

@ -0,0 +1,57 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef SIXEL_WINDOW_H
#define SIXEL_WINDOW_H
#include "image.hpp"
#include "window.hpp"
#include <atomic>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include <sixel.h>
class Sixel : public Window
{
public:
Sixel(std::unique_ptr<Image> new_image, std::mutex *stdout_mutex);
~Sixel() override;
void draw() override;
void generate_frame() override;
private:
std::unique_ptr<Image> image;
std::mutex *stdout_mutex;
std::string str;
std::thread draw_thread;
std::atomic<bool> can_draw{true};
int x;
int y;
int horizontal_cells = 0;
int vertical_cells = 0;
sixel_dither_t *dither = nullptr;
sixel_output_t *output = nullptr;
};
#endif

64
src/canvas/stdout.hpp Normal file
View File

@ -0,0 +1,64 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef STDOUT_CANVAS_H
#define STDOUT_CANVAS_H
#include "canvas.hpp"
#include "image.hpp"
#include "window.hpp"
#include <spdlog/spdlog.h>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
template <WindowType T>
class StdoutCanvas : public Canvas
{
public:
explicit StdoutCanvas(const std::string &output)
{
logger = spdlog::get(output);
logger->info("Canvas created");
}
~StdoutCanvas() override = default;
void add_image(const std::string &identifier, std::unique_ptr<Image> new_image) override
{
logger->info("Displaying image with id {}", identifier);
images.erase(identifier);
const auto [entry, success] =
images.emplace(identifier, std::make_unique<T>(std::move(new_image), &stdout_mutex));
entry->second->draw();
}
void remove_image(const std::string &identifier) override
{
logger->info("Removing image with id {}", identifier);
images.erase(identifier);
}
private:
std::mutex stdout_mutex;
std::shared_ptr<spdlog::logger> logger;
std::unordered_map<std::string, std::unique_ptr<T>> images;
};
#endif

View File

@ -0,0 +1,43 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "config.hpp"
#include "os.hpp"
#include "config/sway.hpp"
#include "config/hyprland.hpp"
#include "config/wayfire.hpp"
#include "config/dummy.hpp"
auto WaylandConfig::get() -> std::unique_ptr<WaylandConfig>
{
const auto sway_sock = os::getenv("SWAYSOCK");
if (sway_sock.has_value()) {
return std::make_unique<SwaySocket>(sway_sock.value());
}
const auto hypr_sig = os::getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (hypr_sig.has_value()) {
return std::make_unique<HyprlandSocket>(hypr_sig.value());
}
const auto wayfire_sock = os::getenv("WAYFIRE_SOCKET");
if (wayfire_sock.has_value()) {
return std::make_unique<WayfireSocket>(wayfire_sock.value());
}
return std::make_unique<DummyWaylandConfig>();
}

View File

@ -0,0 +1,45 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef WAYLAND_CONFIG_H
#define WAYLAND_CONFIG_H
#include <memory>
#include <string>
#include <string_view>
struct WaylandWindowGeometry {
int width;
int height;
int x;
int y;
};
class WaylandConfig
{
public:
static auto get() -> std::unique_ptr<WaylandConfig>;
virtual ~WaylandConfig() = default;
virtual auto get_focused_output_name() -> std::string = 0;
virtual auto get_window_info() -> struct WaylandWindowGeometry = 0;
virtual auto is_dummy() -> bool { return false; }
virtual void initial_setup(std::string_view appid) = 0;
virtual void move_window(std::string_view appid, int xcoord, int ycoord) = 0;
};
#endif

View File

@ -0,0 +1,29 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "dummy.hpp"
auto DummyWaylandConfig::get_window_info() -> struct WaylandWindowGeometry
{
return {};
}
void DummyWaylandConfig::initial_setup([[maybe_unused]] std::string_view appid)
{}
void DummyWaylandConfig::move_window([[maybe_unused]] std::string_view appid,
[[maybe_unused]] int xcoord, [[maybe_unused]] int ycoord)
{}

View File

@ -0,0 +1,35 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef DUMMY_WAYLAND_CONFIG_H
#define DUMMY_WAYLAND_CONFIG_H
#include "../config.hpp"
class DummyWaylandConfig : public WaylandConfig
{
public:
DummyWaylandConfig() = default;
~DummyWaylandConfig() override = default;
auto get_focused_output_name() -> std::string override { return {}; };
auto get_window_info() -> struct WaylandWindowGeometry override;
auto is_dummy() -> bool override { return true; }
void initial_setup(std::string_view appid) override;
void move_window(std::string_view appid, int xcoord, int ycoord) override;
};
#endif

View File

@ -0,0 +1,151 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "hyprland.hpp"
#include "os.hpp"
#include "tmux.hpp"
#include "util/socket.hpp"
#include <filesystem>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <range/v3/all.hpp>
using njson = nlohmann::json;
namespace fs = std::filesystem;
HyprlandSocket::HyprlandSocket(const std::string_view signature)
: logger(spdlog::get("wayland"))
{
const auto socket_base_dir = os::getenv("XDG_RUNTIME_DIR").value_or("/tmp");
const auto socket_rel_path = fmt::format("hypr/{}/.socket.sock", signature);
socket_path = fmt::format("{}/{}", socket_base_dir, socket_rel_path);
// XDG_RUNTIME_DIR set but hyprland < 0.40
if (!fs::exists(socket_path)) {
socket_path = fmt::format("/tmp/{}", socket_rel_path);
}
logger->info("Using hyprland socket {}", socket_path);
const auto active = request_result("j/activewindow");
address = active.at("address");
set_active_monitor();
}
void HyprlandSocket::set_active_monitor()
{
const auto monitors = request_result("j/monitors");
for (const auto &monitor : monitors) {
bool focused = monitor.at("focused");
if (focused) {
output_name = monitor.at("name");
output_scale = monitor.at("scale");
break;
}
}
}
auto HyprlandSocket::get_focused_output_name() -> std::string
{
return output_name;
}
auto HyprlandSocket::request_result(const std::string_view payload) -> nlohmann::json
{
const UnixSocket socket{socket_path};
socket.write(payload.data(), payload.size());
const std::string result = socket.read_until_empty();
return njson::parse(result);
}
void HyprlandSocket::request(const std::string_view payload)
{
const UnixSocket socket{socket_path};
logger->debug("Running socket command {}", payload);
socket.write(payload.data(), payload.length());
}
auto HyprlandSocket::get_active_window() -> nlohmann::json
{
// recalculate address in case it changed
if (tmux::is_used()) {
const auto active = request_result("j/activewindow");
address = active.at("address");
}
const auto clients = request_result("j/clients");
const auto client = ranges::find_if(clients, [this](const njson &json) { return json.at("address") == address; });
if (client == clients.end()) {
throw std::runtime_error("Active window not found");
}
return *client;
}
auto HyprlandSocket::get_window_info() -> struct WaylandWindowGeometry {
const auto terminal = get_active_window();
const auto &sizes = terminal.at("size");
const auto &coords = terminal.at("at");
return {
.width = sizes.at(0),
.height = sizes.at(1),
.x = coords.at(0),
.y = coords.at(1),
};
}
void HyprlandSocket::initial_setup(const std::string_view appid)
{
disable_focus(appid);
enable_floating(appid);
remove_borders(appid);
remove_rounding(appid);
}
void HyprlandSocket::remove_rounding(const std::string_view appid)
{
const auto payload = fmt::format("/keyword windowrulev2 rounding 0,title:{}", appid);
request(payload);
}
void HyprlandSocket::disable_focus(const std::string_view appid)
{
const auto payload = fmt::format("/keyword windowrulev2 nofocus,title:{}", appid);
request(payload);
}
void HyprlandSocket::enable_floating(const std::string_view appid)
{
const auto payload = fmt::format("/keyword windowrulev2 float,title:{}", appid);
request(payload);
}
void HyprlandSocket::remove_borders(const std::string_view appid)
{
const auto payload = fmt::format("/keyword windowrulev2 noborder,title:{}", appid);
request(payload);
}
void HyprlandSocket::move_window(const std::string_view appid, int xcoord, int ycoord)
{
int res_x = xcoord;
int res_y = ycoord;
if (output_scale > 1.0F) {
const int offset = 10;
res_x = res_x / 2 + offset;
res_y = res_y / 2 + offset;
}
const auto payload = fmt::format("/dispatch movewindowpixel exact {} {},title:{}", res_x, res_y, appid);
request(payload);
}

View File

@ -0,0 +1,53 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef HYPRLAND_SOCKET_H
#define HYPRLAND_SOCKET_H
#include "../config.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
class HyprlandSocket : public WaylandConfig
{
public:
explicit HyprlandSocket(std::string_view signature);
~HyprlandSocket() override = default;
auto get_window_info() -> struct WaylandWindowGeometry override;
auto get_focused_output_name() -> std::string override;
void initial_setup(std::string_view appid) override;
void move_window(std::string_view appid, int xcoord, int ycoord) override;
private:
void disable_focus(std::string_view appid);
void enable_floating(std::string_view appid);
void remove_borders(std::string_view appid);
void remove_rounding(std::string_view appid);
void request(std::string_view payload);
auto request_result(std::string_view payload) -> nlohmann::json;
auto get_active_window() -> nlohmann::json;
void set_active_monitor();
std::shared_ptr<spdlog::logger> logger;
std::string socket_path;
std::string address;
std::string output_name;
float output_scale = 1.0F;
};
#endif

View File

@ -0,0 +1,183 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "sway.hpp"
#include "application.hpp"
#include "os.hpp"
#include "tmux.hpp"
#include "util.hpp"
#include <algorithm>
#include <array>
#include <stack>
#include <string>
#include <fmt/format.h>
#include <range/v3/all.hpp>
using njson = nlohmann::json;
constexpr auto ipc_magic = std::string_view{"i3-ipc"};
constexpr auto ipc_header_size = ipc_magic.size() + 8;
SwaySocket::SwaySocket(const std::string_view endpoint)
: socket(endpoint),
logger(spdlog::get("wayland"))
{
logger->info("Using sway socket {}", endpoint);
set_active_output_info();
}
struct __attribute__((packed)) ipc_header {
std::array<char, ipc_magic.size()> magic;
uint32_t len;
uint32_t type;
};
auto SwaySocket::get_window_info() -> struct WaylandWindowGeometry {
const auto nodes = get_nodes();
const auto window = get_active_window(nodes);
const auto &rect = window.at("rect");
return {.width = rect.at("width"),
.height = rect.at("height"),
.x = rect.at("x").get<int>() - output_info.x,
.y = rect.at("y").get<int>() - output_info.y};
}
auto SwaySocket::get_active_window(const std::vector<nlohmann::json>& nodes) -> nlohmann::json
{
const auto pids = tmux::get_client_pids().value_or(std::vector<int>{Application::parent_pid});
for (const auto pid : pids) {
const auto tree = util::get_process_tree(pid);
const auto found = ranges::find_if(nodes, [&tree](const njson &json) -> bool {
try {
return ranges::find(tree, json.at("pid").get<int>()) != tree.end();
} catch (const njson::out_of_range &err) {
return false;
}
});
if (found != nodes.end()) {
return *found;
}
}
return nullptr;
}
auto SwaySocket::get_focused_output_name() -> std::string
{
return output_info.name;
};
void SwaySocket::set_active_output_info()
{
const auto outputs = ipc_message(IPC_GET_OUTPUTS);
for (const auto &node : outputs) {
bool focused = node.at("focused");
if (focused) {
const auto &rect = node.at("rect");
output_info = {
.x = rect.at("x"),
.y = rect.at("y"),
.scale = node.at("scale"),
.name = node.at("name"),
};
break;
}
}
}
void SwaySocket::initial_setup(const std::string_view appid)
{
disable_focus(appid);
enable_floating(appid);
}
void SwaySocket::disable_focus(const std::string_view appid)
{
const auto payload = fmt::format(R"(no_focus [app_id="{}"])", appid);
ipc_command(payload);
}
void SwaySocket::enable_floating(const std::string_view appid)
{
ipc_command(appid, "floating enable");
}
void SwaySocket::move_window(const std::string_view appid, int xcoord, int ycoord)
{
int res_x = xcoord;
int res_y = ycoord;
if (output_info.scale > 1.0F) {
res_x = res_x / 2;
res_y = res_y / 2;
}
const auto payload = fmt::format(R"([app_id="{}"] move position {} {})", appid, res_x, res_y);
ipc_command(payload);
}
auto SwaySocket::ipc_message(ipc_message_type type, const std::string_view payload) const -> nlohmann::json
{
struct ipc_header header;
header.len = payload.size();
header.type = type;
ipc_magic.copy(header.magic.data(), ipc_magic.size());
if (!payload.empty()) {
logger->debug("Running socket command {}", payload);
}
socket.write(&header, ipc_header_size);
socket.write(payload.data(), payload.size());
socket.read(&header, ipc_header_size);
std::string buff(header.len, 0);
socket.read(buff.data(), buff.size());
return njson::parse(buff);
}
auto SwaySocket::get_nodes() const -> std::vector<nlohmann::json>
{
logger->debug("Obtaining sway tree");
const auto tree = ipc_message(IPC_GET_TREE);
std::stack<njson> nodes_st;
std::vector<njson> nodes_vec;
nodes_st.push(tree);
while (!nodes_st.empty()) {
const auto top = nodes_st.top();
nodes_st.pop();
nodes_vec.push_back(top);
for (const auto &node : top.at("nodes")) {
nodes_st.push(node);
}
for (const auto &node : top.at("floating_nodes")) {
nodes_st.push(node);
}
}
return nodes_vec;
}
void SwaySocket::ipc_command(const std::string_view appid, const std::string_view command) const
{
const auto payload = fmt::format(R"(for_window [app_id="{}"] {})", appid, command);
ipc_command(payload);
}
void SwaySocket::ipc_command(const std::string_view payload) const
{
std::ignore = ipc_message(IPC_COMMAND, payload);
}

View File

@ -0,0 +1,62 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef SWAY_SOCKET_H
#define SWAY_SOCKET_H
#include "../config.hpp"
#include "util/socket.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
enum ipc_message_type { IPC_COMMAND = 0, IPC_GET_WORKSPACES = 1, IPC_GET_OUTPUTS = 3, IPC_GET_TREE = 4 };
struct SwayOutputInfo {
int x;
int y;
float scale;
std::string name;
};
class SwaySocket : public WaylandConfig
{
public:
explicit SwaySocket(std::string_view endpoint);
~SwaySocket() override = default;
auto get_focused_output_name() -> std::string override;
auto get_window_info() -> struct WaylandWindowGeometry override;
void initial_setup(std::string_view appid) override;
void move_window(std::string_view appid, int xcoord, int ycoord) override;
static auto get_active_window(const std::vector<nlohmann::json> &nodes) -> nlohmann::json;
private:
void disable_focus(std::string_view appid);
void enable_floating(std::string_view appid);
void ipc_command(std::string_view appid, std::string_view command) const;
void ipc_command(std::string_view payload) const;
void set_active_output_info();
[[nodiscard]] auto get_nodes() const -> std::vector<nlohmann::json>;
[[nodiscard]] auto ipc_message(ipc_message_type type, std::string_view payload = "") const -> nlohmann::json;
UnixSocket socket;
std::shared_ptr<spdlog::logger> logger;
struct SwayOutputInfo output_info;
};
#endif

View File

@ -0,0 +1,73 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "wayfire.hpp"
using njson = nlohmann::json;
WayfireSocket::WayfireSocket(const std::string_view endpoint):
socket(endpoint),
logger(spdlog::get("wayland"))
{
logger->info("Using wayfire socket {}", endpoint);
}
auto WayfireSocket::get_window_info() -> struct WaylandWindowGeometry
{
const auto response = request("window-rules/get-focused-view");
const auto& info = response.at("info");
const auto& geometry = info.at("geometry");
const int decoration_height = 25;
return {
.width = geometry.at("width"),
.height = geometry.at("height").get<int>() - decoration_height,
.x = 0,
.y = 0
};
}
void WayfireSocket::initial_setup([[maybe_unused]] const std::string_view appid)
{
// all is handled by the ueberzug plugin
}
void WayfireSocket::move_window(const std::string_view appid, int xcoord, int ycoord)
{
const njson payload_data = {
{"app-id", appid},
{"x", xcoord},
{"y", ycoord}
};
std::ignore = request("ueberzugpp/set_offset", payload_data);
}
auto WayfireSocket::request(const std::string_view method, const njson& data) const -> njson
{
const njson json = {
{"method", method},
{"data", data}
};
const auto payload = json.dump();
const uint32_t payload_size = payload.length();
socket.write(&payload_size, sizeof(uint32_t));
socket.write(payload.c_str(), payload_size);
uint32_t response_size = 0;
socket.read(&response_size, sizeof(uint32_t));
std::string buffer (response_size, 0);
socket.read(buffer.data(), response_size);
return njson::parse(buffer);
}

View File

@ -0,0 +1,44 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef WAYFIRE_SOCKET_H
#define WAYFIRE_SOCKET_H
#include "../config.hpp"
#include "util/socket.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
class WayfireSocket : public WaylandConfig
{
public:
explicit WayfireSocket(std::string_view endpoint);
~WayfireSocket() override = default;
auto get_window_info() -> struct WaylandWindowGeometry override;
auto get_focused_output_name() -> std::string override { return {}; };
void initial_setup(std::string_view appid) override;
void move_window(std::string_view appid, int xcoord, int ycoord) override;
private:
[[nodiscard]] auto request(std::string_view method, const nlohmann::json &data = {}) const -> nlohmann::json;
UnixSocket socket;
std::shared_ptr<spdlog::logger> logger;
};
#endif

View File

@ -0,0 +1,222 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "wayland.hpp"
#include "application.hpp"
#include "image.hpp"
#include "os.hpp"
#include "util.hpp"
#ifdef ENABLE_OPENGL
# include "window/waylandegl.hpp"
# include <EGL/eglext.h>
#endif
#include "window/waylandshm.hpp"
constexpr struct wl_registry_listener registry_listener = {.global = WaylandCanvas::registry_handle_global,
.global_remove = [](auto...) { /*unused*/ }};
constexpr struct xdg_wm_base_listener xdg_wm_base_listener = {.ping = WaylandCanvas::xdg_wm_base_ping};
constexpr struct wl_output_listener wl_output_listener = {.geometry = [](auto...) { /*unused*/ },
.mode = [](auto...) { /*unused*/ },
.done = WaylandCanvas::output_done,
.scale = WaylandCanvas::output_scale,
.name = WaylandCanvas::output_name,
.description = [](auto...) { /*unused*/ }};
void WaylandCanvas::registry_handle_global(void *data, wl_registry *registry, uint32_t name, const char *interface,
[[maybe_unused]] uint32_t version)
{
const std::string_view interface_str{interface};
#ifdef WSL
const uint32_t compositor_ver = 4;
const uint32_t shm_ver = 1;
const uint32_t xdg_base_ver = 1;
const uint32_t output_ver = 3;
#else
const uint32_t compositor_ver = 5;
const uint32_t shm_ver = 1;
const uint32_t xdg_base_ver = 2;
const uint32_t output_ver = 4;
#endif
auto *canvas = static_cast<WaylandCanvas *>(data);
if (interface_str == wl_compositor_interface.name) {
canvas->compositor = static_cast<struct wl_compositor *>(
wl_registry_bind(registry, name, &wl_compositor_interface, compositor_ver));
} else if (interface_str == wl_shm_interface.name) {
canvas->wl_shm = static_cast<struct wl_shm *>(wl_registry_bind(registry, name, &wl_shm_interface, shm_ver));
} else if (interface_str == xdg_wm_base_interface.name) {
canvas->xdg_base =
static_cast<struct xdg_wm_base *>(wl_registry_bind(registry, name, &xdg_wm_base_interface, xdg_base_ver));
xdg_wm_base_add_listener(canvas->xdg_base, &xdg_wm_base_listener, canvas);
} else if (interface_str == wl_output_interface.name) {
auto *output =
static_cast<struct wl_output *>(wl_registry_bind(registry, name, &wl_output_interface, output_ver));
wl_output_add_listener(output, &wl_output_listener, canvas);
}
}
void WaylandCanvas::xdg_wm_base_ping([[maybe_unused]] void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial)
{
xdg_wm_base_pong(xdg_wm_base, serial);
}
void WaylandCanvas::output_name(void *data, [[maybe_unused]] struct wl_output *output, const char *name)
{
auto *canvas = static_cast<WaylandCanvas *>(data);
canvas->output_pair.first = name;
}
void WaylandCanvas::output_scale(void *data, [[maybe_unused]] struct wl_output *output, int32_t scale)
{
auto *canvas = static_cast<WaylandCanvas *>(data);
canvas->output_pair.second = scale;
}
void WaylandCanvas::output_done(void *data, [[maybe_unused]] struct wl_output *output)
{
auto *canvas = static_cast<WaylandCanvas *>(data);
const auto active_output = canvas->config->get_focused_output_name();
if (active_output == canvas->output_pair.first) {
canvas->flags->scale_factor = canvas->output_pair.second;
canvas->flags->needs_scaling = canvas->output_pair.second > 1;
}
canvas->output_info.insert(canvas->output_pair);
}
WaylandCanvas::WaylandCanvas()
: display(wl_display_connect(nullptr))
{
if (display == nullptr) {
throw std::runtime_error("Failed to connect to wayland display.");
}
logger = spdlog::get("wayland");
flags = Flags::instance();
config = WaylandConfig::get();
registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener, this);
wl_display_roundtrip(display);
event_handler = std::thread(&WaylandCanvas::handle_events, this);
#ifdef ENABLE_OPENGL
if (flags->use_opengl) {
try {
egl = std::make_unique<EGLUtil<struct wl_display, struct wl_egl_window>>(EGL_PLATFORM_WAYLAND_EXT, display);
} catch (const std::runtime_error &) {
egl_available = false;
}
} else {
egl_available = false;
}
#endif
logger->info("Canvas created");
}
void WaylandCanvas::show()
{
for (const auto &[key, value] : windows) {
value->show();
}
}
void WaylandCanvas::hide()
{
for (const auto &[key, value] : windows) {
value->hide();
}
}
WaylandCanvas::~WaylandCanvas()
{
windows.clear();
if (event_handler.joinable()) {
event_handler.join();
}
#ifdef ENABLE_OPENGL
egl.reset();
#endif
if (wl_shm != nullptr) {
wl_shm_destroy(wl_shm);
}
if (compositor != nullptr) {
wl_compositor_destroy(compositor);
}
if (registry != nullptr) {
wl_registry_destroy(registry);
}
wl_display_disconnect(display);
}
void WaylandCanvas::add_image(const std::string &identifier, std::unique_ptr<Image> new_image)
{
std::shared_ptr<WaylandWindow> window;
#ifdef ENABLE_OPENGL
if (egl_available) {
try {
window = std::make_shared<WaylandEglWindow>(compositor, xdg_base, egl.get(), std::move(new_image),
config.get(), &xdg_agg);
} catch (const std::runtime_error &err) {
return;
}
}
#endif
if (window == nullptr) {
window = std::make_shared<WaylandShmWindow>(this, std::move(new_image), &xdg_agg, config.get());
}
window->finish_init();
windows.insert_or_assign(identifier, std::move(window));
}
void WaylandCanvas::handle_events()
{
const auto wl_fd = wl_display_get_fd(display);
bool in_event = false;
while (!Application::stop_flag) {
// prepare to read wayland events
while (wl_display_prepare_read(display) != 0) {
wl_display_dispatch_pending(display);
}
wl_display_flush(display);
try {
constexpr int waitms = 100;
in_event = os::wait_for_data_on_fd(wl_fd, waitms);
} catch (const std::system_error &err) {
Application::stop_flag = true;
break;
}
if (in_event) {
wl_display_read_events(display);
wl_display_dispatch_pending(display);
} else {
wl_display_cancel_read(display);
}
}
}
void WaylandCanvas::remove_image(const std::string &identifier)
{
windows.erase(identifier);
wl_display_flush(display);
}

View File

@ -0,0 +1,84 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef WAYLAND_CANVAS_H
#define WAYLAND_CANVAS_H
#include "canvas.hpp"
#include "config.hpp"
#include "flags.hpp"
#include "wayland-xdg-shell-client-protocol.h"
#include "window/waylandwindow.hpp"
#include <memory>
#include <thread>
#include <unordered_map>
#include <spdlog/spdlog.h>
#include <wayland-client.h>
#ifdef ENABLE_OPENGL
# include "util/egl.hpp"
# include <wayland-egl.h>
#endif
class WaylandCanvas : public Canvas
{
public:
explicit WaylandCanvas();
~WaylandCanvas() override;
static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface,
uint32_t version);
static void xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial);
static void output_scale(void *data, struct wl_output *output, int32_t scale);
static void output_name(void *data, struct wl_output *output, const char *name);
static void output_done(void *data, struct wl_output *output);
void add_image(const std::string &identifier, std::unique_ptr<Image> new_image) override;
void remove_image(const std::string &identifier) override;
void show() override;
void hide() override;
struct wl_compositor *compositor = nullptr;
struct wl_shm *wl_shm = nullptr;
struct xdg_wm_base *xdg_base = nullptr;
std::pair<std::string, int32_t> output_pair;
std::unordered_map<std::string, int32_t> output_info;
private:
struct wl_display *display = nullptr;
struct wl_registry *registry = nullptr;
std::thread event_handler;
std::shared_ptr<spdlog::logger> logger;
std::unique_ptr<WaylandConfig> config;
std::shared_ptr<Flags> flags;
std::unordered_map<std::string, std::shared_ptr<WaylandWindow>> windows;
#ifdef ENABLE_OPENGL
std::unique_ptr<EGLUtil<struct wl_display, struct wl_egl_window>> egl;
bool egl_available = true;
#endif
struct XdgStructAgg xdg_agg;
void handle_events();
};
#endif

View File

@ -0,0 +1,75 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "shm.hpp"
#include "util.hpp"
#include "util/ptr.hpp"
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fmt/format.h>
#include <cerrno>
#include <system_error>
WaylandShm::WaylandShm(int width, int height, int scale_factor, struct wl_shm *shm)
: shm(shm),
width(width),
height(height),
stride(width * 4),
pool_size(height * stride * scale_factor)
{
const int path_size = 32;
shm_path = fmt::format("/{}", util::generate_random_string(path_size));
create_shm_file();
allocate_pool_buffers();
}
void WaylandShm::create_shm_file()
{
fd = memfd_create("ueberzugpp-shm", 0);
if (fd == -1) {
throw std::system_error(errno, std::system_category());
}
int res = ftruncate(fd, pool_size);
if (res == -1) {
throw std::system_error(errno, std::system_category());
}
auto *pool_ptr = mmap(nullptr, pool_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (pool_ptr == MAP_FAILED) {
throw std::system_error(errno, std::system_category());
}
pool_data = static_cast<uint8_t *>(pool_ptr);
}
void WaylandShm::allocate_pool_buffers()
{
const auto pool = c_unique_ptr<struct wl_shm_pool, wl_shm_pool_destroy>{wl_shm_create_pool(shm, fd, pool_size)};
buffer = wl_shm_pool_create_buffer(pool.get(), 0, width, height, stride, WL_SHM_FORMAT_ARGB8888);
}
WaylandShm::~WaylandShm()
{
shm_unlink(shm_path.c_str());
close(fd);
munmap(pool_data, pool_size);
if (buffer != nullptr) {
wl_buffer_destroy(buffer);
}
}

View File

@ -0,0 +1,47 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef WAYLAND_SHM_H
#define WAYLAND_SHM_H
#include <string>
#include <wayland-client.h>
class WaylandShm
{
public:
WaylandShm(int width, int height, int scale_factor, struct wl_shm *shm);
~WaylandShm();
struct wl_buffer *buffer = nullptr;
uint8_t *pool_data;
private:
void create_shm_file();
void allocate_pool_buffers();
struct wl_shm *shm = nullptr;
int fd = 0;
std::string shm_path;
int width = 0;
int height = 0;
int stride = 0;
int pool_size = 0;
};
#endif

View File

@ -0,0 +1,235 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "waylandegl.hpp"
#include "dimensions.hpp"
#include "util.hpp"
#include <fmt/format.h>
#include <thread>
constexpr int id_len = 10;
constexpr struct xdg_surface_listener xdg_surface_listener_egl = {
.configure = WaylandEglWindow::xdg_surface_configure,
};
constexpr struct wl_callback_listener frame_listener_egl = {.done = WaylandEglWindow::wl_surface_frame_done};
WaylandEglWindow::WaylandEglWindow(struct wl_compositor *compositor, struct xdg_wm_base *xdg_base,
const EGLUtil<struct wl_display, struct wl_egl_window> *egl,
std::unique_ptr<Image> new_image, WaylandConfig *new_config,
struct XdgStructAgg *xdg_agg)
: compositor(compositor),
xdg_base(xdg_base),
surface(wl_compositor_create_surface(compositor)),
xdg_surface(xdg_wm_base_get_xdg_surface(xdg_base, surface)),
xdg_toplevel(xdg_surface_get_toplevel(xdg_surface)),
image(std::move(new_image)),
config(new_config),
egl_window(wl_egl_window_create(surface, image->width(), image->height())),
egl(egl),
appid(fmt::format("ueberzugpp_{}", util::generate_random_string(id_len))),
xdg_agg(xdg_agg)
{
config->initial_setup(appid);
opengl_setup();
xdg_setup();
}
WaylandEglWindow::~WaylandEglWindow()
{
opengl_cleanup();
delete_xdg_structs();
delete_wayland_structs();
}
void WaylandEglWindow::opengl_cleanup()
{
egl->run_contained(egl_context, egl_surface, [this] {
glDeleteTextures(1, &texture);
glDeleteFramebuffers(1, &fbo);
});
eglDestroySurface(egl->display, egl_surface);
eglDestroyContext(egl->display, egl_context);
}
void WaylandEglWindow::finish_init()
{
auto xdg = std::make_unique<XdgStruct>();
xdg->ptr = weak_from_this();
this_ptr = xdg.get();
xdg_agg->ptrs.push_back(std::move(xdg));
setup_listeners();
visible = true;
}
void WaylandEglWindow::opengl_setup()
{
egl_surface = egl->create_surface(egl_window);
if (egl_surface == EGL_NO_SURFACE) {
throw std::runtime_error("");
}
egl_context = egl->create_context(egl_surface);
if (egl_context == EGL_NO_CONTEXT) {
throw std::runtime_error("");
}
egl->run_contained(egl_surface, egl_context, [this] {
eglSwapInterval(egl->display, 0);
glGenFramebuffers(1, &fbo);
glGenTextures(1, &texture);
});
}
void WaylandEglWindow::setup_listeners()
{
xdg_surface_add_listener(xdg_surface, &xdg_surface_listener_egl, this_ptr);
wl_surface_commit(surface);
if (image->is_animated()) {
callback = wl_surface_frame(surface);
wl_callback_add_listener(callback, &frame_listener_egl, this_ptr);
}
}
void WaylandEglWindow::xdg_setup()
{
xdg_toplevel_set_app_id(xdg_toplevel, appid.c_str());
xdg_toplevel_set_title(xdg_toplevel, appid.c_str());
}
void WaylandEglWindow::delete_xdg_structs()
{
if (xdg_toplevel != nullptr) {
xdg_toplevel_destroy(xdg_toplevel);
xdg_toplevel = nullptr;
}
if (xdg_surface != nullptr) {
xdg_surface_destroy(xdg_surface);
xdg_surface = nullptr;
}
}
void WaylandEglWindow::delete_wayland_structs()
{
if (egl_window != nullptr) {
wl_egl_window_destroy(egl_window);
egl_window = nullptr;
}
if (surface != nullptr) {
wl_surface_destroy(surface);
surface = nullptr;
}
}
void WaylandEglWindow::draw()
{
load_framebuffer();
wl_surface_commit(surface);
move_window();
}
void WaylandEglWindow::load_framebuffer()
{
std::scoped_lock lock{egl_mutex};
egl->run_contained(egl_surface, egl_context, [this] {
egl->get_texture_from_image(*image, texture);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glBlitFramebuffer(0, 0, image->width(), image->height(), 0, 0, image->width(), image->height(),
GL_COLOR_BUFFER_BIT, GL_NEAREST);
eglSwapBuffers(egl->display, egl_surface);
});
}
void WaylandEglWindow::move_window()
{
const auto dims = image->dimensions();
const auto cur_window = config->get_window_info();
const int wayland_x = dims.xpixels() + dims.padding_horizontal;
const int wayland_y = dims.ypixels() + dims.padding_vertical;
const int xcoord = cur_window.x + wayland_x;
const int ycoord = cur_window.y + wayland_y;
config->move_window(appid, xcoord, ycoord);
}
void WaylandEglWindow::generate_frame()
{
std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
callback = wl_surface_frame(surface);
wl_callback_add_listener(callback, &frame_listener_egl, this_ptr);
image->next_frame();
load_framebuffer();
wl_surface_commit(surface);
}
void WaylandEglWindow::show()
{
if (visible) {
return;
}
visible = true;
xdg_surface = xdg_wm_base_get_xdg_surface(xdg_base, surface);
xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
xdg_setup();
setup_listeners();
}
void WaylandEglWindow::hide()
{
if (!visible) {
return;
}
visible = false;
const std::scoped_lock lock{draw_mutex};
delete_xdg_structs();
wl_surface_attach(surface, nullptr, 0, 0);
wl_surface_commit(surface);
}
void WaylandEglWindow::xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
{
xdg_surface_ack_configure(xdg_surface, serial);
const auto *tmp = static_cast<struct XdgStruct *>(data);
const auto window = tmp->ptr.lock();
if (!window) {
return;
}
auto *egl_window = dynamic_cast<WaylandEglWindow *>(window.get());
egl_window->draw();
}
void WaylandEglWindow::wl_surface_frame_done(void *data, struct wl_callback *callback, [[maybe_unused]] uint32_t time)
{
wl_callback_destroy(callback);
const auto *tmp = static_cast<struct XdgStruct *>(data);
const auto window = tmp->ptr.lock();
if (!window) {
return;
}
auto *egl_window = dynamic_cast<WaylandEglWindow *>(window.get());
const std::scoped_lock lock{egl_window->draw_mutex};
if (!egl_window->visible) {
return;
}
egl_window->generate_frame();
}

View File

@ -0,0 +1,88 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef WAYLAND_EGL_WINDOW_H
#define WAYLAND_EGL_WINDOW_H
#include "../config.hpp"
#include "image.hpp"
#include "util/egl.hpp"
#include "wayland-xdg-shell-client-protocol.h"
#include "waylandwindow.hpp"
#include <wayland-client.h>
#include <wayland-egl.h>
#include <memory>
#include <mutex>
class WaylandEglWindow : public WaylandWindow
{
public:
WaylandEglWindow(struct wl_compositor *compositor, struct xdg_wm_base *xdg_base,
const EGLUtil<struct wl_display, struct wl_egl_window> *egl, std::unique_ptr<Image> new_image,
WaylandConfig *new_config, struct XdgStructAgg *xdg_agg);
~WaylandEglWindow() override;
static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial);
static void wl_surface_frame_done(void *data, struct wl_callback *callback, uint32_t time);
void draw() override;
void generate_frame() override;
void show() override;
void hide() override;
void finish_init() override;
private:
struct wl_compositor *compositor;
struct xdg_wm_base *xdg_base;
struct wl_surface *surface = nullptr;
struct xdg_surface *xdg_surface = nullptr;
struct xdg_toplevel *xdg_toplevel = nullptr;
struct wl_callback *callback;
std::unique_ptr<Image> image;
WaylandConfig *config;
EGLSurface egl_surface;
EGLContext egl_context;
struct wl_egl_window *egl_window = nullptr;
const EGLUtil<struct wl_display, struct wl_egl_window> *egl;
GLuint texture;
GLuint fbo;
std::mutex draw_mutex;
std::mutex egl_mutex;
std::string appid;
void *this_ptr;
struct XdgStructAgg *xdg_agg;
bool visible = false;
void move_window();
void delete_wayland_structs();
void delete_xdg_structs();
void opengl_cleanup();
void xdg_setup();
void setup_listeners();
void opengl_setup();
void load_framebuffer();
};
#endif

View File

@ -0,0 +1,186 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "waylandshm.hpp"
#include "dimensions.hpp"
#include "shm.hpp"
#include "util.hpp"
#include <fmt/format.h>
constexpr int id_len = 10;
constexpr struct xdg_surface_listener xdg_surface_listener = {
.configure = WaylandShmWindow::xdg_surface_configure,
};
constexpr struct wl_callback_listener frame_listener = {.done = WaylandShmWindow::wl_surface_frame_done};
void WaylandShmWindow::xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
{
xdg_surface_ack_configure(xdg_surface, serial);
const auto *tmp = static_cast<struct XdgStruct *>(data);
const auto window = tmp->ptr.lock();
if (!window) {
return;
}
auto *shm_window = dynamic_cast<WaylandShmWindow *>(window.get());
shm_window->wl_draw(shm_window->output_scale);
}
void WaylandShmWindow::wl_surface_frame_done(void *data, struct wl_callback *callback, [[maybe_unused]] uint32_t time)
{
wl_callback_destroy(callback);
const auto *tmp = static_cast<struct XdgStruct *>(data);
const auto window = tmp->ptr.lock();
if (!window) {
return;
}
auto *shm_window = dynamic_cast<WaylandShmWindow *>(window.get());
const std::scoped_lock lock{shm_window->draw_mutex};
if (!shm_window->visible) {
return;
}
shm_window->generate_frame();
}
WaylandShmWindow::WaylandShmWindow(WaylandCanvas *canvas, std::unique_ptr<Image> new_image,
struct XdgStructAgg *xdg_agg, WaylandConfig *config)
: config(config),
xdg_base(canvas->xdg_base),
surface(wl_compositor_create_surface(canvas->compositor)),
xdg_surface(xdg_wm_base_get_xdg_surface(xdg_base, surface)),
xdg_toplevel(xdg_surface_get_toplevel(xdg_surface)),
image(std::move(new_image)),
appid(fmt::format("ueberzugpp_{}", util::generate_random_string(id_len))),
xdg_agg(xdg_agg)
{
config->initial_setup(appid);
xdg_setup();
output_scale = canvas->output_info.at(config->get_focused_output_name());
shm = std::make_unique<WaylandShm>(image->width(), image->height(), output_scale, canvas->wl_shm);
}
void WaylandShmWindow::finish_init()
{
auto xdg = std::make_unique<XdgStruct>();
xdg->ptr = weak_from_this();
this_ptr = xdg.get();
xdg_agg->ptrs.push_back(std::move(xdg));
setup_listeners();
visible = true;
}
void WaylandShmWindow::setup_listeners()
{
xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, this_ptr);
wl_surface_commit(surface);
if (image->is_animated()) {
callback = wl_surface_frame(surface);
wl_callback_add_listener(callback, &frame_listener, this_ptr);
}
}
void WaylandShmWindow::xdg_setup()
{
xdg_toplevel_set_app_id(xdg_toplevel, appid.c_str());
xdg_toplevel_set_title(xdg_toplevel, appid.c_str());
}
WaylandShmWindow::~WaylandShmWindow()
{
delete_xdg_structs();
delete_wayland_structs();
}
void WaylandShmWindow::wl_draw(int32_t scale_factor)
{
std::memcpy(shm->pool_data, image->data(), image->size());
wl_surface_attach(surface, shm->buffer, 0, 0);
wl_surface_set_buffer_scale(surface, scale_factor);
wl_surface_commit(surface);
move_window();
}
void WaylandShmWindow::show()
{
if (visible) {
return;
}
visible = true;
xdg_surface = xdg_wm_base_get_xdg_surface(xdg_base, surface);
xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
xdg_setup();
setup_listeners();
}
void WaylandShmWindow::hide()
{
if (!visible) {
return;
}
visible = false;
const std::scoped_lock lock{draw_mutex};
delete_xdg_structs();
wl_surface_attach(surface, nullptr, 0, 0);
wl_surface_commit(surface);
}
void WaylandShmWindow::delete_xdg_structs()
{
if (xdg_toplevel != nullptr) {
xdg_toplevel_destroy(xdg_toplevel);
xdg_toplevel = nullptr;
}
if (xdg_surface != nullptr) {
xdg_surface_destroy(xdg_surface);
xdg_surface = nullptr;
}
}
void WaylandShmWindow::delete_wayland_structs()
{
if (surface != nullptr) {
wl_surface_destroy(surface);
surface = nullptr;
}
}
void WaylandShmWindow::move_window()
{
const auto dims = image->dimensions();
const auto cur_window = config->get_window_info();
const int wayland_x = dims.xpixels() + dims.padding_horizontal;
const int wayland_y = dims.ypixels() + dims.padding_vertical;
const int xcoord = cur_window.x + wayland_x;
const int ycoord = cur_window.y + wayland_y;
config->move_window(appid, xcoord, ycoord);
}
void WaylandShmWindow::generate_frame()
{
std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
callback = wl_surface_frame(surface);
wl_callback_add_listener(callback, &frame_listener, this_ptr);
image->next_frame();
std::memcpy(shm->pool_data, image->data(), image->size());
wl_surface_attach(surface, shm->buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, image->width(), image->height());
wl_surface_commit(surface);
}

View File

@ -0,0 +1,77 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef WAYLAND_SHM_WINDOW_H
#define WAYLAND_SHM_WINDOW_H
#include "../wayland.hpp"
#include "image.hpp"
#include "shm.hpp"
#include "wayland-xdg-shell-client-protocol.h"
#include "waylandwindow.hpp"
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <wayland-client.h>
class WaylandShmWindow : public WaylandWindow
{
public:
WaylandShmWindow(WaylandCanvas *canvas, std::unique_ptr<Image> new_image, struct XdgStructAgg *xdg_agg,
WaylandConfig *config);
~WaylandShmWindow() override;
static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial);
static void wl_surface_frame_done(void *data, struct wl_callback *callback, uint32_t time);
void draw() override {}
void wl_draw(int32_t scale_factor) override;
void generate_frame() override;
void show() override;
void hide() override;
void finish_init() override;
std::mutex draw_mutex;
std::atomic<bool> visible{false};
std::unique_ptr<WaylandShm> shm;
int32_t output_scale;
private:
WaylandConfig *config;
struct xdg_wm_base *xdg_base = nullptr;
struct wl_surface *surface = nullptr;
struct xdg_surface *xdg_surface = nullptr;
struct xdg_toplevel *xdg_toplevel = nullptr;
struct wl_callback *callback;
std::unique_ptr<Image> image;
std::string appid;
struct XdgStructAgg *xdg_agg;
void *this_ptr;
void move_window();
void xdg_setup();
void setup_listeners();
void delete_wayland_structs();
void delete_xdg_structs();
};
#endif

View File

@ -0,0 +1,46 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef WAYLAND_WINDOW_H
#define WAYLAND_WINDOW_H
#include "window.hpp"
#include <memory>
#include <vector>
class WaylandWindow:
public Window,
public std::enable_shared_from_this<WaylandWindow>
{
public:
~WaylandWindow() override = default;
virtual void wl_draw([[maybe_unused]] int32_t scale_factor) {};
virtual void finish_init() = 0;
};
struct XdgStruct
{
std::weak_ptr<WaylandWindow> ptr;
};
struct XdgStructAgg
{
std::vector<std::unique_ptr<XdgStruct>> ptrs;
};
#endif

View File

@ -0,0 +1,119 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "x11.hpp"
#include "dimensions.hpp"
#include <string_view>
#include <xcb/xcb.h>
constexpr std::string_view win_name = "ueberzugpp";
X11Window::X11Window(xcb_connection_t *connection, xcb_screen_t *screen, xcb_window_t window, xcb_window_t parent,
std::shared_ptr<Image> image)
: connection(connection),
screen(screen),
window(window),
parent(parent),
gc(xcb_generate_id(connection)),
image(std::move(image))
{
logger = spdlog::get("X11");
create();
change_title();
}
void X11Window::create()
{
const uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP;
struct xcb_create_window_value_list_t value_list;
value_list.background_pixel = screen->black_pixel;
value_list.border_pixel = screen->black_pixel;
value_list.event_mask = XCB_EVENT_MASK_EXPOSURE;
value_list.colormap = screen->default_colormap;
const auto dimensions = image->dimensions();
const auto xcoord = static_cast<int16_t>(dimensions.xpixels() + dimensions.padding_horizontal);
const auto ycoord = static_cast<int16_t>(dimensions.ypixels() + dimensions.padding_vertical);
xcb_create_window_aux(connection, screen->root_depth, window, this->parent, xcoord, ycoord, image->width(),
image->height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, value_mask,
&value_list);
xcb_create_gc(connection, gc, window, 0, nullptr);
logger->debug("Created child window {} at ({},{}) with parent {}", window, xcoord, ycoord, parent);
}
void X11Window::change_title()
{
const int bits_in_char = 8;
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, bits_in_char,
win_name.size(), win_name.data());
}
void X11Window::show()
{
if (visible) {
return;
}
visible = true;
xcb_map_window(connection, window);
xcb_flush(connection);
}
void X11Window::hide()
{
if (!visible) {
return;
}
visible = false;
xcb_unmap_window(connection, window);
xcb_flush(connection);
}
void X11Window::draw()
{
if (!xcb_image) {
return;
}
xcb_image_put(connection, window, gc, xcb_image.get(), 0, 0, 0);
}
void X11Window::generate_frame()
{
xcb_image.reset(xcb_image_create_native(connection, image->width(), image->height(), XCB_IMAGE_FORMAT_Z_PIXMAP,
screen->root_depth, nullptr, 0, nullptr));
xcb_image->data = const_cast<unsigned char *>(image->data());
send_expose_event();
}
X11Window::~X11Window()
{
xcb_destroy_window(connection, window);
xcb_free_gc(connection, gc);
xcb_flush(connection);
}
void X11Window::send_expose_event()
{
const int event_size = 32;
std::array<char, event_size> buffer;
auto *event = reinterpret_cast<xcb_expose_event_t *>(buffer.data());
event->response_type = XCB_EXPOSE;
event->window = window;
xcb_send_event(connection, 0, window, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast<char *>(event));
xcb_flush(connection);
}

View File

@ -0,0 +1,60 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef X11_WINDOW_H
#define X11_WINDOW_H
#include "image.hpp"
#include "util/ptr.hpp"
#include "window.hpp"
#include <xcb/xcb_image.h>
#include <spdlog/spdlog.h>
class Dimensions;
class X11Window : public Window
{
public:
X11Window(xcb_connection_t* connection, xcb_screen_t *screen,
xcb_window_t window, xcb_window_t parent, std::shared_ptr<Image> image);
~X11Window() override;
void draw() override;
void generate_frame() override;
void show() override;
void hide() override;
private:
xcb_connection_t *connection;
xcb_screen_t *screen;
xcb_window_t window;
xcb_window_t parent;
xcb_gcontext_t gc;
c_unique_ptr<xcb_image_t, xcb_image_destroy> xcb_image;
std::shared_ptr<spdlog::logger> logger;
std::shared_ptr<Image> image;
bool visible = false;
void send_expose_event();
void create();
void change_title();
};
#endif

View File

@ -0,0 +1,138 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "x11egl.hpp"
#include "dimensions.hpp"
#include "image.hpp"
#include "util.hpp"
#include <array>
X11EGLWindow::X11EGLWindow(xcb_connection_t *connection, xcb_screen_t *screen, xcb_window_t windowid,
xcb_window_t parentid, const EGLUtil<xcb_connection_t, xcb_window_t> *egl,
std::shared_ptr<Image> new_image)
: connection(connection),
screen(screen),
windowid(windowid),
parentid(parentid),
image(std::move(new_image)),
egl(egl)
{
logger = spdlog::get("x11");
create();
opengl_setup();
}
X11EGLWindow::~X11EGLWindow()
{
egl->run_contained(egl_surface, egl_context, [this] {
glDeleteTextures(1, &texture);
glDeleteFramebuffers(1, &fbo);
});
eglDestroySurface(egl->display, egl_surface);
eglDestroyContext(egl->display, egl_context);
xcb_destroy_window(connection, windowid);
xcb_flush(connection);
}
void X11EGLWindow::create()
{
const uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP;
struct xcb_create_window_value_list_t value_list;
value_list.background_pixel = screen->black_pixel;
value_list.border_pixel = screen->black_pixel;
value_list.event_mask = XCB_EVENT_MASK_EXPOSURE;
value_list.colormap = screen->default_colormap;
const auto dimensions = image->dimensions();
const auto xcoord = static_cast<int16_t>(dimensions.xpixels() + dimensions.padding_horizontal);
const auto ycoord = static_cast<int16_t>(dimensions.ypixels() + dimensions.padding_vertical);
xcb_create_window_aux(connection, screen->root_depth, windowid, parentid, xcoord, ycoord, image->width(),
image->height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, value_mask,
&value_list);
}
void X11EGLWindow::opengl_setup()
{
egl_surface = egl->create_surface(&windowid);
if (egl_surface == EGL_NO_SURFACE) {
throw std::runtime_error("");
}
egl_context = egl->create_context(egl_surface);
if (egl_context == EGL_NO_CONTEXT) {
throw std::runtime_error("");
}
egl->run_contained(egl_surface, egl_context, [this] {
glGenFramebuffers(1, &fbo);
glGenTextures(1, &texture);
});
}
void X11EGLWindow::draw()
{
const std::scoped_lock lock{egl_mutex};
egl->run_contained(egl_surface, egl_context, [this] {
glBlitFramebuffer(0, 0, image->width(), image->height(), 0, 0, image->width(), image->height(),
GL_COLOR_BUFFER_BIT, GL_NEAREST);
eglSwapBuffers(egl->display, egl_surface);
});
}
void X11EGLWindow::generate_frame()
{
const std::scoped_lock lock{egl_mutex};
egl->run_contained(egl_surface, egl_context, [this] {
egl->get_texture_from_image(*image, texture);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
});
send_expose_event();
}
void X11EGLWindow::show()
{
if (visible) {
return;
}
visible = true;
xcb_map_window(connection, windowid);
xcb_flush(connection);
}
void X11EGLWindow::hide()
{
if (!visible) {
return;
}
visible = false;
xcb_unmap_window(connection, windowid);
xcb_flush(connection);
}
void X11EGLWindow::send_expose_event()
{
const int event_size = 32;
std::array<char, event_size> buffer;
auto *event = reinterpret_cast<xcb_expose_event_t *>(buffer.data());
event->response_type = XCB_EXPOSE;
event->window = windowid;
xcb_send_event(connection, 0, windowid, XCB_EVENT_MASK_EXPOSURE, reinterpret_cast<char *>(event));
xcb_flush(connection);
}

View File

@ -0,0 +1,68 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef X11_EGL_WINDOW_H
#define X11_EGL_WINDOW_H
#include "window.hpp"
#include "util/egl.hpp"
#include <xcb/xcb.h>
#include <spdlog/spdlog.h>
#include <memory>
#include <mutex>
class Image;
class X11EGLWindow : public Window
{
public:
X11EGLWindow(xcb_connection_t* connection, xcb_screen_t* screen,
xcb_window_t windowid, xcb_window_t parentid, const EGLUtil<xcb_connection_t, xcb_window_t>* egl,
std::shared_ptr<Image> new_image);
~X11EGLWindow() override;
void draw() override;
void generate_frame() override;
void show() override;
void hide() override;
private:
xcb_connection_t* connection;
xcb_screen_t* screen;
xcb_window_t windowid;
xcb_window_t parentid;
std::shared_ptr<Image> image;
const EGLUtil<xcb_connection_t, xcb_window_t>* egl;
GLuint texture;
GLuint fbo;
EGLContext egl_context;
EGLSurface egl_surface;
std::mutex egl_mutex;
std::shared_ptr<spdlog::logger> logger;
bool visible = false;
void send_expose_event();
void create();
void change_title();
void opengl_setup();
};
#endif

257
src/canvas/x11/x11.cpp Normal file
View File

@ -0,0 +1,257 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "x11.hpp"
#include "application.hpp"
#include "flags.hpp"
#include "os.hpp"
#include "tmux.hpp"
#include "util.hpp"
#include <string_view>
#include <range/v3/all.hpp>
#ifdef ENABLE_OPENGL
# include "window/x11egl.hpp"
# include <EGL/eglext.h>
#endif
#include "window/x11.hpp"
X11Canvas::X11Canvas()
: connection(xcb_connect(nullptr, nullptr))
{
if (xcb_connection_has_error(connection) > 0) {
throw std::runtime_error("Can't connect to X11 server");
}
screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data;
#ifdef ENABLE_XCB_ERRORS
xcb_errors_context_new(connection, &err_ctx);
#endif
flags = Flags::instance();
#ifdef ENABLE_OPENGL
if (flags->use_opengl) {
try {
egl = std::make_unique<EGLUtil<xcb_connection_t, xcb_window_t>>(EGL_PLATFORM_XCB_EXT, connection);
} catch (const std::runtime_error &err) {
egl_available = false;
}
} else {
egl_available = false;
}
#endif
xutil = std::make_unique<X11Util>(connection);
logger = spdlog::get("X11");
event_handler = std::thread(&X11Canvas::handle_events, this);
logger->info("Canvas created");
}
X11Canvas::~X11Canvas()
{
draw_threads.clear();
windows.clear();
image_windows.clear();
if (event_handler.joinable()) {
event_handler.join();
}
#ifdef ENABLE_XCB_ERRORS
xcb_errors_context_free(err_ctx);
#endif
xcb_disconnect(connection);
}
void X11Canvas::draw(const std::string &identifier)
{
if (!images.at(identifier)->is_animated()) {
for (const auto &[wid, window] : image_windows.at(identifier)) {
window->generate_frame();
}
return;
}
draw_threads.insert_or_assign(identifier, std::jthread([this, identifier](const std::stop_token &stoken) {
const auto image = images.at(identifier);
const auto wins = image_windows.at(identifier);
while (!stoken.stop_requested()) {
for (const auto &[wid, window] : wins) {
window->generate_frame();
}
image->next_frame();
std::this_thread::sleep_for(std::chrono::milliseconds(image->frame_delay()));
}
}));
}
void X11Canvas::show()
{
const std::scoped_lock lock{windows_mutex};
for (const auto &[wid, window] : windows) {
window->show();
}
}
void X11Canvas::hide()
{
const std::scoped_lock lock{windows_mutex};
for (const auto &[wid, window] : windows) {
window->hide();
}
}
void X11Canvas::handle_events()
{
const int event_mask = 0x80;
const int waitms = 100;
const int connfd = xcb_get_file_descriptor(connection);
bool status = false;
while (!Application::stop_flag) {
try {
status = os::wait_for_data_on_fd(connfd, waitms);
} catch (const std::system_error &err) {
Application::stop_flag = true;
break;
}
if (!status) {
continue;
}
const std::scoped_lock lock{windows_mutex};
auto event = unique_C_ptr<xcb_generic_event_t>{xcb_poll_for_event(connection)};
while (event) {
const int real_event = event->response_type & ~event_mask;
switch (real_event) {
case 0: {
const auto *err = reinterpret_cast<xcb_generic_error_t *>(event.get());
print_xcb_error(err);
break;
}
case XCB_EXPOSE: {
const auto *expose = reinterpret_cast<xcb_expose_event_t *>(event.get());
try {
logger->debug("Received expose event for window {}", expose->window);
const auto window = windows.at(expose->window);
window->draw();
} catch (const std::out_of_range &oor) {
logger->debug("Discarding expose event for window {}", expose->window);
}
break;
}
default: {
logger->debug("Received unknown event {}", real_event);
break;
}
}
event.reset(xcb_poll_for_event(connection));
}
}
}
void X11Canvas::add_image(const std::string &identifier, std::unique_ptr<Image> new_image)
{
remove_image(identifier);
logger->debug("Initializing canvas");
images.insert({identifier, std::move(new_image)});
image_windows.insert({identifier, {}});
const auto image = images.at(identifier);
const auto dims = image->dimensions();
std::unordered_set<xcb_window_t> parent_ids{dims.terminal->x11_wid};
get_tmux_window_ids(parent_ids);
ranges::for_each(parent_ids, [this, &identifier, &image](xcb_window_t parent) {
const auto window_id = xcb_generate_id(connection);
std::shared_ptr<Window> window;
#ifdef ENABLE_OPENGL
if (egl_available) {
try {
window = std::make_shared<X11EGLWindow>(connection, screen, window_id, parent, egl.get(), image);
} catch (const std::runtime_error &err) {
return;
}
}
#endif
if (window == nullptr) {
window = std::make_shared<X11Window>(connection, screen, window_id, parent, image);
}
windows.insert({window_id, window});
image_windows.at(identifier).insert({window_id, window});
window->show();
});
draw(identifier);
}
void X11Canvas::get_tmux_window_ids(std::unordered_set<xcb_window_t> &windows)
{
const auto pids = tmux::get_client_pids();
if (!pids.has_value()) {
return;
}
const auto pid_window_map = xutil->get_pid_window_map();
for (const auto pid : pids.value()) {
const auto ppids = util::get_process_tree(pid);
for (const auto ppid : ppids) {
const auto win = pid_window_map.find(ppid);
if (win == pid_window_map.end()) {
continue;
}
windows.insert(win->second);
break; // prevent multiple windows being created
}
}
}
void X11Canvas::print_xcb_error(const xcb_generic_error_t *err)
{
#ifdef ENABLE_XCB_ERRORS
const char *extension = nullptr;
const char *major = xcb_errors_get_name_for_major_code(err_ctx, err->major_code);
const char *minor = xcb_errors_get_name_for_minor_code(err_ctx, err->major_code, err->minor_code);
const char *error = xcb_errors_get_name_for_error(err_ctx, err->error_code, &extension);
const std::string_view ext_str = extension != nullptr ? extension : "no_extension";
const std::string_view minor_str = minor != nullptr ? minor : "no_minor";
logger->error("XCB: {}:{}, {}:{}, resource {} sequence {}", error, ext_str, major, minor_str, err->resource_id,
err->sequence);
#else
logger->error("XCB: resource {} sequence {}", err->resource_id, err->sequence);
#endif
}
void X11Canvas::remove_image(const std::string &identifier)
{
draw_threads.erase(identifier);
images.erase(identifier);
const std::scoped_lock lock{windows_mutex};
const auto old_windows = image_windows.extract(identifier);
if (old_windows.empty()) {
return;
}
for (const auto &[key, value] : old_windows.mapped()) {
windows.erase(key);
}
}

93
src/canvas/x11/x11.hpp Normal file
View File

@ -0,0 +1,93 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef X11_CANVAS_H
#define X11_CANVAS_H
#include "canvas.hpp"
#include "image.hpp"
#include "window.hpp"
#include "dimensions.hpp"
#include "util/x11.hpp"
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <thread>
#include <mutex>
#include <xcb/xcb.h>
#include <spdlog/spdlog.h>
#ifdef ENABLE_XCB_ERRORS
# include <xcb/xcb_errors.h>
#endif
#ifdef ENABLE_OPENGL
# include "util/egl.hpp"
#endif
class Flags;
class X11Canvas : public Canvas
{
public:
explicit X11Canvas();
~X11Canvas() override;
void add_image(const std::string& identifier, std::unique_ptr<Image> new_image) override;
void remove_image(const std::string& identifier) override;
void hide() override;
void show() override;
private:
xcb_connection_t *connection;
xcb_screen_t *screen;
#ifdef ENABLE_XCB_ERRORS
xcb_errors_context_t *err_ctx;
#endif
std::unique_ptr<X11Util> xutil;
// map for event handler
std::unordered_map<xcb_window_t, std::shared_ptr<Window>> windows;
// windows per image
std::unordered_map<std::string,
std::unordered_map<xcb_window_t, std::shared_ptr<Window>>> image_windows;
std::unordered_map<std::string, std::shared_ptr<Image>> images;
std::unordered_map<std::string, std::jthread> draw_threads;
std::thread event_handler;
std::mutex windows_mutex;
std::shared_ptr<spdlog::logger> logger;
std::shared_ptr<Flags> flags;
#ifdef ENABLE_OPENGL
std::unique_ptr<EGLUtil<xcb_connection_t, xcb_window_t>> egl;
bool egl_available = true;
#endif
void draw(const std::string& identifier);
void handle_events();
void get_tmux_window_ids(std::unordered_set<xcb_window_t>& windows);
void print_xcb_error(const xcb_generic_error_t* err);
};
#endif

62
src/dimensions.cpp Normal file
View File

@ -0,0 +1,62 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "dimensions.hpp"
#include "tmux.hpp"
#include "terminal.hpp"
#include <utility>
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;
}

52
src/flags.cpp Normal file
View File

@ -0,0 +1,52 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "flags.hpp"
#include "os.hpp"
#include "util.hpp"
#include <fmt/format.h>
#include <fstream>
#include <nlohmann/json.hpp>
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);
}

185
src/image.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#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 <opencv2/imgcodecs.hpp>
#endif
#include <spdlog/spdlog.h>
#include <vips/vips.h>
namespace fs = std::filesystem;
using njson = nlohmann::json;
auto Image::load(const njson &command, const Terminal *terminal) -> std::unique_ptr<Image>
{
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> 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<OpencvImage>(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<LibvipsImage>(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, int>
{
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<int>(img_width * new_scale);
new_height = static_cast<int>(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<int>(new_width * new_scale);
new_height = static_cast<int>(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<Dimensions>
{
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<Dimensions>(terminal, xcoord, ycoord, max_width, max_height, scaler);
}

216
src/image/libvips.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "libvips.hpp"
#include "dimensions.hpp"
#include "flags.hpp"
#include "terminal.hpp"
#include "util.hpp"
#include <algorithm>
#include <unordered_set>
#ifdef ENABLE_OPENCV
# include <opencv2/videoio.hpp>
#endif
using vips::VError;
using vips::VImage;
LibvipsImage::LibvipsImage(std::shared_ptr<Dimensions> 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<int>((1.0 / video.get(cv::CAP_PROP_FPS)) * ms_per_sec);
}
#endif
return static_cast<int>((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<double>(width()) / dims->terminal->font_width;
const double img_height = static_cast<double>(height()) / dims->terminal->font_height;
dims->x -= std::floor(img_width / 2);
dims->y -= std::floor(img_height / 2);
}
const std::unordered_set<std::string_view> 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<unsigned char *>(image.write_to_memory(&_size)));
}

71
src/image/libvips.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#ifndef LIBVIPS_IMAGE_H
#define LIBVIPS_IMAGE_H
#include "image.hpp"
#include "util/ptr.hpp"
#include <filesystem>
#include <spdlog/spdlog.h>
#include <string>
#include <vips/vips8>
class LibvipsImage : public Image
{
public:
LibvipsImage(std::shared_ptr<Dimensions> 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<unsigned char, g_free> _data;
std::filesystem::path path;
std::shared_ptr<Dimensions> dims;
std::shared_ptr<Flags> flags;
std::shared_ptr<spdlog::logger> 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

249
src/image/opencv.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "opencv.hpp"
#include "dimensions.hpp"
#include "flags.hpp"
#include "terminal.hpp"
#include "util.hpp"
#include <string_view>
#include <unordered_set>
#include <opencv2/core/ocl.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
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<Dimensions> 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<double>(width()) / dims->terminal->font_width;
const double img_height = static_cast<double>(height()) / dims->terminal->font_height;
dims->x -= std::floor(img_width / 2);
dims->y -= std::floor(img_height / 2);
}
const std::unordered_set<std::string_view> 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>([](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();
}

68
src/image/opencv.hpp Normal file
View File

@ -0,0 +1,68 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef OPENCV_IMAGE_H
#define OPENCV_IMAGE_H
#include "image.hpp"
#include <filesystem>
#include <opencv2/core.hpp>
#include <spdlog/spdlog.h>
#include <string>
namespace fs = std::filesystem;
class OpencvImage : public Image
{
public:
OpencvImage(std::shared_ptr<Dimensions> 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<Dimensions> dims;
uint64_t _size = 0;
uint32_t max_width;
uint32_t max_height;
bool in_cache;
bool opencl_available = false;
std::shared_ptr<spdlog::logger> logger;
std::shared_ptr<Flags> 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

151
src/main.cpp Normal file
View File

@ -0,0 +1,151 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <csignal>
#include <CLI/App.hpp>
#include <CLI/Config.hpp>
#include <CLI/Formatter.hpp>
#include <spdlog/cfg/env.h>
#include <spdlog/spdlog.h>
#include <CLI/Version.hpp>
#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 <CLI/ExtraValidators.hpp>
#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> 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;
}

126
src/os.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "os.hpp"
#include "util/ptr.hpp"
#include <array>
#include <cstdlib>
#include <system_error>
#include <cerrno>
#include <cstdio>
#include <poll.h>
#include <unistd.h>
auto os::exec(const std::string &cmd) -> std::string
{
const int bufsize = 128;
std::array<char, bufsize> buffer{};
std::string result;
const c_unique_ptr<FILE, &pclose> 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<std::string>
{
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
}
}

43
src/process/apple.cpp Normal file
View File

@ -0,0 +1,43 @@
// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "os.hpp"
#include "process.hpp"
#include "tmux.hpp"
#include "util.hpp"
#include <fmt/format.h>
#include <libproc.h>
#include <sys/types.h>
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<int>(sproc.pbsi_ppid);
}
status = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &proc, PROC_PIDTBSDINFO_SIZE);
if (status == PROC_PIDTBSDINFO_SIZE) {
tty_nr = static_cast<int>(proc.e_tdev);
minor_dev = minor(tty_nr);
pty_path = fmt::format("/dev/ttys{:0>3}", minor_dev);
}
}

39
src/process/linux.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "process.hpp"
#include <fmt/format.h>
#include <fstream>
#include <limits>
#if defined(__FreeBSD__) || defined(__NetBSD__)
# include <sys/types.h>
#else
# include <sys/sysmacros.h>
#endif
constexpr auto max_size = std::numeric_limits<std::streamsize>::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);
}

335
src/terminal.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#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 <cmath>
#include <iostream>
#include <unordered_set>
#include <fcntl.h>
#include <range/v3/all.hpp>
#include <spdlog/spdlog.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <system_error>
#include <unistd.h>
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<uint16_t>(std::max(padding_horiz, padding_vert));
padding_vertical = padding_horizontal;
font_width = static_cast<uint16_t>(
std::floor(guess_font_size(cols, static_cast<float>(xpixel), static_cast<float>(padding_horizontal))));
font_height = static_cast<uint16_t>(
std::floor(guess_font_size(rows, static_cast<float>(ypixel), static_cast<float>(padding_vertical))));
if (xpixel < fallback_xpixel && ypixel < fallback_ypixel) {
padding_horizontal = static_cast<uint16_t>((fallback_xpixel - xpixel) / 2);
padding_vertical = static_cast<uint16_t>((fallback_ypixel - ypixel) / 2);
font_width = static_cast<uint16_t>(xpixel / cols);
font_height = static_cast<uint16_t>(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<float>(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<std::string_view>{"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<std::string_view>{"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<uint16_t>(window.width);
fallback_ypixel = static_cast<uint16_t>(window.height);
#endif
}
void Terminal::open_first_pty()
{
const auto pids = tmux::get_client_pids().value_or(std::vector<int>{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<int>(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;
}

142
src/tmux.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "tmux.hpp"
#include "os.hpp"
#include "util.hpp"
#include <array>
#include <fmt/format.h>
#include <string>
#include <utility>
constexpr auto session_hooks = std::to_array<std::string_view>(
{"session-window-changed", "client-detached", "window-layout-changed"});
constexpr auto global_hooks = std::to_array<std::string_view>(
{"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<std::vector<int>>
{
if (!tmux::is_used()) {
return {};
}
if (!tmux::is_window_focused()) {
return {};
}
std::vector<int> 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<int, int>
{
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<int, int>
{
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);
}
}

15
src/util/dbus.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "util/dbus.hpp"
#include <stdexcept>
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);
}

228
src/util/egl.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "util/egl.hpp"
#include "image.hpp"
#include <range/v3/all.hpp>
#include <spdlog/spdlog.h>
#include <string_view>
#include <unordered_map>
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<EGLint>({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<EGLint>(
{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 <class T, class V>
EGLUtil<T, V>::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 <class T, class V>
EGLUtil<T, V>::~EGLUtil()
{
eglTerminate(display);
}
template <class T, class V>
auto EGLUtil<T, V>::error_to_string() const -> std::string
{
using pair_type = std::pair<EGLint, std::string_view>;
constexpr auto error_codes = std::to_array<pair_type>({
{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 <class T, class V>
auto EGLUtil<T, V>::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 <class T, class V>
auto EGLUtil<T, V>::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 <class T, class V>
void EGLUtil<T, V>::make_current(EGLSurface surface, EGLContext context) const
{
eglMakeCurrent(display, surface, surface, context);
}
template <class T, class V>
void EGLUtil<T, V>::restore() const
{
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
template <class T, class V>
void EGLUtil<T, V>::run_contained(EGLSurface surface, EGLContext context, const std::function<void()> &func) const
{
make_current(surface, context);
func();
restore();
}
template <class T, class V>
void EGLUtil<T, V>::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 <xcb/xcb.h>
template class EGLUtil<xcb_connection_t, xcb_window_t>;
#endif
#ifdef ENABLE_WAYLAND
# include <wayland-client.h>
# include <wayland-egl.h>
template class EGLUtil<struct wl_display, struct wl_egl_window>;
#endif

169
src/util/socket.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "util/socket.hpp"
#include <array>
#include <cerrno>
#include <cstring>
#include <filesystem>
#include <system_error>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#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<const struct sockaddr *>(&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<const struct sockaddr *>(&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<std::string>
{
// 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<char, read_buffer_size> 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<const uint8_t *>(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<uint8_t *>(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<char, read_buffer_size> 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);
}

262
src/util/util.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "util.hpp"
#include "flags.hpp"
#include "os.hpp"
#include "process.hpp"
#include "util/ptr.hpp"
#include "util/socket.hpp"
#include <array>
#include <iomanip>
#include <iostream>
#include <memory>
#include <random>
#include <sstream>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <openssl/evp.h>
#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 <turbobase64/turbob64.h>
# else
# include "turbob64.h"
# endif
#endif
#include <range/v3/all.hpp>
#include <vips/vips8>
namespace fs = std::filesystem;
using njson = nlohmann::json;
auto util::str_split(std::string_view str, std::string_view delim) -> std::vector<std::string>
{
std::vector<std::string> result;
for (const auto word : ranges::views::split(str, delim)) {
result.emplace_back(ranges::to<std::string>(word));
}
return result;
}
auto util::get_process_tree(int pid) -> std::vector<int>
{
std::vector<int> 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<Process>
{
std::vector<Process> 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<char> res(bufsize, 0);
base64_encode_v2(input, length, reinterpret_cast<unsigned char *>(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<int>(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, EVP_MD_CTX_free>{EVP_MD_CTX_new()};
#ifdef LIBRESSL_VERSION_NUMBER
const auto *evp = EVP_sha256();
#else
const auto *evp = EVP_blake2b512();
#endif
auto digest = std::array<unsigned char, EVP_MAX_MD_SIZE>();
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<int>(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<void(void)> &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<double, std::milli> 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<std::uint16_t>
{
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());
}

171
src/util/x11.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#include "util/x11.hpp"
#include "flags.hpp"
#include "os.hpp"
#include "util.hpp"
#include "util/ptr.hpp"
#include <range/v3/all.hpp>
#include <xcb/res.h>
#include <xcb/xcb.h>
#include <algorithm>
#include <stack>
#include <string>
#include <utility>
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<xcb_window_t>
{
const int num_clients = 256;
std::vector<xcb_window_t> windows;
std::stack<xcb_query_tree_cookie_t> 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_t>{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<uint32_t, xcb_window_t>
{
const auto windows = get_server_window_ids();
std::unordered_map<uint32_t, xcb_window_t> res;
std::vector<xcb_res_query_client_ids_cookie_t> 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_t>{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<xcb_atom_t> properties) const -> bool
{
std::vector<xcb_get_property_cookie_t> 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_t>{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<uint16_t, uint16_t>
{
const auto cookie = xcb_get_geometry_unchecked(connection, window);
const auto reply = unique_C_ptr<xcb_get_geometry_reply_t>{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;
}