diff --git a/kitty/data_types.py b/kitty/data_types.py index 952172b47..4b9c36474 100644 --- a/kitty/data_types.py +++ b/kitty/data_types.py @@ -129,7 +129,7 @@ class Line: ((c.italic & 0b1) << ITALIC_SHIFT) | ((c.reverse & 0b1) << REVERSE_SHIFT) | ((c.strikethrough & 0b1) << STRIKE_SHIFT) def apply_cursor(self, c: Cursor, at: int=0, num: int=1, clear_char=False, char=' ') -> None: - for i in range(at, at + num): + for i in range(at, min(len(self), at + num)): self.color[i] = ((c.bg & COL_MASK) << COL_SHIFT) | (c.fg & COL_MASK) self.decoration_fg[i] = c.decoration_fg sc = self.char[i] @@ -170,6 +170,12 @@ class Line: for i in range(cursor.x, cursor.x + sz): self.combining_chars.pop(i, None) + def clear_text(self, start, num, clear_char=' '): + ' Clear the text in the specified range, preserving existing attributes ' + ch = ord(clear_char) & CHAR_MASK + for i in range(start, min(len(self), start + num)): + self.char[i] = (self.char[i] & ~CHAR_MASK) | ch + def copy_slice(self, src, dest, num): if self.combining_chars: scc = self.combining_chars.copy() @@ -189,6 +195,10 @@ class Line: dnum = min(ls - dest_start, ls) if dnum: self.copy_slice(src_start, dest_start, dnum) + # Check if a wide character was split at the right edge + w = (self.char[-1] >> ATTRS_SHIFT) & 0b11 + if w != 1: + self.char[-1] = (w << ATTRS_SHIFT) | ord(' ') def left_shift(self, at: int, num: int) -> None: src_start, dest_start = at + num, at diff --git a/kitty/screen.py b/kitty/screen.py index b26eeb0fb..0ea4a04f5 100644 --- a/kitty/screen.py +++ b/kitty/screen.py @@ -543,12 +543,13 @@ class Screen(QObject): 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] - # TODO: Handle wide chars that get split at the right edge. line.right_shift(x, num) line.apply_cursor(self.cursor, x, num, clear_char=True) - self.update_cell_range(y, x, self.columns) + self.update_cell_range(y, x, self.columns - 1) def delete_characters(self, count=1): """Deletes the indicated # of characters, starting with the @@ -565,15 +566,19 @@ class Screen(QObject): if top <= y <= bottom: x = self.cursor.x num = min(self.columns - x, count) - # TODO: Handle deletion of wide chars + # 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=None): + def erase_characters(self, count=1): """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. + to cursor attributes. The cursor remains in the same position. :param int count: number of characters to erase. @@ -585,10 +590,11 @@ class Screen(QObject): to all ``erase_*()`` and ``delete_*()`` methods. """ count = count or 1 - - for column in range(self.cursor.x, - min(self.cursor.x + count, self.columns)): - self.buffer[self.cursor.y][column] = self.cursor.attrs + 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. @@ -601,22 +607,30 @@ class Screen(QObject): including cursor position. * ``2`` -- Erases complete line. :param bool private: when ``True`` character attributes are left - unchanged **not implemented**. + unchanged. """ + s = n = 0 if how == 0: # a) erase from the cursor to the end of line, including # the cursor, - interval = range(self.cursor.x, self.columns) + 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, - interval = range(self.cursor.x + 1) + s, n = 0, self.cursor.x + 1 elif how == 2: # c) erase the entire line. - interval = range(self.columns) - - for column in interval: - self.buffer[self.cursor.y][column] = self.cursor.attrs + 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. diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index c7d228e83..79722f0db 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -100,3 +100,64 @@ class TestScreen(BaseTest): self.ae(str(s.linebuf[4]), 'a\u0306b1\u030623') self.ae((s.cursor.x, s.cursor.y), (2, 4)) self.assertChanges(t, ignore='cursor', cells={4: ((0, 4),)}) + + def test_char_manipulation(self): + s, t = self.create_screen() + + def init(): + s.reset(), t.reset() + s.draw(b'abcde') + s.cursor.bold = True + s.cursor_back(4) + t.reset() + self.ae(s.cursor.x, 1) + + init() + s.insert_characters(2) + self.ae(str(s.linebuf[0]), 'a bc') + self.assertTrue(s.linebuf[0].cursor_from(1).bold) + self.assertChanges(t, ignore='cursor', cells={0: ((1, 4),)}) + s.cursor_back(1) + s.insert_characters(20) + self.ae(str(s.linebuf[0]), ' ') + self.assertChanges(t, ignore='cursor', cells={0: ((0, 4),)}) + s.draw('xココ'.encode('utf-8')) + s.cursor_back(5) + t.reset() + s.insert_characters(1) + self.ae(str(s.linebuf[0]), ' xコ ') + self.assertChanges(t, ignore='cursor', cells={0: ((0, 4),)}) + + init() + s.delete_characters(2) + self.ae(str(s.linebuf[0]), 'ade ') + self.assertTrue(s.linebuf[0].cursor_from(4).bold) + self.assertFalse(s.linebuf[0].cursor_from(2).bold) + self.assertChanges(t, ignore='cursor', cells={0: ((1, 4),)}) + + init() + s.erase_characters(2) + self.ae(str(s.linebuf[0]), 'a de') + self.assertTrue(s.linebuf[0].cursor_from(1).bold) + self.assertFalse(s.linebuf[0].cursor_from(4).bold) + self.assertChanges(t, cells={0: ((1, 2),)}) + s.erase_characters(20) + self.ae(str(s.linebuf[0]), 'a ') + + init() + s.erase_in_line() + self.ae(str(s.linebuf[0]), 'a ') + self.assertTrue(s.linebuf[0].cursor_from(1).bold) + self.assertFalse(s.linebuf[0].cursor_from(0).bold) + self.assertChanges(t, cells={0: ((1, 4),)}) + init() + s.erase_in_line(1) + self.ae(str(s.linebuf[0]), ' cde') + self.assertChanges(t, cells={0: ((0, 2),)}) + init() + s.erase_in_line(2) + self.ae(str(s.linebuf[0]), ' ') + self.assertChanges(t, cells={0: ((0, 4),)}) + init() + s.erase_in_line(2, private=True) + self.ae((False, False, False, False, False), tuple(map(lambda i: s.linebuf[0].cursor_from(i).bold, range(5))))