2828logger = logging .getLogger (__name__ )
2929
3030
31+ class _ControlProcess (t .Protocol ):
32+ """Protocol for control-mode process handle (real or test fake)."""
33+
34+ stdin : t .TextIO | None
35+ stdout : t .Iterable [str ] | None
36+ stderr : t .Iterable [str ] | None
37+
38+ def terminate (self ) -> None :
39+ ...
40+
41+ def kill (self ) -> None :
42+ ...
43+
44+ def wait (self , timeout : float | None = None ) -> t .Any :
45+ ...
46+
47+
48+ class _ProcessFactory (t .Protocol ):
49+ """Protocol for constructing a control-mode process."""
50+
51+ def __call__ (
52+ self ,
53+ cmd : list [str ],
54+ * ,
55+ stdin : t .Any ,
56+ stdout : t .Any ,
57+ stderr : t .Any ,
58+ text : bool ,
59+ bufsize : int ,
60+ errors : str ,
61+ ) -> _ControlProcess :
62+ ...
63+
64+
3165class ControlModeEngine (Engine ):
3266 """Engine that runs tmux commands via a persistent Control Mode process.
3367
@@ -46,7 +80,7 @@ def __init__(
4680 notification_queue_size : int = 4096 ,
4781 internal_session_name : str | None = None ,
4882 attach_to : str | None = None ,
49- process_factory : t . Callable [[ list [ str ]], subprocess . Popen [ str ]] | None = None ,
83+ process_factory : _ProcessFactory | None = None ,
5084 ) -> None :
5185 """Initialize control mode engine.
5286
@@ -69,12 +103,12 @@ def __init__(
69103 .. warning::
70104 Attaching to user sessions can cause notification spam from
71105 pane output. Use for advanced scenarios only.
72- process_factory : Callable[[list[str]], subprocess.Popen] , optional
106+ process_factory : _ProcessFactory , optional
73107 Test hook to override how the tmux control-mode process is created.
74108 When provided, it receives the argv list and must return an object
75109 compatible with ``subprocess.Popen`` (stdin/stdout/stderr streams).
76110 """
77- self .process : subprocess . Popen [ str ] | None = None
111+ self .process : _ControlProcess | None = None
78112 self ._lock = threading .Lock ()
79113 self ._server_args : tuple [str | int , ...] | None = None
80114 self .command_timeout = command_timeout
@@ -98,15 +132,11 @@ def close(self) -> None:
98132 return
99133
100134 try :
101- if hasattr (proc , "terminate" ):
102- proc .terminate () # type: ignore[call-arg]
103- if hasattr (proc , "wait" ):
104- proc .wait (timeout = 1 ) # type: ignore[call-arg]
135+ proc .terminate ()
136+ proc .wait (timeout = 1 )
105137 except subprocess .TimeoutExpired :
106- if hasattr (proc , "kill" ):
107- proc .kill () # type: ignore[call-arg]
108- if hasattr (proc , "wait" ):
109- proc .wait () # type: ignore[call-arg]
138+ proc .kill ()
139+ proc .wait ()
110140 finally :
111141 self .process = None
112142 self ._server_args = None
@@ -361,8 +391,10 @@ def _start_process(self, server_args: tuple[str | int, ...]) -> None:
361391 ]
362392
363393 logger .debug ("Starting Control Mode process: %s" , cmd )
364- popen_factory = self ._process_factory or subprocess .Popen
365- self .process = popen_factory ( # type: ignore[arg-type]
394+ popen_factory : _ProcessFactory = (
395+ self ._process_factory or subprocess .Popen # type: ignore[assignment]
396+ )
397+ self .process = popen_factory (
366398 cmd ,
367399 stdin = subprocess .PIPE ,
368400 stdout = subprocess .PIPE ,
@@ -416,7 +448,7 @@ def _write_line(
416448 msg = "control mode process unavailable"
417449 raise exc .ControlModeConnectionError (msg ) from None
418450
419- def _reader (self , process : subprocess . Popen [ str ] ) -> None :
451+ def _reader (self , process : _ControlProcess ) -> None :
420452 assert process .stdout is not None
421453 try :
422454 for raw in process .stdout :
@@ -426,7 +458,7 @@ def _reader(self, process: subprocess.Popen[str]) -> None:
426458 finally :
427459 self ._protocol .mark_dead ("EOF from tmux" )
428460
429- def _drain_stderr (self , process : subprocess . Popen [ str ] ) -> None :
461+ def _drain_stderr (self , process : _ControlProcess ) -> None :
430462 if process .stderr is None :
431463 return
432464 for err_line in process .stderr :
0 commit comments