From 6480f9f156ecf31835de681bf82ae50b85f678fd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Oct 2016 10:17:02 +0530 Subject: [PATCH] Wire up all the new components --- kitty/boss.py | 73 +++++++++++++++++++++++++++++-------- kitty/char_grid.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++ kitty/screen.py | 38 +++++++++----------- kitty/tracker.py | 15 +++----- 4 files changed, 169 insertions(+), 46 deletions(-) create mode 100644 kitty/char_grid.py diff --git a/kitty/boss.py b/kitty/boss.py index 16d151655..5773b23c9 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -11,6 +11,11 @@ from threading import Thread from queue import Queue, Empty import glfw +from pyte.streams import Stream, DebugStream + +from .char_grid import CharGrid +from .screen import Screen +from .tracker import ChangeTracker from .utils import resize_pty, create_pty @@ -27,24 +32,36 @@ class Boss(Thread): daemon = True shutting_down = False + pending_title_change = pending_icon_change = None + pending_color_changes = {} def __init__(self, window, opts, args): Thread.__init__(self, name='ChildMonitor') - self.window = window - self.write_queue = Queue() + self.window, self.opts = window, opts + self.action_queue = Queue() + self.read_wakeup_fd, self.write_wakeup_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) + self.tracker = ChangeTracker(self.mark_dirtied) + self.char_grid = CharGrid(opts) + self.screen = Screen(self.opts, self.tracker, self) + sclass = DebugStream if args.dump_commands else Stream + self.stream = sclass(self.screen) self.write_buf = memoryview(b'') self.child_fd = create_pty()[0] self.signal_fd = handle_unix_signals() - self.read_wakeup_fd, self.write_wakeup_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) self.readers = [self.child_fd, self.signal_fd, self.read_wakeup_fd] self.writers = [self.child_fd] resize_pty(80, 24) def on_window_resize(self, window, w, h): - pass + self.char_grid.on_resize(window, w, h) def render(self): - pass + if self.pending_title_change is not None: + glfw.glfwSetWindowTitle(self.window, self.pending_title_change) + self.pending_title_change = None + if self.pending_icon_change is not None: + self.pending_icon_change = None # TODO: Implement this + self.char_grid.render() def wakeup(self): os.write(self.write_wakeup_fd, b'1') @@ -54,14 +71,12 @@ class Boss(Thread): os.read(self.read_wakeup_fd, 1024) except (EnvironmentError, BlockingIOError): pass - buf = b'' - while True: + while not self.shutting_down: try: - buf += self.write_queue.get_nowait() + func, args = self.action_queue.get_nowait() except Empty: break - if buf: - self.write_buf = memoryview(self.write_buf.tobytes() + buf) + getattr(self, func)(*args) def run(self): while not self.shutting_down: @@ -100,10 +115,10 @@ class Boss(Thread): return except EnvironmentError: data = b'' - if not data: - # EOF + if data: + self.stream.feed(data) + else: # EOF self.shutdown() - return def write_ready(self): if not self.shutting_down: @@ -114,5 +129,35 @@ class Boss(Thread): self.write_buf = self.write_buf[n:] def write_to_child(self, data): - self.write_queue.put(data) + if data: + self.action_queue.put(('queue_write', data)) + self.wakeup() + + def queue_write(self, data): + self.write_buf = memoryview(self.write_buf.tobytes() + data) + + def mark_dirtied(self): + self.action_queue.put(('update_screen', ())) self.wakeup() + + def update_screen(self): + changes = self.tracker.consolidate_changes() + self.char_grid.update_screen(changes) + glfw.glfwPostEmptyEvent() + + def title_changed(self, new_title): + self.pending_title_change = new_title + glfw.glfwPostEmptyEvent() + + def icon_changed(self, new_icon): + self.pending_icon_change = new_icon + glfw.glfwPostEmptyEvent() + + def change_default_color(self, which, value): + self.pending_color_changes[which] = value + self.action_queue.put(('change_colors', ())) + + def change_colors(self): + self.char_grid.change_colors(self.pending_color_changes) + self.pending_color_changes = {} + glfw.glfwPostEmptyEvent() diff --git a/kitty/char_grid.py b/kitty/char_grid.py new file mode 100644 index 000000000..6179c50bc --- /dev/null +++ b/kitty/char_grid.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +from threading import Lock + +from .config import build_ansi_color_tables, to_color +from .fonts import load_font_family + +from OpenGL.arrays import ArrayDatatype +from OpenGL.GL import ( + GL_ARRAY_BUFFER, + GL_COLOR_BUFFER_BIT, GL_COMPILE_STATUS, + GL_FALSE, GL_FLOAT, GL_FRAGMENT_SHADER, + GL_LINK_STATUS, GL_RENDERER, + GL_SHADING_LANGUAGE_VERSION, + GL_STATIC_DRAW, GL_TEXTURE_2D, GL_TRIANGLES, + GL_TRUE, GL_UNPACK_ALIGNMENT, GL_VENDOR, GL_VERSION, + GL_VERTEX_SHADER, GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T, + GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER, + GL_LINEAR, GL_RGB, GL_RGBA, GL_UNSIGNED_BYTE, GL_TEXTURE0, + GL_REPEAT, + glActiveTexture, glAttachShader, + glBindBuffer, glBindTexture, glBindVertexArray, + glBufferData, glClear, glClearColor, + glCompileShader, glCreateProgram, + glCreateShader, glDeleteProgram, + glDeleteShader, glDrawArrays, + glEnableVertexAttribArray, glGenBuffers, glGenTextures, + glGenVertexArrays, glGetAttribLocation, + glGetProgramInfoLog, glGetProgramiv, + glGetShaderInfoLog, glGetShaderiv, glGetString, + glGetUniformLocation, glLinkProgram, glPixelStorei, + glShaderSource, glTexImage2D, glTexParameteri, glUniform1i, glUseProgram, + glVertexAttribPointer, glViewport) + + +class CharGrid: + + def __init__(self, opts): + self.apply_opts(opts) + self.lock = Lock() + + def apply_clear_color(self): + bg = self.default_bg + glClearColor(bg[0]/255, bg[1]/255, bg[2]/255, 1) + + def apply_opts(self, opts): + self.opts = opts + build_ansi_color_tables(opts) + self.opts = opts + self.default_bg = self.original_bg = opts.background + self.default_fg = self.original_fg = opts.foreground + self.base_font_family = load_font_family(opts.font_family) + self.font_size = int(opts.font_size * 64) + self.apply_clear_color() + + def on_resize(self, window, w, h): + glViewport(0, 0, w, h) + self.do_layout(w, h) + + def do_layout(self, w, h): + pass + + def redraw(self): + pass + + def render(self): + with self.lock: + glClear(GL_COLOR_BUFFER_BIT) + + def change_colors(self, changes): + dirtied = False + for which, val in changes.items(): + if which in ('fg', 'bg'): + if not val: + setattr(self, 'default_' + which, getattr(self, 'original_' + which)) + dirtied = True + else: + val = to_color(val) + if val is not None: + setattr(self, 'default_' + which, val) + dirtied = True + if dirtied: + self.apply_clear_color() + self.redraw() + + def update_screen(self, changes): + self.redraw() diff --git a/kitty/screen.py b/kitty/screen.py index e32e0ae18..f4ae45713 100644 --- a/kitty/screen.py +++ b/kitty/screen.py @@ -8,8 +8,6 @@ import unicodedata from collections import deque, namedtuple from typing import Sequence -from PyQt5.QtCore import QObject, pyqtSignal - from pyte import charsets as cs, graphics as g, modes as mo from .data_types import Line, Cursor, rewrap_lines from .utils import wcwidth, is_simple_string, sanitize_title @@ -40,23 +38,21 @@ def wrap_cursor_position(x, y, lines, columns): return x, y -class Screen(QObject): +class Screen: """ 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``. """ - title_changed = pyqtSignal(object) - icon_changed = pyqtSignal(object) - write_to_child = pyqtSignal(object) - change_default_color = pyqtSignal(object, object) + tracker_callbacks = 'cursor_changed cursor_position_changed update_screen update_line_range update_cell_range line_added_to_history'.split() + callbacks = 'title_changed icon_changed write_to_child change_default_color'.split() _notify_cursor_position = True - def __init__(self, opts, tracker, columns: int=80, lines: int=24, parent=None): - QObject.__init__(self, parent) - self.write_process_input = self.write_to_child.emit - for attr in 'cursor_changed cursor_position_changed update_screen update_line_range update_cell_range line_added_to_history'.split(): + def __init__(self, opts, tracker, callbacks, columns: int=80, lines: int=24): + for attr in self.tracker_callbacks: setattr(self, attr, getattr(tracker, attr)) + for attr in self.callbacks: + setattr(self, attr, getattr(callbacks, attr)) self.main_savepoints, self.alt_savepoints = deque(), deque() self.savepoints = self.main_savepoints self.columns = columns @@ -134,8 +130,8 @@ class Screen(QObject): self.cursor = Cursor(0, 0) self.cursor_changed(self.cursor) self.cursor_position() - self.change_default_color.emit('fg', None) - self.change_default_color.emit('bg', None) + self.change_default_color('fg', None) + self.change_default_color('bg', None) if notify: self.update_screen() @@ -435,14 +431,14 @@ class Screen(QObject): .. note:: This is an XTerm extension supported by the Linux terminal. """ - self.title_changed.emit(sanitize_title(self._decode(param))) + self.title_changed(sanitize_title(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(sanitize_title(self._decode(param))) + self.icon_changed(sanitize_title(self._decode(param))) def carriage_return(self): """Move the cursor to the beginning of the current line.""" @@ -951,13 +947,13 @@ class Screen(QObject): # If you implement xterm keycode querying # http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Device-Control-functions # you can enable this. - self.write_process_input(b'\x1b[>1;4600;0c') + self.write_to_child(b'\x1b[>1;4600;0c') else: # xterm gives: [[?64;1;2;6;9;15;18;21;22c # use the simpler vte response, since we dont support # windowing/horizontal scrolling etc. # [[?64;1;2;6;9;15;18;21;22c - self.write_process_input(b"\x1b[?62c") + self.write_to_child(b"\x1b[?62c") def report_device_status(self, mode): """Reports terminal status or cursor position. @@ -968,7 +964,7 @@ class Screen(QObject): .. versionadded:: 0.5.0 """ if mode == 5: # Request for terminal status. - self.write_process_input(b"\x1b[0n") + self.write_to_child(b"\x1b[0n") elif mode == 6: # Request for cursor position. x, y = wrap_cursor_position(self.cursor.x, self.cursor.y, self.lines, self.columns) x, y = x + 1, y + 1 @@ -976,7 +972,7 @@ class Screen(QObject): # "Origin mode (DECOM) selects line numbering." if mo.DECOM in self.mode: y -= self.margins.top - self.write_process_input("\x1b[{0};{1}R".format(y, x).encode('ascii')) + self.write_to_child("\x1b[{0};{1}R".format(y, x).encode('ascii')) def set_cursor_shape(self, mode, secondary=None): if secondary == ' ': @@ -1002,9 +998,9 @@ class Screen(QObject): def handle_val(val, param=None): val %= 100 if val == 10: # foreground - self.change_default_color.emit('fg', param) + self.change_default_color('fg', param) elif val == 11: # background - self.change_default_color.emit('bg', param) + self.change_default_color('bg', param) elif val == 12: # cursor color old, self.cursor.color = self.cursor.color, param if old != self.cursor.color: diff --git a/kitty/tracker.py b/kitty/tracker.py index 7ced52f93..e6335e483 100644 --- a/kitty/tracker.py +++ b/kitty/tracker.py @@ -6,8 +6,6 @@ from collections import defaultdict from operator import itemgetter from typing import Set, Tuple, Iterator -from PyQt5.QtCore import QObject, pyqtSignal, Qt - from .data_types import Cursor @@ -27,15 +25,11 @@ def merge_ranges(ranges: Set[Tuple[int]]) -> Iterator[Tuple[int]]: yield low, high # end the final run -class ChangeTracker(QObject): +class ChangeTracker: - dirtied = pyqtSignal(object) - mark_dirtied = pyqtSignal() - - def __init__(self, parent=None): - QObject.__init__(self, parent) + def __init__(self, mark_dirtied): self.reset() - self.mark_dirtied.connect(self.consolidate_changes, type=Qt.QueuedConnection) + self.mark_dirtied = mark_dirtied def reset(self): self._dirty = False @@ -48,7 +42,7 @@ class ChangeTracker(QObject): def dirty(self): if not self._dirty: self._dirty = True - self.mark_dirtied.emit() + self.mark_dirtied() def cursor_changed(self, cursor: Cursor) -> None: self.changed_cursor = cursor @@ -83,5 +77,4 @@ class ChangeTracker(QObject): changes = {'screen': self.screen_changed, 'cursor': self.changed_cursor, 'lines': self.changed_lines, 'cells': cc, 'history_line_added_count': self.history_line_added_count} self.reset() - self.dirtied.emit(changes) return changes