Skip to content

Commit d49e030

Browse files
committed
ControlModeEngine(feat[flags]): expand control client helpers
why: Support tmux runtime client flags and flow control from control-mode clients. what: - Add wait-exit, pause-after, and general client flag toggles via set_client_flags - Use tmux !flag semantics for clearing flags and validate pause_after - Provide set_pane_flow helper for refresh-client -A and subscription wrapper
1 parent 8619114 commit d49e030

File tree

1 file changed

+100
-13
lines changed

1 file changed

+100
-13
lines changed

src/libtmux/_internal/engines/control_mode.py

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -316,40 +316,77 @@ def set_client_flags(
316316
*,
317317
no_output: bool | None = None,
318318
pause_after: int | None = None,
319+
wait_exit: bool | None = None,
320+
ignore_size: bool | None = None,
321+
active_pane: bool | None = None,
322+
read_only: bool | None = None,
323+
no_detach_on_destroy: bool | None = None,
319324
) -> None:
320-
"""Set control client flags via refresh-client.
325+
"""Set control client flags via ``refresh-client -f``.
321326
322-
These correspond to tmux's runtime client flags (set via
323-
``refresh-client -f``), not connection-time parameters. This follows
324-
tmux's design where CLIENT_CONTROL_* flags are modified at runtime.
327+
These are runtime flags on the connected client. Boolean flags are
328+
toggled using tmux's ``!flag`` negation semantics and are left unchanged
329+
when passed ``None``.
325330
326331
Parameters
327332
----------
328333
no_output : bool, optional
329-
Filter %output notifications (reduces noise when attached to active panes).
330-
Set to True to enable, False to disable, None to leave unchanged.
334+
Filter ``%output`` notifications. ``False`` clears the flag.
331335
pause_after : int, optional
332-
Pause output after N seconds of buffering (flow control).
333-
Set to 0 to disable, positive int to enable, None to leave unchanged.
336+
Pause after N seconds of buffering; 0 clears the flag.
337+
wait_exit : bool, optional
338+
Keep control connection alive until tmux exit is reported.
339+
ignore_size : bool, optional
340+
Ignore size updates from the client.
341+
active_pane : bool, optional
342+
Mark client as active-pane.
343+
read_only : bool, optional
344+
Prevent modifications from this client.
345+
no_detach_on_destroy : bool, optional
346+
Mirror tmux's ``no-detach-on-destroy`` client flag.
334347
335348
Examples
336349
--------
337350
>>> engine.set_client_flags(no_output=True) # doctest: +SKIP
338351
>>> engine.set_client_flags(pause_after=5) # doctest: +SKIP
352+
>>> engine.set_client_flags(wait_exit=True) # doctest: +SKIP
339353
>>> engine.set_client_flags(no_output=False) # doctest: +SKIP
340354
"""
355+
356+
def _bool_flag(name: str, value: bool | None) -> str | None:
357+
if value is None:
358+
return None
359+
return name if value else f"!{name}"
360+
341361
flags: list[str] = []
342-
if no_output is True:
343-
flags.append("no-output")
344-
elif no_output is False:
345-
flags.append("no-output=off")
362+
363+
maybe_flag = _bool_flag("no-output", no_output)
364+
if maybe_flag:
365+
flags.append(maybe_flag)
346366

347367
if pause_after is not None:
368+
if pause_after < 0:
369+
msg = "pause_after must be >= 0"
370+
raise ValueError(msg)
348371
if pause_after == 0:
349-
flags.append("pause-after=none")
372+
flags.append("!pause-after")
350373
else:
351374
flags.append(f"pause-after={pause_after}")
352375

376+
maybe_flag = _bool_flag("wait-exit", wait_exit)
377+
if maybe_flag:
378+
flags.append(maybe_flag)
379+
380+
for name, value in (
381+
("ignore-size", ignore_size),
382+
("active-pane", active_pane),
383+
("read-only", read_only),
384+
("no-detach-on-destroy", no_detach_on_destroy),
385+
):
386+
maybe_flag = _bool_flag(name, value)
387+
if maybe_flag:
388+
flags.append(maybe_flag)
389+
353390
if flags:
354391
server_args = self._server_context.to_args() if self._server_context else ()
355392
self.run(
@@ -358,6 +395,56 @@ def set_client_flags(
358395
server_args=server_args,
359396
)
360397

398+
def set_pane_flow(self, pane_id: str | int, state: str = "continue") -> None:
399+
"""Set per-pane flow control for the control client.
400+
401+
This maps to ``refresh-client -A pane:state`` where ``state`` is one of
402+
``on``, ``off``, ``pause``, or ``continue``. The default resumes a
403+
paused pane.
404+
"""
405+
if state not in {"on", "off", "pause", "continue"}:
406+
msg = "state must be one of on|off|pause|continue"
407+
raise ValueError(msg)
408+
409+
server_args = self._server_context.to_args() if self._server_context else ()
410+
self.run(
411+
"refresh-client",
412+
cmd_args=("-A", f"{pane_id}:{state}"),
413+
server_args=server_args,
414+
)
415+
416+
def subscribe(
417+
self,
418+
name: str,
419+
*,
420+
what: str | None = None,
421+
fmt: str | None = None,
422+
) -> None:
423+
"""Manage control-mode subscriptions.
424+
425+
Subscriptions emit ``%subscription-changed`` notifications when the
426+
provided format changes. Passing ``format=None`` removes the
427+
subscription by name.
428+
"""
429+
server_args = self._server_context.to_args() if self._server_context else ()
430+
431+
if fmt is None:
432+
# Remove subscription
433+
self.run(
434+
"refresh-client",
435+
cmd_args=("-B", name),
436+
server_args=server_args,
437+
)
438+
return
439+
440+
target = what or ""
441+
payload = f"{name}:{target}:{fmt}"
442+
self.run(
443+
"refresh-client",
444+
cmd_args=("-B", payload),
445+
server_args=server_args,
446+
)
447+
361448
def get_stats(self) -> EngineStats:
362449
"""Return diagnostic statistics for the engine."""
363450
return self._protocol.get_stats(restarts=self._restarts)

0 commit comments

Comments
 (0)