Coverage for src/secchi/ui/widgets/status_bar.py: 40%

50 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 23:28 +0000

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

2 

3from __future__ import annotations 

4 

5import logging 

6from datetime import UTC, datetime 

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('l')} Logs " 

29 f"{_format_key('?')} Help " 

30 f"{_format_key('q')} Quit" 

31) 

32 

33 

34def format_path(path: Path | None) -> str: 

35 if path is None: 

36 return "—" 

37 try: 

38 return "~/" + str(path.relative_to(Path.home())) 

39 except ValueError: 

40 return str(path) 

41 

42 

43def _age_text(refreshed_at: datetime | None) -> str: 

44 if refreshed_at is None: 

45 return "refreshing…" 

46 now = datetime.now(UTC) 

47 mins = int((now - refreshed_at).total_seconds() / 60) 

48 if mins < 1: 

49 return "just now" 

50 if mins == 1: 

51 return "1m ago" 

52 if mins < 60: 

53 return f"{mins}m ago" 

54 return f"{mins // 60}h ago" 

55 

56 

57class SecchiFooter(Horizontal): 

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

59 

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

61 super().__init__() 

62 self._config_path = config_path 

63 

64 def compose(self) -> ComposeResult: 

65 yield Static("", id="footer-left") 

66 yield Static(SHORTCUTS, id="footer-center") 

67 yield Static(f"Config: {format_path(self._config_path)}", id="footer-right") 

68 

69 def on_mount(self) -> None: 

70 self._tick() 

71 self.set_interval(30, self._tick) 

72 

73 def _tick(self) -> None: 

74 refreshed_at = getattr(self.app, "refreshed_at", None) 

75 age = _age_text(refreshed_at) 

76 try: 

77 self.query_one("#footer-left", Static).update( 

78 f"[{palette.GREEN}]secchi[/] {__version__} [dim]│[/] Data: {age}" 

79 ) 

80 except Exception: 

81 # The interval may tick while the footer is being unmounted. 

82 logger.debug("Unable to update status bar", exc_info=True)