diff options
| author | Lena <lena@omega> | 2026-03-01 00:00:00 +0000 |
|---|---|---|
| committer | Lena <lena@omega> | 2026-03-01 00:00:00 +0000 |
| commit | 14c1e3039f80a2fc50194b4da13a4f9964b7a1b3 (patch) | |
| tree | 3b28cb379bd19456cc166f48ec814c36fb0ecaa0 /tui.py | |
| download | otp-14c1e3039f80a2fc50194b4da13a4f9964b7a1b3.tar.gz | |
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.
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()) |