def test_erase_in_line(): screen = update(pyte.Screen(5, 5), ["sam i", "s foo", "but a", "re yo", "u? "], colored=[0]) screen.cursor_position(1, 3) # a) erase from cursor to the end of line screen.erase_in_line(0) assert (screen.cursor.y, screen.cursor.x) == (0, 2) assert screen.display == ["sa ", "s foo", "but a", "re yo", "u? "] assert screen.buffer[0] == [ Char("s", fg="red"), Char("a", fg="red"), screen.default_char, screen.default_char, screen.default_char ] # b) erase from the beginning of the line to the cursor screen = update(screen, ["sam i", "s foo", "but a", "re yo", "u? "], colored=[0]) screen.erase_in_line(1) assert (screen.cursor.y, screen.cursor.x) == (0, 2) assert screen.display == [" i", "s foo", "but a", "re yo", "u? "] assert screen.buffer[0] == [ screen.default_char, screen.default_char, screen.default_char, Char(" ", fg="red"), Char("i", fg="red") ] # c) erase the entire line screen = update(screen, ["sam i", "s foo", "but a", "re yo", "u? "], colored=[0]) screen.erase_in_line(2) assert (screen.cursor.y, screen.cursor.x) == (0, 2) assert screen.display == [" ", "s foo", "but a", "re yo", "u? "] assert screen.buffer[0] == [screen.default_char] * 5
def test_draw_width2_irm(): screen = pyte.Screen(2, 1) screen.draw("コ") assert screen.display == ["コ"] assert tolist(screen) == [[Char("コ"), Char(" ")]] # Overwrite the stub part of a width 2 character. screen.set_mode(mo.IRM) screen.cursor_to_column(screen.columns) screen.draw("x") assert screen.display == [" x"]
def test_insert_characters(): screen = update(Screen(3, 3), ["sam", "is ", "foo"], colored=[0]) # a) normal case cursor = copy.copy(screen.cursor) screen.insert_characters(2) assert (screen.cursor.y, screen.cursor.x) == (cursor.y, cursor.x) assert screen[0] == [ screen.default_char, screen.default_char, Char("s", fg="red") ] # b) now inserting from the middle of the line screen.cursor.y, screen.cursor.x = 2, 1 screen.insert_characters(1) assert screen[2] == [Char("f"), screen.default_char, Char("o")] # c) inserting more than we have screen.insert_characters(10) assert screen[2] == [Char("f"), screen.default_char, screen.default_char] # d) 0 is 1 screen = update(Screen(3, 3), ["sam", "is ", "foo"], colored=[0]) screen.cursor_position() screen.insert_characters() assert screen[0] == [screen.default_char, Char("s", fg="red"), Char("a", fg="red")] screen = update(Screen(3, 3), ["sam", "is ", "foo"], colored=[0]) screen.cursor_position() screen.insert_characters(1) assert screen[0] == [screen.default_char, Char("s", fg="red"), Char("a", fg="red")]
def test_blink(): screen = pyte.Screen(2, 2) assert tolist(screen) == [[screen.default_char, screen.default_char]] * 2 screen.select_graphic_rendition(5) # blink. screen.draw("f") assert tolist(screen) == [[ Char("f", "default", "default", blink=True), screen.default_char ], [screen.default_char, screen.default_char]]
def test_erase_character(): screen = update(pyte.Screen(3, 3), ["sam", "is ", "foo"], colored=[0]) screen.erase_characters(2) assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == [" m", "is ", "foo"] assert tolist(screen)[0] == [ screen.default_char, screen.default_char, Char("m", fg="red") ] screen.cursor.y, screen.cursor.x = 2, 2 screen.erase_characters() assert (screen.cursor.y, screen.cursor.x) == (2, 2) assert screen.display == [" m", "is ", "fo "] screen.cursor.y, screen.cursor.x = 1, 1 screen.erase_characters(0) assert (screen.cursor.y, screen.cursor.x) == (1, 1) assert screen.display == [" m", "i ", "fo "] # ! extreme cases. screen = update(pyte.Screen(5, 1), ["12345"], colored=[0]) screen.cursor.x = 1 screen.erase_characters(3) assert (screen.cursor.y, screen.cursor.x) == (0, 1) assert screen.display == ["1 5"] assert tolist(screen)[0] == [ Char("1", fg="red"), screen.default_char, screen.default_char, screen.default_char, Char("5", "red") ] screen = update(pyte.Screen(5, 1), ["12345"], colored=[0]) screen.cursor.x = 2 screen.erase_characters(10) assert (screen.cursor.y, screen.cursor.x) == (0, 2) assert screen.display == ["12 "] assert tolist(screen)[0] == [ Char("1", fg="red"), Char("2", fg="red"), screen.default_char, screen.default_char, screen.default_char ] screen = update(pyte.Screen(5, 1), ["12345"], colored=[0]) screen.erase_characters(4) assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == [" 5"] assert tolist(screen)[0] == [ screen.default_char, screen.default_char, screen.default_char, screen.default_char, Char("5", fg="red") ]
def update(screen, lines, colored=[]): """Updates a given screen object with given lines, colors each line from ``colored`` in "red" and returns the modified screen. """ for y, line in enumerate(lines): for x, char in enumerate(line): if y in colored: attrs = {"fg": "red"} else: attrs = {} screen.buffer[y][x] = Char(data=char, **attrs) return screen
def test_attributes(): screen = pyte.Screen(2, 2) assert screen.buffer == [[screen.default_char, screen.default_char]] * 2 screen.select_graphic_rendition(1) # bold. # Still default, since we haven't written anything. assert screen.buffer == [[screen.default_char, screen.default_char]] * 2 assert screen.cursor.attrs.bold screen.draw("f") assert screen.buffer == [[ Char("f", "default", "default", bold=True), screen.default_char ], [screen.default_char, screen.default_char]]
def test_erase_in_display(): screen = update(pyte.Screen(5, 5), ["sam i", "s foo", "but a", "re yo", "u? "], colored=[2, 3]) screen.cursor_position(3, 3) # a) erase from cursor to the end of the display, including # the cursor screen.erase_in_display(0) assert (screen.cursor.y, screen.cursor.x) == (2, 2) assert screen.display == ["sam i", "s foo", "bu ", " ", " "] assert screen.buffer[2:] == [[ Char("b", fg="red"), Char("u", fg="red"), screen.default_char, screen.default_char, screen.default_char ], [screen.default_char] * 5, [screen.default_char] * 5] # b) erase from the beginning of the display to the cursor, # including it screen = update(screen, ["sam i", "s foo", "but a", "re yo", "u? "], colored=[2, 3]) screen.erase_in_display(1) assert (screen.cursor.y, screen.cursor.x) == (2, 2) assert screen.display == [" ", " ", " a", "re yo", "u? "] assert screen.buffer[:3] == [ [screen.default_char] * 5, [screen.default_char] * 5, [ screen.default_char, screen.default_char, screen.default_char, Char(" ", fg="red"), Char("a", fg="red") ], ] # c) erase the while display screen.erase_in_display(2) assert (screen.cursor.y, screen.cursor.x) == (2, 2) assert screen.display == [" ", " ", " ", " ", " "] assert screen.buffer == [[screen.default_char] * 5] * 5
def test_save_cursor(): # a) cursor position screen = pyte.Screen(10, 10) screen.save_cursor() screen.cursor.x, screen.cursor.y = 3, 5 screen.save_cursor() screen.cursor.x, screen.cursor.y = 4, 4 screen.restore_cursor() assert screen.cursor.x == 3 assert screen.cursor.y == 5 screen.restore_cursor() assert screen.cursor.x == 0 assert screen.cursor.y == 0 # b) modes screen = pyte.Screen(10, 10) screen.set_mode(mo.DECAWM, mo.DECOM) screen.save_cursor() screen.reset_mode(mo.DECAWM) screen.restore_cursor() assert mo.DECAWM in screen.mode assert mo.DECOM in screen.mode # c) attributes screen = pyte.Screen(10, 10) screen.select_graphic_rendition(4) screen.save_cursor() screen.select_graphic_rendition(24) assert screen.cursor.attrs == screen.default_char screen.restore_cursor() assert screen.cursor.attrs != screen.default_char assert screen.cursor.attrs == Char(" ", underscore=True)
def test_attributes_reset(): screen = pyte.Screen(2, 2) screen.set_mode(mo.LNM) assert tolist(screen) == [[screen.default_char, screen.default_char]] * 2 screen.select_graphic_rendition(1) screen.draw("f") screen.draw("o") screen.draw("o") assert tolist(screen) == [ [Char("f", bold=True), Char("o", bold=True)], [Char("o", bold=True), screen.default_char], ] screen.cursor_position() screen.select_graphic_rendition(0) # Reset screen.draw("f") assert tolist(screen) == [ [Char("f"), Char("o", bold=True)], [Char("o", bold=True), screen.default_char], ]
def test_reverse_index(): screen = update(pyte.Screen(2, 2), ["wo", "ot"], colored=[0]) # a) reverse indexing on the first row should push rows down # and create a new row at the top. screen.reverse_index() assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert tolist(screen) == [ [screen.default_char, screen.default_char], [Char("w", fg="red"), Char("o", fg="red")] ] # b) once again ... screen.reverse_index() assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert tolist(screen) == [ [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], ] # c) same with margins screen = update(pyte.Screen(2, 5), ["bo", "sh", "th", "er", "oh"], colored=[2, 3]) screen.set_margins(2, 4) screen.cursor.y = 1 # ... go! screen.reverse_index() assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["bo", " ", "sh", "th", "oh"] assert tolist(screen) == [ [Char("b"), Char("o")], [screen.default_char, screen.default_char], [Char("s"), Char("h")], [Char("t", fg="red"), Char("h", fg="red")], [Char("o"), Char("h")], ] # ... and again ... screen.reverse_index() assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["bo", " ", " ", "sh", "oh"] assert tolist(screen) == [ [Char("b"), Char("o")], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [Char("s"), Char("h")], [Char("o"), Char("h")], ] # ... and again ... screen.reverse_index() assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["bo", " ", " ", " ", "oh"] assert tolist(screen) == [ [Char("b"), Char("o")], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [Char("o"), Char("h")], ] # look, nothing changes! screen.reverse_index() assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["bo", " ", " ", " ", "oh"] assert tolist(screen) == [ [Char("b"), Char("o")], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [Char("o"), Char("h")], ]
def test_index(): screen = update(pyte.Screen(2, 2), ["wo", "ot"], colored=[1]) # a) indexing on a row that isn't the last should just move # the cursor down. screen.index() assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert tolist(screen) == [ [Char("w"), Char("o")], [Char("o", fg="red"), Char("t", fg="red")] ] # b) indexing on the last row should push everything up and # create a new row at the bottom. screen.index() assert screen.cursor.y == 1 assert tolist(screen) == [ [Char("o", fg="red"), Char("t", fg="red")], [screen.default_char, screen.default_char] ] # c) same with margins screen = update(pyte.Screen(2, 5), ["bo", "sh", "th", "er", "oh"], colored=[1, 2]) screen.set_margins(2, 4) screen.cursor.y = 3 # ... go! screen.index() assert (screen.cursor.y, screen.cursor.x) == (3, 0) assert screen.display == ["bo", "th", "er", " ", "oh"] assert tolist(screen) == [ [Char("b"), Char("o", "default")], [Char("t", "red"), Char("h", "red")], [Char("e"), Char("r")], [screen.default_char, screen.default_char], [Char("o"), Char("h")], ] # ... and again ... screen.index() assert (screen.cursor.y, screen.cursor.x) == (3, 0) assert screen.display == ["bo", "er", " ", " ", "oh"] assert tolist(screen) == [ [Char("b"), Char("o")], [Char("e"), Char("r")], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [Char("o"), Char("h")], ] # ... and again ... screen.index() assert (screen.cursor.y, screen.cursor.x) == (3, 0) assert screen.display == ["bo", " ", " ", " ", "oh"] assert tolist(screen) == [ [Char("b"), Char("o")], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [Char("o"), Char("h")], ] # look, nothing changes! screen.index() assert (screen.cursor.y, screen.cursor.x) == (3, 0) assert screen.display == ["bo", " ", " ", " ", "oh"] assert tolist(screen) == [ [Char("b"), Char("o")], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [screen.default_char, screen.default_char], [Char("o"), Char("h")], ]
def default_char(self): return Char(data=" ", fg=0)
def test_initialize_char(): # Make sure that char can be correctly initialized with keyword # arguments. See #24 on GitHub for details. for field in Char._fields[1:]: char = Char(field[0], **{field: True}) assert getattr(char, field)
def test_erase_in_display(): screen = update(pyte.Screen(5, 5), ["sam i", "s foo", "but a", "re yo", "u? "], colored=[2, 3]) screen.cursor_position(3, 3) # a) erase from cursor to the end of the display, including # the cursor screen.erase_in_display(0) assert (screen.cursor.y, screen.cursor.x) == (2, 2) assert screen.display == ["sam i", "s foo", "bu ", " ", " "] assert tolist(screen)[2:] == [ [Char("b", fg="red"), Char("u", fg="red"), screen.default_char, screen.default_char, screen.default_char], [screen.default_char] * 5, [screen.default_char] * 5 ] # b) erase from the beginning of the display to the cursor, # including it screen = update(screen, ["sam i", "s foo", "but a", "re yo", "u? "], colored=[2, 3]) screen.erase_in_display(1) assert (screen.cursor.y, screen.cursor.x) == (2, 2) assert screen.display == [" ", " ", " a", "re yo", "u? "] assert tolist(screen)[:3] == [ [screen.default_char] * 5, [screen.default_char] * 5, [screen.default_char, screen.default_char, screen.default_char, Char(" ", fg="red"), Char("a", fg="red")], ] # c) erase the while display screen.erase_in_display(2) assert (screen.cursor.y, screen.cursor.x) == (2, 2) assert screen.display == [" ", " ", " ", " ", " "] assert tolist(screen) == [[screen.default_char] * 5] * 5 # d) erase with private mode screen = update(pyte.Screen(5, 5), ["sam i", "s foo", "but a", "re yo", "u? "], colored=[2, 3]) screen.erase_in_display(3, private=True) assert screen.display == [" ", " ", " ", " ", " "] # e) erase with extra args screen = update(pyte.Screen(5, 5), ["sam i", "s foo", "but a", "re yo", "u? "], colored=[2, 3]) args = [3, 0] screen.erase_in_display(*args) assert screen.display == [" ", " ", " ", " ", " "] # f) erase with extra args and private screen = update(pyte.Screen(5, 5), ["sam i", "s foo", "but a", "re yo", "u? "], colored=[2, 3]) screen.erase_in_display(*args, private=True) assert screen.display == [" ", " ", " ", " ", " "]
from pyte import graphics as g from pyte.screens import Char, wcwidth, Margins from multiplex.ansi import CSI TERMINATE = "m" UNDEFINED = object() RESET_TEXT_ATTRS = set(list(range(1, 10))) BOLD = 1 empty_meta = Char( None, fg=None, bg=None, bold=(), italics=None, underscore=None, strikethrough=None, reverse=None, ) reset = f"{CSI}0{TERMINATE}" index_to_char_meta = {0: empty_meta} char_meta_to_index = {empty_meta: 0} index_to_ansi = {0: reset} counter = 0 class Screen(pyte.Screen): def __init__(self, columns, lines, line_buffer):
def test_insert_lines(): # a) without margins screen = update(Screen(3, 3), ["sam", "is ", "foo"], colored=[1]) screen.insert_lines() assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == [" ", "sam", "is "] assert screen == [ [screen.default_char] * 3, [Char("s"), Char("a"), Char("m")], [Char("i", fg="red"), Char("s", fg="red"), Char(" ", fg="red")], ] screen = update(Screen(3, 3), ["sam", "is ", "foo"], colored=[1]) screen.insert_lines(2) assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == [" ", " ", "sam"] assert screen == [ [screen.default_char] * 3, [screen.default_char] * 3, [Char("s"), Char("a"), Char("m")] ] # b) with margins screen = update(Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], colored=[2, 3]) screen.set_margins(1, 4) screen.cursor.y = 1 screen.insert_lines(1) assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["sam", " ", "is ", "foo", "baz"] assert screen == [ [Char("s"), Char("a"), Char("m")], [screen.default_char] * 3, [Char("i"), Char("s"), Char(" ")], [Char("f", fg="red"), Char("o", fg="red"), Char("o", fg="red")], [Char("b"), Char("a"), Char("z")], ] screen = update(Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], colored=[2, 3]) screen.set_margins(1, 3) screen.cursor.y = 1 screen.insert_lines(1) assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["sam", " ", "is ", "bar", "baz"] assert screen == [ [Char("s"), Char("a"), Char("m")], [screen.default_char] * 3, [Char("i"), Char("s"), Char(" ")], [Char("b", fg="red"), Char("a", fg="red"), Char("r", fg="red")], [Char("b"), Char("a"), Char("z")], ] screen.insert_lines(2) assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["sam", " ", " ", "bar", "baz"] assert screen == [ [Char("s"), Char("a"), Char("m")], [screen.default_char] * 3, [screen.default_char] * 3, [Char("b", fg="red"), Char("a", fg="red"), Char("r", fg="red")], [Char("b"), Char("a"), Char("z")], ] # c) with margins -- trying to insert more than we have available screen = update(Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], colored=[2, 3]) screen.set_margins(2, 4) screen.cursor.y = 1 screen.insert_lines(20) assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["sam", " ", " ", " ", "baz"] assert screen == [ [Char("s"), Char("a"), Char("m")], [screen.default_char] * 3, [screen.default_char] * 3, [screen.default_char] * 3, [Char("b"), Char("a"), Char("z")], ] # d) with margins -- trying to insert outside scroll boundaries; # expecting nothing to change screen = update(Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], colored=[2, 3]) screen.set_margins(2, 4) screen.insert_lines(5) assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == ["sam", "is ", "foo", "bar", "baz"] assert screen == [ [Char("s"), Char("a"), Char("m")], [Char("i"), Char("s"), Char(" ")], [Char("f", fg="red"), Char("o", fg="red"), Char("o", fg="red")], [Char("b", fg="red"), Char("a", fg="red"), Char("r", fg="red")], [Char("b"), Char("a"), Char("z")], ]
def select_graphic_rendition(self, *attrs): if not attrs or attrs == (0,): self.cursor.attrs = self.default_char return fg = UNDEFINED bg = UNDEFINED added_text_attrs = set() removed_text_attrs = set() attrs = list(reversed(attrs)) while attrs: attr = attrs.pop() if attr == 0: fg = None bg = None removed_text_attrs = RESET_TEXT_ATTRS elif attr in g.FG_ANSI: fg = (attr,) elif attr in g.BG: bg = (attr,) elif attr in g.FG_AIXTERM: fg = (attr,) added_text_attrs.add(BOLD) elif attr in g.BG_AIXTERM: bg = (attr,) added_text_attrs.add(BOLD) elif attr in (g.FG_256, g.BG_256): n = attrs.pop() if n == 5: value = attr, n, attrs.pop() if attr == g.FG_256: fg = value else: bg = value elif n == 2: value = attr, n, attrs.pop(), attrs.pop(), attrs.pop() if attr == g.FG_256: fg = value else: bg = value elif 1 <= attr <= 9: added_text_attrs.add(attr) elif 21 <= attr <= 29: removed_text_attrs.add(attr) current_meta = index_to_char_meta[self.cursor.attrs.fg] current_text_attrs = set(current_meta.bold) new_text_attrs = (current_text_attrs | added_text_attrs) - removed_text_attrs replace = {} if fg is not UNDEFINED: replace["fg"] = fg if bg is not UNDEFINED: replace["bg"] = bg replace["bold"] = tuple(sorted(new_text_attrs)) new_char_meta = current_meta._replace(**replace) if new_char_meta in char_meta_to_index: index = char_meta_to_index[new_char_meta] else: global counter counter += 1 index = counter char_meta_to_index[new_char_meta] = index index_to_char_meta[index] = new_char_meta codes = [] c = new_char_meta if c.fg: codes.extend(c.fg) if c.bg: codes.extend(c.bg) if c.bold: codes.extend(list(c.bold)) ansi = f'{CSI}{";".join(str(c) for c in codes)}{TERMINATE}' index_to_ansi[index] = ansi self.cursor.attrs = Char(" ", fg=index)
def test_delete_lines(): # a) without margins screen = update(pyte.Screen(3, 3), ["sam", "is ", "foo"], colored=[1]) screen.delete_lines() assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == ["is ", "foo", " "] assert tolist(screen) == [ [Char("i", fg="red"), Char("s", fg="red"), Char(" ", fg="red")], [Char("f"), Char("o"), Char("o")], [screen.default_char] * 3, ] screen.delete_lines(0) assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == ["foo", " ", " "] assert tolist(screen) == [ [Char("f"), Char("o"), Char("o")], [screen.default_char] * 3, [screen.default_char] * 3, ] # b) with margins screen = update(pyte.Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], colored=[2, 3]) screen.set_margins(1, 4) screen.cursor.y = 1 screen.delete_lines(1) assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["sam", "foo", "bar", " ", "baz"] assert tolist(screen) == [ [Char("s"), Char("a"), Char("m")], [Char("f", fg="red"), Char("o", fg="red"), Char("o", fg="red")], [Char("b", fg="red"), Char("a", fg="red"), Char("r", fg="red")], [screen.default_char] * 3, [Char("b"), Char("a"), Char("z")], ] screen = update(pyte.Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], colored=[2, 3]) screen.set_margins(1, 4) screen.cursor.y = 1 screen.delete_lines(2) assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["sam", "bar", " ", " ", "baz"] assert tolist(screen) == [ [Char("s"), Char("a"), Char("m")], [Char("b", fg="red"), Char("a", fg="red"), Char("r", fg="red")], [screen.default_char] * 3, [screen.default_char] * 3, [Char("b"), Char("a"), Char("z")], ] # c) with margins -- trying to delete more than we have available screen = update(pyte.Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], [None, None, [("red", "default")] * 3, [("red", "default")] * 3, None]) screen.set_margins(1, 4) screen.cursor.y = 1 screen.delete_lines(5) assert (screen.cursor.y, screen.cursor.x) == (1, 0) assert screen.display == ["sam", " ", " ", " ", "baz"] assert tolist(screen) == [ [Char("s"), Char("a"), Char("m")], [screen.default_char] * 3, [screen.default_char] * 3, [screen.default_char] * 3, [Char("b"), Char("a"), Char("z")], ] # d) with margins -- trying to delete outside scroll boundaries; # expecting nothing to change screen = update(pyte.Screen(3, 5), ["sam", "is ", "foo", "bar", "baz"], colored=[2, 3]) screen.set_margins(2, 4) screen.cursor.y = 0 screen.delete_lines(5) assert (screen.cursor.y, screen.cursor.x) == (0, 0) assert screen.display == ["sam", "is ", "foo", "bar", "baz"] assert tolist(screen) == [ [Char("s"), Char("a"), Char("m")], [Char("i"), Char("s"), Char(" ")], [Char("f", fg="red"), Char("o", fg="red"), Char("o", fg="red")], [Char("b", fg="red"), Char("a", fg="red"), Char("r", fg="red")], [Char("b"), Char("a"), Char("z")], ]
class TermScreen(pyte.Screen): def __init__(self, columns, lines): self.savepoints = [] # terminal dimensions in characters self.lines, self.columns = lines, columns self.linecontainer = browserscreen.BrowserScreen() # list of drawing events for the cljs screen self.events = [] # current iframe_mode, # one of None, 'open' or 'closed' # None .. no active iframe # 'open' .. active iframe which is still being sent data to # 'closed' .. active iframe where the initial data has already been sent self.iframe_mode = None self.iframe_id = None self.reset() self.events = [] def _flush_events(self): self.events.extend(self.linecontainer.pop_events()) def pop_events(self): self.linecontainer.cursor(self.cursor.y, self.cursor.x) self.linecontainer.check_scrollback() if self.events: self.events.extend(self.linecontainer.pop_events()) ev = self.events self.events = [] return ev else: return self.linecontainer.pop_events() # pyte.Screen implementation def __before__(self, command): pass def __after__(self, command): pass _default_char = Char(data=" ", fg="default", bg="default") def _create_line(self, default_char=None): return Line(self.columns, default_char or self._default_char) def _is_empty_line(self, line): return line.is_empty() 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() 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 set_mode(self, *modes, **kwargs): """Sets (enables) a given list of modes. :param list modes: modes to set, where each mode is a constant from :mod:`pyte.modes`. """ mode_id = (modes[:1] or [None])[0] if mode_id in (IFRAME_DOCUMENT_MODE_ID, IFRAME_RESPONSE_MODE_ID): cookie = ';'.join(map(str, modes[1:])) # Need the cookie to prove that the request comes from a # real program, an not just by 'cat'-ing some file. # Javascript in iframes will only be activated with a # valid cookie. self.iframe_set_mode(mode_id, cookie) return # Private mode codes are shifted, to be distingiushed from non # private ones. if kwargs.get("private"): modes = set([mode << mo.PRIVATE_MODE_SHIFT for mode in modes]) # translate mode shortcuts and aliases if mo.DECAPPMODE in modes: # DECAPP is a combination of DECALTBUF and DECSAVECUR and # additionally erase the alternative buffer modes.update([mo.DECALTBUF, mo.DECSAVECUR]) if mo.DECALTBUF_ALT in modes: modes.remove(mo.DECALTBUF_ALT) modes.add(mo.DECALTBUF) self.mode.update(modes) if mo.DECAPPKEYS in modes: # use application mode keys, see termkey.py (app_key_mode arg) pass # 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) 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: # todo: check that iter(self.linecontainer) lazily creates and returns all lines linecontainer.reverse_all_lines() self.select_graphic_rendition(g._SGR["+reverse"]) # Make the cursor visible. if mo.DECTCEM in modes: self.cursor.hidden = False if mo.DECSAVECUR in modes: # save cursor position and restore it on mode reset self.save_cursor() if mo.DECAPPMODE in modes: self.cursor_position() if mo.DECALTBUF in modes: # enable alternative draw buffer self._flush_events() self.linecontainer.enter_altbuf_mode() # if mo.DECAPPMODE in modes: # self.erase_in_display(2) def reset_mode(self, *modes, **kwargs): """Resets (disables) a given list of modes. :param list modes: modes to reset -- hopefully, each mode is a constant from :mod:`pyte.modes`. """ mode_id = (modes[:1] or [None])[0] if mode_id in (IFRAME_DOCUMENT_MODE_ID, IFRAME_RESPONSE_MODE_ID) \ and kwargs.get('private'): cookie = ';'.join(map(str, modes[1:])) # Need the cookie to prove that the request comes from a # real program, an not just by 'cat'-ing some file. # Javascript in iframes will only be activated with a # valid cookie. self.iframe_reset_mode(mode_id, cookie) return # Private mode codes aree shifted, to be distingiushed from non # private ones. if kwargs.get("private"): modes = set([mode << mo.PRIVATE_MODE_SHIFT for mode in modes]) # translate mode shortcuts and aliases if mo.DECAPPMODE in modes: # DECAPP is a combination of DECALTBUF and DECSAVECUR modes.remove(mo.DECAPPMODE) modes.update([mo.DECALTBUF, mo.DECSAVECUR]) if mo.DECALTBUF_ALT in modes: modes.remove(mo.DECALTBUF_ALT) modes.add(mo.DECALTBUF) self.mode.difference_update(modes) # Lines below follow the logic in :meth:`set_mode`. if mo.DECCOLM in modes: self.resize(columns=80) self.erase_in_display(2) self.cursor_position() if mo.DECOM in modes: self.cursor_position() if mo.DECSCNM in modes: self.linecontainer.reverse_all_lines() self.select_graphic_rendition(g._SGR["-reverse"]) # Hide the cursor. if mo.DECTCEM in modes: self.cursor.hidden = True if mo.DECSAVECUR in modes: # save cursor position and restore it on mode reset self.restore_cursor() if mo.DECALTBUF in modes: # disable alternative draw buffer, switch internal # linecontainer while preserving generated events self._flush_events() self.linecontainer.leave_altbuf_mode() def draw_string(self, string): """Like draw, but for a whole string at once. String MUST NOT contain any control characters like newlines or carriage-returns. """ def _write_string(s): self.linecontainer.insert_overwrite(self.cursor.y, self.cursor.x, s, self.cursor.attrs) # iframe mode? just write the string if self.iframe_mode: if self.iframe_mode == 'document': self.linecontainer.iframe_write(self.iframe_id, string) else: # non-string writes to the terminal - useful for frame app debugging # TODO: print them within the terminal (popup?, menu?, # panel?) to help debugging the application print ">", string else: if mo.IRM in self.mode: # move existing chars to the right before inserting string # (no wrapping) self.insert_characters(len(string)) _write_string(''.join( reversed(string[-(self.columns - self.cursor.x):]))) elif mo.DECAWM in self.mode: # Auto Wrap Mode # all chars up to the end of the current line line_end = self.columns - self.cursor.x s = string[:line_end] _write_string(s) self.cursor.x += len(s) # remaining chars will be written on subsequent lines i = 0 while len(string) > (line_end + (i * self.columns)): self.linefeed() s = string[line_end + (i * self.columns):line_end + ((i + 1) * self.columns)] _write_string(s) self.cursor.x += len(s) i += 1 else: # no overwrap, just replace the last old char if string # will draw over the end of the current line line_end = self.columns - self.cursor.x if len(string) > line_end: s = string[:line_end - 1] + string[-1] else: s = string _write_string(s) self.cursor.x += len(s) 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. """ if self.iframe_mode: self.linecontainer.iframe_write(self.iframe_id, "\n") return top, bottom = self.margins if self.cursor.y == bottom: if top == 0 and bottom == (self.lines - 1): # surplus lines move the scrollback if no margin is active self.linecontainer.append_line(self.columns) else: self.linecontainer.insert_line(bottom + 1, self.cursor.attrs) # delete surplus lines to achieve scrolling within in the margins self.linecontainer.remove_line(top) 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 and remove the last one, scrolling all lines in between. """ top, bottom = self.margins if self.cursor.y == top: self.linecontainer.remove_line(bottom) self.linecontainer.insert_line(top) else: self.cursor_up() 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 # If cursor is outside scrolling margins it -- 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.linecontainer.remove_line(bottom) self.linecontainer.insert_line(line, self.cursor.attrs) self.carriage_return() 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 # 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.linecontainer.remove_line(self.cursor.y) # TODO: get and use the attributes for the *last* line self.linecontainer.insert_line(bottom, self.cursor.attrs) self.carriage_return() def insert_characters(self, count=None): """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 self.linecontainer.insert(self.cursor.y, self.cursor.x, ' ' * count, self.cursor.attrs) def delete_characters(self, count=None): """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 # TODO: which style is used for the space characters created # on the left? is it really the cursor attrs or is it the # style of the rightmost character? self.linecontainer.remove(self.cursor.y, self.cursor.x, count) def erase_characters(self, count=None): """Erases the indicated # of characters, starting with the character at cursor position. Character attributes are set 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 too all ``erase_*()`` and ``delete_*()`` methods. """ count = count or 1 self.linecontainer.insert_overwrite(self.cursor.y, self.cursor.x, ' ' * count, self.cursor.attrs) def erase_in_line(self, type_of=0, private=False): """Erases a line in a specific way. :param int type_of: 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 **not implemented**. """ if type_of == 0: start = self.cursor.x end = self.columns elif type_of == 1: start = 0 end = self.cursor.x else: start = 0 end = self.columns self.linecontainer.insert_overwrite(self.cursor.y, start, ' ' * (end - start), self.cursor.attrs) def erase_in_display(self, type_of=0, private=False): """Erases display in a specific way. :param int type_of: 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 aren left unchanged **not implemented**. """ if type_of in [0, 1]: # erase parts of the display -> don't care about history interval = ( # a) erase from cursor to the end of the display, including # the cursor, range(self.cursor.y + 1, self.lines), # b) erase from the beginning of the display to the cursor, # including it, range(0, self.cursor.y))[type_of] s = ' ' * self.columns for line in interval: # erase the whole line self.linecontainer.insert_overwrite(line, 0, s, self.cursor.attrs) # erase the line with the cursor. self.erase_in_line(type_of) else: # type_of == 2 if mo.DECALTBUF in self.mode: s = ' ' * self.columns for line in range(self.lines): # erase the whole line self.linecontainer.insert_overwrite( line, 0, s, self.cursor.attrs) else: # c) erase the whole display -> # Push every visible line to the history == add blank # lines until all current non-blank lines are above the # top of the term window. (thats what xterm does and # linux-term not, try using top in both term emulators and # see what happens to the history) self.linecontainer.add_line_origin(self.lines) def string(self, string): if self.iframe_mode: # in document mode -> 'register resource', debug-message, ... # in request mode -> response, send-message, debug-message, ... self.linecontainer.iframe_string(self.iframe_id, string) else: # ignore strings (xterm behaviour) in plain terminal mode self.draw_string(string) ## xterm title hack def os_command(self, string): """Parse OS commands. Xterm will alter the terminals title according to these commands. """ res = (string or '').split(';', 1) if len(res) == 2: command_id, data = res if command_id == '0': self.linecontainer.set_title(data) ## iframe extensions def _next_iframe_id(self): # unique random id to hide the terminals url return utils.roll_id() def _insert_iframe_line(self): self.linecontainer.iframe_enter(self.iframe_id, self.cursor.y) def _iframe_close_document(self): # add some script to iframes that handles resizing, default key events, ... self.linecontainer.iframe_write(self.iframe_id, IFRAME_SCRIPT) self.linecontainer.iframe_close(self.iframe_id) def iframe_set_mode(self, mode_id, cookie): if mode_id == IFRAME_DOCUMENT_MODE_ID: # replace the current line with an iframe line at the current # cursor position (like self.index()) # all following chars are written to the iframes root document connection if self.iframe_mode == None: self.iframe_mode = 'document' self.iframe_id = self._next_iframe_id() self._insert_iframe_line() elif self.iframe_mode == 'response': self.iframe_mode = 'document' elif self.iframe_mode == 'document': pass else: assert False # Illegal Iframe Mode elif mode_id == IFRAME_RESPONSE_MODE_ID: if self.iframe_mode == 'document': self.iframe_mode = 'response' self._iframe_close_document() elif self.iframe_mode == 'response': pass elif self.iframe_mode == None: # TODO: insert the iframe and directly switch into # response mode, use '/' as the path for the iframe. assert False # TODO else: assert False # unknown iframe mode else: assert False # unknown mode_id def iframe_reset_mode(self, mode_id, cookie): if mode_id in (IFRAME_DOCUMENT_MODE_ID, IFRAME_RESPONSE_MODE_ID): # always reset the iframe mode, regardless of the exact mode id if self.iframe_mode == 'document': self._iframe_close_document() self.linecontainer.iframe_leave(self.iframe_id) self.iframe_mode = None elif self.iframe_mode == 'response': self.linecontainer.iframe_leave(self.iframe_id) self.iframe_mode = None else: # not in iframe mode - ignore pass else: assert False # unknown mode_id def close_stream(self): # just hand of the event to the linecontainer self.linecontainer.close_stream() def iframe_resize(self, iframe_id, height): self.linecontainer.iframe_resize(iframe_id, height) def start_clojurescript_repl(self): self.linecontainer.start_clojurescript_repl()