mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-04 13:42:33 +02:00
Previously, every window resize reinitialised the tab stops to the default of every 8 columns, discarding any stops set via HTS or cleared via TBC. ECMA-48 only treats RIS, DECSTR, and DECCOLM as events that reset tab stops, and other terminal emulators all preserve user-set stops across an interactive resize. Copy the surviving prefix of the previous tab stops into the freshly allocated array on both main and alt screens. Newly added columns when growing the window keep the default every 8 columns pattern. Also point the active tabstops pointer at the alt screen's array when a resize happens while the alt screen is active, instead of unconditionally resetting it to the main screen's array. Signed-off-by: Ayman Bagabas <aymanbagabas@gmail.com>
6369 lines
263 KiB
C
6369 lines
263 KiB
C
/*
|
|
* 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);
|
|
|
|
void
|
|
screen_reset(Screen *self) {
|
|
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 (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;
|
|
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", NULL)
|
|
}
|
|
|
|
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 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 \
|
|
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 = ©
|
|
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_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(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
|
|
Line *line = range_line_(self, self->cursor->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 (line->xnum > 0) cell_color_x = line->xnum - 1;
|
|
char_type right_char = line_get_char(line, cell_color_x);
|
|
colors_for_cell(line, self->color_profile, &cell_color_x, &right_cell_fg, &right_cell_bg, &reversed);
|
|
*left = effective_cell_edge_color(left_char, left_cell_fg, left_cell_bg, true);
|
|
*right = effective_cell_edge_color(right_char, right_cell_fg, right_cell_bg, false);
|
|
return true;
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
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) {
|
|
int include_prompt = 1;
|
|
if (!PyArg_ParseTuple(args, "|p", &include_prompt)) return NULL;
|
|
OutputOffset oo = {.screen=self};
|
|
if (self->linebuf != self->main_linebuf || !find_cmd_output(self, &oo, self->cursor->y + self->scrolled_by, self->scrolled_by, -1, false)) Py_RETURN_FALSE;
|
|
if (include_prompt) {
|
|
int y = oo.start - 1; Line *line;
|
|
while ((line = checked_range_line(self, y))) {
|
|
oo.start--; oo.num_lines++; y--;
|
|
if (line->attrs.prompt_kind == PROMPT_START) break;
|
|
}
|
|
}
|
|
index_type num_lines_to_erase_in_screen = oo.start >= 0 ? oo.num_lines : oo.num_lines + oo.start;
|
|
num_lines_to_erase_in_screen = MIN(self->cursor->y, num_lines_to_erase_in_screen);
|
|
if (num_lines_to_erase_in_screen) {
|
|
screen_delete_lines_impl(self, self->cursor->y - num_lines_to_erase_in_screen, num_lines_to_erase_in_screen, 0, self->lines - 1);
|
|
self->cursor->y -= num_lines_to_erase_in_screen;
|
|
}
|
|
if (oo.num_lines > num_lines_to_erase_in_screen) {
|
|
index_type num_of_lines_to_erase_from_history = oo.num_lines - num_lines_to_erase_in_screen;
|
|
historybuf_delete_newest_lines(self->historybuf, num_of_lines_to_erase_from_history);
|
|
}
|
|
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)
|
|
WRAP0(reset)
|
|
WRAP0(set_tab_stop)
|
|
WRAP1(clear_tab_stop, 0)
|
|
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_VARARGS)
|
|
MND(set_window_char, METH_VARARGS)
|
|
MND(set_progress, METH_VARARGS)
|
|
MND(set_mode, METH_VARARGS)
|
|
MND(reset_mode, METH_VARARGS)
|
|
MND(reset, METH_NOARGS)
|
|
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(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(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)
|
|
// }}}
|