From 9a7b23fd2350ff8570f79efe5653073fd35b125f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 10 Dec 2016 12:59:20 +0530 Subject: [PATCH] Implement viewing of the scrollback buffer in a separate window --- README.asciidoc | 19 +++++++++++++++++++ kitty/boss.py | 14 +++++++++++++- kitty/char_grid.py | 6 ++++++ kitty/child.py | 28 +++++++++++++++++++++++----- kitty/config.py | 3 +++ kitty/kitty.conf | 9 +++++++++ kitty/tabs.py | 31 ++++++++++++++++++++++--------- kitty/window.py | 16 +++++++++++----- 8 files changed, 106 insertions(+), 20 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index ef81b7006..7cb02b51a 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -32,6 +32,7 @@ :sc_scroll_page_up: pass:quotes[`ctrl+shift+page_up`] :sc_second_window: pass:quotes[`ctrl+shift+2`] :sc_seventh_window: pass:quotes[`ctrl+shift+7`] +:sc_show_scrollback: pass:quotes[`ctrl+shift+h`] :sc_sixth_window: pass:quotes[`ctrl+shift+6`] :sc_tenth_window: pass:quotes[`ctrl+shift+0`] :sc_third_window: pass:quotes[`ctrl+shift+3`] @@ -49,6 +50,9 @@ layouts without needing to use an extra program like tmux * Supports all modern terminal features: unicode, true-color, mouse protocol, focus tracking, bracketed paste and so on. +* Allows you to view the scrollback buffer in a separate window +using your favorite pager program such as less + * Easily hackable (UI layer written in python, inner loops in C for speed). Less than ten thousand lines of code. @@ -121,6 +125,7 @@ different layouts, like windows are organized in a tiling window manager. The keyboard controls (which are all customizable) for tabs and windows are: +[[scrolling-shortcuts]] [options="header"] .Scrolling |=== @@ -168,6 +173,20 @@ windows are: |=== +== The scrollback buffer + +kitty supports scrolling back to view history, just like most terminals. You +can use either the <> or the mouse +scroll wheel to do so. However, kitty has an extra, neat feature. Sometime 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 a new window, using your favorite +pager program (which is less by default). You can then explore the scrollback +buffer using whatever program you normally use. Colors and text formatting are +preserved. + + == Configuration kitty is highly customizable, everything from keyboard shortcuts, to diff --git a/kitty/boss.py b/kitty/boss.py index 3c4e0931d..191cb0fba 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -12,6 +12,7 @@ from functools import wraps from threading import Thread, current_thread from time import monotonic from queue import Queue, Empty +from gettext import gettext as _ from .constants import ( viewport_size, set_boss, wakeup, cell_size, MODIFIER_KEYS, @@ -29,7 +30,7 @@ from .constants import is_key_pressed from .keys import interpret_text_event, interpret_key_event, get_shortcut from .session import create_session from .shaders import Sprites, ShaderProgram -from .tabs import TabManager +from .tabs import TabManager, SpecialWindow from .timers import Timers from .utils import handle_unix_signals @@ -291,6 +292,14 @@ class Boss(Thread): yield w w.focus_changed(focused) + def display_scrollback(self, data): + if self.opts.scrollback_in_new_tab: + self.queue_ui_action(self.display_scrollback_in_new_tab, data) + else: + tab = self.active_tab + if tab is not None: + tab.new_special_window(SpecialWindow(self.opts.scrollback_pager, data, _('History'))) + def window_for_pos(self, x, y): tab = self.active_tab if tab is not None: @@ -454,4 +463,7 @@ class Boss(Thread): def move_tab_backward(self): self.queue_action(self.tab_manager.move_tab, -1) + + def display_scrollback_in_new_tab(self, data): + self.tab_manager.new_tab(special_window=SpecialWindow(self.opts.scrollback_pager, data, _('History'))) # }}} diff --git a/kitty/char_grid.py b/kitty/char_grid.py index 6f50c89bf..5bc44db2f 100644 --- a/kitty/char_grid.py +++ b/kitty/char_grid.py @@ -412,6 +412,12 @@ class CharGrid: if ps: set_primary_selection(ps) + def get_scrollback_as_ansi(self): + ans = [] + self.screen.historybuf.as_ansi(ans.append) + self.screen.linebuf.as_ansi(ans.append) + return ''.join(ans).encode('utf-8') + def text_for_selection(self, sel=None): s = sel or self.current_selection return s.text(self.screen.linebuf, self.screen.historybuf) diff --git a/kitty/child.py b/kitty/child.py index 84b016ac3..b24135e25 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -7,27 +7,37 @@ import termios import struct import fcntl import signal +from threading import Thread from .constants import terminfo_dir +def remove_cloexec(fd): + fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC) + + class Child: child_fd = pid = None forked = False - def __init__(self, argv, cwd, opts): + def __init__(self, argv, cwd, opts, stdin=None): self.argv = argv self.cwd = os.path.abspath(os.path.expandvars(os.path.expanduser(cwd or os.getcwd()))) self.opts = opts + self.stdin = stdin def fork(self): if self.forked: return self.forked = True - master, slave = os.openpty() - fcntl.fcntl(slave, fcntl.F_SETFD, fcntl.fcntl(slave, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC) - # Note that master and slave are in blocking mode + master, slave = os.openpty() # Note that master and slave are in blocking mode + remove_cloexec(slave) + stdin, self.stdin = self.stdin, None + if stdin is not None: + stdin_read_fd, stdin_write_fd = os.pipe() + remove_cloexec(stdin_read_fd) + stdin_file = os.fdopen(stdin_write_fd, 'wb') pid = os.fork() if pid == 0: # child try: @@ -36,7 +46,11 @@ class Child: os.chdir('/') os.setsid() for i in range(3): - os.dup2(slave, i) + if stdin is not None and i == 0: + os.dup2(stdin_read_fd, i) + os.close(stdin_read_fd), os.close(stdin_write_fd) + else: + os.dup2(slave, i) os.close(slave), os.close(master) os.closerange(3, 200) # Establish the controlling terminal (see man 7 credentials) @@ -55,6 +69,10 @@ class Child: os.close(slave) self.pid = pid self.child_fd = master + if stdin is not None: + t = Thread(name='WriteStdin', target=stdin_file.write, args=(stdin,)) + t.daemon = True + t.start() return pid def resize_pty(self, w, h): diff --git a/kitty/config.py b/kitty/config.py index bc8fb36e1..01d85becf 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -5,6 +5,7 @@ import re import sys import os +import shlex from collections import namedtuple from .fast_data_types import ( @@ -91,6 +92,8 @@ def to_layout_names(raw): type_map = { 'scrollback_lines': int, + 'scrollback_pager': shlex.split, + 'scrollback_in_new_tab': to_bool, 'font_size': to_font_size, 'cursor_shape': to_cursor_shape, 'cursor_opacity': to_opacity, diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 12936999b..8d67d0696 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -38,6 +38,14 @@ cursor_stop_blinking_after 15.0 # Number of lines of history to keep in memory for scrolling back scrollback_lines 2000 +# Program with which to view scrollback in a new window. The scrollback buffer is passed as +# STDIN to this program. If you change it, make sure the program you use can +# handle ANSI escape sequences for colors and text formatting. +scrollback_pager less +G -R + +# When viewing scrollback in a new window, put it in a new tab as well +scrollback_in_new_tab no + # Wheel scroll multiplier (modify the amount scrolled by the mouse wheel) wheel_scroll_multiplier 5.0 @@ -139,6 +147,7 @@ map ctrl+shift+page_up scroll_page_up map ctrl+shift+page_down scroll_page_down map ctrl+shift+home scroll_home map ctrl+shift+end scroll_end +map ctrl+shift+h show_scrollback # Window management map ctrl+shift+enter new_window diff --git a/kitty/tabs.py b/kitty/tabs.py index f355817a5..ec964d34d 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -18,9 +18,13 @@ from .borders import Borders from .window import Window +def SpecialWindow(cmd, stdin=None, override_title=None): + return (cmd, stdin, override_title) + + class Tab: - def __init__(self, opts, args, on_title_change, session_tab=None): + def __init__(self, opts, args, on_title_change, session_tab=None, special_window=None): self.opts, self.args = opts, args self.name = getattr(session_tab, 'name', '') self.on_title_change = on_title_change @@ -31,7 +35,10 @@ class Tab: if session_tab is None: self.cwd = args.directory l = self.enabled_layouts[0] - queue_action(self.new_window) + if special_window is None: + queue_action(self.new_window) + else: + queue_action(self.new_special_window, special_window) else: self.cwd = session_tab.cwd or args.directory l = session_tab.layout @@ -86,23 +93,29 @@ class Tab: w.is_visible_in_layout = True self.relayout() - def launch_child(self, use_shell=False, cmd=None): + def launch_child(self, use_shell=False, cmd=None, stdin=None): if cmd is None: if use_shell: cmd = [shell_path] else: cmd = self.args.args or [shell_path] - ans = Child(cmd, self.cwd, self.opts) + ans = Child(cmd, self.cwd, self.opts, stdin) ans.fork() return ans - def new_window(self, use_shell=True, cmd=None): - child = self.launch_child(use_shell=use_shell, cmd=cmd) + def new_window(self, use_shell=True, cmd=None, stdin=None, override_title=None): + child = self.launch_child(use_shell=use_shell, cmd=cmd, stdin=stdin) window = Window(self, child, self.opts, self.args) + if override_title is not None: + window.title = window.override_title = override_title get_boss().add_child_fd(child.child_fd, window.read_ready, window.write_ready) self.active_window_idx = self.current_layout.add_window(self.windows, window, self.active_window_idx) self.borders(self.windows, self.active_window, self.current_layout.needs_window_borders and len(self.windows) > 1) glfw_post_empty_event() + return window + + def new_special_window(self, special_window): + self.new_window(False, *special_window) def close_window(self): if self.windows: @@ -129,7 +142,7 @@ class Tab: def nth_window(self, num=0): if self.windows: - self.set_active_window_idx(min(num, len(self.windows)-1)) + self.set_active_window_idx(min(num, len(self.windows) - 1)) def _next_window(self, delta=1): if len(self.windows) > 1: @@ -241,9 +254,9 @@ class TabManager: def __len__(self): return len(self.tabs) - def new_tab(self): + def new_tab(self, special_window=None): self.active_tab_idx = len(self.tabs) - self.tabs.append(Tab(self.opts, self.args, self.title_changed)) + self.tabs.append(Tab(self.opts, self.args, self.title_changed, special_window=special_window)) @property def active_tab(self): diff --git a/kitty/window.py b/kitty/window.py index 35876a05d..8ca134678 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -27,6 +27,7 @@ class Window: def __init__(self, tab, child, opts, args): self.tabref = weakref.ref(tab) + self.override_title = None self.last_mouse_cursor_pos = 0, 0 self.destroyed = False self.click_queue = deque(maxlen=3) @@ -117,11 +118,12 @@ class Window: self.write_to_child(b'\x1b[O') def title_changed(self, new_title): - self.title = sanitize_title(new_title or appname) - t = self.tabref() - if t is not None: - t.title_changed(self) - glfw_post_empty_event() + if self.override_title is not None: + self.title = sanitize_title(new_title or appname) + t = self.tabref() + if t is not None: + t.title_changed(self) + glfw_post_empty_event() def icon_changed(self, new_icon): pass # TODO: Implement this @@ -260,6 +262,10 @@ class Window: # actions {{{ + def show_scrollback(self): + data = self.char_grid.get_scrollback_as_ansi() + get_boss().display_scrollback(data) + def paste(self, text): if text and not self.destroyed: if isinstance(text, str):