#!/usr/bin/env python3 # File: fprintd_tk.py # Location: https://bgstack15.cgit/fprintd-tk # Author: bgstack15 # Startdate: 2024-09-22-1 14:26 # SPDX-License-Identifier: GPL-3.0-only # Title: Gui for fprintd # Purpose: tkinter desktop gui app for management fingerprint enrollments # Project: fprintd-tk # History: # Usage: # Run from window manager application menu # Reference: # stackrpms_tk.py # Improve: # Dependencies: # dep-devuan: python3:any, python3-tkstackrpms, python3-tk, python3-pil # fprintd_tk_lib.py # Documentation: # README.md import tkinter as tk, os, tkinter.simpledialog, sys, threading, time import tkstackrpms as stk import fprintd_tk_lib as lib from PIL import Image, ImageTk 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) """ # These will be used a lot in the program. str_hands = ["left","right"] str_fingers = ["thumb","index-finger","middle-finger","ring-finger","little-finger"] class App(tk.Frame): def __init__(self, master): super().__init__(master) # variables self.statustext = tk.StringVar() self.advanced = tk.BooleanVar(value=False) self.advanced.trace_add("write",self.load_data_into_form) self.current_username = os.getenv("USER") self.username = tk.StringVar(value=self.current_username) self.verbose = tk.BooleanVar(value=False) # configurable by admin or installation img_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"images") 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_checkbutton(label="Advanced features", variable=self.advanced, underline=0, state="disabled") menu_file.add_checkbutton(label="Verbose", variable=self.verbose, underline=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() self.background_color = self.master.cget("bg") # 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)) # advanced pane, top self.frm_advanced = tk.Frame(self.master) # start hidden #self.frm_advanced.grid(row=0,column=0,columnspan=100) tk.Label(self.frm_advanced,text="User").grid(row=0,column=0) self.ent_username = stk.Entry(self.frm_advanced,textvariable=self.username,func=self.load_data_into_form) self.ent_username.grid(row=0,column=1) # action pane, left side self.frm_actions = tk.Frame(self.master) self.frm_actions.grid(row=1,column=0) self.action = tk.IntVar(value=1) self.last_fingerbutton_used = tk.StringVar() 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) tk.Button(self.frm_actions,text="Refresh",underline=0,command=self.load_data_into_form).grid(row=2,column=0) # array of fingers # we want 2 rows, 5 columns self.frm_fingers = tk.Frame(self.master) self.frm_fingers.grid(row=1,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 takes 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)) self.master.bind("",self.load_data_into_form) # status bar stk.StatusBar(self.master,var=self.statustext) # check if user has setusername polkit-1 permission, which lets him control fingerprint enrollments for other users. temp1 = lib.check_setusername_permission(self.func_update_status) if self.verbose.get(): print(f"DEBUG (init): has set_username: {temp1}") if temp1: #self.chk_advanced.configure(state="enabled") #menu_file.child[0].child[0].configure(state="enabled") menu_file.entryconfigure(0,state="normal") #print(menu_file.children) # 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 get_used_user(self): advanced = self.advanced.get() if advanced: used_user = self.username.get() else: used_user = self.current_username return used_user def load_data_into_form(self, a = None, b = None, c = None): time.sleep(0.05) advanced = self.advanced.get() # show advanced toolbar used_user = self.get_used_user() if advanced: if self.verbose.get(): print(f"DEBUG: showing advanced toolbar") self.frm_advanced.grid(row=0,column=0,columnspan=100) else: if self.verbose.get(): print(f"DEBUG: hiding advanced toolbar") self.frm_advanced.grid_forget() # update enrolled fingers icons try: self.old_enrolled_fingers = self.enrolled_fingers except: # will happen if self.enrolled_fingers is not present self.old_enrolled_fingers = [] temp1 = lib.get_enrolled_fingers(used_user) if temp1: self.enrolled_fingers = temp1 else: if self.verbose.get(): print(f"DEBUG (load_data_into_form): having to skip empty response from get_enrolled_fingers") self.func_update_status(f"Unable to read enrolled fingers for user {used_user}.", reload = False) if self.verbose.get(): print(f"DEBUG (load_data_into_form): got user {used_user} 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): if self.verbose.get(): print(f"DEBUG: func_finger_button finger {finger}, action {self.action.get()}") action = self.action.get() used_user = self.get_used_user() # position in array is same as the value coming from radio button for actions. available_actions = ["none","enroll","verify"] try: action_str = available_actions[action] except ValueError: action_str = "OFF" if action_str in available_actions: self.last_fingerbutton_used.set(finger) try: t1 = threading.Thread(target=lib.fprintd_action, args=(action_str, finger, self.func_update_status, used_user)) t1.start() except Exception as e: self.statustext.set(e) else: self.statustext.set(f"Invalid action {action}, string {action_str}.") # This blocks everything! Do not use this. #t1.join() # unfortunately useless here, because of the threading. #self.load_data_into_form() def func_update_status(self, msg, reload = True): msg = msg.strip() if self.verbose.get(): print(f"DEBUG (func_update_status): msg {msg}",file=sys.stderr) self.statustext.set(msg) try: this_finger = [i for i in self.fingers if i.cget("text") == self.last_fingerbutton_used.get()][0] except Exception as e: this_finger = None if self.verbose.get(): print(f"DEBUG (func_update_status): while evaluating last-used finger, got {e}") # flash these red failure_messages = [ "enroll-duplicate", "verify-no-match" ] # flash these green success_messages = [ "verify-match", "enroll-completed" ] flashed = False for i in failure_messages: if i in msg: flashed = True if this_finger: stk.flash_entry(this_finger,["red",self.background_color]*3,300) break if not flashed: for i in success_messages: if i in msg: if this_finger: stk.flash_entry(this_finger,["green",self.background_color]*3,300) break if reload: self.load_data_into_form() def func_delete(self): used_user = self.get_used_user() sure = tkinter.messagebox.askokcancel(title="Delete all enrolled fingerprints",message=f"Are you sure you want to delete all enrolled fingerprints for user {used_user}?") if sure: t1 = threading.Thread(target=lib.fprintd_action, args=("delete", "none", self.func_update_status, used_user)) t1.start() else: self.statustext.set("Cancelled the delete action.") # main root = tk.Tk() fprintd_tk = App(root) fprintd_tk.mainloop()