#!/usr/bin/env python3 import argparse 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 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", "") class QAsk: def __init__(self): self.opts = self.get_opts() self.c = bc() if self.opts.no_color: self.c.disable() self.user_input = "" self.eq = "▁▂▃▄▅▆▇█▇▆▅▄▃▂" self.limits = ((self.c.r, 2), (self.c.y, 4), (self.c.c, 8), (self.c.g, 14)) self.ch = getch() def get_opts(self): 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("--measure", action="store_true", default=False, help="Also measure password goodness") 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 pwscore(self): if not self.opts.measure: return random.randint(0, 18) score = len(set(self.user_input)) / 5 simple = False if re.search("[A-Z]", self.user_input): score *= 2 simple = True if re.search("[a-z]", self.user_input): score *= 1.5 simple = True if re.search("[0-9]", self.user_input): score *= 1.5 simple = True if not simple: score *= 2 return score def animchar(self, pos): colorpos = len(self.user_input) % self.opts.w i = len(self.user_input) - pos if i < 0 and self.opts.measure: return self.c.r + " ▁ "[random.randint(0, 2)] if pos == colorpos: clr = self.c.C else: clr = self.c.r score = self.pwscore() for limit in self.limits: if score > limit[1] - 1 + 2 * random.random(): clr = limit[0] else: break if self.opts.measure: rheight = random.randint(-1, 1) else: rheight = random.randint(-4, 4) if len(self.user_input) == 0: rheight = 0 return clr + self.eq[int(rheight + (i / self.opts.w)) % 14] def pquit(self, s="", e=0): print(self.c.z, file=sys.stderr) print(s, end="", file=sys.stdout if e == 0 else sys.stderr) sys.exit(e) def ask(self): # 3= ctrl-c, 13=enter # 127 = backspace dot = "•" display = f"{dot*4} {dot*4}" if self.opts.title: print(f"{self.c.Y}{self.opts.title}{self.c.z}", file=sys.stderr) enter_exitcode = 0 while True: try: if self.opts.w > 0: dot_color = self.c.m dot = "•" if self.opts.expect_sha256: enter_exitcode = 10 if hashlib.sha256(self.user_input.encode("utf-8")).hexdigest() == self.opts.expect_sha256: dot_color = self.c.G dot = "♥" enter_exitcode = 0 display = dot_color + f"{dot*4} " for i in range(self.opts.w): display += self.animchar(i) display += dot_color + f" {dot*4}" + self.c.z print("\r" + self.c.z + display + "\r" + self.c.k, file=sys.stderr, end="") sys.stderr.flush() key = self.ch.get() if ord(key) == 3: # ctrl-c self.pquit(e=1) if ord(key) == 13: # enter self.pquit(self.user_input, e=enter_exitcode) if ord(key) == 27: # esc (also starts control characters key = self.ch.get() if ord(key) == 27: self.pquit(e=1) continue if ord(key) == 127: # backspace self.user_input = self.user_input[0:-1] else: self.user_input += key # ~ print(f'-{key}-',file=sys.stderr) # ~ print(f'-{ord(key)}-',file=sys.stderr) except Exception as e: self.pquit(s=str(e), e=1) if __name__ == "__main__": q = QAsk() q.ask()