From 9a2dd7c4987679106fba24fb7702a11aba6b6ccf Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Fri, 13 Feb 2015 12:06:05 -0300 Subject: [PATCH] ui: Rewrite the builtin terminal UI Now all terminal-handling code was moved to src/nvim/tui, which implements a new terminal UI based on libtermkey and unibilium --- CMakeLists.txt | 8 + src/nvim/CMakeLists.txt | 11 +- src/nvim/main.c | 5 + src/nvim/os_unix.c | 2 + src/nvim/tui/term_input.inl | 246 ++++++++++++ src/nvim/tui/tui.c | 752 ++++++++++++++++++++++++++++++++++++ src/nvim/tui/tui.h | 8 + src/nvim/ui.c | 15 +- src/nvim/ui.h | 1 + 9 files changed, 1044 insertions(+), 4 deletions(-) create mode 100644 src/nvim/tui/term_input.inl create mode 100644 src/nvim/tui/tui.c create mode 100644 src/nvim/tui/tui.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 42eb50ac43..111e8b76d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,6 +182,14 @@ include_directories(SYSTEM ${MSGPACK_INCLUDE_DIRS}) find_package(LuaJit REQUIRED) include_directories(SYSTEM ${LUAJIT_INCLUDE_DIRS}) +set(LIBUNIBILIUM_USE_STATIC ON) +find_package(LibUnibilium REQUIRED) +include_directories(SYSTEM ${LIBUNIBILIUM_INCLUDE_DIRS}) + +set(LIBTERMKEY_USE_STATIC ON) +find_package(LibTermkey REQUIRED) +include_directories(SYSTEM ${LIBTERMEY_INCLUDE_DIRS}) + find_package(LibIntl) if(LibIntl_FOUND) include_directories(SYSTEM ${LibIntl_INCLUDE_DIRS}) diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 5bff744dd5..922b8b85a1 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -26,13 +26,16 @@ file(MAKE_DIRECTORY ${GENERATED_DIR}/os) file(MAKE_DIRECTORY ${GENERATED_DIR}/api) file(MAKE_DIRECTORY ${GENERATED_DIR}/api/private) file(MAKE_DIRECTORY ${GENERATED_DIR}/msgpack_rpc) +file(MAKE_DIRECTORY ${GENERATED_DIR}/tui) file(MAKE_DIRECTORY ${GENERATED_INCLUDES_DIR}) file(MAKE_DIRECTORY ${GENERATED_INCLUDES_DIR}/os) file(MAKE_DIRECTORY ${GENERATED_INCLUDES_DIR}/api) file(MAKE_DIRECTORY ${GENERATED_INCLUDES_DIR}/api/private) file(MAKE_DIRECTORY ${GENERATED_INCLUDES_DIR}/msgpack_rpc) +file(MAKE_DIRECTORY ${GENERATED_INCLUDES_DIR}/tui) -file(GLOB NEOVIM_SOURCES *.c os/*.c api/*.c api/private/*.c msgpack_rpc/*.c) +file(GLOB NEOVIM_SOURCES *.c os/*.c api/*.c api/private/*.c msgpack_rpc/*.c + tui/*.c) file(GLOB_RECURSE NEOVIM_HEADERS *.h) foreach(sfile ${NEOVIM_SOURCES}) @@ -173,8 +176,12 @@ list(APPEND NVIM_LINK_LIBRARIES ${LIBUV_LIBRARIES} ${MSGPACK_LIBRARIES} ${LUAJIT_LIBRARIES} + ${LIBTICKIT_LIBRARIES} + ${LIBTERMKEY_LIBRARIES} + ${LIBUNIBILIUM_LIBRARIES} m - ${CMAKE_THREAD_LIBS_INIT}) + ${CMAKE_THREAD_LIBS_INIT} + ) add_executable(nvim ${NEOVIM_GENERATED_SOURCES} ${NEOVIM_SOURCES} ${NEOVIM_HEADERS}) diff --git a/src/nvim/main.c b/src/nvim/main.c index 65672370a3..f2891f0979 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -383,6 +383,8 @@ int main(int argc, char **argv) TIME_MSG("waiting for return"); input_stop_stdin(); } + + ui_builtin_start(); } setmouse(); // may start using the mouse @@ -984,6 +986,8 @@ static void command_line_scan(mparm_T *parmp) } mch_exit(0); + } else if (STRICMP(argv[0] + argv_idx, "headless") == 0) { + parmp->headless = true; } else if (STRICMP(argv[0] + argv_idx, "embed") == 0) { embedded_mode = true; parmp->headless = true; @@ -2092,6 +2096,7 @@ static void usage(void) mch_msg(_(" -i Use instead of .nviminfo\n")); mch_msg(_(" --api-info Dump API metadata serialized to msgpack and exit\n")); mch_msg(_(" --embed Use stdin/stdout as a msgpack-rpc channel\n")); + mch_msg(_(" --headless Don't start a user interface\n")); mch_msg(_(" --version Print version information and exit\n")); mch_msg(_(" -h | --help Print this help message and exit\n")); diff --git a/src/nvim/os_unix.c b/src/nvim/os_unix.c index 094d3fa1ba..3bf67c2290 100644 --- a/src/nvim/os_unix.c +++ b/src/nvim/os_unix.c @@ -409,6 +409,8 @@ void mch_exit(int r) if (swapping_screen() && !newline_on_exit) exit_scroll(); + ui_builtin_stop(); + /* * A newline is only required after a message in the alternate screen. * This is set to TRUE by wait_return(). diff --git a/src/nvim/tui/term_input.inl b/src/nvim/tui/term_input.inl new file mode 100644 index 0000000000..0c0e6c07c9 --- /dev/null +++ b/src/nvim/tui/term_input.inl @@ -0,0 +1,246 @@ +#include + +#include "nvim/ascii.h" +#include "nvim/os/os.h" +#include "nvim/os/input.h" +#include "nvim/os/rstream.h" + + +struct term_input { + int in_fd; + TermKey *tk; + uv_tty_t input_handle; + uv_timer_t timer_handle; + RBuffer *read_buffer; + RStream *read_stream; +}; + +static void forward_simple_utf8(TermKeyKey *key) +{ + size_t len = 0; + char buf[64]; + char *ptr = key->utf8; + + while (*ptr) { + if (*ptr == '<') { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, ""); + } else { + buf[len++] = *ptr; + } + ptr++; + } + + buf[len] = 0; + input_enqueue((String){.data = buf, .size = len}); +} + +static void forward_modified_utf8(TermKey *tk, TermKeyKey *key) +{ + size_t len; + char buf[64]; + + if (key->type == TERMKEY_TYPE_KEYSYM + && key->code.sym == TERMKEY_SYM_ESCAPE) { + len = (size_t)snprintf(buf, sizeof(buf), ""); + } else { + len = termkey_strfkey(tk, buf, sizeof(buf), key, TERMKEY_FORMAT_VIM); + } + + input_enqueue((String){.data = buf, .size = len}); +} + +static void forward_mouse_event(TermKey *tk, TermKeyKey *key) +{ + char buf[64]; + size_t len = 0; + int button, row, col; + TermKeyMouseEvent ev; + termkey_interpret_mouse(tk, key, &ev, &button, &row, &col); + + if (ev != TERMKEY_MOUSE_PRESS && ev != TERMKEY_MOUSE_DRAG) { + return; + } + + row--; col--; // Termkey uses 1-based coordinates + buf[len++] = '<'; + + if (key->modifiers & TERMKEY_KEYMOD_SHIFT) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "S-"); + } + + if (key->modifiers & TERMKEY_KEYMOD_CTRL) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "C-"); + } + + if (key->modifiers & TERMKEY_KEYMOD_ALT) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "A-"); + } + + if (button == 1) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Left"); + } else if (button == 2) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Middle"); + } else if (button == 3) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Right"); + } + + if (ev == TERMKEY_MOUSE_PRESS) { + if (button == 4) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelUp"); + } else if (button == 5) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelDown"); + } else { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Mouse"); + } + } else if (ev == TERMKEY_MOUSE_DRAG) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Drag"); + } + + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "><%d,%d>", col, row); + input_enqueue((String){.data = buf, .size = len}); +} + +static TermKeyResult tk_getkey(TermKey *tk, TermKeyKey *key, bool force) +{ + return force ? termkey_getkey_force(tk, key) : termkey_getkey(tk, key); +} + +static void timer_cb(uv_timer_t *handle); + +static int get_key_code_timeout(void) +{ + Integer ms = 0; + bool timeout = false; + // Check 'timeout' and 'ttimeout' to determine if we should send ESC + // after 'ttimeoutlen'. See :help 'ttimeout' for more information + Error err; + timeout = vim_get_option(cstr_as_string("timeout"), &err).data.boolean; + if (!timeout) { + timeout = vim_get_option(cstr_as_string("ttimeout"), &err).data.boolean; + } + + if (timeout) { + ms = vim_get_option(cstr_as_string("ttimeoutlen"), &err).data.integer; + } + + return (int)ms; +} + +static void tk_getkeys(TermInput *input, bool force) +{ + TermKeyKey key; + TermKeyResult result; + + while ((result = tk_getkey(input->tk, &key, force)) == TERMKEY_RES_KEY) { + if (key.type == TERMKEY_TYPE_UNICODE && !key.modifiers) { + forward_simple_utf8(&key); + } else if (key.type == TERMKEY_TYPE_UNICODE || + key.type == TERMKEY_TYPE_FUNCTION || + key.type == TERMKEY_TYPE_KEYSYM) { + forward_modified_utf8(input->tk, &key); + } else if (key.type == TERMKEY_TYPE_MOUSE) { + forward_mouse_event(input->tk, &key); + } + } + + if (result != TERMKEY_RES_AGAIN) { + return; + } + + int ms = get_key_code_timeout(); + + if (ms > 0) { + // Stop the current timer if already running + uv_timer_stop(&input->timer_handle); + uv_timer_start(&input->timer_handle, timer_cb, (uint32_t)ms, 0); + } else { + tk_getkeys(input, true); + } +} + + +static void timer_cb(uv_timer_t *handle) +{ + tk_getkeys(handle->data, true); +} + +static void read_cb(RStream *rstream, void *rstream_data, bool eof) +{ + if (eof) { + input_done(); + return; + } + + TermInput *input = rstream_data; + + do { + char *ptr = rbuffer_read_ptr(input->read_buffer); + size_t len = rbuffer_pending(input->read_buffer); + if (len > 1 && ptr[0] == ESC && ptr[1] == NUL) { + // skip the ESC and NUL and push one to the input buffer + termkey_push_bytes(input->tk, ptr, 1); + rbuffer_consumed(input->read_buffer, 2); + tk_getkeys(input, true); + continue; + } + // Find the next 'esc' and push everything up to it(excluding) + size_t i; + for (i = ptr[0] == ESC ? 1 : 0; i < len; i++) { + if (ptr[i] == '\x1b') { + break; + } + } + size_t consumed = termkey_push_bytes(input->tk, ptr, i); + rbuffer_consumed(input->read_buffer, consumed); + tk_getkeys(input, false); + } while (rbuffer_pending(input->read_buffer)); +} + +static TermInput *term_input_new(void) +{ + TermInput *rv = xmalloc(sizeof(TermInput)); + // read input from stderr if stdin is not a tty + rv->in_fd = os_isatty(0) ? 0 : (os_isatty(2) ? 2 : 0); + + // Set terminal encoding based on environment(taken from libtermkey source + // code) + const char *e; + int flags = 0; + if (((e = os_getenv("LANG")) || (e = os_getenv("LC_MESSAGES")) + || (e = os_getenv("LC_ALL"))) && (e = strchr(e, '.')) && e++ && + (strcasecmp(e, "UTF-8") == 0 || strcasecmp(e, "UTF8") == 0)) { + flags |= TERMKEY_FLAG_UTF8; + } else { + flags |= TERMKEY_FLAG_RAW; + } + + rv->tk = termkey_new_abstract(os_getenv("TERM"), flags); + int curflags = termkey_get_canonflags(rv->tk); + termkey_set_canonflags(rv->tk, curflags | TERMKEY_CANON_DELBS); + // setup input handle + uv_tty_init(uv_default_loop(), &rv->input_handle, rv->in_fd, 1); + uv_tty_set_mode(&rv->input_handle, UV_TTY_MODE_RAW); + rv->input_handle.data = NULL; + rv->read_buffer = rbuffer_new(0xfff); + rv->read_stream = rstream_new(read_cb, rv->read_buffer, rv); + rstream_set_stream(rv->read_stream, (uv_stream_t *)&rv->input_handle); + rstream_start(rv->read_stream); + // initialize a timer handle for handling ESC with libtermkey + uv_timer_init(uv_default_loop(), &rv->timer_handle); + rv->timer_handle.data = rv; + return rv; +} + +static void term_input_destroy(TermInput *input) +{ + uv_tty_reset_mode(); + uv_timer_stop(&input->timer_handle); + rstream_stop(input->read_stream); + rstream_free(input->read_stream); + uv_close((uv_handle_t *)&input->input_handle, NULL); + uv_close((uv_handle_t *)&input->timer_handle, NULL); + termkey_destroy(input->tk); + event_poll(0); // Run once to remove references to input/timer handles + free(input->input_handle.data); + free(input); +} diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c new file mode 100644 index 0000000000..720970c424 --- /dev/null +++ b/src/nvim/tui/tui.c @@ -0,0 +1,752 @@ +#include +#include +#include + +#include +#include + +#include "nvim/lib/kvec.h" + +#include "nvim/vim.h" +#include "nvim/ui.h" +#include "nvim/map.h" +#include "nvim/memory.h" +#include "nvim/api/vim.h" +#include "nvim/api/private/helpers.h" +#include "nvim/os/event.h" +#include "nvim/tui/tui.h" + +typedef struct term_input TermInput; + +#include "term_input.inl" + +typedef struct { + int top, bot, left, right; +} Rect; + +typedef struct { + char data[7]; + HlAttrs attrs; +} Cell; + +typedef struct { + PMap(cstr_t) *option_cache; + unibi_var_t params[9]; + char buf[0xffff]; + size_t bufpos; + TermInput *input; + uv_loop_t *write_loop; + unibi_term *ut; + uv_tty_t output_handle; + uv_signal_t winch_handle; + Rect scroll_region; + kvec_t(Rect) invalid_regions; + int row, col; + int bg, fg; + int out_fd; + int old_height; + bool can_use_terminal_scroll; + HlAttrs attrs, print_attrs; + Cell **screen; + struct { + size_t enable_mouse, disable_mouse; + } unibi_ext; +} TUIData; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/tui.c.generated.h" +#endif + +#define EMPTY_ATTRS ((HlAttrs){false, false, false, false, false, -1, -1}) + +#define FOREACH_CELL(ui, top, bot, left, right, go, code) \ + do { \ + TUIData *data = ui->data; \ + for (int row = top; row <= bot; ++row) { \ + Cell *cells = data->screen[row]; \ + if (go) { \ + unibi_goto(ui, row, left); \ + } \ + for (int col = left; col <= right; ++col) { \ + Cell *cell = cells + col; \ + (void)(cell); \ + code; \ + } \ + } \ + } while (0) + + +void tui_start(void) +{ + TUIData *data = xcalloc(1, sizeof(TUIData)); + UI *ui = xcalloc(1, sizeof(UI)); + ui->data = data; + data->attrs = data->print_attrs = EMPTY_ATTRS; + data->fg = data->bg = -1; + data->can_use_terminal_scroll = true; + data->bufpos = 0; + data->option_cache = pmap_new(cstr_t)(); + + // write output to stderr if stdout is not a tty + data->out_fd = os_isatty(1) ? 1 : (os_isatty(2) ? 2 : 1); + kv_init(data->invalid_regions); + // setup term input + data->input = term_input_new(); + // setup unibilium + data->ut = unibi_from_env(); + if (!data->ut) { + // For some reason could not read terminfo file, use a dummy entry that + // will be populated with common values by fix_terminfo below + data->ut = unibi_dummy(); + } + fix_terminfo(data); + // Enter alternate screen and clear + unibi_out(ui, unibi_enter_ca_mode, NULL); + unibi_out(ui, unibi_clear_screen, NULL); + + // setup output handle in a separate event loop(we wanna do synchronous + // write to the tty) + data->write_loop = xmalloc(sizeof(uv_loop_t)); + uv_loop_init(data->write_loop); + uv_tty_init(data->write_loop, &data->output_handle, data->out_fd, 0); + + // Obtain screen dimensions + update_size(ui); + + // listen for SIGWINCH + uv_signal_init(uv_default_loop(), &data->winch_handle); + uv_signal_start(&data->winch_handle, sigwinch_cb, SIGWINCH); + data->winch_handle.data = ui; + + ui->stop = tui_stop; + ui->rgb = false; + ui->data = data; + ui->resize = tui_resize; + ui->clear = tui_clear; + ui->eol_clear = tui_eol_clear; + ui->cursor_goto = tui_cursor_goto; + ui->cursor_on = tui_cursor_on; + ui->cursor_off = tui_cursor_off; + ui->mouse_on = tui_mouse_on; + ui->mouse_off = tui_mouse_off; + ui->insert_mode = tui_insert_mode; + ui->normal_mode = tui_normal_mode; + ui->set_scroll_region = tui_set_scroll_region; + ui->scroll = tui_scroll; + ui->highlight_set = tui_highlight_set; + ui->put = tui_put; + ui->bell = tui_bell; + ui->visual_bell = tui_visual_bell; + ui->update_fg = tui_update_fg; + ui->update_bg = tui_update_bg; + ui->flush = tui_flush; + ui->suspend = tui_suspend; + ui->set_title = tui_set_title; + ui->set_icon = tui_set_icon; + // Attach + ui_attach(ui); +} + +static void tui_stop(UI *ui) +{ + TUIData *data = ui->data; + // Destroy common stuff + kv_destroy(data->invalid_regions); + uv_signal_stop(&data->winch_handle); + uv_close((uv_handle_t *)&data->winch_handle, NULL); + // Destroy input stuff + term_input_destroy(data->input); + // Destroy output stuff + tui_normal_mode(ui); + tui_mouse_off(ui); + unibi_out(ui, unibi_exit_attribute_mode, NULL); + unibi_out(ui, unibi_cursor_normal, NULL); + unibi_out(ui, unibi_exit_ca_mode, NULL); + flush_buf(ui); + uv_close((uv_handle_t *)&data->output_handle, NULL); + uv_run(data->write_loop, UV_RUN_DEFAULT); + if (uv_loop_close(data->write_loop)) { + abort(); + } + free(data->write_loop); + unibi_destroy(data->ut); + char *opt_value; + map_foreach_value(data->option_cache, opt_value, { + free(opt_value); + }); + pmap_free(cstr_t)(data->option_cache); + destroy_screen(data); + free(data); + free(ui); + ui_detach(ui); +} + +static void try_resize(Event ev) +{ + UI *ui = ev.data; + update_size(ui); + ui_refresh(); +} + +static void sigwinch_cb(uv_signal_t *handle, int signum) +{ + // Queue the event because resizing can result in recursive event_poll calls + event_push((Event) { + .data = handle->data, + .handler = try_resize + }, false); +} + +static bool attrs_differ(HlAttrs a1, HlAttrs a2) +{ + return a1.foreground != a2.foreground || a1.background != a2.background + || a1.bold != a2.bold || a1.italic != a2.italic + || a1.undercurl != a2.undercurl || a1.underline != a2.underline + || a1.reverse != a2.reverse; +} + +static void update_attrs(UI *ui, HlAttrs attrs) +{ + TUIData *data = ui->data; + unibi_out(ui, unibi_exit_attribute_mode, NULL); + + data->params[0].i = attrs.foreground != -1 ? attrs.foreground : data->fg; + if (data->params[0].i != -1) { + unibi_out(ui, unibi_set_a_foreground, NULL); + } + + data->params[0].i = attrs.background != -1 ? attrs.background : data->bg; + if (data->params[0].i != -1) { + unibi_out(ui, unibi_set_a_background, NULL); + } + + if (attrs.bold) { + unibi_out(ui, unibi_enter_bold_mode, NULL); + } + if (attrs.italic) { + unibi_out(ui, unibi_enter_italics_mode, NULL); + } + if (attrs.underline) { + unibi_out(ui, unibi_enter_underline_mode, NULL); + } + if (attrs.reverse) { + unibi_out(ui, unibi_enter_reverse_mode, NULL); + } +} + +static void print_cell(UI *ui, Cell *ptr) +{ + TUIData *data = ui->data; + if (attrs_differ(ptr->attrs, data->print_attrs)) { + update_attrs(ui, ptr->attrs); + data->print_attrs = ptr->attrs; + } + out(ui, ptr->data); +} + +static void clear_region(UI *ui, int top, int bot, int left, int right, + bool refresh) +{ + TUIData *data = ui->data; + HlAttrs clear_attrs = EMPTY_ATTRS; + clear_attrs.foreground = data->fg; + clear_attrs.background = data->bg; + + bool cleared = false; + if (refresh && data->bg == -1 && right == ui->width -1) { + // Background is set to the default color and the right edge matches the + // screen end, try to use terminal codes for clearing the requested area. + if (left == 0) { + if (bot == ui->height - 1) { + if (top == 0) { + unibi_out(ui, unibi_clear_screen, NULL); + } else { + unibi_goto(ui, top, 0); + unibi_out(ui, unibi_clr_eos, NULL); + } + cleared = true; + } + } + + if (!cleared) { + // iterate through each line and clear with clr_eol + for (int row = top; row <= bot; ++row) { + unibi_goto(ui, row, left); + unibi_out(ui, unibi_clr_eol, NULL); + } + cleared = true; + } + } + + bool clear = refresh && !cleared; + FOREACH_CELL(ui, top, bot, left, right, clear, { + cell->data[0] = ' '; + cell->data[1] = 0; + cell->attrs = clear_attrs; + if (clear) { + print_cell(ui, cell); + } + }); + + // restore cursor + unibi_goto(ui, data->row, data->col); +} + +static void tui_resize(UI *ui, int width, int height) +{ + TUIData *data = ui->data; + destroy_screen(data); + + data->screen = xmalloc((size_t)height * sizeof(Cell *)); + for (int i = 0; i < height; i++) { + data->screen[i] = xcalloc((size_t)width, sizeof(Cell)); + } + + data->old_height = height; + data->scroll_region.top = 0; + data->scroll_region.bot = height - 1; + data->scroll_region.left = 0; + data->scroll_region.right = width - 1; + data->row = data->col = 0; +} + +static void tui_clear(UI *ui) +{ + TUIData *data = ui->data; + clear_region(ui, data->scroll_region.top, data->scroll_region.bot, + data->scroll_region.left, data->scroll_region.right, true); +} + +static void tui_eol_clear(UI *ui) +{ + TUIData *data = ui->data; + clear_region(ui, data->row, data->row, data->col, + data->scroll_region.right, true); +} + +static void tui_cursor_goto(UI *ui, int row, int col) +{ + TUIData *data = ui->data; + data->row = row; + data->col = col; + unibi_goto(ui, row, col); +} + +static void tui_cursor_on(UI *ui) +{ + unibi_out(ui, unibi_cursor_normal, NULL); +} + +static void tui_cursor_off(UI *ui) +{ + unibi_out(ui, unibi_cursor_invisible, NULL); +} + +static void tui_mouse_on(UI *ui) +{ + TUIData *data = ui->data; + unibi_out(ui, (int)data->unibi_ext.enable_mouse, NULL); +} + +static void tui_mouse_off(UI *ui) +{ + TUIData *data = ui->data; + unibi_out(ui, (int)data->unibi_ext.disable_mouse, NULL); +} + +static void tui_insert_mode(UI *ui) +{ + unibi_out(ui, -1, "t_SI"); +} + +static void tui_normal_mode(UI *ui) +{ + unibi_out(ui, -1, "t_EI"); +} + +static void tui_set_scroll_region(UI *ui, int top, int bot, int left, + int right) +{ + TUIData *data = ui->data; + data->scroll_region.top = top; + data->scroll_region.bot = bot; + data->scroll_region.left = left; + data->scroll_region.right = right; + + data->can_use_terminal_scroll = + left == 0 && right == ui->width - 1 + && ((top == 0 && bot == ui->height - 1) + || unibi_get_str(data->ut, unibi_change_scroll_region)); +} + +static void tui_scroll(UI *ui, int count) +{ + TUIData *data = ui->data; + int top = data->scroll_region.top; + int bot = data->scroll_region.bot; + int left = data->scroll_region.left; + int right = data->scroll_region.right; + + if (data->can_use_terminal_scroll) { + // Change terminal scroll region and move cursor to the top + data->params[0].i = top; + data->params[1].i = bot; + unibi_out(ui, unibi_change_scroll_region, NULL); + unibi_goto(ui, top, left); + } + + // Compute start/stop/step for the loop below, also use terminal scroll + // if possible + int start, stop, step; + if (count > 0) { + start = top; + stop = bot - count + 1; + step = 1; + if (data->can_use_terminal_scroll) { + if (count == 1) { + unibi_out(ui, unibi_delete_line, NULL); + } else { + data->params[0].i = count; + unibi_out(ui, unibi_parm_delete_line, NULL); + } + } + + } else { + start = bot; + stop = top - count - 1; + step = -1; + if (data->can_use_terminal_scroll) { + if (count == -1) { + unibi_out(ui, unibi_insert_line, NULL); + } else { + data->params[0].i = -count; + unibi_out(ui, unibi_parm_insert_line, NULL); + } + } + } + + if (data->can_use_terminal_scroll) { + // Restore terminal scroll region and cursor + data->params[0].i = 0; + data->params[1].i = ui->height - 1; + unibi_out(ui, unibi_change_scroll_region, NULL); + unibi_goto(ui, data->row, data->col); + } + + int i; + // Scroll internal screen + for (i = start; i != stop; i += step) { + Cell *target_row = data->screen[i] + left; + Cell *source_row = data->screen[i + count] + left; + memcpy(target_row, source_row, sizeof(Cell) * (size_t)(right - left + 1)); + } + + // clear emptied region, updating the terminal if its builtin scrolling + // facility was used. This is done when the background color is not the + // default, since scrolling may leave wrong background in the cleared area. + bool update_clear = data->bg != -1 && data->can_use_terminal_scroll; + if (count > 0) { + clear_region(ui, stop, stop + count - 1, left, right, update_clear); + } else { + clear_region(ui, stop + count + 1, stop, left, right, update_clear); + } + + if (!data->can_use_terminal_scroll) { + // Mark the entire scroll region as invalid for redrawing later + invalidate(ui, data->scroll_region.top, data->scroll_region.bot, + data->scroll_region.left, data->scroll_region.right); + } +} + +static void tui_highlight_set(UI *ui, HlAttrs attrs) +{ + ((TUIData *)ui->data)->attrs = attrs; +} + +static void tui_put(UI *ui, uint8_t *text, size_t size) +{ + TUIData *data = ui->data; + Cell *cell = data->screen[data->row] + data->col; + cell->data[size] = 0; + cell->attrs = data->attrs; + + if (text) { + memcpy(cell->data, text, size); + } + + print_cell(ui, cell); + data->col += 1; +} + +static void tui_bell(UI *ui) +{ + unibi_out(ui, unibi_bell, NULL); +} + +static void tui_visual_bell(UI *ui) +{ + unibi_out(ui, unibi_flash_screen, NULL); +} + +static void tui_update_fg(UI *ui, int fg) +{ + ((TUIData *)ui->data)->fg = fg; +} + +static void tui_update_bg(UI *ui, int bg) +{ + ((TUIData *)ui->data)->bg = bg; +} + +static void tui_flush(UI *ui) +{ + TUIData *data = ui->data; + + while (kv_size(data->invalid_regions)) { + Rect r = kv_pop(data->invalid_regions); + FOREACH_CELL(ui, r.top, r.bot, r.left, r.right, true, { + print_cell(ui, cell); + }); + } + + unibi_goto(ui, data->row, data->col); + flush_buf(ui); +} + +static void tui_suspend(UI *ui) +{ + tui_stop(ui); + kill(0, SIGTSTP); + tui_start(); +} + +static void tui_set_title(UI *ui, char *title) +{ + TUIData *data = ui->data; + if (!(unibi_get_str(data->ut, unibi_to_status_line) + && unibi_get_str(data->ut, unibi_from_status_line))) { + return; + } + unibi_out(ui, unibi_to_status_line, NULL); + out(ui, title); + unibi_out(ui, unibi_from_status_line, NULL); +} + +static void tui_set_icon(UI *ui, char *icon) +{ +} + +static void invalidate(UI *ui, int top, int bot, int left, int right) +{ + TUIData *data = ui->data; + Rect *intersects = NULL; + // Increase dimensions before comparing to ensure adjacent regions are + // treated as intersecting + --top; + ++bot; + --left; + ++right; + + for (size_t i = 0; i < kv_size(data->invalid_regions); i++) { + Rect *r = &kv_A(data->invalid_regions, i); + if (!(top > r->bot || bot < r->top + || left > r->right || right < r->left)) { + intersects = r; + break; + } + } + + ++top; + --bot; + ++left; + --right; + + if (intersects) { + // If top/bot/left/right intersects with a invalid rect, we replace it + // by the union + intersects->top = MIN(top, intersects->top); + intersects->bot = MAX(bot, intersects->bot); + intersects->left = MIN(left, intersects->left); + intersects->right = MAX(right, intersects->right); + } else { + // Else just add a new entry; + kv_push(Rect, data->invalid_regions, ((Rect){top, bot, left, right})); + } +} + +static void update_size(UI *ui) +{ + TUIData *data = ui->data; + int width = 0, height = 0; + // 1 - try from a system call(ioctl/TIOCGWINSZ on unix) + if (!uv_tty_get_winsize(&data->output_handle, &width, &height)) { + goto end; + } + + // 2 - use $LINES/$COLUMNS if available + const char *val; + int advance; + if ((val = os_getenv("LINES")) + && sscanf(val, "%d%n", &height, &advance) != EOF && advance + && (val = os_getenv("COLUMNS")) + && sscanf(val, "%d%n", &width, &advance) != EOF && advance) { + goto end; + } + + // 3- read from terminfo if available + height = unibi_get_num(data->ut, unibi_lines); + width = unibi_get_num(data->ut, unibi_columns); + +end: + if (width <= 0 || height <= 0) { + // use a default of 80x24 + width = 80; + height = 24; + } + + ui->width = width; + ui->height = height; +} + +static void unibi_goto(UI *ui, int row, int col) +{ + TUIData *data = ui->data; + data->params[0].i = row; + data->params[1].i = col; + unibi_out(ui, unibi_cursor_address, NULL); +} + +static void unibi_out(UI *ui, int unibi_index, char *nvim_override) +{ + TUIData *data = ui->data; + + const char *str = NULL; + + if (nvim_override) { + str = get_term_option(ui, nvim_override); + } else if (unibi_index >= 0) { + if (unibi_index < unibi_string_begin_) { + str = unibi_get_ext_str(data->ut, (unsigned)unibi_index); + } else { + str = unibi_get_str(data->ut, (unsigned)unibi_index); + } + } + + if (str) { + data->bufpos += unibi_run(str, data->params, data->buf + data->bufpos, + sizeof(data->buf) - data->bufpos); + } +} + +static void out(UI *ui, const char *str) +{ + TUIData *data = ui->data; + data->bufpos += (size_t)snprintf(data->buf + data->bufpos, + sizeof(data->buf) - data->bufpos, "%s", str); +} + +static void unibi_set_if_empty(unibi_term *ut, enum unibi_string str, + const char *val) +{ + if (!unibi_get_str(ut, str)) { + unibi_set_str(ut, str, val); + } +} + +static void fix_terminfo(TUIData *data) +{ + unibi_term *ut = data->ut; + + const char *term = os_getenv("TERM"); + if (!term) { + goto end; + } + +#define STARTS_WITH(str, prefix) (!memcmp(str, prefix, sizeof(prefix) - 1)) + + if (STARTS_WITH(term, "rxvt")) { + unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b[m\x1b(B"); + unibi_set_if_empty(ut, unibi_flash_screen, "\x1b[?5h$<20/>\x1b[?5l"); + unibi_set_if_empty(ut, unibi_enter_italics_mode, "\x1b[3m"); + } else if (STARTS_WITH(term, "screen")) { + unibi_set_if_empty(ut, unibi_to_status_line, "\x1b_"); + unibi_set_if_empty(ut, unibi_from_status_line, "\x1b\\"); + } + + if (STARTS_WITH(term, "xterm") || STARTS_WITH(term, "rxvt")) { + unibi_set_if_empty(ut, unibi_cursor_normal, "\x1b[?12l\x1b[?25h"); + unibi_set_if_empty(ut, unibi_cursor_invisible, "\x1b[?25l"); + unibi_set_if_empty(ut, unibi_flash_screen, "\x1b[?5h$<100/>\x1b[?5l"); + unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b(B\x1b[m"); + unibi_set_if_empty(ut, unibi_change_scroll_region, "\x1b[%i%p1%d;%p2%dr"); + unibi_set_if_empty(ut, unibi_clear_screen, "\x1b[H\x1b[2J"); + unibi_set_if_empty(ut, unibi_to_status_line, "\x1b]2"); + unibi_set_if_empty(ut, unibi_from_status_line, "\x07"); + } + +end: + // Fill some empty slots with common terminal strings + data->unibi_ext.enable_mouse = unibi_add_ext_str(ut, NULL, + "\x1b[?1002h\x1b[?1006h"); + data->unibi_ext.disable_mouse = unibi_add_ext_str(ut, NULL, + "\x1b[?1002l\x1b[?1006l"); + + unibi_set_if_empty(ut, unibi_cursor_address, "\x1b[%i%p1%d;%p2%dH"); + unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b[0;10m"); + unibi_set_if_empty(ut, unibi_set_a_foreground, + "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"); + unibi_set_if_empty(ut, unibi_set_a_background, + "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"); + unibi_set_if_empty(ut, unibi_enter_bold_mode, "\x1b[1m"); + unibi_set_if_empty(ut, unibi_enter_underline_mode, "\x1b[4m"); + unibi_set_if_empty(ut, unibi_enter_reverse_mode, "\x1b[7m"); + unibi_set_if_empty(ut, unibi_bell, "\x07"); + unibi_set_if_empty(data->ut, unibi_enter_ca_mode, "\x1b[?1049h"); + unibi_set_if_empty(data->ut, unibi_exit_ca_mode, "\x1b[?1049l"); + unibi_set_if_empty(ut, unibi_delete_line, "\x1b[M"); + unibi_set_if_empty(ut, unibi_parm_delete_line, "\x1b[%p1%dM"); + unibi_set_if_empty(ut, unibi_insert_line, "\x1b[L"); + unibi_set_if_empty(ut, unibi_parm_insert_line, "\x1b[%p1%dL"); + unibi_set_if_empty(ut, unibi_clear_screen, "\x1b[H\x1b[J"); + unibi_set_if_empty(ut, unibi_clr_eol, "\x1b[K"); + unibi_set_if_empty(ut, unibi_clr_eos, "\x1b[J"); +} + +static void flush_buf(UI *ui) +{ + static uv_write_t req; + static uv_buf_t buf; + TUIData *data = ui->data; + buf.base = data->buf; + buf.len = data->bufpos; + uv_write(&req, (uv_stream_t *)&data->output_handle, &buf, 1, NULL); + uv_run(data->write_loop, UV_RUN_DEFAULT); + data->bufpos = 0; +} + +static char *get_term_option(UI *ui, char *option) +{ + TUIData *data = ui->data; + + char *rv = pmap_get(cstr_t)(data->option_cache, option); + if (!rv) { + Error err; + Object val = vim_get_option(cstr_as_string(option), &err); + if (val.type == kObjectTypeString) { + rv = val.data.string.data; + pmap_put(cstr_t)(data->option_cache, option, rv); + } + } + + return rv; +} + +static void destroy_screen(TUIData *data) +{ + if (data->screen) { + for (int i = 0; i < data->old_height; i++) { + free(data->screen[i]); + } + free(data->screen); + } +} diff --git a/src/nvim/tui/tui.h b/src/nvim/tui/tui.h new file mode 100644 index 0000000000..07523bc124 --- /dev/null +++ b/src/nvim/tui/tui.h @@ -0,0 +1,8 @@ +#ifndef NVIM_TUI_TUI_H +#define NVIM_TUI_TUI_H + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/tui.h.generated.h" +#endif + +#endif // NVIM_TUI_TUI_H diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 20c6c9eccd..6e8fe4b635 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -46,6 +46,7 @@ #include "nvim/syntax.h" #include "nvim/term.h" #include "nvim/window.h" +#include "nvim/tui/tui.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ui.c.generated.h" @@ -81,8 +82,18 @@ static int height, width; #define SELECT_NTH(a1, a2, a3, a4, a5, a6, ...) a6 #define UI_CALL_HELPER(c, ...) UI_CALL_HELPER2(c, __VA_ARGS__) #define UI_CALL_HELPER2(c, ...) UI_CALL_##c(__VA_ARGS__) -#define UI_CALL_MORE(method, ...) ui->method(ui, __VA_ARGS__) -#define UI_CALL_ZERO(method) ui->method(ui) +#define UI_CALL_MORE(method, ...) if (ui->method) ui->method(ui, __VA_ARGS__) +#define UI_CALL_ZERO(method) if (ui->method) ui->method(ui) + +void ui_builtin_start(void) +{ + tui_start(); +} + +void ui_builtin_stop(void) +{ + UI_CALL(stop); +} void ui_write(uint8_t *s, int len) { diff --git a/src/nvim/ui.h b/src/nvim/ui.h index 099f2643d5..4bc3983578 100644 --- a/src/nvim/ui.h +++ b/src/nvim/ui.h @@ -38,6 +38,7 @@ struct ui_t { void (*suspend)(UI *ui); void (*set_title)(UI *ui, char *title); void (*set_icon)(UI *ui, char *icon); + void (*stop)(UI *ui); }; #ifdef INCLUDE_GENERATED_DECLARATIONS