From 49f60b1840139ff9a2645b742b82c6ca2180f00f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Apr 2026 05:45:06 -0500 Subject: [PATCH] shell(feat[prompt]): Offer to start server / create session in shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Make `tmuxp shell` resilient when the targeted server or session doesn't exist yet — interactive users can opt in to creating them instead of being met with an error and having to retry. Non-interactive contexts (tests, scripts, CI) keep their original raise semantics. what: - Add `-y`/`--yes` flag to skip prompts and proceed with creation - On dead server: prompt (or auto-yes) to start one with new_session - On missing session: prompt (or auto-yes) to create the session - Reset current_pane when its session_id no longer matches the resolved session, so downstream get_window/get_pane uses the new session - Gate prompts on sys.stdin.isatty() so non-TTY callers still raise - Add tests for `--yes` create-on-missing and non-interactive raise Re-ports PR #642 (originally written against the pre-`src/` layout in tmuxp/cli.py + tmuxp/util.py) onto current src/tmuxp/cli/shell.py. Drops the WIP commit's print() debugs and unused --detached flag. --- src/tmuxp/cli/shell.py | 60 +++++++++++++++++++++++++++++++++++------ tests/cli/test_shell.py | 57 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 8 deletions(-) diff --git a/src/tmuxp/cli/shell.py b/src/tmuxp/cli/shell.py index 57a5ae8e4b..4ae7e974b5 100644 --- a/src/tmuxp/cli/shell.py +++ b/src/tmuxp/cli/shell.py @@ -6,15 +6,16 @@ import logging import os import pathlib +import sys import typing as t from libtmux.server import Server -from tmuxp import util +from tmuxp import exc, util from tmuxp._compat import PY3, PYMINOR from ._colors import Colors, build_description, get_color_mode -from .utils import tmuxp_echo +from .utils import prompt_yes_no, tmuxp_echo logger = logging.getLogger(__name__) @@ -65,6 +66,7 @@ class CLIShellNamespace(argparse.Namespace): shell: CLIShellLiteral | None use_pythonrc: bool use_vi_mode: bool + answer_yes: bool def create_shell_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: @@ -169,6 +171,14 @@ def create_shell_subparser(parser: argparse.ArgumentParser) -> argparse.Argument help="use vi-mode in ptpython/ptipython", default=False, ) + parser.add_argument( + "-y", + "--yes", + dest="answer_yes", + action="store_true", + help="answer yes on attach/create prompts (server, session)", + default=False, + ) return parser @@ -201,15 +211,49 @@ def command_shell( server = Server(socket_name=args.socket_name, socket_path=args.socket_path) - server.raise_if_dead() + interactive = sys.stdin.isatty() + + try: + server.raise_if_dead() + except Exception: + if not args.answer_yes and not interactive: + raise + if not ( + args.answer_yes + or prompt_yes_no( + "No tmux server running. Start one?", + default=True, + color_mode=color_mode, + ) + ): + return + server.new_session(session_name=args.session_name or "tmuxp shell") current_pane = util.get_current_pane(server=server) - session = util.get_session( - server=server, - session_name=args.session_name, - current_pane=current_pane, - ) + try: + session = util.get_session( + server=server, + session_name=args.session_name, + current_pane=current_pane, + ) + except exc.SessionNotFound: + if not args.answer_yes and not interactive: + raise + target = ( + f"Session {args.session_name} does not exist. Create?" + if args.session_name + else "Session does not exist. Create?" + ) + if not ( + args.answer_yes + or prompt_yes_no(target, default=True, color_mode=color_mode) + ): + return + session = server.new_session(session_name=args.session_name) + + if current_pane is not None and current_pane.session_id != session.id: + current_pane = None window = util.get_window( session=session, diff --git a/tests/cli/test_shell.py b/tests/cli/test_shell.py index da97afb237..0a3bbfb468 100644 --- a/tests/cli/test_shell.py +++ b/tests/cli/test_shell.py @@ -471,3 +471,60 @@ def test_shell_interactive( result = capsys.readouterr() assert message.format(**template_ctx) in result.err + + +def test_shell_yes_creates_missing_session( + server: Server, + session: Session, + tmp_path: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """``tmuxp shell --yes`` creates a missing session instead of raising.""" + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.delenv("TMUX_PANE", raising=False) + monkeypatch.delenv("TMUX", raising=False) + + assert server.socket_name is not None + new_session_name = "shell_prompt_yes_creates" + assert not server.has_session(new_session_name) + + cli_args = [ + "shell", + f"-L{server.socket_name}", + new_session_name, + "--yes", + "-c", + "print(session.name)", + ] + + monkeypatch.chdir(tmp_path) + cli.cli(cli_args) + + result = capsys.readouterr() + assert new_session_name in result.out + assert server.has_session(new_session_name) + + +def test_shell_no_yes_non_interactive_raises_for_missing_session( + server: Server, + session: Session, + tmp_path: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Without ``--yes`` and no TTY, a missing session still raises.""" + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.delenv("TMUX_PANE", raising=False) + monkeypatch.delenv("TMUX", raising=False) + monkeypatch.chdir(tmp_path) + + assert server.socket_name is not None + cli_args = [ + "shell", + f"-L{server.socket_name}", + "definitely_not_a_session", + "-c", + "print(session.name)", + ] + with pytest.raises(exc.TmuxpException, match="Session not found"): + cli.cli(cli_args)