Refactor to support multiple windows/tabs
This commit is contained in:
parent
03f7ced17e
commit
b5000c2ec0
342
kitty/boss.py
342
kitty/boss.py
@ -1,342 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
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)
|
||||
# }}}
|
||||
@ -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)
|
||||
|
||||
@ -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'
|
||||
|
||||
57
kitty/layout.py
Normal file
57
kitty/layout.py
Normal file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
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)
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
356
kitty/tabs.py
Normal file
356
kitty/tabs.py
Normal file
@ -0,0 +1,356 @@
|
||||
#!/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 .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
|
||||
# }}}
|
||||
@ -3,6 +3,8 @@
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
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
|
||||
|
||||
162
kitty/window.py
Normal file
162
kitty/window.py
Normal file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user