basic-password-manager 0.4.4__tar.gz → 0.4.6__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.4.4 → basic_password_manager-0.4.6}/PKG-INFO +21 -6
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/README.md +20 -5
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/basic_password_manager.egg-info/PKG-INFO +21 -6
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/cli.py +25 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/logo.py +11 -2
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/mobile.py +117 -10
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/tui.py +18 -3
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pyproject.toml +1 -1
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/LICENSE +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/basic_password_manager.egg-info/SOURCES.txt +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/basic_password_manager.egg-info/dependency_links.txt +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/basic_password_manager.egg-info/entry_points.txt +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/basic_password_manager.egg-info/requires.txt +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/basic_password_manager.egg-info/top_level.txt +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/__init__.py +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/crypto.py +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/vault.py +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/web/argon2.umd.min.js +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/web/page.html +0 -0
- {basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-password-manager
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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
|
|
@@ -102,6 +102,7 @@ Same vault, no interface, for when you just want the thing:
|
|
|
102
102
|
| `paladin gen -l 32` | just print a random password |
|
|
103
103
|
| `paladin import passwords.csv` | import a browser CSV export (see below) |
|
|
104
104
|
| `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
|
|
105
|
+
| `paladin mobile -b` | same, but keeps running after you close the terminal |
|
|
105
106
|
| `paladin about` | meet the knight |
|
|
106
107
|
|
|
107
108
|
The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
|
|
@@ -154,11 +155,25 @@ use (reverse proxy, named tunnel — `PALADIN_MOBILE_PORT` changes the port).
|
|
|
154
155
|
From then on plain `paladin mobile` serves at your subdomain, every time, so
|
|
155
156
|
a phone bookmark keeps working.
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
Every new session opens at *your* address instead of a random one, and the
|
|
159
|
+
secret path at the end of the URL still expires every 30 days (your bookmark
|
|
160
|
+
needs refreshing then, on purpose).
|
|
161
|
+
|
|
162
|
+
### Keeping it up after you close the terminal
|
|
163
|
+
|
|
164
|
+
By default a session ends when you press Ctrl+C — fine for a quick lookup,
|
|
165
|
+
useless if you want the bookmark to work while you're out. So:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
paladin mobile -b # background: survives closing the terminal
|
|
169
|
+
paladin mobile --status # what's running, and when the link expires
|
|
170
|
+
paladin mobile --stop # end it now
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A background session keeps serving until you stop it or the link hits its
|
|
174
|
+
30-day expiry, at which point it shuts itself down rather than lingering
|
|
175
|
+
forever. What's exposed the whole time is the same encrypted blob as always
|
|
176
|
+
— useless without the master password — behind a random 128-bit path.
|
|
162
177
|
|
|
163
178
|
The one thing crossing the internet is the encrypted blob, which is useless
|
|
164
179
|
without the master password that never leaves your phone. The tunnel provider
|
|
@@ -86,6 +86,7 @@ Same vault, no interface, for when you just want the thing:
|
|
|
86
86
|
| `paladin gen -l 32` | just print a random password |
|
|
87
87
|
| `paladin import passwords.csv` | import a browser CSV export (see below) |
|
|
88
88
|
| `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
|
|
89
|
+
| `paladin mobile -b` | same, but keeps running after you close the terminal |
|
|
89
90
|
| `paladin about` | meet the knight |
|
|
90
91
|
|
|
91
92
|
The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
|
|
@@ -138,11 +139,25 @@ use (reverse proxy, named tunnel — `PALADIN_MOBILE_PORT` changes the port).
|
|
|
138
139
|
From then on plain `paladin mobile` serves at your subdomain, every time, so
|
|
139
140
|
a phone bookmark keeps working.
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
Every new session opens at *your* address instead of a random one, and the
|
|
143
|
+
secret path at the end of the URL still expires every 30 days (your bookmark
|
|
144
|
+
needs refreshing then, on purpose).
|
|
145
|
+
|
|
146
|
+
### Keeping it up after you close the terminal
|
|
147
|
+
|
|
148
|
+
By default a session ends when you press Ctrl+C — fine for a quick lookup,
|
|
149
|
+
useless if you want the bookmark to work while you're out. So:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
paladin mobile -b # background: survives closing the terminal
|
|
153
|
+
paladin mobile --status # what's running, and when the link expires
|
|
154
|
+
paladin mobile --stop # end it now
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A background session keeps serving until you stop it or the link hits its
|
|
158
|
+
30-day expiry, at which point it shuts itself down rather than lingering
|
|
159
|
+
forever. What's exposed the whole time is the same encrypted blob as always
|
|
160
|
+
— useless without the master password — behind a random 128-bit path.
|
|
146
161
|
|
|
147
162
|
The one thing crossing the internet is the encrypted blob, which is useless
|
|
148
163
|
without the master password that never leaves your phone. The tunnel provider
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-password-manager
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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
|
|
@@ -102,6 +102,7 @@ Same vault, no interface, for when you just want the thing:
|
|
|
102
102
|
| `paladin gen -l 32` | just print a random password |
|
|
103
103
|
| `paladin import passwords.csv` | import a browser CSV export (see below) |
|
|
104
104
|
| `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
|
|
105
|
+
| `paladin mobile -b` | same, but keeps running after you close the terminal |
|
|
105
106
|
| `paladin about` | meet the knight |
|
|
106
107
|
|
|
107
108
|
The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
|
|
@@ -154,11 +155,25 @@ use (reverse proxy, named tunnel — `PALADIN_MOBILE_PORT` changes the port).
|
|
|
154
155
|
From then on plain `paladin mobile` serves at your subdomain, every time, so
|
|
155
156
|
a phone bookmark keeps working.
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
Every new session opens at *your* address instead of a random one, and the
|
|
159
|
+
secret path at the end of the URL still expires every 30 days (your bookmark
|
|
160
|
+
needs refreshing then, on purpose).
|
|
161
|
+
|
|
162
|
+
### Keeping it up after you close the terminal
|
|
163
|
+
|
|
164
|
+
By default a session ends when you press Ctrl+C — fine for a quick lookup,
|
|
165
|
+
useless if you want the bookmark to work while you're out. So:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
paladin mobile -b # background: survives closing the terminal
|
|
169
|
+
paladin mobile --status # what's running, and when the link expires
|
|
170
|
+
paladin mobile --stop # end it now
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A background session keeps serving until you stop it or the link hits its
|
|
174
|
+
30-day expiry, at which point it shuts itself down rather than lingering
|
|
175
|
+
forever. What's exposed the whole time is the same encrypted blob as always
|
|
176
|
+
— useless without the master password — behind a random 128-bit path.
|
|
162
177
|
|
|
163
178
|
The one thing crossing the internet is the encrypted blob, which is useless
|
|
164
179
|
without the master password that never leaves your phone. The tunnel provider
|
|
@@ -64,9 +64,28 @@ def cmd_init(args):
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def cmd_mobile(args):
|
|
67
|
+
import time
|
|
68
|
+
|
|
67
69
|
from . import mobile
|
|
68
70
|
|
|
69
71
|
try:
|
|
72
|
+
if args.stop:
|
|
73
|
+
print("background session stopped" if mobile.stop_daemon() else "nothing running")
|
|
74
|
+
return
|
|
75
|
+
if args.status:
|
|
76
|
+
state = mobile.read_state()
|
|
77
|
+
if not state:
|
|
78
|
+
print("no background session running")
|
|
79
|
+
return
|
|
80
|
+
left = (state["expires"] - time.time()) / 86400
|
|
81
|
+
print(f"serving {state['url']}\nlink expires in {left:.1f} days (pid {state['pid']})")
|
|
82
|
+
return
|
|
83
|
+
if args.background:
|
|
84
|
+
url = mobile.start_daemon(VAULT_PATH, stable_url=args.url)
|
|
85
|
+
mobile.print_qr(url)
|
|
86
|
+
print(" running in the background — it survives closing this terminal.")
|
|
87
|
+
print(" 'paladin mobile --status' to check, '--stop' to end it.\n")
|
|
88
|
+
return
|
|
70
89
|
mobile.run(VAULT_PATH, stable_url=args.url)
|
|
71
90
|
except mobile.MobileError as e:
|
|
72
91
|
sys.exit(str(e))
|
|
@@ -237,6 +256,12 @@ def main():
|
|
|
237
256
|
i.add_argument("csv_file")
|
|
238
257
|
m = sub.add_parser("mobile", help="open your vault on your phone over an HTTPS tunnel")
|
|
239
258
|
m.add_argument("--url", help="serve at your own https subdomain instead of a quick tunnel")
|
|
259
|
+
m.add_argument(
|
|
260
|
+
"-b", "--background", action="store_true",
|
|
261
|
+
help="keep serving after this terminal closes (until the link expires)",
|
|
262
|
+
)
|
|
263
|
+
m.add_argument("--status", action="store_true", help="is a background session running?")
|
|
264
|
+
m.add_argument("--stop", action="store_true", help="stop the background session")
|
|
240
265
|
sub.add_parser("about", help="meet the knight")
|
|
241
266
|
|
|
242
267
|
args = p.parse_args()
|
|
@@ -75,9 +75,18 @@ def _tinted(pixel, tint, accent):
|
|
|
75
75
|
return tuple(int(c * lum) for c in base)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def _hex_rgb(color: str) -> tuple:
|
|
78
|
+
def _hex_rgb(color: str | None) -> tuple | None:
|
|
79
|
+
"""Parse #rrggbb to (r,g,b). Returns None for anything that isn't hex —
|
|
80
|
+
ANSI themes use names like 'ansi_blue', and those just mean 'no tint'."""
|
|
81
|
+
if not color:
|
|
82
|
+
return None
|
|
79
83
|
color = color.lstrip("#")
|
|
80
|
-
|
|
84
|
+
if len(color) != 6:
|
|
85
|
+
return None
|
|
86
|
+
try:
|
|
87
|
+
return tuple(int(color[i : i + 2], 16) for i in (0, 2, 4))
|
|
88
|
+
except ValueError:
|
|
89
|
+
return None
|
|
81
90
|
|
|
82
91
|
|
|
83
92
|
def rich_text(tint: str | None = None, accent: str | None = None) -> Text:
|
|
@@ -9,11 +9,13 @@ and the network only ever carry ciphertext.
|
|
|
9
9
|
import base64
|
|
10
10
|
import hashlib
|
|
11
11
|
import http.server
|
|
12
|
+
import json
|
|
12
13
|
import os
|
|
13
14
|
import platform
|
|
14
15
|
import re
|
|
15
16
|
import secrets
|
|
16
17
|
import shutil
|
|
18
|
+
import signal
|
|
17
19
|
import stat
|
|
18
20
|
import subprocess
|
|
19
21
|
import sys
|
|
@@ -113,6 +115,81 @@ def _load_token() -> str:
|
|
|
113
115
|
return token
|
|
114
116
|
|
|
115
117
|
|
|
118
|
+
def token_expires_at() -> float:
|
|
119
|
+
"""Unix time when the current link stops working."""
|
|
120
|
+
try:
|
|
121
|
+
created, _ = open(os.path.join(_data_dir(), "mobile-token")).read().split(":", 1)
|
|
122
|
+
return float(created) + TOKEN_TTL
|
|
123
|
+
except (OSError, ValueError):
|
|
124
|
+
return time.time() + TOKEN_TTL
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _state_path() -> str:
|
|
128
|
+
return os.path.join(_data_dir(), "mobile-session.json")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def read_state() -> dict | None:
|
|
132
|
+
"""The running background session, or None if there isn't a live one."""
|
|
133
|
+
try:
|
|
134
|
+
with open(_state_path()) as f:
|
|
135
|
+
state = json.load(f)
|
|
136
|
+
os.kill(state["pid"], 0) # raises if that process is gone
|
|
137
|
+
return state
|
|
138
|
+
except (OSError, ValueError, KeyError):
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def stop_daemon() -> bool:
|
|
143
|
+
"""Stop a background session. True if one was running."""
|
|
144
|
+
state = read_state()
|
|
145
|
+
if not state:
|
|
146
|
+
return False
|
|
147
|
+
try:
|
|
148
|
+
os.kill(state["pid"], signal.SIGTERM)
|
|
149
|
+
except OSError:
|
|
150
|
+
pass
|
|
151
|
+
try:
|
|
152
|
+
os.remove(_state_path())
|
|
153
|
+
except OSError:
|
|
154
|
+
pass
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def start_daemon(vault_path: str, stable_url: str | None = None, port: int = 0) -> str:
|
|
159
|
+
"""Re-launch ourselves detached so the session outlives this shell.
|
|
160
|
+
Returns the URL the background session is serving."""
|
|
161
|
+
if read_state():
|
|
162
|
+
raise MobileError("a background session is already running — 'paladin mobile --stop' first")
|
|
163
|
+
if not os.path.exists(vault_path):
|
|
164
|
+
raise MobileError(f"no vault at {vault_path} — run 'paladin init' first")
|
|
165
|
+
|
|
166
|
+
log = os.path.join(_data_dir(), "mobile.log")
|
|
167
|
+
cmd = [sys.executable, "-m", "pw_manager.cli", "mobile"]
|
|
168
|
+
if stable_url:
|
|
169
|
+
cmd += ["--url", stable_url]
|
|
170
|
+
env = {**os.environ, "PALADIN_MOBILE_DAEMON_CHILD": "1"}
|
|
171
|
+
with open(log, "ab") as logfile:
|
|
172
|
+
proc = subprocess.Popen(
|
|
173
|
+
cmd,
|
|
174
|
+
stdout=logfile,
|
|
175
|
+
stderr=logfile,
|
|
176
|
+
stdin=subprocess.DEVNULL,
|
|
177
|
+
start_new_session=True, # survives this shell closing
|
|
178
|
+
env=env,
|
|
179
|
+
)
|
|
180
|
+
# wait for the child to publish its URL (tunnel start can take seconds)
|
|
181
|
+
deadline = time.time() + 45
|
|
182
|
+
while time.time() < deadline:
|
|
183
|
+
state = read_state()
|
|
184
|
+
if state and state.get("pid") == proc.pid:
|
|
185
|
+
return state["url"]
|
|
186
|
+
if proc.poll() is not None:
|
|
187
|
+
raise MobileError(f"background session failed to start — see {log}")
|
|
188
|
+
time.sleep(0.3)
|
|
189
|
+
proc.terminate()
|
|
190
|
+
raise MobileError(f"background session didn't come up in time — see {log}")
|
|
191
|
+
|
|
192
|
+
|
|
116
193
|
def _theme_colors() -> dict:
|
|
117
194
|
"""Resolve the theme saved by the TUI to a full color palette. The page
|
|
118
195
|
is baked with whatever theme is current when the QR is generated."""
|
|
@@ -130,6 +207,10 @@ def _theme_colors() -> dict:
|
|
|
130
207
|
registry = {t.name: t for t in TUXEDO_THEMES}
|
|
131
208
|
registry.update(BUILTIN_THEMES)
|
|
132
209
|
cs = registry[name].to_color_system().generate()
|
|
210
|
+
# ANSI themes resolve to names like 'ansi_blue', not hex — the browser
|
|
211
|
+
# can't use those and the knight can't tint from them, so fall back
|
|
212
|
+
if not str(cs.get("background", "")).startswith("#"):
|
|
213
|
+
return default
|
|
133
214
|
return {**default, **cs}
|
|
134
215
|
except (OSError, KeyError, ImportError):
|
|
135
216
|
return default
|
|
@@ -247,13 +328,7 @@ def stop_session(server, proc) -> None:
|
|
|
247
328
|
pass
|
|
248
329
|
|
|
249
330
|
|
|
250
|
-
def
|
|
251
|
-
def fetched():
|
|
252
|
-
print(f" vault page served to a device at {time.strftime('%H:%M:%S')}")
|
|
253
|
-
|
|
254
|
-
print("\n creating your secure link…", flush=True)
|
|
255
|
-
server, proc, url = start_session(vault_path, stable_url, port, on_fetch=fetched)
|
|
256
|
-
|
|
331
|
+
def print_qr(url: str) -> None:
|
|
257
332
|
import qrcode
|
|
258
333
|
|
|
259
334
|
qr = qrcode.QRCode(border=1)
|
|
@@ -261,13 +336,45 @@ def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
|
261
336
|
print("\n scan with your phone camera:\n")
|
|
262
337
|
qr.print_ascii(invert=True)
|
|
263
338
|
print(f"\n {url}\n")
|
|
264
|
-
|
|
265
|
-
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
342
|
+
is_child = os.environ.get("PALADIN_MOBILE_DAEMON_CHILD") == "1"
|
|
343
|
+
|
|
344
|
+
def fetched():
|
|
345
|
+
print(f" vault page served to a device at {time.strftime('%H:%M:%S')}", flush=True)
|
|
346
|
+
|
|
347
|
+
print("\n creating your secure link…", flush=True)
|
|
348
|
+
server, proc, url = start_session(vault_path, stable_url, port, on_fetch=fetched)
|
|
349
|
+
|
|
350
|
+
if proc is None: # stable mode: serving your own address, no quick tunnel
|
|
351
|
+
print(
|
|
352
|
+
f" serving your configured address on 127.0.0.1:{server.server_address[1]}"
|
|
353
|
+
" — your tunnel/proxy must forward to this port"
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
print(" using a temporary tunnel (set PALADIN_MOBILE_URL for your own subdomain)")
|
|
357
|
+
|
|
358
|
+
expires = token_expires_at()
|
|
359
|
+
if is_child: # background session: publish state so --status/--stop can find us
|
|
360
|
+
with open(_state_path(), "w") as f:
|
|
361
|
+
json.dump({"pid": os.getpid(), "url": url, "expires": expires}, f)
|
|
362
|
+
signal.signal(signal.SIGTERM, lambda *a: (_ for _ in ()).throw(KeyboardInterrupt))
|
|
363
|
+
else:
|
|
364
|
+
print_qr(url)
|
|
365
|
+
print(" the master password is typed on your phone; only ciphertext leaves this machine.")
|
|
366
|
+
print(" every page load is logged below — Ctrl+C to stop the session.\n")
|
|
266
367
|
|
|
267
368
|
try:
|
|
268
|
-
while
|
|
369
|
+
while time.time() < expires: # the link's 30 days, enforced for real
|
|
269
370
|
time.sleep(1)
|
|
371
|
+
print("\nlink expired after 30 days — run 'paladin mobile' again for a fresh one.")
|
|
270
372
|
except KeyboardInterrupt:
|
|
271
373
|
print("\nsession ended.")
|
|
272
374
|
finally:
|
|
273
375
|
stop_session(server, proc)
|
|
376
|
+
if is_child:
|
|
377
|
+
try:
|
|
378
|
+
os.remove(_state_path())
|
|
379
|
+
except OSError:
|
|
380
|
+
pass
|
|
@@ -11,7 +11,7 @@ import time
|
|
|
11
11
|
|
|
12
12
|
from rich.style import Style
|
|
13
13
|
from rich.text import Text
|
|
14
|
-
from textual.app import App, ComposeResult
|
|
14
|
+
from textual.app import App, ComposeResult, SystemCommand
|
|
15
15
|
from textual.binding import Binding
|
|
16
16
|
from textual.command import DiscoveryHit, Hit, Hits, Provider
|
|
17
17
|
from textual.containers import Vertical
|
|
@@ -103,6 +103,8 @@ class UnlockScreen(Screen):
|
|
|
103
103
|
self.query_one("#master", Input).clear()
|
|
104
104
|
return
|
|
105
105
|
self.app.master = event.value
|
|
106
|
+
# stop repainting the (now-hidden) unlock knight on every theme change
|
|
107
|
+
self.app.theme_changed_signal.unsubscribe(self)
|
|
106
108
|
self.app.push_screen(MainScreen())
|
|
107
109
|
|
|
108
110
|
|
|
@@ -167,7 +169,8 @@ def _qr_text(url: str, primary: str) -> tuple[Text, int]:
|
|
|
167
169
|
must give the widget exactly that width: a wrapped QR is a dead QR."""
|
|
168
170
|
import qrcode
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
rgb = logo._hex_rgb(primary) or (0, 0, 0) # ansi themes → plain black QR
|
|
173
|
+
r, g, b = rgb
|
|
171
174
|
while (r + g + b) / 765 > 0.45:
|
|
172
175
|
r, g, b = int(r * 0.8), int(g * 0.8), int(b * 0.8)
|
|
173
176
|
dark, light = f"rgb({r},{g},{b})", "rgb(255,255,255)"
|
|
@@ -473,7 +476,7 @@ class MainScreen(Screen):
|
|
|
473
476
|
self.app.push_screen(ConfirmModal(f"delete '{name}'?"), done)
|
|
474
477
|
|
|
475
478
|
def action_theme(self) -> None:
|
|
476
|
-
self.app.
|
|
479
|
+
self.app._theme() # same picker the palette opens
|
|
477
480
|
|
|
478
481
|
def action_search(self) -> None:
|
|
479
482
|
self.query_one("#search", Input).focus()
|
|
@@ -609,6 +612,18 @@ class PwApp(App):
|
|
|
609
612
|
with open(THEME_FILE, "w") as f:
|
|
610
613
|
f.write(theme.name)
|
|
611
614
|
|
|
615
|
+
def get_system_commands(self, screen: Screen):
|
|
616
|
+
"""Swap Textual's built-in 'Theme' entry for our picker, so the
|
|
617
|
+
palette and the `t` key open the same thing."""
|
|
618
|
+
for command in super().get_system_commands(screen):
|
|
619
|
+
if command.title == "Theme":
|
|
620
|
+
yield SystemCommand("Theme", "Change the current theme", self._theme)
|
|
621
|
+
else:
|
|
622
|
+
yield command
|
|
623
|
+
|
|
624
|
+
def _theme(self) -> None:
|
|
625
|
+
self.push_screen(ThemeModal())
|
|
626
|
+
|
|
612
627
|
def _import_csv(self) -> None:
|
|
613
628
|
def done(path: str | None) -> None:
|
|
614
629
|
if not path:
|
|
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
|
{basic_password_manager-0.4.4 → basic_password_manager-0.4.6}/pw_manager/web/argon2.umd.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|