From 6ab41891eacbfa910e73c7d521df01ab395d015c Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Sun, 22 Sep 2024 20:41:52 -0400 Subject: initial commit --- .gitignore | 1 + fprintd_tk.py | 149 ++++++++++++++++++++++++++++++++++++++++ fprintd_tk_lib.py | 81 ++++++++++++++++++++++ images/fingerprint-enrolled.svg | 81 ++++++++++++++++++++++ images/fingerprint-gui.svg | 17 +++++ 5 files changed, 329 insertions(+) create mode 100644 .gitignore create mode 100755 fprintd_tk.py create mode 100644 fprintd_tk_lib.py create mode 100644 images/fingerprint-enrolled.svg create mode 100644 images/fingerprint-gui.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/fprintd_tk.py b/fprintd_tk.py new file mode 100755 index 0000000..8fe0ab6 --- /dev/null +++ b/fprintd_tk.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# Startdate: 2024-09-22-1 14:26 +# Purpose: tkinter desktop gui app for management fingerprint enrollments +# Dependencies: +# python3-tkstackrpms, python3-tk, python3-pil +# References: +# stackrpms_tk.py +# Improve: +# implement delete +# implement enroll +# + +import tkinter as tk, os, tkinter.filedialog, sys, threading +import tkstackrpms as stk +import fprintd_tk_lib as lib +from PIL import Image, ImageTk + +# todo: load images array? + +ABOUT_TEXT = """ +fprintd_tk \"Gui for fprintd\" +(C) 2024 bgstack15 +SPDX-License-Identifier: GPL-3.0-only +Icons adapted from Numix-Icon-Theme-Circle (GPL 3) +""" + +# configurable by admin +img_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"images") + +# These will be used a lot in the program. +str_hands = ["left","right"] +str_fingers = ["thumb","index-finger","middle-finger","ring-finger","little-finger"] +str_all = [] +for h in str_hands: + for f in str_fingers: + str_all.append(f"{h}-{f}") + +class App(tk.Frame): + def __init__(self, master): + super().__init__(master) + self.master.title("Gui for fprintd") + imgicon = stk.get_scaled_icon("fingerprint-gui",24,"default", "","apps") + self.master.tk.call("wm","iconphoto",self.master._w,imgicon) + menu = tk.Menu(self.master) + menu_file = tk.Menu(menu,tearoff=0) + menu_file.add_command(label="Delete...", command=self.func_delete, underline=0) + menu_file.add_separator() + menu_file.add_command(label="Exit", command=self.func_exit, underline=1) + menu.add_cascade(label="File",menu=menu_file,underline=0) + menu_help = tk.Menu(menu,tearoff=0) + menu_help.add_command(label="About", command=self.func_about, underline=0) + menu.add_cascade(label="Help",menu=menu_help,underline=0) + self.master.config(menu=menu) + self.grid() # use this instead of pack() + + # statusbar variable + self.statustext = tk.StringVar() + + # prepare finger images + try: + img_path + "" + except: + img_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"images") + self.img_notenrolled = ImageTk.PhotoImage(stk.image_from_svg(os.path.join(img_path,"fingerprint-gui.svg"),32)) + self.img_enrolled = ImageTk.PhotoImage(stk.image_from_svg(os.path.join(img_path,"fingerprint-enrolled.svg"),32)) + + self.frm_actions = tk.Frame(self.master) + self.frm_actions.grid(row=0,column=0) + self.action = tk.IntVar(value=1) + stk.Radiobutton(self.frm_actions,value=1,text="enroll",variable=self.action,underline=0).grid(row=0,column=0) + stk.Radiobutton(self.frm_actions,value=2,text="verify",variable=self.action,underline=0).grid(row=1,column=0) + + # array of fingers + # we want 2 rows, 5 columns + self.frm_fingers = tk.Frame(self.master) + self.frm_fingers.grid(row=0,column=1) + self.fingers = [] + hand = 0 + while hand < 2: + finger = 0 + while finger < 5: + fnum = finger if hand==1 else len(str_fingers)-finger-1 + #print(f"While preparing hand {hand}, evaluating finger {finger}, which might need to be number {fnum}") + tf = str_hands[hand] + "-" + str_fingers[fnum] + self.fingers.append(tk.Button(self.frm_fingers,text=tf,padx=0,pady=0,command=lambda f=tf: self.func_finger_button(f),image=self.img_notenrolled,compound="top")) + self.fingers[-1].grid(row=hand,column=finger) + finger = finger + 1 + hand = hand + 1 + # because the underline business does not work on the radio button itself + # somehow the keypress takse over the lambda first var, so just set a dummy value, and pass our useful value as second parameter. + self.master.bind("",lambda a=1,b=1: self.set_action(a,b)) + self.master.bind("",lambda a=1,b=2: self.set_action(a,b)) + + # status bar + stk.StatusBar(self.master,var=self.statustext) + # and now, load data into form for the first time + self.load_data_into_form() + + def set_action(self, keypress, a): + #print(f"DEBUG: got keypress {keypress}, a={a}") + # this is used by Alt+E, Alt+V to select the radio buttons for enroll, verify, etc. + self.action.set(a) + + def load_data_into_form(self): + self.enrolled_fingers = lib.get_enrolled_fingers() + print(f"DEBUG (load_data_into_form): got enrolled fingers {self.enrolled_fingers}") + for i in self.fingers: + if str(i.cget('text')) in self.enrolled_fingers: + i.config(image=self.img_enrolled) + else: + i.config(image=self.img_notenrolled) + + # functions + def func_about(self): + """ Display about dialog. """ + tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show() + + def func_exit(self): + # in case we need to manually do stuff + # otherwise command=self.client_exit would have sufficed. + self.master.quit() + + def func_finger_button(self, finger): + print(f"DEBUG: func_finger_button finger {finger}, action {self.action.get()}") + action = self.action.get() + if 1 == action: # enroll + try: + # WORKHERE: pass a tk text var to set for intermediate operations? + self.statustext.set(lib.enroll_finger(finger, self.func_update_status)) + except Exception as e: + self.statustext.set(e) + elif 2 == action: # verify + #print(f"stub action: verify, finger {finger}") + t1 = threading.Thread(target=lib.verify_finger,args=(finger, self.func_update_status)) + t1.run() + self.load_data_into_form() + + def func_update_status(self, msg): + msg = msg.strip() + print(f"DEBUG: func_update_status called with msg {msg}",file=sys.stderr) + self.statustext.set(msg) + + def func_delete(self): + print(f"stub, please ask user if he is certain to delete all enrolled fingerprints") + +# main +root = tk.Tk() +fprintd_tk = App(root) +fprintd_tk.mainloop() diff --git a/fprintd_tk_lib.py b/fprintd_tk_lib.py new file mode 100644 index 0000000..1f1b022 --- /dev/null +++ b/fprintd_tk_lib.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# Startdate: 2024-09-22-1 15:33 +# Purpose: backend for fprintd_tk that uses fprintd-* binaries, in case I ever rewrite this to use dbus directly? +# Dependencies: +# /usr/bin/fprintd-* +# Improve: +# add a verbose/debug param to everything? + +import os, subprocess, re + +_user = os.getenv("USER") +#fre = re.compile("^.* . #[0-9]: \w+$") +fre = re.compile ("^.* - #[0-9]+: ([^ ]+)$") + +def get_enrolled_fingers(user = None): + # return list of full strings of fingers that are enrolled. + enrolled_fingers = [] + if user is None: + user = _user + proc = subprocess.Popen( + ["fprintd-list",user], + stdout = subprocess.PIPE, + universal_newlines = True # or maybe text=True + ) + #print(result.stdout) + while True: + line = proc.stdout.readline() + if not line: + break + if fre.match(line): + #print(f"Got {fre.match(line).groups()[0]}") + enrolled_fingers.append(fre.match(line).groups()[0].strip()) + else: + #print(f"Not-matching: {line}",end="") + pass + return enrolled_fingers + +def enroll_finger(finger, status_function = None, user = None): + if user is None: + user = _user + proc = subprocess.Popen( + ["fprintd-enroll","-f",finger,user], + stdout = subprocess.PIPE, + universal_newlines = True + ) + while True: + line = proc.stdout.readline() + if not line: + break + if re.match("^.*enroll-duplicate.*",line): + # Using the same finger as any other finger enrolled by any user already is considered an error by fprintd, and there is nothing we can do about it. It is an error and we must raise an error here. + return "enroll-duplicate" + elif re.match("^.*enroll-completed.*",line): + return "enroll-completed" + elif re.match("^.*enroll-stage-passed.*",line): + if status_function: + #print(f"Will use function {status_function}(\"enroll-stage-passed\")") + status_function("enroll-stage-passed") + print(f"Great, keep going!") + # so the process has ended, now what? + return f"Last line was: {line}" + +def verify_finger(finger, status_function = None, user = None): + if user is None: + user = _user + proc = subprocess.Popen( + ["fprintd-verify","-f",finger,user], + stdout = subprocess.PIPE, + universal_newlines = True + ) + while True: + line = proc.stdout.readline() + if not line: + break + if re.match("^.*verify-match (done).*",line): + return "verify-match" + else: + if status_function: + #print(f"Will use function {status_function}(\"enroll-stage-passed\")") + status_function(line) + return line diff --git a/images/fingerprint-enrolled.svg b/images/fingerprint-enrolled.svg new file mode 100644 index 0000000..b6d2cd7 --- /dev/null +++ b/images/fingerprint-enrolled.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + diff --git a/images/fingerprint-gui.svg b/images/fingerprint-gui.svg new file mode 100644 index 0000000..2162ef2 --- /dev/null +++ b/images/fingerprint-gui.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + -- cgit