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
|
import re
|
||||||
from collections import namedtuple
|
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+(.+)$')
|
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
|
||||||
|
|
||||||
defaults = {}
|
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 '''
|
for line in '''
|
||||||
term xterm-termite
|
term xterm-termite
|
||||||
foreground #dddddd
|
foreground #dddddd
|
||||||
@ -18,7 +38,8 @@ foreground_bold #ffffff
|
|||||||
cursor #dddddd
|
cursor #dddddd
|
||||||
background #000000
|
background #000000
|
||||||
font_family monospace
|
font_family monospace
|
||||||
font_size system
|
font_size 10.0
|
||||||
|
scrollback_lines 10000
|
||||||
|
|
||||||
# black
|
# black
|
||||||
color0 #000000
|
color0 #000000
|
||||||
@ -58,12 +79,15 @@ color15 #ffffff
|
|||||||
m = key_pat.match(line)
|
m = key_pat.match(line)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
key, val = m.groups()
|
key, val = m.groups()
|
||||||
|
tm = type_map.get(key)
|
||||||
|
if tm is not None:
|
||||||
|
val = tm(val)
|
||||||
defaults[key] = val
|
defaults[key] = val
|
||||||
Options = namedtuple('Defaults', ','.join(defaults.keys()))
|
Options = namedtuple('Defaults', ','.join(defaults.keys()))
|
||||||
defaults = Options(**defaults)
|
defaults = Options(**defaults)
|
||||||
|
|
||||||
|
|
||||||
def load_config(path):
|
def load_config(path: str) -> Options:
|
||||||
if not path:
|
if not path:
|
||||||
return defaults
|
return defaults
|
||||||
ans = defaults._asdict()
|
ans = defaults._asdict()
|
||||||
@ -80,10 +104,24 @@ def load_config(path):
|
|||||||
if m is not None:
|
if m is not None:
|
||||||
key, val = m.groups()
|
key, val = m.groups()
|
||||||
if key in ans:
|
if key in ans:
|
||||||
|
tm = type_map.get(key)
|
||||||
|
if tm is not None:
|
||||||
|
val = tm(val)
|
||||||
ans[key] = val
|
ans[key] = val
|
||||||
return Options(**ans)
|
return Options(**ans)
|
||||||
|
|
||||||
|
|
||||||
def validate_font(opts):
|
def validate_font(opts: Options):
|
||||||
if not QFontInfo(QFont(opts.font_family)).fixedPitch():
|
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))
|
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
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
from typing import Tuple, Dict, Union
|
||||||
|
|
||||||
from numpy import zeros, dtype
|
from numpy import zeros, dtype
|
||||||
|
from PyQt5.QtGui import QColor
|
||||||
|
|
||||||
color_type = dtype([('type', 'u1'), ('r', 'u1'), ('g', 'u1'), ('b', 'u1')])
|
color_type = dtype([('type', 'u1'), ('r', 'u1'), ('g', 'u1'), ('b', 'u1')])
|
||||||
|
|
||||||
@ -22,3 +25,11 @@ class Line:
|
|||||||
self.strikethrough = zeros(sz, bool)
|
self.strikethrough = zeros(sz, bool)
|
||||||
self.decoration = zeros(sz, 'u1')
|
self.decoration = zeros(sz, 'u1')
|
||||||
self.decoration_fg = zeros(sz, color_type)
|
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 .config import load_config, validate_font
|
||||||
from .constants import appname, str_version, config_dir
|
from .constants import appname, str_version, config_dir
|
||||||
from .term import TerminalWidget
|
from .boss import Boss
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
@ -24,8 +24,8 @@ class MainWindow(QMainWindow):
|
|||||||
self.setWindowTitle(appname)
|
self.setWindowTitle(appname)
|
||||||
sys.excepthook = self.on_unhandled_error
|
sys.excepthook = self.on_unhandled_error
|
||||||
self.handle_unix_signals()
|
self.handle_unix_signals()
|
||||||
self.terminal = TerminalWidget(opts, self)
|
self.boss = Boss(opts, self)
|
||||||
self.setCentralWidget(self.terminal)
|
self.setCentralWidget(self.boss.term)
|
||||||
|
|
||||||
def on_unhandled_error(self, etype, value, tb):
|
def on_unhandled_error(self, etype, value, tb):
|
||||||
if etype == KeyboardInterrupt:
|
if etype == KeyboardInterrupt:
|
||||||
@ -35,7 +35,6 @@ class MainWindow(QMainWindow):
|
|||||||
msg = str(value)
|
msg = str(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = repr(value)
|
msg = repr(value)
|
||||||
msg = '<p>' + msg + '<br>' + _('Click "Show details" for more information')
|
|
||||||
QMessageBox.critical(self, _('Unhandled exception'), msg)
|
QMessageBox.critical(self, _('Unhandled exception'), msg)
|
||||||
|
|
||||||
def handle_unix_signals(self):
|
def handle_unix_signals(self):
|
||||||
|
|||||||
106
kitty/term.py
106
kitty/term.py
@ -2,10 +2,114 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# 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 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):
|
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)
|
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