diff --git a/kitty/glfw.c b/kitty/glfw.c index 6cb03b3ee..ed3b9ff9b 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -457,8 +457,6 @@ refresh_callback(GLFWwindow *w) { request_tick_callback(); } -static int mods_at_last_key_or_button_event = 0; - #ifndef __APPLE__ typedef struct modifier_key_state { bool left, right; @@ -526,8 +524,9 @@ key_callback(GLFWwindow *w, GLFWkeyevent *ev) { bool is_left; int key_modifier = key_to_modifier(ev->key, &is_left); if (key_modifier != -1) update_modifier_state_on_modifier_key_event(ev, key_modifier, is_left); -#endif - mods_at_last_key_or_button_event = ev->mods; + #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; @@ -546,10 +545,10 @@ cursor_enter_callback(GLFWwindow *w, int entered) { if (entered) { debug_input("Mouse cursor entered window: %llu at %fx%f\n", global_state.callback_os_window->id, x, y); cursor_active_callback(now); - if (is_window_ready_for_callbacks()) enter_event(mods_at_last_key_or_button_event); + if (is_window_ready_for_callbacks()) enter_event(global_state.mods_at_last_key_or_button_event); } else { debug_input("Mouse cursor left window: %llu\n", global_state.callback_os_window->id); - if (is_window_ready_for_callbacks()) leave_event(mods_at_last_key_or_button_event); + if (is_window_ready_for_callbacks()) leave_event(global_state.mods_at_last_key_or_button_event); } request_tick_callback(); global_state.callback_os_window = NULL; @@ -563,7 +562,8 @@ mouse_button_callback(GLFWwindow *w, int button, int action, int mods) { #endif monotonic_t now = monotonic(); cursor_active_callback(now); - mods_at_last_key_or_button_event = mods; + 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)) { @@ -591,7 +591,7 @@ on_mouse_position_update(double x, double y) { global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; global_state.callback_os_window->mouse_y = y * global_state.callback_os_window->viewport_y_ratio; global_state.callback_os_window->has_received_cursor_pos_event = true; - if (is_window_ready_for_callbacks()) mouse_event(-1, mods_at_last_key_or_button_event, -1); + if (is_window_ready_for_callbacks()) mouse_event(-1, global_state.mods_at_last_key_or_button_event, -1); request_tick_callback(); } diff --git a/kitty/options/definition.py b/kitty/options/definition.py index fe17ef422..13083266d 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -714,11 +714,15 @@ a double backslash. ''' ) -opt('show_hyperlink_targets', 'no', - option_type='to_bool', ctype='bool', +opt('show_hyperlink_targets', 'never', choices=('ctrl', 'cmd', 'shift', 'always', 'never'), + 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. +be activated when the hyperlink is clicked. Set to :code:`ctrl`, :code:`cmd` 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. +:code:`always` to always show, or :code:`never` to never show. ''') diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 2c541a988..502834ebd 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1290,7 +1290,16 @@ class Parser: ans['shell_integration'] = shell_integration(val) def show_hyperlink_targets(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['show_hyperlink_targets'] = to_bool(val) + 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')) 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-generated.h b/kitty/options/to-c-generated.h index 5a36c5415..0d875441b 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -605,7 +605,7 @@ convert_from_opts_url_excluded_characters(PyObject *py_opts, Options *opts) { static void convert_from_python_show_hyperlink_targets(PyObject *val, Options *opts) { - opts->show_hyperlink_targets = PyObject_IsTrue(val); + opts->show_hyperlink_targets = show_hyperlink_targets(val); } static void diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index ac24696f9..fc3751258 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -98,6 +98,17 @@ underline_hyperlinks(PyObject *x) { } } +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; +} + static inline 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 3502d2fce..8870cb49d 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -28,6 +28,7 @@ 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'] @@ -646,7 +647,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: bool = False + show_hyperlink_targets: choices_for_show_hyperlink_targets = '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 @@ -1149,4 +1150,4 @@ special_colors = frozenset({ }) -secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass') \ No newline at end of file +secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass') diff --git a/kitty/shaders.c b/kitty/shaders.c index 1e5d4c8fd..9d482ed65 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -854,20 +854,41 @@ render_a_bar(const UIRenderData *ui, WindowBarData *bar, PyObject *title, bool a return border_rect.height; } +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; + } +} + static bool has_hyperlink_target(OSWindow *os_window, Window *w, Screen *screen) { - return OPT(show_hyperlink_targets) && screen->current_hyperlink_under_mouse.id && w && !is_mouse_hidden(os_window) && global_state.mouse_hover_in_window == w->id; + return show_hyperlink_targets_with_modifiers(global_state.mouse_modifiers) && + screen->current_hyperlink_under_mouse.id && + w && !is_mouse_hidden(os_window) && + global_state.mouse_hover_in_window == w->id; } static void draw_hyperlink_target(const UIRenderData *ui) { if (!has_hyperlink_target(ui->os_window, ui->window, ui->screen)) return; Screen *screen = ui->screen; - const bool along_bottom = screen->current_hyperlink_under_mouse.y < 3; Window *window = ui->window; + const bool along_bottom = screen->current_hyperlink_under_mouse.y < 3; WindowBarData *bd = &window->url_target_bar_data; - if (bd->hyperlink_id_for_title_object != screen->current_hyperlink_under_mouse.id) { - bd->hyperlink_id_for_title_object = screen->current_hyperlink_under_mouse.id; + hyperlink_id_type hid = screen->current_hyperlink_under_mouse.id; + if (bd->hyperlink_id_for_title_object != hid) { + bd->hyperlink_id_for_title_object = hid; Py_CLEAR(bd->last_drawn_title_object_id); const char *url = get_hyperlink_for_id(screen->hyperlink_pool, bd->hyperlink_id_for_title_object, true); if (url == NULL) url = ""; diff --git a/kitty/state.h b/kitty/state.h index 553d55050..f03ba5893 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -31,6 +31,13 @@ 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; +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 +} ShowHyperlinkTargets; struct MenuItem { const char* *location; @@ -122,7 +129,7 @@ typedef struct Options { struct { float val; AdjustmentUnit unit; } underline_position, underline_thickness, strikethrough_position, strikethrough_thickness, cell_width, cell_height, baseline; - bool show_hyperlink_targets; + ShowHyperlinkTargets show_hyperlink_targets; UnderlineHyperlinks underline_hyperlinks; int background_blur; long macos_titlebar_color; @@ -398,6 +405,8 @@ 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; WindowLogoTable *all_window_logos;