/*
 * screen.c
 * Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the GPL3 license.
 */

#define EXTRA_INIT { \
    PyModule_AddIntMacro(module, SCROLL_LINE); PyModule_AddIntMacro(module, SCROLL_PAGE); PyModule_AddIntMacro(module, SCROLL_FULL); \
    PyModule_AddIntMacro(module, EXTEND_CELL); PyModule_AddIntMacro(module, EXTEND_WORD); PyModule_AddIntMacro(module, EXTEND_LINE); \
    PyModule_AddIntMacro(module, SCALE_BITS); PyModule_AddIntMacro(module, WIDTH_BITS); PyModule_AddIntMacro(module, SUBSCALE_BITS); \
    if (PyModule_AddFunctions(module, module_methods) != 0) return false; \
}

#include "data-types.h"
#include "control-codes.h"
#include "screen.h"
#include "dnd.h"
#include "state.h"
#include "iqsort.h"
#include "fonts.h"
#include "charsets.h"
#include "lineops.h"
#include "hyperlink.h"
#include <structmember.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "unicode-data.h"
#include "modes.h"
#include "char-props.h"
#include "wcswidth.h"
#include <stdalign.h>
#include <stdio.h>
#include "keys.h"
#include "vt-parser.h"
#include "resize.h"

static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true};

#define CSI_REP_MAX_REPETITIONS 65535u

// Constructor/destructor {{{

static void
clear_selection(Selections *selections) {
    selections->in_progress = false;
    selections->extension_in_progress = false;
    selections->extend_mode = EXTEND_CELL;
    selections->count = 0;
}

static void
clear_all_selections(Screen *self) { clear_selection(&self->selections); clear_selection(&self->url_ranges); }


static void
init_tabstops(bool *tabstops, index_type count) {
    // In terminfo we specify the number of initial tabstops (it) as 8
    for (unsigned int t=0; t < count; t++) {
        tabstops[t] = t % 8 == 0 ? true : false;
    }
}

static bool
init_overlay_line(Screen *self, index_type columns, bool keep_active) {
    PyMem_Free(self->overlay_line.cpu_cells);
    PyMem_Free(self->overlay_line.gpu_cells);
    PyMem_Free(self->overlay_line.original_line.cpu_cells);
    PyMem_Free(self->overlay_line.original_line.gpu_cells);
    self->overlay_line.cpu_cells = PyMem_Calloc(columns, sizeof(CPUCell));
    self->overlay_line.gpu_cells = PyMem_Calloc(columns, sizeof(GPUCell));
    self->overlay_line.original_line.cpu_cells = PyMem_Calloc(columns, sizeof(CPUCell));
    self->overlay_line.original_line.gpu_cells = PyMem_Calloc(columns, sizeof(GPUCell));
    if (!self->overlay_line.cpu_cells || !self->overlay_line.gpu_cells ||
        !self->overlay_line.original_line.cpu_cells || !self->overlay_line.original_line.gpu_cells) {
        PyErr_NoMemory(); return false;
    }
    if (!keep_active) {
        self->overlay_line.is_active = false;
        self->overlay_line.xnum = 0;
    }
    self->overlay_line.is_dirty = true;
    self->overlay_line.ynum = 0;
    self->overlay_line.xstart = 0;
    self->overlay_line.cursor_x = 0;
    self->overlay_line.last_ime_pos.x = 0;
    self->overlay_line.last_ime_pos.y = 0;

    return true;
}

static void deactivate_overlay_line(Screen *self);
static void update_overlay_position(Screen *self);
static void render_overlay_line(Screen *self, Line *line, FONTS_DATA_HANDLE fonts_data);
static void update_overlay_line_data(Screen *self, uint8_t *data);

#define CALLBACK(...) \
    if (self->callbacks != Py_None) { \
        PyObject *callback_ret = PyObject_CallMethod(self->callbacks, __VA_ARGS__); \
        if (callback_ret == NULL) PyErr_Print(); else Py_DECREF(callback_ret); \
    }

static PyObject*
new_screen_object(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
    Screen *self;
    int ret = 0;
    PyObject *callbacks = Py_None, *test_child = Py_None;
    unsigned int columns=80, lines=24, scrollback=0, cell_width=10, cell_height=20;
    id_type window_id=0;
    if (!PyArg_ParseTuple(args, "|OIIIIIKO", &callbacks, &lines, &columns, &scrollback, &cell_width, &cell_height, &window_id, &test_child)) return NULL;

    self = (Screen *)type->tp_alloc(type, 0);
    if (self != NULL) {
        if ((ret = pthread_mutex_init(&self->write_buf_lock, NULL)) != 0) {
            Py_CLEAR(self); PyErr_Format(PyExc_RuntimeError, "Failed to create Screen write_buf_lock mutex: %s", strerror(ret));
            return NULL;
        }
        self->vt_parser = alloc_vt_parser(window_id);
        if (self->vt_parser == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); }
        self->text_cache = tc_alloc(); if (!self->text_cache) { Py_CLEAR(self); return PyErr_NoMemory(); }
        self->reload_all_gpu_data = true;
        self->cell_size.width = cell_width; self->cell_size.height = cell_height;
        self->columns = columns; self->lines = lines;
        self->write_buf_sz = BUFSIZ;
        self->write_buf = PyMem_RawMalloc(self->write_buf_sz);
        if (self->write_buf == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); }
        self->window_id = window_id;
        self->modes = empty_modes;
        self->saved_modes = empty_modes;
        self->is_dirty = true;
        self->scroll_changed = false;
        self->margin_top = 0; self->margin_bottom = self->lines - 1;
        self->history_line_added_count = 0;
        reset_vt_parser(self->vt_parser);
        self->callbacks = callbacks; Py_INCREF(callbacks);
        self->test_child = test_child; Py_INCREF(test_child);
        self->cursor = alloc_cursor();
        self->color_profile = alloc_color_profile();
        self->main_linebuf = alloc_linebuf(lines, columns, self->text_cache); self->alt_linebuf = alloc_linebuf(lines, columns, self->text_cache);
        self->linebuf = self->main_linebuf;
        self->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size), self->text_cache);
        self->main_grman = grman_alloc(false);
        self->alt_grman = grman_alloc(false);
        self->active_hyperlink_id = 0;

        self->grman = self->main_grman;
        self->disable_ligatures = OPT(disable_ligatures);
        self->main_tabstops = PyMem_Calloc(2 * self->columns, sizeof(bool));
        self->lc = alloc_list_of_chars();
        if (
            self->cursor == NULL || self->main_linebuf == NULL || self->alt_linebuf == NULL ||
            self->main_tabstops == NULL || self->historybuf == NULL || self->main_grman == NULL ||
            self->alt_grman == NULL || self->color_profile == NULL || self->lc == NULL
        ) {
            Py_CLEAR(self); return NULL;
        }
        grman_set_window_id(self->main_grman, self->window_id);
        grman_set_window_id(self->alt_grman, self->window_id);
        self->alt_tabstops = self->main_tabstops + self->columns;
        self->tabstops = self->main_tabstops;
        init_tabstops(self->main_tabstops, self->columns);
        init_tabstops(self->alt_tabstops, self->columns);
        self->key_encoding_flags = self->main_key_encoding_flags;
        if (!init_overlay_line(self, self->columns, false)) { Py_CLEAR(self); return NULL; }
        self->hyperlink_pool = alloc_hyperlink_pool();
        if (!self->hyperlink_pool) { Py_CLEAR(self); return PyErr_NoMemory(); }
        self->as_ansi_buf.hyperlink_pool = self->hyperlink_pool;
    }
    return (PyObject*) self;
}

static Line* range_line_(Screen *self, int y);

static void
do_screen_reset(Screen *self, bool is_hard_reset) {
    screen_pause_rendering(self, false, 0);
    self->dnd_chunking.active = false;
    self->extra_cursors.count = 0; zero_at_ptr(&self->extra_cursors.color); self->extra_cursors.dirty = true;
    self->main_pointer_shape_stack.count = 0; self->alternate_pointer_shape_stack.count = 0;
    if (is_hard_reset && self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true);
    if (screen_is_overlay_active(self)) {
        deactivate_overlay_line(self);
        // Cancel IME composition
        update_ime_position_for_window(self->window_id, false, -1);
    }
    Py_CLEAR(self->last_reported_cwd);
    self->cursor_render_info.render_even_when_unfocused = false;
    memset(self->main_key_encoding_flags, 0, sizeof(self->main_key_encoding_flags));
    memset(self->alt_key_encoding_flags, 0, sizeof(self->alt_key_encoding_flags));
    self->display_window_char = 0;
    self->progress_state = PROGRESS_STATE_UNSET;
    self->progress_percent = 0;
    self->progress_indeterminate_anim_at = 0;
    self->prompt_settings.val = 0;
    self->last_graphic_char = 0;
    self->main_savepoint.is_valid = false;
    self->alt_savepoint.is_valid = false;
    if (is_hard_reset) {
        linebuf_clear(self->linebuf, BLANK_CHAR);
        historybuf_clear(self->historybuf);
        clear_hyperlink_pool(self->hyperlink_pool);
        grman_clear(self->main_grman, false, self->cell_size);  // dont delete images in scrollback
        grman_clear(self->alt_grman, true, self->cell_size);
    }
    self->modes = empty_modes;
    self->saved_modes = empty_modes;
    self->active_hyperlink_id = 0;
    zero_at_ptr(&self->color_profile->overridden);
    reset_vt_parser(self->vt_parser);
    zero_at_ptr(&self->charset);
    self->margin_top = 0; self->margin_bottom = self->lines - 1;
    screen_normal_keypad_mode(self);
    init_tabstops(self->main_tabstops, self->columns);
    init_tabstops(self->alt_tabstops, self->columns);
    cursor_reset(self->cursor);
    self->is_dirty = true;
    clear_all_selections(self);
    screen_cursor_position(self, 1, 1);
    set_dynamic_color(self, 111, NULL);  // does default_bg_changed processing
    colorprofile_reset(self->color_profile);
    CALLBACK("on_reset", "O", is_hard_reset ? Py_True : Py_False);
}

void
screen_reset(Screen *self) { do_screen_reset(self, true); }

void
screen_soft_reset(Screen *self) {
    index_type x = self->cursor->x, y = self->cursor->y;
    do_screen_reset(self, false);
    self->cursor->x = x; self->cursor->y = y;
}

void
screen_dirty_sprite_positions(Screen *self) {
    self->is_dirty = true;
    for (index_type i = 0; i < self->lines; i++) {
        linebuf_mark_line_dirty(self->main_linebuf, i);
        linebuf_mark_line_dirty(self->alt_linebuf, i);
    }
    for (index_type i = 0; i < self->historybuf->count; i++) historybuf_mark_line_dirty(self->historybuf, i);
}

typedef struct CursorTrack {
    index_type num_content_lines;
    bool is_beyond_content;
    struct { index_type x, y; } before;
    struct { index_type x, y; } after;
    struct { index_type x, y; } temp;
} CursorTrack;

static bool
rewrap(Screen *screen, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, CursorTrack *cursor, CursorTrack *main_saved_cursor, CursorTrack *alt_saved_cursor, bool main_is_active) {
    TrackCursor cursors[3];
    cursors[2].is_sentinel = true;
    cursors[0] = (TrackCursor){.x=main_saved_cursor->before.x, .y=main_saved_cursor->before.y};
    if (main_is_active) cursors[1] = (TrackCursor){.x=cursor->before.x, .y=cursor->before.y};
    else cursors[1].is_sentinel = true;
    ResizeResult mr = resize_screen_buffers(screen->main_linebuf, screen->historybuf, lines, columns, &screen->as_ansi_buf, cursors);
    if (!mr.ok) { PyErr_NoMemory(); return false; }
    main_saved_cursor->temp.x = cursors[0].dest_x; main_saved_cursor->temp.y = cursors[0].dest_y;
    if (main_is_active) { cursor->temp.x = cursors[1].dest_x; cursor->temp.y = cursors[1].dest_y; }

    cursors[0] = (TrackCursor){.x=alt_saved_cursor->before.x, .y=alt_saved_cursor->before.y};
    if (!main_is_active) cursors[1] = (TrackCursor){.x=cursor->before.x, .y=cursor->before.y};
    else cursors[1].is_sentinel = true;
    ResizeResult ar = resize_screen_buffer_without_rewrap(screen->alt_linebuf, lines, columns, cursors);
    if (!ar.ok) {
        Py_DecRef((PyObject*)ar.lb); PyErr_NoMemory(); return false;
    }
    alt_saved_cursor->temp.x = cursors[0].dest_x; alt_saved_cursor->temp.y = cursors[0].dest_y;
    if (!main_is_active) { cursor->temp.x = cursors[1].dest_x; cursor->temp.y = cursors[1].dest_y; }
    Py_CLEAR(screen->main_linebuf); Py_CLEAR(screen->alt_linebuf); Py_CLEAR(screen->historybuf);
    screen->main_linebuf = mr.lb; screen->historybuf = mr.hb; screen->alt_linebuf = ar.lb;
    screen->linebuf = main_is_active ? screen->main_linebuf : screen->alt_linebuf;
    if (main_is_active) {
        *nclb = mr.num_content_lines_before; *ncla = mr.num_content_lines_after;
    } else {
        *nclb = ar.num_content_lines_before; *ncla = ar.num_content_lines_after;
    }
    return true;
}

static bool
is_selection_empty(const Selection *s) {
    int start_y = (int)s->start.y - (int)s->start_scrolled_by, end_y = (int)s->end.y - (int)s->end_scrolled_by;
    return s->start.x == s->end.x && s->start.in_left_half_of_cell == s->end.in_left_half_of_cell && start_y == end_y;
}

static bool
selection_intersects_screen_lines(const Selections *selections, int a, int b) {
    if (a > b) SWAP(a, b);
    for (size_t i = 0; i < selections->count; i++) {
        const Selection *s = selections->items + i;
        if (!is_selection_empty(s)) {
            int start = (int)s->start.y - s->start_scrolled_by;
            int end = (int)s->end.y - s->end_scrolled_by;
            int top = MIN(start, end);
            int bottom = MAX(start, end);
            if ((top <= a && bottom >= a) || (top >= a && top <= b)) return true;
        }
    }
    return false;
}


static void
index_selection(const Screen *self, Selections *selections, bool up, index_type top, index_type bottom) {
    const bool needs_special_handling = self->linebuf == self->alt_linebuf && (top > 0 || bottom < self->lines - 1);
    for (size_t i = 0; i < selections->count; i++) {
        Selection *s = selections->items + i;
        if (needs_special_handling) {
            if (is_selection_empty(s)) continue;
            int start = (int)s->start.y - s->start_scrolled_by;
            int end = (int)s->end.y - s->end_scrolled_by;
            int stop = MIN(start, end);
            int sbottom = MAX(start, end);
            if (stop < (int)top) {
                if (sbottom < (int)top) continue;
                clear_selection(selections); return;
            } else {
                if (stop > (int)bottom) continue;
                if (sbottom > (int)bottom) { clear_selection(selections); return; }
            }
        }
        if (up) {
            if (s->start.y == 0) s->start_scrolled_by += 1;
            else {
                s->start.y--;
                if (s->input_start.y) s->input_start.y--;
                if (s->input_current.y) s->input_current.y--;
                if (s->initial_extent.start.y) s->initial_extent.start.y--;
                if (s->initial_extent.end.y) s->initial_extent.end.y--;
            }
            if (s->end.y == 0) s->end_scrolled_by += 1;
            else s->end.y--;
        } else {
            if (s->start.y >= self->lines - 1) s->start_scrolled_by -= 1;
            else {
                s->start.y++;
                if (s->input_start.y < self->lines - 1) s->input_start.y++;
                if (s->input_current.y < self->lines - 1) s->input_current.y++;
            }
            if (s->end.y >= self->lines - 1) s->end_scrolled_by -= 1;
            else s->end.y++;
        }
    }
}


#define INDEX_GRAPHICS(amtv) { \
    bool is_main = self->linebuf == self->main_linebuf; \
    static ScrollData s; \
    s.amt = amtv; s.limit = is_main ? -self->historybuf->ynum : 0; \
    s.has_margins = self->margin_top != 0 || self->margin_bottom != self->lines - 1; \
    s.margin_top = top; s.margin_bottom = bottom; \
    grman_scroll_images(self->grman, &s, self->cell_size); \
}


#define INDEX_DOWN \
    linebuf_reverse_index(self->linebuf, top, bottom); \
    linebuf_clear_line(self->linebuf, top, true); \
    if (self->linebuf == self->main_linebuf && self->last_visited_prompt.is_set) { \
        if (self->last_visited_prompt.scrolled_by > 0) self->last_visited_prompt.scrolled_by--; \
        else if(self->last_visited_prompt.y < self->lines - 1) self->last_visited_prompt.y++; \
        else self->last_visited_prompt.is_set = false; \
    } \
    INDEX_GRAPHICS(1) \
    self->is_dirty = true; \
    index_selection(self, &self->selections, false, top, bottom); \
    clear_selection(&self->url_ranges);


static void
nuke_in_line(CPUCell *cp, GPUCell *gp, index_type start, index_type x_limit, char_type ch) {
    for (index_type x = start; x < x_limit; x++) {
        cell_set_char(cp + x, ch); cp[x].is_multicell = false;
        clear_sprite_position(gp[x]);
    }
}

static void
nuke_multicell_char_at(Screen *self, index_type x_, index_type y_, bool replace_with_spaces) {
    CPUCell *cp; GPUCell *gp;
    linebuf_init_cells(self->linebuf, y_, &cp, &gp);
    index_type num_lines_above = cp[x_].y;
    index_type y_max_limit = MIN(self->lines, y_ + cp[x_].scale - num_lines_above);
    while (cp[x_].x && x_ > 0) x_--;
    index_type x_limit = MIN(self->columns, x_ + mcd_x_limit(&cp[x_]));
    char_type ch = replace_with_spaces ? ' ' : 0;
    for (index_type y = y_; y < y_max_limit; y++) {
        linebuf_init_cells(self->linebuf, y, &cp, &gp);
        nuke_in_line(cp, gp, x_, x_limit, ch); linebuf_mark_line_dirty(self->linebuf, y);
    }
    int y_min_limit = -1;
    if (self->linebuf == self->main_linebuf) y_min_limit = -(self->historybuf->count + 1);
    for (int y = (int)y_ - 1; y > y_min_limit && num_lines_above; y--, num_lines_above--) {
        Line *line = range_line_(self, y); cp = line->cpu_cells; gp = line->gpu_cells;
        nuke_in_line(cp, gp, x_, x_limit, ch);
        if (y > -1) linebuf_mark_line_dirty(self->linebuf, y);
        else historybuf_mark_line_dirty(self->historybuf, -(y + 1));
    }
    self->is_dirty = true;
}

static void
nuke_multiline_char_intersecting_with(Screen *self, index_type x_start, index_type x_limit, index_type y_start, index_type y_limit, bool replace_with_spaces) {
    for (index_type y = y_start; y < y_limit; y++) {
        CPUCell *cp; GPUCell *gp;
        linebuf_init_cells(self->linebuf, y, &cp, &gp);
        for (index_type x = x_start; x < x_limit; x++) {
            if (cp[x].is_multicell && cp[x].scale > 1) nuke_multicell_char_at(self, x, y, replace_with_spaces);
        }
    }
}

static void
nuke_multicell_char_intersecting_with(Screen *self, index_type x_start, index_type x_limit, index_type y_start, index_type y_limit, bool replace_with_spaces) {
    for (index_type y = y_start; y < y_limit; y++) {
        CPUCell *cp; GPUCell *gp;
        linebuf_init_cells(self->linebuf, y, &cp, &gp);
        for (index_type x = x_start; x < x_limit; x++) {
            if (cp[x].is_multicell) nuke_multicell_char_at(self, x, y, replace_with_spaces);
        }
    }
}


static void
nuke_split_multicell_char_at_left_boundary(Screen *self, index_type x, index_type y, bool replace_with_spaces) {
    CPUCell *cp = linebuf_cpu_cells_for_line(self->linebuf, y);
    if (cp[x].is_multicell && cp[x].x) {
        nuke_multicell_char_at(self, x, y, replace_with_spaces);  // remove split multicell char at left edge
    }
}

static void
nuke_split_multicell_char_at_right_boundary(Screen *self, index_type x, index_type y, bool replace_with_spaces) {
    CPUCell *cp = linebuf_cpu_cells_for_line(self->linebuf, y);
    CPUCell *c = cp + x;
    if (c->is_multicell) {
        unsigned max_x = mcd_x_limit(c) - 1;
        if (c->x < max_x) {
            nuke_multicell_char_at(self, x, y, replace_with_spaces);
        }
    }
}

static void
nuke_incomplete_single_line_multicell_chars_in_range(
    Screen *self, index_type start, index_type limit, index_type y, bool replace_with_spaces
) {
    CPUCell *cpu_cells; GPUCell *gpu_cells;
    linebuf_init_cells(self->linebuf, y, &cpu_cells, &gpu_cells);
    for (index_type x = start; x < limit; x++) {
        if (cpu_cells[x].is_multicell) {
            index_type mcd_x_limit = x + cpu_cells[x].width - cpu_cells[x].x;
            if (cpu_cells[x].x || mcd_x_limit > limit) nuke_in_line(cpu_cells, gpu_cells, x, MIN(mcd_x_limit, limit), replace_with_spaces ? ' ': 0);
            x = mcd_x_limit - 1;
        }
    }
}


static index_type
prevent_current_prompt_from_rewrapping(Screen *self, LineBuf *prompt_copy, index_type *num_of_prompt_lines_above_cursor) {
    index_type num_of_prompt_lines = 0; *num_of_prompt_lines_above_cursor = 0;
    if (!self->prompt_settings.redraws_prompts_at_all) return num_of_prompt_lines;
    int y = self->cursor->y;
    while (y >= 0) {
        linebuf_init_line(self->main_linebuf, y);
        Line *line = self->linebuf->line;
        switch (line->attrs.prompt_kind) {
            case UNKNOWN_PROMPT_KIND:
                break;
            case PROMPT_START:
            case SECONDARY_PROMPT:
                goto found;
                break;
            case OUTPUT_START:
                return num_of_prompt_lines;
        }
        y--;
    }
found:
    if (y < 0) return num_of_prompt_lines;
    // we have identified a prompt at which the cursor is present, the shell
    // will redraw this prompt. However when doing so it gets confused if the
    // cursor vertical position relative to the first prompt line changes. This
    // can easily be seen for instance in zsh when a right side prompt is used
    // so when resizing, simply blank all lines after the current
    // prompt and trust the shell to redraw them.
    LineBuf *orig = self->linebuf; self->linebuf = self->main_linebuf;
    // technically only need to nuke partial multichar cells but since we dont
    // know what the shell will do in terms of clearing, best to be safe and
    // nuke all
    nuke_multiline_char_intersecting_with(self, 0, self->columns, y, self->main_linebuf->ynum, true);
    self->linebuf = orig;
    for (; y < (int)self->main_linebuf->ynum; y++) {
        linebuf_init_line(self->main_linebuf, y);
        linebuf_copy_line_to(prompt_copy, self->main_linebuf->line, num_of_prompt_lines++);
        linebuf_clear_line(self->main_linebuf, y, false);
        if (y <= (int)self->cursor->y) {
            linebuf_init_line(self->main_linebuf, y);
            // this is needed because screen_resize() checks to see if the cursor is beyond the content,
            // so insert some fake content
            cell_set_char(self->main_linebuf->line->cpu_cells, ' ');
            if (y < (int)self->cursor->y) (*num_of_prompt_lines_above_cursor)++;
        }
    }
    return num_of_prompt_lines;
}

static bool
linebuf_is_line_continued(LineBuf *linebuf, index_type y) {
    return y ? linebuf_line_ends_with_continuation(linebuf, y - 1) : false;
}

static bool
preserve_blank_output_start_line(Cursor *cursor, LineBuf *linebuf) {
    if (cursor->x == 0 && cursor->y < linebuf->ynum && !linebuf_is_line_continued(linebuf, cursor->y)) {
        linebuf_init_line(linebuf, cursor->y);
        if (!cell_has_text(linebuf->line->cpu_cells)) {
            // we have a blank output start line, we need it to be preserved by
            // reflow, so insert a dummy char
            cell_set_char(linebuf->line->cpu_cells + cursor->x++, '<');
            return true;
        }
    }
    return false;
}

static void
remove_blank_output_line_reservation_marker(Cursor *cursor, LineBuf *linebuf) {
    if (cursor->y < linebuf->ynum) {
        linebuf_init_line(linebuf, cursor->y);
        cell_set_char(linebuf->line->cpu_cells, 0);
        cursor->x = 0;
    }
}

static bool
screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
    screen_pause_rendering(self, false, 0);
    lines = MAX(1u, lines); columns = MAX(1u, columns);

    bool is_main = self->linebuf == self->main_linebuf;
    index_type num_content_lines_before, num_content_lines_after;
    bool main_has_blank_line = false, alt_has_blank_line = false;
    if (is_main) {
        main_has_blank_line = preserve_blank_output_start_line(self->cursor, self->linebuf);
        if (self->alt_savepoint.is_valid) alt_has_blank_line = preserve_blank_output_start_line(&self->alt_savepoint.cursor, self->alt_linebuf);
    } else {
        if (self->main_savepoint.is_valid) main_has_blank_line = preserve_blank_output_start_line(&self->main_savepoint.cursor, self->main_linebuf);
        alt_has_blank_line = preserve_blank_output_start_line(self->cursor, self->linebuf);
    }
    unsigned int lines_after_cursor_before_resize = self->lines - self->cursor->y;
    CursorTrack cursor = {.before = {self->cursor->x, self->cursor->y}};
    CursorTrack main_saved_cursor = {.before = {self->main_savepoint.cursor.x, self->main_savepoint.cursor.y}};
    CursorTrack alt_saved_cursor = {.before = {self->alt_savepoint.cursor.x, self->alt_savepoint.cursor.y}};
#define setup_cursor(which) { \
    which.after.x = which.temp.x; which.after.y = which.temp.y; \
    which.is_beyond_content = num_content_lines_before > 0 && self->cursor->y >= num_content_lines_before; \
    which.num_content_lines = num_content_lines_after; \
}
    // Resize overlay and blank lines
    if (!init_overlay_line(self, columns, true)) return false;

    // Resize main linebuf
    RAII_PyObject(prompt_copy, NULL);
    index_type num_of_prompt_lines = 0, num_of_prompt_lines_above_cursor = 0;
    if (is_main) {
        prompt_copy = (PyObject*)alloc_linebuf(self->lines, self->columns, self->text_cache);
        num_of_prompt_lines = prevent_current_prompt_from_rewrapping(self, (LineBuf*)prompt_copy, &num_of_prompt_lines_above_cursor);
    }
    if (!rewrap(self, lines, columns, &num_content_lines_before, &num_content_lines_after, &cursor, &main_saved_cursor, &alt_saved_cursor, is_main)) return false;
    setup_cursor(cursor);
    /* printf("old_cursor: (%u, %u) new_cursor: (%u, %u) beyond_content: %d\n", self->cursor->x, self->cursor->y, cursor.after.x, cursor.after.y, cursor.is_beyond_content); */
    setup_cursor(main_saved_cursor);
    grman_remove_all_cell_images(self->main_grman);
    grman_resize(self->main_grman, self->lines, lines, self->columns, columns, num_content_lines_before, num_content_lines_after);
    setup_cursor(alt_saved_cursor);
    grman_remove_all_cell_images(self->alt_grman);
    grman_resize(self->alt_grman, self->lines, lines, self->columns, columns, num_content_lines_before, num_content_lines_after);
#undef setup_cursor
    /* printf("\nold_size: (%u, %u) new_size: (%u, %u)\n", self->columns, self->lines, columns, lines); */
    index_type old_columns_for_tabs = self->columns;
    self->lines = lines; self->columns = columns;
    self->margin_top = 0; self->margin_bottom = self->lines - 1;

    bool *old_tabstops = self->main_tabstops;
    bool *new_tabstops = PyMem_Calloc(2 * self->columns, sizeof(bool));
    if (new_tabstops == NULL) { PyErr_NoMemory(); return false; }
    bool *new_main = new_tabstops;
    bool *new_alt = new_tabstops + self->columns;
    init_tabstops(new_main, self->columns);
    init_tabstops(new_alt, self->columns);
    if (old_tabstops && old_columns_for_tabs) {
        index_type to_copy = MIN(old_columns_for_tabs, self->columns);
        memcpy(new_main, old_tabstops, to_copy * sizeof(bool));
        memcpy(new_alt, old_tabstops + old_columns_for_tabs, to_copy * sizeof(bool));
    }
    PyMem_Free(old_tabstops);
    self->main_tabstops = new_main;
    self->alt_tabstops = new_alt;
    self->tabstops = is_main ? self->main_tabstops : self->alt_tabstops;
    self->is_dirty = true;
    clear_all_selections(self);
    self->last_visited_prompt.is_set = false;
#define S(c, w) c->x = MIN(w.after.x, self->columns - 1); c->y = MIN(w.after.y, self->lines - 1);
    S(self->cursor, cursor);
    S((&(self->main_savepoint.cursor)), main_saved_cursor);
    S((&(self->alt_savepoint.cursor)), alt_saved_cursor);
#undef S
    if (cursor.is_beyond_content) {
        self->cursor->y = cursor.num_content_lines;
        if (self->cursor->y >= self->lines) { self->cursor->y = self->lines - 1; screen_index(self); }
    }
    if (is_main && OPT(scrollback_fill_enlarged_window)) {
        const unsigned int top = 0, bottom = self->lines-1;
        Savepoint *sp = is_main ? &self->main_savepoint : &self->alt_savepoint;
        while (self->cursor->y + 1 < self->lines && self->lines - self->cursor->y > lines_after_cursor_before_resize) {
            if (!historybuf_pop_line(self->historybuf, self->alt_linebuf->line)) break;
            INDEX_DOWN;
            linebuf_copy_line_to(self->main_linebuf, self->alt_linebuf->line, 0);
            self->cursor->y++;
            sp->cursor.y = MIN(sp->cursor.y + 1, self->lines - 1);
        }
    }
    if (main_has_blank_line) remove_blank_output_line_reservation_marker(is_main ? self->cursor : &self->main_savepoint.cursor, self->main_linebuf);
    if (alt_has_blank_line) remove_blank_output_line_reservation_marker(is_main ? &self->alt_savepoint.cursor : self->cursor, self->alt_linebuf);
    if (num_of_prompt_lines) {
        // Copy the old prompt lines without any reflow this prevents
        // flickering of prompt during resize. The flicker is caused by the
        // prompt being first cleared by kitty then sometime later redrawn by
        // the shell.
        LineBuf *src = (LineBuf*)prompt_copy;
        for (index_type
                src_line = 0,
                y = num_of_prompt_lines_above_cursor <= self->cursor->y ? self->cursor->y - num_of_prompt_lines_above_cursor : 0;

                src_line < num_of_prompt_lines && y < self->lines;

                y++, src_line++
        ) {
            linebuf_init_line(src, src_line);
            linebuf_copy_line_to(self->main_linebuf, src->line, y);
        }
    }
    return true;
}

void
screen_rescale_images(Screen *self) {
    grman_remove_all_cell_images(self->main_grman);
    grman_remove_all_cell_images(self->alt_grman);
    grman_rescale(self->main_grman, self->cell_size);
    grman_rescale(self->alt_grman, self->cell_size);
}


static PyObject*
reset_callbacks(Screen *self, PyObject *a UNUSED) {
    Py_CLEAR(self->callbacks);
    self->callbacks = Py_None;
    Py_INCREF(self->callbacks);
    Py_RETURN_NONE;
}

static void
dealloc(Screen* self) {
    pthread_mutex_destroy(&self->write_buf_lock);
    free_vt_parser(self->vt_parser); self->vt_parser = NULL;
    self->text_cache = tc_decref(self->text_cache);
    Py_CLEAR(self->main_grman);
    Py_CLEAR(self->alt_grman);
    Py_CLEAR(self->last_reported_cwd);
    PyMem_RawFree(self->write_buf);
    Py_CLEAR(self->callbacks);
    Py_CLEAR(self->test_child);
    Py_CLEAR(self->cursor);
    Py_CLEAR(self->main_linebuf);
    Py_CLEAR(self->alt_linebuf);
    Py_CLEAR(self->historybuf);
    Py_CLEAR(self->color_profile);
    Py_CLEAR(self->marker);
    PyMem_Free(self->overlay_line.cpu_cells);
    PyMem_Free(self->overlay_line.gpu_cells);
    PyMem_Free(self->overlay_line.original_line.cpu_cells);
    PyMem_Free(self->overlay_line.original_line.gpu_cells);
    Py_CLEAR(self->overlay_line.overlay_text);
    PyMem_Free(self->main_tabstops);
    Py_CLEAR(self->paused_rendering.linebuf);
    Py_CLEAR(self->paused_rendering.grman);
    free(self->selections.items);
    free(self->url_ranges.items);
    free(self->paused_rendering.url_ranges.items);
    free(self->paused_rendering.selections.items);
    free_hyperlink_pool(self->hyperlink_pool);
    free(self->as_ansi_buf.buf);
    free(self->last_rendered_window_char.canvas);
    free(self->extra_cursors.locations); free(self->paused_rendering.extra_cursors.locations);
    if (self->lc) { cleanup_list_of_chars(self->lc); free(self->lc); self->lc = NULL; }
    Py_TYPE(self)->tp_free((PyObject*)self);
} // }}}

// Draw text {{{
typedef struct text_loop_state {
    bool image_placeholder_marked;
    const CPUCell cc; const GPUCell g;
    CPUCell *cp; GPUCell *gp;
    GraphemeSegmentationResult seg;
    struct {
        index_type x, y; CPUCell *cc;
    } prev;
} text_loop_state;

static void
continue_to_next_line(Screen *self) {
    linebuf_set_last_char_as_continuation(self->linebuf, self->cursor->y, true);
    self->cursor->x = 0;
    screen_linefeed(self);
}

static bool
selection_has_screen_line(const Selections *selections, const int y) {
    for (size_t i = 0; i < selections->count; i++) {
        const Selection *s = selections->items + i;
        if (!is_selection_empty(s)) {
            int start = (int)s->start.y - s->start_scrolled_by;
            int end = (int)s->end.y - s->end_scrolled_by;
            int top = MIN(start, end);
            int bottom = MAX(start, end);
            if (top <= y && y <= bottom) return true;
        }
    }
    return false;
}

static void
clear_intersecting_selections(Screen *self, index_type y) {
    if (selection_has_screen_line(&self->selections, y)) clear_selection(&self->selections);
    if (selection_has_screen_line(&self->url_ranges, y)) clear_selection(&self->url_ranges);
}

static void
init_prev_cell(Screen *self, text_loop_state *s) {
    zero_at_ptr(&s->prev);
    if (self->cursor->x) {
        s->prev.y = self->cursor->y;
        s->prev.x = self->cursor->x - 1;
        s->prev.cc = linebuf_cpu_cell_at(self->linebuf, s->prev.x, s->prev.y);
    } else if (self->cursor->y) {
        s->prev.y = self->cursor->y - 1;
        s->prev.x = self->columns - 1;
        s->prev.cc = linebuf_cpu_cell_at(self->linebuf, s->prev.x, s->prev.y);
        if (!s->prev.cc->next_char_was_wrapped) s->prev.cc = NULL;
    }
}
static void
init_segmentation_state(Screen *self, text_loop_state *s) {
    init_prev_cell(self, s);
    grapheme_segmentation_reset(&s->seg);
    if (s->prev.cc) {
        text_in_cell(s->prev.cc, self->text_cache, self->lc);
        for (index_type i = 0; i < self->lc->count; i++) s->seg = grapheme_segmentation_step(s->seg, char_props_for(self->lc->chars[i]));
    }
}

static void
init_text_loop_line(Screen *self, text_loop_state *s) {
    linebuf_init_cells(self->linebuf, self->cursor->y, &s->cp, &s->gp);
    clear_intersecting_selections(self, self->cursor->y);
    linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
    s->image_placeholder_marked = false;
    init_segmentation_state(self, s);
}

static void
zero_cells(text_loop_state *s, CPUCell *c, GPUCell *g) { *c = s->cc; *g = s->g; }

typedef Line*(linefunc_t)(Screen*, int);

static void
init_line_(Screen *self, index_type y, Line *line) {
    linebuf_init_line_at(self->linebuf, y, line);
}


static Line*
init_line(Screen *self, index_type y) {
    init_line_(self, y, self->linebuf->line);
    return self->linebuf->line;
}

static void
visual_line(Screen *self, int y_, Line *line) {
    index_type y = MAX(0, y_);
    if (self->scrolled_by) {
        if (y < self->scrolled_by) {
            historybuf_init_line(self->historybuf, self->scrolled_by - 1 - y, line);
            return;
        }
        y -= self->scrolled_by;
    }
    init_line_(self, y, line);
}

static Line*
visual_line_(Screen *self, int y_) {
    index_type y = MAX(0, y_);
    if (self->scrolled_by) {
        if (y < self->scrolled_by) {
            historybuf_init_line(self->historybuf, self->scrolled_by - 1 - y, self->historybuf->line);
            return self->historybuf->line;
        }
        y -= self->scrolled_by;
    }
    return init_line(self, y);
}

static bool
visual_line_is_continued(Screen *self, int y_) {
    index_type y = MAX(0, y_);
    if (self->scrolled_by) {
        if (y < self->scrolled_by) return historybuf_is_line_continued(self->historybuf, self->scrolled_by - 1 - y);
        y -= self->scrolled_by;
    }
    if (y) return linebuf_is_line_continued(self->linebuf, y);
    return self->linebuf == self->main_linebuf ? history_buf_endswith_wrap(self->historybuf) : false;
}

static Line*
range_line_(Screen *self, int y) {
    if (y < 0) {
        historybuf_init_line(self->historybuf, -(y + 1), self->historybuf->line);
        return self->historybuf->line;
    }
    return init_line(self, y);
}

static void
range_line(Screen *self, int y, Line *line) {
    if (y < 0) historybuf_init_line(self->historybuf, -(y + 1), line);
    else init_line_(self, y, line);
}

static Line*
checked_range_line(Screen *self, int y) {
    if (-(int)self->historybuf->count <= y && y < (int)self->lines) return range_line_(self, y);
    return NULL;
}

static bool
range_line_is_continued(Screen *self, int y) {
    if (!(-(int)self->historybuf->count <= y && y < (int)self->lines)) return false;
    if (y < 0) return historybuf_is_line_continued(self->historybuf, -(y + 1));
    if (y) return linebuf_is_line_continued(self->linebuf, y);
    return self->linebuf == self->main_linebuf ? history_buf_endswith_wrap(self->historybuf) : false;
}

static void
insert_characters(Screen *self, index_type at, index_type num, index_type y, bool replace_with_spaces) {
    // insert num chars at x=at setting them to the value of the num chars at [at, at + num)
    // multiline chars at x >= at are deleted and multicell chars split at x=at
    // and x=at + num - 1 are deleted
    nuke_multiline_char_intersecting_with(self, at, self->columns, y, y + 1, replace_with_spaces);
    nuke_split_multicell_char_at_left_boundary(self, at, y, replace_with_spaces);
    CPUCell *cp; GPUCell *gp;
    linebuf_init_cells(self->linebuf, y, &cp, &gp);
    // right shift
    for(index_type i = self->columns - 1; i >= at + num; i--) {
        cp[i] = cp[i - num]; gp[i] = gp[i - num];
    }
    nuke_incomplete_single_line_multicell_chars_in_range(self, at, at + num, y, replace_with_spaces);
    nuke_split_multicell_char_at_right_boundary(self, self->columns - 1, y, replace_with_spaces);
}

static bool
halve_multicell_width(Screen *self, index_type x_, index_type y_) {
    CPUCell *cp; GPUCell *gp;
    linebuf_init_cells(self->linebuf, y_, &cp, &gp);
    int y_min_limit = -1;
    if (self->linebuf == self->main_linebuf) y_min_limit = -(self->historybuf->count + 1);
    int expected_y_min_limit = ((int)y_) - cp[x_].scale;
    if (expected_y_min_limit < y_min_limit) return false;
    y_min_limit = expected_y_min_limit;
    unsigned new_width = cp[x_].width / 2;
    while (cp[x_].x && x_ > 0) x_--;
    const index_type ws = mcd_x_limit(&cp[x_]);
    const index_type x_limit = MIN(self->columns, x_ + ws);
    const index_type half_x_limit = MIN(self->columns, x_ + ws / 2);
    int y_max_limit = MIN(self->lines, y_ + cp[x_].scale);
    for (int y = y_min_limit + 1; y < y_max_limit; y++) {
        Line *line = range_line_(self, y); cp = line->cpu_cells; gp = line->gpu_cells;
        for (index_type x = x_; x < half_x_limit; x++) cp[x].width = new_width;
        for (index_type x = half_x_limit; x < x_limit; x++) {
            cp[x] = (CPUCell){0}; clear_sprite_position(gp[x]);
        }
        if (y > -1) linebuf_mark_line_dirty(self->linebuf, y);
    }
    self->is_dirty = true;
    return true;
}

void
set_active_hyperlink(Screen *self, char *id, char *url) {
    if (OPT(allow_hyperlinks)) {
        if (!url || !url[0]) {
            self->active_hyperlink_id = 0;
            return;
        }
        self->active_hyperlink_id = get_id_for_hyperlink(self, id, url);
    }
}

static void
text_cache_gc_process_cells(TextCache *tc, TextCacheGCData *gc, CPUCell *cells, size_t count) {
    for (size_t i = 0; i < count; i++) {
        CPUCell *c = cells + i;
        if (!c->ch_is_idx) continue;
        char_type new_idx;
        if (tc_gc_map_index(tc, gc, c->ch_or_idx, &new_idx)) c->ch_or_idx = new_idx;
        else cell_set_char(c, 0);  // stale index, should not happen
    }
}

static void
text_cache_gc_process_linebuf(TextCache *tc, TextCacheGCData *gc, LineBuf *lb) {
    if (lb) text_cache_gc_process_cells(tc, gc, lb->cpu_cell_buf, (size_t)lb->ynum * lb->xnum);
}

void
screen_garbage_collect_text_cache(Screen *self) {
    // The TextCache interns unique cell texts forever; remap every live cell
    // index onto a fresh cache so entries that scrolled out of the history
    // buffer are freed. Mirrors screen_garbage_collect_hyperlink_pool().
    TextCacheGCData *gc = tc_gc_begin(self->text_cache);
    if (!gc) return;  // allocation failure, cache left unchanged
    if (self->historybuf->count) {
        for (index_type y = self->historybuf->count; y-- > 0;) {
            CPUCell *cells = historybuf_cpu_cells(self->historybuf, y);
            text_cache_gc_process_cells(self->text_cache, gc, cells, self->historybuf->xnum);
        }
    }
    text_cache_gc_process_linebuf(self->text_cache, gc, self->main_linebuf);
    text_cache_gc_process_linebuf(self->text_cache, gc, self->alt_linebuf);
    text_cache_gc_process_linebuf(self->text_cache, gc, self->paused_rendering.linebuf);
    if (self->overlay_line.cpu_cells) text_cache_gc_process_cells(
        self->text_cache, gc, self->overlay_line.cpu_cells, self->overlay_line.xnum);
    if (self->overlay_line.original_line.cpu_cells) text_cache_gc_process_cells(
        self->text_cache, gc, self->overlay_line.original_line.cpu_cells, self->overlay_line.xnum);
    tc_gc_end(self->text_cache, gc);
}

static bool
add_combining_char(Screen *self, char_type ch, index_type x, index_type y) {
    CPUCell *cpu_cells = linebuf_cpu_cells_for_line(self->linebuf, y);
    CPUCell *cell = cpu_cells + x;
    if (!cell_has_text(cell) || (cell->is_multicell && cell->y)) return false; // don't allow adding combining chars to a null cell
    text_in_cell(cell, self->text_cache, self->lc);
    if (self->lc->count >= MAX_NUM_CODEPOINTS_PER_CELL) return false; // don't allow too many combining chars to prevent DoS attacks
    ensure_space_for_chars(self->lc, self->lc->count + 1);
    self->lc->chars[self->lc->count++] = ch;
    cell->ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc);
    cell->ch_is_idx = true;
    if (cell->is_multicell) {
        char_type ch_and_idx = cell->ch_and_idx;
        while (cell->x && x) cell = cpu_cells + --x;
        index_type x_limit = MIN(x + mcd_x_limit(cell), self->columns);
        for (index_type v = y; v < y + cell->scale; v++) {
            cpu_cells = linebuf_cpu_cells_for_line(self->linebuf, v);
            for (index_type h = x; h < x_limit; h++) cpu_cells[h].ch_and_idx = ch_and_idx;
            linebuf_mark_line_dirty(self->linebuf, v);
        }
    }
    return true;
}


static bool
has_multiline_cells_in_span(const CPUCell *cells, const index_type start, const index_type count) {
    for (index_type x = start; x < start + count; x++) if (cells[x].y) return true;
    return false;
}

static bool
move_cursor_past_multicell(Screen *self, index_type required_width) {
    if (required_width > self->columns) return false;
    index_type orig_x = self->cursor->x, orig_y = self->cursor->y;
    while(true) {
        CPUCell *cp = linebuf_cpu_cells_for_line(self->linebuf, self->cursor->y);
        while (self->cursor->x + required_width <= self->columns) {
            if (!has_multiline_cells_in_span(cp, self->cursor->x, required_width)) {
                if (cp[self->cursor->x].is_multicell) nuke_multicell_char_at(self, self->cursor->x, self->cursor->y, cp[self->cursor->x].x != 0);
                return true;
            }
            self->cursor->x++;
        }
        if (self->modes.mDECAWM || has_multiline_cells_in_span(cp, self->columns - required_width, required_width)) {
            continue_to_next_line(self);
        } else {
            self->cursor->x = self->columns - required_width;
            if (cp[self->cursor->x].is_multicell) nuke_multicell_char_at(self, self->cursor->x, self->cursor->y, cp[self->cursor->x].x != 0);
            return true;
        }
    }
    self->cursor->x = orig_x; self->cursor->y = orig_y;
    return false;
}

static void
move_widened_char_past_multiline_chars(Screen *self, text_loop_state *s, CPUCell* cpu_cell, GPUCell *gpu_cell, index_type xpos, index_type ypos) {
    index_type before = self->cursor->y;
    self->cursor->x = xpos; self->cursor->y = ypos;
    if (move_cursor_past_multicell(self, 2)) {
        CPUCell *cp; GPUCell *gp;
        clear_sprite_position(*gpu_cell);
        linebuf_init_cells(self->linebuf, self->cursor->y, &cp, &gp);
        cp[self->cursor->x] = *cpu_cell; gp[self->cursor->x] = *gpu_cell;
        self->cursor->x++;
        cp[self->cursor->x] = *cpu_cell; gp[self->cursor->x] = *gpu_cell;
        cp[self->cursor->x].x = 1;
        self->cursor->x++;
    }
    *cpu_cell = (CPUCell){0}; *gpu_cell = (GPUCell){0};
    if (self->cursor->y == before) init_segmentation_state(self, s);
    else init_text_loop_line(self, s);
}

static bool
is_emoji_presentation_base(char_type ch) {
    return char_props_for(ch).is_emoji_presentation_base == 1;
}

static void
draw_combining_char(Screen *self, text_loop_state *s, char_type ch) {
    CPUCell *cp; GPUCell *gp;
    linebuf_init_cells(self->linebuf, s->prev.y, &cp, &gp);
    index_type xpos = s->prev.x;
    while (xpos && cp[xpos].is_multicell && cp[xpos].x) xpos--;
    if (!add_combining_char(self, ch, xpos, s->prev.y) || self->lc->count < 2) return;
    unsigned base_pos = self->lc->count -  2;
    if (ch == VS16) {  // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji
        CPUCell *cpu_cell = cp + xpos;
        GPUCell *gpu_cell = gp + xpos;
        if (self->lc->chars[base_pos + 1] == VS16 && !cpu_cell->is_multicell && is_emoji_presentation_base(self->lc->chars[base_pos])) {
            cpu_cell->is_multicell = true;
            cpu_cell->width = 2;
            cpu_cell->natural_width = true;
            if (!cpu_cell->scale) cpu_cell->scale = 1;
            if (xpos + 1 < self->columns) {
                CPUCell *second = cp + xpos + 1;
                if (second->is_multicell) {
                    if (second->y) {
                        move_widened_char_past_multiline_chars(self, s, cpu_cell, gpu_cell, xpos, s->prev.y);
                        return;
                    }
                    nuke_multicell_char_at(self, xpos + 1, s->prev.y, false);
                }
                zero_cells(s, second, gp + xpos + 1);
                self->cursor->x++;
                *second = *cpu_cell; second->x = 1;
            } else {
                move_widened_char_past_multiline_chars(self, s, cpu_cell, gpu_cell, xpos, s->prev.y);
            }
        }
    } else if (ch == VS15) {
        const CPUCell *cpu_cell = cp + xpos;
        if (self->lc->chars[base_pos + 1] == VS15 && cpu_cell->is_multicell && cpu_cell->width == 2 && is_emoji_presentation_base(self->lc->chars[base_pos])) {
            index_type deltax = (cpu_cell->scale * cpu_cell->width) / 2;
            if (halve_multicell_width(self, xpos, s->prev.y)) {
                self->cursor->x -= deltax;
                init_segmentation_state(self, s);
            }
        }
    }
}

static void
screen_on_input(Screen *self) {
    if (!self->has_activity_since_last_focus && !self->has_focus && self->callbacks != Py_None) {
        PyObject *ret = PyObject_CallMethod(self->callbacks, "on_activity_since_last_focus", NULL);
        if (ret == NULL) PyErr_Print();
        else {
            if (ret == Py_True) self->has_activity_since_last_focus = true;
            Py_DECREF(ret);
        }
    }
}

static void
replace_multicell_char_under_cursor_with_spaces(Screen *self) {
    nuke_multicell_char_at(self, self->cursor->x, self->cursor->y, true);
}

static void
screen_change_charset(Screen *self, uint32_t which) {
    switch(which) {
        case 0:
            self->charset.current_num = 0;
            self->charset.current = self->charset.zero;
            break;
        case 1:
            self->charset.current_num = 1;
            self->charset.current = self->charset.one;
            break;
    }
}

void
screen_designate_charset(Screen *self, uint32_t which, uint32_t as) {
    switch(which) {
        case 0:
            self->charset.zero = translation_table(as);
            if (self->charset.current_num == 0) self->charset.current = self->charset.zero;
            break;
        case 1:
            self->charset.one = translation_table(as);
            if (self->charset.current_num == 1) self->charset.current = self->charset.one;
            break;
    }
}


static uint32_t
map_char(Screen *self, const uint32_t ch) {
    return UNLIKELY(self->charset.current && ch < 256) ? self->charset.current[ch] : ch;
}

static void
draw_control_char(Screen *self, text_loop_state *s, uint32_t ch) {
    switch (ch) {
        case BEL:
            screen_bell(self); break;
        case BS: {
            index_type before = self->cursor->y;
            screen_backspace(self);
            if (before == self->cursor->y) init_segmentation_state(self, s);
            else init_text_loop_line(self, s);
            } break;
        case HT:
            if (UNLIKELY(self->cursor->x >= self->columns)) {
                if (self->modes.mDECAWM) {
                    // xterm discards the TAB in this case so match its behavior
                    continue_to_next_line(self);
                    init_text_loop_line(self, s);
                } else if (self->columns > 0){
                    self->cursor->x = self->columns - 1;
                    if (s->cp[self->cursor->x].is_multicell) {
                        if (s->cp[self->cursor->x].y) move_cursor_past_multicell(self, 1);
                        else replace_multicell_char_under_cursor_with_spaces(self);
                    }
                    screen_tab(self);
                }
            } else screen_tab(self);
            init_segmentation_state(self, s);
            break;
        case SI:
            screen_change_charset(self, 0); break;
        case SO:
            screen_change_charset(self, 1); break;
        case LF:
        case VT:
        case FF:
            screen_linefeed(self); init_text_loop_line(self, s); break;
        case CR:
            screen_carriage_return(self); init_segmentation_state(self, s); break;
        default:
            break;
    }
}

static void
draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_state *s) {
    init_text_loop_line(self, s);
    int char_width;
    for (size_t i = 0; i < num_chars; i++) {
        uint32_t ch = map_char(self, chars[i]);
        if (ch < DEL && s->seg.grapheme_break <= GBP_None) {  // fast path for printable ASCII
            if (ch < ' ') {
                draw_control_char(self, s, ch);
                continue;
            }
            char_width = 1;
            s->seg = (GraphemeSegmentationResult){.grapheme_break=GBP_None};
        } else {
            CharProps cp = char_props_for(ch);
            if (cp.is_invalid) {
                if (ch < ' ') draw_control_char(self, s, ch);
                continue;
            }
            s->seg = grapheme_segmentation_step(s->seg, cp);
            if (UNLIKELY(s->seg.add_to_current_cell && s->prev.cc)) {
                draw_combining_char(self, s, ch);
                continue;
            }
            char_width = wcwidth_std(cp);
            if (UNLIKELY(char_width < 1)) {
                if (char_width == 0) {
                    // Preserve zero width chars as combining chars even though
                    // they were not added to the prev cell by grapheme segmentation.
                    // Zero width chars can only be represented as combining chars.
                    if (s->prev.cc) draw_combining_char(self, s, ch);
                    continue;
                }
                char_width = 1;
            }
        }

        if (self->cursor->x < self->columns && s->cp[self->cursor->x].is_multicell) {
            if (s->cp[self->cursor->x].y) {
                move_cursor_past_multicell(self, 1);
                init_text_loop_line(self, s);
            } else nuke_multicell_char_at(self, self->cursor->x, self->cursor->y, s->cp[self->cursor->x].x != 0);
        }

        self->last_graphic_char = ch;
        if (UNLIKELY(self->columns < self->cursor->x + (unsigned int)char_width)) {
            if (self->modes.mDECAWM) {
                continue_to_next_line(self);
                init_text_loop_line(self, s);
            } else self->cursor->x = self->columns - char_width;
            CPUCell *c = &s->cp[self->cursor->x];
            if (c->is_multicell) {
                if (c->y) { move_cursor_past_multicell(self, char_width); init_text_loop_line(self, s); }
                nuke_multicell_char_at(self, self->cursor->x, self->cursor->y, c->x > 0);
            }
        }
        if (self->modes.mIRM) insert_characters(self, self->cursor->x, char_width, self->cursor->y, true);
        if (UNLIKELY(!s->image_placeholder_marked && ch == IMAGE_PLACEHOLDER_CHAR)) {
            linebuf_set_line_has_image_placeholders(self->linebuf, self->cursor->y, true);
            s->image_placeholder_marked = true;
        }
        CPUCell *fc = s->cp + self->cursor->x;
        if (char_width == 2) {
            CPUCell *second = fc + 1;
            if (second->is_multicell) {
                if (second->y) {
                    self->cursor->x++;
                    move_cursor_past_multicell(self, 2);
                    fc = s->cp + self->cursor->x; second = fc + 1;
                } else nuke_multicell_char_at(self, self->cursor->x + 1, self->cursor->y, true);
            }
            zero_cells(s, fc, s->gp + self->cursor->x);
            *fc = (CPUCell){.ch_or_idx=ch, .is_multicell=true, .width=2, .scale=1, .natural_width=true, .hyperlink_id=s->cc.hyperlink_id};
            *second = *fc; second->x = 1;
            s->gp[self->cursor->x + 1] = s->gp[self->cursor->x];
            s->prev.y = self->cursor->y; s->prev.x = self->cursor->x; s->prev.cc = fc;
            self->cursor->x += 2;
        } else {
            zero_cells(s, fc, s->gp + self->cursor->x);
            cell_set_char(fc, ch);
            s->prev.y = self->cursor->y; s->prev.x = self->cursor->x; s->prev.cc = fc;
            self->cursor->x++;
            fc->is_multicell = false;
        }
    }
#undef init_line
}

#define PREPARE_FOR_DRAW_TEXT \
    if (tc_should_gc(self->text_cache)) screen_garbage_collect_text_cache(self); \
    const bool force_underline = OPT(underline_hyperlinks) == UNDERLINE_ALWAYS && self->active_hyperlink_id != 0; \
    CellAttrs attrs = cursor_to_attrs(self->cursor); \
    if (force_underline) attrs.decoration = OPT(url_style); \
    text_loop_state s={ \
        .cc=(CPUCell){.hyperlink_id=self->active_hyperlink_id}, \
        .g=(GPUCell){ \
            .attrs=attrs, \
            .fg=self->cursor->sgr.fg & COL_MASK, .bg=self->cursor->sgr.bg & COL_MASK, \
            .decoration_fg=force_underline ? ((OPT(url_color) & COL_MASK) << 8) | 2 : self->cursor->sgr.decoration_fg & COL_MASK, \
        } \
    };

static void
draw_text(Screen *self, const uint32_t *chars, size_t num_chars) {
    PREPARE_FOR_DRAW_TEXT;
    self->is_dirty = true;
    draw_text_loop(self, chars, num_chars, &s);
}

void
screen_draw_text(Screen *self, const uint32_t *chars, size_t num_chars) {
    screen_on_input(self);
    draw_text(self, chars, num_chars);
}

static void
draw_codepoint(Screen *self, char_type ch) {
    uint32_t lch = self->last_graphic_char;
    draw_text(self, &ch, 1);
    self->last_graphic_char = lch;
}

void
screen_align(Screen *self) {
    self->margin_top = 0; self->margin_bottom = self->lines - 1;
    screen_cursor_position(self, 1, 1);
    linebuf_clear(self->linebuf, 'E');
}

static size_t
decode_utf8_safe_string(const uint8_t *src, size_t sz, uint32_t *dest) {
    // dest must be an array of size at least sz
    uint32_t codep = 0;
    UTF8State state = 0, prev = UTF8_ACCEPT;
    size_t i = 0, d = 0;
    for (; i < sz; i++) {
        switch(decode_utf8(&state, &codep, src[i])) {
            case UTF8_ACCEPT:
                // Ignore C0 and C1 chars
                if (codep >= ' ' && !(DEL <= codep && codep <= 159)) dest[d++] = codep;
                break;
            case UTF8_REJECT:
                state = UTF8_ACCEPT;
                if (prev != UTF8_ACCEPT && i > 0) i--;
                break;
        }
        prev = state;
    }
    return d;
}

static void
handle_fixed_width_multicell_command(Screen *self, CPUCell mcd, ListOfChars *lc) {
    index_type width = mcd.width * mcd.scale;
    index_type height = mcd.scale;
    index_type max_height = self->margin_bottom - self->margin_top + 1;
    if (width > self->columns || height > max_height) return;
    lc->count = MIN(lc->count, MAX_NUM_CODEPOINTS_PER_CELL);
    PREPARE_FOR_DRAW_TEXT;
    mcd.hyperlink_id = s.cc.hyperlink_id;
    cell_set_chars(&mcd, self->text_cache, lc);
    move_cursor_past_multicell(self, width);
    if (height > 1) {
        index_type available_height = self->margin_bottom - self->cursor->y + 1;
        if (height > available_height) {
            index_type extra_lines = height - available_height;
            screen_scroll(self, extra_lines);
            self->cursor->y -= extra_lines;
        }
    }
    if (self->modes.mIRM) {
        for (index_type y = self->cursor->y; y < self->cursor->y + height; y++) {
            if (self->modes.mIRM) insert_characters(self, self->cursor->x, width, y, true);
        }
    }
    for (index_type y = self->cursor->y; y < self->cursor->y + height; y++) {
        linebuf_init_cells(self->linebuf, y, &s.cp, &s.gp);
        linebuf_mark_line_dirty(self->linebuf, y);
        mcd.x = 0; mcd.y = y - self->cursor->y;
        for (index_type x = self->cursor->x; x < self->cursor->x + width; x++, mcd.x++) {
            if (s.cp[x].is_multicell) nuke_multicell_char_at(self, x, y, s.cp[x].x + s.cp[x].y > 0);
            s.cp[x] = mcd; s.gp[x] = s.g;
        }
    }
    self->cursor->x += width;
    self->is_dirty = true;
}

static void
handle_variable_width_multicell_command(Screen *self, CPUCell mcd, ListOfChars *lc) {
    ensure_space_for_chars(lc, lc->count + 1); lc->chars[lc->count] = 0;
    mcd.width = wcswidth_string(lc->chars);
    if (!mcd.width) { lc->count = 0; return; }
    handle_fixed_width_multicell_command(self, mcd, lc);
}

void
screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload) {
    screen_on_input(self);
    if (!cmd->payload_sz) return;
    ensure_space_for_chars(self->lc, cmd->payload_sz + 1);
    self->lc->count = decode_utf8_safe_string(payload, cmd->payload_sz, self->lc->chars);
    if (!self->lc->count) return;
#define M(x) ( (1u << x) - 1u)
    CPUCell mcd = {
        .width=MIN(cmd->width, M(WIDTH_BITS)), .scale=MAX(1u, MIN(cmd->scale, M(SCALE_BITS))),
        .subscale_n=MIN(cmd->subscale_n, M(SUBSCALE_BITS)), .subscale_d=MIN(cmd->subscale_d, M(SUBSCALE_BITS)),
        .valign=MIN(cmd->vertical_align, M(VALIGN_BITS)), .halign=MIN(cmd->horizontal_align, M(HALIGN_BITS)),
        .is_multicell=true
    };
#undef M
    if (mcd.width) handle_fixed_width_multicell_command(self, mcd, self->lc);
    else {
        RAII_ListOfChars(lc);
        GraphemeSegmentationResult s; grapheme_segmentation_reset(&s);
        mcd.natural_width = true;
        for (unsigned i = 0; i < self->lc->count; i++) {
            char_type ch = self->lc->chars[i];
            CharProps cp = char_props_for(ch);
            if (cp.is_invalid) continue;
            if ((s = grapheme_segmentation_step(s, cp)).add_to_current_cell || (wcwidth_std(cp) == 0 && lc.count)) lc.chars[lc.count++] = ch;
            else {
                if (lc.count) handle_variable_width_multicell_command(self, mcd, &lc);
                switch(wcwidth_std(cp)) {
                    case 0: case -1: lc.count = 0; break;
                    default: lc.chars[0] = ch; lc.count = 1; break;
                }
            }
        }
        if (lc.count) handle_variable_width_multicell_command(self, mcd, &lc);
    }
}

// }}}

// Graphics {{{

void
screen_alignment_display(Screen *self) {
    // https://www.vt100.net/docs/vt510-rm/DECALN.html
    screen_cursor_position(self, 1, 1);
    self->margin_top = 0; self->margin_bottom = self->lines - 1;
    for (unsigned int y = 0; y < self->linebuf->ynum; y++) {
        linebuf_init_line(self->linebuf, y);
        line_clear_text(self->linebuf->line, 0, self->linebuf->xnum, 'E');
        linebuf_mark_line_dirty(self->linebuf, y);
    }
}

void
select_graphic_rendition(Screen *self, int *params, unsigned int count, bool is_group, Region *region_) {
    if (region_) {
        Region region = *region_;
        if (!region.top) region.top = 1;
        if (!region.left) region.left = 1;
        if (!region.bottom) region.bottom = self->lines;
        if (!region.right) region.right = self->columns;
        if (self->modes.mDECOM) {
            region.top += self->margin_top; region.bottom += self->margin_top;
        }
        region.left -= 1; region.top -= 1; region.right -= 1; region.bottom -= 1;  // switch to zero based indexing
        if (self->modes.mDECSACE) {
            index_type x = MIN(region.left, self->columns - 1);
            index_type num = region.right >= x ? region.right - x + 1 : 0;
            num = MIN(num, self->columns - x);
            for (index_type y = region.top; y < MIN(region.bottom + 1, self->lines); y++) {
                linebuf_init_line(self->linebuf, y);
                apply_sgr_to_cells(self->linebuf->line->gpu_cells + x, num, params, count, is_group);
            }
        } else {
            index_type x, num;
            if (region.top == region.bottom) {
                linebuf_init_line(self->linebuf, region.top);
                x = MIN(region.left, self->columns-1);
                num = MIN(self->columns - x, region.right - x + 1);
                apply_sgr_to_cells(self->linebuf->line->gpu_cells + x, num, params, count, is_group);
            } else {
                for (index_type y = region.top; y < MIN(region.bottom + 1, self->lines); y++) {
                    if (y == region.top) { x = MIN(region.left, self->columns - 1); num = self->columns - x; }
                    else if (y == region.bottom) { x = 0; num = MIN(region.right + 1, self->columns); }
                    else { x = 0; num = self->columns; }
                    linebuf_init_line(self->linebuf, y);
                    apply_sgr_to_cells(self->linebuf->line->gpu_cells + x, num, params, count, is_group);
                }
            }
        }
    } else {
        cursor_from_sgr(self->cursor, params, count, is_group);
        self->sgr_blink_was_used |= self->cursor->sgr.blink;
    }
}

static void
write_to_test_child(Screen *self, const char *data, size_t sz) {
    PyObject *r = PyObject_CallMethod(self->test_child, "write", "y#", data, sz); if (r == NULL) PyErr_Print(); Py_CLEAR(r);
}

static bool
write_to_child(Screen *self, const char *data, size_t sz) {
    bool written = false;
    if (self->window_id) written = schedule_write_to_child(self->window_id, 1, data, sz);
    if (self->test_child != Py_None) { write_to_test_child(self, data, sz); }
    return written;
}

static void
get_prefix_and_suffix_for_escape_code(unsigned char which, const char ** prefix, const char ** suffix) {
    *suffix = "\033\\";
    switch(which) {
        case ESC_DCS:
            *prefix = "\033P";
            break;
        case ESC_CSI:
            *prefix = "\033["; *suffix = "";
            break;
        case ESC_OSC:
            *prefix = "\033]";
            break;
        case ESC_PM:
            *prefix = "\033^";
            break;
        case ESC_APC:
            *prefix = "\033_";
            break;
        default:
            fatal("Unknown escape code to write: %u", which);
    }
}

bool
write_escape_code_to_child(Screen *self, unsigned char which, const char *data) {
    bool written = false;
    const char *prefix, *suffix;
    get_prefix_and_suffix_for_escape_code(which, &prefix, &suffix);
    if (self->window_id) {
        if (suffix[0]) {
            written = schedule_write_to_child(self->window_id, 3, prefix, strlen(prefix), data, strlen(data), suffix, strlen(suffix));
        } else {
            written = schedule_write_to_child(self->window_id, 2, prefix, strlen(prefix), data, strlen(data));
        }
    }
    if (self->test_child != Py_None) {
        write_to_test_child(self, prefix, strlen(prefix));
        write_to_test_child(self, data, strlen(data));
        if (suffix[0]) write_to_test_child(self, suffix, strlen(suffix));
    }
    return written;
}

static bool
write_escape_code_to_child_python(Screen *self, unsigned char which, PyObject *data) {
    bool written = false;
    const char *prefix, *suffix;
    get_prefix_and_suffix_for_escape_code(which, &prefix, &suffix);
    if (self->window_id) written = schedule_write_to_child_python(self->window_id, prefix, data, suffix);
    if (self->test_child != Py_None) {
        write_to_test_child(self, prefix, strlen(prefix));
        for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(data); i++) {
            PyObject *t = PyTuple_GET_ITEM(data, i);
            if (PyBytes_Check(t)) write_to_test_child(self, PyBytes_AS_STRING(t), PyBytes_GET_SIZE(t));
            else {
                Py_ssize_t sz;
                const char *d = PyUnicode_AsUTF8AndSize(t, &sz);
                if (d) write_to_test_child(self, d, sz);
            }
        }
        if (suffix[0]) write_to_test_child(self, suffix, strlen(suffix));
    }
    return written;
}

static bool
cursor_within_margins(Screen *self) {
    return self->margin_top <= self->cursor->y && self->cursor->y <= self->margin_bottom;
}

static inline void
reset_pixel_scroll(Screen *self, unsigned val) { self->pixel_scroll_offset_y = val; }


// Remove all cell images from a portion of the screen and mark lines that
// contain image placeholders as dirty to make sure they are redrawn. This is
// needed when we perform commands that may move some lines without marking them
// as dirty (like screen_insert_lines) and at the same time don't move image
// references (i.e. unlike screen_scroll, which moves everything).
static void
screen_dirty_line_graphics(Screen *self, const unsigned int top, const unsigned int bottom, const bool main_buf) {
    bool need_to_remove = false;
    const unsigned int limit = MIN(bottom+1, self->lines);
    LineBuf *linebuf = main_buf ? self->main_linebuf : self->alt_linebuf;
    for (unsigned int y = top; y < limit; y++) {
        if (linebuf->line_attrs[y].has_image_placeholders) {
            need_to_remove = true;
            linebuf_mark_line_dirty(linebuf, y);
            self->is_dirty = true;
        }
    }
    if (need_to_remove)
        grman_remove_cell_images(main_buf ? self->main_grman : self->alt_grman, top, bottom);
}

static bool
screen_mark_potential_url_drag(Screen *self) {
    Window *w;
    if ((!self->current_hyperlink_under_mouse.id && !self->current_hyperlink_under_mouse.has_detected_url) || !self->window_id || !(w = window_for_window_id(self->window_id))) return false;
    w->drag_source.potential_url_drag.active = true;
    w->drag_source.potential_url_drag.x = w->mouse_pos.cell_x;
    w->drag_source.potential_url_drag.y = w->mouse_pos.cell_y;
    return true;
}

void
screen_handle_dnd_command(Screen *self, const DnDCommand *cmd_, const uint8_t *payload) {
    Window *w;
    if (!self->window_id || !(w = window_for_window_id(self->window_id))) return;
    const DnDCommand *cmd; DnDCommand copy;
    if (self->dnd_chunking.active) {
        copy = self->dnd_chunking.metadata;
        copy.more = cmd_->more; copy.payload_sz = cmd_->payload_sz;
        cmd = &copy;
        self->dnd_chunking.active = cmd->more != 0;
    } else {
        cmd = cmd_;
        if (cmd_->more) {
            self->dnd_chunking.active = true;
            self->dnd_chunking.metadata = *cmd_;
        }
    }
    switch(cmd->type) {
        case 'a':
            if (cmd->cell_x == 1) drop_register_machine_id(w, payload, cmd->payload_sz);
            else drop_register_window(w, payload, cmd->payload_sz, true, cmd->client_id, cmd->more);
            break;
        case 'A': drop_register_window(w, NULL, 0, false, cmd->client_id, cmd->more); break;
        case 'm': drop_set_status(w, cmd->operation, (const char*)payload, cmd->payload_sz, cmd->more); break;
        case 'r': {
            drop_enqueue_request(w, cmd->cell_x, cmd->cell_y, cmd->pixel_y, cmd->operation);
        } break;
        case 'o': {
            switch (cmd->cell_x) {
                case 1: drag_start_offerring(w, (const char*)payload, cmd->payload_sz); break;
                case 2: drag_stop_offerring(w); break;
                case 0:
                    drag_add_mimes(
                        w, (int)cmd->operation, cmd->client_id, (const char*)payload, cmd->payload_sz, cmd->more);
                    break;
            }
        } break;
        case 'p': {
            if (cmd->cell_x >= 0) drag_add_pre_sent_data(w, cmd->cell_x, payload, cmd->payload_sz);
            else drag_add_image(w, -cmd->cell_x, cmd->cell_y, cmd->pixel_x, cmd->pixel_y, (int)cmd->operation, payload, cmd->payload_sz);
        } break;
        case 'P': {
            if (cmd->cell_x >= 0) drag_change_image(w, cmd->cell_x);
            else drag_start(w);
        } break;
        case 'e': {
            drag_process_item_data(w, cmd->cell_y, cmd->more, payload, cmd->payload_sz);
        } break;
        case 'E': {
            if (cmd->cell_y == -1) {
                drag_free_offer(w, true);
                if (global_state.drag_source.is_active && global_state.drag_source.from_window == w->id) {
                    cancel_current_drag_source();
                }
            } else drag_process_item_data(w, cmd->cell_y, -1, payload, cmd->payload_sz);
        } break;
        case 'k': {
            drag_remote_file_data(
                w, cmd->cell_x, cmd->cell_y, cmd->pixel_x, cmd->pixel_y, cmd->more != 0, payload, cmd->payload_sz);
        } break;
        case 'q': {
            dnd_query(w, cmd->client_id);
        } break;
    }
}

void
screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload) {
    unsigned int x = self->cursor->x, y = self->cursor->y;
    const char *response = grman_handle_command(self->grman, cmd, payload, self->cursor, &self->is_dirty, self->cell_size);
    if (response != NULL) write_escape_code_to_child(self, ESC_APC, response);
    if (x != self->cursor->x || y != self->cursor->y) {
        bool in_margins = cursor_within_margins(self);
        if (self->cursor->x >= self->columns) { self->cursor->x = 0; self->cursor->y++; }
        if (self->cursor->y > self->margin_bottom) screen_scroll(self, self->cursor->y - self->margin_bottom);
        screen_ensure_bounds(self, false, in_margins);
    }
    if (cmd->unicode_placement) {
        // Make sure the placeholders are redrawn if we add or change a virtual placement.
        screen_dirty_line_graphics(self, 0, self->lines, self->linebuf == self->main_linebuf);
    }
}
// }}}

// Modes {{{


void
screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) {
    bool to_alt = self->linebuf == self->main_linebuf;
    self->active_hyperlink_id = 0;
    reset_pixel_scroll(self, 0);
    if (to_alt) {
        if (clear_alt_screen) {
            linebuf_clear(self->alt_linebuf, BLANK_CHAR);
            grman_clear(self->alt_grman, true, self->cell_size);
        }
        if (save_cursor) screen_save_cursor(self);
        self->linebuf = self->alt_linebuf;
        self->tabstops = self->alt_tabstops;
        self->key_encoding_flags = self->alt_key_encoding_flags;
        self->grman = self->alt_grman;
        screen_cursor_position(self, 1, 1);
        cursor_reset(self->cursor);
    } else {
        self->linebuf = self->main_linebuf;
        self->tabstops = self->main_tabstops;
        self->key_encoding_flags = self->main_key_encoding_flags;
        if (save_cursor) screen_restore_cursor(self);
        self->grman = self->main_grman;
    }
    screen_history_scroll(self, SCROLL_FULL, false);
    self->is_dirty = true;
    grman_mark_layers_dirty(self->grman);
    clear_all_selections(self);
    self->extra_cursors.count = 0;
    // Force re-upload of the selection buffer as the number of render lines
    // changes when pixel_scroll_enabled changes (which depends on which
    // linebuf is active). Without this, the selection buffer can be smaller
    // than the cell data buffer, causing OOB reads that produce cursor
    // artifacts (see #9725).
    self->extra_cursors.dirty = true;
    global_state.check_for_active_animated_images = true;
}

void screen_normal_keypad_mode(Screen UNUSED *self) {} // Not implemented as this is handled by the GUI
void screen_alternate_keypad_mode(Screen UNUSED *self) {}  // Not implemented as this is handled by the GUI

static void
set_mode_from_const(Screen *self, unsigned int mode, bool val) {
#define SIMPLE_MODE(name) \
    case name: \
        self->modes.m##name = val; break;

#define MOUSE_MODE(name, attr, value) \
    case name: \
        self->modes.attr = val ? value : 0; break;

    bool private;
    switch(mode) {
        SIMPLE_MODE(LNM)
        SIMPLE_MODE(PASTE_EVENTS)
        SIMPLE_MODE(IRM)
        SIMPLE_MODE(DECARM)
        SIMPLE_MODE(BRACKETED_PASTE)
        SIMPLE_MODE(FOCUS_TRACKING)
        SIMPLE_MODE(COLOR_PREFERENCE_NOTIFICATION)
        SIMPLE_MODE(HANDLE_TERMIOS_SIGNALS)
        MOUSE_MODE(MOUSE_BUTTON_TRACKING, mouse_tracking_mode, BUTTON_MODE)
        MOUSE_MODE(MOUSE_MOTION_TRACKING, mouse_tracking_mode, MOTION_MODE)
        MOUSE_MODE(MOUSE_MOVE_TRACKING, mouse_tracking_mode, ANY_MODE)
        MOUSE_MODE(MOUSE_UTF8_MODE, mouse_tracking_protocol, UTF8_PROTOCOL)
        MOUSE_MODE(MOUSE_SGR_MODE, mouse_tracking_protocol, SGR_PROTOCOL)
        MOUSE_MODE(MOUSE_SGR_PIXEL_MODE, mouse_tracking_protocol, SGR_PIXEL_PROTOCOL)
        MOUSE_MODE(MOUSE_URXVT_MODE, mouse_tracking_protocol, URXVT_PROTOCOL)

        case DECSCLM:
        case DECNRCM:
            break;  // we ignore these modes
        case DECCKM:
            self->modes.mDECCKM = val;
            break;
        case DECTCEM:
            self->modes.mDECTCEM = val;
            break;
        case DECSCNM:
            // Render screen in reverse video
            if (self->modes.mDECSCNM != val) {
                self->modes.mDECSCNM = val;
                self->is_dirty = true;
            }
            break;
        case DECOM:
            self->modes.mDECOM = val;
            // According to `vttest`, DECOM should also home the cursor, see
            // vttest/main.c:369.
            screen_cursor_position(self, 1, 1);
            break;
        case DECAWM:
            self->modes.mDECAWM = val; break;
        case DECCOLM:
            self->modes.mDECCOLM = val;
            if (val) {
                // When DECCOLM mode is set, the screen is erased and the cursor
                // moves to the home position.
                screen_erase_in_display(self, 2, false);
                screen_cursor_position(self, 1, 1);
            }
            break;
        case CONTROL_CURSOR_BLINK:
            self->cursor->non_blinking = !val;
            break;
        case SAVE_CURSOR:
            screen_save_cursor(self);
            break;
        case TOGGLE_ALT_SCREEN_1:
        case TOGGLE_ALT_SCREEN_2:
        case ALTERNATE_SCREEN:
            if (val && self->linebuf == self->main_linebuf) screen_toggle_screen_buffer(self, mode == ALTERNATE_SCREEN, mode == ALTERNATE_SCREEN);
            else if (!val && self->linebuf != self->main_linebuf) screen_toggle_screen_buffer(self, mode == ALTERNATE_SCREEN, mode == ALTERNATE_SCREEN);
            break;
        case 7727 << 5:
            log_error("Application escape mode is not supported, the extended keyboard protocol should be used instead");
            break;
        case PENDING_MODE << 5:
            if (!screen_pause_rendering(self, val, 0)) {
                log_error("Pending mode change to already current mode (%d) requested. Either pending mode expired or there is an application bug.", val);
            }
            break;
        case INBAND_RESIZE_NOTIFICATION:
            self->modes.mINBAND_RESIZE_NOTIFICATION = val;
            if (val) CALLBACK("notify_child_of_resize", NULL);
            break;
        default:
            private = mode >= 1 << 5;
            if (private) mode >>= 5;
            log_error("%s %s %u %s", ERROR_PREFIX, "Unsupported screen mode: ", mode, private ? "(private)" : "");
    }
#undef SIMPLE_MODE
#undef MOUSE_MODE
}

void
screen_set_mode(Screen *self, unsigned int mode) {
    set_mode_from_const(self, mode, true);
}

void
screen_decsace(Screen *self, unsigned int val) {
    self->modes.mDECSACE = val == 2 ? true : false;
}

void
screen_reset_mode(Screen *self, unsigned int mode) {
    set_mode_from_const(self, mode, false);
}

void
screen_modify_other_keys(Screen *self, unsigned val, unsigned val2) {
    // Only report an error about modifyOtherKeys if the kitty keyboard
    // protocol is not in effect and the application is trying to turn it on.
    // There are some applications that try to enable both.
    if (
        self->test_child == Py_None && !screen_current_key_encoding_flags(self) &&
        val == 4 && val2 != INT_MAX && val2 != 0
    ) {
        log_error("The application is trying to use xterm's modifyOtherKeys. This is superseded by the kitty keyboard protocol https://sw.kovidgoyal.net/kitty/keyboard-protocol. The application should be updated to use that.");
    }
}

uint8_t
screen_current_key_encoding_flags(Screen *self) {
    for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) {
        if (self->key_encoding_flags[i] & 0x80) return self->key_encoding_flags[i] & 0x7f;
    }
    return 0;
}

void
screen_report_key_encoding_flags(Screen *self) {
    char buf[16] = {0};
    debug_input("\x1b[35mReporting key encoding flags: %u\x1b[39m\n", screen_current_key_encoding_flags(self));
    snprintf(buf, sizeof(buf), "?%uu", screen_current_key_encoding_flags(self));
    write_escape_code_to_child(self, ESC_CSI, buf);
}

void
screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how) {
    unsigned idx = 0;
    for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) {
        if (self->key_encoding_flags[i] & 0x80) { idx = i; break; }
    }
    uint8_t q = val & 0x7f;
    if (how == 1) self->key_encoding_flags[idx] = q;
    else if (how == 2) self->key_encoding_flags[idx] |= q;
    else if (how == 3) self->key_encoding_flags[idx] &= ~q;
    self->key_encoding_flags[idx] |= 0x80;
    debug_input("\x1b[35mSet key encoding flags to: %u\x1b[39m\n", screen_current_key_encoding_flags(self));
}

void
screen_push_key_encoding_flags(Screen *self, uint32_t val) {
    uint8_t q = val & 0x7f;
    const unsigned sz = arraysz(self->main_key_encoding_flags);
    unsigned current_idx = 0;
    for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) {
        if (self->key_encoding_flags[i] & 0x80) { current_idx = i; break; }
    }
    if (current_idx == sz - 1) memmove(self->key_encoding_flags, self->key_encoding_flags + 1, (sz - 1) * sizeof(self->main_key_encoding_flags[0]));
    else self->key_encoding_flags[current_idx++] |= 0x80;
    self->key_encoding_flags[current_idx] = 0x80 | q;
    debug_input("\x1b[35mPushed key encoding flags to: %u\x1b[39m\n", screen_current_key_encoding_flags(self));
}

void
screen_pop_key_encoding_flags(Screen *self, uint32_t num) {
    for (unsigned i = arraysz(self->main_key_encoding_flags); num && i-- > 0; ) {
        if (self->key_encoding_flags[i] & 0x80) { num--; self->key_encoding_flags[i] = 0; }
    }
    debug_input("\x1b[35mPopped key encoding flags to: %u\x1b[39m\n", screen_current_key_encoding_flags(self));
}

// }}}

// Cursor {{{

MouseShape
screen_pointer_shape(Screen *self) {
    if (self->linebuf == self->main_linebuf) {
        if (self->main_pointer_shape_stack.count) return self->main_pointer_shape_stack.stack[self->main_pointer_shape_stack.count-1];
    } else {
        if (self->alternate_pointer_shape_stack.count) return self->alternate_pointer_shape_stack.stack[self->alternate_pointer_shape_stack.count-1];
    }
    return INVALID_POINTER;
}

static PyObject*
current_pointer_shape(Screen *self, PyObject *args UNUSED) {
    MouseShape s = screen_pointer_shape(self);
    const char *ans = "0";
    switch(s) {
        case INVALID_POINTER: break;
        /* start enum to css (auto generated by gen-key-constants.py do not edit) */
        case DEFAULT_POINTER: ans = "default"; break;
        case TEXT_POINTER: ans = "text"; break;
        case POINTER_POINTER: ans = "pointer"; break;
        case HELP_POINTER: ans = "help"; break;
        case WAIT_POINTER: ans = "wait"; break;
        case PROGRESS_POINTER: ans = "progress"; break;
        case CROSSHAIR_POINTER: ans = "crosshair"; break;
        case CELL_POINTER: ans = "cell"; break;
        case VERTICAL_TEXT_POINTER: ans = "vertical-text"; break;
        case MOVE_POINTER: ans = "move"; break;
        case E_RESIZE_POINTER: ans = "e-resize"; break;
        case NE_RESIZE_POINTER: ans = "ne-resize"; break;
        case NW_RESIZE_POINTER: ans = "nw-resize"; break;
        case N_RESIZE_POINTER: ans = "n-resize"; break;
        case SE_RESIZE_POINTER: ans = "se-resize"; break;
        case SW_RESIZE_POINTER: ans = "sw-resize"; break;
        case S_RESIZE_POINTER: ans = "s-resize"; break;
        case W_RESIZE_POINTER: ans = "w-resize"; break;
        case EW_RESIZE_POINTER: ans = "ew-resize"; break;
        case NS_RESIZE_POINTER: ans = "ns-resize"; break;
        case NESW_RESIZE_POINTER: ans = "nesw-resize"; break;
        case NWSE_RESIZE_POINTER: ans = "nwse-resize"; break;
        case ZOOM_IN_POINTER: ans = "zoom-in"; break;
        case ZOOM_OUT_POINTER: ans = "zoom-out"; break;
        case ALIAS_POINTER: ans = "alias"; break;
        case COPY_POINTER: ans = "copy"; break;
        case NOT_ALLOWED_POINTER: ans = "not-allowed"; break;
        case NO_DROP_POINTER: ans = "no-drop"; break;
        case GRAB_POINTER: ans = "grab"; break;
        case GRABBING_POINTER: ans = "grabbing"; break;
/* end enum to css */
    }
    return PyUnicode_FromString(ans);
}

static PyObject*
change_pointer_shape(Screen *self, PyObject *args) {
    char op; const char *css_name, *b;
    if (!PyArg_ParseTuple(args, "ss", &b, &css_name)) return NULL;
    op = b[0];
    uint8_t *count, *stack;
    if (self->main_linebuf == self->linebuf) { count = &self->main_pointer_shape_stack.count; stack = self->main_pointer_shape_stack.stack; }
    else { count = &self->alternate_pointer_shape_stack.count; stack = self->alternate_pointer_shape_stack.stack; }
    if (op == '<') {
        if (*count) *count -= 1;
    } else {
        MouseShape s = INVALID_POINTER;
        if (css_name[0] == 0) s = INVALID_POINTER;
        /* start css to enum (auto generated by gen-key-constants.py do not edit) */
        else if (strcmp("default", css_name) == 0) s = DEFAULT_POINTER;
        else if (strcmp("text", css_name) == 0) s = TEXT_POINTER;
        else if (strcmp("pointer", css_name) == 0) s = POINTER_POINTER;
        else if (strcmp("help", css_name) == 0) s = HELP_POINTER;
        else if (strcmp("wait", css_name) == 0) s = WAIT_POINTER;
        else if (strcmp("progress", css_name) == 0) s = PROGRESS_POINTER;
        else if (strcmp("crosshair", css_name) == 0) s = CROSSHAIR_POINTER;
        else if (strcmp("cell", css_name) == 0) s = CELL_POINTER;
        else if (strcmp("vertical-text", css_name) == 0) s = VERTICAL_TEXT_POINTER;
        else if (strcmp("move", css_name) == 0) s = MOVE_POINTER;
        else if (strcmp("e-resize", css_name) == 0) s = E_RESIZE_POINTER;
        else if (strcmp("ne-resize", css_name) == 0) s = NE_RESIZE_POINTER;
        else if (strcmp("nw-resize", css_name) == 0) s = NW_RESIZE_POINTER;
        else if (strcmp("n-resize", css_name) == 0) s = N_RESIZE_POINTER;
        else if (strcmp("se-resize", css_name) == 0) s = SE_RESIZE_POINTER;
        else if (strcmp("sw-resize", css_name) == 0) s = SW_RESIZE_POINTER;
        else if (strcmp("s-resize", css_name) == 0) s = S_RESIZE_POINTER;
        else if (strcmp("w-resize", css_name) == 0) s = W_RESIZE_POINTER;
        else if (strcmp("ew-resize", css_name) == 0) s = EW_RESIZE_POINTER;
        else if (strcmp("ns-resize", css_name) == 0) s = NS_RESIZE_POINTER;
        else if (strcmp("nesw-resize", css_name) == 0) s = NESW_RESIZE_POINTER;
        else if (strcmp("nwse-resize", css_name) == 0) s = NWSE_RESIZE_POINTER;
        else if (strcmp("zoom-in", css_name) == 0) s = ZOOM_IN_POINTER;
        else if (strcmp("zoom-out", css_name) == 0) s = ZOOM_OUT_POINTER;
        else if (strcmp("alias", css_name) == 0) s = ALIAS_POINTER;
        else if (strcmp("copy", css_name) == 0) s = COPY_POINTER;
        else if (strcmp("not-allowed", css_name) == 0) s = NOT_ALLOWED_POINTER;
        else if (strcmp("no-drop", css_name) == 0) s = NO_DROP_POINTER;
        else if (strcmp("grab", css_name) == 0) s = GRAB_POINTER;
        else if (strcmp("grabbing", css_name) == 0) s = GRABBING_POINTER;
        else if (strcmp("left_ptr", css_name) == 0) s = DEFAULT_POINTER;
        else if (strcmp("xterm", css_name) == 0) s = TEXT_POINTER;
        else if (strcmp("ibeam", css_name) == 0) s = TEXT_POINTER;
        else if (strcmp("pointing_hand", css_name) == 0) s = POINTER_POINTER;
        else if (strcmp("hand2", css_name) == 0) s = POINTER_POINTER;
        else if (strcmp("hand", css_name) == 0) s = POINTER_POINTER;
        else if (strcmp("question_arrow", css_name) == 0) s = HELP_POINTER;
        else if (strcmp("whats_this", css_name) == 0) s = HELP_POINTER;
        else if (strcmp("clock", css_name) == 0) s = WAIT_POINTER;
        else if (strcmp("watch", css_name) == 0) s = WAIT_POINTER;
        else if (strcmp("half-busy", css_name) == 0) s = PROGRESS_POINTER;
        else if (strcmp("left_ptr_watch", css_name) == 0) s = PROGRESS_POINTER;
        else if (strcmp("tcross", css_name) == 0) s = CROSSHAIR_POINTER;
        else if (strcmp("plus", css_name) == 0) s = CELL_POINTER;
        else if (strcmp("cross", css_name) == 0) s = CELL_POINTER;
        else if (strcmp("fleur", css_name) == 0) s = MOVE_POINTER;
        else if (strcmp("pointer-move", css_name) == 0) s = MOVE_POINTER;
        else if (strcmp("right_side", css_name) == 0) s = E_RESIZE_POINTER;
        else if (strcmp("top_right_corner", css_name) == 0) s = NE_RESIZE_POINTER;
        else if (strcmp("top_left_corner", css_name) == 0) s = NW_RESIZE_POINTER;
        else if (strcmp("top_side", css_name) == 0) s = N_RESIZE_POINTER;
        else if (strcmp("bottom_right_corner", css_name) == 0) s = SE_RESIZE_POINTER;
        else if (strcmp("bottom_left_corner", css_name) == 0) s = SW_RESIZE_POINTER;
        else if (strcmp("bottom_side", css_name) == 0) s = S_RESIZE_POINTER;
        else if (strcmp("left_side", css_name) == 0) s = W_RESIZE_POINTER;
        else if (strcmp("sb_h_double_arrow", css_name) == 0) s = EW_RESIZE_POINTER;
        else if (strcmp("split_h", css_name) == 0) s = EW_RESIZE_POINTER;
        else if (strcmp("sb_v_double_arrow", css_name) == 0) s = NS_RESIZE_POINTER;
        else if (strcmp("split_v", css_name) == 0) s = NS_RESIZE_POINTER;
        else if (strcmp("size_bdiag", css_name) == 0) s = NESW_RESIZE_POINTER;
        else if (strcmp("size-bdiag", css_name) == 0) s = NESW_RESIZE_POINTER;
        else if (strcmp("size_fdiag", css_name) == 0) s = NWSE_RESIZE_POINTER;
        else if (strcmp("size-fdiag", css_name) == 0) s = NWSE_RESIZE_POINTER;
        else if (strcmp("zoom_in", css_name) == 0) s = ZOOM_IN_POINTER;
        else if (strcmp("zoom_out", css_name) == 0) s = ZOOM_OUT_POINTER;
        else if (strcmp("dnd-link", css_name) == 0) s = ALIAS_POINTER;
        else if (strcmp("dnd-copy", css_name) == 0) s = COPY_POINTER;
        else if (strcmp("forbidden", css_name) == 0) s = NOT_ALLOWED_POINTER;
        else if (strcmp("crossed_circle", css_name) == 0) s = NOT_ALLOWED_POINTER;
        else if (strcmp("dnd-no-drop", css_name) == 0) s = NO_DROP_POINTER;
        else if (strcmp("openhand", css_name) == 0) s = GRAB_POINTER;
        else if (strcmp("hand1", css_name) == 0) s = GRAB_POINTER;
        else if (strcmp("closedhand", css_name) == 0) s = GRABBING_POINTER;
        else if (strcmp("dnd-none", css_name) == 0) s = GRABBING_POINTER;
/* end css to enum */
        if (s == INVALID_POINTER && css_name[0] != 0) { PyErr_Format(PyExc_KeyError, "Not a known pointer shape: %s", css_name); return NULL; }
        if (op == '=') {
            if (!*count) *count += 1;
            stack[*count - 1] = s;
        } else if (op == '>') {
            if ((*count + 1u) >= arraysz(self->main_pointer_shape_stack.stack)) {
                remove_i_from_array(stack, 0, *count);
            }
            *count += 1;
            stack[*count - 1] = s;
        } else {
            PyErr_SetString(PyExc_KeyError, "Not a known stack operation");
            return NULL;
        }
    }
    Py_RETURN_NONE;
}

bool
screen_is_cursor_visible(const Screen *self) {
    return self->paused_rendering.expires_at ? self->paused_rendering.cursor_visible : self->modes.mDECTCEM;
}

void
screen_backspace(Screen *self) {
    screen_cursor_move(self, 1, -1, true);
}

void
screen_tab(Screen *self) {
    // Move to the next tab space, or the end of the screen if there aren't anymore left.
    unsigned int found = 0;
    for (unsigned int i = self->cursor->x + 1; i < self->columns; i++) {
        if (self->tabstops[i]) { found = i; break; }
    }
    if (!found) found = self->columns - 1;
    if (found != self->cursor->x) {
        if (self->cursor->x < self->columns) {
            CPUCell *cpu_cell = linebuf_cpu_cells_for_line(self->linebuf, self->cursor->y) + self->cursor->x;
            combining_type diff = found - self->cursor->x;
            bool ok = true;
            for (combining_type i = 0; i < diff; i++) {
                CPUCell *c = cpu_cell + i;
                if (cell_has_text(c) && !cell_is_char(c, ' ')) { ok = false; break; }
            }
            if (ok) {
                for (combining_type i = 0; i < diff; i++) {
                    CPUCell *c = cpu_cell + i;
                    cell_set_char(c, ' ');
                }
                self->lc->count = 2; self->lc->chars[0] = '\t'; self->lc->chars[1] = diff;
                cell_set_chars(cpu_cell, self->text_cache, self->lc);
            }
        }
        self->cursor->x = found;
    }
}

void
screen_backtab(Screen *self, unsigned int count) {
    // Move back count tabs
    if (!count) count = 1;
    int i;
    while (count > 0 && self->cursor->x > 0) {
        count--;
        for (i = self->cursor->x - 1; i >= 0; i--) {
            if (self->tabstops[i]) { self->cursor->x = i; break; }
        }
        if (i <= 0) self->cursor->x = 0;
    }
}

void
screen_clear_tab_stop(Screen *self, unsigned int how) {
    switch(how) {
        case 0:
            if (self->cursor->x < self->columns) self->tabstops[self->cursor->x] = false;
            break;
        case 2:
            break;  // no-op
        case 3:
            for (unsigned int i = 0; i < self->columns; i++) self->tabstops[i] = false;
            break;
        default:
            log_error("%s %s %u", ERROR_PREFIX, "Unsupported clear tab stop mode: ", how);
            break;
    }
}

void
screen_set_tab_stop(Screen *self) {
    if (self->cursor->x < self->columns)
        self->tabstops[self->cursor->x] = true;
}

void
screen_reset_tab_stops(Screen *self) {
    init_tabstops(self->tabstops, self->columns);
}

void
screen_cursor_move(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/, bool allow_move_to_previous_line) {
    if (count == 0) count = 1;
    bool in_margins = cursor_within_margins(self);
    if (move_direction > 0) {
        self->cursor->x += count;
        screen_ensure_bounds(self, false, in_margins);
    } else {
        index_type top = in_margins && self->modes.mDECOM ? self->margin_top : 0;
        while (count > 0) {
            if (count <= self->cursor->x) {
                self->cursor->x -= count;
                count = 0;
            } else {
                if (self->cursor->x > 0) {
                    count -= self->cursor->x;
                    self->cursor->x = 0;
                } else {
                    if (self->cursor->y == top || !allow_move_to_previous_line) count = 0;
                    else {
                        count--; self->cursor->y--;
                        self->cursor->x = self->columns-1;
                    }
                }
            }
        }
    }
}

void
screen_cursor_forward(Screen *self, unsigned int count/*=1*/) {
    screen_cursor_move(self, count, 1, false);
}

void
screen_cursor_up(Screen *self, unsigned int count/*=1*/, bool do_carriage_return/*=false*/, int move_direction/*=-1*/) {
    bool in_margins = cursor_within_margins(self);
    if (count == 0) count = 1;
    if (move_direction < 0 && count > self->cursor->y) self->cursor->y = 0;
    else self->cursor->y += move_direction * count;
    if (do_carriage_return) self->cursor->x = 0;
    screen_ensure_bounds(self, true, in_margins);
}

void
screen_cursor_up1(Screen *self, unsigned int count/*=1*/) {
    screen_cursor_up(self, count, true, -1);
}

void
screen_cursor_down(Screen *self, unsigned int count/*=1*/) {
    screen_cursor_up(self, count, false, 1);
}

void
screen_cursor_down1(Screen *self, unsigned int count/*=1*/) {
    screen_cursor_up(self, count, true, 1);
}

void
screen_cursor_to_column(Screen *self, unsigned int column) {
    unsigned int x = MAX(column, 1u) - 1;
    if (x != self->cursor->x) {
        self->cursor->x = x;
        screen_ensure_bounds(self, false, cursor_within_margins(self));
    }
}

#define INDEX_UP(add_to_history) \
    linebuf_index(self->linebuf, top, bottom); \
    INDEX_GRAPHICS(-1) \
    if (add_to_history) { \
        /* Only add to history when no top margin has been set */ \
        linebuf_init_line(self->linebuf, bottom); \
        historybuf_add_line(self->historybuf, self->linebuf->line, &self->as_ansi_buf); \
        self->history_line_added_count++; \
        if (self->last_visited_prompt.is_set) { \
            if (self->last_visited_prompt.scrolled_by < self->historybuf->count) self->last_visited_prompt.scrolled_by++; \
            else self->last_visited_prompt.is_set = false; \
        } \
    } \
    linebuf_clear_line(self->linebuf, bottom, true); \
    self->is_dirty = true; \
    index_selection(self, &self->selections, true, top, bottom); \
    clear_selection(&self->url_ranges);

void
screen_index(Screen *self) {
    // Move cursor down one line, scrolling screen if needed
    unsigned int top = self->margin_top, bottom = self->margin_bottom;
    if (self->cursor->y == bottom) {
        const bool add_to_history = self->linebuf == self->main_linebuf && self->margin_top == 0;
        INDEX_UP(add_to_history);
    } else screen_cursor_down(self, 1);
}

static void
screen_index_without_adding_to_history(Screen *self) {
    // Move cursor down one line, scrolling screen if needed
    unsigned int top = self->margin_top, bottom = self->margin_bottom;
    if (self->cursor->y == bottom) {
        INDEX_UP(false);
    } else screen_cursor_down(self, 1);
}


void
screen_scroll(Screen *self, unsigned int count) {
    // Scroll the screen up by count lines, not moving the cursor
    unsigned int top = self->margin_top, bottom = self->margin_bottom;
    const bool add_to_history = self->linebuf == self->main_linebuf && self->margin_top == 0;
    while (count > 0) {
        count--;
        INDEX_UP(add_to_history);
    }
}

void
screen_reverse_index(Screen *self) {
    // Move cursor up one line, scrolling screen if needed
    unsigned int top = self->margin_top, bottom = self->margin_bottom;
    if (self->cursor->y == top) {
        INDEX_DOWN;
    } else screen_cursor_up(self, 1, false, -1);
}

static void
_reverse_scroll(Screen *self, unsigned int count, bool fill_from_scrollback) {
    // Scroll the screen down by count lines, not moving the cursor
    unsigned int top = self->margin_top, bottom = self->margin_bottom;
    fill_from_scrollback = fill_from_scrollback && self->linebuf == self->main_linebuf;
    if (fill_from_scrollback) {
        unsigned limit = MAX(self->lines, self->historybuf->count);
        count = MIN(limit, count);
    } else count = MIN(self->lines, count);
    while (count-- > 0) {
        bool copied = false;
        if (fill_from_scrollback) copied = historybuf_pop_line(self->historybuf, self->alt_linebuf->line);
        INDEX_DOWN;
        if (copied) linebuf_copy_line_to(self->main_linebuf, self->alt_linebuf->line, 0);
    }
}

void
screen_reverse_scroll(Screen *self, unsigned int count) {
    _reverse_scroll(self, count, false);
}

void
screen_reverse_scroll_and_fill_from_scrollback(Screen *self, unsigned int count) {
    _reverse_scroll(self, count, true);
}


void
screen_carriage_return(Screen *self) {
    self->cursor->x = 0;
}

void
screen_linefeed(Screen *self) {
    bool in_margins = cursor_within_margins(self);
    screen_index(self);
    if (self->modes.mLNM) screen_carriage_return(self);
    screen_ensure_bounds(self, false, in_margins);
}

#define buffer_push(self, ans) { \
    ans = (self)->buf + (((self)->start_of_data + (self)->count) % SAVEPOINTS_SZ); \
    if ((self)->count == SAVEPOINTS_SZ) (self)->start_of_data = ((self)->start_of_data + 1) % SAVEPOINTS_SZ; \
    else (self)->count++; \
}

#define buffer_pop(self, ans) { \
    if ((self)->count == 0) ans = NULL; \
    else { \
        (self)->count--; \
        ans = (self)->buf + (((self)->start_of_data + (self)->count) % SAVEPOINTS_SZ); \
    } \
}

void
screen_save_cursor(Screen *self) {
    Savepoint *sp = self->linebuf == self->main_linebuf ? &self->main_savepoint : &self->alt_savepoint;
    cursor_copy_to(self->cursor, &(sp->cursor));
    sp->mDECOM = self->modes.mDECOM;
    sp->mDECAWM = self->modes.mDECAWM;
    sp->mDECSCNM = self->modes.mDECSCNM;
    memcpy(&sp->charset, &self->charset, sizeof(self->charset));
    sp->is_valid = true;
}

static void
copy_specific_mode(Screen *self, unsigned int mode, const ScreenModes *src, ScreenModes *dest) {
#define SIMPLE_MODE(name) case name: dest->m##name = src->m##name; break;
#define SIDE_EFFECTS(name) case name: if (do_side_effects) set_mode_from_const(self, name, src->m##name); else dest->m##name = src->m##name; break;

    const bool do_side_effects = dest == &self->modes;

    switch(mode) {
        SIMPLE_MODE(LNM)  // kitty extension
        SIMPLE_MODE(IRM)  // kitty extension
        SIMPLE_MODE(DECARM)
        SIMPLE_MODE(BRACKETED_PASTE)
        SIMPLE_MODE(FOCUS_TRACKING)
        SIMPLE_MODE(COLOR_PREFERENCE_NOTIFICATION)
        SIMPLE_MODE(PASTE_EVENTS)
        SIMPLE_MODE(INBAND_RESIZE_NOTIFICATION)
        SIMPLE_MODE(DECCKM)
        SIMPLE_MODE(DECTCEM)
        SIMPLE_MODE(DECAWM)
        case MOUSE_BUTTON_TRACKING: case MOUSE_MOTION_TRACKING: case MOUSE_MOVE_TRACKING:
            dest->mouse_tracking_mode = src->mouse_tracking_mode; break;
        case MOUSE_UTF8_MODE: case MOUSE_SGR_MODE: case MOUSE_URXVT_MODE:
            dest->mouse_tracking_protocol = src->mouse_tracking_protocol; break;
        case DECSCLM:
        case DECNRCM:
            break;  // we ignore these modes
        case DECSCNM:
            if (dest->mDECSCNM != src->mDECSCNM) {
                dest->mDECSCNM = src->mDECSCNM;
                if (do_side_effects) self->is_dirty = true;
            }
            break;
        SIDE_EFFECTS(DECOM)
        SIDE_EFFECTS(DECCOLM)
    }
#undef SIMPLE_MODE
#undef SIDE_EFFECTS
}

void
screen_save_mode(Screen *self, unsigned int mode) { // XTSAVE
    copy_specific_mode(self, mode, &self->modes, &self->saved_modes);
}

void
screen_restore_mode(Screen *self, unsigned int mode) { // XTRESTORE
    copy_specific_mode(self, mode, &self->saved_modes, &self->modes);
}

static void
copy_specific_modes(Screen *self, const ScreenModes *src, ScreenModes *dest) {
    copy_specific_mode(self, LNM, src, dest);
    copy_specific_mode(self, IRM, src, dest);
    copy_specific_mode(self, DECARM, src, dest);
    copy_specific_mode(self, BRACKETED_PASTE, src, dest);
    copy_specific_mode(self, FOCUS_TRACKING, src, dest);
    copy_specific_mode(self, COLOR_PREFERENCE_NOTIFICATION, src, dest);
    copy_specific_mode(self, INBAND_RESIZE_NOTIFICATION, src, dest);
    copy_specific_mode(self, PASTE_EVENTS, src, dest);
    copy_specific_mode(self, DECCKM, src, dest);
    copy_specific_mode(self, DECTCEM, src, dest);
    copy_specific_mode(self, DECAWM, src, dest);
    copy_specific_mode(self, MOUSE_BUTTON_TRACKING, src, dest);
    copy_specific_mode(self, MOUSE_UTF8_MODE, src, dest);
    copy_specific_mode(self, DECSCNM, src, dest);
}

void
screen_save_modes(Screen *self) {
    // kitty extension to XTSAVE that saves a bunch of no side-effect modes
    copy_specific_modes(self, &self->modes, &self->saved_modes);
}

void
screen_restore_cursor(Screen *self) {
    Savepoint *sp = self->linebuf == self->main_linebuf ? &self->main_savepoint : &self->alt_savepoint;
    if (!sp->is_valid) {
        screen_cursor_position(self, 1, 1);
        screen_reset_mode(self, DECOM);
        screen_reset_mode(self, DECSCNM);
        zero_at_ptr(&self->charset);
    } else {
        set_mode_from_const(self, DECOM, sp->mDECOM);
        set_mode_from_const(self, DECAWM, sp->mDECAWM);
        set_mode_from_const(self, DECSCNM, sp->mDECSCNM);
        cursor_copy_to(&(sp->cursor), self->cursor);
        memcpy(&self->charset, &sp->charset, sizeof(self->charset));
        screen_ensure_bounds(self, false, false);
    }
}

void
screen_restore_modes(Screen *self) {
    // kitty extension to XTRESTORE that saves a bunch of no side-effect modes
    copy_specific_modes(self, &self->saved_modes, &self->modes);
}

void
screen_ensure_bounds(Screen *self, bool force_use_margins/*=false*/, bool in_margins) {
    unsigned int top, bottom;
    if (in_margins && (force_use_margins || self->modes.mDECOM)) {
        top = self->margin_top; bottom = self->margin_bottom;
    } else {
        top = 0; bottom = self->lines - 1;
    }
    self->cursor->x = MIN(self->cursor->x, self->columns - 1);
    self->cursor->y = MAX(top, MIN(self->cursor->y, bottom));
}

void
screen_cursor_position(Screen *self, unsigned int line, unsigned int column) {
    bool in_margins = cursor_within_margins(self);
    line = (line == 0 ? 1 : line) - 1;
    column = (column == 0 ? 1: column) - 1;
    if (self->modes.mDECOM) {
        line += self->margin_top;
        line = MAX(self->margin_top, MIN(line, self->margin_bottom));
    }
    self->cursor->position_changed_by_client_at = self->parsing_at;
    self->cursor->x = column; self->cursor->y = line;
    screen_ensure_bounds(self, false, in_margins);
}

void
screen_cursor_to_line(Screen *self, unsigned int line) {
    screen_cursor_position(self, line, self->cursor->x + 1);
}

int
screen_cursor_at_a_shell_prompt(const Screen *self) {
    if (self->cursor->y >= self->lines || self->linebuf != self->main_linebuf || !screen_is_cursor_visible(self)) return -1;
    for (index_type y=self->cursor->y + 1; y-- > 0; ) {
        switch(self->linebuf->line_attrs[y].prompt_kind) {
            case OUTPUT_START:
                return -1;
            case PROMPT_START:
            case SECONDARY_PROMPT:
                return y;
            case UNKNOWN_PROMPT_KIND:
                break;
        }
    }
    return -1;
}

bool
screen_prompt_supports_click_events(const Screen *self, bool *is_relative) {
    *is_relative = (bool) self->prompt_settings.relative_click_events;
    return (bool) self->prompt_settings.supports_click_events;
}

bool
screen_fake_move_cursor_to_position(Screen *self, index_type start_x, index_type start_y) {
    SelectionBoundary a = {.x=start_x, .y=start_y}, b = {.x=self->cursor->x, .y=self->cursor->y};
    SelectionBoundary *start, *end; int key;
    if (a.y < b.y || (a.y == b.y && a.x < b.x)) { start = &a; end = &b; key = GLFW_FKEY_LEFT; }
    else { start = &b; end = &a; key = GLFW_FKEY_RIGHT; }
    unsigned int count = 0;

    for (unsigned y = start->y, x = start->x; y <= end->y && y < self->lines; y++) {
        unsigned x_limit = y == end->y ? end->x : self->columns;
        x_limit = MIN(x_limit, self->columns);
        bool found_non_empty_cell = false;
        while (x < x_limit) {
            const CPUCell *c = linebuf_cpu_cell_at(self->linebuf, x, y);
            if (!cell_has_text(c)) {
                // we only stop counting the cells in the line at an empty cell
                // if at least one non-empty cell is found. zsh uses empty cells
                // between the end of the text ad the right prompt. fish uses empty
                // cells at the start of a line when editing multiline text
                if (!found_non_empty_cell) { x++; continue; }
                count += 1;
                break;
            }
            found_non_empty_cell = true;
            if (c->is_multicell) {
                x += mcd_x_limit(c);
            } else x++;
            count += 1;  // zsh requires a single arrow press to move past dualwidth chars
        }
        if (!found_non_empty_cell) count++;  // blank line
        x = 0;
    }
    if (count) {
        char output[KEY_BUFFER_SIZE+1] = {0};
        if (self->prompt_settings.uses_special_keys_for_cursor_movement) {
            const char *k = key == GLFW_FKEY_RIGHT ? "1" : "1;1";
            int num = snprintf(output, KEY_BUFFER_SIZE, "\x1b[%su", k);
            for (unsigned i = 0; i < count; i++) write_to_child(self, output, num);
        } else {
            GLFWkeyevent ev = { .key = key, .action = GLFW_PRESS };
            int num = encode_glfw_key_event(&ev, false, 0, output);
            if (num != SEND_TEXT_TO_CHILD) {
                for (unsigned i = 0; i < count; i++) write_to_child(self, output, num);
            }
        }
    }
    return count > 0;
}

// }}}

// Editing {{{

void
screen_erase_in_line(Screen *self, unsigned int how, bool private) {
    /*Erases a line in a specific way.

        :param int how: defines the way the line should be erased in:

            * ``0`` -- Erases from cursor to end of line, including cursor
              position.
            * ``1`` -- Erases from beginning of line to cursor,
              including cursor position.
            * ``2`` -- Erases complete line.
        :param bool private: when ``True`` character attributes are left
                             unchanged.
        */
    unsigned int s = 0, n = 0;
    switch(how) {
        case 0:
            s = self->cursor->x;
            n = self->columns - self->cursor->x;
            break;
        case 1:
            n = self->cursor->x + 1;
            break;
        case 2:
            n = self->columns;
            break;
        default:
            break;
    }
    if (n > 0) {
        nuke_multicell_char_intersecting_with(self, s, n, self->cursor->y, self->cursor->y + 1, false);
        screen_dirty_line_graphics(self, self->cursor->y, self->cursor->y, self->linebuf == self->main_linebuf);
        linebuf_init_line(self->linebuf, self->cursor->y);
        if (private) {
            line_clear_text(self->linebuf->line, s, n, BLANK_CHAR);
        } else {
            line_apply_cursor(self->linebuf->line, self->cursor, s, n, true);
        }
        self->is_dirty = true;
        clear_intersecting_selections(self, self->cursor->y);
        linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
    }
}

static void
dirty_scroll(Screen *self) {
    self->scroll_changed = true;
    screen_pause_rendering(self, false, 0);
}

static void
screen_clear_scrollback(Screen *self) {
    historybuf_clear(self->historybuf);
    reset_pixel_scroll(self, 0);
    if (self->scrolled_by != 0) {
        self->scrolled_by = 0;
        dirty_scroll(self);
    }
    LineBuf *orig = self->linebuf; self->linebuf = self->main_linebuf;
    CPUCell *cells = linebuf_cpu_cells_for_line(self->linebuf, 0);
    for (index_type x = 0; x < self->columns; x++) {
        CPUCell *c = cells + x;
        if (c->is_multicell && c->y > 0) {  // multiline char that extended into scrollback
            nuke_multicell_char_at(self, x, 0, false);
        }
    }
    self->linebuf = orig;
}

static Line* visual_line_(Screen *self, int y_);

static void
screen_move_into_scrollback(Screen *self) {
    if (self->linebuf != self->main_linebuf || self->margin_top != 0 || self->margin_bottom != self->lines - 1) return;
    unsigned int num_of_lines_to_move = self->lines;
    while (num_of_lines_to_move) {
        Line *line = visual_line_(self, num_of_lines_to_move-1);
        if (!line_is_empty(line)) break;
        num_of_lines_to_move--;
    }
    if (num_of_lines_to_move) {
        unsigned int top, bottom;
        const bool add_to_history = self->linebuf == self->main_linebuf && self->margin_top == 0;
        for (; num_of_lines_to_move; num_of_lines_to_move--) {
            top = 0, bottom = num_of_lines_to_move - 1;
            INDEX_UP(add_to_history);
        }
    }
}

void
screen_erase_in_display(Screen *self, unsigned int how, bool private) {
    /* Erases display in a specific way.

        :param int how: defines the way the screen should be erased:

            * ``0`` -- Erases from cursor to end of screen, including
              cursor position.
            * ``1`` -- Erases from beginning of screen to cursor,
              including cursor position.
            * ``2`` -- Erases complete display. All lines are erased
              and changed to single-width. Cursor does not move.
            * ``22`` -- Copy screen contents into scrollback if in main screen,
              then do the same as ``2``.
            * ``3`` -- Erase complete display and scrollback buffer as well.
        :param bool private: when ``True`` character attributes are left unchanged
    */
    unsigned int a, b;
    bool nuke_multicell_chars = true;
    switch(how) {
        case 0:
            a = self->cursor->y + 1; b = self->lines; break;
        case 1:
            a = 0; b = self->cursor->y; break;
        case 22:
            screen_move_into_scrollback(self);
            nuke_multicell_chars = false;  // they have been moved into scrollback and we would get double deletions
            how = 2;
            /* fallthrough */
        case 2:
        case 3:
            if (self->extra_cursors.count) {
                self->extra_cursors.count = 0;
                self->extra_cursors.dirty = true;
            }
            grman_clear(self->grman, how == 3, self->cell_size);
            a = 0; b = self->lines; nuke_multicell_chars = false;
            break;
        default:
            return;
    }
    if (b > a) {
        if (how != 3) screen_dirty_line_graphics(self, a, b, self->linebuf == self->main_linebuf);
        if (private) {
            for (unsigned int i=a; i < b; i++) {
                linebuf_init_line(self->linebuf, i);
                line_clear_text(self->linebuf->line, 0, self->columns, BLANK_CHAR);
                linebuf_set_last_char_as_continuation(self->linebuf, i, false);
                linebuf_clear_attrs_and_dirty(self->linebuf, i);
            }
        } else linebuf_clear_lines(self->linebuf, self->cursor, a, b);
        if (nuke_multicell_chars) nuke_multicell_char_intersecting_with(self, 0, self->columns, a, b, false);
        self->is_dirty = true;
        if (selection_intersects_screen_lines(&self->selections, a, b)) clear_selection(&self->selections);
        if (selection_intersects_screen_lines(&self->url_ranges, a, b)) clear_selection(&self->url_ranges);
    }
    if (how < 2) {
        screen_erase_in_line(self, how, private);
        if (how == 1) linebuf_clear_attrs_and_dirty(self->linebuf, self->cursor->y);
    }
    if (how == 3 && self->linebuf == self->main_linebuf) {
        screen_clear_scrollback(self);
    }
}

void
screen_insert_lines(Screen *self, unsigned int count) {
    unsigned int top = self->margin_top, bottom = self->margin_bottom;
    if (count == 0) count = 1;
    if (top <= self->cursor->y && self->cursor->y <= bottom) {
        // remove split multiline chars at top edge
        CPUCell *cells = linebuf_cpu_cells_for_line(self->linebuf, self->cursor->y);
        for (index_type x = 0; x < self->columns; x++) {
            if (cells[x].is_multicell && cells[x].y) nuke_multicell_char_at(self, x, self->cursor->y, false);
        }
        screen_dirty_line_graphics(self, top, bottom, self->linebuf == self->main_linebuf);
        linebuf_insert_lines(self->linebuf, count, self->cursor->y, bottom);
        self->is_dirty = true;
        clear_all_selections(self);
        screen_carriage_return(self);
        // remove split multiline chars at bottom of screen
        cells = linebuf_cpu_cells_for_line(self->linebuf, bottom);
        for (index_type x = 0; x < self->columns; x++) {
            if (cells[x].is_multicell) {
                index_type y_limit = cells[x].scale;
                if (cells[x].y + 1u < y_limit) {
                    index_type orig = self->lines;
                    self->lines = bottom + 1;
                    nuke_multicell_char_at(self, x, bottom, false);
                    self->lines = orig;
                }
            }
        }
    }
}

static void
screen_scroll_until_cursor_prompt(Screen *self, bool add_to_scrollback) {
    bool in_margins = cursor_within_margins(self);
    int q = screen_cursor_at_a_shell_prompt(self);
    unsigned int y = q > -1 ? (unsigned int)q : self->cursor->y;
    unsigned int num_lines_to_scroll = MIN(self->margin_bottom, y);
    unsigned int final_y = num_lines_to_scroll <= self->cursor->y ? self->cursor->y - num_lines_to_scroll : 0;
    self->cursor->y = self->margin_bottom;
    if (add_to_scrollback) while (num_lines_to_scroll--) screen_index(self);
    else while (num_lines_to_scroll--) screen_index_without_adding_to_history(self);
    self->cursor->y = final_y;
    screen_ensure_bounds(self, false, in_margins);
}

static void
screen_delete_lines_impl(Screen *self, index_type start, index_type count, index_type top, index_type bottom) {
    index_type y = start;
    nuke_multiline_char_intersecting_with(self, 0, self->columns, y, y + 1, false);
    y += count;
    y = MIN(bottom, y);
    nuke_multiline_char_intersecting_with(self, 0, self->columns, y, y + 1, false);
    screen_dirty_line_graphics(self, top, bottom, self->linebuf == self->main_linebuf);
    linebuf_delete_lines(self->linebuf, count, start, bottom);
    self->is_dirty = true;
    clear_all_selections(self);
}

void
screen_delete_lines(Screen *self, unsigned int count) {
    unsigned int top = self->margin_top, bottom = self->margin_bottom;
    if (count == 0) count = 1;
    if (top <= self->cursor->y && self->cursor->y <= bottom) {
        screen_delete_lines_impl(self, self->cursor->y, count, top, bottom);
        screen_carriage_return(self);
    }
}

void
screen_insert_characters(Screen *self, unsigned int count) {
    const unsigned int bottom = self->lines ? self->lines - 1 : 0;
    if (count == 0) count = 1;
    if (self->cursor->y <= bottom) {
        unsigned int x = self->cursor->x;
        unsigned int num = MIN(self->columns - x, count);
        insert_characters(self, x, num, self->cursor->y, false);
        linebuf_init_line(self->linebuf, self->cursor->y);
        line_apply_cursor(self->linebuf->line, self->cursor, x, num, true);
        linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
        self->is_dirty = true;
        clear_intersecting_selections(self, self->cursor->y);
    }
}

void
screen_repeat_character(Screen *self, unsigned int count) {
    if (self->last_graphic_char) {
        if (count == 0) count = 1;
        unsigned int num = MIN(count, CSI_REP_MAX_REPETITIONS);
        alignas(64) uint32_t buf[64];
        for (unsigned i = 0; i < arraysz(buf); i++) buf[i] = self->last_graphic_char;
        for (unsigned i = 0; i < num; i += arraysz(buf)) screen_draw_text(self, buf, MIN(num - i, arraysz(buf)));
    }
}

static void
remove_characters(Screen *self, index_type at, index_type num, index_type y, bool replace_with_spaces) {
    // delete num chars at x=at setting them to the value of the num chars at [at + num, at + num + num)
    // multiline chars at x >= at are deleted and multicell chars split at x=at
    // and x=at + num - 1 are deleted
    nuke_multiline_char_intersecting_with(self, at, self->columns, y, y + 1, replace_with_spaces);
    nuke_split_multicell_char_at_left_boundary(self, at, y, replace_with_spaces);
    CPUCell *cp; GPUCell *gp;
    linebuf_init_cells(self->linebuf, y, &cp, &gp);
    // left shift
    for (index_type i = at; i < self->columns - num; i++) {
        cp[i] = cp[i+num]; gp[i] = gp[i+num];
    }
    nuke_incomplete_single_line_multicell_chars_in_range(self, at, self->columns, y, replace_with_spaces);
}

void
screen_delete_characters(Screen *self, unsigned int count) {
    // Delete characters, later characters are moved left
    const unsigned int bottom = self->lines ? self->lines - 1 : 0;
    if (count == 0) count = 1;
    if (self->cursor->y <= bottom) {
        unsigned int x = self->cursor->x;
        unsigned int num = MIN(self->columns - x, count);
        remove_characters(self, x, num, self->cursor->y, false);
        linebuf_init_line(self->linebuf, self->cursor->y);
        line_apply_cursor(self->linebuf->line, self->cursor, self->columns - num, num, true);
        linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
        self->is_dirty = true;
        clear_intersecting_selections(self, self->cursor->y);
    }
}

void
screen_erase_characters(Screen *self, unsigned int count) {
    // Delete characters clearing the cells
    if (count == 0) count = 1;
    unsigned int x = self->cursor->x;
    unsigned int num = MIN(self->columns - x, count);
    nuke_multicell_char_intersecting_with(self, x, x + num, self->cursor->y, self->cursor->y + 1, false);
    linebuf_init_line(self->linebuf, self->cursor->y);
    line_apply_cursor(self->linebuf->line, self->cursor, x, num, true);
    linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
    self->is_dirty = true;
    clear_intersecting_selections(self, self->cursor->y);
}

// }}}

// Device control {{{

bool
screen_invert_colors(Screen *self) {
    return self->paused_rendering.expires_at ? self->paused_rendering.inverted : (self->modes.mDECSCNM ? true : false);
}

void
screen_bell(Screen *self) {
    if (self->ignore_bells.start) {
        monotonic_t now = monotonic();
        if (now < self->ignore_bells.start + self->ignore_bells.duration) {
            self->ignore_bells.start = now;
            return;
        }
        self->ignore_bells.start = 0;
    }
    request_window_attention(self->window_id, OPT(enable_audio_bell));
    if (OPT(visual_bell_duration) > 0.0f) self->start_visual_bell_at = monotonic();
    CALLBACK("on_bell", NULL);
}

void
report_device_attributes(Screen *self, unsigned int mode, char start_modifier) {
    if (mode == 0) {
        switch(start_modifier) {
            case 0:
                CALLBACK("on_da1", NULL);
                break;
            case '>':
                write_escape_code_to_child(self, ESC_CSI, ">1;" xstr(PRIMARY_VERSION) ";" xstr(SECONDARY_VERSION) "c");  // VT-220 + primary version + secondary version
                break;
        }
    }
}

void
screen_xtversion(Screen *self, unsigned int mode) {
    if (mode == 0) {
        write_escape_code_to_child(self, ESC_DCS, ">|kitty(" XT_VERSION ")");
    }
}

void
screen_report_size(Screen *self, unsigned which, unsigned modifier) {
    char buf[32] = {0};
    unsigned code = 0, width = 0, height = 0;
    switch(which) {
        case 14:
            code = 4;
            width = self->cell_size.width * self->columns;
            height = self->cell_size.height * self->lines;
            if (modifier == 2 && self->window_id) {
                OSWindow *osw = os_window_for_kitty_window(self->window_id);
                if (osw) {
                    int w, h, fw, fh; get_os_window_size(osw, &w, &h, &fw, &fh);
                    width = fw; height = fh;
                }
            }
            break;
        case 16:
            code = 6;
            width = self->cell_size.width;
            height = self->cell_size.height;
            break;
        case 18:
            code = 8;
            width = self->columns;
            height = self->lines;
            break;
    }
    if (code) {
        snprintf(buf, sizeof(buf), "%u;%u;%ut", code, height, width);
        write_escape_code_to_child(self, ESC_CSI, buf);
    }
}

void
screen_manipulate_title_stack(Screen *self, unsigned int op, unsigned int which) {
    CALLBACK("manipulate_title_stack", "OOO",
        op == 23 ? Py_True : Py_False,
        which == 0 || which == 2 ? Py_True : Py_False,
        which == 0 || which == 1 ? Py_True : Py_False
    );
}

void
report_device_status(Screen *self, unsigned int which, bool private) {
    unsigned int x, y;
    static char buf[64];
    switch(which) {
        case 5:  // device status
            write_escape_code_to_child(self, ESC_CSI, "0n");
            break;
        case 6:  // cursor position
            x = self->cursor->x; y = self->cursor->y;
            if (x >= self->columns) {
                if (y < self->lines - 1) { x = 0; y++; }
                else x--;
            }
            if (self->modes.mDECOM) y -= MAX(y, self->margin_top);
            // 1-based indexing
            int sz = snprintf(buf, sizeof(buf) - 1, "%s%u;%uR", (private ? "?": ""), y + 1, x + 1);
            if (sz > 0) write_escape_code_to_child(self, ESC_CSI, buf);
            break;
        case 996: // https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
            if (private) {
                CALLBACK("report_color_scheme_preference", NULL);
            } break;
    }
}

void
report_mode_status(Screen *self, unsigned int which, bool private) {
    unsigned int q = private ? which << 5 : which;
    unsigned int ans = 0;
    char buf[50] = {0};
    switch(q) {
#define KNOWN_MODE(x) \
        case x: \
            ans = self->modes.m##x ? 1 : 2; break;
        KNOWN_MODE(LNM);
        KNOWN_MODE(IRM);
        KNOWN_MODE(DECTCEM);
        KNOWN_MODE(DECSCNM);
        KNOWN_MODE(DECOM);
        KNOWN_MODE(DECAWM);
        KNOWN_MODE(DECCOLM);
        KNOWN_MODE(DECARM);
        KNOWN_MODE(DECCKM);
        KNOWN_MODE(BRACKETED_PASTE);
        KNOWN_MODE(FOCUS_TRACKING);
        KNOWN_MODE(COLOR_PREFERENCE_NOTIFICATION);
        KNOWN_MODE(INBAND_RESIZE_NOTIFICATION);
        KNOWN_MODE(PASTE_EVENTS);
#undef KNOWN_MODE
        case ALTERNATE_SCREEN:
            ans = self->linebuf == self->alt_linebuf ? 1 : 2; break;
        case MOUSE_BUTTON_TRACKING:
            ans = self->modes.mouse_tracking_mode == BUTTON_MODE ? 1 : 2; break;
        case MOUSE_MOTION_TRACKING:
            ans = self->modes.mouse_tracking_mode == MOTION_MODE ? 1 : 2; break;
        case MOUSE_MOVE_TRACKING:
            ans = self->modes.mouse_tracking_mode == ANY_MODE ? 1 : 2; break;
        case MOUSE_SGR_MODE:
            ans = self->modes.mouse_tracking_protocol == SGR_PROTOCOL ? 1 : 2; break;
        case MOUSE_UTF8_MODE:
            ans = self->modes.mouse_tracking_protocol == UTF8_PROTOCOL ? 1 : 2; break;
        case MOUSE_SGR_PIXEL_MODE:
            ans = self->modes.mouse_tracking_protocol == SGR_PIXEL_PROTOCOL ? 1 : 2; break;
        case PENDING_UPDATE:
            ans = self->paused_rendering.expires_at ? 1 : 2; break;
    }
    int sz = snprintf(buf, sizeof(buf) - 1, "%s%u;%u$y", (private ? "?" : ""), which, ans);
    if (sz > 0) write_escape_code_to_child(self, ESC_CSI, buf);
}

void
screen_set_margins(Screen *self, unsigned int top, unsigned int bottom) {
    if (!top) top = 1;
    if (!bottom) bottom = self->lines;
    top = MIN(self->lines, top);
    bottom = MIN(self->lines, bottom);
    top--; bottom--;  // 1 based indexing
    if (bottom > top) {
        // Even though VT102 and VT220 require DECSTBM to ignore regions
        // of width less than 2, some programs (like aptitude for example)
        // rely on it. Practicality beats purity.
        self->margin_top = top; self->margin_bottom = bottom;
        // The cursor moves to the home position when the top and
        // bottom margins of the scrolling region (DECSTBM) changes.
        screen_cursor_position(self, 1, 1);
    }
}

void
screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary) {
    uint8_t shape; bool blink;
    switch(secondary) {
        case 0: // DECLL
            break;
        case '"':  // DECCSA
            break;
        case ' ': // DECSCUSR
            shape = 0; blink = true;
            if (mode > 0) {
                blink = mode % 2;
                shape = (mode < 3) ? CURSOR_BLOCK : (mode < 5) ? CURSOR_UNDERLINE : (mode < 7) ? CURSOR_BEAM : NO_CURSOR_SHAPE;
            }
            self->cursor->shape = shape; self->cursor->non_blinking = !blink;
            break;
    }
}

#define NAME multi_cursor_map
#define KEY_TY index_type
#define VAL_TY uint8_t
#include "kitty-verstable.h"

unsigned
screen_multi_cursor_count(const Screen *self) {
    return self->paused_rendering.expires_at ? self->paused_rendering.extra_cursors.count : self->extra_cursors.count;
}

void
screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_params) {
    // printf("%d;", queried_shape); for (unsigned i = 0; i < num_params; i++) {printf("%d:", params[i]);} printf("\n");
    if (!num_params) {
#define pr(...) { int n = snprintf(p, sz - (p - buf), __VA_ARGS__); if (n >= 0 && (unsigned)n <= (sz - (p - buf))) p += n; }
        if (params == NULL) {
            write_escape_code_to_child(self, ESC_CSI, ">1;2;3;29;30;40;100;101 q");
        } else if (queried_shape == 100) {
            size_t sz = self->extra_cursors.count * 32 + 64;
            RAII_ALLOC(char, buf, malloc(sz)); sz -= 4;
            if (buf) {
                char *p = buf + snprintf(buf, sz, ">100;");
                for (unsigned i = 0; i < self->extra_cursors.count; i++) {
                    index_type cell = self->extra_cursors.locations[i].cell, shape = self->extra_cursors.locations[i].shape;
                    index_type y = cell / self->columns, x = cell - (y * self->columns);
                    pr("%d:2:%u:%u;", shape > 3 ? 29 : (int)shape, y+1, x+1);
                }
                if (*(p-1) == ';') p--;
                *(p++) = ' '; *(p++) = 'q'; *(p++) = 0;
                write_escape_code_to_child(self, ESC_CSI, buf);
            }
        } else if (queried_shape == 101) {
            char buf[64], *p = buf; size_t sz = sizeof(buf);
            pr(">101;30:"); DynamicColor ecc = self->extra_cursors.color.text;
#define o() switch(ecc.type) { \
                case COLOR_NOT_SET: pr("0"); break; \
                case COLOR_IS_SPECIAL: pr("1"); break; \
                case COLOR_IS_INDEX: pr("5:%u", ecc.rgb & 0xff); break;  \
                case COLOR_IS_RGB:  pr("2:%u:%u:%u", (ecc.rgb >> 16) & 0xff, (ecc.rgb >> 8) & 0xff, ecc.rgb & 0xff); break; \
            } \

            o(); pr(";40:"); ecc = self->extra_cursors.color.cursor; o();
#undef o
            pr(" q");
            write_escape_code_to_child(self, ESC_CSI, buf);
        }
        return;
#undef pr
    }
    if (queried_shape == 30 || queried_shape == 40) {
        DynamicColor *ecc = queried_shape == 40 ? &self->extra_cursors.color.cursor : &self->extra_cursors.color.text;
        self->extra_cursors.dirty = true;
        switch (params[0]) {
            case 0: ecc->type = COLOR_NOT_SET; break;
            case 1: ecc->type = COLOR_IS_SPECIAL; break;
            case 2: if (num_params > 3) {
                ecc->type = COLOR_IS_RGB;
                ecc->rgb = ((params[1] & 0xff) << 16) | ((params[2] & 0xff) << 8) | (params[3] & 0xff);
            } break;
            case 5: if (num_params > 1) {
                ecc->type = COLOR_IS_INDEX;
                ecc->rgb = params[1] & 0xff;
            } break;
        }
        return;
    }
    uint8_t shape = 0;
    switch(queried_shape) {
        case 29: shape = 4; break;
        case 0: case 1: case 2: case 3: shape = queried_shape; break;
        default: return;
    }
    self->extra_cursors.dirty = true;
    int type = params[0]; params++; num_params--;
    int extra[2];
    switch (type) {
    case 0:
        extra[0] = MIN(self->cursor->y, self->lines-1) + 1;
        extra[1] = MIN(self->cursor->x, self->columns-1) + 1;
        params = extra; num_params = 2;
        /* fallthrough */
    case 2: {
        multi_cursor_map s; vt_init(&s);
        for (unsigned i = 0; i < self->extra_cursors.count; i++) {
            vt_insert(&s, self->extra_cursors.locations[i].cell, self->extra_cursors.locations[i].shape);
        }
        for (unsigned i = 0; i+1 < num_params; i+=2) {
            index_type y = params[i]-1, x = params[i+1]-1;
            if (!shape) { vt_erase(&s, y * self->columns + x); }
            else if (y < self->lines && x < self->columns) vt_insert(&s, y * self->columns + x, shape);
        }
        self->extra_cursors.count = vt_size(&s);
        ensure_space_for(&self->extra_cursors, locations, ExtraCursor, self->extra_cursors.count, capacity, 20 * 80, false);
        self->extra_cursors.count = 0;
        vt_create_for_loop(multi_cursor_map_itr, i, &s) {
            self->extra_cursors.locations[self->extra_cursors.count++] = (ExtraCursor){
                .shape = i.data->val, .cell = i.data->key};
        }
        vt_cleanup(&s);
    } break;
    case 4: {
        if (num_params < 4) {  // full screen
            switch(shape) {
                default: self->extra_cursors.count = 0; break;
                case 1: case 2: case 3: case 4:
                    ensure_space_for(&self->extra_cursors, locations, ExtraCursor, self->lines * self->columns, capacity, 20 * 80, false);
                    self->extra_cursors.count = self->lines * self->columns;
                    for (index_type cell = 0; cell < self->lines * self->columns; cell++) {
                        self->extra_cursors.locations[cell].shape = shape;
                        self->extra_cursors.locations[cell].cell = cell;
                    }
                    break;
            }
            break;
        }
        unsigned count = 0;
        for (unsigned i = 0; i < self->extra_cursors.count; i++) {
            bool in_some_region = false;
            index_type y = self->extra_cursors.locations[i].cell / self->columns, x = self->extra_cursors.locations[i].cell - (self->columns * y);
            for (unsigned i = 0; i + 3 < num_params && !in_some_region; i += 4) {
                index_type top = params[i]-1, left = params[i+1]-1, bottom = params[i+2]-1, right = params[i+3]-1;
                in_some_region = top <= y && y <= bottom && left <= x && x <= right;
            }
            if (!in_some_region) self->extra_cursors.locations[count++] = self->extra_cursors.locations[i];
        }
        self->extra_cursors.count = count;
        if (shape) {
            for (unsigned i = 0; i + 3 < num_params; i += 4) {
                index_type top = params[i]-1, left = params[i+1]-1, bottom = params[i+2]-1, right = params[i+3]-1;
                bottom = MIN(bottom, self->lines-1); right = MIN(right, self->columns -1);
                if (right < left || bottom < top) continue;
                size_t xnum = right + 1 - left, ynum = bottom + 1 - top;
                ensure_space_for(&self->extra_cursors, locations, ExtraCursor,
                        self->extra_cursors.count + xnum * ynum, capacity, 20 * 80, false);
                for (index_type y = top; y <= bottom; y++) {
                    for (index_type x = left; x <= right; x++) {
                        self->extra_cursors.locations[self->extra_cursors.count++] = (ExtraCursor){
                            .shape=shape, .cell=y*self->columns + x};
                    }
                }
            }
        }
    } break;
    }
}

void
set_title(Screen *self, PyObject *title) {
    CALLBACK("title_changed", "O", title);
}

void
osc_context(Screen *self, PyObject *ctx) {
    CALLBACK("osc_context", "O", ctx);
}

void
desktop_notify(Screen *self, unsigned int osc_code, PyObject *data) {
    CALLBACK("desktop_notify", "IO", osc_code, data);
}

void
set_icon(Screen *self, PyObject *icon) {
    CALLBACK("icon_changed", "O", icon);
}

void
set_dynamic_color(Screen *self, unsigned int code, PyObject *color) {
    if (color == NULL) { CALLBACK("set_dynamic_color", "I", code); }
    else { CALLBACK("set_dynamic_color", "IO", code, color); }
}

void
color_control(Screen *self, unsigned int code, PyObject *spec) {
    if (spec) CALLBACK("color_control", "IO", code, spec);
}

void
clipboard_control(Screen *self, int code, PyObject *data) {
    if (code == 52 || code == -52) { CALLBACK("clipboard_control", "OO", data, code == -52 ? Py_True: Py_False); }
    else { CALLBACK("clipboard_control", "OO", data, Py_None);}
}

void
file_transmission(Screen *self, PyObject *data) {
    CALLBACK("file_transmission", "O", data);
}

static void
parse_prompt_mark(Screen *self, char *buf, PromptKind *pk) {
    char *saveptr, *str = buf;
    while (true) {
        const char *token = strtok_r(str, ";", &saveptr); str = NULL;
        if (token == NULL) return;
        if (strcmp(token, "k=s") == 0) *pk = SECONDARY_PROMPT;
        else if (strcmp(token, "redraw=0") == 0) self->prompt_settings.redraws_prompts_at_all = 0;
        else if (strcmp(token, "special_key=1") == 0) self->prompt_settings.uses_special_keys_for_cursor_movement = 1;
        else if (strcmp(token, "click_events=1") == 0) {
            self->prompt_settings.supports_click_events = 1;
            self->prompt_settings.relative_click_events = 0;
        } else if (strcmp(token, "click_events=2") == 0) {
            self->prompt_settings.supports_click_events = 1;
            self->prompt_settings.relative_click_events = 1;
        }
    }
}

void
shell_prompt_marking(Screen *self, char *buf) {
    if (self->cursor->y < self->lines) {
        char ch = buf[0];
        switch (ch) {
            case 'A': {
                PromptKind pk = PROMPT_START;
                self->prompt_settings.val = 0;
                self->prompt_settings.redraws_prompts_at_all = 1;
                parse_prompt_mark(self, buf+1, &pk);
                self->linebuf->line_attrs[self->cursor->y].prompt_kind = pk;
                if (pk == PROMPT_START) CALLBACK("cmd_output_marking", "O", Py_False);
            } break;
            case 'C': {
                self->linebuf->line_attrs[self->cursor->y].prompt_kind = OUTPUT_START;
                const char *cmdline = "";
                if (strstr(buf + 1, ";cmdline") == buf + 1) {
                    cmdline = buf + 2;
                }
                RAII_PyObject(c, PyUnicode_DecodeUTF8(cmdline, strlen(cmdline), "replace"));
                if (c) { CALLBACK("cmd_output_marking", "OO", Py_True, c); }
                else PyErr_Print();
            } break;
            case 'D': {
                const char *exit_status = buf[1] == ';' ? buf + 2 : "";
                CALLBACK("cmd_output_marking", "Os", Py_None, exit_status);
            } break;
        }
    }
}

static bool
screen_history_scroll_to_prompt(Screen *self, int num_of_prompts_to_jump, int scroll_offset) {
    if (self->linebuf != self->main_linebuf) return false;
    unsigned int old = self->scrolled_by;
    if (num_of_prompts_to_jump == 0) {
        if (!self->last_visited_prompt.is_set || self->last_visited_prompt.scrolled_by > self->historybuf->count || self->last_visited_prompt.y >= self->lines) return false;
        self->scrolled_by = self->last_visited_prompt.scrolled_by;
    } else {
        int delta = num_of_prompts_to_jump < 0 ? -1 : 1;
        num_of_prompts_to_jump = num_of_prompts_to_jump < 0 ? -num_of_prompts_to_jump : num_of_prompts_to_jump;
        int y = -self->scrolled_by;
#define ensure_y_ok if (y >= (int)self->lines || -y > (int)self->historybuf->count) return false;
        ensure_y_ok;
        y += scroll_offset;
        while (num_of_prompts_to_jump) {
            y += delta;
            ensure_y_ok;
            if (range_line_(self, y)->attrs.prompt_kind == PROMPT_START) {
                num_of_prompts_to_jump--;
            }
        }
        y -= scroll_offset;
#undef ensure_y_ok
        self->scrolled_by = y >= 0 ? 0 : -y;
        screen_set_last_visited_prompt(self, 0);
    }
    if (old != self->scrolled_by) {
        reset_pixel_scroll(self, 0);
        dirty_scroll(self);
    }
    return old != self->scrolled_by;
}

void
set_color_table_color(Screen *self, unsigned int code, PyObject *color) {
    if (color == NULL) { CALLBACK("set_color_table_color", "I", code); }
    else { CALLBACK("set_color_table_color", "IO", code, color); }
}

void
process_cwd_notification(Screen *self, unsigned int code, const char *data, size_t sz) {
    if (code == 7) {
        PyObject *x = PyBytes_FromStringAndSize(data, sz);
        if (x) {
            Py_CLEAR(self->last_reported_cwd);
            self->last_reported_cwd = x;
        } else { PyErr_Clear(); }
    }  // we ignore OSC 6 document reporting as we dont have a use for it
}

bool
screen_send_signal_for_key(Screen *self, char key) {
    int ret = 0;
    if (self->callbacks != Py_None) {
        int cchar = key;
        PyObject *callback_ret = PyObject_CallMethod(self->callbacks, "send_signal_for_key", "c", cchar);
        if (callback_ret) {
            ret = PyObject_IsTrue(callback_ret);
            Py_DECREF(callback_ret);
        } else { PyErr_Print(); }
    }
    return ret != 0;
}

void
screen_push_colors(Screen *self, unsigned int idx) {
    if (colorprofile_push_colors(self->color_profile, idx)) self->color_profile->dirty = true;
}

void
screen_pop_colors(Screen *self, unsigned int idx) {
    color_type bg_before = colorprofile_to_color(self->color_profile, self->color_profile->overridden.default_bg, self->color_profile->configured.default_bg).rgb;
    if (colorprofile_pop_colors(self->color_profile, idx)) {
        self->color_profile->dirty = true;
        color_type bg_after = colorprofile_to_color(self->color_profile, self->color_profile->overridden.default_bg, self->color_profile->configured.default_bg).rgb;
        CALLBACK("color_profile_popped", "O", bg_before == bg_after ? Py_False : Py_True);
    }
}

void
screen_report_color_stack(Screen *self) {
    unsigned int idx, count;
    colorprofile_report_stack(self->color_profile, &idx, &count);
    char buf[128] = {0};
    snprintf(buf, arraysz(buf), "%u;%u#Q", idx, count);
    write_escape_code_to_child(self, ESC_CSI, buf);
}

void screen_handle_kitty_dcs(Screen *self, const char *callback_name, PyObject *cmd) {
    CALLBACK(callback_name, "O", cmd);
}

void
screen_request_capabilities(Screen *self, char c, const char *query) {
    static char buf[128];
    int shape = 0;
    switch(c) {
        case '+': {
            CALLBACK("request_capabilities", "s", query);
        } break;
        case '$':
            // report status DECRQSS
            if (strcmp(" q", query) == 0) {
                // cursor shape DECSCUSR
                switch(self->cursor->shape) {
                    case NO_CURSOR_SHAPE: case CURSOR_HOLLOW: case NUM_OF_CURSOR_SHAPES:
                        shape = 1; break;
                    case CURSOR_BLOCK:
                        shape = self->cursor->non_blinking ? 2 : 0; break;
                    case CURSOR_UNDERLINE:
                        shape = self->cursor->non_blinking ? 4 : 3; break;
                    case CURSOR_BEAM:
                        shape = self->cursor->non_blinking ? 6 : 5; break;
                }
                shape = snprintf(buf, sizeof(buf), "1$r%d q", shape);
            } else if (strcmp("m", query) == 0) {
                // SGR
                const char *s = cursor_as_sgr(self->cursor);
                if (s && s[0]) shape = snprintf(buf, sizeof(buf), "1$r0;%sm", s);
                else shape = snprintf(buf, sizeof(buf), "1$rm");
            } else if (strcmp("r", query) == 0) { // DECSTBM
                shape = snprintf(buf, sizeof(buf), "1$r%u;%ur", self->margin_top + 1, self->margin_bottom + 1);
            } else if (strcmp("*x", query) == 0) { // DECSACE
                shape = snprintf(buf, sizeof(buf), "1$r%d*x", self->modes.mDECSACE ? 1 : 0);
            } else {
                shape = snprintf(buf, sizeof(buf), "0$r");
            }
            if (shape > 0) write_escape_code_to_child(self, ESC_DCS, buf);
            break;
    }
}

// }}}

// Rendering {{{

void
screen_check_pause_rendering(Screen *self, monotonic_t now) {
    if (self->paused_rendering.expires_at && now > self->paused_rendering.expires_at) screen_pause_rendering(self, false, 0);
}

static bool
copy_selections(Selections *dest, const Selections *src) {
    if (dest->capacity < src->count) {
        dest->items = realloc(dest->items, sizeof(dest->items[0]) * src->count);
        if (!dest->items) { dest->capacity = 0; dest->count = 0; return false; }
        dest->capacity = src->count;
    }
    dest->count = src->count;
    for (unsigned i = 0; i < dest->count; i++) memcpy(dest->items + i, src->items + i, sizeof(dest->items[0]));
    dest->last_rendered_count = src->last_rendered_count;
    return true;
}

bool
screen_pause_rendering(Screen *self, bool pause, int for_in_ms) {
    if (!pause) {
        if (!self->paused_rendering.expires_at) return false;
        self->paused_rendering.expires_at = 0;
        // ensure cell data is updated on GPU
        self->is_dirty = true;
        // ensure selection data is updated on GPU
        self->selections.last_rendered_count = SIZE_MAX; self->url_ranges.last_rendered_count = SIZE_MAX;
        self->extra_cursors.dirty = true;
        // free grman data
        grman_pause_rendering(NULL, self->paused_rendering.grman);
        // free extra cursors
        free(self->paused_rendering.extra_cursors.locations); zero_at_ptr(&self->paused_rendering.extra_cursors);
        return true;
    }
    if (self->paused_rendering.expires_at) return false;
    if (!self->paused_rendering.grman) self->paused_rendering.grman = grman_alloc(true);
    if (!self->paused_rendering.grman) return false;
    if (for_in_ms <= 0) for_in_ms = 2000;
    self->paused_rendering.expires_at = monotonic() + ms_to_monotonic_t(for_in_ms);
    self->paused_rendering.inverted = self->modes.mDECSCNM;
    self->paused_rendering.scrolled_by = self->scrolled_by;
    self->paused_rendering.cell_data_updated = false;
    self->paused_rendering.cursor_visible = self->modes.mDECTCEM;
    memcpy(&self->paused_rendering.cursor, self->cursor, sizeof(self->paused_rendering.cursor));
    memcpy(&self->paused_rendering.color_profile, self->color_profile, sizeof(self->paused_rendering.color_profile));
    if (!self->paused_rendering.linebuf || self->paused_rendering.linebuf->xnum != self->columns || self->paused_rendering.linebuf->ynum != self->lines) {
        if (self->paused_rendering.linebuf) Py_CLEAR(self->paused_rendering.linebuf);
        self->paused_rendering.linebuf = alloc_linebuf(self->lines, self->columns, self->text_cache);
        if (!self->paused_rendering.linebuf) { PyErr_Clear(); self->paused_rendering.expires_at = 0; return false; }
    }
    for (index_type y = 0; y < self->lines; y++) {
        Line *src = visual_line_(self, y);
        linebuf_init_line(self->paused_rendering.linebuf, y);
        copy_line(src, self->paused_rendering.linebuf->line);
        self->paused_rendering.linebuf->line_attrs[y] = src->attrs;
    }
    copy_selections(&self->paused_rendering.selections, &self->selections);
    copy_selections(&self->paused_rendering.url_ranges, &self->url_ranges);
    if (self->extra_cursors.count) {
        self->paused_rendering.extra_cursors.locations = calloc(self->extra_cursors.count, sizeof(self->extra_cursors.locations[0]));
        if (self->paused_rendering.extra_cursors.locations) {
            self->paused_rendering.extra_cursors.count = self->extra_cursors.count;
            self->paused_rendering.extra_cursors.dirty = self->extra_cursors.dirty;
            memcpy(self->paused_rendering.extra_cursors.locations, self->extra_cursors.locations, sizeof(self->extra_cursors.locations[0]) * self->extra_cursors.count);
        }
    }
    grman_pause_rendering(self->grman, self->paused_rendering.grman);
    return true;
}

static color_type
effective_cell_edge_color(char_type ch, color_type fg, color_type bg, bool is_left_edge) {
    START_ALLOW_CASE_RANGE
    if (ch == 0x2588) return fg; // full block
    if (is_left_edge) {
        switch (ch) {
            case 0x2589 ... 0x258f: // left eighth blocks
            case 0xe0b0: case 0xe0b4: case 0xe0b8: case 0xe0bc:  // powerline blocks
            case 0x1fb6a: // 🭪
                return fg;
        }
    } else {
        switch (ch) {
            case 0x2590:  // right half block
            case 0x1fb87 ... 0x1fb8b:  // eighth right blocks
            case 0xe0b2: case 0xe0b6: case 0xe0ba: case 0xe0be:
            case 0x1fb68: // 🭨
                return fg;
        }
    }
    return bg;
    END_ALLOW_CASE_RANGE
}


bool
get_line_edge_colors_at_row(Screen *self, index_type y, color_type *left, color_type *right, bool *left_is_default, bool *right_is_default) {
    // Return the color at the left and right edges of the specified row.
    // Any of the output pointers may be NULL if that value is not needed.
    Line *line = range_line_(self, y);
    if (!line) return false;
    color_type left_cell_fg = OPT(foreground), left_cell_bg = OPT(background), right_cell_bg = OPT(background), right_cell_fg = OPT(foreground);
    index_type cell_color_x = 0;
    char_type left_char = line_get_char(line, cell_color_x);
    bool reversed = false;
    colors_for_cell(line, self->color_profile, &cell_color_x, &left_cell_fg, &left_cell_bg, &reversed);
    if (left_is_default) *left_is_default = (line->gpu_cells[cell_color_x].bg & 0xff) == 0;
    if (left) *left = effective_cell_edge_color(left_char, left_cell_fg, left_cell_bg, true);
    if (line->xnum > 0) cell_color_x = line->xnum - 1;
    char_type right_char = line_get_char(line, cell_color_x);
    reversed = false;  // reset: colors_for_cell only sets this flag, never clears it
    colors_for_cell(line, self->color_profile, &cell_color_x, &right_cell_fg, &right_cell_bg, &reversed);
    if (right_is_default) *right_is_default = (line->gpu_cells[cell_color_x].bg & 0xff) == 0;
    if (right) *right = effective_cell_edge_color(right_char, right_cell_fg, right_cell_bg, false);
    return true;
}

bool
get_line_edge_colors(Screen *self, color_type *left, color_type *right) {
    // Return the color at the left and right edges of the line with the cursor on it
    return get_line_edge_colors_at_row(self, self->cursor->y, left, right, NULL, NULL);
}


static void
update_line_data(Line *line, unsigned int dest_y, uint8_t *data) {
    size_t base = sizeof(GPUCell) * dest_y * line->xnum;
    memcpy(data + base, line->gpu_cells, line->xnum * sizeof(GPUCell));
}

static void
update_line_data_blank(unsigned xnum, unsigned int dest_y, uint8_t *data) {
    const size_t sz = xnum * sizeof(GPUCell);
    memset(data + sz * dest_y, 0, sz);
}


static Line*
render_line_for_virtual_y(Screen *self, int y, Line *line, index_type *lnum, bool *is_history) {
    if (y < (int)self->scrolled_by) {
        int idx = (int)self->scrolled_by - 1 - y;
        if (idx >= 0 && (unsigned)idx < self->historybuf->count) {
            historybuf_init_line(self->historybuf, idx, line);
            line->xnum = self->columns;
            line->ynum = (index_type)MIN(MAX(y, 0), (int)self->lines - 1);
            *lnum = (index_type)idx;
            *is_history = true;
            return line;
        }
        return NULL;
    }
    y -= self->scrolled_by;
    if (y >= 0 && y < (int)self->lines) {
        linebuf_init_line_at(self->linebuf, (index_type)y, line);
        *lnum = (index_type)y;
        *is_history = false;
        return line;
    }
    return NULL;
}


static void
screen_reset_dirty(Screen *self) {
    self->is_dirty = false;
    self->history_line_added_count = 0;
}

static bool
screen_has_marker(Screen *self) {
    return self->marker != NULL;
}

static uint32_t diacritic_to_rowcolumn(char_type c) {
    return diacritic_to_num(c);
}

static uint32_t color_to_id(color_type c) {
    // Just take 24 most significant bits of the color. This works both for
    // 24-bit and 8-bit colors.
    return (c >> 8) & 0xffffff;
}

// Scan the line and create cell images in place of unicode placeholders
// reserved for image placement.
static void
screen_render_line_graphics(Screen *self, Line *line, int32_t row) {
    // If there are no image placeholders now, no need to rescan the line.
    if (!line->attrs.has_image_placeholders)
        return;
    // Remove existing images.
    grman_remove_cell_images(self->grman, row, row);
    // The placeholders might be erased. We will update the attribute.
    line->attrs.has_image_placeholders = false;
    index_type i;
    uint32_t run_length = 0;
    uint32_t prev_img_id_lower24bits = 0;
    uint32_t prev_placement_id = 0;
    // Note that the following values are 1-based, zero means unknown or incorrect.
    uint32_t prev_img_id_higher8bits = 0;
    uint32_t prev_img_row = 0;
    uint32_t prev_img_col = 0;
    for (i = 0; i < line->xnum; i++) {
        CPUCell *cpu_cell = line->cpu_cells + i;
        GPUCell *gpu_cell = line->gpu_cells + i;
        uint32_t cur_img_id_lower24bits = 0;
        uint32_t cur_placement_id = 0;
        uint32_t cur_img_id_higher8bits = 0;
        uint32_t cur_img_row = 0;
        uint32_t cur_img_col = 0;
        if (cell_first_char(cpu_cell, self->text_cache) == IMAGE_PLACEHOLDER_CHAR) {
            line->attrs.has_image_placeholders = true;
            // The lower 24 bits of the image id are encoded in the foreground
            // color, and the placement id is (optionally) in the underline color.
            cur_img_id_lower24bits = color_to_id(gpu_cell->fg);
            cur_placement_id = color_to_id(gpu_cell->decoration_fg);
            text_in_cell(cpu_cell, self->text_cache, self->lc);
            // If the char has diacritics, use them as row and column indices.
            if (self->lc->count > 1 && self->lc->chars[1])
                cur_img_row = diacritic_to_rowcolumn(self->lc->chars[1]);
            if (self->lc->count > 2 && self->lc->chars[2])
                cur_img_col = diacritic_to_rowcolumn(self->lc->chars[2]);
            // The third diacritic is used to encode the higher 8 bits of the
            // image id (optional).
            if (self->lc->count > 3 && self->lc->chars[3])
                cur_img_id_higher8bits = diacritic_to_rowcolumn(self->lc->chars[3]);
        }
        // The current run is continued if the lower 24 bits of the image id and
        // the placement id are the same as in the previous cell and everything
        // else is unknown or compatible with the previous cell.
        if (run_length > 0 && cur_img_id_lower24bits == prev_img_id_lower24bits &&
            cur_placement_id == prev_placement_id &&
            (!cur_img_row || cur_img_row == prev_img_row) &&
            (!cur_img_col || cur_img_col == prev_img_col + 1) &&
            (!cur_img_id_higher8bits || cur_img_id_higher8bits == prev_img_id_higher8bits)) {
            // This cell continues the current run.
            run_length++;
            // If some values are unknown, infer them from the previous cell.
            cur_img_row = MAX(prev_img_row, 1u);
            cur_img_col = prev_img_col + 1;
            cur_img_id_higher8bits = MAX(prev_img_id_higher8bits, 1u);
        } else {
            // This cell breaks the current run. Render the current run if it
            // has a non-zero length.
            if (run_length > 0) {
                uint32_t img_id = prev_img_id_lower24bits | (prev_img_id_higher8bits - 1) << 24;
                grman_put_cell_image(
                    self->grman, row, i - run_length, img_id,
                    prev_placement_id, prev_img_col - run_length,
                    prev_img_row - 1, run_length, 1, self->cell_size);
            }
            // Start a new run.
            if (cell_first_char(cpu_cell, self->text_cache) == IMAGE_PLACEHOLDER_CHAR) {
                run_length = 1;
                if (!cur_img_col) cur_img_col = 1;
                if (!cur_img_row) cur_img_row = 1;
                if (!cur_img_id_higher8bits) cur_img_id_higher8bits = 1;
            }
        }
        prev_img_id_lower24bits = cur_img_id_lower24bits;
        prev_img_id_higher8bits = cur_img_id_higher8bits;
        prev_placement_id = cur_placement_id;
        prev_img_row = cur_img_row;
        prev_img_col = cur_img_col;
    }
    if (run_length > 0) {
        // Render the last run.
        uint32_t img_id = prev_img_id_lower24bits | (prev_img_id_higher8bits - 1) << 24;
        grman_put_cell_image(self->grman, row, i - run_length, img_id,
                             prev_placement_id, prev_img_col - run_length,
                             prev_img_row - 1, run_length, 1, self->cell_size);
    }
}

// This functions is similar to screen_update_cell_data, but it only updates
// line graphics (cell images) and then marks lines as clean. It's used
// exclusively for testing unicode placeholders.
static void
screen_update_only_line_graphics_data(Screen *self) {
    unsigned int history_line_added_count = self->history_line_added_count;
    index_type lnum = 0;
    if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count);
    screen_reset_dirty(self);
    self->scroll_changed = false;
    const unsigned int render_lines = render_lines_for_screen(self);
    const int render_row_offset = pixel_scroll_enabled(self);
    Line line = {.text_cache = self->text_cache};
    for (unsigned int render_row = 0; render_row < render_lines; render_row++) {
        const int virtual_y = (int)render_row - render_row_offset;
        bool is_history = false;
        Line *linep = render_line_for_virtual_y(self, virtual_y, &line, &lnum, &is_history);
        if (linep) {
            screen_render_line_graphics(self, linep, virtual_y - (int)self->scrolled_by);
            if (linep->attrs.has_dirty_text) {
                if (is_history) historybuf_mark_line_clean(self->historybuf, lnum);
                else linebuf_mark_line_clean(self->linebuf, lnum);
            }
        }
    }
}

void
screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_data, bool cursor_has_moved) {
    if (self->paused_rendering.expires_at) {
        if (!self->paused_rendering.cell_data_updated) {
            LineBuf *linebuf = self->paused_rendering.linebuf;
            for (index_type y = 0; y < self->lines; y++) {
                linebuf_init_line(linebuf, y);
                if (linebuf->line->attrs.has_dirty_text) {
                    render_line(fonts_data, linebuf->line, y, &self->paused_rendering.cursor, self->disable_ligatures, self->lc);
                    screen_render_line_graphics(self, linebuf->line, y);
                    if (linebuf->line->attrs.has_dirty_text && screen_has_marker(self)) mark_text_in_line(
                            self->marker, linebuf->line, &self->as_ansi_buf);
                    linebuf_mark_line_clean(linebuf, y);
                }
                update_line_data(linebuf->line, y, address);
            }
        }
        return;
    }
    const bool is_overlay_active = screen_is_overlay_active(self);
    unsigned int history_line_added_count = self->history_line_added_count;
    screen_reset_dirty(self);
    update_overlay_position(self);
    const bool force_history_render = pixel_scroll_enabled(self) && self->scroll_changed;
    if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count);
    self->scroll_changed = false;
    const unsigned int render_lines = render_lines_for_screen(self);
    const int render_row_offset = pixel_scroll_enabled(self);
    Line line = {.text_cache = self->text_cache};
    for (unsigned int render_row = 0; render_row < render_lines; render_row++) {
        const int virtual_y = (int)render_row - render_row_offset;
        bool is_history = false;
        index_type lnum = 0;
        Line *linep = render_line_for_virtual_y(self, virtual_y, &line, &lnum, &is_history);
        if (linep == NULL) {
            update_line_data_blank(self->columns, render_row, address);
            continue;
        }
        if (is_history) {
            // we render line graphics even if the line is not dirty as graphics commands received after
            // the unicode placeholder was first scanned can alter it.
            screen_render_line_graphics(self, linep, virtual_y - (int)self->scrolled_by);
            if (force_history_render || linep->attrs.has_dirty_text) {
                render_line(fonts_data, linep, lnum, self->cursor, self->disable_ligatures, self->lc);
                if (screen_has_marker(self)) mark_text_in_line(self->marker, linep, &self->as_ansi_buf);
                historybuf_mark_line_clean(self->historybuf, lnum);
            }
        } else {
            if (linep->attrs.has_dirty_text ||
                (cursor_has_moved && (self->cursor->y == lnum || self->last_rendered.cursor.y == lnum))) {
                render_line(fonts_data, linep, lnum, self->cursor, self->disable_ligatures, self->lc);
                screen_render_line_graphics(self, linep, virtual_y - (int)self->scrolled_by);
                if (linep->attrs.has_dirty_text && screen_has_marker(self)) mark_text_in_line(
                        self->marker, linep, &self->as_ansi_buf);
                if (is_overlay_active && lnum == self->overlay_line.ynum) render_overlay_line(self, linep, fonts_data);
                linebuf_mark_line_clean(self->linebuf, lnum);
            }
        }
        update_line_data(linep, render_row, address);
    }
    if (is_overlay_active && self->overlay_line.ynum + self->scrolled_by < self->lines) {
        if (self->overlay_line.is_dirty) {
            linebuf_init_line(self->linebuf, self->overlay_line.ynum);
            render_overlay_line(self, self->linebuf->line, fonts_data);
        }
        update_overlay_line_data(self, address);
    }
}

static bool
selection_boundary_less_than(const SelectionBoundary *a, const SelectionBoundary *b) {
    // y -values must be absolutized (aka adjusted with scrolled_by)
    // this means the oldest line has the highest value and is thus the least
    if (a->y > b->y) return true;
    if (a->y < b->y) return false;
    if (a->x < b->x) return true;
    if (a->x > b->x) return false;
    if (a->in_left_half_of_cell && !b->in_left_half_of_cell) return true;
    return false;
}

static index_type
num_cells_between_selection_boundaries(const Screen *self, const SelectionBoundary *a, const SelectionBoundary *b) {
    const SelectionBoundary *before, *after;
    if (selection_boundary_less_than(a, b)) { before = a; after = b; }
    else { before = b; after = a; }
    index_type ans = 0;
    if (before->y + 1 < after->y) ans += self->columns * (after->y - before->y - 1);
    if (before->y == after->y) ans += after->x - before->x;
    else ans += (self->columns - before->x) + after->x;
    return ans;
}

static index_type
num_lines_between_selection_boundaries(const SelectionBoundary *a, const SelectionBoundary *b) {
    const SelectionBoundary *before, *after;
    if (selection_boundary_less_than(a, b)) { before = a; after = b; }
    else { before = b; after = a; }
    return before->y - after->y;
}

static bool
selection_is_left_to_right(const Selection *self) {
    return self->input_start.x < self->input_current.x || (self->input_start.x == self->input_current.x && self->input_start.in_left_half_of_cell);
}

static void
iteration_data(const Selection *sel, IterationData *ans, unsigned x_limit, int min_y, unsigned add_scrolled_by) {
    memset(ans, 0, sizeof(IterationData));
    const SelectionBoundary *start = &sel->start, *end = &sel->end;
    int start_y = (int)start->y - sel->start_scrolled_by, end_y = (int)end->y - sel->end_scrolled_by;
    // empty selection
    if (start->x == end->x && start_y == end_y && start->in_left_half_of_cell == end->in_left_half_of_cell) return;

    if (sel->rectangle_select) {
        // empty selection
        if (start->x == end->x && (!start->in_left_half_of_cell || end->in_left_half_of_cell)) return;

        ans->y = MIN(start_y, end_y); ans->y_limit = MAX(start_y, end_y) + 1;
        index_type x, x_limit;
        bool left_to_right = selection_is_left_to_right(sel);

        if (start->x == end->x) {
            x = start->x; x_limit = start->x + 1;
        } else {
            if (left_to_right) {
                x = start->x + (start->in_left_half_of_cell ? 0 : 1);
                x_limit = 1 + end->x + (end->in_left_half_of_cell ? -1: 0);
            } else {
                x = end->x + (end->in_left_half_of_cell ? 0 : 1);
                x_limit = 1 + start->x + (start->in_left_half_of_cell ? -1 : 0);
            }
        }
        ans->first.x = x; ans->body.x = x; ans->last.x = x;
        ans->first.x_limit = x_limit; ans->body.x_limit = x_limit; ans->last.x_limit = x_limit;
    } else {
        index_type line_limit = x_limit;

        if (start_y == end_y) {
            if (start->x == end->x) {
                if (start->in_left_half_of_cell && !end->in_left_half_of_cell) {
                    // single cell selection
                    ans->first.x = start->x; ans->body.x = start->x; ans->last.x = start->x;
                    ans->first.x_limit = start->x + 1; ans->body.x_limit = start->x + 1; ans->last.x_limit = start->x + 1;
                } else return; // empty selection
            }
            // single line selection
            else if (start->x <= end->x) {
                ans->first.x = start->x + (start->in_left_half_of_cell ? 0 : 1);
                ans->first.x_limit = 1 + end->x + (end->in_left_half_of_cell ? -1 : 0);
            } else {
                ans->first.x = end->x + (end->in_left_half_of_cell ? 0 : 1);
                ans->first.x_limit = 1 + start->x + (start->in_left_half_of_cell ? -1 : 0);
            }
        } else if (start_y < end_y) { // downwards
            ans->body.x_limit = line_limit;
            ans->first.x_limit = line_limit;
            ans->first.x = start->x + (start->in_left_half_of_cell ? 0 : 1);
            ans->last.x_limit = 1 + end->x + (end->in_left_half_of_cell ? -1 : 0);
        } else { // upwards
            ans->body.x_limit = line_limit;
            ans->first.x_limit = line_limit;
            ans->first.x = end->x + (end->in_left_half_of_cell ? 0 : 1);
            ans->last.x_limit = 1 + start->x + (start->in_left_half_of_cell ? -1 : 0);
        }
        ans->y = MIN(start_y, end_y); ans->y_limit = MAX(start_y, end_y) + 1;

    }
    ans->y += add_scrolled_by; ans->y_limit += add_scrolled_by;
    ans->y = MAX(ans->y, min_y);
    ans->y_limit = MAX(ans->y, ans->y_limit);  // iteration is from y to y_limit
}

static XRange
xrange_for_iteration(const IterationData *idata, const int y, const Line *line) {
    XRange ans = {.x_limit=xlimit_for_line(line)};
    if (y == idata->y) {
        ans.x_limit = MIN(idata->first.x_limit, ans.x_limit);
        ans.x = idata->first.x;
    } else if (y == idata->y_limit - 1) {
        ans.x_limit = MIN(idata->last.x_limit, ans.x_limit);
        ans.x = idata->last.x;
    } else {
        ans.x_limit = MIN(idata->body.x_limit, ans.x_limit);
        ans.x = idata->body.x;
    }
    return ans;
}

static XRange
xrange_for_iteration_with_multicells(const IterationData *idata, const int y, const Line *line) {
    XRange ans = xrange_for_iteration(idata, y, line);
    if (ans.x_limit > ans.x) {
        CPUCell *c; index_type ml;
        if (ans.x && (c = &line->cpu_cells[ans.x])->is_multicell && c->x) ans.x = ans.x > c->x ? ans.x - c->x : 0;
        if (ans.x_limit < line->xnum && (c = &line->cpu_cells[ans.x_limit-1])->is_multicell && c->x + 1u < (ml = mcd_x_limit(c))) {
            ans.x_limit += ml - 1 - c->x; if (ans.x_limit > line->xnum) ans.x_limit = line->xnum;
        }
    }
    return ans;
}

static bool
iteration_data_is_empty(const Screen *self, const IterationData *idata) {
    if (idata->y >= idata->y_limit) return true;
    index_type xl = MIN(idata->first.x_limit, self->columns);
    if (idata->first.x < xl) return false;
    xl = MIN(idata->body.x_limit, self->columns);
    if (idata->body.x < xl) return false;
    xl = MIN(idata->last.x_limit, self->columns);
    if (idata->last.x < xl) return false;
    return true;
}

static void
apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask, int extra_leading_rows, int scrolled_by) {
    iteration_data(s, &s->last_rendered, self->columns, -self->historybuf->count, 0);
    Line *line;
    const int y_min = MAX(-extra_leading_rows - scrolled_by, s->last_rendered.y),
          y_limit = MIN(s->last_rendered.y_limit, (int)self->lines - scrolled_by);
    for (int y = y_min; y < y_limit; y++) {
        if (self->paused_rendering.expires_at) {
            // paused_rendering.linebuf stores only the visible rows captured at pause time
            const int paused_y = y + scrolled_by;
            if (paused_y < 0 || paused_y >= (int)self->lines) continue;
            linebuf_init_line(self->paused_rendering.linebuf, paused_y);
            line = self->paused_rendering.linebuf->line;
        } else {
            line = checked_range_line(self, y);
            if (!line) continue;
        }
        const int y_in_data = (y + extra_leading_rows + scrolled_by);
        uint8_t *line_start = data + self->columns * y_in_data;
        XRange xr = xrange_for_iteration_with_multicells(&s->last_rendered, y, line);
        for (index_type x = xr.x; x < xr.x_limit; x++) {
            line_start[x] |= set_mask;
            CPUCell *c = &line->cpu_cells[x];
            if (c->is_multicell && c->scale > 1) {
                for (int ym = MAX(0, y_in_data - c->y); ym < y_in_data; ym++) data[self->columns * ym + x] |= set_mask;
                for (int ym = y_in_data + 1; ym < MIN((int)self->lines + extra_leading_rows, y_in_data + c->scale - c->y); ym++)
                    data[self->columns * ym + x] |= set_mask;
            }
        }
    }
    s->last_rendered.y = MAX(0, s->last_rendered.y);
}

bool
screen_has_selection(Screen *self) {
    IterationData idata;
    for (size_t i = 0; i < self->selections.count; i++) {
        Selection *s = self->selections.items + i;
        if (!is_selection_empty(s)) {
            iteration_data(s, &idata, self->columns, -self->historybuf->count, self->scrolled_by);
            if (!iteration_data_is_empty(self, &idata)) return true;
        }
    }
    return false;
}

void
screen_apply_selection(Screen *self, void *address_, size_t size) {
    uint8_t *address = address_;
    memset(address, 0, size);
    const unsigned offset = pixel_scroll_enabled(self);
    const unsigned scrolled_by = self->paused_rendering.expires_at ? self->paused_rendering.scrolled_by : self->scrolled_by;
    Selections *sel = self->paused_rendering.expires_at ? &self->paused_rendering.selections : &self->selections;
    for (size_t i = 0; i < sel->count; i++) apply_selection(self, address, sel->items + i, 1, offset, scrolled_by);
    sel->last_rendered_count = sel->count;
    sel = self->paused_rendering.expires_at ? &self->paused_rendering.url_ranges : &self->url_ranges;
    for (size_t i = 0; i < sel->count; i++) {
        Selection *s = sel->items + i;
        if (OPT(underline_hyperlinks) == UNDERLINE_NEVER && s->is_hyperlink) continue;
        apply_selection(self, address, s, 2, offset, scrolled_by);
    }
    sel->last_rendered_count = sel->count;
    address += (size_t)offset * self->columns; size -= (size_t)offset * self->columns;
    ExtraCursors *ec = self->paused_rendering.expires_at ? &self->paused_rendering.extra_cursors : &self->extra_cursors;
    for (unsigned i = 0; i < ec->count; i++) {
        if (ec->locations[i].cell < size) address[ec->locations[i].cell] |= (ec->locations[i].shape & 7) << 2;
    }
    ec->dirty = false;
}

static index_type
limit_without_trailing_whitespace(const Line *line, index_type limit) {
    if (!limit) return limit;
    if (limit > line->xnum) limit = line->xnum;
    while (limit > 0) {
        const CPUCell *cell = line->cpu_cells + limit - 1;
        if (cell->is_multicell && (cell->x || cell->y)) { limit--; continue; }
        if (cell->ch_is_idx) break;
        switch(cell->ch_or_idx) {
            case ' ': case '\t': case '\n': case '\r': case 0: break;
            default:
                return limit;
        }
        limit--;
    }
    return limit;
}

static void
flag_selection_to_extract_text(Screen *self, const Selection *s, int *miny, int *y_limit) {
    IterationData idata;
    bool has_history = self->linebuf == self->main_linebuf;
    iteration_data(s, &idata, self->columns, has_history ? -self->historybuf->count : 0, 0);
    Line *line;
    *miny = idata.y; *y_limit = MIN(idata.y_limit, (int)self->lines);
    if (*miny >= *y_limit) return;
    static const int max_scale = ( (1u << SCALE_BITS) - 1u);
    for (int y = idata.y - max_scale; y < *y_limit; y++) {
        line = checked_range_line(self, y);
        if (line) for (index_type x = 0; x < line->xnum; x++) line->cpu_cells[x].temp_flag = 0;
    }
    Line temp = {.xnum=self->columns, .text_cache=self->text_cache};
    for (int y = idata.y; y < *y_limit; y++) {
        range_line(self, y, &temp);
        CPUCell *c;
        XRange xr = xrange_for_iteration_with_multicells(&idata, y, &temp);
        for (index_type x = xr.x; x < xr.x_limit; x++) {
            c = temp.cpu_cells + x;
            c->temp_flag = 1;
            if (c->is_multicell && c->y) {
                for (int ym = y - c->y; ym < y; ym++) {
                    line = checked_range_line(self, ym);
                    if (line) {
                        line->cpu_cells[x].temp_flag = 1;
                        *miny = MIN(*miny, ym);
                    }
                }
            }
        }
    }
    // remove lines from bottom that contain only y > 0 cells from multicell
    while (*y_limit > *miny) {
        range_line(self, *y_limit - 1, &temp);
        for (index_type x = 0; x < temp.xnum; x++) {
            if (temp.cpu_cells[x].temp_flag && temp.cpu_cells[x].ch_and_idx && (!temp.cpu_cells[x].is_multicell || !temp.cpu_cells[x].y)) return;
        }
        (*y_limit)--;
    }
}

static PyObject*
text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
    int min_y, y_limit;
    flag_selection_to_extract_text(self, sel, &min_y, &y_limit);
    if (min_y >= y_limit) return PyTuple_New(0);
    size_t before = self->as_ansi_buf.len;
    RAII_PyObject(ans, PyTuple_New(y_limit - min_y));
    RAII_PyObject(nl, PyUnicode_FromString("\n"));
    RAII_PyObject(empty, PyUnicode_FromString(""));
    if (!ans || !nl || !empty) return NULL;
    for (int i = 0, y = min_y; y < y_limit; y++, i++) {
        Line *line = range_line_(self, y);
        index_type x_limit = line->xnum, x_start = 0;
        while (x_limit && !line->cpu_cells[x_limit - 1].temp_flag) x_limit--;
        while (x_start < x_limit && !line->cpu_cells[x_start].temp_flag) x_start++;
        bool is_only_whitespace_line = false;
        // Don't strip trailing whitespace from soft-wrapped lines as those spaces
        // are part of the original text content that continues on the next line
        const bool line_is_continued = x_limit == line->xnum && line->cpu_cells[line->xnum-1].next_char_was_wrapped;
        if (strip_trailing_whitespace && !line_is_continued) {
            index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
            if (new_limit != x_limit) {
                x_limit = new_limit;
                is_only_whitespace_line = new_limit <= x_start;
            }
        }
        const bool is_first_line = y == min_y, is_last_line = y + 1 >= y_limit;
        const bool add_trailing_newline = insert_newlines && !is_last_line;
        PyObject *text = NULL;
        if (x_limit <= x_start && (is_only_whitespace_line || line_is_empty(line))) {
            // we want a newline on only whitespace lines even if they are continued
            text = add_trailing_newline ? nl : empty;
            text = Py_NewRef(text);
        } else {
            while (x_start < x_limit) {
                index_type end = x_start;
                while (end < x_limit && line->cpu_cells[end].temp_flag) end++;
                if (!unicode_in_range(line, x_start, end, true, add_trailing_newline, false, !is_first_line, &self->as_ansi_buf)) return PyErr_NoMemory();
                x_start = MAX(x_start + 1, end);
            }
            text = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, self->as_ansi_buf.buf + before, self->as_ansi_buf.len - before);
        }
        self->as_ansi_buf.len = before;
        if (!text) return NULL;
        PyTuple_SET_ITEM(ans, i, text);
    }
    return Py_NewRef(ans);
}

static PyObject*
ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
    int min_y, y_limit;
    flag_selection_to_extract_text(self, sel, &min_y, &y_limit);
    if (min_y >= y_limit) return PyTuple_New(0);
    ANSILineState s = {.output_buf=&self->as_ansi_buf};
    s.output_buf->active_hyperlink_id = 0; s.output_buf->len = 0;
    RAII_PyObject(ans, PyTuple_New(y_limit - min_y + 1));
    RAII_PyObject(nl, PyUnicode_FromString("\n"));
    RAII_PyObject(empty_string, PyUnicode_FromString(""));
    if (!ans || !nl || !empty_string) return NULL;
    bool has_escape_codes = false;
    bool need_newline = false;
    for (int i = 0, y = min_y; y < y_limit && i < PyTuple_GET_SIZE(ans) - 1; y++, i++) {
        const bool is_first_line = y == min_y;
        s.output_buf->len = 0;
        Line *line = range_line_(self, y);
        index_type x_limit = line->xnum, x_start = 0;
        while (x_limit && !line->cpu_cells[x_limit - 1].temp_flag) x_limit--;
        while (x_start < x_limit && !line->cpu_cells[x_start].temp_flag) x_start++;
        bool is_only_whitespace_line = false;
        // Don't strip trailing whitespace from soft-wrapped lines as those spaces
        // are part of the original text content that continues on the next line
        const bool line_is_continued = x_limit == line->xnum && line->cpu_cells[line->xnum-1].next_char_was_wrapped;
        if (strip_trailing_whitespace && !line_is_continued) {
            index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
            if (new_limit != x_limit) {
                x_limit = new_limit;
                is_only_whitespace_line = new_limit <= x_start;
            }
        }

        if (x_limit <= x_start && (is_only_whitespace_line || line_is_empty(line))) {
            // we want a newline on only whitespace lines even if they are continued
            if (insert_newlines) need_newline = true;
            PyTuple_SET_ITEM(ans, i, Py_NewRef(need_newline ? nl : empty_string));
        } else {
            char_type prefix_char = need_newline ? '\n' : 0;
            while (x_start < x_limit) {
                index_type end = x_start;
                while (end < x_limit && line->cpu_cells[end].temp_flag) end++;
                if (line_as_ansi(line, &s, x_start, end, prefix_char, !is_first_line)) has_escape_codes = true;
                need_newline = insert_newlines && !line->cpu_cells[line->xnum-1].next_char_was_wrapped;
                prefix_char = 0;
                x_start = MAX(x_start + 1, end);
            }
            PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, s.output_buf->buf, s.output_buf->len);
            if (!t) return NULL;
            PyTuple_SET_ITEM(ans, i, t);
        }
    }
    PyObject *t = PyUnicode_FromFormat("%s%s", has_escape_codes ? "\x1b[m" : "", s.output_buf->active_hyperlink_id ? "\x1b]8;;\x1b\\" : "");
    if (!t) return NULL;
    PyTuple_SET_ITEM(ans, PyTuple_GET_SIZE(ans) - 1, t);
    return Py_NewRef(ans);
}


static hyperlink_id_type
hyperlink_id_for_range(Screen *self, const Selection *sel) {
    IterationData idata;
    iteration_data(sel, &idata, self->columns, -self->historybuf->count, 0);
    for (int y = idata.y; y < idata.y_limit && y < (int)self->lines; y++) {
        Line *line = range_line_(self, y);
        XRange xr = xrange_for_iteration(&idata, y, line);
        for (index_type x = xr.x; x < xr.x_limit; x++) {
            if (line->cpu_cells[x].hyperlink_id) return line->cpu_cells[x].hyperlink_id;
        }
    }
    return 0;
}

static PyObject*
extend_tuple(PyObject *a, PyObject *b) {
    Py_ssize_t bs = PyTuple_GET_SIZE(b);
    if (bs < 1) return a;
    Py_ssize_t off = PyTuple_GET_SIZE(a);
    if (_PyTuple_Resize(&a, off + bs) != 0) return NULL;
    for (Py_ssize_t y = 0; y < bs; y++) {
        PyObject *t = PyTuple_GET_ITEM(b, y);
        Py_INCREF(t);
        PyTuple_SET_ITEM(a, off + y, t);
    }
    return a;
}

static PyObject*
current_url_text(Screen *self, PyObject *args UNUSED) {
    RAII_PyObject(empty_string, PyUnicode_FromString(""));
    if (!empty_string) return NULL;
    RAII_PyObject(ans, NULL);
    for (size_t i = 0; i < self->url_ranges.count; i++) {
        Selection *s = self->url_ranges.items + i;
        if (!is_selection_empty(s)) {
            RAII_PyObject(temp, text_for_range(self, s, false, false));
            if (!temp) return NULL;
            RAII_PyObject(text, PyUnicode_Join(empty_string, temp));
            if (!text) return NULL;
            if (ans) {
                PyObject *t = PyUnicode_Concat(ans, text);
                if (!t) return NULL;
                Py_CLEAR(ans); ans = t;
            } else ans = Py_NewRef(text);
        }
    }
    return Py_NewRef(ans ? ans : Py_None);
}


bool
screen_open_url(Screen *self, const char *callback) {
    if (!self->url_ranges.count) return false;
    hyperlink_id_type hid = hyperlink_id_for_range(self, self->url_ranges.items);
    if (hid) {
        const char *url = get_hyperlink_for_id(self->hyperlink_pool, hid, true);
        if (url) {
            CALLBACK(callback, "sH", url, hid);
            return true;
        }
    }
    PyObject *text = current_url_text(self, NULL);
    if (!text) {
        if (PyErr_Occurred()) PyErr_Print();
        return false;
    }
    bool found = false;
    if (PyUnicode_Check(text)) {
        CALLBACK(callback, "OH", text, 0);
        found = true;
    }
    Py_CLEAR(text);
    return found;
}

// }}}

// URLs {{{
static index_type
get_last_hostname_char_pos(Line *line, index_type url_start) {
    index_type slash_count = 0;
    while (url_start < line->xnum) {
        index_type pos = find_char(line, url_start, '/');
        if (pos >= line->xnum) return line->xnum;
        if (++slash_count > 2) return prev_char_pos(line, pos, 1);
        url_start = next_char_pos(line, pos, 1);
    }
    return line->xnum;
}

static void
extend_url(Screen *screen, Line *line, index_type *x, index_type *y, char_type sentinel, bool newlines_allowed, index_type last_hostname_char_pos, index_type scale) {
    unsigned int count = 0;
    bool has_newline = false;
    index_type orig_y = *y;
    while (count++ < 20) {
        bool in_hostname = last_hostname_char_pos >= line->xnum;
        has_newline = !line->cpu_cells[line->xnum-1].next_char_was_wrapped;
        if (next_char_pos(line, *x, 1) < line->xnum || (!newlines_allowed && has_newline)) break;
        bool next_line_starts_with_url_chars = false;
        line = screen_visual_line(screen, *y + 2 * scale);
        if (line) {
            next_line_starts_with_url_chars = line_startswith_url_chars(line, in_hostname, screen->lc);
            has_newline = !visual_line_is_continued(screen, *y + 2 * scale);
            if (next_line_starts_with_url_chars && has_newline && !newlines_allowed) next_line_starts_with_url_chars = false;
            if (sentinel && next_line_starts_with_url_chars && cell_is_char(line->cpu_cells, sentinel)) next_line_starts_with_url_chars = false;
        }
        line = screen_visual_line(screen, *y + scale);
        if (!line) break;
        if (in_hostname) {
            last_hostname_char_pos = find_char(line, 0, '/');
            if (last_hostname_char_pos < line->xnum) {
                last_hostname_char_pos = prev_char_pos(line, last_hostname_char_pos, 1);
                if (last_hostname_char_pos >= line->xnum) in_hostname = false;
            }
        }
        index_type new_x = line_url_end_at(line, 0, false, sentinel, next_line_starts_with_url_chars, in_hostname, last_hostname_char_pos, screen->lc);
        if (!new_x && !line_startswith_url_chars(line, in_hostname, screen->lc)) break;
        *y += scale; *x = new_x;
    }
    if (sentinel && *x == 0 && *y > orig_y) {
        line = screen_visual_line(screen, *y);
        if (line && cell_is_char(line->cpu_cells, sentinel)) {
            *y -= scale;
            *x = line->xnum - 1;
            if (line->cpu_cells[*x].is_multicell) *x -= line->cpu_cells[*x].x;
        }
    }
}

int
screen_detect_url(Screen *screen, unsigned int x, unsigned int y) {
    bool has_url = false;
    index_type url_start, url_end = 0;
    Line *line = screen_visual_line(screen, y);
    if (!line || x >= screen->columns) return 0;
    if (line->cpu_cells[x].is_multicell && line->cpu_cells[x].scale > 1 && line->cpu_cells[x].y) {
        if (line->cpu_cells[x].y > y) return 0;
        y -= line->cpu_cells[x].y;
        line = screen_visual_line(screen, y);
    }
    if (line->cpu_cells[x].is_multicell && line->cpu_cells[x].x) x = x > line->cpu_cells[x].x ? x - line->cpu_cells[x].x : 0;
    hyperlink_id_type hid;
    if ((hid = line->cpu_cells[x].hyperlink_id)) {
        screen_mark_hyperlink(screen, x, y);
        return hid;
    }
    char_type sentinel = 0;
    const bool newlines_allowed = !is_excluded_from_url('\n');
    index_type last_hostname_char_pos = screen->columns;
    url_start = line_url_start_at(line, x, screen->lc);
    Line scratch = {.xnum=line->xnum, .text_cache=line->text_cache};
    index_type scale = 1;
    if (url_start < line->xnum) {
        scale = cell_scale(line->cpu_cells + url_start);
        bool next_line_starts_with_url_chars = false;
        if (y + scale < screen->lines) {
            visual_line(screen, y + scale, &scratch);
            next_line_starts_with_url_chars = line_startswith_url_chars(&scratch, last_hostname_char_pos >= line->xnum, screen->lc);
            if (next_line_starts_with_url_chars && !newlines_allowed && !visual_line_is_continued(screen, y + scale)) next_line_starts_with_url_chars = false;
        }
        sentinel = get_url_sentinel(line, url_start);
        last_hostname_char_pos = get_last_hostname_char_pos(line, url_start);
        url_end = line_url_end_at(line, x, true, sentinel, next_line_starts_with_url_chars, x <= last_hostname_char_pos, last_hostname_char_pos, screen->lc);
    }
    has_url = url_end > url_start;
    if (has_url) {
        index_type y_extended = y;
        extend_url(screen, line, &url_end, &y_extended, sentinel, newlines_allowed, last_hostname_char_pos, scale);
        screen_mark_url(screen, url_start, y, url_end, y_extended);
    } else {
        screen_mark_url(screen, 0, 0, 0, 0);
    }
    return has_url ? -1 : 0;
}

// }}}

// IME Overlay {{{
bool
screen_is_overlay_active(Screen *self) {
    return self->overlay_line.is_active;
}

static void
deactivate_overlay_line(Screen *self) {
    if (self->overlay_line.is_active && self->overlay_line.xnum && self->overlay_line.ynum < self->lines) {
        self->is_dirty = true;
        linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum);
    }
    self->overlay_line.is_active = false;
    self->overlay_line.is_dirty = true;
    self->overlay_line.ynum = 0;
    self->overlay_line.xstart = 0;
    self->overlay_line.cursor_x = 0;
}

void
screen_update_overlay_text(Screen *self, const char *utf8_text) {
    if (screen_is_overlay_active(self)) deactivate_overlay_line(self);
    if (!utf8_text || !utf8_text[0]) return;
    PyObject *text = PyUnicode_FromString(utf8_text);
    if (!text) return;
    Py_XDECREF(self->overlay_line.overlay_text);
    // Calculate the total number of cells for initial overlay cursor position
    RAII_PyObject(text_len, wcswidth_std(NULL, text));
    self->overlay_line.overlay_text = text;
    self->overlay_line.is_active = true;
    self->overlay_line.is_dirty = true;
    self->overlay_line.xstart = self->cursor->x;
    self->overlay_line.xnum = !text_len ? 0 : PyLong_AsLong(text_len);
    self->overlay_line.text_len = self->overlay_line.xnum;
    self->overlay_line.cursor_x = MIN(self->overlay_line.xstart + self->overlay_line.xnum, self->columns);
    self->overlay_line.ynum = self->cursor->y;
    cursor_copy_to(self->cursor, &(self->overlay_line.original_line.cursor));
    linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum);
    self->is_dirty = true;
    // Since we are typing, scroll to the bottom
    if (self->scrolled_by != 0) {
        self->scrolled_by = 0;
        reset_pixel_scroll(self, 0);
        dirty_scroll(self);
    }
}

static void
screen_draw_overlay_line(Screen *self) {
    if (!self->overlay_line.overlay_text) return;
    // self->linebuf->line is a shared view that callers may not have pointed
    // at the overlay row (e.g. render_line_for_virtual_y inits a stack-local
    // Line instead). Without this, line->cpu_cells can be NULL or stale,
    // crashing the cell loops below.
    linebuf_init_line(self->linebuf, self->overlay_line.ynum);
    // Right-align the overlay to ensure that the pre-edit text just entered is visible when the cursor is near the end of the line.
    index_type xstart = self->overlay_line.text_len <= self->columns ? self->columns - self->overlay_line.text_len : 0;
    if (self->overlay_line.xstart < xstart) xstart = self->overlay_line.xstart;
    index_type columns_exceeded = self->overlay_line.text_len <= self->columns ? 0 : self->overlay_line.text_len - self->columns;
    bool orig_line_wrap_mode = self->modes.mDECAWM;
    bool orig_cursor_enable_mode = self->modes.mDECTCEM;
    bool orig_insert_replace_mode = self->modes.mIRM;
    self->modes.mDECAWM = false;
    self->modes.mDECTCEM = false;
    self->modes.mIRM = false;
    Cursor *orig_cursor = self->cursor;
    self->cursor = &(self->overlay_line.original_line.cursor);
    self->cursor->sgr.reverse ^= true;
    self->cursor->x = xstart;
    self->cursor->y = self->overlay_line.ynum;
    self->overlay_line.xnum = 0;
    if (xstart > 0) {
        // remove any multicell characters temporarily that intersect the left boundary,
        // the characters are not actually removed, just deleted on this line
        CPUCell *c = self->linebuf->line->cpu_cells + xstart;
        while (c->is_multicell && c->x && c < self->linebuf->line->cpu_cells + self->columns) {
            c->is_multicell = false; c->ch_or_idx = ' '; c->ch_is_idx = false;
            c++;
        }
    }
    index_type before;
    const int kind = PyUnicode_KIND(self->overlay_line.overlay_text);
    const void *data = PyUnicode_DATA(self->overlay_line.overlay_text);
    const Py_ssize_t sz = PyUnicode_GET_LENGTH(self->overlay_line.overlay_text);
    for (Py_ssize_t pos = 0; pos < sz; pos++) {
        before = self->cursor->x;
        draw_codepoint(self, PyUnicode_READ(kind, data, pos));
        index_type len = self->cursor->x - before;
        if (columns_exceeded > 0) {
            // Reset the cursor to maintain right alignment when the overlay exceeds the screen width.
            if (columns_exceeded > len) {
                columns_exceeded -= len;
                len = 0;
            } else {
                len = len > columns_exceeded ? len - columns_exceeded : 0;
                columns_exceeded = 0;
                if (len > 0) {
                    // When the last character is a split multicell, make sure the next character is visible.
                    CPUCell *c = self->linebuf->line->cpu_cells + len - 1;
                    if (c->is_multicell) {
                        if (c->x < mcd_x_limit(c) - 1) {
                            do {
                                c->is_multicell = false; c->ch_is_idx = false; c->ch_or_idx = ' ';
                                if (!c->x) break;
                                c--;
                            } while(c->is_multicell && c >= self->linebuf->line->cpu_cells);
                        }
                    }
                }
            }
            self->cursor->x = len;
        }
        self->overlay_line.xnum += len;
    }
    self->overlay_line.cursor_x = self->cursor->x;
    self->cursor->sgr.reverse ^= true;
    self->cursor = orig_cursor;
    self->modes.mDECAWM = orig_line_wrap_mode;
    self->modes.mDECTCEM = orig_cursor_enable_mode;
    self->modes.mIRM = orig_insert_replace_mode;
}

static void
update_overlay_position(Screen *self) {
    if (screen_is_overlay_active(self) && screen_is_cursor_visible(self)) {
        bool cursor_update = false;
        if (self->cursor->x != self->overlay_line.xstart) {
            cursor_update = true;
            self->overlay_line.xstart = self->cursor->x;
            self->overlay_line.cursor_x = MIN(self->overlay_line.xstart + self->overlay_line.xnum, self->columns);
        }
        if (self->cursor->y != self->overlay_line.ynum) {
            cursor_update = true;
            linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum);
            self->overlay_line.ynum = self->cursor->y;
        }
        if (cursor_update) {
            linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum);
            self->overlay_line.is_dirty = true;
            self->is_dirty = true;
        }
    }
}

static void
render_overlay_line(Screen *self, Line *line, FONTS_DATA_HANDLE fonts_data) {
#define ol self->overlay_line
    line_save_cells(line, 0, line->xnum, ol.original_line.gpu_cells, ol.original_line.cpu_cells);
    screen_draw_overlay_line(self);
    render_line(fonts_data, line, ol.ynum, self->cursor, self->disable_ligatures, self->lc);
    line_save_cells(line, 0, line->xnum, ol.gpu_cells, ol.cpu_cells);
    line_reset_cells(line, 0, line->xnum, ol.original_line.gpu_cells, ol.original_line.cpu_cells);
    ol.is_dirty = false;
    const index_type y = MIN(ol.ynum + self->scrolled_by, self->lines - 1);
    if (ol.last_ime_pos.x != ol.cursor_x || ol.last_ime_pos.y != y) {
        ol.last_ime_pos.x = ol.cursor_x; ol.last_ime_pos.y = y;
        update_ime_position_for_window(self->window_id, false, 0);
    }
#undef ol
}

static void
update_overlay_line_data(Screen *self, uint8_t *data) {
    const int render_row_offset = pixel_scroll_enabled(self);
    const size_t base = sizeof(GPUCell) * (self->overlay_line.ynum + self->scrolled_by + render_row_offset) * self->columns;
    memcpy(data + base, self->overlay_line.gpu_cells, self->columns * sizeof(GPUCell));
}

// }}}

// Python interface {{{
#define WRAP0(name) static PyObject* name(Screen *self, PyObject *a UNUSED) { screen_##name(self); Py_RETURN_NONE; }
#define WRAP0x(name) static PyObject* xxx_##name(Screen *self, PyObject *a UNUSED) { screen_##name(self); Py_RETURN_NONE; }
#define WRAP1(name, defval) static PyObject* name(Screen *self, PyObject *args) { unsigned int v=defval; if(!PyArg_ParseTuple(args, "|I", &v)) return NULL; screen_##name(self, v); Py_RETURN_NONE; }
#define WRAP1B(name, defval) static PyObject* name(Screen *self, PyObject *args) { unsigned int v=defval; int b=false; if(!PyArg_ParseTuple(args, "|Ip", &v, &b)) return NULL; screen_##name(self, v, b); Py_RETURN_NONE; }
#define WRAP1E(name, defval, ...) static PyObject* name(Screen *self, PyObject *args) { unsigned int v=defval; if(!PyArg_ParseTuple(args, "|I", &v)) return NULL; screen_##name(self, v, __VA_ARGS__); Py_RETURN_NONE; }
#define WRAP2(name, defval1, defval2) static PyObject* name(Screen *self, PyObject *args) { unsigned int a=defval1, b=defval2; if(!PyArg_ParseTuple(args, "|II", &a, &b)) return NULL; screen_##name(self, a, b); Py_RETURN_NONE; }
#define WRAP2B(name) static PyObject* name(Screen *self, PyObject *args) { unsigned int a, b; int p; if(!PyArg_ParseTuple(args, "IIp", &a, &b, &p)) return NULL; screen_##name(self, a, b, (bool)p); Py_RETURN_NONE; }

WRAP0(garbage_collect_hyperlink_pool)
WRAP0(garbage_collect_text_cache)

static PyObject*
text_cache_count(Screen *self, PyObject *a UNUSED) {
    return PyLong_FromUnsignedLong((unsigned long)tc_num_entries(self->text_cache));
}

static PyObject*
has_selection(Screen *self, PyObject *a UNUSED) {
    if (screen_has_selection(self)) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

static PyObject*
hyperlinks_as_set(Screen *self, PyObject *args UNUSED) {
    return screen_hyperlinks_as_set(self);
}

static PyObject*
hyperlink_for_id(Screen *self, PyObject *val) {
    unsigned long id = PyLong_AsUnsignedLong(val);
    if (id > HYPERLINK_MAX_NUMBER) { PyErr_SetString(PyExc_IndexError, "Out of bounds"); return NULL; }
    return Py_BuildValue("s", get_hyperlink_for_id(self->hyperlink_pool, id, true));
}

static Line* get_visual_line(void *x, int y) { return visual_line_(x, y); }
static Line* get_range_line(void *x, int y) { return range_line_(x, y); }

static PyObject*
as_text(Screen *self, PyObject *args) {
    return as_text_generic(args, self, get_visual_line, self->lines, &self->as_ansi_buf, false);
}

static PyObject*
as_text_non_visual(Screen *self, PyObject *args) {
    return as_text_generic(args, self, get_range_line, self->lines, &self->as_ansi_buf, false);
}

static PyObject*
as_text_for_history_buf(Screen *self, PyObject *args) {
    return as_text_history_buf(self->historybuf, args, &self->as_ansi_buf);
}

static PyObject*
as_text_generic_wrapper(Screen *self, PyObject *args, get_line_func get_line) {
    return as_text_generic(args, self, get_line, self->lines, &self->as_ansi_buf, false);
}

static PyObject*
as_text_alternate(Screen *self, PyObject *args) {
    LineBuf *original = self->linebuf;
    self->linebuf = original == self->main_linebuf ? self->alt_linebuf : self->main_linebuf;
    PyObject *ans = as_text_generic_wrapper(self, args, get_range_line);
    self->linebuf = original;
    return ans;
}

typedef struct OutputOffset {
    Screen *screen;
    int start;
    unsigned num_lines;
    bool reached_upper_limit;
} OutputOffset;

static Line*
get_line_from_offset(void *x, int y) {
    OutputOffset *r = x;
    return range_line_(r->screen, r->start + y);
}

static bool
find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsigned int scrolled_by, int direction, bool on_screen_only) {
    bool found_prompt = false, found_output = false, found_next_prompt = false;
    int start = 0, end = 0;
    int init_y = start_screen_y - scrolled_by, y1 = init_y, y2 = init_y;
    const int upward_limit = -self->historybuf->count;
    const int downward_limit = self->lines - 1;
    const int screen_limit = -scrolled_by + downward_limit;
    Line *line = NULL;

    // find around
    if (direction == 0) {
        line = checked_range_line(self, y1);
        if (line && line->attrs.prompt_kind == PROMPT_START) {
            found_prompt = true;
            // change direction to downwards to find command output
            direction = 1;
        } else if (line && line->attrs.prompt_kind == OUTPUT_START) {
            found_output = true; start = y1;
            found_prompt = true;
            direction = 1;
        }
        y1--; y2++;
    }

    // find upwards
    if (direction <= 0) {
        // find around: only needs to find the first output start
        // find upwards: find prompt after the output, and the first output
        while (y1 >= upward_limit) {
            line = checked_range_line(self, y1);
            if (line && line->attrs.prompt_kind == PROMPT_START) {
                if (direction == 0) {
                    found_prompt = true;
                    break;
                }
                found_next_prompt = true; end = y1;
            } else if (line && line->attrs.prompt_kind == OUTPUT_START) {
                found_output = true; start = y1;
                found_prompt = true;
                break;
            }
            y1--;
        }
        if (y1 < upward_limit) {
            oo->reached_upper_limit = true;
            found_output = direction != 0; start = upward_limit;
            found_prompt = direction != 0;
        }
    }

    // find downwards
    if (direction >= 0) {
        while (y2 <= downward_limit) {
            if (on_screen_only && !found_output && y2 > screen_limit) break;
            line = checked_range_line(self, y2);
            if (line && line->attrs.prompt_kind == PROMPT_START) {
                if (!found_prompt) {
                    if (direction == 0) {
                        found_next_prompt = true; end = y2;
                        break;
                    }
                    found_prompt = true;
                } else if (!found_output) {
                    // skip fetching wrapped prompt lines
                    while (range_line_is_continued(self, y2)) {
                        y2++;
                    }
                } else if (!found_next_prompt) {
                    found_next_prompt = true; end = y2;
                    break;
                }
            } else if (line && line->attrs.prompt_kind == OUTPUT_START && !found_output) {
                found_output = true; start = y2;
                if (!found_prompt) found_prompt = true;
            }
            y2++;
        }
    }

    if (found_next_prompt) {
        oo->num_lines = end >= start ? end - start : 0;
    } else if (found_output) {
        end = (direction < 0 ? MIN(init_y, downward_limit) : downward_limit) + 1;
        oo->num_lines = end >= start ? end - start : 0;
    } else return false;
    oo->start = start;
    return oo->num_lines > 0;
}

static PyObject*
erase_last_command(Screen *self, PyObject *args UNUSED) {
    if (self->linebuf != self->main_linebuf) Py_RETURN_FALSE;
    Line *line;
    // Erase the most recent command: the prompt block immediately above the
    // current (live) prompt, regardless of whether that command produced any
    // output. Commands without output (an empty Enter, a comment, cd, export,
    // ...) emit no OSC 133;C, so a search anchored on OUTPUT_START would skip
    // them and erase an older command instead. Selecting by prompt marks makes
    // every submitted command one unit, removed newest-first.
    int y = self->cursor->y + self->scrolled_by;
    bool found = false;
    for (; (line = checked_range_line(self, y)); y--) {
        if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
    }
    if (!found) Py_RETURN_FALSE;
    const int live_prompt_start = y;
    found = false;
    for (y = live_prompt_start - 1; (line = checked_range_line(self, y)); y--) {
        if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
    }
    if (!found) Py_RETURN_FALSE;  // nothing above the current prompt to erase
    const int start = y;
    const index_type num_lines = (index_type)(live_prompt_start - start);
    index_type num_lines_to_erase_in_screen = start >= 0 ? num_lines : num_lines + start;
    num_lines_to_erase_in_screen = MIN(self->cursor->y, num_lines_to_erase_in_screen);
    if (num_lines_to_erase_in_screen) {
        // Anchor the on-screen deletion at the top of the region, not at
        // cursor->y - count: a multi-line prompt or rows skipped above would
        // otherwise shift the deletion and leave residual lines on screen.
        index_type screen_erase_start = start >= 0 ? (index_type)start : 0;
        screen_delete_lines_impl(self, screen_erase_start, num_lines_to_erase_in_screen, 0, self->lines - 1);
        self->cursor->y -= num_lines_to_erase_in_screen;
    }
    if (num_lines > num_lines_to_erase_in_screen) {
        historybuf_delete_newest_lines(self->historybuf, num_lines - num_lines_to_erase_in_screen);
        // The scrollback shrank: clamp the scroll position and signal a redraw,
        // otherwise the deletion of off-screen lines is not reflected until the
        // next scroll event forces the history viewport to be recomputed.
        self->scrolled_by = MIN(self->scrolled_by, self->historybuf->count);
        self->is_dirty = true;
        dirty_scroll(self);
    }
    Py_RETURN_TRUE;
}

static PyObject*
cmd_output(Screen *self, PyObject *args) {
    unsigned int which = 0;
    RAII_PyObject(which_args, PyTuple_GetSlice(args, 0, 1));
    RAII_PyObject(as_text_args, PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args)));
    if (!which_args || !as_text_args) return NULL;
    if (!PyArg_ParseTuple(which_args, "I", &which)) return NULL;
    if (self->linebuf != self->main_linebuf) Py_RETURN_NONE;
    OutputOffset oo = {.screen=self};
    bool found = false;

    switch (which) {
        case 0: // last run cmd
            // When scrolled, the starting point of the search for the last command output
            // is actually out of the screen, so add the number of scrolled lines
            found = find_cmd_output(self, &oo, self->cursor->y + self->scrolled_by, self->scrolled_by, -1, false);
            break;
        case 1: // first on screen
            found = find_cmd_output(self, &oo, 0, self->scrolled_by, 1, true);
            break;
        case 2: // last visited cmd
            if (self->last_visited_prompt.scrolled_by <= self->historybuf->count && self->last_visited_prompt.is_set) {
                found = find_cmd_output(self, &oo, self->last_visited_prompt.y, self->last_visited_prompt.scrolled_by, 0, false);
            } break;
        case 3: { // last non-empty output
            int y = self->cursor->y;
            Line *line;
            bool reached_upper_limit = false;
            while (!found && !reached_upper_limit) {
                line = checked_range_line(self, y);
                if (!line || (line->attrs.prompt_kind == OUTPUT_START)) {
                    int start = line ? y : y + 1; reached_upper_limit = !line;
                    int y2 = start; unsigned int num_lines = 0;
                    bool found_content = false;
                    while ((line = checked_range_line(self, y2)) && line->attrs.prompt_kind != PROMPT_START) {
                        if (!found_content) found_content = !line_is_empty(line);
                        num_lines++; y2++;
                    }
                    if (found_content) {
                        found = true;
                        oo.reached_upper_limit = reached_upper_limit;
                        oo.start = start; oo.num_lines = num_lines;
                        break;
                    }
                }
                y--;
            }
        } break;
        default:
            PyErr_Format(PyExc_KeyError, "%u is not a valid type of command", which);
            return NULL;
    }
    if (found) {
        RAII_PyObject(ret, as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf, false));
        if (!ret) return NULL;
    }
    if (oo.reached_upper_limit && self->linebuf == self->main_linebuf && OPT(scrollback_pager_history_size) > 0) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

bool
screen_set_last_visited_prompt(Screen *self, index_type y) {
    if (y >= self->lines) return false;
    self->last_visited_prompt.scrolled_by = self->scrolled_by;
    self->last_visited_prompt.y = y;
    self->last_visited_prompt.is_set = true;
    return true;
}

bool
screen_select_cmd_output(Screen *self, index_type y) {
    if (y >= self->lines) return false;
    OutputOffset oo = {.screen=self};
    if (!find_cmd_output(self, &oo, y, self->scrolled_by, 0, true)) return false;

    screen_start_selection(self, 0, y, true, false, EXTEND_LINE);
    Selection *s = self->selections.items;
#define S(which, offset_y, scrolled_by) \
    if (offset_y < 0) { \
        s->scrolled_by = -(offset_y); s->which.y = 0; \
    } else { \
        s->scrolled_by = 0; s->which.y = offset_y; \
    }
    S(start, oo.start, start_scrolled_by);
    S(end, oo.start + (int)oo.num_lines - 1, end_scrolled_by);
#undef S
    s->start.x = 0; s->start.in_left_half_of_cell = true;
    s->end.x = self->columns; s->end.in_left_half_of_cell = false;
    self->selections.in_progress = false;

    call_boss(set_primary_selection, NULL);
    return true;
}

static PyObject*
screen_truncate_point_for_length(PyObject UNUSED *self, PyObject *args) {
    PyObject *str; unsigned int num_cells, start_pos = 0;
    if (!PyArg_ParseTuple(args, "UI|I", &str, &num_cells, &start_pos)) return NULL;
    if (PyUnicode_READY(str) != 0) return NULL;
    int kind = PyUnicode_KIND(str);
    void *data = PyUnicode_DATA(str);
    Py_ssize_t len = PyUnicode_GET_LENGTH(str), i;
    char_type prev_ch = 0;
    int prev_width = 0;
    bool in_sgr = false;
    unsigned long width_so_far = 0;
    for (i = start_pos; i < len && width_so_far < num_cells; i++) {
        char_type ch = PyUnicode_READ(kind, data, i);
        if (in_sgr) {
            if (ch == 'm') in_sgr = false;
            continue;
        }
        if (ch == 0x1b && i + 1 < len && PyUnicode_READ(kind, data, i + 1) == '[') { in_sgr = true; continue; }
        if (ch == 0xfe0f) {
            if (is_emoji_presentation_base(prev_ch) && prev_width == 1) {
                width_so_far += 1;
                prev_width = 2;
            } else prev_width = 0;
        } else {
            int w = wcwidth_std(char_props_for(ch));
            switch(w) {
                case -1:
                case 0:
                    prev_width = 0; break;
                case 2:
                    prev_width = 2; break;
                default:
                    prev_width = 1; break;
            }
            if (width_so_far + prev_width > num_cells) { break; }
            width_so_far += prev_width;
        }
        prev_ch = ch;

    }
    return PyLong_FromUnsignedLong(i);
}


static PyObject*
line(Screen *self, PyObject *val) {
    unsigned long y = PyLong_AsUnsignedLong(val);
    if (y >= self->lines) { PyErr_SetString(PyExc_IndexError, "Out of bounds"); return NULL; }
    linebuf_init_line(self->linebuf, y);
    Py_INCREF(self->linebuf->line);
    return (PyObject*) self->linebuf->line;
}

Line*
screen_visual_line(Screen *self, index_type y) {
    if (y >= self->lines) return NULL;
    return visual_line_(self, y);
}

static PyObject*
pyvisual_line(Screen *self, PyObject *args) {
    // The line corresponding to the yth visual line, taking into account scrolling
    unsigned int y;
    if (!PyArg_ParseTuple(args, "I", &y)) return NULL;
    if (y >= self->lines) { Py_RETURN_NONE; }
    return Py_BuildValue("O", visual_line_(self, y));
}

static PyObject*
draw(Screen *self, PyObject *src) {
    if (!PyUnicode_Check(src)) { PyErr_SetString(PyExc_TypeError, "A unicode string is required"); return NULL; }
    if (PyUnicode_READY(src) != 0) { return PyErr_NoMemory(); }
    Py_UCS4 *buf = PyUnicode_AsUCS4Copy(src);
    if (!buf) return NULL;
    draw_text(self, buf, PyUnicode_GetLength(src));
    PyMem_Free(buf);
    Py_RETURN_NONE;
}

static PyObject*
apply_sgr(Screen *self, PyObject *src) {
    if (!PyUnicode_Check(src)) { PyErr_SetString(PyExc_TypeError, "A unicode string is required"); return NULL; }
    if (PyUnicode_READY(src) != 0) { return PyErr_NoMemory(); }
    Py_ssize_t sz;
    const char *s = PyUnicode_AsUTF8AndSize(src, &sz);
    if (s == NULL) return NULL;
    if (!parse_sgr(self, (const uint8_t*)s, sz, "parse_sgr", false)) {
        PyErr_Format(PyExc_ValueError, "Invalid SGR: %s", PyUnicode_AsUTF8(src));
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyObject*
reset_mode(Screen *self, PyObject *args) {
    int private = false;
    unsigned int mode;
    if (!PyArg_ParseTuple(args, "I|p", &mode, &private)) return NULL;
    if (private) mode <<= 5;
    screen_reset_mode(self, mode);
    Py_RETURN_NONE;
}

static PyObject*
_select_graphic_rendition(Screen *self, PyObject *args) {
    int params[256] = {0};
    for (int i = 0; i < PyTuple_GET_SIZE(args); i++) { params[i] = PyLong_AsLong(PyTuple_GET_ITEM(args, i)); }
    select_graphic_rendition(self, params, PyTuple_GET_SIZE(args), false, NULL);
    Py_RETURN_NONE;
}

static PyObject*
set_mode(Screen *self, PyObject *args) {
    int private = false;
    unsigned int mode;
    if (!PyArg_ParseTuple(args, "I|p", &mode, &private)) return NULL;
    if (private) mode <<= 5;
    screen_set_mode(self, mode);
    Py_RETURN_NONE;
}

static PyObject*
reset_dirty(Screen *self, PyObject *a UNUSED) {
    screen_reset_dirty(self);
    Py_RETURN_NONE;
}

static PyObject*
set_window_char(Screen *self, PyObject *a) {
    const char *text = "";
    if (!PyArg_ParseTuple(a, "|s", &text)) return NULL;
    self->display_window_char = text[0];
    self->is_dirty = true;
    Py_RETURN_NONE;
}

static PyObject*
set_progress(Screen *self, PyObject *a) {
    unsigned int state = 0, percent = 0;
    if (!PyArg_ParseTuple(a, "II", &state, &percent)) return NULL;
    ProgressBarState new_state = (ProgressBarState)(state > PROGRESS_STATE_PAUSED ? PROGRESS_STATE_UNSET : state);
    uint8_t new_percent = (uint8_t)(percent > 100 ? 100 : percent);
    if (self->progress_state != new_state || self->progress_percent != new_percent) {
        self->progress_state = new_state;
        self->progress_percent = new_percent;
        // Start or stop indeterminate animation
        if (new_state == PROGRESS_STATE_INDETERMINATE && self->progress_indeterminate_anim_at == 0) {
            self->progress_indeterminate_anim_at = monotonic();
        } else if (new_state != PROGRESS_STATE_INDETERMINATE) {
            self->progress_indeterminate_anim_at = 0;
        }
        self->is_dirty = true;
    }
    Py_RETURN_NONE;
}

static PyObject*
is_using_alternate_linebuf(Screen *self, PyObject *a UNUSED) {
    if (self->linebuf == self->alt_linebuf) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

WRAP1E(cursor_move, 1, -1, true)
WRAP1B(erase_in_line, 0)
WRAP1B(erase_in_display, 0)
static PyObject* scroll_until_cursor_prompt(Screen *self, PyObject *args) { int b=false; if(!PyArg_ParseTuple(args, "|p", &b)) return NULL; screen_scroll_until_cursor_prompt(self, b); Py_RETURN_NONE; }

WRAP0(clear_scrollback)

#define MODE_GETSET(name, uname) \
    static PyObject* name##_get(Screen *self, void UNUSED *closure) { PyObject *ans = self->modes.m##uname ? Py_True : Py_False; Py_INCREF(ans); return ans; } \
    static int name##_set(Screen *self, PyObject *val, void UNUSED *closure) { if (val == NULL) { PyErr_SetString(PyExc_TypeError, "Cannot delete attribute"); return -1; } set_mode_from_const(self, uname, PyObject_IsTrue(val) ? true : false); return 0; }

MODE_GETSET(in_bracketed_paste_mode, BRACKETED_PASTE)
MODE_GETSET(focus_tracking_enabled, FOCUS_TRACKING)
MODE_GETSET(color_preference_notification, COLOR_PREFERENCE_NOTIFICATION)
MODE_GETSET(in_band_resize_notification, INBAND_RESIZE_NOTIFICATION)
MODE_GETSET(paste_events, PASTE_EVENTS)
MODE_GETSET(auto_repeat_enabled, DECARM)
MODE_GETSET(cursor_visible, DECTCEM)
MODE_GETSET(cursor_key_mode, DECCKM)

static PyObject* disable_ligatures_get(Screen *self, void UNUSED *closure) {
    const char *ans = NULL;
    switch(self->disable_ligatures) {
        case DISABLE_LIGATURES_NEVER:
            ans = "never";
            break;
        case DISABLE_LIGATURES_CURSOR:
            ans = "cursor";
            break;
        case DISABLE_LIGATURES_ALWAYS:
            ans = "always";
            break;
    }
    return PyUnicode_FromString(ans);
}

static int disable_ligatures_set(Screen *self, PyObject *val, void UNUSED *closure) {
    if (val == NULL) { PyErr_SetString(PyExc_TypeError, "Cannot delete attribute"); return -1; }
    if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "unicode string expected"); return -1; }
    if (PyUnicode_READY(val) != 0) return -1;
    const char *q = PyUnicode_AsUTF8(val);
    DisableLigature dl = DISABLE_LIGATURES_NEVER;
    if (strcmp(q, "always") == 0) dl = DISABLE_LIGATURES_ALWAYS;
    else if (strcmp(q, "cursor") == 0) dl = DISABLE_LIGATURES_CURSOR;
    if (dl != self->disable_ligatures) {
        self->disable_ligatures = dl;
        screen_dirty_sprite_positions(self);
    }
    return 0;
}

static PyObject*
render_unfocused_cursor_get(Screen *self, void UNUSED *closure) {
    if (self->cursor_render_info.render_even_when_unfocused) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

static int
render_unfocused_cursor_set(Screen *self, PyObject *val, void UNUSED *closure) {
    if (val == NULL) { PyErr_SetString(PyExc_TypeError, "Cannot delete attribute"); return -1; }
    self->cursor_render_info.render_even_when_unfocused = PyObject_IsTrue(val);
    return 0;
}

static PyObject*
cursor_up(Screen *self, PyObject *args) {
    unsigned int count = 1;
    int do_carriage_return = false, move_direction = -1;
    if (!PyArg_ParseTuple(args, "|Ipi", &count, &do_carriage_return, &move_direction)) return NULL;
    screen_cursor_up(self, count, do_carriage_return, move_direction);
    Py_RETURN_NONE;
}

static PyObject*
update_selection(Screen *self, PyObject *args) {
    unsigned int x, y;
    int in_left_half_of_cell = 0, ended = 1, nearest = 0;
    if (!PyArg_ParseTuple(args, "II|ppp", &x, &y, &in_left_half_of_cell, &ended, &nearest)) return NULL;
    screen_update_selection(self, x, y, in_left_half_of_cell, (SelectionUpdate){.ended = ended, .set_as_nearest_extend=nearest});
    Py_RETURN_NONE;
}

static PyObject*
clear_selection_(Screen *s, PyObject *args UNUSED) {
    clear_selection(&s->selections);
    Py_RETURN_NONE;
}

static PyObject*
resize(Screen *self, PyObject *args) {
    unsigned int a=1, b=1;
    if(!PyArg_ParseTuple(args, "|II", &a, &b)) return NULL;
    screen_resize(self, a, b);
    if (PyErr_Occurred()) return NULL;
    Py_RETURN_NONE;
}

WRAP0x(index)
WRAP0(reverse_index)
static PyObject* reset(Screen *self, PyObject *args) { int hard = true; if (!PyArg_ParseTuple(args, "|p", &hard)) return NULL; if (hard) screen_reset(self); else screen_soft_reset(self); Py_RETURN_NONE; }
WRAP0(set_tab_stop)
WRAP1(clear_tab_stop, 0)
WRAP0(reset_tab_stops)
WRAP0(backspace)
WRAP0(tab)
WRAP0(linefeed)
WRAP0(carriage_return)
WRAP2(set_margins, 1, 1)
WRAP2(detect_url, 0, 0)
WRAP0(rescale_images)

static PyObject*
current_key_encoding_flags(Screen *self, PyObject *args UNUSED) {
    unsigned long ans = screen_current_key_encoding_flags(self);
    return PyLong_FromUnsignedLong(ans);
}

static PyObject*
ignore_bells_for(Screen *self, PyObject *args) {
    double duration = 1;
    if (!PyArg_ParseTuple(args, "|d", &duration)) return NULL;
    self->ignore_bells.start = monotonic();
    self->ignore_bells.duration = s_double_to_monotonic_t(duration);
    Py_RETURN_NONE;
}

static PyObject*
start_selection(Screen *self, PyObject *args) {
    unsigned int x, y;
    int rectangle_select = 0, extend_mode = EXTEND_CELL, in_left_half_of_cell = 1;
    if (!PyArg_ParseTuple(args, "II|pip", &x, &y, &rectangle_select, &extend_mode, &in_left_half_of_cell)) return NULL;
    screen_start_selection(self, x, y, in_left_half_of_cell, rectangle_select, extend_mode);
    Py_RETURN_NONE;
}

static PyObject*
is_rectangle_select(Screen *self, PyObject *a UNUSED) {
    if (self->selections.count && self->selections.items[0].rectangle_select) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

static PyObject*
copy_colors_from(Screen *self, Screen *other) {
    copy_color_profile(self->color_profile, other->color_profile);
    Py_RETURN_NONE;
}

static PyObject*
text_for_selections(Screen *self, Selections *selections, bool ansi, bool strip_trailing_whitespace) {
    PyObject *lines = NULL;
    for (size_t i = 0; i < selections->count; i++) {
        PyObject *temp = ansi ? ansi_for_range(self, selections->items +i, true, strip_trailing_whitespace) : text_for_range(self, selections->items + i, true, strip_trailing_whitespace);
        if (temp) {
            if (lines) {
                lines = extend_tuple(lines, temp);
                Py_DECREF(temp);
            } else lines = temp;
        } else break;
    }
    if (PyErr_Occurred()) { Py_CLEAR(lines); return NULL; }
    if (!lines) lines = PyTuple_New(0);
    return lines;
}

static PyObject*
text_for_selection(Screen *self, PyObject *args) {
    int ansi = 0, strip_trailing_whitespace = 0;
    if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
    return text_for_selections(self, &self->selections, ansi, strip_trailing_whitespace);
}

static PyObject*
text_for_marked_url(Screen *self, PyObject *args) {
    int ansi = 0, strip_trailing_whitespace = 0;
    if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
    return text_for_selections(self, &self->url_ranges, ansi, strip_trailing_whitespace);
}

static bool
cell_is_blank(const CPUCell *c) {
    return !cell_has_text(c) || cell_is_char(c, ' ');
}

static void
screen_selection_range_for_line_(Line *line, index_type *start, index_type *end) {
    index_type xlimit = line->xnum, xstart = 0;
    while (xlimit > 0 && cell_is_blank(line->cpu_cells + xlimit - 1)) xlimit--;
    while (xstart < xlimit && cell_is_blank(line->cpu_cells + xstart)) xstart++;
    *start = xstart; *end = xlimit > 0 ? xlimit - 1 : 0;
}

bool
screen_selection_range_for_line(Screen *self, index_type y, index_type *start, index_type *end) {
    if (y >= self->lines) { return false; }
    screen_selection_range_for_line_(visual_line_(self, y), start, end);
    return true;
}

static bool
is_opt_word_char(char_type ch, bool forward) {
    if (forward && OPT(select_by_word_characters_forward)) {
        for (const char_type *p = OPT(select_by_word_characters_forward); *p; p++) {
            if (ch == *p) return true;
        }
        if (*OPT(select_by_word_characters_forward)) {
            return false;
        }
    }
    if (OPT(select_by_word_characters)) {
        for (const char_type *p = OPT(select_by_word_characters); *p; p++) {
            if (ch == *p) return true;
        }
    }
    return false;
}

static bool
is_char_ok_for_word_extension(Line* line, index_type x, bool forward) {
    char_type ch = cell_first_char(line->cpu_cells + x, line->text_cache);
    if (char_props_for(ch).is_word_char || is_opt_word_char(ch, forward)) return true;
    // pass : from :// so that common URLs are matched
    return ch == ':' && x + 2 < line->xnum && cell_is_char(line->cpu_cells + x + 1, '/') && cell_is_char(line->cpu_cells + x + 2,  '/');
}

bool
screen_selection_range_for_word(Screen *self, const index_type x, const index_type y, index_type *y1, index_type *y2, index_type *s, index_type *e, bool initial_selection) {
    if (y >= self->lines || x >= self->columns) return false;
    index_type start, end;
    Line *line = visual_line_(self, y);
    *y1 = y;
    *y2 = y;
#define is_ok(x, forward) is_char_ok_for_word_extension(line, x, forward)
    if (!is_ok(x, false)) {
        if (initial_selection) return false;
        *s = x; *e = x;
        return true;
    }
    start = x; end = x;
    while(true) {
        while(start > 0 && is_ok(start - 1, false)) start--;
        if (start > 0 || !visual_line_is_continued(self, y) || *y1 == 0) break;
        line = visual_line_(self, *y1 - 1);
        if (!is_ok(self->columns - 1, false)) break;
        (*y1)--; start = self->columns - 1;
    }
    line = visual_line_(self, *y2);
    while(true) {
        while(end < self->columns - 1 && is_ok(end + 1, true)) end++;
        if (end < self->columns - 1 || *y2 >= self->lines - 1) break;
        line = visual_line_(self, *y2 + 1);
        if (!visual_line_is_continued(self, *y2 + 1) || !is_ok(0, true)) break;
        (*y2)++; end = 0;
    }
    *s = start; *e = end;
    return true;
#undef is_ok
}

void
screen_history_scroll_to_absolute(Screen *self, double target_scrolled_by) {
    if (self->linebuf != self->main_linebuf) return;
    index_type target_scrolled_by_line = (index_type)target_scrolled_by;
    unsigned pixel_scroll_offset_y = (unsigned)((target_scrolled_by - target_scrolled_by_line) * self->cell_size.height);
    if (!OPT(pixel_scroll)) pixel_scroll_offset_y = 0;
    if (target_scrolled_by_line > self->historybuf->count) target_scrolled_by_line = self->historybuf->count;
    if (target_scrolled_by_line >= self->historybuf->count) pixel_scroll_offset_y = 0;
    if (target_scrolled_by_line != self->scrolled_by || self->pixel_scroll_offset_y != pixel_scroll_offset_y) {
        self->scrolled_by = target_scrolled_by_line;
        reset_pixel_scroll(self, pixel_scroll_offset_y);
        dirty_scroll(self);
    }
}

bool
screen_apply_pixel_scroll(Screen *self, double delta_pixels) {
    if (!pixel_scroll_enabled(self)) return false;
    if (!self->historybuf->count) return false;
    const double cell_height = (double)self->cell_size.height;
    if (cell_height <= 0.0 || delta_pixels == 0.0) return false;

    double total = self->pixel_scroll_offset_y + (double)self->scrolled_by * cell_height + delta_pixels;
    const double max_total = (double)self->historybuf->count * cell_height;
    if (total < 0.0) total = 0.0;
    if (total > max_total) total = max_total;
    const unsigned int new_scrolled_by = (unsigned int)floor(total / cell_height);
    const unsigned offset = (unsigned)(total - (double)new_scrolled_by * cell_height);
    bool changed = false;
    if (new_scrolled_by != self->scrolled_by) {
        self->scrolled_by = new_scrolled_by;
        changed = true;
    }

    if (offset != self->pixel_scroll_offset_y) {
        self->pixel_scroll_offset_y = offset;
        changed = true;
    }
    if (changed) dirty_scroll(self);
    return changed;
}

bool
screen_history_scroll(Screen *self, int amt, bool upwards) {
    switch(amt) {
        case SCROLL_LINE:
            amt = 1;
            break;
        case SCROLL_PAGE:
            amt = self->lines - 1;
            break;
        case SCROLL_FULL:
            amt = self->historybuf->count;
            break;
        default:
            amt = MAX(0, amt);
            break;
    }
    if (!upwards) {
        amt = MIN((unsigned int)amt, self->scrolled_by);
        amt *= -1;
    }
    unsigned int new_scroll = MIN(self->scrolled_by + amt, self->historybuf->count);
    if (new_scroll != self->scrolled_by || (new_scroll == 0 && self->pixel_scroll_offset_y != 0)) {
        self->scrolled_by = new_scroll;
        reset_pixel_scroll(self, 0);
        dirty_scroll(self);
        return true;
    }
    return false;
}

static bool
screen_fractional_scroll(Screen *self, double amt) {
    if (amt == 0) return false;
    index_type before_scrolled_by = self->scrolled_by;
    double before_pixels = self->pixel_scroll_offset_y;
    double integral_part, fractional_part = modf(amt, &integral_part);
    int lines = (int)integral_part;
    int pixels = (int)(fractional_part * self->cell_size.height);
    if (amt > 0) {  // downwards
        if (fractional_part != 0) pixels = MAX(1, pixels);
        if (lines > (int)self->scrolled_by) {
            self->scrolled_by = 0; self->pixel_scroll_offset_y = 0;
        } else {
            self->scrolled_by -= lines;
            if (pixels <= (int)self->pixel_scroll_offset_y) self->pixel_scroll_offset_y -= pixels;
            else {
                self->pixel_scroll_offset_y = 0;
                if (self->scrolled_by) {
                    self->scrolled_by--; self->pixel_scroll_offset_y = self->cell_size.height - pixels;
                }
            }
        }
    } else {
        if (fractional_part != 0) pixels = MIN(-1, pixels);
        self->pixel_scroll_offset_y -= pixels;  // pixels is negative
        if (self->pixel_scroll_offset_y >= self->cell_size.height) {
            self->pixel_scroll_offset_y = 0; self->scrolled_by++;
        }
        self->scrolled_by = MIN(self->scrolled_by - lines, self->historybuf->count);
        if (self->scrolled_by >= self->historybuf->count) self->pixel_scroll_offset_y = 0;
    }
    if (self->scrolled_by != before_scrolled_by || self->pixel_scroll_offset_y != before_pixels) {
        dirty_scroll(self);
        return true;
    }
    return false;
}

static PyObject*
fractional_scroll(Screen *self, PyObject *amt) {
    double y;
    if (PyFloat_Check(amt)) y = PyFloat_AS_DOUBLE(amt);
    else if (PyLong_Check(amt)) y = PyLong_AsDouble(amt);
    else { PyErr_SetString(PyExc_TypeError, "amt must be a float"); return NULL; }
    return Py_NewRef(screen_fractional_scroll(self, y) ? Py_True : Py_False);
}

static PyObject*
scroll_to_absolute(Screen *self, PyObject *amt) {
    double y;
    if (PyFloat_Check(amt)) y = PyFloat_AS_DOUBLE(amt);
    else if (PyLong_Check(amt)) y = PyLong_AsDouble(amt);
    else { PyErr_SetString(PyExc_TypeError, "amt must be a number"); return NULL; }
    screen_history_scroll_to_absolute(self, y);
    Py_RETURN_NONE;
}

static PyObject*
scroll(Screen *self, PyObject *args) {
    int amt, upwards;
    if (!PyArg_ParseTuple(args, "ip", &amt, &upwards)) return NULL;
    if (screen_history_scroll(self, amt, upwards)) { Py_RETURN_TRUE; }
    Py_RETURN_FALSE;
}

static PyObject*
scroll_to_prompt(Screen *self, PyObject *args) {
    int num_of_prompts = -1;
    int scroll_offset = 0;
    if (!PyArg_ParseTuple(args, "|ii", &num_of_prompts, &scroll_offset)) return NULL;
    if (screen_history_scroll_to_prompt(self, num_of_prompts, scroll_offset)) { Py_RETURN_TRUE; }
    Py_RETURN_FALSE;
}

static PyObject*
set_last_visited_prompt(Screen *self, PyObject *args) {
    index_type visual_y = 0;
    if (!PyArg_ParseTuple(args, "|I", &visual_y)) return NULL;
    if (screen_set_last_visited_prompt(self, visual_y)) { Py_RETURN_TRUE; }
    Py_RETURN_FALSE;
}

bool
screen_is_selection_dirty(Screen *self) {
    IterationData q;
    if (self->paused_rendering.expires_at) return false;
    if (self->scrolled_by != self->last_rendered.scrolled_by) return true;
    if (self->selections.last_rendered_count != self->selections.count || self->url_ranges.last_rendered_count != self->url_ranges.count || self->extra_cursors.dirty) return true;
    for (size_t i = 0; i < self->selections.count; i++) {
        iteration_data(self->selections.items + i, &q, self->columns, 0, self->scrolled_by);
        if (memcmp(&q, &self->selections.items[i].last_rendered, sizeof(IterationData)) != 0) return true;
    }
    for (size_t i = 0; i < self->url_ranges.count; i++) {
        iteration_data(self->url_ranges.items + i, &q, self->columns, 0, self->scrolled_by);
        if (memcmp(&q, &self->url_ranges.items[i].last_rendered, sizeof(IterationData)) != 0) return true;
    }
    return false;
}

void
screen_start_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool rectangle_select, SelectionExtendMode extend_mode) {
    screen_pause_rendering(self, false, 0);
#define A(attr, val) self->selections.items->attr = val;
    ensure_space_for(&self->selections, items, Selection, self->selections.count + 1, capacity, 1, false);
    memset(self->selections.items, 0, sizeof(Selection));
    self->selections.count = 1;
    self->selections.in_progress = true;
    self->selections.extend_mode = extend_mode;
    self->selections.items[0].last_rendered.y = INT_MAX;
    A(start.x, x); A(end.x, x); A(start.y, y); A(end.y, y); A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
    A(rectangle_select, rectangle_select); A(start.in_left_half_of_cell, in_left_half_of_cell); A(end.in_left_half_of_cell, in_left_half_of_cell);
    A(input_start.x, x); A(input_start.y, y); A(input_start.in_left_half_of_cell, in_left_half_of_cell);
    A(input_current.x, x); A(input_current.y, y); A(input_current.in_left_half_of_cell, in_left_half_of_cell);
#undef A
}

static void
add_url_range(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y, bool is_hyperlink) {
#define A(attr, val) r->attr = val;
    ensure_space_for(&self->url_ranges, items, Selection, self->url_ranges.count + 8, capacity, 8, false);
    Selection *r = self->url_ranges.items + self->url_ranges.count++;
    memset(r, 0, sizeof(Selection));
    r->last_rendered.y = INT_MAX;
    r->is_hyperlink = is_hyperlink;
    A(start.x, start_x); A(end.x, end_x); A(start.y, start_y); A(end.y, end_y);
    A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
    A(start.in_left_half_of_cell, true);
#undef A
}

void
screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) {
    self->url_ranges.count = 0;
    if (start_x || start_y || end_x || end_y) add_url_range(self, start_x, start_y, end_x, end_y, false);
}

static bool
mark_hyperlinks_in_line(Screen *self, Line *line, hyperlink_id_type id, index_type y, bool *found_nonzero_multiline) {
    index_type start = 0;
    bool found = false;
    bool in_range = false;
    *found_nonzero_multiline = false;
    for (index_type x = 0; x < line->xnum; x++) {
        bool has_hyperlink = line->cpu_cells[x].hyperlink_id == id;
        bool is_nonzero_multiline = line->cpu_cells[x].is_multicell && line->cpu_cells[x].y > 0;
        if (has_hyperlink && is_nonzero_multiline) {
            has_hyperlink = false;
            *found_nonzero_multiline = true;
        }
        if (in_range) {
            if (!has_hyperlink) {
                add_url_range(self, start, y, x - 1, y, true);
                in_range = false;
                start = 0;
            }
        } else {
            if (has_hyperlink) {
                start = x; in_range = true;
                found = true;
            }
        }
    }
    if (in_range) add_url_range(self, start, y, self->columns - 1, y, true);
    return found;
}

static void
sort_ranges(const Screen *self, Selections *s) {
    IterationData a;
    for (size_t i = 0; i < s->count; i++) {
        iteration_data(s->items + i, &a, self->columns, 0, 0);
        s->items[i].sort_x = a.first.x;
        s->items[i].sort_y = a.y;
    }
#define range_lt(a, b) ((a)->sort_y < (b)->sort_y || ((a)->sort_y == (b)->sort_y && (a)->sort_x < (b)->sort_x))
    QSORT(Selection, s->items, s->count, range_lt);
#undef range_lt
}

hyperlink_id_type
screen_mark_hyperlink(Screen *self, index_type x, index_type y) {
    self->url_ranges.count = 0;
    Line *line = screen_visual_line(self, y);
    hyperlink_id_type id = line->cpu_cells[x].hyperlink_id;
    if (!id) return 0;
    index_type ypos = y, last_marked_line = y;
    bool found_nonzero_multiline;
    do {
        if (mark_hyperlinks_in_line(self, line, id, ypos, &found_nonzero_multiline) || found_nonzero_multiline) last_marked_line = ypos;
        if (ypos == 0) break;
        ypos--;
        line = screen_visual_line(self, ypos);
    } while (last_marked_line - ypos < 5);
    ypos = y + 1; last_marked_line = y;
    while (ypos < self->lines - 1 && ypos - last_marked_line < 5) {
        line = screen_visual_line(self, ypos);
        if (mark_hyperlinks_in_line(self, line, id, ypos, &found_nonzero_multiline)) last_marked_line = ypos;
        ypos++;
    }
    if (self->url_ranges.count > 1) sort_ranges(self, &self->url_ranges);
    return id;
}

static index_type
continue_line_upwards(Screen *self, index_type top_line, SelectionBoundary *start, SelectionBoundary *end) {
    while (top_line > 0 && visual_line_is_continued(self, top_line)) {
        if (!screen_selection_range_for_line(self, top_line - 1, &start->x, &end->x)) break;
        top_line--;
    }
    return top_line;
}

static index_type
continue_line_upwards_scrollback(Screen *self, int top_line, SelectionBoundary *start, SelectionBoundary *end) {
    index_type num_in_scrollback = 0;
    Line *line = NULL;
    while (range_line_is_continued(self, top_line) && (line = range_line_(self, top_line-1))) {
        screen_selection_range_for_line_(line, &start->x, &end->x) ;
        top_line--; num_in_scrollback++;
    }
    return num_in_scrollback;
}


static index_type
continue_line_downwards(Screen *self, index_type bottom_line, SelectionBoundary *start, SelectionBoundary *end) {
    while (bottom_line + 1 < self->lines && visual_line_is_continued(self, bottom_line + 1)) {
        if (!screen_selection_range_for_line(self, bottom_line + 1, &start->x, &end->x)) break;
        bottom_line++;
    }
    return bottom_line;
}

static index_type
continue_line_downwards_offscreen(Screen *self, int bottom_line, SelectionBoundary *start, SelectionBoundary *end) {
    index_type num_offscreen = 0;
    Line *line = NULL;
    while ((line = checked_range_line(self, bottom_line + 1)) && range_line_is_continued(self, bottom_line + 1)) {
        screen_selection_range_for_line_(line, &start->x, &end->x);
        bottom_line++; num_offscreen++;
    }
    return num_offscreen;
}

static index_type
continue_word_upwards_scrollback(Screen *self, int range_y, index_type *start_x) {
    index_type num_in_scrollback = 0;
    Line *line = NULL;
    while (*start_x == 0 && range_line_is_continued(self, range_y) && (line = checked_range_line(self, range_y - 1))) {
        if (!is_char_ok_for_word_extension(line, self->columns - 1, false)) break;
        range_y--;
        num_in_scrollback++;
        index_type s = self->columns - 1;
        while (s > 0 && is_char_ok_for_word_extension(line, s - 1, false)) s--;
        *start_x = s;
    }
    return num_in_scrollback;
}

static index_type
continue_word_downwards_offscreen(Screen *self, int range_y, index_type *end_x) {
    index_type num_offscreen = 0;
    Line *line = NULL;
    while (*end_x >= self->columns - 1 && (line = checked_range_line(self, range_y + 1)) && range_line_is_continued(self, range_y + 1)) {
        if (!is_char_ok_for_word_extension(line, 0, true)) break;
        range_y++;
        num_offscreen++;
        index_type e = 0;
        while (e < self->columns - 1 && is_char_ok_for_word_extension(line, e + 1, true)) e++;
        *end_x = e;
    }
    return num_offscreen;
}

static int
clamp_selection_input_to_multicell(Screen *self, const Selection *s, index_type x, index_type y, bool in_left_half_of_cell) {
    int delta = 0;
    int abs_y = y - self->scrolled_by, abs_start_y = s->start.y - s->start_scrolled_by;
    if (abs_y == abs_start_y) return delta;
    Line *line = checked_range_line(self, abs_start_y);
    CPUCell *start, *current;
    if (!line || s->start.x >= line->xnum || !(start = &line->cpu_cells[s->start.x])->is_multicell || start->scale < 2) return delta;
    int abs_start_top = abs_start_y - start->y;
    line = checked_range_line(self, abs_y);
    if (x > s->start.x && in_left_half_of_cell) x--;
    else if (x < s->start.x && !in_left_half_of_cell) x++;
    if (!line || x >= line->xnum) return delta;
    current = line->cpu_cells + x;
    if (!current->is_multicell) return delta;
    int abs_current_top = abs_y - current->y;
    if (current->scale == start->scale && current->subscale_n == start->subscale_n && current->subscale_d == start->subscale_d && abs_current_top == abs_start_top) delta = abs_y - abs_start_y;
    return delta;
}

static void
do_update_selection(Screen *self, Selection *s, index_type x, index_type y, bool in_left_half_of_cell, SelectionUpdate upd) {
    s->input_current.x = x; s->input_current.y = y;
    s->input_current.in_left_half_of_cell = in_left_half_of_cell;
    SelectionBoundary start, end, *a = &s->start, *b = &s->end, abs_start, abs_end, abs_current_input;
#define set_abs(which, initializer, scrolled_by) which = initializer; which.y = scrolled_by + self->lines - 1 - which.y;
    set_abs(abs_start, s->start, s->start_scrolled_by);
    set_abs(abs_end, s->end, s->end_scrolled_by);
    set_abs(abs_current_input, s->input_current, self->scrolled_by);
    bool return_word_sel_to_start_line = false;
    if (upd.set_as_nearest_extend || self->selections.extension_in_progress) {
        self->selections.extension_in_progress = true;
        bool start_is_nearer = false;
        if (self->selections.extend_mode == EXTEND_LINE || self->selections.extend_mode == EXTEND_LINE_FROM_BEGIN || self->selections.extend_mode == EXTEND_LINE_FROM_POINT || self->selections.extend_mode == EXTEND_WORD_AND_LINE_FROM_POINT) {
            if (abs_start.y == abs_end.y) {
                if (abs_current_input.y == abs_start.y) start_is_nearer = selection_boundary_less_than(&abs_start, &abs_end) ? (abs_current_input.x <= abs_start.x) : (abs_current_input.x <= abs_end.x);
                else start_is_nearer = selection_boundary_less_than(&abs_start, &abs_end) ? (abs_current_input.y > abs_start.y) : (abs_current_input.y < abs_end.y);
            } else {
                start_is_nearer = num_lines_between_selection_boundaries(&abs_start, &abs_current_input) < num_lines_between_selection_boundaries(&abs_end, &abs_current_input);
            }
        } else start_is_nearer = num_cells_between_selection_boundaries(self, &abs_start, &abs_current_input) < num_cells_between_selection_boundaries(self, &abs_end, &abs_current_input);
        if (start_is_nearer) s->adjusting_start = true;
    } else if (!upd.start_extended_selection && self->selections.extend_mode != EXTEND_CELL) {
        SelectionBoundary abs_initial_start, abs_initial_end;
        set_abs(abs_initial_start, s->initial_extent.start, s->initial_extent.scrolled_by);
        set_abs(abs_initial_end, s->initial_extent.end, s->initial_extent.scrolled_by);
        if (self->selections.extend_mode == EXTEND_WORD) {
            if (abs_current_input.y == abs_initial_start.y && abs_start.y != abs_end.y) {
                if (abs_start.y != abs_initial_start.y) s->adjusting_start = true;
                else if (abs_end.y != abs_initial_start.y) s->adjusting_start = false;
                else s->adjusting_start = selection_boundary_less_than(&abs_current_input, &abs_initial_end);
                return_word_sel_to_start_line = true;
            } else {
                if (s->adjusting_start) s->adjusting_start = selection_boundary_less_than(&abs_current_input, &abs_initial_end);
                else s->adjusting_start = selection_boundary_less_than(&abs_current_input, &abs_initial_start);
            }
        } else {
            const unsigned int initial_line = abs_initial_start.y;
            if (initial_line == abs_current_input.y) {
                s->adjusting_start = false;
                s->start = s->initial_extent.start; s->start_scrolled_by = s->initial_extent.scrolled_by;
                s->end = s->initial_extent.end; s->end_scrolled_by = s->initial_extent.scrolled_by;
            }
            else {
                s->adjusting_start = abs_current_input.y > initial_line;
            }
        }
    }
#undef set_abs
    bool adjusted_boundary_is_before;
    if (s->adjusting_start) adjusted_boundary_is_before = selection_boundary_less_than(&abs_start, &abs_end);
    else { adjusted_boundary_is_before = selection_boundary_less_than(&abs_end, &abs_start); }

    switch(self->selections.extend_mode) {
        case EXTEND_WORD: {
            if (!s->adjusting_start) { a = &s->end; b = &s->start; }
            const bool word_found_at_cursor = screen_selection_range_for_word(self, s->input_current.x, s->input_current.y, &start.y, &end.y, &start.x, &end.x, true);
            bool adjust_both_ends = is_selection_empty(s);
            if (return_word_sel_to_start_line) {
                index_type ox = a->x;
                if (s->adjusting_start) { *a = s->initial_extent.start; if (ox < a->x) a->x = ox; }
                else { *a = s->initial_extent.end; if (ox > a->x) a->x = ox; }
            } else if (word_found_at_cursor) {
                if (adjusted_boundary_is_before) {
                    *a = start; a->in_left_half_of_cell = true;
                    if (adjust_both_ends) { *b = end; b->in_left_half_of_cell = false; }
                } else {
                    *a = end; a->in_left_half_of_cell = false;
                    if (adjust_both_ends) { *b = start; b->in_left_half_of_cell = true; }
                }
                if (s->adjusting_start || adjust_both_ends) s->start_scrolled_by = self->scrolled_by;
                if (!s->adjusting_start || adjust_both_ends) s->end_scrolled_by = self->scrolled_by;
                // extend word into scrollback if needed
                if (start.y == 0 && self->linebuf == self->main_linebuf &&
                        (adjust_both_ends || adjusted_boundary_is_before)) {
                    index_type num_in_scrollback = continue_word_upwards_scrollback(self, 0, &start.x);
                    if (num_in_scrollback) {
                        s->start_scrolled_by += num_in_scrollback;
                        s->start.x = start.x;
                    }
                }
                // extend word below viewport if needed
                if (end.y >= self->lines - 1 && self->scrolled_by > 0 && self->linebuf == self->main_linebuf &&
                        (adjust_both_ends || !adjusted_boundary_is_before)) {
                    int range_bottom = (int)end.y - (int)self->scrolled_by;
                    index_type num_below_viewport = continue_word_downwards_offscreen(self, range_bottom, &end.x);
                    if (num_below_viewport) {
                        s->end_scrolled_by -= num_below_viewport;
                        s->end.x = end.x;
                    }
                }
            } else {
                *a = s->input_current;
                if (s->adjusting_start) s->start_scrolled_by = self->scrolled_by; else s->end_scrolled_by = self->scrolled_by;
            }
            break;
        }
        case EXTEND_LINE_FROM_BEGIN:
        case EXTEND_LINE_FROM_POINT:
        case EXTEND_WORD_AND_LINE_FROM_POINT:
        case EXTEND_LINE: {
            bool adjust_both_ends = is_selection_empty(s);
            if (s->adjusting_start || adjust_both_ends) s->start_scrolled_by = self->scrolled_by;
            if (!s->adjusting_start || adjust_both_ends) s->end_scrolled_by = self->scrolled_by;
            index_type top_line, bottom_line;
            SelectionBoundary up_start, up_end, down_start, down_end;
            if (adjust_both_ends) {
                // empty initial selection
                top_line = s->input_current.y; bottom_line = s->input_current.y;
                if (screen_selection_range_for_line(self, top_line, &up_start.x, &up_end.x)) {
#define S \
    s->start.y = top_line; s->end.y = bottom_line; \
    s->start.in_left_half_of_cell = true; s->end.in_left_half_of_cell = false; \
    s->start.x = up_start.x; s->end.x = bottom_line == top_line ? up_end.x : down_end.x;
                    down_start = up_start; down_end = up_end;
                    bottom_line = continue_line_downwards(self, bottom_line, &down_start, &down_end);
                    if (self->selections.extend_mode == EXTEND_LINE_FROM_BEGIN) {
                        S; s->start.x = 0;
                    } else if (self->selections.extend_mode == EXTEND_LINE_FROM_POINT) {
                        if (x <= up_end.x) {
                            S; s->start.x = MAX(x, up_start.x);
                        }
                    } else if (self->selections.extend_mode == EXTEND_WORD_AND_LINE_FROM_POINT) {
                        if (x <= up_end.x) {
                            S; s->start.x = MAX(x, up_start.x);
                        }
                        const bool word_found_at_cursor = screen_selection_range_for_word(self, s->input_current.x, s->input_current.y, &start.y, &end.y, &start.x, &end.x, true);
                        if (word_found_at_cursor) {
                            *a = start; a->in_left_half_of_cell = true;
                        }
                    } else {
                        top_line = continue_line_upwards(self, top_line, &up_start, &up_end);
                        S;
                        // extend into scrollback if needed
                        if (top_line == 0 && self->linebuf == self->main_linebuf) {
                            index_type num_in_scrollback = continue_line_upwards_scrollback(
                                    self, top_line, &up_start, &up_end);
                            if (num_in_scrollback) {
                                s->start_scrolled_by += num_in_scrollback;
                                s->start.x = up_start.x;
                            }
                        }
                        // extend below viewport if needed
                        if (bottom_line >= self->lines - 1 && self->scrolled_by > 0 && self->linebuf == self->main_linebuf) {
                            int range_bottom = (int)bottom_line - (int)self->scrolled_by;
                            index_type num_below_viewport = continue_line_downwards_offscreen(
                                    self, range_bottom, &down_start, &down_end);
                            if (num_below_viewport) {
                                s->end_scrolled_by -= num_below_viewport;
                                s->end.x = down_end.x;
                            }
                        }
                    }
                }
#undef S
            } else {
                // extending an existing selection
                top_line = s->input_current.y; bottom_line = s->input_current.y;
                if (screen_selection_range_for_line(self, top_line, &up_start.x, &up_end.x)) {
                    down_start = up_start; down_end = up_end;
                    top_line = continue_line_upwards(self, top_line, &up_start, &up_end);
                    bottom_line = continue_line_downwards(self, bottom_line, &down_start, &down_end);
                    if (!s->adjusting_start) { a = &s->end; b = &s->start; }
                    if (adjusted_boundary_is_before) {
                        a->in_left_half_of_cell = true; a->x = up_start.x; a->y = top_line;
                        // extend into scrollback if needed
                        if (top_line == 0 && self->linebuf == self->main_linebuf) {
                            index_type num_in_scrollback = continue_line_upwards_scrollback(
                                    self, top_line, &up_start, &up_end);
                            if (num_in_scrollback) {
                                s->start_scrolled_by += num_in_scrollback;
                                s->start.x = up_start.x;
                            }
                        }
                    } else {
                        a->in_left_half_of_cell = false; a->x = down_end.x; a->y = bottom_line;
                        // extend below viewport if needed
                        if (bottom_line >= self->lines - 1 && self->scrolled_by > 0 && self->linebuf == self->main_linebuf) {
                            int range_bottom = (int)bottom_line - (int)self->scrolled_by;
                            index_type num_below_viewport = continue_line_downwards_offscreen(
                                    self, range_bottom, &down_start, &down_end);
                            if (num_below_viewport) {
                                s->end_scrolled_by -= num_below_viewport;
                                s->end.x = down_end.x;
                            }
                        }
                    }
                    // allow selecting whitespace at the start of the top line
                    if (a->y == top_line && s->input_current.y == top_line && s->input_current.x < a->x && adjusted_boundary_is_before) a->x = s->input_current.x;
                }
            }
        }
        break;
        case EXTEND_CELL:
            if (s->adjusting_start) b = &s->start;
            b->x = x; b->y = y; b->in_left_half_of_cell = in_left_half_of_cell;
            if (s->adjusting_start) s->start_scrolled_by = self->scrolled_by; else s->end_scrolled_by = self->scrolled_by;
            break;
    }
    if (!self->selections.in_progress) {
        s->adjusting_start = false;
        self->selections.extension_in_progress = false;
        call_boss(set_primary_selection, NULL);
    } else {
        if (upd.start_extended_selection && self->selections.extend_mode != EXTEND_CELL) {
            s->initial_extent.start = s->start; s->initial_extent.end = s->end;
            s->initial_extent.scrolled_by = s->start_scrolled_by;
        }
    }
}

void
screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, SelectionUpdate upd) {
    if (!self->selections.count) return;
    self->selections.in_progress = !upd.ended;
    Selection *s = self->selections.items;
    int delta = clamp_selection_input_to_multicell(self, s, x, y, in_left_half_of_cell);
    index_type orig = self->scrolled_by;
    if (delta) {
        int new_y = y - delta;
        if (new_y < 0) {
            y = 0; self->scrolled_by += - new_y;
        } else y = new_y;
    }
    do_update_selection(self, s, x, y, in_left_half_of_cell, upd);
    self->scrolled_by = orig;
}

static PyObject*
mark_as_dirty(Screen *self, PyObject *a UNUSED) {
    self->is_dirty = true;
    Py_RETURN_NONE;
}

static PyObject*
reload_all_gpu_data(Screen *self, PyObject *a UNUSED) {
    self->reload_all_gpu_data = true;
    Py_RETURN_NONE;
}


static PyObject*
current_char_width(Screen *self, PyObject *a UNUSED) {
#define current_char_width_doc "The width of the character under the cursor"
    unsigned long ans = 1;
    if (self->cursor->x < self->columns && self->cursor->y < self->lines) {
        const CPUCell *c = linebuf_cpu_cells_for_line(self->linebuf, self->cursor->y) + self->cursor->x;
        if (c->is_multicell) {
            if (c->x || c->y) ans = 0;
            else ans = c->width;
        }
    }
    return PyLong_FromUnsignedLong(ans);
}

static PyObject*
is_main_linebuf(Screen *self, PyObject *a UNUSED) {
    PyObject *ans = (self->linebuf == self->main_linebuf) ? Py_True : Py_False;
    Py_INCREF(ans);
    return ans;
}

static PyObject*
toggle_alt_screen(Screen *self, PyObject *a UNUSED) {
    screen_toggle_screen_buffer(self, true, true);
    Py_RETURN_NONE;
}

static PyObject*
pause_rendering(Screen *self, PyObject *args) {
    int msec = 100;
    int pause = 1;
    if (!PyArg_ParseTuple(args, "|pi", &pause, &msec)) return NULL;
    if (screen_pause_rendering(self, pause, msec)) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

static PyObject*
send_escape_code_to_child(Screen *self, PyObject *args) {
    int code;
    PyObject *O;
    if (!PyArg_ParseTuple(args, "iO", &code, &O)) return NULL;
    bool written = false;
    if (PyBytes_Check(O)) written = write_escape_code_to_child(self, code, PyBytes_AS_STRING(O));
    else if (PyUnicode_Check(O)) {
        const char *t = PyUnicode_AsUTF8(O);
        if (t) written = write_escape_code_to_child(self, code, t);
        else return NULL;
    } else if (PyTuple_Check(O)) written = write_escape_code_to_child_python(self, code, O);
    else PyErr_SetString(PyExc_TypeError, "escape code must be str, bytes or tuple");
    if (PyErr_Occurred()) return NULL;
    if (written) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; }
}

static void
screen_mark_all(Screen *self) {
    for (index_type y = 0; y < self->main_linebuf->ynum; y++) {
        linebuf_init_line(self->main_linebuf, y);
        mark_text_in_line(self->marker, self->main_linebuf->line, &self->as_ansi_buf);
    }
    for (index_type y = 0; y < self->alt_linebuf->ynum; y++) {
        linebuf_init_line(self->alt_linebuf, y);
        mark_text_in_line(self->marker, self->alt_linebuf->line, &self->as_ansi_buf);
    }
    for (index_type y = 0; y < self->historybuf->count; y++) {
        historybuf_init_line(self->historybuf, y, self->historybuf->line);
        mark_text_in_line(self->marker, self->historybuf->line, &self->as_ansi_buf);
    }
    self->is_dirty = true;
}

static PyObject*
set_marker(Screen *self, PyObject *args) {
    PyObject *marker = NULL;
    if (!PyArg_ParseTuple(args, "|O", &marker)) return NULL;
    if (!marker) {
        if (self->marker) {
            Py_CLEAR(self->marker);
            screen_mark_all(self);
        }
        Py_RETURN_NONE;
    }
    if (!PyCallable_Check(marker)) {
        PyErr_SetString(PyExc_TypeError, "marker must be a callable");
        return NULL;
    }
    self->marker = marker;
    Py_INCREF(marker);
    screen_mark_all(self);
    Py_RETURN_NONE;
}


static PyObject*
scroll_to_next_mark(Screen *self, PyObject *args) {
    int backwards = 1;
    unsigned int mark = 0;
    if (!PyArg_ParseTuple(args, "|Ip", &mark, &backwards)) return NULL;
    if (!screen_has_marker(self) || self->linebuf == self->alt_linebuf) Py_RETURN_FALSE;
    if (backwards) {
        for (unsigned int y = self->scrolled_by; y < self->historybuf->count; y++) {
            historybuf_init_line(self->historybuf, y, self->historybuf->line);
            if (line_has_mark(self->historybuf->line, mark)) {
                screen_history_scroll(self, y - self->scrolled_by + 1, true);
                Py_RETURN_TRUE;
            }
        }
    } else {
        Line *line;
        for (unsigned int y = self->scrolled_by; y > 0; y--) {
            if (y > self->lines) {
                historybuf_init_line(self->historybuf, y - self->lines, self->historybuf->line);
                line = self->historybuf->line;
            } else {
                linebuf_init_line(self->linebuf, self->lines - y);
                line = self->linebuf->line;
            }
            if (line_has_mark(line, mark)) {
                screen_history_scroll(self, self->scrolled_by - y + 1, false);
                Py_RETURN_TRUE;
            }
        }
    }
    Py_RETURN_FALSE;
}

static PyObject*
marked_cells(Screen *self, PyObject *o UNUSED) {
    RAII_PyObject(ans, PyList_New(0));
    if (!ans) return ans;
    for (index_type y = 0; y < self->lines; y++) {
        linebuf_init_line(self->linebuf, y);
        for (index_type x = 0; x < self->columns; x++) {
            GPUCell *gpu_cell = self->linebuf->line->gpu_cells + x;
            const unsigned int mark = gpu_cell->attrs.mark;
            if (mark) {
                RAII_PyObject(t, Py_BuildValue("III", x, y, mark));
                if (!t) { return NULL; }
                if (PyList_Append(ans, t) != 0) return NULL;
            }
        }
    }
    return Py_NewRef(ans);
}

static PyObject*
paste_(Screen *self, PyObject *bytes, bool allow_bracketed_paste) {
    const char *data; Py_ssize_t sz;
    if (PyBytes_Check(bytes)) {
        data = PyBytes_AS_STRING(bytes); sz = PyBytes_GET_SIZE(bytes);
    } else if (PyMemoryView_Check(bytes)) {
        RAII_PyObject(mv, PyMemoryView_GetContiguous(bytes, PyBUF_READ, PyBUF_C_CONTIGUOUS));
        if (mv == NULL) return NULL;
        Py_buffer *buf = PyMemoryView_GET_BUFFER(mv);
        data = buf->buf;
        sz = buf->len;
    } else {
        PyErr_SetString(PyExc_TypeError, "Must paste() bytes"); return NULL;
    }
    if (allow_bracketed_paste && self->modes.mBRACKETED_PASTE) write_escape_code_to_child(self, ESC_CSI, BRACKETED_PASTE_START);
    write_to_child(self, data, sz);
    if (allow_bracketed_paste && self->modes.mBRACKETED_PASTE) write_escape_code_to_child(self, ESC_CSI, BRACKETED_PASTE_END);
    Py_RETURN_NONE;
}


static PyObject*
paste(Screen *self, PyObject *bytes) {
    return paste_(self, bytes, true);
}

static PyObject*
paste_bytes(Screen *self, PyObject *bytes) {
    return paste_(self, bytes, false);
}

static PyObject*
focus_changed(Screen *self, PyObject *has_focus_) {
    bool previous = self->has_focus;
    bool has_focus = PyObject_IsTrue(has_focus_) ? true : false;
    if (has_focus != previous) {
        self->has_focus = has_focus;
        if (has_focus) self->has_activity_since_last_focus = false;
        else if (screen_is_overlay_active(self)) deactivate_overlay_line(self);
        if (self->modes.mFOCUS_TRACKING) write_escape_code_to_child(self, ESC_CSI, has_focus ? "I" : "O");
        Py_RETURN_TRUE;
    }
    Py_RETURN_FALSE;
}

static PyObject*
has_focus(Screen *self, PyObject *args UNUSED) {
    if (self->has_focus) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

static PyObject*
has_activity_since_last_focus(Screen *self, PyObject *args UNUSED) {
    if (self->has_activity_since_last_focus) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

WRAP2(cursor_position, 1, 1)

#define COUNT_WRAP(name) WRAP1(name, 1)
COUNT_WRAP(insert_lines)
COUNT_WRAP(delete_lines)
COUNT_WRAP(delete_characters)
COUNT_WRAP(erase_characters)
COUNT_WRAP(cursor_up1)
COUNT_WRAP(cursor_down)
COUNT_WRAP(cursor_down1)
COUNT_WRAP(cursor_forward)

static PyObject*
py_insert_characters(Screen *self, PyObject *count_) {
    if (!PyLong_Check(count_)) { PyErr_SetString(PyExc_TypeError, "count must be an integer"); return NULL; }
    unsigned long count = PyLong_AsUnsignedLong(count_);
    screen_insert_characters(self, count);
    Py_RETURN_NONE;
}

static PyObject*
screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) {
    unsigned long code = PyLong_AsUnsignedLong(code_);
    if (is_emoji_presentation_base(code)) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

static PyObject*
hyperlink_at(Screen *self, PyObject *args) {
    unsigned int x, y;
    if (!PyArg_ParseTuple(args, "II", &x, &y)) return NULL;
    screen_mark_hyperlink(self, x, y);
    if (!self->url_ranges.count) Py_RETURN_NONE;
    hyperlink_id_type hid = hyperlink_id_for_range(self, self->url_ranges.items);
    if (!hid) Py_RETURN_NONE;
    const char *url = get_hyperlink_for_id(self->hyperlink_pool, hid, true);
    return Py_BuildValue("s", url);
}

static PyObject*
reverse_scroll(Screen *self, PyObject *args) {
    int fill_from_scrollback = 0;
    unsigned int amt;
    if (!PyArg_ParseTuple(args, "I|p", &amt, &fill_from_scrollback)) return NULL;
    _reverse_scroll(self, amt, fill_from_scrollback);
    Py_RETURN_NONE;
}


static PyObject*
scroll_prompt_to_bottom(Screen *self, PyObject *args UNUSED) {
    if (self->linebuf != self->main_linebuf || !self->historybuf->count) Py_RETURN_NONE;
    int q = screen_cursor_at_a_shell_prompt(self);
    index_type limit_y = q > -1 ? (unsigned int)q : self->cursor->y;
    index_type y = self->lines - 1;
    // not before prompt or cursor line
    while (y > limit_y) {
        Line *line = checked_range_line(self, y);
        if (!line || line_length(line)) break;
        y--;
    }
    // don't scroll back beyond the history buffer range
    unsigned int count = MIN(self->lines - (y + 1), self->historybuf->count);
    if (count > 0) {
        _reverse_scroll(self, count, true);
        screen_cursor_down(self, count);
    }
    // always scroll to the bottom
    if (self->scrolled_by != 0) {
        self->scrolled_by = 0;
        reset_pixel_scroll(self, 0);
        dirty_scroll(self);
    }
    Py_RETURN_NONE;
}

static void
dump_line_with_attrs(Screen *self, int y, PyObject *accum) {
    Line *line = range_line_(self, y);
    RAII_PyObject(u, PyUnicode_FromFormat("\x1b[31m%d: \x1b[39m", y++));
    if (!u) return;
    RAII_PyObject(r1, PyObject_CallOneArg(accum, u));
    if (!r1) return;
#define call_string(s) { RAII_PyObject(ret, PyObject_CallFunction(accum, "s", s)); if (!ret) return; }
    switch (line->attrs.prompt_kind) {
        case UNKNOWN_PROMPT_KIND: break;
        case PROMPT_START: call_string("\x1b[32mprompt \x1b[39m"); break;
        case SECONDARY_PROMPT: call_string("\x1b[32msecondary_prompt \x1b[39m"); break;
        case OUTPUT_START: call_string("\x1b[33moutput \x1b[39m"); break;
    }
    if (range_line_is_continued(self, y)) call_string("continued ");
    if (line->attrs.has_dirty_text) call_string("dirty ");
    call_string("\n");
    RAII_PyObject(t, line_as_unicode(line, false, &self->as_ansi_buf)); if (!t) return;
    RAII_PyObject(r2, PyObject_CallOneArg(accum, t)); if (!r2) return;
    call_string("\n");
#undef call_string
}

static PyObject*
dump_lines_with_attrs(Screen *self, PyObject *args) {
    PyObject *accum; int which_screen = -1;
    if (!PyArg_ParseTuple(args, "O|i", &accum, &which_screen)) return NULL;
    LineBuf *orig = self->linebuf;
    switch(which_screen) {
        case 0: self->linebuf = self->main_linebuf; break;
        case 1: self->linebuf = self->alt_linebuf; break;
    }
    int y = (self->linebuf == self->main_linebuf) ? -self->historybuf->count : 0;
    while (y < (int)self->lines && !PyErr_Occurred()) dump_line_with_attrs(self, y++, accum);
    self->linebuf = orig;
    if (PyErr_Occurred()) return NULL;
    Py_RETURN_NONE;
}

static PyObject*
cursor_at_prompt(Screen *self, PyObject *args UNUSED) {
    int y = screen_cursor_at_a_shell_prompt(self);
    if (y > -1) { Py_RETURN_TRUE; }
    Py_RETURN_FALSE;
}

static PyObject*
line_edge_colors(Screen *self, PyObject *a UNUSED) {
    color_type left, right;
    if (!get_line_edge_colors(self, &left, &right)) { PyErr_SetString(PyExc_IndexError, "Line number out of range"); return NULL; }
    return Py_BuildValue("kk", (unsigned long)left, (unsigned long)right);
}

static PyObject*
current_selections(Screen *self, PyObject *a UNUSED) {
    PyObject *ans = PyBytes_FromStringAndSize(NULL, (Py_ssize_t)render_lines_for_screen(self) * self->columns);
    if (!ans) return NULL;
    screen_apply_selection(self, PyBytes_AS_STRING(ans), PyBytes_GET_SIZE(ans));
    return ans;
}

WRAP0(update_only_line_graphics_data)
WRAP0(bell)

static PyObject*
mark_potential_url_drag(Screen *self, PyObject *a UNUSED) {
    if (screen_mark_potential_url_drag(self)) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

#define MND(name, args) {#name, (PyCFunction)name, args, #name},
#define MODEFUNC(name) MND(name, METH_NOARGS) MND(set_##name, METH_O)

static PyObject*
test_create_write_buffer(Screen *screen UNUSED, PyObject *args UNUSED) {
    size_t s;
    uint8_t *buf = vt_parser_create_write_buffer(screen->vt_parser, &s);
    return PyMemoryView_FromMemory((char*)buf, s, PyBUF_WRITE);
}

static PyObject*
test_draw_overlay_line(Screen *self, PyObject *args) {
    PyObject *text;
    unsigned int xstart, ynum;
    if (!PyArg_ParseTuple(args, "UII", &text, &xstart, &ynum)) return NULL;
    if (ynum >= self->lines || xstart >= self->columns) {
        PyErr_SetString(PyExc_IndexError, "ynum or xstart out of range");
        return NULL;
    }
    Py_INCREF(text);
    Py_XDECREF(self->overlay_line.overlay_text);
    self->overlay_line.overlay_text = text;
    self->overlay_line.text_len = (index_type)PyUnicode_GET_LENGTH(text);
    self->overlay_line.xstart = xstart;
    self->overlay_line.ynum = ynum;
    self->overlay_line.is_active = true;
    screen_draw_overlay_line(self);
    Py_RETURN_NONE;
}

static PyObject*
test_commit_write_buffer(Screen *screen, PyObject *args) {
    RAII_PY_BUFFER(srcbuf); RAII_PY_BUFFER(destbuf);
    if (!PyArg_ParseTuple(args, "y*y*", &srcbuf, &destbuf)) return NULL;
    size_t s = MIN(srcbuf.len, destbuf.len);
    memcpy(destbuf.buf, srcbuf.buf, s);
    vt_parser_commit_write(screen->vt_parser, s);
    return PyLong_FromSize_t(s);
}

static PyObject*
test_parse_written_data(Screen *screen, PyObject *args) {
    ParseData pd = {.now=monotonic()};
    if (!PyArg_ParseTuple(args, "|O", &pd.dump_callback)) return NULL;
    if (pd.dump_callback && pd.dump_callback != Py_None) parse_worker_dump(screen, &pd, true);
    else parse_worker(screen, &pd, true);
    Py_RETURN_NONE;
}

static PyObject*
multicell_data_as_dict(CPUCell mcd) {
    return Py_BuildValue("{sI sI sI sI sO sI sI}",
            "scale", (unsigned int)mcd.scale, "width", (unsigned int)mcd.width,
            "subscale_n", (unsigned int)mcd.subscale_n, "subscale_d", (unsigned int)mcd.subscale_d,
            "natural_width", mcd.natural_width ? Py_True : Py_False, "vertical_align", mcd.valign, "horizontal_align", mcd.halign);
}

static PyObject*
cpu_cell_as_dict(CPUCell *c, TextCache *tc, ListOfChars *lc, HYPERLINK_POOL_HANDLE h) {
    text_in_cell(c, tc, lc);
    RAII_PyObject(mcd, c->is_multicell ? multicell_data_as_dict(*c) : Py_NewRef(Py_None));
    if ((c->is_multicell && (c->x + c->y)) || (lc->count == 1 && lc->chars[0] == 0)) lc->count = 0;
    RAII_PyObject(text, PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, lc->chars, lc->count));
    const char *url = c->hyperlink_id ? get_hyperlink_for_id(h, c->hyperlink_id, false) : NULL;
    RAII_PyObject(hyperlink, url ? PyUnicode_FromString(url) : Py_NewRef(Py_None));
    return Py_BuildValue("{sO sO sI sI sO sO}",
        "text", text, "hyperlink", hyperlink, "x", (unsigned int)c->x, "y", (unsigned int)c->y,
        "mcd", mcd, "next_char_was_wrapped", c->next_char_was_wrapped ? Py_True : Py_False
    );
}

static PyObject*
cpu_cells(Screen *self, PyObject *args) {
    int y, x = -1;
    if (!PyArg_ParseTuple(args, "i|i", &y, &x)) return NULL;
    if (y >= (int)self->lines) { PyErr_SetString(PyExc_IndexError, "y out of bounds"); return NULL; }
    CPUCell *cells;
    if (y >= 0) cells = linebuf_cpu_cells_for_line(self->linebuf, y);
    else {
        Line *l = self->linebuf == self->main_linebuf ? checked_range_line(self, y) : NULL;
        if (!l) { PyErr_SetString(PyExc_IndexError, "y out of bounds"); return NULL; }
        cells = l->cpu_cells;
    }
    if (x > -1) {
        if (x >= (int)self->columns) { PyErr_SetString(PyExc_IndexError, "x out of bounds"); return NULL; }
        return cpu_cell_as_dict(cells + x, self->text_cache, self->lc, self->hyperlink_pool);
    }
    index_type start_x = 0, x_limit = self->columns;
    RAII_PyObject(ans, PyTuple_New(x_limit - start_x));
    if (ans) {
        for (index_type x = start_x; x < x_limit; x++) {
            PyObject *d = cpu_cell_as_dict(cells + x, self->text_cache, self->lc, self->hyperlink_pool);
            if (!d) return NULL;
            PyTuple_SET_ITEM(ans, x, d);
        }
    }
    return Py_NewRef(ans);
}

static PyObject*
test_ch_and_idx(PyObject *self UNUSED, PyObject *val) {
    CPUCell c = {0};
    if (PyLong_Check(val)) {
        unsigned long x = PyLong_AsUnsignedLong(val);
        c.ch_and_idx = x;
    } else if (PyTuple_Check(val)) {
        c.ch_is_idx = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(val, 0));
        c.ch_or_idx = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(val, 1));
    }
    unsigned long is_idx = c.ch_is_idx, idx = c.ch_or_idx, ca = c.ch_and_idx;
    return Py_BuildValue("kkk", is_idx, idx, ca);
}

static PyMethodDef methods[] = {
    METHODB(test_create_write_buffer, METH_NOARGS),
    METHODB(test_commit_write_buffer, METH_VARARGS),
    METHODB(test_parse_written_data, METH_VARARGS),
    METHODB(test_draw_overlay_line, METH_VARARGS),
    MND(line_edge_colors, METH_NOARGS)
    MND(line, METH_O)
    MND(dump_lines_with_attrs, METH_VARARGS)
    MND(cpu_cells, METH_VARARGS)
    MND(cursor_at_prompt, METH_NOARGS)
    {"visual_line", (PyCFunction)pyvisual_line, METH_VARARGS, ""},
    MND(current_url_text, METH_NOARGS)
    MND(draw, METH_O)
    MND(apply_sgr, METH_O)
    MND(cursor_position, METH_VARARGS)
    MND(erase_last_command, METH_NOARGS)
    MND(set_window_char, METH_VARARGS)
    MND(set_progress, METH_VARARGS)
    MND(set_mode, METH_VARARGS)
    MND(reset_mode, METH_VARARGS)
    MND(reset, METH_VARARGS)
    MND(reset_dirty, METH_NOARGS)
    MND(is_using_alternate_linebuf, METH_NOARGS)
    MND(is_main_linebuf, METH_NOARGS)
    MND(cursor_move, METH_VARARGS)
    MND(erase_in_line, METH_VARARGS)
    MND(erase_in_display, METH_VARARGS)
    MND(clear_scrollback, METH_NOARGS)
    MND(scroll_until_cursor_prompt, METH_VARARGS)
    MND(hyperlinks_as_set, METH_NOARGS)
    MND(garbage_collect_hyperlink_pool, METH_NOARGS)
    MND(garbage_collect_text_cache, METH_NOARGS)
    MND(text_cache_count, METH_NOARGS)
    MND(hyperlink_for_id, METH_O)
    MND(reverse_scroll, METH_VARARGS)
    MND(scroll_prompt_to_bottom, METH_NOARGS)
    METHOD(current_char_width, METH_NOARGS)
    MND(insert_lines, METH_VARARGS)
    MND(delete_lines, METH_VARARGS)
    {"insert_characters", (PyCFunction)py_insert_characters, METH_O, ""},
    MND(delete_characters, METH_VARARGS)
    MND(erase_characters, METH_VARARGS)
    MND(current_pointer_shape, METH_NOARGS)
    MND(change_pointer_shape, METH_VARARGS)
    MND(cursor_up, METH_VARARGS)
    MND(cursor_up1, METH_VARARGS)
    MND(cursor_down, METH_VARARGS)
    MND(cursor_down1, METH_VARARGS)
    MND(cursor_forward, METH_VARARGS)
    {"index", (PyCFunction)xxx_index, METH_VARARGS, ""},
    {"has_selection", (PyCFunction)has_selection, METH_VARARGS, ""},
    MND(as_text, METH_VARARGS)
    MND(as_text_non_visual, METH_VARARGS)
    MND(as_text_for_history_buf, METH_VARARGS)
    MND(as_text_alternate, METH_VARARGS)
    MND(cmd_output, METH_VARARGS)
    MND(tab, METH_NOARGS)
    MND(backspace, METH_NOARGS)
    MND(linefeed, METH_NOARGS)
    MND(carriage_return, METH_NOARGS)
    MND(set_tab_stop, METH_NOARGS)
    MND(clear_tab_stop, METH_VARARGS)
    MND(reset_tab_stops, METH_NOARGS)
    MND(start_selection, METH_VARARGS)
    MND(update_selection, METH_VARARGS)
    {"clear_selection", (PyCFunction)clear_selection_, METH_NOARGS, ""},
    MND(reverse_index, METH_NOARGS)
    MND(mark_as_dirty, METH_NOARGS)
    MND(reload_all_gpu_data, METH_NOARGS)
    MND(resize, METH_VARARGS)
    MND(ignore_bells_for, METH_VARARGS)
    MND(set_margins, METH_VARARGS)
    MND(detect_url, METH_VARARGS)
    MND(rescale_images, METH_NOARGS)
    MND(current_key_encoding_flags, METH_NOARGS)
    MND(text_for_selection, METH_VARARGS)
    MND(text_for_marked_url, METH_VARARGS)
    MND(is_rectangle_select, METH_NOARGS)
    MND(scroll, METH_VARARGS)
    MND(scroll_to_absolute, METH_O)
    MND(fractional_scroll, METH_O)
    MND(scroll_to_prompt, METH_VARARGS)
    MND(set_last_visited_prompt, METH_VARARGS)
    MND(send_escape_code_to_child, METH_VARARGS)
    MND(pause_rendering, METH_VARARGS)
    MND(hyperlink_at, METH_VARARGS)
    MND(toggle_alt_screen, METH_NOARGS)
    MND(reset_callbacks, METH_NOARGS)
    MND(paste, METH_O)
    MND(paste_bytes, METH_O)
    MND(focus_changed, METH_O)
    MND(has_focus, METH_NOARGS)
    MND(has_activity_since_last_focus, METH_NOARGS)
    MND(copy_colors_from, METH_O)
    MND(set_marker, METH_VARARGS)
    MND(marked_cells, METH_NOARGS)
    MND(scroll_to_next_mark, METH_VARARGS)
    MND(update_only_line_graphics_data, METH_NOARGS)
    MND(bell, METH_NOARGS)
    MND(mark_potential_url_drag, METH_NOARGS)
    MND(current_selections, METH_NOARGS)
    {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""},

    {NULL}  /* Sentinel */
};

static PyGetSetDef getsetters[] = {
    GETSET(in_bracketed_paste_mode)
    GETSET(color_preference_notification)
    GETSET(auto_repeat_enabled)
    GETSET(focus_tracking_enabled)
    GETSET(in_band_resize_notification)
    GETSET(paste_events)
    GETSET(cursor_visible)
    GETSET(cursor_key_mode)
    GETSET(disable_ligatures)
    GETSET(render_unfocused_cursor)
    {NULL}  /* Sentinel */
};

#if UINT_MAX == UINT32_MAX
#define T_COL T_UINT
#elif ULONG_MAX == UINT32_MAX
#define T_COL T_ULONG
#else
#error Neither int nor long is 4-bytes in size
#endif

static PyMemberDef members[] = {
    {"callbacks", T_OBJECT_EX, offsetof(Screen, callbacks), 0, "callbacks"},
    {"cursor", T_OBJECT_EX, offsetof(Screen, cursor), READONLY, "cursor"},
    {"vt_parser", T_OBJECT_EX, offsetof(Screen, vt_parser), READONLY, "vt_parser"},
    {"last_reported_cwd", T_OBJECT, offsetof(Screen, last_reported_cwd), READONLY, "last_reported_cwd"},
    {"grman", T_OBJECT_EX, offsetof(Screen, grman), READONLY, "grman"},
    {"color_profile", T_OBJECT_EX, offsetof(Screen, color_profile), READONLY, "color_profile"},
    {"linebuf", T_OBJECT_EX, offsetof(Screen, linebuf), READONLY, "linebuf"},
    {"main_linebuf", T_OBJECT_EX, offsetof(Screen, main_linebuf), READONLY, "main_linebuf"},
    {"historybuf", T_OBJECT_EX, offsetof(Screen, historybuf), READONLY, "historybuf"},
    {"scrolled_by", T_UINT, offsetof(Screen, scrolled_by), READONLY, "scrolled_by"},
    {"lines", T_UINT, offsetof(Screen, lines), READONLY, "lines"},
    {"columns", T_UINT, offsetof(Screen, columns), READONLY, "columns"},
    {"margin_top", T_UINT, offsetof(Screen, margin_top), READONLY, "margin_top"},
    {"margin_bottom", T_UINT, offsetof(Screen, margin_bottom), READONLY, "margin_bottom"},
    {"history_line_added_count", T_UINT, offsetof(Screen, history_line_added_count), 0, "history_line_added_count"},
    {NULL}
};

PyTypeObject Screen_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "fast_data_types.Screen",
    .tp_basicsize = sizeof(Screen),
    .tp_dealloc = (destructor)dealloc,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_doc = "Screen",
    .tp_methods = methods,
    .tp_members = members,
    .tp_new = new_screen_object,
    .tp_getset = getsetters,
};

static PyMethodDef module_methods[] = {
    {"is_emoji_presentation_base", (PyCFunction)screen_is_emoji_presentation_base, METH_O, ""},
    {"truncate_point_for_length", (PyCFunction)screen_truncate_point_for_length, METH_VARARGS, ""},
    {"test_ch_and_idx", test_ch_and_idx, METH_O, ""},
    {NULL}  /* Sentinel */
};

INIT_TYPE(Screen)
// }}}
