1// This is an open source non-commercial project. Dear PVS-Studio, please check
2// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
4// VT220/xterm-like terminal emulator.
5// Powered by libvterm http://www.leonerd.org.uk/code/libvterm
6//
7// libvterm is a pure C99 terminal emulation library with abstract input and
8// display. This means that the library needs to read data from the master fd
9// and feed VTerm instances, which will invoke user callbacks with screen
10// update instructions that must be mirrored to the real display.
11//
12// Keys are sent to VTerm instances by calling
13// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that
14// must be fed back to the master fd.
15//
16// Nvim buffers are used as the display mechanism for both the visible screen
17// and the scrollback buffer.
18//
19// When a line becomes invisible due to a decrease in screen height or because
20// a line was pushed up during normal terminal output, we store the line
21// information in the scrollback buffer, which is mirrored in the nvim buffer
22// by appending lines just above the visible part of the buffer.
23//
24// When the screen height increases, libvterm will ask for a row in the
25// scrollback buffer, which is mirrored in the nvim buffer displaying lines
26// that were previously invisible.
27//
28// The vterm->nvim synchronization is performed in intervals of 10 milliseconds,
29// to minimize screen updates when receiving large bursts of data.
30//
31// This module is decoupled from the processes that normally feed it data, so
32// it's possible to use it as a general purpose console buffer (possibly as a
33// log/display mechanism for nvim in the future)
34//
35// Inspired by: vimshell http://www.wana.at/vimshell
36// Conque https://code.google.com/p/conque
37// Some code from pangoterm http://www.leonerd.org.uk/code/pangoterm
38
39#include <assert.h>
40#include <stdio.h>
41#include <stdint.h>
42#include <stdbool.h>
43
44#include <vterm.h>
45
46#include "nvim/log.h"
47#include "nvim/vim.h"
48#include "nvim/terminal.h"
49#include "nvim/message.h"
50#include "nvim/memory.h"
51#include "nvim/option.h"
52#include "nvim/highlight.h"
53#include "nvim/macros.h"
54#include "nvim/mbyte.h"
55#include "nvim/buffer.h"
56#include "nvim/change.h"
57#include "nvim/ascii.h"
58#include "nvim/getchar.h"
59#include "nvim/ui.h"
60#include "nvim/syntax.h"
61#include "nvim/screen.h"
62#include "nvim/keymap.h"
63#include "nvim/edit.h"
64#include "nvim/mouse.h"
65#include "nvim/memline.h"
66#include "nvim/map.h"
67#include "nvim/misc1.h"
68#include "nvim/move.h"
69#include "nvim/main.h"
70#include "nvim/state.h"
71#include "nvim/ex_docmd.h"
72#include "nvim/ex_cmds.h"
73#include "nvim/window.h"
74#include "nvim/fileio.h"
75#include "nvim/event/loop.h"
76#include "nvim/event/time.h"
77#include "nvim/os/input.h"
78#include "nvim/api/private/helpers.h"
79#include "nvim/api/private/handle.h"
80
81typedef struct terminal_state {
82 VimState state;
83 Terminal *term;
84 int save_rd; // saved value of RedrawingDisabled
85 bool close;
86 bool got_bsl; // if the last input was <C-\>
87} TerminalState;
88
89#ifdef INCLUDE_GENERATED_DECLARATIONS
90# include "terminal.c.generated.h"
91#endif
92
93// Delay for refreshing the terminal buffer after receiving updates from
94// libvterm. Improves performance when receiving large bursts of data.
95#define REFRESH_DELAY 10
96
97static TimeWatcher refresh_timer;
98static bool refresh_pending = false;
99
100typedef struct {
101 size_t cols;
102 VTermScreenCell cells[];
103} ScrollbackLine;
104
105struct terminal {
106 TerminalOptions opts; // options passed to terminal_open
107 VTerm *vt;
108 VTermScreen *vts;
109 // buffer used to:
110 // - convert VTermScreen cell arrays into utf8 strings
111 // - receive data from libvterm as a result of key presses.
112 char textbuf[0x1fff];
113
114 ScrollbackLine **sb_buffer; // Scrollback buffer storage for libvterm
115 size_t sb_current; // number of rows pushed to sb_buffer
116 size_t sb_size; // sb_buffer size
117 // "virtual index" that points to the first sb_buffer row that we need to
118 // push to the terminal buffer when refreshing the scrollback. When negative,
119 // it actually points to entries that are no longer in sb_buffer (because the
120 // window height has increased) and must be deleted from the terminal buffer
121 int sb_pending;
122
123 // buf_T instance that acts as a "drawing surface" for libvterm
124 // we can't store a direct reference to the buffer because the
125 // refresh_timer_cb may be called after the buffer was freed, and there's
126 // no way to know if the memory was reused.
127 handle_T buf_handle;
128 // program exited
129 bool closed, destroy;
130
131 // some vterm properties
132 bool forward_mouse;
133 int invalid_start, invalid_end; // invalid rows in libvterm screen
134 struct {
135 int row, col;
136 bool visible;
137 } cursor;
138 int pressed_button; // which mouse button is pressed
139 bool pending_resize; // pending width/height
140
141 size_t refcount; // reference count
142};
143
144static VTermScreenCallbacks vterm_screen_callbacks = {
145 .damage = term_damage,
146 .moverect = term_moverect,
147 .movecursor = term_movecursor,
148 .settermprop = term_settermprop,
149 .bell = term_bell,
150 .sb_pushline = term_sb_push,
151 .sb_popline = term_sb_pop,
152};
153
154static PMap(ptr_t) *invalidated_terminals;
155
156void terminal_init(void)
157{
158 invalidated_terminals = pmap_new(ptr_t)();
159 time_watcher_init(&main_loop, &refresh_timer, NULL);
160 // refresh_timer_cb will redraw the screen which can call vimscript
161 refresh_timer.events = multiqueue_new_child(main_loop.events);
162}
163
164void terminal_teardown(void)
165{
166 time_watcher_stop(&refresh_timer);
167 multiqueue_free(refresh_timer.events);
168 time_watcher_close(&refresh_timer, NULL);
169 pmap_free(ptr_t)(invalidated_terminals);
170}
171
172// public API {{{
173
174Terminal *terminal_open(TerminalOptions opts)
175{
176 // Create a new terminal instance and configure it
177 Terminal *rv = xcalloc(1, sizeof(Terminal));
178 rv->opts = opts;
179 rv->cursor.visible = true;
180 // Associate the terminal instance with the new buffer
181 rv->buf_handle = curbuf->handle;
182 curbuf->terminal = rv;
183 // Create VTerm
184 rv->vt = vterm_new(opts.height, opts.width);
185 vterm_set_utf8(rv->vt, 1);
186 // Setup state
187 VTermState *state = vterm_obtain_state(rv->vt);
188 // Set up screen
189 rv->vts = vterm_obtain_screen(rv->vt);
190 vterm_screen_enable_altscreen(rv->vts, true);
191 // delete empty lines at the end of the buffer
192 vterm_screen_set_callbacks(rv->vts, &vterm_screen_callbacks, rv);
193 vterm_screen_set_damage_merge(rv->vts, VTERM_DAMAGE_SCROLL);
194 vterm_screen_reset(rv->vts, 1);
195 // force a initial refresh of the screen to ensure the buffer will always
196 // have as many lines as screen rows when refresh_scrollback is called
197 rv->invalid_start = 0;
198 rv->invalid_end = opts.height;
199 refresh_screen(rv, curbuf);
200 set_option_value("buftype", 0, "terminal", OPT_LOCAL); // -V666
201
202 // Default settings for terminal buffers
203 curbuf->b_p_ma = false; // 'nomodifiable'
204 curbuf->b_p_ul = -1; // 'undolevels'
205 curbuf->b_p_scbk = // 'scrollback' (initialize local from global)
206 (p_scbk < 0) ? 10000 : MAX(1, p_scbk);
207 curbuf->b_p_tw = 0; // 'textwidth'
208 set_option_value("wrap", false, NULL, OPT_LOCAL);
209 set_option_value("list", false, NULL, OPT_LOCAL);
210 buf_set_term_title(curbuf, (char *)curbuf->b_ffname);
211 RESET_BINDING(curwin);
212 // Reset cursor in current window.
213 curwin->w_cursor = (pos_T){ .lnum = 1, .col = 0, .coladd = 0 };
214 // Apply TermOpen autocmds _before_ configuring the scrollback buffer.
215 apply_autocmds(EVENT_TERMOPEN, NULL, NULL, false, curbuf);
216 // Local 'scrollback' _after_ autocmds.
217 curbuf->b_p_scbk = (curbuf->b_p_scbk < 1) ? SB_MAX : curbuf->b_p_scbk;
218
219 // Configure the scrollback buffer.
220 rv->sb_size = (size_t)curbuf->b_p_scbk;
221 rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size);
222
223 vterm_state_set_bold_highbright(state, true);
224
225 // Configure the color palette. Try to get the color from:
226 //
227 // - b:terminal_color_{NUM}
228 // - g:terminal_color_{NUM}
229 // - the VTerm instance
230 for (int i = 0; i < 16; i++) {
231 RgbValue color_val = -1;
232 char var[64];
233 snprintf(var, sizeof(var), "terminal_color_%d", i);
234 char *name = get_config_string(var);
235 if (name) {
236 color_val = name_to_color((uint8_t *)name);
237 xfree(name);
238
239 if (color_val != -1) {
240 VTermColor color;
241 vterm_color_rgb(&color,
242 (uint8_t)((color_val >> 16) & 0xFF),
243 (uint8_t)((color_val >> 8) & 0xFF),
244 (uint8_t)((color_val >> 0) & 0xFF));
245 vterm_state_set_palette_color(state, i, &color);
246 }
247 }
248 }
249
250 return rv;
251}
252
253void terminal_close(Terminal *term, char *msg)
254{
255 if (term->closed) {
256 return;
257 }
258
259 term->forward_mouse = false;
260
261 // flush any pending changes to the buffer
262 if (!exiting) {
263 block_autocmds();
264 refresh_terminal(term);
265 unblock_autocmds();
266 }
267
268 buf_T *buf = handle_get_buffer(term->buf_handle);
269 term->closed = true;
270
271 if (!msg || exiting) {
272 // If no msg was given, this was called by close_buffer(buffer.c). Or if
273 // exiting, we must inform the buffer the terminal no longer exists so that
274 // close_buffer() doesn't call this again.
275 term->buf_handle = 0;
276 if (buf) {
277 buf->terminal = NULL;
278 }
279 if (!term->refcount) {
280 // We should not wait for the user to press a key.
281 term->opts.close_cb(term->opts.data);
282 }
283 } else {
284 terminal_receive(term, msg, strlen(msg));
285 }
286
287 if (buf) {
288 apply_autocmds(EVENT_TERMCLOSE, NULL, NULL, false, buf);
289 }
290}
291
292void terminal_check_size(Terminal *term)
293{
294 if (term->closed) {
295 return;
296 }
297
298 int curwidth, curheight;
299 vterm_get_size(term->vt, &curheight, &curwidth);
300 uint16_t width = 0, height = 0;
301
302
303 FOR_ALL_TAB_WINDOWS(tp, wp) {
304 if (wp->w_buffer && wp->w_buffer->terminal == term) {
305 const uint16_t win_width =
306 (uint16_t)(MAX(0, wp->w_width_inner - win_col_off(wp)));
307 width = MAX(width, win_width);
308 height = (uint16_t)MAX(height, wp->w_height_inner);
309 }
310 }
311
312 // if no window displays the terminal, or such all windows are zero-height,
313 // don't resize the terminal.
314 if ((curheight == height && curwidth == width) || height == 0 || width == 0) {
315 return;
316 }
317
318 vterm_set_size(term->vt, height, width);
319 vterm_screen_flush_damage(term->vts);
320 term->pending_resize = true;
321 invalidate_terminal(term, -1, -1);
322}
323
324void terminal_enter(void)
325{
326 buf_T *buf = curbuf;
327 assert(buf->terminal); // Should only be called when curbuf has a terminal.
328 TerminalState state, *s = &state;
329 memset(s, 0, sizeof(TerminalState));
330 s->term = buf->terminal;
331 stop_insert_mode = false;
332
333 // Ensure the terminal is properly sized. Ideally window size management
334 // code should always have resized the terminal already, but check here to
335 // be sure.
336 terminal_check_size(s->term);
337
338 int save_state = State;
339 s->save_rd = RedrawingDisabled;
340 State = TERM_FOCUS;
341 mapped_ctrl_c |= TERM_FOCUS; // Always map CTRL-C to avoid interrupt.
342 RedrawingDisabled = false;
343
344 // Disable these options in terminal-mode. They are nonsense because cursor is
345 // placed at end of buffer to "follow" output.
346 win_T *save_curwin = curwin;
347 int save_w_p_cul = curwin->w_p_cul;
348 int save_w_p_cuc = curwin->w_p_cuc;
349 curwin->w_p_cul = false;
350 curwin->w_p_cuc = false;
351
352 adjust_topline(s->term, buf, 0); // scroll to end
353 // erase the unfocused cursor
354 invalidate_terminal(s->term, s->term->cursor.row, s->term->cursor.row + 1);
355 showmode();
356 curwin->w_redr_status = true; // For mode() in statusline. #8323
357 ui_busy_start();
358
359 s->state.execute = terminal_execute;
360 s->state.check = terminal_check;
361 state_enter(&s->state);
362
363 restart_edit = 0;
364 State = save_state;
365 RedrawingDisabled = s->save_rd;
366 if (save_curwin == curwin) { // save_curwin may be invalid (window closed)!
367 curwin->w_p_cul = save_w_p_cul;
368 curwin->w_p_cuc = save_w_p_cuc;
369 }
370
371 // draw the unfocused cursor
372 invalidate_terminal(s->term, s->term->cursor.row, s->term->cursor.row + 1);
373 if (curbuf->terminal == s->term && !s->close) {
374 terminal_check_cursor();
375 }
376 unshowmode(true);
377 ui_busy_stop();
378 if (s->close) {
379 bool wipe = s->term->buf_handle != 0;
380 s->term->opts.close_cb(s->term->opts.data);
381 if (wipe) {
382 do_cmdline_cmd("bwipeout!");
383 }
384 }
385}
386
387static void terminal_check_cursor(void)
388{
389 Terminal *term = curbuf->terminal;
390 curwin->w_wrow = term->cursor.row;
391 curwin->w_wcol = term->cursor.col + win_col_off(curwin);
392 curwin->w_cursor.lnum = MIN(curbuf->b_ml.ml_line_count,
393 row_to_linenr(term, term->cursor.row));
394 // Nudge cursor when returning to normal-mode.
395 int off = is_focused(term) ? 0 : (curwin->w_p_rl ? 1 : -1);
396 curwin->w_cursor.col = MAX(0, term->cursor.col + win_col_off(curwin) + off);
397 curwin->w_cursor.coladd = 0;
398 mb_check_adjust_col(curwin);
399}
400
401// Function executed before each iteration of terminal mode.
402// Return:
403// 1 if the iteration should continue normally
404// 0 if the main loop must exit
405static int terminal_check(VimState *state)
406{
407 if (stop_insert_mode) {
408 return 0;
409 }
410
411 terminal_check_cursor();
412
413 if (must_redraw) {
414 update_screen(0);
415 }
416
417 if (need_maketitle) { // Update title in terminal-mode. #7248
418 maketitle();
419 }
420
421 setcursor();
422 ui_flush();
423 return 1;
424}
425
426static int terminal_execute(VimState *state, int key)
427{
428 TerminalState *s = (TerminalState *)state;
429
430 switch (key) {
431 case K_LEFTMOUSE:
432 case K_LEFTDRAG:
433 case K_LEFTRELEASE:
434 case K_MIDDLEMOUSE:
435 case K_MIDDLEDRAG:
436 case K_MIDDLERELEASE:
437 case K_RIGHTMOUSE:
438 case K_RIGHTDRAG:
439 case K_RIGHTRELEASE:
440 case K_MOUSEDOWN:
441 case K_MOUSEUP:
442 if (send_mouse_event(s->term, key)) {
443 return 0;
444 }
445 break;
446
447 case K_EVENT:
448 // We cannot let an event free the terminal yet. It is still needed.
449 s->term->refcount++;
450 multiqueue_process_events(main_loop.events);
451 s->term->refcount--;
452 if (s->term->buf_handle == 0) {
453 s->close = true;
454 return 0;
455 }
456 break;
457
458 case K_COMMAND:
459 do_cmdline(NULL, getcmdkeycmd, NULL, 0);
460 break;
461
462 case Ctrl_N:
463 if (s->got_bsl) {
464 return 0;
465 }
466 FALLTHROUGH;
467
468 default:
469 if (key == Ctrl_BSL && !s->got_bsl) {
470 s->got_bsl = true;
471 break;
472 }
473 if (s->term->closed) {
474 s->close = true;
475 return 0;
476 }
477
478 s->got_bsl = false;
479 terminal_send_key(s->term, key);
480 }
481
482 return curbuf->handle == s->term->buf_handle;
483}
484
485void terminal_destroy(Terminal *term)
486{
487 buf_T *buf = handle_get_buffer(term->buf_handle);
488 if (buf) {
489 term->buf_handle = 0;
490 buf->terminal = NULL;
491 }
492
493 if (!term->refcount) {
494 if (pmap_has(ptr_t)(invalidated_terminals, term)) {
495 // flush any pending changes to the buffer
496 block_autocmds();
497 refresh_terminal(term);
498 unblock_autocmds();
499 pmap_del(ptr_t)(invalidated_terminals, term);
500 }
501 for (size_t i = 0; i < term->sb_current; i++) {
502 xfree(term->sb_buffer[i]);
503 }
504 xfree(term->sb_buffer);
505 vterm_free(term->vt);
506 xfree(term);
507 }
508}
509
510void terminal_send(Terminal *term, char *data, size_t size)
511{
512 if (term->closed) {
513 return;
514 }
515 term->opts.write_cb(data, size, term->opts.data);
516}
517
518void terminal_paste(long count, char_u **y_array, size_t y_size)
519{
520 for (int i = 0; i < count; i++) { // -V756
521 // feed the lines to the terminal
522 for (size_t j = 0; j < y_size; j++) {
523 if (j) {
524 // terminate the previous line
525 terminal_send(curbuf->terminal, "\n", 1);
526 }
527 terminal_send(curbuf->terminal, (char *)y_array[j], STRLEN(y_array[j]));
528 }
529 }
530}
531
532void terminal_flush_output(Terminal *term)
533{
534 size_t len = vterm_output_read(term->vt, term->textbuf,
535 sizeof(term->textbuf));
536 terminal_send(term, term->textbuf, len);
537}
538
539void terminal_send_key(Terminal *term, int c)
540{
541 VTermModifier mod = VTERM_MOD_NONE;
542
543 // Convert K_ZERO back to ASCII
544 if (c == K_ZERO) {
545 c = Ctrl_AT;
546 }
547
548 VTermKey key = convert_key(c, &mod);
549
550 if (key) {
551 vterm_keyboard_key(term->vt, key, mod);
552 } else {
553 vterm_keyboard_unichar(term->vt, (uint32_t)c, mod);
554 }
555
556 terminal_flush_output(term);
557}
558
559void terminal_receive(Terminal *term, char *data, size_t len)
560{
561 if (!data) {
562 return;
563 }
564
565 vterm_input_write(term->vt, data, len);
566 vterm_screen_flush_damage(term->vts);
567}
568
569static int get_rgb(VTermState *state, VTermColor color)
570{
571 vterm_state_convert_color_to_rgb(state, &color);
572 return RGB_(color.rgb.red, color.rgb.green, color.rgb.blue);
573}
574
575
576void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr,
577 int *term_attrs)
578{
579 int height, width;
580 vterm_get_size(term->vt, &height, &width);
581 VTermState *state = vterm_obtain_state(term->vt);
582 assert(linenr);
583 int row = linenr_to_row(term, linenr);
584 if (row >= height) {
585 // Terminal height was decreased but the change wasn't reflected into the
586 // buffer yet
587 return;
588 }
589
590 for (int col = 0; col < width; col++) {
591 VTermScreenCell cell;
592 bool color_valid = fetch_cell(term, row, col, &cell);
593 bool fg_default = !color_valid || VTERM_COLOR_IS_DEFAULT_FG(&cell.fg);
594 bool bg_default = !color_valid || VTERM_COLOR_IS_DEFAULT_BG(&cell.bg);
595
596 // Get the rgb value set by libvterm.
597 int vt_fg = fg_default ? -1 : get_rgb(state, cell.fg);
598 int vt_bg = bg_default ? -1 : get_rgb(state, cell.bg);
599
600 int vt_fg_idx = ((!fg_default && VTERM_COLOR_IS_INDEXED(&cell.fg))
601 ? cell.fg.indexed.idx + 1 : 0);
602 int vt_bg_idx = ((!bg_default && VTERM_COLOR_IS_INDEXED(&cell.bg))
603 ? cell.bg.indexed.idx + 1 : 0);
604
605 int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0)
606 | (cell.attrs.italic ? HL_ITALIC : 0)
607 | (cell.attrs.reverse ? HL_INVERSE : 0)
608 | (cell.attrs.underline ? HL_UNDERLINE : 0)
609 | (cell.attrs.strike ? HL_STRIKETHROUGH: 0);
610
611 int attr_id = 0;
612
613 if (hl_attrs ||!fg_default || !bg_default) {
614 attr_id = hl_get_term_attr(&(HlAttrs) {
615 .cterm_ae_attr = (int16_t)hl_attrs,
616 .cterm_fg_color = vt_fg_idx,
617 .cterm_bg_color = vt_bg_idx,
618 .rgb_ae_attr = (int16_t)hl_attrs,
619 .rgb_fg_color = vt_fg,
620 .rgb_bg_color = vt_bg,
621 .rgb_sp_color = -1,
622 .hl_blend = -1,
623 });
624 }
625
626 if (term->cursor.visible && term->cursor.row == row
627 && term->cursor.col == col) {
628 attr_id = hl_combine_attr(attr_id,
629 is_focused(term) && wp == curwin
630 ? win_hl_attr(wp, HLF_TERM)
631 : win_hl_attr(wp, HLF_TERMNC));
632 }
633
634 term_attrs[col] = attr_id;
635 }
636}
637
638Buffer terminal_buf(const Terminal *term)
639{
640 return term->buf_handle;
641}
642
643// }}}
644// libvterm callbacks {{{
645
646static int term_damage(VTermRect rect, void *data)
647{
648 invalidate_terminal(data, rect.start_row, rect.end_row);
649 return 1;
650}
651
652static int term_moverect(VTermRect dest, VTermRect src, void *data)
653{
654 invalidate_terminal(data, MIN(dest.start_row, src.start_row),
655 MAX(dest.end_row, src.end_row));
656 return 1;
657}
658
659static int term_movecursor(VTermPos new, VTermPos old, int visible,
660 void *data)
661{
662 Terminal *term = data;
663 term->cursor.row = new.row;
664 term->cursor.col = new.col;
665 invalidate_terminal(term, old.row, old.row + 1);
666 invalidate_terminal(term, new.row, new.row + 1);
667 return 1;
668}
669
670static void buf_set_term_title(buf_T *buf, char *title)
671 FUNC_ATTR_NONNULL_ALL
672{
673 Error err = ERROR_INIT;
674 dict_set_var(buf->b_vars,
675 STATIC_CSTR_AS_STRING("term_title"),
676 STRING_OBJ(cstr_as_string(title)),
677 false,
678 false,
679 &err);
680 api_clear_error(&err);
681 status_redraw_buf(buf);
682}
683
684static int term_settermprop(VTermProp prop, VTermValue *val, void *data)
685{
686 Terminal *term = data;
687
688 switch (prop) {
689 case VTERM_PROP_ALTSCREEN:
690 break;
691
692 case VTERM_PROP_CURSORVISIBLE:
693 term->cursor.visible = val->boolean;
694 invalidate_terminal(term, term->cursor.row, term->cursor.row + 1);
695 break;
696
697 case VTERM_PROP_TITLE: {
698 buf_T *buf = handle_get_buffer(term->buf_handle);
699 buf_set_term_title(buf, val->string);
700 break;
701 }
702
703 case VTERM_PROP_MOUSE:
704 term->forward_mouse = (bool)val->number;
705 break;
706
707 default:
708 return 0;
709 }
710
711 return 1;
712}
713
714static int term_bell(void *data)
715{
716 ui_call_bell();
717 return 1;
718}
719
720// Scrollback push handler (from pangoterm).
721static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
722{
723 Terminal *term = data;
724
725 if (!term->sb_size) {
726 return 0;
727 }
728
729 // copy vterm cells into sb_buffer
730 size_t c = (size_t)cols;
731 ScrollbackLine *sbrow = NULL;
732 if (term->sb_current == term->sb_size) {
733 if (term->sb_buffer[term->sb_current - 1]->cols == c) {
734 // Recycle old row if it's the right size
735 sbrow = term->sb_buffer[term->sb_current - 1];
736 } else {
737 xfree(term->sb_buffer[term->sb_current - 1]);
738 }
739
740 // Make room at the start by shifting to the right.
741 memmove(term->sb_buffer + 1, term->sb_buffer,
742 sizeof(term->sb_buffer[0]) * (term->sb_current - 1));
743
744 } else if (term->sb_current > 0) {
745 // Make room at the start by shifting to the right.
746 memmove(term->sb_buffer + 1, term->sb_buffer,
747 sizeof(term->sb_buffer[0]) * term->sb_current);
748 }
749
750 if (!sbrow) {
751 sbrow = xmalloc(sizeof(ScrollbackLine) + c * sizeof(sbrow->cells[0]));
752 sbrow->cols = c;
753 }
754
755 // New row is added at the start of the storage buffer.
756 term->sb_buffer[0] = sbrow;
757 if (term->sb_current < term->sb_size) {
758 term->sb_current++;
759 }
760
761 if (term->sb_pending < (int)term->sb_size) {
762 term->sb_pending++;
763 }
764
765 memcpy(sbrow->cells, cells, sizeof(cells[0]) * c);
766 pmap_put(ptr_t)(invalidated_terminals, term, NULL);
767
768 return 1;
769}
770
771/// Scrollback pop handler (from pangoterm).
772///
773/// @param cols
774/// @param cells VTerm state to update.
775/// @param data Terminal
776static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
777{
778 Terminal *term = data;
779
780 if (!term->sb_current) {
781 return 0;
782 }
783
784 if (term->sb_pending) {
785 term->sb_pending--;
786 }
787
788 ScrollbackLine *sbrow = term->sb_buffer[0];
789 term->sb_current--;
790 // Forget the "popped" row by shifting the rest onto it.
791 memmove(term->sb_buffer, term->sb_buffer + 1,
792 sizeof(term->sb_buffer[0]) * (term->sb_current));
793
794 size_t cols_to_copy = (size_t)cols;
795 if (cols_to_copy > sbrow->cols) {
796 cols_to_copy = sbrow->cols;
797 }
798
799 // copy to vterm state
800 memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy);
801 for (size_t col = cols_to_copy; col < (size_t)cols; col++) {
802 cells[col].chars[0] = 0;
803 cells[col].width = 1;
804 }
805
806 xfree(sbrow);
807 pmap_put(ptr_t)(invalidated_terminals, term, NULL);
808
809 return 1;
810}
811
812// }}}
813// input handling {{{
814
815static void convert_modifiers(int key, VTermModifier *statep)
816{
817 if (mod_mask & MOD_MASK_SHIFT) { *statep |= VTERM_MOD_SHIFT; }
818 if (mod_mask & MOD_MASK_CTRL) { *statep |= VTERM_MOD_CTRL; }
819 if (mod_mask & MOD_MASK_ALT) { *statep |= VTERM_MOD_ALT; }
820
821 switch (key) {
822 case K_S_TAB:
823 case K_S_UP:
824 case K_S_DOWN:
825 case K_S_LEFT:
826 case K_S_RIGHT:
827 case K_S_F1:
828 case K_S_F2:
829 case K_S_F3:
830 case K_S_F4:
831 case K_S_F5:
832 case K_S_F6:
833 case K_S_F7:
834 case K_S_F8:
835 case K_S_F9:
836 case K_S_F10:
837 case K_S_F11:
838 case K_S_F12:
839 *statep |= VTERM_MOD_SHIFT;
840 break;
841
842 case K_C_LEFT:
843 case K_C_RIGHT:
844 *statep |= VTERM_MOD_CTRL;
845 break;
846 }
847}
848
849static VTermKey convert_key(int key, VTermModifier *statep)
850{
851 convert_modifiers(key, statep);
852
853 switch (key) {
854 case K_BS: return VTERM_KEY_BACKSPACE;
855 case K_S_TAB: FALLTHROUGH;
856 case TAB: return VTERM_KEY_TAB;
857 case Ctrl_M: return VTERM_KEY_ENTER;
858 case ESC: return VTERM_KEY_ESCAPE;
859
860 case K_S_UP: FALLTHROUGH;
861 case K_UP: return VTERM_KEY_UP;
862 case K_S_DOWN: FALLTHROUGH;
863 case K_DOWN: return VTERM_KEY_DOWN;
864 case K_S_LEFT: FALLTHROUGH;
865 case K_C_LEFT: FALLTHROUGH;
866 case K_LEFT: return VTERM_KEY_LEFT;
867 case K_S_RIGHT: FALLTHROUGH;
868 case K_C_RIGHT: FALLTHROUGH;
869 case K_RIGHT: return VTERM_KEY_RIGHT;
870
871 case K_INS: return VTERM_KEY_INS;
872 case K_DEL: return VTERM_KEY_DEL;
873 case K_HOME: return VTERM_KEY_HOME;
874 case K_END: return VTERM_KEY_END;
875 case K_PAGEUP: return VTERM_KEY_PAGEUP;
876 case K_PAGEDOWN: return VTERM_KEY_PAGEDOWN;
877
878 case K_K0: FALLTHROUGH;
879 case K_KINS: return VTERM_KEY_KP_0;
880 case K_K1: FALLTHROUGH;
881 case K_KEND: return VTERM_KEY_KP_1;
882 case K_K2: FALLTHROUGH;
883 case K_KDOWN: return VTERM_KEY_KP_2;
884 case K_K3: FALLTHROUGH;
885 case K_KPAGEDOWN: return VTERM_KEY_KP_3;
886 case K_K4: FALLTHROUGH;
887 case K_KLEFT: return VTERM_KEY_KP_4;
888 case K_K5: FALLTHROUGH;
889 case K_KORIGIN: return VTERM_KEY_KP_5;
890 case K_K6: FALLTHROUGH;
891 case K_KRIGHT: return VTERM_KEY_KP_6;
892 case K_K7: FALLTHROUGH;
893 case K_KHOME: return VTERM_KEY_KP_7;
894 case K_K8: FALLTHROUGH;
895 case K_KUP: return VTERM_KEY_KP_8;
896 case K_K9: FALLTHROUGH;
897 case K_KPAGEUP: return VTERM_KEY_KP_9;
898 case K_KDEL: FALLTHROUGH;
899 case K_KPOINT: return VTERM_KEY_KP_PERIOD;
900 case K_KENTER: return VTERM_KEY_KP_ENTER;
901 case K_KPLUS: return VTERM_KEY_KP_PLUS;
902 case K_KMINUS: return VTERM_KEY_KP_MINUS;
903 case K_KMULTIPLY: return VTERM_KEY_KP_MULT;
904 case K_KDIVIDE: return VTERM_KEY_KP_DIVIDE;
905
906 case K_S_F1: FALLTHROUGH;
907 case K_F1: return VTERM_KEY_FUNCTION(1);
908 case K_S_F2: FALLTHROUGH;
909 case K_F2: return VTERM_KEY_FUNCTION(2);
910 case K_S_F3: FALLTHROUGH;
911 case K_F3: return VTERM_KEY_FUNCTION(3);
912 case K_S_F4: FALLTHROUGH;
913 case K_F4: return VTERM_KEY_FUNCTION(4);
914 case K_S_F5: FALLTHROUGH;
915 case K_F5: return VTERM_KEY_FUNCTION(5);
916 case K_S_F6: FALLTHROUGH;
917 case K_F6: return VTERM_KEY_FUNCTION(6);
918 case K_S_F7: FALLTHROUGH;
919 case K_F7: return VTERM_KEY_FUNCTION(7);
920 case K_S_F8: FALLTHROUGH;
921 case K_F8: return VTERM_KEY_FUNCTION(8);
922 case K_S_F9: FALLTHROUGH;
923 case K_F9: return VTERM_KEY_FUNCTION(9);
924 case K_S_F10: FALLTHROUGH;
925 case K_F10: return VTERM_KEY_FUNCTION(10);
926 case K_S_F11: FALLTHROUGH;
927 case K_F11: return VTERM_KEY_FUNCTION(11);
928 case K_S_F12: FALLTHROUGH;
929 case K_F12: return VTERM_KEY_FUNCTION(12);
930
931 case K_F13: return VTERM_KEY_FUNCTION(13);
932 case K_F14: return VTERM_KEY_FUNCTION(14);
933 case K_F15: return VTERM_KEY_FUNCTION(15);
934 case K_F16: return VTERM_KEY_FUNCTION(16);
935 case K_F17: return VTERM_KEY_FUNCTION(17);
936 case K_F18: return VTERM_KEY_FUNCTION(18);
937 case K_F19: return VTERM_KEY_FUNCTION(19);
938 case K_F20: return VTERM_KEY_FUNCTION(20);
939 case K_F21: return VTERM_KEY_FUNCTION(21);
940 case K_F22: return VTERM_KEY_FUNCTION(22);
941 case K_F23: return VTERM_KEY_FUNCTION(23);
942 case K_F24: return VTERM_KEY_FUNCTION(24);
943 case K_F25: return VTERM_KEY_FUNCTION(25);
944 case K_F26: return VTERM_KEY_FUNCTION(26);
945 case K_F27: return VTERM_KEY_FUNCTION(27);
946 case K_F28: return VTERM_KEY_FUNCTION(28);
947 case K_F29: return VTERM_KEY_FUNCTION(29);
948 case K_F30: return VTERM_KEY_FUNCTION(30);
949 case K_F31: return VTERM_KEY_FUNCTION(31);
950 case K_F32: return VTERM_KEY_FUNCTION(32);
951 case K_F33: return VTERM_KEY_FUNCTION(33);
952 case K_F34: return VTERM_KEY_FUNCTION(34);
953 case K_F35: return VTERM_KEY_FUNCTION(35);
954 case K_F36: return VTERM_KEY_FUNCTION(36);
955 case K_F37: return VTERM_KEY_FUNCTION(37);
956
957 default: return VTERM_KEY_NONE;
958 }
959}
960
961static void mouse_action(Terminal *term, int button, int row, int col,
962 bool drag, VTermModifier mod)
963{
964 if (term->pressed_button && (term->pressed_button != button || !drag)) {
965 // release the previous button
966 vterm_mouse_button(term->vt, term->pressed_button, 0, mod);
967 term->pressed_button = 0;
968 }
969
970 // move the mouse
971 vterm_mouse_move(term->vt, row, col, mod);
972
973 if (!term->pressed_button) {
974 // press the button if not already pressed
975 vterm_mouse_button(term->vt, button, 1, mod);
976 term->pressed_button = button;
977 }
978}
979
980// process a mouse event while the terminal is focused. return true if the
981// terminal should lose focus
982static bool send_mouse_event(Terminal *term, int c)
983{
984 int row = mouse_row, col = mouse_col, grid = mouse_grid;
985 win_T *mouse_win = mouse_find_win(&grid, &row, &col);
986 if (mouse_win == NULL) {
987 goto end;
988 }
989
990 if (term->forward_mouse && mouse_win->w_buffer->terminal == term) {
991 // event in the terminal window and mouse events was enabled by the
992 // program. translate and forward the event
993 int button;
994 bool drag = false;
995
996 switch (c) {
997 case K_LEFTDRAG: drag = true; FALLTHROUGH;
998 case K_LEFTMOUSE: button = 1; break;
999 case K_MIDDLEDRAG: drag = true; FALLTHROUGH;
1000 case K_MIDDLEMOUSE: button = 2; break;
1001 case K_RIGHTDRAG: drag = true; FALLTHROUGH;
1002 case K_RIGHTMOUSE: button = 3; break;
1003 case K_MOUSEDOWN: button = 4; break;
1004 case K_MOUSEUP: button = 5; break;
1005 default: return false;
1006 }
1007
1008 mouse_action(term, button, row, col, drag, 0);
1009 size_t len = vterm_output_read(term->vt, term->textbuf,
1010 sizeof(term->textbuf));
1011 terminal_send(term, term->textbuf, (size_t)len);
1012 return false;
1013 }
1014
1015 if (c == K_MOUSEDOWN || c == K_MOUSEUP) {
1016 win_T *save_curwin = curwin;
1017 // switch window/buffer to perform the scroll
1018 curwin = mouse_win;
1019 curbuf = curwin->w_buffer;
1020 int direction = c == K_MOUSEDOWN ? MSCR_DOWN : MSCR_UP;
1021 if (mod_mask & (MOD_MASK_SHIFT | MOD_MASK_CTRL)) {
1022 scroll_redraw(direction, curwin->w_botline - curwin->w_topline);
1023 } else {
1024 scroll_redraw(direction, 3L);
1025 }
1026
1027 curwin->w_redr_status = true;
1028 curwin = save_curwin;
1029 curbuf = curwin->w_buffer;
1030 redraw_win_later(mouse_win, NOT_VALID);
1031 invalidate_terminal(term, -1, -1);
1032 // Only need to exit focus if the scrolled window is the terminal window
1033 return mouse_win == curwin;
1034 }
1035
1036end:
1037 ins_char_typebuf(c);
1038 return true;
1039}
1040
1041// }}}
1042// terminal buffer refresh & misc {{{
1043
1044
1045static void fetch_row(Terminal *term, int row, int end_col)
1046{
1047 int col = 0;
1048 size_t line_len = 0;
1049 char *ptr = term->textbuf;
1050
1051 while (col < end_col) {
1052 VTermScreenCell cell;
1053 fetch_cell(term, row, col, &cell);
1054 int cell_len = 0;
1055 if (cell.chars[0]) {
1056 for (int i = 0; cell.chars[i]; i++) {
1057 cell_len += utf_char2bytes((int)cell.chars[i],
1058 (uint8_t *)ptr + cell_len);
1059 }
1060 } else {
1061 *ptr = ' ';
1062 cell_len = 1;
1063 }
1064 char c = *ptr;
1065 ptr += cell_len;
1066 if (c != ' ') {
1067 // only increase the line length if the last character is not whitespace
1068 line_len = (size_t)(ptr - term->textbuf);
1069 }
1070 col += cell.width;
1071 }
1072
1073 // trim trailing whitespace
1074 term->textbuf[line_len] = 0;
1075}
1076
1077static bool fetch_cell(Terminal *term, int row, int col,
1078 VTermScreenCell *cell)
1079{
1080 if (row < 0) {
1081 ScrollbackLine *sbrow = term->sb_buffer[-row - 1];
1082 if ((size_t)col < sbrow->cols) {
1083 *cell = sbrow->cells[col];
1084 } else {
1085 // fill the pointer with an empty cell
1086 *cell = (VTermScreenCell) {
1087 .chars = { 0 },
1088 .width = 1,
1089 };
1090 return false;
1091 }
1092 } else {
1093 vterm_screen_get_cell(term->vts, (VTermPos){.row = row, .col = col},
1094 cell);
1095 }
1096 return true;
1097}
1098
1099// queue a terminal instance for refresh
1100static void invalidate_terminal(Terminal *term, int start_row, int end_row)
1101{
1102 if (start_row != -1 && end_row != -1) {
1103 term->invalid_start = MIN(term->invalid_start, start_row);
1104 term->invalid_end = MAX(term->invalid_end, end_row);
1105 }
1106
1107 pmap_put(ptr_t)(invalidated_terminals, term, NULL);
1108 if (!refresh_pending) {
1109 time_watcher_start(&refresh_timer, refresh_timer_cb, REFRESH_DELAY, 0);
1110 refresh_pending = true;
1111 }
1112}
1113
1114static void refresh_terminal(Terminal *term)
1115{
1116 buf_T *buf = handle_get_buffer(term->buf_handle);
1117 bool valid = true;
1118 if (!buf || !(valid = buf_valid(buf))) {
1119 // Destroyed by `close_buffer`. Do not do anything else.
1120 if (!valid) {
1121 term->buf_handle = 0;
1122 }
1123 return;
1124 }
1125 long ml_before = buf->b_ml.ml_line_count;
1126
1127 // refresh_ functions assume the terminal buffer is current
1128 aco_save_T aco;
1129 aucmd_prepbuf(&aco, buf);
1130 refresh_size(term, buf);
1131 refresh_scrollback(term, buf);
1132 refresh_screen(term, buf);
1133 aucmd_restbuf(&aco);
1134
1135 long ml_added = buf->b_ml.ml_line_count - ml_before;
1136 adjust_topline(term, buf, ml_added);
1137}
1138// Calls refresh_terminal() on all invalidated_terminals.
1139static void refresh_timer_cb(TimeWatcher *watcher, void *data)
1140{
1141 refresh_pending = false;
1142 if (exiting) { // Cannot redraw (requires event loop) during teardown/exit.
1143 return;
1144 }
1145 Terminal *term;
1146 void *stub; (void)(stub);
1147 // don't process autocommands while updating terminal buffers
1148 block_autocmds();
1149 map_foreach(invalidated_terminals, term, stub, {
1150 refresh_terminal(term);
1151 });
1152 pmap_clear(ptr_t)(invalidated_terminals);
1153 unblock_autocmds();
1154}
1155
1156static void refresh_size(Terminal *term, buf_T *buf)
1157{
1158 if (!term->pending_resize || term->closed) {
1159 return;
1160 }
1161
1162 term->pending_resize = false;
1163 int width, height;
1164 vterm_get_size(term->vt, &height, &width);
1165 term->invalid_start = 0;
1166 term->invalid_end = height;
1167 term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data);
1168}
1169
1170/// Adjusts scrollback storage after 'scrollback' option changed.
1171static void on_scrollback_option_changed(Terminal *term, buf_T *buf)
1172{
1173 if (buf->b_p_scbk < 1) { // Local 'scrollback' was set to -1.
1174 buf->b_p_scbk = SB_MAX;
1175 }
1176 const size_t scbk = (size_t)buf->b_p_scbk;
1177 assert(term->sb_current < SIZE_MAX);
1178 if (term->sb_pending > 0) { // Pending rows must be processed first.
1179 abort();
1180 }
1181
1182 // Delete lines exceeding the new 'scrollback' limit.
1183 if (scbk < term->sb_current) {
1184 size_t diff = term->sb_current - scbk;
1185 for (size_t i = 0; i < diff; i++) {
1186 ml_delete(1, false);
1187 term->sb_current--;
1188 xfree(term->sb_buffer[term->sb_current]);
1189 }
1190 deleted_lines(1, (long)diff);
1191 }
1192
1193 // Resize the scrollback storage.
1194 size_t sb_region = sizeof(ScrollbackLine *) * scbk;
1195 if (scbk != term->sb_size) {
1196 term->sb_buffer = xrealloc(term->sb_buffer, sb_region);
1197 }
1198
1199 term->sb_size = scbk;
1200}
1201
1202// Refresh the scrollback of an invalidated terminal.
1203static void refresh_scrollback(Terminal *term, buf_T *buf)
1204{
1205 int width, height;
1206 vterm_get_size(term->vt, &height, &width);
1207
1208 while (term->sb_pending > 0) {
1209 // This means that either the window height has decreased or the screen
1210 // became full and libvterm had to push all rows up. Convert the first
1211 // pending scrollback row into a string and append it just above the visible
1212 // section of the buffer
1213 if (((int)buf->b_ml.ml_line_count - height) >= (int)term->sb_size) {
1214 // scrollback full, delete lines at the top
1215 ml_delete(1, false);
1216 deleted_lines(1, 1);
1217 }
1218 fetch_row(term, -term->sb_pending, width);
1219 int buf_index = (int)buf->b_ml.ml_line_count - height;
1220 ml_append(buf_index, (uint8_t *)term->textbuf, 0, false);
1221 appended_lines(buf_index, 1);
1222 term->sb_pending--;
1223 }
1224
1225 // Remove extra lines at the bottom
1226 int max_line_count = (int)term->sb_current + height;
1227 while (buf->b_ml.ml_line_count > max_line_count) {
1228 ml_delete(buf->b_ml.ml_line_count, false);
1229 deleted_lines(buf->b_ml.ml_line_count, 1);
1230 }
1231
1232 on_scrollback_option_changed(term, buf);
1233}
1234
1235// Refresh the screen (visible part of the buffer when the terminal is
1236// focused) of a invalidated terminal
1237static void refresh_screen(Terminal *term, buf_T *buf)
1238{
1239 int changed = 0;
1240 int added = 0;
1241 int height;
1242 int width;
1243 vterm_get_size(term->vt, &height, &width);
1244 // Terminal height may have decreased before `invalid_end` reflects it.
1245 term->invalid_end = MIN(term->invalid_end, height);
1246
1247 for (int r = term->invalid_start, linenr = row_to_linenr(term, r);
1248 r < term->invalid_end; r++, linenr++) {
1249 fetch_row(term, r, width);
1250
1251 if (linenr <= buf->b_ml.ml_line_count) {
1252 ml_replace(linenr, (uint8_t *)term->textbuf, true);
1253 changed++;
1254 } else {
1255 ml_append(linenr - 1, (uint8_t *)term->textbuf, 0, false);
1256 added++;
1257 }
1258 }
1259
1260 int change_start = row_to_linenr(term, term->invalid_start);
1261 int change_end = change_start + changed;
1262 changed_lines(change_start, 0, change_end, added, true);
1263 term->invalid_start = INT_MAX;
1264 term->invalid_end = -1;
1265}
1266
1267static void adjust_topline(Terminal *term, buf_T *buf, long added)
1268{
1269 FOR_ALL_TAB_WINDOWS(tp, wp) {
1270 if (wp->w_buffer == buf) {
1271 linenr_T ml_end = buf->b_ml.ml_line_count;
1272 bool following = ml_end == wp->w_cursor.lnum + added; // cursor at end?
1273
1274 if (following || (wp == curwin && is_focused(term))) {
1275 // "Follow" the terminal output
1276 wp->w_cursor.lnum = ml_end;
1277 set_topline(wp, MAX(wp->w_cursor.lnum - wp->w_height_inner + 1, 1));
1278 } else {
1279 // Ensure valid cursor for each window displaying this terminal.
1280 wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, ml_end);
1281 }
1282 mb_check_adjust_col(wp);
1283 }
1284 }
1285}
1286
1287static int row_to_linenr(Terminal *term, int row)
1288{
1289 return row != INT_MAX ? row + (int)term->sb_current + 1 : INT_MAX;
1290}
1291
1292static int linenr_to_row(Terminal *term, int linenr)
1293{
1294 return linenr - (int)term->sb_current - 1;
1295}
1296
1297static bool is_focused(Terminal *term)
1298{
1299 return State & TERM_FOCUS && curbuf->terminal == term;
1300}
1301
1302static char *get_config_string(char *key)
1303{
1304 Error err = ERROR_INIT;
1305 // Only called from terminal_open where curbuf->terminal is the context.
1306 Object obj = dict_get_value(curbuf->b_vars, cstr_as_string(key), &err);
1307 api_clear_error(&err);
1308 if (obj.type == kObjectTypeNil) {
1309 obj = dict_get_value(&globvardict, cstr_as_string(key), &err);
1310 api_clear_error(&err);
1311 }
1312 if (obj.type == kObjectTypeString) {
1313 return obj.data.string.data;
1314 }
1315 api_free_object(obj);
1316 return NULL;
1317}
1318
1319// }}}
1320
1321// vim: foldmethod=marker
1322