299 lines
10 KiB
Python
299 lines
10 KiB
Python
#!/usr/bin/env python
|
|
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
import sys
|
|
import weakref
|
|
from collections import deque
|
|
from enum import Enum
|
|
from itertools import count
|
|
|
|
from .config import build_ansi_color_table
|
|
from .constants import (
|
|
ScreenGeometry, WindowGeometry, appname, cell_size, get_boss,
|
|
viewport_size, wakeup
|
|
)
|
|
from .fast_data_types import (
|
|
BRACKETED_PASTE_END, BRACKETED_PASTE_START, CELL_BACKGROUND_PROGRAM,
|
|
CELL_FOREGROUND_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM,
|
|
CURSOR_PROGRAM, GRAPHICS_PROGRAM, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE,
|
|
Screen, compile_program, create_cell_vao, create_graphics_vao,
|
|
glfw_post_empty_event, init_cell_program, init_cursor_program, remove_vao,
|
|
set_window_render_data, update_window_title, update_window_visibility
|
|
)
|
|
from .rgb import to_color
|
|
from .terminfo import get_capabilities
|
|
from .utils import color_as_int, load_shaders, parse_color_set, sanitize_title, open_url, open_cmd
|
|
|
|
|
|
class DynamicColor(Enum):
|
|
default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6)
|
|
|
|
|
|
DYNAMIC_COLOR_CODES = {
|
|
10: DynamicColor.default_fg,
|
|
11: DynamicColor.default_bg,
|
|
12: DynamicColor.cursor_color,
|
|
17: DynamicColor.highlight_bg,
|
|
19: DynamicColor.highlight_fg,
|
|
}
|
|
DYNAMIC_COLOR_CODES.update({k+100: v for k, v in DYNAMIC_COLOR_CODES.items()})
|
|
window_counter = count()
|
|
next(window_counter)
|
|
|
|
|
|
def calculate_gl_geometry(window_geometry, viewport_width, viewport_height, cell_width, cell_height):
|
|
dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
|
|
xmargin = window_geometry.left / viewport_width
|
|
ymargin = window_geometry.top / viewport_height
|
|
xstart = -1 + 2 * xmargin
|
|
ystart = 1 - 2 * ymargin
|
|
return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)
|
|
|
|
|
|
def load_shader_programs():
|
|
v, f = load_shaders('cell')
|
|
compile_program(GRAPHICS_PROGRAM, *load_shaders('graphics'))
|
|
for which, p in {
|
|
'ALL': CELL_PROGRAM, 'BACKGROUND': CELL_BACKGROUND_PROGRAM, 'SPECIAL': CELL_SPECIAL_PROGRAM,
|
|
'FOREGROUND': CELL_FOREGROUND_PROGRAM
|
|
}.items():
|
|
vv, ff = v.replace('WHICH_PROGRAM', which), f.replace('WHICH_PROGRAM', which)
|
|
compile_program(p, vv, ff)
|
|
init_cell_program()
|
|
compile_program(CURSOR_PROGRAM, *load_shaders('cursor'))
|
|
init_cursor_program()
|
|
|
|
|
|
def setup_colors(screen, opts):
|
|
screen.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
|
|
screen.color_profile.set_configured_colors(*map(color_as_int, (
|
|
opts.foreground, opts.background, opts.cursor, opts.selection_foreground, opts.selection_background)))
|
|
|
|
|
|
class Window:
|
|
|
|
def __init__(self, tab, child, opts, args):
|
|
self.id = next(window_counter)
|
|
self.vao_id = create_cell_vao()
|
|
self.gvao_id = create_graphics_vao()
|
|
self.tab_id = tab.id
|
|
self.tabref = weakref.ref(tab)
|
|
self.override_title = None
|
|
self.destroyed = False
|
|
self.click_queue = deque(maxlen=3)
|
|
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.screen = Screen(self, 24, 80, opts.scrollback_lines, self.id)
|
|
setup_colors(self.screen, opts)
|
|
|
|
def __repr__(self):
|
|
return 'Window(title={}, id={})'.format(self.title, self.id)
|
|
|
|
def set_visible_in_layout(self, window_idx, val):
|
|
val = bool(val)
|
|
if val is not self.is_visible_in_layout:
|
|
self.is_visible_in_layout = val
|
|
update_window_visibility(self.tab_id, window_idx, val)
|
|
if val:
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
self.screen.mark_as_dirty()
|
|
wakeup()
|
|
|
|
def update_position(self, window_geometry):
|
|
self.screen_geometry = sg = calculate_gl_geometry(window_geometry, viewport_size.width, viewport_size.height, cell_size.width, cell_size.height)
|
|
return sg
|
|
|
|
def set_geometry(self, window_idx, new_geometry):
|
|
if self.destroyed:
|
|
return
|
|
if self.needs_layout or new_geometry.xnum != self.screen.columns or new_geometry.ynum != self.screen.lines:
|
|
boss = get_boss()
|
|
self.screen.resize(new_geometry.ynum, new_geometry.xnum)
|
|
current_pty_size = (
|
|
self.screen.lines, self.screen.columns,
|
|
max(0, new_geometry.right - new_geometry.left), max(0, new_geometry.bottom - new_geometry.top))
|
|
sg = self.update_position(new_geometry)
|
|
self.needs_layout = False
|
|
boss.child_monitor.resize_pty(self.id, *current_pty_size)
|
|
else:
|
|
sg = self.update_position(new_geometry)
|
|
self.geometry = g = new_geometry
|
|
set_window_render_data(self.tab_id, window_idx, self.vao_id, self.gvao_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen, *g[:4])
|
|
|
|
def contains(self, x, y):
|
|
g = self.geometry
|
|
return g.left <= x <= g.right and g.top <= y <= g.bottom
|
|
|
|
def close(self):
|
|
get_boss().close_window(self)
|
|
|
|
def on_child_death(self):
|
|
if self.destroyed:
|
|
return
|
|
self.destroyed = True
|
|
# Remove cycles so that screen is de-allocated immediately
|
|
boss = get_boss()
|
|
self.screen.reset_callbacks()
|
|
boss.gui_close_window(self)
|
|
self.screen = None
|
|
|
|
def write_to_child(self, data):
|
|
if data:
|
|
if get_boss().child_monitor.needs_write(self.id, data) is not True:
|
|
print('Failed to write to child %d as it does not exist' % self.id, file=sys.stderr)
|
|
|
|
# screen callbacks {{{
|
|
def use_utf8(self, on):
|
|
get_boss().child_monitor.set_iutf8(self.window_id, on)
|
|
|
|
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):
|
|
if self.override_title is None:
|
|
self.title = sanitize_title(new_title or appname)
|
|
update_window_title(self.tab_id, self.id, self.title)
|
|
t = self.tabref()
|
|
if t is not None:
|
|
t.title_changed(self)
|
|
glfw_post_empty_event()
|
|
|
|
def icon_changed(self, new_icon):
|
|
pass # TODO: Implement this
|
|
|
|
def change_colors(self, changes):
|
|
dirtied = False
|
|
|
|
def item(raw):
|
|
if raw is None:
|
|
return 0
|
|
val = to_color(raw)
|
|
return None if val is None else (color_as_int(val) << 8) | 2
|
|
|
|
for which, val in changes.items():
|
|
val = item(val)
|
|
if val is None:
|
|
continue
|
|
dirtied = True
|
|
setattr(self.screen.color_profile, which.name, val)
|
|
if dirtied:
|
|
self.screen.mark_as_dirty()
|
|
|
|
def set_dynamic_color(self, code, value):
|
|
if isinstance(value, bytes):
|
|
value = value.decode('utf-8')
|
|
color_changes = {}
|
|
for val in value.split(';'):
|
|
w = DYNAMIC_COLOR_CODES.get(code)
|
|
if w is not None:
|
|
if code >= 110:
|
|
val = None
|
|
color_changes[w] = val
|
|
code += 1
|
|
self.change_colors(color_changes)
|
|
glfw_post_empty_event()
|
|
|
|
def set_color_table_color(self, code, value):
|
|
cp = self.screen.color_profile
|
|
if code == 4:
|
|
for c, val in parse_color_set(value):
|
|
cp.set_color(c, val)
|
|
self.refresh()
|
|
elif code == 104:
|
|
if not value.strip():
|
|
cp.reset_color_table()
|
|
else:
|
|
for c in value.split(';'):
|
|
try:
|
|
c = int(c)
|
|
except Exception:
|
|
continue
|
|
if 0 <= c <= 255:
|
|
cp.reset_color(c)
|
|
self.refresh()
|
|
|
|
def request_capabilities(self, q):
|
|
self.write_to_child(get_capabilities(q))
|
|
|
|
# }}}
|
|
|
|
def text_for_selection(self):
|
|
return ''.join(self.screen.text_for_selection())
|
|
|
|
def destroy(self):
|
|
if self.vao_id is not None:
|
|
remove_vao(self.vao_id)
|
|
remove_vao(self.gvao_id)
|
|
self.vao_id = self.gvao_id = None
|
|
|
|
def buffer_as_ansi(self):
|
|
data = []
|
|
self.screen.historybuf.as_ansi(data.append)
|
|
self.screen.linebuf.as_ansi(data.append)
|
|
return ''.join(data)
|
|
|
|
def buffer_as_text(self):
|
|
return str(self.screen.historybuf) + '\n' + str(self.screen.linebuf)
|
|
|
|
# actions {{{
|
|
|
|
def show_scrollback(self):
|
|
get_boss().display_scrollback(self.buffer_as_ansi().encode('utf-8'))
|
|
|
|
def paste(self, text):
|
|
if text and not self.destroyed:
|
|
if isinstance(text, str):
|
|
text = text.encode('utf-8')
|
|
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 copy_to_clipboard(self):
|
|
text = self.text_for_selection()
|
|
if text:
|
|
get_boss().glfw_window.set_clipboard_string(text)
|
|
|
|
def pass_selection_to_program(self, *args):
|
|
text = self.text_for_selection()
|
|
if text:
|
|
if args:
|
|
open_cmd(args, text)
|
|
else:
|
|
open_url(text)
|
|
|
|
def scroll_line_up(self):
|
|
if self.screen.is_main_linebuf():
|
|
self.screen.scroll(SCROLL_LINE, True)
|
|
|
|
def scroll_line_down(self):
|
|
if self.screen.is_main_linebuf():
|
|
self.screen.scroll(SCROLL_LINE, False)
|
|
|
|
def scroll_page_up(self):
|
|
if self.screen.is_main_linebuf():
|
|
self.screen.scroll(SCROLL_PAGE, True)
|
|
|
|
def scroll_page_down(self):
|
|
if self.screen.is_main_linebuf():
|
|
self.screen.scroll(SCROLL_PAGE, False)
|
|
|
|
def scroll_home(self):
|
|
if self.screen.is_main_linebuf():
|
|
self.screen.scroll(SCROLL_FULL, True)
|
|
|
|
def scroll_end(self):
|
|
if self.screen.is_main_linebuf():
|
|
self.screen.scroll(SCROLL_FULL, False)
|
|
# }}}
|