Start work on screen implementation
This commit is contained in:
parent
23bc25eb64
commit
0259d88f37
@ -2,11 +2,9 @@
|
||||
# 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 .data_types import Line, rewrap_lines
|
||||
from .screen import Screen
|
||||
from .term import TerminalWidget
|
||||
|
||||
|
||||
@ -14,19 +12,13 @@ class Boss(QObject):
|
||||
|
||||
def __init__(self, opts, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.linebuf = deque(maxlen=max(1000, opts.scrollback_lines))
|
||||
self.term = TerminalWidget(opts, self.linebuf, parent)
|
||||
self.screen = Screen(opts, parent=self)
|
||||
self.term = TerminalWidget(opts, self.screen.linebuf, parent)
|
||||
self.term.relayout_lines.connect(self.relayout_lines)
|
||||
|
||||
def apply_opts(self, opts):
|
||||
if opts.scrollback_lines != self.linebuf.maxlen:
|
||||
self.linebuf = deque(self.linebuf, maxlen=max(1000, opts.scrollback_lines))
|
||||
self.term.linebuf = self.linebuf
|
||||
self.screen.apply_opts(opts)
|
||||
self.term.apply_opts(opts)
|
||||
|
||||
def relayout_lines(self, previous, cells_per_line):
|
||||
if previous == cells_per_line:
|
||||
return
|
||||
old = self.linebuf.copy()
|
||||
self.linebuf.clear()
|
||||
self.linebuf.extend(rewrap_lines(old, cells_per_line))
|
||||
def relayout_lines(self, previous, cells_per_line, previousl, lines_per_screen):
|
||||
self.screen.resize(lines_per_screen, cells_per_line)
|
||||
|
||||
@ -17,32 +17,79 @@ def get_zeroes(sz: int) -> Tuple[array.array]:
|
||||
get_zeroes.ans = (
|
||||
array.array('B', 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
|
||||
return ans
|
||||
|
||||
|
||||
class Line:
|
||||
|
||||
__slots__ = 'char fg bg bold italic reverse strikethrough decoration decoration_fg width'.split()
|
||||
continued = False
|
||||
|
||||
def __init__(self, sz: int):
|
||||
z1, z4 = get_zeroes(sz)
|
||||
self.char = z4[:]
|
||||
self.fg = z4[:]
|
||||
self.bg = z4[:]
|
||||
self.bold = z1[:]
|
||||
self.italic = z1[:]
|
||||
self.reverse = z1[:]
|
||||
self.strikethrough = z1[:]
|
||||
self.decoration = z1[:]
|
||||
self.decoration_fg = z4[:]
|
||||
self.width = z1[:]
|
||||
def __init__(self, sz: int, other=None):
|
||||
if other is None:
|
||||
z1, z4, spaces = get_zeroes(sz)
|
||||
self.char = spaces[:]
|
||||
self.fg = z4[:]
|
||||
self.bg = z4[:]
|
||||
self.bold = z1[:]
|
||||
self.italic = z1[:]
|
||||
self.reverse = z1[:]
|
||||
self.strikethrough = z1[:]
|
||||
self.decoration = z1[:]
|
||||
self.decoration_fg = z4[:]
|
||||
self.width = z1[:]
|
||||
else:
|
||||
self.char = other.char[:]
|
||||
self.fg = other.fg[:]
|
||||
self.bg = other.bg[:]
|
||||
self.bold = other.bold[:]
|
||||
self.italic = other.italic[:]
|
||||
self.reverse = other.reverse[:]
|
||||
self.strikethrough = other.strikethrough[:]
|
||||
self.decoration = other.decoration[:]
|
||||
self.decoration_fg = other.decoration_fg[:]
|
||||
self.width = other.width[:]
|
||||
|
||||
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.fg[dest] = self.fg[src]
|
||||
@ -55,8 +102,53 @@ class Line:
|
||||
to.decoration_fg[dest] = self.decoration_fg[src]
|
||||
to.width[dest] = self.width[src]
|
||||
|
||||
def apply_cursor(self, c: Cursor, at: int=0, num: int=1, clear_char=False, char=' ') -> None:
|
||||
if num < 2:
|
||||
self.fg[at] = c.fg
|
||||
self.bg[at] = c.bg
|
||||
self.bold[at] = c.bold
|
||||
self.italic[at] = c.italic
|
||||
self.reverse[at] = c.reverse
|
||||
self.strikethrough[at] = c.strikethrough
|
||||
self.decoration[at] = c.decoration
|
||||
self.decoration_fg[at] = c.decoration_fg
|
||||
if clear_char:
|
||||
self.width[at], self.char[at] = 1, ord(char)
|
||||
else:
|
||||
num = min(len(self) - at, num)
|
||||
at = slice(at, at + num)
|
||||
self.fg[at] = repeat(c.fg, num)
|
||||
self.bg[at] = repeat(c.bg, num)
|
||||
self.bold[at] = repeat(c.bold, num)
|
||||
self.italic[at] = repeat(c.italic, num)
|
||||
self.reverse[at] = repeat(c.reverse, num)
|
||||
self.strikethrough[at] = repeat(c.strikethrough, num)
|
||||
self.decoration[at] = repeat(c.decoration, num)
|
||||
self.decoration_fg[at] = repeat(c.decoration_fg, num)
|
||||
if clear_char:
|
||||
self.width[at], self.char[at] = repeat(1, num), repeat(ord(char), num)
|
||||
|
||||
def copy_slice(self, src, dest, num):
|
||||
src, dest = slice(src, src + num), slice(dest, dest + num)
|
||||
for a in (self.char, self.fg, self.bg, self.bold, self.italic, self.reverse, self.strikethrough, self.decoration, self.decoration_fg, self.width):
|
||||
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(ord, self.char)).rstrip('\0')
|
||||
return ''.join(map(ord, filter(None, self.char)))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(str(self))
|
||||
|
||||
872
kitty/screen.py
Normal file
872
kitty/screen.py
Normal file
@ -0,0 +1,872 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import codecs
|
||||
import unicodedata
|
||||
from collections import deque, namedtuple
|
||||
from typing import Sequence
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
|
||||
from pyte import charsets as cs, control as ctrl, graphics as g, modes as mo
|
||||
from .data_types import Line, Cursor, rewrap_lines
|
||||
from .utils import wcwidth
|
||||
|
||||
|
||||
#: A container for screen's scroll margins.
|
||||
Margins = namedtuple("Margins", "top bottom")
|
||||
|
||||
#: A container for savepoint, created on :data:`~pyte.escape.DECSC`.
|
||||
Savepoint = namedtuple("Savepoint", [
|
||||
"cursor",
|
||||
"g0_charset",
|
||||
"g1_charset",
|
||||
"charset",
|
||||
"use_utf8",
|
||||
"origin",
|
||||
"wrap"
|
||||
])
|
||||
|
||||
|
||||
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()
|
||||
_notify_cursor_position = True
|
||||
|
||||
def __init__(self, opts, columns: int=80, lines: int=24, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.savepoints = deque()
|
||||
self.columns = columns
|
||||
self.lines = lines
|
||||
self.linebuf = []
|
||||
sz = max(1000, opts.scrollback_lines)
|
||||
self.tophistorybuf = deque(maxlen=sz)
|
||||
self.reset()
|
||||
|
||||
def apply_opts(self, opts):
|
||||
sz = max(1000, opts.scrollback_lines)
|
||||
if sz != self.tophistorybuf.maxlen:
|
||||
self.tophistorybuf = deque(self.tophistorybuf, maxlen=sz)
|
||||
|
||||
def __repr__(self):
|
||||
return ("{0}({1}, {2})".format(self.__class__.__name__,
|
||||
self.columns, self.lines))
|
||||
|
||||
def notify_cursor_position(self, x, y):
|
||||
if self._notify_cursor_position:
|
||||
self.cursor_position_changed.emit(self.cursor, x, y)
|
||||
|
||||
@property
|
||||
def display(self) -> Sequence[str]:
|
||||
return [str(l) for l in self.linebuf]
|
||||
|
||||
def reset(self):
|
||||
"""Resets the terminal to its initial state.
|
||||
|
||||
* Scroll margins are reset to screen boundaries.
|
||||
* Cursor is moved to home location -- ``(0, 0)`` and its
|
||||
attributes are set to defaults (see :attr:`default_char`).
|
||||
* Screen is cleared -- each character is reset to
|
||||
:attr:`default_char`.
|
||||
* Tabstops are reset to "every eight columns".
|
||||
|
||||
.. note::
|
||||
|
||||
Neither VT220 nor VT102 manuals mentioned that terminal modes
|
||||
and tabstops should be reset as well, thanks to
|
||||
:manpage:`xterm` -- we now know that.
|
||||
"""
|
||||
self.linebuf.clear()
|
||||
self.linebuf[:] = (Line(self.columns) for i in range(self.lines))
|
||||
self.mode = {mo.DECAWM, mo.DECTCEM}
|
||||
self.margins = Margins(0, self.lines - 1)
|
||||
|
||||
self.charset = 0
|
||||
self.g0_charset = cs.LAT1_MAP
|
||||
self.g1_charset = cs.VT100_MAP
|
||||
self.use_utf8 = True
|
||||
self.utf8_decoder = codecs.getincrementaldecoder("utf-8")("replace")
|
||||
|
||||
# From ``man terminfo`` -- "... hardware tabs are initially
|
||||
# set every `n` spaces when the terminal is powered up. Since
|
||||
# we aim to support VT102 / VT220 and linux -- we use n = 8.
|
||||
self.tabstops = set(range(7, self.columns, 8))
|
||||
|
||||
self.cursor = Cursor(0, 0)
|
||||
self.cursor_changed.emit(self.cursor)
|
||||
self.cursor_position()
|
||||
|
||||
def resize(self, lines: int, columns: int):
|
||||
"""Resize the screen to the given dimensions.
|
||||
|
||||
.. note:: According to `xterm`, we should also reset origin
|
||||
mode and screen margins, see ``xterm/screen.c:1761``.
|
||||
|
||||
"""
|
||||
for hb in (self.tophistorybuf, ):
|
||||
old = hb.copy()
|
||||
hb.clear(), hb.extend(rewrap_lines(old, columns))
|
||||
old_lines = self.linebuf[:]
|
||||
self.linebuf.clear()
|
||||
self.lines, self.columns = lines, columns
|
||||
self.linebuf[:] = rewrap_lines(old_lines, self.columns)
|
||||
extra = len(self.linebuf) - self.lines
|
||||
if extra > 0:
|
||||
self.tophistorybuf.extend(self.linebuf[:extra])
|
||||
del self.linebuf[:extra]
|
||||
self.margins = Margins(0, self.lines - 1)
|
||||
self.reset_mode(mo.DECOM)
|
||||
self.ensure_bounds()
|
||||
|
||||
def set_margins(self, top=None, bottom=None):
|
||||
"""Selects top and bottom margins for the scrolling region.
|
||||
|
||||
Margins determine which screen lines move during scrolling
|
||||
(see :meth:`index` and :meth:`reverse_index`). Characters added
|
||||
outside the scrolling region do not cause the screen to scroll.
|
||||
|
||||
:param int top: the smallest line number that is scrolled.
|
||||
:param int bottom: the biggest line number that is scrolled.
|
||||
"""
|
||||
if top is None or bottom is None:
|
||||
return
|
||||
|
||||
# Arguments are 1-based, while :attr:`margins` are zero based --
|
||||
# so we have to decrement them by one. We also make sure that
|
||||
# both of them is bounded by [0, lines - 1].
|
||||
top = max(0, min(top - 1, self.lines - 1))
|
||||
bottom = max(0, min(bottom - 1, self.lines - 1))
|
||||
|
||||
# Even though VT102 and VT220 require DECSTBM to ignore regions
|
||||
# of width less than 2, some programs (like aptitude for example)
|
||||
# rely on it. Practicality beats purity.
|
||||
if bottom - top >= 1:
|
||||
self.margins = Margins(top, bottom)
|
||||
|
||||
# The cursor moves to the home position when the top and
|
||||
# bottom margins of the scrolling region (DECSTBM) changes.
|
||||
self.cursor_position()
|
||||
|
||||
def set_mode(self, *modes, private=False):
|
||||
"""Sets (enables) a given list of modes.
|
||||
|
||||
:param list modes: modes to set, where each mode is a constant
|
||||
from :mod:`pyte.modes`.
|
||||
"""
|
||||
# Private mode codes are shifted, to be distingiushed from non
|
||||
# private ones.
|
||||
if private:
|
||||
modes = [mode << 5 for mode in modes]
|
||||
|
||||
self.mode.update(modes)
|
||||
|
||||
# When DECOLM mode is set, the screen is erased and the cursor
|
||||
# moves to the home position.
|
||||
if mo.DECCOLM in modes:
|
||||
# self.resize(columns=132) Disabled since we only allow resizing
|
||||
# by the user
|
||||
self.erase_in_display(2)
|
||||
self.cursor_position()
|
||||
|
||||
# According to `vttest`, DECOM should also home the cursor, see
|
||||
# vttest/main.c:303.
|
||||
if mo.DECOM in modes:
|
||||
self.cursor_position()
|
||||
|
||||
# Mark all displayed characters as reverse.
|
||||
if mo.DECSCNM in modes:
|
||||
for line in self.linebuf:
|
||||
for i in range(len(line)):
|
||||
line.reverse[i] = True
|
||||
self.update_screen.emit()
|
||||
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)
|
||||
|
||||
def reset_mode(self, *modes, private=False):
|
||||
"""Resets (disables) a given list of modes.
|
||||
|
||||
:param list modes: modes to reset -- hopefully, each mode is a
|
||||
constant from :mod:`pyte.modes`.
|
||||
"""
|
||||
# Private mode codes are shifted, to be distinguished from non
|
||||
# private ones.
|
||||
if private:
|
||||
modes = [mode << 5 for mode in modes]
|
||||
|
||||
self.mode.difference_update(modes)
|
||||
|
||||
# Lines below follow the logic in :meth:`set_mode`.
|
||||
if mo.DECCOLM in modes:
|
||||
# self.resize(columns=80) Disabled since we only allow resizing by
|
||||
# the user
|
||||
self.erase_in_display(2)
|
||||
self.cursor_position()
|
||||
|
||||
if mo.DECOM in modes:
|
||||
self.cursor_position()
|
||||
|
||||
if mo.DECSCNM in modes:
|
||||
for line in self.linebuf:
|
||||
for i in range(len(line)):
|
||||
line.reverse[i] = False
|
||||
self.update_screen.emit()
|
||||
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)
|
||||
|
||||
def define_charset(self, code, mode):
|
||||
"""Defines ``G0`` or ``G1`` charset.
|
||||
|
||||
:param str code: character set code, should be a character
|
||||
from ``b"B0UK"``, otherwise ignored.
|
||||
:param str mode: if ``"("`` ``G0`` charset is defined, if
|
||||
``")"`` -- we operate on ``G1``.
|
||||
|
||||
.. warning:: User-defined charsets are currently not supported.
|
||||
"""
|
||||
if code in cs.MAPS:
|
||||
if mode == b"(":
|
||||
self.g0_charset = cs.MAPS[code]
|
||||
elif mode == b")":
|
||||
self.g1_charset = cs.MAPS[code]
|
||||
|
||||
def shift_in(self):
|
||||
"""Selects ``G0`` character set."""
|
||||
self.charset = 0
|
||||
|
||||
def shift_out(self):
|
||||
"""Selects ``G1`` character set."""
|
||||
self.charset = 1
|
||||
|
||||
def select_other_charset(self, code):
|
||||
"""Selects other (non G0 or G1) charset.
|
||||
|
||||
:param str code: character set code, should be a character from
|
||||
``b"@G8"``, otherwise ignored.
|
||||
|
||||
.. note:: We currently follow ``"linux"`` and only use this
|
||||
command to switch from ISO-8859-1 to UTF-8 and back.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Standard ECMA-35, Section 15.4 \
|
||||
<http://www.ecma-international.org/publications/standards/Ecma-035.htm>`_
|
||||
for a description of VTXXX character set machinery.
|
||||
"""
|
||||
if code == b"@":
|
||||
self.use_utf8 = False
|
||||
self.utf8_decoder.reset()
|
||||
elif code in b"G8":
|
||||
self.use_utf8 = True
|
||||
|
||||
def _decode(self, data):
|
||||
"""Decodes bytes to text according to the selected charset.
|
||||
|
||||
:param bytes data: bytes to decode.
|
||||
"""
|
||||
if self.charset:
|
||||
return "".join(self.g1_charset[b] for b in data)
|
||||
if self.use_utf8:
|
||||
return self.utf8_decoder.decode(data)
|
||||
return "".join(self.g0_charset[b] for b in data)
|
||||
|
||||
def draw(self, data: bytes):
|
||||
""" Displays decoded characters at the current cursor position and
|
||||
creates new lines as need if DECAWM is set. """
|
||||
orig_x, orig_y = self.cursor.x, self.cursor.y
|
||||
self._notify_cursor_position = False
|
||||
try:
|
||||
for char in self._decode(data):
|
||||
if unicodedata.category(char) in ('Cc', 'Cf', 'Cn', 'Cs'):
|
||||
continue
|
||||
char_width = wcwidth(char)
|
||||
|
||||
# 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:
|
||||
if mo.DECAWM in self.mode:
|
||||
self.carriage_return()
|
||||
self.linefeed()
|
||||
self.linebuf[self.cursor.y].continued = True
|
||||
else:
|
||||
extra = self.cursor.x + char_width + 1 - self.columns
|
||||
self.cursor.x -= extra
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
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])
|
||||
normalized = unicodedata.normalize("NFC", last + char)
|
||||
line.char[cx - 1] = ord(normalized)
|
||||
elif self.cursor.y:
|
||||
lline = self.linebuf[self.cursor.y - 1]
|
||||
last = chr(lline.char[self.columns - 1])
|
||||
normalized = unicodedata.normalize("NFC", last + char)
|
||||
lline.char[self.columns - 1] = ord(normalized)
|
||||
|
||||
# .. 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)
|
||||
finally:
|
||||
self._notify_cursor_position = True
|
||||
if orig_x != self.cursor.x or orig_y != self.cursor.y:
|
||||
self.notify_cursor_position(orig_x, orig_y)
|
||||
|
||||
def set_title(self, param):
|
||||
"""Sets terminal title.
|
||||
|
||||
.. note:: This is an XTerm extension supported by the Linux terminal.
|
||||
"""
|
||||
self.title_changed.emit(self._decode(param))
|
||||
|
||||
def set_icon_name(self, param):
|
||||
"""Sets icon name.
|
||||
|
||||
.. note:: This is an XTerm extension supported by the Linux terminal.
|
||||
"""
|
||||
self.icon_changed.emit(self._decode(param))
|
||||
|
||||
def carriage_return(self):
|
||||
"""Move the cursor to the beginning of the current line."""
|
||||
x, self.cursor.x = self.cursor.x, 0
|
||||
if x != self.cursor.x:
|
||||
self.notify_cursor_position(x, self.cursor.y)
|
||||
|
||||
def index(self):
|
||||
"""Move the cursor down one line in the same column. If the
|
||||
cursor is at the last line, create a new line at the bottom.
|
||||
"""
|
||||
top, bottom = self.margins
|
||||
|
||||
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()
|
||||
else:
|
||||
self.cursor_down()
|
||||
|
||||
def reverse_index(self):
|
||||
"""Move the cursor up one line in the same column. If the cursor
|
||||
is at the first line, create a new line at the top.
|
||||
"""
|
||||
top, bottom = self.margins
|
||||
|
||||
if self.cursor.y == top:
|
||||
self.linebuf.pop(bottom)
|
||||
self.linebuf.insert(top, Line(self.columns))
|
||||
self.update_screen.emit()
|
||||
else:
|
||||
self.cursor_up()
|
||||
|
||||
def linefeed(self):
|
||||
"""Performs an index and, if :data:`~pyte.modes.LNM` is set, a
|
||||
carriage return.
|
||||
"""
|
||||
self.index()
|
||||
|
||||
if mo.LNM in self.mode:
|
||||
self.carriage_return()
|
||||
|
||||
self.ensure_bounds()
|
||||
|
||||
def tab(self):
|
||||
"""Move to the next tab space, or the end of the screen if there
|
||||
aren't anymore left.
|
||||
"""
|
||||
for stop in sorted(self.tabstops):
|
||||
if self.cursor.x < stop:
|
||||
column = stop
|
||||
break
|
||||
else:
|
||||
column = self.columns - 1
|
||||
|
||||
if column != self.cursor.x:
|
||||
x, self.cursor.x = self.cursor.x, column
|
||||
self.notify_cursor_position(x, self.cursor.y)
|
||||
|
||||
def backspace(self):
|
||||
"""Move cursor to the left one or keep it in it's position if
|
||||
it's at the beginning of the line already.
|
||||
"""
|
||||
self.cursor_back()
|
||||
|
||||
def save_cursor(self):
|
||||
"""Push the current cursor position onto the stack."""
|
||||
self.savepoints.append(Savepoint(self.cursor.copy(),
|
||||
self.g0_charset,
|
||||
self.g1_charset,
|
||||
self.charset,
|
||||
self.use_utf8,
|
||||
mo.DECOM in self.mode,
|
||||
mo.DECAWM in self.mode))
|
||||
|
||||
def restore_cursor(self):
|
||||
"""Set the current cursor position to whatever cursor is on top
|
||||
of the stack.
|
||||
"""
|
||||
if self.savepoints:
|
||||
savepoint = self.savepoints.pop()
|
||||
|
||||
self.g0_charset = savepoint.g0_charset
|
||||
self.g1_charset = savepoint.g1_charset
|
||||
self.charset = savepoint.charset
|
||||
self.use_utf8 = savepoint.use_utf8
|
||||
|
||||
if savepoint.origin:
|
||||
self.set_mode(mo.DECOM)
|
||||
if savepoint.wrap:
|
||||
self.set_mode(mo.DECAWM)
|
||||
|
||||
self.cursor = savepoint.cursor
|
||||
self.cursor_changed.emit(self.cursor)
|
||||
self.ensure_bounds(use_margins=True)
|
||||
else:
|
||||
# If nothing was saved, the cursor moves to home position;
|
||||
# origin mode is reset. TODO: DECAWM?
|
||||
self.reset_mode(mo.DECOM)
|
||||
self.cursor_position()
|
||||
|
||||
def insert_lines(self, count=1):
|
||||
"""Inserts the indicated # of lines at line with cursor. Lines
|
||||
displayed **at** and below the cursor move down. Lines moved
|
||||
past the bottom margin are lost.
|
||||
|
||||
:param count: number of lines to delete.
|
||||
"""
|
||||
count = count or 1
|
||||
top, bottom = self.margins
|
||||
|
||||
# If cursor is outside scrolling margins -- do nothin'.
|
||||
if top <= self.cursor.y <= bottom:
|
||||
# v +1, because range() is exclusive.
|
||||
for line in range(self.cursor.y,
|
||||
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.carriage_return()
|
||||
|
||||
def delete_lines(self, count=1):
|
||||
"""Deletes the indicated # of lines, starting at line with
|
||||
cursor. As lines are deleted, lines displayed below cursor
|
||||
move up.
|
||||
|
||||
:param int count: number of lines to delete.
|
||||
"""
|
||||
count = count or 1
|
||||
top, bottom = self.margins
|
||||
|
||||
# If cursor is outside scrolling margins it -- do nothin'.
|
||||
if top <= self.cursor.y <= bottom:
|
||||
# v -- +1 to include the bottom margin.
|
||||
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.carriage_return()
|
||||
|
||||
def insert_characters(self, count=1):
|
||||
"""Inserts the indicated # of blank characters at the cursor
|
||||
position. The cursor does not move and remains at the beginning
|
||||
of the inserted blank characters. Data on the line is shifted
|
||||
forward.
|
||||
|
||||
:param int count: number of characters to insert.
|
||||
"""
|
||||
count = count or 1
|
||||
top, bottom = self.margins
|
||||
|
||||
y = self.cursor.y
|
||||
if top <= y <= bottom:
|
||||
x = self.cursor.x
|
||||
num = min(self.columns - x, count)
|
||||
line = self.linebuf[y]
|
||||
line.right_shift(x, num)
|
||||
line.apply_cursor(self.cursor, x, num, clear_char=True)
|
||||
self.update_cell_range(y, x, self.columns)
|
||||
|
||||
def delete_characters(self, count=1):
|
||||
"""Deletes the indicated # of characters, starting with the
|
||||
character at cursor position. When a character is deleted, all
|
||||
characters to the right of cursor move left. Character attributes
|
||||
move with the characters.
|
||||
|
||||
:param int count: number of characters to delete.
|
||||
"""
|
||||
count = count or 1
|
||||
top, bottom = self.margins
|
||||
|
||||
y = self.cursor.y
|
||||
if top <= y <= bottom:
|
||||
x = self.cursor.x
|
||||
num = min(self.columns - x, count)
|
||||
line = self.linebuf[y]
|
||||
line.left_shift(x, num)
|
||||
line.apply_cursor(self.cursor, self.columns - num, num, clear_char=True)
|
||||
|
||||
def erase_characters(self, count=None):
|
||||
"""Erases the indicated # of characters, starting with the
|
||||
character at cursor position. Character attributes are set
|
||||
cursor attributes. The cursor remains in the same position.
|
||||
|
||||
:param int count: number of characters to erase.
|
||||
|
||||
.. warning::
|
||||
|
||||
Even though *ALL* of the VTXXX manuals state that character
|
||||
attributes **should be reset to defaults**, ``libvte``,
|
||||
``xterm`` and ``ROTE`` completely ignore this. Same applies
|
||||
to all ``erase_*()`` and ``delete_*()`` methods.
|
||||
"""
|
||||
count = count or 1
|
||||
|
||||
for column in range(self.cursor.x,
|
||||
min(self.cursor.x + count, self.columns)):
|
||||
self.buffer[self.cursor.y][column] = self.cursor.attrs
|
||||
|
||||
def erase_in_line(self, how=0, private=False):
|
||||
"""Erases a line in a specific way.
|
||||
|
||||
:param int how: defines the way the line should be erased in:
|
||||
|
||||
* ``0`` -- Erases from cursor to end of line, including cursor
|
||||
position.
|
||||
* ``1`` -- Erases from beginning of line to cursor,
|
||||
including cursor position.
|
||||
* ``2`` -- Erases complete line.
|
||||
:param bool private: when ``True`` character attributes are left
|
||||
unchanged **not implemented**.
|
||||
"""
|
||||
if how == 0:
|
||||
# a) erase from the cursor to the end of line, including
|
||||
# the cursor,
|
||||
interval = range(self.cursor.x, self.columns)
|
||||
elif how == 1:
|
||||
# b) erase from the beginning of the line to the cursor,
|
||||
# including it,
|
||||
interval = range(self.cursor.x + 1)
|
||||
elif how == 2:
|
||||
# c) erase the entire line.
|
||||
interval = range(self.columns)
|
||||
|
||||
for column in interval:
|
||||
self.buffer[self.cursor.y][column] = self.cursor.attrs
|
||||
|
||||
def erase_in_display(self, how=0, private=False):
|
||||
"""Erases display in a specific way.
|
||||
|
||||
:param int how: defines the way the line should be erased in:
|
||||
|
||||
* ``0`` -- Erases from cursor to end of screen, including
|
||||
cursor position.
|
||||
* ``1`` -- Erases from beginning of screen to cursor,
|
||||
including cursor position.
|
||||
* ``2`` -- Erases complete display. All lines are erased
|
||||
and changed to single-width. Cursor does not move.
|
||||
:param bool private: when ``True`` character attributes are left
|
||||
unchanged **not implemented**.
|
||||
"""
|
||||
if how == 0:
|
||||
# a) erase from cursor to the end of the display, including
|
||||
# the cursor,
|
||||
interval = range(self.cursor.y + 1, self.lines)
|
||||
elif how == 1:
|
||||
# b) erase from the beginning of the display to the cursor,
|
||||
# including it,
|
||||
interval = range(self.cursor.y)
|
||||
elif how == 2:
|
||||
# c) erase the whole display.
|
||||
interval = range(self.lines)
|
||||
|
||||
for line in interval:
|
||||
self.buffer[line][:] = \
|
||||
(self.cursor.attrs for _ in range(self.columns))
|
||||
|
||||
# In case of 0 or 1 we have to erase the line with the cursor.
|
||||
if how == 0 or how == 1:
|
||||
self.erase_in_line(how)
|
||||
|
||||
def set_tab_stop(self):
|
||||
"""Sets a horizontal tab stop at cursor position."""
|
||||
self.tabstops.add(self.cursor.x)
|
||||
|
||||
def clear_tab_stop(self, how=0):
|
||||
"""Clears a horizontal tab stop.
|
||||
|
||||
:param int how: defines a way the tab stop should be cleared:
|
||||
|
||||
* ``0`` or nothing -- Clears a horizontal tab stop at cursor
|
||||
position.
|
||||
* ``3`` -- Clears all horizontal tab stops.
|
||||
"""
|
||||
if how == 0:
|
||||
# Clears a horizontal tab stop at cursor position, if it's
|
||||
# present, or silently fails if otherwise.
|
||||
self.tabstops.discard(self.cursor.x)
|
||||
elif how == 3:
|
||||
self.tabstops = set() # Clears all horizontal tab stops.
|
||||
|
||||
def ensure_bounds(self, use_margins=False):
|
||||
"""Ensure that current cursor position is within screen bounds.
|
||||
|
||||
:param bool use_margins: when ``True`` or when
|
||||
:data:`~pyte.modes.DECOM` is set,
|
||||
cursor is bounded by top and and bottom
|
||||
margins, instead of ``[0; lines - 1]``.
|
||||
"""
|
||||
if use_margins or mo.DECOM in self.mode:
|
||||
top, bottom = self.margins
|
||||
else:
|
||||
top, bottom = 0, self.lines - 1
|
||||
|
||||
self.cursor.x = max(0, min(self.cursor.x, self.columns - 1))
|
||||
self.cursor.y = max(top, min(self.cursor.y, bottom))
|
||||
|
||||
def cursor_up(self, count=None):
|
||||
"""Moves cursor up the indicated # of lines in same column.
|
||||
Cursor stops at top margin.
|
||||
|
||||
:param int count: number of lines to skip.
|
||||
"""
|
||||
self.cursor.y -= count or 1
|
||||
self.ensure_bounds(use_margins=True)
|
||||
|
||||
def cursor_up1(self, count=None):
|
||||
"""Moves cursor up the indicated # of lines to column 1. Cursor
|
||||
stops at bottom margin.
|
||||
|
||||
:param int count: number of lines to skip.
|
||||
"""
|
||||
self.cursor_up(count)
|
||||
self.carriage_return()
|
||||
|
||||
def cursor_down(self, count=None):
|
||||
"""Moves cursor down the indicated # of lines in same column.
|
||||
Cursor stops at bottom margin.
|
||||
|
||||
:param int count: number of lines to skip.
|
||||
"""
|
||||
self.cursor.y += count or 1
|
||||
self.ensure_bounds(use_margins=True)
|
||||
|
||||
def cursor_down1(self, count=None):
|
||||
"""Moves cursor down the indicated # of lines to column 1.
|
||||
Cursor stops at bottom margin.
|
||||
|
||||
:param int count: number of lines to skip.
|
||||
"""
|
||||
self.cursor_down(count)
|
||||
self.carriage_return()
|
||||
|
||||
def cursor_back(self, count=None):
|
||||
"""Moves cursor left the indicated # of columns. Cursor stops
|
||||
at left margin.
|
||||
|
||||
:param int count: number of columns to skip.
|
||||
"""
|
||||
self.cursor.x -= count or 1
|
||||
self.ensure_bounds()
|
||||
|
||||
def cursor_forward(self, count=None):
|
||||
"""Moves cursor right the indicated # of columns. Cursor stops
|
||||
at right margin.
|
||||
|
||||
:param int count: number of columns to skip.
|
||||
"""
|
||||
self.cursor.x += count or 1
|
||||
self.ensure_bounds()
|
||||
|
||||
def cursor_position(self, line=None, column=None):
|
||||
"""Set the cursor to a specific `line` and `column`.
|
||||
|
||||
Cursor is allowed to move out of the scrolling region only when
|
||||
:data:`~pyte.modes.DECOM` is reset, otherwise -- the position
|
||||
doesn't change.
|
||||
|
||||
:param int line: line number to move the cursor to.
|
||||
:param int column: column number to move the cursor to.
|
||||
"""
|
||||
column = (column or 1) - 1
|
||||
line = (line or 1) - 1
|
||||
|
||||
# If origin mode (DECOM) is set, line number are relative to
|
||||
# the top scrolling margin.
|
||||
if mo.DECOM in self.mode:
|
||||
line += self.margins.top
|
||||
|
||||
# Cursor is not allowed to move out of the scrolling region.
|
||||
if not self.margins.top <= line <= self.margins.bottom:
|
||||
return
|
||||
|
||||
self.cursor.x, self.cursor.y = column, line
|
||||
self.ensure_bounds()
|
||||
|
||||
def cursor_to_column(self, column=None):
|
||||
"""Moves cursor to a specific column in the current line.
|
||||
|
||||
:param int column: column number to move the cursor to.
|
||||
"""
|
||||
self.cursor.x = (column or 1) - 1
|
||||
self.ensure_bounds()
|
||||
|
||||
def cursor_to_line(self, line=None):
|
||||
"""Moves cursor to a specific line in the current column.
|
||||
|
||||
:param int line: line number to move the cursor to.
|
||||
"""
|
||||
self.cursor.y = (line or 1) - 1
|
||||
|
||||
# If origin mode (DECOM) is set, line number are relative to
|
||||
# the top scrolling margin.
|
||||
if mo.DECOM in self.mode:
|
||||
self.cursor.y += self.margins.top
|
||||
|
||||
# TODO: should we also restrict the cursor to the scrolling
|
||||
# region?
|
||||
|
||||
self.ensure_bounds()
|
||||
|
||||
def bell(self, *args):
|
||||
"""Bell stub -- the actual implementation should probably be
|
||||
provided by the end-user.
|
||||
"""
|
||||
|
||||
def alignment_display(self):
|
||||
"""Fills screen with uppercase E's for screen focus and alignment."""
|
||||
for line in self.buffer:
|
||||
for column, char in enumerate(line):
|
||||
line[column] = char._replace(data="E")
|
||||
|
||||
def select_graphic_rendition(self, *attrs):
|
||||
"""Set display attributes.
|
||||
|
||||
:param list attrs: a list of display attributes to set.
|
||||
"""
|
||||
replace = {}
|
||||
|
||||
if not attrs:
|
||||
attrs = [0]
|
||||
else:
|
||||
attrs = list(reversed(attrs))
|
||||
|
||||
while attrs:
|
||||
attr = attrs.pop()
|
||||
if attr in g.FG_ANSI:
|
||||
replace["fg"] = g.FG_ANSI[attr]
|
||||
elif attr in g.BG:
|
||||
replace["bg"] = g.BG_ANSI[attr]
|
||||
elif attr in g.TEXT:
|
||||
attr = g.TEXT[attr]
|
||||
replace[attr[1:]] = attr.startswith("+")
|
||||
elif not attr:
|
||||
replace = self.default_char._asdict()
|
||||
elif attr in g.FG_AIXTERM:
|
||||
replace.update(fg=g.FG_AIXTERM[attr], bold=True)
|
||||
elif attr in g.BG_AIXTERM:
|
||||
replace.update(bg=g.BG_AIXTERM[attr], bold=True)
|
||||
elif attr in (g.FG_256, g.BG_256):
|
||||
key = "fg" if attr == g.FG_256 else "bg"
|
||||
n = attrs.pop()
|
||||
try:
|
||||
if n == 5: # 256.
|
||||
m = attrs.pop()
|
||||
replace[key] = g.FG_BG_256[m]
|
||||
elif n == 2: # 24bit.
|
||||
# This is somewhat non-standard but is nonetheless
|
||||
# supported in quite a few terminals. See discussion
|
||||
# here https://gist.github.com/XVilka/8346728.
|
||||
replace[key] = "{0:02x}{1:02x}{2:02x}".format(
|
||||
attrs.pop(), attrs.pop(), attrs.pop())
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
self.cursor.attrs = self.cursor.attrs._replace(**replace)
|
||||
|
||||
def report_device_attributes(self, mode=0, **kwargs):
|
||||
"""Reports terminal identity.
|
||||
|
||||
.. versionadded:: 0.5.0
|
||||
"""
|
||||
# We only implement "primary" DA which is the only DA request
|
||||
# VT102 understood, see ``VT102ID`` in ``linux/drivers/tty/vt.c``.
|
||||
if mode == 0:
|
||||
self.write_process_input(ctrl.CSI + b"?6c")
|
||||
|
||||
def report_device_status(self, mode):
|
||||
"""Reports terminal status or cursor position.
|
||||
|
||||
:param int mode: if 5 -- terminal status, 6 -- cursor position,
|
||||
otherwise a noop.
|
||||
|
||||
.. versionadded:: 0.5.0
|
||||
"""
|
||||
if mode == 5: # Request for terminal status.
|
||||
self.write_process_input(ctrl.CSI + b"0n")
|
||||
elif mode == 6: # Request for cursor position.
|
||||
x = self.cursor.x + 1
|
||||
y = self.cursor.y + 1
|
||||
|
||||
# "Origin mode (DECOM) selects line numbering."
|
||||
if mo.DECOM in self.mode:
|
||||
y -= self.margins.top
|
||||
self.write_process_input(
|
||||
ctrl.CSI + "{0};{1}R".format(y, x).encode())
|
||||
|
||||
def write_process_input(self, data):
|
||||
"""Writes data to the process running inside the terminal.
|
||||
|
||||
By default is a noop.
|
||||
|
||||
:param bytes data: data to write to the process ``stdin``.
|
||||
|
||||
.. versionadded:: 0.5.0
|
||||
"""
|
||||
|
||||
def debug(self, *args, **kwargs):
|
||||
"""Endpoint for unrecognized escape sequences.
|
||||
|
||||
By default is a noop.
|
||||
"""
|
||||
@ -2,14 +2,15 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Tuple, Iterator, Union
|
||||
from typing import Tuple, Iterator, Union, Sequence
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QTimer, QRect
|
||||
from PyQt5.QtGui import QColor, QPainter, QFont, QFontMetrics, QRegion, QPen
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
||||
from .config import build_ansi_color_tables
|
||||
from .config import build_ansi_color_tables, Options
|
||||
from .data_types import Line, as_color
|
||||
from .utils import set_current_font_metrics
|
||||
|
||||
|
||||
def ascii_width(fm: QFontMetrics) -> int:
|
||||
@ -21,11 +22,11 @@ def ascii_width(fm: QFontMetrics) -> int:
|
||||
|
||||
class TerminalWidget(QWidget):
|
||||
|
||||
relayout_lines = pyqtSignal(object, object)
|
||||
relayout_lines = pyqtSignal(object, object, object, object)
|
||||
cells_per_line = 80
|
||||
scroll_amount = 0
|
||||
lines_per_screen = 24
|
||||
|
||||
def __init__(self, opts, linebuf, parent=None):
|
||||
def __init__(self, opts: Options, linebuf: Sequence[Line], parent: QWidget=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.linebuf = linebuf
|
||||
self.setAutoFillBackground(True)
|
||||
@ -50,14 +51,15 @@ class TerminalWidget(QWidget):
|
||||
self.font_metrics = fm = QFontMetrics(self.font())
|
||||
self.cell_height = fm.lineSpacing()
|
||||
self.cell_width = ascii_width(fm)
|
||||
set_current_font_metrics(fm, self.cell_width)
|
||||
self.baseline_offset = fm.ascent()
|
||||
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
|
||||
previousl, self.lines_per_screen = self.lines_per_screen, self.height() // self.cell_height
|
||||
if (previous, previousl) != (self.cells_per_line, self.lines_per_screen):
|
||||
self.relayout_lines.emit(previous, self.cells_per_line, previousl, self.lines_per_screen)
|
||||
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))
|
||||
@ -82,10 +84,7 @@ class TerminalWidget(QWidget):
|
||||
|
||||
def line(self, screen_line: int) -> Union[Line, None]:
|
||||
try:
|
||||
if self.lines_per_screen > len(self.linebuf):
|
||||
return self.linebuf[screen_line]
|
||||
lpos = max(0, len(self.linebuf) - self.lines_per_screen - self.scroll_amount)
|
||||
return self.linebuf[lpos]
|
||||
return self.linebuf[screen_line]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
@ -113,5 +112,5 @@ class TerminalWidget(QWidget):
|
||||
r = QRect(x, y, self.cell_width, self.cell_height)
|
||||
painter.fillRect(r, bg)
|
||||
char = line.char[col]
|
||||
if char:
|
||||
if char not in (0, 32): # 32 = <space>
|
||||
painter.drawText(x, y + self.baseline_offset, chr(char))
|
||||
|
||||
26
kitty/utils.py
Normal file
26
kitty/utils.py
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from PyQt5.QtGui import QFontMetrics
|
||||
|
||||
current_font_metrics = cell_width = None
|
||||
|
||||
|
||||
@lru_cache(maxsize=2**13)
|
||||
def wcwidth(c: str) -> int:
|
||||
if current_font_metrics is None:
|
||||
return 1
|
||||
w = current_font_metrics.widthChar(c)
|
||||
cells, extra = divmod(w, cell_width)
|
||||
if extra > 0.1 * cell_width:
|
||||
cells += 1
|
||||
return cells
|
||||
|
||||
|
||||
def set_current_font_metrics(fm: QFontMetrics, cw: int) -> None:
|
||||
global current_font_metrics, cell_width
|
||||
current_font_metrics, cell_width = fm, cw
|
||||
wcwidth.cache_clear()
|
||||
Loading…
x
Reference in New Issue
Block a user