diff --git a/README.md b/README.md index 7947568e2..88e3c94bd 100644 --- a/README.md +++ b/README.md @@ -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 ------------------------------------------ diff --git a/kitty/boss.py b/kitty/boss.py index 0702a627c..59b2f726f 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -27,6 +27,7 @@ from .borders import BordersProgram 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 @@ class Boss(Thread): 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() diff --git a/kitty/main.py b/kitty/main.py index 802dc5781..8abefa087 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -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.' diff --git a/kitty/session.py b/kitty/session.py new file mode 100644 index 000000000..a2cad6a02 --- /dev/null +++ b/kitty/session.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +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 diff --git a/kitty/tabs.py b/kitty/tabs.py index 5985dedc6..620bb9dd6 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -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 @@ from .window import Window 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 @@ class Tab: 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 @@ class Tab: 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 @@ class TabManager: @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):