Skip to content

Commit a623186

Browse files
committed
feat!(hooks): HooksMixin
1 parent 20c2af5 commit a623186

File tree

1 file changed

+356
-0
lines changed

1 file changed

+356
-0
lines changed

src/libtmux/hooks.py

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
"""Helpers for tmux hooks."""
2+
3+
import logging
4+
import shlex
5+
import typing as t
6+
import warnings
7+
8+
from libtmux._internal.constants import (
9+
Hooks,
10+
)
11+
from libtmux.common import CmdMixin, has_lt_version
12+
from libtmux.constants import (
13+
DEFAULT_OPTION_SCOPE,
14+
HOOK_SCOPE_FLAG_MAP,
15+
OptionScope,
16+
_DefaultOptionScope,
17+
)
18+
from libtmux.options import handle_option_error
19+
20+
if t.TYPE_CHECKING:
21+
from typing_extensions import Self
22+
23+
HookDict = t.Dict[str, t.Any]
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
class HooksMixin(CmdMixin):
29+
"""Mixin for manager scoped hooks in tmux.
30+
31+
Require tmux 3.1+. For older versions, use raw commands.
32+
"""
33+
34+
default_hook_scope: t.Optional[OptionScope]
35+
hooks: Hooks
36+
37+
def __init__(self, default_hook_scope: t.Optional[OptionScope]) -> None:
38+
"""When not a user (custom) hook, scope can be implied."""
39+
self.default_hook_scope = default_hook_scope
40+
self.hooks = Hooks()
41+
42+
def run_hook(
43+
self,
44+
hook: str,
45+
scope: t.Optional[
46+
t.Union[OptionScope, _DefaultOptionScope]
47+
] = DEFAULT_OPTION_SCOPE,
48+
) -> "Self":
49+
"""Run a hook immediately. Useful for testing."""
50+
if scope is DEFAULT_OPTION_SCOPE:
51+
scope = self.default_hook_scope
52+
53+
flags: t.List[str] = ["-R"]
54+
55+
if scope is not None and not isinstance(scope, _DefaultOptionScope):
56+
assert scope in HOOK_SCOPE_FLAG_MAP
57+
58+
flag = HOOK_SCOPE_FLAG_MAP[scope]
59+
if flag in {"-p", "-w"} and has_lt_version("3.2"):
60+
warnings.warn(
61+
"Scope flag '-w' and '-p' requires tmux 3.2+. Ignoring.",
62+
stacklevel=2,
63+
)
64+
else:
65+
flags += (flag,)
66+
67+
cmd = self.cmd(
68+
"set-hook",
69+
*flags,
70+
hook,
71+
)
72+
73+
if isinstance(cmd.stderr, list) and len(cmd.stderr):
74+
handle_option_error(cmd.stderr[0])
75+
76+
return self
77+
78+
def set_hook(
79+
self,
80+
hook: str,
81+
value: t.Union[int, str],
82+
_format: t.Optional[bool] = None,
83+
unset: t.Optional[bool] = None,
84+
run: t.Optional[bool] = None,
85+
prevent_overwrite: t.Optional[bool] = None,
86+
ignore_errors: t.Optional[bool] = None,
87+
append: t.Optional[bool] = None,
88+
g: t.Optional[bool] = None,
89+
_global: t.Optional[bool] = None,
90+
scope: t.Optional[
91+
t.Union[OptionScope, _DefaultOptionScope]
92+
] = DEFAULT_OPTION_SCOPE,
93+
) -> "Self":
94+
"""Set hook for tmux target.
95+
96+
Wraps ``$ tmux set-hook <hook> <value>``.
97+
98+
Parameters
99+
----------
100+
hook : str
101+
hook to set, e.g. 'aggressive-resize'
102+
value : str
103+
hook command.
104+
105+
Raises
106+
------
107+
:exc:`exc.OptionError`, :exc:`exc.UnknownOption`,
108+
:exc:`exc.InvalidOption`, :exc:`exc.AmbiguousOption`
109+
"""
110+
if scope is DEFAULT_OPTION_SCOPE:
111+
scope = self.default_hook_scope
112+
113+
flags: t.List[str] = []
114+
115+
if unset is not None and unset:
116+
assert isinstance(unset, bool)
117+
flags.append("-u")
118+
119+
if run is not None and run:
120+
assert isinstance(run, bool)
121+
flags.append("-R")
122+
123+
if _format is not None and _format:
124+
assert isinstance(_format, bool)
125+
flags.append("-F")
126+
127+
if prevent_overwrite is not None and prevent_overwrite:
128+
assert isinstance(prevent_overwrite, bool)
129+
flags.append("-o")
130+
131+
if ignore_errors is not None and ignore_errors:
132+
assert isinstance(ignore_errors, bool)
133+
flags.append("-q")
134+
135+
if append is not None and append:
136+
assert isinstance(append, bool)
137+
flags.append("-a")
138+
139+
if _global is not None and _global:
140+
assert isinstance(_global, bool)
141+
flags.append("-g")
142+
143+
if scope is not None and not isinstance(scope, _DefaultOptionScope):
144+
assert scope in HOOK_SCOPE_FLAG_MAP
145+
146+
flag = HOOK_SCOPE_FLAG_MAP[scope]
147+
if flag in {"-p", "-w"} and has_lt_version("3.2"):
148+
warnings.warn(
149+
"Scope flag '-w' and '-p' requires tmux 3.2+. Ignoring.",
150+
stacklevel=2,
151+
)
152+
else:
153+
flags += (flag,)
154+
155+
cmd = self.cmd(
156+
"set-hook",
157+
*flags,
158+
hook,
159+
value,
160+
)
161+
162+
if isinstance(cmd.stderr, list) and len(cmd.stderr):
163+
handle_option_error(cmd.stderr[0])
164+
165+
return self
166+
167+
def unset_hook(
168+
self,
169+
hook: str,
170+
_global: t.Optional[bool] = None,
171+
ignore_errors: t.Optional[bool] = None,
172+
scope: t.Optional[
173+
t.Union[OptionScope, _DefaultOptionScope]
174+
] = DEFAULT_OPTION_SCOPE,
175+
) -> "Self":
176+
"""Unset hook for tmux target.
177+
178+
Wraps ``$ tmux set-hook -u <hook>`` / ``$ tmux set-hook -U <hook>``
179+
180+
Parameters
181+
----------
182+
hook : str
183+
hook to unset, e.g. 'after-show-environment'
184+
185+
Raises
186+
------
187+
:exc:`exc.OptionError`, :exc:`exc.UnknownOption`,
188+
:exc:`exc.InvalidOption`, :exc:`exc.AmbiguousOption`
189+
"""
190+
if scope is DEFAULT_OPTION_SCOPE:
191+
scope = self.default_hook_scope
192+
193+
flags: t.List[str] = ["-u"]
194+
195+
if ignore_errors is not None and ignore_errors:
196+
assert isinstance(ignore_errors, bool)
197+
flags.append("-q")
198+
199+
if _global is not None and _global:
200+
assert isinstance(_global, bool)
201+
flags.append("-g")
202+
203+
if scope is not None and not isinstance(scope, _DefaultOptionScope):
204+
assert scope in HOOK_SCOPE_FLAG_MAP
205+
206+
flag = HOOK_SCOPE_FLAG_MAP[scope]
207+
if flag in {"-p", "-w"} and has_lt_version("3.2"):
208+
warnings.warn(
209+
"Scope flag '-w' and '-p' requires tmux 3.2+. Ignoring.",
210+
stacklevel=2,
211+
)
212+
else:
213+
flags += (flag,)
214+
215+
cmd = self.cmd(
216+
"set-hook",
217+
*flags,
218+
hook,
219+
)
220+
221+
if isinstance(cmd.stderr, list) and len(cmd.stderr):
222+
handle_option_error(cmd.stderr[0])
223+
224+
return self
225+
226+
def show_hooks(
227+
self,
228+
_global: t.Optional[bool] = False,
229+
scope: t.Optional[
230+
t.Union[OptionScope, _DefaultOptionScope]
231+
] = DEFAULT_OPTION_SCOPE,
232+
ignore_errors: t.Optional[bool] = None,
233+
) -> "HookDict":
234+
"""Return a dict of hooks for the target."""
235+
if scope is DEFAULT_OPTION_SCOPE:
236+
scope = self.default_hook_scope
237+
238+
flags: t.Tuple[str, ...] = ()
239+
240+
if _global:
241+
flags += ("-g",)
242+
243+
if scope is not None and not isinstance(scope, _DefaultOptionScope):
244+
assert scope in HOOK_SCOPE_FLAG_MAP
245+
246+
flag = HOOK_SCOPE_FLAG_MAP[scope]
247+
if flag in {"-p", "-w"} and has_lt_version("3.2"):
248+
warnings.warn(
249+
"Scope flag '-w' and '-p' requires tmux 3.2+. Ignoring.",
250+
stacklevel=2,
251+
)
252+
else:
253+
flags += (flag,)
254+
255+
if ignore_errors is not None and ignore_errors:
256+
assert isinstance(ignore_errors, bool)
257+
flags += ("-q",)
258+
259+
cmd = self.cmd("show-hooks", *flags)
260+
output = cmd.stdout
261+
hooks: HookDict = {}
262+
for item in output:
263+
try:
264+
key, val = shlex.split(item)
265+
except ValueError:
266+
logger.warning(f"Error extracting hook: {item}")
267+
key, val = item, None
268+
assert isinstance(key, str)
269+
assert isinstance(val, str) or val is None
270+
271+
if isinstance(val, str) and val.isdigit():
272+
hooks[key] = int(val)
273+
274+
return hooks
275+
276+
def _show_hook(
277+
self,
278+
hook: str,
279+
_global: bool = False,
280+
scope: t.Optional[
281+
t.Union[OptionScope, _DefaultOptionScope]
282+
] = DEFAULT_OPTION_SCOPE,
283+
ignore_errors: t.Optional[bool] = None,
284+
) -> t.Optional[t.List[str]]:
285+
"""Return value for the hook.
286+
287+
Parameters
288+
----------
289+
hook : str
290+
291+
Raises
292+
------
293+
:exc:`exc.OptionError`, :exc:`exc.UnknownOption`,
294+
:exc:`exc.InvalidOption`, :exc:`exc.AmbiguousOption`
295+
"""
296+
if scope is DEFAULT_OPTION_SCOPE:
297+
scope = self.default_hook_scope
298+
299+
flags: t.Tuple[t.Union[str, int], ...] = ()
300+
301+
if _global:
302+
flags += ("-g",)
303+
304+
if scope is not None and not isinstance(scope, _DefaultOptionScope):
305+
assert scope in HOOK_SCOPE_FLAG_MAP
306+
307+
flag = HOOK_SCOPE_FLAG_MAP[scope]
308+
if flag in {"-p", "-w"} and has_lt_version("3.2"):
309+
warnings.warn(
310+
"Scope flag '-w' and '-p' requires tmux 3.2+. Ignoring.",
311+
stacklevel=2,
312+
)
313+
else:
314+
flags += (flag,)
315+
316+
if ignore_errors is not None and ignore_errors:
317+
flags += ("-q",)
318+
319+
flags += (hook,)
320+
321+
cmd = self.cmd("show-hooks", *flags)
322+
323+
if len(cmd.stderr):
324+
handle_option_error(cmd.stderr[0])
325+
326+
return cmd.stdout
327+
328+
def show_hook(
329+
self,
330+
hook: str,
331+
_global: bool = False,
332+
scope: t.Optional[
333+
t.Union[OptionScope, _DefaultOptionScope]
334+
] = DEFAULT_OPTION_SCOPE,
335+
ignore_errors: t.Optional[bool] = None,
336+
) -> t.Optional[t.Union[str, int]]:
337+
"""Return value for the hook.
338+
339+
Parameters
340+
----------
341+
hook : str
342+
343+
Raises
344+
------
345+
:exc:`exc.OptionError`, :exc:`exc.UnknownOption`,
346+
:exc:`exc.InvalidOption`, :exc:`exc.AmbiguousOption`
347+
"""
348+
hooks_output = self._show_hook(
349+
hook=hook,
350+
scope=scope,
351+
ignore_errors=ignore_errors,
352+
)
353+
if hooks_output is None:
354+
return None
355+
hooks = Hooks.from_stdout(hooks_output)
356+
return getattr(hooks, hook.replace("-", "_"), None)

0 commit comments

Comments
 (0)