artemis_framework 0.4.0__tar.gz → 0.4.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.
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/PKG-INFO +1 -1
- artemis_framework-0.4.2/artemis_framework/asphodel/tui/widgets/key_list.py +371 -0
- artemis_framework-0.4.2/artemis_framework/asphodel/tui/widgets/operation_log.py +115 -0
- artemis_framework-0.4.2/artemis_framework/tartarus/__init__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/pyproject.toml +1 -1
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/LICENSE +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/README.md +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/__init__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/__main__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/__init__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/__main__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/__init__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/_gpg_binary.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/armor_binary.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/encrypt_decrypt.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/gpg_context.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/key_management.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/macos_shim.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/sign_verify.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/__init__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/app.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/modals/__init__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/screens/__init__.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/screens/app_config.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/settings.py +0 -0
- {artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/widgets/__init__.py +0 -0
- /artemis_framework-0.4.0/artemis_framework/elysium/__ini → /artemis_framework-0.4.2/artemis_framework/asphodel/tui/widgets/key_detail.py +0 -0
- /artemis_framework-0.4.0/artemis_framework/elysium/__init__.py → /artemis_framework-0.4.2/artemis_framework/elysium/__ini +0 -0
- {artemis_framework-0.4.0/artemis_framework/tartarus → artemis_framework-0.4.2/artemis_framework/elysium}/__init__.py +0 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
from textual.widgets import DataTable, Label
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class KeyInfo:
|
|
16
|
+
fingerprint: str
|
|
17
|
+
key_id: str
|
|
18
|
+
uids: list[str]
|
|
19
|
+
name: str
|
|
20
|
+
algorithm: str
|
|
21
|
+
expires: str
|
|
22
|
+
trust: str
|
|
23
|
+
has_secret: bool
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_gnupg_dict(
|
|
27
|
+
cls,
|
|
28
|
+
pub_dict: dict,
|
|
29
|
+
secret_fingerprints: set[str]
|
|
30
|
+
) -> "KeyInfo":
|
|
31
|
+
fp = pub_dict.get("fingerprint", "")
|
|
32
|
+
key_id = fp[-16:] if len(fp) >= 16 else fp
|
|
33
|
+
uids = pub_dict.get("uids", []) or ["<no UID>"]
|
|
34
|
+
name = _first_uid_name(uids[0])
|
|
35
|
+
algo = _format_algorithm(pub_dict)
|
|
36
|
+
expires = _format_expiry(pub_dict.get("expires", ""))
|
|
37
|
+
trust = _expand_trust(pub_dict.get("ownertrust", pub_dict.get("trust", "?")))
|
|
38
|
+
has_sec = fp in secret_fingerprints
|
|
39
|
+
|
|
40
|
+
return cls(
|
|
41
|
+
fingerprint=fp,
|
|
42
|
+
key_id=key_id,
|
|
43
|
+
uids=uids,
|
|
44
|
+
name=name,
|
|
45
|
+
algorithm=algo,
|
|
46
|
+
expires=expires,
|
|
47
|
+
trust=trust,
|
|
48
|
+
has_secret=has_sec
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_COLUMNS: list[tuple[str, int]] = [
|
|
53
|
+
("✦", 3),
|
|
54
|
+
("Name / UID", 28),
|
|
55
|
+
("Key ID", 18),
|
|
56
|
+
("Algorithm", 12),
|
|
57
|
+
("Expires", 12),
|
|
58
|
+
("Trust", 14),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class KeyListWidget(DataTable):
|
|
63
|
+
|
|
64
|
+
class CursorMoved(Message):
|
|
65
|
+
def __init__(self, key_info: KeyInfo) -> None:
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.key_info = key_info
|
|
68
|
+
|
|
69
|
+
class KeyConfirmed(Message):
|
|
70
|
+
def __init__(self, key_info: KeyInfo) -> None:
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.key_info = key_info
|
|
73
|
+
|
|
74
|
+
class SelectionChanged(Message):
|
|
75
|
+
def __init__(self, fingerprints: frozenset[str]) -> None:
|
|
76
|
+
super().__init__()
|
|
77
|
+
self.fingerprints = fingerprints
|
|
78
|
+
|
|
79
|
+
BINDINGS = [
|
|
80
|
+
Binding("space", "toggle_select", "Add » selection", show=True),
|
|
81
|
+
Binding("escape", "clear_select", "Clear selection", show=True),
|
|
82
|
+
Binding("enter", "confirm_key", "Confirm", show=True),
|
|
83
|
+
Binding("r", "refresh_table", "Refresh", show=True),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
DEFAULT_CSS = """
|
|
87
|
+
KeyListWidget {
|
|
88
|
+
height: 1fr;
|
|
89
|
+
border: double $primary-darken-1;
|
|
90
|
+
scrollbar-color: $primary;
|
|
91
|
+
scrollbar-color-hover: $primary-lighten-1;
|
|
92
|
+
background: $surface;
|
|
93
|
+
}
|
|
94
|
+
KeyListWidget:focus {
|
|
95
|
+
border: double $primary;
|
|
96
|
+
background: $background;
|
|
97
|
+
}
|
|
98
|
+
KeyListWidget > .datatable--header {
|
|
99
|
+
background: $primary-darken-2;
|
|
100
|
+
color: $text-muted;
|
|
101
|
+
text-style: bold;
|
|
102
|
+
}
|
|
103
|
+
KeyListWidget > .datatable--cursor {
|
|
104
|
+
background: $primary-darken-1;
|
|
105
|
+
color: $text;
|
|
106
|
+
}
|
|
107
|
+
KeyListWidget:focus > .datatable--cursor {
|
|
108
|
+
background: $primary;
|
|
109
|
+
color: $background;
|
|
110
|
+
text-style: bold;
|
|
111
|
+
}
|
|
112
|
+
KeyListWidget > .datatable--even-row {
|
|
113
|
+
background: $surface;
|
|
114
|
+
}
|
|
115
|
+
KeyListWidget > .datatable--odd-row {
|
|
116
|
+
background: $background;
|
|
117
|
+
}
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
gpg=None,
|
|
123
|
+
*,
|
|
124
|
+
confirm_callback: Callable[[KeyInfo], None] | None = None,
|
|
125
|
+
**kwargs
|
|
126
|
+
) -> None:
|
|
127
|
+
super().__init__(
|
|
128
|
+
cursor_type="row",
|
|
129
|
+
zebra_stripes=True,
|
|
130
|
+
**kwargs
|
|
131
|
+
)
|
|
132
|
+
self._gpg = gpg
|
|
133
|
+
self._confirm_callback = confirm_callback
|
|
134
|
+
|
|
135
|
+
self._key_map: dict[str, KeyInfo] = {}
|
|
136
|
+
self._row_key_to_fp: dict[str, str] = {}
|
|
137
|
+
self._selected_fps: set[str] = set()
|
|
138
|
+
|
|
139
|
+
def on_mount(self) -> None:
|
|
140
|
+
for label, width in _COLUMNS:
|
|
141
|
+
self.add_column(label, width=width)
|
|
142
|
+
if self._gpg is not None:
|
|
143
|
+
self.load(self._gpg)
|
|
144
|
+
|
|
145
|
+
def load(self, gpg) -> None:
|
|
146
|
+
self._gpg = gpg
|
|
147
|
+
self.clear(columns=False)
|
|
148
|
+
self._key_map.clear()
|
|
149
|
+
self._row_key_to_fp.clear()
|
|
150
|
+
self._selected_fps.clear()
|
|
151
|
+
|
|
152
|
+
pub_keys = gpg.list_keys(False)
|
|
153
|
+
secret_fps = {k["fingerprint"] for k in gpg.list_keys(True)}
|
|
154
|
+
|
|
155
|
+
for pub_dict in pub_keys:
|
|
156
|
+
info = KeyInfo.from_gnupg_dict(pub_dict, secret_fps)
|
|
157
|
+
self._key_map[info.fingerprint] = info
|
|
158
|
+
row_key = info.fingerprint
|
|
159
|
+
self._row_key_to_fp[row_key] = info.fingerprint
|
|
160
|
+
self.add_row(
|
|
161
|
+
*self._build_row_cells(info, selected=False),
|
|
162
|
+
key=row_key
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def refresh_keys(self, gpg=None) -> None:
|
|
166
|
+
self.load(gpg or self._gpg)
|
|
167
|
+
|
|
168
|
+
def get_selected(self) -> frozenset[str]:
|
|
169
|
+
return frozenset(self._selected_fps)
|
|
170
|
+
|
|
171
|
+
def get_cursor_key(self) -> KeyInfo | None:
|
|
172
|
+
return self._info_at_cursor()
|
|
173
|
+
|
|
174
|
+
def clear_selection(self) -> None:
|
|
175
|
+
self.action_clear_select()
|
|
176
|
+
|
|
177
|
+
def select_by_fingerprint(self, fingerprint: str) -> None:
|
|
178
|
+
if fingerprint in self._key_map:
|
|
179
|
+
self._selected_fps.add(fingerprint)
|
|
180
|
+
self._redraw_row(fingerprint)
|
|
181
|
+
self.post_message(self.SelectionChanged(self.get_selected()))
|
|
182
|
+
|
|
183
|
+
def on_data_table_row_highlighted(
|
|
184
|
+
self,
|
|
185
|
+
event: DataTable.RowHighlighted
|
|
186
|
+
) -> None:
|
|
187
|
+
event.stop()
|
|
188
|
+
fp = self._row_key_to_fp.get(str(event.row_key.value), "")
|
|
189
|
+
info = self._key_map.get(fp)
|
|
190
|
+
if info:
|
|
191
|
+
self.post_message(self.CursorMoved(info))
|
|
192
|
+
|
|
193
|
+
def action_toggle_select(self) -> None:
|
|
194
|
+
info = self._info_at_cursor()
|
|
195
|
+
if info is None:
|
|
196
|
+
return
|
|
197
|
+
fp = info.fingerprint
|
|
198
|
+
if fp in self._selected_fps:
|
|
199
|
+
self._selected_fps.discard(fp)
|
|
200
|
+
else:
|
|
201
|
+
self._selected_fps.add(fp)
|
|
202
|
+
self._redraw_row(fp)
|
|
203
|
+
self.post_message(self.SelectionChanged(self.get_selected()))
|
|
204
|
+
|
|
205
|
+
def action_clear_select(self) -> None:
|
|
206
|
+
fps_to_redraw = set(self._selected_fps)
|
|
207
|
+
self._selected_fps.clear()
|
|
208
|
+
for fp in fps_to_redraw:
|
|
209
|
+
self._redraw_row(fp)
|
|
210
|
+
self.post_message(self.SelectionChanged(self.get_selected()))
|
|
211
|
+
|
|
212
|
+
def action_confirm_key(self) -> None:
|
|
213
|
+
info = self._info_at_cursor()
|
|
214
|
+
if info is None:
|
|
215
|
+
return
|
|
216
|
+
self.post_message(self.KeyConfirmed(info))
|
|
217
|
+
if self._confirm_callback:
|
|
218
|
+
self._confirm_callback(info)
|
|
219
|
+
|
|
220
|
+
def action_refresh_table(self) -> None:
|
|
221
|
+
if self._gpg:
|
|
222
|
+
self.load(self._gpg)
|
|
223
|
+
|
|
224
|
+
def _info_at_cursor(self) -> KeyInfo | None:
|
|
225
|
+
if self.row_count == 0:
|
|
226
|
+
return None
|
|
227
|
+
try:
|
|
228
|
+
fp_list = list(self._row_key_to_fp.values())
|
|
229
|
+
if self.cursor_row >= len(fp_list):
|
|
230
|
+
return None
|
|
231
|
+
fp = fp_list[self.cursor_row]
|
|
232
|
+
return self._key_map.get(fp)
|
|
233
|
+
except Exception:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def _build_row_cells(
|
|
237
|
+
self,
|
|
238
|
+
info: KeyInfo,
|
|
239
|
+
*,
|
|
240
|
+
selected: bool
|
|
241
|
+
) -> tuple[Text | str, ...]:
|
|
242
|
+
sel_marker = "▶" if selected else " "
|
|
243
|
+
sec_marker = "✦" if info.has_secret else "•"
|
|
244
|
+
indicator = Text(f"{sel_marker}{sec_marker}", no_wrap=True)
|
|
245
|
+
if selected:
|
|
246
|
+
indicator.stylize("bold yellow")
|
|
247
|
+
elif info.has_secret:
|
|
248
|
+
indicator.stylize("bold cyan")
|
|
249
|
+
else:
|
|
250
|
+
indicator.stylize("dim")
|
|
251
|
+
|
|
252
|
+
name_cell = Text(info.name[:26], no_wrap=True)
|
|
253
|
+
if info.has_secret:
|
|
254
|
+
name_cell.stylize("cyan")
|
|
255
|
+
|
|
256
|
+
key_id_cell = Text(info.key_id, no_wrap=True, style="dim")
|
|
257
|
+
|
|
258
|
+
algo_cell = Text(info.algorithm, no_wrap=True)
|
|
259
|
+
|
|
260
|
+
expires_cell = Text(info.expires, no_wrap=True)
|
|
261
|
+
if info.expires.lower() not in ("never", ""):
|
|
262
|
+
expires_cell.stylize("yellow")
|
|
263
|
+
|
|
264
|
+
trust_cell = Text(info.trust, no_wrap=True)
|
|
265
|
+
trust_cell.stylize(_trust_style(info.trust))
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
indicator,
|
|
269
|
+
name_cell,
|
|
270
|
+
key_id_cell,
|
|
271
|
+
algo_cell,
|
|
272
|
+
expires_cell,
|
|
273
|
+
trust_cell,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def _redraw_row(self, fingerprint: str) -> None:
|
|
277
|
+
info = self._key_map.get(fingerprint)
|
|
278
|
+
if info is None:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
selected = fingerprint in self._selected_fps
|
|
282
|
+
cells = self._build_row_cells(info, selected=selected)
|
|
283
|
+
col_keys = [col.key for col in self.columns.values()]
|
|
284
|
+
for col_key, cell_value in zip(col_keys, cells):
|
|
285
|
+
try:
|
|
286
|
+
self.update_cell(
|
|
287
|
+
row_key=fingerprint,
|
|
288
|
+
column_key=col_key,
|
|
289
|
+
value=cell_value,
|
|
290
|
+
update_width=False
|
|
291
|
+
)
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _first_uid_name(uid: str) -> str:
|
|
297
|
+
m = re.match(r"^([^<(]+)", uid)
|
|
298
|
+
return m.group(1).strip() if m else uid.strip()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _format_algorithm(key_dict: dict) -> str:
|
|
302
|
+
algo = key_dict.get("algo", "")
|
|
303
|
+
length = key_dict.get("length", "")
|
|
304
|
+
curve = key_dict.get("curve", "")
|
|
305
|
+
|
|
306
|
+
algo_names = {
|
|
307
|
+
"1": "RSA",
|
|
308
|
+
"2": "RSA",
|
|
309
|
+
"3": "RSA",
|
|
310
|
+
"17": "DSA",
|
|
311
|
+
"18": "ECDH",
|
|
312
|
+
"19": "ECDSA",
|
|
313
|
+
"22": "EdDSA",
|
|
314
|
+
}
|
|
315
|
+
name = algo_names.get(str(algo), str(algo))
|
|
316
|
+
|
|
317
|
+
if curve:
|
|
318
|
+
curve_short = {
|
|
319
|
+
"ed25519": "255",
|
|
320
|
+
"cv25519": "255",
|
|
321
|
+
"nistp256": "P256",
|
|
322
|
+
"nistp384": "P384",
|
|
323
|
+
"nistp521": "P521",
|
|
324
|
+
}.get(curve.lower(), curve)
|
|
325
|
+
return f"{name} {curve_short}"
|
|
326
|
+
|
|
327
|
+
if length:
|
|
328
|
+
return f"{name} {length}"
|
|
329
|
+
|
|
330
|
+
return name
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _format_expiry(expires: str) -> str:
|
|
334
|
+
if not expires:
|
|
335
|
+
return "never"
|
|
336
|
+
try:
|
|
337
|
+
import datetime
|
|
338
|
+
ts = int(expires)
|
|
339
|
+
|
|
340
|
+
return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
|
|
341
|
+
except (ValueError, OSError):
|
|
342
|
+
return expires
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _expand_trust(code: str) -> str:
|
|
346
|
+
return {
|
|
347
|
+
"u": "ultimate",
|
|
348
|
+
"f": "full",
|
|
349
|
+
"m": "marginal",
|
|
350
|
+
"n": "not trusted",
|
|
351
|
+
"e": "expired",
|
|
352
|
+
"r": "revoked",
|
|
353
|
+
"?": "unknown",
|
|
354
|
+
"-": "unknown",
|
|
355
|
+
"q": "undefined",
|
|
356
|
+
}.get(code.lower(), code)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _trust_style(trust_label: str) -> str:
|
|
360
|
+
return {
|
|
361
|
+
"ultimate": "bold green",
|
|
362
|
+
"full": "green",
|
|
363
|
+
"marginal": "yellow",
|
|
364
|
+
"not trusted": "dim red",
|
|
365
|
+
"expired": "bold red",
|
|
366
|
+
"revoked": "bold red",
|
|
367
|
+
"unknown": "dim",
|
|
368
|
+
"undefined": "dim",
|
|
369
|
+
}.get(trust_label, "")
|
|
370
|
+
|
|
371
|
+
|
|
@@ -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
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/__init__.py
RENAMED
|
File without changes
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/_gpg_binary.py
RENAMED
|
File without changes
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/armor_binary.py
RENAMED
|
File without changes
|
|
File without changes
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/gpg_context.py
RENAMED
|
File without changes
|
|
File without changes
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/macos_shim.py
RENAMED
|
File without changes
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/core/sign_verify.py
RENAMED
|
File without changes
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{artemis_framework-0.4.0 → artemis_framework-0.4.2}/artemis_framework/asphodel/tui/settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|