diff --git a/doc/api.rst b/doc/api.rst index 031e3d9f1bf..8cf77d104f0 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -18,7 +18,6 @@ Internals CLI --- -.. automethod:: tmuxp.cli._reattach .. automethod:: tmuxp.cli.get_config_dir .. automethod:: tmuxp.cli.get_teamocil_dir .. automethod:: tmuxp.cli.get_tmuxinator_dir diff --git a/doc/cli.rst b/doc/cli.rst index 9fc5ba7b07c..40b10438ef2 100644 --- a/doc/cli.rst +++ b/doc/cli.rst @@ -84,6 +84,24 @@ without being attached. The last one will be attached if there is no $ tmuxp load ... +If you try to load a config file within a tmux session, it will ask you +if you want to load and attach in the new session or just load detached. +You can also load config appending windows in the current active session. + +:: + + Already inside TMUX, switch to session? yes/no + Or (a)ppend windows in the current active session? + [y/n/a]: + +All this modes you can force doing: + +.. code-block:: bash + + $ tmuxp load -y config # load attached + $ tmuxp load -d config # load detached + $ tmuxp load -a config # load appending + .. _cli_import: Import diff --git a/doc/quickstart.rst b/doc/quickstart.rst index 380c8edae1c..c89b13871f1 100644 --- a/doc/quickstart.rst +++ b/doc/quickstart.rst @@ -55,7 +55,8 @@ Load multiple tmux sessions at once: $ tmuxp load example.yaml anothersession.yaml tmuxp will offer to ``switch-client`` for you if you're already in a -session. +session. You can also load config appending windows in the current +active session. You can also have a custom tmuxp config directory by setting the ``TMUX_CONFIGDIR`` in your environment variables. diff --git a/tests/fixtures/workspacebuilder/three_windows.yaml b/tests/fixtures/workspacebuilder/three_windows.yaml new file mode 100644 index 00000000000..6d571088aa5 --- /dev/null +++ b/tests/fixtures/workspacebuilder/three_windows.yaml @@ -0,0 +1,15 @@ +session_name: sample_three_windows +windows: +- window_name: first + panes: + - shell_command: + - echo 'first window' +- window_name: second + panes: + - shell_command: + - echo 'second window' +- window_name: third + panes: + - shell_command: + - echo 'third window' + diff --git a/tests/fixtures/workspacebuilder/two_windows.yaml b/tests/fixtures/workspacebuilder/two_windows.yaml new file mode 100644 index 00000000000..0f1d40f34a9 --- /dev/null +++ b/tests/fixtures/workspacebuilder/two_windows.yaml @@ -0,0 +1,10 @@ +session_name: sample_two_windows +windows: +- window_name: first + panes: + - shell_command: + - echo 'first window' +- window_name: second + panes: + - shell_command: + - echo 'second window' diff --git a/tests/test_workspacebuilder.py b/tests/test_workspacebuilder.py index 6085ea46d47..93729eedd17 100644 --- a/tests/test_workspacebuilder.py +++ b/tests/test_workspacebuilder.py @@ -673,3 +673,51 @@ def test_before_load_true_if_test_passes_with_args(server): with temp_session(server) as session: builder.build(session=session) + + +def test_load_configs_same_session(server): + yaml_config = loadfixture("workspacebuilder/three_windows.yaml") + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(yaml_config).get() + + builder = WorkspaceBuilder(sconf=sconfig, server=server) + builder.build() + + assert len(server.sessions) == 1 + assert len(server.sessions[0]._windows) == 3 + + yaml_config = loadfixture("workspacebuilder/two_windows.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(yaml_config).get() + + builder = WorkspaceBuilder(sconf=sconfig, server=server) + builder.build(server.sessions[0], True) + + assert len(server.sessions) == 1 + assert len(server.sessions[0]._windows) == 5 + + +def test_load_configs_separate_sessions(server): + yaml_config = loadfixture("workspacebuilder/three_windows.yaml") + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(yaml_config).get() + + builder = WorkspaceBuilder(sconf=sconfig, server=server) + builder.build() + + assert len(server.sessions) == 1 + assert len(server.sessions[0]._windows) == 3 + + yaml_config = loadfixture("workspacebuilder/two_windows.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(yaml_config).get() + + builder = WorkspaceBuilder(sconf=sconfig, server=server) + builder.build() + + assert len(server.sessions) == 2 + assert len(server.sessions[0]._windows) == 3 + assert len(server.sessions[1]._windows) == 2 + diff --git a/tmuxp/__about__.py b/tmuxp/__about__.py index 37322d441f1..859816bd088 100644 --- a/tmuxp/__about__.py +++ b/tmuxp/__about__.py @@ -1,6 +1,6 @@ __title__ = 'tmuxp' __package_name__ = 'tmuxp' -__version__ = '1.5.3' +__version__ = '1.6.0' __description__ = 'tmux session manager' __email__ = 'tony@git-pull.com' __author__ = 'Tony Narlock' diff --git a/tmuxp/cli.py b/tmuxp/cli.py index f74b187c383..fe4bb3bf178 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -368,29 +368,6 @@ def scan_config(config, config_dir=None): return config -def _reattach(session): - """ - Reattach session (depending on env being inside tmux already or not) - - Parameters - ---------- - session : :class:`libtmux.Session` - - Notes - ----- - If ``TMUX`` environmental variable exists in the environment this script is - running, that means we're in a tmux client. So ``tmux switch-client`` will - load the session. - - If not, ``tmux attach-session`` loads the client to the target session. - """ - if 'TMUX' in os.environ: - session.switch_client() - - else: - session.attach_session() - - def load_workspace( config_file, socket_name=None, @@ -398,6 +375,7 @@ def load_workspace( colors=None, detached=False, answer_yes=False, + append=False, ): """ Load a tmux "workspace" session via tmuxp file. @@ -416,7 +394,10 @@ def load_workspace( detached : bool Force detached state. default False. answer_yes : bool - Assume yes when given prompt. default False. + Assume yes when given prompt to attach in a new sesion. default False. + append: bool + Assume current when given prompt to append windows in in same Session. + Default False. Notes ----- @@ -509,60 +490,41 @@ def load_workspace( click.echo('%s is empty or parsed no config data' % config_file, err=True) return - session_name = sconfig['session_name'] - - # if the session already exists, prompt the user to attach. tmuxp doesn't - # support incremental session building or appending (yet, PR's welcome!) - if builder.session_exists(session_name): - if not detached and ( - answer_yes - or click.confirm( - '%s is already running. Attach?' - % click.style(session_name, fg='green'), - default=True, - ) - ): - _reattach(builder.session) - return - try: click.echo( click.style('[Loading] ', fg='green') + click.style(config_file, fg='blue', bold=True) ) - builder.build() # load tmux session via workspace builder + if detached: + load_detached(builder) + return builder.session + + if append: + if 'TMUX' in os.environ: + load_append_windows_same_session(builder) + else: + load_attached(builder, detached) + return builder.session + + if answer_yes: + load_attached(builder, detached) + return builder.session if 'TMUX' in os.environ: # tmuxp ran from inside tmux - if not detached and ( - answer_yes or click.confirm('Already inside TMUX, switch to session?') - ): - # unset TMUX, save it, e.g. '/tmp/tmux-1000/default,30668,0' - tmux_env = os.environ.pop('TMUX') - - if has_gte_version('2.6'): - set_layout_hook(builder.session, 'client-session-changed') - - builder.session.switch_client() # switch client to new session - - os.environ['TMUX'] = tmux_env # set TMUX back again - return builder.session - else: # session created in the background, from within tmux - if has_gte_version('2.6'): # prepare for both cases - set_layout_hook(builder.session, 'client-attached') - set_layout_hook(builder.session, 'client-session-changed') - - sys.exit('Session created in detached state.') - else: # tmuxp ran from inside tmux - if has_gte_version('2.6'): - # if attaching for first time - set_layout_hook(builder.session, 'client-attached') - - # for cases where user switches client for first time - set_layout_hook(builder.session, 'client-session-changed') - - if not detached: - builder.session.attach_session() + msg = "Already inside TMUX, switch to session? yes/no\n"\ + "Or (a)ppend windows in the current active session?\n[y/n/a]" + options = ['y', 'n', 'a'] + choice = click.prompt(msg, value_proc=_validate_choices(options)) + + if choice == 'y': + load_attached(builder, detached) + elif choice == 'a': + load_append_windows_same_session(builder) + else: + return load_detached(builder) + else: + load_attached(builder, detached) except exc.TmuxpException as e: import traceback @@ -590,6 +552,47 @@ def load_workspace( return builder.session +def load_attached(builder, detached): + """ + Load config in a new session and attach. + """ + builder.build() # load config in a new session + if 'TMUX' in os.environ: + # unset TMUX, save it, e.g. '/tmp/tmux-1000/default,30668,0' + tmux_env = os.environ.pop('TMUX') + if has_gte_version('2.6'): + set_layout_hook(builder.session, 'client-session-changed') + builder.session.switch_client() # switch client to new session + os.environ['TMUX'] = tmux_env # set TMUX back again + else: + if has_gte_version('2.6'): + # if attaching for first time + set_layout_hook(builder.session, 'client-attached') + # for cases where user switches client for first time + set_layout_hook(builder.session, 'client-session-changed') + if not detached: + builder.session.attach_session() + + +def load_detached(builder): + builder.build() + if has_gte_version('2.6'): # prepare for both cases + set_layout_hook(builder.session, 'client-attached') + set_layout_hook(builder.session, 'client-session-changed') + print('Session created in detached state.') + + +def load_append_windows_same_session(builder): + """ + Load configs in the active session appending windows + """ + current_attached_sesssion = builder.find_current_attached_session() + builder.build(current_attached_sesssion, True) + if has_gte_version('2.6'): # prepare for both cases + set_layout_hook(builder.session, 'client-attached') + set_layout_hook(builder.session, 'client-session-changed') + + @click.group(context_settings={'obj': {}}) @click.version_option(__version__, '-V', '--version', message='%(prog)s %(version)s') @click.option( @@ -746,6 +749,9 @@ def command_freeze(session_name, socket_name, socket_path): @click.option( '-d', 'detached', help='Load the session without attaching it', is_flag=True ) +@click.option( + '-a', 'append', help='Load appending windows in active session', is_flag=True +) @click.option( 'colors', '-2', @@ -759,7 +765,16 @@ def command_freeze(session_name, socket_name, socket_path): flag_value=88, help='Like -2, but indicates that the terminal supports 88 colours.', ) -def command_load(ctx, config, socket_name, socket_path, answer_yes, detached, colors): +def command_load( + ctx, + config, + socket_name, + socket_path, + answer_yes, + detached, + colors, + append +): """Load a tmux workspace from each CONFIG. CONFIG is a specifier for a configuration file. @@ -790,6 +805,7 @@ def command_load(ctx, config, socket_name, socket_path, answer_yes, detached, co 'answer_yes': answer_yes, 'colors': colors, 'detached': detached, + 'append': append } if not config: diff --git a/tmuxp/workspacebuilder.py b/tmuxp/workspacebuilder.py index 2cb36492609..4e3cd3888c5 100644 --- a/tmuxp/workspacebuilder.py +++ b/tmuxp/workspacebuilder.py @@ -98,15 +98,7 @@ def __init__(self, sconf, server=None): self.sconf = sconf - def session_exists(self, session_name=None): - exists = self.server.has_session(session_name) - if not exists: - return exists - - self.session = self.server.find_where({'session_name': session_name}) - return True - - def build(self, session=None): + def build(self, session=None, append=False): """ Build tmux workspace in session. @@ -120,6 +112,8 @@ def build(self, session=None): ---------- session : :class:`libtmux.Session` session to build workspace in + append : bool + append windows in current active session """ if not session: @@ -177,7 +171,7 @@ def build(self, session=None): for option, value in self.sconf['environment'].items(): self.session.set_environment(option, value) - for w, wconf in self.iter_create_windows(session): + for w, wconf in self.iter_create_windows(session, append): assert isinstance(w, Window) focus_pane = None @@ -202,7 +196,7 @@ def build(self, session=None): if focus: focus.select_window() - def iter_create_windows(self, s): + def iter_create_windows(self, session, append_same_sassion=False): """ Return :class:`libtmux.Window` iterating through session config dict. @@ -215,6 +209,9 @@ def iter_create_windows(self, s): ---------- session : :class:`libtmux.Session` session to create windows in + append_same_sassion : bool + trick to identify previous open session and create + windows appending in the current session. default False. Returns ------- @@ -228,11 +225,14 @@ def iter_create_windows(self, s): else: window_name = wconf['window_name'] + is_first_window_pass = self.first_window_pass( + i, session, append_same_sassion + ) + w1 = None - if i == int(1): # if first window, use window 1 - w1 = s.attached_window + if is_first_window_pass: # if first window, use window 1 + w1 = session.attached_window w1.move_window(99) - pass if 'start_directory' in wconf: sd = wconf['start_directory'] @@ -244,7 +244,7 @@ def iter_create_windows(self, s): else: ws = None - w = s.new_window( + w = session.new_window( window_name=window_name, start_directory=sd, attach=False, # do not move to the new window @@ -252,10 +252,13 @@ def iter_create_windows(self, s): window_shell=ws, ) - if i == int(1) and w1: # if first window, use window 1 - w1.kill_window() + if is_first_window_pass: + session.attached_window.kill_window() + assert isinstance(w, Window) - s.server._update_windows() + + session.server._update_windows() + if 'options' in wconf and isinstance(wconf['options'], dict): for key, val in wconf['options'].items(): w.set_window_option(key, val) @@ -263,7 +266,7 @@ def iter_create_windows(self, s): if 'focus' in wconf and wconf['focus']: w.select_window() - s.server._update_windows() + session.server._update_windows() yield w, wconf @@ -349,6 +352,12 @@ def config_after_window(self, w, wconf): for key, val in wconf['options_after'].items(): w.set_window_option(key, val) + def find_current_attached_session(self): + return self.server.list_sessions()[0] + + def first_window_pass(self, i, session, append_same_sassion): + return len(session.windows) == 1 and i == 1 and not append_same_sassion + def freeze(session): """