diff options
Diffstat (limited to 'src/nvim/terminal.c')
-rw-r--r-- | src/nvim/terminal.c | 1129 |
1 files changed, 1129 insertions, 0 deletions
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c new file mode 100644 index 0000000000..87b2d8ff99 --- /dev/null +++ b/src/nvim/terminal.c @@ -0,0 +1,1129 @@ +// VT220/xterm-like terminal emulator implementation for Neovim. Powered by +// libvterm(http://www.leonerd.org.uk/code/libvterm/). +// +// libvterm is a pure C99 terminal emulation library with abstract input and +// display. This means that the library needs to read data from the master fd +// and feed VTerm instances, which will invoke user callbacks with screen +// update instructions that must be mirrored to the real display. +// +// Keys are pressed in VTerm instances by calling +// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that +// must be fed back to the master fd. +// +// This implementation uses Neovim buffers as the display mechanism for both +// the visible screen and the scrollback buffer. When focused, the window +// "pins" to the bottom of the buffer and mirrors libvterm screen state. +// +// When a line becomes invisible due to a decrease in screen height or because +// a line was pushed up during normal terminal output, we store the line +// information in the scrollback buffer, which is mirrored in the Neovim buffer +// by appending lines just above the visible part of the buffer. +// +// When the screen height increases, libvterm will ask for a row in the +// scrollback buffer, which is mirrored in the Neovim buffer displaying lines +// that were previously invisible. +// +// The vterm->Neovim synchronization is performed in intervals of 10 +// milliseconds. This is done to minimize screen updates when receiving when +// receiving large bursts of data. +// +// This module is decoupled from the processes that normally feed it data, so +// it's possible to use it as a general purpose console buffer(possibly as a +// log/display mechanism for Neovim in the future) +// +// Inspired by vimshell(http://www.wana.at/vimshell/) and +// Conque(https://code.google.com/p/conque/). Libvterm usage instructions (plus +// some extra code) were taken from +// pangoterm(http://www.leonerd.org.uk/code/pangoterm/) +#include <assert.h> +#include <stdio.h> +#include <stdint.h> +#include <stdbool.h> + +#include <vterm.h> + +#include "nvim/vim.h" +#include "nvim/terminal.h" +#include "nvim/message.h" +#include "nvim/memory.h" +#include "nvim/option.h" +#include "nvim/macros.h" +#include "nvim/mbyte.h" +#include "nvim/buffer.h" +#include "nvim/ascii.h" +#include "nvim/getchar.h" +#include "nvim/ui.h" +#include "nvim/syntax.h" +#include "nvim/screen.h" +#include "nvim/keymap.h" +#include "nvim/edit.h" +#include "nvim/mouse.h" +#include "nvim/memline.h" +#include "nvim/mark.h" +#include "nvim/map.h" +#include "nvim/misc1.h" +#include "nvim/move.h" +#include "nvim/ex_docmd.h" +#include "nvim/ex_cmds.h" +#include "nvim/window.h" +#include "nvim/fileio.h" +#include "nvim/os/event.h" +#include "nvim/api/private/helpers.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "terminal.c.generated.h" +#endif + +#define SCROLLBACK_BUFFER_DEFAULT_SIZE 1000 +// Delay for refreshing the terminal buffer after receiving updates from +// libvterm. This is greatly improves performance when receiving large bursts +// of data. +#define REFRESH_DELAY 10 + +static uv_timer_t refresh_timer; +static bool refresh_pending = false; + +typedef struct { + size_t cols; + VTermScreenCell cells[]; +} ScrollbackLine; + +struct terminal { + // options passed to terminal_open + TerminalOptions opts; + // libvterm structures + VTerm *vt; + VTermScreen *vts; + // buffer used to: + // - convert VTermScreen cell arrays into utf8 strings + // - receive data from libvterm as a result of key presses. + char textbuf[0x1fff]; + // Scrollback buffer storage for libvterm. + // TODO(tarruda): Use a doubly-linked list + ScrollbackLine **sb_buffer; + // number of rows pushed to sb_buffer + size_t sb_current; + // sb_buffer size; + size_t sb_size; + // "virtual index" that points to the first sb_buffer row that we need to + // push to the terminal buffer when refreshing the scrollback. When negative, + // it actually points to entries that are no longer in sb_buffer (because the + // window height has increased) and must be deleted from the terminal buffer + int sb_pending; + // buf_T instance that acts as a "drawing surface" for libvterm + buf_T *buf; + // program exited + bool closed, destroy; + // some vterm properties + bool forward_mouse; + // invalid rows libvterm screen + int invalid_start, invalid_end; + struct { + int row, col; + bool visible; + } cursor; + // which mouse button is pressed + int pressed_button; + // pending width/height + bool pending_resize; + // color palette. this isn't set directly in the vterm instance because + // the default values are used to obtain the color numbers passed to cterm + // colors + RgbValue colors[256]; + // attributes for focused/unfocused cursor cells + int focused_cursor_attr_id, unfocused_cursor_attr_id; +}; + +static VTermScreenCallbacks vterm_screen_callbacks = { + .damage = term_damage, + .moverect = term_moverect, + .movecursor = term_movecursor, + .settermprop = term_settermprop, + .bell = term_bell, + .sb_pushline = term_sb_push, + .sb_popline = term_sb_pop, +}; + +static PMap(ptr_t) *invalidated_terminals; +static Map(int, int) *color_indexes; +static int default_vt_fg, default_vt_bg; +static VTermColor default_vt_bg_rgb; + +void terminal_init(void) +{ + invalidated_terminals = pmap_new(ptr_t)(); + uv_timer_init(uv_default_loop(), &refresh_timer); + + // initialize a rgb->color index map for cterm attributes(VTermScreenCell + // only has RGB information and we need color indexes for terminal UIs) + color_indexes = map_new(int, int)(); + VTerm *vt = vterm_new(24, 80); + VTermState *state = vterm_obtain_state(vt); + + for (int color_index = 0; color_index < 256; color_index++) { + VTermColor color; + vterm_state_get_palette_color(state, color_index, &color); + map_put(int, int)(color_indexes, + RGB(color.red, color.green, color.blue), color_index + 1); + } + + VTermColor fg, bg; + vterm_state_get_default_colors(state, &fg, &bg); + default_vt_fg = RGB(fg.red, fg.green, fg.blue); + default_vt_bg = RGB(bg.red, bg.green, bg.blue); + default_vt_bg_rgb = bg; + vterm_free(vt); +} + +void terminal_teardown(void) +{ + uv_timer_stop(&refresh_timer); + uv_close((uv_handle_t *)&refresh_timer, NULL); + pmap_free(ptr_t)(invalidated_terminals); + map_free(int, int)(color_indexes); +} + +// public API {{{ + +Terminal *terminal_open(TerminalOptions opts) +{ + // Create a new terminal instance and configure it + Terminal *rv = xcalloc(1, sizeof(Terminal)); + rv->opts = opts; + rv->cursor.visible = true; + // Associate the terminal instance with the new buffer + rv->buf = curbuf; + curbuf->terminal = rv; + // Create VTerm + rv->vt = vterm_new(opts.height, opts.width); + vterm_set_utf8(rv->vt, 1); + // Setup state + VTermState *state = vterm_obtain_state(rv->vt); + vterm_state_set_bold_highbright(state, true); + // Set up screen + rv->vts = vterm_obtain_screen(rv->vt); + vterm_screen_enable_altscreen(rv->vts, true); + // delete empty lines at the end of the buffer + vterm_screen_set_callbacks(rv->vts, &vterm_screen_callbacks, rv); + vterm_screen_set_damage_merge(rv->vts, VTERM_DAMAGE_SCROLL); + vterm_screen_reset(rv->vts, 1); + // force a initial refresh of the screen to ensure the buffer will always + // have as many lines as screen rows when refresh_scrollback is called + rv->invalid_start = 0; + rv->invalid_end = opts.height; + refresh_screen(rv); + set_option_value((uint8_t *)"buftype", 0, (uint8_t *)"terminal", OPT_LOCAL); + // some sane settings for terminal buffers + set_option_value((uint8_t *)"wrap", false, NULL, OPT_LOCAL); + set_option_value((uint8_t *)"number", false, NULL, OPT_LOCAL); + set_option_value((uint8_t *)"relativenumber", false, NULL, OPT_LOCAL); + RESET_BINDING(curwin); + // Apply TermOpen autocmds so the user can configure the terminal + apply_autocmds(EVENT_TERMOPEN, NULL, NULL, true, curbuf); + + // Configure the scrollback buffer. Try to get the size from: + // + // - b:terminal_scrollback_buffer_size + // - g:terminal_scrollback_buffer_size + // - SCROLLBACK_BUFFER_DEFAULT_SIZE + // + // but limit to 100k. + int size = get_config_int(rv, "terminal_scrollback_buffer_size"); + rv->sb_size = size > 0 ? (size_t)size : SCROLLBACK_BUFFER_DEFAULT_SIZE; + rv->sb_size = MIN(rv->sb_size, 100000); + rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size); + + // Configure the color palette. Try to get the color from: + // + // - b:terminal_color_{NUM} + // - g:terminal_color_{NUM} + // - the VTerm instance + for (int i = 0; i < (int)ARRAY_SIZE(rv->colors); i++) { + RgbValue color_val = -1; + char var[64]; + snprintf(var, sizeof(var), "terminal_color_%d", i); + char *name = get_config_string(rv, var); + if (name) { + color_val = name_to_color((uint8_t *)name); + free(name); + + if (color_val != -1) { + rv->colors[i] = color_val; + } + } + + if (color_val == -1) { + // the default is taken from vterm + VTermColor color; + vterm_state_get_palette_color(state, i, &color); + rv->colors[i] = RGB(color.red, color.green, color.blue); + } + } + + // Configure cursor highlighting when focused/unfocused + char *group = get_config_string(rv, "terminal_focused_cursor_highlight"); + if (group) { + int group_id = syn_name2id((uint8_t *)group); + free(group); + + if (group_id) { + rv->focused_cursor_attr_id = syn_id2attr(group_id); + } + } + if (!rv->focused_cursor_attr_id) { + rv->focused_cursor_attr_id = get_attr_entry(&(attrentry_T) { + .rgb_ae_attr = HL_INVERSE, .rgb_fg_color = -1, .rgb_bg_color = -1, + .cterm_ae_attr = HL_INVERSE, .cterm_fg_color = 0, .cterm_bg_color = 0 + }); + } + + group = get_config_string(rv, "terminal_unfocused_cursor_highlight"); + if (group) { + int group_id = syn_name2id((uint8_t *)group); + free(group); + + if (group_id) { + rv->unfocused_cursor_attr_id = syn_id2attr(group_id); + } + } + if (!rv->unfocused_cursor_attr_id) { + int yellow_rgb = RGB(0xfc, 0xe9, 0x4f); + int yellow_term = 12; + rv->unfocused_cursor_attr_id = get_attr_entry(&(attrentry_T) { + .rgb_ae_attr = 0, .rgb_fg_color = -1, .rgb_bg_color = yellow_rgb, + .cterm_ae_attr = 0, .cterm_fg_color = 0, .cterm_bg_color = yellow_term, + }); + } + + return rv; +} + +void terminal_close(Terminal *term, char *msg) +{ + if (term->closed) { + return; + } + + term->forward_mouse = false; + term->closed = true; + if (!msg || exiting) { + // If no msg was given, this was called by close_buffer(buffer.c) so we + // should not wait for the user to press a key. Also cannot wait if + // `exiting == true` + term->opts.close_cb(term->opts.data); + } else { + terminal_receive(term, msg, strlen(msg)); + } +} + +void terminal_resize(Terminal *term, uint16_t width, uint16_t height) +{ + if (term->closed) { + // will be called after exited if two windows display the same terminal and + // one of the is closed as a consequence of pressing a key. + return; + } + int curwidth, curheight; + vterm_get_size(term->vt, &curheight, &curwidth); + + if (!width) { + width = (uint16_t)curwidth; + } + + if (!height) { + height = (uint16_t)curheight; + } + + // The new width/height are the minimum for all windows that display the + // terminal in the current tab. + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (!wp->w_closing && wp->w_buffer == term->buf) { + width = (uint16_t)MIN(width, (uint16_t)wp->w_width); + height = (uint16_t)MIN(height, (uint16_t)wp->w_height); + } + } + + if (curheight == height && curwidth == width) { + return; + } + + vterm_set_size(term->vt, height, width); + vterm_screen_flush_damage(term->vts); + term->pending_resize = true; + invalidate_terminal(term, -1, -1); +} + +void terminal_enter(Terminal *term, bool process_deferred) +{ + checkpcmark(); + setpcmark(); + int save_state = State; + int save_rd = RedrawingDisabled; + State = TERM_FOCUS; + RedrawingDisabled = false; + bool save_mapped_ctrl_c = mapped_ctrl_c; + mapped_ctrl_c = true; + // go to the bottom when the terminal is focused + adjust_topline(term, false); + // erase the unfocused cursor + invalidate_terminal(term, term->cursor.row, term->cursor.row + 1); + showmode(); + ui_busy_start(); + redraw(false); + int c; + bool close = false; + + for (;;) { + if (process_deferred) { + event_enable_deferred(); + } + + c = safe_vgetc(); + + if (process_deferred) { + event_disable_deferred(); + } + + switch (c) { + case Ctrl_BSL: + c = safe_vgetc(); + if (c == Ctrl_N) { + goto end; + } + terminal_send_key(term, c); + break; + + case K_LEFTMOUSE: + case K_LEFTDRAG: + case K_LEFTRELEASE: + case K_MIDDLEMOUSE: + case K_MIDDLEDRAG: + case K_MIDDLERELEASE: + case K_RIGHTMOUSE: + case K_RIGHTDRAG: + case K_RIGHTRELEASE: + case K_MOUSEDOWN: + case K_MOUSEUP: + if (send_mouse_event(term, c)) { + goto end; + } + break; + + case K_EVENT: + event_process(); + break; + + default: + if (term->closed) { + close = true; + goto end; + } + + terminal_send_key(term, c); + } + } + +end: + restart_edit = 0; + State = save_state; + RedrawingDisabled = save_rd; + // draw the unfocused cursor + invalidate_terminal(term, term->cursor.row, term->cursor.row + 1); + mapped_ctrl_c = save_mapped_ctrl_c; + unshowmode(true); + redraw(false); + ui_busy_stop(); + if (close) { + term->opts.close_cb(term->opts.data); + do_cmdline_cmd((uint8_t *)"bwipeout!"); + } +} + +void terminal_destroy(Terminal *term) +{ + term->buf->terminal = NULL; + term->buf = NULL; + pmap_del(ptr_t)(invalidated_terminals, term); + for (size_t i = 0 ; i < term->sb_current; i++) { + free(term->sb_buffer[i]); + } + free(term->sb_buffer); + vterm_free(term->vt); + free(term); +} + +void terminal_send(Terminal *term, char *data, size_t size) +{ + if (term->closed) { + return; + } + term->opts.write_cb(data, size, term->opts.data); +} + +void terminal_send_key(Terminal *term, int c) +{ + VTermModifier mod = VTERM_MOD_NONE; + VTermKey key = convert_key(c, &mod); + + if (key) { + vterm_keyboard_key(term->vt, key, mod); + } else { + vterm_keyboard_unichar(term->vt, (uint32_t)c, mod); + } + + size_t len = vterm_output_read(term->vt, term->textbuf, + sizeof(term->textbuf)); + terminal_send(term, term->textbuf, (size_t)len); +} + +void terminal_receive(Terminal *term, char *data, size_t len) +{ + if (!data) { + return; + } + + vterm_input_write(term->vt, data, len); + vterm_screen_flush_damage(term->vts); +} + +void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, + int *term_attrs) +{ + int height, width; + vterm_get_size(term->vt, &height, &width); + assert(linenr); + int row = linenr_to_row(term, linenr); + if (row >= height) { + // Terminal height was decreased but the change wasn't reflected into the + // buffer yet + return; + } + + for (int col = 0; col < width; col++) { + VTermScreenCell cell; + fetch_cell(term, row, col, &cell); + // Get the rgb value set by libvterm. + int vt_fg = RGB(cell.fg.red, cell.fg.green, cell.fg.blue); + int vt_bg = RGB(cell.bg.red, cell.bg.green, cell.bg.blue); + vt_fg = vt_fg != default_vt_fg ? vt_fg : - 1; + vt_bg = vt_bg != default_vt_bg ? vt_bg : - 1; + // Since libvterm does not expose the color index used by the program, we + // use the rgb value to find the appropriate index in the cache computed by + // `terminal_init`. + int vt_fg_idx = vt_fg != default_vt_fg ? + map_get(int, int)(color_indexes, vt_fg) : 0; + int vt_bg_idx = vt_bg != default_vt_bg ? + map_get(int, int)(color_indexes, vt_bg) : 0; + // The index is now used to get the final rgb value from the + // user-customizable palette. + int vt_fg_rgb = vt_fg_idx != 0 ? term->colors[vt_fg_idx - 1] : -1; + int vt_bg_rgb = vt_bg_idx != 0 ? term->colors[vt_bg_idx - 1] : -1; + + int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0) + | (cell.attrs.italic ? HL_ITALIC : 0) + | (cell.attrs.reverse ? HL_INVERSE : 0) + | (cell.attrs.underline ? HL_UNDERLINE : 0); + + int attr_id = 0; + + if (hl_attrs || vt_fg != -1 || vt_bg != -1) { + attr_id = get_attr_entry(&(attrentry_T) { + .cterm_ae_attr = (int16_t)hl_attrs, + .cterm_fg_color = vt_fg_idx, + .cterm_bg_color = vt_bg_idx, + .rgb_ae_attr = (int16_t)hl_attrs, + .rgb_fg_color = vt_fg_rgb, + .rgb_bg_color = vt_bg_rgb, + }); + } + + if (term->cursor.visible && term->cursor.row == row + && term->cursor.col == col) { + attr_id = hl_combine_attr(attr_id, is_focused(term) && wp == curwin ? + term->focused_cursor_attr_id : term->unfocused_cursor_attr_id); + } + + term_attrs[col] = attr_id; + } +} + +// }}} +// libvterm callbacks {{{ + +static int term_damage(VTermRect rect, void *data) +{ + invalidate_terminal(data, rect.start_row, rect.end_row); + return 1; +} + +static int term_moverect(VTermRect dest, VTermRect src, void *data) +{ + invalidate_terminal(data, MIN(dest.start_row, src.start_row), + MAX(dest.end_row, src.end_row)); + return 1; +} + +static int term_movecursor(VTermPos new, VTermPos old, int visible, + void *data) +{ + Terminal *term = data; + term->cursor.row = new.row; + term->cursor.col = new.col; + invalidate_terminal(term, old.row, old.row + 1); + invalidate_terminal(term, new.row, new.row + 1); + return 1; +} + +static int term_settermprop(VTermProp prop, VTermValue *val, void *data) +{ + Terminal *term = data; + + switch (prop) { + case VTERM_PROP_ALTSCREEN: + break; + + case VTERM_PROP_CURSORVISIBLE: + term->cursor.visible = val->boolean; + invalidate_terminal(term, term->cursor.row, term->cursor.row + 1); + break; + + case VTERM_PROP_TITLE: { + Error err; + dict_set_value(term->buf->b_vars, + cstr_as_string("term_title"), + STRING_OBJ(cstr_as_string(val->string)), &err); + break; + } + + case VTERM_PROP_MOUSE: + term->forward_mouse = (bool)val->number; + break; + + default: + return 0; + } + + return 1; +} + +static int term_bell(void *data) +{ + ui_putc('\x07'); + return 1; +} + +// the scrollback push/pop handlers were copied almost verbatim from pangoterm +static int term_sb_push(int cols, const VTermScreenCell *cells, void *data) +{ + Terminal *term = data; + // copy vterm cells into sb_buffer + size_t c = (size_t)cols; + ScrollbackLine *sbrow = NULL; + if (term->sb_current == term->sb_size) { + if (term->sb_buffer[term->sb_current - 1]->cols == c) { + // Recycle old row if it's the right size + sbrow = term->sb_buffer[term->sb_current - 1]; + } else { + free(term->sb_buffer[term->sb_current - 1]); + } + + memmove(term->sb_buffer + 1, term->sb_buffer, + sizeof(term->sb_buffer[0]) * (term->sb_current - 1)); + + } else if (term->sb_current > 0) { + memmove(term->sb_buffer + 1, term->sb_buffer, + sizeof(term->sb_buffer[0]) * term->sb_current); + } + + if (!sbrow) { + sbrow = xmalloc(sizeof(ScrollbackLine) + c * sizeof(sbrow->cells[0])); + sbrow->cols = c; + } + + term->sb_buffer[0] = sbrow; + if (term->sb_current < term->sb_size) { + term->sb_current++; + } + + if (term->sb_pending < (int)term->sb_size) { + term->sb_pending++; + } + + memcpy(sbrow->cells, cells, sizeof(cells[0]) * c); + pmap_put(ptr_t)(invalidated_terminals, term, NULL); + + return 1; +} + +static int term_sb_pop(int cols, VTermScreenCell *cells, void *data) +{ + Terminal *term = data; + + if (!term->sb_current) { + return 0; + } + + if (term->sb_pending) { + term->sb_pending--; + } + + // restore vterm state + size_t c = (size_t)cols; + ScrollbackLine *sbrow = term->sb_buffer[0]; + term->sb_current--; + memmove(term->sb_buffer, term->sb_buffer + 1, + sizeof(term->sb_buffer[0]) * (term->sb_current)); + + size_t cols_to_copy = c; + if (cols_to_copy > sbrow->cols) { + cols_to_copy = sbrow->cols; + } + + // copy to vterm state + memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy); + for (size_t col = cols_to_copy; col < c; col++) { + cells[col].chars[0] = 0; + cells[col].width = 1; + } + free(sbrow); + pmap_put(ptr_t)(invalidated_terminals, term, NULL); + + return 1; +} + +// }}} +// input handling {{{ + +static void convert_modifiers(VTermModifier *statep) +{ + if (mod_mask & MOD_MASK_SHIFT) { *statep |= VTERM_MOD_SHIFT; } + if (mod_mask & MOD_MASK_CTRL) { *statep |= VTERM_MOD_CTRL; } + if (mod_mask & MOD_MASK_ALT) { *statep |= VTERM_MOD_ALT; } +} + +static VTermKey convert_key(int key, VTermModifier *statep) +{ + convert_modifiers(statep); + + switch (key) { + case K_BS: return VTERM_KEY_BACKSPACE; + case TAB: return VTERM_KEY_TAB; + case Ctrl_M: return VTERM_KEY_ENTER; + case ESC: return VTERM_KEY_ESCAPE; + + case K_UP: return VTERM_KEY_UP; + case K_DOWN: return VTERM_KEY_DOWN; + case K_LEFT: return VTERM_KEY_LEFT; + case K_RIGHT: return VTERM_KEY_RIGHT; + + case K_INS: return VTERM_KEY_INS; + case K_DEL: return VTERM_KEY_DEL; + case K_HOME: return VTERM_KEY_HOME; + case K_END: return VTERM_KEY_END; + case K_PAGEUP: return VTERM_KEY_PAGEUP; + case K_PAGEDOWN: return VTERM_KEY_PAGEDOWN; + + case K_K0: + case K_KINS: return VTERM_KEY_KP_0; + case K_K1: + case K_KEND: return VTERM_KEY_KP_1; + case K_K2: return VTERM_KEY_KP_2; + case K_K3: + case K_KPAGEDOWN: return VTERM_KEY_KP_3; + case K_K4: return VTERM_KEY_KP_4; + case K_K5: return VTERM_KEY_KP_5; + case K_K6: return VTERM_KEY_KP_6; + case K_K7: + case K_KHOME: return VTERM_KEY_KP_7; + case K_K8: return VTERM_KEY_KP_8; + case K_K9: + case K_KPAGEUP: return VTERM_KEY_KP_9; + case K_KDEL: + case K_KPOINT: return VTERM_KEY_KP_PERIOD; + case K_KENTER: return VTERM_KEY_KP_ENTER; + case K_KPLUS: return VTERM_KEY_KP_PLUS; + case K_KMINUS: return VTERM_KEY_KP_MINUS; + case K_KMULTIPLY: return VTERM_KEY_KP_MULT; + case K_KDIVIDE: return VTERM_KEY_KP_DIVIDE; + + default: return VTERM_KEY_NONE; + } +} + +static void mouse_action(Terminal *term, int button, int row, int col, + bool drag, VTermModifier mod) +{ + if (term->pressed_button && (term->pressed_button != button || !drag)) { + // release the previous button + vterm_mouse_button(term->vt, term->pressed_button, 0, mod); + term->pressed_button = 0; + } + + // move the mouse + vterm_mouse_move(term->vt, row, col, mod); + + if (!term->pressed_button) { + // press the button if not already pressed + vterm_mouse_button(term->vt, button, 1, mod); + term->pressed_button = button; + } +} + +// process a mouse event while the terminal is focused. return true if the +// terminal should lose focus +static bool send_mouse_event(Terminal *term, int c) +{ + int row = mouse_row, col = mouse_col; + win_T *mouse_win = mouse_find_win(&row, &col); + + if (term->forward_mouse && mouse_win->w_buffer == term->buf) { + // event in the terminal window and mouse events was enabled by the + // program. translate and forward the event + int button; + bool drag = false; + + switch (c) { + case K_LEFTDRAG: drag = true; + case K_LEFTMOUSE: button = 1; break; + case K_MIDDLEDRAG: drag = true; + case K_MIDDLEMOUSE: button = 2; break; + case K_RIGHTDRAG: drag = true; + case K_RIGHTMOUSE: button = 3; break; + case K_MOUSEDOWN: button = 4; break; + case K_MOUSEUP: button = 5; break; + default: return false; + } + + mouse_action(term, button, row, col, drag, 0); + size_t len = vterm_output_read(term->vt, term->textbuf, + sizeof(term->textbuf)); + terminal_send(term, term->textbuf, (size_t)len); + return false; + } + + if (c == K_MOUSEDOWN || c == K_MOUSEUP) { + win_T *save_curwin = curwin; + // switch window/buffer to perform the scroll + curwin = mouse_win; + curbuf = curwin->w_buffer; + int direction = c == K_MOUSEDOWN ? MSCR_DOWN : MSCR_UP; + if (mod_mask & (MOD_MASK_SHIFT | MOD_MASK_CTRL)) { + scroll_redraw(direction, curwin->w_botline - curwin->w_topline); + } else { + scroll_redraw(direction, 3L); + } + + curwin->w_redr_status = true; + curwin = save_curwin; + curbuf = curwin->w_buffer; + redraw_win_later(mouse_win, NOT_VALID); + invalidate_terminal(term, -1, -1); + // Only need to exit focus if the scrolled window is the terminal window + return mouse_win == curwin; + } + + ins_char_typebuf(c); + return true; +} + +// }}} +// terminal buffer refresh & misc {{{ + + +void fetch_row(Terminal *term, int row, int end_col) +{ + int col = 0; + size_t line_len = 0; + char *ptr = term->textbuf; + + while (col < end_col) { + VTermScreenCell cell; + fetch_cell(term, row, col, &cell); + int cell_len = 0; + if (cell.chars[0]) { + for (int i = 0; cell.chars[i]; i++) { + cell_len += utf_char2bytes((int)cell.chars[i], + (uint8_t *)ptr + cell_len); + } + } else { + *ptr = ' '; + cell_len = 1; + } + char c = *ptr; + ptr += cell_len; + if (c != ' ') { + // only increase the line length if the last character is not whitespace + line_len = (size_t)(ptr - term->textbuf); + } + col += cell.width; + } + + // trim trailing whitespace + term->textbuf[line_len] = 0; +} + +static void fetch_cell(Terminal *term, int row, int col, + VTermScreenCell *cell) +{ + if (row < 0) { + ScrollbackLine *sbrow = term->sb_buffer[-row - 1]; + if ((size_t)col < sbrow->cols) { + *cell = sbrow->cells[col]; + } else { + // fill the pointer with an empty cell + *cell = (VTermScreenCell) { + .chars = { 0 }, + .width = 1, + .bg = default_vt_bg_rgb + }; + } + } else { + vterm_screen_get_cell(term->vts, (VTermPos){.row = row, .col = col}, + cell); + } +} + +// queue a terminal instance for refresh +static void invalidate_terminal(Terminal *term, int start_row, int end_row) +{ + if (start_row != -1 && end_row != -1) { + term->invalid_start = MIN(term->invalid_start, start_row); + term->invalid_end = MAX(term->invalid_end, end_row); + } + + pmap_put(ptr_t)(invalidated_terminals, term, NULL); + if (!refresh_pending) { + uv_timer_start(&refresh_timer, refresh_timer_cb, REFRESH_DELAY, 0); + refresh_pending = true; + } +} + +// libuv timer callback. This will enqueue on_refresh to be processed as an +// event. +static void refresh_timer_cb(uv_timer_t *handle) +{ + event_push((Event) {.handler = on_refresh}, false); + refresh_pending = false; +} + +// Refresh all invalidated terminals +static void on_refresh(Event event) +{ + if (exiting) { + // bad things can happen if we redraw when exiting, and there's no need to + // update the buffer. + return; + } + Terminal *term; + void *stub; (void)(stub); + // dont process autocommands while updating terminal buffers. JobActivity can + // be used act on terminal output. + block_autocmds(); + map_foreach(invalidated_terminals, term, stub, { + if (!term->buf) { + // destroyed by `close_buffer`. Dont do anything else + continue; + } + bool pending_resize = term->pending_resize; + WITH_BUFFER(term->buf, { + refresh_size(term); + refresh_scrollback(term); + refresh_screen(term); + redraw_buf_later(term->buf, NOT_VALID); + }); + adjust_topline(term, pending_resize); + }); + pmap_clear(ptr_t)(invalidated_terminals); + unblock_autocmds(); + redraw(true); +} + +static void refresh_size(Terminal *term) +{ + if (!term->pending_resize || term->closed) { + return; + } + + term->pending_resize = false; + int width, height; + vterm_get_size(term->vt, &height, &width); + term->invalid_start = 0; + term->invalid_end = height; + term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data); +} + +// Refresh the scrollback of a invalidated terminal +static void refresh_scrollback(Terminal *term) +{ + int width, height; + vterm_get_size(term->vt, &height, &width); + + while (term->sb_pending > 0) { + // This means that either the window height has decreased or the screen + // became full and libvterm had to push all rows up. Convert the first + // pending scrollback row into a string and append it just above the visible + // section of the buffer + if (((int)term->buf->b_ml.ml_line_count - height) >= (int)term->sb_size) { + // scrollback full, delete lines at the top + ml_delete(1, false); + deleted_lines(1, 1); + } + fetch_row(term, -term->sb_pending, width); + int buf_index = (int)term->buf->b_ml.ml_line_count - height; + ml_append(buf_index, (uint8_t *)term->textbuf, 0, false); + appended_lines(buf_index, 1); + term->sb_pending--; + } + + // Remove extra lines at the bottom + int max_line_count = (int)term->sb_current + height; + while (term->buf->b_ml.ml_line_count > max_line_count) { + ml_delete(term->buf->b_ml.ml_line_count, false); + deleted_lines(term->buf->b_ml.ml_line_count, 1); + } +} + +// Refresh the screen(visible part of the buffer when the terminal is +// focused) of a invalidated terminal +static void refresh_screen(Terminal *term) +{ + int changed = 0; + int added = 0; + int height; + int width; + vterm_get_size(term->vt, &height, &width); + // It's possible that the terminal height decreased and `term->invalid_end` + // doesn't reflect it yet + term->invalid_end = MIN(term->invalid_end, height); + + for (int r = term->invalid_start, linenr = row_to_linenr(term, r); + r < term->invalid_end; r++, linenr++) { + fetch_row(term, r, width); + + if (linenr <= term->buf->b_ml.ml_line_count) { + ml_replace(linenr, (uint8_t *)term->textbuf, true); + changed++; + } else { + ml_append(linenr - 1, (uint8_t *)term->textbuf, 0, false); + added++; + } + } + + int change_start = row_to_linenr(term, term->invalid_start); + int change_end = change_start + changed; + changed_lines(change_start, 0, change_end, added); + term->invalid_start = INT_MAX; + term->invalid_end = -1; +} + +static void redraw(bool restore_cursor) +{ + int save_row, save_col; + if (restore_cursor) { + // save the current row/col to restore after updating screen when not + // focused + save_row = ui_current_row(); + save_col = ui_current_col(); + } + block_autocmds(); + validate_cursor(); + + if (must_redraw) { + update_screen(0); + } + + redraw_statuslines(); + + if (need_maketitle) { + maketitle(); + } + + showruler(false); + + Terminal *term = curbuf->terminal; + if (term && is_focused(term)) { + curwin->w_wrow = term->cursor.row; + curwin->w_wcol = term->cursor.col + win_col_off(curwin); + setcursor(); + } else if (restore_cursor) { + ui_cursor_goto(save_row, save_col); + } else { + // exiting terminal focus, put the window cursor in a valid position + int height, width; + vterm_get_size(term->vt, &height, &width); + curwin->w_wrow = height - 1; + curwin->w_wcol = 0; + setcursor(); + } + + unblock_autocmds(); + ui_flush(); +} + +static void adjust_topline(Terminal *term, bool force) +{ + int height, width; + vterm_get_size(term->vt, &height, &width); + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (wp->w_buffer == term->buf) { + // for every window that displays a terminal, ensure the cursor is in a + // valid line + wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, term->buf->b_ml.ml_line_count); + if (force || curbuf != term->buf || is_focused(term)) { + // if the terminal is not in the current window or if it's focused, + // adjust topline/cursor so the window will "follow" the terminal + // output + wp->w_cursor.lnum = term->buf->b_ml.ml_line_count; + set_topline(wp, MAX(wp->w_cursor.lnum - height + 1, 1)); + } + } + } +} + +static int row_to_linenr(Terminal *term, int row) +{ + return row != INT_MAX ? row + (int)term->sb_current + 1 : INT_MAX; +} + +static int linenr_to_row(Terminal *term, int linenr) +{ + return linenr - (int)term->sb_current - 1; +} + +static bool is_focused(Terminal *term) +{ + return State & TERM_FOCUS && curbuf == term->buf; +} + +#define GET_CONFIG_VALUE(t, k, o) \ + do { \ + Error err; \ + o = dict_get_value(t->buf->b_vars, cstr_as_string(k), &err); \ + if (obj.type == kObjectTypeNil) { \ + o = dict_get_value(&globvardict, cstr_as_string(k), &err); \ + } \ + } while (0) + +static char *get_config_string(Terminal *term, char *key) +{ + Object obj = OBJECT_INIT; + GET_CONFIG_VALUE(term, key, obj); + if (obj.type == kObjectTypeString) { + return obj.data.string.data; + } + return NULL; +} + +static int get_config_int(Terminal *term, char *key) +{ + Object obj = OBJECT_INIT; + GET_CONFIG_VALUE(term, key, obj); + if (obj.type == kObjectTypeInteger) { + return (int)obj.data.integer; + } + return 0; +} + +// }}} + +// vim: foldmethod=marker foldenable |