/* * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jonas 'Sortie' Termansen. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * showline.c * Display a line on the terminal. */ #include #include #include #include #include #include #include #include #include "showline.h" struct wincurpos predict_cursor(struct cursor_predict* cursor_predict, struct wincurpos wcp, struct winsize ws, wchar_t c) { if ( c == L'\0' ) return wcp; if ( cursor_predict->escaped ) { if ( (L'a' <= c && c <= L'z') || (L'A' <= c && c <= L'Z') ) cursor_predict->escaped = false; return wcp; } if ( c == L'\e' ) { cursor_predict->escaped = true; return wcp; } if ( c == L'\n' || ws.ws_col <= wcp.wcp_col + 1 ) { wcp.wcp_col = 0; if ( wcp.wcp_row + 1 < ws.ws_row ) wcp.wcp_row++; } else { wcp.wcp_col++; } return wcp; } bool predict_will_scroll(struct cursor_predict cursor_predict, struct wincurpos wcp, struct winsize ws, wchar_t c) { if ( c == L'\0' ) return false; if ( cursor_predict.escaped ) return false; return (c == L'\n' || ws.ws_col <= wcp.wcp_col + 1) && !(wcp.wcp_row + 1 < ws.ws_row); } void show_line_begin(struct show_line* show_state, int out_fd) { memset(show_state, 0, sizeof(*show_state)); show_state->out_fd = out_fd; show_state->current_line = NULL; show_state->current_cursor = 0; tcgetwinsize(show_state->out_fd, &show_state->ws); if ( tcgetwincurpos(out_fd, &show_state->wcp_start) == 0 ) show_state->wcp_current = show_state->wcp_start; else { dprintf(show_state->out_fd, "\e[6n"); show_state->wcp_pending = true; } } void show_line_wincurpos(struct show_line* show_state, unsigned int r, unsigned int c) { if ( !show_state->wcp_pending ) return; show_state->wcp_start.wcp_row = r; show_state->wcp_start.wcp_col = c; show_state->wcp_current = show_state->wcp_start; show_state->wcp_pending = false; } bool show_line_is_weird(const char* line) { for ( size_t i = 0; line[i]; i++ ) { if ( line[i] == '\e' ) { i++; if ( line[i] != '[' ) return true; i++; while ( ('0' <= line[i] && line[i] <= '9') || line[i] == ';' ) i++; switch ( line[i] ) { case 'm': break; default: return true; } continue; } switch ( line[i] ) { case '\a': return true; case '\b': return true; case '\f': return true; case '\r': return true; case '\t': return true; // TODO: This isn't weird. case '\v': return true; default: break; } } return false; } void show_line_change_cursor(struct show_line* show_state, struct wincurpos wcp) { if ( wcp.wcp_col == show_state->wcp_current.wcp_col && wcp.wcp_row == show_state->wcp_current.wcp_row ) return; if ( wcp.wcp_col == 0 ) dprintf(show_state->out_fd, "\e[%zuH", wcp.wcp_row + 1); else dprintf(show_state->out_fd, "\e[%zu;%zuH", wcp.wcp_row + 1, wcp.wcp_col+ 1); show_state->wcp_current = wcp; } bool show_line_optimized(struct show_line* show_state, const char* line, size_t cursor) { struct winsize ws = show_state->ws; mbstate_t old_ps; mbstate_t new_ps; memset(&old_ps, 0, sizeof(old_ps)); memset(&new_ps, 0, sizeof(new_ps)); struct wincurpos old_wcp = show_state->wcp_start; struct wincurpos new_wcp = show_state->wcp_start; struct cursor_predict old_cursor_predict; struct cursor_predict new_cursor_predict; memset(&old_cursor_predict, 0, sizeof(old_cursor_predict)); memset(&new_cursor_predict, 0, sizeof(new_cursor_predict)); size_t old_line_offset = 0; size_t new_line_offset = 0; const char* old_line = show_state->current_line; const char* new_line = line; struct wincurpos cursor_wcp = show_state->wcp_start; while ( true ) { if ( cursor == new_line_offset ) cursor_wcp = new_wcp; wchar_t old_wc; wchar_t new_wc; size_t old_num_bytes = mbrtowc(&old_wc, old_line + old_line_offset, SIZE_MAX, &old_ps); size_t new_num_bytes = mbrtowc(&new_wc, new_line + new_line_offset, SIZE_MAX, &new_ps); assert(old_num_bytes != (size_t) -2); assert(new_num_bytes != (size_t) -2); assert(old_num_bytes != (size_t) -1); assert(new_num_bytes != (size_t) -1); if ( old_num_bytes == 0 && new_num_bytes == 0 ) break; bool will_scroll = predict_will_scroll(new_cursor_predict, new_wcp, ws, new_wc); bool can_scroll = show_state->wcp_start.wcp_row != 0; if ( will_scroll && !can_scroll ) { if ( new_line_offset < cursor ) cursor_wcp = new_wcp; break; } if ( predict_will_scroll(old_cursor_predict, old_wcp, ws, old_wc) ) break; struct wincurpos next_old_wcp = predict_cursor(&old_cursor_predict, old_wcp, ws, old_wc); struct wincurpos next_new_wcp = predict_cursor(&new_cursor_predict, new_wcp, ws, new_wc); if ( old_wc != new_wc || old_wcp.wcp_row != new_wcp.wcp_row || old_wcp.wcp_col != new_wcp.wcp_col ) { // TODO: Use a reliable write instead! if ( old_wc == L'\n' && new_wc == L'\n' ) { // Good enough as newlines are invisible. } else if ( old_wc == L'\n' && new_wc != L'\0' ) { show_line_change_cursor(show_state, new_wcp); write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); show_state->wcp_current = next_new_wcp; old_num_bytes = 0; } else if ( old_wc != L'\0' && new_wc == '\n' ) { show_line_change_cursor(show_state, old_wcp); write(show_state->out_fd, " ", 1); show_state->wcp_current = next_old_wcp; new_num_bytes = 0; } else if ( old_wc == L'\n' && new_wc == L'\0' ) { // No need to do anything here as newlines are visible. } else if ( old_wc == L'\0' && new_wc == L'\n' ) { show_line_change_cursor(show_state, new_wcp); write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); show_state->wcp_current = next_new_wcp; } else if ( old_wcp.wcp_row != new_wcp.wcp_row || old_wcp.wcp_col != new_wcp.wcp_col ) return false; else if ( new_wc == L'\0' && old_wc != L'\0' ) { show_line_change_cursor(show_state, old_wcp); write(show_state->out_fd, " ", 1); show_state->wcp_current = next_old_wcp; } else if ( new_wc != L'\0' ) { show_line_change_cursor(show_state, new_wcp); write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); show_state->wcp_current = next_new_wcp; } } if ( will_scroll && can_scroll ) { cursor_wcp.wcp_row--; next_old_wcp.wcp_row--; show_state->wcp_start.wcp_row--; } old_wcp = next_old_wcp; new_wcp = next_new_wcp; old_line_offset += old_num_bytes; new_line_offset += new_num_bytes; } show_line_change_cursor(show_state, cursor_wcp); if ( show_state->current_line != line ) { free(show_state->current_line); show_state->current_line = strdup(line); assert(show_state->current_line); } show_state->current_cursor = cursor; return true; } void show_line(struct show_line* show_state, const char* line, size_t cursor) { if ( show_state->wcp_pending ) return; // TODO: We don't currently invalidate on SIGWINCH. struct winsize ws; tcgetwinsize(show_state->out_fd, &ws); if ( ws.ws_col != show_state->ws.ws_col || ws.ws_row != show_state->ws.ws_row ) { // TODO: What if wcp_start isn't inside the window any longer? show_state->invalidated = true; show_state->ws = ws; } // Attempt to do an optimized line re-rendering reusing the characters // already present on the console. Bail out if this turns out to be harder // than expected and re-render everything from scratch instead. if ( !show_state->invalidated && show_state->current_line && !show_line_is_weird(show_state->current_line) && !show_line_is_weird(line) ) { if ( show_line_optimized(show_state, line, cursor) ) return; show_state->invalidated = true; } show_line_change_cursor(show_state, show_state->wcp_start); dprintf(show_state->out_fd, "\e[m"); if ( show_state->invalidated || show_state->current_line ) dprintf(show_state->out_fd, "\e[0J"); struct cursor_predict cursor_predict; memset(&cursor_predict, 0, sizeof(cursor_predict)); struct wincurpos wcp = show_state->wcp_start; struct wincurpos cursor_wcp = wcp; mbstate_t ps; memset(&ps, 0, sizeof(ps)); for ( size_t i = 0; true; ) { if ( cursor == i ) cursor_wcp = wcp; wchar_t wc; size_t num_bytes = mbrtowc(&wc, line + i, SIZE_MAX, &ps); assert(num_bytes != (size_t) -2); assert(num_bytes != (size_t) -1); if ( num_bytes == 0 ) break; bool will_scroll = predict_will_scroll(cursor_predict, wcp, ws, wc); bool can_scroll = show_state->wcp_start.wcp_row != 0; if ( will_scroll && !can_scroll ) { if ( i < cursor ) cursor_wcp = wcp; break; } // TODO: Use a reliable write. write(show_state->out_fd, line + i, num_bytes); if ( will_scroll && can_scroll ) { cursor_wcp.wcp_row--; show_state->wcp_start.wcp_row--; } wcp = predict_cursor(&cursor_predict, wcp, ws, wc); i += num_bytes; } dprintf(show_state->out_fd, "\e[%zu;%zuH", cursor_wcp.wcp_row + 1, cursor_wcp.wcp_col + 1); show_state->wcp_current = wcp; if ( show_state->current_line != line ) { free(show_state->current_line); show_state->current_line = strdup(line); assert(show_state->current_line); } show_state->current_cursor = cursor; show_state->invalidated = false; } void show_line_end(struct show_line* show_state, const char* line) { size_t cursor = line ? strlen(line) : 0; show_line(show_state, line, cursor); } void show_line_clear(struct show_line* show_state) { dprintf(show_state->out_fd, "\e[H\e[2J"); show_state->wcp_start.wcp_row = 0; show_state->wcp_start.wcp_col = 0; show_state->invalidated = true; show_line_end(show_state, show_state->current_line); } void show_line_abort(struct show_line* show_state) { free(show_state->current_line); show_state->current_line = NULL; show_state->current_cursor = 0; } void show_line_finish(struct show_line* show_state) { show_line_end(show_state, show_state->current_line); dprintf(show_state->out_fd, "\n"); show_line_abort(show_state); }