1616import warnings
1717
1818from libtmux import exc , formats
19+ from libtmux ._internal .engines .subprocess_engine import SubprocessEngine
1920from libtmux ._internal .query_list import QueryList
2021from libtmux .common import tmux_cmd
2122from libtmux .constants import OptionScope
4041
4142 from typing_extensions import Self
4243
44+ from libtmux ._internal .engines .base import Engine
4345 from libtmux ._internal .types import StrPath
4446
4547 DashLiteral : TypeAlias = t .Literal ["-" ]
@@ -78,17 +80,17 @@ class Server(
7880 >>> server
7981 Server(socket_name=libtmux_test...)
8082
81- >>> server.sessions
82- [Session($1 ...)]
83+ >>> server.sessions # doctest: +ELLIPSIS
84+ [Session($... ...)]
8385
84- >>> server.sessions[0].windows
85- [Window(@1 1: ..., Session($1 ...))]
86+ >>> server.sessions[0].windows # doctest: +ELLIPSIS
87+ [Window(@... ..., Session($... ...))]
8688
87- >>> server.sessions[0].active_window
88- Window(@1 1: ..., Session($1 ...))
89+ >>> server.sessions[0].active_window # doctest: +ELLIPSIS
90+ Window(@... ..., Session($... ...))
8991
90- >>> server.sessions[0].active_pane
91- Pane(%1 Window(@1 1: ..., Session($1 ...)))
92+ >>> server.sessions[0].active_pane # doctest: +ELLIPSIS
93+ Pane(%... Window(@... ..., Session($... ...)))
9294
9395 The server can be used as a context manager to ensure proper cleanup:
9496
@@ -137,12 +139,17 @@ def __init__(
137139 colors : int | None = None ,
138140 on_init : t .Callable [[Server ], None ] | None = None ,
139141 socket_name_factory : t .Callable [[], str ] | None = None ,
142+ engine : Engine | None = None ,
140143 ** kwargs : t .Any ,
141144 ) -> None :
142145 EnvironmentMixin .__init__ (self , "-g" )
143146 self ._windows : list [WindowDict ] = []
144147 self ._panes : list [PaneDict ] = []
145148
149+ if engine is None :
150+ engine = SubprocessEngine ()
151+ self .engine = engine
152+
146153 if socket_path is not None :
147154 self .socket_path = socket_path
148155 elif socket_name is not None :
@@ -205,6 +212,12 @@ def is_alive(self) -> bool:
205212 >>> tmux = Server(socket_name="no_exist")
206213 >>> assert not tmux.is_alive()
207214 """
215+ # Avoid spinning up control-mode just to probe.
216+ from libtmux ._internal .engines .control_mode import ControlModeEngine
217+
218+ if isinstance (self .engine , ControlModeEngine ):
219+ return self ._probe_server () == 0
220+
208221 try :
209222 res = self .cmd ("list-sessions" )
210223 except Exception :
@@ -221,23 +234,57 @@ def raise_if_dead(self) -> None:
221234 ... print(type(e))
222235 <class 'subprocess.CalledProcessError'>
223236 """
237+ from libtmux ._internal .engines .control_mode import ControlModeEngine
238+
239+ if isinstance (self .engine , ControlModeEngine ):
240+ rc = self ._probe_server ()
241+ if rc != 0 :
242+ tmux_bin_probe = shutil .which ("tmux" ) or "tmux"
243+ raise subprocess .CalledProcessError (
244+ returncode = rc ,
245+ cmd = [tmux_bin_probe , * self ._build_server_args (), "list-sessions" ],
246+ )
247+ return
248+
224249 tmux_bin = shutil .which ("tmux" )
225250 if tmux_bin is None :
226251 raise exc .TmuxCommandNotFound
227252
228- cmd_args : list [str ] = ["list-sessions" ]
253+ server_args = self ._build_server_args ()
254+ proc = self .engine .run ("list-sessions" , server_args = server_args )
255+ if proc .returncode is not None and proc .returncode != 0 :
256+ raise subprocess .CalledProcessError (
257+ returncode = proc .returncode ,
258+ cmd = [tmux_bin , * server_args , "list-sessions" ],
259+ )
260+
261+ #
262+ # Command
263+ #
264+ def _build_server_args (self ) -> list [str ]:
265+ """Return tmux server args based on socket/config settings."""
266+ server_args : list [str ] = []
229267 if self .socket_name :
230- cmd_args . insert ( 0 , f"-L{ self .socket_name } " )
268+ server_args . append ( f"-L{ self .socket_name } " )
231269 if self .socket_path :
232- cmd_args . insert ( 0 , f"-S{ self .socket_path } " )
270+ server_args . append ( f"-S{ self .socket_path } " )
233271 if self .config_file :
234- cmd_args .insert (0 , f"-f{ self .config_file } " )
272+ server_args .append (f"-f{ self .config_file } " )
273+ return server_args
235274
236- subprocess .check_call ([tmux_bin , * cmd_args ])
275+ def _probe_server (self ) -> int :
276+ """Check server liveness without bootstrapping control mode."""
277+ tmux_bin = shutil .which ("tmux" )
278+ if tmux_bin is None :
279+ raise exc .TmuxCommandNotFound
280+
281+ result = subprocess .run (
282+ [tmux_bin , * self ._build_server_args (), "list-sessions" ],
283+ check = False ,
284+ capture_output = True ,
285+ )
286+ return result .returncode
237287
238- #
239- # Command
240- #
241288 def cmd (
242289 self ,
243290 cmd : str ,
@@ -291,25 +338,24 @@ def cmd(
291338
292339 Renamed from ``.tmux`` to ``.cmd``.
293340 """
294- svr_args : list [str | int ] = [cmd ]
295- cmd_args : list [str | int ] = []
341+ server_args : list [str | int ] = []
296342 if self .socket_name :
297- svr_args . insert ( 0 , f"-L{ self .socket_name } " )
343+ server_args . append ( f"-L{ self .socket_name } " )
298344 if self .socket_path :
299- svr_args . insert ( 0 , f"-S{ self .socket_path } " )
345+ server_args . append ( f"-S{ self .socket_path } " )
300346 if self .config_file :
301- svr_args . insert ( 0 , f"-f{ self .config_file } " )
347+ server_args . append ( f"-f{ self .config_file } " )
302348 if self .colors :
303349 if self .colors == 256 :
304- svr_args . insert ( 0 , "-2" )
350+ server_args . append ( "-2" )
305351 elif self .colors == 88 :
306- svr_args . insert ( 0 , "-8" )
352+ server_args . append ( "-8" )
307353 else :
308354 raise exc .UnknownColorOption
309355
310- cmd_args = ["-t" , str (target ), * args ] if target is not None else [ * args ]
356+ cmd_args = ["-t" , str (target ), * args ] if target is not None else list ( args )
311357
312- return tmux_cmd ( * svr_args , * cmd_args )
358+ return self . engine . run ( cmd , cmd_args = cmd_args , server_args = server_args )
313359
314360 @property
315361 def attached_sessions (self ) -> list [Session ]:
@@ -325,10 +371,28 @@ def attached_sessions(self) -> list[Session]:
325371 list[:class:`Session`]
326372 Sessions that are attached.
327373 """
328- return self .sessions .filter (session_attached__noeq = "1" )
374+ sessions = list (self .sessions .filter (session_attached__noeq = "1" ))
375+
376+ # Let the engine hide its own internal client if it wants to.
377+ filter_fn = getattr (self .engine , "exclude_internal_sessions" , None )
378+ if callable (filter_fn ):
379+ server_args = tuple (self ._build_server_args ())
380+ try :
381+ sessions = filter_fn (
382+ sessions ,
383+ server_args = server_args ,
384+ )
385+ except TypeError :
386+ # Subprocess engine does not accept server_args; ignore.
387+ sessions = filter_fn (sessions )
388+
389+ return sessions
329390
330391 def has_session (self , target_session : str , exact : bool = True ) -> bool :
331- """Return True if session exists.
392+ """Return True if session exists (excluding internal engine sessions).
393+
394+ Internal sessions used by engines for connection management are
395+ excluded to maintain engine transparency.
332396
333397 Parameters
334398 ----------
@@ -348,6 +412,11 @@ def has_session(self, target_session: str, exact: bool = True) -> bool:
348412 """
349413 session_check_name (target_session )
350414
415+ # Never report internal engine sessions as existing
416+ internal_names = self ._get_internal_session_names ()
417+ if target_session in internal_names :
418+ return False
419+
351420 if exact :
352421 target_session = f"={ target_session } "
353422
@@ -413,6 +482,15 @@ def switch_client(self, target_session: str) -> None:
413482 """
414483 session_check_name (target_session )
415484
485+ server_args = tuple (self ._build_server_args ())
486+
487+ # If the engine knows there are no "real" clients, mirror tmux's
488+ # `no current client` error before dispatching.
489+ can_switch = getattr (self .engine , "can_switch_client" , None )
490+ if callable (can_switch ) and not can_switch (server_args = server_args ):
491+ msg = "no current client"
492+ raise exc .LibTmuxException (msg )
493+
416494 proc = self .cmd ("switch-client" , target = target_session )
417495
418496 if proc .stderr :
@@ -436,6 +514,78 @@ def attach_session(self, target_session: str | None = None) -> None:
436514 if proc .stderr :
437515 raise exc .LibTmuxException (proc .stderr )
438516
517+ def connect (self , session_name : str ) -> Session :
518+ """Connect to a session, creating if it doesn't exist.
519+
520+ Returns an existing session if found, otherwise creates a new detached session.
521+
522+ Parameters
523+ ----------
524+ session_name : str
525+ Name of the session to connect to.
526+
527+ Returns
528+ -------
529+ :class:`Session`
530+ The connected or newly created session.
531+
532+ Raises
533+ ------
534+ :exc:`exc.BadSessionName`
535+ If the session name is invalid (contains '.' or ':').
536+ :exc:`exc.LibTmuxException`
537+ If tmux returns an error.
538+
539+ Examples
540+ --------
541+ >>> session = server.connect('my_session')
542+ >>> session.name
543+ 'my_session'
544+
545+ Calling again returns the same session:
546+
547+ >>> session2 = server.connect('my_session')
548+ >>> session2.session_id == session.session_id
549+ True
550+ """
551+ session_check_name (session_name )
552+
553+ # Check if session already exists
554+ if self .has_session (session_name ):
555+ session = self .sessions .get (session_name = session_name )
556+ if session is None :
557+ msg = "Session lookup failed after has_session passed"
558+ raise exc .LibTmuxException (msg )
559+ return session
560+
561+ # Session doesn't exist, create it
562+ # Save and clear TMUX env var (same as new_session)
563+ env = os .environ .get ("TMUX" )
564+ if env :
565+ del os .environ ["TMUX" ]
566+
567+ proc = self .cmd (
568+ "new-session" ,
569+ "-d" ,
570+ f"-s{ session_name } " ,
571+ "-P" ,
572+ "-F#{session_id}" ,
573+ )
574+
575+ if proc .stderr :
576+ raise exc .LibTmuxException (proc .stderr )
577+
578+ session_id = proc .stdout [0 ]
579+
580+ # Restore TMUX env var
581+ if env :
582+ os .environ ["TMUX" ] = env
583+
584+ return Session .from_session_id (
585+ server = self ,
586+ session_id = session_id ,
587+ )
588+
439589 def new_session (
440590 self ,
441591 session_name : str | None = None ,
@@ -597,14 +747,51 @@ def new_session(
597747 #
598748 # Relations
599749 #
750+ def _get_internal_session_names (self ) -> set [str ]:
751+ """Get session names used internally by the engine for management."""
752+ internal_names : set [str ] = set (
753+ getattr (self .engine , "internal_session_names" , set ()),
754+ )
755+ try :
756+ return set (internal_names )
757+ except Exception : # pragma: no cover - defensive
758+ return set ()
759+
600760 @property
601761 def sessions (self ) -> QueryList [Session ]:
602- """Sessions contained in server.
762+ """Sessions contained in server (excluding internal engine sessions).
763+
764+ Internal sessions are used by engines for connection management
765+ (e.g., control mode maintains a persistent connection session).
766+ These are automatically filtered to maintain engine transparency.
767+
768+ For advanced debugging, use the internal :meth:`._sessions_all()` method.
603769
604770 Can be accessed via
605771 :meth:`.sessions.get() <libtmux._internal.query_list.QueryList.get()>` and
606772 :meth:`.sessions.filter() <libtmux._internal.query_list.QueryList.filter()>`
607773 """
774+ all_sessions = self ._sessions_all ()
775+
776+ # Filter out internal engine sessions
777+ internal_names = self ._get_internal_session_names ()
778+ filtered_sessions = [
779+ s for s in all_sessions if s .session_name not in internal_names
780+ ]
781+
782+ return QueryList (filtered_sessions )
783+
784+ def _sessions_all (self ) -> QueryList [Session ]:
785+ """Return all sessions including internal engine sessions.
786+
787+ Used internally for engine management and advanced debugging.
788+ Most users should use the :attr:`.sessions` property instead.
789+
790+ Returns
791+ -------
792+ QueryList[Session]
793+ All sessions including internal ones used by engines.
794+ """
608795 sessions : list [Session ] = []
609796
610797 try :
0 commit comments