// Display images inside a terminal
// Copyright (C) 2023 JustKidding
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
#include "application.hpp"
#include "image.hpp"
#include "tmux.hpp"
#include "util.hpp"
#include "util/socket.hpp"
#include "version.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
using njson = nlohmann::json;
namespace fs = std::filesystem;
Application::Application(const char *executable)
{
flags = Flags::instance();
print_header();
setup_logger();
set_silent();
terminal = std::make_unique();
if (flags->no_stdin) {
daemonize();
}
canvas = Canvas::create();
const auto cache_path = util::get_cache_path();
if (!fs::exists(cache_path) && !flags->no_cache) {
fs::create_directories(cache_path);
}
tmux::register_hooks();
socket_thread = std::thread([this] {
const auto sock_path = util::get_socket_path();
logger->info("Listening for commands on socket {}", sock_path);
socket_loop();
});
if (flags->no_cache) {
logger->info("Image caching is disabled");
}
if (VIPS_INIT(executable)) {
vips_error_exit(nullptr);
}
vips_cache_set_max(1);
}
Application::~Application()
{
if (socket_thread.joinable()) {
socket_thread.join();
}
logger->info("Exiting ueberzugpp");
canvas.reset();
vips_shutdown();
tmux::unregister_hooks();
fs::remove(util::get_socket_path());
}
void Application::execute(const std::string_view cmd)
{
if (!canvas) {
return;
}
njson json;
try {
json = njson::parse(cmd);
} catch (const njson::parse_error &e) {
logger->error("Command received is not valid json");
return;
}
const auto json_str = json.dump();
logger->info("Command received: {}", json_str);
const std::string &action = json.at("action");
if (action == "tmux") {
const std::string &hook = json.at("hook");
handle_tmux_hook(hook);
return;
}
const std::string &identifier = json.at("identifier");
if (action == "add") {
if (!json.at("path").is_string()) {
logger->error("Path received is not valid");
return;
}
auto image = Image::load(json, terminal.get());
if (!image) {
logger->error("Unable to load image file");
return;
}
canvas->add_image(identifier, std::move(image));
} else if (action == "remove") {
canvas->remove_image(identifier);
} else {
logger->warn("Command not supported");
}
}
void Application::handle_tmux_hook(const std::string_view hook)
{
const std::unordered_map> hook_fns{
{"client-session-changed",
[this] {
if (tmux::is_window_focused()) {
canvas->show();
} else {
canvas->hide();
}
}},
{"session-window-changed",
[this] {
if (tmux::is_window_focused()) {
canvas->show();
} else {
canvas->hide();
}
}},
{"client-detached", [this] { canvas->hide(); }},
{"window-layout-changed",
[this] {
if (tmux::is_window_focused()) {
canvas->hide();
}
}},
};
try {
hook_fns.at(hook)();
} catch (const std::out_of_range &oor) {
logger->warn("TMUX hook not recognized");
}
}
void Application::setup_logger()
{
const auto log_tmp = util::get_log_filename();
const auto log_path = util::temp_directory_path() / log_tmp;
try {
spdlog::flush_on(spdlog::level::debug);
const auto sink = std::make_shared(log_path);
const auto main_logger = std::make_shared("main", sink);
const auto terminal_logger = std::make_shared("terminal", sink);
const auto cv_logger = std::make_shared("opencv", sink);
const auto vips_logger = std::make_shared("vips", sink);
const auto x11_logger = std::make_shared("X11", sink);
const auto sixel_logger = std::make_shared("sixel", sink);
const auto kitty_logger = std::make_shared("kitty", sink);
const auto iterm2_logger = std::make_shared("iterm2", sink);
const auto chafa_logger = std::make_shared("chafa", sink);
const auto wayland_logger = std::make_shared("wayland", sink);
const auto opengl_logger = std::make_shared("opengl", sink);
spdlog::initialize_logger(main_logger);
spdlog::initialize_logger(terminal_logger);
spdlog::initialize_logger(cv_logger);
spdlog::initialize_logger(vips_logger);
spdlog::initialize_logger(x11_logger);
spdlog::initialize_logger(sixel_logger);
spdlog::initialize_logger(kitty_logger);
spdlog::initialize_logger(iterm2_logger);
spdlog::initialize_logger(chafa_logger);
spdlog::initialize_logger(wayland_logger);
spdlog::initialize_logger(opengl_logger);
logger = spdlog::get("main");
} catch (const spdlog::spdlog_ex &ex) {
std::cout << "Log init failed: " << ex.what() << '\n';
}
}
void Application::command_loop()
{
if (flags->no_stdin) {
return;
}
while (!stop_flag) {
try {
const auto in_event = os::wait_for_data_on_stdin(100);
if (!in_event) {
continue;
}
const auto cmd = os::read_data_from_stdin();
execute(cmd);
} catch (const std::system_error &err) {
stop_flag = true;
break;
}
}
}
void Application::socket_loop()
{
UnixSocket socket;
socket.bind_to_endpoint(util::get_socket_path());
const int waitms = 100;
int conn = -1;
while (!stop_flag) {
try {
conn = socket.wait_for_connections(waitms);
} catch (const std::system_error &err) {
stop_flag = true;
break;
}
if (conn == -1) {
continue;
}
const auto data = socket.read_data_from_connection(conn);
for (const auto &cmd : data) {
if (cmd == "EXIT") {
stop_flag = true;
return;
}
execute(cmd);
}
}
}
void Application::print_header()
{
const auto log_tmp = util::get_log_filename();
const auto log_path = util::temp_directory_path() / log_tmp;
const auto art = fmt::format(R"(
_ _ _
| | | | | | _ _
| | | | ___| |__ ___ _ __ _____ _ __ _ _| |_ _| |_
| | | |/ _ \ '_ \ / _ \ '__|_ / | | |/ _` |_ _|_ _|
| |_| | __/ |_) | __/ | / /| |_| | (_| | |_| |_|
\___/ \___|_.__/ \___|_| /___|\__,_|\__, |
__/ |
|___/ v{}.{}.{})",
ueberzugpp_VERSION_MAJOR, ueberzugpp_VERSION_MINOR, ueberzugpp_VERSION_PATCH);
std::ofstream ofs(log_path, std::ios::out | std::ios::app);
ofs << art << '\n' << std::flush;
}
void Application::set_silent()
{
if (!flags->silent) {
return;
}
f_stderr.reset(std::freopen("/dev/null", "w", stderr));
}
void Application::print_version()
{
const auto ver_str = fmt::format("ueberzugpp {}.{}.{}", ueberzugpp_VERSION_MAJOR, ueberzugpp_VERSION_MINOR,
ueberzugpp_VERSION_PATCH);
std::cout << ver_str << '\n';
}
void Application::daemonize()
{
os::daemonize();
std::ofstream ofs(flags->pid_file);
ofs << os::get_pid() << std::flush;
}