Implement viewing of the scrollback buffer in a separate window
This commit is contained in:
parent
41c63917c8
commit
9a7b23fd23
@ -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
|
||||
|
||||
@ -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')))
|
||||
# }}}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user