diff --git a/kitty/boss.py b/kitty/boss.py new file mode 100644 index 000000000..77e74d6fd --- /dev/null +++ b/kitty/boss.py @@ -0,0 +1,425 @@ +#!/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 +import inspect +from collections import deque +from functools import wraps +from threading import Thread, current_thread +from time import monotonic +from queue import Queue, Empty + +from .constants import ( + viewport_size, set_tab_manager, wakeup, cell_size, MODIFIER_KEYS, + main_thread, mouse_button_pressed, mouse_cursor_pos +) +from .fast_data_types import ( + glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLFW_PRESS, + GLFW_REPEAT, GLFW_MOUSE_BUTTON_1, glfw_post_empty_event, + GLFW_CURSOR_NORMAL, GLFW_CURSOR, GLFW_CURSOR_HIDDEN, drain_read +) +from .fonts import set_font_family +from .borders import BordersProgram +from .char_grid import cursor_shader, cell_shader +from .constants import is_key_pressed +from .keys import interpret_text_event, interpret_key_event, get_shortcut +from .shaders import Sprites, ShaderProgram +from .tabs import Tab +from .timers import Timers +from .utils import handle_unix_signals + + +def conditional_run(w, i): + if w is None or not w.destroyed: + next(i, None) + + +def callback(func): + ''' Wrapper for function that executes first half (up to a yield statement) + in the UI thread and the rest in the child thread. If the function yields + something, the destroyed attribute of that something is checked before + running the second half. If the function returns before the yield, the + second half is not run. ''' + + assert inspect.isgeneratorfunction(func) + + @wraps(func) + def f(self, *a): + i = func(self, *a) + try: + w = next(i) + except StopIteration: + pass + else: + self.queue_action(conditional_run, w, i) + return f + + +class Boss(Thread): + + daemon = True + + def __init__(self, glfw_window, opts, args): + Thread.__init__(self, name='ChildMonitor') + self.cursor_blinking = True + self.glfw_window_title = None + self.current_tab_bar_height = 0 + self.action_queue = Queue() + self.pending_resize = True + 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 = Timers() + self.ui_timers = Timers() + self.pending_ui_thread_calls = Queue() + 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_window.framebuffer_size_callback = self.on_window_resize + glfw_window.char_mods_callback = self.on_text_input + glfw_window.key_callback = self.on_key + glfw_window.mouse_button_callback = self.on_mouse_button + glfw_window.scroll_callback = self.on_mouse_scroll + glfw_window.cursor_pos_callback = self.on_mouse_move + glfw_window.window_focus_callback = 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) + self.borders_program = BordersProgram() + 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) + self.glfw_window.set_click_cursor(False) + self.show_mouse_cursor() + self.start_cursor_blink() + + 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: + if not self.shutting_down: + self.glfw_window.set_should_close(True) + glfw_post_empty_event() + + 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: + drain_read(self.read_wakeup_fd) + while True: + try: + func, args = self.action_queue.get_nowait() + except Empty: + break + try: + func(*args) + except Exception: + import traceback + traceback.print_exc() + + 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 queue_ui_action(self, func, *args): + self.pending_ui_thread_calls.put((func, args)) + glfw_post_empty_event() + + def close_window(self, window=None): + ' Can be called in either thread, will first kill the child, then remove the window from the gui ' + if window is None: + window = self.active_window + if current_thread() is main_thread: + self.queue_action(self.close_window, window) + else: + self.remove_child_fd(window.child_fd) + window.destroy() + self.queue_ui_action(self.gui_close_window, window) + + def close_tab(self, tab=None): + ' Can be called in either thread, will first kill all children, then remove the tab from the gui ' + if tab is None: + tab = self.active_tab + if current_thread() is main_thread: + self.queue_action(self.close_tab, tab) + else: + for window in tab: + self.remove_child_fd(window.child_fd) + window.destroy() + self.queue_ui_action(self.gui_close_window, window) + + 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] + readers, writers, _ = select.select(all_readers, all_writers, [], self.timers.timeout()) + for r in readers: + self.read_dispatch_map[r]() + for w in writers: + self.write_dispatch_map[w]() + self.timers() + for w in self.iterwindows(): + if w.screen.is_dirty(): + self.timers.add_if_missing(self.screen_update_delay, w.update_screen) + + @callback + def on_window_resize(self, window, w, h): + # debounce resize events + self.pending_resize = True + yield + self.timers.add(0.02, self.apply_pending_resize, w, h) + + def apply_pending_resize(self, w, h): + viewport_size.width, viewport_size.height = w, h + for tab in self.tabs: + tab.relayout() + self.resize_gl_viewport = True + self.pending_resize = False + glfw_post_empty_event() + + @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 + + @callback + 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: + yield w + w.write_to_child(data) + + @callback + def on_key(self, window, key, scancode, action, mods): + is_key_pressed[key] = action == GLFW_PRESS + self.start_cursor_blink() + if action == GLFW_PRESS or action == GLFW_REPEAT: + func = get_shortcut(self.opts.keymap, mods, key) + tab = self.active_tab + if tab is None: + return + if func is not None: + f = getattr(self, func, None) + if f is not None: + passthrough = f() + if not passthrough: + return + window = self.active_window + if window is not None: + yield window + if func is not None: + f = getattr(tab, func, getattr(window, func, None)) + if f is not None: + passthrough = f() + if not passthrough: + return + if window.screen.auto_repeat_enabled() or action == GLFW_PRESS: + if window.char_grid.scrolled_by and key not in MODIFIER_KEYS: + window.scroll_end() + data = interpret_key_event(key, scancode, mods) + if data: + window.write_to_child(data) + + @callback + def on_focus(self, window, focused): + w = self.active_window + if w is not None: + yield w + w.focus_changed(focused) + + def window_for_pos(self, x, y): + tab = self.active_tab + if tab is not None: + for w in tab: + if w.is_visible_in_layout and w.contains(x, y): + return w + + @callback + def on_mouse_button(self, window, button, action, mods): + mouse_button_pressed[button] = action == GLFW_PRESS + self.show_mouse_cursor() + w = self.window_for_pos(*mouse_cursor_pos) + if w is None: + return + focus_moved = False + old_focus = self.active_window + tab = self.active_tab + yield + if button == GLFW_MOUSE_BUTTON_1 and w is not old_focus: + tab.set_active_window(w) + focus_moved = True + if focus_moved: + if old_focus is not None and not old_focus.destroyed: + old_focus.focus_changed(False) + w.focus_changed(True) + w.on_mouse_button(button, action, mods) + + @callback + def on_mouse_move(self, window, xpos, ypos): + mouse_cursor_pos[:2] = xpos, ypos + self.show_mouse_cursor() + w = self.window_for_pos(xpos, ypos) + if w is not None: + yield w + w.on_mouse_move(xpos, ypos) + + @callback + def on_mouse_scroll(self, window, x, y): + self.show_mouse_cursor() + w = self.window_for_pos(*mouse_cursor_pos) + if w is not None: + yield w + w.on_mouse_scroll(x, y) + + # GUI thread API {{{ + + def show_mouse_cursor(self): + self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_NORMAL) + if self.opts.mouse_hide_wait > 0: + self.ui_timers.add(self.opts.mouse_hide_wait, self.hide_mouse_cursor) + + def hide_mouse_cursor(self): + self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_HIDDEN) + + def change_mouse_cursor(self, click=False): + self.glfw_window.set_click_cursor(click) + + def start_cursor_blink(self): + self.cursor_blinking = True + if self.opts.cursor_stop_blinking_after > 0: + self.ui_timers.add(self.opts.cursor_stop_blinking_after, self.stop_cursor_blinking) + + def stop_cursor_blinking(self): + self.cursor_blinking = False + + 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 + self.glfw_window.set_title(self.glfw_window_title) + 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() if not window.needs_layout} + 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) + active = self.active_window + rd = render_data.get(active) + if rd is not None: + draw_cursor = True + if self.cursor_blinking and self.opts.cursor_blink_interval > 0: + now = monotonic() + t = int(now * 1000) + d = int(self.opts.cursor_blink_interval * 1000) + n = t // d + draw_cursor = n % 2 == 0 + self.ui_timers.add_if_missing(((n + 1) * d / 1000) - now, glfw_post_empty_event) + if draw_cursor: + with self.cursor_program: + active.char_grid.render_cursor(rd, self.cursor_program) + + def gui_close_window(self, window): + if window.char_grid.buffer_id is not None: + self.sprites.destroy_sprite_map(window.char_grid.buffer_id) + window.char_grid.buffer_id = None + for tab in self.tabs: + if window in tab: + break + else: + return + tab.remove_window(window) + if len(tab) == 0: + self.tabs.remove(tab) + tab.destroy() + if len(self.tabs) == 0: + if not self.shutting_down: + self.glfw_window.set_should_close(True) + glfw_post_empty_event() + + 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 + del self.glfw_window + + def paste_from_clipboard(self): + text = self.glfw_window.get_clipboard_string() + if text: + w = self.active_window + if w is not None: + self.queue_action(w.paste, text) + + # }}} diff --git a/kitty/main.py b/kitty/main.py index 5c7f37cdf..802dc5781 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -13,7 +13,7 @@ from gettext import gettext as _ from .config import load_config from .constants import appname, str_version, config_dir, viewport_size from .layout import all_layouts -from .tabs import TabManager +from .boss import Boss from .shaders import GL_VERSION from .fast_data_types import ( glewInit, enable_automatic_opengl_error_checking, glClear, glClearColor, @@ -66,10 +66,10 @@ def clear_buffers(window, opts): # glfw_swap_interval(1) -def dispatch_pending_calls(tabs): +def dispatch_pending_calls(boss): while True: try: - func, args = tabs.pending_ui_thread_calls.get_nowait() + func, args = boss.pending_ui_thread_calls.get_nowait() except Empty: break try: @@ -77,7 +77,7 @@ def dispatch_pending_calls(tabs): except Exception: import traceback traceback.print_exc() - tabs.ui_timers() + boss.ui_timers() def run_app(opts, args): @@ -87,17 +87,17 @@ def run_app(opts, args): window.set_title(appname) window.make_context_current() glewInit() - tabs = TabManager(window, opts, args) - tabs.start() + boss = Boss(window, opts, args) + boss.start() clear_buffers(window, opts) try: while not window.should_close(): - tabs.render() + boss.render() window.swap_buffers() - glfw_wait_events(tabs.ui_timers.timeout()) - dispatch_pending_calls(tabs) + glfw_wait_events(boss.ui_timers.timeout()) + dispatch_pending_calls(boss) finally: - tabs.destroy() + boss.destroy() del window diff --git a/kitty/tabs.py b/kitty/tabs.py index 23bd74471..7d2e0c86d 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -2,66 +2,16 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -import os -import io -import select -import signal -import struct -import inspect from collections import deque -from functools import wraps -from threading import Thread, current_thread -from time import monotonic -from queue import Queue, Empty from .child import Child -from .constants import ( - viewport_size, shell_path, appname, set_tab_manager, tab_manager, wakeup, - cell_size, MODIFIER_KEYS, main_thread, mouse_button_pressed, mouse_cursor_pos -) -from .fast_data_types import ( - glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLFW_PRESS, - GLFW_REPEAT, GLFW_MOUSE_BUTTON_1, glfw_post_empty_event, - GLFW_CURSOR_NORMAL, GLFW_CURSOR, GLFW_CURSOR_HIDDEN, drain_read -) -from .fonts import set_font_family -from .borders import Borders, BordersProgram -from .char_grid import cursor_shader, cell_shader -from .constants import is_key_pressed -from .keys import interpret_text_event, interpret_key_event, get_shortcut +from .constants import tab_manager, appname, shell_path +from .fast_data_types import glfw_post_empty_event from .layout import all_layouts -from .shaders import Sprites, ShaderProgram -from .timers import Timers -from .utils import handle_unix_signals +from .borders import Borders from .window import Window -def conditional_run(w, i): - if w is None or not w.destroyed: - next(i, None) - - -def callback(func): - ''' Wrapper for function that executes first half (up to a yield statement) - in the UI thread and the rest in the child thread. If the function yields - something, the destroyed attribute of that something is checked before - running the second half. If the function returns before the yield, the - second half is not run. ''' - - assert inspect.isgeneratorfunction(func) - - @wraps(func) - def f(self, *a): - i = func(self, *a) - try: - w = next(i) - except StopIteration: - pass - else: - self.queue_action(conditional_run, w, i) - return f - - class Tab: def __init__(self, opts, args): @@ -174,368 +124,3 @@ class Tab: def render(self): self.borders.render(tab_manager().borders_program) - - -class TabManager(Thread): - - daemon = True - - def __init__(self, glfw_window, opts, args): - Thread.__init__(self, name='ChildMonitor') - self.cursor_blinking = True - self.glfw_window_title = None - self.current_tab_bar_height = 0 - self.action_queue = Queue() - self.pending_resize = True - 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 = Timers() - self.ui_timers = Timers() - self.pending_ui_thread_calls = Queue() - 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_window.framebuffer_size_callback = self.on_window_resize - glfw_window.char_mods_callback = self.on_text_input - glfw_window.key_callback = self.on_key - glfw_window.mouse_button_callback = self.on_mouse_button - glfw_window.scroll_callback = self.on_mouse_scroll - glfw_window.cursor_pos_callback = self.on_mouse_move - glfw_window.window_focus_callback = 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) - self.borders_program = BordersProgram() - 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) - self.glfw_window.set_click_cursor(False) - self.show_mouse_cursor() - self.start_cursor_blink() - - 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: - if not self.shutting_down: - self.glfw_window.set_should_close(True) - glfw_post_empty_event() - - 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: - drain_read(self.read_wakeup_fd) - while True: - try: - func, args = self.action_queue.get_nowait() - except Empty: - break - try: - func(*args) - except Exception: - import traceback - traceback.print_exc() - - 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 queue_ui_action(self, func, *args): - self.pending_ui_thread_calls.put((func, args)) - glfw_post_empty_event() - - def close_window(self, window=None): - ' Can be called in either thread, will first kill the child, then remove the window from the gui ' - if window is None: - window = self.active_window - if current_thread() is main_thread: - self.queue_action(self.close_window, window) - else: - self.remove_child_fd(window.child_fd) - window.destroy() - self.queue_ui_action(self.gui_close_window, window) - - def close_tab(self, tab=None): - ' Can be called in either thread, will first kill all children, then remove the tab from the gui ' - if tab is None: - tab = self.active_tab - if current_thread() is main_thread: - self.queue_action(self.close_tab, tab) - else: - for window in tab: - self.remove_child_fd(window.child_fd) - window.destroy() - self.queue_ui_action(self.gui_close_window, window) - - 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] - readers, writers, _ = select.select(all_readers, all_writers, [], self.timers.timeout()) - for r in readers: - self.read_dispatch_map[r]() - for w in writers: - self.write_dispatch_map[w]() - self.timers() - for w in self.iterwindows(): - if w.screen.is_dirty(): - self.timers.add_if_missing(self.screen_update_delay, w.update_screen) - - @callback - def on_window_resize(self, window, w, h): - # debounce resize events - self.pending_resize = True - yield - self.timers.add(0.02, self.apply_pending_resize, w, h) - - def apply_pending_resize(self, w, h): - viewport_size.width, viewport_size.height = w, h - for tab in self.tabs: - tab.relayout() - self.resize_gl_viewport = True - self.pending_resize = False - glfw_post_empty_event() - - @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 - - @callback - 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: - yield w - w.write_to_child(data) - - @callback - def on_key(self, window, key, scancode, action, mods): - is_key_pressed[key] = action == GLFW_PRESS - self.start_cursor_blink() - if action == GLFW_PRESS or action == GLFW_REPEAT: - func = get_shortcut(self.opts.keymap, mods, key) - tab = self.active_tab - if tab is None: - return - if func is not None: - f = getattr(self, func, None) - if f is not None: - passthrough = f() - if not passthrough: - return - window = self.active_window - if window is not None: - yield window - if func is not None: - f = getattr(tab, func, getattr(window, func, None)) - if f is not None: - passthrough = f() - if not passthrough: - return - if window.screen.auto_repeat_enabled() or action == GLFW_PRESS: - if window.char_grid.scrolled_by and key not in MODIFIER_KEYS: - window.scroll_end() - data = interpret_key_event(key, scancode, mods) - if data: - window.write_to_child(data) - - @callback - def on_focus(self, window, focused): - w = self.active_window - if w is not None: - yield w - w.focus_changed(focused) - - def window_for_pos(self, x, y): - tab = self.active_tab - if tab is not None: - for w in tab: - if w.is_visible_in_layout and w.contains(x, y): - return w - - @callback - def on_mouse_button(self, window, button, action, mods): - mouse_button_pressed[button] = action == GLFW_PRESS - self.show_mouse_cursor() - w = self.window_for_pos(*mouse_cursor_pos) - if w is None: - return - focus_moved = False - old_focus = self.active_window - tab = self.active_tab - yield - if button == GLFW_MOUSE_BUTTON_1 and w is not old_focus: - tab.set_active_window(w) - focus_moved = True - if focus_moved: - if old_focus is not None and not old_focus.destroyed: - old_focus.focus_changed(False) - w.focus_changed(True) - w.on_mouse_button(button, action, mods) - - @callback - def on_mouse_move(self, window, xpos, ypos): - mouse_cursor_pos[:2] = xpos, ypos - self.show_mouse_cursor() - w = self.window_for_pos(xpos, ypos) - if w is not None: - yield w - w.on_mouse_move(xpos, ypos) - - @callback - def on_mouse_scroll(self, window, x, y): - self.show_mouse_cursor() - w = self.window_for_pos(*mouse_cursor_pos) - if w is not None: - yield w - w.on_mouse_scroll(x, y) - - # GUI thread API {{{ - - def show_mouse_cursor(self): - self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_NORMAL) - if self.opts.mouse_hide_wait > 0: - self.ui_timers.add(self.opts.mouse_hide_wait, self.hide_mouse_cursor) - - def hide_mouse_cursor(self): - self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_HIDDEN) - - def change_mouse_cursor(self, click=False): - self.glfw_window.set_click_cursor(click) - - def start_cursor_blink(self): - self.cursor_blinking = True - if self.opts.cursor_stop_blinking_after > 0: - self.ui_timers.add(self.opts.cursor_stop_blinking_after, self.stop_cursor_blinking) - - def stop_cursor_blinking(self): - self.cursor_blinking = False - - 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 - self.glfw_window.set_title(self.glfw_window_title) - 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() if not window.needs_layout} - 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) - active = self.active_window - rd = render_data.get(active) - if rd is not None: - draw_cursor = True - if self.cursor_blinking and self.opts.cursor_blink_interval > 0: - now = monotonic() - t = int(now * 1000) - d = int(self.opts.cursor_blink_interval * 1000) - n = t // d - draw_cursor = n % 2 == 0 - self.ui_timers.add_if_missing(((n + 1) * d / 1000) - now, glfw_post_empty_event) - if draw_cursor: - with self.cursor_program: - active.char_grid.render_cursor(rd, self.cursor_program) - - def gui_close_window(self, window): - if window.char_grid.buffer_id is not None: - self.sprites.destroy_sprite_map(window.char_grid.buffer_id) - window.char_grid.buffer_id = None - for tab in self.tabs: - if window in tab: - break - else: - return - tab.remove_window(window) - if len(tab) == 0: - self.tabs.remove(tab) - tab.destroy() - if len(self.tabs) == 0: - if not self.shutting_down: - self.glfw_window.set_should_close(True) - glfw_post_empty_event() - - 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 - del self.glfw_window - - def paste_from_clipboard(self): - text = self.glfw_window.get_clipboard_string() - if text: - w = self.active_window - if w is not None: - self.queue_action(w.paste, text) - - # }}}