// VT220/xterm-like terminal emulator.
// 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 sent to VTerm instances by calling
// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that
// must be fed back to the master fd.
//
// Nvim buffers are used as the display mechanism for both the visible screen
// and the scrollback buffer.
//
// 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 nvim 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 nvim buffer displaying lines
// that were previously invisible.
//
// The vterm->nvim synchronization is performed in intervals of 10 milliseconds,
// to minimize screen updates 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 nvim in the future)
//
// Inspired by: vimshell http://www.wana.at/vimshell
//              Conque https://code.google.com/p/conque
// Some code from pangoterm http://www.leonerd.org.uk/code/pangoterm

#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "klib/kvec.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/change.h"
#include "nvim/channel.h"
#include "nvim/channel_defs.h"
#include "nvim/cursor.h"
#include "nvim/cursor_shape.h"
#include "nvim/drawline.h"
#include "nvim/drawscreen.h"
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/vars.h"
#include "nvim/event/defs.h"
#include "nvim/event/loop.h"
#include "nvim/event/multiqueue.h"
#include "nvim/event/time.h"
#include "nvim/ex_docmd.h"
#include "nvim/getchar.h"
#include "nvim/globals.h"
#include "nvim/grid.h"
#include "nvim/highlight.h"
#include "nvim/highlight_defs.h"
#include "nvim/highlight_group.h"
#include "nvim/keycodes.h"
#include "nvim/macros_defs.h"
#include "nvim/main.h"
#include "nvim/map_defs.h"
#include "nvim/mark.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/mouse.h"
#include "nvim/move.h"
#include "nvim/msgpack_rpc/channel_defs.h"
#include "nvim/normal_defs.h"
#include "nvim/ops.h"
#include "nvim/option.h"
#include "nvim/option_defs.h"
#include "nvim/option_vars.h"
#include "nvim/optionstr.h"
#include "nvim/pos_defs.h"
#include "nvim/state.h"
#include "nvim/state_defs.h"
#include "nvim/strings.h"
#include "nvim/terminal.h"
#include "nvim/types_defs.h"
#include "nvim/ui.h"
#include "nvim/vim_defs.h"
#include "nvim/vterm/keyboard.h"
#include "nvim/vterm/mouse.h"
#include "nvim/vterm/parser.h"
#include "nvim/vterm/pen.h"
#include "nvim/vterm/screen.h"
#include "nvim/vterm/state.h"
#include "nvim/vterm/vterm.h"
#include "nvim/vterm/vterm_keycodes_defs.h"
#include "nvim/window.h"

typedef struct {
  VimState state;
  Terminal *term;
  int save_rd;          ///< saved value of RedrawingDisabled
  bool close;
  bool got_bsl;         ///< if the last input was <C-\>
  bool got_bsl_o;       ///< if left terminal mode with <c-\><c-o>
  bool cursor_visible;  ///< cursor's current visibility; ensures matched busy_start/stop UI events

  // These fields remember the prior values of window options before entering terminal mode.
  // Valid only when save_curwin_handle != 0.
  handle_T save_curwin_handle;
  bool save_w_p_cul;
  char *save_w_p_culopt;
  uint8_t save_w_p_culopt_flags;
  int save_w_p_cuc;
  OptInt save_w_p_so;
  OptInt save_w_p_siso;
} TerminalState;

#include "terminal.c.generated.h"

// Delay for refreshing the terminal buffer after receiving updates from
// libvterm. Improves performance when receiving large bursts of data.
#define REFRESH_DELAY 10

#define TEXTBUF_SIZE      0x1fff
#define SELECTIONBUF_SIZE 0x0400

static TimeWatcher refresh_timer;
static bool refresh_pending = false;

typedef struct {
  size_t cols;
  VTermScreenCell cells[];
} ScrollbackLine;

struct terminal {
  TerminalOptions opts;  // options passed to terminal_alloc()
  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[TEXTBUF_SIZE];

  ScrollbackLine **sb_buffer;  ///< Scrollback storage.
  size_t sb_current;           ///< Lines stored in sb_buffer.
  size_t sb_size;              ///< Capacity of sb_buffer.
  /// "virtual index" that points to the first sb_buffer row that we need to
  /// push to the terminal buffer when refreshing the scrollback.
  int sb_pending;
  size_t sb_deleted;      ///< Lines deleted from sb_buffer.
  size_t old_sb_deleted;  ///< Value of sb_deleted on last refresh_scrollback().
  /// Lines in the terminal buffer belonging to the screen instead of the scrollback.
  int old_height;

  char *title;     // VTermStringFragment buffer
  size_t title_len;
  size_t title_size;

  // buf_T instance that acts as a "drawing surface" for libvterm
  // we can't store a direct reference to the buffer because the
  // refresh_timer_cb may be called after the buffer was freed, and there's
  // no way to know if the memory was reused.
  handle_T buf_handle;
  bool in_altscreen;
  // program suspended
  bool suspended;
  // program exited
  bool closed;
  // when true, the terminal's destruction is already enqueued.
  bool destroy;

  // some vterm properties
  bool forward_mouse;
  int invalid_start, invalid_end;   // invalid rows in libvterm screen
  struct {
    int row, col;
    int shape;
    bool visible;  ///< Terminal wants to show cursor.
                   ///< `TerminalState.cursor_visible` indicates whether it is actually shown.
    bool blink;
  } cursor;

  struct {
    bool resize;          ///< pending width/height
    bool cursor;          ///< pending cursor shape or blink change
    StringBuilder *send;  ///< When there is a pending TermRequest autocommand, block and store input.
    MultiQueue *events;   ///< Events waiting for refresh.
  } pending;

  bool streamed_paste;  ///< Streamed pasting
  bool theme_updates;  ///< Send a theme update notification when 'bg' changes
  bool synchronized_output;  ///< Mode 2026: suppress redraws until end of synchronized update
  bool sync_flush_pending;   ///< Set when mode 2026 ends; triggers immediate buffer refresh

  bool color_set[16];

  char *selection_buffer;  ///< libvterm selection buffer
  StringBuilder selection;  ///< Growable array containing full selection data

  StringBuilder termrequest_buffer;  ///< Growable array containing unfinished request sequence
  VTermTerminator termrequest_terminator;  ///< Terminator (BEL or ST) used in the termrequest

  size_t refcount;                  // reference count
};

static VTermScreenCallbacks vterm_screen_callbacks = {
  .damage = term_damage,
  .moverect = term_moverect,
  .movecursor = term_movecursor,
  .settermprop = term_settermprop,
  .bell = term_bell,
  .theme = term_theme,
  .sb_pushline = term_sb_push,  // Called before a line goes offscreen.
  .sb_popline = term_sb_pop,
  .sb_clear = term_sb_clear,
};

static VTermSelectionCallbacks vterm_selection_callbacks = {
  .set = term_selection_set,
  // For security reasons we don't support querying the system clipboard from the embedded terminal
  .query = NULL,
};

static Set(ptr_t) invalidated_terminals = SET_INIT;

static void emit_termrequest(void **argv)
{
  handle_T buf_handle = (handle_T)(intptr_t)argv[0];
  char *sequence = argv[1];
  size_t sequence_length = (size_t)argv[2];
  StringBuilder *pending_send = argv[3];
  int row = (int)(intptr_t)argv[4];
  int col = (int)(intptr_t)argv[5];
  size_t sb_deleted = (size_t)(intptr_t)argv[6];
  VTermTerminator terminator = (VTermTerminator)(intptr_t)argv[7];

  buf_T *buf = handle_get_buffer(buf_handle);
  if (!buf || buf->terminal == NULL) {  // Terminal already closed.
    xfree(sequence);
    kv_destroy(*pending_send);
    xfree(pending_send);
    return;
  }
  Terminal *term = buf->terminal;

  if (term->sb_pending > 0) {
    // Don't emit the event while there is pending scrollback because we need
    // the buffer contents to be fully updated. If this is the case, schedule
    // the event onto the pending queue where it will be executed after the
    // terminal is refreshed and the pending scrollback is cleared.
    multiqueue_put(term->pending.events, emit_termrequest, argv[0], argv[1], argv[2],
                   argv[3], argv[4], argv[5], argv[6], argv[7]);
    return;
  }

  set_vim_var_string(VV_TERMREQUEST, sequence, (ptrdiff_t)sequence_length);

  MAXSIZE_TEMP_ARRAY(cursor, 2);
  ADD_C(cursor, INTEGER_OBJ(row - (int64_t)(term->sb_deleted - sb_deleted)));
  ADD_C(cursor, INTEGER_OBJ(col));

  MAXSIZE_TEMP_DICT(data, 3);
  String termrequest = { .data = sequence, .size = sequence_length };
  PUT_C(data, "sequence", STRING_OBJ(termrequest));
  PUT_C(data, "cursor", ARRAY_OBJ(cursor));
  PUT_C(data, "terminator",
        terminator ==
        VTERM_TERMINATOR_BEL ? STATIC_CSTR_AS_OBJ("\x07") : STATIC_CSTR_AS_OBJ("\x1b\\"));

  term->refcount++;
  apply_autocmds_group(EVENT_TERMREQUEST, NULL, NULL, true, AUGROUP_ALL, buf, NULL,
                       &DICT_OBJ(data));
  term->refcount--;
  xfree(sequence);

  StringBuilder *term_pending_send = term->pending.send;
  term->pending.send = NULL;
  if (kv_size(*pending_send)) {
    terminal_send(term, pending_send->items, pending_send->size);
    kv_destroy(*pending_send);
  }
  if (term_pending_send != pending_send) {
    term->pending.send = term_pending_send;
  }
  xfree(pending_send);

  // Terminal buffer closed during TermRequest in Normal mode: destroy the terminal.
  // In Terminal mode term->refcount should still be non-zero here.
  if (term->buf_handle == 0 && !term->refcount) {
    term->destroy = true;
    term->opts.close_cb(term->opts.data);
  }
}

static void schedule_termrequest(Terminal *term)
{
  term->pending.send = xmalloc(sizeof(StringBuilder));
  kv_init(*term->pending.send);

  int line = row_to_linenr(term, term->cursor.row);
  multiqueue_put(main_loop.events, emit_termrequest, (void *)(intptr_t)term->buf_handle,
                 xmemdup(term->termrequest_buffer.items, term->termrequest_buffer.size),
                 (void *)(intptr_t)term->termrequest_buffer.size, term->pending.send,
                 (void *)(intptr_t)line, (void *)(intptr_t)term->cursor.col,
                 (void *)(intptr_t)term->sb_deleted,
                 (void *)(intptr_t)term->termrequest_terminator);
}

static int parse_osc8(const char *str, int *attr)
  FUNC_ATTR_NONNULL_ALL
{
  // Parse the URI from the OSC 8 sequence and add the URL to our URL set.
  // Skip the ID, we don't use it (for now)
  size_t i = 0;
  for (; str[i] != NUL; i++) {
    if (str[i] == ';') {
      break;
    }
  }

  if (str[i] != ';') {
    // Invalid OSC sequence
    return 0;
  }

  // Move past the semicolon
  i++;

  if (str[i] == NUL) {
    // Empty OSC 8, no URL
    *attr = 0;
    return 1;
  }

  *attr = hl_add_url(0, str + i);
  return 1;
}

static int on_osc(int command, VTermStringFragment frag, void *user)
  FUNC_ATTR_NONNULL_ALL
{
  Terminal *term = user;

  if (frag.str == NULL || frag.len == 0) {
    return 0;
  }

  if (command != 8 && !has_event(EVENT_TERMREQUEST)) {
    return 1;
  }

  if (frag.initial) {
    kv_size(term->termrequest_buffer) = 0;
    kv_printf(term->termrequest_buffer, "\x1b]%d;", command);
  }
  kv_concat_len(term->termrequest_buffer, frag.str, frag.len);
  if (frag.final) {
    term->termrequest_terminator = frag.terminator;
    if (has_event(EVENT_TERMREQUEST)) {
      schedule_termrequest(term);
    }
    if (command == 8) {
      kv_push(term->termrequest_buffer, NUL);
      const size_t off = STRLEN_LITERAL("\x1b]8;");
      int attr = 0;
      if (parse_osc8(term->termrequest_buffer.items + off, &attr)) {
        VTermState *state = vterm_obtain_state(term->vt);
        VTermValue value = { .number = attr };
        vterm_state_set_penattr(state, VTERM_ATTR_URI, VTERM_VALUETYPE_INT, &value);
      }
    }
  }
  return 1;
}

static int on_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user)
{
  Terminal *term = user;

  if (command == NULL || frag.str == NULL) {
    return 0;
  }
  if (!has_event(EVENT_TERMREQUEST)) {
    return 1;
  }

  if (frag.initial) {
    kv_size(term->termrequest_buffer) = 0;
    kv_printf(term->termrequest_buffer, "\x1bP%.*s", (int)commandlen, command);
  }
  kv_concat_len(term->termrequest_buffer, frag.str, frag.len);
  if (frag.final) {
    term->termrequest_terminator = frag.terminator;
    schedule_termrequest(term);
  }
  return 1;
}

static int on_apc(VTermStringFragment frag, void *user)
{
  Terminal *term = user;
  if (frag.str == NULL || frag.len == 0) {
    return 0;
  }

  if (!has_event(EVENT_TERMREQUEST)) {
    return 1;
  }

  if (frag.initial) {
    kv_size(term->termrequest_buffer) = 0;
    kv_printf(term->termrequest_buffer, "\x1b_");
  }
  kv_concat_len(term->termrequest_buffer, frag.str, frag.len);
  if (frag.final) {
    term->termrequest_terminator = frag.terminator;
    schedule_termrequest(term);
  }
  return 1;
}

static VTermStateFallbacks vterm_fallbacks = {
  .control = NULL,
  .csi = NULL,
  .osc = on_osc,
  .dcs = on_dcs,
  .apc = on_apc,
  .pm = NULL,
  .sos = NULL,
};

void terminal_init(void)
{
  time_watcher_init(&main_loop, &refresh_timer, NULL);
  // refresh_timer_cb will redraw the screen which can call vimscript
  refresh_timer.events = multiqueue_new_child(main_loop.events);
}

void terminal_teardown(void)
{
  time_watcher_stop(&refresh_timer);
  multiqueue_free(refresh_timer.events);
  time_watcher_close(&refresh_timer, NULL);
  set_destroy(ptr_t, &invalidated_terminals);
  // terminal_destroy might be called after terminal_teardown is invoked
  // make sure it is in an empty, valid state
  invalidated_terminals = (Set(ptr_t)) SET_INIT;
}

static void term_output_callback(const char *s, size_t len, void *user_data)
{
  terminal_send((Terminal *)user_data, s, len);
}

/// Allocates a terminal's scrollback buffer if it hasn't been allocated yet.
/// Does nothing if it's already allocated, unlike adjust_scrollback().
///
/// @param term Terminal instance.
/// @param buf  The terminal's buffer, or NULL to get it from buf_handle.
///
/// @return whether the terminal now has a scrollback buffer.
static bool term_may_alloc_scrollback(Terminal *term, buf_T *buf)
{
  if (term->sb_buffer != NULL) {
    return true;
  }
  if (buf == NULL) {
    buf = handle_get_buffer(term->buf_handle);
    if (buf == NULL) {  // No need to allocate scrollback if buffer is deleted.
      return false;
    }
  }

  if (buf->b_p_scbk < 1) {
    buf->b_p_scbk = SB_MAX;
  }
  // Configure the scrollback buffer.
  term->sb_size = (size_t)buf->b_p_scbk;
  term->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * term->sb_size);
  return true;
}

// public API {{{

/// Allocates a terminal instance and initializes terminal properties.
///
/// The PTY process (TerminalOptions.data) was already started by jobstart(),
/// via ex_terminal() or the term:// BufReadCmd.
///
/// @param buf Buffer used for presentation of the terminal.
/// @param opts PTY process channel, various terminal properties and callbacks.
///
/// @return the terminal instance.
Terminal *terminal_alloc(buf_T *buf, TerminalOptions opts)
  FUNC_ATTR_NONNULL_ALL
{
  // Create a new terminal instance and configure it
  Terminal *term = xcalloc(1, sizeof(Terminal));
  term->opts = opts;

  // Associate the terminal instance with the new buffer
  term->buf_handle = buf->handle;
  buf->terminal = term;
  // Create VTerm
  term->vt = vterm_new(opts.height, opts.width);
  vterm_set_utf8(term->vt, 1);
  // Setup state
  VTermState *state = vterm_obtain_state(term->vt);
  // Set up screen
  term->vts = vterm_obtain_screen(term->vt);
  vterm_screen_enable_altscreen(term->vts, true);
  vterm_screen_enable_reflow(term->vts, true);
  // delete empty lines at the end of the buffer
  vterm_screen_set_callbacks(term->vts, &vterm_screen_callbacks, term);
  vterm_screen_set_unrecognised_fallbacks(term->vts, &vterm_fallbacks, term);
  vterm_screen_set_damage_merge(term->vts, VTERM_DAMAGE_SCROLL);
  vterm_screen_reset(term->vts, 1);
  vterm_output_set_callback(term->vt, term_output_callback, term);

  term->selection_buffer = xcalloc(SELECTIONBUF_SIZE, 1);
  vterm_state_set_selection_callbacks(state, &vterm_selection_callbacks, term,
                                      term->selection_buffer, SELECTIONBUF_SIZE);

  VTermValue cursor_shape;
  switch (shape_table[SHAPE_IDX_TERM].shape) {
  case SHAPE_BLOCK:
    cursor_shape.number = VTERM_PROP_CURSORSHAPE_BLOCK;
    break;
  case SHAPE_HOR:
    cursor_shape.number = VTERM_PROP_CURSORSHAPE_UNDERLINE;
    break;
  case SHAPE_VER:
    cursor_shape.number = VTERM_PROP_CURSORSHAPE_BAR_LEFT;
    break;
  }
  vterm_state_set_termprop(state, VTERM_PROP_CURSORSHAPE, &cursor_shape);

  VTermValue cursor_blink;
  if (shape_table[SHAPE_IDX_TERM].blinkon != 0 && shape_table[SHAPE_IDX_TERM].blinkoff != 0) {
    cursor_blink.boolean = true;
  } else {
    cursor_blink.boolean = false;
  }
  vterm_state_set_termprop(state, VTERM_PROP_CURSORBLINK, &cursor_blink);

  // 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.
  term->invalid_start = 0;
  term->invalid_end = opts.height;

  // Create a separate queue for events which need to wait for a terminal
  // refresh. We cannot reschedule events back onto the main queue because this
  // can create an infinite loop (#32753).
  // This queue is never processed directly: when the terminal is refreshed, all
  // events from this queue are copied back onto the main event queue.
  term->pending.events = multiqueue_new(NULL, NULL);

  if (!(buf->b_ml.ml_flags & ML_EMPTY)) {
    linenr_T line_count = buf->b_ml.ml_line_count;
    while (!(buf->b_ml.ml_flags & ML_EMPTY)) {
      ml_delete_buf(buf, 1, false);
    }
    deleted_lines_buf(buf, 1, line_count);
  }
  term->old_height = 1;

  return term;
}

/// Triggers TermOpen and allocates terminal scrollback buffer.
///
/// @param termpp  Pointer to the terminal channel's `term` field.
/// @param buf     Buffer used for presentation of the terminal.
void terminal_open(Terminal **termpp, buf_T *buf)
  FUNC_ATTR_NONNULL_ALL
{
  Terminal *term = *termpp;
  assert(term != NULL);

  aco_save_T aco;
  aucmd_prepbuf(&aco, buf);

  if (term->sb_buffer != NULL) {
    // If scrollback has been allocated by autocommands between terminal_alloc()
    // and terminal_open(), it also needs to be refreshed.
    refresh_scrollback(term, buf);
  } else {
    assert(term->invalid_start >= 0);
  }
  refresh_screen(term, buf);
  buf->b_locked++;
  set_option_value(kOptBuftype, STATIC_CSTR_AS_OPTVAL("terminal"), OPT_LOCAL);
  buf->b_locked--;

  if (buf->b_ffname != NULL) {
    buf_set_term_title(buf, buf->b_ffname, strlen(buf->b_ffname));
  }
  RESET_BINDING(curwin);
  // Reset cursor in current window.
  curwin->w_cursor = (pos_T){ .lnum = 1, .col = 0, .coladd = 0 };

  // Apply TermOpen autocmds _before_ configuring the scrollback buffer, to avoid
  // over-allocating in case TermOpen reduces 'scrollback'.
  // In the rare case where TermOpen polls for events, the scrollback buffer will be
  // allocated anyway if needed.
  apply_autocmds(EVENT_TERMOPEN, NULL, NULL, false, buf);

  aucmd_restbuf(&aco);

  if (*termpp == NULL || term->buf_handle == 0) {
    return;  // Terminal has already been destroyed.
  }

  // Local 'scrollback' _after_ autocmds.
  if (!term_may_alloc_scrollback(term, buf)) {
    abort();
  }

  VTermState *state = vterm_obtain_state(term->vt);
  // 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 < 16; i++) {
    char var[64];
    snprintf(var, sizeof(var), "terminal_color_%d", i);
    char *name = get_config_string(buf, var);
    if (name) {
      int dummy;
      RgbValue color_val = name_to_color(name, &dummy);

      if (color_val != -1) {
        VTermColor color;
        vterm_color_rgb(&color,
                        (uint8_t)((color_val >> 16) & 0xFF),
                        (uint8_t)((color_val >> 8) & 0xFF),
                        (uint8_t)((color_val >> 0) & 0xFF));
        vterm_state_set_palette_color(state, i, &color);
        term->color_set[i] = true;
      }
    }
  }
}

/// Closes the Terminal buffer.
///
/// May call terminal_destroy, which sets caller storage to NULL.
void terminal_close(Terminal **termpp, int status)
  FUNC_ATTR_NONNULL_ALL
{
  Terminal *term = *termpp;

#ifdef EXITFREE
  if (entered_free_all_mem) {
    // If called from buf_close_terminal() inside free_all_mem(), the main loop has
    // already been freed, so it is not safe to call the close callback here.
    terminal_destroy(termpp);
    return;
  }
#endif

  if (term->destroy) {  // Destruction already scheduled on the main loop.
    return;
  }

  bool only_destroy = false;

  buf_T *buf = handle_get_buffer(term->buf_handle);

  if (term->closed) {
    // If called from buf_close_terminal() after the process has already exited, we
    // only need to call the close callback to clean up the terminal object.
    only_destroy = true;
  } else {
    // flush any pending changes to the buffer
    if (!exiting) {
      block_autocmds();
      refresh_terminal(term);
      unblock_autocmds();
    }
    term->closed = true;
  }

  int pos = buf ? buf->b_ml.ml_line_count - 1 : 0;
  if (status == -1 || exiting) {
    // If this was called by buf_close_terminal() (status is -1), or if exiting, we
    // must inform the buffer the terminal no longer exists so that buf_freeall()
    // won't call buf_close_terminal() again.
    // If inside Terminal mode event handling, setting buf_handle to 0 also
    // informs terminal_enter() to call the close callback before returning.
    term->buf_handle = 0;
    if (buf) {
      buf->terminal = NULL;
    }
    if (!term->refcount) {
      // Not inside Terminal mode event handling.
      // We should not wait for the user to press a key.
      term->destroy = true;
      term->opts.close_cb(term->opts.data);
    }
  } else if (!only_destroy) {
    // Associated channel has been closed and the editor is not exiting.
    // Do not call the close callback now. Wait for the user to press a key.
    // Redraw statusline to show the exit code.
    FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
      if (wp->w_buffer == buf) {
        wp->w_redr_status = true;
      }
    }

    // Gets the line number to display "[Process exited]" virt text
    pos = MIN(row_to_linenr(term, term->cursor.row), pos);
  }

  if (only_destroy) {
    return;
  }

  if (buf && !is_autocmd_blocked()) {
    save_v_event_T save_v_event;
    dict_T *dict = get_v_event(&save_v_event);
    tv_dict_add_nr(dict, S_LEN("status"), status);
    tv_dict_set_keys_readonly(dict);

    MAXSIZE_TEMP_DICT(data, 1);
    PUT_C(data, "pos", INTEGER_OBJ(pos));

    apply_autocmds_group(EVENT_TERMCLOSE, NULL, NULL, status >= 0, AUGROUP_ALL,
                         buf, NULL, &DICT_OBJ(data));

    restore_v_event(dict, &save_v_event);
  }
}

static void terminal_state_change_event(void **argv)
{
  handle_T buf_handle = (handle_T)(intptr_t)argv[0];
  buf_T *buf = handle_get_buffer(buf_handle);
  if (buf && buf->terminal) {
    // Don't change the actual terminal content to indicate the suspended state here,
    // as unlike the process exit case the change needs to be reversed on resume.
    // Instead, the code in win_update() will add a "[Process suspended]" virtual text
    // at the botton-left of the buffer.
    redraw_buf_line_later(buf, buf->b_ml.ml_line_count, false);
  }
}

/// Updates the suspended state of the terminal program.
void terminal_set_state(Terminal *term, bool suspended)
  FUNC_ATTR_NONNULL_ALL
{
  if (term->suspended != suspended) {
    // Trigger a main loop iteration to redraw the buffer.
    multiqueue_put(refresh_timer.events, terminal_state_change_event,
                   (void *)(intptr_t)term->buf_handle);
  }
  term->suspended = suspended;
}

void terminal_check_size(Terminal *term)
  FUNC_ATTR_NONNULL_ALL
{
  if (term->closed) {
    return;
  }

  int curwidth, curheight;
  vterm_get_size(term->vt, &curheight, &curwidth);
  uint16_t width = 0;
  uint16_t height = 0;

  // Check if there is a window that displays the terminal and find the maximum width and height.
  // Skip the autocommand window which isn't actually displayed.
  FOR_ALL_TAB_WINDOWS(tp, wp) {
    if (is_aucmd_win(wp)) {
      continue;
    }
    if (wp->w_buffer && wp->w_buffer->terminal == term) {
      const uint16_t win_width =
        (uint16_t)(MAX(0, wp->w_view_width - win_col_off(wp)));
      width = MAX(width, win_width);
      height = (uint16_t)MAX(height, wp->w_view_height);
    }
  }

  // if no window displays the terminal, or such all windows are zero-height,
  // don't resize the terminal.
  if ((curheight == height && curwidth == width) || height == 0 || width == 0) {
    return;
  }

  vterm_set_size(term->vt, height, width);
  vterm_screen_flush_damage(term->vts);
  term->pending.resize = true;
  invalidate_terminal(term, -1, -1);
}

static void set_terminal_winopts(TerminalState *const s)
  FUNC_ATTR_NONNULL_ALL
{
  assert(s->save_curwin_handle == 0);

  // Disable these options in terminal-mode. They are nonsense because cursor is
  // placed at end of buffer to "follow" output. #11072
  s->save_curwin_handle = curwin->handle;
  s->save_w_p_cul = curwin->w_p_cul;
  s->save_w_p_culopt = NULL;
  s->save_w_p_culopt_flags = curwin->w_p_culopt_flags;
  s->save_w_p_cuc = curwin->w_p_cuc;
  s->save_w_p_so = curwin->w_p_so;
  s->save_w_p_siso = curwin->w_p_siso;

  if (curwin->w_p_cul && curwin->w_p_culopt_flags & kOptCuloptFlagNumber) {
    if (!strequal(curwin->w_p_culopt, "number")) {
      s->save_w_p_culopt = curwin->w_p_culopt;
      curwin->w_p_culopt = xstrdup("number");
    }
    curwin->w_p_culopt_flags = kOptCuloptFlagNumber;
  } else {
    curwin->w_p_cul = false;
  }
  curwin->w_p_cuc = false;
  curwin->w_p_so = 0;
  curwin->w_p_siso = 0;

  if (curwin->w_p_cuc != s->save_w_p_cuc) {
    redraw_later(curwin, UPD_SOME_VALID);
  } else if (curwin->w_p_cul != s->save_w_p_cul
             || (curwin->w_p_cul && curwin->w_p_culopt_flags != s->save_w_p_culopt_flags)) {
    redraw_later(curwin, UPD_VALID);
  }
}

static void unset_terminal_winopts(TerminalState *const s)
  FUNC_ATTR_NONNULL_ALL
{
  assert(s->save_curwin_handle != 0);

  win_T *const wp = handle_get_window(s->save_curwin_handle);
  if (!wp) {
    goto end;
  }

  winopt_T *winopts = NULL;
  if (wp->w_buffer->handle != s->term->buf_handle) {  // Buffer no longer in "wp".
    buf_T *buf = handle_get_buffer(s->term->buf_handle);
    if (buf == NULL) {
      goto end;  // Nothing to restore as the buffer was deleted.
    }
    for (size_t i = 0; i < kv_size(buf->b_wininfo); i++) {
      WinInfo *wip = kv_A(buf->b_wininfo, i);
      if (wip->wi_win == wp && wip->wi_optset) {
        winopts = &wip->wi_opt;
        break;
      }
    }
    if (winopts == NULL) {
      goto end;  // Nothing to restore as there is no matching WinInfo.
    }
  } else {
    winopts = &wp->w_onebuf_opt;
    if (win_valid(wp)) {  // No need to redraw if window not in curtab.
      if (s->save_w_p_cuc != wp->w_p_cuc) {
        redraw_later(wp, UPD_SOME_VALID);
      } else if (s->save_w_p_cul != wp->w_p_cul
                 || (s->save_w_p_cul && s->save_w_p_culopt_flags != wp->w_p_culopt_flags)) {
        redraw_later(wp, UPD_VALID);
      }
    }
    wp->w_p_culopt_flags = s->save_w_p_culopt_flags;
  }

  if (s->save_w_p_culopt) {
    free_string_option(winopts->wo_culopt);
    winopts->wo_culopt = s->save_w_p_culopt;
    s->save_w_p_culopt = NULL;
  }
  winopts->wo_cul = s->save_w_p_cul;
  winopts->wo_cuc = s->save_w_p_cuc;
  winopts->wo_so = s->save_w_p_so;
  winopts->wo_siso = s->save_w_p_siso;

end:
  free_string_option(s->save_w_p_culopt);
  s->save_curwin_handle = 0;
}

/// Implements MODE_TERMINAL state. :help Terminal-mode
bool terminal_enter(void)
{
  buf_T *buf = curbuf;
  assert(buf->terminal);  // Should only be called when curbuf has a terminal.
  TerminalState s[1] = { 0 };
  s->term = buf->terminal;
  s->cursor_visible = true;  // Assume visible; may change via refresh_cursor later.
  stop_insert_mode = false;

  // Ensure the terminal is properly sized. Ideally window size management
  // code should always have resized the terminal already, but check here to
  // be sure.
  terminal_check_size(s->term);

  int save_state = State;
  s->save_rd = RedrawingDisabled;
  State = MODE_TERMINAL;
  mapped_ctrl_c |= MODE_TERMINAL;  // Always map CTRL-C to avoid interrupt.
  RedrawingDisabled = false;

  set_terminal_winopts(s);

  s->term->pending.cursor = true;  // Update the cursor shape table
  adjust_topline_cursor(s->term, buf, 0);  // scroll to end
  showmode();
  ui_cursor_shape();

  // Tell the terminal it has focus
  terminal_focus(s->term, true);
  // Don't fire TextChangedT from changes in Normal mode.
  curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);

  // Don't let autocommands free the terminal now!
  s->term->refcount++;
  apply_autocmds(EVENT_TERMENTER, NULL, NULL, false, curbuf);
  may_trigger_modechanged();
  s->term->refcount--;
  if (s->term->buf_handle == 0) {
    s->close = true;
  }

  s->state.execute = terminal_execute;
  s->state.check = terminal_check;
  state_enter(&s->state);

  if (!s->got_bsl_o) {
    restart_edit = 0;
  }
  State = save_state;
  RedrawingDisabled = s->save_rd;
  if (!s->cursor_visible) {
    // If cursor was hidden, show it again. Do so right after restoring State.
    ui_busy_stop();
  }

  // Restore the terminal cursor to what is set in 'guicursor'
  (void)parse_shape_opt(SHAPE_CURSOR);

  unset_terminal_winopts(s);

  // Tell the terminal it lost focus
  terminal_focus(s->term, false);
  // Don't fire TextChanged from changes in terminal mode.
  curbuf->b_last_changedtick = buf_get_changedtick(curbuf);

  if (curbuf->terminal == s->term && !s->close) {
    terminal_check_cursor();
  }
  if (restart_edit) {
    showmode();
  } else {
    unshowmode(true);
  }
  ui_cursor_shape();

  // If we're to close the terminal, don't let TermLeave autocommands free it first!
  if (s->close) {
    s->term->refcount++;
  }
  apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf);
  if (s->close) {
    s->term->refcount--;
    const handle_T buf_handle = s->term->buf_handle;  // Callback may free s->term.
    s->term->destroy = true;
    s->term->opts.close_cb(s->term->opts.data);
    if (buf_handle != 0) {
      do_buffer(DOBUF_WIPE, DOBUF_FIRST, FORWARD, buf_handle, true);
    }
  }

  return s->got_bsl_o;
}

static void terminal_check_cursor(void)
{
  Terminal *term = curbuf->terminal;
  curwin->w_cursor.lnum = MIN(curbuf->b_ml.ml_line_count,
                              row_to_linenr(term, term->cursor.row));
  const linenr_T topline = MAX(curbuf->b_ml.ml_line_count - curwin->w_view_height + 1, 1);
  // Don't update topline if unchanged to avoid unnecessary redraws.
  if (topline != curwin->w_topline) {
    set_topline(curwin, topline);
  }
  if (term->suspended && (State & MODE_TERMINAL)) {
    // Put cursor at the "[Process suspended]" text to hint that pressing a key will
    // change the suspended state.
    curwin->w_cursor = (pos_T){ .lnum = curbuf->b_ml.ml_line_count };
  } else {
    // Nudge cursor when returning to normal-mode.
    int off = (State & MODE_TERMINAL) ? 0 : (curwin->w_p_rl ? 1 : -1);
    coladvance(curwin, MAX(0, term->cursor.col + off));
  }
}

static bool terminal_check_focus(TerminalState *const s)
  FUNC_ATTR_NONNULL_ALL
{
  if (curbuf->terminal == NULL) {
    return false;
  }

  if (s->save_curwin_handle != curwin->handle) {
    // Terminal window changed, update window options.
    unset_terminal_winopts(s);
    set_terminal_winopts(s);
  }
  if (s->term != curbuf->terminal) {
    // Active terminal changed, flush terminal's cursor state to the UI.
    terminal_focus(s->term, false);
    if (s->close) {
      s->term->destroy = true;
      s->term->opts.close_cb(s->term->opts.data);
      s->close = false;
    }

    s->term = curbuf->terminal;
    s->term->pending.cursor = true;
    invalidate_terminal(s->term, -1, -1);
    terminal_focus(s->term, true);
  }
  return true;
}

/// Function executed before each iteration of terminal mode.
///
/// @return:
///           1 if the iteration should continue normally
///           0 if the main loop must exit
static int terminal_check(VimState *state)
{
  TerminalState *const s = (TerminalState *)state;

  // Shouldn't reach here when pressing a key to close the terminal buffer.
  assert(!s->close || (s->term->buf_handle == 0 && s->term != curbuf->terminal));

  if (stop_insert_mode || !terminal_check_focus(s)) {
    return 0;
  }

  terminal_check_refresh();

  // Validate topline and cursor position for autocommands. Especially important for WinScrolled.
  terminal_check_cursor();
  validate_cursor(curwin);

  // Don't let autocommands free the terminal from under our fingers.
  s->term->refcount++;
  if (has_event(EVENT_TEXTCHANGEDT)
      && curbuf->b_last_changedtick_i != buf_get_changedtick(curbuf)) {
    apply_autocmds(EVENT_TEXTCHANGEDT, NULL, NULL, false, curbuf);
    curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);
  }
  may_trigger_win_scrolled_resized();
  s->term->refcount--;
  if (s->term->buf_handle == 0) {
    s->close = true;
  }

  // Autocommands above may have changed focus, scrolled, or moved the cursor.
  if (!terminal_check_focus(s)) {
    return 0;
  }
  terminal_check_cursor();
  validate_cursor(curwin);

  show_cursor_info_later(false);
  if (must_redraw) {
    update_screen();
  } else {
    redraw_statuslines();
    if (clear_cmdline || redraw_cmdline || redraw_mode) {
      showmode();  // clear cmdline and show mode
    }
  }

  setcursor();
  refresh_cursor(s->term, &s->cursor_visible);
  ui_flush();
  return 1;
}

/// Processes one char of terminal-mode input.
static int terminal_execute(VimState *state, int key)
{
  TerminalState *s = (TerminalState *)state;

  // Check for certain control keys like Ctrl-C and Ctrl-\. We still send the
  // unmerged key and modifiers to the terminal.
  int tmp_mod_mask = mod_mask;
  int mod_key = merge_modifiers(key, &tmp_mod_mask);

  switch (mod_key) {
  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_X1MOUSE:
  case K_X1DRAG:
  case K_X1RELEASE:
  case K_X2MOUSE:
  case K_X2DRAG:
  case K_X2RELEASE:
  case K_MOUSEDOWN:
  case K_MOUSEUP:
  case K_MOUSELEFT:
  case K_MOUSERIGHT:
  case K_MOUSEMOVE:
    if (send_mouse_event(s->term, key)) {
      return 0;
    }
    break;

  case K_PASTE_START:
    paste_repeat(1);
    break;

  case K_EVENT:
    // We cannot let an event free the terminal yet. It is still needed.
    s->term->refcount++;
    state_handle_k_event();
    s->term->refcount--;
    if (s->term->buf_handle == 0) {
      s->close = true;
    }
    break;

  case K_COMMAND:
    do_cmdline(NULL, getcmdkeycmd, NULL, 0);
    break;

  case K_LUA:
    map_execute_lua(false, false);
    break;

  case K_IGNORE:
  case K_NOP:
    // Do not interrupt a Ctrl-\ sequence or close a finished terminal.
    break;

  case Ctrl_N:
    if (s->got_bsl) {
      return 0;
    }
    FALLTHROUGH;

  case Ctrl_O:
    if (s->got_bsl) {
      s->got_bsl_o = true;
      restart_edit = 'I';
      return 0;
    }
    FALLTHROUGH;

  default:
    if (mod_key == Ctrl_C) {
      // terminal_enter() always sets `mapped_ctrl_c` to avoid `got_int`. 8eeda7169aa4
      // But `got_int` may be set elsewhere, e.g. by interrupt() or an autocommand,
      // so ensure that it is cleared.
      got_int = false;
    }
    if (mod_key == Ctrl_BSL && !s->got_bsl) {
      s->got_bsl = true;
      break;
    }
    if (s->term->suspended) {
      s->term->opts.resume_cb(s->term->opts.data);
      // XXX: detecting continued process via waitpid() on SIGCHLD doesn't always work
      // (e.g. on macOS), so also consider it continued after sending SIGCONT.
      terminal_set_state(s->term, false);
      break;
    }
    if (s->term->closed) {
      s->close = true;
      return 0;
    }

    s->got_bsl = false;
    terminal_send_key(s->term, key);
  }

  return 1;
}

/// Frees the given Terminal structure and sets the caller storage to NULL (in the spirit of
/// XFREE_CLEAR).
void terminal_destroy(Terminal **termpp)
  FUNC_ATTR_NONNULL_ALL
{
  Terminal *term = *termpp;
  buf_T *buf = handle_get_buffer(term->buf_handle);
  if (buf) {
    term->buf_handle = 0;
    buf->terminal = NULL;
  }

  if (!term->refcount) {
    if (set_has(ptr_t, &invalidated_terminals, term)) {
      // flush any pending changes to the buffer
      block_autocmds();
      refresh_terminal(term);
      unblock_autocmds();
      set_del(ptr_t, &invalidated_terminals, term);
    }
    for (size_t i = 0; i < term->sb_current; i++) {
      xfree(term->sb_buffer[i]);
    }
    xfree(term->sb_buffer);
    xfree(term->title);
    xfree(term->selection_buffer);
    kv_destroy(term->selection);
    kv_destroy(term->termrequest_buffer);
    vterm_free(term->vt);
    multiqueue_free(term->pending.events);
    xfree(term);
    *termpp = NULL;  // coverity[dead-store]
  }
}

static void terminal_send(Terminal *term, const char *data, size_t size)
{
  if (term->closed) {
    return;
  }
  if (term->pending.send) {
    kv_concat_len(*term->pending.send, data, size);
    return;
  }
  term->opts.write_cb(data, size, term->opts.data);
}

static bool is_filter_char(int c)
{
  unsigned flag = 0;
  switch (c) {
  case 0x08:
    flag = kOptTpfFlagBS;
    break;
  case 0x09:
    flag = kOptTpfFlagHT;
    break;
  case 0x0A:
  case 0x0D:
    break;
  case 0x0C:
    flag = kOptTpfFlagFF;
    break;
  case 0x1b:
    flag = kOptTpfFlagESC;
    break;
  case 0x7F:
    flag = kOptTpfFlagDEL;
    break;
  default:
    if (c < ' ') {
      flag = kOptTpfFlagC0;
    } else if (c >= 0x80 && c <= 0x9F) {
      flag = kOptTpfFlagC1;
    }
  }
  return !!(tpf_flags & flag);
}

void terminal_set_streamed_paste(Terminal *term, bool streamed)
  FUNC_ATTR_NONNULL_ALL
{
  if (term->streamed_paste != streamed) {
    if (streamed) {
      vterm_keyboard_start_paste(curbuf->terminal->vt);
    } else {
      vterm_keyboard_end_paste(curbuf->terminal->vt);
    }
  }
  term->streamed_paste = streamed;
}

void terminal_paste(int count, String *y_array, size_t y_size)
{
  if (y_size == 0) {
    return;
  }
  if (!curbuf->terminal->streamed_paste) {
    vterm_keyboard_start_paste(curbuf->terminal->vt);
  }
  size_t buff_len = y_array[0].size;
  char *buff = xmalloc(buff_len);
  for (int i = 0; i < count; i++) {
    // feed the lines to the terminal
    for (size_t j = 0; j < y_size; j++) {
      if (j) {
        // terminate the previous line
#ifdef MSWIN
        terminal_send(curbuf->terminal, "\r\n", 2);
#else
        terminal_send(curbuf->terminal, "\n", 1);
#endif
      }
      size_t len = y_array[j].size;
      if (len > buff_len) {
        buff = xrealloc(buff, len);
        buff_len = len;
      }
      char *dst = buff;
      char *src = y_array[j].data;
      while (*src != NUL) {
        len = (size_t)utf_ptr2len(src);
        int c = utf_ptr2char(src);
        if (!is_filter_char(c)) {
          memcpy(dst, src, len);
          dst += len;
        }
        src += len;
      }
      terminal_send(curbuf->terminal, buff, (size_t)(dst - buff));
    }
  }
  xfree(buff);
  if (!curbuf->terminal->streamed_paste) {
    vterm_keyboard_end_paste(curbuf->terminal->vt);
  }
}

static void terminal_send_key(Terminal *term, int c)
{
  VTermModifier mod = VTERM_MOD_NONE;

  // Convert K_ZERO back to ASCII
  if (c == K_ZERO) {
    c = Ctrl_AT;
  }

  VTermKey key = convert_key(&c, &mod);

  if (key != VTERM_KEY_NONE) {
    vterm_keyboard_key(term->vt, key, mod);
  } else if (!IS_SPECIAL(c)) {
    vterm_keyboard_unichar(term->vt, (uint32_t)c, mod);
  }
}

/// Callback scheduled on the main loop when a synchronized update ends.
/// Refreshes a single terminal with full-screen damage.
static void on_sync_flush(void **argv)
{
  if (exiting) {
    return;
  }
  handle_T buf_handle = (handle_T)(intptr_t)argv[0];
  buf_T *buf = handle_get_buffer(buf_handle);
  if (!buf || !buf->terminal) {
    return;
  }
  block_autocmds();
  refresh_terminal(buf->terminal);
  unblock_autocmds();
}

void terminal_receive(Terminal *term, const char *data, size_t len)
{
  if (!data) {
    return;
  }

  if (term->opts.force_crlf) {
    StringBuilder crlf_data = KV_INITIAL_VALUE;

    for (size_t i = 0; i < len; i++) {
      if (data[i] == '\n' && (i == 0 || (i > 0 && data[i - 1] != '\r'))) {
        kv_push(crlf_data, '\r');
      }
      kv_push(crlf_data, data[i]);
    }

    vterm_input_write(term->vt, crlf_data.items, kv_size(crlf_data));
    kv_destroy(crlf_data);
  } else {
    vterm_input_write(term->vt, data, len);
  }
  vterm_screen_flush_damage(term->vts);

  // When a synchronized update just ended, refresh the buffer immediately
  // instead of waiting for the 10ms timer.  This eliminates the window where
  // neovim's UI could repaint showing stale buffer content.
  if (term->sync_flush_pending) {
    term->sync_flush_pending = false;
    // Schedule a full-screen refresh for this terminal on the main loop.
    // Force full-screen damage so every row is updated, not just
    // the rows with accumulated damage from individual callbacks.
    int height;
    vterm_get_size(term->vt, &height, NULL);
    term->invalid_start = 0;
    term->invalid_end = height;
    multiqueue_put(main_loop.events, on_sync_flush,
                   (void *)(intptr_t)term->buf_handle);
  }
}

static int get_rgb(VTermState *state, VTermColor color)
{
  vterm_state_convert_color_to_rgb(state, &color);
  return RGB_(color.rgb.red, color.rgb.green, color.rgb.blue);
}

static int get_underline_hl_flag(VTermScreenCellAttrs attrs)
{
  switch (attrs.underline) {
  case VTERM_UNDERLINE_OFF:
    return 0;
  case VTERM_UNDERLINE_SINGLE:
    return HL_UNDERLINE;
  case VTERM_UNDERLINE_DOUBLE:
    return HL_UNDERDOUBLE;
  case VTERM_UNDERLINE_CURLY:
    return HL_UNDERCURL;
  default:
    return HL_UNDERLINE;
  }
}

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);
  VTermState *state = vterm_obtain_state(term->vt);
  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;
  }

  width = MIN(TERM_ATTRS_MAX, width);
  for (int col = 0; col < width; col++) {
    VTermScreenCell cell;
    bool color_valid = fetch_cell(term, row, col, &cell);
    bool fg_default = !color_valid || VTERM_COLOR_IS_DEFAULT_FG(&cell.fg);
    bool bg_default = !color_valid || VTERM_COLOR_IS_DEFAULT_BG(&cell.bg);

    // Get the rgb value set by libvterm.
    int vt_fg = fg_default ? -1 : get_rgb(state, cell.fg);
    int vt_bg = bg_default ? -1 : get_rgb(state, cell.bg);

    bool fg_indexed = VTERM_COLOR_IS_INDEXED(&cell.fg);
    bool bg_indexed = VTERM_COLOR_IS_INDEXED(&cell.bg);

    int16_t vt_fg_idx = ((!fg_default && fg_indexed) ? cell.fg.indexed.idx + 1 : 0);
    int16_t vt_bg_idx = ((!bg_default && bg_indexed) ? cell.bg.indexed.idx + 1 : 0);

    bool fg_set = vt_fg_idx && vt_fg_idx <= 16 && term->color_set[vt_fg_idx - 1];
    bool bg_set = vt_bg_idx && vt_bg_idx <= 16 && term->color_set[vt_bg_idx - 1];

    int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0)
                   | (cell.attrs.dim ? HL_DIM : 0)
                   | (cell.attrs.blink ? HL_BLINK : 0)
                   | (cell.attrs.conceal ? HL_CONCEALED : 0)
                   | (cell.attrs.overline ? HL_OVERLINE : 0)
                   | (cell.attrs.italic ? HL_ITALIC : 0)
                   | (cell.attrs.reverse ? HL_INVERSE : 0)
                   | get_underline_hl_flag(cell.attrs)
                   | (cell.attrs.strike ? HL_STRIKETHROUGH : 0)
                   | ((fg_indexed && !fg_set) ? HL_FG_INDEXED : 0)
                   | ((bg_indexed && !bg_set) ? HL_BG_INDEXED : 0);

    int attr_id = 0;

    if (hl_attrs || !fg_default || !bg_default) {
      attr_id = hl_get_term_attr(&(HlAttrs) {
        .cterm_ae_attr = (int32_t)hl_attrs,
        .cterm_fg_color = vt_fg_idx,
        .cterm_bg_color = vt_bg_idx,
        .rgb_ae_attr = (int32_t)hl_attrs,
        .rgb_fg_color = vt_fg,
        .rgb_bg_color = vt_bg,
        .rgb_sp_color = -1,
        .hl_blend = -1,
        .url = -1,
      });
    }

    if (cell.uri > 0) {
      attr_id = hl_combine_attr(attr_id, cell.uri);
    }

    term_attrs[col] = attr_id;
  }
}

Buffer terminal_buf(const Terminal *term)
  FUNC_ATTR_NONNULL_ALL
{
  return term->buf_handle;
}

bool terminal_running(const Terminal *term)
  FUNC_ATTR_NONNULL_ALL
{
  return !term->closed;
}

bool terminal_suspended(const Terminal *term)
  FUNC_ATTR_NONNULL_ALL
{
  return term->suspended;
}

void terminal_notify_theme(Terminal *term, bool dark)
  FUNC_ATTR_NONNULL_ALL
{
  if (!term->theme_updates) {
    return;
  }

  char buf[10];
  ssize_t ret = snprintf(buf, sizeof(buf), "\x1b[997;%cn", dark ? '1' : '2');
  assert(ret > 0);
  assert((size_t)ret <= sizeof(buf));
  terminal_send(term, buf, (size_t)ret);
}

static void terminal_focus(const Terminal *term, bool focus)
  FUNC_ATTR_NONNULL_ALL
{
  VTermState *state = vterm_obtain_state(term->vt);
  if (focus) {
    vterm_state_focus_in(state);
  } else {
    vterm_state_focus_out(state);
  }
}

// }}}
// 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_pos, VTermPos old_pos, int visible, void *data)
{
  Terminal *term = data;
  term->cursor.row = new_pos.row;
  term->cursor.col = new_pos.col;
  invalidate_terminal(term, -1, -1);
  return 1;
}

static void buf_set_term_title(buf_T *buf, const char *title, size_t len)
{
  if (!buf) {
    return;  // In case of receiving OSC 2 between buffer close and job exit.
  }

  Error err = ERROR_INIT;
  buf->b_locked++;
  dict_set_var(buf->b_vars,
               STATIC_CSTR_AS_STRING("term_title"),
               STRING_OBJ(((String){ .data = (char *)title, .size = len })),
               false,
               false,
               NULL,
               &err);
  buf->b_locked--;
  api_clear_error(&err);
  status_redraw_buf(buf);
}

static int term_settermprop(VTermProp prop, VTermValue *val, void *data)
{
  Terminal *term = data;

  switch (prop) {
  case VTERM_PROP_ALTSCREEN:
    term->in_altscreen = val->boolean;
    break;

  case VTERM_PROP_CURSORVISIBLE:
    term->cursor.visible = val->boolean;
    invalidate_terminal(term, -1, -1);
    break;

  case VTERM_PROP_TITLE: {
    buf_T *buf = handle_get_buffer(term->buf_handle);  // May be NULL
    VTermStringFragment frag = val->string;

    if (frag.initial && frag.final) {
      buf_set_term_title(buf, frag.str, frag.len);
      break;
    }

    if (frag.initial) {
      term->title_len = 0;
      term->title_size = MAX(frag.len, 1024);
      term->title = xmalloc(sizeof(char *) * term->title_size);
    } else if (term->title_len + frag.len > term->title_size) {
      term->title_size *= 2;
      term->title = xrealloc(term->title, sizeof(char *) * term->title_size);
    }

    memcpy(term->title + term->title_len, frag.str, frag.len);
    term->title_len += frag.len;

    if (frag.final) {
      buf_set_term_title(buf, term->title, term->title_len);
      xfree(term->title);
      term->title = NULL;
    }
    break;
  }

  case VTERM_PROP_MOUSE:
    term->forward_mouse = (bool)val->number;
    break;

  case VTERM_PROP_CURSORBLINK:
    term->cursor.blink = val->boolean;
    term->pending.cursor = true;
    invalidate_terminal(term, -1, -1);
    break;

  case VTERM_PROP_CURSORSHAPE:
    term->cursor.shape = val->number;
    term->pending.cursor = true;
    invalidate_terminal(term, -1, -1);
    break;

  case VTERM_PROP_THEMEUPDATES:
    term->theme_updates = val->boolean;
    break;

  case VTERM_PROP_SYNCOUTPUT:
    term->synchronized_output = val->boolean;
    if (!val->boolean) {
      // Mark that sync just ended; terminal_receive() will flush
      // the buffer immediately rather than waiting for the 10ms timer.
      term->sync_flush_pending = true;
    }
    break;

  default:
    return 0;
  }

  return 1;
}

/// Called when the terminal wants to ring the system bell.
static int term_bell(void *data)
{
  vim_beep(kOptBoFlagTerm);
  return 1;
}

/// Called when the terminal wants to query the system theme.
static int term_theme(bool *dark, void *data)
  FUNC_ATTR_NONNULL_ALL
{
  *dark = (*p_bg == 'd');
  return 1;
}

/// Scrollback push handler: called just before a line goes offscreen (and libvterm will forget it),
/// giving us a chance to store it.
///
/// Code adapted from pangoterm.
static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
{
  Terminal *term = data;

  if (!term_may_alloc_scrollback(term, NULL)) {
    return 0;
  }
  assert(term->sb_size > 0);

  // 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 {
      xfree(term->sb_buffer[term->sb_current - 1]);
    }
    term->sb_deleted++;

    // Make room at the start by shifting to the right.
    memmove(term->sb_buffer + 1, term->sb_buffer,
            sizeof(term->sb_buffer[0]) * (term->sb_current - 1));
  } else if (term->sb_current > 0) {
    // Make room at the start by shifting to the right.
    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;
  }

  // New row is added at the start of the storage buffer.
  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);
  if (!term->synchronized_output) {
    set_put(ptr_t, &invalidated_terminals, term);
  }

  return 1;
}

/// Scrollback pop handler (from pangoterm).
///
/// @param cols
/// @param cells  VTerm state to update.
/// @param data   Terminal
static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
{
  Terminal *term = data;

  if (!term->sb_current) {
    return 0;
  }

  if (term->sb_pending > 0) {
    term->sb_pending--;
  } else {
    term->old_height++;
  }

  ScrollbackLine *sbrow = term->sb_buffer[0];
  term->sb_current--;
  // Forget the "popped" row by shifting the rest onto it.
  memmove(term->sb_buffer, term->sb_buffer + 1,
          sizeof(term->sb_buffer[0]) * (term->sb_current));

  size_t cols_to_copy = MIN((size_t)cols, sbrow->cols);

  // copy to vterm state
  memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy);
  for (size_t col = cols_to_copy; col < (size_t)cols; col++) {
    cells[col].schar = 0;
    cells[col].width = 1;
  }

  xfree(sbrow);
  if (!term->synchronized_output) {
    set_put(ptr_t, &invalidated_terminals, term);
  }

  return 1;
}

static int term_sb_clear(void *data)
{
  Terminal *term = data;

  if (term->in_altscreen || !term->sb_size || !term->sb_current) {
    return 1;
  }

  for (size_t i = 0; i < term->sb_current; i++) {
    xfree(term->sb_buffer[i]);
  }

  term->sb_deleted += term->sb_current;
  term->sb_current = 0;
  term->sb_pending = 0;
  invalidate_terminal(term, -1, -1);

  return 1;
}

static void term_clipboard_set(void **argv)
{
  VTermSelectionMask mask = (VTermSelectionMask)(long)argv[0];
  char *data = argv[1];

  char regname;
  switch (mask) {
  case VTERM_SELECTION_CLIPBOARD:
    regname = '+';
    break;
  case VTERM_SELECTION_PRIMARY:
    regname = '*';
    break;
  default:
    regname = '+';
    break;
  }

  list_T *lines = tv_list_alloc(1);
  tv_list_append_allocated_string(lines, data);

  list_T *args = tv_list_alloc(3);
  tv_list_append_list(args, lines);

  const char regtype = 'v';
  tv_list_append_string(args, &regtype, 1);

  tv_list_append_string(args, &regname, 1);
  eval_call_provider("clipboard", "set", args, true);
}

static int term_selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user)
{
  Terminal *term = user;
  if (frag.initial) {
    kv_size(term->selection) = 0;
  }

  kv_concat_len(term->selection, frag.str, frag.len);

  if (frag.final) {
    char *data = xmemdupz(term->selection.items, kv_size(term->selection));
    multiqueue_put(main_loop.events, term_clipboard_set, (void *)mask, data);
  }

  return 1;
}

// }}}
// input handling {{{

static void convert_modifiers(int *key, 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_SHIFT) && *key >= 'A' && *key <= 'Z') {
      // vterm interprets CTRL+A as SHIFT+CTRL, change to CTRL+a
      *key += ('a' - 'A');
    }
  }
  if (mod_mask & MOD_MASK_ALT) {
    *statep |= VTERM_MOD_ALT;
  }

  switch (*key) {
  case K_S_TAB:
  case K_S_UP:
  case K_S_DOWN:
  case K_S_LEFT:
  case K_S_RIGHT:
  case K_S_HOME:
  case K_S_END:
  case K_S_F1:
  case K_S_F2:
  case K_S_F3:
  case K_S_F4:
  case K_S_F5:
  case K_S_F6:
  case K_S_F7:
  case K_S_F8:
  case K_S_F9:
  case K_S_F10:
  case K_S_F11:
  case K_S_F12:
    *statep |= VTERM_MOD_SHIFT;
    break;

  case K_C_LEFT:
  case K_C_RIGHT:
  case K_C_HOME:
  case K_C_END:
    *statep |= VTERM_MOD_CTRL;
    break;
  }
}

static VTermKey convert_key(int *key, VTermModifier *statep)
{
  convert_modifiers(key, statep);

  switch (*key) {
  case K_BS:
    return VTERM_KEY_BACKSPACE;
  case K_S_TAB:
    FALLTHROUGH;
  case TAB:
    return VTERM_KEY_TAB;
  case Ctrl_M:
    return VTERM_KEY_ENTER;
  case ESC:
    return VTERM_KEY_ESCAPE;

  case K_S_UP:
    FALLTHROUGH;
  case K_UP:
    return VTERM_KEY_UP;
  case K_S_DOWN:
    FALLTHROUGH;
  case K_DOWN:
    return VTERM_KEY_DOWN;
  case K_S_LEFT:
    FALLTHROUGH;
  case K_C_LEFT:
    FALLTHROUGH;
  case K_LEFT:
    return VTERM_KEY_LEFT;
  case K_S_RIGHT:
    FALLTHROUGH;
  case K_C_RIGHT:
    FALLTHROUGH;
  case K_RIGHT:
    return VTERM_KEY_RIGHT;

  case K_INS:
    return VTERM_KEY_INS;
  case K_DEL:
    return VTERM_KEY_DEL;
  case K_S_HOME:
    FALLTHROUGH;
  case K_C_HOME:
    FALLTHROUGH;
  case K_HOME:
    return VTERM_KEY_HOME;
  case K_S_END:
    FALLTHROUGH;
  case K_C_END:
    FALLTHROUGH;
  case K_END:
    return VTERM_KEY_END;
  case K_PAGEUP:
    return VTERM_KEY_PAGEUP;
  case K_PAGEDOWN:
    return VTERM_KEY_PAGEDOWN;

  case K_K0:
    FALLTHROUGH;
  case K_KINS:
    return VTERM_KEY_KP_0;
  case K_K1:
    FALLTHROUGH;
  case K_KEND:
    return VTERM_KEY_KP_1;
  case K_K2:
    FALLTHROUGH;
  case K_KDOWN:
    return VTERM_KEY_KP_2;
  case K_K3:
    FALLTHROUGH;
  case K_KPAGEDOWN:
    return VTERM_KEY_KP_3;
  case K_K4:
    FALLTHROUGH;
  case K_KLEFT:
    return VTERM_KEY_KP_4;
  case K_K5:
    FALLTHROUGH;
  case K_KORIGIN:
    return VTERM_KEY_KP_5;
  case K_K6:
    FALLTHROUGH;
  case K_KRIGHT:
    return VTERM_KEY_KP_6;
  case K_K7:
    FALLTHROUGH;
  case K_KHOME:
    return VTERM_KEY_KP_7;
  case K_K8:
    FALLTHROUGH;
  case K_KUP:
    return VTERM_KEY_KP_8;
  case K_K9:
    FALLTHROUGH;
  case K_KPAGEUP:
    return VTERM_KEY_KP_9;
  case K_KDEL:
    FALLTHROUGH;
  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;

  case K_S_F1:
    FALLTHROUGH;
  case K_F1:
    return VTERM_KEY_FUNCTION(1);
  case K_S_F2:
    FALLTHROUGH;
  case K_F2:
    return VTERM_KEY_FUNCTION(2);
  case K_S_F3:
    FALLTHROUGH;
  case K_F3:
    return VTERM_KEY_FUNCTION(3);
  case K_S_F4:
    FALLTHROUGH;
  case K_F4:
    return VTERM_KEY_FUNCTION(4);
  case K_S_F5:
    FALLTHROUGH;
  case K_F5:
    return VTERM_KEY_FUNCTION(5);
  case K_S_F6:
    FALLTHROUGH;
  case K_F6:
    return VTERM_KEY_FUNCTION(6);
  case K_S_F7:
    FALLTHROUGH;
  case K_F7:
    return VTERM_KEY_FUNCTION(7);
  case K_S_F8:
    FALLTHROUGH;
  case K_F8:
    return VTERM_KEY_FUNCTION(8);
  case K_S_F9:
    FALLTHROUGH;
  case K_F9:
    return VTERM_KEY_FUNCTION(9);
  case K_S_F10:
    FALLTHROUGH;
  case K_F10:
    return VTERM_KEY_FUNCTION(10);
  case K_S_F11:
    FALLTHROUGH;
  case K_F11:
    return VTERM_KEY_FUNCTION(11);
  case K_S_F12:
    FALLTHROUGH;
  case K_F12:
    return VTERM_KEY_FUNCTION(12);

  case K_F13:
    return VTERM_KEY_FUNCTION(13);
  case K_F14:
    return VTERM_KEY_FUNCTION(14);
  case K_F15:
    return VTERM_KEY_FUNCTION(15);
  case K_F16:
    return VTERM_KEY_FUNCTION(16);
  case K_F17:
    return VTERM_KEY_FUNCTION(17);
  case K_F18:
    return VTERM_KEY_FUNCTION(18);
  case K_F19:
    return VTERM_KEY_FUNCTION(19);
  case K_F20:
    return VTERM_KEY_FUNCTION(20);
  case K_F21:
    return VTERM_KEY_FUNCTION(21);
  case K_F22:
    return VTERM_KEY_FUNCTION(22);
  case K_F23:
    return VTERM_KEY_FUNCTION(23);
  case K_F24:
    return VTERM_KEY_FUNCTION(24);
  case K_F25:
    return VTERM_KEY_FUNCTION(25);
  case K_F26:
    return VTERM_KEY_FUNCTION(26);
  case K_F27:
    return VTERM_KEY_FUNCTION(27);
  case K_F28:
    return VTERM_KEY_FUNCTION(28);
  case K_F29:
    return VTERM_KEY_FUNCTION(29);
  case K_F30:
    return VTERM_KEY_FUNCTION(30);
  case K_F31:
    return VTERM_KEY_FUNCTION(31);
  case K_F32:
    return VTERM_KEY_FUNCTION(32);
  case K_F33:
    return VTERM_KEY_FUNCTION(33);
  case K_F34:
    return VTERM_KEY_FUNCTION(34);
  case K_F35:
    return VTERM_KEY_FUNCTION(35);
  case K_F36:
    return VTERM_KEY_FUNCTION(36);
  case K_F37:
    return VTERM_KEY_FUNCTION(37);
  case K_F38:
    return VTERM_KEY_FUNCTION(38);
  case K_F39:
    return VTERM_KEY_FUNCTION(39);
  case K_F40:
    return VTERM_KEY_FUNCTION(40);
  case K_F41:
    return VTERM_KEY_FUNCTION(41);
  case K_F42:
    return VTERM_KEY_FUNCTION(42);
  case K_F43:
    return VTERM_KEY_FUNCTION(43);
  case K_F44:
    return VTERM_KEY_FUNCTION(44);
  case K_F45:
    return VTERM_KEY_FUNCTION(45);
  case K_F46:
    return VTERM_KEY_FUNCTION(46);
  case K_F47:
    return VTERM_KEY_FUNCTION(47);
  case K_F48:
    return VTERM_KEY_FUNCTION(48);
  case K_F49:
    return VTERM_KEY_FUNCTION(49);
  case K_F50:
    return VTERM_KEY_FUNCTION(50);
  case K_F51:
    return VTERM_KEY_FUNCTION(51);
  case K_F52:
    return VTERM_KEY_FUNCTION(52);
  case K_F53:
    return VTERM_KEY_FUNCTION(53);
  case K_F54:
    return VTERM_KEY_FUNCTION(54);
  case K_F55:
    return VTERM_KEY_FUNCTION(55);
  case K_F56:
    return VTERM_KEY_FUNCTION(56);
  case K_F57:
    return VTERM_KEY_FUNCTION(57);
  case K_F58:
    return VTERM_KEY_FUNCTION(58);
  case K_F59:
    return VTERM_KEY_FUNCTION(59);
  case K_F60:
    return VTERM_KEY_FUNCTION(60);
  case K_F61:
    return VTERM_KEY_FUNCTION(61);
  case K_F62:
    return VTERM_KEY_FUNCTION(62);
  case K_F63:
    return VTERM_KEY_FUNCTION(63);

  default:
    return VTERM_KEY_NONE;
  }
}

static void mouse_action(Terminal *term, int button, int row, int col, bool pressed,
                         VTermModifier mod)
{
  vterm_mouse_move(term->vt, row, col, mod);
  if (button) {
    vterm_mouse_button(term->vt, button, pressed, mod);
  }
}

// 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;
  int col = mouse_col;
  int grid = mouse_grid;
  win_T *mouse_win = mouse_find_win_inner(&grid, &row, &col);
  if (mouse_win == NULL) {
    goto end;
  }

  int offset;
  if (!term->suspended && !term->closed
      && term->forward_mouse && mouse_win->w_buffer->terminal == term && row >= 0
      && (grid > 1 || row + mouse_win->w_winbar_height < mouse_win->w_height)
      && col >= (offset = win_col_off(mouse_win))
      && (grid > 1 || col < mouse_win->w_width)) {
    // event in the terminal window and mouse events was enabled by the
    // program. translate and forward the event
    int button;
    bool pressed = false;

    switch (c) {
    case K_LEFTDRAG:
    case K_LEFTMOUSE:
      pressed = true; FALLTHROUGH;
    case K_LEFTRELEASE:
      button = 1; break;
    case K_MIDDLEDRAG:
    case K_MIDDLEMOUSE:
      pressed = true; FALLTHROUGH;
    case K_MIDDLERELEASE:
      button = 2; break;
    case K_RIGHTDRAG:
    case K_RIGHTMOUSE:
      pressed = true; FALLTHROUGH;
    case K_RIGHTRELEASE:
      button = 3; break;
    case K_X1DRAG:
    case K_X1MOUSE:
      pressed = true; FALLTHROUGH;
    case K_X1RELEASE:
      button = 8; break;
    case K_X2DRAG:
    case K_X2MOUSE:
      pressed = true; FALLTHROUGH;
    case K_X2RELEASE:
      button = 9; break;
    case K_MOUSEDOWN:
      pressed = true; button = 4; break;
    case K_MOUSEUP:
      pressed = true; button = 5; break;
    case K_MOUSELEFT:
      pressed = true; button = 7; break;
    case K_MOUSERIGHT:
      pressed = true; button = 6; break;
    case K_MOUSEMOVE:
      button = 0; break;
    default:
      return false;
    }

    VTermModifier mod = VTERM_MOD_NONE;
    convert_modifiers(&c, &mod);
    mouse_action(term, button, row, col - offset, pressed, mod);
    return false;
  }

  if (c == K_MOUSEUP || c == K_MOUSEDOWN || c == K_MOUSELEFT || c == K_MOUSERIGHT) {
    win_T *save_curwin = curwin;
    // switch window/buffer to perform the scroll
    curwin = mouse_win;
    curbuf = curwin->w_buffer;

    cmdarg_T cap;
    oparg_T oa;
    CLEAR_FIELD(cap);
    clear_oparg(&oa);
    cap.oap = &oa;

    switch (cap.cmdchar = c) {
    case K_MOUSEUP:
      cap.arg = MSCR_UP;
      break;
    case K_MOUSEDOWN:
      cap.arg = MSCR_DOWN;
      break;
    case K_MOUSELEFT:
      cap.arg = MSCR_LEFT;
      break;
    case K_MOUSERIGHT:
      cap.arg = MSCR_RIGHT;
      break;
    default:
      abort();
    }

    // Call the common mouse scroll function shared with other modes.
    do_mousescroll(&cap);

    curwin->w_redr_status = true;
    curwin = save_curwin;
    curbuf = curwin->w_buffer;
    redraw_later(mouse_win, UPD_NOT_VALID);
    invalidate_terminal(term, -1, -1);
    // Only need to exit focus if the scrolled window is the terminal window
    return mouse_win == curwin;
  }

end:
  // Ignore left release action if it was not forwarded to prevent
  // leaving Terminal mode after entering to it using a mouse.
  if ((c == K_LEFTRELEASE && mouse_win != NULL && mouse_win->w_buffer->terminal == term)
      || c == K_MOUSEMOVE) {
    return false;
  }

  int len = ins_char_typebuf(vgetc_char, vgetc_mod_mask, true);
  if (KeyTyped) {
    ungetchars(len);
  }
  return true;
}

// }}}
// terminal buffer refresh & misc {{{

static 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);
    if (cell.schar) {
      schar_get_adv(&ptr, cell.schar);
      line_len = (size_t)(ptr - term->textbuf);
    } else {
      *ptr++ = ' ';
    }
    col += cell.width;
  }

  // end of line
  term->textbuf[line_len] = NUL;
}

static bool 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) {
        .schar = 0,
        .width = 1,
      };
      return false;
    }
  } else {
    vterm_screen_get_cell(term->vts, (VTermPos){ .row = row, .col = col },
                          cell);
  }
  return true;
}

// 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);
  }

  // During synchronized output (mode 2026), accumulate damage but defer
  // the actual refresh until the synchronized update ends.
  if (term->synchronized_output) {
    return;
  }

  set_put(ptr_t, &invalidated_terminals, term);
  if (!refresh_pending) {
    time_watcher_start(&refresh_timer, refresh_timer_cb, REFRESH_DELAY, 0);
    refresh_pending = true;
  }
}

/// Normally refresh_timer_cb() is called when processing main_loop.events, but with
/// partial mappings main_loop.events isn't processed, while terminal buffers still
/// need refreshing after processing a key, so call this function before redrawing.
void terminal_check_refresh(void)
{
  multiqueue_process_events(refresh_timer.events);
}

static void refresh_terminal(Terminal *term)
{
  buf_T *buf = handle_get_buffer(term->buf_handle);
  if (!buf) {
    // Destroyed by `buf_freeall()`. Do not do anything else.
    return;
  }
  linenr_T ml_before = buf->b_ml.ml_line_count;

  bool resized = refresh_size(term, buf);
  refresh_scrollback(term, buf);
  refresh_screen(term, buf);

  int ml_added = buf->b_ml.ml_line_count - ml_before;
  adjust_topline_cursor(term, buf, ml_added);

  // Resized window may have scrolled horizontally to keep its cursor in-view using the old terminal
  // size. Reset the scroll, and let curs_columns correct it if that sends the cursor out-of-view.
  if (resized) {
    FOR_ALL_TAB_WINDOWS(tp, wp) {
      if (wp->w_buffer == buf && wp->w_leftcol != 0) {
        wp->w_leftcol = 0;
        curs_columns(wp, true);
      }
    }
  }

  // Copy pending events back to the main event queue
  multiqueue_move_events(main_loop.events, term->pending.events);
}

static void refresh_cursor(Terminal *term, bool *cursor_visible)
  FUNC_ATTR_NONNULL_ALL
{
  if (!is_focused(term)) {
    return;
  }
  if (term->cursor.visible != *cursor_visible) {
    *cursor_visible = term->cursor.visible;
    if (*cursor_visible) {
      ui_busy_stop();
    } else {
      ui_busy_start();
    }
  }

  if (!term->pending.cursor) {
    return;
  }
  term->pending.cursor = false;

  if (term->cursor.blink) {
    // For the TUI, this value doesn't actually matter, as long as it's non-zero. The terminal
    // emulator dictates the blink frequency, not the application.
    // For GUIs we just pick an arbitrary value, for now.
    shape_table[SHAPE_IDX_TERM].blinkon = 500;
    shape_table[SHAPE_IDX_TERM].blinkoff = 500;
  } else {
    shape_table[SHAPE_IDX_TERM].blinkon = 0;
    shape_table[SHAPE_IDX_TERM].blinkoff = 0;
  }

  switch (term->cursor.shape) {
  case VTERM_PROP_CURSORSHAPE_BLOCK:
    shape_table[SHAPE_IDX_TERM].shape = SHAPE_BLOCK;
    break;
  case VTERM_PROP_CURSORSHAPE_UNDERLINE:
    shape_table[SHAPE_IDX_TERM].shape = SHAPE_HOR;
    shape_table[SHAPE_IDX_TERM].percentage = 20;
    break;
  case VTERM_PROP_CURSORSHAPE_BAR_LEFT:
    shape_table[SHAPE_IDX_TERM].shape = SHAPE_VER;
    shape_table[SHAPE_IDX_TERM].percentage = 25;
    break;
  }

  ui_mode_info_set();
}

/// Calls refresh_terminal() on all invalidated_terminals.
static void refresh_timer_cb(TimeWatcher *watcher, void *data)
{
  refresh_pending = false;
  if (exiting) {  // Cannot redraw (requires event loop) during teardown/exit.
    return;
  }

  // Don't process autocommands while updating terminal buffers.
  block_autocmds();
  // Refreshing one terminal may poll for output to another, which should not
  // interfere with the set_foreach() below.
  Set(ptr_t) to_refresh = invalidated_terminals;
  invalidated_terminals = (Set(ptr_t)) SET_INIT;

  Terminal *term;
  set_foreach(&to_refresh, term, {
    // Skip terminals in synchronized output — they will be refreshed
    // when the synchronized update ends (mode 2026 reset).
    if (!term->synchronized_output) {
      refresh_terminal(term);
    }
  });

  set_destroy(ptr_t, &to_refresh);
  unblock_autocmds();
}

static bool refresh_size(Terminal *term, buf_T *buf)
{
  if (!term->pending.resize || term->closed) {
    return false;
  }

  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);
  return true;
}

void on_scrollback_option_changed(Terminal *term)
{
  // Scrollback buffer may not exist yet, e.g. if 'scrollback' is set in a TermOpen autocmd.
  if (term->sb_buffer != NULL) {
    refresh_terminal(term);
  }
}

/// Adjusts scrollback storage and the terminal buffer scrollback lines
static void adjust_scrollback(Terminal *term, buf_T *buf)
{
  if (buf->b_p_scbk < 1) {  // Local 'scrollback' was set to -1.
    buf->b_p_scbk = SB_MAX;
  }
  const size_t scbk = (size_t)buf->b_p_scbk;
  assert(term->sb_current < SIZE_MAX);
  if (term->sb_pending > 0) {  // Pending rows must be processed first.
    abort();
  }

  // Delete lines exceeding the new 'scrollback' limit.
  if (scbk < term->sb_current) {
    size_t diff = term->sb_current - scbk;
    for (size_t i = 0; i < diff; i++) {
      ml_delete_buf(buf, 1, false);
      term->sb_current--;
      xfree(term->sb_buffer[term->sb_current]);
    }
    mark_adjust_buf(buf, 1, (linenr_T)diff, MAXLNUM, -(linenr_T)diff, true,
                    kMarkAdjustTerm, kExtmarkUndo);
    deleted_lines_buf(buf, 1, (linenr_T)diff);
  }

  // Resize the scrollback storage.
  size_t sb_region = sizeof(ScrollbackLine *) * scbk;
  if (scbk != term->sb_size) {
    term->sb_buffer = xrealloc(term->sb_buffer, sb_region);
  }

  term->sb_size = scbk;
}

// Refresh the scrollback of an invalidated terminal.
static void refresh_scrollback(Terminal *term, buf_T *buf)
{
  // Buffer update callbacks may poll for uv events.
  // Avoid polling for output to the same terminal as the one being refreshed.
  term->opts.read_pause_cb(true, term->opts.data);

  linenr_T deleted = (linenr_T)(term->sb_deleted - term->old_sb_deleted);
  deleted = MIN(deleted, buf->b_ml.ml_line_count);
  mark_adjust_buf(buf, 1, deleted, MAXLNUM, -deleted, true, kMarkAdjustTerm, kExtmarkUndo);
  term->old_sb_deleted = term->sb_deleted;

  int old_height = term->old_height;
  int width, height;
  vterm_get_size(term->vt, &height, &width);

  // Remove deleted scrollback lines at the top, but don't unnecessarily remove
  // lines that will be overwritten by refresh_screen().
  while (deleted > 0 && buf->b_ml.ml_line_count > old_height) {
    ml_delete_buf(buf, 1, false);
    deleted_lines_buf(buf, 1, 1);
    deleted--;
  }

  // Clamp old_height in case buffer lines have been deleted by the user.
  old_height = MIN(old_height, buf->b_ml.ml_line_count);
  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.
    fetch_row(term, -term->sb_pending, width);
    int buf_index = buf->b_ml.ml_line_count - old_height;
    ml_append_buf(buf, buf_index, term->textbuf, 0, false);
    appended_lines_buf(buf, buf_index, 1);
    term->sb_pending--;
  }

  int max_line_count = (int)term->sb_current + height;
  // Remove extra lines at the bottom.
  while (buf->b_ml.ml_line_count > max_line_count) {
    ml_delete_buf(buf, buf->b_ml.ml_line_count, false);
    deleted_lines_buf(buf, buf->b_ml.ml_line_count, 1);
  }

  adjust_scrollback(term, buf);

  term->opts.read_pause_cb(false, term->opts.data);
}

// Refresh the screen (visible part of the buffer when the terminal is
// focused) of a invalidated terminal
static void refresh_screen(Terminal *term, buf_T *buf)
{
  int changed = 0;
  int added = 0;
  int height;
  int width;
  vterm_get_size(term->vt, &height, &width);
  // Terminal height may have decreased before `invalid_end` reflects it.
  term->invalid_end = MIN(term->invalid_end, height);

  // There are no invalid rows.
  if (term->invalid_start >= term->invalid_end) {
    term->invalid_start = INT_MAX;
    term->invalid_end = -1;
    return;
  }

  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 <= buf->b_ml.ml_line_count) {
      ml_replace_buf(buf, linenr, term->textbuf, true, false);
      changed++;
    } else {
      ml_append_buf(buf, linenr - 1, term->textbuf, 0, false);
      added++;
    }
  }
  term->old_height = height;

  int change_start = row_to_linenr(term, term->invalid_start);
  int change_end = change_start + changed;
  term->invalid_start = INT_MAX;
  term->invalid_end = -1;
  // Call this after resetting the invalid region, as buffer update callbacks may
  // poll for terminal output and lead to new invalidations.
  changed_lines(buf, change_start, 0, change_end, added, true);
}

static void adjust_topline_cursor(Terminal *term, buf_T *buf, int added)
{
  linenr_T ml_end = buf->b_ml.ml_line_count;

  FOR_ALL_TAB_WINDOWS(tp, wp) {
    if (wp->w_buffer == buf) {
      if (wp == curwin && is_focused(term)) {
        // Move window cursor to terminal cursor's position and "follow" output.
        terminal_check_cursor();
        continue;
      }

      bool following = ml_end == wp->w_cursor.lnum + added;  // cursor at end?
      if (following) {
        // "Follow" the terminal output
        wp->w_cursor.lnum = ml_end;
        set_topline(wp, MAX(wp->w_cursor.lnum - wp->w_view_height + 1, 1));
      } else {
        // Ensure valid cursor for each window displaying this terminal.
        wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, ml_end);
      }
      mb_check_adjust_col(wp);
    }
  }

  if (ml_end == buf->b_last_cursor.mark.lnum + added) {
    buf->b_last_cursor.mark.lnum = ml_end;
  }

  for (size_t i = 0; i < kv_size(buf->b_wininfo); i++) {
    WinInfo *wip = kv_A(buf->b_wininfo, i);
    if (ml_end == wip->wi_mark.mark.lnum + added) {
      wip->wi_mark.mark.lnum = ml_end;
    }
  }
}

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 & MODE_TERMINAL && curbuf->terminal == term;
}

static char *get_config_string(buf_T *buf, char *key)
{
  Error err = ERROR_INIT;
  Object obj = dict_get_value(buf->b_vars, cstr_as_string(key), NULL, &err);
  api_clear_error(&err);
  if (obj.type == kObjectTypeNil) {
    obj = dict_get_value(get_globvar_dict(), cstr_as_string(key), NULL, &err);
    api_clear_error(&err);
  }
  if (obj.type == kObjectTypeString) {
    return obj.data.string.data;
  }
  api_free_object(obj);
  return NULL;
}

// }}}

// vim: foldmethod=marker
