From 645890ccc8b63b0b3eea3a0de2c1c88003410050 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 8 Mar 2020 13:16:34 +0530 Subject: [PATCH] Make the set of URL prefixes that are recognized while hovering with the mouse configurable Fixes #2416 --- docs/changelog.rst | 3 +++ kitty/config_data.py | 14 +++++++++++--- kitty/line.c | 21 +++++++-------------- kitty/state.c | 36 ++++++++++++++++++++++++++++++++++++ kitty/state.h | 9 +++++++++ kitty_tests/__init__.py | 5 ++++- kitty_tests/datatypes.py | 2 ++ 7 files changed, 72 insertions(+), 18 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bf0f33e14..b6d15c72c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -40,6 +40,9 @@ To update |kitty|, :doc:`follow the instructions `. - Fix a bug that prevented using custom functions with the new marks feature (:iss:`2344`) +- Make the set of URL prefixes that are recognized while hovering with the + mouse configurable (:iss:`2416`) + - Fix border/margin/padding sizes not being recalculated on DPI change (:iss:`2346`) diff --git a/kitty/config_data.py b/kitty/config_data.py index 5b5e22fe3..3fe17bc85 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -6,7 +6,7 @@ import os from gettext import gettext as _ from typing import ( - Any, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar, Union + Any, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar, Union, cast ) from . import fast_data_types as defines @@ -502,6 +502,14 @@ def url_style(x: str) -> int: operating system's default URL handler.''')) +def url_prefixes(x: str) -> Tuple[str, ...]: + return tuple(a.lower() for a in x.replace(',', ' ').split()) + + +o('url_prefixes', 'http https file ftp', option_type=url_prefixes, long_text=_(''' +The set of URL prefixes to look for when detecting a URL under the mouse cursor.''')) + + def copy_on_select(raw: str) -> str: q = raw.lower() # boolean values special cased for backwards compat @@ -784,7 +792,7 @@ def tab_font_style(x: str) -> Tuple[bool, bool]: def tab_bar_min_tabs(x: str) -> int: - return max(1, positive_int(x)) + return cast(int, max(1, positive_int(x))) o('tab_bar_min_tabs', 2, option_type=tab_bar_min_tabs, long_text=_(''' @@ -1068,7 +1076,7 @@ def macos_titlebar_color(x: str) -> int: return 0 if x == 'background': return 1 - return (color_as_int(to_color(x)) << 8) | 2 + return cast(int, (color_as_int(to_color(x)) << 8) | 2) o('macos_titlebar_color', 'system', option_type=macos_titlebar_color, long_text=_(''' diff --git a/kitty/line.c b/kitty/line.c index 50c4eab7d..9cd3fc756 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -5,7 +5,7 @@ * Distributed under terms of the GPL3 license. */ -#include "data-types.h" +#include "state.h" #include "unicode-data.h" #include "lineops.h" #include "charsets.h" @@ -49,9 +49,6 @@ cell_text(CPUCell *cell) { // URL detection {{{ -static const char* url_prefixes[4] = {"https", "http", "file", "ftp"}; -static size_t url_prefix_lengths[arraysz(url_prefixes)] = {0}; - static inline index_type find_colon_slash(Line *self, index_type x, index_type limit) { // Find :// at or before x @@ -88,29 +85,25 @@ find_colon_slash(Line *self, index_type x, index_type limit) { } static inline bool -prefix_matches(Line *self, index_type at, const char* prefix, index_type prefix_len) { +prefix_matches(Line *self, index_type at, const char_type* prefix, index_type prefix_len) { if (prefix_len > at) return false; index_type p, i; for (p = at - prefix_len, i = 0; i < prefix_len && p < self->xnum; i++, p++) { - if ((self->cpu_cells[p].ch) != (unsigned char)prefix[i]) return false; + if ((self->cpu_cells[p].ch) != prefix[i]) return false; } return i == prefix_len; } static inline bool has_url_prefix_at(Line *self, index_type at, index_type min_prefix_len, index_type *ans) { - if (UNLIKELY(!url_prefix_lengths[0])) { - for (index_type i = 0; i < arraysz(url_prefixes); i++) url_prefix_lengths[i] = strlen(url_prefixes[i]); - } - for (index_type i = 0; i < arraysz(url_prefixes); i++) { - index_type prefix_len = url_prefix_lengths[i]; + for (size_t i = 0; i < OPT(url_prefixes.num); i++) { + index_type prefix_len = OPT(url_prefixes.values[i].len); if (at < prefix_len || prefix_len < min_prefix_len) continue; - if (prefix_matches(self, at, url_prefixes[i], prefix_len)) { *ans = at - prefix_len; return true; } + if (prefix_matches(self, at, OPT(url_prefixes.values[i].string), prefix_len)) { *ans = at - prefix_len; return true; } } return false; } -#define MAX_URL_SCHEME_LEN 5 #define MIN_URL_LEN 5 static inline bool @@ -129,7 +122,7 @@ line_url_start_at(Line *self, index_type x) { if (x >= self->xnum || self->xnum <= MIN_URL_LEN + 3) return self->xnum; index_type ds_pos = 0, t; // First look for :// ahead of x - if (self->xnum - x > MAX_URL_SCHEME_LEN + 3) ds_pos = find_colon_slash(self, x + MAX_URL_SCHEME_LEN + 3, x < 2 ? 0 : x - 2); + if (self->xnum - x > OPT(url_prefixes).max_prefix_len + 3) ds_pos = find_colon_slash(self, x + OPT(url_prefixes).max_prefix_len + 3, x < 2 ? 0 : x - 2); if (ds_pos != 0 && has_url_beyond(self, ds_pos)) { if (has_url_prefix_at(self, ds_pos, ds_pos > x ? ds_pos - x: 0, &t)) return t; } diff --git a/kitty/state.c b/kitty/state.c index 8723b53e8..6577160c7 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -513,6 +513,34 @@ pointer_shape(PyObject *shape_name) { return BEAM; } +static inline void +free_url_prefixes(void) { + OPT(url_prefixes).num = 0; + OPT(url_prefixes).max_prefix_len = 0; + if (OPT(url_prefixes).values) { + free(OPT(url_prefixes.values)); + OPT(url_prefixes).values = NULL; + } +} + +static void +set_url_prefixes(PyObject *up) { + free_url_prefixes(); + OPT(url_prefixes).values = calloc(PyTuple_GET_SIZE(up), sizeof(UrlPrefix)); + if (!OPT(url_prefixes).values) { PyErr_NoMemory(); return; } + OPT(url_prefixes).num = PyTuple_GET_SIZE(up); + for (size_t i = 0; i < OPT(url_prefixes).num; i++) { + PyObject *t = PyTuple_GET_ITEM(up, i); + if (!PyUnicode_Check(t)) { PyErr_SetString(PyExc_TypeError, "url_prefixes must be strings"); return; } + OPT(url_prefixes).values[i].len = MIN(arraysz(OPT(url_prefixes).values[i].string) - 1, (size_t)PyUnicode_GET_LENGTH(t)); + int kind = PyUnicode_KIND(t); + OPT(url_prefixes).max_prefix_len = MAX(OPT(url_prefixes).max_prefix_len, OPT(url_prefixes).values[i].len); + for (size_t x = 0; x < OPT(url_prefixes).values[i].len; x++) { + OPT(url_prefixes).values[i].string[x] = PyUnicode_READ(kind, PyUnicode_DATA(t), x); + } + } +} + #define dict_iter(d) { \ PyObject *key, *value; Py_ssize_t pos = 0; \ while (PyDict_Next(d, &pos, &key, &value)) @@ -628,6 +656,13 @@ PYWRAP1(set_options) { Py_CLEAR(ret); if (PyErr_Occurred()) return NULL; + PyObject *up = PyObject_GetAttrString(opts, "url_prefixes"); + if (up == NULL) return NULL; + if (!PyTuple_Check(up)) { PyErr_SetString(PyExc_TypeError, "url_prefixes must be a tuple"); return NULL; } + set_url_prefixes(up); + Py_DECREF(up); + if (PyErr_Occurred()) return NULL; + PyObject *chars = PyObject_GetAttrString(opts, "select_by_word_characters"); if (chars == NULL) return NULL; for (size_t i = 0; i < MIN((size_t)PyUnicode_GET_LENGTH(chars), sizeof(OPT(select_by_word_characters))/sizeof(OPT(select_by_word_characters[0]))); i++) { @@ -1081,6 +1116,7 @@ finalize(void) { // destroyed. free_bgimage(&global_state.bgimage, false); global_state.bgimage = NULL; + free_url_prefixes(); } bool diff --git a/kitty/state.h b/kitty/state.h index 6f21a8c89..ce7f3dc98 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -15,6 +15,11 @@ typedef enum { LEFT_EDGE, TOP_EDGE, RIGHT_EDGE, BOTTOM_EDGE } Edge; typedef enum { RESIZE_DRAW_STATIC, RESIZE_DRAW_SCALED, RESIZE_DRAW_BLANK, RESIZE_DRAW_SIZE } ResizeDrawStrategy; typedef enum { REPEAT_MIRROR, REPEAT_CLAMP, REPEAT_DEFAULT } RepeatStrategy; +typedef struct { + char_type string[16]; + size_t len; +} UrlPrefix; + typedef struct { monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval; double wheel_scroll_multiplier, touch_scroll_multiplier; @@ -61,6 +66,10 @@ typedef struct { bool debug_keyboard; monotonic_t resize_debounce_time; MouseShape pointer_shape_when_grabbed; + struct { + UrlPrefix *values; + size_t num, max_prefix_len; + } url_prefixes; } Options; typedef struct { diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index f4a248310..96438a131 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -74,12 +74,15 @@ class BaseTest(TestCase): ae = TestCase.assertEqual maxDiff = 2000 - def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_height=20, options=None): + def set_options(self, options=None): final_options = {'scrollback_pager_history_size': 1024, 'click_interval': 0.5} if options: final_options.update(options) options = Options(merge_configs(defaults._asdict(), final_options)) set_options(options) + + def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_height=20, options=None): + self.set_options(options) c = Callbacks() return Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c) diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index c205c87b3..21161f03a 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -232,6 +232,8 @@ def test_line(self): self.assertEqualAttributes(l3.cursor_from(0), q) def test_url_at(self): + self.set_options() + def create(t): lb = create.lb = LineBuf(1, len(t)) lf = lb.line(0)