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 and bottom is None: return margins = self.margins or Margins(0, self.lines - 1) top = margins.top if top is None else top - 1 bottom = margins.bottom if bottom is None else bottom - 1 # 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, self.lines - 1)) bottom = max(0, min(bottom, 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 resize(self, lines=None, columns=None): """Resize the screen to the given dimensions keeping the history intact. If the requested screen size has more lines than the existing screen, lines will be added at the bottom. If the requested size has less lines than the existing screen, lines will be clipped at the top of the screen. Similarly, if the existing screen has less columns than the requested screen, columns will be added at the right, and if it has more -- columns will be clipped at the right. .. note:: According to `xterm`, we should also reset origin mode and screen margins, see ``xterm/screen.c:1761``. -> but we don't do this here :param int lines: number of lines in the new screen. :param int columns: number of columns in the new screen. """ self._flush_events() old_lines = self.lines self.lines = (lines or self.lines) self.columns = (columns or self.columns) if mo.DECALTBUF in self.mode and False: # home cursor self.reset_mode(mo.DECOM) else: # cursor: make sure that it 'stays' on its current line cursor_delta = self.linecontainer.resize(old_lines, self.lines, self.columns) self.cursor.y += cursor_delta self.cursor.x = min(max(self.cursor.x, 0), self.columns - 1) self.margins = Margins(0, self.lines - 1)
def delete_lines(self, count=None): """Deletes the indicated # of lines, starting at line with cursor. As lines are deleted, lines displayed below cursor move up. Lines added to bottom of screen have spaces with same character attributes as last line moved up. :param int count: number of lines to delete. """ count = count or 1 top, bottom = self.margins or Margins(0, self.lines - 1) line_offset = self.line_offset pt_cursor_position = self.pt_cursor_position # If cursor is outside scrolling margins it -- do nothin'. if top <= pt_cursor_position.y - line_offset <= bottom: data_buffer = self.data_buffer # Iterate from the cursor Y position until the end of the visible input. for line in range(pt_cursor_position.y - line_offset, bottom + 1): # When 'x' lines further are out of the margins, replace by an empty line, # Otherwise copy the line from there. if line + count > bottom: data_buffer.pop(line + line_offset, None) else: data_buffer[line + line_offset] = self.data_buffer[line + count + line_offset]
def insert_lines(self, count=None): """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 or Margins(0, self.lines - 1) data_buffer = self.data_buffer line_offset = self.line_offset pt_cursor_position = self.pt_cursor_position # If cursor is outside scrolling margins it -- do nothing. if top <= pt_cursor_position.y - self.line_offset <= bottom: for line in range(bottom, pt_cursor_position.y - line_offset, -1): if line - count < top: data_buffer.pop(line + line_offset, None) else: data_buffer[line + line_offset] = data_buffer[line + line_offset - count] data_buffer.pop(line + line_offset - count, None) self.carriage_return()
def scroll_down(self, n): top, bottom = self.margins or Margins(0, self.lines - 1) for y in reversed(range(top, bottom + 1)): if y - n < top: self.buffer[y].clear() else: self.buffer[y] = copy(self.buffer[y - n]) self.dirty.update(range(self.lines))
def scroll_up(self, n): top, bottom = self.margins or Margins(0, self.lines - 1) for y in range(top, bottom + 1): if y + n > bottom: self.buffer[y].clear() else: self.buffer[y] = copy(self.buffer[y + n]) self.dirty.update(range(self.lines))
def index(self): top, bottom = self.margins or Margins(0, self.lines - 1) if self.cursor.y == bottom: # TODO: mark only the lines within margins? self.dirty.update(range(self.lines)) for y in range(top, bottom): self.buffer[y] = self.buffer[y + 1] self.buffer.pop(bottom, None) else: self.cursor_down()
def reverse_index(self): max_line = self.line_buffer.max_line min_line = self.line_buffer.min_line top, bottom = self.margins or Margins(min_line, max_line) if self.cursor.y == top: self.dirty.update(range(min_line, max_line + 1)) for y in range(bottom, top, -1): self.buffer[y] = self.buffer[y - 1] self.buffer.pop(top, None) else: self.cursor_up()
def reverse_index(self): margins = self.margins or Margins(0, self.lines - 1) top, bottom = margins line_offset = self.line_offset # When scrolling over the full screen -> keep history. if self.pt_cursor_position.y - line_offset == top: for i in range(bottom - 1, top - 1, -1): self.data_buffer[i + line_offset + 1] = self.data_buffer[i + line_offset] del self.data_buffer[i + line_offset] else: self.cursor_up()
def _reset_offset_and_margins(self): """ Recalculate offset and move cursor (make sure that the bottom is visible.) """ self.margins = Margins(0, self.lines - 1) if self._in_alternate_screen: self.line_offset = 0 elif self.data_buffer: self.line_offset = max(0, self.max_y - self.lines + 1)
def cursor_down(self, count=None): """Moves cursor down the indicated # of lines in same column. Cursor stops at bottom margin. :param int count: number of lines to skip. """ cursor_position = self.pt_cursor_position margins = self.margins or Margins(0, self.lines - 1) # Ensure bounds. # (Following code is faster than calling `self.ensure_bounds`.) _, bottom = margins cursor_position.y = min(cursor_position.y + (count or 1), bottom + self.line_offset + 1) self.max_y = max(self.max_y, cursor_position.y)
def _reset_screen(self): """ Reset the Screen content. (also called when switching from/to alternate buffer. """ self.pt_screen = Screen(default_char=Char(' ', DEFAULT_TOKEN)) self.pt_screen.cursor_position = CursorPosition(0, 0) self.pt_screen.show_cursor = True self.data_buffer = self.pt_screen.data_buffer self._attrs = Attrs(color=None, bgcolor=None, bold=False, underline=False, italic=False, blink=False, reverse=False) self.margins = Margins(0, self.lines - 1) self.line_offset = 0 # Index of the line that's currently displayed on top. self.max_y = 0 # Max 'y' position to which is written.
def reset(self): """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. """ self._flush_events() self.linecontainer.reset(self.lines) if self.iframe_mode: self.iframe_leave() self.mode = set([mo.DECAWM, mo.DECTCEM, mo.LNM, mo.DECTCEM]) self.margins = Margins(0, self.lines - 1) # According to VT220 manual and ``linux/drivers/tty/vt.c`` # the default G0 charset is latin-1, but for reasons unknown # latin-1 breaks ascii-graphics; so G0 defaults to cp437. self.charset = 0 self.g0_charset = cs.IBMPC_MAP self.g1_charset = cs.VT100_MAP # 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.cursor = Cursor(0, 0) self.cursor_position()