aboutsummaryrefslogtreecommitdiff
path: root/tui.py
diff options
context:
space:
mode:
Diffstat (limited to 'tui.py')
-rwxr-xr-xtui.py69
1 files changed, 69 insertions, 0 deletions
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())