/* ---------------------------------------------------------------------------- Copyright (c) 2021, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT License. A copy of the license can be found in the "LICENSE" file at the root of this distribution. -----------------------------------------------------------------------------*/ #include #include #include #include // getenv #include #include "common.h" #include "tty.h" #include "term.h" #include "stringbuf.h" // str_next_ofs #if defined(_WIN32) #include #define STDOUT_FILENO 1 #else #include #include #include #if defined(__linux__) #include #endif #endif #define IC_CSI "\x1B[" // color support; colors are auto mapped smaller palettes if needed. (see `term_color.c`) typedef enum palette_e { MONOCHROME, // no color ANSI8, // only basic 8 ANSI color (ESC[m, idx: 30-37, +10 for background) ANSI16, // basic + bright ANSI colors (ESC[m, idx: 30-37, 90-97, +10 for background) ANSI256, // ANSI 256 color palette (ESC[38;5;m, idx: 0-15 standard color, 16-231 6x6x6 rbg colors, 232-255 gray shades) ANSIRGB // direct rgb colors supported (ESC[38;2;;;m) } palette_t; // The terminal screen struct term_s { int fd_out; // output handle ssize_t width; // screen column width ssize_t height; // screen row height ssize_t raw_enabled; // is raw mode active? counted by start/end pairs bool nocolor; // show colors? bool silent; // enable beep? bool is_utf8; // utf-8 output? determined by the tty attr_t attr; // current text attributes palette_t palette; // color support buffer_mode_t bufmode; // buffer mode stringbuf_t* buf; // buffer for buffered output tty_t* tty; // used on posix to get the cursor position alloc_t* mem; // allocator #ifdef _WIN32 HANDLE hcon; // output console handler WORD hcon_default_attr; // default text attributes WORD hcon_orig_attr; // original text attributes DWORD hcon_orig_mode; // original console mode DWORD hcon_mode; // used console mode UINT hcon_orig_cp; // original console code-page (locale) COORD hcon_save_cursor; // saved cursor position (for escape sequence emulation) #endif }; static bool term_write_direct(term_t* term, const char* s, ssize_t n ); static void term_append_buf(term_t* term, const char* s, ssize_t n); //------------------------------------------------------------- // Colors //------------------------------------------------------------- #include "term_color.c" //------------------------------------------------------------- // Helpers //------------------------------------------------------------- ic_private void term_left(term_t* term, ssize_t n) { if (n <= 0) return; term_writef( term, IC_CSI "%zdD", n ); } ic_private void term_right(term_t* term, ssize_t n) { if (n <= 0) return; term_writef( term, IC_CSI "%zdC", n ); } ic_private void term_up(term_t* term, ssize_t n) { if (n <= 0) return; term_writef( term, IC_CSI "%zdA", n ); } ic_private void term_down(term_t* term, ssize_t n) { if (n <= 0) return; term_writef( term, IC_CSI "%zdB", n ); } ic_private void term_clear_line(term_t* term) { term_write( term, "\r" IC_CSI "K"); } ic_private void term_clear_to_end_of_line(term_t* term) { term_write(term, IC_CSI "K"); } ic_private void term_start_of_line(term_t* term) { term_write( term, "\r" ); } ic_private ssize_t term_get_width(term_t* term) { return term->width; } ic_private ssize_t term_get_height(term_t* term) { return term->height; } ic_private void term_attr_reset(term_t* term) { term_write(term, IC_CSI "m" ); } ic_private void term_underline(term_t* term, bool on) { term_write(term, on ? IC_CSI "4m" : IC_CSI "24m" ); } ic_private void term_reverse(term_t* term, bool on) { term_write(term, on ? IC_CSI "7m" : IC_CSI "27m"); } ic_private void term_bold(term_t* term, bool on) { term_write(term, on ? IC_CSI "1m" : IC_CSI "22m" ); } ic_private void term_italic(term_t* term, bool on) { term_write(term, on ? IC_CSI "3m" : IC_CSI "23m" ); } ic_private void term_writeln(term_t* term, const char* s) { term_write(term,s); term_write(term,"\n"); } ic_private void term_write_char(term_t* term, char c) { char buf[2]; buf[0] = c; buf[1] = 0; term_write_n(term, buf, 1 ); } ic_private attr_t term_get_attr( const term_t* term ) { return term->attr; } ic_private void term_set_attr( term_t* term, attr_t attr ) { if (attr.x.color != term->attr.x.color && attr.x.color != IC_COLOR_NONE) { term_color(term,attr.x.color); if (term->palette < ANSIRGB && color_is_rgb(attr.x.color)) { term->attr.x.color = attr.x.color; // actual color may have been approximated but we keep the actual color to avoid updating every time } } if (attr.x.bgcolor != term->attr.x.bgcolor && attr.x.bgcolor != IC_COLOR_NONE) { term_bgcolor(term,attr.x.bgcolor); if (term->palette < ANSIRGB && color_is_rgb(attr.x.bgcolor)) { term->attr.x.bgcolor = attr.x.bgcolor; } } if (attr.x.bold != term->attr.x.bold && attr.x.bold != IC_NONE) { term_bold(term,attr.x.bold == IC_ON); } if (attr.x.underline != term->attr.x.underline && attr.x.underline != IC_NONE) { term_underline(term,attr.x.underline == IC_ON); } if (attr.x.reverse != term->attr.x.reverse && attr.x.reverse != IC_NONE) { term_reverse(term,attr.x.reverse == IC_ON); } if (attr.x.italic != term->attr.x.italic && attr.x.italic != IC_NONE) { term_italic(term,attr.x.italic == IC_ON); } assert(attr.x.color == term->attr.x.color || attr.x.color == IC_COLOR_NONE); assert(attr.x.bgcolor == term->attr.x.bgcolor || attr.x.bgcolor == IC_COLOR_NONE); assert(attr.x.bold == term->attr.x.bold || attr.x.bold == IC_NONE); assert(attr.x.reverse == term->attr.x.reverse || attr.x.reverse == IC_NONE); assert(attr.x.underline == term->attr.x.underline || attr.x.underline == IC_NONE); assert(attr.x.italic == term->attr.x.italic || attr.x.italic == IC_NONE); } /* ic_private void term_clear_lines_to_end(term_t* term) { term_write(term, "\r" IC_CSI "J"); } ic_private void term_show_cursor(term_t* term, bool on) { term_write(term, on ? IC_CSI "?25h" : IC_CSI "?25l"); } */ //------------------------------------------------------------- // Formatted output //------------------------------------------------------------- ic_private void term_writef(term_t* term, const char* fmt, ...) { va_list ap; va_start(ap, fmt); term_vwritef(term,fmt,ap); va_end(ap); } ic_private void term_vwritef(term_t* term, const char* fmt, va_list args ) { sbuf_append_vprintf(term->buf, fmt, args); } ic_private void term_write_formatted( term_t* term, const char* s, const attr_t* attrs ) { term_write_formatted_n( term, s, attrs, ic_strlen(s)); } ic_private void term_write_formatted_n( term_t* term, const char* s, const attr_t* attrs, ssize_t len ) { if (attrs == NULL) { // write directly term_write(term,s); } else { // ensure raw mode from now on if (term->raw_enabled <= 0) { term_start_raw(term); } // and output with text attributes const attr_t default_attr = term_get_attr(term); attr_t attr = attr_none(); ssize_t i = 0; ssize_t n = 0; while( i+n < len && s[i+n] != 0 ) { if (!attr_is_eq(attr,attrs[i+n])) { if (n > 0) { term_write_n( term, s+i, n ); i += n; n = 0; } attr = attrs[i]; term_set_attr( term, attr_update_with(default_attr,attr) ); } n++; } if (n > 0) { term_write_n( term, s+i, n ); i += n; n = 0; } assert(s[i] != 0 || i == len); term_set_attr(term, default_attr); } } //------------------------------------------------------------- // Write to the terminal // The buffered functions are used to reduce cursor flicker // during refresh //------------------------------------------------------------- ic_private void term_beep(term_t* term) { if (term->silent) return; fprintf(stderr,"\x7"); fflush(stderr); } ic_private void term_write_repeat(term_t* term, const char* s, ssize_t count) { for (; count > 0; count--) { term_write(term, s); } } ic_private void term_write(term_t* term, const char* s) { if (s == NULL || s[0] == 0) return; ssize_t n = ic_strlen(s); term_write_n(term,s,n); } // Primitive terminal write; all writes go through here ic_private void term_write_n(term_t* term, const char* s, ssize_t n) { if (s == NULL || n <= 0) return; // write to buffer to reduce flicker and to process escape sequences (this may flush too) term_append_buf(term, s, n); } //------------------------------------------------------------- // Buffering //------------------------------------------------------------- ic_private void term_flush(term_t* term) { if (sbuf_len(term->buf) > 0) { //term_show_cursor(term,false); term_write_direct(term, sbuf_string(term->buf), sbuf_len(term->buf)); //term_show_cursor(term,true); sbuf_clear(term->buf); } } ic_private buffer_mode_t term_set_buffer_mode(term_t* term, buffer_mode_t mode) { buffer_mode_t oldmode = term->bufmode; if (oldmode != mode) { if (mode == UNBUFFERED) { term_flush(term); } term->bufmode = mode; } return oldmode; } static void term_check_flush(term_t* term, bool contains_nl) { if (term->bufmode == UNBUFFERED || sbuf_len(term->buf) > 4000 || (term->bufmode == LINEBUFFERED && contains_nl)) { term_flush(term); } } //------------------------------------------------------------- // Init //------------------------------------------------------------- static void term_init_raw(term_t* term); ic_private term_t* term_new(alloc_t* mem, tty_t* tty, bool nocolor, bool silent, int fd_out ) { term_t* term = mem_zalloc_tp(mem, term_t); if (term == NULL) return NULL; term->fd_out = (fd_out < 0 ? STDOUT_FILENO : fd_out); term->nocolor = nocolor; term->silent = silent; term->mem = mem; term->tty = tty; term->width = 80; term->height = 25; term->is_utf8 = tty_is_utf8(tty); term->palette = ANSI16; // almost universally supported term->buf = sbuf_new(mem); term->bufmode = LINEBUFFERED; term->attr = attr_default(); // respect NO_COLOR if (getenv("NO_COLOR") != NULL) { term->nocolor = true; } // detect color palette // COLORTERM takes precedence const char* colorterm = getenv("COLORTERM"); const char* eterm = getenv("TERM"); if (ic_contains(colorterm,"24bit") || ic_contains(colorterm,"truecolor") || ic_contains(colorterm,"direct")) { term->palette = ANSIRGB; } else if (ic_contains(colorterm,"8bit") || ic_contains(colorterm,"256color")) { term->palette = ANSI256; } else if (ic_contains(colorterm,"4bit") || ic_contains(colorterm,"16color")) { term->palette = ANSI16; } else if (ic_contains(colorterm,"3bit") || ic_contains(colorterm,"8color")) { term->palette = ANSI8; } else if (ic_contains(colorterm,"1bit") || ic_contains(colorterm,"nocolor") || ic_contains(colorterm,"monochrome")) { term->palette = MONOCHROME; } // otherwise check for some specific terminals else if (getenv("WT_SESSION") != NULL) { term->palette = ANSIRGB; } // Windows terminal else if (getenv("ITERM_SESSION_ID") != NULL) { term->palette = ANSIRGB; } // iTerm2 terminal else if (getenv("VSCODE_PID") != NULL) { term->palette = ANSIRGB; } // vscode terminal else { // and otherwise fall back to checking TERM if (ic_contains(eterm,"truecolor") || ic_contains(eterm,"direct") || ic_contains(colorterm,"24bit")) { term->palette = ANSIRGB; } else if (ic_contains(eterm,"alacritty") || ic_contains(eterm,"kitty")) { term->palette = ANSIRGB; } else if (ic_contains(eterm,"256color") || ic_contains(eterm,"gnome")) { term->palette = ANSI256; } else if (ic_contains(eterm,"16color")){ term->palette = ANSI16; } else if (ic_contains(eterm,"8color")) { term->palette = ANSI8; } else if (ic_contains(eterm,"monochrome") || ic_contains(eterm,"nocolor") || ic_contains(eterm,"dumb")) { term->palette = MONOCHROME; } } debug_msg("term: color-bits: %d (COLORTERM=%s, TERM=%s)\n", term_get_color_bits(term), colorterm, eterm); // read COLUMS/LINES from the environment for a better initial guess. const char* env_columns = getenv("COLUMNS"); if (env_columns != NULL) { ic_atoz(env_columns, &term->width); } const char* env_lines = getenv("LINES"); if (env_lines != NULL) { ic_atoz(env_lines, &term->height); } // initialize raw terminal output and terminal dimensions term_init_raw(term); term_update_dim(term); term_attr_reset(term); // ensure we are at default settings return term; } ic_private bool term_is_interactive(const term_t* term) { ic_unused(term); // check dimensions (0 is used for debuggers) // if (term->width <= 0) return false; // check editing support const char* eterm = getenv("TERM"); debug_msg("term: TERM=%s\n", eterm); if (eterm != NULL && (strstr("dumb|DUMB|cons25|CONS25|emacs|EMACS",eterm) != NULL)) { return false; } return true; } ic_private bool term_enable_beep(term_t* term, bool enable) { bool prev = term->silent; term->silent = !enable; return prev; } ic_private bool term_enable_color(term_t* term, bool enable) { bool prev = !term->nocolor; term->nocolor = !enable; return prev; } ic_private void term_free(term_t* term) { if (term == NULL) return; term_flush(term); term_end_raw(term, true); sbuf_free(term->buf); term->buf = NULL; mem_free(term->mem, term); } //------------------------------------------------------------- // For best portability and applications inserting CSI SGR (ESC[ .. m) // codes themselves in strings, we interpret these at the // lowest level so we can have a `term_get_attr` function which // is needed for bracketed styles etc. //------------------------------------------------------------- static void term_append_esc(term_t* term, const char* const s, ssize_t len) { if (s[1]=='[' && s[len-1] == 'm') { // it is a CSI SGR sequence: ESC[ ... m if (term->nocolor) return; // ignore escape sequences if nocolor is set term->attr = attr_update_with(term->attr, attr_from_esc_sgr(s,len)); } // and write out the escape sequence as-is sbuf_append_n(term->buf, s, len); } static void term_append_utf8(term_t* term, const char* s, ssize_t len) { ssize_t nread; unicode_t uchr = unicode_from_qutf8((const uint8_t*)s, len, &nread); uint8_t c; if (unicode_is_raw(uchr, &c)) { // write bytes as is; this also ensure that on non-utf8 terminals characters between 0x80-0xFF // go through _as is_ due to the qutf8 encoding. sbuf_append_char(term->buf,(char)c); } else if (!term->is_utf8) { // on non-utf8 terminals still send utf-8 and hope for the best // todo: we could try to convert to the locale first? sbuf_append_n(term->buf, s, len); // sbuf_appendf(term->buf, "\x1B[%" PRIu32 "u", uchr); // unicode escape code } else { // write utf-8 as is sbuf_append_n(term->buf, s, len); } } static void term_append_buf( term_t* term, const char* s, ssize_t len ) { ssize_t pos = 0; bool newline = false; while (pos < len) { // handle ascii sequences in bulk ssize_t ascii = 0; ssize_t next; while ((next = str_next_ofs(s, len, pos+ascii, NULL)) > 0 && (uint8_t)s[pos + ascii] > '\x1B' && (uint8_t)s[pos + ascii] <= 0x7F ) { ascii += next; } if (ascii > 0) { sbuf_append_n(term->buf, s+pos, ascii); pos += ascii; } if (next <= 0) break; const uint8_t c = (uint8_t)s[pos]; // handle utf8 sequences (for non-utf8 terminals) if (c >= 0x80) { term_append_utf8(term, s+pos, next); } // handle escape sequence (note: str_next_ofs considers whole CSI escape sequences at a time) else if (next > 1 && c == '\x1B') { term_append_esc(term, s+pos, next); } else if (c < ' ' && c != 0 && (c < '\x07' || c > '\x0D')) { // ignore control characters except \a, \b, \t, \n, \r, and form-feed and vertical tab. } else { if (c == '\n') { newline = true; } sbuf_append_n(term->buf, s+pos, next); } pos += next; } // possibly flush term_check_flush(term, newline); } //------------------------------------------------------------- // Platform dependent: Write directly to the terminal //------------------------------------------------------------- #if !defined(_WIN32) // write to the console without further processing static bool term_write_direct(term_t* term, const char* s, ssize_t n) { ssize_t count = 0; while( count < n ) { ssize_t nwritten = write(term->fd_out, s + count, to_size_t(n - count)); if (nwritten > 0) { count += nwritten; } else if (errno != EINTR && errno != EAGAIN) { debug_msg("term: write failed: length %i, errno %i: \"%s\"\n", n, errno, s); return false; } } return true; } #else //---------------------------------------------------------------------------------- // On windows we use the new virtual terminal processing if it is available (Windows Terminal) // but fall back to ansi escape emulation on older systems but also for example // the PS terminal // // note: we use row/col as 1-based ANSI escape while windows X/Y coords are 0-based. //----------------------------------------------------------------------------------- #if !defined(ENABLE_VIRTUAL_TERMINAL_PROCESSING) #define ENABLE_VIRTUAL_TERMINAL_PROCESSING (0) #endif #if !defined(ENABLE_LVB_GRID_WORLDWIDE) #define ENABLE_LVB_GRID_WORLDWIDE (0) #endif // direct write to the console without further processing static bool term_write_console(term_t* term, const char* s, ssize_t n ) { DWORD written; // WriteConsoleA(term->hcon, s, (DWORD)(to_size_t(n)), &written, NULL); WriteFile(term->hcon, s, (DWORD)(to_size_t(n)), &written, NULL); // so it can be redirected return (written == (DWORD)(to_size_t(n))); } static bool term_get_cursor_pos( term_t* term, ssize_t* row, ssize_t* col) { *row = 0; *col = 0; CONSOLE_SCREEN_BUFFER_INFO info; if (!GetConsoleScreenBufferInfo(term->hcon, &info)) return false; *row = (ssize_t)info.dwCursorPosition.Y + 1; *col = (ssize_t)info.dwCursorPosition.X + 1; return true; } static void term_move_cursor_to( term_t* term, ssize_t row, ssize_t col ) { CONSOLE_SCREEN_BUFFER_INFO info; if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return; if (col > info.dwSize.X) col = info.dwSize.X; if (row > info.dwSize.Y) row = info.dwSize.Y; if (col <= 0) col = 1; if (row <= 0) row = 1; COORD coord; coord.X = (SHORT)col - 1; coord.Y = (SHORT)row - 1; SetConsoleCursorPosition( term->hcon, coord); } static void term_cursor_save(term_t* term) { memset(&term->hcon_save_cursor, 0, sizeof(term->hcon_save_cursor)); CONSOLE_SCREEN_BUFFER_INFO info; if (!GetConsoleScreenBufferInfo(term->hcon, &info)) return; term->hcon_save_cursor = info.dwCursorPosition; } static void term_cursor_restore(term_t* term) { if (term->hcon_save_cursor.X == 0) return; SetConsoleCursorPosition(term->hcon, term->hcon_save_cursor); } static void term_move_cursor( term_t* term, ssize_t drow, ssize_t dcol, ssize_t n ) { CONSOLE_SCREEN_BUFFER_INFO info; if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return; COORD cur = info.dwCursorPosition; ssize_t col = (ssize_t)cur.X + 1 + n*dcol; ssize_t row = (ssize_t)cur.Y + 1 + n*drow; term_move_cursor_to( term, row, col ); } static void term_cursor_visible( term_t* term, bool visible ) { CONSOLE_CURSOR_INFO info; if (!GetConsoleCursorInfo(term->hcon,&info)) return; info.bVisible = visible; SetConsoleCursorInfo(term->hcon,&info); } static void term_erase_line( term_t* term, ssize_t mode ) { CONSOLE_SCREEN_BUFFER_INFO info; if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return; DWORD written; COORD start; ssize_t length; if (mode == 2) { // entire line start.X = 0; start.Y = info.dwCursorPosition.Y; length = (ssize_t)info.srWindow.Right + 1; } else if (mode == 1) { // to start of line start.X = 0; start.Y = info.dwCursorPosition.Y; length = info.dwCursorPosition.X; } else { // to end of line length = (ssize_t)info.srWindow.Right - info.dwCursorPosition.X + 1; start = info.dwCursorPosition; } FillConsoleOutputAttribute( term->hcon, term->hcon_default_attr, (DWORD)length, start, &written ); FillConsoleOutputCharacterA( term->hcon, ' ', (DWORD)length, start, &written ); } static void term_clear_screen(term_t* term, ssize_t mode) { CONSOLE_SCREEN_BUFFER_INFO info; if (!GetConsoleScreenBufferInfo(term->hcon, &info)) return; COORD start; start.X = 0; start.Y = 0; ssize_t length; ssize_t width = (ssize_t)info.dwSize.X; if (mode == 2) { // entire screen length = width * info.dwSize.Y; } else if (mode == 1) { // to cursor length = (width * ((ssize_t)info.dwCursorPosition.Y - 1)) + info.dwCursorPosition.X; } else { // from cursor start = info.dwCursorPosition; length = (width * ((ssize_t)info.dwSize.Y - info.dwCursorPosition.Y)) + (width - info.dwCursorPosition.X + 1); } DWORD written; FillConsoleOutputAttribute(term->hcon, term->hcon_default_attr, (DWORD)length, start, &written); FillConsoleOutputCharacterA(term->hcon, ' ', (DWORD)length, start, &written); } static WORD attr_color[8] = { 0, // black FOREGROUND_RED, // maroon FOREGROUND_GREEN, // green FOREGROUND_RED | FOREGROUND_GREEN, // orange FOREGROUND_BLUE, // navy FOREGROUND_RED | FOREGROUND_BLUE, // purple FOREGROUND_GREEN | FOREGROUND_BLUE, // teal FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // light gray }; static void term_set_win_attr( term_t* term, attr_t ta ) { WORD def_attr = term->hcon_default_attr; CONSOLE_SCREEN_BUFFER_INFO info; if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return; WORD cur_attr = info.wAttributes; WORD attr = cur_attr; if (ta.x.color != IC_COLOR_NONE) { if (ta.x.color >= IC_ANSI_BLACK && ta.x.color <= IC_ANSI_SILVER) { attr = (attr & 0xFFF0) | attr_color[ta.x.color - IC_ANSI_BLACK]; } else if (ta.x.color >= IC_ANSI_GRAY && ta.x.color <= IC_ANSI_WHITE) { attr = (attr & 0xFFF0) | attr_color[ta.x.color - IC_ANSI_GRAY] | FOREGROUND_INTENSITY; } else if (ta.x.color == IC_ANSI_DEFAULT) { attr = (attr & 0xFFF0) | (def_attr & 0x000F); } } if (ta.x.bgcolor != IC_COLOR_NONE) { if (ta.x.bgcolor >= IC_ANSI_BLACK && ta.x.bgcolor <= IC_ANSI_SILVER) { attr = (attr & 0xFF0F) | (WORD)(attr_color[ta.x.bgcolor - IC_ANSI_BLACK] << 4); } else if (ta.x.bgcolor >= IC_ANSI_GRAY && ta.x.bgcolor <= IC_ANSI_WHITE) { attr = (attr & 0xFF0F) | (WORD)(attr_color[ta.x.bgcolor - IC_ANSI_GRAY] << 4) | BACKGROUND_INTENSITY; } else if (ta.x.bgcolor == IC_ANSI_DEFAULT) { attr = (attr & 0xFF0F) | (def_attr & 0x00F0); } } if (ta.x.underline != IC_NONE) { attr = (attr & ~COMMON_LVB_UNDERSCORE) | (ta.x.underline == IC_ON ? COMMON_LVB_UNDERSCORE : 0); } if (ta.x.reverse != IC_NONE) { attr = (attr & ~COMMON_LVB_REVERSE_VIDEO) | (ta.x.reverse == IC_ON ? COMMON_LVB_REVERSE_VIDEO : 0); } if (attr != cur_attr) { SetConsoleTextAttribute(term->hcon, attr); } } static ssize_t esc_param( const char* s, ssize_t def ) { if (*s == '?') s++; ssize_t n = def; ic_atoz(s, &n); return n; } static void esc_param2( const char* s, ssize_t* p1, ssize_t* p2, ssize_t def ) { if (*s == '?') s++; *p1 = def; *p2 = def; ic_atoz2(s, p1, p2); } // Emulate escape sequences on older windows. static void term_write_esc( term_t* term, const char* s, ssize_t len ) { ssize_t row; ssize_t col; if (s[1] == '[') { switch (s[len-1]) { case 'A': term_move_cursor(term, -1, 0, esc_param(s+2, 1)); break; case 'B': term_move_cursor(term, 1, 0, esc_param(s+2, 1)); break; case 'C': term_move_cursor(term, 0, 1, esc_param(s+2, 1)); break; case 'D': term_move_cursor(term, 0, -1, esc_param(s+2, 1)); break; case 'H': esc_param2(s+2, &row, &col, 1); term_move_cursor_to(term, row, col); break; case 'K': term_erase_line(term, esc_param(s+2, 0)); break; case 'm': term_set_win_attr( term, attr_from_esc_sgr(s,len) ); break; // support some less standard escape codes (currently not used by isocline) case 'E': // line down term_get_cursor_pos(term, &row, &col); row += esc_param(s+2, 1); term_move_cursor_to(term, row, 1); break; case 'F': // line up term_get_cursor_pos(term, &row, &col); row -= esc_param(s+2, 1); term_move_cursor_to(term, row, 1); break; case 'G': // absolute column term_get_cursor_pos(term, &row, &col); col = esc_param(s+2, 1); term_move_cursor_to(term, row, col); break; case 'J': term_clear_screen(term, esc_param(s+2, 0)); break; case 'h': if (strncmp(s+2, "?25h", 4) == 0) { term_cursor_visible(term, true); } break; case 'l': if (strncmp(s+2, "?25l", 4) == 0) { term_cursor_visible(term, false); } break; case 's': term_cursor_save(term); break; case 'u': term_cursor_restore(term); break; // otherwise ignore } } else if (s[1] == '7') { term_cursor_save(term); } else if (s[1] == '8') { term_cursor_restore(term); } else { // otherwise ignore } } static bool term_write_direct(term_t* term, const char* s, ssize_t len ) { term_cursor_visible(term,false); // reduce flicker ssize_t pos = 0; if ((term->hcon_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { // use the builtin virtual terminal processing. (enables truecolor for example) term_write_console(term, s, len); pos = len; } else { // emulate escape sequences while( pos < len ) { // handle non-control in bulk (including utf-8 sequences) // (We don't need to handle utf-8 separately as we set the codepage to always be in utf-8 mode) ssize_t nonctrl = 0; ssize_t next; while( (next = str_next_ofs( s, len, pos+nonctrl, NULL )) > 0 && (uint8_t)s[pos + nonctrl] >= ' ' && (uint8_t)s[pos + nonctrl] <= 0x7F) { nonctrl += next; } if (nonctrl > 0) { term_write_console(term, s+pos, nonctrl); pos += nonctrl; } if (next <= 0) break; if ((uint8_t)s[pos] >= 0x80) { // utf8 is already processed term_write_console(term, s+pos, next); } else if (next > 1 && s[pos] == '\x1B') { // handle control (note: str_next_ofs considers whole CSI escape sequences at a time) term_write_esc(term, s+pos, next); } else if (next == 1 && (s[pos] == '\r' || s[pos] == '\n' || s[pos] == '\t' || s[pos] == '\b')) { term_write_console( term, s+pos, next); } else { // ignore } pos += next; } } term_cursor_visible(term,true); assert(pos == len); return (pos == len); } #endif //------------------------------------------------------------- // Update terminal dimensions //------------------------------------------------------------- #if !defined(_WIN32) // send escape query that may return a response on the tty static bool term_esc_query_raw( term_t* term, const char* query, char* buf, ssize_t buflen ) { if (buf==NULL || buflen <= 0 || query[0] == 0) return false; bool osc = (query[1] == ']'); if (!term_write_direct(term, query, ic_strlen(query))) return false; debug_msg("term: read tty query response to: ESC %s\n", query + 1); return tty_read_esc_response( term->tty, query[1], osc, buf, buflen ); } static bool term_esc_query( term_t* term, const char* query, char* buf, ssize_t buflen ) { if (!tty_start_raw(term->tty)) return false; bool ok = term_esc_query_raw(term,query,buf,buflen); tty_end_raw(term->tty); return ok; } // get the cursor position via an ESC[6n static bool term_get_cursor_pos( term_t* term, ssize_t* row, ssize_t* col) { // send escape query char buf[128]; if (!term_esc_query(term,"\x1B[6n",buf,128)) return false; if (!ic_atoz2(buf,row,col)) return false; return true; } static void term_set_cursor_pos( term_t* term, ssize_t row, ssize_t col ) { term_writef( term, IC_CSI "%zd;%zdH", row, col ); } ic_private bool term_update_dim(term_t* term) { ssize_t cols = 0; ssize_t rows = 0; struct winsize ws; if (ioctl(term->fd_out, TIOCGWINSZ, &ws) >= 0) { // ioctl succeeded cols = ws.ws_col; // debuggers return 0 for the column rows = ws.ws_row; } else { // determine width by querying the cursor position debug_msg("term: ioctl term-size failed: %d,%d\n", ws.ws_row, ws.ws_col); ssize_t col0 = 0; ssize_t row0 = 0; if (term_get_cursor_pos(term,&row0,&col0)) { term_set_cursor_pos(term,999,999); ssize_t col1 = 0; ssize_t row1 = 0; if (term_get_cursor_pos(term,&row1,&col1)) { cols = col1; rows = row1; } term_set_cursor_pos(term,row0,col0); } else { // cannot query position // return 0 column } } // update width and return whether it changed. bool changed = (term->width != cols || term->height != rows); debug_msg("terminal dim: %zd,%zd: %s\n", rows, cols, changed ? "changed" : "unchanged"); if (cols > 0) { term->width = cols; term->height = rows; } return changed; } #else ic_private bool term_update_dim(term_t* term) { if (term->hcon == 0) { term->hcon = GetConsoleWindow(); } ssize_t rows = 0; ssize_t cols = 0; CONSOLE_SCREEN_BUFFER_INFO sbinfo; if (GetConsoleScreenBufferInfo(term->hcon, &sbinfo)) { cols = (ssize_t)sbinfo.srWindow.Right - (ssize_t)sbinfo.srWindow.Left + 1; rows = (ssize_t)sbinfo.srWindow.Bottom - (ssize_t)sbinfo.srWindow.Top + 1; } bool changed = (term->width != cols || term->height != rows); term->width = cols; term->height = rows; debug_msg("term: update dim: %zd, %zd\n", term->height, term->width ); return changed; } #endif //------------------------------------------------------------- // Enable/disable terminal raw mode //------------------------------------------------------------- #if !defined(_WIN32) // On non-windows, the terminal is set in raw mode by the tty. ic_private void term_start_raw(term_t* term) { term->raw_enabled++; } ic_private void term_end_raw(term_t* term, bool force) { if (term->raw_enabled <= 0) return; if (!force) { term->raw_enabled--; } else { term->raw_enabled = 0; } } static bool term_esc_query_color_raw(term_t* term, int color_idx, uint32_t* color ) { char buf[128+1]; snprintf(buf,128,"\x1B]4;%d;?\x1B\\", color_idx); if (!term_esc_query_raw( term, buf, buf, 128 )) { debug_msg("esc query response not received\n"); return false; } if (buf[0] != '4') return false; const char* rgb = strchr(buf,':'); if (rgb==NULL) return false; rgb++; // skip ':' unsigned int r,g,b; if (sscanf(rgb,"%x/%x/%x",&r,&g,&b) != 3) return false; if (rgb[2]!='/') { // 48-bit rgb, hexadecimal round to 24-bit r = (r+0x7F)/0x100; // note: can "overflow", e.g. 0xFFFF -> 0x100. (and we need `ic_cap8` to convert.) g = (g+0x7F)/0x100; b = (b+0x7F)/0x100; } *color = (ic_cap8(r)<<16) | (ic_cap8(g)<<8) | ic_cap8(b); debug_msg("color query: %02x,%02x,%02x: %06x\n", r, g, b, *color); return true; } // update ansi 16 color palette for better color approximation static void term_update_ansi16(term_t* term) { debug_msg("update ansi colors\n"); #if defined(GIO_CMAP) // try ioctl first (on Linux) uint8_t cmap[48]; memset(cmap,0,48); if (ioctl(term->fd_out,GIO_CMAP,&cmap) >= 0) { // success for(ssize_t i = 0; i < 48; i+=3) { uint32_t color = ((uint32_t)(cmap[i]) << 16) | ((uint32_t)(cmap[i+1]) << 8) | cmap[i+2]; debug_msg("term (ioctl) ansi color %d: 0x%06x\n", i, color); ansi256[i] = color; } return; } else { debug_msg("ioctl GIO_CMAP failed: entry 1: 0x%02x%02x%02x\n", cmap[3], cmap[4], cmap[5]); } #endif // this seems to be unreliable on some systems (Ubuntu+Gnome terminal) so only enable when known ok. #if __APPLE__ // otherwise use OSC 4 escape sequence query tty_start_raw(term->tty); for(ssize_t i = 0; i < 16; i++) { uint32_t color; if (!term_esc_query_color_raw(term, i, &color)) break; debug_msg("term ansi color %d: 0x%06x\n", i, color); ansi256[i] = color; } tty_end_raw(term->tty); #endif } static void term_init_raw(term_t* term) { if (term->palette < ANSIRGB) { term_update_ansi16(term); } } #else ic_private void term_start_raw(term_t* term) { if (term->raw_enabled++ > 0) return; CONSOLE_SCREEN_BUFFER_INFO info; if (GetConsoleScreenBufferInfo(term->hcon, &info)) { term->hcon_orig_attr = info.wAttributes; } term->hcon_orig_cp = GetConsoleOutputCP(); SetConsoleOutputCP(CP_UTF8); if (term->hcon_mode == 0) { // first time initialization DWORD mode = ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_LVB_GRID_WORLDWIDE; // for \r \n and \b // use escape sequence handling if available and the terminal supports it (so we can use rgb colors in Windows terminal) // Unfortunately, in plain powershell, we can successfully enable terminal processing // but it still fails to render correctly; so we require the palette be large enough (like in Windows Terminal) if (term->palette >= ANSI256 && SetConsoleMode(term->hcon, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { term->hcon_mode = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; debug_msg("term: console mode: virtual terminal processing enabled\n"); } // no virtual terminal processing, emulate instead else if (SetConsoleMode(term->hcon, mode)) { term->hcon_mode = mode; term->palette = ANSI16; } GetConsoleMode(term->hcon, &mode); debug_msg("term: console mode: orig: 0x%x, new: 0x%x, current 0x%x\n", term->hcon_orig_mode, term->hcon_mode, mode); } else { SetConsoleMode(term->hcon, term->hcon_mode); } } ic_private void term_end_raw(term_t* term, bool force) { if (term->raw_enabled <= 0) return; if (!force && term->raw_enabled > 1) { term->raw_enabled--; } else { term->raw_enabled = 0; SetConsoleMode(term->hcon, term->hcon_orig_mode); SetConsoleOutputCP(term->hcon_orig_cp); SetConsoleTextAttribute(term->hcon, term->hcon_orig_attr); } } static void term_init_raw(term_t* term) { term->hcon = GetStdHandle(STD_OUTPUT_HANDLE); GetConsoleMode(term->hcon, &term->hcon_orig_mode); CONSOLE_SCREEN_BUFFER_INFOEX info; memset(&info, 0, sizeof(info)); info.cbSize = sizeof(info); if (GetConsoleScreenBufferInfoEx(term->hcon, &info)) { // store default attributes term->hcon_default_attr = info.wAttributes; // update our color table with the actual colors used. for (unsigned i = 0; i < 16; i++) { COLORREF cr = info.ColorTable[i]; uint32_t color = (ic_cap8(GetRValue(cr))<<16) | (ic_cap8(GetGValue(cr))<<8) | ic_cap8(GetBValue(cr)); // COLORREF = BGR // index is also in reverse in the bits 0 and 2 unsigned j = (i&0x08) | ((i&0x04)>>2) | (i&0x02) | (i&0x01)<<2; debug_msg("term: ansi color %d is 0x%06x\n", j, color); ansi256[j] = color; } } else { DWORD err = GetLastError(); debug_msg("term: cannot get console screen buffer: %d %x", err, err); } term_start_raw(term); // initialize the hcon_mode term_end_raw(term,false); } #endif