Implement painting of cells
This commit is contained in:
parent
59f92f9db8
commit
0336873afc
25
kitty/boss.py
Normal file
25
kitty/boss.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
106
kitty/term.py
106
kitty/term.py
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user