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: