artemis_framework 0.3.2__tar.gz → 0.4.1__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.
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/PKG-INFO +1 -1
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/app.py +2 -2
- artemis_framework-0.4.1/artemis_framework/asphodel/tui/screens/app_config.py +434 -0
- artemis_framework-0.4.1/artemis_framework/asphodel/tui/widgets/operation_log.py +115 -0
- artemis_framework-0.4.1/artemis_framework/elysium/__init__.py +0 -0
- artemis_framework-0.4.1/artemis_framework/tartarus/__init__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/pyproject.toml +1 -1
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/LICENSE +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/README.md +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/__init__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/__main__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/__init__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/__main__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/__init__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/_gpg_binary.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/armor_binary.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/encrypt_decrypt.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/gpg_context.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/key_management.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/macos_shim.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/sign_verify.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/__init__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/modals/__init__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/screens/__init__.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/settings.py +0 -0
- {artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/widgets/__init__.py +0 -0
- /artemis_framework-0.3.2/artemis_framework/elysium/__ini → /artemis_framework-0.4.1/artemis_framework/asphodel/tui/widgets/key_detail.py +0 -0
- /artemis_framework-0.3.2/artemis_framework/elysium/__init__.py → /artemis_framework-0.4.1/artemis_framework/asphodel/tui/widgets/key_list.py +0 -0
- /artemis_framework-0.3.2/artemis_framework/tartarus/__init__.py → /artemis_framework-0.4.1/artemis_framework/elysium/__ini +0 -0
|
@@ -239,8 +239,8 @@ _WELCOME_TEXT = """\
|
|
|
239
239
|
|
|
240
240
|
[b]K[/b] → Key Management [i](generate, import, export, delete keys)[/i]
|
|
241
241
|
[b]E[/b] → Encrypt / Decrypt [i](encrypt, decrypt, sign, verify)[/i]
|
|
242
|
-
[b]
|
|
243
|
-
[b]Esc[/b]
|
|
242
|
+
[b]C[/b] → Configuration [i](keyring path, GPG binary, key defaults)[/i]
|
|
243
|
+
[b]Esc[/b] → Quit
|
|
244
244
|
|
|
245
245
|
The keyring path is shown in the header subtitle.
|
|
246
246
|
[i]Use [b]`--gnupghome`[/b] on the command line to point at a different keyring.[/i]
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
|
|
8
|
+
from textual.message import Message
|
|
9
|
+
from textual.screen import Screen
|
|
10
|
+
from textual.widgets import (
|
|
11
|
+
Button,
|
|
12
|
+
Footer,
|
|
13
|
+
Header,
|
|
14
|
+
Input,
|
|
15
|
+
Label,
|
|
16
|
+
Select,
|
|
17
|
+
Static,
|
|
18
|
+
Switch,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from artemis_framework.asphodel.tui.settings import AppSettings, CONFIG_FILE, save_settings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_ALGO_OPTIONS: list[tuple[str, str]] = [
|
|
25
|
+
("RSA 4096", "rsa"),
|
|
26
|
+
("ECC (Ed25519 / Cv25519)", "ecc"),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
_EXPIRE_OPTIONS: list[tuple[str, str]] = [
|
|
30
|
+
("Never", "0"),
|
|
31
|
+
("1 year", "1y"),
|
|
32
|
+
("2 years", "2y"),
|
|
33
|
+
("5 years", "5y"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
_CACHE_WARNING = (
|
|
37
|
+
"⚠ SECURITY RISK ⚠\n"
|
|
38
|
+
"Passphrases will be held in application memory for the entire "
|
|
39
|
+
"session and reused automatically. They are never written to disk, "
|
|
40
|
+
"but any process that can read this application's memory - such as "
|
|
41
|
+
"a debugger, crash dump, or another process running as the same "
|
|
42
|
+
"user - could potentially recover them.\n"
|
|
43
|
+
"Enable only on a trusted, single-user system."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AppConfigScreen(Screen):
|
|
48
|
+
TITLE = "App Configuration"
|
|
49
|
+
|
|
50
|
+
DEFAULT_CSS = """
|
|
51
|
+
#cfg-scroll {
|
|
52
|
+
height: 1fr;
|
|
53
|
+
width: 100%;
|
|
54
|
+
padding: 1 3;
|
|
55
|
+
}
|
|
56
|
+
.cfg-section-label {
|
|
57
|
+
color: $primary;
|
|
58
|
+
text-style: bold;
|
|
59
|
+
margin-top: 1;
|
|
60
|
+
margin-bottom: 0;
|
|
61
|
+
padding-left: 0;
|
|
62
|
+
}
|
|
63
|
+
.cfg-divider {
|
|
64
|
+
color: $primary-darken-2;
|
|
65
|
+
margin-bottom: 1;
|
|
66
|
+
margin-top: 0;
|
|
67
|
+
}
|
|
68
|
+
.cfg-row {
|
|
69
|
+
height: auto;
|
|
70
|
+
width: 100%;
|
|
71
|
+
margin-bottom: 1;
|
|
72
|
+
align: left top;
|
|
73
|
+
}
|
|
74
|
+
.cfg-label {
|
|
75
|
+
width: 22;
|
|
76
|
+
color: $text-muted;
|
|
77
|
+
padding-top: 1;
|
|
78
|
+
}
|
|
79
|
+
.cfg-input-col {
|
|
80
|
+
width: 1fr;
|
|
81
|
+
height: auto;
|
|
82
|
+
}
|
|
83
|
+
.cfg-row Input {
|
|
84
|
+
width: 100%;
|
|
85
|
+
border: double $primary-darken-1;
|
|
86
|
+
}
|
|
87
|
+
.cfg-row Input:focus {
|
|
88
|
+
border: double $primary;
|
|
89
|
+
}
|
|
90
|
+
.cfg-row Select {
|
|
91
|
+
width: 1fr;
|
|
92
|
+
border: double $primary-darken-1;
|
|
93
|
+
}
|
|
94
|
+
.cfg-row Select:focus {
|
|
95
|
+
border: double $primary;
|
|
96
|
+
}
|
|
97
|
+
.cfg-status {
|
|
98
|
+
color: $text-muted;
|
|
99
|
+
text-style: italic;
|
|
100
|
+
height: auto;
|
|
101
|
+
padding-left: 0;
|
|
102
|
+
margin-top: 0;
|
|
103
|
+
}
|
|
104
|
+
.cfg-status-ok {
|
|
105
|
+
color: $success;
|
|
106
|
+
}
|
|
107
|
+
.cfg-status-err {
|
|
108
|
+
color: $error;
|
|
109
|
+
}
|
|
110
|
+
.cfg-about-value {
|
|
111
|
+
color: $text-muted;
|
|
112
|
+
padding-top: 0;
|
|
113
|
+
width: 1fr;
|
|
114
|
+
}
|
|
115
|
+
.cfg-spacer {
|
|
116
|
+
height: 1;
|
|
117
|
+
}
|
|
118
|
+
#cfg-buttons {
|
|
119
|
+
height: auto;
|
|
120
|
+
width: 100%;
|
|
121
|
+
align: left middle;
|
|
122
|
+
margin-bottom: 1;
|
|
123
|
+
}
|
|
124
|
+
#cfg-buttons Button {
|
|
125
|
+
margin-right: 1;
|
|
126
|
+
min-width: 12;
|
|
127
|
+
}
|
|
128
|
+
.cfg-switch-row {
|
|
129
|
+
align: left middle;
|
|
130
|
+
}
|
|
131
|
+
.cfg-switch-row .cfg-label {
|
|
132
|
+
padding-top: 0;
|
|
133
|
+
}
|
|
134
|
+
.cfg-cache-warning {
|
|
135
|
+
margin-top: 1;
|
|
136
|
+
margin-bottom: 1;
|
|
137
|
+
padding: 1 2;
|
|
138
|
+
border: double $warning;
|
|
139
|
+
color: $warning;
|
|
140
|
+
text-style: bold;
|
|
141
|
+
background: $surface;
|
|
142
|
+
width: 100%;
|
|
143
|
+
height: auto;
|
|
144
|
+
}
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
BINDINGS = [
|
|
148
|
+
Binding("s", "save", "Save", show=True, priority=True),
|
|
149
|
+
Binding("q,d", "discard", "Discard", show=True, priority=True),
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
class Saved(Message):
|
|
153
|
+
def __init__(self, settings: AppSettings) -> None:
|
|
154
|
+
super().__init__()
|
|
155
|
+
self.settings = settings
|
|
156
|
+
|
|
157
|
+
def __init__(self, current_settings: AppSettings, **kwargs) -> None:
|
|
158
|
+
super().__init__(**kwargs)
|
|
159
|
+
self._cfg = current_settings
|
|
160
|
+
|
|
161
|
+
def compose(self) -> ComposeResult:
|
|
162
|
+
yield Header()
|
|
163
|
+
|
|
164
|
+
with ScrollableContainer(id="cfg-scroll"):
|
|
165
|
+
|
|
166
|
+
yield Static("GPG ENVIRONMENT", classes="cfg-section-label")
|
|
167
|
+
yield Static("-" * 58, classes="cfg-divider")
|
|
168
|
+
|
|
169
|
+
with Horizontal(classes="cfg-row"):
|
|
170
|
+
yield Label("GNUPGHOME path", classes="cfg-label")
|
|
171
|
+
with Vertical(classes="cfg-input-col"):
|
|
172
|
+
yield Input(
|
|
173
|
+
value=self._cfg.gnupghome,
|
|
174
|
+
placeholder="(leave blank to use the built-in test keyring)",
|
|
175
|
+
id="cfg-gnupghome"
|
|
176
|
+
)
|
|
177
|
+
yield Static("", id="cfg-gnupghome-status", classes="cfg-status")
|
|
178
|
+
|
|
179
|
+
yield Static("GPG BINARY", classes="cfg-section-label")
|
|
180
|
+
yield Static("-" * 58, classes="cfg-divider")
|
|
181
|
+
|
|
182
|
+
with Horizontal(classes="cfg-row"):
|
|
183
|
+
yield Label("Binary name / path", classes="cfg-label")
|
|
184
|
+
with Vertical(classes="cfg-input-col"):
|
|
185
|
+
yield Input(
|
|
186
|
+
value=self._cfg.gpg_binary,
|
|
187
|
+
placeholder="gpg",
|
|
188
|
+
id="cfg-binary"
|
|
189
|
+
)
|
|
190
|
+
yield Static("", id="cfg-binary-status", classes="cfg-status")
|
|
191
|
+
|
|
192
|
+
yield Static("KEY GENERATION DEFAULTS", classes="cfg-section-label")
|
|
193
|
+
yield Static("-" * 58, classes="cfg-divider")
|
|
194
|
+
|
|
195
|
+
with Horizontal(classes="cfg-row"):
|
|
196
|
+
yield Label("Default algorithm", classes="cfg-label")
|
|
197
|
+
yield Select(
|
|
198
|
+
_ALGO_OPTIONS,
|
|
199
|
+
value=self._cfg.algorithm,
|
|
200
|
+
id="cfg-algo",
|
|
201
|
+
allow_blank=False
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
with Horizontal(classes="cfg-row"):
|
|
205
|
+
yield Label("Default expiry", classes="cfg-label")
|
|
206
|
+
yield Select(
|
|
207
|
+
_EXPIRE_OPTIONS,
|
|
208
|
+
value=self._cfg.expire,
|
|
209
|
+
id="cfg-expire",
|
|
210
|
+
allow_blank=False
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
yield Static("KEYSERVER", classes="cfg-section-label")
|
|
214
|
+
yield Static("-" * 58, classes="cfg-divider")
|
|
215
|
+
|
|
216
|
+
with Horizontal(classes="cfg-row"):
|
|
217
|
+
yield Label("Keyserver URL", classes="cfg-label")
|
|
218
|
+
with Vertical(classes="cfg-input-col"):
|
|
219
|
+
yield Input(
|
|
220
|
+
value=self._cfg.keyserver_url,
|
|
221
|
+
placeholder="https://keys.openpgp.org",
|
|
222
|
+
id="cfg-keyserver",
|
|
223
|
+
|
|
224
|
+
)
|
|
225
|
+
yield Static("", id="cfg-keyserver-status", classes="cfg-status")
|
|
226
|
+
|
|
227
|
+
yield Static("SECURITY", classes="cfg-section-label")
|
|
228
|
+
yield Static("-" * 58, classes="cfg-divider")
|
|
229
|
+
|
|
230
|
+
with Horizontal(classes="cfg-row cfg-switch-row"):
|
|
231
|
+
yield Label("Passphrase caching", classes="cfg-label")
|
|
232
|
+
with Vertical(classes="cfg-input-col"):
|
|
233
|
+
yield Switch(
|
|
234
|
+
value=self._cfg.passphrase_cache_enabled,
|
|
235
|
+
id="cfg-cache-switch"
|
|
236
|
+
)
|
|
237
|
+
yield Static(
|
|
238
|
+
"Disabled by default. Read the warning below before enabling.",
|
|
239
|
+
id="cfg-cache-switch-hint",
|
|
240
|
+
classes="cfg-status"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
yield Static(
|
|
244
|
+
_CACHE_WARNING,
|
|
245
|
+
id="cfg-cache-warning",
|
|
246
|
+
classes="cfg-cache-warning"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
yield Static("ABOUT", classes="cfg-section-label")
|
|
250
|
+
yield Static("-" * 58, classes="cfg-divider")
|
|
251
|
+
|
|
252
|
+
with Horizontal(classes="cfg-row"):
|
|
253
|
+
yield Label("Config file", classes="cfg-label")
|
|
254
|
+
yield Static(str(CONFIG_FILE), classes="cfg-about-value")
|
|
255
|
+
|
|
256
|
+
yield Static("", classes="cfg-spacer")
|
|
257
|
+
with Horizontal(id="cfg-buttons"):
|
|
258
|
+
yield Button("Save", variant="primary", id="cfg-btn-save")
|
|
259
|
+
yield Button("Discard", variant="default", id="cfg-btn-discard")
|
|
260
|
+
|
|
261
|
+
yield Footer()
|
|
262
|
+
|
|
263
|
+
def on_mount(self) -> None:
|
|
264
|
+
self.query_one("#cfg-gnupghome", Input).focus()
|
|
265
|
+
self._validate_gnupghome()
|
|
266
|
+
self._validate_binary()
|
|
267
|
+
self._validate_keyserver()
|
|
268
|
+
self._update_cache_warning()
|
|
269
|
+
|
|
270
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
271
|
+
if event.input.id == "cfg-gnupghome":
|
|
272
|
+
self._validate_gnupghome()
|
|
273
|
+
elif event.input.id == "cfg-binary":
|
|
274
|
+
self._validate_binary()
|
|
275
|
+
elif event.input.id == "cfg-keyserver":
|
|
276
|
+
self._validate_keyserver()
|
|
277
|
+
|
|
278
|
+
def on_switch_changed(self, event: Switch.Changed) -> None:
|
|
279
|
+
if event.switch.id == "cfg-cache-switch":
|
|
280
|
+
self._update_cache_warning()
|
|
281
|
+
|
|
282
|
+
def _update_cache_warning(self) -> None:
|
|
283
|
+
enabled = self.query_one("#cfg-cache-switch", Switch).value
|
|
284
|
+
warning = self.query_one("#cfg-cache-warning", Static)
|
|
285
|
+
hint = self.query_one("#cfg-cache-switch-hint", Static)
|
|
286
|
+
|
|
287
|
+
warning.display = enabled
|
|
288
|
+
|
|
289
|
+
if enabled:
|
|
290
|
+
hint.update("⚠ Enabled - passphrases cached in memory this session.")
|
|
291
|
+
hint.remove_class("cfg-status-ok")
|
|
292
|
+
hint.add_class("cfg-status-warn")
|
|
293
|
+
|
|
294
|
+
else:
|
|
295
|
+
hint.update("Disabled by default. Read the warning below before enabling.")
|
|
296
|
+
hint.remove_class("cfg-status-warn")
|
|
297
|
+
hint.add_class("cfg-status-ok")
|
|
298
|
+
|
|
299
|
+
def _validate_gnupghome(self) -> None:
|
|
300
|
+
raw = self.query_one("#cfg-gnupghome", Input).value.strip()
|
|
301
|
+
status = self.query_one("#cfg-gnupghome-status", Static)
|
|
302
|
+
|
|
303
|
+
if not raw:
|
|
304
|
+
self._ok(status, " » using built-in test keyring (asphodel-pgp library default)")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
p = Path(raw).expanduser()
|
|
308
|
+
if p.exists():
|
|
309
|
+
if p.is_dir():
|
|
310
|
+
self._ok(status, f" ✔ exists: {p.resolve()}")
|
|
311
|
+
else:
|
|
312
|
+
self._err(status, f" ✗ path exists but is not a directory")
|
|
313
|
+
else:
|
|
314
|
+
self._ok(status, f" » will be created: {p.resolve()}")
|
|
315
|
+
|
|
316
|
+
def _validate_binary(self) -> None:
|
|
317
|
+
import shutil
|
|
318
|
+
raw = self.query_one("#cfg-binary", Input).value.strip() or "gpg"
|
|
319
|
+
status = self.query_one("#cfg-binary-status", Static)
|
|
320
|
+
|
|
321
|
+
found = shutil.which(raw)
|
|
322
|
+
if found:
|
|
323
|
+
version = self._read_gpg_version(found)
|
|
324
|
+
self._ok(status, f" ✔ {found}" + (f" ({version})" if version else ""))
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
p = Path(raw)
|
|
328
|
+
if p.exists():
|
|
329
|
+
import os
|
|
330
|
+
if os.access(p, os.X_OK):
|
|
331
|
+
self._ok(status, f" ✔ {p.resolve()}")
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
self._err(status, " ✗ '{raw}' not found on PATH")
|
|
335
|
+
|
|
336
|
+
def _validate_keyserver(self) -> None:
|
|
337
|
+
raw = self.query_one("#cfg-keyserver", Input).value.strip()
|
|
338
|
+
status = self.query_one("#cfg-keyserver-status", Static)
|
|
339
|
+
|
|
340
|
+
if not raw:
|
|
341
|
+
self._ok(status, f" » using default: https://keys.openpgp.org")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
if raw.startswith("https://") or raw.startswith("http://"):
|
|
345
|
+
self._ok(status, f" ✔ {raw}")
|
|
346
|
+
else:
|
|
347
|
+
self._err(status, " ✗ URL must start with https:// -or- http://")
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def _ok(widget: Static, text: str) -> None:
|
|
351
|
+
widget.update(text)
|
|
352
|
+
widget.remove_class("cfg-status-err")
|
|
353
|
+
widget.remove_class("cfg-status-warn")
|
|
354
|
+
widget.add_class("cfg-status-ok")
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _err(widget: Static, text: str) -> None:
|
|
358
|
+
widget.update(text)
|
|
359
|
+
widget.remove_class("cfg-status-ok")
|
|
360
|
+
widget.remove_class("cfg-status-warn")
|
|
361
|
+
widget.add_class("cfg-status-err")
|
|
362
|
+
|
|
363
|
+
@staticmethod
|
|
364
|
+
def _read_gpg_version(binary: str) -> str:
|
|
365
|
+
import subprocess
|
|
366
|
+
try:
|
|
367
|
+
result = subprocess.run(
|
|
368
|
+
[binary, "--version"],
|
|
369
|
+
capture_output=True,
|
|
370
|
+
timeout=2,
|
|
371
|
+
text=True
|
|
372
|
+
)
|
|
373
|
+
first = result.stdout.splitlines()[0] if result.stdout else ""
|
|
374
|
+
parts = first.split()
|
|
375
|
+
if len(parts) >= 3:
|
|
376
|
+
return f"{parts[1].strip('()')} {parts[2]}"
|
|
377
|
+
return first.strip()
|
|
378
|
+
|
|
379
|
+
except Exception:
|
|
380
|
+
return ""
|
|
381
|
+
|
|
382
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
383
|
+
if event.button.id == "cfg-btn-save":
|
|
384
|
+
self.action_save()
|
|
385
|
+
elif event.button.id == "cfg-btn-discard":
|
|
386
|
+
self.action_discard()
|
|
387
|
+
|
|
388
|
+
def action_save(self) -> None:
|
|
389
|
+
gnupghome = self.query_one("#cfg-gnupghome", Input).value.strip()
|
|
390
|
+
gpg_binary = self.query_one("#cfg-binary", Input).value.strip() or "gpg"
|
|
391
|
+
algorithm = str(self.query_one("#cfg-algo", Select).value)
|
|
392
|
+
expire = str(self.query_one("#cfg-expire", Select).value)
|
|
393
|
+
keyserver = self.query_one("#cfg-keyserver", Input).value.strip()
|
|
394
|
+
cache_on = self.query_one("#cfg-cache-switch", Switch).value
|
|
395
|
+
|
|
396
|
+
if keyserver and not (
|
|
397
|
+
keyserver.startswith("https://") or keyserver.startswith("http://")
|
|
398
|
+
):
|
|
399
|
+
self.notify(
|
|
400
|
+
"Keyserver URL must start with [b][u]https://[/u][/b] [i]or[/i] [b][u]http://[/u][/b]",
|
|
401
|
+
title="Invalid URL",
|
|
402
|
+
severity="warning",
|
|
403
|
+
markup=True
|
|
404
|
+
)
|
|
405
|
+
self.query_one("#cfg-keyserver", Input).focus()
|
|
406
|
+
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
new_cfg = AppSettings(
|
|
410
|
+
gnupghome=gnupghome,
|
|
411
|
+
gpg_binary=gpg_binary,
|
|
412
|
+
algorithm=algorithm,
|
|
413
|
+
expire=expire,
|
|
414
|
+
keyserver_url=keyserver or "https://keys.openpgp.org",
|
|
415
|
+
passphrase_cache_enabled=cache_on
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if not save_settings(new_cfg):
|
|
419
|
+
self.notify(
|
|
420
|
+
f"Could not write settings to:\n{CONFIG_FILE}\n"
|
|
421
|
+
"Check file permissions.",
|
|
422
|
+
title="Save failed",
|
|
423
|
+
severity="error",
|
|
424
|
+
timeout=8
|
|
425
|
+
)
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
self.app.post_message(self.Saved(new_cfg))
|
|
429
|
+
self.app.pop_screen()
|
|
430
|
+
|
|
431
|
+
def action_discard(self) -> None:
|
|
432
|
+
self.app.pop_screen()
|
|
433
|
+
|
|
434
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.widgets import RichLog
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Level = Literal["OK", "ERR", "WARN", "INFO"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_LEVEL_STYLE: dict[str, tuple[str, str]] = {
|
|
17
|
+
"OK": ("[OK] ", "green"),
|
|
18
|
+
"ERR": ("[ERR] ", "bold red"),
|
|
19
|
+
"WARN": ("[WARN] ", "yellow"),
|
|
20
|
+
"INFO": ("[INFO] ", "dim white"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_LEVEL_ALIASES: dict[str, str] = {
|
|
24
|
+
"ok": "OK",
|
|
25
|
+
"err": "ERR",
|
|
26
|
+
"error": "ERR",
|
|
27
|
+
"warn": "WARN",
|
|
28
|
+
"warning": "WARN",
|
|
29
|
+
"info": "INFO",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _normalize_level(level: str) -> str:
|
|
34
|
+
return _LEVEL_ALIASES.get(level.lower(), _LEVEL_ALIASES.get(level, "INFO"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OperationLogWidget(RichLog):
|
|
38
|
+
DEFAULT_CSS = ""
|
|
39
|
+
COMPONENT_CLASSES = {"operation-log"}
|
|
40
|
+
|
|
41
|
+
def __init__(self, **kwargs) -> None:
|
|
42
|
+
super().__init__(
|
|
43
|
+
highlight=False,
|
|
44
|
+
markup=False,
|
|
45
|
+
wrap=True,
|
|
46
|
+
**kwargs
|
|
47
|
+
)
|
|
48
|
+
self._plain_lines: list[str] = []
|
|
49
|
+
|
|
50
|
+
def log(self, message: str, level: str = "INFO") -> None:
|
|
51
|
+
canonical = _normalize_level(level)
|
|
52
|
+
prefix, color = _LEVEL_STYLE[canonical]
|
|
53
|
+
timestamp = _timestamp()
|
|
54
|
+
|
|
55
|
+
line = Text()
|
|
56
|
+
line.append(timestamp, style="dim")
|
|
57
|
+
line.append(" "*3)
|
|
58
|
+
line.append(prefix, style=color)
|
|
59
|
+
line.append(" "*3)
|
|
60
|
+
line.append(message, style=color if canonical == "ERR" else "")
|
|
61
|
+
|
|
62
|
+
plain = f"[{timestamp}] {prefix} {message}"
|
|
63
|
+
self._plain_lines.append(plain)
|
|
64
|
+
|
|
65
|
+
self.write(line)
|
|
66
|
+
|
|
67
|
+
def log_result(
|
|
68
|
+
self,
|
|
69
|
+
ok: bool,
|
|
70
|
+
label: str,
|
|
71
|
+
detail: str | None = None
|
|
72
|
+
) -> None:
|
|
73
|
+
self.log(label, level="OK" if ok else "ERR")
|
|
74
|
+
if detail:
|
|
75
|
+
self.log(detail, level="INFO")
|
|
76
|
+
|
|
77
|
+
def log_separator(self, label: str = "") -> None:
|
|
78
|
+
width = 60
|
|
79
|
+
if label:
|
|
80
|
+
pad = max(0, (width - len(label) - 2) // 2)
|
|
81
|
+
text = f"{'-' * pad} {label} {'-'* pad}"
|
|
82
|
+
else:
|
|
83
|
+
text = "-" * width
|
|
84
|
+
|
|
85
|
+
line = Text(text, style="dim")
|
|
86
|
+
self._plain_lines.append(text)
|
|
87
|
+
self.write(line)
|
|
88
|
+
|
|
89
|
+
def clear(self) -> None:
|
|
90
|
+
super().clear()
|
|
91
|
+
self._plain_lines.clear()
|
|
92
|
+
|
|
93
|
+
def save(self, path: Path | str | None = None) -> Path | None:
|
|
94
|
+
if not self._plain_lines:
|
|
95
|
+
self.log("Log is empty - nothing to save.", level="WARN")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
if path is None:
|
|
99
|
+
stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
100
|
+
path = Path.cwd() / f"asphodel_pgp_log_{stamp}.log"
|
|
101
|
+
|
|
102
|
+
dest = Path(path)
|
|
103
|
+
try:
|
|
104
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
dest.write_text("\n".join(self._plain_lines) + "\n", encoding="utf-8")
|
|
106
|
+
|
|
107
|
+
self.log(f"Log saved » {dest}", level="OK")
|
|
108
|
+
return dest
|
|
109
|
+
except OSError as exc:
|
|
110
|
+
self.log(f"Failed to save log: {exc}", level="ERR")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _timestamp() -> str:
|
|
115
|
+
return datetime.datetime.now().strftime("%H:%M:%S")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/__init__.py
RENAMED
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/_gpg_binary.py
RENAMED
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/armor_binary.py
RENAMED
|
File without changes
|
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/gpg_context.py
RENAMED
|
File without changes
|
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/macos_shim.py
RENAMED
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/core/sign_verify.py
RENAMED
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{artemis_framework-0.3.2 → artemis_framework-0.4.1}/artemis_framework/asphodel/tui/settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|