Implement the fast draw path for single width characters
This commit is contained in:
parent
63d228f21b
commit
355bfce189
@ -187,6 +187,9 @@ class Line:
|
||||
def width(self, i):
|
||||
return (self.char[i] >> ATTRS_SHIFT) & 0b11
|
||||
|
||||
def char_at(self, i):
|
||||
return chr(self.char[i] & CHAR_MASK)
|
||||
|
||||
def set_char(self, i: int, ch: str, width: int=1, cursor: Cursor=None) -> None:
|
||||
if cursor is None:
|
||||
c = self.char[i]
|
||||
|
||||
102
kitty/screen.py
102
kitty/screen.py
@ -36,20 +36,17 @@ class Screen(QObject):
|
||||
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``.
|
||||
"""
|
||||
cursor_changed = pyqtSignal(object)
|
||||
cursor_position_changed = pyqtSignal(object, object, object)
|
||||
update_screen = pyqtSignal()
|
||||
|
||||
title_changed = pyqtSignal(object)
|
||||
icon_changed = pyqtSignal(object)
|
||||
update_line_range = pyqtSignal(object, object)
|
||||
update_cell_range = pyqtSignal(object, object, object)
|
||||
line_added_to_history = pyqtSignal()
|
||||
write_to_child = pyqtSignal(object)
|
||||
_notify_cursor_position = True
|
||||
|
||||
def __init__(self, opts, columns: int=80, lines: int=24, parent=None):
|
||||
def __init__(self, opts, tracker, columns: int=80, lines: int=24, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.write_process_input = self.write_to_child.emit
|
||||
for attr in 'cursor_changed cursor_position_changed update_screen update_line_range update_cell_range line_added_to_history'.split():
|
||||
setattr(self, attr, getattr(tracker, attr))
|
||||
self.savepoints = deque()
|
||||
self.columns = columns
|
||||
self.lines = lines
|
||||
@ -69,7 +66,7 @@ class Screen(QObject):
|
||||
|
||||
def notify_cursor_position(self, x, y):
|
||||
if self._notify_cursor_position:
|
||||
self.cursor_position_changed.emit(self.cursor, x, y)
|
||||
self.cursor_position_changed(self.cursor, x, y)
|
||||
|
||||
@property
|
||||
def display(self) -> Sequence[str]:
|
||||
@ -108,7 +105,7 @@ class Screen(QObject):
|
||||
self.tabstops = set(range(7, self.columns, 8))
|
||||
|
||||
self.cursor = Cursor(0, 0)
|
||||
self.cursor_changed.emit(self.cursor)
|
||||
self.cursor_changed(self.cursor)
|
||||
self.cursor_position()
|
||||
|
||||
def resize(self, lines: int, columns: int):
|
||||
@ -193,13 +190,13 @@ class Screen(QObject):
|
||||
for line in self.linebuf:
|
||||
for i in range(len(line)):
|
||||
line.reverse[i] = True
|
||||
self.update_screen.emit()
|
||||
self.update_screen()
|
||||
self.select_graphic_rendition(7) # +reverse.
|
||||
|
||||
# Make the cursor visible.
|
||||
if mo.DECTCEM in modes and self.cursor.hidden:
|
||||
self.cursor.hidden = False
|
||||
self.cursor_changed.emit(self.cursor)
|
||||
self.cursor_changed(self.cursor)
|
||||
|
||||
def reset_mode(self, *modes, private=False):
|
||||
"""Resets (disables) a given list of modes.
|
||||
@ -228,13 +225,13 @@ class Screen(QObject):
|
||||
for line in self.linebuf:
|
||||
for i in range(len(line)):
|
||||
line.reverse[i] = False
|
||||
self.update_screen.emit()
|
||||
self.update_screen()
|
||||
self.select_graphic_rendition(27) # -reverse.
|
||||
|
||||
# Hide the cursor.
|
||||
if mo.DECTCEM in modes and not self.cursor.hidden:
|
||||
self.cursor.hidden = True
|
||||
self.cursor_changed.emit(self.cursor)
|
||||
self.cursor_changed(self.cursor)
|
||||
|
||||
def define_charset(self, code, mode):
|
||||
"""Defines ``G0`` or ``G1`` charset.
|
||||
@ -293,57 +290,76 @@ class Screen(QObject):
|
||||
return "".join(self.g0_charset[b] for b in data)
|
||||
|
||||
def _fast_draw(self, data: str) -> None:
|
||||
while data:
|
||||
pass # TODO: Implement me
|
||||
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 = min(len_left, self.columns)
|
||||
self.cursor.x = self.columns - space_left_in_line
|
||||
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:
|
||||
# If this was the last column in a line and auto wrap mode is
|
||||
# enabled, move the cursor to the beginning of the next line,
|
||||
# otherwise replace characters already displayed with newly
|
||||
# entered.
|
||||
if self.cursor.x + char_width > self.columns - 1:
|
||||
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
|
||||
space_left_in_line = self.columns
|
||||
else:
|
||||
extra = self.cursor.x + char_width + 1 - self.columns
|
||||
self.cursor.x -= extra
|
||||
self.cursor.x = self.columns - char_width
|
||||
space_left_in_line = char_width
|
||||
|
||||
# If Insert mode is set, new characters move old characters to
|
||||
# the right, otherwise terminal is in Replace mode and new
|
||||
# characters replace old characters at cursor position.
|
||||
do_insert = mo.IRM in self.mode and char_width > 0
|
||||
if do_insert:
|
||||
self.insert_characters(char_width)
|
||||
do_insert = mo.IRM in self.mode
|
||||
|
||||
cx = self.cursor.x
|
||||
line = self.linebuf[self.cursor.y]
|
||||
if char_width:
|
||||
line.char[cx], line.width[cx] = ord(char), char_width
|
||||
if char_width > 1:
|
||||
for i in range(1, char_width):
|
||||
line.char[cx + i] = line.width[cx + i] = 0
|
||||
line.apply_cursor(self.cursor, cx, char_width)
|
||||
if char_width > 0:
|
||||
if do_insert:
|
||||
line.right_shift(self.cursor.x, char_width)
|
||||
line.set_char(cx, char, char_width, self.cursor)
|
||||
if char_width == 2:
|
||||
line.set_char(cx, '\0', 0, self.cursor)
|
||||
elif unicodedata.combining(char):
|
||||
# A zero-cell character is combined with the previous
|
||||
# character either on this or preceeding line.
|
||||
if cx:
|
||||
last = chr(line.char[cx - 1])
|
||||
last = line.char_at(cx - 1)
|
||||
normalized = unicodedata.normalize("NFC", last + char)
|
||||
line.char[cx - 1] = ord(normalized)
|
||||
line.set_char(cx - 1, normalized[0])
|
||||
elif self.cursor.y:
|
||||
lline = self.linebuf[self.cursor.y - 1]
|
||||
last = chr(lline.char[self.columns - 1])
|
||||
last = chr(lline.char_at(self.columns - 1))
|
||||
normalized = unicodedata.normalize("NFC", last + char)
|
||||
lline.char[self.columns - 1] = ord(normalized)
|
||||
lline.set_char(self.columns - 1, normalized[0])
|
||||
|
||||
# .. note:: We can't use :meth:`cursor_forward()`, because that
|
||||
# way, we'll never know when to linefeed.
|
||||
if char_width > 0:
|
||||
self.cursor.x = min(self.cursor.x + char_width, self.columns - 1)
|
||||
if not do_insert:
|
||||
self.update_cell_range(self.cursor.y, cx, self.cursor.x)
|
||||
self.cursor.x = min(self.cursor.x + char_width, self.columns)
|
||||
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(self, data: bytes) -> None:
|
||||
""" Displays decoded characters at the current cursor position and
|
||||
@ -395,8 +411,8 @@ class Screen(QObject):
|
||||
if self.cursor.y == bottom:
|
||||
self.tophistorybuf.append(self.linebuf.pop(top))
|
||||
self.linebuf.insert(bottom, Line(self.columns))
|
||||
self.line_added_to_history.emit()
|
||||
self.update_screen.emit()
|
||||
self.line_added_to_history()
|
||||
self.update_screen()
|
||||
else:
|
||||
self.cursor_down()
|
||||
|
||||
@ -409,7 +425,7 @@ class Screen(QObject):
|
||||
if self.cursor.y == top:
|
||||
self.linebuf.pop(bottom)
|
||||
self.linebuf.insert(top, Line(self.columns))
|
||||
self.update_screen.emit()
|
||||
self.update_screen()
|
||||
else:
|
||||
self.cursor_up()
|
||||
|
||||
@ -473,7 +489,7 @@ class Screen(QObject):
|
||||
self.set_mode(mo.DECAWM)
|
||||
|
||||
self.cursor = savepoint.cursor
|
||||
self.cursor_changed.emit(self.cursor)
|
||||
self.cursor_changed(self.cursor)
|
||||
self.ensure_bounds(use_margins=True)
|
||||
else:
|
||||
# If nothing was saved, the cursor moves to home position;
|
||||
@ -498,7 +514,7 @@ class Screen(QObject):
|
||||
min(bottom + 1, self.cursor.y + count)):
|
||||
self.linebuf.pop(bottom)
|
||||
self.linebuf.insert(line, Line(self.columns))
|
||||
self.update_line_range.emit(self.cursor.y, bottom)
|
||||
self.update_line_range(self.cursor.y, bottom)
|
||||
|
||||
self.carriage_return()
|
||||
|
||||
@ -518,7 +534,7 @@ class Screen(QObject):
|
||||
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.emit(self.cursor.y, bottom)
|
||||
self.update_line_range(self.cursor.y, bottom)
|
||||
|
||||
self.carriage_return()
|
||||
|
||||
|
||||
91
kitty/tracker.py
Normal file
91
kitty/tracker.py
Normal file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
from typing import Set, Tuple, Iterator
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, Qt
|
||||
|
||||
from .data_types import Cursor
|
||||
|
||||
|
||||
def merge_ranges(ranges: Set[Tuple[int]]) -> Iterator[Tuple[int]]:
|
||||
if ranges:
|
||||
sorted_intervals = sorted(ranges, key=itemgetter(0))
|
||||
# low and high represent the bounds of the current run of merges
|
||||
low, high = sorted_intervals[0]
|
||||
|
||||
for iv in sorted_intervals[1:]:
|
||||
if iv[0] <= high: # new interval overlaps current run
|
||||
high = max(high, iv[1]) # merge with the current run
|
||||
else: # current run is over
|
||||
yield low, high # yield accumulated interval
|
||||
low, high = iv # start new run
|
||||
|
||||
yield low, high # end the final run
|
||||
|
||||
|
||||
class ChangeTracker(QObject):
|
||||
|
||||
dirtied = pyqtSignal(object)
|
||||
mark_dirtied = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.reset()
|
||||
self.mark_dirtied.connect(self.consolidate_changes, type=Qt.QueuedConnection)
|
||||
|
||||
def reset(self):
|
||||
self._dirty = False
|
||||
self.changed_cursor = None
|
||||
self.changed_cells = defaultdict(set)
|
||||
self.changed_lines = set()
|
||||
self.screen_changed = False
|
||||
self.history_line_added_count = 0
|
||||
|
||||
def dirty(self):
|
||||
if not self._dirty:
|
||||
self._dirty = True
|
||||
self.mark_dirtied.emit()
|
||||
|
||||
def cursor_changed(self, cursor: Cursor) -> None:
|
||||
self.changed_cursor = cursor
|
||||
self.dirty()
|
||||
|
||||
def cursor_position_changed(self, cursor: Cursor, x: int, y: int) -> None:
|
||||
self.changed_cursor = cursor
|
||||
self.changed_cells[y].add((x, x))
|
||||
self.dirty()
|
||||
|
||||
def update_screen(self):
|
||||
self.screen_changed = True
|
||||
self.dirty()
|
||||
|
||||
def update_line_range(self, first_line, last_line):
|
||||
self.changed_lines |= set(range(first_line, last_line + 1))
|
||||
self.dirty()
|
||||
|
||||
def update_cell_range(self, y, first_cell, last_cell):
|
||||
self.changed_cells[y].add((first_cell, last_cell))
|
||||
self.dirty()
|
||||
|
||||
def line_added_to_history(self):
|
||||
self.history_line_added_count += 1
|
||||
self.dirty()
|
||||
|
||||
def consolidate_changes(self):
|
||||
if self.screen_changed:
|
||||
self.changed_cells.clear(), self.changed_lines.clear()
|
||||
else:
|
||||
if self.changed_lines:
|
||||
for y in self.changed_lines:
|
||||
del self.changed_cells[y]
|
||||
for y, cell_ranges in self.changed_cells.items():
|
||||
self.changed_cells[y] = tuple(merge_ranges(cell_ranges))
|
||||
changes = {'screen': self.screen_changed, 'cursor': self.changed_cursor, 'lines': self.changed_lines,
|
||||
'cells': self.changed_cells, 'history_line_added_count': self.history_line_added_count}
|
||||
self.reset()
|
||||
self.dirtied.emit(changes)
|
||||
return changes
|
||||
@ -2,9 +2,32 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from collections import defaultdict
|
||||
from unittest import TestCase
|
||||
|
||||
from kitty.screen import Screen
|
||||
from kitty.tracker import ChangeTracker
|
||||
from kitty.config import defaults
|
||||
|
||||
|
||||
class BaseTest(TestCase):
|
||||
|
||||
ae = TestCase.assertEqual
|
||||
|
||||
def create_screen(self, cols=5, lines=5, history_size=5):
|
||||
t = ChangeTracker()
|
||||
opts = defaults._replace(scrollback_lines=history_size)
|
||||
s = Screen(opts, t, columns=cols, lines=lines)
|
||||
return s, t
|
||||
|
||||
def assertChanges(self, t, ignore='', **expected_changes):
|
||||
actual_changes = t.consolidate_changes()
|
||||
ignore = frozenset(ignore.split())
|
||||
for k, v in actual_changes.items():
|
||||
if isinstance(v, defaultdict):
|
||||
v = dict(v)
|
||||
if k not in ignore:
|
||||
if k in expected_changes:
|
||||
self.ae(expected_changes[k], v)
|
||||
else:
|
||||
self.assertFalse(v)
|
||||
|
||||
53
kitty_tests/screen.py
Normal file
53
kitty_tests/screen.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
from kitty.screen import mo
|
||||
|
||||
|
||||
class TestScreen(BaseTest):
|
||||
|
||||
def test_draw_fast(self):
|
||||
# Test in line-wrap, non-insert mode
|
||||
s, t = self.create_screen()
|
||||
s.draw(b'a' * 5)
|
||||
self.ae(str(s.linebuf[0]), 'a' * 5)
|
||||
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
|
||||
self.assertChanges(t, ignore='cursor', cells={0: ((0, 4),)})
|
||||
s.draw(b'b' * 7)
|
||||
self.assertTrue(s.linebuf[1].continued)
|
||||
self.assertTrue(s.linebuf[2].continued)
|
||||
self.ae(str(s.linebuf[0]), 'a' * 5)
|
||||
self.ae(str(s.linebuf[1]), 'b' * 5)
|
||||
self.ae(str(s.linebuf[2]), 'b' * 2 + ' ' * 3)
|
||||
self.ae(s.cursor.x, 2), self.ae(s.cursor.y, 2)
|
||||
self.assertChanges(t, ignore='cursor', cells={1: ((0, 4),), 2: ((0, 1),)})
|
||||
s.draw(b'c' * 15)
|
||||
self.ae(str(s.linebuf[0]), 'b' * 5)
|
||||
self.ae(str(s.linebuf[1]), 'bbccc')
|
||||
|
||||
# Now test without line-wrap
|
||||
s.reset(), t.reset()
|
||||
s.reset_mode(mo.DECAWM)
|
||||
s.draw(b'0123456789')
|
||||
self.ae(str(s.linebuf[0]), '56789')
|
||||
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
|
||||
self.assertChanges(t, ignore='cursor', cells={0: ((0, 4),)})
|
||||
s.draw(b'ab')
|
||||
self.ae(str(s.linebuf[0]), '567ab')
|
||||
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
|
||||
self.assertChanges(t, ignore='cursor', cells={0: ((3, 4),)})
|
||||
|
||||
# Now test in insert mode
|
||||
s.reset(), t.reset()
|
||||
s.set_mode(mo.IRM)
|
||||
s.draw(b'12345' * 5)
|
||||
s.cursor_back(5)
|
||||
self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 4)
|
||||
t.reset()
|
||||
s.draw(b'ab')
|
||||
self.ae(str(s.linebuf[4]), 'ab123')
|
||||
self.ae((s.cursor.x, s.cursor.y), (2, 4))
|
||||
self.assertChanges(t, ignore='cursor', cells={4: ((0, 4),)})
|
||||
4
test.py
4
test.py
@ -86,7 +86,9 @@ def run_cli(suite, verbosity=4):
|
||||
r = unittest.TextTestRunner
|
||||
r.resultclass = unittest.TextTestResult
|
||||
init_env()
|
||||
result = r(verbosity=verbosity).run(suite)
|
||||
runner = r(verbosity=verbosity)
|
||||
runner.tb_locals = True
|
||||
result = runner.run(suite)
|
||||
if not result.wasSuccessful():
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user