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.
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/PKG-INFO +11 -4
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/README.md +9 -2
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/PKG-INFO +11 -4
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/SOURCES.txt +1 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/cli.py +22 -1
- basic_password_manager-0.2.4/pw_manager/logo.py +130 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/tui.py +31 -3
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pyproject.toml +5 -2
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/LICENSE +0 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/dependency_links.txt +0 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/entry_points.txt +0 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/requires.txt +0 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/basic_password_manager.egg-info/top_level.txt +0 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/__init__.py +0 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/crypto.py +0 -0
- {basic_password_manager-0.2.2 → basic_password_manager-0.2.4}/pw_manager/vault.py +0 -0
- {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.
|
|
4
|
-
Summary:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
Summary:
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
8
|
-
description = "
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|