summaryrefslogtreecommitdiff
path: root/src/terminal.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/terminal.cpp')
-rw-r--r--src/terminal.cpp335
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;
+}