diff --git a/kitty/boss.py b/kitty/boss.py index a19de6d49..6efa02cb1 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -5,7 +5,6 @@ from gettext import gettext as _ from weakref import WeakValueDictionary -from .char_grid import load_shader_programs from .config import MINIMUM_FONT_SIZE from .constants import ( MODIFIER_KEYS, cell_size, is_key_pressed, mouse_button_pressed, @@ -13,8 +12,8 @@ from .constants import ( ) from .fast_data_types import ( GLFW_MOUSE_BUTTON_1, GLFW_PRESS, GLFW_REPEAT, ChildMonitor, - destroy_global_data, destroy_sprite_map, - glfw_post_empty_event, layout_sprite_map + destroy_global_data, destroy_sprite_map, glfw_post_empty_event, + layout_sprite_map ) from .fonts.render import render_cell_wrapper, set_font_family from .keys import ( @@ -23,6 +22,7 @@ from .keys import ( from .session import create_session from .tabs import SpecialWindow, TabManager from .utils import safe_print +from .window import load_shader_programs class DumpCommands: # {{{ diff --git a/kitty/char_grid.py b/kitty/char_grid.py deleted file mode 100644 index d197a0d11..000000000 --- a/kitty/char_grid.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 -# License: GPL v3 Copyright: 2016, Kovid Goyal - -import re -from enum import Enum - -from .config import build_ansi_color_table -from .constants import ScreenGeometry, cell_size, viewport_size -from .fast_data_types import ( - CELL_PROGRAM, CURSOR_PROGRAM, compile_program, init_cell_program, - init_cursor_program -) -from .rgb import to_color -from .utils import color_as_int, load_shaders, open_url, set_primary_selection - - -class DynamicColor(Enum): - default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6) - - -def load_shader_programs(): - compile_program(CELL_PROGRAM, *load_shaders('cell')) - init_cell_program() - compile_program(CURSOR_PROGRAM, *load_shaders('cursor')) - init_cursor_program() - - -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) - - -class CharGrid: - - url_pat = re.compile('(?:http|https|file|ftp)://\S+', re.IGNORECASE) - - def __init__(self, screen, opts): - self.screen_reversed = False - self.screen = screen - self.opts = opts - self.screen.color_profile.update_ansi_color_table(build_ansi_color_table(opts)) - self.screen.color_profile.set_configured_colors(*map(color_as_int, ( - opts.foreground, opts.background, opts.cursor, opts.selection_foreground, opts.selection_background))) - self.opts = opts - self.opts = opts - - 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 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 cell_for_pos(self, x, y): - x, y = int(x // cell_size.width), int(y // cell_size.height) - if 0 <= x < self.screen.columns and 0 <= y < self.screen.lines: - return x, y - return None, None - - def update_drag(self, is_press, mx, my): - x, y = self.cell_for_pos(mx, my) - if x is None: - x = 0 if mx <= cell_size.width else self.screen.columns - 1 - y = 0 if my <= cell_size.height else self.screen.lines - 1 - ps = None - if is_press: - self.screen.start_selection(x, y) - elif self.screen.is_selection_in_progress(): - ended = is_press is False - self.screen.update_selection(x, y, ended) - if ended: - ps = self.text_for_selection() - if ps and ps.strip(): - set_primary_selection(ps) - - def has_url_at(self, x, y): - x, y = self.cell_for_pos(x, y) - if x is not None: - l = self.screen.visual_line(y) - if l is not None: - text = str(l) - for m in self.url_pat.finditer(text): - if m.start() <= x < m.end(): - return True - return False - - def click_url(self, x, y): - x, y = self.cell_for_pos(x, y) - if x is not None: - l = self.screen.visual_line(y) - if l is not None: - text = str(l) - for m in self.url_pat.finditer(text): - if m.start() <= x < m.end(): - url = ''.join(l[i] for i in range(*m.span())).rstrip('.') - # Remove trailing "] and similar - url = re.sub(r'''["'][)}\]]$''', '', url) - # Remove closing trailing character if it is matched by it's - # corresponding opening character before the url - if m.start() > 0: - before = l[m.start() - 1] - closing = {'(': ')', '[': ']', '{': '}', '<': '>', '"': '"', "'": "'", '`': '`', '|': '|', ':': ':'}.get(before) - if closing is not None and url.endswith(closing): - url = url[:-1] - if url: - open_url(url, self.opts.open_url_with) - - def multi_click(self, count, x, y): - x, y = self.cell_for_pos(x, y) - if x is not None: - line = self.screen.visual_line(y) - if line is not None and count in (2, 3): - if count == 2: - start_x, xlimit = self.screen.selection_range_for_word(x, y, self.opts.select_by_word_characters) - end_x = max(start_x, xlimit - 1) - elif count == 3: - start_x, xlimit = self.screen.selection_range_for_line(y) - end_x = max(start_x, xlimit - 1) - self.screen.start_selection(start_x, y) - self.screen.update_selection(end_x, y, True) - ps = self.text_for_selection() - if ps: - set_primary_selection(ps) - - def get_scrollback_as_ansi(self): - ans = [] - self.screen.historybuf.as_ansi(ans.append) - self.screen.linebuf.as_ansi(ans.append) - return ''.join(ans).encode('utf-8') - - def text_for_selection(self): - return ''.join(self.screen.text_for_selection()) diff --git a/kitty/tabs.py b/kitty/tabs.py index 315a97e7c..7e267cb6c 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -7,7 +7,6 @@ from functools import partial from itertools import count from .borders import Borders -from .char_grid import calculate_gl_geometry from .child import Child from .config import build_ansi_color_table from .constants import ( @@ -20,7 +19,7 @@ from .fast_data_types import ( ) from .layout import Rect, all_layouts from .utils import color_as_int -from .window import Window +from .window import Window, calculate_gl_geometry TabbarData = namedtuple('TabbarData', 'title is_active is_last') borders = None diff --git a/kitty/window.py b/kitty/window.py index 53fd0b315..da5a97ea1 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -2,30 +2,42 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal +import re import sys import weakref from collections import deque +from enum import Enum from itertools import count from time import monotonic -from .char_grid import CharGrid, DynamicColor +from .config import build_ansi_color_table from .constants import ( - WindowGeometry, appname, cell_size, get_boss, is_key_pressed, - mouse_button_pressed, wakeup + ScreenGeometry, WindowGeometry, appname, cell_size, get_boss, + is_key_pressed, mouse_button_pressed, viewport_size, wakeup ) from .fast_data_types import ( - ANY_MODE, BRACKETED_PASTE_END, BRACKETED_PASTE_START, GLFW_KEY_DOWN, - GLFW_KEY_LEFT_SHIFT, GLFW_KEY_RIGHT_SHIFT, GLFW_KEY_UP, GLFW_MOD_SHIFT, - GLFW_MOUSE_BUTTON_1, GLFW_MOUSE_BUTTON_4, GLFW_MOUSE_BUTTON_5, - GLFW_MOUSE_BUTTON_MIDDLE, GLFW_PRESS, GLFW_RELEASE, MOTION_MODE, - SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, Screen, create_cell_vao, - glfw_post_empty_event, remove_vao, set_window_render_data, + ANY_MODE, BRACKETED_PASTE_END, BRACKETED_PASTE_START, CELL_PROGRAM, + CURSOR_PROGRAM, GLFW_KEY_DOWN, GLFW_KEY_LEFT_SHIFT, GLFW_KEY_RIGHT_SHIFT, + GLFW_KEY_UP, GLFW_MOD_SHIFT, GLFW_MOUSE_BUTTON_1, GLFW_MOUSE_BUTTON_4, + GLFW_MOUSE_BUTTON_5, GLFW_MOUSE_BUTTON_MIDDLE, GLFW_PRESS, GLFW_RELEASE, + MOTION_MODE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, Screen, + compile_program, create_cell_vao, glfw_post_empty_event, init_cell_program, + init_cursor_program, remove_vao, set_window_render_data, update_window_title, update_window_visibility ) from .keys import get_key_map from .mouse import DRAG, MOVE, PRESS, RELEASE, encode_mouse_event +from .rgb import to_color from .terminfo import get_capabilities -from .utils import get_primary_selection, parse_color_set, sanitize_title +from .utils import ( + color_as_int, get_primary_selection, load_shaders, open_url, + parse_color_set, sanitize_title, set_primary_selection +) + + +class DynamicColor(Enum): + default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6) + DYNAMIC_COLOR_CODES = { 10: DynamicColor.default_fg, @@ -39,6 +51,22 @@ 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(): + compile_program(CELL_PROGRAM, *load_shaders('cell')) + init_cell_program() + compile_program(CURSOR_PROGRAM, *load_shaders('cursor')) + init_cursor_program() + + class Window: def __init__(self, tab, child, opts, args): @@ -56,7 +84,9 @@ class Window: self.is_visible_in_layout = True self.child, self.opts = child, opts self.screen = Screen(self, 24, 80, opts.scrollback_lines) - self.char_grid = CharGrid(self.screen, opts) + self.screen.color_profile.update_ansi_color_table(build_ansi_color_table(opts)) + self.screen.color_profile.set_configured_colors(*map(color_as_int, ( + opts.foreground, opts.background, opts.cursor, opts.selection_foreground, opts.selection_background))) def __repr__(self): return 'Window(title={}, id={})'.format(self.title, self.id) @@ -73,6 +103,10 @@ class Window: 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 @@ -82,11 +116,11 @@ class Window: 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.char_grid.update_position(new_geometry) + sg = self.update_position(new_geometry) self.needs_layout = False boss.child_monitor.resize_pty(self.id, *current_pty_size) else: - sg = self.char_grid.update_position(new_geometry) + sg = self.update_position(new_geometry) set_window_render_data(self.tab_id, window_idx, self.vao_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen) self.geometry = new_geometry @@ -105,8 +139,7 @@ class Window: boss = get_boss() self.screen.reset_callbacks() boss.gui_close_window(self) - self.screen = self.char_grid.screen = None - self.char_grid = None + self.screen = None def write_to_child(self, data): if data: @@ -115,6 +148,7 @@ class Window: else: print('Failed to write to child %d as it does not exist' % self.id, file=sys.stderr) + # screen callbacks {{{ def bell(self): boss = get_boss() boss.request_attention() @@ -143,6 +177,24 @@ class Window: 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') @@ -154,11 +206,11 @@ class Window: val = None color_changes[w] = val code += 1 - self.char_grid.change_colors(color_changes) + self.change_colors(color_changes) glfw_post_empty_event() def set_color_table_color(self, code, value): - cp = self.char_grid.screen.color_profile + cp = self.screen.color_profile if code == 4: for c, val in parse_color_set(value): cp.set_color(c, val) @@ -181,14 +233,93 @@ class Window: def request_capabilities(self, q): self.write_to_child(get_capabilities(q)) + def buf_toggled(self, is_main_linebuf): + self.screen.scroll(SCROLL_FULL, False) + # }}} + + # mouse handling {{{ + def multi_click(self, count, x, y): + x, y = self.cell_for_pos(x, y) + if x is not None: + line = self.screen.visual_line(y) + if line is not None and count in (2, 3): + if count == 2: + start_x, xlimit = self.screen.selection_range_for_word(x, y, self.opts.select_by_word_characters) + end_x = max(start_x, xlimit - 1) + elif count == 3: + start_x, xlimit = self.screen.selection_range_for_line(y) + end_x = max(start_x, xlimit - 1) + self.screen.start_selection(start_x, y) + self.screen.update_selection(end_x, y, True) + ps = self.text_for_selection() + if ps: + set_primary_selection(ps) + + def cell_for_pos(self, x, y): + x, y = int(x // cell_size.width), int(y // cell_size.height) + if 0 <= x < self.screen.columns and 0 <= y < self.screen.lines: + return x, y + return None, None + def dispatch_multi_click(self, x, y): if len(self.click_queue) > 2 and self.click_queue[-1] - self.click_queue[-3] <= 2 * self.opts.click_interval: - self.char_grid.multi_click(3, x, y) + self.multi_click(3, x, y) glfw_post_empty_event() elif len(self.click_queue) > 1 and self.click_queue[-1] - self.click_queue[-2] <= self.opts.click_interval: - self.char_grid.multi_click(2, x, y) + self.multi_click(2, x, y) glfw_post_empty_event() + def update_drag(self, is_press, mx, my): + x, y = self.cell_for_pos(mx, my) + if x is None: + x = 0 if mx <= cell_size.width else self.screen.columns - 1 + y = 0 if my <= cell_size.height else self.screen.lines - 1 + ps = None + if is_press: + self.screen.start_selection(x, y) + elif self.screen.is_selection_in_progress(): + ended = is_press is False + self.screen.update_selection(x, y, ended) + if ended: + ps = self.text_for_selection() + if ps and ps.strip(): + set_primary_selection(ps) + + def has_url_at(self, x, y): + x, y = self.cell_for_pos(x, y) + if x is not None: + l = self.screen.visual_line(y) + if l is not None: + text = str(l) + for m in self.url_pat.finditer(text): + if m.start() <= x < m.end(): + return True + return False + + def click_url(self, x, y): + x, y = self.cell_for_pos(x, y) + if x is not None: + l = self.screen.visual_line(y) + if l is not None: + text = str(l) + for m in self.url_pat.finditer(text): + if m.start() <= x < m.end(): + url = ''.join(l[i] for i in range(*m.span())).rstrip('.') + # Remove trailing "] and similar + url = re.sub(r'''["'][)}\]]$''', '', url) + # Remove closing trailing character if it is matched by it's + # corresponding opening character before the url + if m.start() > 0: + before = l[m.start() - 1] + closing = {'(': ')', '[': ']', '{': '}', '<': '>', '"': '"', "'": "'", '`': '`', '|': '|', ':': ':'}.get(before) + if closing is not None and url.endswith(closing): + url = url[:-1] + if url: + open_url(url, self.opts.open_url_with) + + def text_for_selection(self): + return ''.join(self.screen.text_for_selection()) + def on_mouse_button(self, button, action, mods): mode = self.screen.mouse_tracking_mode() handle_event = mods == GLFW_MOD_SHIFT or mode == 0 or button == GLFW_MOUSE_BUTTON_MIDDLE or ( @@ -196,17 +327,17 @@ class Window: x, y = self.last_mouse_cursor_pos if handle_event: if button == GLFW_MOUSE_BUTTON_1: - self.char_grid.update_drag(action == GLFW_PRESS, x, y) + self.update_drag(action == GLFW_PRESS, x, y) if action == GLFW_RELEASE: - if mods == self.char_grid.opts.open_url_modifiers: - self.char_grid.click_url(x, y) + if mods == self.opts.open_url_modifiers: + self.click_url(x, y) self.click_queue.append(monotonic()) self.dispatch_multi_click(x, y) elif button == GLFW_MOUSE_BUTTON_MIDDLE: if action == GLFW_RELEASE: self.paste_from_selection() else: - x, y = self.char_grid.cell_for_pos(x, y) + x, y = self.cell_for_pos(x, y) if x is not None: ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(), button, PRESS if action == GLFW_PRESS else RELEASE, mods, x, y) @@ -225,9 +356,9 @@ class Window: is_key_pressed[GLFW_KEY_LEFT_SHIFT] or is_key_pressed[GLFW_KEY_RIGHT_SHIFT]) x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) self.last_mouse_cursor_pos = x, y - get_boss().change_mouse_cursor(self.char_grid.has_url_at(x, y)) + get_boss().change_mouse_cursor(self.has_url_at(x, y)) if send_event: - x, y = self.char_grid.cell_for_pos(x, y) + x, y = self.cell_for_pos(x, y) if x is not None: ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(), button, action, 0, x, y) @@ -235,7 +366,7 @@ class Window: self.write_to_child(ev) else: if self.screen.is_selection_in_progress(): - self.char_grid.update_drag(None, x, y) + self.update_drag(None, x, y) margin = cell_size.height // 2 if y <= margin or y >= self.geometry.bottom - margin: get_boss().ui_timers.add(0.02, self.drag_scroll) @@ -245,7 +376,7 @@ class Window: margin = cell_size.height // 2 if y <= margin or y >= self.geometry.bottom - margin: self.scroll_line_up() if y < 50 else self.scroll_line_down() - self.char_grid.update_drag(None, x, y) + self.update_drag(None, x, y) return 0.02 # causes the timer to be re-added def on_mouse_scroll(self, x, y): @@ -261,7 +392,7 @@ class Window: send_event = mode > 0 if send_event: x, y = self.last_mouse_cursor_pos - x, y = self.char_grid.cell_for_pos(x, y) + x, y = self.cell_for_pos(x, y) if x is not None: ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(), GLFW_MOUSE_BUTTON_4 if upwards else GLFW_MOUSE_BUTTON_5, PRESS, 0, x, y) @@ -270,9 +401,7 @@ class Window: else: k = get_key_map(self.screen)[GLFW_KEY_UP if upwards else GLFW_KEY_DOWN] self.write_to_child(k * abs(s)) - - def buf_toggled(self, is_main_linebuf): - self.screen.scroll(SCROLL_FULL, False) + # }}} def destroy(self): if self.vao_id is not None: @@ -282,7 +411,10 @@ class Window: # actions {{{ def show_scrollback(self): - data = self.char_grid.get_scrollback_as_ansi() + data = [] + self.screen.historybuf.as_ansi(data.append) + self.screen.linebuf.as_ansi(data.append) + data = ''.join(data).encode('utf-8') get_boss().display_scrollback(data) def paste(self, text): @@ -301,7 +433,7 @@ class Window: self.paste(text) def copy_to_clipboard(self): - text = self.char_grid.text_for_selection() + text = self.text_for_selection() if text: get_boss().glfw_window.set_clipboard_string(text)