From b5000c2ec03cf3fb4e3138aaba5dd6bce385d387 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 26 Nov 2016 17:40:28 +0530 Subject: [PATCH] Refactor to support multiple windows/tabs --- kitty/boss.py | 342 ------------------------------------------- kitty/char_grid.py | 227 ++++++++++------------------- kitty/constants.py | 34 +++++ kitty/layout.py | 57 ++++++++ kitty/main.py | 32 ++-- kitty/screen.c | 4 +- kitty/shaders.py | 5 +- kitty/tabs.py | 356 +++++++++++++++++++++++++++++++++++++++++++++ kitty/utils.py | 11 ++ kitty/window.py | 162 +++++++++++++++++++++ 10 files changed, 713 insertions(+), 517 deletions(-) delete mode 100644 kitty/boss.py create mode 100644 kitty/layout.py create mode 100644 kitty/tabs.py create mode 100644 kitty/window.py diff --git a/kitty/boss.py b/kitty/boss.py deleted file mode 100644 index cebc0d66f..000000000 --- a/kitty/boss.py +++ /dev/null @@ -1,342 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 -# License: GPL v3 Copyright: 2016, Kovid Goyal - -import os -import io -import signal -import select -import subprocess -import struct -from itertools import repeat -from functools import partial -from time import monotonic -from threading import Thread, current_thread -from queue import Queue, Empty - -import glfw -import glfw_constants - -from .constants import appname -from .char_grid import CharGrid -from .keys import interpret_text_event, interpret_key_event, get_shortcut -from .utils import sanitize_title, parse_color_set -from .fast_data_types import ( - BRACKETED_PASTE_START, BRACKETED_PASTE_END, Screen, read_bytes_dump, read_bytes -) -from .terminfo import get_capabilities - - -def handle_unix_signals(): - read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) - for sig in (signal.SIGINT, signal.SIGTERM): - signal.signal(sig, lambda x, y: None) - signal.siginterrupt(sig, False) - signal.set_wakeup_fd(write_fd) - return read_fd - - -class Boss(Thread): - - daemon = True - shutting_down = False - pending_title_change = pending_icon_change = None - pending_color_changes = {} - - def __init__(self, window, window_width, window_height, opts, args, child): - Thread.__init__(self, name='ChildMonitor') - self.child = child - self.screen_update_delay = opts.repaint_delay / 1000.0 - self.pending_update_screen = None - self.action_queue = Queue() - self.child.fork() - self.child_fd = self.child.child_fd - self.read_wakeup_fd, self.write_wakeup_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) - self.signal_fd = handle_unix_signals() - self.readers = [self.child_fd, self.signal_fd, self.read_wakeup_fd] - self.writers = [self.child_fd] - self.profile = args.profile - self.window, self.opts = window, opts - self.screen = Screen(self, 24, 80, opts.scrollback_lines) - self.read_bytes = partial(read_bytes_dump, self.dump_commands) if args.dump_commands else read_bytes - self.draw_dump_buf = [] - self.write_buf = memoryview(b'') - self.char_grid = CharGrid(self.screen, opts, window_width, window_height) - glfw.glfwSetCharModsCallback(window, self.on_text_input) - glfw.glfwSetKeyCallback(window, self.on_key) - glfw.glfwSetMouseButtonCallback(window, self.on_mouse_button) - glfw.glfwSetWindowFocusCallback(window, self.on_focus) - - def dump_commands(self, *a): - if a: - if a[0] == 'draw': - if a[1] is None: - if self.draw_dump_buf: - print('draw', ''.join(self.draw_dump_buf)) - self.draw_dump_buf = [] - else: - self.draw_dump_buf.append(a[1]) - else: - if self.draw_dump_buf: - print('draw', ''.join(self.draw_dump_buf)) - self.draw_dump_buf = [] - print(*a) - - def queue_action(self, func, *args): - self.action_queue.put((func, args)) - self.wakeup() - - def wakeup(self): - os.write(self.write_wakeup_fd, b'1') - - def on_wakeup(self): - try: - os.read(self.read_wakeup_fd, io.DEFAULT_BUFFER_SIZE) - except (EnvironmentError, BlockingIOError): - pass - while not self.shutting_down: - try: - func, args = self.action_queue.get_nowait() - except Empty: - break - func(*args) - - def signal_received(self): - try: - data = os.read(self.signal_fd, io.DEFAULT_BUFFER_SIZE) - except BlockingIOError: - return - if data: - signals = struct.unpack('%uB' % len(data), data) - if signal.SIGINT in signals or signal.SIGTERM in signals: - self.shutdown() - - def on_focus(self, window, focused): - if focused: - if self.screen.focus_tracking_enabled(): - self.write_to_child(b'\x1b[I') - else: - if self.screen.focus_tracking_enabled(): - self.write_to_child(b'\x1b[O') - - def on_mouse_button(self, window, button, action, mods): - if action == glfw_constants.GLFW_RELEASE: - if button == glfw_constants.GLFW_MOUSE_BUTTON_MIDDLE: - self.paste_from_selection() - return - - def on_key(self, window, key, scancode, action, mods): - if action == glfw_constants.GLFW_PRESS or action == glfw_constants.GLFW_REPEAT: - func = get_shortcut(self.opts.keymap, mods, key) - if func is not None: - func = getattr(self, func, None) - if func is not None: - passthrough = func() - if not passthrough: - return - if self.char_grid.scrolled_by: - self.scroll_end() - data = interpret_key_event(key, scancode, mods) - if data: - self.write_to_child(data) - - def on_text_input(self, window, codepoint, mods): - data = interpret_text_event(codepoint, mods) - if data: - self.write_to_child(data) - - def on_window_resize(self, window, w, h): - self.queue_action(self.apply_resize_screen, w, h) - - def apply_resize_screen(self, w, h): - self.char_grid.resize_screen(w, h) - sg = self.char_grid.screen_geometry - self.child.resize_pty(sg.xnum, sg.ynum) - glfw.glfwPostEmptyEvent() - - def apply_opts(self, opts): - self.opts = opts - self.screen_update_delay = opts.repaint_delay / 1000.0 - self.queue_action(self.apply_opts_to_screen) - - def apply_opts_to_screen(self): - self.char_grid.apply_opts(self.opts) - self.char_grid.dirty_everything() - self.screen.change_scrollback_size(self.opts.scrollback_lines) - - def render(self): - if self.pending_title_change is not None: - t, self.pending_title_change = sanitize_title(self.pending_title_change or appname), None - glfw.glfwSetWindowTitle(self.window, t.encode('utf-8')) - if self.pending_icon_change is not None: - self.pending_icon_change = None # TODO: Implement this - self.char_grid.render() - - def run(self): - if self.profile: - import cProfile - import pstats - pr = cProfile.Profile() - pr.enable() - self.loop() - if self.profile: - pr.disable() - pr.create_stats() - s = pstats.Stats(pr) - s.dump_stats(self.profile) - - def loop(self): - all_readers, all_writers = self.readers, self.writers - dispatch = list(repeat(None, max(all_readers) + 1)) - dispatch[self.child_fd] = self.read_ready - dispatch[self.read_wakeup_fd] = self.on_wakeup - dispatch[self.signal_fd] = self.signal_received - while not self.shutting_down: - timeout = None if self.pending_update_screen is None else max(0, self.pending_update_screen - monotonic()) - readers, writers, _ = select.select(all_readers, all_writers if self.write_buf else [], [], timeout) - for r in readers: - dispatch[r]() - if writers: - self.write_ready() - if self.pending_update_screen is not None: - if monotonic() > self.pending_update_screen: - self.apply_update_screen() - elif self.screen.is_dirty(): - self.pending_update_screen = monotonic() + self.screen_update_delay - - def close(self): - if not self.shutting_down: - self.queue_action(self.shutdown) - - def destroy(self): - # Must be called in the main thread as it manipulates signal handlers - signal.signal(signal.SIGINT, signal.SIG_DFL) - signal.signal(signal.SIGTERM, signal.SIG_DFL) - self.char_grid.destroy() - self.child.hangup() - self.child.get_child_status() # Ensure child does not become zombie - - def shutdown(self): - self.shutting_down = True - glfw.glfwSetWindowShouldClose(self.window, True) - glfw.glfwPostEmptyEvent() - - def read_ready(self): - if self.shutting_down: - return - if self.read_bytes(self.screen, self.child_fd) is False: - self.shutdown() # EOF - - def write_ready(self): - if not self.shutting_down: - while self.write_buf: - try: - n = os.write(self.child_fd, self.write_buf) - except BlockingIOError: - n = 0 - if not n: - return - self.write_buf = self.write_buf[n:] - - def write_to_child(self, data): - if data: - if current_thread() is self: - self.queue_write(data) - else: - self.queue_action(self.queue_write, data) - - def queue_write(self, data): - self.write_buf = memoryview(self.write_buf.tobytes() + data) - - def apply_update_screen(self): - self.pending_update_screen = None - self.char_grid.update_cell_data() - 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 set_dynamic_color(self, code, value): - wmap = {10: 'fg', 11: 'bg', 110: 'fg', 111: 'bg'} - if isinstance(value, bytes): - value = value.decode('utf-8') - for val in value.split(';'): - w = wmap.get(code) - if w is not None: - if code >= 110: - val = None - self.pending_color_changes[w] = val - code += 1 - self.queue_action(self.apply_change_colors) - - def refresh(self): - self.screen.mark_as_dirty() - self.wakeup() - - def set_color_table_color(self, code, value): - if code == 4: - for c, val in parse_color_set(value): - self.char_grid.color_profile.set_color(c, val) - self.refresh() - elif code == 104: - if not value.strip(): - self.char_grid.color_profile.reset_color_table() - else: - for c in value.split(';'): - try: - c = int(c) - except Exception: - continue - if 0 <= c <= 255: - self.char_grid.color_profile.reset_color(c) - self.refresh() - - def apply_change_colors(self): - self.char_grid.change_colors(self.pending_color_changes) - self.pending_color_changes = {} - glfw.glfwPostEmptyEvent() - - def request_capabilities(self, q): - self.write_to_child(get_capabilities(q)) - - # actions {{{ - - def paste(self, text): - if text: - if self.screen.in_bracketed_paste_mode(): - text = BRACKETED_PASTE_START.encode('ascii') + text + BRACKETED_PASTE_END.encode('ascii') - self.write_to_child(text) - - def paste_from_clipboard(self): - text = glfw.glfwGetClipboardString(self.window) - self.paste(text) - - def paste_from_selection(self): - # glfw has no way to get the primary selection - # https://github.com/glfw/glfw/issues/894 - text = subprocess.check_output(['xsel']) - self.paste(text) - - def scroll_line_up(self): - self.queue_action(self.char_grid.scroll, 'line', True) - - def scroll_line_down(self): - self.queue_action(self.char_grid.scroll, 'line', False) - - def scroll_page_up(self): - self.queue_action(self.char_grid.scroll, 'page', True) - - def scroll_page_down(self): - self.queue_action(self.char_grid.scroll, 'page', False) - - def scroll_home(self): - self.queue_action(self.char_grid.scroll, 'full', True) - - def scroll_end(self): - self.queue_action(self.char_grid.scroll, 'full', False) - # }}} diff --git a/kitty/char_grid.py b/kitty/char_grid.py index 568c3f7fe..4e6fca121 100644 --- a/kitty/char_grid.py +++ b/kitty/char_grid.py @@ -4,24 +4,19 @@ from collections import namedtuple from ctypes import c_uint, addressof, memmove, sizeof -from queue import Queue, Empty +from itertools import count from threading import Lock from .config import build_ansi_color_table -from .fonts import set_font_family -from .shaders import Sprites, ShaderProgram +from .constants import tab_manager, viewport_size, cell_size, ScreenGeometry from .utils import get_logical_dpi, to_color from .fast_data_types import ( - glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, glClear, - GL_COLOR_BUFFER_BIT, glClearColor, glViewport, glUniform2ui, glUniform4f, - glUniform1i, glUniform2f, glDrawArraysInstanced, GL_TRIANGLE_FAN, - glEnable, glDisable, GL_BLEND, glDrawArrays, ColorProfile, + glUniform2ui, glUniform4f, glUniform1i, glUniform2f, glDrawArraysInstanced, + GL_TRIANGLE_FAN, glEnable, glDisable, GL_BLEND, glDrawArrays, ColorProfile, CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, DATA_CELL_SIZE ) -Size = namedtuple('Size', 'width height') Cursor = namedtuple('Cursor', 'x y hidden shape color blink') -ScreenGeometry = namedtuple('ScreenGeometry', 'xstart ystart xnum ynum dx dy') if DATA_CELL_SIZE % 3: raise ValueError('Incorrect data cell size, must be a multiple of 3') @@ -150,96 +145,54 @@ void main() { # }}} -def calculate_screen_geometry(cell_width, cell_height, screen_width, screen_height): - xnum = screen_width // cell_width - ynum = screen_height // cell_height - dx, dy = 2 * cell_width / screen_width, 2 * cell_height / screen_height - xmargin = (screen_width - (xnum * cell_width)) / screen_width - ymargin = (screen_height - (ynum * cell_height)) / screen_height - xstart = -1 + xmargin - ystart = 1 - ymargin - return ScreenGeometry(xstart, ystart, xnum, ynum, dx, dy) - - -class RenderData: - - __slots__ = 'viewport clear_color cell_data_changed screen_geometry sprite_layout cursor'.split() - - def __init__(self, viewport=None, clear_color=None, cell_data_changed=False, screen_geometry=None, sprite_layout=None, cursor=None): - self.viewport, self.clear_color, self.cell_data_changed = viewport, clear_color, cell_data_changed - self.screen_geometry = screen_geometry - self.sprite_layout = sprite_layout - self.cursor = cursor - - def update(self, other): - for k in self.__slots__: - val = getattr(other, k) - if val is not None: - setattr(self, k, val) - - def color_as_int(val): return val[0] << 16 | val[1] << 8 | val[2] +render_data_num = count() + + class CharGrid: - def __init__(self, screen, opts, window_width, window_height): - self.sprites_lock, self.buffer_lock = Lock(), Lock() + def __init__(self, screen, opts): + self.buffer_lock = Lock() + self.render_num = next(render_data_num) + self.render_data = None + self.last_render_send_num = -1 self.scrolled_by = 0 - self.dpix, self.dpiy = get_logical_dpi() - self.width, self.height = window_width, window_height self.color_profile = ColorProfile() - self.as_color = self.color_profile.as_color + self.color_profile.update_ansi_color_table(build_ansi_color_table(opts)) self.screen = screen self.opts = opts self.original_bg = opts.background self.original_fg = opts.foreground - self.render_queue = Queue() - self.program = ShaderProgram(*cell_shader) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - self.sprites = Sprites() - self.cursor_program = ShaderProgram(*cursor_shader) - self.last_render_data = RenderData() self.default_cursor = Cursor(0, 0, False, opts.cursor_shape, opts.cursor, opts.cursor_blink) - self.render_queue.put(RenderData( - viewport=Size(self.width, self.height), clear_color=color_as_int(self.original_bg), - cursor=self.default_cursor)) - self.clear_count = 4 self.default_bg = color_as_int(self.original_bg) self.default_fg = color_as_int(self.original_fg) - self.apply_opts(self.opts) - - def destroy(self): - self.sprites.destroy() - - def apply_opts(self, opts): self.dpix, self.dpiy = get_logical_dpi() self.opts = opts - self.screen.change_scrollback_size(opts.scrollback_lines) - self.color_profile.update_ansi_color_table(build_ansi_color_table(opts)) - self.default_cursor = Cursor(0, 0, False, opts.cursor_shape, opts.cursor, opts.cursor_blink) + self.default_cursor = self.current_cursor = Cursor(0, 0, False, opts.cursor_shape, opts.cursor, opts.cursor_blink) self.opts = opts self.original_bg = opts.background self.original_fg = opts.foreground - self.cell_width, self.cell_height = set_font_family(opts.font_family, opts.font_size) - self.sprites.do_layout(self.cell_width, self.cell_height) - self.do_layout(self.width, self.height) + self.sprite_map_type = self.main_sprite_map = self.scroll_sprite_map = self.render_buf = None - def resize_screen(self, w, h): - ' Screen was resized by the user (called in non-UI thread) ' - self.do_layout(w, h) + def update_position(self, window_geometry): + dx, dy = 2 * cell_size.width / viewport_size.width, 2 * cell_size.height / viewport_size.height + xmargin = window_geometry.left / viewport_size.width + ymargin = window_geometry.top / viewport_size.height + xstart = -1 + xmargin + ystart = 1 - ymargin + self.screen_geometry = ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy) - def do_layout(self, w, h): - self.width, self.height = w, h - self.screen_geometry = sg = calculate_screen_geometry(self.cell_width, self.cell_height, self.width, self.height) - self.screen.resize(sg.ynum, sg.xnum) - self.sprite_map_type = (c_uint * (sg.ynum * sg.xnum * DATA_CELL_SIZE)) + def resize(self, window_geometry): + self.update_position(window_geometry) + self.sprite_map_type = (c_uint * (self.screen_geometry.ynum * self.screen_geometry.xnum * DATA_CELL_SIZE)) self.main_sprite_map = self.sprite_map_type() self.scroll_sprite_map = self.sprite_map_type() - self.render_buf = self.sprite_map_type() - self.update_cell_data(add_viewport_data=True) - self.clear_count = 4 + with self.buffer_lock: + self.render_buf = self.sprite_map_type() + self.render_num = next(render_data_num) def change_colors(self, changes): dirtied = False @@ -254,8 +207,7 @@ class CharGrid: setattr(self, 'default_' + which, color_as_int(val)) dirtied = True if dirtied: - self.render_queue.put(RenderData(clear_color=self.default_bg)) - self.clear_count = 4 + self.screen.mark_as_dirty() def scroll(self, amt, upwards=True): amt = {'line': 1, 'page': self.screen.lines - 1, 'full': self.screen.historybuf.count}[amt] @@ -266,77 +218,49 @@ class CharGrid: self.scrolled_by = y self.update_cell_data() - def update_cell_data(self, add_viewport_data=False): - rd = RenderData(sprite_layout=self.sprites.layout, cell_data_changed=True) - if add_viewport_data: - rd.viewport = Size(self.width, self.height) - rd.screen_geometry = self.screen_geometry - with self.sprites_lock: + def update_cell_data(self, force_full_refresh=False): + sprites = tab_manager().sprites + with sprites.lock: cursor_changed, history_line_added_count = self.screen.update_cell_data( - self.sprites.backend, self.color_profile, addressof(self.main_sprite_map), self.default_fg, self.default_bg, add_viewport_data) + sprites.backend, self.color_profile, addressof(self.main_sprite_map), self.default_fg, self.default_bg, force_full_refresh) if self.scrolled_by: self.scrolled_by = min(self.scrolled_by + history_line_added_count, self.screen.historybuf.count) self.screen.set_scroll_cell_data( - self.sprites.backend, self.color_profile, addressof(self.main_sprite_map), self.default_fg, self.default_bg, + sprites.backend, self.color_profile, addressof(self.main_sprite_map), self.default_fg, self.default_bg, self.scrolled_by, addressof(self.scroll_sprite_map)) data = self.scroll_sprite_map if self.scrolled_by else self.main_sprite_map with self.buffer_lock: memmove(self.render_buf, data, sizeof(type(data))) + self.render_num = next(render_data_num) + self.render_data = self.screen_geometry if cursor_changed: c = self.screen.cursor - rd.cursor = Cursor(c.x, c.y, c.hidden, c.shape, c.color, c.blink) - self.render_queue.put(rd) + self.current_cursor = Cursor(c.x, c.y, c.hidden, c.shape, c.color, c.blink) - def render(self): - ' This is the only method in this class called in the UI thread (apart from __init__) ' - if self.clear_count > 0: - glClear(GL_COLOR_BUFFER_BIT) - self.clear_count -= 1 - cell_data_changed = self.get_all_render_changes() - with self.sprites: - with self.sprites_lock: - self.sprites.render_dirty_cells() - if cell_data_changed: - with self.buffer_lock: - self.sprites.set_sprite_map(self.render_buf) - data = self.last_render_data - - if data.screen_geometry is None: + def prepare_for_render(self, sprites): + with self.buffer_lock: + sg = self.render_data + if sg is None: return - sg = data.screen_geometry - self.render_cells(sg, data.sprite_layout) - if not data.cursor.hidden and not self.scrolled_by: - self.render_cursor(sg, data.cursor) + if self.last_render_send_num != self.render_num: + sprites.set_sprite_map(self.render_buf) + self.last_render_send_num = self.render_num + return sg - def get_all_render_changes(self): - cell_data_changed = False - data = self.last_render_data - while True: - try: - rd = self.render_queue.get_nowait() - except Empty: - break - cell_data_changed |= rd.cell_data_changed - if rd.clear_color is not None: - bg = rd.clear_color - glClearColor((bg >> 16) / 255, ((bg >> 8) & 0xff) / 255, (bg & 0xff) / 255, 1) - if rd.viewport is not None: - glViewport(0, 0, self.width, self.height) - data.update(rd) - return cell_data_changed + def render_cells(self, sg, cell_program, sprites): + ul = cell_program.uniform_location + glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum) + glUniform4f(ul('steps'), sg.xstart, sg.ystart, sg.dx, sg.dy) + glUniform1i(ul('sprites'), sprites.sampler_num) + glUniform1i(ul('sprite_map'), sprites.buffer_sampler_num) + glUniform2f(ul('sprite_layout'), *(sprites.layout)) + glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, sg.xnum * sg.ynum) - def render_cells(self, sg, sprite_layout): - with self.program: - ul = self.program.uniform_location - glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum) - glUniform4f(ul('steps'), sg.xstart, sg.ystart, sg.dx, sg.dy) - glUniform1i(ul('sprites'), self.sprites.sampler_num) - glUniform1i(ul('sprite_map'), self.sprites.buffer_sampler_num) - glUniform2f(ul('sprite_layout'), *sprite_layout) - glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, sg.xnum * sg.ynum) - - def render_cursor(self, sg, cursor): + def render_cursor(self, sg, cursor_program): + cursor = self.current_cursor + if cursor.hidden or self.scrolled_by: + return def width(w=2, vert=True): dpi = self.dpix if vert else self.dpiy @@ -344,22 +268,21 @@ class CharGrid: factor = 2 / (self.width if vert else self.height) return w * factor - with self.cursor_program: - ul = self.cursor_program.uniform_location - left = sg.xstart + cursor.x * sg.dx - top = sg.ystart - cursor.y * sg.dy - col = cursor.color or self.default_cursor.color - shape = cursor.shape or self.default_cursor.shape - alpha = self.opts.cursor_opacity - if alpha < 1.0 and shape == CURSOR_BLOCK: - glEnable(GL_BLEND) - mult = self.screen.current_char_width() - right = left + (width(1.5) if shape == CURSOR_BEAM else sg.dx * mult) - bottom = top - sg.dy - if shape == CURSOR_UNDERLINE: - top = bottom + width(vert=False) - glUniform4f(ul('color'), col[0], col[1], col[2], alpha) - glUniform2f(ul('xpos'), left, right) - glUniform2f(ul('ypos'), top, bottom) - glDrawArrays(GL_TRIANGLE_FAN, 0, 4) - glDisable(GL_BLEND) + ul = cursor_program.uniform_location + left = sg.xstart + cursor.x * sg.dx + top = sg.ystart - cursor.y * sg.dy + col = cursor.color or self.default_cursor.color + shape = cursor.shape or self.default_cursor.shape + alpha = self.opts.cursor_opacity + if alpha < 1.0 and shape == CURSOR_BLOCK: + glEnable(GL_BLEND) + mult = self.screen.current_char_width() + right = left + (width(1.5) if shape == CURSOR_BEAM else sg.dx * mult) + bottom = top - sg.dy + if shape == CURSOR_UNDERLINE: + top = bottom + width(vert=False) + glUniform4f(ul('color'), col[0], col[1], col[2], alpha) + glUniform2f(ul('xpos'), left, right) + glUniform2f(ul('ypos'), top, bottom) + glDrawArrays(GL_TRIANGLE_FAN, 0, 4) + glDisable(GL_BLEND) diff --git a/kitty/constants.py b/kitty/constants.py index 53afa7a0c..7fcc51316 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -4,10 +4,14 @@ import os import threading +import pwd +from collections import namedtuple appname = 'kitty' version = (0, 1, 0) str_version = '.'.join(map(str, version)) +ScreenGeometry = namedtuple('ScreenGeometry', 'xstart ystart xnum ynum dx dy') +WindowGeometry = namedtuple('WindowGeometry', 'left top right bottom xnum ynum') def _get_config_dir(): @@ -22,8 +26,38 @@ def _get_config_dir(): except FileExistsError: pass return ans + + config_dir = _get_config_dir() del _get_config_dir + +class ViewportSize: + + __slots__ = ('width', 'height') + + def __init__(self): + self.width = self.height = 1024 + + +def tab_manager(): + return tab_manager.manager + + +def set_tab_manager(m): + tab_manager.manager = m + + +def wakeup(): + os.write(tab_manager.manager.write_wakeup_fd, b'1') + + +def queue_action(func, *args): + tab_manager.manager.queue_action(func, *args) + + +viewport_size = ViewportSize() +cell_size = ViewportSize() terminfo_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'terminfo') main_thread = threading.current_thread() +shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh' diff --git a/kitty/layout.py b/kitty/layout.py new file mode 100644 index 000000000..7e87e79a1 --- /dev/null +++ b/kitty/layout.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +from .constants import WindowGeometry, viewport_size, cell_size + + +def layout_dimension(length, cell_length, number_of_windows=1, border_length=0): + number_of_cells = length // cell_length + space_needed_for_border = number_of_windows * border_length + extra = length - number_of_cells * cell_length + while extra < space_needed_for_border: + number_of_cells -= 1 + extra = length - number_of_cells * cell_length + extra -= space_needed_for_border + pos = (extra // 2) + border_length + inner_length = number_of_cells * cell_length + window_length = 2 * border_length + inner_length + while number_of_windows > 0: + number_of_windows -= 1 + yield pos, number_of_cells + pos += window_length + + +class Layout: + + name = None + + def add_window(self, windows, window): + raise NotImplementedError() + + def remove_window(self, windows, window): + raise NotImplementedError() + + def __call__(self, windows): + raise NotImplementedError() + + +class Stack: + + name = 'stack' + + def add_window(self, windows, window): + windows.appendleft(window) + self(windows) + + def remove_window(self, windows, window): + windows.remove(window) + self(windows) + + def __call__(self, windows): + xstart, xnum = next(layout_dimension(viewport_size.width, cell_size.width)) + ystart, ynum = next(layout_dimension(viewport_size.height, cell_size.height)) + wg = WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_size.width * xnum, bottom=ystart + cell_size.height * ynum) + for i, w in enumerate(windows): + w.is_visible_in_layout = i == 0 + w.set_geometry(wg) diff --git a/kitty/main.py b/kitty/main.py index 5b50007f1..435f0467b 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -6,17 +6,16 @@ import argparse import tempfile import os import sys -import pwd from gettext import gettext as _ -from .child import Child from .config import load_config -from .constants import appname, str_version, config_dir -from .boss import Boss +from .constants import appname, str_version, config_dir, viewport_size +from .tabs import TabManager from .shaders import GL_VERSION from .fast_data_types import glewInit, enable_automatic_opengl_error_checking -import glfw, glfw_constants +import glfw +import glfw_constants def option_parser(): @@ -44,11 +43,10 @@ def setup_opengl(): glfw.glfwWindowHint(glfw_constants.GLFW_SAMPLES, 0) -def run_app(opts, args, child): +def run_app(opts, args): setup_opengl() - window_width = window_height = 1024 window = glfw.glfwCreateWindow( - window_width, window_height, args.cls.encode('utf-8'), None, None) + viewport_size.width, viewport_size.height, args.cls.encode('utf-8'), None, None) if not window: raise SystemExit("glfwCreateWindow failed") glfw.glfwSetWindowTitle(window, appname.encode('utf-8')) @@ -56,19 +54,15 @@ def run_app(opts, args, child): glfw.glfwMakeContextCurrent(window) glewInit() glfw.glfwSwapInterval(1) - boss = Boss(window, window_width, window_height, opts, args, child) - glfw.glfwSetFramebufferSizeCallback(window, boss.on_window_resize) - boss.start() + tabs = TabManager(window, opts, args) + tabs.start() try: while not glfw.glfwWindowShouldClose(window): - boss.render() + tabs.render() glfw.glfwSwapBuffers(window) glfw.glfwWaitEvents() finally: - if boss.is_alive(): - boss.close() - boss.join() - boss.destroy() + tabs.destroy() finally: glfw.glfwDestroyWindow(window) @@ -88,8 +82,6 @@ def main(): exec(args.cmd) return opts = load_config(args.config) - child = args.args or [pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'] - child = Child(child, args.directory, opts) glfw.glfwSetErrorCallback(on_glfw_error) enable_automatic_opengl_error_checking(False) if not glfw.glfwInit(): @@ -102,7 +94,7 @@ def main(): import pstats pr = cProfile.Profile() pr.enable() - run_app(opts, args, child) + run_app(opts, args) pr.disable() pr.create_stats() s = pstats.Stats(pr) @@ -112,7 +104,7 @@ def main(): s.sort_stats('time', 'name') s.print_stats(30) else: - run_app(opts, args, child) + run_app(opts, args) finally: glfw.glfwTerminate() os.closerange(3, 100) diff --git a/kitty/screen.c b/kitty/screen.c index 3e2b81036..99cac52c9 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -911,7 +911,7 @@ erase_in_display(Screen *self, PyObject *args) { #define MODE_GETTER(name, uname) \ static PyObject* name(Screen *self) { PyObject *ans = self->modes.m##uname ? Py_True : Py_False; Py_INCREF(ans); return ans; } -MODE_GETTER(in_bracketed_paste, BRACKETED_PASTE) +MODE_GETTER(in_bracketed_paste_mode, BRACKETED_PASTE) MODE_GETTER(focus_tracking_enabled, FOCUS_TRACKING) MODE_GETTER(mouse_button_tracking_enabled, MOUSE_BUTTON_TRACKING) MODE_GETTER(mouse_motion_tracking_enabled, MOUSE_MOTION_TRACKING) @@ -1068,7 +1068,7 @@ static PyMethodDef methods[] = { MND(mark_as_dirty, METH_NOARGS) MND(resize, METH_VARARGS) MND(set_scroll_cell_data, METH_VARARGS) - MND(in_bracketed_paste, METH_NOARGS) + MND(in_bracketed_paste_mode, METH_NOARGS) MND(focus_tracking_enabled, METH_NOARGS) MND(mouse_button_tracking_enabled, METH_NOARGS) MND(mouse_motion_tracking_enabled, METH_NOARGS) diff --git a/kitty/shaders.py b/kitty/shaders.py index a01dba753..b5a352a74 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -4,6 +4,7 @@ from ctypes import addressof, sizeof from functools import lru_cache +from threading import Lock from .fonts import render_cell from .fast_data_types import ( @@ -48,6 +49,7 @@ class Sprites: self.last_ynum = -1 self.texture_unit = GL_TEXTURE0 self.backend = SpriteMap(glGetIntegerv(GL_MAX_TEXTURE_SIZE), glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS)) + self.lock = Lock() def do_layout(self, cell_width=1, cell_height=1): self.cell_width, self.cell_height = cell_width, cell_height @@ -84,7 +86,8 @@ class Sprites: return first def render_dirty_cells(self): - self.backend.render_dirty_cells(self.render_cell, self.send_to_gpu) + with self.lock: + self.backend.render_dirty_cells(self.render_cell, self.send_to_gpu) def send_to_gpu(self, x, y, z, buf): if self.backend.z >= self.last_num_of_layers: diff --git a/kitty/tabs.py b/kitty/tabs.py new file mode 100644 index 000000000..b66bd813d --- /dev/null +++ b/kitty/tabs.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +import os +import io +import select +import signal +import struct +from collections import deque +from functools import partial +from itertools import count +from threading import Thread +from time import monotonic +from queue import Queue, Empty + +import glfw +import glfw_constants +from .child import Child +from .constants import viewport_size, shell_path, appname, set_tab_manager, tab_manager, wakeup, cell_size +from .fast_data_types import glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA +from .fonts import set_font_family +from .char_grid import cursor_shader, cell_shader +from .keys import interpret_text_event, interpret_key_event, get_shortcut +from .layout import Stack +from .shaders import Sprites, ShaderProgram +from .utils import handle_unix_signals +from .window import Window + + +timer_id = count() + + +class Tab: + + def __init__(self, opts, args): + self.opts, self.args = opts, args + self.windows = deque() + self.current_layout = Stack() + + @property + def is_visible(self): + return tab_manager().is_tab_visible(self) + + @property + def active_window(self): + return self.windows[0] if self.windows else None + + @property + def title(self): + return getattr(self.active_window, 'title', appname) + + def visible_windows(self): + for w in self.windows: + if w.is_visible_in_layout: + yield w + + def relayout(self): + if self.windows: + self.current_layout(self.windows) + + def launch_child(self, use_shell=False): + if use_shell: + cmd = [shell_path] + else: + cmd = self.args.args or [shell_path] + ans = Child(cmd, self.args.directory, self.opts) + ans.fork() + return ans + + def new_window(self, use_shell=False): + child = self.launch_child(use_shell=use_shell) + window = Window(self, child, self.opts, self.args) + tab_manager().add_child_fd(child.child_fd, window.read_ready, window.write_ready) + self.current_layout.add_window(self.windows, window) + + def remove_window(self, window): + self.current_layout.remove_window(self.windows, window) + + def __iter__(self): + yield from iter(self.windows) + + def __len__(self): + return len(self.windows) + + def __contains__(self, window): + return window in self.windows + + def destroy(self): + for w in self.windows: + w.destroy() + del self.windows + + def render(self): + # TODO: Render window borders and clear the extra pixels + pass + + +class TabManager(Thread): + + daemon = True + + def __init__(self, glfw_window, opts, args): + Thread.__init__(self, name='ChildMonitor') + self.glfw_window_title = None + self.action_queue = Queue() + self.pending_resize = None + self.resize_gl_viewport = False + self.shutting_down = False + self.screen_update_delay = opts.repaint_delay / 1000.0 + self.signal_fd = handle_unix_signals() + self.read_wakeup_fd, self.write_wakeup_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) + self.read_dispatch_map = {self.signal_fd: self.signal_received, self.read_wakeup_fd: self.on_wakeup} + self.all_writers = [] + self.timers = [] + self.write_dispatch_map = {} + set_tab_manager(self) + cell_size.width, cell_size.height = set_font_family(opts.font_family, opts.font_size) + self.opts, self.args = opts, args + self.glfw_window = glfw_window + glfw.glfwSetFramebufferSizeCallback(glfw_window, partial(self.queue_action, self.on_window_resize)) + glfw.glfwSetCharModsCallback(glfw_window, partial(self.queue_action, self.on_text_input)) + glfw.glfwSetKeyCallback(glfw_window, partial(self.queue_action, self.on_key)) + glfw.glfwSetMouseButtonCallback(glfw_window, partial(self.queue_action, self.on_mouse_button)) + glfw.glfwSetWindowFocusCallback(glfw_window, partial(self.queue_action, self.on_focus)) + self.tabs = deque() + self.tabs.append(Tab(opts, args)) + self.sprites = Sprites() + self.cell_program = ShaderProgram(*cell_shader) + self.cursor_program = ShaderProgram(*cursor_shader) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + self.sprites.do_layout(cell_size.width, cell_size.height) + self.queue_action(self.active_tab.new_window, False) + + def signal_received(self): + try: + data = os.read(self.signal_fd, io.DEFAULT_BUFFER_SIZE) + except BlockingIOError: + return + if data: + signals = struct.unpack('%uB' % len(data), data) + if signal.SIGINT in signals or signal.SIGTERM in signals: + self.shutdown() + + def shutdown(self): + if not self.shutting_down: + self.shutting_down = True + glfw.glfwSetWindowShouldClose(self.glfw_window, True) + glfw.glfwPostEmptyEvent() + + def __iter__(self): + yield from iter(self.tabs) + + def iterwindows(self): + for t in self: + yield from t + + def queue_action(self, func, *args): + self.action_queue.put((func, args)) + wakeup() + + def on_wakeup(self): + if not self.shutting_down: + try: + os.read(self.read_wakeup_fd, io.DEFAULT_BUFFER_SIZE) + except (EnvironmentError, BlockingIOError): + pass + while True: + try: + func, args = self.action_queue.get_nowait() + except Empty: + break + func(*args) + + def add_child_fd(self, child_fd, read_ready, write_ready): + self.read_dispatch_map[child_fd] = read_ready + self.write_dispatch_map[child_fd] = write_ready + + def remove_child_fd(self, child_fd): + self.read_dispatch_map.pop(child_fd, None) + self.write_dispatch_map.pop(child_fd, None) + try: + self.all_writers.remove(child_fd) + except Exception: + pass + + def close_window(self, window): + self.remove_child_fd(window.child_fd) + for tab in self.tabs: + if window in tab: + break + else: + return + tab.remove_window(window) + window.destroy() + if len(tab) == 0: + self.tabs.remove(tab) + tab.destroy() + if len(self.tabs) == 0: + self.shutdown() + + def call_after(self, delay, callback): + tid = next(timer_id) + self.timers.append((monotonic() + delay, tid, callback)) + if len(self.timers) > 1: + self.timers.sort() + return tid + + def run(self): + if self.args.profile: + import cProfile + import pstats + pr = cProfile.Profile() + pr.enable() + self.loop() + if self.args.profile: + pr.disable() + pr.create_stats() + s = pstats.Stats(pr) + s.dump_stats(self.args.profile) + + def loop(self): + while not self.shutting_down: + all_readers = list(self.read_dispatch_map) + all_writers = [w.child_fd for w in self.iterwindows() if w.write_buf] + timeout = max(0, self.timers[0][0] - monotonic()) if self.timers else None + readers, writers, _ = select.select(all_readers, all_writers, [], timeout) + for r in readers: + self.read_dispatch_map[r]() + for w in writers: + self.write_dispatch_map[w]() + timers = [] + callbacks = set() + for epoch, tid, callback in self.timers: + if epoch <= monotonic(): + callback() + else: + timers.append((epoch, tid, callback)) + callbacks.add(callback) + update_at = monotonic() + self.screen_update_delay + before = len(timers) + for w in self.iterwindows(): + if w.screen.is_dirty() and w.update_screen not in callbacks: + timers.append((update_at, next(timer_id), w.update_screen)) + if len(timers) > before: + timers.sort() + self.timers = timers + + def on_window_resize(self, window, w, h): + # debounce resize events + self.pending_resize = [monotonic(), w, h] + self.call_after(0.02, self.apply_pending_resize) + + def apply_pending_resize(self): + if self.pending_resize is None: + return + if monotonic() - self.pending_resize[0] < 0.02: + self.call_after(0.02, self.apply_pending_resize) + return + viewport_size.width, viewport_size.height = self.pending_resize[1:] + for tab in self.tabs: + tab.relayout() + self.pending_resize = None + self.resize_gl_viewport = True + glfw.glfwPostEmptyEvent() + + @property + def active_tab(self): + return self.tabs[0] if self.tabs else None + + def is_tab_visible(self, tab): + return self.active_tab is tab + + @property + def active_window(self): + t = self.active_tab + if t is not None: + return t.active_window + + def on_text_input(self, window, codepoint, mods): + data = interpret_text_event(codepoint, mods) + if data: + w = self.active_window + if w is not None: + w.write_to_child(data) + + def on_key(self, window, key, scancode, action, mods): + if action == glfw_constants.GLFW_PRESS or action == glfw_constants.GLFW_REPEAT: + func = get_shortcut(self.opts.keymap, mods, key) + tab = self.active_tab + window = self.active_window + if func is not None: + func = getattr(self, func, getattr(tab, func, getattr(window, func, None))) + if func is not None: + passthrough = func() + if not passthrough: + return + if window: + if window.char_grid.scrolled_by: + window.scroll_end() + data = interpret_key_event(key, scancode, mods) + if data: + window.write_to_child(data) + + def on_focus(self, window, focused): + w = self.active_window + if w is not None: + w.focus_changed(focused) + + def on_mouse_button(self, window, button, action, mods): + if action == glfw_constants.GLFW_RELEASE: + if button == glfw_constants.GLFW_MOUSE_BUTTON_MIDDLE: + w = self.active_window + if w is not None: + w.paste_from_selection() + return + + # GUI thread API {{{ + def render(self): + if self.pending_resize: + return + if self.resize_gl_viewport: + glViewport(0, 0, viewport_size.width, viewport_size.height) + self.resize_gl_viewport = False + tab = self.active_tab + if tab is not None: + if tab.title != self.glfw_window_title: + self.glfw_window_title = tab.title + glfw.glfwSetWindowTitle(self.glfw_window, self.glfw_window_title.encode('utf-8')) + with self.sprites: + self.sprites.render_dirty_cells() + tab.render() + render_data = {window: window.char_grid.prepare_for_render(self.sprites) for window in tab.visible_windows()} + active = self.active_window + with self.cell_program: + for window, rd in render_data.items(): + if rd is not None: + window.char_grid.render_cells(rd, self.cell_program, self.sprites) + rd = render_data.get(active) + if rd is not None: + with self.cursor_program: + active.char_grid.render_cursor(rd, self.cursor_program) + + def destroy(self): + # Must be called in the main thread as it manipulates signal handlers + signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGTERM, signal.SIG_DFL) + self.shutting_down = True + wakeup() + self.join() + for t in self.tabs: + t.destroy() + del self.tabs + self.sprites.destroy() + del self.sprites + # }}} diff --git a/kitty/utils.py b/kitty/utils.py index c25c4f977..de7dd92c0 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -3,6 +3,8 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal import re +import os +import signal import subprocess import ctypes from collections import namedtuple @@ -249,3 +251,12 @@ def parse_color_set(raw): yield c, r << 16 | g << 8 | b except Exception: continue + + +def handle_unix_signals(): + read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) + for sig in (signal.SIGINT, signal.SIGTERM): + signal.signal(sig, lambda x, y: None) + signal.siginterrupt(sig, False) + signal.set_wakeup_fd(write_fd) + return read_fd diff --git a/kitty/window.py b/kitty/window.py new file mode 100644 index 000000000..af8e3c19e --- /dev/null +++ b/kitty/window.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +import os +import weakref +import subprocess +from functools import partial + +import glfw +from .char_grid import CharGrid +from .constants import wakeup, tab_manager, appname, WindowGeometry +from .fast_data_types import ( + BRACKETED_PASTE_START, BRACKETED_PASTE_END, Screen, read_bytes_dump, read_bytes +) +from .terminfo import get_capabilities +from .utils import sanitize_title + + +class Window: + + def __init__(self, tab, child, opts, args): + self.tabref = weakref.ref(tab) + self.geometry = WindowGeometry(0, 0, 0, 0, 0, 0) + self.needs_layout = True + self.title = appname + self.is_visible_in_layout = True + self.child, self.opts = child, opts + self.child_fd = child.child_fd + self.screen = Screen(self, 24, 80, opts.scrollback_lines) + self.read_bytes = partial(read_bytes_dump, self.dump_commands) if args.dump_commands else read_bytes + self.draw_dump_buf = [] + self.write_buf = memoryview(b'') + self.char_grid = CharGrid(self.screen, opts) + + def refresh(self): + self.screen.mark_as_dirty() + wakeup() + + def set_geometry(self, new_geometry): + if self.needs_layout or new_geometry.xnum != self.screen.columns or new_geometry.ynum != self.screen.lines: + self.screen.resize(new_geometry.ynum, new_geometry.xnum) + self.child.resize_pty(self.screen.columns, self.screen.lines) + self.char_grid.resize(new_geometry) + self.needs_layout = False + else: + self.char_grid.update_position(new_geometry) + self.geometry = new_geometry + + def close(self): + tab_manager().close_window(self) + + def destroy(self): + self.child.hangup() + self.child.get_child_status() # Ensure child does not become zombie + + def read_ready(self): + if self.read_bytes(self.screen, self.child_fd) is False: + self.close() # EOF + + def write_ready(self): + while self.write_buf: + try: + n = os.write(self.child_fd, self.write_buf) + except BlockingIOError: + n = 0 + if not n: + return + self.write_buf = self.write_buf[n:] + + def write_to_child(self, data): + self.write_buf = memoryview(self.write_buf.tobytes() + data) + wakeup() + + def update_screen(self): + self.char_grid.update_cell_data() + glfw.glfwPostEmptyEvent() + + def focus_changed(self, focused): + if focused: + if self.screen.focus_tracking_enabled(): + self.write_to_child(b'\x1b[I') + else: + if self.screen.focus_tracking_enabled(): + self.write_to_child(b'\x1b[O') + + def title_changed(self, new_title): + self.title = sanitize_title(new_title or appname) + glfw.glfwPostEmptyEvent() + + def icon_changed(self, new_icon): + pass # TODO: Implement this + + def set_dynamic_color(self, code, value): + wmap = {10: 'fg', 11: 'bg', 110: 'fg', 111: 'bg'} + if isinstance(value, bytes): + value = value.decode('utf-8') + color_changes = {} + for val in value.split(';'): + w = wmap.get(code) + if w is not None: + if code >= 110: + val = None + color_changes[w] = val + code += 1 + self.char_grid.change_colors(color_changes) + glfw.glfwPostEmptyEvent() + + def request_capabilities(self, q): + self.write_to_child(get_capabilities(q)) + + # actions {{{ + + def paste(self, text): + if text: + if self.screen.in_bracketed_paste_mode(): + text = BRACKETED_PASTE_START.encode('ascii') + text + BRACKETED_PASTE_END.encode('ascii') + self.write_to_child(text) + + def paste_from_clipboard(self): + text = glfw.glfwGetClipboardString(self.window) + self.paste(text) + + def paste_from_selection(self): + # glfw has no way to get the primary selection + # https://github.com/glfw/glfw/issues/894 + text = subprocess.check_output(['xsel']) + self.paste(text) + + def scroll_line_up(self): + self.queue_action(self.char_grid.scroll, 'line', True) + + def scroll_line_down(self): + self.queue_action(self.char_grid.scroll, 'line', False) + + def scroll_page_up(self): + self.queue_action(self.char_grid.scroll, 'page', True) + + def scroll_page_down(self): + self.queue_action(self.char_grid.scroll, 'page', False) + + def scroll_home(self): + self.queue_action(self.char_grid.scroll, 'full', True) + + def scroll_end(self): + self.queue_action(self.char_grid.scroll, 'full', False) + # }}} + + def dump_commands(self, *a): + if a: + if a[0] == 'draw': + if a[1] is None: + if self.draw_dump_buf: + print('draw', ''.join(self.draw_dump_buf)) + self.draw_dump_buf = [] + else: + self.draw_dump_buf.append(a[1]) + else: + if self.draw_dump_buf: + print('draw', ''.join(self.draw_dump_buf)) + self.draw_dump_buf = [] + print(*a)