#!/usr/bin/env python # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal import codecs import unicodedata from collections import deque, namedtuple from typing import Sequence from pyte import charsets as cs, graphics as g, modes as mo from .data_types import Line, Cursor, rewrap_lines from .utils import wcwidth, is_simple_string, sanitize_title from .unicode import ignore_pat from .fast_data_types import LineBuf #: A container for screen's scroll margins. Margins = namedtuple("Margins", "top bottom") #: A container for savepoint, created on :data:`~pyte.escape.DECSC`. Savepoint = namedtuple("Savepoint", [ "cursor", "g0_charset", "g1_charset", "charset", "use_utf8", "origin", "wrap" ]) def wrap_cursor_position(x, y, lines, columns): if x >= columns: if y < lines - 1: x, y = 0, y + 1 else: x, y = x - 1, y return x, y default_callbacks = { 'title_changed': lambda t: None, 'icon_changed': lambda i: None, 'write_to_child': lambda data: None, 'change_default_color': lambda which, val: None } class Screen: """ See standard ECMA-48, Section 6.1.1 http://www.ecma-international.org/publications/standards/Ecma-048.htm for a description of the presentational component, implemented by ``Screen``. """ tracker_callbacks = 'cursor_changed cursor_position_changed update_screen update_line_range update_cell_range line_added_to_history'.split() _notify_cursor_position = True def __init__(self, opts, tracker, callbacks=None, columns: int=80, lines: int=24): for attr in self.tracker_callbacks: setattr(self, attr, getattr(tracker, attr)) for attr in default_callbacks: setattr(self, attr, getattr(callbacks, attr, default_callbacks[attr])) self.main_savepoints, self.alt_savepoints = deque(), deque() self.savepoints = self.main_savepoints self.columns = columns self.lines = lines sz = max(1000, opts.scrollback_lines) self.tophistorybuf = LineBuf(sz, self.columns) self.main_linebuf, self.alt_linebuf = LineBuf(self.lines, self.columns), LineBuf(self.lines, self.columns) self.linebuf = self.main_linebuf self.reset(notify=False) def apply_opts(self, opts): sz = max(1000, opts.scrollback_lines) if sz != self.tophistorybuf.maxlen: previous = self.tophistorybuf self.tophistorybuf = LineBuf(opts.scrollback_lines, self.columns) self.tophistorybuf.copy_old(previous) def line(self, i): return self.linebuf[i] def __repr__(self): return ("{0}({1}, {2})".format(self.__class__.__name__, self.columns, self.lines)) def notify_cursor_position(self): if self._notify_cursor_position: self.cursor_position_changed(self.cursor) @property def display(self) -> Sequence[str]: return tuple(map(str, self.linebuf)) def toggle_screen_buffer(self): self.save_cursor() if self.linebuf is self.main_linebuf: self.linebuf, self.savepoints = self.alt_linebuf, self.alt_savepoints else: self.linebuf, self.savepoints = self.main_linebuf, self.main_savepoints self.restore_cursor() self.update_screen() def reset(self, notify=True): """Resets the terminal to its initial state. * Scroll margins are reset to screen boundaries. * Cursor is moved to home location -- ``(0, 0)`` and its attributes are set to defaults (see :attr:`default_char`). * Screen is cleared -- each character is reset to :attr:`default_char`. * Tabstops are reset to "every eight columns". .. note:: Neither VT220 nor VT102 manuals mentioned that terminal modes and tabstops should be reset as well, thanks to :manpage:`xterm` -- we now know that. """ if self.linebuf is self.alt_linebuf: self.toggle_screen_buffer() self.linebuf.clear() self.linebuf[:] = (Line(self.columns) for i in range(self.lines)) self.mode = {mo.DECAWM, mo.DECTCEM} self.margins = Margins(0, self.lines - 1) self.charset = 0 self.g0_charset = cs.LAT1_MAP self.g1_charset = cs.VT100_MAP self.use_utf8 = True self.utf8_decoder = codecs.getincrementaldecoder("utf-8")("replace") # From ``man terminfo`` -- "... hardware tabs are initially # set every `n` spaces when the terminal is powered up. Since # we aim to support VT102 / VT220 and linux -- we use n = 8. self.tabstops = set(range(7, self.columns, 8)) self.normal_keypad_mode() self.cursor = Cursor(0, 0) self.cursor_changed(self.cursor) self.cursor_position() self.change_default_color('fg', None) self.change_default_color('bg', None) if notify: self.update_screen() def resize(self, lines: int, columns: int): """Resize the screen to the given dimensions. .. note:: According to `xterm`, we should also reset origin mode and screen margins, see ``xterm/screen.c:1761``. """ self.lines, self.columns = lines, columns for hb in (self.tophistorybuf, ): old = hb.copy() hb.clear(), hb.extend(rewrap_lines(old, columns)) for lb in (self.main_linebuf, self.alt_linebuf): old_lines = lb[:] lb.clear() lb[:] = rewrap_lines(old_lines, self.columns) while len(lb) < self.lines: lb.append(Line(self.columns)) if len(lb) > self.lines: extra = len(lb) - self.lines slc = lb[:extra] del lb[:extra] if lb is self.main_linebuf: self.tophistorybuf.extend(slc) self.margins = Margins(0, self.lines - 1) self._notify_cursor_position = False try: x, y = self.cursor.x, self.cursor.y self.reset_mode(mo.DECOM) self.cursor.x, self.cursor.y = x, y finally: self._notify_cursor_position = True self.ensure_bounds() def set_margins(self, top=None, bottom=None): """Selects top and bottom margins for the scrolling region. Margins determine which screen lines move during scrolling (see :meth:`index` and :meth:`reverse_index`). Characters added outside the scrolling region do not cause the screen to scroll. :param int top: the smallest line number that is scrolled. :param int bottom: the biggest line number that is scrolled. """ if top is None or bottom is None: return # Arguments are 1-based, while :attr:`margins` are zero based -- # so we have to decrement them by one. We also make sure that # both of them is bounded by [0, lines - 1]. top = max(0, min(top - 1, self.lines - 1)) bottom = max(0, min(bottom - 1, self.lines - 1)) # 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. if bottom - top >= 1: self.margins = Margins(top, bottom) # The cursor moves to the home position when the top and # bottom margins of the scrolling region (DECSTBM) changes. self.cursor_position() def set_mode(self, *modes, private=False): """Sets (enables) a given list of modes. :param list modes: modes to set, where each mode is a constant from :mod:`pyte.modes`. """ # Private mode codes are shifted, to be distingiushed from non # private ones. if private: modes = [mode << 5 for mode in modes] self.mode.update(modes) # When DECOLM mode is set, the screen is erased and the cursor # moves to the home position. if mo.DECCOLM in modes: # self.resize(columns=132) Disabled since we only allow resizing # by the user self.erase_in_display(2) self.cursor_position() # According to `vttest`, DECOM should also home the cursor, see # vttest/main.c:303. if mo.DECOM in modes: self.cursor_position() # Mark all displayed characters as reverse. if mo.DECSCNM in modes: for line in self.linebuf: for i in range(len(line)): line.set_reverse(i, True) self.update_screen() self.select_graphic_rendition(7) # +reverse. # Show/hide the cursor. previous, self.cursor.hidden = self.cursor.hidden, mo.DECTCEM not in self.mode if previous != self.cursor.hidden: self.cursor_changed(self.cursor) if mo.ALTERNATE_SCREEN in self.mode and self.linebuf is self.main_linebuf: self.toggle_screen_buffer() @property def in_bracketed_paste_mode(self): return mo.BRACKETED_PASTE in self.mode @property def enable_focus_tracking(self): return mo.FOCUS_TRACKING in self.mode def reset_mode(self, *modes, private=False): """Resets (disables) a given list of modes. :param list modes: modes to reset -- hopefully, each mode is a constant from :mod:`pyte.modes`. """ # Private mode codes are shifted, to be distinguished from non # private ones. if private: modes = [mode << 5 for mode in modes] self.mode.difference_update(modes) # Lines below follow the logic in :meth:`set_mode`. if mo.DECCOLM in modes: # self.resize(columns=80) Disabled since we only allow resizing by # the user self.erase_in_display(2) self.cursor_position() if mo.DECOM in modes: self.cursor_position() if mo.DECSCNM in modes: for line in self.linebuf: for i in range(len(line)): line.set_reverse(i, False) self.update_screen() self.select_graphic_rendition(27) # -reverse. # Show/hide the cursor. previous, self.cursor.hidden = self.cursor.hidden, mo.DECTCEM not in self.mode if previous != self.cursor.hidden: self.cursor_changed(self.cursor) if mo.ALTERNATE_SCREEN not in self.mode and self.linebuf is not self.main_linebuf: self.toggle_screen_buffer() def define_charset(self, code, mode): """Defines ``G0`` or ``G1`` charset. :param str code: character set code, should be a character from ``b"B0UK"``, otherwise ignored. :param str mode: if ``"("`` ``G0`` charset is defined, if ``")"`` -- we operate on ``G1``. .. warning:: User-defined charsets are currently not supported. """ if code in cs.MAPS: if mode == b"(": self.g0_charset = cs.MAPS[code] elif mode == b")": self.g1_charset = cs.MAPS[code] def shift_in(self): """Selects ``G0`` character set.""" self.charset = 0 def shift_out(self): """Selects ``G1`` character set.""" self.charset = 1 def select_other_charset(self, code): """Selects other (non G0 or G1) charset. :param str code: character set code, should be a character from ``b"@G8"``, otherwise ignored. .. note:: We currently follow ``"linux"`` and only use this command to switch from ISO-8859-1 to UTF-8 and back. .. seealso:: `Standard ECMA-35, Section 15.4 \ `_ for a description of VTXXX character set machinery. """ if code == b"@": self.use_utf8 = False self.utf8_decoder.reset() elif code in b"G8": self.use_utf8 = True def _decode(self, data): """Decodes bytes to text according to the selected charset. :param bytes data: bytes to decode. """ if self.charset: return "".join(self.g1_charset[b] for b in data) if self.use_utf8: return self.utf8_decoder.decode(data) return "".join(self.g0_charset[b] for b in data) def _draw_fast(self, data: str) -> None: do_insert = mo.IRM in self.mode pos = 0 while pos < len(data): space_left_in_line = self.columns - self.cursor.x len_left = len(data) - pos if space_left_in_line < 1: if mo.DECAWM in self.mode: self.carriage_return() self.linefeed() self.linebuf[self.cursor.y].continued = True space_left_in_line = self.columns else: space_left_in_line = 1 len_left = 1 pos = len(data) - 1 self.cursor.x = self.columns - 1 write_sz = min(len_left, space_left_in_line) line = self.linebuf[self.cursor.y] if do_insert: line.right_shift(self.cursor.x, write_sz) line.set_text(data, pos, write_sz, self.cursor) pos += write_sz cx = self.cursor.x self.cursor.x += write_sz right = self.columns - 1 if do_insert else max(0, min(self.cursor.x - 1, self.columns - 1)) self.update_cell_range(self.cursor.y, cx, right) def _draw_char(self, char: str, char_width: int) -> None: space_left_in_line = self.columns - self.cursor.x if space_left_in_line < char_width: if mo.DECAWM in self.mode: self.carriage_return() self.linefeed() self.linebuf[self.cursor.y].continued = True else: self.cursor.x = self.columns - char_width do_insert = mo.IRM in self.mode cx = self.cursor.x line = self.linebuf[self.cursor.y] if char_width > 0: if do_insert: line.right_shift(self.cursor.x, char_width) line.set_char(cx, char, char_width, self.cursor) self.cursor.x += 1 if char_width == 2: line.set_char(self.cursor.x, '\0', 0, self.cursor) self.cursor.x += 1 right = self.columns - 1 if do_insert else max(0, min(self.cursor.x - 1, self.columns - 1)) self.update_cell_range(self.cursor.y, cx, right) elif unicodedata.combining(char): # A zero-cell character is combined with the previous # character either on this or the preceeding line. if cx > 0: line.add_combining_char(cx - 1, char) self.update_cell_range(self.cursor.y, cx - 1, cx - 1) elif self.cursor.y > 0: lline = self.linebuf[self.cursor.y - 1] lline.add_combining_char(self.columns - 1, char) self.update_cell_range(self.cursor.y - 1, self.columns - 1, self.columns - 1) def draw(self, data: bytes) -> None: """ Displays decoded characters at the current cursor position and creates new lines as need if DECAWM is set. """ orig_x, orig_y = self.cursor.x, self.cursor.y self._notify_cursor_position = False data = self._decode(data) try: if is_simple_string(data): return self._draw_fast(data) data = ignore_pat.sub('', data) if data: widths = list(map(wcwidth, data)) if sum(widths) == len(data): return self._draw_fast(data) for char, char_width in zip(data, widths): self._draw_char(char, char_width) finally: self._notify_cursor_position = True if orig_x != self.cursor.x or orig_y != self.cursor.y: self.notify_cursor_position() def set_title(self, param): """Sets terminal title. .. note:: This is an XTerm extension supported by the Linux terminal. """ self.title_changed(sanitize_title(self._decode(param))) def set_icon_name(self, param): """Sets icon name. .. note:: This is an XTerm extension supported by the Linux terminal. """ self.icon_changed(sanitize_title(self._decode(param))) def carriage_return(self): """Move the cursor to the beginning of the current line.""" x, self.cursor.x = self.cursor.x, 0 if x != self.cursor.x: self.notify_cursor_position() def index(self): """Move the cursor down one line in the same column. If the cursor is at the last line, create a new line at the bottom. """ top, bottom = self.margins if self.cursor.y == bottom: l = self.linebuf.pop(top) if self.linebuf is self.main_linebuf: self.tophistorybuf.append(l) self.line_added_to_history() self.linebuf.insert(bottom, Line(self.columns)) if bottom - top >= self.lines - 1: self.update_screen() else: self.update_line_range(top, bottom) else: self.cursor_down() def reverse_index(self): """Move the cursor up one line in the same column. If the cursor is at the first line, create a new line at the top. """ top, bottom = self.margins if self.cursor.y == top: self.linebuf.pop(bottom) self.linebuf.insert(top, Line(self.columns)) if bottom - top >= self.lines - 1: self.update_screen() else: self.update_line_range(top, bottom) else: self.cursor_up() def linefeed(self): """Performs an index and, if :data:`~pyte.modes.LNM` is set, a carriage return. """ self.index() if mo.LNM in self.mode: self.carriage_return() self.ensure_bounds() def tab(self): """Move to the next tab space, or the end of the screen if there aren't anymore left. """ for stop in sorted(self.tabstops): if self.cursor.x < stop: column = stop break else: column = self.columns - 1 if column != self.cursor.x: self.cursor.x = column self.notify_cursor_position() def backspace(self): """Move cursor to the left one or keep it in it's position if it's at the beginning of the line already. """ self.cursor_back() def save_cursor(self): """Push the current cursor position onto the stack.""" self.savepoints.append(Savepoint(self.cursor.copy(), self.g0_charset, self.g1_charset, self.charset, self.use_utf8, mo.DECOM in self.mode, mo.DECAWM in self.mode)) def restore_cursor(self): """Set the current cursor position to whatever cursor is on top of the stack. """ if self.savepoints: savepoint = self.savepoints.pop() self.g0_charset = savepoint.g0_charset self.g1_charset = savepoint.g1_charset self.charset = savepoint.charset self.use_utf8 = savepoint.use_utf8 if savepoint.origin: self.set_mode(mo.DECOM) if savepoint.wrap: self.set_mode(mo.DECAWM) self.cursor = savepoint.cursor self.cursor_changed(self.cursor) self.ensure_bounds(use_margins=True) else: # If nothing was saved, the cursor moves to home position; # origin mode is reset. TODO: DECAWM? self.reset_mode(mo.DECOM) self.cursor_position() def insert_lines(self, count=1): """Inserts the indicated # of lines at line with cursor. Lines displayed **at** and below the cursor move down. Lines moved past the bottom margin are lost. :param count: number of lines to delete. """ count = count or 1 top, bottom = self.margins # If cursor is outside scrolling margins -- do nothin'. if top <= self.cursor.y <= bottom: # v +1, because range() is exclusive. for line in range(self.cursor.y, min(bottom + 1, self.cursor.y + count)): self.linebuf.pop(bottom) self.linebuf.insert(line, Line(self.columns)) self.update_line_range(self.cursor.y, bottom) self.carriage_return() def delete_lines(self, count=1): """Deletes the indicated # of lines, starting at line with cursor. As lines are deleted, lines displayed below cursor move up. :param int count: number of lines to delete. """ count = count or 1 top, bottom = self.margins # If cursor is outside scrolling margins it -- do nothin'. if top <= self.cursor.y <= bottom: # v -- +1 to include the bottom margin. for _ in range(min(bottom - self.cursor.y + 1, count)): self.linebuf.pop(self.cursor.y) self.linebuf.insert(bottom, Line(self.columns)) self.update_line_range(self.cursor.y, bottom) self.carriage_return() def insert_characters(self, count=1): """Inserts the indicated # of blank characters at the cursor position. The cursor does not move and remains at the beginning of the inserted blank characters. Data on the line is shifted forward. :param int count: number of characters to insert. """ count = count or 1 top, bottom = self.margins y = self.cursor.y if top <= y <= bottom: x = self.cursor.x # TODO: Check what to do if x is on the second char of a wide char # pair. num = min(self.columns - x, count) line = self.linebuf[y] line.right_shift(x, num) line.apply_cursor(self.cursor, x, num, clear_char=True) self.update_cell_range(y, x, self.columns - 1) def delete_characters(self, count=1): """Deletes the indicated # of characters, starting with the character at cursor position. When a character is deleted, all characters to the right of cursor move left. Character attributes move with the characters. :param int count: number of characters to delete. """ count = count or 1 top, bottom = self.margins y = self.cursor.y if top <= y <= bottom: x = self.cursor.x num = min(self.columns - x, count) # TODO: Check if we need to count wide chars as one or two chars # for this control code. Also, what happens if we start deleting # at the second cell of a wide character, or delete only the first # cell of a wide character? line = self.linebuf[y] line.left_shift(x, num) line.apply_cursor(self.cursor, self.columns - num, num, clear_char=True) self.update_cell_range(y, x, self.columns - 1) def erase_characters(self, count=1): """Erases the indicated # of characters, starting with the character at cursor position. Character attributes are set to cursor attributes. The cursor remains in the same position. :param int count: number of characters to erase. .. warning:: Even though *ALL* of the VTXXX manuals state that character attributes **should be reset to defaults**, ``libvte``, ``xterm`` and ``ROTE`` completely ignore this. Same applies to all ``erase_*()`` and ``delete_*()`` methods. """ count = count or 1 x, y = self.cursor.x, self.cursor.y # TODO: Same set of wide character questions as for delete_characters() num = min(self.columns - x, count) self.linebuf[y].apply_cursor(self.cursor, x, num, clear_char=True) self.update_cell_range(y, x, min(x + num, self.columns) - 1) def erase_in_line(self, how=0, private=False): """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. """ s = n = 0 if how == 0: # a) erase from the cursor to the end of line, including # the cursor, s, n = self.cursor.x, self.columns - self.cursor.x elif how == 1: # b) erase from the beginning of the line to the cursor, # including it, s, n = 0, self.cursor.x + 1 elif how == 2: # c) erase the entire line. s, n = 0, self.columns if n - s: # TODO: Same set of questions as for delete_characters() y = self.cursor.y line = self.linebuf[y] c = None if private else self.cursor if private: line.clear_text(s, n) else: line.apply_cursor(c, s, n, clear_char=True) self.update_cell_range(y, s, min(s + n, self.columns) - 1) def erase_in_display(self, how=0, private=False): """Erases display in a specific way. :param int how: defines the way the line should be erased in: * ``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. :param bool private: when ``True`` character attributes are left unchanged """ if how == 0: # a) erase from cursor to the end of the display, including # the cursor, interval = self.cursor.y + 1, self.lines elif how == 1: # b) erase from the beginning of the display to the cursor, # including it, interval = 0, self.cursor.y elif how == 2: # c) erase the whole display. interval = 0, self.lines else: return if interval[1] > interval[0]: for line in range(*interval): if private: self.linebuf[line].clear_text(0, self.columns) else: self.linebuf[line].apply_cursor(self.cursor, 0, self.columns, clear_char=True) self.update_line_range(interval[0], interval[1] - 1) # In case of 0 or 1 we have to erase the line with the cursor also if how != 2: self.erase_in_line(how, private=private) def set_tab_stop(self): """Sets a horizontal tab stop at cursor position.""" self.tabstops.add(self.cursor.x) def clear_tab_stop(self, how=0): """Clears a horizontal tab stop. :param int how: defines a way the tab stop should be cleared: * ``0`` or nothing -- Clears a horizontal tab stop at cursor position. * ``3`` -- Clears all horizontal tab stops. """ if how == 0: # Clears a horizontal tab stop at cursor position, if it's # present, or silently fails if otherwise. self.tabstops.discard(self.cursor.x) elif how == 3: self.tabstops = set() # Clears all horizontal tab stops. def ensure_bounds(self, use_margins=False): """Ensure that current cursor position is within screen bounds. :param bool use_margins: when ``True`` or when :data:`~pyte.modes.DECOM` is set, cursor is bounded by top and and bottom margins, instead of ``[0; lines - 1]``. """ if use_margins or mo.DECOM in self.mode: top, bottom = self.margins else: top, bottom = 0, self.lines - 1 self.cursor.x = max(0, min(self.cursor.x, self.columns - 1)) self.cursor.y = max(top, min(self.cursor.y, bottom)) def cursor_up(self, count=1, do_carriage_return=False, move_direction=-1): """Moves cursor up the indicated # of lines in same column. Cursor stops at top margin. :param int count: number of lines to skip. """ x, y = self.cursor.x, self.cursor.y self.cursor.y += move_direction * (count or 1) self.ensure_bounds(use_margins=True) if do_carriage_return: self.cursor.x = 0 if y != self.cursor.y or x != self.cursor.x: self.notify_cursor_position() def cursor_up1(self, count=1): """Moves cursor up the indicated # of lines to column 1. Cursor stops at bottom margin. :param int count: number of lines to skip. """ self.cursor_up(count, do_carriage_return=True) def cursor_down(self, count=1): """Moves cursor down the indicated # of lines in same column. Cursor stops at bottom margin. :param int count: number of lines to skip. """ self.cursor_up(count, move_direction=1) def cursor_down1(self, count=1): """Moves cursor down the indicated # of lines to column 1. Cursor stops at bottom margin. :param int count: number of lines to skip. """ self.cursor_up(count, do_carriage_return=True, move_direction=1) def cursor_back(self, count=1, move_direction=-1): """Moves cursor left the indicated # of columns. Cursor stops at left margin. :param int count: number of columns to skip. """ x = self.cursor.x self.cursor.x += move_direction * (count or 1) self.ensure_bounds() if x != self.cursor.x: self.notify_cursor_position() def cursor_forward(self, count=1): """Moves cursor right the indicated # of columns. Cursor stops at right margin. :param int count: number of columns to skip. """ self.cursor_back(count, move_direction=1) def cursor_position(self, line=1, column=1): """Set the cursor to a specific `line` and `column`. Cursor is allowed to move out of the scrolling region only when :data:`~pyte.modes.DECOM` is reset, otherwise -- the position doesn't change. :param int line: line number to move the cursor to. :param int column: column number to move the cursor to. """ column = (column or 1) - 1 line = (line or 1) - 1 x, y = self.cursor.x, self.cursor.y # If origin mode (DECOM) is set, line number are relative to # the top scrolling margin. if mo.DECOM in self.mode: line += self.margins.top # Cursor is not allowed to move out of the scrolling region. if not self.margins.top <= line <= self.margins.bottom: return self.cursor.x, self.cursor.y = column, line self.ensure_bounds() if y != self.cursor.y or x != self.cursor.x: self.notify_cursor_position() def cursor_to_column(self, column=1): """Moves cursor to a specific column in the current line. :param int column: column number to move the cursor to. """ x, self.cursor.x = self.cursor.x, (column or 1) - 1 self.ensure_bounds() if x != self.cursor.x: self.notify_cursor_position() def cursor_to_line(self, line=1): """Moves cursor to a specific line in the current column. :param int line: line number to move the cursor to. """ y, self.cursor.y = self.cursor.y, (line or 1) - 1 # If origin mode (DECOM) is set, line number are relative to # the top scrolling margin. if mo.DECOM in self.mode: self.cursor.y += self.margins.top # TODO: should we also restrict the cursor to the scrolling # region? self.ensure_bounds() if y != self.cursor.y: self.notify_cursor_position() def bell(self, *args): """ Audbile bell """ try: with open('/dev/tty', 'wb') as f: f.write(b'\x07') except EnvironmentError: pass def alignment_display(self): """Fills screen with uppercase E's for screen focus and alignment.""" for i in range(self.lines): self.linebuf[i].clear_text(0, self.columns, 'E') def select_graphic_rendition(self, *attrs): """Set display attributes. :param list attrs: a list of display attributes to set. """ attrs = list(reversed(attrs or (0,))) c = self.cursor while attrs: attr = attrs.pop() if attr in g.FG_ANSI: c.fg = (attr << 8) | 1 elif attr in g.BG_ANSI: c.bg = (attr << 8) | 1 elif attr in g.DISPLAY: attr, val = g.DISPLAY[attr] setattr(c, attr, val) elif not attr: c.reset_display_attrs() elif attr in g.FG_AIXTERM: c.fg = (attr << 8) | 1 elif attr in g.BG_AIXTERM: c.bg = (attr << 8) | 1 elif attr in (g.FG_256, g.BG_256): key = "fg" if attr == g.FG_256 else "bg" n = attrs.pop() try: if n == 5: # 256. setattr(c, key, (attrs.pop() << 8) | 2) elif n == 2: # 24bit. # This is somewhat non-standard but is nonetheless # supported in quite a few terminals. See discussion # here https://gist.github.com/XVilka/8346728. r, gr, b = attrs.pop() << 8, attrs.pop() << 16, attrs.pop() << 24 setattr(c, key, r | gr | b | 3) except IndexError: pass def report_device_attributes(self, mode=0, **kwargs): """Reports terminal identity. .. versionadded:: 0.5.0 """ # Use the same responses as libvte v0.46 running in termite # Ignore mode since vte seems to ignore it if False and kwargs.get('secondary') == '>': # http://www.vt100.net/docs/vt510-rm/DA2.html # If you implement xterm keycode querying # http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Device-Control-functions # you can enable this. self.write_to_child(b'\x1b[>1;4600;0c') else: # xterm gives: [[?64;1;2;6;9;15;18;21;22c # use the simpler vte response, since we dont support # windowing/horizontal scrolling etc. # [[?64;1;2;6;9;15;18;21;22c self.write_to_child(b"\x1b[?62c") def report_device_status(self, mode): """Reports terminal status or cursor position. :param int mode: if 5 -- terminal status, 6 -- cursor position, otherwise a noop. .. versionadded:: 0.5.0 """ if mode == 5: # Request for terminal status. self.write_to_child(b"\x1b[0n") elif mode == 6: # Request for cursor position. x, y = wrap_cursor_position(self.cursor.x, self.cursor.y, self.lines, self.columns) x, y = x + 1, y + 1 # "Origin mode (DECOM) selects line numbering." if mo.DECOM in self.mode: y -= self.margins.top self.write_to_child("\x1b[{0};{1}R".format(y, x).encode('ascii')) def set_cursor_shape(self, mode, secondary=None): if secondary == ' ': shape = blink = None if mode > 0: blink = bool(mode % 2) shape = 'block' if mode < 3 else 'underline' if mode < 5 else 'beam' if mode < 7 else None if shape != self.cursor.shape or blink != self.cursor.blink: self.cursor.shape, self.cursor.blink = shape, blink self.cursor_changed(self.cursor) elif secondary == '"': # DECSCA pass else: # DECLL pass def set_dynamic_color(self, base, color_names=None): # See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands try: color_names = color_names.decode('utf-8') if color_names else '' except Exception: return def handle_val(val, param=None): val %= 100 if val == 10: # foreground self.change_default_color('fg', param) elif val == 11: # background self.change_default_color('bg', param) elif val == 12: # cursor color old, self.cursor.color = self.cursor.color, param if old != self.cursor.color: self.cursor_changed(self.cursor) if color_names: for i, cn in enumerate(filter(None, color_names.split(';'))): handle_val(base + i, cn) else: handle_val(base) def normal_keypad_mode(self): pass # Useless for us, since Qt takes care of handling the numpad def alternate_keypad_mode(self): pass # Useless for us, since Qt takes care of handling the numpad def debug(self, *args, **kwargs): """Endpoint for unrecognized escape sequences. By default is a noop. """ import traceback traceback.print_stack() print('unknown escape code:', args, kwargs)