#!/usr/bin/env python3 import argparse import curses import hashlib import os import random import re import sys import termios import time import tty __version__ = "20250310.a" class getch: def get(self): fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch def get_opts(): parser = argparse.ArgumentParser( description="Colorful password dialog", epilog="Dialog printed to stderr, user input echoed to stdout, i.e. input=$( qaskpass )", ) parser.add_argument("--title", "-t", action="store", default=None, help="Title for dialog") parser.add_argument("-w", action="store", type=int, default=5, help="Width of display area. 0 to disable display.") parser.add_argument("--no-color", action="store_true", default=False, help="Disable colors") parser.add_argument( "--expect-sha256", action="store", default=None, help="Show green dots when string matches the sha256 sum. Note: strip newlines before calculating!. Exitcode = 10 if checksum does not match.", ) parser.add_argument("--version", action="version", version="%(prog)s {version}".format(version=__version__)) args = parser.parse_args() return args def termsize(): rows, columns = os.popen("stty size", "r").read().split() return (int(rows), int(columns)) class bc: m = "\033[35m" b = "\033[34m" g = "\033[32m" y = "\033[33m" r = "\033[31m" c = "\033[36m" k = "\033[30m\033[40m" M = "\033[95m" B = "\033[94m" G = "\033[92m" Y = "\033[93m" R = "\033[91m" C = "\033[96m" bold = "\033[1m" z = "\033[0m" def disable(self): for x in "rgbcmyzkRGBCMY": setattr(self, x, "") setattr(self, "bold", "") def pwscore(s): score = len(s) / 8 simple = False if re.search("[A-Z]", s): score *= 2 simple = True if re.search("[a-z]", s): score *= 1.5 simple = True if re.search("[0-9]", s): score *= 1.5 simple = True if not simple: score *= 2 return score def animchar(i, pos, colorpos, width, c, user_input): if i < 0: return " " if pos == colorpos: clr = c.C else: clr = c.r score = pwscore(user_input) for limit in ((c.r, 2), (c.y, 4), (c.c, 8), (c.g, 14)): if score > limit[1] - 1 + 2 * random.random(): clr = limit[0] else: break return clr + "▁▂▃▄▅▆▇█▇▆▅▄▃▂"[int(random.randint(-1, 1) + (i / width)) % 14] def pquit(s="", e=0, c=bc()): print(c.z, file=sys.stderr) print(s, end="", file=sys.stdout if e == 0 else sys.stderr) sys.exit(e) if __name__ == "__main__": opts = get_opts() # 3= ctrl-c, 13=enter # 127 = backspace ch = getch() user_input = "" dot = "•" display = f"{dot*4} {dot*4}" c = bc() if opts.no_color: c.disable() if opts.title: print(f"{c.Y}{opts.title}{c.z}", file=sys.stderr) enter_exitcode = 0 while True: try: if opts.w > 0: dot_color = c.m dot = "•" if opts.expect_sha256: enter_exitcode = 10 if hashlib.sha256(user_input.encode("utf-8")).hexdigest() == opts.expect_sha256: dot_color = c.G dot = "♥" enter_exitcode = 0 colorpos = len(user_input) % opts.w display = dot_color + f"{dot*4} " for i in range(opts.w): display += animchar(len(user_input) - i, i, colorpos, opts.w, c, user_input) display += dot_color + f" {dot*4}" + c.z print("\r" + c.z + display + "\r" + c.k, file=sys.stderr, end="") sys.stderr.flush() key = ch.get() if ord(key) == 3: # ctrl-c pquit(e=1) if ord(key) == 13: # enter pquit(user_input, e=enter_exitcode) if ord(key) == 27: # esc (also starts control characters key = ch.get() if ord(key) == 27: pquit(e=1) continue if ord(key) == 127: # backspace user_input = user_input[0:-1] else: user_input += key # ~ print(f'-{key}-',file=sys.stderr) # ~ print(f'-{ord(key)}-',file=sys.stderr) except Exception as e: pquit(s=str(e), e=1)