/** * sixdraw.c - draw lines on your terminal * * Requires sixel graphics and DEC locator support to run on your terminal. * These features are not widely supported. To save yourself from trouble, * use a latest version of XTerm or mlterm. * * Copyright (C) 2020 CismonX * * 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 . */ #ifdef HAVE_CONFIG_H # include "config.h" #endif // HAVE_CONFIG_H #include #include #include #include #include #include #include #include #include #include #ifndef DEFAULT_TIMEOUT_MILLIS # define DEFAULT_TIMEOUT_MILLIS 500 #endif // !DEFAULT_TIMEOUT_MILLIS #define DECRQM_SET 1 #define DECRQM_RST 2 #define DECTCEM 25u #define ALT_SCRBUF 1049u struct sixdraw_ctx { struct termios termios; union ctlseqs_value result[64]; char const *prog_name; struct ctlseqs_matcher *matcher; struct ctlseqs_reader *reader; bool has_termios; bool show_cursor; bool normal_scrbuf; int in_fd; int out_fd; int timeout; int rows; int cols; int ch_width; int ch_height; }; static inline void print_error(struct sixdraw_ctx const *ctx, char const *format, ...) { char msg[1024]; va_list args; va_start(args, format); vsnprintf(msg, 1024, format, args); va_end(args); dprintf(ctx->out_fd, "%s: [error] %s.\n", ctx->prog_name, msg); } static inline void clear_signal(int signum) { sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, signum); sigwait(&sigset, &signum); } static bool handle_signals(struct sixdraw_ctx *ctx) { sigset_t sigset; sigpending(&sigset); if (!sigismember(&sigset, SIGWINCH)) { return false; } clear_signal(SIGWINCH); return true; } static void terminate(struct sixdraw_ctx *ctx) { ctlseqs_matcher_free(ctx->matcher); ctlseqs_reader_free(ctx->reader); // Restore cursor status. if (ctx->show_cursor) { dprintf(ctx->out_fd, CTLSEQS_DECSET("%u"), DECTCEM); } // Restore normal screen buffer. if (ctx->normal_scrbuf) { dprintf(ctx->out_fd, CTLSEQS_DECRST("%u"), ALT_SCRBUF); } // Restore original terminal modes. if (ctx->has_termios) { tcsetattr(ctx->in_fd, TCSANOW, &ctx->termios); } } static bool get_winsize(struct sixdraw_ctx *ctx) { struct winsize ws = { 0 }; if (ioctl(ctx->in_fd, TIOCGWINSZ, &ws) != 0) { print_error(ctx, "failed to get terminal window size"); return false; } if (ws.ws_xpixel == 0 || ws.ws_ypixel == 0) { print_error(ctx, "failed to get terminal window size (in pixels)"); return false; } ctx->rows = ws.ws_row; ctx->cols = ws.ws_col; ctx->ch_width = ws.ws_xpixel / ws.ws_col; ctx->ch_height = ws.ws_ypixel / ws.ws_row; return true; } static bool decrqm(struct sixdraw_ctx *ctx, unsigned mode_id, char const *name) { ssize_t retval; union ctlseqs_value *result = ctx->result; dprintf(ctx->out_fd, CTLSEQS_DECRQM("%u"), mode_id); do { retval = ctlseqs_read(ctx->reader, ctx->matcher, ctx->timeout); } while (retval == CTLSEQS_PARTIAL); if (retval != 1) { print_error(ctx, "failed to get %s status", name); return false; } if (result[0].num != mode_id || (result[1].num != DECRQM_SET && result[1].num != DECRQM_RST)) { print_error(ctx, "%s status not recognizable", name); return false; } return true; } static bool init(struct sixdraw_ctx *ctx, int argc, char **argv) { *ctx = (struct sixdraw_ctx) { .prog_name = argc > 0 ? argv[0] : "sixdraw", .in_fd = STDIN_FILENO, .out_fd = STDOUT_FILENO, .timeout = DEFAULT_TIMEOUT_MILLIS, }; // Process command line arguments. int opt; while (-1 != (opt = getopt(argc, argv, "t:"))) { switch (opt) { case 't': ctx->timeout = atoi(optarg); break; case '?': default: return false; } } // Initialize control sequence matcher. ctx->matcher = ctlseqs_matcher_init(); if (ctx->matcher == NULL) { print_error(ctx, "failed to initialize matcher"); return false; } char const *patterns[] = { CTLSEQS_RESP_PRIMARY_DA(CTLSEQS_PH_NUMS), CTLSEQS_RESP_DECRQM(CTLSEQS_PH_NUM, CTLSEQS_PH_NUM) }; struct ctlseqs_matcher_options matcher_options = { .patterns = patterns, .npatterns = sizeof(patterns) / sizeof(char const *), }; if (ctlseqs_matcher_config(ctx->matcher, &matcher_options) != 0) { print_error(ctx, "failed to set matcher options"); return false; } // Initialize control sequence reader. ctx->reader = ctlseqs_reader_init(); if (ctx->reader == NULL) { print_error(ctx, "failed to initialize reader"); return false; } struct ctlseqs_reader_options reader_options = { .result = ctx->result, .fd = ctx->in_fd, .maxlen = 4096, }; if (ctlseqs_reader_config(ctx->reader, &reader_options) != CTLSEQS_OK) { print_error(ctx, "failed to set reader options"); return false; } // Block SIGWINCH. sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGWINCH); sigprocmask(SIG_BLOCK, &sigset, NULL); signal(SIGWINCH, SIG_DFL); return true; } static bool prepare(struct sixdraw_ctx *ctx) { // Check whether we're running on a terminal. if (!isatty(ctx->in_fd) || !isatty(ctx->out_fd)) { print_error(ctx, "this program can only run in a terminal"); return false; } // Set terminal to noncanonical mode. if (tcgetattr(ctx->in_fd, &ctx->termios) != 0) { print_error(ctx, "failed to get terminal attributes"); return false; } struct termios termios = ctx->termios; termios.c_cc[VMIN] = 0; termios.c_cc[VTIME] = 0; termios.c_lflag &= ~(ICANON | ISIG | ECHO); if (tcsetattr(ctx->in_fd, TCSANOW, &termios) != 0) { print_error(ctx, "failed to set terminal attributes"); return false; } ctx->has_termios = true; // Set STDIN flags to nonblocking. int flags = fcntl(ctx->in_fd, F_GETFL); if (flags == -1) { print_error(ctx, "failed to get file status flags"); return false; } if (fcntl(ctx->in_fd, F_SETFL, flags | O_NONBLOCK) == -1) { print_error(ctx, "failed to set file status flags"); return false; } // Get initial terminal window size. if (!get_winsize(ctx)) { return false; } // Check terminal support for sixel graphics and DEC locator. dprintf(ctx->out_fd, CTLSEQS_PRIMARY_DA()); ssize_t retval; do { retval = ctlseqs_read(ctx->reader, ctx->matcher, ctx->timeout); } while (retval == CTLSEQS_PARTIAL); if (retval != 0) { print_error(ctx, "failed to get terminal features"); return false; } bool has_sixel = false; bool has_dec_locator = false; union ctlseqs_value *result = ctx->result; for (size_t i = 1; i <= result[0].len; ++i) { if (result[i].num == 4) { has_sixel = true; } else if (result[i].num == 29) { has_dec_locator = true; } } if (!has_sixel) { print_error(ctx, "terminal does not support sixel graphics"); return false; } if (!has_dec_locator) { print_error(ctx, "terminal does not support DEC locator mode"); return false; } // Get current cursor status. if (!decrqm(ctx, DECTCEM, "cursor")) { return false; } ctx->show_cursor = result[1].num == DECRQM_SET; // Hide cursor. if (ctx->show_cursor) { dprintf(ctx->out_fd, CTLSEQS_DECRST("%u"), DECTCEM); } // Get current screen buffer status. if (!decrqm(ctx, ALT_SCRBUF, "screen buffer")) { return false; } ctx->normal_scrbuf = result[1].num == DECRQM_RST; // Switch to alternate screen buffer. if (ctx->normal_scrbuf) { dprintf(ctx->out_fd, CTLSEQS_DECSET("%u"), ALT_SCRBUF); } return true; } int main(int argc, char **argv) { int status = 0; struct sixdraw_ctx ctx; if (!init(&ctx, argc, argv) || !prepare(&ctx)) { status = -1; goto terminate; } terminate: terminate(&ctx); return status; }