// Display images inside a terminal // Copyright (C) 2023 JustKidding // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #include "terminal.hpp" #include "flags.hpp" #include "os.hpp" #include "process.hpp" #include "tmux.hpp" #include "util.hpp" #ifdef ENABLE_X11 # include "util/x11.hpp" #endif #ifdef ENABLE_WAYLAND # include "canvas/wayland/config.hpp" #endif #include #include #include #include #include #include #include #include #include #include Terminal::Terminal() : terminal_pid(pid) { flags = Flags::instance(); logger = spdlog::get("terminal"); term = os::getenv("TERM").value_or("xterm-256color"); term_program = os::getenv("TERM_PROGRAM").value_or(""); logger->info("TERM = {}", term); if (!term_program.empty()) { logger->info("TERM_PROGRAM = {}", term_program); } open_first_pty(); get_terminal_size(); set_detected_output(); } Terminal::~Terminal() { if (pty_fd > 0) { close(pty_fd); } } void Terminal::get_terminal_size() { struct winsize size; ioctl(pty_fd, TIOCGWINSZ, &size); cols = size.ws_col; rows = size.ws_row; xpixel = size.ws_xpixel; ypixel = size.ws_ypixel; logger->debug("ioctl sizes: COLS={} ROWS={} XPIXEL={} YPIXEL={}", cols, rows, xpixel, ypixel); check_iterm2_support(); if (flags->use_escape_codes) { init_termios(); if (xpixel == 0 || ypixel == 0) { get_terminal_size_escape_code(); } if (xpixel == 0 || ypixel == 0) { get_terminal_size_xtsm(); } check_sixel_support(); check_kitty_support(); reset_termios(); } get_fallback_x11_terminal_sizes(); get_fallback_wayland_terminal_sizes(); if (xpixel == 0 || ypixel == 0) { xpixel = fallback_xpixel; ypixel = fallback_ypixel; } if (xpixel == 0 || ypixel == 0) { throw std::runtime_error("Unable to calculate terminal sizes"); } const double padding_horiz = guess_padding(cols, xpixel); const double padding_vert = guess_padding(rows, ypixel); padding_horizontal = static_cast(std::max(padding_horiz, padding_vert)); padding_vertical = padding_horizontal; font_width = static_cast( std::floor(guess_font_size(cols, static_cast(xpixel), static_cast(padding_horizontal)))); font_height = static_cast( std::floor(guess_font_size(rows, static_cast(ypixel), static_cast(padding_vertical)))); if (xpixel < fallback_xpixel && ypixel < fallback_ypixel) { padding_horizontal = static_cast((fallback_xpixel - xpixel) / 2); padding_vertical = static_cast((fallback_ypixel - ypixel) / 2); font_width = static_cast(xpixel / cols); font_height = static_cast(ypixel / rows); } logger->debug("padding_horiz={} padding_vert={}", padding_horizontal, padding_vertical); logger->debug("font_width={} font_height={}", font_width, font_height); } void Terminal::set_detected_output() { if (supports_sixel) { detected_output = "sixel"; } if (supports_kitty) { detected_output = "kitty"; } if (supports_iterm2) { detected_output = "iterm2"; } if (supports_x11) { detected_output = "x11"; } if (supports_wayland) { detected_output = "wayland"; } if (flags->output.empty()) { if (detected_output.empty()) { flags->output = "chafa"; } else { flags->output = detected_output; } } } auto Terminal::guess_padding(uint16_t chars, double pixels) -> double { const double font_size = std::floor(pixels / chars); return (pixels - font_size * chars) / 2; } auto Terminal::guess_font_size(uint16_t chars, float pixels, float padding) -> float { return (pixels - 2 * padding) / static_cast(chars); } void Terminal::get_terminal_size_escape_code() { const auto resp = read_raw_str("\033[14t").erase(0, 4); if (resp.empty()) { return; } const auto sizes = util::str_split(resp, ";"); try { ypixel = std::stoi(sizes[0]); xpixel = std::stoi(sizes[1]); } catch (const std::invalid_argument &) { logger->debug("Got unexpected values in get_terminal_size_escape_code"); } // some old vte terminals respond to these values in a different order // assume everything older than 7000 is broken const auto vte_ver_str = os::getenv("VTE_VERSION").value_or(""); if (!vte_ver_str.empty()) { const auto vte_ver = std::stoi(vte_ver_str); const auto working_ver = 7000; if (vte_ver <= working_ver) { std::swap(ypixel, xpixel); } } logger->debug("ESC sizes XPIXEL={} YPIXEL={}", xpixel, ypixel); } void Terminal::get_terminal_size_xtsm() { const auto resp = read_raw_str("\033[?2;1;0S").erase(0, 3); if (resp.empty()) { return; } const auto sizes = util::str_split(resp, ";"); if (sizes.size() != 4) { return; } try { ypixel = std::stoi(sizes[3]); xpixel = std::stoi(sizes[2]); } catch (const std::invalid_argument &) { logger->debug("Got unexpected values in get_terminal_size_xtsm"); } logger->debug("XTSM sizes XPIXEL={} YPIXEL={}", xpixel, ypixel); } void Terminal::check_sixel_support() { // some terminals support sixel but don't respond to escape sequences const auto supported_terms = std::unordered_set{"yaft-256color", "iTerm.app"}; const auto resp = read_raw_str("\033[?1;1;0S").erase(0, 3); const auto vals = util::str_split(resp, ";"); if (vals.size() > 2 || supported_terms.contains(term) || supported_terms.contains(term_program)) { supports_sixel = true; logger->debug("sixel is supported"); } else { logger->debug("sixel is not supported"); } } void Terminal::check_kitty_support() { const auto resp = read_raw_str("\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\\033[c"); if (resp.find("OK") != std::string::npos) { supports_kitty = true; logger->debug("kitty is supported"); } else { logger->debug("kitty is not supported"); } } void Terminal::check_iterm2_support() { const auto supported_terms = std::unordered_set{"WezTerm", "iTerm.app"}; if (supported_terms.contains(term_program)) { supports_iterm2 = true; logger->debug("iterm2 is supported"); } else { logger->debug("iterm2 is not supported"); } } auto Terminal::read_raw_str(const std::string_view esc) -> std::string { const auto waitms = 100; std::cout << esc << std::flush; try { const auto in_event = os::wait_for_data_on_stdin(waitms); if (!in_event) { return ""; } return os::read_data_from_stdin(esc.back()); } catch (const std::system_error &) { return ""; } } void Terminal::init_termios() { tcgetattr(0, &old_term); /* grab old terminal i/o settings */ new_term = old_term; /* make new settings same as old settings */ new_term.c_lflag &= ~ICANON; /* disable buffered i/o */ new_term.c_lflag &= ~ECHO; /* set echo mode */ tcsetattr(0, TCSANOW, &new_term); /* use these new terminal i/o settings now */ } void Terminal::reset_termios() const { tcsetattr(0, TCSANOW, &old_term); } void Terminal::get_fallback_x11_terminal_sizes() { #ifdef ENABLE_X11 const auto xutil = X11Util(); if (xutil.connected) { supports_x11 = true; logger->debug("X11 is supported"); } else { logger->debug("x11 is not supported"); return; } x11_wid = xutil.get_parent_window(terminal_pid); logger->debug("Using fallback X11 window id {}", x11_wid); const auto [xpix, ypix] = xutil.get_window_dimensions(x11_wid); fallback_xpixel = xpix; fallback_ypixel = ypix; logger->debug("X11 sizes: XPIXEL={} YPIXEL={}", fallback_xpixel, fallback_ypixel); #endif } void Terminal::get_fallback_wayland_terminal_sizes() { #ifdef ENABLE_WAYLAND const auto config = WaylandConfig::get(); if (!config->is_dummy()) { supports_wayland = true; logger->debug("Wayland is supported."); } else { logger->debug("Wayland is not supported"); return; } const auto window = config->get_window_info(); fallback_xpixel = static_cast(window.width); fallback_ypixel = static_cast(window.height); #endif } void Terminal::open_first_pty() { const auto pids = tmux::get_client_pids().value_or(std::vector{pid}); struct stat stat_info; for (const auto spid : pids) { auto tree = util::get_process_tree_v2(spid); ranges::reverse(tree); for (const auto &proc : tree) { const int stat_res = stat(proc.pty_path.c_str(), &stat_info); if (stat_res == -1) { const auto err = std::error_code(errno, std::generic_category()); logger->debug("stat failed ({}) for pty {}, pid {}, ignoring", err.message(), proc.pty_path, proc.pid); continue; } if (proc.tty_nr == static_cast(stat_info.st_rdev)) { pty_fd = open(proc.pty_path.c_str(), O_RDONLY | O_NOCTTY); if (pty_fd == -1) { const auto err = std::error_code(errno, std::generic_category()); logger->debug("could not open pty {}, {}, ignoring", proc.pty_path, err.message()); continue; } terminal_pid = proc.pid; logger->info("PTY = {}", proc.pty_path); return; } } } logger->warn("Could not open pty, using stdout as fallback"); pty_fd = STDOUT_FILENO; }