Implement painting of cells

This commit is contained in:
Kovid Goyal 2016-10-15 14:26:53 +05:30
parent 59f92f9db8
commit 0336873afc
5 changed files with 186 additions and 9 deletions

25
kitty/boss.py Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from collections import deque
from PyQt5.QtCore import QObject
from .term import TerminalWidget
class Boss(QObject):
def __init__(self, opts, parent=None):
self.linebuf = deque(maxlen=max(1000, opts.scrollback_lines))
self.term = TerminalWidget(opts, self.linebuf, parent)
self.term.relayout_lines.connect(self.relayout_lines)
def apply_opts(self, opts):
if opts.scrollback_lines != self.linebuf.maxlen:
self.linebuf, old = deque(maxlen=max(1000, opts.scrollback_lines)), self.linebuf
self.linebuf.extend(old)
self.term.apply_opts(opts)
def relayout_lines(self):
pass

View File

@ -4,13 +4,33 @@
import re
from collections import namedtuple
from typing import Tuple
from PyQt5.QtGui import QFont, QFontInfo
from PyQt5.QtGui import QFont, QFontInfo, QColor
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
defaults = {}
def to_qcolor(x):
ans = QColor(x)
if not ans.isValid():
raise ValueError('{} is not a valid color'.format(x))
return ans
def to_font_size(x):
return max(6, float(x))
type_map = {
'scrollback_lines': int,
'font_size': to_font_size,
}
for name in 'foreground foreground_bold background cursor'.split():
type_map[name] = to_qcolor
for line in '''
term xterm-termite
foreground #dddddd
@ -18,7 +38,8 @@ foreground_bold #ffffff
cursor #dddddd
background #000000
font_family monospace
font_size system
font_size 10.0
scrollback_lines 10000
# black
color0 #000000
@ -58,12 +79,15 @@ color15 #ffffff
m = key_pat.match(line)
if m is not None:
key, val = m.groups()
tm = type_map.get(key)
if tm is not None:
val = tm(val)
defaults[key] = val
Options = namedtuple('Defaults', ','.join(defaults.keys()))
defaults = Options(**defaults)
def load_config(path):
def load_config(path: str) -> Options:
if not path:
return defaults
ans = defaults._asdict()
@ -80,10 +104,24 @@ def load_config(path):
if m is not None:
key, val = m.groups()
if key in ans:
tm = type_map.get(key)
if tm is not None:
val = tm(val)
ans[key] = val
return Options(**ans)
def validate_font(opts):
def validate_font(opts: Options):
if not QFontInfo(QFont(opts.font_family)).fixedPitch():
raise ValueError('The font specified in the configuration "{}" is not a monospace font'.format(opts.font_family))
def build_ansi_color_tables(opts: Options) -> Tuple[dict, dict]:
fg = {30 + i: getattr(opts, 'color{}'.format(i)) for i in range(8)}
fg[39] = opts.foreground
fg.update({90 + i: getattr(opts, 'color{}'.format(i + 8)) for i in range(8)})
fg[99] = opts.foreground_bold
bg = {40 + i: getattr(opts, 'color{}'.format(i)) for i in range(8)}
bg[49] = opts.background
bg.update({100 + i: getattr(opts, 'color{}'.format(i + 8)) for i in range(8)})
return fg, bg

View File

@ -2,7 +2,10 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Tuple, Dict, Union
from numpy import zeros, dtype
from PyQt5.QtGui import QColor
color_type = dtype([('type', 'u1'), ('r', 'u1'), ('g', 'u1'), ('b', 'u1')])
@ -22,3 +25,11 @@ class Line:
self.strikethrough = zeros(sz, bool)
self.decoration = zeros(sz, 'u1')
self.decoration_fg = zeros(sz, color_type)
def as_color(entry: Tuple[int, int, int, int], color_table: Dict[int, QColor]) -> Union[QColor, None]:
t, r, g, b = entry
if t == 1:
return color_table.get(r)
if t == 2:
return QColor(r, g, b)

View File

@ -14,7 +14,7 @@ from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
from .config import load_config, validate_font
from .constants import appname, str_version, config_dir
from .term import TerminalWidget
from .boss import Boss
class MainWindow(QMainWindow):
@ -24,8 +24,8 @@ class MainWindow(QMainWindow):
self.setWindowTitle(appname)
sys.excepthook = self.on_unhandled_error
self.handle_unix_signals()
self.terminal = TerminalWidget(opts, self)
self.setCentralWidget(self.terminal)
self.boss = Boss(opts, self)
self.setCentralWidget(self.boss.term)
def on_unhandled_error(self, etype, value, tb):
if etype == KeyboardInterrupt:
@ -35,7 +35,6 @@ class MainWindow(QMainWindow):
msg = str(value)
except Exception:
msg = repr(value)
msg = '<p>' + msg + '<br>' + _('Click "Show details" for more information')
QMessageBox.critical(self, _('Unhandled exception'), msg)
def handle_unix_signals(self):

View File

@ -2,10 +2,114 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Tuple, Iterator, Union
from PyQt5.QtCore import pyqtSignal, QTimer, Qt, QRect
from PyQt5.QtGui import QColor, QPainter, QFont, QFontMetrics, QRegion, QPen
from PyQt5.QtWidgets import QWidget
from .config import build_ansi_color_tables
from .data_types import Line, as_color
def ascii_width(fm: QFontMetrics) -> int:
ans = 0
for i in range(32, 128):
ans = max(ans, fm.widthChar(chr(i)))
return ans
class TerminalWidget(QWidget):
def __init__(self, opts, parent=None):
relayout_lines = pyqtSignal(object, object)
cells_per_line = 80
def __init__(self, opts, linebuf, parent=None):
QWidget.__init__(self, parent)
self.linebuf = linebuf
self.setAutoFillBackground(True)
self.apply_opts(opts)
self.debounce_resize_timer = t = QTimer(self)
t.setSingleShot(True)
t.setInterval(50)
t.timeout.connect(self.do_layout)
def apply_opts(self, opts):
self.opts = opts
pal = self.palette()
pal.setColor(pal.Window, QColor(opts.background))
pal.setColor(pal.WindowText, QColor(opts.foreground))
self.setPalette(pal)
self.current_bg = pal.color(pal.Window)
self.current_fg = pal.color(pal.WindowText)
self.ansi_fg, self.ansi_bg = build_ansi_color_tables(opts)
f = QFont(opts.font_family)
f.setPointSizeF(opts.font_size)
self.setFont(f)
self.font_metrics = fm = QFontMetrics(self.font())
self.cell_height = fm.lineSpacing()
self.cell_width = ascii_width(fm)
self.do_layout()
def do_layout(self):
previous, self.cells_per_line = self.cells_per_line, self.width() // self.cell_width
if previous != self.cells_per_line:
self.relayout_lines.emit(previous, self.cells_per_line)
self.lines_per_screen = self.height() // self.cell_height
self.hmargin = (self.width() - self.cells_per_line * self.cell_width) // 2
self.vmargin = (self.height() % self.cell_height) // 2
self.line_positions = tuple(self.vmargin + i * self.cell_height for i in range(self.lines_per_screen))
self.cell_positions = tuple(self.hmargin + i * self.cell_width for i in range(self.cells_per_line))
self.layout_size = self.size()
self.update()
def resizeEvent(self, ev):
self.debounce_resize_timer.start()
def dirty_lines(self, region: QRegion) -> Iterator[Tuple[int, QRegion]]:
w = self.width() - 2 * self.hmargin
for i, y in enumerate(self.line_positions):
ir = region.intersected(QRect(self.hmargin, y, w, self.cell_height))
if not ir.isEmpty():
yield i, ir
def dirty_cells(self, y: int, line_region: QRegion) -> Iterator[int]:
for i, x in enumerate(self.cell_positions):
if line_region.intersects(QRect(x, y, self.cell_width, self.cell_height)):
yield i
def line(self, screen_line: int) -> Union[Line, None]:
try:
lpos = len(self.linebuf) - self.lines_per_screen
return self.linebuf[lpos]
except IndexError:
pass
def paintEvent(self, ev):
if self.size() != self.layout_size:
return
r = ev.region()
p = QPainter(self)
for lnum, line_region in self.dirty_lines(r):
line = self.line(lnum)
if line is not None:
ypos = self.line_positions[lnum]
for cnum in self.dirty_cells(ypos, line_region):
p.save()
self.paint_cell(p, line, cnum, ypos)
p.restore()
def paint_cell(self, painter: QPainter, line: Line, col: int, y: int) -> None:
char = line.char[col]
if not char:
return
x = self.cell_positions[col]
r = QRect(x, y, self.cell_width, self.cell_height)
t, r, g, b = line.fg[col]
fg = as_color(line.fg[col], self.ansi_fg)
if fg is not None:
painter.setPen(QPen(fg))
bg = as_color(line.bg[col], self.ansi_bg)
if bg is not None:
painter.fillRect(r, bg)
painter.drawText(r, Qt.AlignHCenter | Qt.AlignBaseline | Qt.TextSingleLine, char)