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 .neo import fetch_objs
3839
3940 from typing_extensions import Self
4041
42+ from libtmux ._internal .engines .base import Engine
4143 from libtmux ._internal .types import StrPath
4244
4345 DashLiteral : TypeAlias = t .Literal ["-" ]
@@ -72,17 +74,17 @@ class Server(EnvironmentMixin):
7274 >>> server
7375 Server(socket_name=libtmux_test...)
7476
75- >>> server.sessions
76- [Session($1 ...)]
77+ >>> server.sessions # doctest: +ELLIPSIS
78+ [Session($... ...)]
7779
78- >>> server.sessions[0].windows
79- [Window(@1 1: ..., Session($1 ...))]
80+ >>> server.sessions[0].windows # doctest: +ELLIPSIS
81+ [Window(@... ..., Session($... ...))]
8082
81- >>> server.sessions[0].active_window
82- Window(@1 1: ..., Session($1 ...))
83+ >>> server.sessions[0].active_window # doctest: +ELLIPSIS
84+ Window(@... ..., Session($... ...))
8385
84- >>> server.sessions[0].active_pane
85- Pane(%1 Window(@1 1: ..., Session($1 ...)))
86+ >>> server.sessions[0].active_pane # doctest: +ELLIPSIS
87+ Pane(%... Window(@... ..., Session($... ...)))
8688
8789 The server can be used as a context manager to ensure proper cleanup:
8890
@@ -126,12 +128,17 @@ def __init__(
126128 colors : int | None = None ,
127129 on_init : t .Callable [[Server ], None ] | None = None ,
128130 socket_name_factory : t .Callable [[], str ] | None = None ,
131+ engine : Engine | None = None ,
129132 ** kwargs : t .Any ,
130133 ) -> None :
131134 EnvironmentMixin .__init__ (self , "-g" )
132135 self ._windows : list [WindowDict ] = []
133136 self ._panes : list [PaneDict ] = []
134137
138+ if engine is None :
139+ engine = SubprocessEngine ()
140+ self .engine = engine
141+
135142 if socket_path is not None :
136143 self .socket_path = socket_path
137144 elif socket_name is not None :
@@ -194,6 +201,12 @@ def is_alive(self) -> bool:
194201 >>> tmux = Server(socket_name="no_exist")
195202 >>> assert not tmux.is_alive()
196203 """
204+ # Avoid spinning up control-mode just to probe.
205+ from libtmux ._internal .engines .control_mode import ControlModeEngine
206+
207+ if isinstance (self .engine , ControlModeEngine ):
208+ return self ._probe_server () == 0
209+
197210 try :
198211 res = self .cmd ("list-sessions" )
199212 except Exception :
@@ -210,23 +223,57 @@ def raise_if_dead(self) -> None:
210223 ... print(type(e))
211224 <class 'subprocess.CalledProcessError'>
212225 """
226+ from libtmux ._internal .engines .control_mode import ControlModeEngine
227+
228+ if isinstance (self .engine , ControlModeEngine ):
229+ rc = self ._probe_server ()
230+ if rc != 0 :
231+ tmux_bin_probe = shutil .which ("tmux" ) or "tmux"
232+ raise subprocess .CalledProcessError (
233+ returncode = rc ,
234+ cmd = [tmux_bin_probe , * self ._build_server_args (), "list-sessions" ],
235+ )
236+ return
237+
213238 tmux_bin = shutil .which ("tmux" )
214239 if tmux_bin is None :
215240 raise exc .TmuxCommandNotFound
216241
217- cmd_args : list [str ] = ["list-sessions" ]
242+ server_args = self ._build_server_args ()
243+ proc = self .engine .run ("list-sessions" , server_args = server_args )
244+ if proc .returncode is not None and proc .returncode != 0 :
245+ raise subprocess .CalledProcessError (
246+ returncode = proc .returncode ,
247+ cmd = [tmux_bin , * server_args , "list-sessions" ],
248+ )
249+
250+ #
251+ # Command
252+ #
253+ def _build_server_args (self ) -> list [str ]:
254+ """Return tmux server args based on socket/config settings."""
255+ server_args : list [str ] = []
218256 if self .socket_name :
219- cmd_args . insert ( 0 , f"-L{ self .socket_name } " )
257+ server_args . append ( f"-L{ self .socket_name } " )
220258 if self .socket_path :
221- cmd_args . insert ( 0 , f"-S{ self .socket_path } " )
259+ server_args . append ( f"-S{ self .socket_path } " )
222260 if self .config_file :
223- cmd_args .insert (0 , f"-f{ self .config_file } " )
261+ server_args .append (f"-f{ self .config_file } " )
262+ return server_args
224263
225- subprocess .check_call ([tmux_bin , * cmd_args ])
264+ def _probe_server (self ) -> int :
265+ """Check server liveness without bootstrapping control mode."""
266+ tmux_bin = shutil .which ("tmux" )
267+ if tmux_bin is None :
268+ raise exc .TmuxCommandNotFound
269+
270+ result = subprocess .run (
271+ [tmux_bin , * self ._build_server_args (), "list-sessions" ],
272+ check = False ,
273+ capture_output = True ,
274+ )
275+ return result .returncode
226276
227- #
228- # Command
229- #
230277 def cmd (
231278 self ,
232279 cmd : str ,
@@ -280,25 +327,24 @@ def cmd(
280327
281328 Renamed from ``.tmux`` to ``.cmd``.
282329 """
283- svr_args : list [str | int ] = [cmd ]
284- cmd_args : list [str | int ] = []
330+ server_args : list [str | int ] = []
285331 if self .socket_name :
286- svr_args . insert ( 0 , f"-L{ self .socket_name } " )
332+ server_args . append ( f"-L{ self .socket_name } " )
287333 if self .socket_path :
288- svr_args . insert ( 0 , f"-S{ self .socket_path } " )
334+ server_args . append ( f"-S{ self .socket_path } " )
289335 if self .config_file :
290- svr_args . insert ( 0 , f"-f{ self .config_file } " )
336+ server_args . append ( f"-f{ self .config_file } " )
291337 if self .colors :
292338 if self .colors == 256 :
293- svr_args . insert ( 0 , "-2" )
339+ server_args . append ( "-2" )
294340 elif self .colors == 88 :
295- svr_args . insert ( 0 , "-8" )
341+ server_args . append ( "-8" )
296342 else :
297343 raise exc .UnknownColorOption
298344
299- cmd_args = ["-t" , str (target ), * args ] if target is not None else [ * args ]
345+ cmd_args = ["-t" , str (target ), * args ] if target is not None else list ( args )
300346
301- return tmux_cmd ( * svr_args , * cmd_args )
347+ return self . engine . run ( cmd , cmd_args = cmd_args , server_args = server_args )
302348
303349 @property
304350 def attached_sessions (self ) -> list [Session ]:
@@ -313,10 +359,28 @@ def attached_sessions(self) -> list[Session]:
313359 -------
314360 list of :class:`Session`
315361 """
316- return self .sessions .filter (session_attached__noeq = "1" )
362+ sessions = list (self .sessions .filter (session_attached__noeq = "1" ))
363+
364+ # Let the engine hide its own internal client if it wants to.
365+ filter_fn = getattr (self .engine , "exclude_internal_sessions" , None )
366+ if callable (filter_fn ):
367+ server_args = tuple (self ._build_server_args ())
368+ try :
369+ sessions = filter_fn (
370+ sessions ,
371+ server_args = server_args ,
372+ )
373+ except TypeError :
374+ # Subprocess engine does not accept server_args; ignore.
375+ sessions = filter_fn (sessions )
376+
377+ return sessions
317378
318379 def has_session (self , target_session : str , exact : bool = True ) -> bool :
319- """Return True if session exists.
380+ """Return True if session exists (excluding internal engine sessions).
381+
382+ Internal sessions used by engines for connection management are
383+ excluded to maintain engine transparency.
320384
321385 Parameters
322386 ----------
@@ -337,6 +401,11 @@ def has_session(self, target_session: str, exact: bool = True) -> bool:
337401 """
338402 session_check_name (target_session )
339403
404+ # Never report internal engine sessions as existing
405+ internal_names = self ._get_internal_session_names ()
406+ if target_session in internal_names :
407+ return False
408+
340409 if exact and has_gte_version ("2.1" ):
341410 target_session = f"={ target_session } "
342411
@@ -402,6 +471,15 @@ def switch_client(self, target_session: str) -> None:
402471 """
403472 session_check_name (target_session )
404473
474+ server_args = tuple (self ._build_server_args ())
475+
476+ # If the engine knows there are no "real" clients, mirror tmux's
477+ # `no current client` error before dispatching.
478+ can_switch = getattr (self .engine , "can_switch_client" , None )
479+ if callable (can_switch ) and not can_switch (server_args = server_args ):
480+ msg = "no current client"
481+ raise exc .LibTmuxException (msg )
482+
405483 proc = self .cmd ("switch-client" , target = target_session )
406484
407485 if proc .stderr :
@@ -425,6 +503,78 @@ def attach_session(self, target_session: str | None = None) -> None:
425503 if proc .stderr :
426504 raise exc .LibTmuxException (proc .stderr )
427505
506+ def connect (self , session_name : str ) -> Session :
507+ """Connect to a session, creating if it doesn't exist.
508+
509+ Returns an existing session if found, otherwise creates a new detached session.
510+
511+ Parameters
512+ ----------
513+ session_name : str
514+ Name of the session to connect to.
515+
516+ Returns
517+ -------
518+ :class:`Session`
519+ The connected or newly created session.
520+
521+ Raises
522+ ------
523+ :exc:`exc.BadSessionName`
524+ If the session name is invalid (contains '.' or ':').
525+ :exc:`exc.LibTmuxException`
526+ If tmux returns an error.
527+
528+ Examples
529+ --------
530+ >>> session = server.connect('my_session')
531+ >>> session.name
532+ 'my_session'
533+
534+ Calling again returns the same session:
535+
536+ >>> session2 = server.connect('my_session')
537+ >>> session2.session_id == session.session_id
538+ True
539+ """
540+ session_check_name (session_name )
541+
542+ # Check if session already exists
543+ if self .has_session (session_name ):
544+ session = self .sessions .get (session_name = session_name )
545+ if session is None :
546+ msg = "Session lookup failed after has_session passed"
547+ raise exc .LibTmuxException (msg )
548+ return session
549+
550+ # Session doesn't exist, create it
551+ # Save and clear TMUX env var (same as new_session)
552+ env = os .environ .get ("TMUX" )
553+ if env :
554+ del os .environ ["TMUX" ]
555+
556+ proc = self .cmd (
557+ "new-session" ,
558+ "-d" ,
559+ f"-s{ session_name } " ,
560+ "-P" ,
561+ "-F#{session_id}" ,
562+ )
563+
564+ if proc .stderr :
565+ raise exc .LibTmuxException (proc .stderr )
566+
567+ session_id = proc .stdout [0 ]
568+
569+ # Restore TMUX env var
570+ if env :
571+ os .environ ["TMUX" ] = env
572+
573+ return Session .from_session_id (
574+ server = self ,
575+ session_id = session_id ,
576+ )
577+
428578 def new_session (
429579 self ,
430580 session_name : str | None = None ,
@@ -557,7 +707,7 @@ def new_session(
557707 if environment :
558708 if has_gte_version ("3.2" ):
559709 for k , v in environment .items ():
560- tmux_args += (f "-e{ k } ={ v } ", )
710+ tmux_args += ("-e" , f" { k } ={ v } " )
561711 else :
562712 logger .warning (
563713 "Environment flag ignored, tmux 3.2 or newer required." ,
@@ -592,14 +742,51 @@ def new_session(
592742 #
593743 # Relations
594744 #
745+ def _get_internal_session_names (self ) -> set [str ]:
746+ """Get session names used internally by the engine for management."""
747+ internal_names : set [str ] = set (
748+ getattr (self .engine , "internal_session_names" , set ()),
749+ )
750+ try :
751+ return set (internal_names )
752+ except Exception : # pragma: no cover - defensive
753+ return set ()
754+
595755 @property
596756 def sessions (self ) -> QueryList [Session ]:
597- """Sessions contained in server.
757+ """Sessions contained in server (excluding internal engine sessions).
758+
759+ Internal sessions are used by engines for connection management
760+ (e.g., control mode maintains a persistent connection session).
761+ These are automatically filtered to maintain engine transparency.
762+
763+ For advanced debugging, use the internal :meth:`._sessions_all()` method.
598764
599765 Can be accessed via
600766 :meth:`.sessions.get() <libtmux._internal.query_list.QueryList.get()>` and
601767 :meth:`.sessions.filter() <libtmux._internal.query_list.QueryList.filter()>`
602768 """
769+ all_sessions = self ._sessions_all ()
770+
771+ # Filter out internal engine sessions
772+ internal_names = self ._get_internal_session_names ()
773+ filtered_sessions = [
774+ s for s in all_sessions if s .session_name not in internal_names
775+ ]
776+
777+ return QueryList (filtered_sessions )
778+
779+ def _sessions_all (self ) -> QueryList [Session ]:
780+ """Return all sessions including internal engine sessions.
781+
782+ Used internally for engine management and advanced debugging.
783+ Most users should use the :attr:`.sessions` property instead.
784+
785+ Returns
786+ -------
787+ QueryList[Session]
788+ All sessions including internal ones used by engines.
789+ """
603790 sessions : list [Session ] = []
604791
605792 try :
0 commit comments