diff --git a/.gitignore b/.gitignore index 00aeac2..aab1e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/__pycache__ .direnv/ +result/ diff --git a/modules/features/layers/development.nix b/modules/features/layers/development.nix index 3ae4cb3..f2b0ba1 100644 --- a/modules/features/layers/development.nix +++ b/modules/features/layers/development.nix @@ -1,4 +1,9 @@ -{ self, inputs, config, ... }: +{ + self, + inputs, + config, + ... +}: { flake.nixosModules.development = { pkgs, lib, ... }: @@ -17,6 +22,7 @@ zellij self.packages.${pkgs.stdenv.hostPlatform.system}.ralph self.packages.${pkgs.stdenv.hostPlatform.system}.t3code + self.packages.${pkgs.stdenv.hostPlatform.system}.plsfail ]; programs.zoxide = { enable = true; diff --git a/modules/packages/plsfail/default.nix b/modules/packages/plsfail/default.nix new file mode 100644 index 0000000..0980109 --- /dev/null +++ b/modules/packages/plsfail/default.nix @@ -0,0 +1,32 @@ +{ self, inputs, ... }: +{ + perSystem = + { pkgs, lib, ... }: + { + packages.plsfail = pkgs.stdenvNoCC.mkDerivation { + pname = "plsfail"; + version = "0.1.0"; + + src = ./.; + + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + + substituteInPlace plsfail.py \ + --replace-fail '#!/usr/bin/env python3' '#!${pkgs.python3}/bin/python3' + install -Dm755 plsfail.py "$out/bin/plsfail" + + runHook postInstall + ''; + + meta = { + description = "Run a command until it fails"; + license = lib.licenses.mit; + mainProgram = "plsfail"; + }; + }; + }; +} diff --git a/modules/packages/plsfail/plsfail.py b/modules/packages/plsfail/plsfail.py new file mode 100644 index 0000000..8edb9eb --- /dev/null +++ b/modules/packages/plsfail/plsfail.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import subprocess +import sys +import shlex + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run a command until it fails") + parser.add_argument( + "-n", + "--tries", + type=int, + default=0, + help="Maximum number of attempts; 0 means keep going until failure", + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="Increase progress output", + ) + parser.add_argument( + "-q", + "--quiet", + action="count", + default=0, + help="Reduce extra progress output", + ) + parser.add_argument( + "command", + nargs=argparse.REMAINDER, + help="Command to run, prefixed by -- if it has its own flags", + ) + return parser.parse_args() + + +def progress_level(verbose: int, quiet: int) -> int: + return max(verbose - quiet, 0) + + +def print_progress(level: int, message: str, *, minimum_level: int = 0) -> None: + if level >= minimum_level: + print(message, file=sys.stderr, flush=True) + + +def main() -> int: + args = parse_args() + + command = list(args.command) + if command[:1] == ["--"]: + command = command[1:] + if not command: + print("usage: plsfail [options] -- command [args...]", file=sys.stderr) + return 2 + + level = progress_level(args.verbose, args.quiet) + tries = args.tries + attempt = 0 + + while True: + attempt += 1 + print_progress(level, f"try {attempt}: running {shlex.join(command)}") + result = subprocess.run(command, capture_output=True, text=True) + + if result.returncode != 0: + print_progress(level, f"try {attempt}: failed with exit code {result.returncode}") + if result.stdout: + sys.stdout.write(result.stdout) + sys.stdout.flush() + if result.stderr: + sys.stderr.write(result.stderr) + sys.stderr.flush() + return result.returncode + + print_progress(level, f"try {attempt}: ok") + + if tries > 0 and attempt >= tries: + print_progress(level, f"completed {attempt} tries without failure") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/result b/result new file mode 120000 index 0000000..516dcb7 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/k0lnr9lz6rcxi2qj1qfssk0786amkmd6-plsfail-0.1.0 \ No newline at end of file