kitty/kitty/data_types.py
2016-10-17 14:18:15 +05:30

271 lines
9.7 KiB
Python

#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import array
from typing import Tuple, Dict, Union, Iterator, Sequence
from itertools import repeat
from PyQt5.QtGui import QColor
code = 'I' if array.array('I').itemsize >= 4 else 'L'
lcode = 'L' if array.array('L').itemsize >= 8 else 'Q'
def get_zeroes(sz: int) -> Tuple[array.array]:
if get_zeroes.current_size != sz:
get_zeroes.current_size = sz
get_zeroes.ans = (
array.array(lcode, repeat(0, sz)),
array.array(code, repeat(0, sz)),
array.array(code, repeat(32, sz)),
)
return get_zeroes.ans
get_zeroes.current_size = None
class Cursor:
__slots__ = ("x", "y", "hidden", 'fg', 'bg', 'bold', 'italic', 'reverse', 'strikethrough', 'decoration', 'decoration_fg')
def __init__(self, x: int=0, y: int=0):
self.x = x
self.y = y
self.hidden = False
self.fg = self.bg = self.decoration_fg = 0
self.bold = self.italic = self.reverse = self.strikethrough = False
self.decoration = 0
def copy(self):
ans = Cursor(self.x, self.y)
ans.hidden = self.hidden
ans.fg, ans.bg, ans.decoration_fg = self.fg, self.bg, self.decoration_fg
ans.bold, ans.italic, ans.reverse, ans.strikethrough = self.bold, self.italic, self.reverse, self.strikethrough
ans.decoration = self.decoration
return ans
def __eq__(self, other):
if not isinstance(other, Cursor):
return False
for x in self.__slots__:
if getattr(self, x) != getattr(other, x):
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return self.__class__.__name__ + '({})'.format(', '.join(
'{}={}'.format(x, getattr(self, x)) for x in self.__slots__))
CHAR_MASK = 0xFFFFFF
ATTRS_SHIFT = 24
ATTRS_MASK = 0xFF << ATTRS_SHIFT
WIDTH_MASK = 0xFF
DECORATION_SHIFT = 2
BOLD_SHIFT, ITALIC_SHIFT, REVERSE_SHIFT, STRIKE_SHIFT = range(DECORATION_SHIFT + 2, DECORATION_SHIFT + 6)
DECORATION_MASK = 1 << DECORATION_SHIFT
BOLD_MASK = 1 << BOLD_SHIFT
ITALIC_MASK = 1 << ITALIC_SHIFT
REVERSE_MASK = 1 << REVERSE_SHIFT
STRIKE_MASK = 1 << STRIKE_SHIFT
COL_MASK = 0xFFFFFFFF
COL_SHIFT = 32
class Line:
__slots__ = 'char color decoration_fg continued'.split()
def __init__(self, sz: int, other=None):
if other is None:
z8, z4, spaces = get_zeroes(sz)
self.char = spaces[:]
self.color = z8[:]
self.decoration_fg = z4[:]
self.continued = False
else:
self.char = other.char[:]
self.color = other.color[:]
self.decoration_fg = other.decoration_fg[:]
self.continued = other.continued
def __eq__(self, other):
if not isinstance(other, Line):
return False
for x in self.__slots__:
if getattr(self, x) != getattr(other, x):
return False
return self.continued == other.continued
def __ne__(self, other):
return not self.__eq__(other)
def __len__(self):
return len(self.char)
def copy(self):
return Line(len(self.char), self)
def copy_char(self, src: int, to, dest: int) -> None:
to.char[dest] = self.char[src]
to.color[dest] = self.color[src]
to.decoration_fg[dest] = self.decoration_fg[src]
def cursor_to_attrs(self, c: Cursor) -> int:
return ((c.decoration & 0b11) << DECORATION_SHIFT) | ((c.bold & 0b1) << BOLD_SHIFT) | \
((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):
self.color[i] = ((c.bg & COL_MASK) << COL_SHIFT) | (c.fg & COL_MASK)
self.decoration_fg[i] = c.decoration_fg
sc = self.char[i]
ch = ord(char) if clear_char else sc
sattrs = sc >> ATTRS_SHIFT
w = 1 if clear_char else sattrs & WIDTH_MASK
attrs = w | self.cursor_to_attrs(c)
self.char[i] = (ch & CHAR_MASK) | (attrs << ATTRS_SHIFT)
def cursor_from(self, x: int, ypos: int=0) -> Cursor:
c = Cursor(x, ypos)
c.decoration_fg = self.decoration_fg[x]
col = self.color[x]
c.fg = col & COL_MASK
c.bg = col >> COL_SHIFT
attrs = self.char[x] >> ATTRS_SHIFT
c.decoration = (attrs >> DECORATION_SHIFT) & 0b11
c.bold = bool((attrs >> BOLD_SHIFT) & 0b1)
c.italic = bool((attrs >> ITALIC_SHIFT) & 0b1)
c.reverse = bool((attrs >> REVERSE_SHIFT) & 0b1)
c.strikethrough = bool((attrs >> STRIKE_SHIFT) & 0b1)
return c
def set_text(self, text: str, offset_in_text: int, sz: int, cursor: Cursor) -> None:
' Set the specified text in this line, with attributes taken from the cursor '
attrs = self.cursor_to_attrs(cursor) | 1
fg, bg, dfg = cursor.fg, cursor.bg, cursor.decoration_fg
col = (fg & COL_MASK) | ((bg & COL_MASK) << COL_SHIFT)
dx = cursor.x
for cpos in range(offset_in_text, offset_in_text + sz):
ch = ord(text[cpos]) & CHAR_MASK
self.char[dx] = ch | (attrs << ATTRS_SHIFT)
self.color[dx], self.decoration_fg[dx] = col, dfg
dx += 1
def copy_slice(self, src, dest, num):
src, dest = slice(src, src + num), slice(dest, dest + num)
for a in (self.char, self.color, self.decoration_fg):
a[dest] = a[src]
def right_shift(self, at: int, num: int) -> None:
src_start, dest_start = at, at + num
ls = len(self)
dnum = min(ls - dest_start, ls)
if dnum:
self.copy_slice(src_start, dest_start, dnum)
def left_shift(self, at: int, num: int) -> None:
src_start, dest_start = at + num, at
ls = len(self)
snum = min(ls - src_start, ls)
if snum:
self.copy_slice(src_start, dest_start, snum)
def __str__(self) -> str:
return ''.join(map(lambda c: chr(c & CHAR_MASK), filter(None, self.char)))
def __repr__(self) -> str:
return repr(str(self))
def width(self, i):
return (self.char[i] >> ATTRS_SHIFT) & 0b11
def set_char(self, i: int, ch: str, width: int=1, cursor: Cursor=None) -> None:
if cursor is None:
c = self.char[i]
a = (c >> ATTRS_SHIFT) & ~WIDTH_MASK
else:
a = self.cursor_to_attrs(cursor)
col = (cursor.fg & COL_MASK) | ((cursor.bg & COL_MASK) << COL_SHIFT)
self.color[i], self.decoration_fg[i] = col, cursor.decoration_fg
a |= width & WIDTH_MASK
self.char[i] = (a << ATTRS_SHIFT) | (ord(ch) & CHAR_MASK)
def set_bold(self, i, val):
c = self.char[i]
a = c >> ATTRS_SHIFT
a = (a & ~BOLD_MASK) | ((val & 0b1) << BOLD_SHIFT)
self.char[i] = (a << ATTRS_SHIFT) | (c & CHAR_MASK)
def set_italic(self, i, val):
c = self.char[i]
a = c >> ATTRS_SHIFT
a = (a & ~ITALIC_MASK) | ((val & 0b1) << ITALIC_SHIFT)
self.char[i] = (a << ATTRS_SHIFT) | (c & CHAR_MASK)
def set_reverse(self, i, val):
c = self.char[i]
a = c >> ATTRS_SHIFT
a = (a & ~REVERSE_MASK) | ((val & 0b1) << REVERSE_SHIFT)
self.char[i] = (a << ATTRS_SHIFT) | (c & CHAR_MASK)
def set_strikethrough(self, i, val):
c = self.char[i]
a = c >> ATTRS_SHIFT
a = (a & ~STRIKE_MASK) | ((val & 0b1) << STRIKE_SHIFT)
self.char[i] = (a << ATTRS_SHIFT) | (c & CHAR_MASK)
def set_decoration(self, i, val):
c = self.char[i]
a = c >> ATTRS_SHIFT
a = (a & ~DECORATION_MASK) | ((val & 0b11) << DECORATION_SHIFT)
self.char[i] = (a << ATTRS_SHIFT) | (c & CHAR_MASK)
def as_color(entry: int, color_table: Dict[int, QColor]) -> Union[QColor, None]:
t = entry & 0xff
if t == 1:
r = (entry >> 8) & 0xff
return color_table.get(r)
if t == 2:
r = (entry >> 8) & 0xff
g = (entry >> 16) & 0xff
b = (entry >> 24) & 0xff
return QColor(r, g, b)
def copy_char(src: Line, dest: Line, src_pos: int, dest_pos: int) -> None:
for i in range(src.width(src_pos)):
src.copy_char(src_pos + i, dest, dest_pos + i)
def rewrap_lines(lines: Sequence[Line], width: int) -> Iterator[Line]:
if lines:
current_line, current_dest_pos = Line(width), 0
src_limit = len(lines[0]) - 1
for i, src in enumerate(lines):
current_src_pos = 0
while current_src_pos <= src_limit:
cw = src.width(current_src_pos)
if cw == 0:
# Hard line break, start a new line
yield current_line
current_line, current_dest_pos = Line(width), 0
break
if cw + current_dest_pos > width:
# dest line does not have enough space to hold the current source char
yield current_line
current_line, current_dest_pos = Line(width), 0
current_line.continued = True
copy_char(src, current_line, current_src_pos, current_dest_pos)
current_dest_pos += cw
current_src_pos += cw
else:
hard_break = src is not lines[-1] and not lines[i + 1].continued
if hard_break:
yield current_line
current_line, current_dest_pos = Line(width), 0
if current_dest_pos:
yield current_line