diff --git a/layout.py b/layout.py new file mode 100644 index 0000000..a938a7d --- /dev/null +++ b/layout.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): + pass + + +class Footer(Placeholder): + pass + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") + yield Footer(id="Footer") + + +class LayoutApp(App): + def on_mount(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/src/docker_logs/app.py b/src/docker_logs/app.py new file mode 100644 index 0000000..8584e31 --- /dev/null +++ b/src/docker_logs/app.py @@ -0,0 +1,81 @@ +"""Main application class""" + +import logging +from threading import active_count + +from textual.app import App +from textual import on + +from .screens.main_screen import MainScreen +from .services.docker_service import DockerService +from .utils.colors import ColorManager +from .utils.config import config + +# Setup the logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class DockerLogsApp(App): + """main application""" + + TITLE = "Docker Logs Monitor" + + BINDINGS = [ + ("q", "quit", "Quit"), + ("s", "start_monitoring", "Start Monitoring"), + ("t", "stop_monitoring", "Stop Monitoring"), + ("c", "clear_logs", "Clear Logs"), + ("r", "refresh_containers", "Refresh Containers"), + ] + + def __init__(self): + super().__init__() + self.docker_service = DockerService() + self.color_manager = ColorManager(config.default_colors) + self.main_screen = MainScreen() + + def on_mount(self) -> None: + """Called when the app starts""" + self.push_screen(self.main_screen) + self.set_interval(config.update_interval, self._update_metrics) + self.set_interval(0.1, self._process_logs) + + def _update_metrics(self) -> None: + """ Update the metrics display""" + container_count = self.docker_service.get_container_count() + monitoring = self.docker_service.is_monitoring() + + self.main_screen.refresh_metrics(container_count, monitoring) + + def _process_logs(self) -> None: + """ Process the incoming log messages""" + for container_name, message in self.docker_service.get_log_messages(): + color = self.color_manager.get_color(container_name) + self.main_screen.log_viewer.write_log(container_name, message, color) + + # Setup event handlers + @on(MainScreen.StartMonitoring) + def handle_start_monitoring(self) -> None: + if self.docker_service.start_monitoring(): + logger.info("Started monitoring") + else: + logger.warning("Failed to start monitoring") + + @on(MainScreen.StopMonitoring) + def handle_stop_monitoring(self) -> None: + self.docker_service.stop_monitoring() + logger.info("Stopped Monitoring") + + def action_start_monitoring(self) -> None: + self.handle_start_monitoring() + + def action_stop_monitoring(self) -> None: + self.handle_stop_monitoring() + + def action_clear_logs(self) -> None: + self.main_screen.log_viewer.clear_logs() + logger.info("Cleared logs") + + def action_refresh(self) -> None: + self.handle_refresh_containers() + logger.info("Refreshed container list") diff --git a/dockerLog.py b/src/docker_logs/dockerLog.py similarity index 97% rename from dockerLog.py rename to src/docker_logs/dockerLog.py index 951b1bf..3e6500e 100644 --- a/dockerLog.py +++ b/src/docker_logs/dockerLog.py @@ -1,3 +1,7 @@ +""" +Main app +""" + import docker import random diff --git a/src/docker_logs/screens/__init__.py b/src/docker_logs/screens/__init__.py new file mode 100644 index 0000000..4a5ab39 --- /dev/null +++ b/src/docker_logs/screens/__init__.py @@ -0,0 +1,5 @@ +"""Screens package for DockerLogs app.""" + +from .main_screen import MainScreen + +__all__ = ["MainScreen"] \ No newline at end of file diff --git a/src/docker_logs/screens/main_screen.py b/src/docker_logs/screens/main_screen.py new file mode 100644 index 0000000..2ac8286 --- /dev/null +++ b/src/docker_logs/screens/main_screen.py @@ -0,0 +1,76 @@ +""" Main log viewer screen """ + +from textual.app import Screen +from textual.containers import Vertical, Container +from textual import on +from textual.app import ComposeResult + +from ..widgets.header import Header +from ..widgets.footer import Footer +from ..widgets.log_viewer_widget import LogViewerWidget +from ..widgets.controls_widget import ControlsWidget +from ..widgets.metrics_widget import MetricsWidget + +class MainScreen(Screen): + """Main screen with log viewer, controls and metrics.""" + + CSS = """ + #metrics-container { + height: 3; + border: solid $primary; + } + + #controls-container { + height: 3; + border: solid $secondary; + } + + #logs-container { + border: solid $accent; + } + """ + + def __init__(self): + super().__init__() + self.metrics = MetricsWidget() + self.controls = ControlsWidget() + self.log_viewer = LogViewerWidget() + + def compose(self) -> ComposeResult: + yield Header() + with Container(id="metrics-container"): + yield self.metrics + with Container(id="controls-container"): + yield self.controls + with Container(id="logs-container"): + yield self.log_viewer + yield Footer() + + @on(ControlsWidget.StartMonitoring) + def handle_start_monitoring(self) -> None: + self.post_message(self.StartMonitoring()) + + @on(ControlsWidget.StopMonitoring) + def handle_stop_monitoring(self) -> None: + self.post_message(self.StopMonitoring()) + + @on(ControlsWidget.ClearLogs) + def handle_clear_logs(self) -> None: + self.post_message(self.ClearLogs()) + + @on(ControlsWidget.RefreshContainers) + def handle_refresh_containers(self) -> None: + """Handle refresh containers message.""" + self.post_message(self.RefreshContainers()) + + class StartMonitoring: + pass + + class StopMonitoring: + pass + + class ClearLogs: + pass + + class RefreshContainers: + pass \ No newline at end of file diff --git a/src/docker_logs/services/__init__.py b/src/docker_logs/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/docker_logs/services/docker_service.py b/src/docker_logs/services/docker_service.py new file mode 100644 index 0000000..aa6e30b --- /dev/null +++ b/src/docker_logs/services/docker_service.py @@ -0,0 +1 @@ +""" Docker services """ \ No newline at end of file diff --git a/src/docker_logs/utils/__init__.py b/src/docker_logs/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/docker_logs/utils/colors.py b/src/docker_logs/utils/colors.py new file mode 100644 index 0000000..78c9ac9 --- /dev/null +++ b/src/docker_logs/utils/colors.py @@ -0,0 +1,23 @@ +""" Color utils """ + +import random +from typing import Dict, List + +class ColorManager: + """ Manages color assignments for Docker containers. """ + + def __init__(self, colors: List[str]): + self.colors = colors + self.container_colors: Dict[str, str] = {} + + def random_color(self) -> str: + """Generate a random hex color.""" + return f"#{random.randint(0, 0xFFFFFF):06x}" + + def assign_colors(self, container_names: List[str]) -> None: + """Assign random colors to a list of container names.""" + self.container_colors = {name: self.random_color() for name in container_names} + + def get_color(self, container_name: str) -> str: + """Get the color assigned to a specific container name.""" + return self.container_colors.get(container_name, "#FFFFFF") # Default to white if not found \ No newline at end of file diff --git a/src/docker_logs/utils/config.py b/src/docker_logs/utils/config.py new file mode 100644 index 0000000..0043109 --- /dev/null +++ b/src/docker_logs/utils/config.py @@ -0,0 +1,3 @@ +""" +Config utils +""" \ No newline at end of file diff --git a/src/docker_logs/widgets/__init__.py b/src/docker_logs/widgets/__init__.py new file mode 100644 index 0000000..e6c1490 --- /dev/null +++ b/src/docker_logs/widgets/__init__.py @@ -0,0 +1,15 @@ +"""Widgets package for Docker Logs app.""" + +from .header import Header +from .footer import Footer +from .metrics_widget import MetricsWidget +from .controls_widget import ControlsWidget +from .log_viewer_widget import LogViewerWidget + +__all__ = [ + "Header", + "Footer", + "MetricsWidget", + "ControlsWidget", + "LogViewerWidget" +] \ No newline at end of file diff --git a/src/docker_logs/widgets/controls_widget.py b/src/docker_logs/widgets/controls_widget.py new file mode 100644 index 0000000..14bc8d6 --- /dev/null +++ b/src/docker_logs/widgets/controls_widget.py @@ -0,0 +1,46 @@ +"""Controls widget for Docker Logs app.""" + +from textual.containers import Horizontal +from textual.widgets import Button +from textual import on +from textual.message import Message + +class ControlsWidget(Horizontal): + """Widget with control buttons.""" + + def __init__(self): + super().__init__() + + def compose(self): + yield Button("Start", id="start", variant="success") + yield Button("Stop", id="stop", variant="error") + yield Button("Refresh", id="refresh", variant="primary") + yield Button("Clear", id="clear", variant="warning") + + @on(Button.Pressed, "#start") + def start_monitoring(self) -> None: + self.post_message(self.StartMonitoring()) + + @on(Button.Pressed, "#stop") + def stop_monitoring(self) -> None: + self.post_message(self.StopMonitoring()) + + @on(Button.Pressed, "#refresh") + def refresh_logs(self) -> None: + self.post_message(self.RefreshContainers()) + + @on(Button.Pressed, "#clear") + def clear_logs(self) -> None: + self.post_message(self.ClearLogs()) + + class StartMonitoring(Message): + pass + + class StopMonitoring(Message): + pass + + class RefreshContainers(Message): + pass + + class ClearLogs(Message): + pass \ No newline at end of file diff --git a/src/docker_logs/widgets/footer.py b/src/docker_logs/widgets/footer.py new file mode 100644 index 0000000..af4ce7c --- /dev/null +++ b/src/docker_logs/widgets/footer.py @@ -0,0 +1,9 @@ +"""Footer widget for Docker Logs app.""" + +from textual.widgets import Footer as TextualFooter + +class Footer(TextualFooter): + """Custom footer with keybindings.""" + + def __init__(self): + super().__init__() \ No newline at end of file diff --git a/src/docker_logs/widgets/header.py b/src/docker_logs/widgets/header.py new file mode 100644 index 0000000..69a5c7d --- /dev/null +++ b/src/docker_logs/widgets/header.py @@ -0,0 +1,16 @@ +"""Header Widget for DockerLogs app.""" + +from textual.widgets import Header as TextualHeader +from textual.reactive import reactive + +class Header(TextualHeader): + """Custom header with app title and status.""" + + status = reactive("Stopped") + + def __init__(self): + super().__init__() + self.title = "Docker Log Monitor" + + def watch_status(self, status: str) -> None: + self.sub_title = f"Status: {status}" \ No newline at end of file diff --git a/src/docker_logs/widgets/log_viewer_widget.py b/src/docker_logs/widgets/log_viewer_widget.py new file mode 100644 index 0000000..eb8a8ce --- /dev/null +++ b/src/docker_logs/widgets/log_viewer_widget.py @@ -0,0 +1,21 @@ +"""Log viewer widget.""" + +from textual.widget import RichLog +from textual.reactive import reactive + +class LogViewerWidget(RichLog): + """Widget to display logs.""" + + max_lines = reactive(1000) + + def __init__(self, max_lines: int = 1000): + super().__init__(max_lines=max_lines) + self.max_lines = max_lines + + def write_log(self, container_name: str, message: str, color:str) -> None: + """Write a log message with container name and color.""" + self.write(f"[{color}]{container_name}:[/] {message}") + + def clear_logs(self) -> None: + """Clear all logs.""" + self.clear() \ No newline at end of file diff --git a/src/docker_logs/widgets/metrics_widget.py b/src/docker_logs/widgets/metrics_widget.py new file mode 100644 index 0000000..5d124e0 --- /dev/null +++ b/src/docker_logs/widgets/metrics_widget.py @@ -0,0 +1,37 @@ +"""Metrics Widget for Docker Logs app.""" + +from textual.widget import static +from textual.reactive import reactive +from threading import active_count + +class MetricsWidget(static): + """Widget to display real-time metrics.""" + + thread_count = reactive(0) + container_count = reactive(0) + monitoring_status = reactive("Stopped") + + def __init__(self): + super().__init__() + self.update_display() + + def watch_thread_count(self, count: int) -> None: + self.update_display() + + def watch_container_count(self, count: int) -> None: + self.update_display() + + def watch_monitoring_status(self, status: str) -> None: + self.update_display() + + def update_display(self) -> None: + self.update( + f"[bold]Threads:[/] {self.thread_count} | " + f"[bold]Containers:[/] {self.container_count} | " + f"[bold]Status:[/] {self.monitoring_status}" + ) + + def refresh_metrics(self, container_count: int, monitoring: bool) -> None: + self.thread_count = active_count() + self.container_count = container_count + self.monitoring_status = "Running" if monitoring else "Stopped" \ No newline at end of file