#include "terminal.h"

#if defined(__GLIBC__)
#include <malloc.h>
#endif
#include <errno.h>
#include <limits.h>
#include <regex.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

#include <fcntl.h>
#include <linux/input-event-codes.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/timerfd.h>
#include <sys/wait.h>
#include <xdg-shell.h>

#define LOG_MODULE "terminal"
#define LOG_ENABLE_DBG 0
#include "log.h"

#include "async.h"
#include "commands.h"
#include "config.h"
#include "debug.h"
#include "emoji-variation-sequences.h"
#include "extract.h"
#include "grid.h"
#include "ime.h"
#include "input.h"
#include "notify.h"
#include "quirks.h"
#include "reaper.h"
#include "render.h"
#include "selection.h"
#include "shm.h"
#include "sixel.h"
#include "slave.h"
#include "spawn.h"
#include "url-mode.h"
#include "util.h"
#include "vt.h"
#include "xmalloc.h"
#include "xsnprintf.h"

#define PTMX_TIMING 0

static void enqueue_data_for_slave(const void *data, size_t len, size_t offset,
                                   ptmx_buffer_list_t *buffer_list) {
  struct ptmx_buffer queued = {
      .data = xmemdup(data, len),
      .len = len,
      .idx = offset,
  };
  tll_push_back(*buffer_list, queued);
}

static bool data_to_slave(struct terminal *term, const void *data, size_t len,
                          ptmx_buffer_list_t *buffer_list) {
  /*
   * Try a synchronous write first. If we fail to write everything,
   * switch to asynchronous.
   */

  size_t async_idx = 0;
  switch (async_write(term->ptmx, data, len, &async_idx)) {
  case ASYNC_WRITE_REMAIN:
    /* Switch to asynchronous mode; let FDM write the remaining data */
    if (!fdm_event_add(term->fdm, term->ptmx, EPOLLOUT))
      return false;
    enqueue_data_for_slave(data, len, async_idx, buffer_list);
    return true;

  case ASYNC_WRITE_DONE:
    return true;

  case ASYNC_WRITE_ERR:
    LOG_ERRNO("failed to synchronously write %zu bytes to slave", len);
    return false;
  }

  BUG("Unexpected async_write() return value");
  return false;
}

bool term_paste_data_to_slave(struct terminal *term, const void *data,
                              size_t len) {
  xassert(term->is_sending_paste_data);

  if (term->ptmx < 0) {
    /* We're probably in "hold" */
    return false;
  }

  if (tll_length(term->ptmx_paste_buffers) > 0) {
    /* Don't even try to send data *now* if there's queued up
     * data, since that would result in events arriving out of
     * order. */
    enqueue_data_for_slave(data, len, 0, &term->ptmx_paste_buffers);
    return true;
  }

  return data_to_slave(term, data, len, &term->ptmx_paste_buffers);
}

bool term_to_slave(struct terminal *term, const void *data, size_t len) {
  if (term->ptmx < 0) {
    /* We're probably in "hold" */
    return false;
  }

  if (unlikely(tll_length(term->ptmx_buffers) > 0 ||
               term->is_sending_paste_data ||
               tll_length(term->ptmx_paste_buffers) > 0)) {
    /*
     * Don't even try to send data *now* if there's queued up
     * data, since that would result in events arriving out of
     * order.
     *
     * Furthermore, if we're currently sending paste data to the
     * client, do *not* mix that stream with other events
     * (https://codeberg.org/dnkl/foot/issues/101).
     */
    enqueue_data_for_slave(data, len, 0, &term->ptmx_buffers);
    return true;
  }

  return data_to_slave(term, data, len, &term->ptmx_buffers);
}

static bool fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) {
  struct terminal *term = data;

  /* If there is no queued data, then we shouldn't be in asynchronous mode */
  xassert(tll_length(term->ptmx_buffers) > 0 ||
          tll_length(term->ptmx_paste_buffers) > 0);

  /* Writes a single buffer, returns if not all of it could be written */
#define write_one_buffer(buffer_list)                                          \
  {                                                                            \
    switch (                                                                   \
        async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) { \
    case ASYNC_WRITE_DONE:                                                     \
      free(it->item.data);                                                     \
      tll_remove(buffer_list, it);                                             \
      break;                                                                   \
    case ASYNC_WRITE_REMAIN:                                                   \
      /* to_slave() updated it->item.idx */                                    \
      return true;                                                             \
    case ASYNC_WRITE_ERR:                                                      \
      LOG_ERRNO("failed to asynchronously write %zu bytes to slave",           \
                it->item.len - it->item.idx);                                  \
      return false;                                                            \
    }                                                                          \
  }

  tll_foreach(term->ptmx_paste_buffers, it)
      write_one_buffer(term->ptmx_paste_buffers);

  /* If we get here, *all* paste data buffers were successfully
   * flushed */

  if (!term->is_sending_paste_data) {
    tll_foreach(term->ptmx_buffers, it) write_one_buffer(term->ptmx_buffers);
  }

  /*
   * If we get here, *all* buffers were successfully flushed.
   *
   * Or, we're still sending paste data, in which case we do *not*
   * want to send the "normal" queued up data
   *
   * In both cases, we want to *disable* the FDM callback since
   * otherwise we'd just be called right away again, with nothing to
   * write.
   */
  fdm_event_del(term->fdm, term->ptmx, EPOLLOUT);
  return true;
}

static bool add_utmp_record(const struct config *conf, struct reaper *reaper,
                            int ptmx) {
#if defined(UTMP_ADD)
  if (ptmx < 0)
    return true;
  if (conf->utmp_helper_path == NULL)
    return true;

  char *const argv[] = {conf->utmp_helper_path, UTMP_ADD,
                        getenv("WAYLAND_DISPLAY"), NULL};
  return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0;
#else
  return true;
#endif
}

static bool del_utmp_record(const struct config *conf, struct reaper *reaper,
                            int ptmx) {
#if defined(UTMP_DEL)
  if (ptmx < 0)
    return true;
  if (conf->utmp_helper_path == NULL)
    return true;

  char *del_argument =
#if defined(UTMP_DEL_HAVE_ARGUMENT)
      getenv("WAYLAND_DISPLAY")
#else
      NULL
#endif
      ;

  char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL};
  return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0;
#else
  return true;
#endif
}

#if PTMX_TIMING
static struct timespec last = {0};
#endif

static bool cursor_blink_rearm_timer(struct terminal *term);

/* Externally visible, but not declared in terminal.h, to enable pgo
 * to call this function directly */
bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) {
  struct terminal *term = data;

  const bool pollin = events & EPOLLIN;
  const bool pollout = events & EPOLLOUT;
  const bool hup = events & EPOLLHUP;

  if (pollout) {
    if (!fdm_ptmx_out(fdm, fd, events, data))
      return false;
  }

  /* Prevent blinking while typing */
  if (term->cursor_blink.fd >= 0) {
    term->cursor_blink.state = CURSOR_BLINK_ON;
    cursor_blink_rearm_timer(term);
  }

  if (unlikely(term->interactive_resizing.grid != NULL)) {
    /*
     * Don't consume PTMX while we're doing an interactive resize,
     * since the 'normal' grid we're currently using is a
     * temporary one - all changes done to it will be lost when
     * the interactive resize ends.
     */
    return true;
  }

  uint8_t buf[24 * 1024];
  const size_t max_iterations = !hup ? 10 : SIZE_MAX;

  for (size_t i = 0; i < max_iterations && pollin; i++) {
    xassert(pollin);
    ssize_t count = read(term->ptmx, buf, sizeof(buf));

    if (count < 0) {
      if (errno == EAGAIN || errno == EIO) {
        /*
         * EAGAIN: no more to read - FDM will trigger us again
         * EIO: assume PTY was closed - we already have, or will get, a EPOLLHUP
         */
        break;
      }

      LOG_ERRNO("failed to read from pseudo terminal");
      return false;
    } else if (count == 0) {
      /* Reached end-of-file */
      break;
    }

    xassert(term->interactive_resizing.grid == NULL);
    vt_from_slave(term, buf, count);

    /* Mark inactive tabs as having unread output, so the tab bar
     * can show an indicator. Force a tab-bar refresh on the active
     * terminal, since the active terminal owns the rendered bar. */
    if (term->window != NULL && term->window->tab_count > 1 &&
        term != term->window->term) {
      if (!term->has_unread) {
        term->has_unread = true;
        if (term->window->term != NULL && term->conf->tabs.enabled)
          render_refresh_tab_bar(term->window->term);
      }
    }
  }

  if (!term->render.app_sync_updates.enabled) {
    /*
     * We likely need to re-render. But, we don't want to do it
     * immediately. Often, a single client update is done through
     * multiple writes. This could lead to us rendering one frame with
     * "intermediate" state.
     *
     * For example, we might end up rendering a frame
     * where the client just erased a line, while in the
     * next frame, the client wrote to the same line. This
     * causes screen "flickering".
     *
     * Mitigate by always incuring a small delay before
     * rendering the next frame. This gives the client
     * some time to finish the operation (and thus gives
     * us time to receive the last writes before doing any
     * actual rendering).
     *
     * We incur this delay *every* time we receive
     * input. To ensure we don't delay rendering
     * indefinitely, we start a second timer that is only
     * reset when we render.
     *
     * Note that when the client is producing data at a
     * very high pace, we're rate limited by the wayland
     * compositor anyway. The delay we introduce here only
     * has any effect when the renderer is idle.
     */
    uint64_t lower_ns = term->conf->tweak.delayed_render_lower_ns;
    uint64_t upper_ns = term->conf->tweak.delayed_render_upper_ns;

    if (lower_ns > 0 && upper_ns > 0) {
#if PTMX_TIMING
      struct timespec now;

      clock_gettime(CLOCK_MONOTONIC, &now);
      if (last.tv_sec > 0 || last.tv_nsec > 0) {
        struct timespec diff;

        timespec_sub(&now, &last, &diff);
        LOG_INFO("waited %lds %ldns for more input", (long)diff.tv_sec,
                 diff.tv_nsec);
      }
      last = now;
#endif

      xassert(lower_ns < 1000000000);
      xassert(upper_ns < 1000000000);
      xassert(upper_ns > lower_ns);

      timerfd_settime(term->delayed_render_timer.lower_fd, 0,
                      &(struct itimerspec){.it_value = {.tv_nsec = lower_ns}},
                      NULL);

      /* Second timeout - only reset when we render. Set to one
       * frame (assuming 60Hz) */
      if (!term->delayed_render_timer.is_armed) {
        timerfd_settime(term->delayed_render_timer.upper_fd, 0,
                        &(struct itimerspec){.it_value = {.tv_nsec = upper_ns}},
                        NULL);
        term->delayed_render_timer.is_armed = true;
      }
    } else
      render_refresh(term);
  }

  if (hup) {
    del_utmp_record(term->conf, term->reaper, term->ptmx);
    fdm_del(fdm, fd);
    term->ptmx = -1;

    /*
     * Normally, we do *not* want to shutdown when the PTY is
     * closed. Instead, we want to wait for the client application
     * to exit.
     *
     * However, when we're using a pre-existing PTY (the --pty
     * option), there _is_ no client application. That is, foot
     * does *not* fork+exec anything, and thus the only way to
     * shutdown is to wait for the PTY to be closed.
     */
    if (term->slave < 0 && !term->conf->hold_at_exit) {
      term_shutdown(term);
    }
  }

  return true;
}

bool term_ptmx_pause(struct terminal *term) {
  if (term->ptmx < 0)
    return false;
  return fdm_event_del(term->fdm, term->ptmx, EPOLLIN);
}

bool term_ptmx_resume(struct terminal *term) {
  if (term->ptmx < 0)
    return false;
  return fdm_event_add(term->fdm, term->ptmx, EPOLLIN);
}

static bool fdm_flash(struct fdm *fdm, int fd, int events, void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;
  uint64_t expiration_count;
  ssize_t ret =
      read(term->flash.fd, &expiration_count, sizeof(expiration_count));

  if (ret < 0) {
    if (errno == EAGAIN)
      return true;

    LOG_ERRNO("failed to read flash timer");
    return false;
  }

  LOG_DBG("flash timer expired %llu times",
          (unsigned long long)expiration_count);

  term->flash.active = false;
  render_overlay(term);

  // since the overlay surface is synced with the main window surface, we have
  // to commit the main surface for the compositor to acknowledge the new
  // overlay state.
  wl_surface_commit(term->window->surface.surf);
  return true;
}

static bool fdm_blink(struct fdm *fdm, int fd, int events, void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;
  uint64_t expiration_count;
  ssize_t ret =
      read(term->blink.fd, &expiration_count, sizeof(expiration_count));

  if (ret < 0) {
    if (errno == EAGAIN)
      return true;

    LOG_ERRNO("failed to read blink timer");
    return false;
  }

  LOG_DBG("blink timer expired %llu times",
          (unsigned long long)expiration_count);

  /* Invert blink state */
  term->blink.state = term->blink.state == BLINK_ON ? BLINK_OFF : BLINK_ON;

  /* Scan all visible cells and mark rows with blinking cells dirty */
  bool no_blinking_cells = true;
  for (int r = 0; r < term->rows; r++) {
    struct row *row = grid_row_in_view(term->grid, r);
    for (int col = 0; col < term->cols; col++) {
      struct cell *cell = &row->cells[col];

      if (cell->attrs.blink) {
        cell->attrs.clean = 0;
        row->dirty = true;
        no_blinking_cells = false;
      }
    }
  }

  if (no_blinking_cells) {
    LOG_DBG("disarming blink timer");

    term->blink.state = BLINK_ON;
    fdm_del(term->fdm, term->blink.fd);
    term->blink.fd = -1;
  } else
    render_refresh(term);
  return true;
}

void term_arm_blink_timer(struct terminal *term) {
  if (term->blink.fd >= 0)
    return;

  LOG_DBG("arming blink timer");

  int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
  if (fd < 0) {
    LOG_ERRNO("failed to create blink timer FD");
    return;
  }

  if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_blink, term)) {
    close(fd);
    return;
  }

  struct itimerspec alarm = {
      .it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000},
      .it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000},
  };

  if (timerfd_settime(fd, 0, &alarm, NULL) < 0) {
    LOG_ERRNO("failed to arm blink timer");
    fdm_del(term->fdm, fd);
  }

  term->blink.fd = fd;
}

static void cursor_refresh(struct terminal *term) {
  if (!term->window->is_configured)
    return;

  term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0;
  term->grid->cur_row->dirty = true;
  render_refresh(term);
}

static bool fdm_cursor_blink(struct fdm *fdm, int fd, int events, void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;
  uint64_t expiration_count;
  ssize_t ret =
      read(term->cursor_blink.fd, &expiration_count, sizeof(expiration_count));

  if (ret < 0) {
    if (errno == EAGAIN)
      return true;

    LOG_ERRNO("failed to read cursor blink timer");
    return false;
  }

  LOG_DBG("cursor blink timer expired %llu times",
          (unsigned long long)expiration_count);

  /* Invert blink state */
  term->cursor_blink.state = term->cursor_blink.state == CURSOR_BLINK_ON
                                 ? CURSOR_BLINK_OFF
                                 : CURSOR_BLINK_ON;

  cursor_refresh(term);
  return true;
}

static bool fdm_delayed_render(struct fdm *fdm, int fd, int events,
                               void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;

  uint64_t unused;
  ssize_t ret1 = 0;
  ssize_t ret2 = 0;

  if (fd == term->delayed_render_timer.lower_fd)
    ret1 = read(term->delayed_render_timer.lower_fd, &unused, sizeof(unused));
  if (fd == term->delayed_render_timer.upper_fd)
    ret2 = read(term->delayed_render_timer.upper_fd, &unused, sizeof(unused));

  if ((ret1 < 0 || ret2 < 0)) {
    if (errno == EAGAIN)
      return true;

    LOG_ERRNO("failed to read timeout timer");
    return false;
  }

  if (ret1 > 0)
    LOG_DBG("lower delay timer expired");
  else if (ret2 > 0)
    LOG_DBG("upper delay timer expired");

  if (ret1 == 0 && ret2 == 0)
    return true;

#if PTMX_TIMING
  last = (struct timespec){0};
#endif

  /* Reset timers */
  struct itimerspec reset = {{0}};
  timerfd_settime(term->delayed_render_timer.lower_fd, 0, &reset, NULL);
  timerfd_settime(term->delayed_render_timer.upper_fd, 0, &reset, NULL);
  term->delayed_render_timer.is_armed = false;

  render_refresh(term);
  return true;
}

static bool fdm_app_sync_updates_timeout(struct fdm *fdm, int fd, int events,
                                         void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;
  uint64_t unused;
  ssize_t ret =
      read(term->render.app_sync_updates.timer_fd, &unused, sizeof(unused));

  if (ret < 0) {
    if (errno == EAGAIN)
      return true;
    LOG_ERRNO("failed to read application synchronized updates timeout timer");
    return false;
  }

  term_disable_app_sync_updates(term);
  return true;
}

static bool fdm_title_update_timeout(struct fdm *fdm, int fd, int events,
                                     void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;
  uint64_t unused;
  ssize_t ret = read(term->render.title.timer_fd, &unused, sizeof(unused));

  if (ret < 0) {
    if (errno == EAGAIN)
      return true;
    LOG_ERRNO("failed to read title update throttle timer");
    return false;
  }

  struct itimerspec reset = {{0}};
  timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL);

  render_refresh_title(term);
  return true;
}

static bool fdm_icon_update_timeout(struct fdm *fdm, int fd, int events,
                                    void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;
  uint64_t unused;
  ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused));

  if (ret < 0) {
    if (errno == EAGAIN)
      return true;
    LOG_ERRNO("failed to read icon update throttle timer");
    return false;
  }

  struct itimerspec reset = {{0}};
  timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL);

  render_refresh_icon(term);
  return true;
}

static bool fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events,
                                      void *data) {
  if (events & EPOLLHUP)
    return false;

  struct terminal *term = data;
  uint64_t unused;
  ssize_t ret = read(term->render.app_id.timer_fd, &unused, sizeof(unused));

  if (ret < 0) {
    if (errno == EAGAIN)
      return true;
    LOG_ERRNO("failed to read app ID update throttle timer");
    return false;
  }

  struct itimerspec reset = {{0}};
  timerfd_settime(term->render.app_id.timer_fd, 0, &reset, NULL);

  render_refresh_app_id(term);
  return true;
}

static bool initialize_render_workers(struct terminal *term) {
  LOG_INFO("using %hu rendering threads", term->render.workers.count);

  if (sem_init(&term->render.workers.start, 0, 0) < 0 ||
      sem_init(&term->render.workers.done, 0, 0) < 0) {
    LOG_ERRNO("failed to instantiate render worker semaphores");
    return false;
  }

  int err;
  if ((err = mtx_init(&term->render.workers.lock, mtx_plain)) != thrd_success) {
    LOG_ERR("failed to instantiate render worker mutex: %s (%d)",
            thrd_err_as_string(err), err);
    goto err_sem_destroy;
  }

  mtx_init(&term->render.workers.preapplied_damage.lock, mtx_plain);
  cnd_init(&term->render.workers.preapplied_damage.cond);

  term->render.workers.threads = xcalloc(
      term->render.workers.count, sizeof(term->render.workers.threads[0]));

  for (size_t i = 0; i < term->render.workers.count; i++) {
    struct render_worker_context *ctx = xmalloc(sizeof(*ctx));
    *ctx = (struct render_worker_context){
        .term = term,
        .my_id = 1 + i,
    };

    int ret = thrd_create(&term->render.workers.threads[i],
                          &render_worker_thread, ctx);
    if (ret != thrd_success) {

      LOG_ERR("failed to create render worker thread: %s (%d)",
              thrd_err_as_string(ret), ret);
      term->render.workers.threads[i] = 0;
      return false;
    }
  }

  return true;

err_sem_destroy:
  sem_destroy(&term->render.workers.start);
  sem_destroy(&term->render.workers.done);
  return false;
}

static void free_custom_glyph(struct fcft_glyph **glyph) {
  if (*glyph == NULL)
    return;

  free(pixman_image_get_data((*glyph)->pix));
  pixman_image_unref((*glyph)->pix);
  free(*glyph);
  *glyph = NULL;
}

static void free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) {
  if (*glyphs == NULL)
    return;

  for (size_t i = 0; i < count; i++)
    free_custom_glyph(&(*glyphs)[i]);

  free(*glyphs);
  *glyphs = NULL;
}

static void term_line_height_update(struct terminal *term) {
  const struct config *conf = term->conf;

  if (term->conf->line_height.px < 0) {
    term->font_line_height.pt = 0;
    term->font_line_height.px = -1;
    return;
  }

  const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.;

  const float font_original_pt_size =
      conf->fonts[0].arr[0].px_size > 0
          ? conf->fonts[0].arr[0].px_size * 72. / dpi
          : conf->fonts[0].arr[0].pt_size;
  const float font_current_pt_size =
      term->font_sizes[0][0].px_size > 0
          ? term->font_sizes[0][0].px_size * 72. / dpi
          : term->font_sizes[0][0].pt_size;

  const float change = font_current_pt_size / font_original_pt_size;
  const float line_original_pt_size = conf->line_height.px > 0
                                          ? conf->line_height.px * 72. / dpi
                                          : conf->line_height.pt;

  term->font_line_height.px = 0;
  term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.);
}

static bool term_set_fonts(struct terminal *term,
                           struct fcft_font *fonts[static 4],
                           bool resize_grid) {
  for (size_t i = 0; i < 4; i++) {
    xassert(fonts[i] != NULL);

    fcft_destroy(term->fonts[i]);
    term->fonts[i] = fonts[i];
  }

  free_custom_glyphs(&term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT);
  free_custom_glyphs(&term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT);
  free_custom_glyphs(&term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT);
  free_custom_glyphs(&term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT);

  const struct config *conf = term->conf;

  const struct fcft_glyph *M =
      fcft_rasterize_char_utf32(fonts[0], U'M', term->font_subpixel);
  int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x;

  term_line_height_update(term);

  term->cell_width =
      advance + term_pt_or_px_as_pixels(term, &conf->letter_spacing);

  term->cell_height =
      term->font_line_height.px >= 0
          ? term_pt_or_px_as_pixels(term, &term->font_line_height)
          : max(term->fonts[0]->height,
                term->fonts[0]->ascent + term->fonts[0]->descent);

  if (term->cell_width <= 0)
    term->cell_width = 1;
  if (term->cell_height <= 0)
    term->cell_height = 1;

  term->font_x_ofs =
      term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset);
  term->font_y_ofs =
      term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset);

  term->font_baseline = term_font_baseline(term);

  LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height);

  sixel_cell_size_changed(term);

  /* Optimization - some code paths (are forced to) call
   * render_resize() after this function */
  if (resize_grid) {
    /* Use force, since cell-width/height may have changed */
    enum resize_options resize_opts = RESIZE_FORCE;
    if (conf->resize_keep_grid)
      resize_opts |= RESIZE_KEEP_GRID;

    render_resize(term, (int)roundf(term->width / term->scale),
                  (int)roundf(term->height / term->scale), resize_opts);
  }
  return true;
}

static float get_font_dpi(const struct terminal *term) {
  /*
   * Use output's DPI to scale font. This is to ensure the font has
   * the same physical height (if measured by a ruler) regardless of
   * monitor.
   *
   * Conceptually, we use the physical monitor specs to calculate
   * the DPI, and we ignore the output's scaling factor.
   *
   * However, to deal with legacy fractional scaling, where we're
   * told to render at e.g. 2x, but are then downscaled by the
   * compositor to e.g. 1.25, we use the scaled DPI value multiplied
   * by the scale factor instead.
   *
   * For integral scaling factors the resulting DPI is the same as
   * if we had used the physical DPI.
   *
   * For legacy fractional scaling factors we'll get a DPI *larger*
   * than the physical DPI, that ends up being right when later
   * downscaled by the compositor.
   *
   * With the newer fractional-scale-v1 protocol, we use the
   * monitor's real DPI, since we scale everything to the correct
   * scaling factor (no downscaling done by the compositor).
   */

  const struct wl_window *win = term->window;
  const struct monitor *mon = NULL;

  if (tll_length(win->on_outputs) > 0)
    mon = tll_back(win->on_outputs);
  else {
    if (term->font_dpi_before_unmap > 0.) {
      /*
       * Use last known "good" DPI
       *
       * This avoids flickering when window is unmapped/mapped
       * (some compositors do this when a window is minimized),
       * on a multi-monitor setup with different monitor DPIs.
       */
      return term->font_dpi_before_unmap;
    }

    if (tll_length(term->wl->monitors) > 0)
      mon = &tll_front(term->wl->monitors);
  }

  const float monitor_dpi =
      mon != NULL
          ? term_fractional_scaling(term) ? mon->dpi.physical : mon->dpi.scaled
          : 96.;

  return monitor_dpi > 0. ? monitor_dpi : 96.;
}

static enum fcft_subpixel get_font_subpixel(const struct terminal *term) {
  if (term->colors.alpha != 0xffff) {
    /* Can't do subpixel rendering on transparent background */
    return FCFT_SUBPIXEL_NONE;
  }

  enum wl_output_subpixel wl_subpixel;

  /*
   * Wayland doesn't tell us *which* part of the surface that goes
   * on a specific output, only whether the surface is mapped to an
   * output or not.
   *
   * Thus, when determining which subpixel mode to use, we can't do
   * much but select *an* output. So, we pick the one we were most
   * recently mapped on.
   *
   * If we're not mapped at all, we pick the first available
   * monitor, and hope that's where we'll eventually get mapped.
   *
   * If there aren't any monitors we use the "default" subpixel
   * mode.
   */

  if (tll_length(term->window->on_outputs) > 0)
    wl_subpixel = tll_back(term->window->on_outputs)->subpixel;
  else if (tll_length(term->wl->monitors) > 0)
    wl_subpixel = tll_front(term->wl->monitors).subpixel;
  else
    wl_subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN;

  switch (wl_subpixel) {
  case WL_OUTPUT_SUBPIXEL_UNKNOWN:
    return FCFT_SUBPIXEL_DEFAULT;
  case WL_OUTPUT_SUBPIXEL_NONE:
    return FCFT_SUBPIXEL_NONE;
  case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB:
    return FCFT_SUBPIXEL_HORIZONTAL_RGB;
  case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR:
    return FCFT_SUBPIXEL_HORIZONTAL_BGR;
  case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB:
    return FCFT_SUBPIXEL_VERTICAL_RGB;
  case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR:
    return FCFT_SUBPIXEL_VERTICAL_BGR;
  }

  return FCFT_SUBPIXEL_DEFAULT;
}

int term_pt_or_px_as_pixels(const struct terminal *term,
                            const struct pt_or_px *pt_or_px) {
  float scale = !term->font_is_sized_by_dpi ? term->scale : 1.;
  float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.;

  return pt_or_px->px == 0 ? (int)roundf(pt_or_px->pt * scale * dpi / 72)
                           : (int)roundf(pt_or_px->px * scale);
}

struct font_load_data {
  size_t count;
  const char **names;
  const char *attrs;

  const struct fcft_font_options *options;
  struct fcft_font **font;
};

static int font_loader_thread(void *_data) {
  struct font_load_data *data = _data;
  *data->font =
      fcft_from_name2(data->count, data->names, data->attrs, data->options);
  return *data->font != NULL;
}

static bool reload_fonts(struct terminal *term, bool resize_grid) {
  const struct config *conf = term->conf;

  const size_t counts[4] = {
      conf->fonts[0].count,
      conf->fonts[1].count,
      conf->fonts[2].count,
      conf->fonts[3].count,
  };

  /* Configure size (which may have been changed run-time) */
  char **names[4];
  for (size_t i = 0; i < 4; i++) {
    names[i] = xmalloc(counts[i] * sizeof(names[i][0]));

    const struct config_font_list *font_list = &conf->fonts[i];

    for (size_t j = 0; j < font_list->count; j++) {
      const struct config_font *font = &font_list->arr[j];
      bool use_px_size = term->font_sizes[i][j].px_size > 0;
      char size[64];

      const float scale = term->font_is_sized_by_dpi ? 1. : term->scale;

      if (use_px_size)
        snprintf(size, sizeof(size), ":pixelsize=%d",
                 (int)roundf(term->font_sizes[i][j].px_size * scale));
      else
        snprintf(size, sizeof(size), ":size=%.2f",
                 term->font_sizes[i][j].pt_size * scale);

      names[i][j] = xstrjoin(font->pattern, size);
    }
  }

  /* Did user configure custom bold/italic fonts?
   * Or should we use the regular font, with weight/slant attributes? */
  const bool custom_bold = counts[1] > 0;
  const bool custom_italic = counts[2] > 0;
  const bool custom_bold_italic = counts[3] > 0;

  const size_t count_regular = counts[0];
  const char **names_regular = (const char **)names[0];

  const size_t count_bold = custom_bold ? counts[1] : counts[0];
  const char **names_bold = (const char **)(custom_bold ? names[1] : names[0]);

  const size_t count_italic = custom_italic ? counts[2] : counts[0];
  const char **names_italic =
      (const char **)(custom_italic ? names[2] : names[0]);

  const size_t count_bold_italic = custom_bold_italic ? counts[3] : counts[0];
  const char **names_bold_italic =
      (const char **)(custom_bold_italic ? names[3] : names[0]);

  const bool use_dpi = term->font_is_sized_by_dpi;
  char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.);

  char *attrs[4] = {
      [0] = dpi, /* Takes ownership */
      [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""),
      [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""),
      [3] =
          xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""),
  };

  struct fcft_font_options *options = fcft_font_options_create();

  options->scaling_filter = conf->tweak.fcft_filter;
  options->color_glyphs.format = PIXMAN_a8r8g8b8;
  options->color_glyphs.srgb_decode =
      wayl_do_linear_blending(term->wl, term->conf);

  if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) {
    /*
     * Use a high-res buffer type for emojis. We don't want to use
     * an a2r10g0b10 type of surface, since we need more than 2
     * bits for alpha.
     */
#if defined(HAVE_PIXMAN_RGBA_16)
    options->color_glyphs.format = PIXMAN_a16b16g16r16;
#else
    options->color_glyphs.format = PIXMAN_rgba_float;
#endif
  }

  struct fcft_font *fonts[4];
  struct font_load_data data[4] = {
      {count_regular, names_regular, attrs[0], options, &fonts[0]},
      {count_bold, names_bold, attrs[1], options, &fonts[1]},
      {count_italic, names_italic, attrs[2], options, &fonts[2]},
      {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]},
  };

  thrd_t tids[4] = {0};
  for (size_t i = 0; i < 4; i++) {
    int ret = thrd_create(&tids[i], &font_loader_thread, &data[i]);
    if (ret != thrd_success) {
      LOG_ERR("failed to create font loader thread: %s (%d)",
              thrd_err_as_string(ret), ret);
      break;
    }
  }

  bool success = true;
  for (size_t i = 0; i < 4; i++) {
    if (tids[i] != 0) {
      int ret;
      if (thrd_join(tids[i], &ret) != thrd_success)
        success = false;
      else
        success = success && ret;
    } else
      success = false;
  }

  fcft_font_options_destroy(options);

  for (size_t i = 0; i < 4; i++) {
    for (size_t j = 0; j < counts[i]; j++)
      free(names[i][j]);
    free(names[i]);
    free(attrs[i]);
  }

  if (!success) {
    LOG_ERR("failed to load primary fonts");
    for (size_t i = 0; i < 4; i++) {
      fcft_destroy(fonts[i]);
      fonts[i] = NULL;
    }
  }

  return success ? term_set_fonts(term, fonts, resize_grid) : success;
}

static bool load_fonts_from_conf(struct terminal *term) {
  const struct config *conf = term->conf;

  for (size_t i = 0; i < 4; i++) {
    const struct config_font_list *font_list = &conf->fonts[i];

    for (size_t j = 0; j < font_list->count; j++) {
      const struct config_font *font = &font_list->arr[j];
      term->font_sizes[i][j] = (struct config_font){.pt_size = font->pt_size,
                                                    .px_size = font->px_size};
    }
  }

  return reload_fonts(term, true);
}

static void fdm_client_terminated(struct reaper *reaper, pid_t pid, int status,
                                  void *data);

static const int PTY_OPEN_FLAGS = O_RDWR | O_NOCTTY;

struct terminal *term_init(const struct config *conf, struct fdm *fdm,
                           struct reaper *reaper, struct wayland *wayl,
                           const char *foot_exe, const char *cwd,
                           const char *token, const char *pty_path, int argc,
                           char *const *argv, const char *const *envp,
                           void (*shutdown_cb)(void *data, int exit_code),
                           void *shutdown_data) {
  int ptmx = -1;
  int flash_fd = -1;
  int delay_lower_fd = -1;
  int delay_upper_fd = -1;
  int app_sync_updates_fd = -1;
  int title_update_fd = -1;
  int icon_update_fd = -1;
  int app_id_update_fd = -1;

  struct terminal *term = malloc(sizeof(*term));
  if (unlikely(term == NULL)) {
    LOG_ERRNO("malloc() failed");
    return NULL;
  }

  ptmx =
      pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS);
  if (ptmx < 0) {
    LOG_ERRNO("failed to open PTY");
    goto close_fds;
  }
  if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) <
      0) {
    LOG_ERRNO("failed to create flash timer FD");
    goto close_fds;
  }
  if ((delay_lower_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
      (delay_upper_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) {
    LOG_ERRNO("failed to create delayed rendering timer FDs");
    goto close_fds;
  }

  if ((app_sync_updates_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) {
    LOG_ERRNO("failed to create application synchronized updates timer FD");
    goto close_fds;
  }

  if ((title_update_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) {
    LOG_ERRNO("failed to create title update throttle timer FD");
    goto close_fds;
  }

  if ((icon_update_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) {
    LOG_ERRNO("failed to create icon update throttle timer FD");
    goto close_fds;
  }

  if ((app_id_update_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) {
    LOG_ERRNO("failed to create app ID update throttle timer FD");
    goto close_fds;
  }

  if (ioctl(ptmx, (unsigned int)TIOCSWINSZ,
            &(struct winsize){.ws_row = 24, .ws_col = 80}) < 0) {
    LOG_ERRNO("failed to set initial TIOCSWINSZ");
    goto close_fds;
  }

  /* Need to register *very* early (before the first "goto err"), to
   * ensure term_destroy() doesn't unref a key-binding we haven't
   * yet ref:d */
  key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf);

  int ptmx_flags;
  if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 ||
      fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) {
    LOG_ERRNO("failed to configure ptmx as non-blocking");
    goto err;
  }

  /*
   * Enable all FDM callbackes *except* ptmx - we can't do that
   * until the window has been 'configured' since we don't have a
   * size (and thus no grid) before then.
   */

  if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) ||
      !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) ||
      !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) ||
      !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout,
               term) ||
      !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout,
               term) ||
      !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) ||
      !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout,
               term)) {
    goto err;
  }

  const enum shm_bit_depth desired_bit_depth =
      conf->tweak.surface_bit_depth == SHM_BITS_AUTO
          ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8
          : conf->tweak.surface_bit_depth;

  const struct color_theme *theme = NULL;
  switch (conf->initial_color_theme) {
  case COLOR_THEME_DARK:
    theme = &conf->colors_dark;
    break;
  case COLOR_THEME_LIGHT:
    theme = &conf->colors_light;
    break;
  case COLOR_THEME_1:
    BUG("COLOR_THEME_1 should not be used");
    break;
  case COLOR_THEME_2:
    BUG("COLOR_THEME_2 should not be used");
    break;
  }

  /* Initialize configure-based terminal attributes */
  *term = (struct terminal){
      .fdm = fdm,
      .reaper = reaper,
      .conf = conf,
      .slave = -1,
      .ptmx = ptmx,
      .ptmx_buffers = tll_init(),
      .ptmx_paste_buffers = tll_init(),
      .font_sizes =
          {
              xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count),
              xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count),
              xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count),
              xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count),
          },
      .font_dpi = 0.,
      .font_dpi_before_unmap = -1.,
      .font_subpixel = (theme->alpha == 0xffff /* Can't do subpixel rendering on
                                                  transparent background */
                            ? FCFT_SUBPIXEL_DEFAULT
                            : FCFT_SUBPIXEL_NONE),
      .cursor_keys_mode = CURSOR_KEYS_NORMAL,
      .keypad_keys_mode = KEYPAD_NUMERICAL,
      .reverse_wrap = true,
      .auto_margin = true,
      .window_title_stack = tll_init(),
      .scale = 1.,
      .scale_before_unmap = -1,
      .flash = {.fd = flash_fd},
      .blink = {.fd = -1},
      .vt =
          {
              .state = 0, /* STATE_GROUND */
          },
      .colors =
          {
              .fg = theme->fg,
              .bg = theme->bg,
              .alpha = theme->alpha,
              .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) |
                           theme->cursor.text,
              .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) |
                           theme->cursor.cursor,
              .selection_fg = theme->selection_fg,
              .selection_bg = theme->selection_bg,
              .active_theme = conf->initial_color_theme,
          },
      .color_stack =
          {
              .stack = NULL,
              .size = 0,
              .idx = 0,
          },
      .origin = ORIGIN_ABSOLUTE,
      .cursor_style = conf->cursor.style,
      .cursor_blink =
          {
              .decset = false,
              .deccsusr = conf->cursor.blink.enabled,
              .state = CURSOR_BLINK_ON,
              .fd = -1,
          },
      .selection =
          {
              .coords =
                  {
                      .start = {-1, -1},
                      .end = {-1, -1},
                  },
              .pivot =
                  {
                      .start = {-1, -1},
                      .end = {-1, -1},
                  },
              .auto_scroll =
                  {
                      .fd = -1,
                  },
          },
      .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
      .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
      .grid = &term->normal,
      .composed = NULL,
      .alt_scrolling = conf->mouse.alternate_scroll_mode,
      .meta =
          {
              .esc_prefix = true,
              .eight_bit = true,
          },
      .num_lock_modifier = true,
      .bell_action_enabled = true,
      .tab_stops = tll_init(),
      .wl = wayl,
      .render =
          {
              .chains =
                  {
                      .grid = shm_chain_new(
                          wayl, true, 1 + conf->render_worker_count,
                          desired_bit_depth, &render_buffer_release_callback,
                          term),
                      .search = shm_chain_new(wayl, false, 1, desired_bit_depth,
                                              NULL, NULL),
                      .scrollback_indicator = shm_chain_new(
                          wayl, false, 1, desired_bit_depth, NULL, NULL),
                      .render_timer = shm_chain_new(
                          wayl, false, 1, desired_bit_depth, NULL, NULL),
                      .url = shm_chain_new(wayl, false, 1, desired_bit_depth,
                                           NULL, NULL),
                      .csd = shm_chain_new(wayl, false, 1, desired_bit_depth,
                                           NULL, NULL),
                      .overlay = shm_chain_new(wayl, false, 1,
                                               desired_bit_depth, NULL, NULL),
                      .tab_bar = shm_chain_new(wayl, false, 1,
                                               desired_bit_depth, NULL, NULL),
                      .tab_overview = shm_chain_new(
                          wayl, false, 1, desired_bit_depth, NULL, NULL),
                  },
              .scrollback_lines = conf->scrollback.lines,
              .app_sync_updates.timer_fd = app_sync_updates_fd,
              .title =
                  {
                      .timer_fd = title_update_fd,
                  },
              .icon =
                  {
                      .timer_fd = icon_update_fd,
                  },
              .app_id =
                  {
                      .timer_fd = app_id_update_fd,
                  },
              .workers =
                  {
                      .count = conf->render_worker_count,
                      .queue = tll_init(),
                  },
          },
      .delayed_render_timer =
          {
              .is_armed = false,
              .lower_fd = delay_lower_fd,
              .upper_fd = delay_upper_fd,
          },
      .sixel =
          {
              .scrolling = true,
              .use_private_palette = true,
              .palette_size = SIXEL_MAX_COLORS,
              .max_width = SIXEL_MAX_WIDTH,
              .max_height = SIXEL_MAX_HEIGHT,
          },
      .shutdown =
          {
              .terminate_timeout_fd = -1,
              .cb = shutdown_cb,
              .cb_data = shutdown_data,
          },
      .foot_exe = xstrdup(foot_exe),
      .cwd = xstrdup(cwd),
      .grapheme_shaping = conf->tweak.grapheme_shaping,
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
      .ime_enabled = true,
#endif
      .active_notifications = tll_init(),
  };

  pixman_region32_init(&term->render.last_overlay_clip);

  term_update_ascii_printer(term);
  memcpy(term->colors.table, theme->table, sizeof(term->colors.table));

  for (size_t i = 0; i < 4; i++) {
    const struct config_font_list *font_list = &conf->fonts[i];
    for (size_t j = 0; j < font_list->count; j++) {
      const struct config_font *font = &font_list->arr[j];
      term->font_sizes[i][j] = (struct config_font){.pt_size = font->pt_size,
                                                    .px_size = font->px_size};
    }
  }

  for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
    term->notification_icons[i].tmp_file_fd = -1;
  }

  add_utmp_record(conf, reaper, ptmx);

  if (!pty_path) {
    /* Start the slave/client */
    if ((term->slave = slave_spawn(term->ptmx, argc, term->cwd, argv, envp,
                                   &conf->env_vars, conf->term, conf->shell,
                                   conf->login_shell, &conf->notifications)) ==
        -1) {
      goto err;
    }

    reaper_add(term->reaper, term->slave, &fdm_client_terminated, term);
  }

  /* Guess scale; we're not mapped yet, so we don't know on which
   * output we'll be. Use scaling factor from first monitor */
  xassert(tll_length(term->wl->monitors) > 0);
  term->scale = tll_front(term->wl->monitors).scale;

  /* Initialize the Wayland window backend */
  if ((term->window = wayl_win_init(term, token)) == NULL)
    goto err;

  /* Load fonts */
  if (!term_font_dpi_changed(term, 0.))
    goto err;

  term->font_subpixel = get_font_subpixel(term);

  term_set_window_title(term, conf->title);

  /* Let the Wayland backend know we exist */
  tll_push_back(wayl->terms, term);

  switch (conf->startup_mode) {
  case STARTUP_WINDOWED:
    break;

  case STARTUP_MAXIMIZED:
    xdg_toplevel_set_maximized(term->window->xdg_toplevel);
    break;

  case STARTUP_FULLSCREEN:
    xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL);
    break;
  }

  if (!initialize_render_workers(term))
    goto err;

  return term;

err:
  term->shutdown.in_progress = true;
  term_destroy(term);
  return NULL;

close_fds:
  close(ptmx);
  fdm_del(fdm, flash_fd);
  fdm_del(fdm, delay_lower_fd);
  fdm_del(fdm, delay_upper_fd);
  fdm_del(fdm, app_sync_updates_fd);
  fdm_del(fdm, title_update_fd);
  fdm_del(fdm, icon_update_fd);
  fdm_del(fdm, app_id_update_fd);

  free(term);
  return NULL;
}

struct terminal *term_tab_new(struct terminal *primary, int argc,
                              char *const *argv, const char *const *envp,
                              void (*shutdown_cb)(void *data, int exit_code),
                              void *shutdown_data) {
  return term_tab_new_with_cwd(primary, NULL, argc, argv, envp, shutdown_cb,
                               shutdown_data);
}

struct terminal *term_tab_new_with_cwd(struct terminal *primary,
                                       const char *override_cwd, int argc,
                                       char *const *argv,
                                       const char *const *envp,
                                       void (*shutdown_cb)(void *data,
                                                           int exit_code),
                                       void *shutdown_data) {
  struct wl_window *win = primary->window;

  if (win->tab_count >= TAB_MAX) {
    LOG_ERR("maximum number of tabs (%d) reached", TAB_MAX);
    return NULL;
  }

  const struct config *conf = primary->conf;
  struct fdm *fdm = primary->fdm;
  struct reaper *reaper = primary->reaper;
  struct wayland *wayl = primary->wl;

  int ptmx = -1;
  int flash_fd = -1;
  int delay_lower_fd = -1;
  int delay_upper_fd = -1;
  int app_sync_updates_fd = -1;
  int title_update_fd = -1;
  int icon_update_fd = -1;
  int app_id_update_fd = -1;

  struct terminal *term = malloc(sizeof(*term));
  if (unlikely(term == NULL)) {
    LOG_ERRNO("malloc() failed");
    return NULL;
  }

  ptmx = posix_openpt(PTY_OPEN_FLAGS);
  if (ptmx < 0) {
    LOG_ERRNO("failed to open PTY for new tab");
    goto close_fds;
  }
  if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) <
          0 ||
      (delay_lower_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
      (delay_upper_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
      (app_sync_updates_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
      (title_update_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
      (icon_update_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 ||
      (app_id_update_fd =
           timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) {
    LOG_ERRNO("failed to create timer FDs for new tab");
    goto close_fds;
  }

  if (ioctl(ptmx, (unsigned int)TIOCSWINSZ,
            &(struct winsize){.ws_row = (unsigned short)primary->rows,
                              .ws_col = (unsigned short)primary->cols}) < 0) {
    LOG_ERRNO("failed to set TIOCSWINSZ for new tab");
    goto close_fds;
  }

  key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf);

  int ptmx_flags;
  if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 ||
      fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) {
    LOG_ERRNO("failed to configure ptmx as non-blocking");
    goto err;
  }

  const enum shm_bit_depth desired_bit_depth =
      conf->tweak.surface_bit_depth == SHM_BITS_AUTO
          ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8
          : conf->tweak.surface_bit_depth;

  const struct color_theme *theme = NULL;
  switch (conf->initial_color_theme) {
  case COLOR_THEME_DARK:
    theme = &conf->colors_dark;
    break;
  case COLOR_THEME_LIGHT:
    theme = &conf->colors_light;
    break;
  case COLOR_THEME_1:
    BUG("COLOR_THEME_1 should not be used");
    break;
  case COLOR_THEME_2:
    BUG("COLOR_THEME_2 should not be used");
    break;
  }

  *term = (struct terminal){
      .fdm = fdm,
      .reaper = reaper,
      .conf = conf,
      .is_tab = true,
      .slave = -1,
      .ptmx = ptmx,
      .ptmx_buffers = tll_init(),
      .ptmx_paste_buffers = tll_init(),
      .font_sizes =
          {
              xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count),
              xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count),
              xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count),
              xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count),
          },
      .font_dpi = 0.,
      .font_dpi_before_unmap = -1.,
      .font_subpixel =
          (theme->alpha == 0xffff ? FCFT_SUBPIXEL_DEFAULT : FCFT_SUBPIXEL_NONE),
      .cursor_keys_mode = CURSOR_KEYS_NORMAL,
      .keypad_keys_mode = KEYPAD_NUMERICAL,
      .reverse_wrap = true,
      .auto_margin = true,
      .window_title_stack = tll_init(),
      .scale = primary->scale,
      .scale_before_unmap = -1,
      .flash = {.fd = flash_fd},
      .blink = {.fd = -1},
      .vt = {.state = 0},
      .colors =
          {
              .fg = theme->fg,
              .bg = theme->bg,
              .alpha = theme->alpha,
              .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) |
                           theme->cursor.text,
              .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) |
                           theme->cursor.cursor,
              .selection_fg = theme->selection_fg,
              .selection_bg = theme->selection_bg,
              .active_theme = conf->initial_color_theme,
          },
      .color_stack = {.stack = NULL, .size = 0, .idx = 0},
      .origin = ORIGIN_ABSOLUTE,
      .cursor_style = conf->cursor.style,
      .cursor_blink =
          {
              .decset = false,
              .deccsusr = conf->cursor.blink.enabled,
              .state = CURSOR_BLINK_ON,
              .fd = -1,
          },
      .selection =
          {
              .coords = {.start = {-1, -1}, .end = {-1, -1}},
              .pivot = {.start = {-1, -1}, .end = {-1, -1}},
              .auto_scroll = {.fd = -1},
          },
      .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
      .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
      .grid = &term->normal,
      .composed = NULL,
      .alt_scrolling = conf->mouse.alternate_scroll_mode,
      .meta = {.esc_prefix = true, .eight_bit = true},
      .num_lock_modifier = true,
      .bell_action_enabled = true,
      .tab_stops = tll_init(),
      .wl = wayl,
      .window = win,
      .render =
          {
              .chains =
                  {
                      .grid = shm_chain_new(
                          wayl, true, 1 + conf->render_worker_count,
                          desired_bit_depth, &render_buffer_release_callback,
                          term),
                      .search = shm_chain_new(wayl, false, 1, desired_bit_depth,
                                              NULL, NULL),
                      .scrollback_indicator = shm_chain_new(
                          wayl, false, 1, desired_bit_depth, NULL, NULL),
                      .render_timer = shm_chain_new(
                          wayl, false, 1, desired_bit_depth, NULL, NULL),
                      .url = shm_chain_new(wayl, false, 1, desired_bit_depth,
                                           NULL, NULL),
                      .csd = shm_chain_new(wayl, false, 1, desired_bit_depth,
                                           NULL, NULL),
                      .overlay = shm_chain_new(wayl, false, 1,
                                               desired_bit_depth, NULL, NULL),
                      .tab_bar = shm_chain_new(wayl, false, 1,
                                               desired_bit_depth, NULL, NULL),
                      .tab_overview = shm_chain_new(
                          wayl, false, 1, desired_bit_depth, NULL, NULL),
                  },
              .scrollback_lines = conf->scrollback.lines,
              .app_sync_updates.timer_fd = app_sync_updates_fd,
              .title = {.timer_fd = title_update_fd},
              .icon = {.timer_fd = icon_update_fd},
              .app_id = {.timer_fd = app_id_update_fd},
              .workers =
                  {
                      .count = conf->render_worker_count,
                      .queue = tll_init(),
                  },
          },
      .delayed_render_timer =
          {
              .is_armed = false,
              .lower_fd = delay_lower_fd,
              .upper_fd = delay_upper_fd,
          },
      .sixel =
          {
              .scrolling = true,
              .use_private_palette = true,
              .palette_size = SIXEL_MAX_COLORS,
              .max_width = SIXEL_MAX_WIDTH,
              .max_height = SIXEL_MAX_HEIGHT,
          },
      .shutdown =
          {
              .terminate_timeout_fd = -1,
              .cb = shutdown_cb,
              .cb_data = shutdown_data,
          },
      .foot_exe = xstrdup(primary->foot_exe),
      .cwd = xstrdup(override_cwd != NULL
                         ? override_cwd
                         : (conf->tabs.inherit_cwd
                                ? win->term->cwd
                                : (getenv("HOME") != NULL ? getenv("HOME")
                                                          : "/"))),
      .grapheme_shaping = conf->tweak.grapheme_shaping,
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
      .ime_enabled = true,
#endif
      .active_notifications = tll_init(),
  };

  pixman_region32_init(&term->render.last_overlay_clip);
  term_update_ascii_printer(term);
  memcpy(term->colors.table, theme->table, sizeof(term->colors.table));

  /* Inherit font sizes from primary so the new tab matches its zoom level */
  for (size_t i = 0; i < 4; i++) {
    const struct config_font_list *font_list = &conf->fonts[i];
    for (size_t j = 0; j < font_list->count; j++)
      term->font_sizes[i][j] = primary->font_sizes[i][j];
  }

  for (size_t i = 0; i < ALEN(term->notification_icons); i++)
    term->notification_icons[i].tmp_file_fd = -1;

  if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) ||
      !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) ||
      !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) ||
      !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout,
               term) ||
      !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout,
               term) ||
      !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) ||
      !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout,
               term)) {
    goto err;
  }

  add_utmp_record(conf, reaper, ptmx);

  /* Initialize window title to the configured default; otherwise a freshly
   * spawned child sending CSI 22 t (push title) sees a NULL title. */
  term_set_window_title(term, conf->title);

  if ((term->slave = slave_spawn(
           term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, conf->term,
           conf->shell, conf->login_shell, &conf->notifications)) == -1) {
    goto err;
  }

  reaper_add(term->reaper, term->slave, &fdm_client_terminated, term);

  /* Load fonts (uses primary's scale) */
  if (!term_font_dpi_changed(term, 0.))
    goto err;

  term->font_subpixel = get_font_subpixel(term);

  term->width = primary->width;
  term->height = primary->height;

  tll_push_back(wayl->terms, term);

  /* Register in window's tab list */
  win->tabs[win->tab_count++] = term;

  /* Enable ptmx I/O now (window is already configured) */
  fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term);

  if (!initialize_render_workers(term))
    goto err;

  /* Switch to the new tab */
  term_tab_switch(win, win->tab_count - 1);

  return term;

err:
  term->shutdown.in_progress = true;
  term_destroy(term);
  return NULL;

close_fds:
  if (ptmx >= 0)
    close(ptmx);
  fdm_del(fdm, flash_fd);
  fdm_del(fdm, delay_lower_fd);
  fdm_del(fdm, delay_upper_fd);
  fdm_del(fdm, app_sync_updates_fd);
  fdm_del(fdm, title_update_fd);
  fdm_del(fdm, icon_update_fd);
  fdm_del(fdm, app_id_update_fd);
  free(term);
  return NULL;
}

void term_tab_switch(struct wl_window *win, size_t idx) {
  if (idx >= win->tab_count)
    return;

  struct terminal *prev = win->term;
  /* render_resize expects logical (unscaled) sizes; prev->width/height
   * are physical pixels. Convert back to logical so render_resize's
   * internal scale multiplication doesn't double-apply it. */
  int cur_width = (int)roundf(prev->width / prev->scale);
  int cur_height = (int)roundf(prev->height / prev->scale);

  win->active_tab = idx;
  win->term = win->tabs[idx];

  struct terminal *term = win->term;

  term->has_unread = false;

  /* Inherit the surface kind so cursor updates work without a pointer re-enter
   */
  term->active_surface = prev->active_surface;

  /* Update keyboard and mouse focus for all seats looking at this window.
   * Do this before term_kbd_focus_out(prev) so its guard (checking whether
   * any seat still points to it) sees that no seat does. */
  tll_foreach(term->wl->seats, it) {
    struct seat *seat = &it->item;
    if (seat->kbd_focus != NULL && seat->kbd_focus->window == win)
      seat->kbd_focus = term;
    if (seat->mouse_focus != NULL && seat->mouse_focus->window == win)
      seat->mouse_focus = term;
  }

  /* prev loses keyboard focus now that no seat points to it */
  term_kbd_focus_out(prev);

  bool has_kbd_focus = false;
  tll_foreach(term->wl->seats, it) {
    if (it->item.kbd_focus == term) {
      has_kbd_focus = true;
      break;
    }
  }
  if (has_kbd_focus)
    term_visual_focus_in(term);
  else
    term_visual_focus_out(term);

  const float old_scale = term->scale;
  term_update_scale(term);
  term_font_dpi_changed(term, old_scale);
  term_font_subpixel_changed(term);

  /* Cancel the outgoing tab's in-flight frame_callback. Its listener data
   * points at prev, so when it fires it services prev's empty pending bits
   * and nothing gets drawn. Dropping it lets fdm_hook_refresh_pending_terminals
   * take the immediate-render path for the new active tab. */
  if (win->frame_callback != NULL) {
    wl_callback_destroy(win->frame_callback);
    win->frame_callback = NULL;
  }

  render_resize(term, cur_width, cur_height, RESIZE_FORCE);

  if (has_kbd_focus)
    term_kbd_focus_in(term);

  render_refresh_csd(term);
  render_refresh_tab_bar(term);
  term_xcursor_update(term);
}

void term_tab_close(struct terminal *term) { term_shutdown(term); }

size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y) {
  const struct wl_window *win = term->window;
  if (win->tab_bar.sub == NULL)
    return SIZE_MAX;

  const size_t n = win->tab_layout.count;
  if (n == 0)
    return SIZE_MAX;

  if (y < win->tab_layout.y || y >= win->tab_layout.y + win->tab_layout.h)
    return SIZE_MAX;

  for (size_t i = 0; i < n; i++) {
    const int tx = win->tab_layout.xs[i];
    const int tw = win->tab_layout.ws[i];
    if (x >= tx && x < tx + tw)
      return i;
  }
  return SIZE_MAX;
}

void term_window_configured(struct terminal *term) {
  /* Enable ptmx FDM callback */
  if (!term->shutdown.in_progress) {
    xassert(term->window->is_configured);
    fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term);

    const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf);
    LOG_INFO("gamma-correct blending: %s",
             gamma_correct ? "enabled" : "disabled");
  }
}

/*
 * Shutdown logic
 *
 * A foot instance can be terminated in two ways:
 *
 *  - the client application terminates (user types 'exit', or pressed C-d in
 * the shell, etc)
 *  - the foot window is closed
 *
 * Both variants need to trigger to "other" action. I.e. if the client
 * application is terminated, then we need to close the window. If the window is
 * closed, we need to terminate the client application.
 *
 * Only when *both* tasks have completed do we consider ourselves fully
 * shutdown. This is when we can call term_destroy(), and the user provided
 * shutdown callback.
 *
 * The functions involved with this are:
 *
 * - shutdown_maybe_done(): called after any of the two tasks above have
 *   completed. When it determines that *both* tasks are done, it calls
 *   term_destroy() and the user provided shutdown callback.
 *
 * - fdm_client_terminated(): reaper callback, called when the client
 *   application has terminated.
 *
 *     + Kills the "terminate" timeout timer
 *     + Calls shutdown_maybe_done() if the shutdown procedure has already
 *       started (i.e. the window being closed initiated the shutdown)
 *    -OR-
 *       Initiates the shutdown itself, by calling term_shutdown() (client
 *       application termination initiated the shutdown).
 *
 * - term_shutdown(): unregisters all FDM callbacks, sends SIGTERM to the client
 *   application and installs a "terminate" timeout timer (if it hasn't already
 *   terminated). Finally registers an event FD with the FDM, which is
 *   immediately triggered. This is done to ensure any pending FDM events are
 *   handled before shutting down.
 *
 * - fdm_shutdown(): FDM callback, triggered by the event FD in
 *   term_shutdown(). Unmaps and destroys the window resources, and ensures the
 *   seats' focused pointers don't reference us. Finally calls
 *   shutdown_maybe_done().
 *
 * - fdm_terminate_timeout(): FDM callback for the "terminate" timeout
 *   timer. This function is called when the client application hasn't
 *   terminated after 60 seconds (after the SIGTERM). Sends SIGKILL to the
 *   client application.
 *
 * - term_destroy(): normally called from shutdown_maybe_done(), when both the
 *   window has been unmapped, and the client application has terminated. In
 *   this case, it simply destroys all resources.
 *
 *   It may however also be called without term_shutdown() having been called
 *   (typically in error code paths - for example, when the Wayland connection
 *   is closed by the compositor). In this case, the client application is
 *   typically still running, and we can't assume the FDM is running. To handle
 *   this, we install configure a 60 second SIGALRM, send SIGTERM to the client
 *   application, and then enter a blocking waitpid().
 *
 *   If the alarm triggers, we send SIGKILL and once again enter a blocking
 *   waitpid().
 */

static void shutdown_maybe_done(struct terminal *term) {
  bool shutdown_done =
      term->shutdown.fdm_done && term->shutdown.client_has_terminated;

  LOG_DBG(
      "fdm_done=%d, slave-has-been-reaped=%d --> %s", term->shutdown.fdm_done,
      term->shutdown.client_has_terminated,
      (shutdown_done ? "shutdown done, calling term_destroy()" : "no action"));

  if (!shutdown_done)
    return;

  void (*cb)(void *, int) = term->shutdown.cb;
  void *cb_data = term->shutdown.cb_data;

  int exit_code = term_destroy(term);
  if (cb != NULL)
    cb(cb_data, exit_code);
}

static void fdm_client_terminated(struct reaper *reaper, pid_t pid, int status,
                                  void *data) {
  struct terminal *term = data;
  LOG_DBG("slave (PID=%u) died", pid);

  term->shutdown.client_has_terminated = true;
  term->shutdown.exit_status = status;

  if (term->shutdown.terminate_timeout_fd >= 0) {
    fdm_del(term->fdm, term->shutdown.terminate_timeout_fd);
    term->shutdown.terminate_timeout_fd = -1;
  }

  if (term->shutdown.in_progress)
    shutdown_maybe_done(term);
  else if (!term->conf->hold_at_exit)
    term_shutdown(term);
}

static bool fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) {
  struct terminal *term = data;

  /* Kill the event FD */
  fdm_del(term->fdm, fd);

  struct wl_window *win = term->window;

  if (win != NULL && win->tab_count <= 1) {
    wayl_win_destroy(win);
    term->window = NULL;

    struct wayland *wayl = term->wl;
    tll_foreach(wayl->seats, it) {
      if (it->item.kbd_focus == term)
        it->item.kbd_focus = NULL;
      if (it->item.mouse_focus == term)
        it->item.mouse_focus = NULL;
    }
  }
  /*
   * Multi-tab case: leave term->window intact so term_destroy() can
   * use its existing tab-removal and tab-switch logic.
   */

  term->shutdown.fdm_done = true;
  shutdown_maybe_done(term);
  return true;
}

static bool fdm_terminate_timeout(struct fdm *fdm, int fd, int events,
                                  void *data) {
  uint64_t unused;
  ssize_t bytes = read(fd, &unused, sizeof(unused));
  if (bytes < 0) {
    LOG_ERRNO("failed to read from slave terminate timeout FD");
    return false;
  }

  struct terminal *term = data;
  xassert(!term->shutdown.client_has_terminated);

  LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", term->slave,
          term->shutdown.next_signal == SIGTERM   ? "SIGTERM"
          : term->shutdown.next_signal == SIGKILL ? "SIGKILL"
                                                  : "<unknown>",
          term->shutdown.next_signal);

  kill(-term->slave, term->shutdown.next_signal);

  switch (term->shutdown.next_signal) {
  case SIGTERM:
    term->shutdown.next_signal = SIGKILL;
    break;

  case SIGKILL:
    /* Disarm. Shouldn't be necessary, as we should be able to
       shutdown completely after sending SIGKILL, before the next
       timeout occurs). But lets play it safe... */
    if (term->shutdown.terminate_timeout_fd >= 0) {
      timerfd_settime(term->shutdown.terminate_timeout_fd, 0,
                      &(const struct itimerspec){0}, NULL);
    }
    break;

  default:
    BUG("can only handle SIGTERM and SIGKILL");
    return false;
  }

  return true;
}

bool term_shutdown(struct terminal *term) {
  if (term->shutdown.in_progress)
    return true;

  term->shutdown.in_progress = true;

  /*
   * Close FDs then postpone self-destruction to the next poll
   * iteration, by creating an event FD that we trigger immediately.
   */

  term_cursor_blink_update(term);
  xassert(term->cursor_blink.fd < 0);

  fdm_del(term->fdm, term->selection.auto_scroll.fd);
  fdm_del(term->fdm, term->render.app_sync_updates.timer_fd);
  fdm_del(term->fdm, term->render.app_id.timer_fd);
  fdm_del(term->fdm, term->render.icon.timer_fd);
  fdm_del(term->fdm, term->render.title.timer_fd);
  fdm_del(term->fdm, term->delayed_render_timer.lower_fd);
  fdm_del(term->fdm, term->delayed_render_timer.upper_fd);
  fdm_del(term->fdm, term->blink.fd);
  fdm_del(term->fdm, term->flash.fd);

  del_utmp_record(term->conf, term->reaper, term->ptmx);

  if (term->window != NULL && term->window->is_configured)
    fdm_del(term->fdm, term->ptmx);
  else
    close(term->ptmx);

  if (!term->shutdown.client_has_terminated) {
    if (term->slave <= 0) {
      term->shutdown.client_has_terminated = true;
    } else {
      LOG_DBG("initiating asynchronous terminate of slave; "
              "sending SIGHUP to PID=%u",
              term->slave);

      kill(-term->slave, SIGHUP);

      /*
       * Set up a timer, with an interval - on the first timeout
       * we'll send SIGTERM. If the the client application still
       * isn't terminating, we'll wait an additional interval,
       * and then send SIGKILL.
       */
      const struct itimerspec timeout = {.it_value = {.tv_sec = 30},
                                         .it_interval = {.tv_sec = 30}};

      int timeout_fd =
          timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
      if (timeout_fd < 0 ||
          timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 ||
          !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout,
                   term)) {
        if (timeout_fd >= 0)
          close(timeout_fd);
        LOG_ERRNO("failed to create slave terminate timeout FD");
        return false;
      }

      xassert(term->shutdown.terminate_timeout_fd < 0);
      term->shutdown.terminate_timeout_fd = timeout_fd;
      term->shutdown.next_signal = SIGTERM;
    }
  }

  term->selection.auto_scroll.fd = -1;
  term->render.app_sync_updates.timer_fd = -1;
  term->render.app_id.timer_fd = -1;
  term->render.icon.timer_fd = -1;
  term->render.title.timer_fd = -1;
  term->delayed_render_timer.lower_fd = -1;
  term->delayed_render_timer.upper_fd = -1;
  term->blink.fd = -1;
  term->flash.fd = -1;
  term->ptmx = -1;

  int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
  if (event_fd == -1) {
    LOG_ERRNO("failed to create terminal shutdown event FD");
    return false;
  }

  if (!fdm_add(term->fdm, event_fd, EPOLLIN, &fdm_shutdown, term)) {
    close(event_fd);
    return false;
  }

  if (write(event_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) {
    LOG_ERRNO("failed to send terminal shutdown event");
    fdm_del(term->fdm, event_fd);
    return false;
  }

  return true;
}

static volatile sig_atomic_t alarm_raised;

static void sig_alarm(int signo) {
  LOG_DBG("SIGALRM");
  alarm_raised = 1;
}

int term_destroy(struct terminal *term) {
  if (term == NULL)
    return 0;

  tll_foreach(term->wl->terms, it) {
    if (it->item == term) {
      tll_remove(term->wl->terms, it);
      break;
    }
  }

  del_utmp_record(term->conf, term->reaper, term->ptmx);

  fdm_del(term->fdm, term->selection.auto_scroll.fd);
  fdm_del(term->fdm, term->render.app_sync_updates.timer_fd);
  fdm_del(term->fdm, term->render.app_id.timer_fd);
  fdm_del(term->fdm, term->render.icon.timer_fd);
  fdm_del(term->fdm, term->render.title.timer_fd);
  fdm_del(term->fdm, term->delayed_render_timer.lower_fd);
  fdm_del(term->fdm, term->delayed_render_timer.upper_fd);
  fdm_del(term->fdm, term->cursor_blink.fd);
  fdm_del(term->fdm, term->blink.fd);
  fdm_del(term->fdm, term->flash.fd);
  fdm_del(term->fdm, term->ptmx);
  if (term->shutdown.terminate_timeout_fd >= 0)
    fdm_del(term->fdm, term->shutdown.terminate_timeout_fd);

  if (term->window != NULL) {
    struct wl_window *win = term->window;

    /* Remove ourselves from the window's tab list */
    bool was_active = (win->term == term);
    size_t our_idx = win->tab_count; /* invalid sentinel */
    for (size_t i = 0; i < win->tab_count; i++) {
      if (win->tabs[i] == term) {
        our_idx = i;
        memmove(&win->tabs[i], &win->tabs[i + 1],
                (win->tab_count - i - 1) * sizeof(win->tabs[0]));
        win->tab_count--;
        if (win->active_tab > 0 && win->active_tab >= win->tab_count)
          win->active_tab = win->tab_count - 1;
        else if (our_idx < win->active_tab)
          win->active_tab--;
        break;
      }
    }

    if (win->tab_count == 0) {
      /* Last tab - destroy the window normally */
      wayl_win_destroy(win);
    } else {
      /* Other tabs remain - just purge our render chains */

      /* Cancel any pending frame callback that still holds a pointer
       * to this terminal, preventing a use-after-free when the
       * compositor fires it after term is freed. */
      if (win->frame_callback != NULL) {
        wl_callback_destroy(win->frame_callback);
        win->frame_callback = NULL;
      }

      render_wait_for_preapply_damage(term);
      shm_purge(term->render.chains.search);
      shm_purge(term->render.chains.scrollback_indicator);
      shm_purge(term->render.chains.render_timer);
      shm_purge(term->render.chains.grid);
      shm_purge(term->render.chains.url);
      shm_purge(term->render.chains.csd);
      shm_purge(term->render.chains.tab_bar);
      shm_purge(term->render.chains.tab_overview);
      shm_purge(term->render.chains.overlay);

      /* Switch to the new active tab if needed */
      if (was_active) {
        struct terminal *next = win->tabs[win->active_tab];
        win->term = next;
        next->active_surface = term->active_surface;

        /* Update keyboard and mouse focus */
        bool has_kbd_focus = false;
        tll_foreach(next->wl->seats, it) {
          struct seat *seat = &it->item;
          if (seat->kbd_focus == term)
            seat->kbd_focus = next;
          if (seat->mouse_focus == term)
            seat->mouse_focus = next;
          if (seat->kbd_focus == next)
            has_kbd_focus = true;
        }

        if (has_kbd_focus) {
          term_kbd_focus_in(next);
          term_visual_focus_in(next);
        } else {
          term_visual_focus_out(next);
        }

        render_resize(next, (int)roundf(term->width / term->scale),
                      (int)roundf(term->height / term->scale), RESIZE_FORCE);
        render_refresh_csd(next);
        render_refresh_tab_bar(next);
        term_xcursor_update(next);
      } else {
        /* Just update the tab bar to reflect removed tab */
        render_refresh_tab_bar(win->term);
      }
    }

    term->window = NULL;
  }

  mtx_lock(&term->render.workers.lock);
  xassert(tll_length(term->render.workers.queue) == 0);

  /* Count livinig threads - we may get here when only some of the
   * threads have been successfully started */
  size_t worker_count = 0;
  if (term->render.workers.threads != NULL) {
    for (size_t i = 0; i < term->render.workers.count; i++, worker_count++) {
      if (term->render.workers.threads[i] == 0)
        break;
    }

    for (size_t i = 0; i < worker_count; i++) {
      sem_post(&term->render.workers.start);
      tll_push_back(term->render.workers.queue, -2);
    }
  }
  mtx_unlock(&term->render.workers.lock);

  key_binding_unref(term->wl->key_binding_manager, term->conf);

  urls_reset(term);

  free(term->vt.osc.data);
  free(term->vt.osc8.uri);

  composed_free(term->composed);

  free(term->app_id);
  free(term->window_title);
  tll_free_and_free(term->window_title_stack, free);

  for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++)
    fcft_destroy(term->fonts[i]);
  for (size_t i = 0; i < 4; i++)
    free(term->font_sizes[i]);

  free_custom_glyphs(&term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT);
  free_custom_glyphs(&term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT);
  free_custom_glyphs(&term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT);
  free_custom_glyphs(&term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT);

  free(term->search.buf);
  free(term->search.last.buf);

  /* Free search history */
  {
    struct search_history_entry *e = term->search.history_head;
    while (e != NULL) {
      struct search_history_entry *next = e->next;
      free(e->buf);
      free(e);
      e = next;
    }
  }

  /* Free compiled regex if any */
  if (term->search.regex_compiled != NULL) {
    regfree(term->search.regex_compiled);
    free(term->search.regex_compiled);
  }

  if (term->render.workers.threads != NULL) {
    for (size_t i = 0; i < term->render.workers.count; i++) {
      if (term->render.workers.threads[i] != 0)
        thrd_join(term->render.workers.threads[i], NULL);
    }
  }
  free(term->render.workers.threads);
  mtx_destroy(&term->render.workers.preapplied_damage.lock);
  cnd_destroy(&term->render.workers.preapplied_damage.cond);
  mtx_destroy(&term->render.workers.lock);
  sem_destroy(&term->render.workers.start);
  sem_destroy(&term->render.workers.done);
  xassert(tll_length(term->render.workers.queue) == 0);
  tll_free(term->render.workers.queue);

  shm_unref(term->render.last_buf);
  shm_chain_free(term->render.chains.grid);
  shm_chain_free(term->render.chains.search);
  shm_chain_free(term->render.chains.scrollback_indicator);
  shm_chain_free(term->render.chains.render_timer);
  shm_chain_free(term->render.chains.url);
  shm_chain_free(term->render.chains.csd);
  shm_chain_free(term->render.chains.overlay);
  shm_chain_free(term->render.chains.tab_bar);
  shm_chain_free(term->render.chains.tab_overview);
  pixman_region32_fini(&term->render.last_overlay_clip);

  tll_free(term->tab_stops);

  tll_foreach(term->ptmx_buffers, it) {
    free(it->item.data);
    tll_remove(term->ptmx_buffers, it);
  }
  tll_foreach(term->ptmx_paste_buffers, it) {
    free(it->item.data);
    tll_remove(term->ptmx_paste_buffers, it);
  }

  notify_free(term, &term->kitty_notification);
  tll_foreach(term->active_notifications, it) {
    notify_free(term, &it->item);
    tll_remove(term->active_notifications, it);
  }

  for (size_t i = 0; i < ALEN(term->notification_icons); i++)
    notify_icon_free(&term->notification_icons[i]);

  sixel_fini(term);

  term_ime_reset(term);

  grid_free(&term->normal);
  grid_free(&term->alt);
  grid_free(term->interactive_resizing.grid);
  free(term->interactive_resizing.grid);

  free(term->foot_exe);
  free(term->cwd);
  free(term->mouse_user_cursor);
  free(term->color_stack.stack);

  int ret = EXIT_SUCCESS;

  if (term->slave > 0) {
    /* We'll deal with this explicitly */
    reaper_del(term->reaper, term->slave);

    int exit_status;

    if (term->shutdown.client_has_terminated)
      exit_status = term->shutdown.exit_status;
    else {
      LOG_DBG("initiating blocking terminate of slave; "
              "sending SIGHUP to PID=%u",
              term->slave);

      kill(-term->slave, SIGHUP);

      /*
       * we've closed the ptxm, and sent SIGTERM to the client
       * application. It *should* exit...
       *
       * But, since it is possible to write clients that ignore
       * this, we need to handle it in *some* way.
       *
       * So, what we do is register a SIGALRM handler, and configure a 30
       * second alarm. If the slave hasn't died after this time, we send
       * it a SIGKILL,
       *
       * Note that this solution is *not* asynchronous, and any
       * other events etc will be ignored during this time. This of
       * course only applies to a 'foot --server' instance, where
       * there might be other terminals running.
       */
      struct sigaction action = {.sa_handler = &sig_alarm};
      sigemptyset(&action.sa_mask);
      sigaction(SIGALRM, &action, NULL);

      /* Wait, then send SIGTERM, wait again, then send SIGKILL */
      int next_signal = SIGTERM;

      alarm_raised = 0;
      alarm(30);

      while (true) {
        int r = waitpid(term->slave, &exit_status, 0);

        if (r == term->slave)
          break;

        if (r == -1) {
          xassert(errno == EINTR);

          if (alarm_raised) {
            LOG_DBG("slave (PID=%u) has not terminated yet, "
                    "sending: %s (%d)",
                    term->slave, next_signal == SIGTERM ? "SIGTERM" : "SIGKILL",
                    next_signal);

            kill(-term->slave, next_signal);
            next_signal = SIGKILL;

            alarm_raised = 0;
            alarm(30);
          }
        }
      }

      /* Cancel alarm */
      alarm(0);
      action.sa_handler = SIG_DFL;
      sigaction(SIGALRM, &action, NULL);
    }

    ret = EXIT_FAILURE;
    if (WIFEXITED(exit_status)) {
      ret = WEXITSTATUS(exit_status);
      LOG_DBG("slave exited with code %d", ret);
    } else if (WIFSIGNALED(exit_status)) {
      ret = WTERMSIG(exit_status);
      LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret));
    } else {
      LOG_WARN("slave exited for unknown reason (status = 0x%08x)",
               exit_status);
    }
  }

  free(term);

#if defined(__GLIBC__)
  if (!malloc_trim(0))
    LOG_WARN("failed to trim memory");
#endif

  return ret;
}

static inline void erase_cell_range(struct terminal *term, struct row *row,
                                    int start, int end) {
  xassert(start < term->cols);
  xassert(end < term->cols);

  row->dirty = true;

  const enum color_source bg_src = term->vt.attrs.bg_src;

  if (unlikely(bg_src != COLOR_DEFAULT)) {
    for (int col = start; col <= end; col++) {
      struct cell *c = &row->cells[col];
      c->wc = 0;
      c->attrs = (struct attributes){.bg_src = bg_src, .bg = term->vt.attrs.bg};
    }
  } else
    memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0]));

  if (unlikely(row->extra != NULL)) {
    grid_row_uri_range_erase(row, start, end);
    grid_row_underline_range_erase(row, start, end);
  }
}

static inline void erase_line(struct terminal *term, struct row *row) {
  erase_cell_range(term, row, 0, term->cols - 1);
  row->linebreak = true;
  row->shell_integration.prompt_marker = false;
  row->shell_integration.cmd_start = -1;
  row->shell_integration.cmd_end = -1;
}

static void term_theme_apply(struct terminal *term,
                             const struct color_theme *theme) {
  term->colors.fg = theme->fg;
  term->colors.bg = theme->bg;
  term->colors.alpha = theme->alpha;
  term->colors.cursor_fg =
      (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text;
  term->colors.cursor_bg =
      (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor;
  term->colors.selection_fg = theme->selection_fg;
  term->colors.selection_bg = theme->selection_bg;
  memcpy(term->colors.table, theme->table, sizeof(term->colors.table));
}

void term_reset(struct terminal *term, bool hard) {
  LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft");

  term->cursor_keys_mode = CURSOR_KEYS_NORMAL;
  term->keypad_keys_mode = KEYPAD_NUMERICAL;
  term->reverse = false;
  term->hide_cursor = false;
  term->reverse_wrap = true;
  term->auto_margin = true;
  term->insert_mode = false;
  term->bracketed_paste = false;
  term->focus_events = false;
  term->num_lock_modifier = true;
  term->bell_action_enabled = true;
  term->mouse_tracking = MOUSE_NONE;
  term->mouse_reporting = MOUSE_NORMAL;
  term->charsets.selected = G0;
  term->charsets.set[G0] = CHARSET_ASCII;
  term->charsets.set[G1] = CHARSET_ASCII;
  term->charsets.set[G2] = CHARSET_ASCII;
  term->charsets.set[G3] = CHARSET_ASCII;
  term->saved_charsets = term->charsets;
  tll_free_and_free(term->window_title_stack, free);
  term_set_window_title(term, term->conf->title);
  term_set_app_id(term, NULL);

  term_set_user_mouse_cursor(term, NULL);

  term->modify_other_keys_2 = false;
  memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags));
  memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags));
  term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0;

  term->scroll_region.start = 0;
  term->scroll_region.end = term->rows;

  free(term->vt.osc8.uri);
  free(term->vt.osc.data);

  term->vt = (struct vt){
      .state = 0, /* STATE_GROUND */
  };

  if (term->grid == &term->alt) {
    term->grid = &term->normal;
    selection_cancel(term);
  }

  term->meta.esc_prefix = true;
  term->meta.eight_bit = true;

  tll_foreach(term->normal.sixel_images, it) {
    sixel_destroy(&it->item);
    tll_remove(term->normal.sixel_images, it);
  }
  tll_foreach(term->alt.sixel_images, it) {
    sixel_destroy(&it->item);
    tll_remove(term->alt.sixel_images, it);
  }

  notify_free(term, &term->kitty_notification);
  tll_foreach(term->active_notifications, it) {
    notify_free(term, &it->item);
    tll_remove(term->active_notifications, it);
  }

  for (size_t i = 0; i < ALEN(term->notification_icons); i++)
    notify_icon_free(&term->notification_icons[i]);

  term->grapheme_shaping = term->conf->tweak.grapheme_shaping;

#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  term_ime_enable(term);
#endif

  term->bits_affecting_ascii_printer.value = 0;
  term_update_ascii_printer(term);

  if (!hard)
    return;

  const struct color_theme *theme = NULL;

  switch (term->conf->initial_color_theme) {
  case COLOR_THEME_DARK:
    theme = &term->conf->colors_dark;
    break;
  case COLOR_THEME_LIGHT:
    theme = &term->conf->colors_light;
    break;
  case COLOR_THEME_1:
    BUG("COLOR_THEME_1 should not be used");
    break;
  case COLOR_THEME_2:
    BUG("COLOR_THEME_2 should not be used");
    break;
  }

  term->flash.active = false;
  term->blink.state = BLINK_ON;
  fdm_del(term->fdm, term->blink.fd);
  term->blink.fd = -1;
  term_theme_apply(term, theme);
  term->colors.active_theme = term->conf->initial_color_theme;
  free(term->color_stack.stack);
  term->color_stack.stack = NULL;
  term->color_stack.size = 0;
  term->color_stack.idx = 0;
  term->origin = ORIGIN_ABSOLUTE;
  term->normal.cursor.lcf = false;
  term->alt.cursor.lcf = false;
  term->normal.cursor = (struct cursor){.point = {0, 0}};
  term->normal.saved_cursor = (struct cursor){.point = {0, 0}};
  term->alt.cursor = (struct cursor){.point = {0, 0}};
  term->alt.saved_cursor = (struct cursor){.point = {0, 0}};
  term->cursor_style = term->conf->cursor.style;
  term->cursor_blink.decset = false;
  term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled;
  term_cursor_blink_update(term);
  selection_cancel(term);
  term->normal.offset = term->normal.view = 0;
  term->alt.offset = term->alt.view = 0;
  for (size_t i = 0; i < term->rows; i++) {
    struct row *r = grid_row_and_alloc(&term->normal, i);
    erase_line(term, r);
  }
  for (size_t i = 0; i < term->rows; i++) {
    struct row *r = grid_row_and_alloc(&term->alt, i);
    erase_line(term, r);
  }
  for (size_t i = term->rows; i < term->normal.num_rows; i++) {
    grid_row_free(term->normal.rows[i]);
    term->normal.rows[i] = NULL;
  }
  for (size_t i = term->rows; i < term->alt.num_rows; i++) {
    grid_row_free(term->alt.rows[i]);
    term->alt.rows[i] = NULL;
  }
  term->normal.cur_row = term->normal.rows[0];
  term->alt.cur_row = term->alt.rows[0];
  tll_free(term->normal.scroll_damage);
  tll_free(term->alt.scroll_damage);
  term->render.last_cursor.row = NULL;
  term_damage_all(term);

  term->sixel.scrolling = true;
  term->sixel.cursor_right_of_graphics = false;
  term->sixel.use_private_palette = true;
  term->sixel.max_width = SIXEL_MAX_WIDTH;
  term->sixel.max_height = SIXEL_MAX_HEIGHT;
  term->sixel.palette_size = SIXEL_MAX_COLORS;
  free(term->sixel.private_palette);
  free(term->sixel.shared_palette);
  term->sixel.private_palette = term->sixel.shared_palette = NULL;
}

static bool term_font_size_adjust_by_points(struct terminal *term,
                                            float amount) {
  const struct config *conf = term->conf;
  const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.;

  for (size_t i = 0; i < 4; i++) {
    const struct config_font_list *font_list = &conf->fonts[i];

    for (size_t j = 0; j < font_list->count; j++) {
      struct config_font *font = &term->font_sizes[i][j];
      float old_pt_size = font->pt_size;

      if (font->px_size > 0)
        old_pt_size = font->px_size * 72. / dpi;

      font->pt_size = fmaxf(old_pt_size + amount, 0.);
      font->px_size = -1;
    }
  }

  return reload_fonts(term, true);
}

static bool term_font_size_adjust_by_pixels(struct terminal *term, int amount) {
  const struct config *conf = term->conf;
  const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.;

  for (size_t i = 0; i < 4; i++) {
    const struct config_font_list *font_list = &conf->fonts[i];

    for (size_t j = 0; j < font_list->count; j++) {
      struct config_font *font = &term->font_sizes[i][j];
      int old_px_size = font->px_size;

      if (font->px_size <= 0)
        old_px_size = font->pt_size * dpi / 72.;

      font->px_size = max(old_px_size + amount, 1);
    }
  }

  return reload_fonts(term, true);
}

static bool term_font_size_adjust_by_percent(struct terminal *term,
                                             bool increment, float percent) {
  const struct config *conf = term->conf;
  const float multiplier = increment ? 1. + percent : 1. / (1. + percent);

  for (size_t i = 0; i < 4; i++) {
    const struct config_font_list *font_list = &conf->fonts[i];

    for (size_t j = 0; j < font_list->count; j++) {
      struct config_font *font = &term->font_sizes[i][j];

      if (font->px_size > 0)
        font->px_size = max(font->px_size * multiplier, 1);
      else
        font->pt_size = fmax(font->pt_size * multiplier, 0);
    }
  }

  return reload_fonts(term, true);
}

bool term_font_size_increase(struct terminal *term) {
  const struct config *conf = term->conf;
  const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment;

  if (inc_dec->percent > 0.)
    return term_font_size_adjust_by_percent(term, true, inc_dec->percent);
  else if (inc_dec->pt_or_px.px > 0)
    return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px);
  else
    return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt);
}

bool term_font_size_decrease(struct terminal *term) {
  const struct config *conf = term->conf;
  const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment;

  if (inc_dec->percent > 0.)
    return term_font_size_adjust_by_percent(term, false, inc_dec->percent);
  else if (inc_dec->pt_or_px.px > 0)
    return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px);
  else
    return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt);
}

bool term_font_size_reset(struct terminal *term) {
  return load_fonts_from_conf(term);
}

bool term_fractional_scaling(const struct terminal *term) {
  return term->wl->fractional_scale_manager != NULL &&
         term->wl->viewporter != NULL && term->window->scale > 0.;
}

bool term_preferred_buffer_scale(const struct terminal *term) {
  return term->window->preferred_buffer_scale > 0;
}

bool term_update_scale(struct terminal *term) {
  const struct wl_window *win = term->window;

  /*
   * We have a number of "sources" we can use as scale. We choose
   * the scale in the following order:
   *
   *  - "preferred" scale, from the fractional-scale-v1 protocol
   *  - "preferred" scale, from wl_compositor version 6.
        NOTE: if the compositor advertises version 6 we must use 1.0
        until wl_surface.preferred_buffer_scale is sent
   *  - scaling factor of output we most recently were mapped on
   *  - if we're not mapped, use the last known scaling factor
   *  - if we're not mapped, and we don't have a last known scaling
   *    factor, use the scaling factor from the first available
   *    output.
   *  - if there aren't any outputs available, use 1.0
   */
  const float new_scale =
      (term_fractional_scaling(term)       ? win->scale
       : term_preferred_buffer_scale(term) ? win->preferred_buffer_scale
       : tll_length(win->on_outputs) > 0   ? tll_back(win->on_outputs)->scale
       : term->scale_before_unmap > 0.     ? term->scale_before_unmap
       : tll_length(term->wl->monitors) > 0
           ? tll_front(term->wl->monitors).scale
           : 1.);

  if (new_scale == term->scale)
    return false;

  LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale);
  term->scale_before_unmap = new_scale;
  term->scale = new_scale;
  return true;
}

bool term_font_dpi_changed(struct terminal *term, float old_scale) {
  float dpi = get_font_dpi(term);
  xassert(term->scale > 0.);

  bool was_scaled_using_dpi = term->font_is_sized_by_dpi;
  bool will_scale_using_dpi = term->conf->dpi_aware;

  bool need_font_reload =
      was_scaled_using_dpi != will_scale_using_dpi ||
      (will_scale_using_dpi ? term->font_dpi != dpi : old_scale != term->scale);

  if (need_font_reload) {
    LOG_DBG("DPI/scale change: DPI-aware=%s, "
            "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, "
            "sizing font based on monitor's %s",
            term->conf->dpi_aware ? "yes" : "no", term->font_dpi, dpi,
            old_scale, term->scale,
            will_scale_using_dpi ? "DPI" : "scaling factor");
  }

  term->font_dpi = dpi;
  term->font_dpi_before_unmap = dpi;
  term->font_is_sized_by_dpi = will_scale_using_dpi;

  if (!need_font_reload)
    return false;

  return reload_fonts(term, false);
}

void term_font_subpixel_changed(struct terminal *term) {
  enum fcft_subpixel subpixel = get_font_subpixel(term);

  if (term->font_subpixel == subpixel)
    return;

#if defined(_DEBUG) && LOG_ENABLE_DBG
  static const char *const str[] = {
      [FCFT_SUBPIXEL_DEFAULT] = "default",
      [FCFT_SUBPIXEL_NONE] = "disabled",
      [FCFT_SUBPIXEL_HORIZONTAL_RGB] = "RGB",
      [FCFT_SUBPIXEL_HORIZONTAL_BGR] = "BGR",
      [FCFT_SUBPIXEL_VERTICAL_RGB] = "V-RGB",
      [FCFT_SUBPIXEL_VERTICAL_BGR] = "V-BGR",
  };

  LOG_DBG("subpixel mode changed: %s -> %s", str[term->font_subpixel],
          str[subpixel]);
#endif

  term->font_subpixel = subpixel;
  term_damage_view(term);
  render_refresh(term);
}

int term_font_baseline(const struct terminal *term) {
  const struct fcft_font *font = term->fonts[0];
  const int line_height = term->cell_height;
  const int font_height = font->ascent + font->descent;

  /*
   * Center glyph on the line *if* using a custom line height,
   * otherwise the baseline is simply 'descent' pixels above the
   * bottom of the cell
   */
  const int glyph_top_y = term->font_line_height.px >= 0
                              ? round((line_height - font_height) / 2.)
                              : 0;

  return term->font_y_ofs + line_height - glyph_top_y - font->descent;
}

void term_damage_rows(struct terminal *term, int start, int end) {
  xassert(start <= end);
  for (int r = start; r <= end; r++) {
    struct row *row = grid_row(term->grid, r);
    row->dirty = true;
    for (int c = 0; c < term->grid->num_cols; c++)
      row->cells[c].attrs.clean = 0;
  }
}

void term_damage_rows_in_view(struct terminal *term, int start, int end) {
  xassert(start <= end);
  for (int r = start; r <= end; r++) {
    struct row *row = grid_row_in_view(term->grid, r);
    row->dirty = true;
    for (int c = 0; c < term->grid->num_cols; c++)
      row->cells[c].attrs.clean = 0;
  }
}

void term_damage_all(struct terminal *term) {
  term_damage_rows(term, 0, term->rows - 1);
}

void term_damage_view(struct terminal *term) {
  term_damage_rows_in_view(term, 0, term->rows - 1);
}

void term_damage_cursor(struct terminal *term) {
  term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0;
  term->grid->cur_row->dirty = true;
}

void term_damage_margins(struct terminal *term) { term->render.margins = true; }

void term_damage_color(struct terminal *term, enum color_source src, int idx) {
  xassert(src == COLOR_DEFAULT || src == COLOR_BASE256);

  for (int r = 0; r < term->rows; r++) {
    struct row *row = grid_row_in_view(term->grid, r);
    struct cell *cell = &row->cells[0];
    const struct cell *end = &row->cells[term->cols];

    for (; cell < end; cell++) {
      bool dirty = false;

      switch (cell->attrs.fg_src) {
      case COLOR_BASE16:
      case COLOR_BASE256:
        if (src == COLOR_BASE256 && cell->attrs.fg == idx)
          dirty = true;
        break;

      case COLOR_DEFAULT:
        if (src == COLOR_DEFAULT) {
          /* Doesn't matter whether we've updated the
             default foreground, or background, we still
             want to dirty this cell, to be sure we handle
             all cases of color inversion/reversal */
          dirty = true;
        }
        break;

      case COLOR_RGB:
        /* Not affected */
        break;
      }

      switch (cell->attrs.bg_src) {
      case COLOR_BASE16:
      case COLOR_BASE256:
        if (src == COLOR_BASE256 && cell->attrs.bg == idx)
          dirty = true;
        break;

      case COLOR_DEFAULT:
        if (src == COLOR_DEFAULT) {
          /* Doesn't matter whether we've updated the
             default foreground, or background, we still
             want to dirty this cell, to be sure we handle
             all cases of color inversion/reversal */
          dirty = true;
        }
        break;

      case COLOR_RGB:
        /* Not affected */
        break;
      }

      if (dirty) {
        cell->attrs.clean = 0;
        row->dirty = true;
      }
    }

    /* Colored underlines */
    if (row->extra != NULL) {
      const struct row_ranges *underlines = &row->extra->underline_ranges;

      for (int i = 0; i < underlines->count; i++) {
        const struct row_range *range = &underlines->v[i];

        /* Underline colors are either default, or
           BASE256/RGB, but never BASE16 */
        xassert(range->underline.color_src == COLOR_DEFAULT ||
                range->underline.color_src == COLOR_BASE256 ||
                range->underline.color_src == COLOR_RGB);

        if (range->underline.color_src == src) {
          struct cell *c = &row->cells[range->start];
          const struct cell *e = &row->cells[range->end + 1];

          for (; c < e; c++)
            c->attrs.clean = 0;

          row->dirty = true;
        }
      }
    }
  }
}

void term_damage_scroll(struct terminal *term, enum damage_type damage_type,
                        struct scroll_region region, int lines) {
  if (likely(tll_length(term->grid->scroll_damage) > 0)) {
    struct damage *dmg = &tll_back(term->grid->scroll_damage);

    if (likely(dmg->type == damage_type && dmg->region.start == region.start &&
               dmg->region.end == region.end)) {
      /* Make sure we don't overflow... */
      int new_line_count = (int)dmg->lines + lines;
      if (likely(new_line_count <= UINT16_MAX)) {
        dmg->lines = new_line_count;
        return;
      }
    }
  }
  struct damage dmg = {
      .type = damage_type,
      .region = region,
      .lines = lines,
  };
  tll_push_back(term->grid->scroll_damage, dmg);
}

void term_erase(struct terminal *term, int start_row, int start_col,
                int end_row, int end_col) {
  xassert(start_row <= end_row);
  xassert(start_col <= end_col || start_row < end_row);

  if (start_row == end_row) {
    struct row *row = grid_row(term->grid, start_row);
    erase_cell_range(term, row, start_col, end_col);
    sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1);
    return;
  }

  xassert(end_row > start_row);

  erase_cell_range(term, grid_row(term->grid, start_row), start_col,
                   term->cols - 1);
  sixel_overwrite_by_row(term, start_row, start_col, term->cols - start_col);

  for (int r = start_row + 1; r < end_row; r++)
    erase_line(term, grid_row(term->grid, r));
  sixel_overwrite_by_rectangle(term, start_row + 1, 0, end_row - start_row,
                               term->cols);

  erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col);
  sixel_overwrite_by_row(term, end_row, 0, end_col + 1);
}

void term_erase_scrollback(struct terminal *term) {
  const struct grid *grid = term->grid;
  const int num_rows = grid->num_rows;
  const int mask = num_rows - 1;

  const int scrollback_history_size = num_rows - term->rows;
  if (scrollback_history_size == 0)
    return;

  const int start = (grid->offset + term->rows) & mask;
  const int end = (grid->offset - 1) & mask;

  const int rel_start = grid_row_abs_to_sb(grid, term->rows, start);
  const int rel_end = grid_row_abs_to_sb(grid, term->rows, end);

  const int sel_start = selection_get_start(term).row;
  const int sel_end = selection_get_end(term).row;

  if (sel_end >= 0) {
    /*
     * Cancel selection if it touches any of the rows in the
     * scrollback, since we can't have the selection reference
     * soon-to-be deleted rows.
     *
     * This is done by range checking the selection range against
     * the scrollback range.
     *
     * To make this comparison simpler, the start/end absolute row
     * numbers are "rebased" against the scrollback start, where
     * row 0 is the *first* row in the scrollback. A high number
     * thus means the row is further *down* in the scrollback,
     * closer to the screen bottom.
     */

    const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start);
    const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end);

    if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) ||
        (rel_sel_start <= rel_end && rel_sel_end >= rel_end) ||
        (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) {
      selection_cancel(term);
    }
  }

  tll_foreach(term->grid->sixel_images, it) {
    struct sixel *six = &it->item;
    const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row);
    const int six_end =
        grid_row_abs_to_sb(grid, term->rows, six->pos.row + six->rows - 1);

    if ((six_start <= rel_start && six_end >= rel_start) ||
        (six_start <= rel_end && six_end >= rel_end) ||
        (six_start >= rel_start && six_end <= rel_end)) {
      sixel_destroy(six);
      tll_remove(term->grid->sixel_images, it);
    }
  }

  for (int i = start;; i = (i + 1) & mask) {
    struct row *row = term->grid->rows[i];
    if (row != NULL) {
      if (term->render.last_cursor.row == row)
        term->render.last_cursor.row = NULL;

      grid_row_free(row);
      term->grid->rows[i] = NULL;
    }

    if (i == end)
      break;
  }

  term->grid->view = term->grid->offset;

#if defined(_DEBUG)
  for (int i = 0; i < term->rows; i++) {
    xassert(grid_row_in_view(term->grid, i) != NULL);
  }
#endif

  term_damage_view(term);
}

UNITTEST {
  const int scrollback_rows = 16;
  const int term_rows = 5;
  const int cols = 5;

  struct fdm *fdm = fdm_init();
  xassert(fdm != NULL);

  struct terminal term = {
      .fdm = fdm,
      .rows = term_rows,
      .cols = cols,
      .normal =
          {
              .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])),
              .num_rows = scrollback_rows,
              .num_cols = cols,
          },
      .grid = &term.normal,
      .selection =
          {
              .coords =
                  {
                      .start = {-1, -1},
                      .end = {-1, -1},
                  },
              .kind = SELECTION_NONE,
              .auto_scroll =
                  {
                      .fd = -1,
                  },
          },
  };

#define populate_scrollback()                                                  \
  do {                                                                         \
    for (int i = 0; i < scrollback_rows; i++) {                                \
      if (term.normal.rows[i] == NULL) {                                       \
        struct row *r = xcalloc(1, sizeof(*term.normal.rows[i]));              \
        r->cells = xcalloc(cols, sizeof(r->cells[0]));                         \
        term.normal.rows[i] = r;                                               \
      }                                                                        \
    }                                                                          \
  } while (0)

  /*
   * Test case 1 - no selection, just verify all rows except those
   * on screen have been deleted.
   */

  populate_scrollback();
  term.normal.offset = 11;
  term_erase_scrollback(&term);
  for (int i = 0; i < scrollback_rows; i++) {
    if (i >= term.normal.offset && i < term.normal.offset + term_rows)
      xassert(term.normal.rows[i] != NULL);
    else
      xassert(term.normal.rows[i] == NULL);
  }

  /*
   * Test case 2 - selection that touches the scrollback. Verify the
   * selection is cancelled.
   */

  term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */

  /* Selection covers rows 15,0,1,2,3 */
  term.selection.coords.start = (struct coord){.row = 15};
  term.selection.coords.end = (struct coord){.row = 19};
  term.selection.kind = SELECTION_CHAR_WISE;

  populate_scrollback();
  term_erase_scrollback(&term);
  xassert(term.selection.coords.start.row < 0);
  xassert(term.selection.coords.end.row < 0);
  xassert(term.selection.kind == SELECTION_NONE);

  /*
   * Test case 3 - selection that does *not* touch the
   * scrollback. Verify the selection is *not* cancelled.
   */

  /* Selection covers rows 15,0 */
  term.selection.coords.start = (struct coord){.row = 15};
  term.selection.coords.end = (struct coord){.row = 16};
  term.selection.kind = SELECTION_CHAR_WISE;

  populate_scrollback();
  term_erase_scrollback(&term);
  xassert(term.selection.coords.start.row == 15);
  xassert(term.selection.coords.end.row == 16);
  xassert(term.selection.kind == SELECTION_CHAR_WISE);

  term.selection.coords.start = (struct coord){-1, -1};
  term.selection.coords.end = (struct coord){-1, -1};
  term.selection.kind = SELECTION_NONE;

  /*
   * Test case 4 - sixel that touch the scrollback
   */

  struct sixel six = {
      .rows = 5,
      .pos =
          {
              .row = 15,
          },
  };
  tll_push_back(term.normal.sixel_images, six);
  populate_scrollback();
  term_erase_scrollback(&term);
  xassert(tll_length(term.normal.sixel_images) == 0);

  /*
   * Test case 5 - sixel that does *not* touch the scrollback
   */
  six.rows = 3;
  tll_push_back(term.normal.sixel_images, six);
  populate_scrollback();
  term_erase_scrollback(&term);
  xassert(tll_length(term.normal.sixel_images) == 1);

  /* Cleanup */
  tll_free(term.normal.sixel_images);
  xassert(term.selection.auto_scroll.fd == -1);
  for (int i = 0; i < scrollback_rows; i++)
    grid_row_free(term.normal.rows[i]);
  free(term.normal.rows);
  fdm_destroy(fdm);
}

int term_row_rel_to_abs(const struct terminal *term, int row) {
  switch (term->origin) {
  case ORIGIN_ABSOLUTE:
    return min(row, term->rows - 1);

  case ORIGIN_RELATIVE:
    return min(row + term->scroll_region.start, term->scroll_region.end - 1);
  }

  BUG("Invalid cursor_origin value");
  return -1;
}

void term_cursor_to(struct terminal *term, int row, int col) {
  xassert(row < term->rows);
  xassert(col < term->cols);

  term->grid->cursor.lcf = false;

  term->grid->cursor.point.col = col;
  term->grid->cursor.point.row = row;

  term_reset_grapheme_state(term);

  term->grid->cur_row = grid_row(term->grid, row);
}

void term_cursor_home(struct terminal *term) {
  term_cursor_to(term, term_row_rel_to_abs(term, 0), 0);
}

void term_cursor_col(struct terminal *term, int col) {
  xassert(col < term->cols);

  term->grid->cursor.lcf = false;
  term->grid->cursor.point.col = col;
  term_reset_grapheme_state(term);
}

void term_cursor_left(struct terminal *term, int count) {
  int move_amount = min(term->grid->cursor.point.col, count);
  term->grid->cursor.point.col -= move_amount;
  xassert(term->grid->cursor.point.col >= 0);
  term->grid->cursor.lcf = false;
  term_reset_grapheme_state(term);
}

void term_cursor_right(struct terminal *term, int count) {
  int move_amount = min(term->cols - term->grid->cursor.point.col - 1, count);
  term->grid->cursor.point.col += move_amount;
  xassert(term->grid->cursor.point.col < term->cols);
  term->grid->cursor.lcf = false;
  term_reset_grapheme_state(term);
}

void term_cursor_up(struct terminal *term, int count) {
  int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start;
  xassert(term->grid->cursor.point.row >= top);

  int move_amount = min(term->grid->cursor.point.row - top, count);
  term_cursor_to(term, term->grid->cursor.point.row - move_amount,
                 term->grid->cursor.point.col);
}

void term_cursor_down(struct terminal *term, int count) {
  int bottom =
      term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end;
  xassert(bottom >= term->grid->cursor.point.row);

  int move_amount = min(bottom - term->grid->cursor.point.row - 1, count);
  term_cursor_to(term, term->grid->cursor.point.row + move_amount,
                 term->grid->cursor.point.col);
}

static bool cursor_blink_rearm_timer(struct terminal *term) {
  if (term->cursor_blink.fd < 0) {
    int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
    if (fd < 0) {
      LOG_ERRNO("failed to create cursor blink timer FD");
      return false;
    }

    if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_cursor_blink, term)) {
      close(fd);
      return false;
    }

    term->cursor_blink.fd = fd;
  }

  const int rate_ms = term->conf->cursor.blink.rate_ms;
  const long secs = rate_ms / 1000;
  const long nsecs = (rate_ms % 1000) * 1000000;

  const struct itimerspec timer = {
      .it_value = {.tv_sec = secs, .tv_nsec = nsecs},
      .it_interval = {.tv_sec = secs, .tv_nsec = nsecs},
  };

  if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) {
    LOG_ERRNO("failed to arm cursor blink timer");
    fdm_del(term->fdm, term->cursor_blink.fd);
    term->cursor_blink.fd = -1;
    return false;
  }

  return true;
}

static bool cursor_blink_disarm_timer(struct terminal *term) {
  fdm_del(term->fdm, term->cursor_blink.fd);
  term->cursor_blink.fd = -1;
  return true;
}

void term_cursor_blink_update(struct terminal *term) {
  bool enable = term->cursor_blink.decset || term->cursor_blink.deccsusr;
  bool activate = !term->shutdown.in_progress && enable && term->visual_focus;

  LOG_DBG("decset=%d, deccsrusr=%d, focus=%d, shutting-down=%d, enable=%d, "
          "activate=%d",
          term->cursor_blink.decset, term->cursor_blink.deccsusr,
          term->visual_focus, term->shutdown.in_progress, enable, activate);

  if (activate && term->cursor_blink.fd < 0) {
    term->cursor_blink.state = CURSOR_BLINK_ON;
    cursor_blink_rearm_timer(term);
  } else if (!activate && term->cursor_blink.fd >= 0)
    cursor_blink_disarm_timer(term);
}

static bool selection_on_top_region(const struct terminal *term,
                                    struct scroll_region region) {
  return region.start > 0 && selection_on_rows(term, 0, region.start - 1);
}

static bool selection_on_bottom_region(const struct terminal *term,
                                       struct scroll_region region) {
  return region.end < term->rows &&
         selection_on_rows(term, region.end, term->rows - 1);
}

void term_scroll_partial(struct terminal *term, struct scroll_region region,
                         int rows) {
  LOG_DBG("scroll: rows=%d, region.start=%d, region.end=%d", rows, region.start,
          region.end);

  /* Verify scroll amount has been clamped */
  xassert(rows <= region.end - region.start);

  /* Cancel selections that cannot be scrolled */
  if (unlikely(term->selection.coords.end.row >= 0)) {
    /*
     * Selection is (partly) inside either the top or bottom
     * scrolling regions, or on (at least one) of the lines
     * scrolled in (i.e. reused lines).
     */
    if (selection_on_top_region(term, region) ||
        selection_on_bottom_region(term, region)) {
      selection_cancel(term);
    } else
      selection_scroll_up(term, rows);
  }

  sixel_scroll_up(term, rows);

  /* How many lines from the scrollback start is the current viewport? */
  int view_sb_start_distance =
      grid_row_abs_to_sb(term->grid, term->rows, term->grid->view);

  bool view_follows = term->grid->view == term->grid->offset;
  term->grid->offset += rows;
  term->grid->offset &= term->grid->num_rows - 1;

  if (likely(view_follows)) {
    term_damage_scroll(term, DAMAGE_SCROLL, region, rows);
    selection_view_down(term, term->grid->offset);
    term->grid->view = term->grid->offset;
  } else if (unlikely(rows > view_sb_start_distance)) {
    /* Part of current view is being scrolled out */
    int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0);
    selection_view_down(term, new_view);
    cmd_scrollback_down(term, rows - view_sb_start_distance);
  }

  /* Top non-scrolling region. */
  for (int i = region.start - 1; i >= 0; i--)
    grid_swap_row(term->grid, i - rows, i);

  /* Bottom non-scrolling region */
  for (int i = term->rows - 1; i >= region.end; i--)
    grid_swap_row(term->grid, i - rows, i);

  /* Erase scrolled in lines */
  for (int r = region.end - rows; r < region.end; r++) {
    struct row *row = grid_row_and_alloc(term->grid, r);
    erase_line(term, row);
  }

  term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row);

#if defined(_DEBUG)
  for (int r = 0; r < term->rows; r++)
    xassert(grid_row(term->grid, r) != NULL);
#endif
}

void term_scroll(struct terminal *term, int rows) {
  term_scroll_partial(term, term->scroll_region, rows);
}

void term_scroll_reverse_partial(struct terminal *term,
                                 struct scroll_region region, int rows) {
  LOG_DBG("scroll reverse: rows=%d, region.start=%d, region.end=%d", rows,
          region.start, region.end);

  /* Verify scroll amount has been clamped */
  xassert(rows <= region.end - region.start);

  /* Cancel selections that cannot be scrolled */
  if (unlikely(term->selection.coords.end.row >= 0)) {
    /*
     * Selection is (partly) inside either the top or bottom
     * scrolling regions, or on (at least one) of the lines
     * scrolled in (i.e. reused lines).
     */
    if (selection_on_top_region(term, region) ||
        selection_on_bottom_region(term, region)) {
      selection_cancel(term);
    } else
      selection_scroll_down(term, rows);
  }

  /* Unallocate scrolled out lines */
  for (int r = region.end - rows; r < region.end; r++) {
    const int abs_r = grid_row_absolute(term->grid, r);
    struct row *row = term->grid->rows[abs_r];

    grid_row_free(row);
    term->grid->rows[abs_r] = NULL;

    if (term->render.last_cursor.row == row)
      term->render.last_cursor.row = NULL;
  }

  sixel_scroll_down(term, rows);

  const bool view_follows = term->grid->view == term->grid->offset;
  term->grid->offset -= rows;
  term->grid->offset += term->grid->num_rows;
  term->grid->offset &= term->grid->num_rows - 1;

  /* How many lines from the scrollback start is the current viewport? */
  const int view_sb_start_distance =
      grid_row_abs_to_sb(term->grid, term->rows, term->grid->view);
  const int offset_sb_start_distance =
      grid_row_abs_to_sb(term->grid, term->rows, term->grid->offset);

  xassert(term->grid->offset >= 0);
  xassert(term->grid->offset < term->grid->num_rows);

  if (view_follows) {
    term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows);
    selection_view_up(term, term->grid->offset);
    term->grid->view = term->grid->offset;
  } else if (unlikely(view_sb_start_distance > offset_sb_start_distance)) {
    /* Part of current view is being scrolled out */
    int new_view = term->grid->offset;
    selection_view_up(term, new_view);
    term->grid->view = new_view;
  }

  /* Bottom non-scrolling region */
  for (int i = region.end + rows; i < term->rows + rows; i++)
    grid_swap_row(term->grid, i, i - rows);

  /* Top non-scrolling region */
  for (int i = 0 + rows; i < region.start + rows; i++)
    grid_swap_row(term->grid, i, i - rows);

  /* Erase scrolled in lines */
  for (int r = region.start; r < region.start + rows; r++) {
    struct row *row = grid_row_and_alloc(term->grid, r);
    erase_line(term, row);
  }

  if (unlikely(view_sb_start_distance > offset_sb_start_distance))
    term_damage_view(term);

  term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row);

#if defined(_DEBUG)
  for (int r = 0; r < term->rows; r++)
    xassert(grid_row(term->grid, r) != NULL);
  for (int r = 0; r < term->rows; r++)
    xassert(grid_row_in_view(term->grid, r) != NULL);
#endif
}

void term_scroll_reverse(struct terminal *term, int rows) {
  term_scroll_reverse_partial(term, term->scroll_region, rows);
}

void term_carriage_return(struct terminal *term) {
  term_cursor_left(term, term->grid->cursor.point.col);
}

void term_linefeed(struct terminal *term) {
  term->grid->cursor.lcf = false;

  if (term->grid->cursor.point.row == term->scroll_region.end - 1)
    term_scroll(term, 1);
  else
    term_cursor_down(term, 1);

  term_reset_grapheme_state(term);
}

void term_reverse_index(struct terminal *term) {
  if (term->grid->cursor.point.row == term->scroll_region.start)
    term_scroll_reverse(term, 1);
  else
    term_cursor_up(term, 1);
}

void term_reset_view(struct terminal *term) {
  if (term->grid->view == term->grid->offset)
    return;

  term->grid->view = term->grid->offset;
  term_damage_view(term);
}

void term_save_cursor(struct terminal *term) {
  term->grid->saved_cursor = term->grid->cursor;
  term->vt.saved_attrs = term->vt.attrs;
  term->saved_charsets = term->charsets;
}

void term_restore_cursor(struct terminal *term, const struct cursor *cursor) {
  int row = min(cursor->point.row, term->rows - 1);
  int col = min(cursor->point.col, term->cols - 1);

  term_cursor_to(term, row, col);
  term->grid->cursor.lcf = cursor->lcf;

  term->vt.attrs = term->vt.saved_attrs;
  term->charsets = term->saved_charsets;

  term->bits_affecting_ascii_printer.charset =
      term->charsets.set[term->charsets.selected] != CHARSET_ASCII;
  term_update_ascii_printer(term);
}

void term_visual_focus_in(struct terminal *term) {
  if (term->visual_focus)
    return;

  term->visual_focus = true;
  term_cursor_blink_update(term);
  render_refresh_csd(term);
}

void term_visual_focus_out(struct terminal *term) {
  if (!term->visual_focus)
    return;

  term->visual_focus = false;
  term_cursor_blink_update(term);
  render_refresh_csd(term);
}

void term_kbd_focus_in(struct terminal *term) {
  if (term->kbd_focus)
    return;

  term->kbd_focus = true;

  if (term->render.urgency) {
    term->render.urgency = false;
    term_damage_margins(term);
  }

  cursor_refresh(term);

  if (term->focus_events)
    term_to_slave(term, "\033[I", 3);
}

void term_kbd_focus_out(struct terminal *term) {
  if (!term->kbd_focus)
    return;

  tll_foreach(term->wl->seats, it) if (it->item.kbd_focus == term) return;

#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  if (term_ime_reset(term))
    render_refresh(term);
#endif

  term->kbd_focus = false;
  cursor_refresh(term);

  if (term->focus_events)
    term_to_slave(term, "\033[O", 3);
}

static int linux_mouse_button_to_x(int button) {
  /* Note: on X11, scroll events where reported as buttons. Not so
   * on Wayland. We manually map scroll events to custom "button"
   * defines (BTN_WHEEL_*).
   */
  switch (button) {
  case BTN_LEFT:
    return 1;
  case BTN_MIDDLE:
    return 2;
  case BTN_RIGHT:
    return 3;
  case BTN_WHEEL_BACK:
    return 4; /* Foot custom define */
  case BTN_WHEEL_FORWARD:
    return 5; /* Foot custom define */
  case BTN_WHEEL_LEFT:
    return 6; /* Foot custom define */
  case BTN_WHEEL_RIGHT:
    return 7; /* Foot custom define */
  case BTN_SIDE:
    return 8;
  case BTN_EXTRA:
    return 9;
  case BTN_FORWARD:
    return 10;
  case BTN_BACK:
    return 11;
  case BTN_TASK:
    return 12; /* Guessing... */

  default:
    LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button);
    return -1;
  }
}

static int encode_xbutton(int xbutton) {
  switch (xbutton) {
  case 1:
  case 2:
  case 3:
    return xbutton - 1;

  case 4:
  case 5:
  case 6:
  case 7:
    /* Like button 1 and 2, but with 64 added */
    return xbutton - 4 + 64;

  case 8:
  case 9:
  case 10:
  case 11:
    /* Similar to 4 and 5, but adding 128 instead of 64 */
    return xbutton - 8 + 128;

  default:
    LOG_ERR("cannot encode X mouse button: %d", xbutton);
    return -1;
  }
}

static void report_mouse_click(struct terminal *term, int encoded_button,
                               int row, int col, int row_pixels, int col_pixels,
                               bool release) {
  char response[128];

  switch (term->mouse_reporting) {
  case MOUSE_NORMAL: {
    int encoded_col = 32 + col + 1;
    int encoded_row = 32 + row + 1;
    if (encoded_col > 255 || encoded_row > 255)
      return;

    snprintf(response, sizeof(response), "\033[M%c%c%c",
             32 + (release ? 3 : encoded_button), encoded_col, encoded_row);
    break;
  }

  case MOUSE_SGR:
    snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", encoded_button,
             col + 1, row + 1, release ? 'm' : 'M');
    break;

  case MOUSE_SGR_PIXELS: {
    const int bounded_col = max(col_pixels, 0);
    const int bounded_row = max(row_pixels, 0);
    snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", encoded_button,
             bounded_col + 1, bounded_row + 1, release ? 'm' : 'M');
    break;
  }

  case MOUSE_URXVT:
    snprintf(response, sizeof(response), "\033[%d;%d;%dM",
             32 + (release ? 3 : encoded_button), col + 1, row + 1);
    break;

  case MOUSE_UTF8:
    /* Unimplemented */
    return;
  }

  term_to_slave(term, response, strlen(response));
}

static void report_mouse_motion(struct terminal *term, int encoded_button,
                                int row, int col, int row_pixels,
                                int col_pixels) {
  report_mouse_click(term, encoded_button, row, col, row_pixels, col_pixels,
                     false);
}

bool term_mouse_grabbed(const struct terminal *term, const struct seat *seat) {
  /*
   * Mouse is grabbed by us, regardless of whether mouse tracking
   * has been enabled or not.
   */

  xkb_mod_mask_t mods;
  get_current_modifiers(seat, &mods, NULL, 0, true);

  const struct key_binding_set *bindings =
      key_binding_for(term->wl->key_binding_manager, term->conf, seat);
  const xkb_mod_mask_t override_modmask = bindings->selection_overrides;
  bool override_mods_pressed = (mods & override_modmask) == override_modmask;

  return term->mouse_tracking == MOUSE_NONE ||
         (seat->kbd_focus == term && override_mods_pressed);
}

void term_mouse_down(struct terminal *term, int button, int row, int col,
                     int row_pixels, int col_pixels, bool _shift, bool _alt,
                     bool _ctrl) {
  /* Map libevent button event code to X button number */
  int xbutton = linux_mouse_button_to_x(button);
  if (xbutton == -1)
    return;

  int encoded = encode_xbutton(xbutton);
  if (encoded == -1)
    return;

  bool has_focus = term->kbd_focus;
  bool shift = has_focus ? _shift : false;
  bool alt = has_focus ? _alt : false;
  bool ctrl = has_focus ? _ctrl : false;

  encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);

  switch (term->mouse_tracking) {
  case MOUSE_NONE:
    break;

  case MOUSE_CLICK:
  case MOUSE_DRAG:
  case MOUSE_MOTION:
    report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, false);
    break;

  case MOUSE_X10:
    /* Never enabled */
    BUG("X10 mouse mode not implemented");
    break;
  }
}

void term_mouse_up(struct terminal *term, int button, int row, int col,
                   int row_pixels, int col_pixels, bool _shift, bool _alt,
                   bool _ctrl) {
  /* Map libevent button event code to X button number */
  int xbutton = linux_mouse_button_to_x(button);
  if (xbutton == -1)
    return;

  if (xbutton == 4 || xbutton == 5) {
    /* No release events for vertical scroll wheel buttons */
    return;
  }

  int encoded = encode_xbutton(xbutton);
  if (encoded == -1)
    return;

  bool has_focus = term->kbd_focus;
  bool shift = has_focus ? _shift : false;
  bool alt = has_focus ? _alt : false;
  bool ctrl = has_focus ? _ctrl : false;

  encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);

  switch (term->mouse_tracking) {
  case MOUSE_NONE:
    break;

  case MOUSE_CLICK:
  case MOUSE_DRAG:
  case MOUSE_MOTION:
    report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, true);
    break;

  case MOUSE_X10:
    /* Never enabled */
    BUG("X10 mouse mode not implemented");
    break;
  }
}

void term_mouse_motion(struct terminal *term, int button, int row, int col,
                       int row_pixels, int col_pixels, bool _shift, bool _alt,
                       bool _ctrl) {
  int encoded = 0;

  if (button != 0) {
    /* Map libevent button event code to X button number */
    int xbutton = linux_mouse_button_to_x(button);
    if (xbutton == -1)
      return;

    encoded = encode_xbutton(xbutton);
    if (encoded == -1)
      return;
  } else
    encoded = 3; /* "released" */

  bool has_focus = term->kbd_focus;
  bool shift = has_focus ? _shift : false;
  bool alt = has_focus ? _alt : false;
  bool ctrl = has_focus ? _ctrl : false;

  encoded += 32; /* Motion event */
  encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);

  switch (term->mouse_tracking) {
  case MOUSE_NONE:
  case MOUSE_CLICK:
    return;

  case MOUSE_DRAG:
    if (button == 0)
      return;
    /* FALLTHROUGH */

  case MOUSE_MOTION:
    report_mouse_motion(term, encoded, row, col, row_pixels, col_pixels);
    break;

  case MOUSE_X10:
    /* Never enabled */
    BUG("X10 mouse mode not implemented");
    break;
  }
}

void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) {
  enum cursor_shape shape = CURSOR_SHAPE_NONE;

  switch (term->active_surface) {
  case TERM_SURF_GRID:
    if (seat->pointer.hidden)
      shape = CURSOR_SHAPE_HIDDEN;

    else if (cursor_string_to_server_shape(term->mouse_user_cursor,
                                           term->wl->shape_manager_version) !=
                 0 ||
             render_xcursor_is_valid(seat, term->mouse_user_cursor)) {
      shape = CURSOR_SHAPE_CUSTOM;
    }

    else if (term_mouse_grabbed(term, seat)) {
      shape = CURSOR_SHAPE_TEXT;
    }

    else
      shape = CURSOR_SHAPE_LEFT_PTR;
    break;

  case TERM_SURF_TITLE:
  case TERM_SURF_BUTTON_MINIMIZE:
  case TERM_SURF_BUTTON_MAXIMIZE:
  case TERM_SURF_BUTTON_CLOSE:
    shape = CURSOR_SHAPE_LEFT_PTR;
    break;

  case TERM_SURF_TAB_BAR: {
    size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y);
    shape = (idx != SIZE_MAX) ? CURSOR_SHAPE_POINTER : CURSOR_SHAPE_LEFT_PTR;
    break;
  }

  case TERM_SURF_TAB_OVERVIEW:
    shape = CURSOR_SHAPE_POINTER;
    break;

  case TERM_SURF_BORDER_LEFT:
  case TERM_SURF_BORDER_RIGHT:
  case TERM_SURF_BORDER_TOP:
  case TERM_SURF_BORDER_BOTTOM:
    shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y);
    break;

  case TERM_SURF_NONE:
    return;
  }

  if (shape == CURSOR_SHAPE_NONE)
    BUG("xcursor not set");

  render_xcursor_set(seat, term, shape);
}

void term_xcursor_update(struct terminal *term) {
  tll_foreach(term->wl->seats, it)
      term_xcursor_update_for_seat(term, &it->item);
}

void term_set_window_title(struct terminal *term, const char *title) {
  if (term->conf->locked_title && term->window_title_has_been_set)
    return;

  if (term->window_title != NULL && streq(term->window_title, title))
    return;

  if (!is_valid_utf8_and_printable(title)) {
    /* It's an xdg_toplevel::set_title() protocol violation to set
       a title with an invalid UTF-8 sequence */
    LOG_WARN("%s: title is not valid UTF-8, ignoring", title);
    return;
  }

  free(term->window_title);
  term->window_title = xstrdup(title);
  render_refresh_title(term);
  render_refresh_tab_bar(term);
  term->window_title_has_been_set = true;
}

void term_set_app_id(struct terminal *term, const char *app_id) {
  if (app_id != NULL && *app_id == '\0')
    app_id = NULL;

  if (term->app_id == NULL && app_id == NULL)
    return;

  if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id))
    return;

  if (app_id != NULL && !is_valid_utf8_and_printable(app_id)) {
    LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id);
    return;
  }

  free(term->app_id);
  if (app_id != NULL) {
    term->app_id = xstrdup(app_id);
  } else {
    term->app_id = NULL;
  }

  const size_t length = app_id != NULL ? strlen(app_id) : 0;
  if (length > 2048) {
    /*
     * Not sure if there's a limit in the protocol, or the
     * libwayland implementation, or e.g. wlroots, but too long
     * app-id's (not e.g. title) causes at least river and sway to
     * peg the CPU at 100%, and stop sending e.g. frame callbacks.
     *
     */
    term->app_id[2048] = '\0';
  }

  render_refresh_app_id(term);
  render_refresh_icon(term);
}

const char *term_icon(const struct terminal *term) {
  const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id;

  return
#if 0
term->window_icon != NULL
        ? term->window_icon
        :
#endif
      streq(app_id, "footclient") ? "foot" : app_id;
}

void term_flash(struct terminal *term, unsigned duration_ms) {
  LOG_DBG("FLASH for %ums", duration_ms);

  struct itimerspec alarm = {
      .it_value = {.tv_sec = 0, .tv_nsec = duration_ms * 1000000},
  };

  if (timerfd_settime(term->flash.fd, 0, &alarm, NULL) < 0)
    LOG_ERRNO("failed to arm flash timer");
  else {
    term->flash.active = true;
  }
}

void term_bell(struct terminal *term) {

  if (!term->bell_action_enabled)
    return;

  if (term->conf->bell.urgent && !term->kbd_focus) {
    if (!wayl_win_set_urgent(term->window)) {
      /*
       * Urgency (xdg-activation) is relatively new in
       * Wayland. Fallback to our old, "faked", urgency -
       * rendering our window margins in red
       */
      term->render.urgency = true;
      term_damage_margins(term);
    }
  }

  if (term->conf->bell.system_bell)
    wayl_win_ring_bell(term->window);

  if (term->conf->bell.notify) {
    notify_notify(term, &(struct notification){
                            .title = xstrdup("Bell"),
                            .body = xstrdup("Bell in terminal"),
                            .expire_time = -1,
                            .focus = true,
                        });
  }

  if (term->conf->bell.flash)
    term_flash(term, 100);

  if ((term->conf->bell.command.argv.args != NULL) &&
      (!term->kbd_focus || term->conf->bell.command_focused)) {
    int devnull = open("/dev/null", O_RDONLY);
    spawn(term->reaper, NULL, term->conf->bell.command.argv.args, devnull, -1,
          -1, NULL, NULL, NULL);

    if (devnull >= 0)
      close(devnull);
  }
}

bool term_spawn_new(const struct terminal *term) {
  char *argv[4];
  int argc = 0;

  argv[argc++] = term->foot_exe;
  if (term->conf->conf_path != NULL) {
    argv[argc++] = "--config";
    argv[argc++] = term->conf->conf_path;
  }
  argv[argc] = NULL;

  return spawn(term->reaper, term->cwd, argv, -1, -1, -1, NULL, NULL, NULL) >=
         0;
}

void term_enable_app_sync_updates(struct terminal *term) {
  term->render.app_sync_updates.enabled = true;

  if (timerfd_settime(term->render.app_sync_updates.timer_fd, 0,
                      &(struct itimerspec){.it_value = {.tv_sec = 1}},
                      NULL) < 0) {
    LOG_ERR("failed to arm timer for application synchronized updates");
  }

  /* Disable pending refresh *iff* the grid is the *only* thing
   * scheduled to be re-rendered */
  if (!term->render.refresh.csd && !term->render.refresh.search &&
      !term->render.pending.csd && !term->render.pending.search) {
    term->render.refresh.grid = false;
    term->render.pending.grid = false;
  }

  /* Disarm delayed rendering timers */
  timerfd_settime(term->delayed_render_timer.lower_fd, 0,
                  &(struct itimerspec){{0}}, NULL);
  timerfd_settime(term->delayed_render_timer.upper_fd, 0,
                  &(struct itimerspec){{0}}, NULL);
  term->delayed_render_timer.is_armed = false;
}

void term_disable_app_sync_updates(struct terminal *term) {
  if (!term->render.app_sync_updates.enabled)
    return;

  term->render.app_sync_updates.enabled = false;
  render_refresh(term);

  /* Reset timers */
  timerfd_settime(term->render.app_sync_updates.timer_fd, 0,
                  &(struct itimerspec){{0}}, NULL);
}

static inline void print_linewrap(struct terminal *term) {
  if (likely(!term->grid->cursor.lcf)) {
    /* Not and end of line */
    return;
  }

  if (unlikely(!term->auto_margin)) {
    /* Auto-wrap disabled */
    return;
  }

  term->grid->cur_row->linebreak = false;
  term->grid->cursor.lcf = false;

  const int row = term->grid->cursor.point.row;

  if (row == term->scroll_region.end - 1)
    term_scroll(term, 1);
  else {
    const int new_row = min(row + 1, term->rows - 1);
    term->grid->cursor.point.row = new_row;
    term->grid->cur_row = grid_row(term->grid, new_row);
  }

  term->grid->cursor.point.col = 0;
}

static inline void print_insert(struct terminal *term, int width) {
  if (likely(!term->insert_mode))
    return;

  xassert(width > 0);

  struct row *row = term->grid->cur_row;
  const size_t move_count =
      max(0, term->cols - term->grid->cursor.point.col - width);

  memmove(&row->cells[term->grid->cursor.point.col + width],
          &row->cells[term->grid->cursor.point.col],
          move_count * sizeof(struct cell));

  /* Mark moved cells as dirty */
  for (size_t i = term->grid->cursor.point.col + width; i < term->cols; i++)
    row->cells[i].attrs.clean = 0;
}

static void print_spacer(struct terminal *term, int col, int remaining) {
  struct grid *grid = term->grid;
  struct row *row = grid->cur_row;
  struct cell *cell = &row->cells[col];

  cell->wc = CELL_SPACER + remaining;
  cell->attrs = (struct attributes){0};
}

/*
 * Puts a character on the grid. Coordinates are in screen coordinates
 * (i.e. ‘cursor’ coordinates).
 *
 * Does NOT:
 *  - update the cursor
 *  - linewrap
 *  - erase sixels
 *
 * Limitations:
 *   - double width characters not supported
 */
void term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count,
               bool use_sgr_attrs) {
  struct row *row = grid_row(term->grid, r);
  row->dirty = true;

  xassert(c + count <= term->cols);

  struct attributes attrs =
      use_sgr_attrs ? term->vt.attrs : (struct attributes){0};

  const struct cell *last = &row->cells[c + count];
  for (struct cell *cell = &row->cells[c]; cell < last; cell++) {
    cell->wc = data;
    cell->attrs = attrs;

    /* TODO: why do we print the URI here, and then erase it below? */
    if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) {
      grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id);

      switch (term->conf->url.osc8_underline) {
      case OSC8_UNDERLINE_ALWAYS:
        cell->attrs.url = true;
        break;

      case OSC8_UNDERLINE_URL_MODE:
        break;
      }
    }

    if (unlikely(use_sgr_attrs &&
                 (term->vt.underline.style > UNDERLINE_SINGLE ||
                  term->vt.underline.color_src != COLOR_DEFAULT))) {
      grid_row_underline_range_put(row, c, term->vt.underline);
    }
  }

  if (unlikely(row->extra != NULL)) {
    if (likely(term->vt.osc8.uri != NULL))
      grid_row_uri_range_erase(row, c, c + count - 1);

    if (likely(term->vt.underline.style <= UNDERLINE_SINGLE &&
               term->vt.underline.color_src == COLOR_DEFAULT)) {
      /* No extended/styled underlines active, so erase any such
         attributes at the target columns */
      grid_row_underline_range_erase(row, c, c + count - 1);
    }
  }
}

void term_print(struct terminal *term, char32_t wc, int width,
                bool insert_mode_disable) {
  xassert(width > 0);

  struct grid *grid = term->grid;

  if (unlikely(term->charsets.set[term->charsets.selected] ==
               CHARSET_GRAPHIC) &&
      wc >= 0x60 && wc <= 0x7e) {
    /* 0x60 - 0x7e */
    static const char32_t vt100_0[] = {
        U'◆', U'▒', U'␉', U'␌', U'␍', U'␊', U'°', U'±', /* ` - g */
        U'␤', U'␋', U'┘', U'┐', U'┌', U'└', U'┼', U'⎺', /* h - o */
        U'⎻', U'─', U'⎼', U'⎽', U'├', U'┤', U'┴', U'┬', /* p - w */
        U'│', U'≤', U'≥', U'π', U'≠', U'£', U'·',       /* x - ~ */
    };

    xassert(width == 1);
    wc = vt100_0[wc - 0x60];
  }

  print_linewrap(term);
  if (!insert_mode_disable)
    print_insert(term, width);

  int col = grid->cursor.point.col;

  if (unlikely(width > 1) && likely(term->auto_margin) &&
      col + width > term->cols) {
    /* Multi-column character that doesn't fit on current line -
     * pad with spacers */
    for (size_t i = col; i < term->cols; i++)
      print_spacer(term, i, 0);

    /* And force a line-wrap */
    grid->cursor.lcf = 1;
    print_linewrap(term);
    col = 0;
  }

  sixel_overwrite_at_cursor(term, width);

  /* *Must* get current cell *after* linewrap+insert */
  struct row *row = grid->cur_row;
  row->dirty = true;
  row->linebreak = true;

  struct cell *cell = &row->cells[col];
  cell->wc = term->vt.last_printed = wc;
  cell->attrs = term->vt.attrs;

  if (unlikely(term->vt.osc8.uri != NULL)) {
    for (int i = 0; i < width && (col + i) < term->cols; i++) {
      grid_row_uri_range_put(row, col + i, term->vt.osc8.uri, term->vt.osc8.id);
    }

    switch (term->conf->url.osc8_underline) {
    case OSC8_UNDERLINE_ALWAYS:
      cell->attrs.url = true;
      break;

    case OSC8_UNDERLINE_URL_MODE:
      break;
    }
  } else if (row->extra != NULL)
    grid_row_uri_range_erase(row, col, col + width - 1);

  if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE ||
               term->vt.underline.color_src != COLOR_DEFAULT)) {
    grid_row_underline_range_put(row, col, term->vt.underline);
  } else if (row->extra != NULL)
    grid_row_underline_range_erase(row, col, col + width - 1);

  /* Advance cursor the 'additional' columns while dirty:ing the cells */
  for (int i = 1; i < width && (col + 1) < term->cols; i++) {
    col++;
    print_spacer(term, col, width - i);
  }

  xassert(col < term->cols);

  /* Advance cursor */
  if (unlikely(++col >= term->cols)) {
    grid->cursor.lcf = true;
    col--;
  } else
    xassert(!grid->cursor.lcf);

  grid->cursor.point.col = col;
}

static void ascii_printer_generic(struct terminal *term, char32_t wc) {
  term_print(term, wc, 1, false);
}

static void ascii_printer_fast(struct terminal *term, char32_t wc) {
  struct grid *grid = term->grid;

  xassert(term->charsets.set[term->charsets.selected] == CHARSET_ASCII);
  xassert(!term->insert_mode);
  xassert(tll_length(grid->sixel_images) == 0);

  print_linewrap(term);

  /* *Must* get current cell *after* linewrap+insert */
  int col = grid->cursor.point.col;
  const int uri_start = col;

  struct row *row = grid->cur_row;
  row->dirty = true;
  row->linebreak = true;

  struct cell *cell = &row->cells[col];
  cell->wc = term->vt.last_printed = wc;
  cell->attrs = term->vt.attrs;

  /* Advance cursor */
  if (unlikely(++col >= term->cols)) {
    xassert(col == term->cols);
    grid->cursor.lcf = true;
    col--;
  } else
    xassert(!grid->cursor.lcf);

  grid->cursor.point.col = col;

  if (unlikely(row->extra != NULL)) {
    grid_row_uri_range_erase(row, uri_start, uri_start);
    grid_row_underline_range_erase(row, uri_start, uri_start);
  }
}

static void ascii_printer_single_shift(struct terminal *term, char32_t wc) {
  ascii_printer_generic(term, wc);
  term->charsets.selected = term->charsets.saved;

  term->bits_affecting_ascii_printer.charset =
      term->charsets.set[term->charsets.selected] != CHARSET_ASCII;
  term_update_ascii_printer(term);
}

void term_update_ascii_printer(struct terminal *term) {
  _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t),
                 "bad size");

  void (*new_printer)(struct terminal *term, char32_t wc) =
      unlikely(term->bits_affecting_ascii_printer.value != 0)
          ? &ascii_printer_generic
          : &ascii_printer_fast;

#if defined(_DEBUG) && LOG_ENABLE_DBG
  if (term->ascii_printer != new_printer) {
    LOG_DBG("switching ASCII printer %s -> %s",
            term->ascii_printer == &ascii_printer_fast ? "fast" : "generic",
            new_printer == &ascii_printer_fast ? "fast" : "generic");
  }
#endif

  term->ascii_printer = new_printer;
}

void term_single_shift(struct terminal *term, enum charset_designator idx) {
  term->charsets.saved = term->charsets.selected;
  term->charsets.selected = idx;
  term->ascii_printer = &ascii_printer_single_shift;
}

#if defined(FOOT_GRAPHEME_CLUSTERING)
static int emoji_vs_compare(const void *_key, const void *_entry) {
  const struct emoji_vs *key = _key;
  const struct emoji_vs *entry = _entry;

  uint32_t cp = key->start;

  if (cp < entry->start)
    return -1;
  else if (cp > entry->end)
    return 1;
  else
    return 0;
}

UNITTEST {
  /* Verify the emoji_vs list is sorted */
  int64_t last_end = -1;

  for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) {
    const struct emoji_vs *vs = &emoji_vs[i];
    xassert(vs->start <= vs->end);
    xassert(vs->start > last_end);
    xassert(vs->vs15 || vs->vs16);
    last_end = vs->end;
  }
}
#endif

void term_process_and_print_non_ascii(struct terminal *term, char32_t wc) {
  int width = c32width(wc);
  bool insert_mode_disable = false;
  const bool grapheme_clustering = term->grapheme_shaping;

#if !defined(FOOT_GRAPHEME_CLUSTERING)
  xassert(!grapheme_clustering);
#endif

  if (term->grid->cursor.point.col > 0 &&
      (grapheme_clustering ||
       (!grapheme_clustering && width == 0 && wc >= 0x300))) {
    int col = term->grid->cursor.point.col;
    if (!term->grid->cursor.lcf)
      col--;

    /* Skip past spacers */
    struct row *row = term->grid->cur_row;
    while (row->cells[col].wc >= CELL_SPACER && col > 0)
      col--;

    xassert(col >= 0 && col < term->cols);
    char32_t base = row->cells[col].wc;
    char32_t UNUSED last = base;

    /* Is base cell already a cluster? */
    const struct composed *composed =
        (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
            ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO)
            : NULL;

    uint32_t key;

    if (composed != NULL) {
      base = composed->chars[0];
      last = composed->chars[composed->count - 1];
      key = composed_key_from_key(composed->key, wc);
    } else
      key = composed_key_from_key(base, wc);

#if defined(FOOT_GRAPHEME_CLUSTERING)
    if (grapheme_clustering) {
      /* Check if we're on a grapheme cluster break */
      if (utf8proc_grapheme_break_stateful(last, wc,
                                           &term->vt.grapheme_state)) {
        term_reset_grapheme_state(term);
        goto out;
      }
    }
#endif

    int base_width = c32width(base);
    if (base_width > 0) {
      term->grid->cursor.point.col = col;
      term->grid->cursor.lcf = false;
      insert_mode_disable = true;

      if (composed == NULL) {
        bool base_from_primary;
        bool comb_from_primary;
        bool pre_from_primary;

        char32_t precomposed =
            term->fonts[0] != NULL
                ? fcft_precompose(term->fonts[0], base, wc, &base_from_primary,
                                  &comb_from_primary, &pre_from_primary)
                : (char32_t)-1;

        int precomposed_width = c32width(precomposed);

        /*
         * Only use the pre-composed character if:
         *
         *  1. we *have* a pre-composed character
         *  2. the width matches the base characters width
         *  3. it's in the primary font, OR one of the base or
         *     combining characters are *not* from the primary
         *     font
         */

        if (precomposed != (char32_t)-1 && precomposed_width == base_width &&
            (pre_from_primary || !base_from_primary || !comb_from_primary)) {
          wc = precomposed;
          width = precomposed_width;
          term_reset_grapheme_state(term);
          goto out;
        }
      }

      size_t wanted_count = composed != NULL ? composed->count + 1 : 2;
      if (wanted_count > 255) {
        xassert(composed != NULL);

#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
        LOG_WARN("combining character overflow:");
        LOG_WARN("  base: 0x%04x", composed->chars[0]);
        for (size_t i = 1; i < composed->count; i++)
          LOG_WARN("    cc: 0x%04x", composed->chars[i]);
        LOG_ERR("   new: 0x%04x", wc);
#endif
        /* This is going to break anyway... */
        wanted_count--;
      }

      xassert(wanted_count <= 255);

      /* Check if we already have a match for the entire compose chain */
      const struct composed *cc = composed_lookup_without_collision(
          term->composed, &key,
          composed != NULL ? composed->chars : &(char32_t){base},
          composed != NULL ? composed->count : 1, wc, 0);

      if (cc != NULL) {
        /* We *do* have a match! */
        wc = CELL_COMB_CHARS_LO + cc->key;
        width = cc->width;
        goto out;
      } else {
        /* No match - allocate a new chain below */
      }

      if (unlikely(term->composed_count >=
                   (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) {
        /* We reached our maximum number of allowed composed
         * character chains. Fall through here and print the
         * current zero-width character to the current cell */
        LOG_WARN("maximum number of composed characters reached");
        term_reset_grapheme_state(term);
        goto out;
      }

      /* Allocate new chain */
      struct composed *new_cc = xmalloc(sizeof(*new_cc));
      new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0]));
      new_cc->key = key;
      new_cc->count = wanted_count;
      new_cc->chars[0] = base;
      new_cc->chars[wanted_count - 1] = wc;
      new_cc->forced_width = composed != NULL ? composed->forced_width : 0;

      if (composed != NULL) {
        memcpy(&new_cc->chars[1], &composed->chars[1],
               (wanted_count - 2) * sizeof(new_cc->chars[0]));
      }

      const int grapheme_width =
          composed != NULL ? composed->width : base_width;

      switch (term->conf->tweak.grapheme_width_method) {
      case GRAPHEME_WIDTH_MAX:
        new_cc->width = max(grapheme_width, width);
        break;

      case GRAPHEME_WIDTH_DOUBLE:
        new_cc->width = min(grapheme_width + width, 2);

#if defined(FOOT_GRAPHEME_CLUSTERING)
        /* Handle VS-15 and VS-16 variation selectors */
        if (unlikely(grapheme_clustering && (wc == 0xfe0e || wc == 0xfe0f) &&
                     new_cc->count == 2)) {
          const struct emoji_vs *vs =
              bsearch(&(struct emoji_vs){.start = new_cc->chars[0]}, emoji_vs,
                      sizeof(emoji_vs) / sizeof(emoji_vs[0]),
                      sizeof(struct emoji_vs), &emoji_vs_compare);

          if (vs != NULL) {
            xassert(new_cc->chars[0] >= vs->start &&
                    new_cc->chars[0] <= vs->end);

            /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */
            if (wc == 0xfe0e) {
              if (vs->vs15)
                new_cc->width = 1;
            } else if (wc == 0xfe0f) {
              if (vs->vs16)
                new_cc->width = 2;
            }
          }
        }
#endif

        break;

      case GRAPHEME_WIDTH_WCSWIDTH:
        new_cc->width = grapheme_width + width;
        break;
      }

      term->composed_count++;
      composed_insert(&term->composed, new_cc);

      wc = CELL_COMB_CHARS_LO + new_cc->key;
      width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width;

      xassert(wc >= CELL_COMB_CHARS_LO);
      xassert(wc <= CELL_COMB_CHARS_HI);
      goto out;
    }
  } else
    term_reset_grapheme_state(term);

out:
  if (width > 0)
    term_print(term, wc, width, insert_mode_disable);
}

enum term_surface term_surface_kind(const struct terminal *term,
                                    const struct wl_surface *surface) {
  if (likely(surface == term->window->surface.surf))
    return TERM_SURF_GRID;
  else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf)
    return TERM_SURF_TITLE;
  else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf)
    return TERM_SURF_BORDER_LEFT;
  else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf)
    return TERM_SURF_BORDER_RIGHT;
  else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf)
    return TERM_SURF_BORDER_TOP;
  else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf)
    return TERM_SURF_BORDER_BOTTOM;
  else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf)
    return TERM_SURF_BUTTON_MINIMIZE;
  else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf)
    return TERM_SURF_BUTTON_MAXIMIZE;
  else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf)
    return TERM_SURF_BUTTON_CLOSE;
  else if (surface == term->window->tab_bar.surface.surf)
    return TERM_SURF_TAB_BAR;
  else if (surface == term->window->tab_overview.surface.surf)
    return TERM_SURF_TAB_OVERVIEW;
  else
    return TERM_SURF_NONE;
}

static bool rows_to_text(const struct terminal *term, int start, int end,
                         int col_start, int col_end, char **text, size_t *len) {
  struct extraction_context *ctx = extract_begin(SELECTION_NONE, true);
  if (ctx == NULL)
    return false;

  const int grid_rows = term->grid->num_rows;
  int r = start;

  while (true) {
    const struct row *row = term->grid->rows[r];
    xassert(row != NULL);

    const int c_end = r == end ? col_end : term->cols;

    for (int c = col_start; c < c_end; c++) {
      if (!extract_one(term, row, &row->cells[c], c, ctx))
        goto out;
    }

    if (r == end)
      break;

    r++;
    r &= grid_rows - 1;

    col_start = 0;
  }

out:
  return extract_finish(ctx, text, len);
}

bool term_scrollback_to_text(const struct terminal *term, char **text,
                             size_t *len) {
  const int grid_rows = term->grid->num_rows;
  int start = (term->grid->offset + term->rows) & (grid_rows - 1);
  int end = (term->grid->offset + term->rows - 1) & (grid_rows - 1);

  xassert(start >= 0);
  xassert(start < grid_rows);
  xassert(end >= 0);
  xassert(end < grid_rows);

  /* If scrollback isn't full yet, this may be NULL, so scan forward
   * until we find the first non-NULL row */
  while (term->grid->rows[start] == NULL) {
    start++;
    start &= grid_rows - 1;
  }

  while (term->grid->rows[end] == NULL) {
    end--;
    if (end < 0)
      end += term->grid->num_rows;
  }

  return rows_to_text(term, start, end, 0, term->cols, text, len);
}

bool term_view_to_text(const struct terminal *term, char **text, size_t *len) {
  int start = grid_row_absolute_in_view(term->grid, 0);
  int end = grid_row_absolute_in_view(term->grid, term->rows - 1);
  return rows_to_text(term, start, end, 0, term->cols, text, len);
}

bool term_command_output_to_text(const struct terminal *term, char **text,
                                 size_t *len) {
  int start_row = -1;
  int end_row = -1;
  int start_col = -1;
  int end_col = -1;

  const struct grid *grid = term->grid;
  const int sb_end = grid_row_absolute(grid, term->rows - 1);
  const int sb_start = (sb_end + 1) & (grid->num_rows - 1);
  int r = sb_end;

  while (start_row < 0) {
    const struct row *row = grid->rows[r];
    if (row == NULL)
      break;

    if (row->shell_integration.cmd_end >= 0) {
      end_row = r;
      end_col = row->shell_integration.cmd_end;
    }

    if (end_row >= 0 && row->shell_integration.cmd_start >= 0) {
      start_row = r;
      start_col = row->shell_integration.cmd_start;
    }

    if (r == sb_start)
      break;

    r = (r - 1 + grid->num_rows) & (grid->num_rows - 1);
  }

  if (start_row < 0)
    return false;

  bool ret =
      rows_to_text(term, start_row, end_row, start_col, end_col, text, len);
  if (!ret)
    return false;

  /*
   * If the FTCS_COMMAND_FINISHED marker was emitted at the *first*
   * column, then the *entire* previous line is part of the command
   * output. *Including* the newline, if any.
   *
   * Since rows_to_text() doesn't extract the column
   * FTCS_COMMAND_FINISHED was emitted at (that would be wrong -
   * FTCS_COMMAND_FINISHED is emitted *after* the command output,
   * not at its last character), the extraction logic will not see
   * the last newline (this is true for all non-line-wise selection
   * types), and the extracted text will *not* end with a newline.
   *
   * Here we try to compensate for that. Note that if 'end_col' is
   * not 0, then the command output only covers a partial row, and
   * thus we do *not* want to append a newline.
   */

  if (end_col > 0) {
    /* Command output covers partial row - don't append newline */
    return true;
  }

  int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1);
  const struct row *row = grid->rows[next_to_last_row];

  /* Add newline if last row has a hard linebreak */
  if (row->linebreak) {
    char *new_text = xrealloc(*text, *len + 1 + 1);

    if (new_text == NULL) {
      /* Ignore failure - use text as is (without inserting newline) */
      return true;
    }

    *text = new_text;
    (*len)++;
    (*text)[*len - 1] = '\n';
    (*text)[*len] = '\0';
  }

  return true;
}

bool term_ime_is_enabled(const struct terminal *term) {
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  return term->ime_enabled;
#else
  return false;
#endif
}

void term_ime_enable(struct terminal *term) {
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  if (term->ime_enabled)
    return;

  LOG_DBG("IME enabled");

  term->ime_enabled = true;

  /* IME is per seat - enable on all seat currently focusing us */
  tll_foreach(term->wl->seats, it) {
    if (it->item.kbd_focus == term)
      ime_enable(&it->item);
  }
#endif
}

void term_ime_disable(struct terminal *term) {
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  if (!term->ime_enabled)
    return;

  LOG_DBG("IME disabled");

  term->ime_enabled = false;

  /* IME is per seat - disable on all seat currently focusing us */
  tll_foreach(term->wl->seats, it) {
    if (it->item.kbd_focus == term)
      ime_disable(&it->item);
  }
#endif
}

bool term_ime_reset(struct terminal *term) {
  bool at_least_one_seat_was_reset = false;

#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  tll_foreach(term->wl->seats, it) {
    struct seat *seat = &it->item;

    if (seat->kbd_focus != term)
      continue;

    ime_reset_preedit(seat);
    at_least_one_seat_was_reset = true;
  }
#endif

  return at_least_one_seat_was_reset;
}

void term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width,
                              int height) {
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  tll_foreach(term->wl->seats, it) {
    if (it->item.kbd_focus == term) {
      it->item.ime.cursor_rect.pending.x = x;
      it->item.ime.cursor_rect.pending.y = y;
      it->item.ime.cursor_rect.pending.width = width;
      it->item.ime.cursor_rect.pending.height = height;
    }
  }
#endif
}

void term_osc8_open(struct terminal *term, uint64_t id, const char *uri) {
  term_osc8_close(term);
  xassert(term->vt.osc8.uri == NULL);

  term->vt.osc8.id = id;
  term->vt.osc8.uri = xstrdup(uri);

  term->bits_affecting_ascii_printer.osc8 = true;
  term_update_ascii_printer(term);
}

void term_osc8_close(struct terminal *term) {
  free(term->vt.osc8.uri);
  term->vt.osc8.uri = NULL;
  term->vt.osc8.id = 0;
  term->bits_affecting_ascii_printer.osc8 = false;
  term_update_ascii_printer(term);
}

void term_set_user_mouse_cursor(struct terminal *term, const char *cursor) {
  free(term->mouse_user_cursor);
  term->mouse_user_cursor =
      cursor != NULL && strlen(cursor) > 0 ? xstrdup(cursor) : NULL;
  term_xcursor_update(term);
}

void term_enable_size_notifications(struct terminal *term) {
  /* Note: always send current size upon activation, regardless of
     previous state */
  term->size_notifications = true;
  term_send_size_notification(term);
}

void term_disable_size_notifications(struct terminal *term) {
  if (!term->size_notifications)
    return;

  term->size_notifications = false;
}

void term_send_size_notification(struct terminal *term) {
  if (!term->size_notifications)
    return;

  const int height = term->height - term->margins.top - term->margins.bottom;
  const int width = term->width - term->margins.left - term->margins.right;

  char buf[128];
  const size_t n = xsnprintf(buf, sizeof(buf), "\033[48;%d;%d;%d;%dt",
                             term->rows, term->cols, height, width);
  term_to_slave(term, buf, n);
}

void term_theme_switch_to_dark(struct terminal *term) {
  if (term->colors.active_theme == COLOR_THEME_DARK)
    return;

  term_theme_apply(term, &term->conf->colors_dark);
  term->colors.active_theme = COLOR_THEME_DARK;

  wayl_win_alpha_changed(term->window);
  term_font_subpixel_changed(term);

  if (term->report_theme_changes)
    term_to_slave(term, "\033[?997;1n", 9);

  term_damage_view(term);
  term_damage_margins(term);
  render_refresh(term);
}

void term_theme_switch_to_light(struct terminal *term) {
  if (term->colors.active_theme == COLOR_THEME_LIGHT)
    return;

  term_theme_apply(term, &term->conf->colors_light);
  term->colors.active_theme = COLOR_THEME_LIGHT;

  wayl_win_alpha_changed(term->window);
  term_font_subpixel_changed(term);

  if (term->report_theme_changes)
    term_to_slave(term, "\033[?997;2n", 9);

  term_damage_view(term);
  term_damage_margins(term);
  render_refresh(term);
}

void term_theme_toggle(struct terminal *term) {
  if (term->colors.active_theme == COLOR_THEME_DARK) {
    term_theme_apply(term, &term->conf->colors_light);
    term->colors.active_theme = COLOR_THEME_LIGHT;

    if (term->report_theme_changes)
      term_to_slave(term, "\033[?997;2n", 9);
  } else {
    term_theme_apply(term, &term->conf->colors_dark);
    term->colors.active_theme = COLOR_THEME_DARK;

    if (term->report_theme_changes)
      term_to_slave(term, "\033[?997;1n", 9);
  }

  wayl_win_alpha_changed(term->window);
  term_font_subpixel_changed(term);

  term_damage_view(term);
  term_damage_margins(term);
  render_refresh(term);
}

const struct color_theme *term_theme_get(const struct terminal *term) {
  return term->colors.active_theme == COLOR_THEME_DARK
             ? &term->conf->colors_dark
             : &term->conf->colors_light;
}
