kitty/kitty/screen.py
2016-11-04 14:44:46 +05:30

1039 lines
38 KiB
Python

#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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 \
<http://www.ecma-international.org/publications/standards/Ecma-035.htm>`_
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)