Add modifier-based hyperlink target display

This commit is contained in:
zhaolei 2026-03-25 11:06:00 +08:00
parent 2ce9d575ef
commit 47d89faa23
8 changed files with 75 additions and 20 deletions

View file

@ -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();
}

View file

@ -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.
''')

11
kitty/options/parse.py generated
View file

@ -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)

View file

@ -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

View file

@ -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);

View file

@ -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')
secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass')

View file

@ -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 = "";

View file

@ -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;