basic-password-manager 0.2.1__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.1 → basic_password_manager-0.2.2}/PKG-INFO +12 -6
  2. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/README.md +10 -5
  3. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/PKG-INFO +12 -6
  4. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/cli.py +16 -11
  5. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/tui.py +92 -18
  6. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pyproject.toml +2 -1
  7. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/LICENSE +0 -0
  8. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/SOURCES.txt +0 -0
  9. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/dependency_links.txt +0 -0
  10. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/entry_points.txt +0 -0
  11. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/requires.txt +0 -0
  12. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/top_level.txt +0 -0
  13. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/__init__.py +0 -0
  14. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/crypto.py +0 -0
  15. {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/vault.py +0 -0
  16. {basic_password_manager-0.2.1 → 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.1
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,8 +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.
51
- Rarer operations live in the command palette (`ctrl+p`), e.g. changing the
52
- master password.
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.
53
58
  The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
54
59
  admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
55
60
  built-in themes are in the picker too.
@@ -66,7 +71,7 @@ Or use the subcommands:
66
71
  | `pw passwd` | change the master password |
67
72
  | `pw ls` | list entry names |
68
73
  | `pw rm github` | delete an entry |
69
- | `pw find bank` | search entry names, usernames, and notes |
74
+ | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
70
75
  | `pw gen -l 32` | just print a random password |
71
76
  | `pw import passwords.csv` | import a browser CSV export (see below) |
72
77
 
@@ -131,9 +136,10 @@ memory for the session; the CLI re-derives the key per command.
131
136
 
132
137
  argparse subcommands over the two modules above. Passwords are copied to the
133
138
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
134
- 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,
135
140
  usernames, and notes — deliberately *not* passwords, so fragments of secrets
136
- 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.
137
143
 
138
144
  ## Backups
139
145
 
@@ -34,8 +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
- Rarer operations live in the command palette (`ctrl+p`), e.g. changing the
38
- master password.
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.
39
43
  The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
40
44
  admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
41
45
  built-in themes are in the picker too.
@@ -52,7 +56,7 @@ Or use the subcommands:
52
56
  | `pw passwd` | change the master password |
53
57
  | `pw ls` | list entry names |
54
58
  | `pw rm github` | delete an entry |
55
- | `pw find bank` | search entry names, usernames, and notes |
59
+ | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
56
60
  | `pw gen -l 32` | just print a random password |
57
61
  | `pw import passwords.csv` | import a browser CSV export (see below) |
58
62
 
@@ -117,9 +121,10 @@ memory for the session; the CLI re-derives the key per command.
117
121
 
118
122
  argparse subcommands over the two modules above. Passwords are copied to the
119
123
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
120
- 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,
121
125
  usernames, and notes — deliberately *not* passwords, so fragments of secrets
122
- 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.
123
128
 
124
129
  ## Backups
125
130
 
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.2.1
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,8 +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.
51
- Rarer operations live in the command palette (`ctrl+p`), e.g. changing the
52
- master password.
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.
53
58
  The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
54
59
  admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
55
60
  built-in themes are in the picker too.
@@ -66,7 +71,7 @@ Or use the subcommands:
66
71
  | `pw passwd` | change the master password |
67
72
  | `pw ls` | list entry names |
68
73
  | `pw rm github` | delete an entry |
69
- | `pw find bank` | search entry names, usernames, and notes |
74
+ | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
70
75
  | `pw gen -l 32` | just print a random password |
71
76
  | `pw import passwords.csv` | import a browser CSV export (see below) |
72
77
 
@@ -131,9 +136,10 @@ memory for the session; the CLI re-derives the key per command.
131
136
 
132
137
  argparse subcommands over the two modules above. Passwords are copied to the
133
138
  clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
134
- 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,
135
140
  usernames, and notes — deliberately *not* passwords, so fragments of secrets
136
- 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.
137
143
 
138
144
  ## Backups
139
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,18 +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
- from typing import Iterable
10
10
 
11
- from textual.app import App, ComposeResult, SystemCommand
11
+ from rich.text import Text
12
+ from textual.app import App, ComposeResult
12
13
  from textual.binding import Binding
14
+ from textual.command import DiscoveryHit, Hit, Hits, Provider
13
15
  from textual.containers import Vertical
14
16
  from textual.screen import ModalScreen, Screen
15
17
  from textual.theme import Theme
16
18
  from textual.widgets import DataTable, Footer, Input, Label, Static
17
19
 
18
20
  from . import vault
19
- from .cli import VAULT_PATH, generate, to_clipboard
21
+ from .cli import VAULT_PATH, generate, import_csv, to_clipboard
20
22
 
21
23
  THEME_FILE = os.path.expanduser("~/.local/share/pw-manager/theme")
22
24
 
@@ -126,6 +128,22 @@ class EntryModal(ModalScreen):
126
128
  )
127
129
 
128
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
+
129
147
  class PasswdModal(ModalScreen):
130
148
  """Change the master password. Asks for the current one again so a
131
149
  walked-up-to unlocked session can't lock the real owner out."""
@@ -203,18 +221,40 @@ class MainScreen(Screen):
203
221
  table.add_columns("name", "username", "notes")
204
222
  self.refresh_table()
205
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
206
240
 
207
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)."""
208
245
  table = self.query_one(DataTable)
209
246
  table.clear()
210
- 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}"
211
249
  for name, e in sorted(self.app.entries.items()):
212
- if (
213
- term in name.lower()
214
- or term in e["user"].lower()
215
- or term in e.get("notes", "").lower()
216
- ):
217
- 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
+ )
218
258
 
219
259
  def save(self) -> None:
220
260
  vault.save(VAULT_PATH, self.app.master, self.app.entries)
@@ -295,8 +335,35 @@ class MainScreen(Screen):
295
335
  self.query_one(DataTable).focus()
296
336
 
297
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
+
298
364
  class PwApp(App):
299
365
  TITLE = "pw-manager"
366
+ COMMANDS = App.COMMANDS | {VaultCommands}
300
367
 
301
368
  # Colors come from $theme-variables so every theme applies everywhere;
302
369
  # widgets without rules here (table, footer, toasts) use Textual's own
@@ -372,14 +439,21 @@ class PwApp(App):
372
439
  with open(THEME_FILE, "w") as f:
373
440
  f.write(theme.name)
374
441
 
375
- def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
376
- yield from super().get_system_commands(screen)
377
- if isinstance(screen, MainScreen):
378
- yield SystemCommand(
379
- "Change master password",
380
- "re-encrypt the vault under a new master password",
381
- self._change_master,
382
- )
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)
383
457
 
384
458
  def _change_master(self) -> None:
385
459
  def done(new: str | None) -> None:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "basic-password-manager"
7
- version = "0.2.1"
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"