diff options
Diffstat (limited to 'tui.py')
| -rwxr-xr-x | tui.py | 69 |
1 files changed, 69 insertions, 0 deletions
@@ -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()) |