Implement clicking on URLs to open them

This commit is contained in:
Kovid Goyal 2016-12-01 16:03:32 +05:30
parent a05b64f2fe
commit 63f8fd5929
6 changed files with 100 additions and 35 deletions

View File

@ -9,7 +9,7 @@ from threading import Lock
from .config import build_ansi_color_table from .config import build_ansi_color_table
from .constants import tab_manager, viewport_size, cell_size, ScreenGeometry, GLuint 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 ( from .fast_data_types import (
glUniform2ui, glUniform4f, glUniform1i, glUniform2f, glDrawArraysInstanced, glUniform2ui, glUniform4f, glUniform1i, glUniform2f, glDrawArraysInstanced,
GL_TRIANGLE_FAN, glEnable, glDisable, GL_BLEND, glDrawArrays, ColorProfile, GL_TRIANGLE_FAN, glEnable, glDisable, GL_BLEND, glDrawArrays, ColorProfile,
@ -169,6 +169,8 @@ class Selection:
class CharGrid: class CharGrid:
url_pat = re.compile('(?:http|https|file|ftp)://\S+', re.IGNORECASE)
def __init__(self, screen, opts): def __init__(self, screen, opts):
self.buffer_lock = Lock() self.buffer_lock = Lock()
self.current_selection = Selection() self.current_selection = Selection()
@ -283,6 +285,27 @@ class CharGrid:
if ps and ps.strip(): if ps and ps.strip():
set_primary_selection(ps) 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): def screen_line(self, y):
' Return the Line object corresponding to the yth line on the rendered screen ' ' Return the Line object corresponding to the yth line on the rendered screen '
if y >= 0 and y < self.screen.lines: if y >= 0 and y < self.screen.lines:

View File

@ -38,11 +38,45 @@ def to_opacity(x):
return max(0.3, min(float(x), 1)) 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 = { type_map = {
'scrollback_lines': int, 'scrollback_lines': int,
'font_size': to_font_size, 'font_size': to_font_size,
'cursor_shape': to_cursor_shape, 'cursor_shape': to_cursor_shape,
'cursor_opacity': to_opacity, 'cursor_opacity': to_opacity,
'open_url_modifiers': to_open_url_modifiers,
'repaint_delay': int, 'repaint_delay': int,
'window_border_width': float, 'window_border_width': float,
'wheel_scroll_multiplier': 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) 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): def parse_config(lines):
ans = {'keymap': {}} ans = {'keymap': {}}
for line in lines: for line in lines:

View File

@ -1,13 +1,20 @@
# vim:fileencoding=utf-8:ft=config # vim:fileencoding=utf-8:ft=config
# The value of the TERM environment variable to set # Font family
term xterm-kitty font_family monospace
# Font size (in pts)
font_size 11.0
# The foreground color # The foreground color
foreground #dddddd foreground #dddddd
# The background color # The background color
background #000000 background #000000
# The foreground for selections # The foreground for selections
selection_foreground #000000 selection_foreground #000000
# The background for selections # The background for selections
selection_background #FFFACD selection_background #FFFACD
@ -28,12 +35,6 @@ cursor_blink_interval 0.5
# zero or a negative number to never stop blinking. # zero or a negative number to never stop blinking.
cursor_stop_blinking_after 15.0 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 # Number of lines of history to keep in memory for scrolling back
scrollback_lines 2000 scrollback_lines 2000
@ -52,6 +53,16 @@ repaint_delay 20
# zero or a negative number to disable mouse cursor hiding. # zero or a negative number to disable mouse cursor hiding.
mouse_hide_wait 3.0 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. # The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution.
window_border_width 2 window_border_width 2
@ -61,6 +72,9 @@ active_border_color #00ff00
# The color for the border of inactive windows # The color for the border of inactive windows
inactive_border_color #cccccc inactive_border_color #cccccc
# The 16 terminal colors. There are 8 basic colors, each color has a dull and
# bright version.
# black # black
color0 #000000 color0 #000000
color8 #4d4d4d color8 #4d4d4d

View File

@ -19,7 +19,7 @@ from .constants import viewport_size, shell_path, appname, set_tab_manager, tab_
from .fast_data_types import ( from .fast_data_types import (
glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLFW_PRESS, glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLFW_PRESS,
GLFW_REPEAT, GLFW_MOUSE_BUTTON_1, glfw_post_empty_event, 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 .fonts import set_font_family
from .borders import Borders, BordersProgram from .borders import Borders, BordersProgram
@ -385,6 +385,9 @@ class TabManager(Thread):
def hide_mouse_cursor(self): def hide_mouse_cursor(self):
self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_HIDDEN) 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): def start_cursor_blink(self):
self.cursor_blinking = True self.cursor_blinking = True
if self.opts.cursor_stop_blinking_after > 0: if self.opts.cursor_stop_blinking_after > 0:

View File

@ -5,6 +5,7 @@
import re import re
import os import os
import signal import signal
import shlex
import subprocess import subprocess
import ctypes import ctypes
from collections import namedtuple from collections import namedtuple
@ -270,3 +271,12 @@ def set_primary_selection(text):
p = subprocess.Popen(['xsel', '-i', '-p'], stdin=subprocess.PIPE) p = subprocess.Popen(['xsel', '-i', '-p'], stdin=subprocess.PIPE)
p.stdin.write(text), p.stdin.close() p.stdin.write(text), p.stdin.close()
p.wait() 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()

View File

@ -156,6 +156,8 @@ class Window:
x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top)
self.char_grid.update_drag(action == GLFW_PRESS, x, y) self.char_grid.update_drag(action == GLFW_PRESS, x, y)
if action == GLFW_RELEASE: 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.click_queue.append(monotonic())
self.dispatch_multi_click(x, y) self.dispatch_multi_click(x, y)
elif button == GLFW_MOUSE_BUTTON_MIDDLE: elif button == GLFW_MOUSE_BUTTON_MIDDLE:
@ -167,8 +169,11 @@ class Window:
x, y = self.char_grid.cell_for_pos(x, y) x, y = self.char_grid.cell_for_pos(x, y)
def on_mouse_move(self, window, 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: 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): def on_mouse_scroll(self, window, x, y):
handle_event = ( handle_event = (