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;