#!/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())