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

1"""Custom bottom status bar — terminal-editor style with shortcut keys.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from datetime import datetime, timezone 

7from pathlib import Path 

8 

9from textual.app import ComposeResult 

10from textual.containers import Horizontal 

11from textual.widgets import Static 

12 

13from secchi import __version__ 

14from secchi.ui import palette 

15 

16logger = logging.getLogger(__name__) 

17 

18 

19def _format_key(key: str) -> str: 

20 return f"[black on {palette.SECCHI}] {key} [/]" 

21 

22 

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) 

31 

32 

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) 

40 

41 

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" 

54 

55 

56class SecchiFooter(Horizontal): 

57 """Docked bottom bar: info left, shortcuts center, config right.""" 

58 

59 def __init__(self, config_path: Path | None) -> None: 

60 super().__init__() 

61 self._config_path = config_path 

62 

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") 

67 

68 def on_mount(self) -> None: 

69 self._tick() 

70 self.set_interval(30, self._tick) 

71 

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)