diff --git a/kitty/config.py b/kitty/config.py index 28941e6e0..92bba7bb8 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -23,10 +23,25 @@ def to_qcolor(x): def to_font_size(x): return max(6, float(x)) + +def to_cursor_shape(x): + shapes = 'block underline beam' + x = x.lower() + if x not in shapes.split(): + raise ValueError('Invalid cursor shape: {} allowed values are {}'.format(x, shapes)) + return x + + +def to_bool(x): + return x.lower() in 'y yes true'.split() + + type_map = { 'scrollback_lines': int, 'font_size': to_font_size, 'cursor_opacity': float, + 'cursor_shape': to_cursor_shape, + 'cursor_blink': to_bool, } for name in 'foreground foreground_bold background cursor'.split(): type_map[name] = to_qcolor @@ -37,7 +52,9 @@ term xterm-kitty foreground #dddddd foreground_bold #ffffff cursor #eeeeee -cursor_opacity 0.8 +cursor_opacity 1.0 +cursor_shape block +cursor_blink no background #000000 font_family monospace font_size 10.0 diff --git a/kitty/data_types.py b/kitty/data_types.py index b1a1ee22d..ee93653fd 100644 --- a/kitty/data_types.py +++ b/kitty/data_types.py @@ -27,12 +27,14 @@ get_zeroes.current_size = None class Cursor: - __slots__ = ("x", "y", "hidden", 'fg', 'bg', 'bold', 'italic', 'reverse', 'strikethrough', 'decoration', 'decoration_fg') + __slots__ = ("x", "y", 'shape', 'blink', "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.shape = None + self.blink = None self.reset_display_attrs() def reset_display_attrs(self): diff --git a/kitty/screen.py b/kitty/screen.py index 0b3229152..42bbd777f 100644 --- a/kitty/screen.py +++ b/kitty/screen.py @@ -217,14 +217,19 @@ class Screen(QObject): self.update_screen() self.select_graphic_rendition(7) # +reverse. - # Make the cursor visible. - if mo.DECTCEM in modes and self.cursor.hidden: - self.cursor.hidden = False + # Show/hide the cursor. + previous, self.cursor.hidden = self.cursor.hidden, mo.DECTCEM not in self.mode + if previous != self.cursor.hidden: self.cursor_changed(self.cursor) + @property def in_bracketed_paste_mode(self): return mo.BRACKETED_PASTE in self.mode + @property + def enable_focus_tracking(self): + return mo.FOCUS_TRACKING in self.mode + def reset_mode(self, *modes, private=False): """Resets (disables) a given list of modes. @@ -255,9 +260,9 @@ class Screen(QObject): self.update_screen() self.select_graphic_rendition(27) # -reverse. - # Hide the cursor. - if mo.DECTCEM in modes and not self.cursor.hidden: - self.cursor.hidden = True + # Show/hide the cursor. + previous, self.cursor.hidden = self.cursor.hidden, mo.DECTCEM not in self.mode + if previous != self.cursor.hidden: self.cursor_changed(self.cursor) def define_charset(self, code, mode): @@ -940,6 +945,20 @@ class Screen(QObject): y -= self.margins.top self.write_process_input("\x1b[{0};{1}R".format(y, x).encode('ascii')) + def set_cursor_shape(self, mode, secondary=None): + if secondary == ' ': + shape = blink = None + if mode > 0: + blink = bool(mode % 2) + shape = 'block' if mode < 3 else 'underline' if mode < 5 else 'beam' if mode < 7 else None + if shape != self.cursor.shape or blink != self.cursor.blink: + self.cursor.shape, self.cursor.blink = shape, blink + self.cursor_changed(self.cursor) + elif secondary == '"': # DECSCA + pass + else: # DECLL + pass + def numeric_keypad_mode(self): pass # TODO: Implement this diff --git a/kitty/term.py b/kitty/term.py index d96377920..f820bdd25 100644 --- a/kitty/term.py +++ b/kitty/term.py @@ -179,47 +179,53 @@ class TerminalWidget(QWidget): r = ev.region() p = QPainter(self) - try: - self.paint_cursor(p, r) - except Exception: - import traceback - traceback.print_exc() - for lnum, cnum in self.dirty_cells(r): try: self.paint_cell(p, cnum, lnum) except Exception: pass + if not self.cursor.hidden: + x, y = wrap_cursor_position(self.cursor.x, self.cursor.y, len(self.line_positions), len(self.cell_positions)) + cr = QRect(self.cell_positions[x], self.line_positions[y], self.cell_width, self.cell_height) + if r.intersects(cr): + self.paint_cell(p, x, y, True) + p.end() - def paint_cursor(self, painter, region): - if self.cursor.hidden: - return - x, y = wrap_cursor_position(self.cursor.x, self.cursor.y, len(self.line_positions), len(self.cell_positions)) + def paint_cursor(self, painter, x, y): r = QRect(self.cell_positions[x], self.line_positions[y], self.cell_width, self.cell_height) - if not region.intersects(r): - return self.last_drew_cursor_at = x, y - line = self.screen.line(y) - colors = line.basic_cell_data(y)[2] - if colors & HAS_BG_MASK: - bg = as_color(colors >> COL_SHIFT, bg_color_table()) - if bg is not None: - painter.fillRect(r, QColor(*bg)) + cc = self.cursor_color + + def width(w=2, vert=True): + dpi = self.logicalDpiX() if vert else self.logicalDpiY() + return int(w * dpi / 72) + if self.hasFocus(): - painter.fillRect(r, self.cursor_color) + cs = self.cursor.shape or self.opts.cursor_shape + if cs == 'block': + painter.fillRect(r, cc) + elif cs == 'beam': + w = width(1.5) + painter.fillRect(r.left(), r.top(), w, self.cell_height, cc) + elif cs == 'underline': + y = r.top() + self.font_metrics.underlinePos() + self.baseline_offset + w = width(vert=False) + painter.fillRect(r.left(), min(y, r.bottom() - w), self.cell_width, w, cc) else: - painter.setPen(QPen(self.cursor_color)) + painter.setPen(QPen(cc)) painter.drawRect(r) - def paint_cell(self, painter: QPainter, col: int, row: int) -> None: + def paint_cell(self, painter: QPainter, col: int, row: int, draw_cursor: bool=False) -> None: line = self.screen.line(row) ch, attrs, colors = line.basic_cell_data(col) x, y = self.cell_positions[col], self.line_positions[row] - if colors & HAS_BG_MASK and (col != self.last_drew_cursor_at[0] or row != self.last_drew_cursor_at[1]): + if colors & HAS_BG_MASK: bg = as_color(colors >> COL_SHIFT, bg_color_table()) if bg is not None: r = QRect(x, y, self.cell_width, self.cell_height) painter.fillRect(r, QColor(*bg)) + if draw_cursor: + self.paint_cursor(painter, col, row) if ch == 0 or ch == 32: # An empty cell pass @@ -247,9 +253,19 @@ class TerminalWidget(QWidget): text = c.text(c.Selection) if text: text = text.encode('utf-8') - if self.screen.in_bracketed_paste_mode(): + if self.screen.in_bracketed_paste_mode: text = mo.BRACKETED_PASTE_START + text + mo.BRACKETED_PASTE_END self.send_data_to_child.emit(text) ev.accept() return return QWidget.mousePressEvent(self, ev) + + def focusInEvent(self, ev): + if self.screen.enable_focus_tracking: + self.send_data_to_child.emit(b'\x1b[I') + return QWidget.focusInEvent(self, ev) + + def focusOutEvent(self, ev): + if self.screen.enable_focus_tracking: + self.send_data_to_child.emit(b'\x1b[O') + return QWidget.focusOutEvent(self, ev) diff --git a/pyte/escape.py b/pyte/escape.py index 472cccea5..45daaec8c 100644 --- a/pyte/escape.py +++ b/pyte/escape.py @@ -157,3 +157,9 @@ DECSTBM = b"r" #: *Horizontal position adjust*: Same as :data:`CHA`. HPA = b"'" + + +# Misc sequences + +#: Change cursor shape/blink +DECSCUSR = b'q' diff --git a/pyte/streams.py b/pyte/streams.py index 15240520b..cc308a1bc 100644 --- a/pyte/streams.py +++ b/pyte/streams.py @@ -119,7 +119,8 @@ class Stream(object): esc.SGR: "select_graphic_rendition", esc.DSR: "report_device_status", esc.DECSTBM: "set_margins", - esc.HPA: "cursor_to_column" + esc.HPA: "cursor_to_column", + esc.DECSCUSR: 'set_cursor_shape', } #: A set of all events dispatched by the stream.