kitty/kitty/tabs.py

381 lines
13 KiB
Python

#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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 .borders import Borders, BordersProgram
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.borders = Borders(opts)
self.current_layout = Stack(opts, self.borders.border_width)
@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)
self.borders(self.windows, self.active_window, self.current_layout.needs_window_borders)
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):
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.glfw_window_title = None
self.current_tab_bar_height = 0
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.glfwSetScrollCallback(glfw_window, partial(self.queue_action, self.on_mouse_scroll))
glfw.glfwSetCursorPosCallback(glfw_window, partial(self.queue_action, self.on_mouse_move))
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)
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)
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
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 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 and key not in glfw_constants.MODIFIER_KEYS:
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 window_for_pos(self, x, y):
for w in self.active_tab:
if w.is_visible_in_layout and w.contains(x, y):
return w
def on_mouse_button(self, window, button, action, mods):
w = self.window_for_pos(*glfw.glfwGetCursorPos(window))
if w is not None:
if button == glfw_constants.GLFW_MOUSE_BUTTON_1 and w is not self.active_window:
pass # TODO: Switch focus to this window
w.on_mouse_button(window, button, action, mods)
def on_mouse_move(self, window, xpos, ypos):
w = self.window_for_pos(*glfw.glfwGetCursorPos(window))
if w is not None:
w.on_mouse_move(xpos, ypos)
def on_mouse_scroll(self, window, x, y):
w = self.window_for_pos(*glfw.glfwGetCursorPos(window))
if w is not None:
w.on_mouse_scroll(x, y)
# 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
# }}}