basic-password-manager 0.2.0__tar.gz → 0.2.2__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.2.0 → basic_password_manager-0.2.2}/PKG-INFO +12 -4
  2. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/README.md +10 -3
  3. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/PKG-INFO +12 -4
  4. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/pw_manager/cli.py +16 -11
  5. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/pw_manager/tui.py +132 -8
  6. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/pyproject.toml +2 -1
  7. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/LICENSE +0 -0
  8. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/SOURCES.txt +0 -0
  9. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/dependency_links.txt +0 -0
  10. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/entry_points.txt +0 -0
  11. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/requires.txt +0 -0
  12. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/top_level.txt +0 -0
  13. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/pw_manager/__init__.py +0 -0
  14. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/pw_manager/crypto.py +0 -0
  15. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/pw_manager/vault.py +0 -0
  16. {basic_password_manager-0.2.0 → basic_password_manager-0.2.2}/setup.cfg +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
7
+ Project-URL: Changelog, https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md
7
8
  Requires-Python: >=3.10
8
9
  Description-Content-Type: text/markdown
9
10
  License-File: LICENSE
@@ -48,6 +49,12 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
48
49
  Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
49
50
  filter, Enter to copy the selected password, `n` to add, `e` to edit,
50
51
  `d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
52
+ The search box matches every word of your query anywhere in an entry —
53
+ name, username, notes, and the password itself — with matches highlighted
54
+ in the theme's accent color (password matches show the row but highlight
55
+ nothing, since passwords are never displayed). Rarer operations live in the
56
+ command palette (`ctrl+p`): importing a browser CSV, changing the master
57
+ password.
51
58
  The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
52
59
  admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
53
60
  built-in themes are in the picker too.
@@ -64,7 +71,7 @@ Or use the subcommands:
64
71
  | `pw passwd` | change the master password |
65
72
  | `pw ls` | list entry names |
66
73
  | `pw rm github` | delete an entry |
67
- | `pw find bank` | search entry names, usernames, and notes |
74
+ | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
68
75
  | `pw gen -l 32` | just print a random password |
69
76
  | `pw import passwords.csv` | import a browser CSV export (see below) |
70
77
 
@@ -129,9 +136,10 @@ memory for the session; the CLI re-derives the key per command.
129
136
 
130
137
  argparse subcommands over the two modules above. Passwords are copied to the
131
138
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
132
- so they don't sit in your terminal scrollback. `find` searches names,
139
+ so they don't sit in your terminal scrollback. CLI `find` searches names,
133
140
  usernames, and notes — deliberately *not* passwords, so fragments of secrets
134
- never land in your shell history.
141
+ never land in your shell history; the TUI search box does cover passwords,
142
+ because nothing typed there is logged anywhere.
135
143
 
136
144
  ## Backups
137
145
 
@@ -34,6 +34,12 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
34
34
  Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
35
35
  filter, Enter to copy the selected password, `n` to add, `e` to edit,
36
36
  `d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
37
+ The search box matches every word of your query anywhere in an entry —
38
+ name, username, notes, and the password itself — with matches highlighted
39
+ in the theme's accent color (password matches show the row but highlight
40
+ nothing, since passwords are never displayed). Rarer operations live in the
41
+ command palette (`ctrl+p`): importing a browser CSV, changing the master
42
+ password.
37
43
  The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
38
44
  admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
39
45
  built-in themes are in the picker too.
@@ -50,7 +56,7 @@ Or use the subcommands:
50
56
  | `pw passwd` | change the master password |
51
57
  | `pw ls` | list entry names |
52
58
  | `pw rm github` | delete an entry |
53
- | `pw find bank` | search entry names, usernames, and notes |
59
+ | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
54
60
  | `pw gen -l 32` | just print a random password |
55
61
  | `pw import passwords.csv` | import a browser CSV export (see below) |
56
62
 
@@ -115,9 +121,10 @@ memory for the session; the CLI re-derives the key per command.
115
121
 
116
122
  argparse subcommands over the two modules above. Passwords are copied to the
117
123
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
118
- so they don't sit in your terminal scrollback. `find` searches names,
124
+ so they don't sit in your terminal scrollback. CLI `find` searches names,
119
125
  usernames, and notes — deliberately *not* passwords, so fragments of secrets
120
- never land in your shell history.
126
+ never land in your shell history; the TUI search box does cover passwords,
127
+ because nothing typed there is logged anywhere.
121
128
 
122
129
  ## Backups
123
130
 
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
7
+ Project-URL: Changelog, https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md
7
8
  Requires-Python: >=3.10
8
9
  Description-Content-Type: text/markdown
9
10
  License-File: LICENSE
@@ -48,6 +49,12 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
48
49
  Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
49
50
  filter, Enter to copy the selected password, `n` to add, `e` to edit,
50
51
  `d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
52
+ The search box matches every word of your query anywhere in an entry —
53
+ name, username, notes, and the password itself — with matches highlighted
54
+ in the theme's accent color (password matches show the row but highlight
55
+ nothing, since passwords are never displayed). Rarer operations live in the
56
+ command palette (`ctrl+p`): importing a browser CSV, changing the master
57
+ password.
51
58
  The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
52
59
  admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
53
60
  built-in themes are in the picker too.
@@ -64,7 +71,7 @@ Or use the subcommands:
64
71
  | `pw passwd` | change the master password |
65
72
  | `pw ls` | list entry names |
66
73
  | `pw rm github` | delete an entry |
67
- | `pw find bank` | search entry names, usernames, and notes |
74
+ | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
68
75
  | `pw gen -l 32` | just print a random password |
69
76
  | `pw import passwords.csv` | import a browser CSV export (see below) |
70
77
 
@@ -129,9 +136,10 @@ memory for the session; the CLI re-derives the key per command.
129
136
 
130
137
  argparse subcommands over the two modules above. Passwords are copied to the
131
138
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
132
- so they don't sit in your terminal scrollback. `find` searches names,
139
+ so they don't sit in your terminal scrollback. CLI `find` searches names,
133
140
  usernames, and notes — deliberately *not* passwords, so fragments of secrets
134
- never land in your shell history.
141
+ never land in your shell history; the TUI search box does cover passwords,
142
+ because nothing typed there is logged anywhere.
135
143
 
136
144
  ## Backups
137
145
 
@@ -135,16 +135,15 @@ def cmd_gen(args):
135
135
 
136
136
 
137
137
  def cmd_find(args):
138
- """Substring search over names, usernames, notes deliberately not over
139
- passwords: a password fragment typed as an argument lands in shell history."""
138
+ """Every word of the query must appear somewhere in name/username/notes.
139
+ Deliberately not over passwords: a password fragment typed as a CLI
140
+ argument lands in shell history (the TUI search does cover passwords)."""
140
141
  entries = vault.load(VAULT_PATH, ask_master())
141
- term = args.term.lower()
142
+ words = args.term.lower().split()
142
143
  hits = {
143
144
  name: e
144
145
  for name, e in sorted(entries.items())
145
- if term in name.lower()
146
- or term in e["user"].lower()
147
- or term in e.get("notes", "").lower()
146
+ if all(w in f"{name} {e['user']} {e.get('notes', '')}".lower() for w in words)
148
147
  }
149
148
  for name, e in hits.items():
150
149
  print(f"{name} ({e['user']})")
@@ -152,12 +151,11 @@ def cmd_find(args):
152
151
  sys.exit(f"nothing matches '{args.term}'")
153
152
 
154
153
 
155
- def cmd_import(args):
156
- """Browser CSV export (Chrome: name,url,username,password / Firefox: url,...)."""
157
- master = ask_master()
158
- entries = vault.load(VAULT_PATH, master)
154
+ def import_csv(csv_file: str, entries: dict) -> int:
155
+ """Merge a browser CSV export (Chrome: name,url,username,password /
156
+ Firefox: url,...) into entries. Returns how many were added."""
159
157
  added = 0
160
- with open(args.csv_file, newline="") as f:
158
+ with open(csv_file, newline="") as f:
161
159
  for row in csv.DictReader(f):
162
160
  name = row.get("name") or urlparse(row.get("url", "")).netloc
163
161
  pw = row.get("password")
@@ -165,6 +163,13 @@ def cmd_import(args):
165
163
  continue
166
164
  entries[name] = {"user": row.get("username", ""), "pw": pw, "notes": ""}
167
165
  added += 1
166
+ return added
167
+
168
+
169
+ def cmd_import(args):
170
+ master = ask_master()
171
+ entries = vault.load(VAULT_PATH, master)
172
+ added = import_csv(args.csv_file, entries)
168
173
  vault.save(VAULT_PATH, master, entries)
169
174
  print(f"imported {added} entries — now delete {args.csv_file} securely")
170
175
 
@@ -5,17 +5,20 @@ src/theme.rs); "muted-slate" is the default. `t` opens the theme picker,
5
5
  and Textual's own built-in themes are available there too.
6
6
  """
7
7
 
8
+ import csv
8
9
  import os
9
10
 
11
+ from rich.text import Text
10
12
  from textual.app import App, ComposeResult
11
13
  from textual.binding import Binding
14
+ from textual.command import DiscoveryHit, Hit, Hits, Provider
12
15
  from textual.containers import Vertical
13
16
  from textual.screen import ModalScreen, Screen
14
17
  from textual.theme import Theme
15
18
  from textual.widgets import DataTable, Footer, Input, Label, Static
16
19
 
17
20
  from . import vault
18
- from .cli import VAULT_PATH, generate, to_clipboard
21
+ from .cli import VAULT_PATH, generate, import_csv, to_clipboard
19
22
 
20
23
  THEME_FILE = os.path.expanduser("~/.local/share/pw-manager/theme")
21
24
 
@@ -125,6 +128,53 @@ class EntryModal(ModalScreen):
125
128
  )
126
129
 
127
130
 
131
+ class ImportModal(ModalScreen):
132
+ """Ask for the path of a browser CSV export."""
133
+
134
+ BINDINGS = [Binding("escape", "dismiss", "cancel")]
135
+
136
+ def compose(self) -> ComposeResult:
137
+ with Vertical(classes="modal-box"):
138
+ yield Label("import browser CSV", classes="modal-title")
139
+ yield Input(placeholder="path to export, e.g. ~/Downloads/passwords.csv", id="i-path")
140
+ yield Static("enter imports · esc cancels", classes="modal-hint")
141
+
142
+ def on_input_submitted(self, event: Input.Submitted) -> None:
143
+ if event.value.strip():
144
+ self.dismiss(os.path.expanduser(event.value.strip()))
145
+
146
+
147
+ class PasswdModal(ModalScreen):
148
+ """Change the master password. Asks for the current one again so a
149
+ walked-up-to unlocked session can't lock the real owner out."""
150
+
151
+ BINDINGS = [Binding("escape", "dismiss", "cancel")]
152
+
153
+ def compose(self) -> ComposeResult:
154
+ with Vertical(classes="modal-box"):
155
+ yield Label("change master password", classes="modal-title")
156
+ yield Input(password=True, placeholder="current master password", id="p-current")
157
+ yield Input(password=True, placeholder="new master password (8+ chars)", id="p-new")
158
+ yield Input(password=True, placeholder="confirm new master password", id="p-confirm")
159
+ yield Static("enter saves · esc cancels", classes="modal-hint")
160
+ yield Static("", id="p-error")
161
+
162
+ def on_input_submitted(self, event: Input.Submitted) -> None:
163
+ current = self.query_one("#p-current", Input).value
164
+ new = self.query_one("#p-new", Input).value
165
+ error = self.query_one("#p-error", Static)
166
+ if current != self.app.master:
167
+ error.update("current master password is wrong")
168
+ return
169
+ if len(new) < 8:
170
+ error.update("new master password must be at least 8 characters")
171
+ return
172
+ if new != self.query_one("#p-confirm", Input).value:
173
+ error.update("new passwords do not match")
174
+ return
175
+ self.dismiss(new)
176
+
177
+
128
178
  class ConfirmModal(ModalScreen):
129
179
  """y/n confirmation."""
130
180
 
@@ -171,18 +221,40 @@ class MainScreen(Screen):
171
221
  table.add_columns("name", "username", "notes")
172
222
  self.refresh_table()
173
223
  table.focus()
224
+ # match-highlight color comes from the theme, so repaint on switch
225
+ self.app.theme_changed_signal.subscribe(self, self._on_theme_change)
226
+
227
+ def _on_theme_change(self, _theme: Theme) -> None:
228
+ self.refresh_table()
229
+
230
+ @staticmethod
231
+ def _highlight(value: str, words: list[str], style: str) -> Text:
232
+ text = Text(value)
233
+ low = value.lower()
234
+ for w in words:
235
+ start = 0
236
+ while (i := low.find(w, start)) != -1:
237
+ text.stylize(style, i, i + len(w))
238
+ start = i + 1
239
+ return text
174
240
 
175
241
  def refresh_table(self) -> None:
242
+ """Every search word must appear somewhere in the entry — name,
243
+ username, notes, or the password itself. Passwords are searched but
244
+ never displayed, so matches there can't be highlighted (by design)."""
176
245
  table = self.query_one(DataTable)
177
246
  table.clear()
178
- term = self.query_one("#search", Input).value.lower()
247
+ words = self.query_one("#search", Input).value.lower().split()
248
+ style = f"bold {self.app.current_theme.primary}"
179
249
  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)
250
+ hay = f"{name} {e['user']} {e.get('notes', '')} {e['pw']}".lower()
251
+ if all(w in hay for w in words):
252
+ table.add_row(
253
+ self._highlight(name, words, style),
254
+ self._highlight(e["user"], words, style),
255
+ self._highlight(e.get("notes", ""), words, style),
256
+ key=name,
257
+ )
186
258
 
187
259
  def save(self) -> None:
188
260
  vault.save(VAULT_PATH, self.app.master, self.app.entries)
@@ -263,8 +335,35 @@ class MainScreen(Screen):
263
335
  self.query_one(DataTable).focus()
264
336
 
265
337
 
338
+ class VaultCommands(Provider):
339
+ """Palette commands for the unlocked vault. Unlike Textual's built-in
340
+ provider, the query matches descriptions too, not just titles."""
341
+
342
+ @property
343
+ def _commands(self):
344
+ if not isinstance(self.screen, MainScreen):
345
+ return []
346
+ app = self.app
347
+ return [
348
+ ("Import browser CSV", "add every entry from a Chrome/Firefox password export", app._import_csv),
349
+ ("Change master password", "re-encrypt the vault under a new master password", app._change_master),
350
+ ]
351
+
352
+ async def search(self, query: str) -> Hits:
353
+ matcher = self.matcher(query)
354
+ for title, help_text, callback in self._commands:
355
+ score = matcher.match(f"{title} {help_text}")
356
+ if score > 0:
357
+ yield Hit(score, matcher.highlight(title), callback, help=help_text)
358
+
359
+ async def discover(self) -> Hits:
360
+ for title, help_text, callback in self._commands:
361
+ yield DiscoveryHit(title, callback, help=help_text)
362
+
363
+
266
364
  class PwApp(App):
267
365
  TITLE = "pw-manager"
366
+ COMMANDS = App.COMMANDS | {VaultCommands}
268
367
 
269
368
  # Colors come from $theme-variables so every theme applies everywhere;
270
369
  # widgets without rules here (table, footer, toasts) use Textual's own
@@ -340,6 +439,31 @@ class PwApp(App):
340
439
  with open(THEME_FILE, "w") as f:
341
440
  f.write(theme.name)
342
441
 
442
+ def _import_csv(self) -> None:
443
+ def done(path: str | None) -> None:
444
+ if not path:
445
+ return
446
+ try:
447
+ added = import_csv(path, self.entries)
448
+ except (OSError, csv.Error) as e:
449
+ self.notify(str(e), severity="error")
450
+ return
451
+ vault.save(VAULT_PATH, self.master, self.entries)
452
+ if isinstance(self.screen, MainScreen):
453
+ self.screen.refresh_table()
454
+ self.notify(f"imported {added} entries — delete the CSV securely")
455
+
456
+ self.push_screen(ImportModal(), done)
457
+
458
+ def _change_master(self) -> None:
459
+ def done(new: str | None) -> None:
460
+ if new:
461
+ self.master = new
462
+ vault.save(VAULT_PATH, self.master, self.entries)
463
+ self.notify("master password changed (old one is now useless)")
464
+
465
+ self.push_screen(PasswdModal(), done)
466
+
343
467
 
344
468
  def main() -> None:
345
469
  PwApp().run()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "basic-password-manager"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Local encrypted password manager that lives in your terminal"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -20,3 +20,4 @@ pw = "pw_manager.cli:main"
20
20
 
21
21
  [project.urls]
22
22
  Repository = "https://github.com/Daisentaur/password-manager"
23
+ Changelog = "https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md"