/** * sixdraw.c - draw on your terminal * * Requires sixel graphics and 1016 mouse mode to run on your terminal. * These features are not widely supported. * To save yourself from trouble, use a latest version of XTerm or mintty. * * Before 1016 mode was introduced in XTerm patch #359, it is also possible * to report mouse position in pixels using DEC locator (which is also rarely * implemented). However, it is not used in this example. * * Copyright (C) 2020,2021 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 #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 25 #define DECSDM 80 #define BTN_EVENT_TRACKING 1002 #define SGR_MOUSE_PIXELMODE 1016 #define ALT_SCRBUF 1049 #define SIXEL_SEQ_HEAD CTLSEQS_DCS "0;1q\"1;1;" static char sixel_seq[4096] = SIXEL_SEQ_HEAD; struct sixdraw_ctx { struct termios termios; union ctlseqs_value result[64]; char const *prog_name; FILE* out_file; FILE* err_file; struct ctlseqs_matcher *matcher; struct ctlseqs_reader *reader; bool has_termios; bool hide_cursor; bool alt_scrbuf; bool btnev_tracking; bool legacy_xterm; bool sgr_pixelmode; bool sixel_scroll; int in_fd; int out_fd; int timeout; unsigned rows; unsigned canvas_width; unsigned canvas_height; unsigned ch_width; unsigned ch_height; unsigned line_color; unsigned sixel_init_size; }; 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); fprintf(ctx->err_file, "%s: [error] %s.\n", ctx->prog_name, msg); } static void terminate( struct sixdraw_ctx *ctx ) { ctlseqs_matcher_free(ctx->matcher); ctlseqs_reader_free(ctx->reader); // Restore normal screen buffer. if (ctx->alt_scrbuf) { fprintf(ctx->out_file, CTLSEQS_DECRST("%d"), ALT_SCRBUF); } // Restore original mouse modes. if (ctx->btnev_tracking) { fprintf(ctx->out_file, CTLSEQS_DECRST("%d"), BTN_EVENT_TRACKING); } if (ctx->sgr_pixelmode) { fprintf(ctx->out_file, CTLSEQS_DECRST("%d"), SGR_MOUSE_PIXELMODE); } // Restore cursor status. if (ctx->hide_cursor) { fprintf(ctx->out_file, CTLSEQS_DECSET("%d"), DECTCEM); } // Restore original sixel mode. if (ctx->sixel_scroll) { fprintf( ctx->out_file, ctx->legacy_xterm ? CTLSEQS_DECRST("%d") : CTLSEQS_DECSET("%d"), DECSDM ); } // 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->ch_width = ws.ws_xpixel / ws.ws_col; ctx->ch_height = ws.ws_ypixel / ws.ws_row; ctx->canvas_width = ctx->ch_width * ws.ws_col; ctx->canvas_height = ctx->ch_height * ws.ws_row - ctx->ch_height * 2; return true; } static bool decrqm( struct sixdraw_ctx *ctx, unsigned mode, char const *name ) { ssize_t retval; union ctlseqs_value *result = ctx->result; fprintf(ctx->out_file, CTLSEQS_DECRQM("%d"), mode); 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; } int mode_value = result[1].num; if ( result[0].num != mode || (mode_value != DECRQM_SET && mode_value != DECRQM_RST) ) { print_error(ctx, "%s status (%d) not recognizable", name, mode_value); return false; } return true; } static long xtversion( struct sixdraw_ctx *ctx ) { ssize_t retval; union ctlseqs_value *result = ctx->result; fprintf(ctx->out_file, CTLSEQS_XTVERSION()); do { retval = ctlseqs_read(ctx->reader, ctx->matcher, ctx->timeout); } while (retval == CTLSEQS_PARTIAL); if (retval != 3) { // Terminal emulator does not recognize XTVERSION; return -1; } char const *xtversion = result[1].str; if (result[0].len < sizeof("XTerm(")) { return -2; } if (strncmp(xtversion, "XTerm(", sizeof("XTerm(") - 1) != 0) { // Terminal emulator is not XTerm. return -2; } errno = 0; long version_num = strtol(xtversion + (sizeof("XTerm(") - 1), NULL, 10); if (errno) { return -3; } return version_num; } static void print_sixel_dot( struct sixdraw_ctx *ctx, unsigned x, unsigned y ) { if (x >= ctx->canvas_width || y >= ctx->canvas_height) { return; } // Move cursor. unsigned row = y / ctx->ch_height + 1; unsigned col = x / ctx->ch_width + 1; fprintf(ctx->out_file, CTLSEQS_CUP("%d", "%d"), row, col); // Build sixel sequence. row = y % ctx->ch_height; col = x % ctx->ch_width; unsigned seq_size = ctx->sixel_init_size; seq_size += sprintf( sixel_seq + seq_size, "%.*s!%u?%c" CTLSEQS_ST, row / 6, "------------------------", col, (1 << row % 6) + 0x3F ); fwrite(sixel_seq, seq_size, 1, ctx->out_file); } static bool init( struct sixdraw_ctx *ctx, int argc, char *argv[] ) { *ctx = (struct sixdraw_ctx) { .prog_name = argc > 0 ? argv[0] : "sixdraw", .out_file = stdout, .err_file = stderr, .in_fd = STDIN_FILENO, .out_fd = STDOUT_FILENO, .timeout = DEFAULT_TIMEOUT_MILLIS, .line_color = 0x00FF00 }; // Process command line arguments. int opt; while (-1 != (opt = getopt(argc, argv, "t:c:"))) { switch (opt) { case 't': ctx->timeout = atoi(optarg); break; case 'c': ctx->line_color = strtoul(optarg, NULL, 16); break; case '?': default: fprintf(ctx->out_file, "%s\n", "Usage: sixdraw [-t timeout] [-c line-color]"); 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), CTLSEQS_RESP_SGR_MOUSE( CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, "M"), CTLSEQS_RESP_XTVERSION(CTLSEQS_PH_STR), }; 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_OK != ctlseqs_reader_config(ctx->reader, &reader_options)) { print_error(ctx, "failed to set reader options"); return false; } 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, and disable output buffering. 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; } if (setvbuf(stdout, NULL, _IONBF, 0) != 0) { print_error(ctx, "failed to disable output buffering"); return false; } // Get initial terminal window size. if (!get_winsize(ctx)) { return false; } // Check terminal support for sixel graphics. fprintf(ctx->out_file, 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; union ctlseqs_value *result = ctx->result; for (size_t i = 1; i <= result[0].len; ++i) { if (result[i].num == 4) { has_sixel = true; } } if (!has_sixel) { print_error(ctx, "terminal does not support sixel graphics"); return false; } // Hide cursor. if (!decrqm(ctx, DECTCEM, "cursor")) { return false; } ctx->hide_cursor = result[1].num == DECRQM_SET; if (ctx->hide_cursor) { fprintf(ctx->out_file, CTLSEQS_DECRST("%d"), DECTCEM); } // Check terminal name and version. long xterm_version = xtversion(ctx); if (xterm_version >= 0 && xterm_version < 369) { ctx->legacy_xterm = true; } else { ctx->legacy_xterm = false; } // Enable sixel scrolling. if (!decrqm(ctx, DECSDM, "sixel scrolling")) { return false; } // Before patch #369, XTerm implemented DECSDM incorrectly. // See https://invisible-island.net/xterm/xterm.log.html#xterm_369 ctx->sixel_scroll = result[1].num == (ctx->legacy_xterm ? DECRQM_RST : DECRQM_SET); if (ctx->sixel_scroll) { fprintf( ctx->out_file, ctx->legacy_xterm ? CTLSEQS_DECSET("%d") : CTLSEQS_DECRST("%d"), DECSDM ); } // Enable button event tracking mode. if (!decrqm(ctx, BTN_EVENT_TRACKING, "button event tracking mode")) { return false; } ctx->btnev_tracking = result[1].num == DECRQM_RST; if (ctx->btnev_tracking) { fprintf(ctx->out_file, CTLSEQS_DECSET("%d"), BTN_EVENT_TRACKING); } // Enable SGR mouse pixel mode. if (!decrqm(ctx, SGR_MOUSE_PIXELMODE, "SGR mouse pixel mode")) { return false; } ctx->sgr_pixelmode = result[1].num == DECRQM_RST; if (ctx->sgr_pixelmode) { fprintf(ctx->out_file, CTLSEQS_DECSET("%d"), SGR_MOUSE_PIXELMODE); } // Switch to alternate screen buffer. if (!decrqm(ctx, ALT_SCRBUF, "screen buffer")) { return false; } ctx->alt_scrbuf = result[1].num == DECRQM_RST; if (ctx->alt_scrbuf) { fprintf(ctx->out_file, CTLSEQS_DECSET("%d"), ALT_SCRBUF); } // Build the immutable part of sixel sequence. ctx->sixel_init_size = sizeof(SIXEL_SEQ_HEAD) - 1; ctx->sixel_init_size += sprintf( sixel_seq + ctx->sixel_init_size, "%u;%u#0;2;%d;%d;%d#0", ctx->ch_width, ctx->ch_height, ((ctx->line_color >> 16) & 0xFF) * 100 / 0xFF, ((ctx->line_color >> 8) & 0xFF) * 100 / 0xFF, ((ctx->line_color >> 0) & 0xFF) * 100 / 0xFF ); return true; } static bool draw( struct sixdraw_ctx *ctx ) { fprintf( ctx->out_file, CTLSEQS_CUP("%d", "1") "Canvas size: %ux%u. Line color: #%06X.", ctx->rows - 1, ctx->canvas_width, ctx->canvas_height, ctx->line_color ); fprintf( ctx->out_file, CTLSEQS_CUP("%d", "1") "Usage: Draw with mouse. Press Ctrl+C to exit.", ctx->rows ); union ctlseqs_value *result = ctx->result; while (true) { switch (ctlseqs_read(ctx->reader, ctx->matcher, -1)) { case 2: // CTLSEQS_RESP_SGR_MOUSE // Pixel coordinates start at 1 instead of 0. print_sixel_dot(ctx, result[1].num - 1, result[2].num - 1); break; case CTLSEQS_NOSEQ: // Press Ctrl+C to exit. if (result[1].str[0] == 0x03) { return true; } break; case CTLSEQS_ERROR: case CTLSEQS_NOMEM: case CTLSEQS_EOF: return false; } } } int main( int argc, char *argv[] ) { int status; struct sixdraw_ctx ctx; if (init(&ctx, argc, argv) && prepare(&ctx)) { status = draw(&ctx) ? 0 : -1; } else { status = -1; } terminate(&ctx); return status; }