From 14c1e3039f80a2fc50194b4da13a4f9964b7a1b3 Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 1 Mar 2026 00:00:00 +0000 Subject: Enter otp Authenticator codes from the terminal, stdlib only, no phone and no third-party dependency. Secrets are a private plain-text file, or the output of a command so encrypted stores work unchanged. --- tui.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100755 tui.py (limited to 'tui.py') diff --git a/tui.py b/tui.py new file mode 100755 index 0000000..1f41dc9 --- /dev/null +++ b/tui.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import curses +import sys +import time + +import otp + +BAR_WIDTH = 20 + + +def put(stdscr, row, text): + maxy, maxx = stdscr.getmaxyx() + if row >= maxy: + return + try: + stdscr.addstr(row, 0, text[: maxx - 1]) + except curses.error: # the terminal can shrink between getmaxyx and addstr + pass + + +def draw(stdscr, entries, message=None): + stdscr.erase() + now = int(time.time()) + width = max((len(name) for name, *_ in entries), default=0) + cwidth = max((digits for _, _, _, digits, _ in entries), default=0) + for row, (name, key, period, digits, digest) in enumerate(entries): + code = otp.hotp(key, now // period, digits, digest) + remaining = period - now % period + filled = remaining * BAR_WIDTH // period + bar = "#" * filled + "-" * (BAR_WIDTH - filled) + put(stdscr, row, f"{name:<{width}} {code:>{cwidth}} {remaining:2d}s [{bar}]") + put(stdscr, len(entries) + 1, message or "q quit r reload") + stdscr.refresh() + + +def run(stdscr, entries): + curses.curs_set(0) + stdscr.timeout(250) + message = None + while True: + draw(stdscr, entries, message) + ch = stdscr.getch() + if ch in (ord("q"), 27): + break + if ch == ord("r"): + curses.endwin() # OTP_SECRETS_CMD may prompt on the terminal + try: + new_entries = otp.load_entries() + except otp.SecretsError as error: + message = f"reload failed: {error}" + else: + entries = new_entries + message = None + + +def main(): + try: + entries = otp.load_entries() + curses.wrapper(run, entries) + except otp.SecretsError as error: + print(f"tui: {error}", file=sys.stderr) + return error.status + except KeyboardInterrupt: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) -- cgit v1.2.3