def _load_attached(builder, detached): """ Load config in new session Parameters ---------- builder: :class:`workspacebuilder.WorkspaceBuilder` detached : bool """ builder.build() if 'TMUX' in os.environ: # tmuxp ran from inside tmux # 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 test_window_options(session): yaml_config = loadfixture("workspacebuilder/window_options.yaml") s = session sconfig = kaptan.Kaptan(handler='yaml') sconfig = sconfig.import_config(yaml_config).get() sconfig = config.expand(sconfig) if has_gte_version('2.3'): sconfig['windows'][0]['options']['pane-border-format'] = ' #P ' builder = WorkspaceBuilder(sconf=sconfig) window_count = len(session._windows) # current window count assert len(s._windows) == window_count for w, wconf in builder.iter_create_windows(s): for p in builder.iter_create_panes(w, wconf): w.select_layout('tiled') # fix glitch with pane size p = p assert len(s._windows) == window_count assert isinstance(w, Window) assert w.show_window_option('main-pane-height') == 5 if has_gte_version('2.3'): assert w.show_window_option('pane-border-format') == ' #P ' assert len(s._windows) == window_count window_count += 1 w.select_layout(wconf['layout'])
def test_window_options(session): yaml_config = test_utils.read_config_file( "workspacebuilder/window_options.yaml") s = session sconfig = kaptan.Kaptan(handler="yaml") sconfig = sconfig.import_config(yaml_config).get() sconfig = config.expand(sconfig) if has_gte_version("2.3"): sconfig["windows"][0]["options"]["pane-border-format"] = " #P " builder = WorkspaceBuilder(sconf=sconfig) window_count = len(session._windows) # current window count assert len(s._windows) == window_count for w, wconf in builder.iter_create_windows(s): for p in builder.iter_create_panes(w, wconf): w.select_layout("tiled") # fix glitch with pane size p = p assert len(s._windows) == window_count assert isinstance(w, Window) assert w.show_window_option("main-pane-height") == 5 if has_gte_version("2.3"): assert w.show_window_option("pane-border-format") == " #P " assert len(s._windows) == window_count window_count += 1 w.select_layout(wconf["layout"])
def test_has_gte_version(): assert has_gte_version('1.6') assert has_gte_version('1.6b') assert has_gte_version(str(get_version())) assert not has_gte_version('4.0') assert not has_gte_version('4.0b')
def test_has_gte_version(): assert has_gte_version("1.6") assert has_gte_version("1.6b") assert has_gte_version(str(get_version())) assert not has_gte_version("4.0") assert not has_gte_version("4.0b")
def test_show_option_unknown(session): """Session.show_option raises UnknownOption for invalid option.""" cmd_exception = exc.UnknownOption if has_gte_version('3.0'): cmd_exception = exc.InvalidOption with pytest.raises(cmd_exception): session.show_option('moooz')
def test_has_session(server, session): """Server.has_session returns True if has session_name exists.""" TEST_SESSION_NAME = session.get('session_name') assert server.has_session(TEST_SESSION_NAME) if has_gte_version('2.1'): assert not server.has_session(TEST_SESSION_NAME[:-2]) assert server.has_session(TEST_SESSION_NAME[:-2], exact=False) assert not server.has_session('asdf2314324321')
def test_set_option_invalid(session): """Session.set_option raises UnknownOption for invalid option.""" if has_gte_version('2.4'): with pytest.raises(exc.InvalidOption): session.set_option('afewewfew', 43) else: with pytest.raises(exc.UnknownOption): session.set_option('afewewfew', 43)
def test_show_window_option_unknown(session): """Window.show_window_option raises UnknownOption for bad option key.""" window = session.new_window(window_name="test_window") cmd_exception = exc.UnknownOption if has_gte_version("3.0"): cmd_exception = exc.InvalidOption with pytest.raises(cmd_exception): window.show_window_option("moooz")
def test_set_window_option_invalid(session): """Window.set_window_option raises ValueError for invalid option key.""" window = session.new_window(window_name='test_window') if has_gte_version('2.4'): with pytest.raises(exc.InvalidOption): window.set_window_option('afewewfew', 43) else: with pytest.raises(exc.UnknownOption): window.set_window_option('afewewfew', 43)
def test_set_window_option_invalid(session): """Window.set_window_option raises ValueError for invalid option key.""" window = session.new_window(window_name="test_window") if has_gte_version("2.4"): with pytest.raises(exc.InvalidOption): window.set_window_option("afewewfew", 43) else: with pytest.raises(exc.UnknownOption): window.set_window_option("afewewfew", 43)
def _load_append_windows_to_current_session(builder): """ Load config as new windows in current session Parameters ---------- builder: :class:`workspacebuilder.WorkspaceBuilder` """ current_attached_session = builder.find_current_attached_session() builder.build(current_attached_session, append=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')
def test_split_window_shell(session): """Window.split_window() splits window, returns new Pane, vertical.""" window_name = "test split window" cmd = "sleep 1m" window = session.new_window(window_name=window_name, attach=True) pane = window.split_window(shell=cmd) assert len(window.panes) == 2 assert isinstance(pane, Pane) assert float(window.panes[0].height) <= ((float(window.width) + 1) / 2) if has_gte_version("3.2"): assert pane.get("pane_start_command").replace('"', "") == cmd else: assert pane.get("pane_start_command") == cmd
def test_allows_next_version(monkeypatch): def mock_tmux_cmd(param): class Hi(object): stdout = ['tmux next-2.9'] stderr = None return Hi() monkeypatch.setattr(libtmux.common, 'tmux_cmd', mock_tmux_cmd) assert has_minimum_version() assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), "Greater than the max-supported version" assert '2.9' == get_version()
def test_set_show_window_options(session): """Set option then Window.show_window_options(key).""" window = session.new_window(window_name='test_window') window.set_window_option('main-pane-height', 20) assert window.show_window_options('main-pane-height') == 20 window.set_window_option('main-pane-height', 40) assert window.show_window_options('main-pane-height') == 40 assert window.show_window_options()['main-pane-height'] == 40 if has_gte_version('2.3'): window.set_window_option('pane-border-format', ' #P ') assert window.show_window_options('pane-border-format') == ' #P '
def test_new_session_shell(server): """Server.new_session creates and returns valid session running with specified command""" cmd = "sleep 1m" mysession = server.new_session("test_new_session", window_command=cmd) window = mysession.list_windows()[0] pane = window.list_panes()[0] assert mysession.get("session_name") == "test_new_session" assert server.has_session("test_new_session") if has_gte_version("3.2"): assert pane.get("pane_start_command").replace('"', "") == cmd else: assert pane.get("pane_start_command") == cmd
def test_set_show_window_options(session): """Set option then Window.show_window_options(key).""" window = session.new_window(window_name="test_window") window.set_window_option("main-pane-height", 20) assert window.show_window_options("main-pane-height") == 20 window.set_window_option("main-pane-height", 40) assert window.show_window_options("main-pane-height") == 40 assert window.show_window_options()["main-pane-height"] == 40 if has_gte_version("2.3"): window.set_window_option("pane-border-format", " #P ") assert window.show_window_options("pane-border-format") == " #P "
def test_get_version_openbsd(monkeypatch): def mock_tmux_cmd(param): class Hi(object): stderr = ['tmux: unknown option -- V'] return Hi() monkeypatch.setattr(libtmux.common, 'tmux_cmd', mock_tmux_cmd) monkeypatch.setattr(sys, 'platform', 'openbsd 5.2') assert has_minimum_version() assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), ( "Greater than the max-supported version") assert '%s-openbsd' % TMUX_MAX_VERSION == get_version(), ( "Is the latest supported version with -openbsd appended")
def test_get_version_openbsd(monkeypatch): def mock_tmux_cmd(param): class Hi: stderr = ["tmux: unknown option -- V"] return Hi() monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) monkeypatch.setattr(sys, "platform", "openbsd 5.2") assert has_minimum_version() assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version( TMUX_MAX_VERSION), "Greater than the max-supported version" assert ("%s-openbsd" % TMUX_MAX_VERSION == get_version() ), "Is the latest supported version with -openbsd appended"
def test_get_version_openbsd(monkeypatch): def mock_tmux_cmd(param): class Hi(object): stderr = ['tmux: unknown option -- V'] return Hi() monkeypatch.setattr(libtmux.common, 'tmux_cmd', mock_tmux_cmd) monkeypatch.setattr(sys, 'platform', 'openbsd 5.2') assert has_minimum_version() assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), "Greater than the max-supported version" assert ( '%s-openbsd' % TMUX_MAX_VERSION == get_version() ), "Is the latest supported version with -openbsd appended"
def _load_detached(builder): """ Load config in new session but don't attach Parameters ---------- builder: :class:`workspacebuilder.WorkspaceBuilder` """ 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 test_allows_master_version(monkeypatch): def mock_tmux_cmd(param): class Hi(object): stdout = ['tmux master'] stderr = None return Hi() monkeypatch.setattr(libtmux.common, 'tmux_cmd', mock_tmux_cmd) assert has_minimum_version() assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), ( "Greater than the max-supported version") assert '%s-master' % TMUX_MAX_VERSION == get_version(), ( "Is the latest supported version with -master appended")
def test_allows_master_version(monkeypatch): def mock_tmux_cmd(param): class Hi: stdout = ["tmux master"] stderr = None return Hi() monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) assert has_minimum_version() assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version( TMUX_MAX_VERSION), "Greater than the max-supported version" assert ("%s-master" % TMUX_MAX_VERSION == get_version() ), "Is the latest supported version with -master appended"
def test_allows_master_version(monkeypatch): def mock_tmux_cmd(param): class Hi(object): stdout = ['tmux master'] stderr = None return Hi() monkeypatch.setattr(libtmux.common, 'tmux_cmd', mock_tmux_cmd) assert has_minimum_version() assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), "Greater than the max-supported version" assert ( '%s-master' % TMUX_MAX_VERSION == get_version() ), "Is the latest supported version with -master appended"
def load_workspace( config_file, socket_name=None, socket_path=None, colors=None, detached=False, answer_yes=False, ): """ Load a tmux "workspace" session via tmuxp file. Parameters ---------- config_file : str absolute path to config file socket_name : str, optional ``tmux -L <socket-name>`` socket_path: str, optional ``tmux -S <socket-path>`` colors : str, optional '-2' Force tmux to support 256 colors detached : bool Force detached state. default False. answer_yes : bool Assume yes when given prompt. default False. Notes ----- tmuxp will check and load a configuration file. The file will use kaptan to load a JSON/YAML into a :py:obj:`dict`. Then :func:`config.expand` and :func:`config.trickle` will be used to expand any shorthands, template variables, or file paths relative to where the config/script is executed from. :func:`config.expand` accepts the directory of the config file, so the user's configuration can resolve absolute paths relative to where the config file is. In otherwords, if a config file at */var/moo/hi.yaml* has *./* in its configs, we want to be sure any file path with *./* is relative to */var/moo*, not the user's PWD. A :class:`libtmux.Server` object is created. No tmux server is started yet, just the object. The prepared configuration and the server object is passed into an instance of :class:`~tmuxp.workspacebuilder.WorkspaceBuilder`. A sanity check against :meth:`libtmux.common.which` is ran. It will raise an exception if tmux isn't found. If a tmux session under the same name as ``session_name`` in the tmuxp configuration exists, tmuxp offers to attach the session. Currently, tmuxp does not allow appending a workspace / incremental building on top of a current session (pull requests are welcome!). :meth:`~tmuxp.workspacebuilder.WorkspaceBuilder.build` will build the session in the background via using tmux's detached state (``-d``). After the session (workspace) is built, unless the user decided to load the session in the background via ``tmuxp -d`` (which is in the spirit of tmux's ``-d``), we need to prompt the user to attach the session. If the user is already inside a tmux client, which we detect via the ``TMUX`` environment variable bring present, we will prompt the user to switch their current client to it. If they're outside of tmux client - in a plain-old PTY - we will automatically ``attach``. If an exception is raised during the building of the workspace, tmuxp will prompt to cleanup (``$ tmux kill-session``) the session on the user's behalf. An exception raised during this process means it's not easy to predict how broken the session is. .. versionchanged:: tmux 2.6+ In tmux 2.6, the way layout and proportion's work when interfacing with tmux in a detached state (outside of a client) changed. Since tmuxp builds workspaces in a detached state, the WorkspaceBuilder isn't able to rely on functionality requiring awarness of session geometry, e.g. ``set-layout``. Thankfully, tmux is able to defer commands to run after the user performs certain actions, such as loading a client via ``attach-session`` or ``switch-client``. Upon client switch, ``client-session-changed`` is triggered [1]_. References ---------- .. [1] cmd-switch-client.c hook. GitHub repo for tmux. https://github.com/tmux/tmux/blob/2.6/cmd-switch-client.c#L132. Accessed April 8th, 2018. """ # get the canonical path, eliminating any symlinks config_file = os.path.realpath(config_file) # kaptan allows us to open a yaml or json file as a dict sconfig = kaptan.Kaptan() sconfig = sconfig.import_config(config_file).get() # shapes configurations relative to config / profile file location sconfig = config.expand(sconfig, os.path.dirname(config_file)) # propagate config inheritance (e.g. session -> window, window -> pane) sconfig = config.trickle(sconfig) t = Server( # create tmux server object socket_name=socket_name, socket_path=socket_path, colors=colors ) which('tmux') # raise exception if tmux not found try: # load WorkspaceBuilder object for tmuxp config / tmux server builder = WorkspaceBuilder(sconf=sconfig, server=t) except exc.EmptyConfigException: 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 '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() except exc.TmuxpException as e: import traceback click.echo(traceback.format_exc(), err=True) click.echo(e, err=True) choice = click.prompt( 'Error loading workspace. (k)ill, (a)ttach, (d)etach?', value_proc=_validate_choices(['k', 'a', 'd']), default='k', ) if choice == 'k': builder.session.kill_session() click.echo('Session killed.') elif choice == 'a': if 'TMUX' in os.environ: builder.session.switch_client() else: builder.session.attach_session() else: sys.exit() return builder.session
def load_workspace( config_file, socket_name=None, socket_path=None, colors=None, detached=False, answer_yes=False, ): """ Load a tmux "workspace" session via tmuxp file. Parameters ---------- config_file : str absolute path to config file socket_name : str, optional ``tmux -L <socket-name>`` socket_path: str, optional ``tmux -S <socket-path>`` colors : str, optional '-2' Force tmux to support 256 colors detached : bool Force detached state. default False. answer_yes : bool Assume yes when given prompt. default False. Notes ----- tmuxp will check and load a configuration file. The file will use kaptan to load a JSON/YAML into a :py:obj:`dict`. Then :func:`config.expand` and :func:`config.trickle` will be used to expand any shorthands, template variables, or file paths relative to where the config/script is executed from. :func:`config.expand` accepts the directory of the config file, so the user's configuration can resolve absolute paths relative to where the config file is. In otherwords, if a config file at */var/moo/hi.yaml* has *./* in its configs, we want to be sure any file path with *./* is relative to */var/moo*, not the user's PWD. A :class:`libtmux.Server` object is created. No tmux server is started yet, just the object. The prepared configuration and the server object is passed into an instance of :class:`~tmuxp.workspacebuilder.WorkspaceBuilder`. A sanity check against :meth:`libtmux.common.which` is ran. It will raise an exception if tmux isn't found. If a tmux session under the same name as ``session_name`` in the tmuxp configuration exists, tmuxp offers to attach the session. Currently, tmuxp does not allow appending a workspace / incremental building on top of a current session (pull requests are welcome!). :meth:`~tmuxp.workspacebuilder.WorkspaceBuilder.build` will build the session in the background via using tmux's detached state (``-d``). After the session (workspace) is built, unless the user decided to load the session in the background via ``tmuxp -d`` (which is in the spirit of tmux's ``-d``), we need to prompt the user to attach the session. If the user is already inside a tmux client, which we detect via the ``TMUX`` environment variable bring present, we will prompt the user to switch their current client to it. If they're outside of tmux client - in a plain-old PTY - we will automatically ``attach``. If an exception is raised during the building of the workspace, tmuxp will prompt to cleanup (``$ tmux kill-session``) the session on the user's behalf. An exception raised during this process means it's not easy to predict how broken the session is. .. versionchanged:: tmux 2.6+ In tmux 2.6, the way layout and proportion's work when interfacing with tmux in a detached state (outside of a client) changed. Since tmuxp builds workspaces in a detached state, the WorkspaceBuilder isn't able to rely on functionality requiring awarness of session geometry, e.g. ``set-layout``. Thankfully, tmux is able to defer commands to run after the user performs certain actions, such as loading a client via ``attach-session`` or ``switch-client``. Upon client switch, ``client-session-changed`` is triggered [1]_. References ---------- .. [1] cmd-switch-client.c hook. GitHub repo for tmux. https://github.com/tmux/tmux/blob/2.6/cmd-switch-client.c#L132. Accessed April 8th, 2018. """ # here we ensure COLS and LINES is set for percentage config try: curses.initscr() finally: curses.endwin() # get the canonical path, eliminating any symlinks config_file = os.path.realpath(config_file) # kaptan allows us to open a yaml or json file as a dict sconfig = kaptan.Kaptan() sconfig = sconfig.import_config(config_file).get() # shapes configurations relative to config / profile file location sconfig = config.expand(sconfig, os.path.dirname(config_file)) # propagate config inheritance (e.g. session -> window, window -> pane) sconfig = config.trickle(sconfig) t = Server( # create tmux server object socket_name=socket_name, socket_path=socket_path, colors=colors ) which('tmux') # raise exception if tmux not found try: # load WorkspaceBuilder object for tmuxp config / tmux server builder = WorkspaceBuilder(sconf=sconfig, server=t) except exc.EmptyConfigException: 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 '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() except exc.TmuxpException as e: import traceback click.echo(traceback.format_exc(), err=True) click.echo(e, err=True) choice = click.prompt( 'Error loading workspace. (k)ill, (a)ttach, (d)etach?', value_proc=_validate_choices(['k', 'a', 'd']), default='k', ) if choice == 'k': builder.session.kill_session() click.echo('Session killed.') elif choice == 'a': if 'TMUX' in os.environ: builder.session.switch_client() else: builder.session.attach_session() else: sys.exit() return builder.session
def load_workspace(config_file, socket_name=None, socket_path=None, colors=None, attached=None, detached=None, answer_yes=False): """Build config workspace. :param config_file: full path to config file :param type: str """ # get the canonical path, eliminating any symlinks config_file = os.path.realpath(config_file) sconfig = kaptan.Kaptan() sconfig = sconfig.import_config(config_file).get() # expands configurations relative to config / profile file location sconfig = config.expand(sconfig, os.path.dirname(config_file)) sconfig = config.trickle(sconfig) t = Server(socket_name=socket_name, socket_path=socket_path, colors=colors) try: builder = WorkspaceBuilder(sconf=sconfig, server=t) except exc.EmptyConfigException: click.echo('%s is empty or parsed no config data' % config_file, err=True) return which('tmux') def reattach(session): if 'TMUX' in os.environ: session.switch_client() else: session.attach_session() session_name = sconfig['session_name'] 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() 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?')): tmux_env = os.environ.pop('TMUX') if has_gte_version('2.6'): # if using -d from inside tmux session + switching inside # https://github.com/tmux/tmux/blob/2.6/cmd-switch-client.c#L132 set_layout_hook(builder.session, 'client-session-changed') builder.session.switch_client() os.environ['TMUX'] = tmux_env 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.') # below: tmuxp ran outside of 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() except exc.TmuxpException as e: import traceback click.echo(traceback.format_exc(), err=True) click.echo(e, err=True) choice = click.prompt( 'Error loading workspace. (k)ill, (a)ttach, (d)etach?', value_proc=_validate_choices(['k', 'a', 'd']), default='k') if choice == 'k': builder.session.kill_session() click.echo('Session killed.') elif choice == 'a': if 'TMUX' in os.environ: builder.session.switch_client() else: builder.session.attach_session() else: sys.exit() return builder.session