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 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 .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 @@ def on_focus(self, window, focused):
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 @@ def move_tab_forward(self):
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 @@ def multi_click(self, count, x, y):
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 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 @@ def fork(self):
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 @@ def fork(self):
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 .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 @@ def __init__(self, opts, args, on_title_change, session_tab=None):
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 @@ def next_layout(self):
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 @@ def set_active_window(self, window):
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 @@ def __iter__(self):
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,11 +118,12 @@ def focus_changed(self, focused):
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 @@ def buf_toggled(self, is_main_linebuf):
# 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):