diff --git a/docs/index.rst b/docs/index.rst index 26e0b362ed7..b7fb8713554 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,200 +53,6 @@ kitty * :doc:`graphics-protocol` * :doc:`keyboard-protocol` * Lots more in :doc:`protocol-extensions` -======= -:doc:`Panel ` - Draw a GPU accelerated dock panel on your desktop showing the output - from an arbitrary terminal program. - - -:doc:`Clipboard ` - Copy/paste to the clipboard from shell scripts, even over SSH. - -You can also :doc:`Learn to create your own kittens `. - - -Configuring kitty -------------------- - -|kitty| is highly configurable, everything from keyboard shortcuts to -painting frames-per-second. Press :sc:`edit_config_file` in kitty -to open its fully commented sample config file in your text editor. -For details see the :doc:`configuration docs `. - - -Remote control ------------------- - -|kitty| has a very powerful system that allows you to control it from the -:doc:`shell prompt, even over SSH `. You can change colors, -fonts, open new :term:`windows `, :term:`tabs `, set their titles, -change window layout, get text -from one window and send text to another, etc, etc. The possibilities are -endless. See the :doc:`tutorial ` to get started. - -.. _sessions: - -Startup Sessions ------------------- - -You can control the :term:`tabs `, `:term:`kitty window ` layout, -working directory, startup programs, -etc. by creating a "session" file and using the :option:`kitty --session` -command line flag or the :opt:`startup_session` option in :file:`kitty.conf`. -For example: - -.. code-block:: session - - # Set the layout for the current tab - layout tall - # Set the working directory for windows in the current tab - cd ~ - # Create a window and run the specified command in it - launch zsh - # Create a window with some environment variables set and run - # vim in it - launch --env FOO=BAR vim - # Set the title for the next window - launch --title "Chat with x" irssi --profile x - - # Create a new tab (the part after new_tab is the optional tab - # name which will be displayed in the tab bar, if omitted, the - # title of the active window will be used instead) - new_tab my tab - cd ~/somewhere - # Set the layouts allowed in this tab - enabled_layouts tall, stack - # Set the current layout - layout stack - launch zsh - - # Create a new OS window - new_os_window - # set new window size to 80x25 cells - os_window_size 80c 25c - # set the --class for the new OS window - os_window_class mywindow - launch sh - # Make the current window the active (focused) window - focus - launch emacs - -.. note:: - The :doc:`launch ` command when used in a session file - cannot create new OS windows, or tabs. - - -Mouse features -------------------- - -* You can click on a URL to open it in a browser. -* You can double click to select a word and then drag to select more words. -* You can triple click to select a line and then drag to select more lines. -* You can triple click while holding :kbd:`ctrl+alt` to select from clicked - point to end of line. -* You can right click to extend a previous selection. -* You can hold down :kbd:`ctrl+alt` and drag with the mouse to select in - columns. -* Selecting text automatically copies it to the primary clipboard (on - platforms with a primary clipboard). -* You can middle click to paste from the primary clipboard (on platforms - with a primary clipboard). -* You can select text with kitty even when a terminal program has grabbed - the mouse by holding down the :kbd:`shift` key. - -All these actions can be customized in :file:`kitty.conf` as described -:ref:`here `. - - -Font control ------------------ - -|kitty| has extremely flexible and powerful font selection features. You can -specify individual families for the regular, bold, italic and bold+italic -fonts. You can even specify specific font families for specific ranges of -unicode characters. This allows precise control over text rendering. It can -come in handy for applications like powerline, without the need to use patched -fonts. See the various font related configuration directives in -:ref:`conf-kitty-fonts`. - - -.. _scrollback: - -The scrollback buffer ------------------------ - -|kitty| supports scrolling back to view history, just like most terminals. You -can use either keyboard shortcuts or the mouse scroll wheel to do so. However, -|kitty| has an extra, neat feature. Sometimes you need to explore the -scrollback buffer in more detail, maybe search for some text or refer to it -side-by-side while typing in a follow-up command. |kitty| allows you to do this -by pressing the :sc:`show_scrollback` key-combination, which will open the -scrollback buffer in your favorite pager program (which is ``less`` by default). -Colors and text formatting are preserved. You can explore the scrollback buffer -comfortably within the pager. - -Additionally, you can pipe the contents of the scrollback buffer to an -arbitrary, command running in a new :term:`window`, :term:`tab` or :term:`overlay`, -for example:: - - map f1 launch --stdin-source=@screen_scrollback --stdin-add-formatting less +G -R - -Would open the scrollback buffer in a new :term:`window` when you press the :kbd:`F1` -key. See :sc:`show_scrollback` for details. - -If you want to use it with an editor such as vim to get more powerful features, -you can see tips for doing so, in -`this thread `_. - -If you wish to store very large amounts of scrollback to view using the piping or -:sc:`show_scrollback` features, you can use the :opt:`scrollback_pager_history_size` -option. - -You can also view the output of the last command to run in the shell, by -pressing :sc:`show_last_command_output`. See :ref:`shell_integration` for -details. - -.. _cpbuf: - -Multiple copy/paste buffers ------------------------------ - -In addition to being able to copy/paste from the system clipboard, in |kitty| you -can also setup an arbitrary number of copy paste buffers. To do so, simply add -something like the following to your :file:`kitty.conf`:: - - map f1 copy_to_buffer a - map f2 paste_from_buffer a - -This will allow you to press :kbd:`F1` to copy the current selection to an -internal buffer named ``a`` and :kbd:`F2` to paste from that buffer. The buffer -names are arbitrary strings, so you can define as many such buffers as you -need. - -Marks -------------- - -kitty has the ability to mark text on the screen based on regular expressions. -This can be useful to highlight words or phrases when browsing output from long -running programs or similar. To learn how this feature works, see :doc:`marks`. - - -Frequently Asked Questions ---------------------------------- - -The list of Frequently Asked Questions (*FAQ*) is :doc:`available here `. - - -Cool integrations for kitty with other CLI tools --------------------------------------------------- - -kitty provides extremely powerful interfaces such as :doc:`remote-control` and -:doc:`kittens/custom` and :doc:`kittens/icat` -that allow it to be integrated with other tools seamlessly. For a list of such -user created integrations, see: :doc:`integrations`. - - ->>>>>>> 1d167ada (Move shell integration docs into own file) .. figure:: screenshots/screenshot.png diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index da967e6f918..b48d125708d 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1190,6 +1190,10 @@ def click_mouse_url(os_window_id: int, tab_id: int, window_id: int) -> bool: pass +def move_cursor_to_mouse_if_in_prompt(os_window_id: int, tab_id: int, window_id: int) -> bool: + pass + + def mouse_selection(os_window_id: int, tab_id: int, window_id: int, code: int, button: int) -> None: pass diff --git a/kitty/mouse.c b/kitty/mouse.c index d35a3b3c35e..0d08940c8cf 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -410,6 +410,15 @@ mouse_open_url(Window *w) { return screen_open_url(screen); } +bool +move_cursor_to_mouse_if_at_shell_prompt(Window *w) { + Screen *screen = w->render_data.screen; + int y = screen_cursor_at_a_shell_prompt(screen); + if (y < 0 || (unsigned)y > w->mouse_pos.cell_y) return false; + return screen_fake_move_cursor_to_position(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y); +} + + typedef struct PendingClick { id_type window_id; int button, count, modifiers; diff --git a/kitty/options/definition.py b/kitty/options/definition.py index d17d8f2f526..5d07862aeac 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -507,16 +507,26 @@ ''' ) -mma('Click the link under the mouse cursor when no selection is created', - 'click_url_or_select left click ungrabbed mouse_click_url_or_select', +mma('Click the link under the mouse or move the cursor', + 'click_url_or_select left click ungrabbed mouse_handle_click selection link prompt', + long_text=''' +First check for a selection and if one exists do nothing. Then check for a link +under the mouse cursor and if one exists, click it. Finally check if the click happened at +the current shell prompt and if so, move the cursor to the click location. Note that this +requires :doc:`shell-integration` to work. +''' ) -mma('Click the link under the mouse cursor when no selection is created even if grabbed', - 'click_url_or_select_grabbed shift+left click grabbed,ungrabbed mouse_click_url_or_select', +mma('Click the link under the mouse or move the cursor even when grabbed', + 'click_url_or_select_grabbed shift+left click grabbed,ungrabbed mouse_handle_click selection link prompt', + long_text=''' +Same as above, except that the action is performed even when the mouse is grabbed +by the program running in the terminal. +''' ) mma('Click the link under the mouse cursor', - 'click_url ctrl+shift+left release grabbed,ungrabbed mouse_click_url', + 'click_url ctrl+shift+left release grabbed,ungrabbed mouse_handle_click link', long_text='Variant with :kbd:`ctrl+shift` is present because the simple' ' click based version has an unavoidable delay of :opt:`click_interval`, to disambiguate clicks from double clicks.' ) diff --git a/kitty/options/types.py b/kitty/options/types.py index 1df8c9ff33c..32bf1c41aaa 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -870,15 +870,15 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any: defaults.map.append(KeyDefinition(False, KeyAction('debug_config'), 10, False, 44, ())) defaults.mouse_map = [ # click_url_or_select - MouseMapping(0, 0, -2, False, KeyAction('mouse_click_url_or_select')), + MouseMapping(0, 0, -2, False, KeyAction('mouse_handle_click', ('selection', 'link', 'prompt'))), # click_url_or_select_grabbed - MouseMapping(0, 1, -2, True, KeyAction('mouse_click_url_or_select')), + MouseMapping(0, 1, -2, True, KeyAction('mouse_handle_click', ('selection', 'link', 'prompt'))), # click_url_or_select_grabbed - MouseMapping(0, 1, -2, False, KeyAction('mouse_click_url_or_select')), + MouseMapping(0, 1, -2, False, KeyAction('mouse_handle_click', ('selection', 'link', 'prompt'))), # click_url - MouseMapping(0, 5, -1, True, KeyAction('mouse_click_url')), + MouseMapping(0, 5, -1, True, KeyAction('mouse_handle_click', ('link',))), # click_url - MouseMapping(0, 5, -1, False, KeyAction('mouse_click_url')), + MouseMapping(0, 5, -1, False, KeyAction('mouse_handle_click', ('link',))), # click_url_discard MouseMapping(0, 5, 1, True, KeyAction('discard_event')), # paste_selection diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 1cc9e79f22f..6e7f0b64d61 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -52,7 +52,7 @@ class InvalidMods(ValueError): @func_with_args( 'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window', 'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd', - 'launch' + 'launch', 'mouse_handle_click' ) def shlex_parse(func: str, rest: str) -> FuncArgsType: return func, to_cmdline(rest) diff --git a/kitty/screen.c b/kitty/screen.c index 3a8727651e8..efadf9426ff 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -25,6 +25,7 @@ #include "wcwidth-std.h" #include "control-codes.h" #include "charsets.h" +#include "keys.h" static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true}; @@ -1344,6 +1345,45 @@ screen_cursor_to_line(Screen *self, unsigned int line) { screen_cursor_position(self, line, self->cursor->x + 1); } +int +screen_cursor_at_a_shell_prompt(const Screen *self) { + if (self->cursor->y >= self->lines || self->linebuf != self->main_linebuf) return false; + for (index_type y=self->cursor->y + 1; y-- > 0; ) { + linebuf_init_line(self->linebuf, y); + if (self->linebuf->line->is_output_start) return -1; + if (self->linebuf->line->is_prompt_start) return y; + } + return -1; +} + +bool +screen_fake_move_cursor_to_position(Screen *self, index_type x, index_type y) { + SelectionBoundary a = {.x=x, .y=y}, b = {.x=self->cursor->x, .y=self->cursor->y}; + SelectionBoundary *start, *end; int key; + if (a.y < b.y || (a.y == b.y && a.x < b.x)) { start = &a; end = &b; key = GLFW_FKEY_LEFT; } + else { start = &b; end = &a; key = GLFW_FKEY_RIGHT; } + unsigned count = 0; + + for (unsigned y = start->y, x = start->x; y <= end->y; y++) { + unsigned x_limit = y == end->y ? end->x : self->columns; + while (x < x_limit) { + unsigned w = MAX(1u, linebuf_char_width_at(self->linebuf, x, y)); + x += w; + count += w; + } + x = 0; + } + if (count) { + GLFWkeyevent ev = { .key = key, .action = GLFW_PRESS }; + char output[KEY_BUFFER_SIZE+1] = {0}; + int num = encode_glfw_key_event(&ev, false, 0, output); + if (num != SEND_TEXT_TO_CHILD) { + for (unsigned i = 0; i < count; i++) write_to_child(self, output, num); + } + } + return count > 0; +} + // }}} // Editing {{{ @@ -1921,7 +1961,6 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat if (was_dirty) clear_selection(&self->url_ranges); } - static bool selection_boundary_less_than(const SelectionBoundary *a, const SelectionBoundary *b) { // y -values must be absolutized (aka adjusted with scrolled_by) diff --git a/kitty/screen.h b/kitty/screen.h index 796c454d068..765f6cca057 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -237,6 +237,8 @@ uint8_t screen_current_key_encoding_flags(Screen *self); void screen_report_key_encoding_flags(Screen *self); void screen_xtmodkeys(Screen *self, uint32_t p1, uint32_t p2); bool screen_detect_url(Screen *screen, unsigned int x, unsigned int y); +int screen_cursor_at_a_shell_prompt(const Screen *); +bool screen_fake_move_cursor_to_position(Screen *, index_type x, index_type y); #define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen); DECLARE_CH_SCREEN_HANDLER(bell) DECLARE_CH_SCREEN_HANDLER(backspace) diff --git a/kitty/state.c b/kitty/state.c index fb1c71816dc..5ad47fd0b5e 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -1029,6 +1029,16 @@ click_mouse_url(id_type os_window_id, id_type tab_id, id_type window_id) { return clicked; } +static bool +move_cursor_to_mouse_if_in_prompt(id_type os_window_id, id_type tab_id, id_type window_id) { + bool moved = false; + WITH_WINDOW(os_window_id, tab_id, window_id); + moved = move_cursor_to_mouse_if_at_shell_prompt(window); + END_WITH_WINDOW; + return moved; +} + + static PyObject* pymouse_selection(PyObject *self UNUSED, PyObject *args) { id_type os_window_id, tab_id, window_id; @@ -1050,6 +1060,7 @@ THREE_ID_OBJ(update_window_title) THREE_ID(remove_window) THREE_ID(detach_window) THREE_ID(attach_window) +THREE_ID(move_cursor_to_mouse_if_in_prompt) PYWRAP1(resolve_key_mods) { int mods, kitty_mod; PA("ii", &kitty_mod, &mods); return PyLong_FromLong(resolve_mods(kitty_mod, mods)); } PYWRAP1(add_tab) { return PyLong_FromUnsignedLongLong(add_tab(PyLong_AsUnsignedLongLong(args))); } PYWRAP1(add_window) { PyObject *title; id_type a, b; PA("KKO", &a, &b, &title); return PyLong_FromUnsignedLongLong(add_window(a, b, title)); } @@ -1069,6 +1080,7 @@ static PyMethodDef module_methods[] = { MW(set_options, METH_VARARGS), MW(get_options, METH_NOARGS), MW(click_mouse_url, METH_VARARGS), + MW(move_cursor_to_mouse_if_in_prompt, METH_VARARGS), MW(mouse_selection, METH_VARARGS), MW(set_in_sequence_mode, METH_O), MW(resolve_key_mods, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index 0e2c0ad707d..5573af99afe 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -303,6 +303,7 @@ void update_os_window_title(OSWindow *os_window); void fake_scroll(Window *w, int amount, bool upwards); Window* window_for_window_id(id_type kitty_window_id); bool mouse_open_url(Window *w); +bool move_cursor_to_mouse_if_at_shell_prompt(Window *w); void mouse_selection(Window *w, int code, int button); const char* format_mods(unsigned mods); void send_pending_click_to_window_id(id_type, void*); diff --git a/kitty/window.py b/kitty/window.py index acd1a4a040c..640a88ac8ac 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -29,9 +29,10 @@ SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, KeyEvent, Screen, add_timer, add_window, cell_size_for_window, click_mouse_url, compile_program, encode_key_for_tty, get_boss, get_clipboard_string, get_options, - init_cell_program, mouse_selection, pt_to_px, set_clipboard_string, - set_titlebar_color, set_window_padding, set_window_render_data, - update_window_title, update_window_visibility, viewport_for_window + init_cell_program, mouse_selection, move_cursor_to_mouse_if_in_prompt, + pt_to_px, set_clipboard_string, set_titlebar_color, set_window_padding, + set_window_render_data, update_window_title, update_window_visibility, + viewport_for_window ) from .keys import keyboard_mode_name, mod_mask from .notify import NotificationCommand, handle_notification_cmd @@ -777,6 +778,7 @@ def handle_remote_cmd(self, cmd: str) -> None: def handle_remote_print(self, msg: bytes) -> None: from base64 import standard_b64decode + from .cli import green text = standard_b64decode(msg).decode('utf-8') text = text.replace('\x1b', green(r'\e')).replace('\a', green(r'\a')).replace('\0', green(r'\0')) @@ -850,15 +852,37 @@ def manipulate_title_stack(self, pop: bool, title: str, icon: Any) -> None: # }}} # mouse actions {{{ + @ac('mouse', ''' + Handle a mouse click + + Try to perform the specified actions one after the other till one of them is successful. + Supported actions are:: + + selection - check for a selection and if one exists abort processing + link - if a link exists under the mouse, click it + prompt - if the mouse click happens at a shell prompt move the cursor to the mouse location + + For examples, see :ref:`conf-kitty-mouse.mousemap` + ''') + def mouse_handle_click(self, *actions: str) -> None: + for a in actions: + if a == 'selection': + if self.screen.has_selection(): + break + if a == 'link': + if click_mouse_url(self.os_window_id, self.tab_id, self.id): + break + if a == 'prompt': + if move_cursor_to_mouse_if_in_prompt(self.os_window_id, self.tab_id, self.id): + break + @ac('mouse', 'Click the URL under the mouse') def mouse_click_url(self) -> None: - click_mouse_url(self.os_window_id, self.tab_id, self.id) + self.mouse_handle_click('link') @ac('mouse', 'Click the URL under the mouse only if the screen has no selection') def mouse_click_url_or_select(self) -> None: - if not self.screen.has_selection(): - if not click_mouse_url(self.os_window_id, self.tab_id, self.id): - pass # no URL found + self.mouse_handle_click('selection', 'link') @ac('mouse', ''' Manipulate the selection based on the current mouse position