passlocker-tk 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .gui import PassLockerGui
2
+
3
+ __all__ = ["PassLockerGui"]
@@ -0,0 +1,8 @@
1
+ from .gui import PassLockerGui
2
+
3
+ def main() -> None:
4
+ app: PassLockerGui = PassLockerGui()
5
+ app.mainloop()
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,16 @@
1
+ from .password_dialog import PasswordDialog
2
+ from .accounts_table import AccountsTable
3
+ from .new_password_account_dialog import NewPasswordAccountDialog
4
+ from .new_otp_account_dialog import NewOTPAccountDialog
5
+ from .new_totp_account_dialog import NewTOTPAccountDialog
6
+ from .about_dialog import AboutDialog
7
+
8
+ __all__ = [
9
+ "PasswordDialog",
10
+ "AccountsTable",
11
+ "NewPasswordAccountDialog",
12
+ "NewOTPAccountDialog",
13
+ "NewTOTPAccountDialog",
14
+ "AboutDialog",
15
+ "PasswordDialog",
16
+ ]
@@ -0,0 +1,88 @@
1
+ import customtkinter as ctk
2
+ import webbrowser
3
+
4
+
5
+ class AboutDialog(ctk.CTkToplevel):
6
+ def __init__(
7
+ self,
8
+ parent,
9
+ *,
10
+ title,
11
+ version,
12
+ author,
13
+ github_url,
14
+ description,
15
+ ):
16
+ super().__init__(parent)
17
+
18
+ self.title("About")
19
+ self.resizable(False, False)
20
+
21
+ # Modal
22
+ self.transient(parent)
23
+ self.grab_set()
24
+
25
+ self._build_ui(
26
+ title=title,
27
+ version=version,
28
+ author=author,
29
+ github_url=github_url,
30
+ description=description,
31
+ )
32
+ self._center(parent)
33
+
34
+ self.bind("<Escape>", lambda _: self.destroy())
35
+ self.wait_window()
36
+
37
+ # --------------------
38
+ # UI
39
+ # --------------------
40
+ def _build_ui(self, *, title, version, author, github_url, description):
41
+ frame = ctk.CTkFrame(self)
42
+ frame.pack(padx=24, pady=24)
43
+
44
+ # Title
45
+ ctk.CTkLabel(frame, text=title, font=ctk.CTkFont(size=20, weight="bold")).pack(
46
+ anchor="w"
47
+ )
48
+
49
+ # Version / Author
50
+ ctk.CTkLabel(frame, text=f"Version {version}\n{author}", justify="left").pack(
51
+ anchor="w", pady=(4, 12)
52
+ )
53
+
54
+ # Description
55
+ ctk.CTkLabel(frame, text=description, justify="left", wraplength=420).pack(
56
+ anchor="w", pady=(0, 12)
57
+ )
58
+
59
+ # GitHub link
60
+ self._link(frame, text="Source code on GitHub", url=github_url).pack(
61
+ anchor="w", pady=(0, 6)
62
+ )
63
+
64
+ # License link
65
+ self._link(
66
+ frame,
67
+ text="Licensed under CC-BY-NC-SA-4.0",
68
+ url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode",
69
+ ).pack(anchor="w", pady=(0, 16))
70
+
71
+ # Close button
72
+ ctk.CTkButton(frame, text="Close", width=120, command=self.destroy).pack(
73
+ anchor="e"
74
+ )
75
+
76
+ # --------------------
77
+ # Helpers
78
+ # --------------------
79
+ def _link(self, parent, *, text, url):
80
+ lbl = ctk.CTkLabel(parent, text=text, text_color="#4ea1ff", cursor="hand2")
81
+ lbl.bind("<Button-1>", lambda _: webbrowser.open(url))
82
+ return lbl
83
+
84
+ def _center(self, parent):
85
+ self.update_idletasks()
86
+ x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
87
+ y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
88
+ self.geometry(f"+{x}+{y}")
@@ -0,0 +1,138 @@
1
+ import customtkinter as ctk
2
+ import tkinter as tk
3
+ from hashlib import sha256
4
+ from secrets import token_bytes
5
+
6
+ from passlocker.models.account import Account
7
+
8
+
9
+ class AccountDisplayDialog(ctk.CTkToplevel):
10
+ def __init__(self, parent, *, account: Account):
11
+ super().__init__(parent)
12
+
13
+ self.title("Password Record")
14
+ self.resizable(False, False)
15
+
16
+ self.account: Account = account
17
+
18
+ self._revealed = False
19
+ self.iv: bytes = b""
20
+ self.passhash: str = ""
21
+
22
+ # Modal behavior
23
+ self.transient(parent)
24
+ self.wait_visibility()
25
+ self.grab_set()
26
+
27
+ self._build_ui()
28
+ self._center(parent)
29
+
30
+ self.bind("<Escape>", lambda _: self.destroy())
31
+ self.wait_window()
32
+
33
+ # --------------------
34
+ # UI
35
+ # --------------------
36
+ def _build_ui(self):
37
+ container: ctk.CTkFrame = ctk.CTkFrame(self)
38
+ container.pack(padx=10, pady=10)
39
+
40
+ # Account
41
+ ctk.CTkLabel(container, text="Account").grid(
42
+ row=0, column=0, sticky="w", padx=5
43
+ )
44
+ ctk.CTkLabel(
45
+ container, text=self.account.account, font=ctk.CTkFont(weight="bold")
46
+ ).grid(row=0, column=1, sticky="w")
47
+
48
+ # Username
49
+ ctk.CTkLabel(container, text="Username").grid(
50
+ row=1, column=0, sticky="w", padx=5
51
+ )
52
+ ctk.CTkLabel(container, text=self.account.username).grid(
53
+ row=1, column=1, sticky="w"
54
+ )
55
+
56
+ # Created on
57
+ ctk.CTkLabel(container, text="Created").grid(
58
+ row=2, column=0, sticky="w", padx=5
59
+ )
60
+ ctk.CTkLabel(container, text=self.account.created_on).grid(
61
+ row=2, column=1, sticky="w"
62
+ )
63
+
64
+ # Password
65
+ ctk.CTkLabel(container, text="Password").grid(
66
+ row=3, column=0, sticky="w", padx=5
67
+ )
68
+
69
+ self.password_var: ctk.StringVar = ctk.StringVar(value="••••••••")
70
+ self.password_label: ctk.CTkLabel = ctk.CTkLabel(
71
+ container, textvariable=self.password_var, font=ctk.CTkFont(size=14)
72
+ )
73
+ self.password_label.grid(row=3, column=1, sticky="w")
74
+
75
+ # Buttons
76
+ btn_frame: ctk.CTkFrame = ctk.CTkFrame(container, fg_color="transparent")
77
+ btn_frame.grid(row=4, column=1, sticky="w", pady=(10, 0))
78
+
79
+ self.reveal_btn: ctk.CTkButton = ctk.CTkButton(
80
+ btn_frame, text="Show Password", width=100, command=self.toggle_reveal
81
+ )
82
+ self.reveal_btn.grid(row=0, column=0, padx=(0, 10))
83
+
84
+ self.copy_btn: ctk.CTkButton = ctk.CTkButton(
85
+ btn_frame, text="Copy to Clipboard", width=100, command=self.copy_password
86
+ )
87
+ self.copy_btn.grid(row=0, column=1)
88
+
89
+ container.grid_columnconfigure(1, weight=1)
90
+
91
+ # --------------------
92
+ # Actions
93
+ # --------------------
94
+ def toggle_reveal(self):
95
+ self._revealed = not self._revealed
96
+
97
+ if self._revealed:
98
+ password: bytes = self.account.get_active_password()
99
+ self.password_var.set(password.decode("utf-8"))
100
+ self.reveal_btn.configure(text="Hide")
101
+ else:
102
+ self.password_var.set("••••••••")
103
+ self.reveal_btn.configure(text="Reveal")
104
+
105
+ def copy_password(self):
106
+ self.clipboard_clear()
107
+ password: bytes = self.account.get_active_password()
108
+ self.iv: bytes = token_bytes(16)
109
+ self.passhash: str = sha256(self.iv + password).hexdigest()
110
+ self.clipboard_append(password.decode("utf-8"))
111
+ self.update() # ensure clipboard is updated
112
+ self._schedule_clipboard_clear()
113
+
114
+ def _schedule_clipboard_clear(self, delay_ms=15000):
115
+ self.after(delay_ms, self._clear_clipboard)
116
+
117
+ def _clear_clipboard(self):
118
+ try:
119
+ checkhash: str = sha256(
120
+ self.iv + self.clipboard_get().encode("utf-8")
121
+ ).hexdigest()
122
+ if checkhash == self.passhash:
123
+ self.clipboard_clear()
124
+ except tk.TclError:
125
+ pass
126
+
127
+ # --------------------
128
+ # Helpers
129
+ # --------------------
130
+ def _center(self, parent):
131
+ self.update_idletasks()
132
+ x: int = (
133
+ parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
134
+ )
135
+ y: int = (
136
+ parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
137
+ )
138
+ self.geometry(f"+{x}+{y}")
@@ -0,0 +1,65 @@
1
+ import customtkinter as ctk
2
+ import tkinter as tk
3
+ from tkinter import ttk
4
+ from passlocker.models.account import Account
5
+ from .account_display_dialog import AccountDisplayDialog
6
+
7
+
8
+ class AccountsTable(ctk.CTkFrame):
9
+ def __init__(self, parent: ctk.CTk) -> None:
10
+ super().__init__(parent)
11
+
12
+ columns = ("account", "username", "type")
13
+
14
+ self.tree: ttk.Treeview = ttk.Treeview(
15
+ self, columns=columns, show="headings", height=10
16
+ )
17
+
18
+ # Column definitions
19
+ self.tree.heading("account", text="Account")
20
+ self.tree.heading("username", text="Username")
21
+ self.tree.heading("type", text="Type")
22
+
23
+ self.tree.column("account", anchor="w", width=200)
24
+ self.tree.column("username", anchor="w", width=180)
25
+ self.tree.column("type", anchor="center", width=80)
26
+
27
+ # Scrollbar
28
+ scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
29
+ self.tree.configure(yscrollcommand=scrollbar.set)
30
+
31
+ # Layout
32
+ self.tree.grid(row=0, column=0, sticky="nsew")
33
+ scrollbar.grid(row=0, column=1, sticky="ns")
34
+
35
+ self.grid_rowconfigure(0, weight=1)
36
+ self.grid_columnconfigure(0, weight=1)
37
+
38
+ self.tree.bind("<Double-1>", self.on_open_account)
39
+ self._accounts_by_id: dict[int, Account] = {}
40
+
41
+ # --------------------
42
+ # Public API
43
+ # --------------------
44
+ def populate(self, accounts: list[Account]) -> None:
45
+ """
46
+ accounts: iterable of objects or dicts
47
+ expected fields: account, username, type
48
+ """
49
+ self.tree.delete(*self.tree.get_children())
50
+
51
+ for acc in accounts:
52
+ self.tree.insert(
53
+ "", "end", iid=id(acc), values=(acc.account, acc.username, acc.type)
54
+ )
55
+ self._accounts_by_id: dict[int, Account] = {id(acc): acc for acc in accounts}
56
+
57
+ def on_open_account(self, event: tk.Event) -> None:
58
+ selected = self.tree.selection()
59
+ if not selected:
60
+ return
61
+ item_id = int(selected[0])
62
+ account = self._accounts_by_id.get(item_id)
63
+ if account is None:
64
+ return
65
+ _ = AccountDisplayDialog(self, account=account)
@@ -0,0 +1,209 @@
1
+ import customtkinter as ctk
2
+ from tkinter import ttk
3
+ import secrets
4
+
5
+
6
+ class GeneratePasswordDialog(ctk.CTkToplevel):
7
+ def __init__(self, parent) -> None:
8
+ super().__init__(parent)
9
+
10
+ self.title("Generate Password")
11
+ self.resizable(False, False)
12
+
13
+ self.result: str | None = None
14
+
15
+ # Modal behavior
16
+ self.transient(parent)
17
+ self.wait_visibility()
18
+ self.grab_set()
19
+
20
+ self._build_ui()
21
+ self._center(parent)
22
+
23
+ self.bind("<Escape>", lambda _: self._cancel())
24
+
25
+ self.wait_window()
26
+
27
+ # --------------------
28
+ # UI
29
+ # --------------------
30
+ def _build_ui(self) -> None:
31
+ root = ctk.CTkFrame(self)
32
+ root.pack(padx=20, pady=20)
33
+
34
+ # ---- Controls ----
35
+ controls = ctk.CTkFrame(root)
36
+ controls.pack(fill="x")
37
+
38
+ # Algorithm selector
39
+ ctk.CTkLabel(controls, text="Algorithm").grid(row=0, column=0, sticky="w")
40
+
41
+ self.algorithm_var = ctk.StringVar(value="random")
42
+ self.algorithm_menu = ctk.CTkOptionMenu(
43
+ controls,
44
+ variable=self.algorithm_var,
45
+ values=["random", "memorable", "numeric"],
46
+ width=140,
47
+ )
48
+ self.algorithm_menu.grid(row=0, column=1, padx=(10, 20))
49
+
50
+ # Length selector
51
+ ctk.CTkLabel(controls, text="Length").grid(row=0, column=2, sticky="w")
52
+
53
+ self.length_var = ctk.IntVar(value=16)
54
+ self.length_slider = ctk.CTkSlider(
55
+ controls, from_=8, to=64, number_of_steps=56, command=self._on_length_change
56
+ )
57
+ self.length_slider.set(16)
58
+ self.length_slider.grid(row=0, column=3, padx=(10, 10))
59
+
60
+ self.length_label = ctk.CTkLabel(controls, text="16", width=32, anchor="e")
61
+ self.length_label.grid(row=0, column=4)
62
+
63
+ controls.grid_columnconfigure(3, weight=1)
64
+
65
+ # Generate button
66
+ self.generate_btn = ctk.CTkButton(root, text="Generate", command=self._generate)
67
+ self.generate_btn.pack(pady=(10, 10))
68
+
69
+ # ---- Table ----
70
+ table_frame = ctk.CTkFrame(root)
71
+ table_frame.pack()
72
+
73
+ self.tree = ttk.Treeview(
74
+ table_frame, columns=("password",), show="headings", height=10
75
+ )
76
+ self.tree.heading("password", text="Generated Password")
77
+ self.tree.column("password", anchor="w", width=320)
78
+
79
+ scrollbar = ttk.Scrollbar(
80
+ table_frame, orient="vertical", command=self.tree.yview
81
+ )
82
+ self.tree.configure(yscrollcommand=scrollbar.set)
83
+
84
+ self.tree.grid(row=0, column=0, sticky="nsew")
85
+ scrollbar.grid(row=0, column=1, sticky="ns")
86
+
87
+ table_frame.grid_rowconfigure(0, weight=1)
88
+ table_frame.grid_columnconfigure(0, weight=1)
89
+
90
+ # Double-click selection
91
+ self.tree.bind("<Double-1>", self._on_select)
92
+
93
+ # --------------------
94
+ # Actions
95
+ # --------------------
96
+ def _on_length_change(self, value):
97
+ self.length_label.configure(text=str(int(value)))
98
+
99
+ def _generate(self):
100
+ algorithm = self.algorithm_var.get()
101
+ length = self.length_var.get()
102
+
103
+ passwords = self.generate_passwords(algorithm, length, count=10)
104
+ self.tree.delete(*self.tree.get_children())
105
+
106
+ for pw in passwords:
107
+ self.tree.insert("", "end", values=(pw,))
108
+
109
+ def _on_select(self, event):
110
+ selection = self.tree.selection()
111
+ if not selection:
112
+ return
113
+
114
+ self.result = self.tree.item(selection[0], "values")[0]
115
+ self.destroy()
116
+
117
+ def _cancel(self):
118
+ self.result = None
119
+ self.destroy()
120
+
121
+ # --------------------
122
+ # Helpers
123
+ # --------------------
124
+ def _center(self, parent):
125
+ self.update_idletasks()
126
+ x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
127
+ y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
128
+ self.geometry(f"+{x}+{y}")
129
+
130
+ def generate_passwords(
131
+ self, algorithm: str, length: int, count: int = 10
132
+ ) -> list[str]:
133
+ if algorithm == "memorable":
134
+ return [self.generate_memorable(length) for _ in range(count)]
135
+ elif algorithm == "random":
136
+ return [self.generate_random(length) for _ in range(count)]
137
+ elif algorithm == "numeric":
138
+ return [self.generate_numbers(length) for _ in range(count)]
139
+ else:
140
+ raise Exception(f"Unknown generation algorithm: {algorithm}")
141
+
142
+ def generate_memorable(self, length: int) -> str:
143
+ if length <= 8:
144
+ maxnum = 100
145
+ special = 0
146
+ elif length <= 16:
147
+ maxnum = 1000
148
+ special = 1
149
+ elif length <= 24:
150
+ maxnum = 10000
151
+ special = 2
152
+ else:
153
+ maxnum = 100000
154
+ special = 3
155
+
156
+ random_int: int = secrets.randbelow(maxnum)
157
+ random_special: str = "".join(
158
+ secrets.choice(
159
+ [
160
+ "!",
161
+ "@",
162
+ "#",
163
+ "$",
164
+ "%",
165
+ "^",
166
+ "&",
167
+ "*",
168
+ "(",
169
+ ")",
170
+ "-",
171
+ "_",
172
+ "+",
173
+ "=",
174
+ "{",
175
+ "}",
176
+ "[",
177
+ "]",
178
+ ":",
179
+ ";",
180
+ "<",
181
+ ">",
182
+ ".",
183
+ ",",
184
+ "?",
185
+ "/",
186
+ "~",
187
+ "`",
188
+ ]
189
+ )
190
+ for i in range(special)
191
+ )
192
+
193
+ secret_sauce = f"{random_int}{random_special}"
194
+ password: str = ""
195
+
196
+ with open("/usr/share/dict/words") as f:
197
+ words: list[str] = [word.strip() for word in f]
198
+ genlen: int = 0
199
+ while genlen != length:
200
+ password = secret_sauce.join(secrets.choice(words) for _ in range(2))
201
+ genlen = len(password)
202
+
203
+ return password
204
+
205
+ def generate_random(self, length: int) -> str:
206
+ return secrets.token_urlsafe(32)[0:length]
207
+
208
+ def generate_numbers(self, length: int) -> str:
209
+ return "".join([str(secrets.choice(range(10))) for i in range(length)])
@@ -0,0 +1,94 @@
1
+ import customtkinter as ctk
2
+
3
+
4
+ class NewOTPAccountDialog(ctk.CTkToplevel):
5
+ def __init__(self, parent):
6
+ super().__init__(parent)
7
+
8
+ self.title("New Account")
9
+ self.resizable(False, False)
10
+
11
+ self.result = None
12
+
13
+ # Modal behavior
14
+ self.transient(parent)
15
+ self.grab_set()
16
+
17
+ self._build_ui()
18
+ self._center(parent)
19
+
20
+ self.bind("<Escape>", lambda _: self._cancel())
21
+ self.bind("<Return>", lambda _: self._ok())
22
+
23
+ self.wait_window()
24
+
25
+ # --------------------
26
+ # UI
27
+ # --------------------
28
+ def _build_ui(self):
29
+ frame = ctk.CTkFrame(self)
30
+ frame.pack(padx=20, pady=20)
31
+
32
+ # Variables
33
+ self.account_var = ctk.StringVar()
34
+ self.username_var = ctk.StringVar()
35
+
36
+ # Account name
37
+ ctk.CTkLabel(frame, text="Account").grid(row=0, column=0, sticky="w")
38
+ self.account_entry = ctk.CTkEntry(
39
+ frame, textvariable=self.account_var, width=260
40
+ )
41
+ self.account_entry.grid(row=0, column=1, pady=5)
42
+ self.account_entry.focus()
43
+
44
+ # Username
45
+ ctk.CTkLabel(frame, text="Username").grid(row=1, column=0, sticky="w")
46
+ ctk.CTkEntry(frame, textvariable=self.username_var, width=260).grid(
47
+ row=1, column=1, pady=5
48
+ )
49
+
50
+ # Password
51
+ ctk.CTkLabel(frame, text="Passwords").grid(row=2, column=0, sticky="w")
52
+ self.password_entry = ctk.CTkTextbox(frame, width=260, height=112)
53
+ self.password_entry.grid(row=2, column=1, pady=5)
54
+
55
+ # Buttons
56
+ btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
57
+ btn_frame.grid(row=3, column=1, pady=(15, 0), sticky="e")
58
+
59
+ ctk.CTkButton(btn_frame, text="Cancel", width=100, command=self._cancel).grid(
60
+ row=0, column=0, padx=(0, 10)
61
+ )
62
+
63
+ self.ok_btn = ctk.CTkButton(btn_frame, text="OK", width=100, command=self._ok)
64
+ self.ok_btn.grid(row=0, column=1)
65
+
66
+ frame.grid_columnconfigure(1, weight=1)
67
+
68
+ def _ok(self):
69
+ account = self.account_var.get().strip()
70
+ username = self.username_var.get().strip()
71
+ password = self.password_entry.get("0.0", ctk.END)
72
+
73
+ if not account or not password:
74
+ return # minimal validation; extend as needed
75
+
76
+ self.result = {
77
+ "account": account,
78
+ "username": username,
79
+ "password": password,
80
+ }
81
+ self.destroy()
82
+
83
+ def _cancel(self):
84
+ self.result = None
85
+ self.destroy()
86
+
87
+ # --------------------
88
+ # Helpers
89
+ # --------------------
90
+ def _center(self, parent):
91
+ self.update_idletasks()
92
+ x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
93
+ y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
94
+ self.geometry(f"+{x}+{y}")
@@ -0,0 +1,126 @@
1
+ import customtkinter as ctk
2
+ from .generate_password_dialog import GeneratePasswordDialog
3
+
4
+
5
+ class NewPasswordAccountDialog(ctk.CTkToplevel):
6
+ def __init__(self, parent):
7
+ super().__init__(parent)
8
+
9
+ self.title("New Account")
10
+ self.resizable(False, False)
11
+
12
+ self.result = None
13
+
14
+ # Modal behavior
15
+ self.transient(parent)
16
+ self.wait_visibility()
17
+ self.grab_set()
18
+
19
+ self._build_ui()
20
+ self._center(parent)
21
+
22
+ self.bind("<Escape>", lambda _: self._cancel())
23
+ self.bind("<Return>", lambda _: self._ok())
24
+
25
+ self.wait_window()
26
+
27
+ # --------------------
28
+ # UI
29
+ # --------------------
30
+ def _build_ui(self):
31
+ frame = ctk.CTkFrame(self)
32
+ frame.pack(padx=20, pady=20)
33
+
34
+ # Variables
35
+ self.account_var = ctk.StringVar()
36
+ self.username_var = ctk.StringVar()
37
+ self.password_var = ctk.StringVar()
38
+
39
+ # Account name
40
+ ctk.CTkLabel(frame, text="Account").grid(row=0, column=0, sticky="w")
41
+ self.account_entry = ctk.CTkEntry(
42
+ frame, textvariable=self.account_var, width=260
43
+ )
44
+ self.account_entry.grid(row=0, column=1, pady=5)
45
+ self.account_entry.focus()
46
+
47
+ # Username
48
+ ctk.CTkLabel(frame, text="Username").grid(row=1, column=0, sticky="w")
49
+ ctk.CTkEntry(frame, textvariable=self.username_var, width=260).grid(
50
+ row=1, column=1, pady=5
51
+ )
52
+
53
+ # Password
54
+ ctk.CTkLabel(frame, text="Password").grid(row=2, column=0, sticky="w")
55
+ self.password_entry = ctk.CTkEntry(
56
+ frame, textvariable=self.password_var, show="•", width=260
57
+ )
58
+ self.password_entry.grid(row=2, column=1, pady=5)
59
+
60
+ self.reveal_btn = ctk.CTkButton(
61
+ frame, text="Reveal", width=80, command=self._toggle_reveal
62
+ )
63
+ self.reveal_btn.grid(row=2, column=2, padx=(5, 0))
64
+
65
+ # Buttons
66
+ btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
67
+ btn_frame.grid(row=3, column=1, pady=(15, 0), sticky="e")
68
+
69
+ ctk.CTkButton(btn_frame, text="Cancel", width=100, command=self._cancel).grid(
70
+ row=0, column=0, padx=(0, 10)
71
+ )
72
+
73
+ self.ok_btn = ctk.CTkButton(btn_frame, text="OK", width=100, command=self._ok)
74
+ self.ok_btn.grid(row=0, column=1)
75
+
76
+ self.generate_btn = ctk.CTkButton(
77
+ btn_frame, text="Generate", width=100, command=self._generate
78
+ )
79
+ self.generate_btn.grid(row=0, column=2)
80
+
81
+ frame.grid_columnconfigure(1, weight=1)
82
+
83
+ # --------------------
84
+ # Actions
85
+ # --------------------
86
+ def _toggle_reveal(self):
87
+ if self.password_entry.cget("show"):
88
+ self.password_entry.configure(show="")
89
+ self.reveal_btn.configure(text="Hide")
90
+ else:
91
+ self.password_entry.configure(show="•")
92
+ self.reveal_btn.configure(text="Reveal")
93
+
94
+ def _ok(self):
95
+ account = self.account_var.get().strip()
96
+ username = self.username_var.get().strip()
97
+ password = self.password_var.get()
98
+
99
+ if not account or not password:
100
+ return # minimal validation; extend as needed
101
+
102
+ self.result = {
103
+ "account": account,
104
+ "username": username,
105
+ "password": password,
106
+ }
107
+ self.destroy()
108
+
109
+ def _cancel(self) -> None:
110
+ self.result = None
111
+ self.destroy()
112
+
113
+ def _generate(self) -> None:
114
+ dlg = GeneratePasswordDialog(self)
115
+ password: str | None = dlg.result
116
+ if password is not None:
117
+ self.password_var.set(password)
118
+
119
+ # --------------------
120
+ # Helpers
121
+ # --------------------
122
+ def _center(self, parent):
123
+ self.update_idletasks()
124
+ x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
125
+ y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
126
+ self.geometry(f"+{x}+{y}")
@@ -0,0 +1,147 @@
1
+ # otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
2
+ import customtkinter as ctk
3
+ from urllib.parse import urlparse, parse_qs, unquote
4
+
5
+
6
+ class NewTOTPAccountDialog(ctk.CTkToplevel):
7
+ DEFAULTS = {
8
+ "algorithm": "SHA1",
9
+ "period": 30,
10
+ "digits": 6,
11
+ }
12
+
13
+ def __init__(self, parent):
14
+ super().__init__(parent)
15
+
16
+ self.title("New TOTP Account")
17
+ self.resizable(False, False)
18
+
19
+ self.result = None
20
+
21
+ # Modal
22
+ self.transient(parent)
23
+ self.grab_set()
24
+
25
+ self._build_ui()
26
+ self._center(parent)
27
+
28
+ self.bind("<Return>", lambda _: self._ok())
29
+ self.bind("<Escape>", lambda _: self._cancel())
30
+
31
+ self.wait_window()
32
+
33
+ # --------------------
34
+ # UI
35
+ # --------------------
36
+ def _build_ui(self):
37
+ frame = ctk.CTkFrame(self)
38
+ frame.pack(padx=20, pady=20)
39
+
40
+ ctk.CTkLabel(frame, text="otpauth:// URL", anchor="w").grid(
41
+ row=0, column=0, sticky="w"
42
+ )
43
+
44
+ self.url_var = ctk.StringVar()
45
+
46
+ self.entry = ctk.CTkEntry(frame, textvariable=self.url_var, width=420)
47
+ self.entry.grid(row=1, column=0, columnspan=2, pady=(5, 0))
48
+ self.entry.focus()
49
+
50
+ self.error_label = ctk.CTkLabel(frame, text="", text_color="red")
51
+ self.error_label.grid(row=2, column=0, columnspan=2, pady=(5, 0), sticky="w")
52
+
53
+ # Buttons
54
+ btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
55
+ btn_frame.grid(row=3, column=1, pady=(15, 0), sticky="e")
56
+
57
+ ctk.CTkButton(btn_frame, text="Cancel", width=100, command=self._cancel).grid(
58
+ row=0, column=0, padx=(0, 10)
59
+ )
60
+
61
+ ctk.CTkButton(btn_frame, text="OK", width=100, command=self._ok).grid(
62
+ row=0, column=1
63
+ )
64
+
65
+ frame.grid_columnconfigure(0, weight=1)
66
+
67
+ # --------------------
68
+ # Actions
69
+ # --------------------
70
+ def _ok(self):
71
+ url = self.url_var.get().strip()
72
+
73
+ try:
74
+ self.result = self._parse_otpauth_url(url)
75
+ except ValueError as e:
76
+ self.error_label.configure(text=str(e))
77
+ return
78
+
79
+ self.destroy()
80
+
81
+ def _cancel(self):
82
+ self.result = None
83
+ self.destroy()
84
+
85
+ # --------------------
86
+ # Parsing / Validation
87
+ # --------------------
88
+ def _parse_otpauth_url(self, url: str) -> dict[str, str | int]:
89
+ parsed = urlparse(url)
90
+
91
+ if parsed.scheme != "otpauth":
92
+ raise ValueError("URL must start with otpauth://")
93
+
94
+ if parsed.netloc.lower() != "totp":
95
+ raise ValueError("Only otpauth://totp/ URLs are supported")
96
+
97
+ # Path: /AccountName:Username
98
+ label = unquote(parsed.path.lstrip("/"))
99
+ if ":" not in label:
100
+ raise ValueError("URL must include account name and username")
101
+
102
+ account_name, username = label.split(":", 1)
103
+ if not account_name or not username:
104
+ raise ValueError("Account name and username must be non-empty")
105
+
106
+ params: dict[str, list[str]] = parse_qs(parsed.query)
107
+
108
+ if "secret" not in params:
109
+ raise ValueError("URL is missing secret parameter")
110
+
111
+ secret = params["secret"][0]
112
+ if not secret:
113
+ raise ValueError("Secret must be non-empty")
114
+
115
+ # Optional parameters
116
+ algorithm: str = params.get("algorithm", [self.DEFAULTS["algorithm"]])[
117
+ 0
118
+ ].upper() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
119
+ period: int = int(params.get("period", [self.DEFAULTS["period"]])[0])
120
+ digits: int = int(params.get("digits", [self.DEFAULTS["digits"]])[0])
121
+
122
+ if algorithm not in {"SHA1", "SHA256", "SHA512"}:
123
+ raise ValueError(f"Unsupported algorithm: {algorithm}")
124
+
125
+ if period <= 0:
126
+ raise ValueError("Period must be a positive integer")
127
+
128
+ if digits not in {6, 8}:
129
+ raise ValueError("Digits must be 6 or 8")
130
+
131
+ return {
132
+ "account": account_name,
133
+ "username": username,
134
+ "secret": secret.upper(),
135
+ "algorithm": algorithm.lower(),
136
+ "period": period,
137
+ "digits": digits,
138
+ }
139
+
140
+ # --------------------
141
+ # Helpers
142
+ # --------------------
143
+ def _center(self, parent):
144
+ self.update_idletasks()
145
+ x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
146
+ y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
147
+ self.geometry(f"+{x}+{y}")
@@ -0,0 +1,48 @@
1
+ import customtkinter as ctk
2
+
3
+
4
+ class PasswordDialog(ctk.CTkToplevel):
5
+ def __init__(self, parent, title="Enter Password"):
6
+ super().__init__(parent)
7
+
8
+ self.title(title)
9
+ self.resizable(False, False)
10
+
11
+ self.password = None
12
+
13
+ # Make dialog modal
14
+ self.transient(parent)
15
+ self.wait_visibility()
16
+ self.grab_set()
17
+
18
+ # Layout
19
+ self.label = ctk.CTkLabel(self, text="Password:")
20
+ self.label.pack(padx=20, pady=(20, 5))
21
+
22
+ self.var = ctk.StringVar()
23
+ self.entry = ctk.CTkEntry(self, textvariable=self.var, show="•", width=220)
24
+ self.entry.pack(padx=20, pady=5)
25
+ self.entry.focus()
26
+
27
+ self.button = ctk.CTkButton(self, text="OK", command=self.on_ok)
28
+ self.button.pack(padx=20, pady=(10, 20))
29
+
30
+ # Keyboard bindings
31
+ self.bind("<Return>", lambda _: self.on_ok())
32
+ self.bind("<Escape>", lambda _: self.destroy())
33
+
34
+ # Center on parent
35
+ self._center(parent)
36
+
37
+ # Wait until dialog is closed
38
+ self.wait_window()
39
+
40
+ def on_ok(self):
41
+ self.password = self.var.get()
42
+ self.destroy()
43
+
44
+ def _center(self, parent):
45
+ self.update_idletasks()
46
+ x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
47
+ y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
48
+ self.geometry(f"+{x}+{y}")
passlocker_tk/gui.py ADDED
@@ -0,0 +1,217 @@
1
+ import tkinter as tk
2
+ from tkinter import ttk
3
+ from tkinter import Menu
4
+ from tkinter.ttk import Style
5
+ import customtkinter as ctk
6
+
7
+ from debounce import debounce
8
+ from passlocker.passlocker import PassLocker
9
+ from passlocker.models.account import Account
10
+ from .dialogs import (
11
+ AboutDialog,
12
+ AccountsTable,
13
+ NewOTPAccountDialog,
14
+ NewPasswordAccountDialog,
15
+ NewTOTPAccountDialog,
16
+ PasswordDialog,
17
+ )
18
+
19
+ from datetime import date
20
+
21
+ ctk.set_window_scaling(1.5) # UI scale
22
+ ctk.set_widget_scaling(2.0) # font + spacing scale
23
+ ctk.set_appearance_mode("dark")
24
+ ctk.set_default_color_theme("dark-blue")
25
+
26
+
27
+ class PassLockerGui(ctk.CTk):
28
+ def __init__(self) -> None:
29
+ super().__init__()
30
+
31
+ self.pl: PassLocker = PassLocker(self.get_master_password)
32
+
33
+ self.set_style()
34
+ self.title("PassLocker GUI")
35
+ self.geometry("800x400")
36
+ self.default_font: ctk.CTkFont = ctk.CTkFont("Arial", size=14)
37
+
38
+ self.pnl: ctk.CTkFrame = ctk.CTkFrame(self)
39
+ label: ctk.CTkLabel = ctk.CTkLabel(
40
+ self.pnl, text="Search ", font=self.default_font
41
+ )
42
+
43
+ self.searchField: ctk.CTkEntry = ctk.CTkEntry(self.pnl, font=self.default_font)
44
+ self.searchField.bind("<KeyRelease>", self.search_changed)
45
+
46
+ label.pack(side=tk.LEFT)
47
+ self.searchField.pack(fill=tk.X)
48
+ self.pnl.pack(fill=tk.X)
49
+
50
+ self.create_menu()
51
+ self.list_passwords()
52
+ # self.createStatusBar()
53
+ # self.setStatusText("Welcome to PassLocker GUI!")
54
+
55
+ def set_style(self) -> None:
56
+ ctk.set_appearance_mode("dark")
57
+ ctk.set_default_color_theme("dark-blue")
58
+
59
+ # ui_font = tkfont.Font(family="Verdana", size=13)
60
+ style: Style = ttk.Style()
61
+ style.theme_use("default")
62
+ style.tk.call("tk", "scaling", 2.0)
63
+
64
+ style.configure(
65
+ "Treeview",
66
+ background="#242424",
67
+ fieldbackground="#242424",
68
+ foreground="#ffffff",
69
+ bordercolor="#444444",
70
+ borderwidth=0,
71
+ rowheight=32,
72
+ font=("Arial", 12),
73
+ )
74
+
75
+ style.configure(
76
+ "Treeview.Heading",
77
+ background="#1f1f1f",
78
+ foreground="#ffffff",
79
+ font=("Arial", 12, "bold"),
80
+ )
81
+
82
+ style.map(
83
+ "Treeview",
84
+ background=[("selected", "#1f6aa5")],
85
+ foreground=[("selected", "#ffffff")],
86
+ )
87
+
88
+ def get_master_password(
89
+ self, prompt: str
90
+ ) -> bytes: # pyright: ignore[reportReturnType]
91
+ password: str | None = None
92
+ while password is None:
93
+ dlg: PasswordDialog = PasswordDialog(self, prompt)
94
+ password: str | None = dlg.password
95
+ if password:
96
+ return password.encode("utf-8")
97
+
98
+ def create_menu(self) -> None:
99
+ menubar: Menu = tk.Menu(
100
+ self,
101
+ background="#242424",
102
+ foreground="#ffffff",
103
+ activebackground="#1f6aa5",
104
+ activeforeground="#ffffff",
105
+ font=("Verdana", 14),
106
+ )
107
+ file_menu: Menu = tk.Menu(
108
+ menubar,
109
+ tearoff=0,
110
+ background="#242424",
111
+ foreground="#ffffff",
112
+ activebackground="#1f6aa5",
113
+ activeforeground="#ffffff",
114
+ font=("Verdana", 14),
115
+ )
116
+ help_menu: Menu = tk.Menu(
117
+ menubar,
118
+ tearoff=0,
119
+ background="#242424",
120
+ foreground="#ffffff",
121
+ activebackground="#1f6aa5",
122
+ activeforeground="#ffffff",
123
+ font=("Verdana", 14),
124
+ )
125
+
126
+ file_menu.add_command(
127
+ label="New Password Entry", command=self.on_new_password_entry
128
+ )
129
+ file_menu.add_command(label="New OTP Entry", command=self.on_new_otp_entry)
130
+ file_menu.add_command(label="New TOTP Entry", command=self.on_new_totp_entry)
131
+
132
+ menubar.add_cascade(label="File", menu=file_menu)
133
+
134
+ help_menu.add_command(label="About", command=self.on_about)
135
+
136
+ menubar.add_cascade(label="Help", menu=help_menu)
137
+ _ = self.config(menu=menubar)
138
+
139
+ def on_new_password_entry(self):
140
+ dlg: NewPasswordAccountDialog = NewPasswordAccountDialog(self)
141
+ account_data: dict[str, str] | None = dlg.result
142
+ if account_data:
143
+ created_on: str = date.today().strftime("%Y-%m-%d")
144
+ acc: Account = Account(
145
+ account_data["account"], account_data["username"], created_on
146
+ )
147
+ acc._passlocker = self.pl
148
+ acc.add_password(account_data["password"])
149
+ acc.save()
150
+
151
+ def on_new_otp_entry(self):
152
+ dlg: NewOTPAccountDialog = NewOTPAccountDialog(self)
153
+ account_data: dict[str, str] | None = dlg.result
154
+ if account_data:
155
+ created_on: str = date.today().strftime("%Y-%m-%d")
156
+ acc: Account = Account(
157
+ account_data["account"],
158
+ account_data["username"],
159
+ created_on,
160
+ type="otp",
161
+ )
162
+ acc._passlocker = self.pl
163
+ passwords: list[str] = account_data["password"].split("\n")
164
+ for password in passwords:
165
+ acc.add_password(password)
166
+ acc.save()
167
+
168
+ def on_new_totp_entry(self):
169
+ dlg: NewTOTPAccountDialog = NewTOTPAccountDialog(self)
170
+ account_data: dict[str, str | int] | None = dlg.result
171
+ created_on: str = date.today().strftime("%Y-%m-%d")
172
+ if account_data:
173
+ acc: Account = Account(
174
+ account=str(account_data["account"]),
175
+ username=str(account_data["username"]),
176
+ created_on=created_on,
177
+ active=0,
178
+ type="totp",
179
+ totp_epoch_start=0,
180
+ totp_time_interval=int(account_data["period"]),
181
+ totp_num_digits=int(account_data["digits"]),
182
+ totp_hash_algorithm=str(
183
+ account_data["algorithm"]
184
+ ), # pyright: ignore[reportArgumentType]
185
+ )
186
+ acc._passlocker = self.pl
187
+ acc.add_password(str(account_data["secret"]))
188
+ acc.save()
189
+
190
+ def on_about(self):
191
+ _ = AboutDialog(
192
+ parent=self,
193
+ title="Passlocker-Tk",
194
+ version="1.0.0",
195
+ author="© 2026 C L",
196
+ github_url="https://github.com/chrislee35/passlocker-tk",
197
+ description=(
198
+ "Passlocker is a secure, offline-first password and OTP manager "
199
+ "designed for transparency, control, and local ownership of secrets."
200
+ ),
201
+ )
202
+
203
+ @debounce(wait=0.3)
204
+ def search_changed(self, event: tk.Event):
205
+ filt: str = self.searchField.get()
206
+ accounts: list[Account] = [
207
+ acc
208
+ for acc in self.pl.accounts()
209
+ if filt in acc.account or filt in acc.username
210
+ ]
211
+ self.table.populate(accounts)
212
+
213
+ def list_passwords(self) -> None:
214
+ self.table: AccountsTable = AccountsTable(self)
215
+ self.table.pack(fill="both", expand=True, padx=10, pady=10)
216
+ accounts: list[Account] = [acc for acc in self.pl.accounts()]
217
+ self.table.populate(accounts)
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: passlocker-tk
3
+ Version: 1.0.0
4
+ Summary: Password Encryption GUI
5
+ Author-email: Chris Lee <github@chrislee.dhs.org>
6
+ License: CC-BY-NC-SA-4.0
7
+ Project-URL: Homepage, https://github.com/chrislee35/passlocker-tk
8
+ Keywords: encryption,passwords,security
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Topic :: Utilities
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: passlocker>=1.0.2
16
+ Requires-Dist: customtkinter>=5.2.2
17
+ Requires-Dist: py-debounce>=0.1.2
18
+ Provides-Extra: dev
19
+ Requires-Dist: build; extra == "dev"
20
+ Requires-Dist: twine; extra == "dev"
21
+ Requires-Dist: black; extra == "dev"
22
+ Requires-Dist: ruff; extra == "dev"
23
+ Requires-Dist: setuptools; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # Overview
27
+
28
+ PassLocker-Tk provides a Tkinter interface around using [Passlocker](https://github.com/chrislee35/passlocker), a local-first password management program.
29
+
30
+ ## Screenshot
31
+
32
+ ![Screenshot of Passlocker-Tk](imgs/screenshot-001.png)
33
+
@@ -0,0 +1,19 @@
1
+ passlocker_tk/__init__.py,sha256=fKkVqrrEEwG0GOF9LpJ2FE5NBi7MxWDnKg1jQ8qpQgo,59
2
+ passlocker_tk/__main__.py,sha256=rRT1jqVsDqw56iLVj1t4pxF3XQUD2TpOK4vHbek17_Y,150
3
+ passlocker_tk/gui.py,sha256=BQLba75OfO_0RS0ew-aNQ-uaSmNLxCI9GO00d5XyqnA,7237
4
+ passlocker_tk/dialogs/__init__.py,sha256=Uy2-JWV-lB8JKyd_zRj5v8dMCkm2hafPM4vBRJ86dQ8,490
5
+ passlocker_tk/dialogs/about_dialog.py,sha256=eHbroi1KqqgswhiQK3nKeeSAVO-61fhvej1BLPdSWEE,2451
6
+ passlocker_tk/dialogs/account_display_dialog.py,sha256=5WRDIhxR7ifNd9CgDBqEl0wfgLweFhnwQkqxB-Lg0hM,4440
7
+ passlocker_tk/dialogs/accounts_table.py,sha256=xKyRr3O5QUWroQv2Ocm9_buklK0AZ2QxvoFh6tbbqu0,2205
8
+ passlocker_tk/dialogs/generate_password_dialog.py,sha256=_u0jh-VDYbsCExl3-ig5csbIIt0VPKv_8-Wb5l6vRso,6370
9
+ passlocker_tk/dialogs/new_otp_account_dialog.py,sha256=AAdWKr0CTJEb4NEe9_9ze59e2UFOOJBLYtFfRgI-y2I,2865
10
+ passlocker_tk/dialogs/new_password_account_dialog.py,sha256=QBBZrPTjEjzoSK07rEPjEWtgqsb5RJEeNvAAJetErJc,3971
11
+ passlocker_tk/dialogs/new_totp_account_dialog.py,sha256=5lE6y6OFlJQ-3jA8xnFVUjxkjoWrdMrumFc2kKnv1Q0,4617
12
+ passlocker_tk/dialogs/password_dialog.py,sha256=1yyOs9685GkU424r23-AxvJH0X153xgXT69BT23YyEc,1424
13
+ passlocker_tk-1.0.0.dist-info/licenses/LICENSE,sha256=QpSZvCTowu2-PPT037J3-vehh3zhzqaLNuewDC_Pmq8,1180
14
+ tests/create_demo.py,sha256=1bPehKcO29PR0Okfv4u8X6MkzGsVI-NAmHcSd5b2Zmk,480
15
+ passlocker_tk-1.0.0.dist-info/METADATA,sha256=FreKef83PrLcFRY3dPCXPm582YNK5Xdt1565r6-RyUw,1064
16
+ passlocker_tk-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
17
+ passlocker_tk-1.0.0.dist-info/entry_points.txt,sha256=soxCksphAO0Lh5BszrgWCpImbxAia5971pjn3WAS_C0,62
18
+ passlocker_tk-1.0.0.dist-info/top_level.txt,sha256=C20iyX3dKT0W5juAHxbFC21sxoS46v08XDKYXxFUfTI,20
19
+ passlocker_tk-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ passlocker-tk = passlocker_tk.__main__:main
@@ -0,0 +1,19 @@
1
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License
2
+
3
+ By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License").
4
+
5
+ You may not use the material for commercial purposes.
6
+
7
+ You are free to:
8
+ - Share — copy and redistribute the material in any medium or format
9
+ - Adapt — remix, transform, and build upon the material
10
+
11
+ Under the following terms:
12
+ - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
13
+ - NonCommercial — You may not use the material for commercial purposes.
14
+ - ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
15
+ - No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
16
+
17
+ For the full license text, see: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
18
+
19
+ SPDX-License-Identifier: CC-BY-NC-SA-4.0
@@ -0,0 +1,2 @@
1
+ passlocker_tk
2
+ tests
tests/create_demo.py ADDED
@@ -0,0 +1,18 @@
1
+ from passlocker_tk import PassLockerGui
2
+ from passlocker.models.account import Account
3
+ from generator_emails import GeneratorEmails
4
+
5
+ app: PassLockerGui = PassLockerGui()
6
+ accounts: list[Account] = []
7
+ generator: GeneratorEmails = GeneratorEmails()
8
+ created_on = "2026-01-01"
9
+ for _ in range(10000):
10
+ accounts.append(
11
+ Account(
12
+ "MyService",
13
+ generator.generate_email(),
14
+ created_on
15
+ )
16
+ )
17
+ app.table.populate(accounts)
18
+ app.mainloop()