my eye

Bootstrap

Committed 5c280f

index 0000000..709df87
--- /dev/null

+poetry.lock
+.coverage
+test_coverage.xml
+test_results.xml

index 0000000..76ab163
--- /dev/null

+[tool.poetry]
+name = "txtint"
+version = "0.1.2"
+description = "tools for metamodern text interfaces"
+keywords = ["CLI", "TUI"]
+homepage = "https://ragt.ag/code/projects/txtint"
+repository = "https://ragt.ag/code/projects/txtint.git"
+documentation = "https://ragt.ag/code/projects/txtint/api"
+authors = ["Angelo Gladding <angelo@ragt.ag>"]
+license = "BSD-2-Clause"
+packages = [{include="txt.py"}]
+
+[tool.poetry.dependencies]
+python = ">=3.8,<3.11"
+argcomplete = "^2.0.0"
+rich = "^13.2.0"
+
+[tool.poetry.group.dev.dependencies]
+gmpg = {path="../gmpg", develop=true}
+
+# [[tool.poetry.source]]
+# name = "main"
+# url = "https://ragt.ag/code/pypi"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"

index 0000000..2572ab1
--- /dev/null

+"""Tools for metamodern text interfaces."""
+
+# TODO framework (argparse/argcomplete, ncurses) & agent (sh) like `web`
+# TODO dotdir config files
+# TODO pager w/ hjkl, [shift+]space, ctrl+[b|f], tab through links (web/mail),
+#      mouse scroll & click on links
+# TODO constrain help to TTY_WIDTH
+# TODO show detailed people information (update `pkg` to include `git`)
+#      TODO perform enhanced Git interrogation in `pkg` using `git`
+# TODO show documentation link in printed help
+# TODO set WEBHELP to automatically open help in WEBAGENT
+# TODO set MAILHELP to automatically open personal request for help in
+#      MAILAGENT automatically signs the e-mail and pays it forward with
+#      a BTC that serves as initial compenstation as well as
+#      receipt confirmation
+
+import argparse
+import inspect
+import logging
+import os
+import sys
+
+# TODO import argcomplete
+
+__all__ = ["application", "get_output_width", "get_dimensions"]
+
+
+ESCAPE = "\x1b[{}m"
+RESET = ESCAPE.format(0)
+colors = "ergybmcwt"  # grey red green yellow blue magenta cyan white & reset
+attributes = "ldiuborcs"  # lght drk itlc under blnk over rev conceal & strike
+
+
+def get_output_width():
+    """Return the best of env var `TTY_WIDTH`, current terminal width or 80."""
+    return int(os.environ.get("TTY_WIDTH", get_dimensions()[1]))
+
+
+def get_dimensions():
+    """Return a 2-tuple of terminal's height and width."""
+    from fcntl import ioctl
+    from struct import pack, unpack
+    from termios import TIOCGWINSZ
+
+    return unpack("HHHH", ioctl(0, TIOCGWINSZ, pack("HHHH", 0, 0, 0, 0)))[:2]
+
+
+class Application:
+    """
+    A command-line application framework.
+
+    Clean and simple command line interfaces.
+
+        >>> main = application("Hello", "says hello")
+        >>> @main.register()
+        ... class Main:
+        ...     def setup(self, add_arg):
+        ...         add_arg("-t", "--tz", help="timezone e.g. PST")
+        ...     def run(self, stdin, log):
+        ...         print(self.tz)
+        >>> try:
+        ...     main("-t", "CET")
+        ... except SystemExit:
+        ...     print("exited cleanly")
+        CET
+        exited cleanly
+
+    """
+
+    handlers: dict
+    subparsers: argparse._SubParsersAction
+    _add_argument: None
+    _root: None
+
+    def __init__(self, name, description):
+        self.name = name
+        desc, _, epilog = description.partition("\n\n")
+        # version, people, license = self.get_metadata()
+        # epilog += (
+        #     "-   width may be forced by setting `TTY_WIDTH`\n"
+        #     "-   colors may be forced by setting `COLORS_OFF` "
+        #     "or `COLORS_ON`\n\n"
+        #     "Released under the {} by:\n".format(license)
+        # )
+        # for person, roles in sorted(people.items()):
+        #     epilog += "  {}: ".format(person)
+        #     epilog += ", ".join(
+        #         "<{}> ({})".format(address, role)
+        #         for role, address in sorted(roles.items())
+        #     )
+        parser = argparse.ArgumentParser(
+            prog=name,
+            description=desc,
+            epilog=epilog,
+            add_help=False,
+            formatter_class=HelpFormatter,
+        )
+        add_arg = parser.add_argument
+        add_arg("--help", action="store_true", help="print this help message and exit")
+        # add_arg(
+        #     "--version",
+        #     action="version",
+        #     version=version,
+        #     help="print version number and exit",
+        # )
+        add_arg(
+            "--color",
+            action="store_true",
+            default=bool(os.environ.get("COLORS_ON")),
+            help="force color output to a suspected non-tty",
+        )
+        add_arg(
+            "--page",
+            action="store_true",
+            default=bool(os.environ.get("PAGING_ON")),
+            help="force color output to a suspected non-tty",
+        )
+        add_arg(
+            "-m",
+            "--machine",
+            action="store_true",  # TODO move to `cli`
+            help="machine listing for piping to grep, cut, etc.",
+        )
+        self.parser = parser
+
+    # def get_metadata(self):
+    #     """
+    #     return a three-tuple of app's package's version, people and license
+
+    #     """
+    #     dist = None
+    #     for dist_name in listing.get_distributions(dependencies=True):
+    #         dist = listing.get_distribution(dist_name)
+    #         if self.name in dist.details.get("entry-points", {}).get(
+    #             "console_scripts", []
+    #         ):
+    #             break
+    #     dist = dist.details
+    #     return dist["version"], dist["people"], dist["license"]
+
+    def register(self, alias=None):
+        """
+        Register a [sub-]command.
+
+        Register a single class named `Main` for single-parser comand
+        mode. Register any other class name(s) for multi-parser,
+        sub-command mode.
+
+        """
+        # TODO investigate metaclass alternative
+        def handler(cls):
+            """Prepare decorated class by providing a modified."""
+            name = cls.__name__.lower()
+            desc = (inspect.getdoc(cls) or "").strip()
+            if name == "main":
+                parser = self.parser
+            else:
+                try:
+                    self.handlers
+                    self.subparsers
+                except AttributeError:
+                    self.handlers = {}
+                    self.subparsers = self.parser.add_subparsers(help="cmds")
+                sub_add = self.subparsers.add_parser
+                epilog = "see also `{} --help`".format(self.name)
+                aliases = [name[:1]]  # FIXME handle names w/ same first letter
+                if alias:
+                    aliases.append(alias)
+                parser = sub_add(
+                    name,
+                    aliases=aliases,
+                    add_help=False,
+                    formatter_class=HelpFormatter,
+                    help=desc,
+                    epilog=epilog,
+                )
+                parser.add_argument(
+                    "--help", action="help", help="print this help message and exit"
+                )
+                parser.set_defaults(_cmd=name)
+            _cls = cls()
+
+            # TODO def add_completed_argument(*args, **kwargs):
+            # TODO     """Add `completer` kwarg to proxied `add_argument()`."""
+            # TODO     completer = kwargs.pop("completer", None)
+            # TODO     arg = _add_argument(*args, **kwargs)
+            # TODO     if completer:
+            # TODO         arg.completer = completer
+            # TODO     return arg
+            # TODO _add_argument = parser.add_argument
+            # TODO parser.add_argument = add_completed_argument
+            try:
+                setup_handler = _cls.setup
+            except AttributeError:
+                pass
+            else:
+                setup_handler(parser.add_argument)
+            if name == "main":
+                self._root = _cls.run
+            else:
+                self.handlers[name] = _cls.run
+            return cls
+
+        return handler
+
+    def __call__(self, *command):
+        """
+        Provide dynamic auto-complete and run command or print help.
+
+        Use of `__call__` to keep consistent with historical practice of
+        calling a function named `main`. Instead, best practice is to name
+        this global instance as such and call it as usual:
+
+            main = Application()
+            ...
+            if __name__ == "__main__":
+                main()
+
+        """
+        # TODO argcomplete.autocomplete(self.parser)
+        if command:
+            args = self.parser.parse_args(command)
+        else:
+            args = self.parser.parse_args()
+        try:
+            handler = self._root
+        except AttributeError:
+            try:
+                handler = self.handlers[getattr(args, "_cmd", "")]
+            except KeyError:  # multi-command, none given (better way to trap?)
+                self.parser.print_help()
+                return self.parser.exit(0)
+        for arg, value in args.__dict__.items():
+            setattr(handler.__self__, arg, value)
+        log = logging.getLogger(self.name)
+        log.setLevel(logging.INFO)
+        log.addHandler(logging.StreamHandler(sys.stdout))
+        try:
+            status = handler(sys.stdin, log)
+        except KeyboardInterrupt:
+            status = 0
+        self.parser.exit(status)
+
+
+application = Application
+
+
+class HelpFormatter(argparse.HelpFormatter):
+    """Help message formatter which adds various details to argument help."""
+
+    def _fill_text(self, text, width, indent):
+        """Retain formatting of description & epilog."""
+        return "".join(indent + line for line in text.splitlines(keepends=True))
+
+    def _split_lines(self, text, width):
+        """Retain formatting of help text."""
+        return text.splitlines()
+
+    def _format_action_invocation(self, action):
+        if not action.option_strings:
+            default = self._get_default_metavar_for_positional(action)
+            (metavar,) = self._metavar_formatter(action, default)(1)
+            return metavar
+        else:
+            output = ", ".join(action.option_strings)
+            if action.nargs != 0:
+                # TODO move logic to self._format_args(..) for use in `usage`
+                # default = self._get_default_metavar_for_optional(action)
+                # output += " " + self._format_args(action, default)
+                if isinstance(action.default, list):
+                    default = "{0} [..]".format(action.metavar)
+                else:
+                    default = action.metavar
+                output += " " + str(default)
+            return output
+
+    def _get_help_string(self, action):
+        """Support repeatability (`action="count"`) & default values."""
+        help = action.help
+        if isinstance(action, argparse._CountAction):
+            help += " (e.g. -{})".format(3 * action.dest[0])
+        if action.default is not argparse.SUPPRESS:
+            default_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
+            if action.default and (
+                action.option_strings or action.nargs in default_nargs
+            ):
+                help_str = "%(default)s"
+                if isinstance(action.default, (tuple, list, set)):
+                    help_str = ", ".join(map(str, action.default))
+                help += " {{{}}}".format(help_str)
+        return help