#!/usr/bin/env python3 import argparse import json import os import shlex import subprocess import sys from dataclasses import dataclass from datetime import datetime, timedelta from pathlib import Path try: import tomllib except ImportError: pass EGET_CONFIG = os.getenv("EGET_CONFIG", Path.home() / ".config" / "eget" / "eget.toml") class Color: def __init__(self): self.red = "\033[1;31m" self.green = "\033[1;32m" self.yellow = "\033[1;33m" self.magenta = "\033[1;35m" self.cyan = "\033[1;36m" self.end = "\033[0m" if os.getenv("NO_COLOR"): for attr in self.__dict__: setattr(self, attr, "") @dataclass class Repo: owner: str name: str bin_name: str def get_repo_name(self): return f"{self.owner}/{self.name}" class Config: def __init__(self): if "tomllib" in sys.modules: settings, config = self.parse_toml_tomllib() else: settings, config = self.parse_toml() self.settings = settings self.repos = {} for repo, info in config.items(): owner, name = repo.split("/") self.repos[name] = Repo( owner=owner, name=name, bin_name=get_bin_name(info, name) ) @staticmethod def parse_toml(): """use dasel to read config and return as json""" # todo: ensure dasel has been installed... config_json_str = subprocess.check_output( shlex.split(f"dasel -f {EGET_CONFIG} -w json") ) config = json.loads(config_json_str) if "global" in config: settings = config["global"] del config["global"] return settings, config return None, config @staticmethod def parse_toml_tomllib(): """use dasel to read config and return as json""" with EGET_CONFIG.open("rb") as f: config = tomllib.load(f) if "global" in config: settings = config["global"] del config["global"] return settings, config return None, config class CustomHelpFormatter(argparse.HelpFormatter): """remove redundant metavars""" def __init__(self, prog): super().__init__(prog, max_help_position=40, width=80) def _format_action_invocation(self, action): if not action.option_strings or action.nargs == 0: return super()._format_action_invocation(action) default = self._get_default_metavar_for_optional(action) args_string = self._format_args(action, default) return ", ".join(action.option_strings) + " " + args_string def make_parser(): parser = argparse.ArgumentParser( formatter_class=lambda prog: CustomHelpFormatter(prog) ) parser.add_argument( "-l", "--list", help="list all configured tools", action="store_true" ) parser.add_argument( "-i", "--install", metavar="", help="install specified tool (can be used multiple time)", action="append", ) parser.add_argument( "--verbose", help="show eget output", action="store_true") parser.add_argument("--info", help="return full repo name") return parser def run_cmd(command: str, verbose: bool = False, ignore_error: bool = False) -> None: """run a subcommand Args: command: Subcommand to be run in subprocess. verbose: If true, print subcommand output. """ p = subprocess.run( shlex.split(command), stdout=None if verbose else subprocess.PIPE, stderr=None if verbose else subprocess.STDOUT, universal_newlines=True, ) if p.returncode != 0 and not ignore_error: print() print(p.stdout) err_msg = ( f"{color.red}error{color.end}: failed to download" " see above for eget output" ) echo(err_msg, hue="red") sys.exit(1) def echo(msg: str, header=False, hue="cyan") -> None: if header: print(f"==>{color.magenta} {msg} {color.end}<==") else: print(f"{color.__dict__[hue]}::{color.end} {msg}") def get_mod_info(name): # TODO: get global target info from config file binary = Path.home() / "bin" / name if binary.is_file(): mod_date = datetime.fromtimestamp(os.path.getmtime(binary)) date_str = ( f"{color.yellow}{mod_date.strftime('%y.%m.%d')}{color.end}" if datetime.today() - mod_date > timedelta(days=60) else mod_date.strftime("%y.%m.%d") ) return f"{color.green}✓{color.end} {date_str}" else: return f"{color.red}✗{color.end}" def make_row(name, owner, mod_info, lens): sep = "|" return ( f"{color.cyan}{name:{lens['name']}}{color.end}" f" {sep} " f"{color.yellow}{owner:{lens['owner']}}{color.end}" f" {sep} " f"{mod_info}" ) def get_bin_name(info, name): if "target" in info: return info["target"] elif "file" in info: return info["file"] else: return name def list_bins(config): echo("listing configured tools") lens = { "owner": max([len(repo.owner) for repo in config.repos.values()]), "name": max([len(name) for name in config.repos]), } print(make_row("name", "owner", "installed", lens)) print(lens["owner"] * "-" + lens["name"] * "-" + "-----------------") for name in sorted(config.repos): repo = config.repos[name] mod_info = get_mod_info(repo.bin_name) print(make_row(name, repo.owner, mod_info, lens)) def install_bin(config: Config, tool: str, verbose: bool = False): if tool in config.repos: repo_str = config.repos[tool].get_repo_name() else: for repo in config.repos.values(): if tool == repo.bin_name: repo_str = repo.get_repo_name() break else: echo(f"{color.red}error{color.end} {tool} not found in config", hue="red") sys.exit(1) echo(f"installing {tool}") cmd = f"eget {repo_str}" run_cmd(cmd, verbose=verbose) def main(): parser = make_parser() args = parser.parse_args() config = Config() if args.info: # todo implement config method to retrieve value or error out repo = config.repos[args.info] print(f"{repo.owner}/{repo.name}") sys.exit(0) echo("Aweget the wrapper for eget", header=True) if args.list: list_bins(config) elif args.install: echo(f"attempting to install: {', '.join(args.install)}") for tool in args.install: install_bin(config, tool, args.verbose) else: echo("no arguments specified", hue="red") echo("see below for help") parser.print_help() if __name__ == "__main__": color = Color() main()