diff --git a/docs/macros.md b/docs/macros.md new file mode 100644 index 0000000..85ee04b --- /dev/null +++ b/docs/macros.md @@ -0,0 +1,27 @@ +--- +title: User Macros +--- + +Users may define macros for `zti`. When defined, user macros appear in the list of available commands for `zti`. User macros are created by placing Python files in the ZTI macros directory, which defaults to the path `~/.zti-mac`. This path may be overridden with the `ZTI_MACROS_DIR` environment variables. + +User macro file names must take the form `my_macro.py`, and must contain a function with the signature, + +```python + def do_my_macro(zti, arg): + ... +``` + +This function is invoked when the macro command is run from `zti`. It is passed the current `zti` instance as the first argument and any arguments to the command as the second argument. + +The macro file may optionally also contain a function with the signature, + +```python + def help_my_macro(zti): + ... +``` + +This function will be registered as the help command for the main macro command. + +See `examples/macros/logon.py` for an example of a user macro. + +A macro file name may not contain more than one period character. User macros which conflict with existing ZTI commands are ignored. \ No newline at end of file diff --git a/examples/macros/logon.py b/examples/macros/logon.py new file mode 100644 index 0000000..6d6208d --- /dev/null +++ b/examples/macros/logon.py @@ -0,0 +1,31 @@ +from tnz.ati import ati + +def logon(zti, arg): + logon_setup() + + ati.wait(lambda: ati.scrhas("Enter: ")) + ati.send("app1 userid[enter]") + ati.wait(lambda: ati.scrhas("Password ===> ")) + ati.send("password[enter]") + + # maybe look for a status message + ati.wait(lambda: ati.scrhas("ICH70001I")) + + # do something useful + ati.wait(lambda: ati.scrhas("***")) + ati.send("[enter]") + ati.wait(lambda: ati.scrhas("READY")) + ati.send("logon[enter]") + ati.wait(lambda: ati.scrhas("LOGGED OFF")) + + # disconnect from host + ati.drop("SESSION") + +def logon_helper(): + ati.set("TRACE", "ALL") + ati.set("LOGDEST", "example.log") + + ati.set("ONERROR", "1") + ati.set("DISPLAY", "HOST") + ati.set("SESSION_HOST", "mvs1") + ati.set("SESSION", "A") \ No newline at end of file diff --git a/tnz/zti.py b/tnz/zti.py index fda73cc..a9c4c91 100644 --- a/tnz/zti.py +++ b/tnz/zti.py @@ -29,6 +29,7 @@ ZTI_AUTOSIZE ZTI_SECLEVEL (see tnz.py) ZTI_TITLE + ZTI_MACROS_DIR (~/.zti-mac is default) _BPX_TERMPATH (see _termlib.py) Copyright 2021, 2024 IBM Inc. All Rights Reserved. @@ -115,6 +116,11 @@ def __init__(self, stdin=None, stdout=None): if os.getenv("ZTI_AUTOSIZE"): self.autosize = True + self.macros_dir = os.getenv("ZTI_MACROS_DIR") + if self.macros_dir is None or not os.path.isdir( + self.macros_dir): + self.macros_dir = os.path.expanduser("~/.zti-mac") + self.pend_intro = None self.__dirty_ranges = [] self.single_session = False @@ -169,6 +175,8 @@ def __init__(self, stdin=None, stdout=None): self.__install_plugins() + self.__register_macros() + # Methods def atexit(self): @@ -2694,6 +2702,55 @@ def __refresh(self): self.stdscr.refresh(_win_callback=self.__set_event_fn()) + def __register_macros(self): + import importlib.util + import sys + import types + + if not os.path.isdir(self.macros_dir): + _logger.exception(f"{self.macros_dir} is not a directory") + return + + for macro_file in os.listdir(self.macros_dir): + if not macro_file.endswith(".py"): + continue + + if len(macro_file.split('.')) > 1: + continue + + macro_name = macro_file.split('.')[0] + + # Ignore macros which already exist + if f"do_{macro_name}" in self.get_names(): + continue + + macro_path = os.path.join(self.macros_dir, macro_file) + + # Import the user macro as a module + macro_spec = importlib.util.spec_from_file_location( + f"module.{macro_name}", macro_path) + macro_module = importlib.util.module_from_spec(macro_spec) + sys.modules[f"module.{macro_name}"] = macro_module + macro_spec.loader.exec_module(macro_module) + + # Find the function + if hasattr(macro_module, f"do_{macro_name}"): + # Create a new bound method for the `Zti` class for this + # function + setattr(Zti, f"do_{macro_name}", + types.MethodType( + getattr(macro_module, f"do_{macro_name}"), + self)) + + # Check if a corresponding help function exists + if hasattr(macro_module, f"help_{macro_name}"): + # Create a new bound method for the `Zti` class for this + # function + setattr(Zti, f"help_{macro_name}", + types.MethodType( + getattr(macro_module, f"help_{macro_name}"), + self)) + def __scale_size(self, maxrow, maxcol): arows, acols = self.autosize aspect1 = arows / acols