Implement startup sessions

This commit is contained in:
Kovid Goyal 2016-12-06 11:19:40 +05:30
parent 60513fcbab
commit 33d58fd7bc
5 changed files with 157 additions and 24 deletions

View file

@ -52,6 +52,33 @@ By default kitty looks for a config file in the OS
config directory (usually `~/.config/kitty/kitty.conf` on linux) but you can pass
a specific path via the `--config` option.
Startup Sessions
-------------------
You can control the tabs, window layout, working directory, startup programs, etc. by creating a
"session" file and using the `--session` command line flag. For example:
```
# Set the window layout for the current tab
layout tall
# Set the working directory for the current tab
cd ~
# Create a window and run the specified command in it
launch zsh
launch vim
launch irssi --profile x
# Create a new tab
new_tab
cd ~/somewhere
# Set the layouts allowed in this tab
enabled_layouts tall, stack
layout stack
launch zsh
# Make the previous window the currently focused window
focus
launch emacs
```
Resources on terminal behavior
------------------------------------------

View file

@ -27,6 +27,7 @@
from .char_grid import cursor_shader, cell_shader
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 .timers import Timers
@ -65,6 +66,7 @@ class Boss(Thread):
def __init__(self, glfw_window, opts, args):
Thread.__init__(self, name='ChildMonitor')
startup_session = create_session(opts, args)
self.cursor_blinking = True
self.glfw_window_title = None
self.action_queue = Queue()
@ -91,14 +93,13 @@ def __init__(self, glfw_window, opts, args):
glfw_window.scroll_callback = self.on_mouse_scroll
glfw_window.cursor_pos_callback = self.on_mouse_move
glfw_window.window_focus_callback = self.on_focus
self.tab_manager = TabManager(opts, args)
self.tab_manager = TabManager(opts, args, startup_session)
self.sprites = Sprites()
self.cell_program = ShaderProgram(*cell_shader)
self.cursor_program = ShaderProgram(*cursor_shader)
self.borders_program = BordersProgram()
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
self.sprites.do_layout(cell_size.width, cell_size.height)
self.queue_action(self.active_tab.new_window, False)
self.glfw_window.set_click_cursor(False)
self.show_mouse_cursor()
self.start_cursor_blink()

View file

@ -39,6 +39,8 @@ def option_parser():
a('--replay-commands', default=None, help=_('Replay previously dumped commands'))
a('--window-layout', default=None, choices=frozenset(all_layouts.keys()), help=_(
'The window layout to use on startup'))
a('--session', default=None, help=_(
'Path to a file containing the startup session (tabs, windows, layout, programs)'))
a('args', nargs=argparse.REMAINDER, help=_(
'The remaining arguments are used to launch a program other than the default shell. Any further options are passed'
' directly to the program being invoked.'

92
kitty/session.py Normal file
View file

@ -0,0 +1,92 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import shlex
from .config import to_layout_names
from .constants import shell_path
from .layout import all_layouts
class Tab:
def __init__(self, opts):
self.windows = []
self.active_window_idx = 0
self.enabled_layouts = opts.enabled_layouts
self.layout = (self.enabled_layouts or ['tall'])[0]
self.cwd = None
class Session:
def __init__(self):
self.tabs = []
self.active_tab_idx = 0
def add_tab(self, opts):
self.tabs.append(Tab(opts))
def set_layout(self, val):
if val not in all_layouts:
raise ValueError('{} is not a valid layout'.format(val))
self.tabs[-1].layout = val
def add_window(self, cmd):
if cmd:
cmd = shlex.split(cmd) if isinstance(cmd, str) else cmd
else:
cmd = None
self.tabs[-1].windows.append(cmd)
def focus(self):
self.active_tab_idx = max(0, len(self.tabs) - 1)
self.tabs[-1].active_window_idx = max(0, len(self.tabs.windows) - 1)
def set_enabled_layouts(self, raw):
self.tabs[-1].enabled_layouts = to_layout_names(raw)
def set_cwd(self, val):
self.tabs[-1].cwd = val
def parse_session(raw, opts):
ans = Session()
for line in raw.splitlines():
line = line.strip()
if line and not line.startswith('#'):
cmd, rest = line.partition(' ')[::2]
cmd, rest = cmd.strip(), rest.strip()
if cmd == 'new_tab':
ans.add_tab(opts)
elif cmd == 'layout':
ans.set_layout(rest)
elif cmd == 'launch':
ans.add_window(rest)
elif cmd == 'focus':
ans.focus()
elif cmd == 'enabled_layouts':
ans.set_enabled_layouts(rest)
elif cmd == 'cd':
ans.set_cwd(rest)
else:
raise ValueError('Unknown command in session file: {}'.format(cmd))
def create_session(opts, args):
if args.session:
with open(args.session) as f:
return parse_session(f.read(), opts)
ans = Session()
if args.window_layout:
if args.window_layout not in opts.enabled_layouts:
opts.enabled_layouts.insert(0, args.window_layout)
current_layout = args.window_layout
else:
current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall'
ans.add_tab(opts)
ans.tabs[-1].layout = current_layout
cmd = args.args or [shell_path]
ans.add_window(cmd)
return ans

View file

@ -5,7 +5,7 @@
from collections import deque
from .child import Child
from .constants import get_boss, appname, shell_path, cell_size
from .constants import get_boss, appname, shell_path, cell_size, queue_action
from .fast_data_types import glfw_post_empty_event
from .layout import all_layouts
from .borders import Borders
@ -14,19 +14,25 @@
class Tab:
def __init__(self, opts, args):
def __init__(self, opts, args, session_tab=None):
self.opts, self.args = opts, args
self.enabled_layouts = opts.enabled_layouts
self.enabled_layouts = list((session_tab or opts).enabled_layouts)
self.borders = Borders(opts)
self.windows = deque()
if args.window_layout:
if args.window_layout not in self.enabled_layouts:
self.enabled_layouts.insert(0, args.window_layout)
self.current_layout = all_layouts[args.window_layout]
else:
self.current_layout = all_layouts[self.enabled_layouts[0]]
self.active_window_idx = 0
self.current_layout = self.current_layout(opts, self.borders.border_width, self.windows)
if session_tab is None:
self.cwd = args.directory
self.current_layout = self.enabled_layouts[0]
queue_action(self.new_window)
else:
self.cwd = session_tab.cwd or args.directory
self.current_layout = all_layouts[session_tab.layout](opts, self.borders.border_width, self.windows)
queue_action(self.startup, session_tab)
def startup(self, session_tab):
for cmd in session_tab.windows:
self.new_window(cmd=cmd)
self.active_window_idx = session_tab.active_window_idx
@property
def is_visible(self):
@ -52,24 +58,28 @@ def relayout(self):
def next_layout(self):
if len(self.opts.enabled_layouts) > 1:
idx = self.opts.enabled_layouts.index(self.current_layout.name)
try:
idx = self.opts.enabled_layouts.index(self.current_layout.name)
except Exception:
idx = -1
nl = self.opts.enabled_layouts[(idx + 1) % len(self.opts.enabled_layouts)]
self.current_layout = all_layouts[nl](self.opts, self.borders.border_width, self.windows)
for w in self.windows:
w.is_visible_in_layout = True
self.relayout()
def launch_child(self, use_shell=False):
if use_shell:
cmd = [shell_path]
else:
cmd = self.args.args or [shell_path]
ans = Child(cmd, self.args.directory, self.opts)
def launch_child(self, use_shell=False, cmd=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.fork()
return ans
def new_window(self, use_shell=True):
child = self.launch_child(use_shell=use_shell)
def new_window(self, use_shell=True, cmd=None):
child = self.launch_child(use_shell=use_shell, cmd=cmd)
window = Window(self, child, self.opts, self.args)
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)
@ -129,9 +139,10 @@ def render(self):
class TabManager:
def __init__(self, opts, args):
def __init__(self, opts, args, startup_session):
self.opts, self.args = opts, args
self.tabs = [Tab(opts, args)]
self.tabs = [Tab(opts, args, t) for t in startup_session.tabs]
self.active_tab_idx = startup_session.active_tab_idx
def __iter__(self):
return iter(self.tabs)
@ -141,7 +152,7 @@ def __len__(self):
@property
def active_tab(self):
return self.tabs[0] if self.tabs else None
return self.tabs[self.active_tab_idx] if self.tabs else None
@property
def tab_bar_height(self):