Coverage for src/lilbee/cli/tui/__init__.py: 100%
60 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +0000
1"""Textual TUI for lilbee -- full-screen interactive knowledge base."""
3from __future__ import annotations
5import io
6import logging
7import os
8import sys
9from dataclasses import dataclass
10from pathlib import Path
12from lilbee.app.services import exit_when_stragglers_would_hang, reset_services_on_exit
13from lilbee.cli.tui.log_routing import setup_tui_log_file
14from lilbee.core.security import OWNER_ONLY_MODE
17def _silence_stderr_log_handlers() -> None:
18 """Drop stderr/stdout-streaming log handlers before mounting the TUI. (bb-82ce)
20 lilbee logs route to ``cfg.data_root/logs/tui.log`` for the duration of
21 the TUI session via :func:`setup_tui_log_file`. FileHandler instances
22 are skipped explicitly here so that file route survives.
23 """
24 root = logging.getLogger()
25 for handler in list(root.handlers):
26 if not isinstance(handler, logging.StreamHandler):
27 continue
28 if isinstance(handler, logging.FileHandler):
29 continue
30 if handler.stream in (sys.stderr, sys.stdout):
31 root.removeHandler(handler)
34@dataclass
35class _StderrRedirect:
36 """State captured by ``_redirect_native_stderr_to`` for restoration on exit."""
38 saved_fd: int
39 saved_sys_stderr: object
40 saved_sys_dunder_stderr: object
43def _redirect_native_stderr_to(log_path: Path) -> _StderrRedirect | None:
44 """Send native fd-2 writes to *log_path* without breaking Textual's render.
46 Textual's Linux/macOS driver writes its alternate-screen ANSI to
47 ``sys.__stderr__`` (see
48 ``textual.drivers.linux_driver.LinuxDriver.write``). Native deps
49 like xberg's vendored tesseract write directly to fd 2 and leak
50 onto the same buffer, e.g. "Detected N diacritics", which corrupts
51 the TUI.
53 Strategy: dup the original fd 2 to a saved fd, repoint
54 ``sys.__stderr__`` and ``sys.stderr`` at that saved fd so Textual
55 keeps drawing to the real terminal, then dup2 fd 2 itself to the
56 log file so any fd-2 writer (xberg, tesseract, poppler) lands
57 in ``tui.log`` instead of on top of the screen.
58 """
59 try:
60 log_fd = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, OWNER_ONLY_MODE)
61 except OSError:
62 return None
63 saved_fd = os.dup(2)
64 terminal_stderr = io.TextIOWrapper(
65 io.FileIO(saved_fd, "w", closefd=False),
66 encoding=sys.stderr.encoding or "utf-8",
67 errors="replace",
68 write_through=True,
69 )
70 saved_sys_stderr = sys.stderr
71 saved_sys_dunder_stderr = sys.__stderr__
72 sys.stderr = terminal_stderr
73 sys.__stderr__ = terminal_stderr # type: ignore[misc]
74 os.dup2(log_fd, 2)
75 os.close(log_fd)
76 return _StderrRedirect(
77 saved_fd=saved_fd,
78 saved_sys_stderr=saved_sys_stderr,
79 saved_sys_dunder_stderr=saved_sys_dunder_stderr,
80 )
83def _restore_native_stderr(redirect: _StderrRedirect | None) -> None:
84 """Undo ``_redirect_native_stderr_to``; safe to call when *redirect* is None."""
85 if redirect is None:
86 return
87 os.dup2(redirect.saved_fd, 2)
88 os.close(redirect.saved_fd)
89 sys.stderr = redirect.saved_sys_stderr # type: ignore[assignment]
90 sys.__stderr__ = redirect.saved_sys_dunder_stderr # type: ignore[misc,assignment]
93def run_tui(*, initial_view: str | None = None) -> None:
94 """Launch the full-screen Textual TUI.
96 *initial_view* deep-links to a named view (e.g. ``"Catalog"``) after
97 the default chat screen is mounted. Used by ``lilbee model browse``.
98 """
99 # heavy: cli.sync transitively imports ingest -> store -> pyarrow (~1.8s
100 # cold-start on bare runners); only needed at TUI shutdown. (bb-oae5)
101 from lilbee.cli.sync import shutdown_executor
102 from lilbee.cli.tui.app import LilbeeApp
104 log_path = setup_tui_log_file()
105 _silence_stderr_log_handlers()
106 stderr_redirect = _redirect_native_stderr_to(log_path)
108 app = LilbeeApp(initial_view=initial_view)
109 try:
110 app.run()
111 except KeyboardInterrupt:
112 pass # Ctrl-C exits the TUI; cleanup runs in the finally block
113 finally:
114 _restore_native_stderr(stderr_redirect)
115 shutdown_executor()
116 reset_services_on_exit()
117 exit_when_stragglers_would_hang()