/* * screen.c * Copyright (C) 2016 Kovid Goyal * * 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); \ if (PyModule_AddFunctions(module, module_methods) != 0) return false; \ } #include "state.h" #include "iqsort.h" #include "fonts.h" #include "lineops.h" #include "hyperlink.h" #include #include #include #include #include #include "unicode-data.h" #include "modes.h" #include "wcwidth-std.h" #include "control-codes.h" #include "charsets.h" static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true}; #define CSI_REP_MAX_REPETITIONS 65535u // Constructor/destructor {{{ static inline void clear_selection(Selections *selections) { selections->in_progress = false; selections->extend_mode = EXTEND_CELL; selections->count = 0; } static inline 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 inline bool init_overlay_line(Screen *self, index_type columns) { PyMem_Free(self->overlay_line.cpu_cells); PyMem_Free(self->overlay_line.gpu_cells); self->overlay_line.cpu_cells = PyMem_Calloc(columns, sizeof(CPUCell)); self->overlay_line.gpu_cells = PyMem_Calloc(columns, sizeof(GPUCell)); if (!self->overlay_line.cpu_cells || !self->overlay_line.gpu_cells) { PyErr_NoMemory(); return false; } self->overlay_line.is_active = false; self->overlay_line.xnum = 0; self->overlay_line.ynum = 0; self->overlay_line.xstart = 0; return true; } #define RESET_CHARSETS \ self->g0_charset = translation_table(0); \ self->g1_charset = self->g0_charset; \ self->g_charset = self->g0_charset; \ self->current_charset = 0; \ self->utf8_state = 0; \ self->utf8_codepoint = 0; \ self->use_latin1 = false; #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(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->read_buf_lock, NULL)) != 0) { Py_CLEAR(self); PyErr_Format(PyExc_RuntimeError, "Failed to create Screen read_buf_lock mutex: %s", strerror(ret)); return 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->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 = PyMem_RawMalloc(BUFSIZ); self->window_id = window_id; if (self->write_buf == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); } self->write_buf_sz = BUFSIZ; self->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_CHARSETS; 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->alt_linebuf = alloc_linebuf(lines, columns); self->linebuf = self->main_linebuf; self->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size)); self->main_grman = grman_alloc(); self->alt_grman = grman_alloc(); self->active_hyperlink_id = 0; self->grman = self->main_grman; self->pending_mode.wait_time = s_double_to_monotonic_t(2.0); self->disable_ligatures = OPT(disable_ligatures); self->main_tabstops = PyMem_Calloc(2 * self->columns, sizeof(bool)); 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 ) { Py_CLEAR(self); return NULL; } self->main_grman->window_id = self->window_id; self->alt_grman->window_id = 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)) { 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 void deactivate_overlay_line(Screen *self); static inline Line* range_line_(Screen *self, int y); void screen_reset(Screen *self) { if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true); if (self->overlay_line.is_active) deactivate_overlay_line(self); 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->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->grman, false, self->cell_size); self->modes = empty_modes; self->active_hyperlink_id = 0; #define R(name) self->color_profile->overridden.name = 0 R(default_fg); R(default_bg); R(cursor_color); R(highlight_fg); R(highlight_bg); #undef R RESET_CHARSETS; 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_selection(&self->selections); clear_selection(&self->url_ranges); screen_cursor_position(self, 1, 1); set_dynamic_color(self, 110, NULL); set_dynamic_color(self, 111, NULL); set_color_table_color(self, 104, NULL); self->parser_state = 0; self->parser_text_start = 0; self->parser_buf_pos = 0; self->parser_has_pending_text = false; } 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); } static inline HistoryBuf* realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns, ANSIBuf *as_ansi_buf) { HistoryBuf *ans = alloc_historybuf(lines, columns, 0); if (ans == NULL) { PyErr_NoMemory(); return NULL; } ans->pagerhist = old->pagerhist; old->pagerhist = NULL; historybuf_rewrap(old, ans, as_ansi_buf); return ans; } static inline LineBuf* realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, index_type *x, index_type *y, ANSIBuf *as_ansi_buf) { LineBuf *ans = alloc_linebuf(lines, columns); if (ans == NULL) { PyErr_NoMemory(); return NULL; } linebuf_rewrap(old, ans, nclb, ncla, hb, x, y, as_ansi_buf); return ans; } static inline 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 inline void index_selection(const Screen *self, Selections *selections, bool up) { for (size_t i = 0; i < selections->count; i++) { Selection *s = selections->items + i; 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->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 \ if (self->overlay_line.is_active) deactivate_overlay_line(self); \ linebuf_reverse_index(self->linebuf, top, bottom); \ linebuf_clear_line(self->linebuf, top); \ INDEX_GRAPHICS(1) \ self->is_dirty = true; \ index_selection(self, &self->selections, false); static bool screen_resize(Screen *self, unsigned int lines, unsigned int columns) { if (self->overlay_line.is_active) deactivate_overlay_line(self); 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, num_content_lines; unsigned int cursor_x = 0, cursor_y = 0; bool cursor_is_beyond_content = false; unsigned int lines_after_cursor_before_resize = self->lines - self->cursor->y; #define setup_cursor() { \ cursor_x = x; cursor_y = y; \ cursor_is_beyond_content = num_content_lines_before > 0 && self->cursor->y >= num_content_lines_before; \ num_content_lines = num_content_lines_after; \ } // Resize overlay line if (!init_overlay_line(self, columns)) return false; // Resize main linebuf HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns, &self->as_ansi_buf); if (nh == NULL) return false; Py_CLEAR(self->historybuf); self->historybuf = nh; index_type x = self->cursor->x, y = self->cursor->y; LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &x, &y, &self->as_ansi_buf); if (n == NULL) return false; Py_CLEAR(self->main_linebuf); self->main_linebuf = n; if (is_main) setup_cursor(); grman_resize(self->main_grman, self->lines, lines, self->columns, columns); // Resize alt linebuf x = self->cursor->x, y = self->cursor->y; n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &x, &y, &self->as_ansi_buf); if (n == NULL) return false; Py_CLEAR(self->alt_linebuf); self->alt_linebuf = n; if (!is_main) setup_cursor(); grman_resize(self->alt_grman, self->lines, lines, self->columns, columns); #undef setup_cursor self->linebuf = is_main ? self->main_linebuf : self->alt_linebuf; /* printf("\nold_size: (%u, %u) new_size: (%u, %u)\n", self->columns, self->lines, columns, lines); */ self->lines = lines; self->columns = columns; self->margin_top = 0; self->margin_bottom = self->lines - 1; PyMem_Free(self->main_tabstops); self->main_tabstops = PyMem_Calloc(2*self->columns, sizeof(bool)); if (self->main_tabstops == NULL) { PyErr_NoMemory(); return false; } 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->is_dirty = true; clear_selection(&self->selections); clear_selection(&self->url_ranges); /* printf("old_cursor: (%u, %u) new_cursor: (%u, %u) beyond_content: %d\n", self->cursor->x, self->cursor->y, cursor_x, cursor_y, cursor_is_beyond_content); */ self->cursor->x = MIN(cursor_x, self->columns - 1); self->cursor->y = MIN(cursor_y, self->lines - 1); if (cursor_is_beyond_content) { self->cursor->y = 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; 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++; } } return true; } void screen_rescale_images(Screen *self) { 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->read_buf_lock); pthread_mutex_destroy(&self->write_buf_lock); Py_CLEAR(self->main_grman); Py_CLEAR(self->alt_grman); 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->main_tabstops); free(self->pending_mode.buf); free(self->selections.items); free(self->url_ranges.items); free_hyperlink_pool(self->hyperlink_pool); free(self->as_ansi_buf.buf); Py_TYPE(self)->tp_free((PyObject*)self); } // }}} // Draw text {{{ void screen_change_charset(Screen *self, uint32_t which) { switch(which) { case 0: self->current_charset = 0; self->g_charset = self->g0_charset; break; case 1: self->current_charset = 1; self->g_charset = self->g1_charset; break; } } void screen_designate_charset(Screen *self, uint32_t which, uint32_t as) { switch(which) { case 0: self->g0_charset = translation_table(as); if (self->current_charset == 0) self->g_charset = self->g0_charset; break; case 1: self->g1_charset = translation_table(as); if (self->current_charset == 1) self->g_charset = self->g1_charset; break; } } static inline void move_widened_char(Screen *self, CPUCell* cpu_cell, GPUCell *gpu_cell, index_type xpos, index_type ypos) { self->cursor->x = xpos; self->cursor->y = ypos; CPUCell src_cpu = *cpu_cell, *dest_cpu; GPUCell src_gpu = *gpu_cell, *dest_gpu; line_clear_text(self->linebuf->line, xpos, 1, BLANK_CHAR); if (self->modes.mDECAWM) { // overflow goes onto next line screen_carriage_return(self); screen_linefeed(self); self->linebuf->line_attrs[self->cursor->y] |= CONTINUED_MASK; linebuf_init_line(self->linebuf, self->cursor->y); dest_cpu = self->linebuf->line->cpu_cells; dest_gpu = self->linebuf->line->gpu_cells; self->cursor->x = MIN(2u, self->columns); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); } else { dest_cpu = cpu_cell - 1; dest_gpu = gpu_cell - 1; self->cursor->x = self->columns; } *dest_cpu = src_cpu; *dest_gpu = src_gpu; } static inline 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; } 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); } } hyperlink_id_type remap_hyperlink_ids(Screen *self, hyperlink_id_type *map) { #define PROCESS_CELL(cell) { hid = (cell).hyperlink_id; if (hid) { if (!map[hid]) map[hid] = ++num; (cell).hyperlink_id = map[hid]; }} hyperlink_id_type num = 0, hid; if (self->historybuf->count) { for (index_type y = self->historybuf->count; y-- > 0;) { CPUCell *cells = historybuf_cpu_cells(self->historybuf, y); for (index_type x = 0; x < self->historybuf->xnum; x++) { PROCESS_CELL(cells[x]); } } } LineBuf *second = self->linebuf, *first = second == self->main_linebuf ? self->alt_linebuf : self->main_linebuf; for (index_type i = 0; i < self->lines * self->columns; i++) { PROCESS_CELL(first->cpu_cell_buf[i]); } for (index_type i = 0; i < self->lines * self->columns; i++) { PROCESS_CELL(second->cpu_cell_buf[i]); } return num; #undef PROCESS_CELL } static inline bool is_flag_pair(char_type a, char_type b) { return is_flag_codepoint(a) && is_flag_codepoint(b); } static inline bool draw_second_flag_codepoint(Screen *self, char_type ch) { index_type xpos = 0, ypos = 0; if (self->cursor->x > 1) { ypos = self->cursor->y; xpos = self->cursor->x - 2; } else if (self->cursor->y > 0 && self->columns > 1) { ypos = self->cursor->y - 1; xpos = self->columns - 2; } else return false; linebuf_init_line(self->linebuf, ypos); CPUCell *cell = self->linebuf->line->cpu_cells + xpos; if (!is_flag_pair(cell->ch, ch) || cell->cc_idx[0]) return false; line_add_combining_char(self->linebuf->line, ch, xpos); self->is_dirty = true; if (selection_has_screen_line(&self->selections, ypos)) clear_selection(&self->selections); linebuf_mark_line_dirty(self->linebuf, ypos); return true; } static inline void draw_combining_char(Screen *self, char_type ch) { bool has_prev_char = false; index_type xpos = 0, ypos = 0; if (self->cursor->x > 0) { ypos = self->cursor->y; linebuf_init_line(self->linebuf, ypos); xpos = self->cursor->x - 1; has_prev_char = true; } else if (self->cursor->y > 0) { ypos = self->cursor->y - 1; linebuf_init_line(self->linebuf, ypos); xpos = self->columns - 1; has_prev_char = true; } if (self->cursor->x > 0) { ypos = self->cursor->y; linebuf_init_line(self->linebuf, ypos); xpos = self->cursor->x - 1; has_prev_char = true; } else if (self->cursor->y > 0) { ypos = self->cursor->y - 1; linebuf_init_line(self->linebuf, ypos); xpos = self->columns - 1; has_prev_char = true; } if (has_prev_char) { line_add_combining_char(self->linebuf->line, ch, xpos); self->is_dirty = true; if (selection_has_screen_line(&self->selections, ypos)) clear_selection(&self->selections); linebuf_mark_line_dirty(self->linebuf, ypos); if (ch == 0xfe0f) { // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji CPUCell *cpu_cell = self->linebuf->line->cpu_cells + xpos; GPUCell *gpu_cell = self->linebuf->line->gpu_cells + xpos; if ((gpu_cell->attrs & WIDTH_MASK) != 2 && cpu_cell->cc_idx[0] == VS16 && is_emoji_presentation_base(cpu_cell->ch)) { if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id); gpu_cell->attrs = (gpu_cell->attrs & !WIDTH_MASK) | 2; if (xpos == self->columns - 1) move_widened_char(self, cpu_cell, gpu_cell, xpos, ypos); else self->cursor->x++; } } else if (ch == 0xfe0e) { CPUCell *cpu_cell = self->linebuf->line->cpu_cells + xpos; GPUCell *gpu_cell = self->linebuf->line->gpu_cells + xpos; if ((gpu_cell->attrs & WIDTH_MASK) == 0 && cpu_cell->ch == 0 && xpos > 0) { xpos--; if (self->cursor->x > 0) self->cursor->x--; cpu_cell = self->linebuf->line->cpu_cells + xpos; gpu_cell = self->linebuf->line->gpu_cells + xpos; } if ((gpu_cell->attrs & WIDTH_MASK) == 2 && cpu_cell->cc_idx[0] == VS15 && is_emoji_presentation_base(cpu_cell->ch)) { gpu_cell->attrs = (gpu_cell->attrs & !WIDTH_MASK) | 1; } } } } void screen_draw(Screen *self, uint32_t och, bool from_input_stream) { if (is_ignored_char(och)) return; if (!self->has_activity_since_last_focus && !self->has_focus) { self->has_activity_since_last_focus = true; CALLBACK("on_activity_since_last_focus", NULL); } uint32_t ch = och < 256 ? self->g_charset[och] : och; bool is_cc = is_combining_char(ch); if (UNLIKELY(is_cc)) { draw_combining_char(self, ch); return; } bool is_flag = is_flag_codepoint(ch); if (UNLIKELY(is_flag)) { if (draw_second_flag_codepoint(self, ch)) return; } int char_width = wcwidth_std(ch); if (UNLIKELY(char_width < 1)) { if (char_width == 0) return; char_width = 1; } if (from_input_stream) self->last_graphic_char = ch; if (UNLIKELY(self->columns - self->cursor->x < (unsigned int)char_width)) { if (self->modes.mDECAWM) { screen_carriage_return(self); screen_linefeed(self); self->linebuf->line_attrs[self->cursor->y] |= CONTINUED_MASK; } else { self->cursor->x = self->columns - char_width; } } linebuf_init_line(self->linebuf, self->cursor->y); if (self->modes.mIRM) { line_right_shift(self->linebuf->line, self->cursor->x, char_width); } line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, self->active_hyperlink_id); self->cursor->x++; if (char_width == 2) { line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id); self->cursor->x++; } self->is_dirty = true; if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); } void screen_draw_overlay_text(Screen *self, const char *utf8_text) { if (self->overlay_line.is_active) deactivate_overlay_line(self); if (!utf8_text || !utf8_text[0]) return; Line *line = range_line_(self, self->cursor->y); if (!line) return; line_save_cells(line, 0, self->columns, self->overlay_line.gpu_cells, self->overlay_line.cpu_cells); self->overlay_line.is_active = true; self->overlay_line.ynum = self->cursor->y; self->overlay_line.xstart = self->cursor->x; self->overlay_line.xnum = 0; uint32_t codepoint = 0; UTF8State state = UTF8_ACCEPT; bool orig_line_wrap_mode = self->modes.mDECAWM; self->modes.mDECAWM = false; self->cursor->reverse ^= true; index_type before; while (*utf8_text) { switch(decode_utf8(&state, &codepoint, *(utf8_text++))) { case UTF8_ACCEPT: before = self->cursor->x; screen_draw(self, codepoint, false); self->overlay_line.xnum += self->cursor->x - before; break; case UTF8_REJECT: break; } } self->cursor->reverse ^= true; self->modes.mDECAWM = orig_line_wrap_mode; } 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'); } // }}} // 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, 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); } } else { index_type x, num; 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); } } } else cursor_from_sgr(self->cursor, params, count); } static inline 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 inline void write_to_child(Screen *self, const char *data, size_t sz) { if (self->window_id) schedule_write_to_child(self->window_id, 1, data, sz); if (self->test_child != Py_None) { write_to_test_child(self, data, sz); } } void write_escape_code_to_child(Screen *self, unsigned char which, const char *data) { const char *prefix, *suffix = self->modes.eight_bit_controls ? "\x9c" : "\033\\"; switch(which) { case DCS: prefix = self->modes.eight_bit_controls ? "\x90" : "\033P"; break; case CSI: prefix = self->modes.eight_bit_controls ? "\x9b" : "\033["; suffix = ""; break; case OSC: prefix = self->modes.eight_bit_controls ? "\x9d" : "\033]"; break; case PM: prefix = self->modes.eight_bit_controls ? "\x9e" : "\033^"; break; case APC: prefix = self->modes.eight_bit_controls ? "\x9f" : "\033_"; break; default: fatal("Unknown escape code to write: %u", which); } if (self->window_id) { if (suffix[0]) { schedule_write_to_child(self->window_id, 3, prefix, strlen(prefix), data, strlen(data), suffix, strlen(suffix)); } else { 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)); } } static inline bool cursor_within_margins(Screen *self) { return self->margin_top <= self->cursor->y && self->cursor->y <= self->margin_bottom; } 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, 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); } } // }}} // 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; 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; clear_selection(&self->selections); } 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 inline 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(IRM) SIMPLE_MODE(DECARM) SIMPLE_MODE(BRACKETED_PASTE) SIMPLE_MODE(FOCUS_TRACKING) 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_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:303. 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->blink = 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; 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_set_8bit_controls(Screen *self, bool yes) { self->modes.eight_bit_controls = yes; } 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}; snprintf(buf, sizeof(buf), "?%uu", screen_current_key_encoding_flags(self)); write_escape_code_to_child(self, 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; } 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; } 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; } } } void screen_xtmodkeys(Screen *self, uint32_t p1, uint32_t p2) { // this is the legacy XTerm escape code for modify keys // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-gt-Pp-m.1DB2 // we handle them as being equivalent to push and pop 1 onto the keyboard stack if ((!p1 && !p2) || (p1 == 4 && p2 == 0)) screen_pop_key_encoding_flags(self, 1); else if (p1 == 4 && p2 == 1) screen_push_key_encoding_flags(self, 1); } // }}} // Cursor {{{ unsigned long screen_current_char_width(Screen *self) { unsigned long ans = 1; if (self->cursor->x < self->columns - 1 && self->cursor->y < self->lines) { ans = linebuf_char_width_at(self->linebuf, self->cursor->x, self->cursor->y); } return ans; } bool screen_is_cursor_visible(Screen *self) { return self->modes.mDECTCEM; } void screen_backspace(Screen *self) { screen_cursor_back(self, 1, -1); } 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) { linebuf_init_line(self->linebuf, self->cursor->y); combining_type diff = found - self->cursor->x; CPUCell *cpu_cell = self->linebuf->line->cpu_cells + self->cursor->x; bool ok = true; for (combining_type i = 0; i < diff; i++) { CPUCell *c = cpu_cell + i; if (c->ch != ' ' && c->ch != 0) { ok = false; break; } } if (ok) { for (combining_type i = 0; i < diff; i++) { CPUCell *c = cpu_cell + i; c->ch = ' '; zero_at_ptr_count(c->cc_idx, arraysz(c->cc_idx)); } cpu_cell->ch = '\t'; cpu_cell->cc_idx[0] = diff; } } 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_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/) { if (count == 0) count = 1; if (move_direction < 0 && count > self->cursor->x) self->cursor->x = 0; else self->cursor->x += move_direction * count; screen_ensure_bounds(self, false, cursor_within_margins(self)); } void screen_cursor_forward(Screen *self, unsigned int count/*=1*/) { screen_cursor_back(self, count, 1); } 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; screen_ensure_bounds(self, true, in_margins); if (do_carriage_return) self->cursor->x = 0; } 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 \ if (self->overlay_line.is_active) deactivate_overlay_line(self); \ linebuf_index(self->linebuf, top, bottom); \ INDEX_GRAPHICS(-1) \ if (self->linebuf == self->main_linebuf && self->margin_top == 0) { \ /* 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++; \ } \ linebuf_clear_line(self->linebuf, bottom); \ self->is_dirty = true; \ index_selection(self, &self->selections, true); 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) { INDEX_UP; } 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; while (count > 0) { count--; INDEX_UP; } } 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 count = MIN(self->lines, count); unsigned int top = self->margin_top, bottom = self->margin_bottom; fill_from_scrollback = fill_from_scrollback && self->linebuf == self->main_linebuf; 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) { if (self->cursor->x != 0) { 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); \ } \ } #define COPY_CHARSETS(self, sp) \ sp->utf8_state = self->utf8_state; \ sp->utf8_codepoint = self->utf8_codepoint; \ sp->g0_charset = self->g0_charset; \ sp->g1_charset = self->g1_charset; \ sp->current_charset = self->current_charset; \ sp->use_latin1 = self->use_latin1; 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; COPY_CHARSETS(self, sp); sp->is_valid = true; } void screen_save_modes(Screen *self) { ScreenModes *m; buffer_push(&self->modes_savepoints, m); *m = self->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); RESET_CHARSETS; screen_reset_mode(self, DECSCNM); } else { COPY_CHARSETS(sp, self); self->g_charset = self->current_charset ? self->g1_charset : self->g0_charset; 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); screen_ensure_bounds(self, false, false); } } void screen_restore_modes(Screen *self) { const ScreenModes *m; buffer_pop(&self->modes_savepoints, m); if (m == NULL) m = &empty_modes; #define S(name) set_mode_from_const(self, name, m->m##name) S(DECTCEM); S(DECSCNM); S(DECSCNM); S(DECOM); S(DECAWM); S(DECARM); S(DECCKM); S(BRACKETED_PASTE); S(FOCUS_TRACKING); self->modes.mouse_tracking_mode = m->mouse_tracking_mode; self->modes.mouse_tracking_protocol = m->mouse_tracking_protocol; #undef S } 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->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); } // }}} // 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) { 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; if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); } } 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. * ``3`` -- Erase complete display and scrollback buffer as well. :param bool private: when ``True`` character attributes are left unchanged */ unsigned int a, b; switch(how) { case 0: a = self->cursor->y + 1; b = self->lines; break; case 1: a = 0; b = self->cursor->y; break; case 2: case 3: grman_clear(self->grman, how == 3, self->cell_size); a = 0; b = self->lines; break; default: return; } if (b > a) { for (unsigned int i=a; i < b; i++) { linebuf_init_line(self->linebuf, i); if (private) { line_clear_text(self->linebuf->line, 0, self->columns, BLANK_CHAR); } else { line_apply_cursor(self->linebuf->line, self->cursor, 0, self->columns, true); } linebuf_mark_line_dirty(self->linebuf, i); linebuf_mark_line_as_not_continued(self->linebuf, i); } self->is_dirty = true; clear_selection(&self->selections); } if (how != 2) { screen_erase_in_line(self, how, private); if (how == 1) linebuf_mark_line_as_not_continued(self->linebuf, self->cursor->y); } if (how == 3 && self->linebuf == self->main_linebuf) { historybuf_clear(self->historybuf); if (self->scrolled_by != 0) { self->scrolled_by = 0; self->scroll_changed = true; } } } 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) { linebuf_insert_lines(self->linebuf, count, self->cursor->y, bottom); self->is_dirty = true; clear_selection(&self->selections); screen_carriage_return(self); } } void screen_scroll_until_cursor(Screen *self) { unsigned int num_lines_to_scroll = MIN(self->margin_bottom, self->cursor->y + 1); index_type y = self->cursor->y; self->cursor->y = self->margin_bottom; while (num_lines_to_scroll--) screen_index(self); self->cursor->y = y; } 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) { linebuf_delete_lines(self->linebuf, count, self->cursor->y, bottom); self->is_dirty = true; clear_selection(&self->selections); screen_carriage_return(self); } } void screen_insert_characters(Screen *self, unsigned int count) { const unsigned int top = 0, bottom = self->lines ? self->lines - 1 : 0; if (count == 0) count = 1; if (top <= self->cursor->y && self->cursor->y <= bottom) { unsigned int x = self->cursor->x; unsigned int num = MIN(self->columns - x, count); linebuf_init_line(self->linebuf, self->cursor->y); line_right_shift(self->linebuf->line, x, num); line_apply_cursor(self->linebuf->line, self->cursor, x, num, true); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); self->is_dirty = true; if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections); } } 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); while (num-- > 0) screen_draw(self, self->last_graphic_char, false); } } void screen_delete_characters(Screen *self, unsigned int count) { // Delete characters, later characters are moved left const unsigned int top = 0, bottom = self->lines ? self->lines - 1 : 0; if (count == 0) count = 1; if (top <= self->cursor->y && self->cursor->y <= bottom) { unsigned int x = self->cursor->x; unsigned int num = MIN(self->columns - x, count); linebuf_init_line(self->linebuf, self->cursor->y); left_shift_line(self->linebuf->line, x, num); 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; if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections); } } void screen_erase_characters(Screen *self, unsigned int count) { // Delete characters replacing them by spaces if (count == 0) count = 1; unsigned int x = self->cursor->x; unsigned int num = MIN(self->columns - x, count); 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; if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections); } // }}} // Device control {{{ void screen_use_latin1(Screen *self, bool on) { self->use_latin1 = on; self->utf8_state = 0; self->utf8_codepoint = 0; CALLBACK("use_utf8", "O", on ? Py_False : Py_True); } bool screen_invert_colors(Screen *self) { bool inverted = false; if (self->start_visual_bell_at > 0) { if (monotonic() - self->start_visual_bell_at <= OPT(visual_bell_duration)) inverted = true; else self->start_visual_bell_at = 0; } if (self->modes.mDECSCNM) inverted = inverted ? false : true; return inverted; } void screen_bell(Screen *self) { 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: write_escape_code_to_child(self, CSI, "?62;c"); break; case '>': write_escape_code_to_child(self, CSI, ">1;" xstr(PRIMARY_VERSION) ";" xstr(SECONDARY_VERSION) "c"); // VT-220 + primary version + secondary version break; } } } void screen_report_size(Screen *self, unsigned int which) { char buf[32] = {0}; unsigned int code = 0; unsigned int width = 0, height = 0; switch(which) { case 14: code = 4; width = self->cell_size.width * self->columns; height = self->cell_size.height * self->lines; 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, 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) { // We don't implement the private device status codes, since I haven't come // across any programs that use them unsigned int x, y; static char buf[64]; switch(which) { case 5: // device status write_escape_code_to_child(self, 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, CSI, buf); 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); #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; } int sz = snprintf(buf, sizeof(buf) - 1, "%s%u;%u$y", (private ? "?" : ""), which, ans); if (sz > 0) write_escape_code_to_child(self, 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 = false; if (mode > 0) { blink = mode % 2; shape = (mode < 3) ? CURSOR_BLOCK : (mode < 5) ? CURSOR_UNDERLINE : (mode < 7) ? CURSOR_BEAM : NO_CURSOR_SHAPE; } if (shape != self->cursor->shape || blink != self->cursor->blink) { self->cursor->shape = shape; self->cursor->blink = blink; } break; } } void set_title(Screen *self, PyObject *title) { CALLBACK("title_changed", "O", title); } 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", "Is", code, ""); } else { CALLBACK("set_dynamic_color", "IO", code, color); } } void clipboard_control(Screen *self, PyObject *data) { CALLBACK("clipboard_control", "O", data); } void set_color_table_color(Screen *self, unsigned int code, PyObject *color) { if (color == NULL) { CALLBACK("set_color_table_color", "Is", code, ""); } else { CALLBACK("set_color_table_color", "IO", code, color); } } void process_cwd_notification(Screen *self, unsigned int code, PyObject *cwd) { (void)self; (void)code; (void)cwd; // we ignore this as we dont need the stupid OSC 7 cwd reporting protocol, // since, being moderately intelligent, we can get CWD directly. } void screen_handle_cmd(Screen *self, PyObject *cmd) { CALLBACK("handle_remote_cmd", "O", cmd); } void screen_push_colors(Screen *self, unsigned int idx) { colorprofile_push_colors(self->color_profile, idx); } void screen_pop_colors(Screen *self, unsigned int idx) { colorprofile_pop_colors(self->color_profile, idx); } 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, CSI, buf); } void screen_handle_print(Screen *self, PyObject *msg) { CALLBACK("handle_remote_print", "O", msg); } void screen_request_capabilities(Screen *self, char c, PyObject *q) { static char buf[128]; int shape = 0; const char *query; switch(c) { case '+': CALLBACK("request_capabilities", "O", q); break; case '$': // report status query = PyUnicode_AsUTF8(q); if (strcmp(" q", query) == 0) { // cursor shape switch(self->cursor->shape) { case NO_CURSOR_SHAPE: case NUM_OF_CURSOR_SHAPES: shape = 1; break; case CURSOR_BLOCK: shape = self->cursor->blink ? 0 : 2; break; case CURSOR_UNDERLINE: shape = self->cursor->blink ? 3 : 4; break; case CURSOR_BEAM: shape = self->cursor->blink ? 5 : 6; break; } shape = snprintf(buf, sizeof(buf), "1$r%d q", shape); } else if (strcmp("m", query) == 0) { // SGR shape = snprintf(buf, sizeof(buf), "1$r%sm", cursor_as_sgr(self->cursor)); } else if (strcmp("r", query) == 0) { shape = snprintf(buf, sizeof(buf), "1$r%u;%ur", self->margin_top + 1, self->margin_bottom + 1); } else { shape = snprintf(buf, sizeof(buf), "0$r%s", query); } if (shape > 0) write_escape_code_to_child(self, DCS, buf); break; } } // }}} // Rendering {{{ static inline 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 inline void screen_reset_dirty(Screen *self) { self->is_dirty = false; self->history_line_added_count = 0; } static inline bool screen_has_marker(Screen *self) { return self->marker != NULL; } void screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_data, bool cursor_has_moved) { unsigned int history_line_added_count = self->history_line_added_count; index_type lnum; bool was_dirty = self->is_dirty; 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; for (index_type y = 0; y < MIN(self->lines, self->scrolled_by); y++) { lnum = self->scrolled_by - 1 - y; historybuf_init_line(self->historybuf, lnum, self->historybuf->line); if (self->historybuf->line->has_dirty_text) { render_line(fonts_data, self->historybuf->line, lnum, self->cursor, self->disable_ligatures); if (screen_has_marker(self)) mark_text_in_line(self->marker, self->historybuf->line); historybuf_mark_line_clean(self->historybuf, lnum); } update_line_data(self->historybuf->line, y, address); } for (index_type y = self->scrolled_by; y < self->lines; y++) { lnum = y - self->scrolled_by; linebuf_init_line(self->linebuf, lnum); if (self->linebuf->line->has_dirty_text || (cursor_has_moved && (self->cursor->y == lnum || self->last_rendered.cursor_y == lnum))) { render_line(fonts_data, self->linebuf->line, lnum, self->cursor, self->disable_ligatures); if (self->linebuf->line->has_dirty_text && screen_has_marker(self)) mark_text_in_line(self->marker, self->linebuf->line); linebuf_mark_line_clean(self->linebuf, lnum); } update_line_data(self->linebuf->line, y, address); } if (was_dirty) clear_selection(&self->url_ranges); } static inline bool selection_boundary_less_than(SelectionBoundary *a, SelectionBoundary *b) { 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; } typedef Line*(linefunc_t)(Screen*, int); static inline 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; } linebuf_init_line(self->linebuf, y); return self->linebuf->line; } static inline Line* range_line_(Screen *self, int y) { if (y < 0) { historybuf_init_line(self->historybuf, -(y + 1), self->historybuf->line); return self->historybuf->line; } linebuf_init_line(self->linebuf, y); return self->linebuf->line; } static inline 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 Screen *self, const Selection *sel, IterationData *ans, int min_y, bool 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 = self->columns; 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; } if (add_scrolled_by) { ans->y += self->scrolled_by; ans->y_limit += self->scrolled_by; } ans->y = MAX(ans->y, min_y); } static inline 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 inline 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 inline void apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask) { iteration_data(self, s, &s->last_rendered, -self->historybuf->count, true); for (int y = MAX(0, s->last_rendered.y); y < s->last_rendered.y_limit && y < (int)self->lines; y++) { Line *line = visual_line_(self, y); uint8_t *line_start = data + self->columns * y; XRange xr = xrange_for_iteration(&s->last_rendered, y, line); for (index_type x = xr.x; x < xr.x_limit; x++) line_start[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(self, s, &idata, -self->historybuf->count, true); if (!iteration_data_is_empty(self, &idata)) return true; } } return false; } void screen_apply_selection(Screen *self, void *address, size_t size) { memset(address, 0, size); for (size_t i = 0; i < self->selections.count; i++) { apply_selection(self, address, self->selections.items + i, 1); } self->selections.last_rendered_count = self->selections.count; for (size_t i = 0; i < self->url_ranges.count; i++) { apply_selection(self, address, self->url_ranges.items + i, 2); } self->url_ranges.last_rendered_count = self->url_ranges.count; } static inline PyObject* text_for_range(Screen *self, const Selection *sel, bool insert_newlines) { IterationData idata; iteration_data(self, sel, &idata, -self->historybuf->count, false); int limit = MIN((int)self->lines, idata.y_limit); PyObject *ans = PyTuple_New(limit - idata.y); if (!ans) return NULL; for (int i = 0, y = idata.y; y < limit; y++, i++) { Line *line = range_line_(self, y); XRange xr = xrange_for_iteration(&idata, y, line); char leading_char = (i > 0 && insert_newlines && !line->continued) ? '\n' : 0; PyObject *text = unicode_in_range(line, xr.x, xr.x_limit, true, leading_char, false); if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); } PyTuple_SET_ITEM(ans, i, text); } return ans; } static inline hyperlink_id_type hyperlink_id_for_range(Screen *self, const Selection *sel) { IterationData idata; iteration_data(self, sel, &idata, -self->historybuf->count, false); for (int i = 0, y = idata.y; y < idata.y_limit && y < (int)self->lines; y++, i++) { 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 inline PyObject* extend_tuple(PyObject *a, PyObject *b) { Py_ssize_t bs = PyBytes_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) { PyObject *empty_string = PyUnicode_FromString(""), *ans = NULL; if (!empty_string) return NULL; for (size_t i = 0; i < self->url_ranges.count; i++) { Selection *s = self->url_ranges.items + i; if (!is_selection_empty(s)) { PyObject *temp = text_for_range(self, s, false); if (!temp) goto error; PyObject *text = PyUnicode_Join(empty_string, temp); Py_CLEAR(temp); if (!text) goto error; if (ans) { PyObject *t = ans; ans = PyUnicode_Concat(ans, text); Py_CLEAR(text); Py_CLEAR(t); if (!ans) goto error; } else ans = text; } } Py_CLEAR(empty_string); if (!ans) Py_RETURN_NONE; return ans; error: Py_CLEAR(empty_string); Py_CLEAR(ans); return NULL; } bool screen_open_url(Screen *self) { 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("open_url", "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("open_url", "OH", text, 0); found = true; } Py_CLEAR(text); return found; } static void deactivate_overlay_line(Screen *self) { if (self->overlay_line.is_active && self->overlay_line.xnum && self->overlay_line.ynum < self->lines) { Line *line = range_line_(self, self->overlay_line.ynum); line_reset_cells(line, self->overlay_line.xstart, self->overlay_line.xnum, self->overlay_line.gpu_cells, self->overlay_line.cpu_cells); if (self->cursor->y == self->overlay_line.ynum) self->cursor->x = self->overlay_line.xstart; self->is_dirty = true; linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum); } self->overlay_line.is_active = false; self->overlay_line.ynum = 0; self->overlay_line.xnum = 0; self->overlay_line.xstart = 0; } // }}} // 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* hyperlinks_as_list(Screen *self, PyObject *args UNUSED) { return screen_hyperlinks_as_list(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 PyObject* set_pending_timeout(Screen *self, PyObject *val) { if (!PyFloat_Check(val)) { PyErr_SetString(PyExc_TypeError, "timeout must be a float"); return NULL; } PyObject *ans = PyFloat_FromDouble(self->pending_mode.wait_time); self->pending_mode.wait_time = s_double_to_monotonic_t(PyFloat_AS_DOUBLE(val)); return ans; } 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); } 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); } static inline 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); } 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; } 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(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* visual_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(); } int kind = PyUnicode_KIND(src); void *buf = PyUnicode_DATA(src); Py_ssize_t sz = PyUnicode_GET_LENGTH(src); for (Py_ssize_t i = 0; i < sz; i++) screen_draw(self, PyUnicode_READ(kind, buf, i), true); Py_RETURN_NONE; } extern void parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject *dump_callback, const char *report_name, Region *region); 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_UCS4 *buf = PyUnicode_AsUCS4Copy(src); if (!buf) return NULL; unsigned int params[MAX_PARAMS] = {0}; parse_sgr(self, buf, PyUnicode_GET_LENGTH(src), params, NULL, "parse_sgr", 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), 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* is_using_alternate_linebuf(Screen *self, PyObject *a UNUSED) { if (self->linebuf == self->alt_linebuf) Py_RETURN_TRUE; Py_RETURN_FALSE; } WRAP1E(cursor_back, 1, -1) WRAP1B(erase_in_line, 0) WRAP1B(erase_in_display, 0) WRAP0(scroll_until_cursor) #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(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* 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; if (!PyArg_ParseTuple(args, "II|pp", &x, &y, &in_left_half_of_cell, &ended)) return NULL; screen_update_selection(self, x, y, in_left_half_of_cell, ended, false); 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) 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* 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_selection(Screen *self, PyObject *a UNUSED) { PyObject *lines = NULL; for (size_t i = 0; i < self->selections.count; i++) { PyObject *temp = text_for_range(self, self->selections.items + i, true); 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; } bool screen_selection_range_for_line(Screen *self, index_type y, index_type *start, index_type *end) { if (y >= self->lines) { return false; } Line *line = visual_line_(self, y); index_type xlimit = line->xnum, xstart = 0; while (xlimit > 0 && CHAR_IS_BLANK(line->cpu_cells[xlimit - 1].ch)) xlimit--; while (xstart < xlimit && CHAR_IS_BLANK(line->cpu_cells[xstart].ch)) xstart++; *start = xstart; *end = xlimit > 0 ? xlimit - 1 : 0; return true; } static inline bool is_opt_word_char(char_type ch) { for (size_t i = 0; i < OPT(select_by_word_characters_count); i++) { if (OPT(select_by_word_characters[i]) == ch) return true; } return false; } static bool is_char_ok_for_word_extension(Line* line, index_type x) { char_type ch = line->cpu_cells[x].ch; if (is_word_char(ch) || is_opt_word_char(ch)) return true; // pass : from :// so that common URLs are matched if (ch == ':' && x + 2 < line->xnum && line->cpu_cells[x+1].ch == '/' && line->cpu_cells[x+2].ch == '/') return true; return false; } 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) is_char_ok_for_word_extension(line, x) if (!is_ok(x)) { if (initial_selection) return false; *s = x; *e = x; return true; } start = x; end = x; while(true) { while(start > 0 && is_ok(start - 1)) start--; if (start > 0 || !line->continued || *y1 == 0) break; line = visual_line_(self, *y1 - 1); if (!is_ok(self->columns - 1)) break; (*y1)--; start = self->columns - 1; } line = visual_line_(self, *y2); while(true) { while(end < self->columns - 1 && is_ok(end + 1)) end++; if (end < self->columns - 1 || *y2 >= self->lines - 1) break; line = visual_line_(self, *y2 + 1); if (!line->continued || !is_ok(0)) break; (*y2)++; end = 0; } *s = start; *e = end; return true; #undef is_ok } 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; } if (amt == 0) return false; unsigned int new_scroll = MIN(self->scrolled_by + amt, self->historybuf->count); if (new_scroll != self->scrolled_by) { self->scrolled_by = new_scroll; self->scroll_changed = true; return true; } return false; } 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; } bool screen_is_selection_dirty(Screen *self) { IterationData q; 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) return true; for (size_t i = 0; i < self->selections.count; i++) { iteration_data(self, self->selections.items + i, &q, 0, true); 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, self->url_ranges.items + i, &q, 0, true); 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) { #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 inline void add_url_range(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) { #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; 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); } static bool mark_hyperlinks_in_line(Screen *self, Line *line, hyperlink_id_type id, index_type y) { index_type start = 0; bool found = false; bool in_range = false; for (index_type x = 0; x < line->xnum; x++) { bool has_hyperlink = line->cpu_cells[x].hyperlink_id == id; if (in_range) { if (!has_hyperlink) { add_url_range(self, start, y, x - 1, y); 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); return found; } static void sort_ranges(const Screen *self, Selections *s) { IterationData a; for (size_t i = 0; i < s->count; i++) { iteration_data(self, s->items + i, &a, 0, false); 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; do { if (mark_hyperlinks_in_line(self, line, id, ypos)) 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)) last_marked_line = ypos; ypos++; } if (self->url_ranges.count > 1) sort_ranges(self, &self->url_ranges); return id; } void screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool ended, bool start_extended_selection) { if (!self->selections.count) return; self->selections.in_progress = !ended; Selection *s = self->selections.items; s->input_current.x = x; s->input_current.y = y; s->input_current.in_left_half_of_cell = in_left_half_of_cell; s->end_scrolled_by = self->scrolled_by; SelectionBoundary start, end, *a, *b; a = &s->start, b = &s->end; switch(self->selections.extend_mode) { case EXTEND_WORD: { SelectionBoundary *before = &s->input_start, *after = &s->input_current; if (selection_boundary_less_than(after, before)) { before = after; after = &s->input_start; } bool found_at_start = screen_selection_range_for_word(self, before->x, before->y, &start.y, &end.y, &start.x, &end.x, true); if (found_at_start) { a->x = start.x; a->y = start.y; a->in_left_half_of_cell = true; b->x = end.x; b->y = end.y; b->in_left_half_of_cell = false; } else { a->x = before->x; a->y = before->y; a->in_left_half_of_cell = before->in_left_half_of_cell; b->x = a->x; b->y = a->y; b->in_left_half_of_cell = a->in_left_half_of_cell; } bool found_at_end = screen_selection_range_for_word(self, after->x, after->y, &start.y, &end.y, &start.x, &end.x, false); if (found_at_end) { b->x = end.x; b->y = end.y; b->in_left_half_of_cell = false; } break; } case EXTEND_LINE: { index_type top_line, bottom_line; if (start_extended_selection || y == s->start.y) { top_line = y; bottom_line = y; } else if (y < s->start.y) { top_line = y; bottom_line = s->start.y; a = &s->end; b = &s->start; } else if (y > s->start.y) { bottom_line = y; top_line = s->start.y; } else break; while (top_line > 0 && visual_line_(self, top_line)->continued) { if (!screen_selection_range_for_line(self, top_line - 1, &start.x, &end.x)) break; top_line--; } while (bottom_line < self->lines - 1 && visual_line_(self, bottom_line + 1)->continued) { if (!screen_selection_range_for_line(self, bottom_line + 1, &start.x, &end.x)) break; bottom_line++; } if (screen_selection_range_for_line(self, top_line, &start.x, &start.y) && screen_selection_range_for_line(self, bottom_line, &end.x, &end.y)) { bool multiline = top_line != bottom_line; a->x = multiline ? 0 : start.x; a->y = top_line; a->in_left_half_of_cell = true; b->x = end.y; b->y = bottom_line; b->in_left_half_of_cell = false; } break; } case EXTEND_CELL: s->end.x = x; s->end.y = y; s->end.in_left_half_of_cell = in_left_half_of_cell; break; } if (!self->selections.in_progress) call_boss(set_primary_selection, NULL); } static PyObject* mark_as_dirty(Screen *self, PyObject *a UNUSED) { self->is_dirty = 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" return PyLong_FromUnsignedLong(screen_current_char_width(self)); } 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* send_escape_code_to_child(Screen *self, PyObject *args) { int code; char *text; if (!PyArg_ParseTuple(args, "is", &code, &text)) return NULL; write_escape_code_to_child(self, code, text); Py_RETURN_NONE; } static inline 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); } 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); } 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->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) { 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; unsigned int mark = (gpu_cell->attrs >> MARK_SHIFT) & MARK_MASK; if (mark) { PyObject *t = Py_BuildValue("III", x, y, mark); if (!t) { Py_DECREF(ans); return NULL; } if (PyList_Append(ans, t) != 0) { Py_DECREF(t); Py_DECREF(ans); return NULL; } Py_DECREF(t); } } } return ans; } static PyObject* paste(Screen *self, PyObject *bytes) { if (!PyBytes_Check(bytes)) { PyErr_SetString(PyExc_TypeError, "Must paste() bytes"); return NULL; } if (self->modes.mBRACKETED_PASTE) write_escape_code_to_child(self, CSI, BRACKETED_PASTE_START); write_to_child(self, PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes)); if (self->modes.mBRACKETED_PASTE) write_escape_code_to_child(self, CSI, BRACKETED_PASTE_END); Py_RETURN_NONE; } static PyObject* paste_bytes(Screen *self, PyObject *bytes) { if (!PyBytes_Check(bytes)) { PyErr_SetString(PyExc_TypeError, "Must paste() bytes"); return NULL; } write_to_child(self, PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes)); Py_RETURN_NONE; } 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; if (self->modes.mFOCUS_TRACKING) write_escape_code_to_child(self, 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(insert_characters) 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* 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; } #define MND(name, args) {#name, (PyCFunction)name, args, #name}, #define MODEFUNC(name) MND(name, METH_NOARGS) MND(set_##name, METH_O) static PyMethodDef methods[] = { MND(line, METH_O) MND(visual_line, METH_VARARGS) MND(current_url_text, METH_NOARGS) MND(draw, METH_O) MND(apply_sgr, METH_O) MND(cursor_position, 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_back, METH_VARARGS) MND(erase_in_line, METH_VARARGS) MND(erase_in_display, METH_VARARGS) MND(scroll_until_cursor, METH_NOARGS) MND(hyperlinks_as_list, METH_NOARGS) MND(garbage_collect_hyperlink_pool, METH_NOARGS) MND(hyperlink_for_id, METH_O) MND(reverse_scroll, METH_VARARGS) METHOD(current_char_width, METH_NOARGS) MND(insert_lines, METH_VARARGS) MND(delete_lines, METH_VARARGS) MND(insert_characters, METH_VARARGS) MND(delete_characters, METH_VARARGS) MND(erase_characters, 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, ""}, MND(set_pending_timeout, METH_O) MND(as_text, METH_VARARGS) MND(as_text_non_visual, METH_VARARGS) MND(as_text_alternate, 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(resize, METH_VARARGS) MND(set_margins, METH_VARARGS) MND(rescale_images, METH_NOARGS) MND(current_key_encoding_flags, METH_NOARGS) MND(text_for_selection, METH_NOARGS) MND(is_rectangle_select, METH_NOARGS) MND(scroll, METH_VARARGS) MND(send_escape_code_to_child, 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) {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, {NULL} /* Sentinel */ }; static PyGetSetDef getsetters[] = { GETSET(in_bracketed_paste_mode) GETSET(auto_repeat_enabled) GETSET(focus_tracking_enabled) GETSET(cursor_visible) GETSET(cursor_key_mode) GETSET(disable_ligatures) {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"}, {"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, .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, ""}, {NULL} /* Sentinel */ }; INIT_TYPE(Screen) // }}}