diff --git a/loggy b/loggy index d860ce9..0de98d3 100755 --- a/loggy +++ b/loggy @@ -2,5 +2,5 @@ # BASEPATH=$(dirname $0) -$BASEPATH/venv/bin/python $BASEPATH/loggy.py +$BASEPATH/venv/bin/python $BASEPATH/loggy.py $* diff --git a/loggy.py b/loggy.py index 4f6cecb..f6150f1 100644 --- a/loggy.py +++ b/loggy.py @@ -1,5 +1,6 @@ import logging import pickle +import re import socket import struct import threading @@ -7,7 +8,7 @@ from datetime import datetime from textual.app import App, ComposeResult from textual.containers import Vertical -from textual.widgets import Footer, Header, Input, RichLog, Checkbox +from textual.widgets import Footer, Header, Input, RichLog, Checkbox, Select # Mapping von Log-Level-Namen zu Farben für die Anzeige LEVEL_STYLES = { @@ -55,6 +56,68 @@ class LogReceiver(threading.Thread): logging.error(f"LogReceiver-Fehler: {e}", exc_info=True) +class OptionSelectList(dict): + def __init__(self): + super().__init__(self) + # + self.__default_value__ = True + self.__selection_regex__ = "" + + def __sorted_keys__(self): + rv = list(self.keys()) + rv.sort() + return rv + + def SetSelectionRegEx(self, regex: str) -> None: + self.__selection_regex__ = regex + + def AddEntry(self, entry) -> None: + if entry not in self: + self[entry] = self.__default_value__ + + def Toggle(self, entry_num): + if entry_num < 0: + self.__default_value__ = entry_num == -1 + for key in self: + self[key] = self.__default_value__ + elif entry_num > len(self): + raise ValueError(f"The Entry '{entry} is not in the list") + else: + entry = self.__sorted_keys__()[entry_num] + self[entry] = not self[entry] + + def GetSelectList(self): + rv = [] + if len(self) > 2: + rv.append(('All', -1)) + rv.append(('None', -2)) + for index, key in enumerate(self.__sorted_keys__()): + try: + match = len(re.findall(self.__selection_regex__, key)) > 0 + except re.error: + match = True # No valid regular expression + if match: + prefix = "\\[X] " if self[key] else "\\[-] " + rv.append((prefix + key, index)) + return rv + + def IsSelected(self, entry): + return self.get(entry, False) + + +class MySelect(Select): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__new_options__ = [] + + def set_options(self, options): + self.__new_options__ = options + + def _on_enter(self, event): + super().set_options(self.__new_options__) + return super()._on_enter(event) + + class LogViewerApp(App): """Eine Textual-App zum Anzeigen und Filtern von Python-Logs.""" @@ -68,8 +131,10 @@ class LogViewerApp(App): super().__init__() # self.all_logs = [] - self.module_filter = "" - self.level_filter = "" + self.__module_select_list__ = OptionSelectList() + self.__module_selection__ = MySelect((), prompt="Module", id="module_filter") + self.__level_select_list__ = OptionSelectList() + self.__level_selection__ = MySelect((), prompt="Level", id="level_filter") self.force_critical = True self.force_error = True self.force_warning = False @@ -81,8 +146,9 @@ class LogViewerApp(App): with Vertical(id="app-grid"): yield self.log_display with Vertical(id="bottom-bar"): - yield Input(placeholder="module filter...", id="module_filter") - yield Input(placeholder="level filter...", id="level_filter") + yield self.__module_selection__ + yield Input(placeholder="Filter", id="select_filter") + yield self.__level_selection__ yield Checkbox("CRITICAL", self.force_critical, id="force_critical") yield Checkbox("ERROR", self.force_error, id="force_error") yield Checkbox("WARNING", self.force_warning, id="force_warning") @@ -95,6 +161,10 @@ class LogViewerApp(App): def add_log(self, record: logging.LogRecord) -> None: """Fügt einen neuen Log-Eintrag hinzu und aktualisiert die Anzeige.""" + self.__module_select_list__.AddEntry(record.name) + self.__module_selection__.set_options(self.__module_select_list__.GetSelectList()) + self.__level_select_list__.AddEntry(record.levelname) + self.__level_selection__.set_options(self.__level_select_list__.GetSelectList()) self.all_logs.append(record) self._apply_filters_to_log(record) @@ -107,13 +177,8 @@ class LogViewerApp(App): def _apply_filters_to_log(self, record: logging.LogRecord): """Prüft einen einzelnen Log-Eintrag gegen die Filter und zeigt ihn ggf. an.""" - module_match = False - for module_filter in self.module_filter.split(","): - module_match |= module_filter.lower() in record.name.lower() - - level_match = False - for level_filter in self.level_filter.split(","): - level_match |= level_filter.lower() in record.levelname.lower() + module_match = self.__module_select_list__.IsSelected(record.name) + level_match = self.__level_select_list__.IsSelected(record.levelname) if self._force(record.levelname.lower()) or (module_match and level_match): level_style = LEVEL_STYLES.get(record.levelname, "white") @@ -140,15 +205,6 @@ class LogViewerApp(App): for record in self.all_logs: self._apply_filters_to_log(record) - def on_input_changed(self, message: Input.Changed) -> None: - """Aktualisiert die Filter und die Anzeige, wenn der Benutzer tippt.""" - if message.input.id == "module_filter": - self.module_filter = message.value - elif message.input.id == "level_filter": - self.level_filter = message.value - - self._update_display() - def on_checkbox_changed(self, message: Checkbox.Changed) -> None: if message.checkbox.id == "force_critical": self.force_critical = message.value @@ -159,6 +215,27 @@ class LogViewerApp(App): self._update_display() + def on_input_changed(self, message: Input.Changed) -> None: + """Update the tui inputs and execute task, if requireed.""" + if message.input.id == "select_filter": + self.__module_select_list__.SetSelectionRegEx(message.value) + self.__module_selection__.set_options(self.__module_select_list__.GetSelectList()) + self.__module_selection__.prompt = "Full" if not message.value else "Filtered" + + def on_select_changed(self, message: Select.Changed) -> None: + if message.select.id == 'module_filter': + if type(message.value) is int: + self.__module_select_list__.Toggle(message.value) + self.__module_selection__.clear() + self.__module_selection__.set_options(self.__module_select_list__.GetSelectList()) + elif message.select.id == 'level_filter': + if type(message.value) is int: + self.__level_select_list__.Toggle(message.value) + self.__level_selection__.clear() + self.__level_selection__.set_options(self.__level_select_list__.GetSelectList()) + + self._update_display() + if __name__ == "__main__": app = LogViewerApp() diff --git a/style.tcss b/style.tcss index 927f385..b2181d9 100644 --- a/style.tcss +++ b/style.tcss @@ -7,8 +7,8 @@ #bottom-bar { layout: grid; - grid-size: 5; - grid-columns: 1fr 1fr 15 15 15; + grid-size: 6; + grid-columns: 4fr 1fr 2fr 15 15 15; height: 4; border-top: solid $primary; }