diff --git a/docs/changelog.rst b/docs/changelog.rst index da16da124..1b930e61a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -183,6 +183,8 @@ Detailed list of changes - :doc:`Remote control `: Expose :code:`session_name` and :code:`last_focused_at` in the output of ``kitten @ ls`` for each window (:iss:`9732`, :iss:`9799`) +- Allow optionally dragging URLs with the mouse, see :sc:`start_simple_selection` (:pull:`9804`) + - Fix thickness of diagonal lines in box drawing characters not the same as horizontal/vertical lines (:iss:`9719`) - Graphics protocol: Fix crash when handling invalid PNG image with direct transmission diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index ac366b83a..73d6feb66 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1260,6 +1260,7 @@ class Screen: def hyperlink_for_id(self, hyperlink_id: int) -> str: ... def erase_last_command(self, include_prompt: bool = True) -> bool: ... def set_progress(self, state: int, percent: int) -> None: ... + def mark_potential_url_drag(self) -> bool: ... def cursor_at_prompt(self) -> bool: pass diff --git a/kitty/mouse.c b/kitty/mouse.c index 7805f9dfd..c473f6a3e 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -413,9 +413,10 @@ handle_mouse_movement_in_kitty(Window *w, int button, bool mouse_cell_changed) { static void detect_url(Screen *screen, unsigned int x, unsigned int y) { int hid = screen_detect_url(screen, x, y); - screen->current_hyperlink_under_mouse.id = 0; + zero_at_ptr(&screen->current_hyperlink_under_mouse); if (hid != 0) { mouse_cursor_shape = POINTER_POINTER; + screen->current_hyperlink_under_mouse.has_detected_url = hid < 0; if (hid > 0) { screen->current_hyperlink_under_mouse.id = (hyperlink_id_type)hid; screen->current_hyperlink_under_mouse.x = x; @@ -672,16 +673,26 @@ HANDLER(handle_move_event) { if (should_handle_in_kitty(w, screen, button)) { handle_mouse_movement_in_kitty(w, button, mouse_cell_changed | cell_half_changed); } else { - if (!mouse_cell_changed && screen->modes.mouse_tracking_protocol != SGR_PIXEL_PROTOCOL) return; - int sz = encode_mouse_button(w, button, button >=0 ? DRAG : MOVE, modifiers); - if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf); } + if (mouse_cell_changed || screen->modes.mouse_tracking_protocol == SGR_PIXEL_PROTOCOL) { + int sz = encode_mouse_button(w, button, button >=0 ? DRAG : MOVE, modifiers); + if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf); } + } } - if (w->drag_source.can_offer && w->drag_source.initial_left_press.at && distance(w->mouse_pos.global_x, w->mouse_pos.global_y, w->drag_source.initial_left_press.x, w->drag_source.initial_left_press.y) > OPT(drag_threshold)) { + if (w->drag_source.initial_left_press.at && distance(w->mouse_pos.global_x, w->mouse_pos.global_y, w->drag_source.initial_left_press.x, w->drag_source.initial_left_press.y) > OPT(drag_threshold)) { zero_at_ptr(&w->drag_source.initial_left_press); - snprintf(mouse_event_buf, sizeof(mouse_event_buf), "%d;t=o:x=%d:y=%d:X=%d:Y=%d", - DND_CODE, w->mouse_pos.cell_x, w->mouse_pos.cell_y, (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y); - write_escape_code_to_child(screen, ESC_OSC, mouse_event_buf); - debug("Sent drag start event to child\n"); + if (w->drag_source.can_offer) { + snprintf(mouse_event_buf, sizeof(mouse_event_buf), "%d;t=o:x=%d:y=%d:X=%d:Y=%d", + DND_CODE, w->mouse_pos.cell_x, w->mouse_pos.cell_y, (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y); + write_escape_code_to_child(screen, ESC_OSC, mouse_event_buf); + debug("Sent drag start event to child\n"); + } else if (w->drag_source.potential_url_drag.active) { + w->drag_source.potential_url_drag.active = false; + screen_detect_url(screen, w->drag_source.potential_url_drag.x, w->drag_source.potential_url_drag.y); + if (screen->current_hyperlink_under_mouse.id || screen->current_hyperlink_under_mouse.has_detected_url) { + screen_open_url(screen, "drag_url"); + debug("Started URL drag\n"); + } + } } } @@ -749,7 +760,7 @@ bool mouse_open_url(Window *w) { Screen *screen = w->render_data.screen; detect_url(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y); - return screen_open_url(screen); + return screen_open_url(screen, "open_url"); } bool @@ -847,6 +858,7 @@ HANDLER(handle_button_event) { modifiers &= ~GLFW_LOCK_MASK; OSWindow *osw = global_state.callback_os_window; if (!osw) return; + w->drag_source.potential_url_drag.active = false; Tab *t = osw->tabs + osw->active_tab; bool is_release = !osw->mouse_button_pressed[button]; @@ -879,7 +891,7 @@ HANDLER(handle_button_event) { if (button == GLFW_MOUSE_BUTTON_LEFT) { if (is_release) { zero_at_ptr(&w->drag_source.initial_left_press); - } else if (w->drag_source.can_offer) { + } else { w->drag_source.initial_left_press.x = w->mouse_pos.global_x; w->drag_source.initial_left_press.y = w->mouse_pos.global_x; w->drag_source.initial_left_press.at = monotonic(); diff --git a/kitty/options/definition.py b/kitty/options/definition.py index deef73fae..e45612808 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -988,6 +988,12 @@ mma('Paste from the primary selection', mma('Start selecting text', 'start_simple_selection left press ungrabbed mouse_selection normal', + long_text=''' +If you would like to drag and drop hyperlinks or detected URLs, instead of +:code:`normal` use :code:`drag_or_normal_select`, then if a hyperlink is under +the mouse it will be dragged based on :opt:`drag_threshold` otherwise a normal +selection will be performed. +''' ) mma('Start selecting text in a rectangle', diff --git a/kitty/options/utils.py b/kitty/options/utils.py index fd686967a..e72b5259b 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -446,6 +446,7 @@ def mouse_selection(func: str, rest: str) -> FuncArgsType: 'normal': defines.MOUSE_SELECTION_NORMAL, 'extend': defines.MOUSE_SELECTION_EXTEND, 'move-end': defines.MOUSE_SELECTION_MOVE_END, + 'move_end': defines.MOUSE_SELECTION_MOVE_END, 'rectangle': defines.MOUSE_SELECTION_RECTANGLE, 'word': defines.MOUSE_SELECTION_WORD, 'line': defines.MOUSE_SELECTION_LINE, @@ -453,6 +454,7 @@ def mouse_selection(func: str, rest: str) -> FuncArgsType: 'line_from_point': defines.MOUSE_SELECTION_LINE_FROM_POINT, 'word_and_line_from_point': defines.MOUSE_SELECTION_WORD_AND_LINE_FROM_POINT, 'upto_surrounding_whitespace': defines.MOUSE_SELECTION_UPTO_SURROUNDING_WHITESPACE, + 'drag_or_normal_select': defines.MOUSE_SELECTION_NORMAL - 1, } setattr(mouse_selection, 'code_map', cmap) return func, [cmap[rest]] diff --git a/kitty/screen.c b/kitty/screen.c index 73bcf3a68..bedeaf6c4 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1512,6 +1512,16 @@ screen_dirty_line_graphics(Screen *self, const unsigned int top, const unsigned grman_remove_cell_images(main_buf ? self->main_grman : self->alt_grman, top, bottom); } +static bool +screen_mark_potential_url_drag(Screen *self) { + Window *w; + if ((!self->current_hyperlink_under_mouse.id && !self->current_hyperlink_under_mouse.has_detected_url) || !self->window_id || !(w = window_for_window_id(self->window_id))) return false; + w->drag_source.potential_url_drag.active = true; + w->drag_source.potential_url_drag.x = w->mouse_pos.cell_x; + w->drag_source.potential_url_drag.y = w->mouse_pos.cell_y; + return true; +} + void screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *payload) { if (!self->window_id) return; @@ -4084,13 +4094,13 @@ current_url_text(Screen *self, PyObject *args UNUSED) { bool -screen_open_url(Screen *self) { +screen_open_url(Screen *self, const char *callback) { if (!self->url_ranges.count) return false; hyperlink_id_type hid = hyperlink_id_for_range(self, self->url_ranges.items); if (hid) { const char *url = get_hyperlink_for_id(self->hyperlink_pool, hid, true); if (url) { - CALLBACK("open_url", "sH", url, hid); + CALLBACK(callback, "sH", url, hid); return true; } } @@ -4101,7 +4111,7 @@ screen_open_url(Screen *self) { } bool found = false; if (PyUnicode_Check(text)) { - CALLBACK("open_url", "OH", text, 0); + CALLBACK(callback, "OH", text, 0); found = true; } Py_CLEAR(text); @@ -5948,6 +5958,12 @@ current_selections(Screen *self, PyObject *a UNUSED) { WRAP0(update_only_line_graphics_data) WRAP0(bell) +static PyObject* +mark_potential_url_drag(Screen *self, PyObject *a UNUSED) { + if (screen_mark_potential_url_drag(self)) Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + #define MND(name, args) {#name, (PyCFunction)name, args, #name}, #define MODEFUNC(name) MND(name, METH_NOARGS) MND(set_##name, METH_O) @@ -6136,6 +6152,7 @@ static PyMethodDef methods[] = { MND(scroll_to_next_mark, METH_VARARGS) MND(update_only_line_graphics_data, METH_NOARGS) MND(bell, METH_NOARGS) + MND(mark_potential_url_drag, METH_NOARGS) MND(current_selections, METH_NOARGS) {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, diff --git a/kitty/screen.h b/kitty/screen.h index 139ac025c..d2554b5a7 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -186,6 +186,7 @@ typedef struct { struct { hyperlink_id_type id; index_type x, y; + bool has_detected_url; } current_hyperlink_under_mouse; struct { uint8_t stack[16], count; @@ -305,7 +306,7 @@ hyperlink_id_type screen_mark_hyperlink(Screen*, index_type, index_type); void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload); void screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload); void screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *payload); -bool screen_open_url(Screen*); +bool screen_open_url(Screen* self, const char* callback); bool screen_set_last_visited_prompt(Screen*, index_type); bool screen_select_cmd_output(Screen*, index_type); void screen_dirty_sprite_positions(Screen *self); diff --git a/kitty/state.h b/kitty/state.h index f09c47d14..fb0a513d0 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -290,6 +290,7 @@ typedef struct Window { } drop; struct { bool can_offer; + struct { index_type x, y; bool active; } potential_url_drag; struct { double x, y; monotonic_t at; } initial_left_press; char *mimes_buf; size_t num_mimes, bufsz; struct { diff --git a/kitty/window.py b/kitty/window.py index f53dac807..0f08e0cd3 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -48,6 +48,7 @@ from .fast_data_types import ( GLFW_PRESS, GLFW_RELEASE, GLFW_REPEAT, + MOUSE_SELECTION_NORMAL, NO_CURSOR_SHAPE, NULL_COLOR_VALUE, SCROLL_FULL, @@ -65,6 +66,7 @@ from .fast_data_types import ( click_mouse_cmd_output, click_mouse_url, current_focused_os_window_id, + draw_single_line_of_text, encode_key_for_tty, get_boss, get_click_interval, @@ -88,6 +90,7 @@ from .fast_data_types import ( set_window_padding, set_window_render_data, set_window_title_bar_render_data, + start_drag_with_data, update_ime_position_for_window, update_pointer_shape, update_window_title, @@ -1309,6 +1312,24 @@ class Window: return False return get_boss().combine(action, window_for_dispatch=self, dispatch_type='MouseEvent') + def drag_url(self, url: str, hyperlink_id: int) -> None: + if not url: + return + if url.startswith('/'): + from urllib.parse import quote + url = 'file://' + quote(os.path.abspath(url)) + fg = color_as_int(self.screen.color_profile.default_fg) + bg = color_as_int(self.screen.color_profile.default_bg) + width = self.geometry.right - self.geometry.left + pixels = draw_single_line_of_text(self.os_window_id, url, 0xff000000 | fg, 0xff000000 | bg, width) + height = len(pixels) // (width * 4) + thumbnails = ((pixels, width, height),) + drag_data = {'text/uri-list': (url + '\r\n').encode()} + try: + start_drag_with_data(self.os_window_id, drag_data, thumbnails) + except OSError as e: + log_error(f'Failed to start URL drag: {e}') + def open_url(self, url: str, hyperlink_id: int, cwd: str | None = None) -> None: boss = get_boss() try: @@ -1841,6 +1862,10 @@ class Window: For examples, see :ref:`conf-kitty-mouse.mousemap` ''') def mouse_selection(self, code: int) -> None: + if code == MOUSE_SELECTION_NORMAL - 1: + code = MOUSE_SELECTION_NORMAL + if self.screen.mark_potential_url_drag(): + return mouse_selection(self.os_window_id, self.tab_id, self.id, code, self.current_mouse_event_button) @ac('mouse', 'Paste the current primary selection')