#include #include #include #include #include #include #include #include #include #include #include #include #include #include /* hack: we can't define _XOPEN_SOURCE because that causes OpenBSD to not * include SIGWINCH. But then this prototype is not included on Linux, * triggering a warning. */ extern int wcwidth (wchar_t); #include "termbox.h" #include "bytebuffer.inl" #include "output.inl" #include "input.inl" #define IS_CURSOR_HIDDEN(cx, cy) (cx == -1 || cy == -1) #define LAST_COORD_INIT -1 static struct termios orig_tios; static struct bytebuffer output_buffer; static struct bytebuffer input_buffer; static int termw = -1; static int termh = -1; static int inout; static int winch_fds[2]; static int lastx = LAST_COORD_INIT; static int lasty = LAST_COORD_INIT; static int cursor_x = -1; static int cursor_y = -1; static uint16_t background = TB_BLACK; static uint16_t foreground = TB_WHITE; static void write_cursor(int x, int y); static void write_sgr(uint16_t fg, uint16_t bg); static void update_size(void); static void update_term_size(void); static void send_attr(uint16_t fg, uint16_t bg); static void send_char(int x, int y, uint32_t c); static void send_clear(void); static void sigwinch_handler(int xxx); static int wait_fill_event(struct tb_event *event, struct timeval *timeout); /* may happen in a different thread */ static volatile int buffer_size_change_request; /* -------------------------------------------------------- */ int tb_init(void) { inout = open("/dev/tty", O_RDWR); if (inout == -1) { return TB_EFAILED_TO_OPEN_TTY; } if (init_term() < 0) { close(inout); return TB_EUNSUPPORTED_TERMINAL; } if (pipe(winch_fds) < 0) { close(inout); return TB_EPIPE_TRAP_ERROR; } struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = sigwinch_handler; sa.sa_flags = 0; sigaction(SIGWINCH, &sa, 0); tcgetattr(inout, &orig_tios); struct termios tios; memcpy(&tios, &orig_tios, sizeof(tios)); tios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); tios.c_oflag &= ~OPOST; tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); tios.c_cflag &= ~(CSIZE | PARENB); tios.c_cflag |= CS8; tios.c_cc[VMIN] = 0; tios.c_cc[VTIME] = 0; tcsetattr(inout, TCSAFLUSH, &tios); bytebuffer_init(&input_buffer, 128); bytebuffer_init(&output_buffer, 32 * 1024); bytebuffer_puts(&output_buffer, funcs[T_ENTER_CA]); bytebuffer_puts(&output_buffer, funcs[T_ENTER_KEYPAD]); bytebuffer_puts(&output_buffer, funcs[T_HIDE_CURSOR]); bytebuffer_puts(&output_buffer, funcs[T_ENTER_MOUSE]); bytebuffer_puts(&output_buffer, funcs[T_ENTER_BRACKETED_PASTE]); update_term_size(); return 0; } void tb_shutdown(void) { if (termw == -1) return; bytebuffer_puts(&output_buffer, funcs[T_SHOW_CURSOR]); bytebuffer_puts(&output_buffer, funcs[T_SGR0]); bytebuffer_puts(&output_buffer, funcs[T_CLEAR_SCREEN]); bytebuffer_puts(&output_buffer, funcs[T_EXIT_CA]); bytebuffer_puts(&output_buffer, funcs[T_EXIT_KEYPAD]); bytebuffer_puts(&output_buffer, funcs[T_EXIT_MOUSE]); bytebuffer_puts(&output_buffer, funcs[T_EXIT_BRACKETED_PASTE]); bytebuffer_flush(&output_buffer, inout); tcsetattr(inout, TCSAFLUSH, &orig_tios); shutdown_term(); close(inout); close(winch_fds[0]); close(winch_fds[1]); bytebuffer_free(&output_buffer); bytebuffer_free(&input_buffer); termw = termh = -1; } int tb_is_active(void) { return termw != -1; } void tb_set_cursor(int cx, int cy) { assert(termw != -1); if (IS_CURSOR_HIDDEN(cursor_x, cursor_y) && !IS_CURSOR_HIDDEN(cx, cy)) bytebuffer_puts(&output_buffer, funcs[T_SHOW_CURSOR]); if (!IS_CURSOR_HIDDEN(cursor_x, cursor_y) && IS_CURSOR_HIDDEN(cx, cy)) bytebuffer_puts(&output_buffer, funcs[T_HIDE_CURSOR]); cursor_x = cx; cursor_y = cy; if (!IS_CURSOR_HIDDEN(cursor_x, cursor_y)) write_cursor(cursor_x, cursor_y); bytebuffer_flush(&output_buffer, inout); } void tb_change_cell(int x, int y, uint32_t ch, uint16_t fg, uint16_t bg) { assert(termw != -1); send_attr(fg, bg); send_char(x, y, ch); bytebuffer_flush(&output_buffer, inout); } int tb_poll_event(struct tb_event *event) { assert(termw != -1); return wait_fill_event(event, 0); } int tb_peek_event(struct tb_event *event, int timeout) { struct timeval tv; tv.tv_sec = timeout / 1000; tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000; assert(termw != -1); return wait_fill_event(event, &tv); } int tb_width(void) { assert(termw != -1); return termw; } int tb_height(void) { assert(termw != -1); return termh; } void tb_clear(void) { assert(termw != -1); if (buffer_size_change_request) { update_size(); buffer_size_change_request = 0; } send_clear(); } void tb_set_clear_attributes(uint16_t fg, uint16_t bg) { assert(termw != -1); foreground = fg; background = bg; } /* -------------------------------------------------------- */ static int convertnum(uint32_t num, char* buf) { int i, l = 0; int ch; do { buf[l++] = '0' + (num % 10); num /= 10; } while (num); for(i = 0; i < l / 2; i++) { ch = buf[i]; buf[i] = buf[l - 1 - i]; buf[l - 1 - i] = ch; } return l; } #define WRITE_LITERAL(X) bytebuffer_append(&output_buffer, (X), sizeof(X)-1) #define WRITE_INT(X) bytebuffer_append(&output_buffer, buf, convertnum((X), buf)) static void write_cursor(int x, int y) { char buf[32]; WRITE_LITERAL("\033["); WRITE_INT(y+1); WRITE_LITERAL(";"); WRITE_INT(x+1); WRITE_LITERAL("H"); } static void write_sgr(uint16_t fg, uint16_t bg) { char buf[32]; WRITE_LITERAL("\033[38;5;"); WRITE_INT(fg); WRITE_LITERAL("m"); WRITE_LITERAL("\033[48;5;"); WRITE_INT(bg); WRITE_LITERAL("m"); } static void get_term_size(int *w, int *h) { struct winsize sz; memset(&sz, 0, sizeof(sz)); ioctl(inout, TIOCGWINSZ, &sz); if (w) *w = sz.ws_col; if (h) *h = sz.ws_row; } static void update_term_size(void) { struct winsize sz; memset(&sz, 0, sizeof(sz)); ioctl(inout, TIOCGWINSZ, &sz); termw = sz.ws_col; termh = sz.ws_row; } static void send_attr(uint16_t fg, uint16_t bg) { #define LAST_ATTR_INIT 0xFFFF static uint16_t lastfg = LAST_ATTR_INIT, lastbg = LAST_ATTR_INIT; if (fg != lastfg || bg != lastbg) { bytebuffer_puts(&output_buffer, funcs[T_SGR0]); uint16_t fgcol = fg & 0xFF; uint16_t bgcol = bg & 0xFF; if (fg & TB_BOLD) bytebuffer_puts(&output_buffer, funcs[T_BOLD]); if (bg & TB_BOLD) bytebuffer_puts(&output_buffer, funcs[T_BLINK]); if (fg & TB_UNDERLINE) bytebuffer_puts(&output_buffer, funcs[T_UNDERLINE]); if ((fg & TB_REVERSE) || (bg & TB_REVERSE)) bytebuffer_puts(&output_buffer, funcs[T_REVERSE]); write_sgr(fgcol, bgcol); lastfg = fg; lastbg = bg; } } static void send_char(int x, int y, uint32_t c) { char buf[7]; int bw = tb_utf8_unicode_to_char(buf, c); buf[bw] = '\0'; if (x-1 != lastx || y != lasty) write_cursor(x, y); lastx = x; lasty = y; if(!c) buf[0] = ' '; // replace 0 with whitespace bytebuffer_puts(&output_buffer, buf); } const char* to_unicode(uint32_t c) { static char buf[7]; int bw = tb_utf8_unicode_to_char(buf, c); buf[bw] = '\0'; return buf; } static void send_clear(void) { send_attr(foreground, background); bytebuffer_puts(&output_buffer, funcs[T_CLEAR_SCREEN]); if (!IS_CURSOR_HIDDEN(cursor_x, cursor_y)) write_cursor(cursor_x, cursor_y); bytebuffer_flush(&output_buffer, inout); /* we need to invalidate cursor position too and these two vars are * used only for simple cursor positioning optimization, cursor * actually may be in the correct place, but we simply discard * optimization once and it gives us simple solution for the case when * cursor moved */ lastx = LAST_COORD_INIT; lasty = LAST_COORD_INIT; } static void sigwinch_handler(int xxx) { (void) xxx; const int zzz = 1; int yyy = write(winch_fds[1], &zzz, sizeof(int)); (void) yyy; } static void update_size(void) { update_term_size(); send_clear(); } static int read_up_to(int n) { assert(n > 0); const int prevlen = input_buffer.len; bytebuffer_resize(&input_buffer, prevlen + n); int read_n = 0; while (read_n <= n) { ssize_t r = 0; if (read_n < n) { r = read(inout, input_buffer.buf + prevlen + read_n, n - read_n); } #ifdef __CYGWIN__ // While linux man for tty says when VMIN == 0 && VTIME == 0, read // should return 0 when there is nothing to read, cygwin's read returns // -1. Not sure why and if it's correct to ignore it, but let's pretend // it's zero. if (r < 0) r = 0; #endif if (r < 0) { // EAGAIN / EWOULDBLOCK shouldn't occur here assert(errno != EAGAIN && errno != EWOULDBLOCK); return -1; } else if (r > 0) { read_n += r; } else { bytebuffer_resize(&input_buffer, prevlen + read_n); return read_n; } } assert(!"unreachable"); return 0; } int tb_event_ready(void) { return input_buffer.len > 0; } static int wait_fill_event(struct tb_event *event, struct timeval *timeout) { // ;-) #define ENOUGH_DATA_FOR_PARSING 64 fd_set events; memset(event, 0, sizeof(struct tb_event)); // try to extract event from input buffer, return on success event->type = TB_EVENT_KEY; if (extract_event(event, &input_buffer)) return event->type; // it looks like input buffer is incomplete, let's try the short path, // but first make sure there is enough space int n = read_up_to(ENOUGH_DATA_FOR_PARSING); if (n < 0) return -1; if (n > 0 && extract_event(event, &input_buffer)) return event->type; // n == 0, or not enough data, let's go to select while (1) { FD_ZERO(&events); FD_SET(inout, &events); FD_SET(winch_fds[0], &events); int maxfd = (winch_fds[0] > inout) ? winch_fds[0] : inout; int result = select(maxfd+1, &events, 0, 0, timeout); if (!result) return 0; if (FD_ISSET(inout, &events)) { event->type = TB_EVENT_KEY; n = read_up_to(ENOUGH_DATA_FOR_PARSING); if (n < 0) return -1; if (n == 0) continue; if (extract_event(event, &input_buffer)) return event->type; } if (FD_ISSET(winch_fds[0], &events)) { event->type = TB_EVENT_RESIZE; int zzz = 0; int yyy = read(winch_fds[0], &zzz, sizeof(int)); (void) yyy; buffer_size_change_request = 1; get_term_size(&event->w, &event->h); return TB_EVENT_RESIZE; } } }