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