Coverage for src/secchi/ui/widgets/status_bar.py: 40%
50 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-04 22:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-04 22:15 +0000
1"""Custom bottom status bar — terminal-editor style with shortcut keys."""
3from __future__ import annotations
5import logging
6from datetime import datetime, timezone
7from pathlib import Path
9from textual.app import ComposeResult
10from textual.containers import Horizontal
11from textual.widgets import Static
13from secchi import __version__
14from secchi.ui import palette
16logger = logging.getLogger(__name__)
19def _format_key(key: str) -> str:
20 return f"[black on {palette.SECCHI}] {key} [/]"
23SHORTCUTS = (
24 f"{_format_key('r')} Refresh "
25 f"{_format_key('e')} Export "
26 f"{_format_key('/')} Search "
27 f"{_format_key('f')} Filter "
28 f"{_format_key('?')} Help "
29 f"{_format_key('q')} Quit"
30)
33def format_path(path: Path | None) -> str:
34 if path is None:
35 return "—"
36 try:
37 return "~/" + str(path.relative_to(Path.home()))
38 except ValueError:
39 return str(path)
42def _age_text(refreshed_at: datetime | None) -> str:
43 if refreshed_at is None:
44 return "refreshing…"
45 now = datetime.now(timezone.utc)
46 mins = int((now - refreshed_at).total_seconds() / 60)
47 if mins < 1:
48 return "just now"
49 if mins == 1:
50 return "1m ago"
51 if mins < 60:
52 return f"{mins}m ago"
53 return f"{mins // 60}h ago"
56class SecchiFooter(Horizontal):
57 """Docked bottom bar: info left, shortcuts center, config right."""
59 def __init__(self, config_path: Path | None) -> None:
60 super().__init__()
61 self._config_path = config_path
63 def compose(self) -> ComposeResult:
64 yield Static("", id="footer-left")
65 yield Static(SHORTCUTS, id="footer-center")
66 yield Static(f"Config: {format_path(self._config_path)}", id="footer-right")
68 def on_mount(self) -> None:
69 self._tick()
70 self.set_interval(30, self._tick)
72 def _tick(self) -> None:
73 refreshed_at = getattr(self.app, "refreshed_at", None)
74 age = _age_text(refreshed_at)
75 try:
76 self.query_one("#footer-left", Static).update(
77 f"[{palette.GREEN}]secchi[/] {__version__} [dim]│[/] Data: {age}"
78 )
79 except Exception:
80 # The interval may tick while the footer is being unmounted.
81 logger.debug("Unable to update status bar", exc_info=True)