Implement viewing of the scrollback buffer in a separate window

This commit is contained in:
Kovid Goyal 2016-12-10 12:59:20 +05:30
parent 41c63917c8
commit 9a7b23fd23
8 changed files with 106 additions and 20 deletions

View File

@ -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 <<scrolling-shortcuts,keyboard shortcuts>> 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

View File

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

View File

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

View File

@ -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,6 +46,10 @@ class Child:
os.chdir('/')
os.setsid()
for i in range(3):
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)
@ -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):

View File

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

View File

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

View File

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

View File

@ -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,6 +118,7 @@ class Window:
self.write_to_child(b'\x1b[O')
def title_changed(self, new_title):
if self.override_title is not None:
self.title = sanitize_title(new_title or appname)
t = self.tabref()
if t is not None:
@ -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):