#!/usr/bin/env python # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal import re import sys from enum import Enum from collections import namedtuple from ctypes import addressof, memmove, sizeof from threading import Lock from .config import build_ansi_color_table, defaults from .constants import ( GLuint, ScreenGeometry, cell_size, get_boss, viewport_size ) from .fast_data_types import ( CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, DATA_CELL_SIZE, GL_BLEND, GL_LINE_LOOP, GL_TRIANGLE_FAN, ColorProfile, glDisable, glDrawArrays, glDrawArraysInstanced, glEnable, glUniform1i, glUniform2f, glUniform2ui, glUniform4f ) from .rgb import to_color from .utils import ( color_as_int, color_from_int, get_logical_dpi, open_url, safe_print, set_primary_selection ) Cursor = namedtuple('Cursor', 'x y shape blink') class DynamicColor(Enum): default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6) if DATA_CELL_SIZE % 3: raise ValueError('Incorrect data cell size, must be a multiple of 3') # cell shader {{{ cell_shader = ( '''\ uniform uvec2 dimensions; // xnum, ynum uniform vec4 steps; // xstart, ystart, dx, dy uniform vec2 sprite_layout; // dx, dy uniform usamplerBuffer sprite_map; // gl_InstanceID -> x, y, z uniform uvec2 color_indices; // which color to use as fg and which as bg out vec3 sprite_pos; out vec3 underline_pos; out vec3 strike_pos; out vec3 foreground; out vec3 background; out vec3 decoration_fg; const uvec2 pos_map[] = uvec2[4]( uvec2(1, 0), // right, top uvec2(1, 1), // right, bottom uvec2(0, 1), // left, bottom uvec2(0, 0) // left, top ); const uint BYTE_MASK = uint(255); const uint ZERO = uint(0); const uint SMASK = uint(3); vec3 to_color(uint c) { uint r, g, b; r = (c >> 16) & BYTE_MASK; g = (c >> 8) & BYTE_MASK; b = c & BYTE_MASK; return vec3(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0); } vec3 to_sprite_pos(uvec2 pos, uint x, uint y, uint z) { vec2 s_xpos = vec2(x, float(x) + 1.0) * sprite_layout[0]; vec2 s_ypos = vec2(y, float(y) + 1.0) * sprite_layout[1]; return vec3(s_xpos[pos[0]], s_ypos[pos[1]], z); } void main() { uint instance_id = uint(gl_InstanceID); uint r = instance_id / dimensions[0]; uint c = instance_id - r * dimensions[0]; float left = steps[0] + c * steps[2]; float top = steps[1] - r * steps[3]; vec2 xpos = vec2(left, left + steps[2]); vec2 ypos = vec2(top, top - steps[3]); uvec2 pos = pos_map[gl_VertexID]; gl_Position = vec4(xpos[pos[0]], ypos[pos[1]], 0, 1); int sprite_id = gl_InstanceID * STRIDE; uvec4 spos = texelFetch(sprite_map, sprite_id); uvec4 colors = texelFetch(sprite_map, sprite_id + 1); sprite_pos = to_sprite_pos(pos, spos[0], spos[1], spos[2]); foreground = to_color(colors[color_indices[0]]); background = to_color(colors[color_indices[1]]); uint decoration = colors[2]; decoration_fg = to_color(decoration); underline_pos = to_sprite_pos(pos, (decoration >> 24) & SMASK, ZERO, ZERO); strike_pos = to_sprite_pos(pos, (decoration >> 26) & SMASK, ZERO, ZERO); } '''.replace('STRIDE', str(DATA_CELL_SIZE // 3)), '''\ uniform sampler2DArray sprites; in vec3 sprite_pos; in vec3 underline_pos; in vec3 strike_pos; in vec3 foreground; in vec3 background; in vec3 decoration_fg; out vec4 final_color; vec3 blend(float alpha, vec3 over, vec3 under) { return over + (1 - alpha) * under; } void main() { float text_alpha = texture(sprites, sprite_pos).r; float underline_alpha = texture(sprites, underline_pos).r; float strike_alpha = texture(sprites, strike_pos).r; vec3 underline = underline_alpha * decoration_fg; vec3 strike = strike_alpha * foreground; vec3 fg = text_alpha * foreground; vec3 decoration = blend(underline_alpha, underline, strike); vec3 combined_fg = blend(text_alpha, fg, decoration); float combined_alpha = max(max(underline_alpha, strike_alpha), text_alpha); final_color = vec4(blend(combined_alpha, combined_fg, background), 1); } ''') # }}} # cursor shader {{{ cursor_shader = ( '''\ uniform vec2 xpos; uniform vec2 ypos; const uvec2 pos_map[] = uvec2[4]( uvec2(1, 0), // right, top uvec2(1, 1), // right, bottom uvec2(0, 1), // left, bottom uvec2(0, 0) // left, top ); void main() { uvec2 pos = pos_map[gl_VertexID]; gl_Position = vec4(xpos[pos[0]], ypos[pos[1]], 0, 1); } ''', '''\ uniform vec4 color; out vec4 final_color; void main() { final_color = color; } ''') # }}} class Selection: # {{{ __slots__ = tuple('in_progress start_x start_y start_scrolled_by end_x end_y end_scrolled_by'.split()) def __init__(self): self.clear() def clear(self): self.in_progress = False self.start_x = self.start_y = self.end_x = self.end_y = 0 self.start_scrolled_by = self.end_scrolled_by = 0 def limits(self, scrolled_by, lines, columns): def coord(x, y, ydelta): y = y - ydelta + scrolled_by if y < 0: x, y = 0, 0 elif y >= lines: x, y = columns - 1, lines - 1 return x, y a = coord(self.start_x, self.start_y, self.start_scrolled_by) b = coord(self.end_x, self.end_y, self.end_scrolled_by) return (a, b) if a[1] < b[1] or (a[1] == b[1] and a[0] <= b[0]) else (b, a) def text(self, linebuf, historybuf): sy = self.start_y - self.start_scrolled_by ey = self.end_y - self.end_scrolled_by if sy == ey and self.start_x == self.end_x: return '' a, b = (sy, self.start_x), (ey, self.end_x) if a > b: a, b = b, a def line(y): if y < 0: return historybuf.line(-1 - y) return linebuf.line(y) lines = [] for y in range(a[0], b[0] + 1): startx, endx = 0, linebuf.xnum - 1 if y == a[0]: startx = max(0, min(a[1], endx)) if y == b[0]: endx = max(0, min(b[1], endx)) l = line(y) is_continued = l.is_continued() if endx - startx >= linebuf.xnum - 1: l = str(l).rstrip(' ') else: l = ''.join(l[x] for x in range(startx, endx + 1)) if not is_continued and startx == 0 and len(lines) > 0: l = '\n' + l lines.append(l) return ''.join(lines) # }}} def calculate_gl_geometry(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 + 2 * xmargin ystart = 1 - 2 * ymargin return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy) def render_cells(buffer_id, sg, cell_program, sprites, invert_colors=False): sprites.bind_sprite_map(buffer_id) ul = cell_program.uniform_location glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum) glUniform2ui(ul('color_indices'), 1 if invert_colors else 0, 0 if invert_colors else 1) 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) class CharGrid: url_pat = re.compile('(?:http|https|file|ftp)://\S+', re.IGNORECASE) def __init__(self, screen, opts): self.buffer_lock = Lock() self.buffer_id = None self.current_selection = Selection() self.last_rendered_selection = self.current_selection.limits(0, screen.lines, screen.columns) self.render_buf_is_dirty = True self.render_data = None self.scrolled_by = 0 self.color_profile = ColorProfile() self.color_profile.update_ansi_color_table(build_ansi_color_table(opts)) self.screen = screen self.opts = opts self.default_bg = color_as_int(opts.background) self.default_fg = color_as_int(opts.foreground) self.dpix, self.dpiy = get_logical_dpi() self.opts = opts self.default_cursor = self.current_cursor = Cursor(0, 0, opts.cursor_shape, opts.cursor_blink_interval > 0) self.opts = opts self.highlight_fg, self.highlight_bg = map(color_as_int, (opts.selection_foreground, opts.selection_background)) self.sprite_map_type = self.main_sprite_map = self.scroll_sprite_map = self.render_buf = None def escape(chars): return ''.join(frozenset(chars)).replace('\\', r'\\').replace(']', r'\]').replace('-', r'\-') try: self.word_pat = re.compile(r'[\w{}]'.format(escape(self.opts.select_by_word_characters)), re.UNICODE) except Exception: safe_print('Invalid characters in select_by_word_characters, ignoring', file=sys.stderr) self.word_pat = re.compile(r'[\w{}]'.format(escape(defaults.select_by_word_characters)), re.UNICODE) def update_position(self, window_geometry): self.screen_geometry = calculate_gl_geometry(window_geometry) def resize(self, window_geometry): self.update_position(window_geometry) self.sprite_map_type = (GLuint * (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() with self.buffer_lock: self.render_buf = self.sprite_map_type() self.selection_buf = self.sprite_map_type() self.render_buf_is_dirty = True self.current_selection.clear() 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) | 1 for which, val in changes.items(): val = item(val) if val is None: continue dirtied = True if which is DynamicColor.default_fg: self.screen.default_fg = val elif which is DynamicColor.default_bg: self.screen.default_bg = val elif which is DynamicColor.cursor_color: self.screen.cursor_color = val elif which is DynamicColor.highlight_fg: self.screen.highlight_fg = val elif which is DynamicColor.highlight_bg: self.screen.highlight_bg = val if dirtied: self.screen.mark_as_dirty() def scroll(self, amt, upwards=True): if not isinstance(amt, int): amt = {'line': 1, 'page': self.screen.lines - 1, 'full': self.screen.historybuf.count}[amt] if not upwards: amt *= -1 y = max(0, min(self.scrolled_by + amt, self.screen.historybuf.count)) if y != self.scrolled_by: self.scrolled_by = y self.update_cell_data() def update_cell_data(self, force_full_refresh=False): sprites = get_boss().sprites is_dirty = self.screen.is_dirty() with sprites.lock: fg = self.screen.default_fg fg = fg >> 8 if fg & 1 else self.default_fg bg = self.screen.default_bg bg = bg >> 8 if bg & 1 else self.default_bg cursor_changed, history_line_added_count = self.screen.update_cell_data( sprites.backend, self.color_profile, addressof(self.main_sprite_map), fg, 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( sprites.backend, self.color_profile, addressof(self.main_sprite_map), fg, 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: if is_dirty: self.current_selection.clear() memmove(self.render_buf, data, sizeof(type(data))) self.render_data = self.screen_geometry self.render_buf_is_dirty = True if cursor_changed: c = self.screen.cursor self.current_cursor = Cursor(c.x, c.y, c.shape, c.blink) 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 with self.buffer_lock: if is_press: self.current_selection.start_x = self.current_selection.end_x = x self.current_selection.start_y = self.current_selection.end_y = y self.current_selection.start_scrolled_by = self.current_selection.end_scrolled_by = self.scrolled_by self.current_selection.in_progress = True elif self.current_selection.in_progress: self.current_selection.end_x = x self.current_selection.end_y = y self.current_selection.end_scrolled_by = self.scrolled_by if is_press is False: self.current_selection.in_progress = False 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_line(y) if l is not None: text = l.as_base_text() 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_line(y) if l is not None: text = l.as_base_text() 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 screen_line(self, y): ' Return the Line object corresponding to the yth line on the rendered screen ' if y >= 0 and y < self.screen.lines: if self.scrolled_by: if y < self.scrolled_by: return self.screen.historybuf.line(self.scrolled_by - 1 - y) return self.screen.line(y - self.scrolled_by) else: return self.screen.line(y) def multi_click(self, count, x, y): x, y = self.cell_for_pos(x, y) if x is not None: line = self.screen_line(y) if line is not None and count in (2, 3): s = self.current_selection s.start_scrolled_by = s.end_scrolled_by = self.scrolled_by s.start_y = s.end_y = y s.in_progress = False if count == 3: for i in range(self.screen.columns): if line[i] != ' ': s.start_x = i break else: s.start_x = 0 for i in range(self.screen.columns): c = self.screen.columns - 1 - i if line[c] != ' ': s.end_x = c break else: s.end_x = self.screen.columns - 1 elif count == 2: i = x while i >= 0 and self.word_pat.match(line[i]) is not None: i -= 1 s.start_x = i if i == x else i + 1 i = x while i < self.screen.columns and self.word_pat.match(line[i]) is not None: i += 1 s.end_x = i if i == x else i - 1 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, sel=None): s = sel or self.current_selection return s.text(self.screen.linebuf, self.screen.historybuf) def prepare_for_render(self, sprites): with self.buffer_lock: sg = self.render_data if sg is None: return if self.buffer_id is None: self.buffer_id = sprites.add_sprite_map() buf = self.render_buf start, end = sel = self.current_selection.limits(self.scrolled_by, self.screen.lines, self.screen.columns) if start != end: buf = self.selection_buf if self.render_buf_is_dirty or sel != self.last_rendered_selection: memmove(buf, self.render_buf, sizeof(type(buf))) fg = self.screen.highlight_fg fg = fg >> 8 if fg & 1 else self.highlight_fg bg = self.screen.highlight_bg bg = bg >> 8 if bg & 1 else self.highlight_bg self.screen.apply_selection(addressof(buf), start[0], start[1], end[0], end[1], fg, bg) if self.render_buf_is_dirty or self.last_rendered_selection != sel: sprites.set_sprite_map(self.buffer_id, buf) self.render_buf_is_dirty = False self.last_rendered_selection = sel return sg def render_cells(self, sg, cell_program, sprites, invert_colors=False): render_cells(self.buffer_id, sg, cell_program, sprites, invert_colors=invert_colors) def render_cursor(self, sg, cursor_program, is_focused): cursor = self.current_cursor if not self.screen.cursor_visible or self.scrolled_by: return def width(w=2, vert=True): dpi = self.dpix if vert else self.dpiy w *= dpi / 72.0 # as pixels factor = 2 / (viewport_size.width if vert else viewport_size.height) return w * factor ul = cursor_program.uniform_location left = sg.xstart + cursor.x * sg.dx top = sg.ystart - cursor.y * sg.dy cc = self.screen.cursor_color col = color_from_int(cc >> 8) if cc & 1 else self.opts.cursor 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] / 255.0, col[1] / 255.0, col[2] / 255.0, alpha) glUniform2f(ul('xpos'), left, right) glUniform2f(ul('ypos'), top, bottom) glDrawArrays(GL_TRIANGLE_FAN if is_focused else GL_LINE_LOOP, 0, 4) glDisable(GL_BLEND)