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 | |
81 | typedef 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 | |
97 | static TimeWatcher refresh_timer; |
98 | static bool refresh_pending = false; |
99 | |
100 | typedef struct { |
101 | size_t cols; |
102 | VTermScreenCell cells[]; |
103 | } ScrollbackLine; |
104 | |
105 | struct 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 | |
144 | static 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 | |
154 | static PMap(ptr_t) *invalidated_terminals; |
155 | |
156 | void 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 | |
164 | void 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 | |
174 | Terminal *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 | |
253 | void 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 | |
292 | void 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 | |
324 | void 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 | |
387 | static 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 |
405 | static 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 | |
426 | static 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 | |
485 | void 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 | |
510 | void 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 | |
518 | void 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 | |
532 | void 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 | |
539 | void 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 | |
559 | void 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 | |
569 | static 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 | |
576 | void 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 | |
638 | Buffer terminal_buf(const Terminal *term) |
639 | { |
640 | return term->buf_handle; |
641 | } |
642 | |
643 | // }}} |
644 | // libvterm callbacks {{{ |
645 | |
646 | static int term_damage(VTermRect rect, void *data) |
647 | { |
648 | invalidate_terminal(data, rect.start_row, rect.end_row); |
649 | return 1; |
650 | } |
651 | |
652 | static 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 | |
659 | static 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 | |
670 | static 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 | |
684 | static 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 | |
714 | static int term_bell(void *data) |
715 | { |
716 | ui_call_bell(); |
717 | return 1; |
718 | } |
719 | |
720 | // Scrollback push handler (from pangoterm). |
721 | static 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 |
776 | static 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 | |
815 | static 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 | |
849 | static 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 | |
961 | static 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 |
982 | static 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 | |
1036 | end: |
1037 | ins_char_typebuf(c); |
1038 | return true; |
1039 | } |
1040 | |
1041 | // }}} |
1042 | // terminal buffer refresh & misc {{{ |
1043 | |
1044 | |
1045 | static 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 | |
1077 | static 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 |
1100 | static 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 | |
1114 | static 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. |
1139 | static 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 | |
1156 | static 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. |
1171 | static 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. |
1203 | static 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 |
1237 | static 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 | |
1267 | static 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 | |
1287 | static int row_to_linenr(Terminal *term, int row) |
1288 | { |
1289 | return row != INT_MAX ? row + (int)term->sb_current + 1 : INT_MAX; |
1290 | } |
1291 | |
1292 | static int linenr_to_row(Terminal *term, int linenr) |
1293 | { |
1294 | return linenr - (int)term->sb_current - 1; |
1295 | } |
1296 | |
1297 | static bool is_focused(Terminal *term) |
1298 | { |
1299 | return State & TERM_FOCUS && curbuf->terminal == term; |
1300 | } |
1301 | |
1302 | static 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 | |