From b4f88b4f8134fe22fca205e2d451cee09a42ca1f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 3 Nov 2023 08:51:58 +0530 Subject: [PATCH] A new option to control when hyperlinks are underlined While kitty is never going to underline detected URLs as the performance of that is absurd, underlining hyperlinks specifically is acceptable, since they dont require detection. See #6766 --- docs/changelog.rst | 2 ++ kitty/line.c | 4 ++++ kitty/options/definition.py | 13 ++++++++++++- kitty/options/parse.py | 8 ++++++++ kitty/options/to-c-generated.h | 15 +++++++++++++++ kitty/options/to-c.h | 10 ++++++++++ kitty/options/types.py | 3 +++ kitty/screen.c | 13 ++++++++----- kitty/screen.h | 2 +- kitty/state.h | 2 ++ 10 files changed, 65 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index dc92d2de7..085fef400 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -58,6 +58,8 @@ Detailed list of changes - A new mouse action ``mouse_selection word_and_line_from_point`` to select the current word under the mouse cursor and extend to end of line (:pull:`6663`) +- A new option :opt:`underline_hyperlinks` to control when hyperlinks are underlined (:iss:`6766`) + - Allow using the full range of standard mouse cursor shapes when customizing the mouse cursor - macOS: When running the default shell with the login program fix :file:`~/.hushlogin` not being respected when opening windows not in the home directory (:iss:`6689`) diff --git a/kitty/line.c b/kitty/line.c index 930b372f2..9895a3cd8 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -660,6 +660,10 @@ line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Curs } self->cpu_cells[at].ch = ch; self->cpu_cells[at].hyperlink_id = hyperlink_id; + if (OPT(underline_hyperlinks) == UNDERLINE_ALWAYS && hyperlink_id) { + g->decoration_fg = ((OPT(url_color) & COL_MASK) << 8) | 2; + g->attrs.decoration = OPT(url_style); + } memset(self->cpu_cells[at].cc_idx, 0, sizeof(self->cpu_cells[at].cc_idx)); } diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 309cc1833..1c39f9efc 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -486,7 +486,8 @@ opt('detect_urls', 'yes', long_text=''' Detect URLs under the mouse. Detected URLs are highlighted with an underline and the mouse cursor becomes a hand over them. Even if this option is disabled, URLs -are still clickable. +are still clickable. See also the :opt:`underline_hyperlinks` option to control +how hyperlinks (as opposed to plain text URLs) are displayed. ''' ) @@ -510,6 +511,16 @@ When the mouse hovers over a terminal hyperlink, show the actual URL that will be activated when the hyperlink is clicked. ''') + +opt('underline_hyperlinks', 'hover', choices=('hover', 'always', 'never'), + ctype='underline_hyperlinks', long_text=''' +Control how hyperlinks are underlined. They can either be underlined on mouse +``hover``, ``always`` (i.e. permanently underlined) or ``never`` which means +that kitty will not apply any underline styling to hyperlinks. +Uses the :opt:`url_style` and :opt:`url_color` settings for the underline style. +''') + + opt('copy_on_select', 'no', option_type='copy_on_select', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index f6cb0c536..752680382 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1308,6 +1308,14 @@ class Parser: choices_for_undercurl_style = frozenset(('thin-sparse', 'thin-dense', 'thick-sparse', 'thick-dense')) + def underline_hyperlinks(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_underline_hyperlinks: + raise ValueError(f"The value {val} is not a valid choice for underline_hyperlinks") + ans["underline_hyperlinks"] = val + + choices_for_underline_hyperlinks = frozenset(('hover', 'always', 'never')) + def update_check_interval(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['update_check_interval'] = float(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index bf2ca731d..4e987456c 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -291,6 +291,19 @@ convert_from_opts_show_hyperlink_targets(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_underline_hyperlinks(PyObject *val, Options *opts) { + opts->underline_hyperlinks = underline_hyperlinks(val); +} + +static void +convert_from_opts_underline_hyperlinks(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "underline_hyperlinks"); + if (ret == NULL) return; + convert_from_python_underline_hyperlinks(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_select_by_word_characters(PyObject *val, Options *opts) { select_by_word_characters(val, opts); @@ -1143,6 +1156,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_show_hyperlink_targets(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_underline_hyperlinks(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_select_by_word_characters(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_select_by_word_characters_forward(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index b9b0f0b0f..76608e200 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -57,6 +57,16 @@ window_title_in(PyObject *title_in) { return ALL; } +static UnderlineHyperlinks +underline_hyperlinks(PyObject *x) { + const char *in = PyUnicode_AsUTF8(x); + switch(in[0]) { + case 'a': return UNDERLINE_ALWAYS; + case 'n': return UNDERLINE_NEVER; + default : return UNDERLINE_ON_HOVER; + } +} + static BackgroundImageLayout bglayout(PyObject *layout_name) { const char *name = PyUnicode_AsUTF8(layout_name); diff --git a/kitty/options/types.py b/kitty/options/types.py index f18f0d619..720acf541 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -29,6 +29,7 @@ choices_for_tab_bar_style = typing.Literal['fade', 'hidden', 'powerline', 'separ choices_for_tab_powerline_style = typing.Literal['angled', 'round', 'slanted'] choices_for_tab_switch_strategy = typing.Literal['last', 'left', 'previous', 'right'] choices_for_undercurl_style = typing.Literal['thin-sparse', 'thin-dense', 'thick-sparse', 'thick-dense'] +choices_for_underline_hyperlinks = typing.Literal['hover', 'always', 'never'] choices_for_window_logo_position = typing.Literal['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'] option_names = ( # {{{ @@ -432,6 +433,7 @@ option_names = ( # {{{ 'text_fg_override_threshold', 'touch_scroll_multiplier', 'undercurl_style', + 'underline_hyperlinks', 'update_check_interval', 'url_color', 'url_excluded_characters', @@ -588,6 +590,7 @@ class Options: text_fg_override_threshold: float = 0.0 touch_scroll_multiplier: float = 1.0 undercurl_style: choices_for_undercurl_style = 'thin-sparse' + underline_hyperlinks: choices_for_underline_hyperlinks = 'hover' update_check_interval: float = 24.0 url_color: Color = Color(0, 135, 189) url_excluded_characters: str = '' diff --git a/kitty/screen.c b/kitty/screen.c index 666b7403e..4ddda5104 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2809,7 +2809,9 @@ screen_apply_selection(Screen *self, void *address, size_t size) { } self->selections.last_rendered_count = self->selections.count; for (size_t i = 0; i < self->url_ranges.count; i++) { - apply_selection(self, address, self->url_ranges.items + i, 2); + Selection *s = self->url_ranges.items + i; + if (OPT(underline_hyperlinks) == UNDERLINE_NEVER && s->is_hyperlink) continue; + apply_selection(self, address, s, 2); } self->url_ranges.last_rendered_count = self->url_ranges.count; } @@ -3945,12 +3947,13 @@ screen_start_selection(Screen *self, index_type x, index_type y, bool in_left_ha } static void -add_url_range(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) { +add_url_range(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y, bool is_hyperlink) { #define A(attr, val) r->attr = val; ensure_space_for(&self->url_ranges, items, Selection, self->url_ranges.count + 8, capacity, 8, false); Selection *r = self->url_ranges.items + self->url_ranges.count++; memset(r, 0, sizeof(Selection)); r->last_rendered.y = INT_MAX; + r->is_hyperlink = is_hyperlink; A(start.x, start_x); A(end.x, end_x); A(start.y, start_y); A(end.y, end_y); A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by); A(start.in_left_half_of_cell, true); @@ -3960,7 +3963,7 @@ add_url_range(Screen *self, index_type start_x, index_type start_y, index_type e void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) { self->url_ranges.count = 0; - if (start_x || start_y || end_x || end_y) add_url_range(self, start_x, start_y, end_x, end_y); + if (start_x || start_y || end_x || end_y) add_url_range(self, start_x, start_y, end_x, end_y, false); } static bool @@ -3972,7 +3975,7 @@ mark_hyperlinks_in_line(Screen *self, Line *line, hyperlink_id_type id, index_ty bool has_hyperlink = line->cpu_cells[x].hyperlink_id == id; if (in_range) { if (!has_hyperlink) { - add_url_range(self, start, y, x - 1, y); + add_url_range(self, start, y, x - 1, y, true); in_range = false; start = 0; } @@ -3983,7 +3986,7 @@ mark_hyperlinks_in_line(Screen *self, Line *line, hyperlink_id_type id, index_ty } } } - if (in_range) add_url_range(self, start, y, self->columns - 1, y); + if (in_range) add_url_range(self, start, y, self->columns - 1, y, true); return found; } diff --git a/kitty/screen.h b/kitty/screen.h index 5d8340eea..abfbdbbcf 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -39,7 +39,7 @@ typedef struct { typedef struct { SelectionBoundary start, end, input_start, input_current; unsigned int start_scrolled_by, end_scrolled_by; - bool rectangle_select, adjusting_start; + bool rectangle_select, adjusting_start, is_hyperlink; IterationData last_rendered; int sort_y, sort_x; struct { diff --git a/kitty/state.h b/kitty/state.h index 298007609..e9d57b75f 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -22,6 +22,7 @@ typedef struct { } UrlPrefix; typedef enum AdjustmentUnit { POINT = 0, PERCENT = 1, PIXEL = 2 } AdjustmentUnit; +typedef enum UnderlineHyperlinks { UNDERLINE_ON_HOVER = 0, UNDERLINE_ALWAYS = 1, UNDERLINE_NEVER = 2 } UnderlineHyperlinks; struct MenuItem { const char* *location; @@ -95,6 +96,7 @@ typedef struct { float val; AdjustmentUnit unit; } underline_position, underline_thickness, strikethrough_position, strikethrough_thickness, cell_width, cell_height, baseline; bool show_hyperlink_targets; + UnderlineHyperlinks underline_hyperlinks; int background_blur; long macos_titlebar_color; unsigned long wayland_titlebar_color;