/** * sixdraw.c - draw lines 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 #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 BTN_EVENT_TRACKING 1002 #define SGR_MOUSE_PIXELMODE 1016 #define ALT_SCRBUF 1049 #define SIXEL_SEQ_HEAD CTLSEQS_DCS "0;1q\"1;1;" 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 hide_cursor; bool alt_scrbuf; bool btnev_tracking; bool sgr_pixelmode; int in_fd; int out_fd; int timeout; unsigned ch_width; unsigned ch_height; unsigned line_color; }; 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 void terminate(struct sixdraw_ctx *ctx) { ctlseqs_matcher_free(ctx->matcher); ctlseqs_reader_free(ctx->reader); // Restore cursor status. if (ctx->hide_cursor) { dprintf(ctx->out_fd, CTLSEQS_DECSET("%d"), DECTCEM); } // Restore normal screen buffer. if (ctx->alt_scrbuf) { dprintf(ctx->out_fd, CTLSEQS_DECRST("%d"), ALT_SCRBUF); } // Restore original mouse modes. if (ctx->btnev_tracking) { dprintf(ctx->out_fd, CTLSEQS_DECRST("%d"), BTN_EVENT_TRACKING); } if (ctx->sgr_pixelmode) { dprintf(ctx->out_fd, CTLSEQS_DECRST("%d"), SGR_MOUSE_PIXELMODE); } // 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->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, char const *name) { ssize_t retval; union ctlseqs_value *result = ctx->result; dprintf(ctx->out_fd, 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; } if (result[0].num != mode || (result[1].num != DECRQM_SET && result[1].num != DECRQM_RST)) { print_error(ctx, "%s status not recognizable", name); return false; } return true; } static inline void get_sixel_color(unsigned rgb888, unsigned *red, unsigned *green, unsigned *blue) { *red = ((rgb888 >> 16) & 0xFF) * 100 / 0xFF; *green = ((rgb888 >> 8) & 0xFF) * 100 / 0xFF; *blue = ((rgb888 >> 0) & 0xFF) * 100 / 0xFF; } static inline char * get_sixel_buffer() { static char sixel_seq_buffer[2048] = SIXEL_SEQ_HEAD; return sixel_seq_buffer; } static void print_sixel_dot(struct sixdraw_ctx *ctx, unsigned x, unsigned y) { char *sixel_seq = get_sixel_buffer(); size_t seq_len = sizeof(SIXEL_SEQ_HEAD) - 1; seq_len += sprintf(sixel_seq + seq_len, "%d;%d", ctx->ch_width, ctx->ch_height); // Select color. unsigned red, green, blue; get_sixel_color(ctx->line_color, &red, &green, &blue); seq_len += sprintf(sixel_seq + seq_len, "#0;2;%d;%d;%d#0", red, green, blue); // Move cursor. unsigned row = y / ctx->ch_height + 1; unsigned col = x / ctx->ch_width + 1; dprintf(ctx->out_fd, CTLSEQS_CUP("%d", "%d"), row, col); // Draw dot. row = y % ctx->ch_height; col = x % ctx->ch_width; seq_len += sprintf(sixel_seq + seq_len, "%.*s", row / 6, "------------------------"); seq_len += sprintf(sixel_seq + seq_len, "!%u?%c" CTLSEQS_ST, col, (1 << row % 6) + 0x3F); // Output sixel sequence. do { ssize_t nbytes = write(ctx->out_fd, sixel_seq, seq_len); if (nbytes > 0) { seq_len -= nbytes; } } while (seq_len > 0); } 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, .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: 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") }; 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; } 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; 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) { dprintf(ctx->out_fd, CTLSEQS_DECRST("%d"), DECTCEM); } // 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) { dprintf(ctx->out_fd, CTLSEQS_DECSET("%d"), ALT_SCRBUF); } // 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) { dprintf(ctx->out_fd, 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) { dprintf(ctx->out_fd, CTLSEQS_DECSET("%d"), SGR_MOUSE_PIXELMODE); } return true; } static bool draw(struct sixdraw_ctx *ctx) { union ctlseqs_value *result = ctx->result; while (true) { switch (ctlseqs_read(ctx->reader, ctx->matcher, -1)) { case 2: // CTLSEQS_RESP_SGR_MOUSE print_sixel_dot(ctx, result[1].num, result[2].num); 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; }