basic-password-manager 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (16) hide show
  1. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/LICENSE +1 -1
  2. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/PKG-INFO +18 -2
  3. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/README.md +16 -1
  4. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/basic_password_manager.egg-info/PKG-INFO +18 -2
  5. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/basic_password_manager.egg-info/SOURCES.txt +1 -0
  6. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/basic_password_manager.egg-info/requires.txt +1 -0
  7. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/pw_manager/cli.py +40 -2
  8. basic_password_manager-0.2.0/pw_manager/tui.py +349 -0
  9. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/pyproject.toml +2 -1
  10. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/basic_password_manager.egg-info/dependency_links.txt +0 -0
  11. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/basic_password_manager.egg-info/entry_points.txt +0 -0
  12. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/basic_password_manager.egg-info/top_level.txt +0 -0
  13. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/pw_manager/__init__.py +0 -0
  14. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/pw_manager/crypto.py +0 -0
  15. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/pw_manager/vault.py +0 -0
  16. {basic_password_manager-0.1.0 → basic_password_manager-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Darsh Thakur (Daisentaur)
3
+ Copyright (c) 2026 Daisentaur
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Local encrypted password manager that lives in your terminal
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/Daisentaur/password-manager
@@ -9,6 +9,7 @@ Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: argon2-cffi
11
11
  Requires-Dist: cryptography
12
+ Requires-Dist: textual
12
13
  Dynamic: license-file
13
14
 
14
15
  # pw-manager
@@ -44,12 +45,23 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
44
45
 
45
46
  ## Usage
46
47
 
48
+ Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
49
+ filter, Enter to copy the selected password, `n` to add, `e` to edit,
50
+ `d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
51
+ The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
52
+ admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
53
+ built-in themes are in the picker too.
54
+
55
+ Or use the subcommands:
56
+
47
57
  | Command | What it does |
48
58
  |---|---|
49
59
  | `pw init` | create a new empty vault |
50
60
  | `pw add github` | store an entry (prompts for username/password/notes) |
51
61
  | `pw add github --gen` | same, but generates a strong random password |
52
62
  | `pw get github` | copy the password to the clipboard, show the username |
63
+ | `pw edit github` | update an entry field by field (Enter keeps current) |
64
+ | `pw passwd` | change the master password |
53
65
  | `pw ls` | list entry names |
54
66
  | `pw rm github` | delete an entry |
55
67
  | `pw find bank` | search entry names, usernames, and notes |
@@ -109,7 +121,11 @@ can't destroy the vault.
109
121
  Your master password is never stored anywhere, in any form. "Wrong password"
110
122
  is detected purely by GCM authentication failing.
111
123
 
112
- ### 3. `cli.py` — the CLI
124
+ ### 3. `cli.py` and `tui.py` — the interfaces
125
+
126
+ Both are thin skins over the same two modules — `tui.py` (built with
127
+ [Textual](https://textual.textualize.io)) holds the decrypted entries in
128
+ memory for the session; the CLI re-derives the key per command.
113
129
 
114
130
  argparse subcommands over the two modules above. Passwords are copied to the
115
131
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
@@ -31,12 +31,23 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
31
31
 
32
32
  ## Usage
33
33
 
34
+ Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
35
+ filter, Enter to copy the selected password, `n` to add, `e` to edit,
36
+ `d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
37
+ The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
38
+ admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
39
+ built-in themes are in the picker too.
40
+
41
+ Or use the subcommands:
42
+
34
43
  | Command | What it does |
35
44
  |---|---|
36
45
  | `pw init` | create a new empty vault |
37
46
  | `pw add github` | store an entry (prompts for username/password/notes) |
38
47
  | `pw add github --gen` | same, but generates a strong random password |
39
48
  | `pw get github` | copy the password to the clipboard, show the username |
49
+ | `pw edit github` | update an entry field by field (Enter keeps current) |
50
+ | `pw passwd` | change the master password |
40
51
  | `pw ls` | list entry names |
41
52
  | `pw rm github` | delete an entry |
42
53
  | `pw find bank` | search entry names, usernames, and notes |
@@ -96,7 +107,11 @@ can't destroy the vault.
96
107
  Your master password is never stored anywhere, in any form. "Wrong password"
97
108
  is detected purely by GCM authentication failing.
98
109
 
99
- ### 3. `cli.py` — the CLI
110
+ ### 3. `cli.py` and `tui.py` — the interfaces
111
+
112
+ Both are thin skins over the same two modules — `tui.py` (built with
113
+ [Textual](https://textual.textualize.io)) holds the decrypted entries in
114
+ memory for the session; the CLI re-derives the key per command.
100
115
 
101
116
  argparse subcommands over the two modules above. Passwords are copied to the
102
117
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Local encrypted password manager that lives in your terminal
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/Daisentaur/password-manager
@@ -9,6 +9,7 @@ Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: argon2-cffi
11
11
  Requires-Dist: cryptography
12
+ Requires-Dist: textual
12
13
  Dynamic: license-file
13
14
 
14
15
  # pw-manager
@@ -44,12 +45,23 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
44
45
 
45
46
  ## Usage
46
47
 
48
+ Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
49
+ filter, Enter to copy the selected password, `n` to add, `e` to edit,
50
+ `d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
51
+ The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
52
+ admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
53
+ built-in themes are in the picker too.
54
+
55
+ Or use the subcommands:
56
+
47
57
  | Command | What it does |
48
58
  |---|---|
49
59
  | `pw init` | create a new empty vault |
50
60
  | `pw add github` | store an entry (prompts for username/password/notes) |
51
61
  | `pw add github --gen` | same, but generates a strong random password |
52
62
  | `pw get github` | copy the password to the clipboard, show the username |
63
+ | `pw edit github` | update an entry field by field (Enter keeps current) |
64
+ | `pw passwd` | change the master password |
53
65
  | `pw ls` | list entry names |
54
66
  | `pw rm github` | delete an entry |
55
67
  | `pw find bank` | search entry names, usernames, and notes |
@@ -109,7 +121,11 @@ can't destroy the vault.
109
121
  Your master password is never stored anywhere, in any form. "Wrong password"
110
122
  is detected purely by GCM authentication failing.
111
123
 
112
- ### 3. `cli.py` — the CLI
124
+ ### 3. `cli.py` and `tui.py` — the interfaces
125
+
126
+ Both are thin skins over the same two modules — `tui.py` (built with
127
+ [Textual](https://textual.textualize.io)) holds the decrypted entries in
128
+ memory for the session; the CLI re-derives the key per command.
113
129
 
114
130
  argparse subcommands over the two modules above. Passwords are copied to the
115
131
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
@@ -10,4 +10,5 @@ basic_password_manager.egg-info/top_level.txt
10
10
  pw_manager/__init__.py
11
11
  pw_manager/cli.py
12
12
  pw_manager/crypto.py
13
+ pw_manager/tui.py
13
14
  pw_manager/vault.py
@@ -84,6 +84,34 @@ def cmd_get(args):
84
84
  print(f"password: {e['pw']}")
85
85
 
86
86
 
87
+ def cmd_edit(args):
88
+ """Per-field update; Enter keeps the shown current value."""
89
+ master = ask_master()
90
+ entries = vault.load(VAULT_PATH, master)
91
+ if args.name not in entries:
92
+ sys.exit(f"no entry '{args.name}' — try 'ls'")
93
+ e = entries[args.name]
94
+ user = input(f"username [{e['user']}]: ") or e["user"]
95
+ pw = getpass.getpass("password (empty = keep current): ") or e["pw"]
96
+ notes = input(f"notes [{e.get('notes', '')}]: ") or e.get("notes", "")
97
+ entries[args.name] = {"user": user, "pw": pw, "notes": notes}
98
+ vault.save(VAULT_PATH, master, entries)
99
+ print(f"updated '{args.name}'")
100
+
101
+
102
+ def cmd_passwd(args):
103
+ """Change the master password: decrypt with old, re-encrypt with new."""
104
+ entries = vault.load(VAULT_PATH, ask_master())
105
+ print("choose a new master password")
106
+ new = getpass.getpass("new master password: ")
107
+ if getpass.getpass("confirm new master password: ") != new:
108
+ sys.exit("passwords do not match")
109
+ if len(new) < 8:
110
+ sys.exit("master password must be at least 8 characters")
111
+ vault.save(VAULT_PATH, new, entries)
112
+ print("master password changed (old one is now useless)")
113
+
114
+
87
115
  def cmd_ls(args):
88
116
  entries = vault.load(VAULT_PATH, ask_master())
89
117
  for name in sorted(entries):
@@ -142,8 +170,10 @@ def cmd_import(args):
142
170
 
143
171
 
144
172
  def main():
145
- p = argparse.ArgumentParser(description="local encrypted password manager")
146
- sub = p.add_subparsers(dest="command", required=True)
173
+ p = argparse.ArgumentParser(
174
+ description="local encrypted password manager (run bare for the TUI)"
175
+ )
176
+ sub = p.add_subparsers(dest="command")
147
177
 
148
178
  sub.add_parser("init", help="create a new empty vault")
149
179
  a = sub.add_parser("add", help="add or update an entry")
@@ -151,6 +181,9 @@ def main():
151
181
  a.add_argument("--gen", action="store_true", help="generate the password")
152
182
  g = sub.add_parser("get", help="copy an entry's password to clipboard")
153
183
  g.add_argument("name")
184
+ e = sub.add_parser("edit", help="update an entry field by field")
185
+ e.add_argument("name")
186
+ sub.add_parser("passwd", help="change the master password")
154
187
  sub.add_parser("ls", help="list entry names")
155
188
  r = sub.add_parser("rm", help="delete an entry")
156
189
  r.add_argument("name")
@@ -162,6 +195,11 @@ def main():
162
195
  i.add_argument("csv_file")
163
196
 
164
197
  args = p.parse_args()
198
+ if args.command is None:
199
+ from . import tui # imported lazily: textual is heavy, subcommands skip it
200
+
201
+ tui.main()
202
+ return
165
203
  try:
166
204
  globals()[f"cmd_{args.command}"](args)
167
205
  except vault.VaultError as e:
@@ -0,0 +1,349 @@
1
+ """Interactive TUI, launched by running `pw` with no arguments.
2
+
3
+ Theme palettes copied from tuxedo (github.com/webstonehq/tuxedo,
4
+ src/theme.rs); "muted-slate" is the default. `t` opens the theme picker,
5
+ and Textual's own built-in themes are available there too.
6
+ """
7
+
8
+ import os
9
+
10
+ from textual.app import App, ComposeResult
11
+ from textual.binding import Binding
12
+ from textual.containers import Vertical
13
+ from textual.screen import ModalScreen, Screen
14
+ from textual.theme import Theme
15
+ from textual.widgets import DataTable, Footer, Input, Label, Static
16
+
17
+ from . import vault
18
+ from .cli import VAULT_PATH, generate, to_clipboard
19
+
20
+ THEME_FILE = os.path.expanduser("~/.local/share/pw-manager/theme")
21
+
22
+ TUXEDO_THEMES = [
23
+ Theme(
24
+ name="muted-slate",
25
+ primary="#8aa9c9",
26
+ secondary="#7fb3a8",
27
+ accent="#9a8fc4",
28
+ foreground="#c8ccd4",
29
+ background="#1a1d23",
30
+ surface="#1f232b",
31
+ panel="#252a33",
32
+ success="#7aa67a",
33
+ warning="#d4b06a",
34
+ error="#e07a7a",
35
+ dark=True,
36
+ ),
37
+ Theme(
38
+ name="dawn",
39
+ primary="#a35d3a",
40
+ secondary="#3a7a6a",
41
+ accent="#7a4a8a",
42
+ foreground="#3d3528",
43
+ background="#faf6f0",
44
+ surface="#f3ede2",
45
+ panel="#ede2cc",
46
+ success="#5a7a3a",
47
+ warning="#a3722a",
48
+ error="#b8483a",
49
+ dark=False,
50
+ ),
51
+ Theme(
52
+ name="matrix",
53
+ primary="#9fff9f",
54
+ secondary="#7fcc7f",
55
+ accent="#cf9fff",
56
+ foreground="#7fcc7f",
57
+ background="#0a120a",
58
+ surface="#0f1a0f",
59
+ panel="#101c10",
60
+ success="#9fff9f",
61
+ warning="#ffd66e",
62
+ error="#ff8c8c",
63
+ dark=True,
64
+ ),
65
+ ]
66
+
67
+
68
+ class UnlockScreen(Screen):
69
+ """Master password prompt shown before anything else."""
70
+
71
+ def compose(self) -> ComposeResult:
72
+ with Vertical(id="unlock-box"):
73
+ yield Label("pw-manager", id="unlock-title")
74
+ yield Input(password=True, placeholder="master password", id="master")
75
+ yield Static("", id="unlock-error")
76
+
77
+ def on_input_submitted(self, event: Input.Submitted) -> None:
78
+ self.query_one("#unlock-error", Static).update("unlocking…")
79
+ try:
80
+ self.app.entries = vault.load(VAULT_PATH, event.value)
81
+ except vault.VaultError as e:
82
+ self.query_one("#unlock-error", Static).update(str(e))
83
+ self.query_one("#master", Input).clear()
84
+ return
85
+ self.app.master = event.value
86
+ self.app.push_screen(MainScreen())
87
+
88
+
89
+ class EntryModal(ModalScreen):
90
+ """Add or edit an entry. Empty password = generate (add) / keep current (edit)."""
91
+
92
+ BINDINGS = [Binding("escape", "dismiss", "cancel")]
93
+
94
+ def __init__(self, name: str = "", entry: dict | None = None) -> None:
95
+ super().__init__()
96
+ self.initial_name = name
97
+ self.entry = entry or {}
98
+
99
+ def compose(self) -> ComposeResult:
100
+ editing = bool(self.entry)
101
+ with Vertical(classes="modal-box"):
102
+ yield Label("edit entry" if editing else "new entry", classes="modal-title")
103
+ yield Input(value=self.initial_name, placeholder="name (e.g. github)", id="e-name")
104
+ yield Input(value=self.entry.get("user", ""), placeholder="username", id="e-user")
105
+ yield Input(
106
+ password=True,
107
+ placeholder="password (empty = keep current)" if editing else "password (empty = generate)",
108
+ id="e-pw",
109
+ )
110
+ yield Input(value=self.entry.get("notes", ""), placeholder="notes (optional)", id="e-notes")
111
+ yield Static("enter saves · esc cancels", classes="modal-hint")
112
+
113
+ def on_input_submitted(self, event: Input.Submitted) -> None:
114
+ name = self.query_one("#e-name", Input).value.strip()
115
+ if not name:
116
+ self.query_one("#e-name", Input).focus()
117
+ return
118
+ self.dismiss(
119
+ {
120
+ "name": name,
121
+ "user": self.query_one("#e-user", Input).value,
122
+ "pw": self.query_one("#e-pw", Input).value or self.entry.get("pw") or generate(),
123
+ "notes": self.query_one("#e-notes", Input).value,
124
+ }
125
+ )
126
+
127
+
128
+ class ConfirmModal(ModalScreen):
129
+ """y/n confirmation."""
130
+
131
+ BINDINGS = [
132
+ Binding("y", "yes", "yes"),
133
+ Binding("n,escape", "no", "no"),
134
+ ]
135
+
136
+ def __init__(self, question: str) -> None:
137
+ super().__init__()
138
+ self.question = question
139
+
140
+ def compose(self) -> ComposeResult:
141
+ with Vertical(classes="modal-box"):
142
+ yield Label(self.question, classes="modal-title")
143
+ yield Static("y confirms · n cancels", classes="modal-hint")
144
+
145
+ def action_yes(self) -> None:
146
+ self.dismiss(True)
147
+
148
+ def action_no(self) -> None:
149
+ self.dismiss(False)
150
+
151
+
152
+ class MainScreen(Screen):
153
+ BINDINGS = [
154
+ Binding("enter", "copy", "copy password", priority=True),
155
+ Binding("n", "new", "new entry"),
156
+ Binding("e", "edit", "edit"),
157
+ Binding("d", "delete", "delete"),
158
+ Binding("slash", "search", "search"),
159
+ Binding("t", "app.change_theme", "theme"),
160
+ Binding("escape", "clear_search", show=False),
161
+ Binding("q", "quit", "quit"),
162
+ ]
163
+
164
+ def compose(self) -> ComposeResult:
165
+ yield Input(placeholder="/ to search", id="search")
166
+ yield DataTable(id="table", cursor_type="row")
167
+ yield Footer()
168
+
169
+ def on_mount(self) -> None:
170
+ table = self.query_one(DataTable)
171
+ table.add_columns("name", "username", "notes")
172
+ self.refresh_table()
173
+ table.focus()
174
+
175
+ def refresh_table(self) -> None:
176
+ table = self.query_one(DataTable)
177
+ table.clear()
178
+ term = self.query_one("#search", Input).value.lower()
179
+ for name, e in sorted(self.app.entries.items()):
180
+ if (
181
+ term in name.lower()
182
+ or term in e["user"].lower()
183
+ or term in e.get("notes", "").lower()
184
+ ):
185
+ table.add_row(name, e["user"], e.get("notes", ""), key=name)
186
+
187
+ def save(self) -> None:
188
+ vault.save(VAULT_PATH, self.app.master, self.app.entries)
189
+
190
+ def selected_name(self) -> str | None:
191
+ table = self.query_one(DataTable)
192
+ if table.row_count == 0:
193
+ return None
194
+ return table.coordinate_to_cell_key(table.cursor_coordinate).row_key.value
195
+
196
+ def on_input_changed(self, event: Input.Changed) -> None:
197
+ self.refresh_table()
198
+
199
+ def on_input_submitted(self, event: Input.Submitted) -> None:
200
+ self.query_one(DataTable).focus()
201
+
202
+ def action_copy(self) -> None:
203
+ name = self.selected_name()
204
+ if name is None:
205
+ return
206
+ if to_clipboard(self.app.entries[name]["pw"]):
207
+ self.notify(f"password for '{name}' copied", severity="information")
208
+ else:
209
+ self.notify("no clipboard tool (install wl-clipboard)", severity="error")
210
+
211
+ def action_new(self) -> None:
212
+ def done(entry: dict | None) -> None:
213
+ if entry:
214
+ name = entry.pop("name")
215
+ self.app.entries[name] = entry
216
+ self.save()
217
+ self.refresh_table()
218
+ self.notify(f"stored '{name}'")
219
+
220
+ self.app.push_screen(EntryModal(), done)
221
+
222
+ def action_edit(self) -> None:
223
+ name = self.selected_name()
224
+ if name is None:
225
+ return
226
+
227
+ def done(entry: dict | None) -> None:
228
+ if not entry:
229
+ return
230
+ new_name = entry.pop("name")
231
+ if new_name != name:
232
+ if new_name in self.app.entries:
233
+ self.notify(f"'{new_name}' already exists", severity="error")
234
+ return
235
+ del self.app.entries[name]
236
+ self.app.entries[new_name] = entry
237
+ self.save()
238
+ self.refresh_table()
239
+ self.notify(f"updated '{new_name}'")
240
+
241
+ self.app.push_screen(EntryModal(name, dict(self.app.entries[name])), done)
242
+
243
+ def action_delete(self) -> None:
244
+ name = self.selected_name()
245
+ if name is None:
246
+ return
247
+
248
+ def done(confirmed: bool | None) -> None:
249
+ if confirmed:
250
+ del self.app.entries[name]
251
+ self.save()
252
+ self.refresh_table()
253
+ self.notify(f"removed '{name}'")
254
+
255
+ self.app.push_screen(ConfirmModal(f"delete '{name}'?"), done)
256
+
257
+ def action_search(self) -> None:
258
+ self.query_one("#search", Input).focus()
259
+
260
+ def action_clear_search(self) -> None:
261
+ search = self.query_one("#search", Input)
262
+ search.clear()
263
+ self.query_one(DataTable).focus()
264
+
265
+
266
+ class PwApp(App):
267
+ TITLE = "pw-manager"
268
+
269
+ # Colors come from $theme-variables so every theme applies everywhere;
270
+ # widgets without rules here (table, footer, toasts) use Textual's own
271
+ # theme-aware defaults.
272
+ CSS = """
273
+ #unlock-box {
274
+ align: center middle;
275
+ width: 44;
276
+ height: auto;
277
+ padding: 1 2;
278
+ background: $surface;
279
+ border: round $panel;
280
+ }
281
+ #unlock-title {
282
+ color: $primary;
283
+ text-style: bold;
284
+ margin-bottom: 1;
285
+ }
286
+ #unlock-error {
287
+ color: $error;
288
+ margin-top: 1;
289
+ }
290
+ UnlockScreen {
291
+ align: center middle;
292
+ }
293
+ Input {
294
+ background: $surface;
295
+ border: round $panel;
296
+ }
297
+ Input:focus {
298
+ border: round $primary;
299
+ }
300
+ ModalScreen {
301
+ align: center middle;
302
+ background: $background 60%;
303
+ }
304
+ .modal-box {
305
+ width: 52;
306
+ height: auto;
307
+ padding: 1 2;
308
+ background: $surface;
309
+ border: round $primary;
310
+ }
311
+ .modal-title {
312
+ color: $primary;
313
+ text-style: bold;
314
+ margin-bottom: 1;
315
+ }
316
+ .modal-hint {
317
+ color: $text-muted;
318
+ margin-top: 1;
319
+ }
320
+ """
321
+
322
+ def __init__(self) -> None:
323
+ super().__init__()
324
+ self.entries: dict = {}
325
+ self.master: str = ""
326
+
327
+ def on_mount(self) -> None:
328
+ for theme in TUXEDO_THEMES:
329
+ self.register_theme(theme)
330
+ try:
331
+ saved = open(THEME_FILE).read().strip()
332
+ except OSError:
333
+ saved = ""
334
+ self.theme = saved if saved in self.available_themes else "muted-slate"
335
+ self.theme_changed_signal.subscribe(self, self._remember_theme)
336
+ self.push_screen(UnlockScreen())
337
+
338
+ def _remember_theme(self, theme: Theme) -> None:
339
+ os.makedirs(os.path.dirname(THEME_FILE), exist_ok=True)
340
+ with open(THEME_FILE, "w") as f:
341
+ f.write(theme.name)
342
+
343
+
344
+ def main() -> None:
345
+ PwApp().run()
346
+
347
+
348
+ if __name__ == "__main__":
349
+ main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "basic-password-manager"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Local encrypted password manager that lives in your terminal"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -12,6 +12,7 @@ requires-python = ">=3.10"
12
12
  dependencies = [
13
13
  "argon2-cffi",
14
14
  "cryptography",
15
+ "textual",
15
16
  ]
16
17
 
17
18
  [project.scripts]