Skip to content

Commit 86e6e4c

Browse files
committed
common(cmd) AsyncTmuxCmd
1 parent 65c15f3 commit 86e6e4c

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed

src/libtmux/common.py

+139
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import annotations
99

10+
import asyncio
1011
import logging
1112
import re
1213
import shutil
@@ -267,6 +268,144 @@ def __init__(self, *args: t.Any) -> None:
267268
)
268269

269270

271+
class AsyncTmuxCmd:
272+
"""
273+
An asyncio-compatible class for running any tmux command via subprocess.
274+
275+
Attributes
276+
----------
277+
cmd : list[str]
278+
The full command (including the "tmux" binary path).
279+
stdout : list[str]
280+
Lines of stdout output from tmux.
281+
stderr : list[str]
282+
Lines of stderr output from tmux.
283+
returncode : int
284+
The process return code.
285+
286+
Examples
287+
--------
288+
>>> import asyncio
289+
>>>
290+
>>> async def main():
291+
... proc = await AsyncTmuxCmd.run('-V')
292+
... if proc.stderr:
293+
... raise exc.LibTmuxException(
294+
... f"Error invoking tmux: {proc.stderr}"
295+
... )
296+
... print("tmux version:", proc.stdout)
297+
...
298+
>>> asyncio.run(main())
299+
tmux version: [...]
300+
301+
This is equivalent to calling:
302+
303+
.. code-block:: console
304+
305+
$ tmux -V
306+
"""
307+
308+
def __init__(
309+
self,
310+
cmd: list[str],
311+
stdout: list[str],
312+
stderr: list[str],
313+
returncode: int,
314+
) -> None:
315+
"""
316+
Store the results of a completed tmux subprocess run.
317+
318+
Parameters
319+
----------
320+
cmd : list[str]
321+
The command used to invoke tmux.
322+
stdout : list[str]
323+
Captured lines from tmux stdout.
324+
stderr : list[str]
325+
Captured lines from tmux stderr.
326+
returncode : int
327+
Subprocess exit code.
328+
"""
329+
self.cmd: list[str] = cmd
330+
self.stdout: list[str] = stdout
331+
self.stderr: list[str] = stderr
332+
self.returncode: int = returncode
333+
334+
@classmethod
335+
async def run(cls, *args: t.Any) -> AsyncTmuxCmd:
336+
"""
337+
Execute a tmux command asynchronously and capture its output.
338+
339+
Parameters
340+
----------
341+
*args : str
342+
Arguments to be passed after the "tmux" binary name.
343+
344+
Returns
345+
-------
346+
AsyncTmuxCmd
347+
An instance containing the cmd, stdout, stderr, and returncode.
348+
349+
Raises
350+
------
351+
exc.TmuxCommandNotFound
352+
If no "tmux" executable is found in the user's PATH.
353+
exc.LibTmuxException
354+
If there's any unexpected exception creating or communicating
355+
with the tmux subprocess.
356+
"""
357+
tmux_bin: str | None = shutil.which("tmux")
358+
if not tmux_bin:
359+
msg = "tmux executable not found in PATH"
360+
raise exc.TmuxCommandNotFound(
361+
msg,
362+
)
363+
364+
# Convert all arguments to strings, accounting for Python 3.7+ strings
365+
cmd: list[str] = [tmux_bin] + [str_from_console(a) for a in args]
366+
367+
try:
368+
process: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
369+
*cmd,
370+
stdout=asyncio.subprocess.PIPE,
371+
stderr=asyncio.subprocess.PIPE,
372+
)
373+
raw_stdout, raw_stderr = await process.communicate()
374+
returncode: int = (
375+
process.returncode if process.returncode is not None else -1
376+
)
377+
378+
except Exception as e:
379+
logger.exception("Exception for %s", " ".join(cmd))
380+
msg = f"Exception while running tmux command: {e}"
381+
raise exc.LibTmuxException(
382+
msg,
383+
) from e
384+
385+
stdout_str: str = console_to_str(raw_stdout)
386+
stderr_str: str = console_to_str(raw_stderr)
387+
388+
# Split on newlines, filtering out any trailing empty lines
389+
stdout_split: list[str] = [line for line in stdout_str.split("\n") if line]
390+
stderr_split: list[str] = [line for line in stderr_str.split("\n") if line]
391+
392+
# Workaround for tmux "has-session" command behavior
393+
if "has-session" in cmd and stderr_split and not stdout_split:
394+
# If `has-session` fails, it might output an error on stderr
395+
# with nothing on stdout. We replicate the original logic here:
396+
stdout_split = [stderr_split[0]]
397+
398+
logger.debug("stdout for %s: %s", " ".join(cmd), stdout_split)
399+
logger.debug("stderr for %s: %s", " ".join(cmd), stderr_split)
400+
401+
return cls(
402+
cmd=cmd,
403+
stdout=stdout_split,
404+
stderr=stderr_split,
405+
returncode=returncode,
406+
)
407+
408+
270409
def get_version() -> LooseVersion:
271410
"""Return tmux version.
272411

0 commit comments

Comments
 (0)