// 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;
}