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_scroll_page_up: pass:quotes[`ctrl+shift+page_up`]
|
||||||
:sc_second_window: pass:quotes[`ctrl+shift+2`]
|
:sc_second_window: pass:quotes[`ctrl+shift+2`]
|
||||||
:sc_seventh_window: pass:quotes[`ctrl+shift+7`]
|
: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_sixth_window: pass:quotes[`ctrl+shift+6`]
|
||||||
:sc_tenth_window: pass:quotes[`ctrl+shift+0`]
|
:sc_tenth_window: pass:quotes[`ctrl+shift+0`]
|
||||||
:sc_third_window: pass:quotes[`ctrl+shift+3`]
|
: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
|
* Supports all modern terminal features: unicode, true-color, mouse
|
||||||
protocol, focus tracking, bracketed paste and so on.
|
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
|
* Easily hackable (UI layer written in python, inner loops in C for
|
||||||
speed). Less than ten thousand lines of code.
|
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
|
manager. The keyboard controls (which are all customizable) for tabs and
|
||||||
windows are:
|
windows are:
|
||||||
|
|
||||||
|
[[scrolling-shortcuts]]
|
||||||
[options="header"]
|
[options="header"]
|
||||||
.Scrolling
|
.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
|
== Configuration
|
||||||
|
|
||||||
kitty is highly customizable, everything from keyboard shortcuts, to
|
kitty is highly customizable, everything from keyboard shortcuts, to
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from functools import wraps
|
|||||||
from threading import Thread, current_thread
|
from threading import Thread, current_thread
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
viewport_size, set_boss, wakeup, cell_size, MODIFIER_KEYS,
|
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 .keys import interpret_text_event, interpret_key_event, get_shortcut
|
||||||
from .session import create_session
|
from .session import create_session
|
||||||
from .shaders import Sprites, ShaderProgram
|
from .shaders import Sprites, ShaderProgram
|
||||||
from .tabs import TabManager
|
from .tabs import TabManager, SpecialWindow
|
||||||
from .timers import Timers
|
from .timers import Timers
|
||||||
from .utils import handle_unix_signals
|
from .utils import handle_unix_signals
|
||||||
|
|
||||||
@ -291,6 +292,14 @@ class Boss(Thread):
|
|||||||
yield w
|
yield w
|
||||||
w.focus_changed(focused)
|
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):
|
def window_for_pos(self, x, y):
|
||||||
tab = self.active_tab
|
tab = self.active_tab
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
@ -454,4 +463,7 @@ class Boss(Thread):
|
|||||||
|
|
||||||
def move_tab_backward(self):
|
def move_tab_backward(self):
|
||||||
self.queue_action(self.tab_manager.move_tab, -1)
|
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:
|
if ps:
|
||||||
set_primary_selection(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):
|
def text_for_selection(self, sel=None):
|
||||||
s = sel or self.current_selection
|
s = sel or self.current_selection
|
||||||
return s.text(self.screen.linebuf, self.screen.historybuf)
|
return s.text(self.screen.linebuf, self.screen.historybuf)
|
||||||
|
|||||||
@ -7,27 +7,37 @@ import termios
|
|||||||
import struct
|
import struct
|
||||||
import fcntl
|
import fcntl
|
||||||
import signal
|
import signal
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from .constants import terminfo_dir
|
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:
|
class Child:
|
||||||
|
|
||||||
child_fd = pid = None
|
child_fd = pid = None
|
||||||
forked = False
|
forked = False
|
||||||
|
|
||||||
def __init__(self, argv, cwd, opts):
|
def __init__(self, argv, cwd, opts, stdin=None):
|
||||||
self.argv = argv
|
self.argv = argv
|
||||||
self.cwd = os.path.abspath(os.path.expandvars(os.path.expanduser(cwd or os.getcwd())))
|
self.cwd = os.path.abspath(os.path.expandvars(os.path.expanduser(cwd or os.getcwd())))
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
|
self.stdin = stdin
|
||||||
|
|
||||||
def fork(self):
|
def fork(self):
|
||||||
if self.forked:
|
if self.forked:
|
||||||
return
|
return
|
||||||
self.forked = True
|
self.forked = True
|
||||||
master, slave = os.openpty()
|
master, slave = os.openpty() # Note that master and slave are in blocking mode
|
||||||
fcntl.fcntl(slave, fcntl.F_SETFD, fcntl.fcntl(slave, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC)
|
remove_cloexec(slave)
|
||||||
# Note that master and slave are in blocking mode
|
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()
|
pid = os.fork()
|
||||||
if pid == 0: # child
|
if pid == 0: # child
|
||||||
try:
|
try:
|
||||||
@ -36,6 +46,10 @@ class Child:
|
|||||||
os.chdir('/')
|
os.chdir('/')
|
||||||
os.setsid()
|
os.setsid()
|
||||||
for i in range(3):
|
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.dup2(slave, i)
|
||||||
os.close(slave), os.close(master)
|
os.close(slave), os.close(master)
|
||||||
os.closerange(3, 200)
|
os.closerange(3, 200)
|
||||||
@ -55,6 +69,10 @@ class Child:
|
|||||||
os.close(slave)
|
os.close(slave)
|
||||||
self.pid = pid
|
self.pid = pid
|
||||||
self.child_fd = master
|
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
|
return pid
|
||||||
|
|
||||||
def resize_pty(self, w, h):
|
def resize_pty(self, w, h):
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
@ -91,6 +92,8 @@ def to_layout_names(raw):
|
|||||||
|
|
||||||
type_map = {
|
type_map = {
|
||||||
'scrollback_lines': int,
|
'scrollback_lines': int,
|
||||||
|
'scrollback_pager': shlex.split,
|
||||||
|
'scrollback_in_new_tab': to_bool,
|
||||||
'font_size': to_font_size,
|
'font_size': to_font_size,
|
||||||
'cursor_shape': to_cursor_shape,
|
'cursor_shape': to_cursor_shape,
|
||||||
'cursor_opacity': to_opacity,
|
'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
|
# Number of lines of history to keep in memory for scrolling back
|
||||||
scrollback_lines 2000
|
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 (modify the amount scrolled by the mouse wheel)
|
||||||
wheel_scroll_multiplier 5.0
|
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+page_down scroll_page_down
|
||||||
map ctrl+shift+home scroll_home
|
map ctrl+shift+home scroll_home
|
||||||
map ctrl+shift+end scroll_end
|
map ctrl+shift+end scroll_end
|
||||||
|
map ctrl+shift+h show_scrollback
|
||||||
|
|
||||||
# Window management
|
# Window management
|
||||||
map ctrl+shift+enter new_window
|
map ctrl+shift+enter new_window
|
||||||
|
|||||||
@ -18,9 +18,13 @@ from .borders import Borders
|
|||||||
from .window import Window
|
from .window import Window
|
||||||
|
|
||||||
|
|
||||||
|
def SpecialWindow(cmd, stdin=None, override_title=None):
|
||||||
|
return (cmd, stdin, override_title)
|
||||||
|
|
||||||
|
|
||||||
class Tab:
|
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.opts, self.args = opts, args
|
||||||
self.name = getattr(session_tab, 'name', '')
|
self.name = getattr(session_tab, 'name', '')
|
||||||
self.on_title_change = on_title_change
|
self.on_title_change = on_title_change
|
||||||
@ -31,7 +35,10 @@ class Tab:
|
|||||||
if session_tab is None:
|
if session_tab is None:
|
||||||
self.cwd = args.directory
|
self.cwd = args.directory
|
||||||
l = self.enabled_layouts[0]
|
l = self.enabled_layouts[0]
|
||||||
|
if special_window is None:
|
||||||
queue_action(self.new_window)
|
queue_action(self.new_window)
|
||||||
|
else:
|
||||||
|
queue_action(self.new_special_window, special_window)
|
||||||
else:
|
else:
|
||||||
self.cwd = session_tab.cwd or args.directory
|
self.cwd = session_tab.cwd or args.directory
|
||||||
l = session_tab.layout
|
l = session_tab.layout
|
||||||
@ -86,23 +93,29 @@ class Tab:
|
|||||||
w.is_visible_in_layout = True
|
w.is_visible_in_layout = True
|
||||||
self.relayout()
|
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 cmd is None:
|
||||||
if use_shell:
|
if use_shell:
|
||||||
cmd = [shell_path]
|
cmd = [shell_path]
|
||||||
else:
|
else:
|
||||||
cmd = self.args.args or [shell_path]
|
cmd = self.args.args or [shell_path]
|
||||||
ans = Child(cmd, self.cwd, self.opts)
|
ans = Child(cmd, self.cwd, self.opts, stdin)
|
||||||
ans.fork()
|
ans.fork()
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def new_window(self, use_shell=True, cmd=None):
|
def new_window(self, use_shell=True, cmd=None, stdin=None, override_title=None):
|
||||||
child = self.launch_child(use_shell=use_shell, cmd=cmd)
|
child = self.launch_child(use_shell=use_shell, cmd=cmd, stdin=stdin)
|
||||||
window = Window(self, child, self.opts, self.args)
|
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)
|
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.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)
|
self.borders(self.windows, self.active_window, self.current_layout.needs_window_borders and len(self.windows) > 1)
|
||||||
glfw_post_empty_event()
|
glfw_post_empty_event()
|
||||||
|
return window
|
||||||
|
|
||||||
|
def new_special_window(self, special_window):
|
||||||
|
self.new_window(False, *special_window)
|
||||||
|
|
||||||
def close_window(self):
|
def close_window(self):
|
||||||
if self.windows:
|
if self.windows:
|
||||||
@ -241,9 +254,9 @@ class TabManager:
|
|||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.tabs)
|
return len(self.tabs)
|
||||||
|
|
||||||
def new_tab(self):
|
def new_tab(self, special_window=None):
|
||||||
self.active_tab_idx = len(self.tabs)
|
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
|
@property
|
||||||
def active_tab(self):
|
def active_tab(self):
|
||||||
|
|||||||
@ -27,6 +27,7 @@ class Window:
|
|||||||
|
|
||||||
def __init__(self, tab, child, opts, args):
|
def __init__(self, tab, child, opts, args):
|
||||||
self.tabref = weakref.ref(tab)
|
self.tabref = weakref.ref(tab)
|
||||||
|
self.override_title = None
|
||||||
self.last_mouse_cursor_pos = 0, 0
|
self.last_mouse_cursor_pos = 0, 0
|
||||||
self.destroyed = False
|
self.destroyed = False
|
||||||
self.click_queue = deque(maxlen=3)
|
self.click_queue = deque(maxlen=3)
|
||||||
@ -117,6 +118,7 @@ class Window:
|
|||||||
self.write_to_child(b'\x1b[O')
|
self.write_to_child(b'\x1b[O')
|
||||||
|
|
||||||
def title_changed(self, new_title):
|
def title_changed(self, new_title):
|
||||||
|
if self.override_title is not None:
|
||||||
self.title = sanitize_title(new_title or appname)
|
self.title = sanitize_title(new_title or appname)
|
||||||
t = self.tabref()
|
t = self.tabref()
|
||||||
if t is not None:
|
if t is not None:
|
||||||
@ -260,6 +262,10 @@ class Window:
|
|||||||
|
|
||||||
# actions {{{
|
# actions {{{
|
||||||
|
|
||||||
|
def show_scrollback(self):
|
||||||
|
data = self.char_grid.get_scrollback_as_ansi()
|
||||||
|
get_boss().display_scrollback(data)
|
||||||
|
|
||||||
def paste(self, text):
|
def paste(self, text):
|
||||||
if text and not self.destroyed:
|
if text and not self.destroyed:
|
||||||
if isinstance(text, str):
|
if isinstance(text, str):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user