Allow adding line wrap markers when piping screen contents

Also make some data such as scroll position, cursor position and screen
geometry available via an env var when piping. See #719
This commit is contained in:
Kovid Goyal 2018-11-26 13:30:03 +05:30
parent ae8ca5272b
commit a7a7216b30
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 136 additions and 33 deletions

View File

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

88
docs/pipe.rst Normal file
View File

@ -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 <input placeholder> <destination window type> <command line to run>
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.

View File

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

View File

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

View File

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