add: логи внутри TUI

This commit is contained in:
2025-06-08 22:28:00 +03:00
parent e6dd940d8a
commit 22bd7ee303
3 changed files with 194 additions and 64 deletions

View File

@@ -2,73 +2,123 @@
# src/pganec/tui.py
import logging
from logging.handlers import MemoryHandler
from typing import Dict, Any, Optional
from textual.app import App, ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Button, Header, Footer, Static
from textual.widgets import Button, Header, Footer, Static, Log
# --- Настройки и инициирование логирования модуля ---
logger = logging.getLogger(__name__)
# Кастомный обработчик логирования для Textual
class TextualLogHandler(logging.Handler):
"""
Обработчик логирования, который направляет записи в виджет Textual Log.
"""
def __init__(self, textual_log_widget: Log):
super().__init__()
self.textual_log_widget = textual_log_widget
def emit(self, record: logging.LogRecord):
"""
Отправляет отформатированную запись лога в виджет.
"""
try:
msg = self.format(record)
# Метод Log.write() является потоко-безопасным, и его можно вызывать напрямую.
self.textual_log_widget.write(msg)
except Exception:
# Стандартная обработка ошибок для Handler, если что-то пошло не так (например, виджет был удален
# или произошла ошибка форматирования)
self.handleError(record)
class MainMenu(Static):
def compose(self) -> ComposeResult:
yield Horizontal(
Button(label="Backup (DB -> Dump)", id="backup", variant="primary"),
Button(label="Restore (Dump -> DB)", id="restore", variant="primary"),
Button(label="Backup (DB -> Dump)", id="backup"),
Button(label="Restore (Dump -> DB)", id="restore"),
Button(label="Copy (DB -> DB)", id="copy", variant="error"),
Button(label="Quit", id="quit", variant="error"),
id="menu",
)
yield Footer()
class PGanecApp(App):
CSS = """
#menu {
width: 100%;
align: left top;
padding: 0;
Screen {
layout: vertical; /* Используем вертикальный layout для всего экрана */
}
#main_content_area { /* Новый контейнер для всего, что НЕ прибито к краям */
/* Этот контейнер будет занимать все доступное пространство
между Header и Log/Footer */
height: 1fr; /* 1fr означает "одна доля доступного пространства" */
overflow-y: auto; /* Если контент не влезет, появится прокрутка */
}
#title { width: 100%; text-align: left; background: purple; padding: 0 1; }
#menu { width: 100%; height: 3; align: left top; padding: 0; }
Button {
align: left middle;
text-align: left;
align: center middle;
width: 25%;
color: orange;
background: transparent;
margin: 0;
padding: 0;
margin: 0; padding: 0;
}
Button.-primary {
color: orange;
border: round orange;
text-align: left;
}
Button#backup, Button#restore { color: orange; border: round orange; }
Button.-error {
color: grey;
border: round orange;
text-align: left;
}
Button#copy, Button#quit { color: grey; border: round orange; }
Button:focus {
border: heavy green;
Button:focus { border: heavy green; }
Log#app_log_viewer {
dock: bottom;
height: 10;
margin-top: 1;
border-top: double grey;
background: 15%;
}
Log#app_log_viewer:focus { height: 50%; border-top: solid green; background: 45%; }
Footer { dock: bottom; height: 1; }
Header { dock: top; height: 1; }
"""
BINDINGS = [
("b", "backup", "Backup"),
("r", "restore", "Restore"),
("с", "copy", "Copy"),
("c", "copy", "Copy"),
("q", "quit", "Quit"),
("right", "focus_next"), ("down", "focus_next"),
("left", "focus_previous"), ("up", "focus_previous"),
("enter", "activate"),
("right", "focus_next"), ("down", "focus_next"), # Стрелочки для навигации (вниз/вправо -- вперед)
("left", "focus_previous"), ("up", "focus_previous"), # Стрелочки для навигации (вверх/влево -- назад)
("enter", "activate", "Activate"),
]
def __init__(self,
log_level_int: int = logging.INFO,
app_config: Dict[str, Any] = None,
early_log_handler: Optional[MemoryHandler] = None,
**kwargs):
"""
Конструктор приложения.
:param cli_log_level_str: Уровень логирования из CLI (задаётся через параметры при запуске приложения).
"""
super().__init__(**kwargs)
self.log_level_int = log_level_int
self.app_config = app_config if app_config is not None else {} # Сохраняем конфигурацию
self.early_log_handler = early_log_handler # <--- Сохраняем
logger.debug(
f"TUI инициализирован. Log Level: `{logging.getLevelName(self.log_level_int)}`; Config Keys: {list(self.app_config.keys())}"
)
def compose(self) -> ComposeResult:
yield Header()
yield Static("PGanec", id="title")
yield MainMenu()
with Vertical(id="main_content_area"):
yield Static("PGanec TUI", id="title")
yield MainMenu()
# Другие элементы основного интерфейса могут быть здесь
yield Log(id="app_log_viewer", highlight=True) # Виджет лога
yield Footer()
async def on_button_pressed(self, event: Button.Pressed) -> None:
@@ -82,6 +132,48 @@ class PGanecApp(App):
elif button_id == "quit":
await self.action_quit()
def on_mount(self) -> None:
"""Вызывается после монтирования DOM приложения."""
# --- Обжужукиваем логирование в #app_log_viewer ---
log_viewer_widget = self.query_one("#app_log_viewer", Log)
textual_handler = TextualLogHandler(log_viewer_widget)
if self.log_level_int >= logging.INFO:
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)-8s]\t %(message)s\n",
datefmt="%H:%M:%S" # Формат времени: часы:минуты:секунды
)
else:
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)-8s] %(module)s.%(funcName)s:%(lineno)04d\t- %(message)s\n",
datefmt="%H:%M:%S.%s" # Формат времени: часы:минуты:секунды.милисекунды
)
textual_handler.setFormatter(formatter)
textual_handler.setLevel(self.log_level_int)
# Получаем корневой логгер
target_logger = logging.getLogger()
target_logger.addHandler(textual_handler) # Добавляем наш обработчик
# --- Перенос и сброс ранних логов из MemoryHandler ---
if self.early_log_handler:
# Устанавливаем наш textual_handler как цель для MemoryHandler
self.early_log_handler.setTarget(textual_handler)
# Сбрасываем все накопленные записи
self.early_log_handler.flush()
# Закрываем и удаляем MemoryHandler из корневого логгера, т.к. он больше не нужен
self.early_log_handler.close()
logger.debug("TUI: Перенесли и закрыли ранние логи…")
target_logger.removeHandler(self.early_log_handler)
logger.debug("TUI: Удалили ранний MemoryHandler из корневого логгера…")
self.early_log_handler = None # Очищаем ссылку
# --- Конец переноса и сброса ---
logger.debug("TUI: DEBUG сообщение для проверки")
logger.info("TUI-логгер инициализирован")
logger.warning("TUI: WARNING для проверки")
logger.error("TUI: ERROR тоже для проверки")
async def action_activate(self) -> None:
focused = self.focused
if isinstance(focused, Button):