diff --git a/kitty/boss.py b/kitty/boss.py index 75bfb5d73..2adf850d9 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -21,7 +21,7 @@ from .keys import ( ) from .session import create_session from .tabs import SpecialWindow, TabManager -from .utils import get_primary_selection, safe_print, set_primary_selection +from .utils import get_primary_selection, safe_print, set_primary_selection, open_url from .window import load_shader_programs @@ -247,6 +247,10 @@ class Boss: k = get_key_map(w.screen)[GLFW_KEY_UP if upwards else GLFW_KEY_DOWN] w.write_to_child(k * amt) + def open_url(self, url): + if url: + open_url(url, self.opts.open_url_with) + def gui_close_window(self, window): window.destroy() for tab in self.tab_manager: diff --git a/kitty/line.c b/kitty/line.c index 6d1350c99..c2212c71f 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -54,6 +54,8 @@ line_text_at(char_type ch, combining_type cc) { return ans; } +// URL detection {{{ + static const char* url_prefixes[4] = {"https", "http", "file", "ftp"}; static size_t url_prefix_lengths[sizeof(url_prefixes)/sizeof(url_prefixes[0])] = {0}; typedef enum URL_PARSER_STATES {ANY, FIRST_SLASH, SECOND_SLASH} URL_PARSER_STATE; @@ -137,12 +139,30 @@ line_url_start_at(Line *self, index_type x) { return self->xnum; } +index_type +line_url_end_at(Line *self, index_type x) { + index_type ans = x; + if (x >= self->xnum || self->xnum <= MIN_URL_LEN + 3) return 0; + while (ans < self->xnum && is_url_char(self->cells[ans].ch & CHAR_MASK)) ans++; + ans--; + while (ans > x && can_strip_from_end_of_url(self->cells[ans].ch & CHAR_MASK)) ans--; + return ans; +} + static PyObject* url_start_at(Line *self, PyObject *x) { #define url_start_at_doc "url_start_at(x) -> Return the start cell number for a URL containing x or self->xnum if not found" return PyLong_FromUnsignedLong((unsigned long)line_url_start_at(self, PyLong_AsUnsignedLong(x))); } +static PyObject* +url_end_at(Line *self, PyObject *x) { +#define url_end_at_doc "url_end_at(x) -> Return the end cell number for a URL containing x or 0 if not found" + return PyLong_FromUnsignedLong((unsigned long)line_url_end_at(self, PyLong_AsUnsignedLong(x))); +} + +// }}} + static PyObject* text_at(Line* self, Py_ssize_t xval) { #define text_at_doc "[x] -> Return the text in the specified cell" @@ -567,6 +587,7 @@ static PyMethodDef methods[] = { METHOD(is_continued, METH_NOARGS) METHOD(width, METH_O) METHOD(url_start_at, METH_O) + METHOD(url_end_at, METH_O) {NULL} /* Sentinel */ }; diff --git a/kitty/lineops.h b/kitty/lineops.h index 02c53ae1a..63fc4ec8f 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -62,6 +62,7 @@ void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *); void line_right_shift(Line *, unsigned int , unsigned int ); void line_add_combining_char(Line *, uint32_t , unsigned int ); index_type line_url_start_at(Line *self, index_type x); +index_type line_url_end_at(Line *self, index_type x); index_type line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen); unsigned int line_length(Line *self); PyObject* unicode_in_range(Line *self, index_type start, index_type limit, bool include_cc, char leading_char); diff --git a/kitty/mouse.c b/kitty/mouse.c index aa3db60d4..3ded5c6d6 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -216,6 +216,20 @@ HANDLER(add_click) { #undef N } +static inline void +open_url(Window *w) { + Line *line = screen_visual_line(w->render_data.screen, w->mouse_cell_y); + if (line) { + index_type start = line_url_start_at(line, w->mouse_cell_x); + if (start < line->xnum) { + index_type end = line_url_end_at(line, w->mouse_cell_x); + if (end > start) { + call_boss(open_url, "N", unicode_in_range(line, start, end + 1, true, 0)); + } + } + } +} + HANDLER(handle_button_event) { Tab *t = global_state.tabs + global_state.active_tab; bool is_release = !global_state.mouse_button_pressed[button]; @@ -236,7 +250,7 @@ HANDLER(handle_button_event) { update_drag(true, w, is_release); if (is_release) { if (modifiers == (int)OPT(open_url_modifiers)) { - // TODO: click_url + open_url(w); } else { if (is_release) add_click(w, button, modifiers, window_idx); } diff --git a/kitty/unicode-data.h b/kitty/unicode-data.h index 3ccbcbaba..15557f04e 100644 --- a/kitty/unicode-data.h +++ b/kitty/unicode-data.h @@ -30,3 +30,12 @@ normalize(uint32_t ch, uint32_t cc1, uint32_t cc2) { if (ans && cc2) ans = uc_composition(ans, cc2); return ans; } + +static inline bool +can_strip_from_end_of_url(uint32_t ch) { + // remove trailing punctuation + return ( + (uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_P) && ch != '/') || + ch == '>' + ) ? true : false; +} diff --git a/kitty/window.py b/kitty/window.py index a252b6026..1de956163 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -2,7 +2,6 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -import re import sys import weakref from collections import deque @@ -23,9 +22,7 @@ from .fast_data_types import ( ) from .rgb import to_color from .terminfo import get_capabilities -from .utils import ( - color_as_int, load_shaders, open_url, parse_color_set, sanitize_title -) +from .utils import color_as_int, load_shaders, parse_color_set, sanitize_title class DynamicColor(Enum): @@ -233,30 +230,6 @@ class Window: def text_for_selection(self): return ''.join(self.screen.text_for_selection()) - # mouse handling {{{ - 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 destroy(self): if self.vao_id is not None: remove_vao(self.vao_id) diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 86c498bac..140e51919 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -222,6 +222,14 @@ class TestDataTypes(BaseTest): l.set_text(t, 0, len(t), C()) return l + for trail in '.,]>)\\': + l = create("http://xyz.com" + trail) + self.ae(l.url_end_at(0), len(l) - 2) + l = create("ftp://abc/") + self.ae(l.url_end_at(0), len(l) - 1) + l = create("http://-abcd] ") + self.ae(l.url_end_at(0), len(l) - 3) + def lspace_test(n, scheme='http'): l = create(' ' * n + scheme + '://acme.com') for i in range(0, n):