diff --git a/docs/changelog.rst b/docs/changelog.rst index 071a01ce0..e79c3aa12 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -182,6 +182,8 @@ Detailed list of changes - hints kitten: A new option to set the background color of matched text (:pull:`9745`) +- The :opt:`show_hyperlink_targets` option now allows specifying a keyboard modifier so that target URLs are only shown on hover when the modifier is pressed (:pull:`9741`) + 0.46.2 [2026-03-21] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/glfw.c b/kitty/glfw.c index ed3b9ff9b..3a82d6e23 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -526,7 +526,6 @@ key_callback(GLFWwindow *w, GLFWkeyevent *ev) { if (key_modifier != -1) update_modifier_state_on_modifier_key_event(ev, key_modifier, is_left); #endif global_state.mods_at_last_key_or_button_event = ev->mods; - global_state.mouse_modifiers = ev->mods & ~GLFW_LOCK_MASK; global_state.callback_os_window->cursor_blink_zero_time = monotonic(); if (is_window_ready_for_callbacks() && !ev->fake_event_on_focus_change) on_key_input(ev); global_state.callback_os_window = NULL; @@ -563,7 +562,6 @@ mouse_button_callback(GLFWwindow *w, int button, int action, int mods) { monotonic_t now = monotonic(); cursor_active_callback(now); global_state.mods_at_last_key_or_button_event = mods; - global_state.mouse_modifiers = mods & ~GLFW_LOCK_MASK; OSWindow *window = global_state.callback_os_window; window->last_mouse_activity_at = now; if (button >= 0 && (unsigned int)button < arraysz(global_state.callback_os_window->mouse_button_pressed)) { diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 13083266d..1dab7ef25 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -714,11 +714,11 @@ a double backslash. ''' ) -opt('show_hyperlink_targets', 'never', choices=('ctrl', 'cmd', 'shift', 'always', 'never'), +opt('show_hyperlink_targets', 'never', option_type='show_hyperlink_targets', ctype='show_hyperlink_targets', long_text=''' When the mouse hovers over a terminal hyperlink, show the actual URL that will -be activated when the hyperlink is clicked. Set to :code:`ctrl`, :code:`cmd` or +be activated when the hyperlink is clicked. Set to :code:`ctrl`, :code:`cmd`, :code:`alt` or :code:`shift` to show only while the corresponding modifier key is pressed (:kbd:`Ctrl`, :kbd:`Super` (macOS :kbd:`Cmd`), :kbd:`Shift`). If multiple modifiers are pressed, the URL is shown as long as the configured modifier is among them. diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 502834ebd..9ae978826 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -18,8 +18,8 @@ from kitty.options.utils import ( mouse_hide_wait, narrow_symbols, notify_on_cmd_finish, optional_edge_width, parse_font_spec, parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remote_control_password, resize_debounce_time, scrollback_lines, scrollback_pager_history_size, scrollbar_color, - shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, - tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, + shell_integration, show_hyperlink_targets, store_multiple, symbol_map, tab_activity_symbol, + tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, text_fg_override_threshold, titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, to_font_size, to_layout_names, to_modifiers, transparent_background_colors, underline_exclusion, url_prefixes, url_style, visual_bell_duration, @@ -1290,16 +1290,7 @@ class Parser: ans['shell_integration'] = shell_integration(val) def show_hyperlink_targets(self, val: str, ans: dict[str, typing.Any]) -> None: - val = val.lower() - if val == 'yes': - val = 'always' - elif val == 'no': - val = 'never' - if val not in self.choices_for_show_hyperlink_targets: - raise ValueError(f"The value {val} is not a valid choice for show_hyperlink_targets") - ans["show_hyperlink_targets"] = val - - choices_for_show_hyperlink_targets = frozenset(('ctrl', 'cmd', 'shift', 'always', 'never')) + ans['show_hyperlink_targets'] = show_hyperlink_targets(val) def single_window_margin_width(self, val: str, ans: dict[str, typing.Any]) -> None: ans['single_window_margin_width'] = optional_edge_width(val) diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index fc3751258..91fae2062 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -102,11 +102,13 @@ static inline ShowHyperlinkTargets show_hyperlink_targets(PyObject *x) { const char *in = PyUnicode_AsUTF8(x); if (!in) return SHOW_HYPERLINK_TARGETS_NEVER; - if (strcmp(in, "always") == 0) return SHOW_HYPERLINK_TARGETS_ALWAYS; - if (strcmp(in, "ctrl") == 0) return SHOW_HYPERLINK_TARGETS_CTRL; - if (strcmp(in, "shift") == 0) return SHOW_HYPERLINK_TARGETS_SHIFT; - if (strcmp(in, "cmd") == 0) return SHOW_HYPERLINK_TARGETS_CMD; - return SHOW_HYPERLINK_TARGETS_NEVER; + switch(in[0]) { + case 'a': return SHOW_HYPERLINK_TARGETS_ALWAYS; + case 'A': return SHOW_HYPERLINK_TARGETS_ALT; + case 'C': return SHOW_HYPERLINK_TARGETS_CTRL; + case 'S': return in[1] == 'u' ? SHOW_HYPERLINK_TARGETS_SUPER : SHOW_HYPERLINK_TARGETS_SHIFT; + default: return SHOW_HYPERLINK_TARGETS_NEVER; + } } static inline BackgroundImageLayout diff --git a/kitty/options/types.py b/kitty/options/types.py index 8870cb49d..5bf444fc5 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -28,7 +28,6 @@ choices_for_macos_show_window_title_in = typing.Literal['all', 'menubar', 'none' choices_for_placement_strategy = typing.Literal['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'] choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape choices_for_scrollbar = typing.Literal['scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered'] -choices_for_show_hyperlink_targets = typing.Literal['ctrl', 'cmd', 'shift', 'always', 'never'] choices_for_strip_trailing_spaces = typing.Literal['always', 'never', 'smart'] choices_for_tab_bar_align = typing.Literal['left', 'center', 'right'] choices_for_tab_bar_style = typing.Literal['fade', 'hidden', 'powerline', 'separator', 'slant', 'custom'] @@ -647,7 +646,7 @@ class Options: selection_foreground: kitty.fast_data_types.Color | None = Color(0, 0, 0) shell: str = '.' shell_integration: frozenset[str] = frozenset({'enabled'}) - show_hyperlink_targets: choices_for_show_hyperlink_targets = 'never' + show_hyperlink_targets: typing.Literal['never', 'always', 'Ctrl', 'Shift', 'Super', 'Alt'] = 'never' single_window_margin_width: FloatEdges = FloatEdges(left=-1.0, top=-1.0, right=-1.0, bottom=-1.0) single_window_padding_width: FloatEdges = FloatEdges(left=-1.0, top=-1.0, right=-1.0, bottom=-1.0) startup_session: str | None = None @@ -1150,4 +1149,4 @@ special_colors = frozenset({ }) -secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass') +secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass') \ No newline at end of file diff --git a/kitty/options/utils.py b/kitty/options/utils.py index ec777282d..0189dc19e 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -601,6 +601,24 @@ def url_prefixes(x: str) -> tuple[str, ...]: return tuple(a.lower() for a in x.replace(',', ' ').split()) + +def show_hyperlink_targets(x: str) -> Literal['never', 'always', 'Ctrl', 'Shift', 'Super', 'Alt']: + q = x.lower() + if q in ('never', 'n', 'no', 'false'): + return 'never' + if q in ('y', 'yes', 'true', 'always'): + return 'always' + if q == 'ctrl': + return 'Ctrl' + if q == 'shift': + return 'Shift' + if q in ('super', 'cmd'): + return 'Super' + if q in ('alt', 'meta'): + return 'Alt' + raise KeyError(f'Unknown value for show_hyperlink_targets: {x!r}') + + def copy_on_select(raw: str) -> str: q = raw.lower() # boolean values special cased for backwards compat diff --git a/kitty/shaders.c b/kitty/shaders.c index 9d482ed65..d8da11c40 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -857,23 +857,18 @@ render_a_bar(const UIRenderData *ui, WindowBarData *bar, PyObject *title, bool a static bool show_hyperlink_targets_with_modifiers(int mods) { switch (OPT(show_hyperlink_targets)) { - case SHOW_HYPERLINK_TARGETS_ALWAYS: - return true; - case SHOW_HYPERLINK_TARGETS_CTRL: - return (mods & GLFW_MOD_CONTROL) != 0; - case SHOW_HYPERLINK_TARGETS_SHIFT: - return (mods & GLFW_MOD_SHIFT) != 0; - case SHOW_HYPERLINK_TARGETS_CMD: - return (mods & GLFW_MOD_SUPER) != 0; - case SHOW_HYPERLINK_TARGETS_NEVER: - default: - return false; + case SHOW_HYPERLINK_TARGETS_ALWAYS: return true; + case SHOW_HYPERLINK_TARGETS_CTRL: return (mods & GLFW_MOD_CONTROL) != 0; + case SHOW_HYPERLINK_TARGETS_SHIFT: return (mods & GLFW_MOD_SHIFT) != 0; + case SHOW_HYPERLINK_TARGETS_SUPER: return (mods & GLFW_MOD_SUPER) != 0; + case SHOW_HYPERLINK_TARGETS_ALT: return (mods & GLFW_MOD_ALT) != 0; + default: return false; } } static bool has_hyperlink_target(OSWindow *os_window, Window *w, Screen *screen) { - return show_hyperlink_targets_with_modifiers(global_state.mouse_modifiers) && + return show_hyperlink_targets_with_modifiers(global_state.mods_at_last_key_or_button_event) && screen->current_hyperlink_under_mouse.id && w && !is_mouse_hidden(os_window) && global_state.mouse_hover_in_window == w->id; diff --git a/kitty/state.h b/kitty/state.h index f03ba5893..caf9c3505 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -35,8 +35,9 @@ typedef enum ShowHyperlinkTargets { SHOW_HYPERLINK_TARGETS_NEVER = 0, SHOW_HYPERLINK_TARGETS_ALWAYS = 1, SHOW_HYPERLINK_TARGETS_CTRL = 2, - SHOW_HYPERLINK_TARGETS_SHIFT = 3, - SHOW_HYPERLINK_TARGETS_CMD = 4 + SHOW_HYPERLINK_TARGETS_SHIFT = 4, + SHOW_HYPERLINK_TARGETS_SUPER = 8, + SHOW_HYPERLINK_TARGETS_ALT = 16 } ShowHyperlinkTargets; struct MenuItem { @@ -405,7 +406,6 @@ typedef struct GlobalState { struct { double x, y; } default_dpi; id_type active_drag_in_window, tracked_drag_in_window, mouse_hover_in_window, active_drag_resize; int active_drag_button, tracked_drag_button; - int mouse_modifiers; int mods_at_last_key_or_button_event; CloseRequest quit_request; bool redirect_mouse_handling;