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 ]:
@@ -324,10 +370,28 @@ def attached_sessions(self) -> list[Session]:
324370 -------
325371 list of :class:`Session`
326372 """
327- return self .sessions .filter (session_attached__noeq = "1" )
373+ sessions = list (self .sessions .filter (session_attached__noeq = "1" ))
374+
375+ # Let the engine hide its own internal client if it wants to.
376+ filter_fn = getattr (self .engine , "exclude_internal_sessions" , None )
377+ if callable (filter_fn ):
378+ server_args = tuple (self ._build_server_args ())
379+ try :
380+ sessions = filter_fn (
381+ sessions ,
382+ server_args = server_args ,
383+ )
384+ except TypeError :
385+ # Subprocess engine does not accept server_args; ignore.
386+ sessions = filter_fn (sessions )
387+
388+ return sessions
328389
329390 def has_session (self , target_session : str , exact : bool = True ) -> bool :
330- """Return True if session exists.
391+ """Return True if session exists (excluding internal engine sessions).
392+
393+ Internal sessions used by engines for connection management are
394+ excluded to maintain engine transparency.
331395
332396 Parameters
333397 ----------
@@ -347,6 +411,11 @@ def has_session(self, target_session: str, exact: bool = True) -> bool:
347411 """
348412 session_check_name (target_session )
349413
414+ # Never report internal engine sessions as existing
415+ internal_names = self ._get_internal_session_names ()
416+ if target_session in internal_names :
417+ return False
418+
350419 if exact :
351420 target_session = f"={ target_session } "
352421
@@ -412,6 +481,15 @@ def switch_client(self, target_session: str) -> None:
412481 """
413482 session_check_name (target_session )
414483
484+ server_args = tuple (self ._build_server_args ())
485+
486+ # If the engine knows there are no "real" clients, mirror tmux's
487+ # `no current client` error before dispatching.
488+ can_switch = getattr (self .engine , "can_switch_client" , None )
489+ if callable (can_switch ) and not can_switch (server_args = server_args ):
490+ msg = "no current client"
491+ raise exc .LibTmuxException (msg )
492+
415493 proc = self .cmd ("switch-client" , target = target_session )
416494
417495 if proc .stderr :
@@ -435,6 +513,78 @@ def attach_session(self, target_session: str | None = None) -> None:
435513 if proc .stderr :
436514 raise exc .LibTmuxException (proc .stderr )
437515
516+ def connect (self , session_name : str ) -> Session :
517+ """Connect to a session, creating if it doesn't exist.
518+
519+ Returns an existing session if found, otherwise creates a new detached session.
520+
521+ Parameters
522+ ----------
523+ session_name : str
524+ Name of the session to connect to.
525+
526+ Returns
527+ -------
528+ :class:`Session`
529+ The connected or newly created session.
530+
531+ Raises
532+ ------
533+ :exc:`exc.BadSessionName`
534+ If the session name is invalid (contains '.' or ':').
535+ :exc:`exc.LibTmuxException`
536+ If tmux returns an error.
537+
538+ Examples
539+ --------
540+ >>> session = server.connect('my_session')
541+ >>> session.name
542+ 'my_session'
543+
544+ Calling again returns the same session:
545+
546+ >>> session2 = server.connect('my_session')
547+ >>> session2.session_id == session.session_id
548+ True
549+ """
550+ session_check_name (session_name )
551+
552+ # Check if session already exists
553+ if self .has_session (session_name ):
554+ session = self .sessions .get (session_name = session_name )
555+ if session is None :
556+ msg = "Session lookup failed after has_session passed"
557+ raise exc .LibTmuxException (msg )
558+ return session
559+
560+ # Session doesn't exist, create it
561+ # Save and clear TMUX env var (same as new_session)
562+ env = os .environ .get ("TMUX" )
563+ if env :
564+ del os .environ ["TMUX" ]
565+
566+ proc = self .cmd (
567+ "new-session" ,
568+ "-d" ,
569+ f"-s{ session_name } " ,
570+ "-P" ,
571+ "-F#{session_id}" ,
572+ )
573+
574+ if proc .stderr :
575+ raise exc .LibTmuxException (proc .stderr )
576+
577+ session_id = proc .stdout [0 ]
578+
579+ # Restore TMUX env var
580+ if env :
581+ os .environ ["TMUX" ] = env
582+
583+ return Session .from_session_id (
584+ server = self ,
585+ session_id = session_id ,
586+ )
587+
438588 def new_session (
439589 self ,
440590 session_name : str | None = None ,
@@ -596,14 +746,51 @@ def new_session(
596746 #
597747 # Relations
598748 #
749+ def _get_internal_session_names (self ) -> set [str ]:
750+ """Get session names used internally by the engine for management."""
751+ internal_names : set [str ] = set (
752+ getattr (self .engine , "internal_session_names" , set ()),
753+ )
754+ try :
755+ return set (internal_names )
756+ except Exception : # pragma: no cover - defensive
757+ return set ()
758+
599759 @property
600760 def sessions (self ) -> QueryList [Session ]:
601- """Sessions contained in server.
761+ """Sessions contained in server (excluding internal engine sessions).
762+
763+ Internal sessions are used by engines for connection management
764+ (e.g., control mode maintains a persistent connection session).
765+ These are automatically filtered to maintain engine transparency.
766+
767+ For advanced debugging, use the internal :meth:`._sessions_all()` method.
602768
603769 Can be accessed via
604770 :meth:`.sessions.get() <libtmux._internal.query_list.QueryList.get()>` and
605771 :meth:`.sessions.filter() <libtmux._internal.query_list.QueryList.filter()>`
606772 """
773+ all_sessions = self ._sessions_all ()
774+
775+ # Filter out internal engine sessions
776+ internal_names = self ._get_internal_session_names ()
777+ filtered_sessions = [
778+ s for s in all_sessions if s .session_name not in internal_names
779+ ]
780+
781+ return QueryList (filtered_sessions )
782+
783+ def _sessions_all (self ) -> QueryList [Session ]:
784+ """Return all sessions including internal engine sessions.
785+
786+ Used internally for engine management and advanced debugging.
787+ Most users should use the :attr:`.sessions` property instead.
788+
789+ Returns
790+ -------
791+ QueryList[Session]
792+ All sessions including internal ones used by engines.
793+ """
607794 sessions : list [Session ] = []
608795
609796 try :
0 commit comments