/** * 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 #ifndef SIXDRAW_DEFAULT_TIMEOUT_MILLIS # define SIXDRAW_DEFAULT_TIMEOUT_MILLIS 500 #endif // !SIXDRAW_DEFAULT_TIMEOUT_MILLIS struct sixdraw_ctx { struct termios termios; union ctlseqs_value buffer[10]; char *prog_name; struct ctlseqs_matcher *matcher; struct ctlseqs_reader *reader; bool has_termios; bool show_cursor; int in_fd; int out_fd; int timeout; int rows; int cols; int ch_width; int ch_height; }; static inline void sixdraw_print_error(struct sixdraw_ctx *ctx, char const *msg) { dprintf(ctx->out_fd, "%s: [error] %s.\n", ctx->prog_name, msg); } static inline void sixdraw_clear_signal(int signum) { sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, signum); sigwait(&sigset, &signum); } static bool sixdraw_handle_signals(struct sixdraw_ctx *ctx) { sigset_t sigset; sigpending(&sigset); if (!sigismember(&sigset, SIGWINCH)) { return false; } sixdraw_clear_signal(SIGWINCH); return true; } static void sixdraw_terminate(struct sixdraw_ctx *ctx) { ctlseqs_matcher_free(ctx->matcher); ctlseqs_reader_free(ctx->reader); // Restore original terminal modes if (ctx->has_termios) { tcsetattr(ctx->in_fd, TCSANOW, &ctx->termios); } } static bool sixdraw_get_winsize(struct sixdraw_ctx *ctx) { struct winsize ws = { 0 }; if (ioctl(ctx->in_fd, TIOCGWINSZ, &ws) != 0) { return false; } if (ws.ws_xpixel == 0 || ws.ws_ypixel == 0) { 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; dprintf(ctx->out_fd, "%d %d \n%d %d %d %d\n", ws.ws_xpixel, ws.ws_ypixel, ctx->rows, ctx->cols, ctx->ch_width, ctx->ch_height); return true; } static bool sixdraw_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 = SIXDRAW_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) { sixdraw_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_opts options = { .patterns = patterns, .npatterns = sizeof(patterns) / sizeof(char const *), }; if (ctlseqs_matcher_setopt(ctx->matcher, &options) != 0) { sixdraw_print_error(ctx, "failed to set matcher options"); return false; } // Initialize control sequence reader ctx->reader = ctlseqs_reader_init(); if (ctx->reader == NULL) { sixdraw_print_error(ctx, "failed to initialize reader"); return false; } struct ctlseqs_reader_opts reader_opts = { .buffer = ctx->buffer, .fd = ctx->in_fd, .maxlen = 32, }; if (ctlseqs_reader_setopt(ctx->reader, &reader_opts) != CTLSEQS_OK) { sixdraw_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 sixdraw_prepare(struct sixdraw_ctx *ctx) { // Check whether we're running on a terminal if (!isatty(ctx->in_fd) || !isatty(ctx->out_fd)) { sixdraw_print_error(ctx, "this program can only run in a terminal"); return false; } // Set terminal to noncanonical mode struct termios termios; if (tcgetattr(ctx->in_fd, &ctx->termios) != 0) { sixdraw_print_error(ctx, "failed to get terminal attributes"); return false; } termios = ctx->termios; termios.c_lflag &= ~(ICANON | ISIG | ECHO); if (tcsetattr(ctx->in_fd, TCSANOW, &termios) != 0) { sixdraw_print_error(ctx, "failed to get terminal attributes"); return false; } ctx->has_termios = true; // Set STDIN flags to non-blocking int flags = fcntl(ctx->in_fd, F_GETFL); if (flags == -1) { sixdraw_print_error(ctx, "failed to get file status flags"); return false; } if (fcntl(ctx->in_fd, F_SETFL, flags | O_NONBLOCK) == -1) { sixdraw_print_error(ctx, "failed to set file status flags"); return false; } // Get initial terminal window size if (!sixdraw_get_winsize(ctx)) { sixdraw_print_error(ctx, "failed to get terminal window size"); return false; } return true; // Check terminal support for sixel graphics and DEC locator dprintf(ctx->out_fd, CTLSEQS_PRIMARY_DA()); ssize_t result = ctlseqs_read(ctx->reader, ctx->matcher, 500); if (result != 0) { sixdraw_print_error(ctx, "failed to get terminal attributes"); return false; } bool has_sixel = false; bool has_dec_locator = false; union ctlseqs_value *buffer = ctx->buffer; for (size_t i = 1; i <= buffer[0].num; ++i) { if (buffer[i].num == 4) { has_sixel = true; } else if (buffer[i].num == 29) { has_dec_locator = true; } } if (!has_sixel) { sixdraw_print_error(ctx, "terminal does not support sixel graphics"); return false; } if (!has_dec_locator) { sixdraw_print_error(ctx, "terminal does not support DEC locator mode"); return false; } // Get current cursor status dprintf(ctx->out_fd, CTLSEQS_DECRQM("25")); result = ctlseqs_read(ctx->reader, ctx->matcher, ctx->timeout); if (result != 1) { sixdraw_print_error(ctx, "failed to get cursor status"); return false; } if (buffer[0].num != 25 || (buffer[1].num != 1 && buffer[1].num != 2)) { sixdraw_print_error(ctx, "cursor status not recognizable"); return false; } ctx->show_cursor = buffer[1].num == 1; return true; } int main(int argc, char **argv) { int status = 0; struct sixdraw_ctx ctx; if (!sixdraw_init(&ctx, argc, argv) || !sixdraw_prepare(&ctx)) { status = -1; goto terminate; } terminate: sixdraw_terminate(&ctx); return status; }