From 63f8fd59299efc059787d77baee0608936d23f5e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 1 Dec 2016 16:03:32 +0530 Subject: [PATCH] Implement clicking on URLs to open them --- kitty/char_grid.py | 25 +++++++++++++++++++- kitty/config.py | 58 +++++++++++++++++++++++++++------------------- kitty/kitty.conf | 30 +++++++++++++++++------- kitty/tabs.py | 5 +++- kitty/utils.py | 10 ++++++++ kitty/window.py | 7 +++++- 6 files changed, 100 insertions(+), 35 deletions(-) diff --git a/kitty/char_grid.py b/kitty/char_grid.py index 7f28bc985..5c472ff8b 100644 --- a/kitty/char_grid.py +++ b/kitty/char_grid.py @@ -9,7 +9,7 @@ from threading import Lock from .config import build_ansi_color_table from .constants import tab_manager, viewport_size, cell_size, ScreenGeometry, GLuint -from .utils import get_logical_dpi, to_color, set_primary_selection +from .utils import get_logical_dpi, to_color, set_primary_selection, open_url from .fast_data_types import ( glUniform2ui, glUniform4f, glUniform1i, glUniform2f, glDrawArraysInstanced, GL_TRIANGLE_FAN, glEnable, glDisable, GL_BLEND, glDrawArrays, ColorProfile, @@ -169,6 +169,8 @@ class Selection: class CharGrid: + url_pat = re.compile('(?:http|https|file|ftp)://\S+', re.IGNORECASE) + def __init__(self, screen, opts): self.buffer_lock = Lock() self.current_selection = Selection() @@ -283,6 +285,27 @@ class CharGrid: if ps and ps.strip(): set_primary_selection(ps) + def has_url_at(self, x, y): + x, y = self.cell_for_pos(x, y) + 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) + 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())) + 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: diff --git a/kitty/config.py b/kitty/config.py index d4c1e99ba..fc1830495 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -38,11 +38,45 @@ def to_opacity(x): return max(0.3, min(float(x), 1)) +def parse_mods(parts): + + def map_mod(m): + return {'CTRL': 'CONTROL', 'CMD': 'CONTROL'}.get(m, m) + + mods = 0 + for m in parts: + try: + mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper())) + except AttributeError: + print('Shortcut: {} has an unknown modifier, ignoring'.format(parts.join('+')), file=sys.stderr) + return + + return mods + + +def parse_key(val, keymap): + sc, action = val.partition(' ')[::2] + if not sc or not action: + return + parts = sc.split('+') + mods = parse_mods(parts[:-1]) + key = getattr(defines, 'GLFW_KEY_' + parts[-1].upper(), None) + if key is None: + print('Shortcut: {} has an unknown key, ignoring'.format(val), file=sys.stderr) + return + keymap[(mods, key)] = action + + +def to_open_url_modifiers(val): + return parse_mods(val.split('+')) + + type_map = { 'scrollback_lines': int, 'font_size': to_font_size, 'cursor_shape': to_cursor_shape, 'cursor_opacity': to_opacity, + 'open_url_modifiers': to_open_url_modifiers, 'repaint_delay': int, 'window_border_width': float, 'wheel_scroll_multiplier': float, @@ -58,30 +92,6 @@ for i in range(16): type_map['color%d' % i] = lambda x: to_color(x, validate=True) -def parse_key(val, keymap): - sc, action = val.partition(' ')[::2] - if not sc or not action: - return - parts = sc.split('+') - - def map_mod(m): - return {'CTRL': 'CONTROL', 'CMD': 'CONTROL'}.get(m, m) - - mods = 0 - for m in parts[:-1]: - try: - mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper())) - except AttributeError: - print('Shortcut: {} has an unknown modifier, ignoring'.format(val), file=sys.stderr) - return - - key = getattr(defines, 'GLFW_KEY_' + parts[-1].upper(), None) - if key is None: - print('Shortcut: {} has an unknown key, ignoring'.format(val), file=sys.stderr) - return - keymap[(mods, key)] = action - - def parse_config(lines): ans = {'keymap': {}} for line in lines: diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 74cc246bc..6d7094855 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -1,13 +1,20 @@ # vim:fileencoding=utf-8:ft=config -# The value of the TERM environment variable to set -term xterm-kitty +# Font family +font_family monospace + +# Font size (in pts) +font_size 11.0 + # The foreground color foreground #dddddd + # The background color background #000000 + # The foreground for selections selection_foreground #000000 + # The background for selections selection_background #FFFACD @@ -28,12 +35,6 @@ cursor_blink_interval 0.5 # zero or a negative number to never stop blinking. cursor_stop_blinking_after 15.0 -# Font family -font_family monospace - -# Font size (in pts) -font_size 11.0 - # Number of lines of history to keep in memory for scrolling back scrollback_lines 2000 @@ -52,6 +53,16 @@ repaint_delay 20 # zero or a negative number to disable mouse cursor hiding. mouse_hide_wait 3.0 +# The modifier keys to press when clicking with the mouse on URLs to open the URL +open_url_modifiers ctrl+shift + +# The program with which to open URLs that are clicked on. The special value "default" means to +# use the operating system's default URL handler. +open_url_with default + +# The value of the TERM environment variable to set +term xterm-kitty + # The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution. window_border_width 2 @@ -61,6 +72,9 @@ active_border_color #00ff00 # The color for the border of inactive windows inactive_border_color #cccccc +# The 16 terminal colors. There are 8 basic colors, each color has a dull and +# bright version. + # black color0 #000000 color8 #4d4d4d diff --git a/kitty/tabs.py b/kitty/tabs.py index a0a78c1fb..184274a12 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -19,7 +19,7 @@ from .constants import viewport_size, shell_path, appname, set_tab_manager, tab_ from .fast_data_types import ( glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLFW_PRESS, GLFW_REPEAT, GLFW_MOUSE_BUTTON_1, glfw_post_empty_event, - GLFW_CURSOR_NORMAL, GLFW_CURSOR, GLFW_CURSOR_HIDDEN + GLFW_CURSOR_NORMAL, GLFW_CURSOR, GLFW_CURSOR_HIDDEN, ) from .fonts import set_font_family from .borders import Borders, BordersProgram @@ -385,6 +385,9 @@ class TabManager(Thread): def hide_mouse_cursor(self): self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_HIDDEN) + def change_mouse_cursor(self, click=False): + self.glfw_window.set_click_cursor(click) + def start_cursor_blink(self): self.cursor_blinking = True if self.opts.cursor_stop_blinking_after > 0: diff --git a/kitty/utils.py b/kitty/utils.py index b513e93db..1347d6bcb 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -5,6 +5,7 @@ import re import os import signal +import shlex import subprocess import ctypes from collections import namedtuple @@ -270,3 +271,12 @@ def set_primary_selection(text): p = subprocess.Popen(['xsel', '-i', '-p'], stdin=subprocess.PIPE) p.stdin.write(text), p.stdin.close() p.wait() + + +def open_url(url, program='default'): + if program == 'default': + cmd = ['xdg-open'] + else: + cmd = shlex.split(program) + cmd.append(url) + subprocess.Popen(cmd).wait() diff --git a/kitty/window.py b/kitty/window.py index 5ba3cdbe1..f8b4411c4 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -156,6 +156,8 @@ class Window: x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) self.char_grid.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) self.click_queue.append(monotonic()) self.dispatch_multi_click(x, y) elif button == GLFW_MOUSE_BUTTON_MIDDLE: @@ -167,8 +169,11 @@ class Window: x, y = self.char_grid.cell_for_pos(x, y) def on_mouse_move(self, window, x, y): + x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) if self.char_grid.current_selection.in_progress: - self.char_grid.update_drag(None, max(0, x - self.geometry.left), max(0, y - self.geometry.top)) + self.char_grid.update_drag(None, x, y) + tm = tab_manager() + tm.queue_ui_action(tab_manager().change_mouse_cursor, self.char_grid.has_url_at(x, y)) def on_mouse_scroll(self, window, x, y): handle_event = (