mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-14 20:44:32 +02:00
Refactor to support multiple windows/tabs
This commit is contained in:
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 collections import namedtuple
|
||||||
from ctypes import c_uint, addressof, memmove, sizeof
|
from ctypes import c_uint, addressof, memmove, sizeof
|
||||||
from queue import Queue, Empty
|
from itertools import count
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
from .config import build_ansi_color_table
|
from .config import build_ansi_color_table
|
||||||
from .fonts import set_font_family
|
from .constants import tab_manager, viewport_size, cell_size, ScreenGeometry
|
||||||
from .shaders import Sprites, ShaderProgram
|
|
||||||
from .utils import get_logical_dpi, to_color
|
from .utils import get_logical_dpi, to_color
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, glClear,
|
glUniform2ui, glUniform4f, glUniform1i, glUniform2f, glDrawArraysInstanced,
|
||||||
GL_COLOR_BUFFER_BIT, glClearColor, glViewport, glUniform2ui, glUniform4f,
|
GL_TRIANGLE_FAN, glEnable, glDisable, GL_BLEND, glDrawArrays, ColorProfile,
|
||||||
glUniform1i, glUniform2f, glDrawArraysInstanced, GL_TRIANGLE_FAN,
|
|
||||||
glEnable, glDisable, GL_BLEND, glDrawArrays, ColorProfile,
|
|
||||||
CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, DATA_CELL_SIZE
|
CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, DATA_CELL_SIZE
|
||||||
)
|
)
|
||||||
|
|
||||||
Size = namedtuple('Size', 'width height')
|
|
||||||
Cursor = namedtuple('Cursor', 'x y hidden shape color blink')
|
Cursor = namedtuple('Cursor', 'x y hidden shape color blink')
|
||||||
ScreenGeometry = namedtuple('ScreenGeometry', 'xstart ystart xnum ynum dx dy')
|
|
||||||
|
|
||||||
if DATA_CELL_SIZE % 3:
|
if DATA_CELL_SIZE % 3:
|
||||||
raise ValueError('Incorrect data cell size, must be a multiple of 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):
|
def color_as_int(val):
|
||||||
return val[0] << 16 | val[1] << 8 | val[2]
|
return val[0] << 16 | val[1] << 8 | val[2]
|
||||||
|
|
||||||
|
|
||||||
|
render_data_num = count()
|
||||||
|
|
||||||
|
|
||||||
class CharGrid:
|
class CharGrid:
|
||||||
|
|
||||||
def __init__(self, screen, opts, window_width, window_height):
|
def __init__(self, screen, opts):
|
||||||
self.sprites_lock, self.buffer_lock = Lock(), Lock()
|
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.scrolled_by = 0
|
||||||
self.dpix, self.dpiy = get_logical_dpi()
|
|
||||||
self.width, self.height = window_width, window_height
|
|
||||||
self.color_profile = ColorProfile()
|
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.screen = screen
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.original_bg = opts.background
|
self.original_bg = opts.background
|
||||||
self.original_fg = opts.foreground
|
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.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_bg = color_as_int(self.original_bg)
|
||||||
self.default_fg = color_as_int(self.original_fg)
|
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.dpix, self.dpiy = get_logical_dpi()
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.screen.change_scrollback_size(opts.scrollback_lines)
|
self.default_cursor = self.current_cursor = Cursor(0, 0, False, opts.cursor_shape, opts.cursor, opts.cursor_blink)
|
||||||
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.opts = opts
|
self.opts = opts
|
||||||
self.original_bg = opts.background
|
self.original_bg = opts.background
|
||||||
self.original_fg = opts.foreground
|
self.original_fg = opts.foreground
|
||||||
self.cell_width, self.cell_height = set_font_family(opts.font_family, opts.font_size)
|
self.sprite_map_type = self.main_sprite_map = self.scroll_sprite_map = self.render_buf = None
|
||||||
self.sprites.do_layout(self.cell_width, self.cell_height)
|
|
||||||
self.do_layout(self.width, self.height)
|
|
||||||
|
|
||||||
def resize_screen(self, w, h):
|
def update_position(self, window_geometry):
|
||||||
' Screen was resized by the user (called in non-UI thread) '
|
dx, dy = 2 * cell_size.width / viewport_size.width, 2 * cell_size.height / viewport_size.height
|
||||||
self.do_layout(w, h)
|
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):
|
def resize(self, window_geometry):
|
||||||
self.width, self.height = w, h
|
self.update_position(window_geometry)
|
||||||
self.screen_geometry = sg = calculate_screen_geometry(self.cell_width, self.cell_height, self.width, self.height)
|
self.sprite_map_type = (c_uint * (self.screen_geometry.ynum * self.screen_geometry.xnum * DATA_CELL_SIZE))
|
||||||
self.screen.resize(sg.ynum, sg.xnum)
|
|
||||||
self.sprite_map_type = (c_uint * (sg.ynum * sg.xnum * DATA_CELL_SIZE))
|
|
||||||
self.main_sprite_map = self.sprite_map_type()
|
self.main_sprite_map = self.sprite_map_type()
|
||||||
self.scroll_sprite_map = self.sprite_map_type()
|
self.scroll_sprite_map = self.sprite_map_type()
|
||||||
self.render_buf = self.sprite_map_type()
|
with self.buffer_lock:
|
||||||
self.update_cell_data(add_viewport_data=True)
|
self.render_buf = self.sprite_map_type()
|
||||||
self.clear_count = 4
|
self.render_num = next(render_data_num)
|
||||||
|
|
||||||
def change_colors(self, changes):
|
def change_colors(self, changes):
|
||||||
dirtied = False
|
dirtied = False
|
||||||
@@ -254,8 +207,7 @@ class CharGrid:
|
|||||||
setattr(self, 'default_' + which, color_as_int(val))
|
setattr(self, 'default_' + which, color_as_int(val))
|
||||||
dirtied = True
|
dirtied = True
|
||||||
if dirtied:
|
if dirtied:
|
||||||
self.render_queue.put(RenderData(clear_color=self.default_bg))
|
self.screen.mark_as_dirty()
|
||||||
self.clear_count = 4
|
|
||||||
|
|
||||||
def scroll(self, amt, upwards=True):
|
def scroll(self, amt, upwards=True):
|
||||||
amt = {'line': 1, 'page': self.screen.lines - 1, 'full': self.screen.historybuf.count}[amt]
|
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.scrolled_by = y
|
||||||
self.update_cell_data()
|
self.update_cell_data()
|
||||||
|
|
||||||
def update_cell_data(self, add_viewport_data=False):
|
def update_cell_data(self, force_full_refresh=False):
|
||||||
rd = RenderData(sprite_layout=self.sprites.layout, cell_data_changed=True)
|
sprites = tab_manager().sprites
|
||||||
if add_viewport_data:
|
with sprites.lock:
|
||||||
rd.viewport = Size(self.width, self.height)
|
|
||||||
rd.screen_geometry = self.screen_geometry
|
|
||||||
with self.sprites_lock:
|
|
||||||
cursor_changed, history_line_added_count = self.screen.update_cell_data(
|
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:
|
if self.scrolled_by:
|
||||||
self.scrolled_by = min(self.scrolled_by + history_line_added_count, self.screen.historybuf.count)
|
self.scrolled_by = min(self.scrolled_by + history_line_added_count, self.screen.historybuf.count)
|
||||||
self.screen.set_scroll_cell_data(
|
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))
|
self.scrolled_by, addressof(self.scroll_sprite_map))
|
||||||
|
|
||||||
data = self.scroll_sprite_map if self.scrolled_by else self.main_sprite_map
|
data = self.scroll_sprite_map if self.scrolled_by else self.main_sprite_map
|
||||||
with self.buffer_lock:
|
with self.buffer_lock:
|
||||||
memmove(self.render_buf, data, sizeof(type(data)))
|
memmove(self.render_buf, data, sizeof(type(data)))
|
||||||
|
self.render_num = next(render_data_num)
|
||||||
|
self.render_data = self.screen_geometry
|
||||||
if cursor_changed:
|
if cursor_changed:
|
||||||
c = self.screen.cursor
|
c = self.screen.cursor
|
||||||
rd.cursor = Cursor(c.x, c.y, c.hidden, c.shape, c.color, c.blink)
|
self.current_cursor = Cursor(c.x, c.y, c.hidden, c.shape, c.color, c.blink)
|
||||||
self.render_queue.put(rd)
|
|
||||||
|
|
||||||
def render(self):
|
def prepare_for_render(self, sprites):
|
||||||
' This is the only method in this class called in the UI thread (apart from __init__) '
|
with self.buffer_lock:
|
||||||
if self.clear_count > 0:
|
sg = self.render_data
|
||||||
glClear(GL_COLOR_BUFFER_BIT)
|
if sg is None:
|
||||||
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:
|
|
||||||
return
|
return
|
||||||
sg = data.screen_geometry
|
if self.last_render_send_num != self.render_num:
|
||||||
self.render_cells(sg, data.sprite_layout)
|
sprites.set_sprite_map(self.render_buf)
|
||||||
if not data.cursor.hidden and not self.scrolled_by:
|
self.last_render_send_num = self.render_num
|
||||||
self.render_cursor(sg, data.cursor)
|
return sg
|
||||||
|
|
||||||
def get_all_render_changes(self):
|
def render_cells(self, sg, cell_program, sprites):
|
||||||
cell_data_changed = False
|
ul = cell_program.uniform_location
|
||||||
data = self.last_render_data
|
glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum)
|
||||||
while True:
|
glUniform4f(ul('steps'), sg.xstart, sg.ystart, sg.dx, sg.dy)
|
||||||
try:
|
glUniform1i(ul('sprites'), sprites.sampler_num)
|
||||||
rd = self.render_queue.get_nowait()
|
glUniform1i(ul('sprite_map'), sprites.buffer_sampler_num)
|
||||||
except Empty:
|
glUniform2f(ul('sprite_layout'), *(sprites.layout))
|
||||||
break
|
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, sg.xnum * sg.ynum)
|
||||||
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, sprite_layout):
|
def render_cursor(self, sg, cursor_program):
|
||||||
with self.program:
|
cursor = self.current_cursor
|
||||||
ul = self.program.uniform_location
|
if cursor.hidden or self.scrolled_by:
|
||||||
glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum)
|
return
|
||||||
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 width(w=2, vert=True):
|
def width(w=2, vert=True):
|
||||||
dpi = self.dpix if vert else self.dpiy
|
dpi = self.dpix if vert else self.dpiy
|
||||||
@@ -344,22 +268,21 @@ class CharGrid:
|
|||||||
factor = 2 / (self.width if vert else self.height)
|
factor = 2 / (self.width if vert else self.height)
|
||||||
return w * factor
|
return w * factor
|
||||||
|
|
||||||
with self.cursor_program:
|
ul = cursor_program.uniform_location
|
||||||
ul = self.cursor_program.uniform_location
|
left = sg.xstart + cursor.x * sg.dx
|
||||||
left = sg.xstart + cursor.x * sg.dx
|
top = sg.ystart - cursor.y * sg.dy
|
||||||
top = sg.ystart - cursor.y * sg.dy
|
col = cursor.color or self.default_cursor.color
|
||||||
col = cursor.color or self.default_cursor.color
|
shape = cursor.shape or self.default_cursor.shape
|
||||||
shape = cursor.shape or self.default_cursor.shape
|
alpha = self.opts.cursor_opacity
|
||||||
alpha = self.opts.cursor_opacity
|
if alpha < 1.0 and shape == CURSOR_BLOCK:
|
||||||
if alpha < 1.0 and shape == CURSOR_BLOCK:
|
glEnable(GL_BLEND)
|
||||||
glEnable(GL_BLEND)
|
mult = self.screen.current_char_width()
|
||||||
mult = self.screen.current_char_width()
|
right = left + (width(1.5) if shape == CURSOR_BEAM else sg.dx * mult)
|
||||||
right = left + (width(1.5) if shape == CURSOR_BEAM else sg.dx * mult)
|
bottom = top - sg.dy
|
||||||
bottom = top - sg.dy
|
if shape == CURSOR_UNDERLINE:
|
||||||
if shape == CURSOR_UNDERLINE:
|
top = bottom + width(vert=False)
|
||||||
top = bottom + width(vert=False)
|
glUniform4f(ul('color'), col[0], col[1], col[2], alpha)
|
||||||
glUniform4f(ul('color'), col[0], col[1], col[2], alpha)
|
glUniform2f(ul('xpos'), left, right)
|
||||||
glUniform2f(ul('xpos'), left, right)
|
glUniform2f(ul('ypos'), top, bottom)
|
||||||
glUniform2f(ul('ypos'), top, bottom)
|
glDrawArrays(GL_TRIANGLE_FAN, 0, 4)
|
||||||
glDrawArrays(GL_TRIANGLE_FAN, 0, 4)
|
glDisable(GL_BLEND)
|
||||||
glDisable(GL_BLEND)
|
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
import pwd
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
appname = 'kitty'
|
appname = 'kitty'
|
||||||
version = (0, 1, 0)
|
version = (0, 1, 0)
|
||||||
str_version = '.'.join(map(str, version))
|
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():
|
def _get_config_dir():
|
||||||
@@ -22,8 +26,38 @@ def _get_config_dir():
|
|||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
pass
|
pass
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
config_dir = _get_config_dir()
|
config_dir = _get_config_dir()
|
||||||
del _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')
|
terminfo_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'terminfo')
|
||||||
main_thread = threading.current_thread()
|
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 tempfile
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import pwd
|
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
|
||||||
from .child import Child
|
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
from .constants import appname, str_version, config_dir
|
from .constants import appname, str_version, config_dir, viewport_size
|
||||||
from .boss import Boss
|
from .tabs import TabManager
|
||||||
from .shaders import GL_VERSION
|
from .shaders import GL_VERSION
|
||||||
from .fast_data_types import glewInit, enable_automatic_opengl_error_checking
|
from .fast_data_types import glewInit, enable_automatic_opengl_error_checking
|
||||||
import glfw, glfw_constants
|
import glfw
|
||||||
|
import glfw_constants
|
||||||
|
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
@@ -44,11 +43,10 @@ def setup_opengl():
|
|||||||
glfw.glfwWindowHint(glfw_constants.GLFW_SAMPLES, 0)
|
glfw.glfwWindowHint(glfw_constants.GLFW_SAMPLES, 0)
|
||||||
|
|
||||||
|
|
||||||
def run_app(opts, args, child):
|
def run_app(opts, args):
|
||||||
setup_opengl()
|
setup_opengl()
|
||||||
window_width = window_height = 1024
|
|
||||||
window = glfw.glfwCreateWindow(
|
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:
|
if not window:
|
||||||
raise SystemExit("glfwCreateWindow failed")
|
raise SystemExit("glfwCreateWindow failed")
|
||||||
glfw.glfwSetWindowTitle(window, appname.encode('utf-8'))
|
glfw.glfwSetWindowTitle(window, appname.encode('utf-8'))
|
||||||
@@ -56,19 +54,15 @@ def run_app(opts, args, child):
|
|||||||
glfw.glfwMakeContextCurrent(window)
|
glfw.glfwMakeContextCurrent(window)
|
||||||
glewInit()
|
glewInit()
|
||||||
glfw.glfwSwapInterval(1)
|
glfw.glfwSwapInterval(1)
|
||||||
boss = Boss(window, window_width, window_height, opts, args, child)
|
tabs = TabManager(window, opts, args)
|
||||||
glfw.glfwSetFramebufferSizeCallback(window, boss.on_window_resize)
|
tabs.start()
|
||||||
boss.start()
|
|
||||||
try:
|
try:
|
||||||
while not glfw.glfwWindowShouldClose(window):
|
while not glfw.glfwWindowShouldClose(window):
|
||||||
boss.render()
|
tabs.render()
|
||||||
glfw.glfwSwapBuffers(window)
|
glfw.glfwSwapBuffers(window)
|
||||||
glfw.glfwWaitEvents()
|
glfw.glfwWaitEvents()
|
||||||
finally:
|
finally:
|
||||||
if boss.is_alive():
|
tabs.destroy()
|
||||||
boss.close()
|
|
||||||
boss.join()
|
|
||||||
boss.destroy()
|
|
||||||
finally:
|
finally:
|
||||||
glfw.glfwDestroyWindow(window)
|
glfw.glfwDestroyWindow(window)
|
||||||
|
|
||||||
@@ -88,8 +82,6 @@ def main():
|
|||||||
exec(args.cmd)
|
exec(args.cmd)
|
||||||
return
|
return
|
||||||
opts = load_config(args.config)
|
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)
|
glfw.glfwSetErrorCallback(on_glfw_error)
|
||||||
enable_automatic_opengl_error_checking(False)
|
enable_automatic_opengl_error_checking(False)
|
||||||
if not glfw.glfwInit():
|
if not glfw.glfwInit():
|
||||||
@@ -102,7 +94,7 @@ def main():
|
|||||||
import pstats
|
import pstats
|
||||||
pr = cProfile.Profile()
|
pr = cProfile.Profile()
|
||||||
pr.enable()
|
pr.enable()
|
||||||
run_app(opts, args, child)
|
run_app(opts, args)
|
||||||
pr.disable()
|
pr.disable()
|
||||||
pr.create_stats()
|
pr.create_stats()
|
||||||
s = pstats.Stats(pr)
|
s = pstats.Stats(pr)
|
||||||
@@ -112,7 +104,7 @@ def main():
|
|||||||
s.sort_stats('time', 'name')
|
s.sort_stats('time', 'name')
|
||||||
s.print_stats(30)
|
s.print_stats(30)
|
||||||
else:
|
else:
|
||||||
run_app(opts, args, child)
|
run_app(opts, args)
|
||||||
finally:
|
finally:
|
||||||
glfw.glfwTerminate()
|
glfw.glfwTerminate()
|
||||||
os.closerange(3, 100)
|
os.closerange(3, 100)
|
||||||
|
|||||||
@@ -911,7 +911,7 @@ erase_in_display(Screen *self, PyObject *args) {
|
|||||||
#define MODE_GETTER(name, uname) \
|
#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; }
|
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(focus_tracking_enabled, FOCUS_TRACKING)
|
||||||
MODE_GETTER(mouse_button_tracking_enabled, MOUSE_BUTTON_TRACKING)
|
MODE_GETTER(mouse_button_tracking_enabled, MOUSE_BUTTON_TRACKING)
|
||||||
MODE_GETTER(mouse_motion_tracking_enabled, MOUSE_MOTION_TRACKING)
|
MODE_GETTER(mouse_motion_tracking_enabled, MOUSE_MOTION_TRACKING)
|
||||||
@@ -1068,7 +1068,7 @@ static PyMethodDef methods[] = {
|
|||||||
MND(mark_as_dirty, METH_NOARGS)
|
MND(mark_as_dirty, METH_NOARGS)
|
||||||
MND(resize, METH_VARARGS)
|
MND(resize, METH_VARARGS)
|
||||||
MND(set_scroll_cell_data, 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(focus_tracking_enabled, METH_NOARGS)
|
||||||
MND(mouse_button_tracking_enabled, METH_NOARGS)
|
MND(mouse_button_tracking_enabled, METH_NOARGS)
|
||||||
MND(mouse_motion_tracking_enabled, METH_NOARGS)
|
MND(mouse_motion_tracking_enabled, METH_NOARGS)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
from ctypes import addressof, sizeof
|
from ctypes import addressof, sizeof
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
from .fonts import render_cell
|
from .fonts import render_cell
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
@@ -48,6 +49,7 @@ class Sprites:
|
|||||||
self.last_ynum = -1
|
self.last_ynum = -1
|
||||||
self.texture_unit = GL_TEXTURE0
|
self.texture_unit = GL_TEXTURE0
|
||||||
self.backend = SpriteMap(glGetIntegerv(GL_MAX_TEXTURE_SIZE), glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS))
|
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):
|
def do_layout(self, cell_width=1, cell_height=1):
|
||||||
self.cell_width, self.cell_height = cell_width, cell_height
|
self.cell_width, self.cell_height = cell_width, cell_height
|
||||||
@@ -84,7 +86,8 @@ class Sprites:
|
|||||||
return first
|
return first
|
||||||
|
|
||||||
def render_dirty_cells(self):
|
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):
|
def send_to_gpu(self, x, y, z, buf):
|
||||||
if self.backend.z >= self.last_num_of_layers:
|
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>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import ctypes
|
import ctypes
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
@@ -249,3 +251,12 @@ def parse_color_set(raw):
|
|||||||
yield c, r << 16 | g << 8 | b
|
yield c, r << 16 | g << 8 | b
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
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)
|
||||||
Reference in New Issue
Block a user