diff options
Diffstat (limited to 'src/terminal.cpp')
| -rw-r--r-- | src/terminal.cpp | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..e093238 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,335 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <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; +} |
