basic-password-manager 0.2.2__tar.gz → 0.2.4__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 (17) hide show
  1. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/PKG-INFO +11 -4
  2. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/README.md +9 -2
  3. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/PKG-INFO +11 -4
  4. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/SOURCES.txt +1 -0
  5. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/cli.py +22 -1
  6. basic_password_manager-0.2.4/pw_manager/logo.py +130 -0
  7. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/tui.py +31 -3
  8. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pyproject.toml +5 -2
  9. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/LICENSE +0 -0
  10. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/dependency_links.txt +0 -0
  11. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/entry_points.txt +0 -0
  12. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/requires.txt +0 -0
  13. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/top_level.txt +0 -0
  14. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/__init__.py +0 -0
  15. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/crypto.py +0 -0
  16. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/vault.py +0 -0
  17. {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.2.2
4
- Summary: Local encrypted password manager that lives in your terminal
3
+ Version: 0.2.4
4
+ Summary: The Paladin — a 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
7
  Project-URL: Changelog, https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md
@@ -13,11 +13,17 @@ Requires-Dist: cryptography
13
13
  Requires-Dist: textual
14
14
  Dynamic: license-file
15
15
 
16
- # pw-manager
16
+ <p align="center">
17
+ <img src="assets/logo.png" width="160" alt="The Paladin — a pixel-art knight" />
18
+ </p>
19
+
20
+ <h1 align="center">The Paladin</h1>
21
+
22
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, no mercy.</em></p>
17
23
 
18
24
  A local, encrypted password manager that lives in your terminal. No browser,
19
25
  no cloud, no daemon — one encrypted file on disk and a small Python CLI in
20
- front of it.
26
+ front of it. (Installs as `basic-password-manager`; answers to `pw`.)
21
27
 
22
28
  ```
23
29
  $ python pw.py get github
@@ -74,6 +80,7 @@ Or use the subcommands:
74
80
  | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
75
81
  | `pw gen -l 32` | just print a random password |
76
82
  | `pw import passwords.csv` | import a browser CSV export (see below) |
83
+ | `pw about` | meet the knight |
77
84
 
78
85
  The vault lives at `~/.local/share/pw-manager/vault`. Set the `PW_VAULT`
79
86
  environment variable to put it somewhere else.
@@ -1,8 +1,14 @@
1
- # pw-manager
1
+ <p align="center">
2
+ <img src="assets/logo.png" width="160" alt="The Paladin — a pixel-art knight" />
3
+ </p>
4
+
5
+ <h1 align="center">The Paladin</h1>
6
+
7
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, no mercy.</em></p>
2
8
 
3
9
  A local, encrypted password manager that lives in your terminal. No browser,
4
10
  no cloud, no daemon — one encrypted file on disk and a small Python CLI in
5
- front of it.
11
+ front of it. (Installs as `basic-password-manager`; answers to `pw`.)
6
12
 
7
13
  ```
8
14
  $ python pw.py get github
@@ -59,6 +65,7 @@ Or use the subcommands:
59
65
  | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
60
66
  | `pw gen -l 32` | just print a random password |
61
67
  | `pw import passwords.csv` | import a browser CSV export (see below) |
68
+ | `pw about` | meet the knight |
62
69
 
63
70
  The vault lives at `~/.local/share/pw-manager/vault`. Set the `PW_VAULT`
64
71
  environment variable to put it somewhere else.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.2.2
4
- Summary: Local encrypted password manager that lives in your terminal
3
+ Version: 0.2.4
4
+ Summary: The Paladin — a 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
7
  Project-URL: Changelog, https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md
@@ -13,11 +13,17 @@ Requires-Dist: cryptography
13
13
  Requires-Dist: textual
14
14
  Dynamic: license-file
15
15
 
16
- # pw-manager
16
+ <p align="center">
17
+ <img src="assets/logo.png" width="160" alt="The Paladin — a pixel-art knight" />
18
+ </p>
19
+
20
+ <h1 align="center">The Paladin</h1>
21
+
22
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, no mercy.</em></p>
17
23
 
18
24
  A local, encrypted password manager that lives in your terminal. No browser,
19
25
  no cloud, no daemon — one encrypted file on disk and a small Python CLI in
20
- front of it.
26
+ front of it. (Installs as `basic-password-manager`; answers to `pw`.)
21
27
 
22
28
  ```
23
29
  $ python pw.py get github
@@ -74,6 +80,7 @@ Or use the subcommands:
74
80
  | `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
75
81
  | `pw gen -l 32` | just print a random password |
76
82
  | `pw import passwords.csv` | import a browser CSV export (see below) |
83
+ | `pw about` | meet the knight |
77
84
 
78
85
  The vault lives at `~/.local/share/pw-manager/vault`. Set the `PW_VAULT`
79
86
  environment variable to put it somewhere else.
@@ -10,5 +10,6 @@ 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/logo.py
13
14
  pw_manager/tui.py
14
15
  pw_manager/vault.py
@@ -47,7 +47,27 @@ def cmd_init(args):
47
47
  sys.exit(f"vault already exists at {VAULT_PATH}")
48
48
  os.makedirs(os.path.dirname(VAULT_PATH), exist_ok=True)
49
49
  vault.save(VAULT_PATH, ask_master(confirm=True), {})
50
- print(f"empty vault created at {VAULT_PATH}")
50
+ if sys.stdout.isatty():
51
+ from . import logo
52
+
53
+ print(logo.says(f"empty vault created at {VAULT_PATH}"))
54
+ else:
55
+ print(f"empty vault created at {VAULT_PATH}")
56
+
57
+
58
+ def cmd_about(args):
59
+ from importlib.metadata import PackageNotFoundError, version
60
+
61
+ try:
62
+ v = version("basic-password-manager")
63
+ except PackageNotFoundError:
64
+ v = "dev" # running from source without an install
65
+ from . import logo
66
+
67
+ print(logo.ascii_art())
68
+ print(f"The Paladin ⚔ v{v}")
69
+ print("your passwords, guarded locally — no browser, no cloud, no mercy")
70
+ print("https://github.com/Daisentaur/password-manager")
51
71
 
52
72
 
53
73
  def cmd_add(args):
@@ -198,6 +218,7 @@ def main():
198
218
  f.add_argument("term")
199
219
  i = sub.add_parser("import", help="import a browser CSV export")
200
220
  i.add_argument("csv_file")
221
+ sub.add_parser("about", help="meet the knight")
201
222
 
202
223
  args = p.parse_args()
203
224
  if args.command is None:
@@ -0,0 +1,130 @@
1
+ """The Paladin, as terminal pixel art.
2
+
3
+ Extracted once from assets/logo.png (each letter below is one pixel; dev-time
4
+ script, no image libraries at runtime). Rendered with half-block characters:
5
+ one character cell shows two pixels — top half foreground, bottom half
6
+ background.
7
+ """
8
+
9
+ from rich.style import Style
10
+ from rich.text import Text
11
+
12
+ PALETTE = {
13
+ "k": (0, 0, 0), "s": (140, 139, 147), "w": (194, 191, 198),
14
+ "o": (243, 243, 245), "g": (86, 85, 91), "d": (32, 32, 32),
15
+ "l": (158, 157, 163), "b": (246, 120, 59), "r": (143, 64, 47),
16
+ }
17
+
18
+ ROWS = [
19
+ "......kkk.........",
20
+ ".....kswwkk.......",
21
+ "....wwwwwwwk......",
22
+ "...kwwwwwwwok.....",
23
+ "...kowwwwwwwk.....",
24
+ "...kgdooodgwk.....",
25
+ "...kdddddddwk...k.",
26
+ "...kwwdddwwwk..kok",
27
+ "..kkwwwddwwwk.kgl.",
28
+ ".ookoowddookk.glk.",
29
+ "kgskkkwookkkbblk..",
30
+ "kssswwkkksskkrk...",
31
+ ".kkkssgggwwksk....",
32
+ "...ksswwwwwkk.....",
33
+ "...kgkkkkggk......",
34
+ "....k.....k.......",
35
+ ]
36
+
37
+
38
+ def _pixel_pairs():
39
+ """Yield rows of (top_color, bottom_color) tuples, one per char cell."""
40
+ for top, bottom in zip(ROWS[::2], ROWS[1::2]):
41
+ yield [
42
+ (PALETTE.get(t), PALETTE.get(b))
43
+ for t, b in zip(top, bottom)
44
+ ]
45
+
46
+
47
+ def ansi(indent: str = "") -> str:
48
+ """The knight as a raw ANSI truecolor string, for print()."""
49
+ out = []
50
+ for row in _pixel_pairs():
51
+ line = indent
52
+ for top, bottom in row:
53
+ if top is None and bottom is None:
54
+ line += "\x1b[0m "
55
+ elif bottom is None:
56
+ line += "\x1b[0m\x1b[38;2;%d;%d;%dm▀" % top
57
+ elif top is None:
58
+ line += "\x1b[0m\x1b[38;2;%d;%d;%dm▄" % bottom
59
+ else:
60
+ line += "\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm▀" % (top + bottom)
61
+ out.append(line + "\x1b[0m")
62
+ return "\n".join(out)
63
+
64
+
65
+ _ORANGE = (PALETTE["b"], PALETTE["r"])
66
+
67
+
68
+ def _tinted(pixel, tint, accent):
69
+ """Recolor a pixel: keep its brightness, take its hue from the theme —
70
+ the accent color for the sword guard, the tint for everything else."""
71
+ if tint is None or pixel is None:
72
+ return pixel
73
+ base = accent if pixel in _ORANGE else tint
74
+ lum = sum(pixel) / 765
75
+ return tuple(int(c * lum) for c in base)
76
+
77
+
78
+ def _hex_rgb(color: str) -> tuple:
79
+ color = color.lstrip("#")
80
+ return tuple(int(color[i : i + 2], 16) for i in (0, 2, 4))
81
+
82
+
83
+ def rich_text(tint: str | None = None, accent: str | None = None) -> Text:
84
+ """The knight as a Rich Text, for Textual widgets. Pass theme colors as
85
+ hex strings to render him in the theme's livery instead of silver."""
86
+ tint_rgb = _hex_rgb(tint) if tint else None
87
+ accent_rgb = _hex_rgb(accent) if accent else tint_rgb
88
+ text = Text()
89
+ for row in _pixel_pairs():
90
+ for top, bottom in row:
91
+ top = _tinted(top, tint_rgb, accent_rgb)
92
+ bottom = _tinted(bottom, tint_rgb, accent_rgb)
93
+ if top is None and bottom is None:
94
+ text.append(" ")
95
+ elif bottom is None:
96
+ text.append("▀", Style(color="rgb(%d,%d,%d)" % top))
97
+ elif top is None:
98
+ text.append("▄", Style(color="rgb(%d,%d,%d)" % bottom))
99
+ else:
100
+ text.append("▀", Style(color="rgb(%d,%d,%d)" % top, bgcolor="rgb(%d,%d,%d)" % bottom))
101
+ text.append("\n")
102
+ return text
103
+
104
+
105
+ def ascii_art() -> str:
106
+ """The knight as plain monochrome ASCII — two chars per pixel to keep
107
+ his proportions in terminal cells, denser glyphs for brighter armor."""
108
+ lines = []
109
+ for row in ROWS:
110
+ line = ""
111
+ for ch in row:
112
+ pixel = PALETTE.get(ch)
113
+ if pixel is None:
114
+ line += " "
115
+ elif pixel in _ORANGE:
116
+ line += "%%"
117
+ else:
118
+ lum = sum(pixel) / 765
119
+ line += "::" if lum < 0.15 else "++" if lum < 0.45 else "==" if lum < 0.8 else "##"
120
+ lines.append(line.rstrip())
121
+ return "\n".join(lines)
122
+
123
+
124
+ def says(message: str) -> str:
125
+ """Cowsay, but it's a knight."""
126
+ border = "_" * (len(message) + 2)
127
+ return (
128
+ f" {border}\n< {message} >\n {'-' * (len(message) + 2)}\n"
129
+ f" \\\n" + ansi(indent=" ")
130
+ )
@@ -17,11 +17,14 @@ from textual.screen import ModalScreen, Screen
17
17
  from textual.theme import Theme
18
18
  from textual.widgets import DataTable, Footer, Input, Label, Static
19
19
 
20
- from . import vault
20
+ from . import logo, vault
21
21
  from .cli import VAULT_PATH, generate, import_csv, to_clipboard
22
22
 
23
23
  THEME_FILE = os.path.expanduser("~/.local/share/pw-manager/theme")
24
24
 
25
+ APP_NAME = "The Paladin"
26
+ APP_ICON = "⚔"
27
+
25
28
  TUXEDO_THEMES = [
26
29
  Theme(
27
30
  name="muted-slate",
@@ -73,10 +76,21 @@ class UnlockScreen(Screen):
73
76
 
74
77
  def compose(self) -> ComposeResult:
75
78
  with Vertical(id="unlock-box"):
76
- yield Label("pw-manager", id="unlock-title")
79
+ yield Static(id="unlock-logo")
80
+ yield Label(f"{APP_ICON} {APP_NAME}", id="unlock-title")
77
81
  yield Input(password=True, placeholder="master password", id="master")
78
82
  yield Static("", id="unlock-error")
79
83
 
84
+ def on_mount(self) -> None:
85
+ self._paint_logo()
86
+ self.app.theme_changed_signal.subscribe(self, self._paint_logo)
87
+
88
+ def _paint_logo(self, _theme: Theme | None = None) -> None:
89
+ theme = self.app.current_theme
90
+ self.query_one("#unlock-logo", Static).update(
91
+ logo.rich_text(tint=theme.primary, accent=theme.warning or theme.primary)
92
+ )
93
+
80
94
  def on_input_submitted(self, event: Input.Submitted) -> None:
81
95
  self.query_one("#unlock-error", Static).update("unlocking…")
82
96
  try:
@@ -212,6 +226,7 @@ class MainScreen(Screen):
212
226
  ]
213
227
 
214
228
  def compose(self) -> ComposeResult:
229
+ yield Static(f" {APP_ICON} {APP_NAME}", id="topbar")
215
230
  yield Input(placeholder="/ to search", id="search")
216
231
  yield DataTable(id="table", cursor_type="row")
217
232
  yield Footer()
@@ -362,13 +377,19 @@ class VaultCommands(Provider):
362
377
 
363
378
 
364
379
  class PwApp(App):
365
- TITLE = "pw-manager"
380
+ TITLE = APP_NAME
366
381
  COMMANDS = App.COMMANDS | {VaultCommands}
367
382
 
368
383
  # Colors come from $theme-variables so every theme applies everywhere;
369
384
  # widgets without rules here (table, footer, toasts) use Textual's own
370
385
  # theme-aware defaults.
371
386
  CSS = """
387
+ #topbar {
388
+ height: 1;
389
+ background: $panel;
390
+ color: $primary;
391
+ text-style: bold;
392
+ }
372
393
  #unlock-box {
373
394
  align: center middle;
374
395
  width: 44;
@@ -377,7 +398,14 @@ class PwApp(App):
377
398
  background: $surface;
378
399
  border: round $panel;
379
400
  }
401
+ #unlock-logo {
402
+ width: 100%;
403
+ text-align: center;
404
+ margin-bottom: 1;
405
+ }
380
406
  #unlock-title {
407
+ width: 100%;
408
+ text-align: center;
381
409
  color: $primary;
382
410
  text-style: bold;
383
411
  margin-bottom: 1;
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "basic-password-manager"
7
- version = "0.2.2"
8
- description = "Local encrypted password manager that lives in your terminal"
7
+ version = "0.2.4"
8
+ description = "The Paladin — a local encrypted password manager that lives in your terminal"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
@@ -15,6 +15,9 @@ dependencies = [
15
15
  "textual",
16
16
  ]
17
17
 
18
+ [tool.setuptools]
19
+ packages = ["pw_manager"]
20
+
18
21
  [project.scripts]
19
22
  pw = "pw_manager.cli:main"
20
23