diff --git a/docs/changelog.rst b/docs/changelog.rst index 87a83456c..0a57fe8b6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,7 +13,7 @@ Changelog - Modify the kittens sub-system to allow creating custom kittens without any user interface. This is useful for creating more complex actions that can be bound to key presses in :file:`kitty.conf`. See - `https://sw.kovidgoyal.net/kitty/kittens/custom.html`_. (:iss:`870`) + doc:`kittens/custom`. (:iss:`870`) - Add a new ``nth_window`` action that can be used to go to the nth window and also previously active windows, using negative numbers. Similarly, @@ -443,7 +443,7 @@ Changelog - Add a config option (:opt:`editor`) to set the EDITOR kitty uses (:iss:`580`) -- Add a config option (:opt:`x11_hide_window_decorations`) to hide window +- Add a config option (``x11_hide_window_decorations``) to hide window decorations under X11/Wayland (:iss:`607`) - Add an option to @set-window-title to make the title change non-permanent diff --git a/docs/pipe.rst b/docs/pipe.rst new file mode 100644 index 000000000..4b3d68390 --- /dev/null +++ b/docs/pipe.rst @@ -0,0 +1,88 @@ +Working with the screen and history buffer contents +====================================================== + + +You can pipe the contents of the current screen and history buffer as +:file:`STDIN` to an arbitrary program using the ``pipe`` function. The program +can be displayed in a kitty window or overlay. + +For example, the following in :file:`kitty.conf` will open the scrollback +buffer in less in an overlay window, when you press :kbd:`F1`:: + + map f1 pipe @ansi overlay less +G -R + +The syntax of the ``pipe`` function is:: + + pipe + + +The piping environment +-------------------------- + +The program to which the data is piped has a special environment variable +declared, ``KITTY_PIPE_DATA`` whose contents are:: + + KITTY_PIPE_DATA={scrolled_by}:{cursor_x},{cursor_y}:{lines},{columns} + +where ``scrolled_by`` is the number of lines kitty is currently scrolled by, +``cursor_(x|y)`` is the position of the cursor on the screen with ``(1,1)`` +being the top left corner and ``{lines},{columns}`` being the number of rows +and columns of the screen. + +You can choose where to run the pipe program: + +``overlay`` + An overlay window over the current kitty window + +``window`` + A new kitty window + +``os_window`` + A new top-level window + +``tab`` + A new window in a new tab + +``none`` + Run it in the background + + +Input placeholders +-------------------- + +There are various different kinds of placeholders + +``@text`` + Plain text, current screen + scrollback buffer + +``@ansi`` + Text with formatting, current screen + scrollback buffer + +``@screen`` + Plain text, only current screen + +``@ansi_screen`` + Text with formatting, only current screen + +``@alternate`` + Plain text, secondary screen. The secondary screen is the screen not currently displayed. For + example if you run a fullscreen terminal application, the secondary screen will + be the screen you return to when quitting the application. + +``@ansi_alternate`` + Text with formatting, secondary screen. + +``@alternate_scrollback`` + Plain text, secondary screen + scrollback, if any. + +``@ansi_alternate_scrollback`` + Text with formatting, secondary screen + scrollback, if any. + +``none`` + No input + + +You can also add the suffix ``_wrap`` to the placeholder, in which case kitty +will insert the carriage return at every line wrap location (where long lines +are wrapped at screen edges). This is useful if you want to pipe to program +that wants to duplicate the screen layout of the screen. diff --git a/kitty/boss.py b/kitty/boss.py index 068fbc87e..c53fdc7a6 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -48,25 +48,29 @@ def listen_on(spec): return s.fileno() -def data_for_at(w, arg): +def data_for_at(w, arg, add_wrap_markers=False): + def as_text(**kw): + kw['add_wrap_markers'] = add_wrap_markers + return w.as_text(**kw) + if arg == '@selection': return w.text_for_selection() if arg == '@ansi': - return w.as_text(as_ansi=True, add_history=True) + return as_text(as_ansi=True, add_history=True) if arg == '@text': - return w.as_text(add_history=True) + return as_text(add_history=True) if arg == '@screen': - return w.as_text() + return as_text() if arg == '@ansi_screen': - return w.as_text(as_ansi=True) + return as_text(as_ansi=True) if arg == '@alternate': - return w.as_text(alternate_screen=True) + return as_text(alternate_screen=True) if arg == '@alternate_scrollback': - return w.as_text(alternate_screen=True, add_history=True) + return as_text(alternate_screen=True, add_history=True) if arg == '@ansi_alternate': - return w.as_text(as_ansi=True, alternate_screen=True) + return as_text(as_ansi=True, alternate_screen=True) if arg == '@ansi_alternate_scrollback': - return w.as_text(as_ansi=True, alternate_screen=True, add_history=True) + return as_text(as_ansi=True, alternate_screen=True, add_history=True) class DumpCommands: # {{{ @@ -816,9 +820,19 @@ class Boss: def special_window_for_cmd(self, cmd, window=None, stdin=None, cwd_from=None, as_overlay=False): w = window or self.active_window + env = None if stdin: - stdin = data_for_at(w, stdin) + add_wrap_markers = stdin.endswith('_wrap') + if add_wrap_markers: + stdin = stdin[:-len('_wrap')] + stdin = data_for_at(w, stdin, add_wrap_markers=add_wrap_markers) if stdin is not None: + pipe_data = w.pipe_data(stdin, has_wrap_markers=add_wrap_markers) if w else {} + if pipe_data: + env = { + 'KITTY_PIPE_DATA': + '{scrolled_by}:{cursor_x},{cursor_y}:{lines},{columns}'.format(**pipe_data) + } stdin = stdin.encode('utf-8') cmdline = [] for arg in cmd: @@ -828,7 +842,7 @@ class Boss: continue cmdline.append(arg) overlay_for = w.id if as_overlay and w.overlay_for is None else None - return SpecialWindow(cmd, stdin, cwd_from=cwd_from, overlay_for=overlay_for) + return SpecialWindow(cmd, stdin, cwd_from=cwd_from, overlay_for=overlay_for, env=env) def pipe(self, source, dest, exe, *args): cmd = [exe] + list(args) diff --git a/kitty/config_data.py b/kitty/config_data.py index 74fae825f..941feaeb6 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -783,7 +783,7 @@ an arbitrary color, such as :code:`#12af59` or :code:`red`. WARNING: This option using a hack, as there is no proper Cocoa API for it. It sets the background color of the entire window and makes the titlebar transparent. As such it is incompatible with :opt:`background_opacity`. If you want to use both, you are -probably better off just hiding the titlebar with :opt:`macos_hide_titlebar`. +probably better off just hiding the titlebar with :opt:`hide_window_decorations`. ''')) o('macos_option_as_alt', True, long_text=_(''' @@ -877,19 +877,9 @@ the following opens the scrollback buffer in less in an overlay window:: map f1 pipe @ansi overlay less +G -R -Placeholders available are: ``@text`` (which is plain text) and ``@ansi`` (which -includes text styling escape codes). For only the current screen, use ``@screen`` -or ``@ansi_screen``. For the secondary screen, use ``@alternate`` and ``@ansi_alternate``. -The secondary screen is the screen not currently displayed. For -example if you run a fullscreen terminal application, the secondary screen will -be the screen you return to when quitting the application. If you want access to the -secondary screen scrollback, use ``@alternate_scrollback``. You can also use -``none`` for no :file:`STDIN` input. - -To open in a new window, tab or new OS window, use ``window``, ``tab``, or -``os_window`` respectively. You can also use ``none`` in which case the data -will be piped into the program without creating any windows, useful if the -program is a GUI program that creates its own windows. ''')) +For more details on piping screen and buffer contents to external programs, +see :doc:`pipe`. +''')) # }}} diff --git a/kitty/window.py b/kitty/window.py index c1e0f988d..cda1610be 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -483,15 +483,26 @@ class Window: if pid is not None: return cwd_of_process(pid) or None + def pipe_data(self, text, has_wrap_markers=False): + text = text or '' + if has_wrap_markers: + text = text.replace('\r\n', '\n').replace('\r', '\n') + lines = text.count('\n') + input_line_number = (lines - (self.screen.lines - 1) - self.screen.scrolled_by) + return { + 'input_line_number': input_line_number, 'scrolled_by': self.screen.scrolled_by, + 'cursor_x': self.screen.cursor.x + 1, 'cursor_y': self.screen.cursor.y + 1, + 'lines': self.screen.lines, 'columns': self.screen.columns, + 'text': text + } + # actions {{{ def show_scrollback(self): - data = self.as_text(as_ansi=True, add_history=True, add_wrap_markers=True) - data = data.replace('\r\n', '\n').replace('\r', '\n') - lines = data.count('\n') - input_line_number = (lines - (self.screen.lines - 1) - self.screen.scrolled_by) - cmd = [x.replace('INPUT_LINE_NUMBER', str(input_line_number)) for x in self.opts.scrollback_pager] - get_boss().display_scrollback(self, data, cmd) + text = self.as_text(as_ansi=True, add_history=True, add_wrap_markers=True) + data = self.pipe_data(text, has_wrap_markers=True) + cmd = [x.replace('INPUT_LINE_NUMBER', str(data['input_line_number'])) for x in self.opts.scrollback_pager] + get_boss().display_scrollback(self, data['text'], cmd) def paste(self, text): if text and not self.destroyed: