Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions layout.py
Original file line number Diff line number Diff line change
@@ -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()
Empty file added main.py
Empty file.
Empty file added requirements.txt
Empty file.
Empty file added setup.py
Empty file.
81 changes: 81 additions & 0 deletions src/docker_logs/app.py
Original file line number Diff line number Diff line change
@@ -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
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempting to import DockerService from a file that only contains a docstring comment. The docker_service.py file needs to implement the DockerService class before this import will work.

Copilot uses AI. Check for mistakes.
from .utils.colors import ColorManager
from .utils.config import config
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing config from config.py, but that file only contains a docstring with no config object defined. This will result in an ImportError.

Copilot uses AI. Check for mistakes.

Comment on lines +12 to +13
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempting to import config from a file that only contains a docstring comment. The config.py file needs to define the config object with attributes like 'default_colors' and 'update_interval' before this import will work.

Suggested change
from .utils.config import config
try:
from .utils.config import config
except ImportError:
from types import SimpleNamespace
config = SimpleNamespace(
default_colors=["red", "green", "blue", "yellow", "magenta", "cyan"],
update_interval=1.0
)

Copilot uses AI. Check for mistakes.
# 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)
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempting to access config.default_colors, but config is not defined in utils/config.py. This will cause an AttributeError at runtime.

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempting to access config.update_interval, but config is not defined in utils/config.py. This will cause an AttributeError at runtime.

Copilot uses AI. Check for mistakes.
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()
Comment on lines +45 to +46
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling methods on docker_service, but DockerService class is not defined. These method calls will fail at runtime.

Copilot uses AI. Check for mistakes.

self.main_screen.refresh_metrics(container_count, monitoring)
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MainScreen class doesn't have a 'refresh_metrics' method. Based on the structure, this should likely call 'self.main_screen.metrics.refresh_metrics(container_count, monitoring)'.

Suggested change
self.main_screen.refresh_metrics(container_count, monitoring)
self.main_screen.metrics.refresh_metrics(container_count, monitoring)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling refresh_metrics on main_screen, but MainScreen class has no such method defined. This will result in an AttributeError.

Copilot uses AI. Check for mistakes.

def _process_logs(self) -> None:
""" Process the incoming log messages"""
for container_name, message in self.docker_service.get_log_messages():
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling get_log_messages on docker_service, but this method is not defined in the DockerService class. This will fail at runtime.

Copilot uses AI. Check for mistakes.
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():
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling start_monitoring on docker_service, but this method is not defined. This will fail at runtime.

Copilot uses AI. Check for mistakes.
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()
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling stop_monitoring on docker_service, but this method is not defined. This will fail at runtime.

Copilot uses AI. Check for mistakes.
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")

Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method calls 'self.handle_refresh_containers()' which is not defined in this class. The method should either be implemented or this should post a RefreshContainers message.

Suggested change
def handle_refresh_containers(self) -> None:
"""Refresh the list of containers and update the UI."""
containers = self.docker_service.get_containers()
self.main_screen.refresh_container_list(containers)

Copilot uses AI. Check for mistakes.
def action_refresh(self) -> None:
self.handle_refresh_containers()
Comment on lines +79 to +80
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method handle_refresh_containers is called but not defined in the DockerLogsApp class. This will result in an AttributeError.

Copilot uses AI. Check for mistakes.
logger.info("Refreshed container list")
4 changes: 4 additions & 0 deletions dockerLog.py β†’ src/docker_logs/dockerLog.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Main app
"""

import docker
import random

Expand Down
5 changes: 5 additions & 0 deletions src/docker_logs/screens/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Screens package for DockerLogs app."""

from .main_screen import MainScreen

__all__ = ["MainScreen"]
Comment on lines +3 to +5
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement references 'MainScreen' class which doesn't exist in main_screen.py. The file only contains an import of textual.app.App.

Suggested change
from .main_screen import MainScreen
__all__ = ["MainScreen"]
__all__ = []

Copilot uses AI. Check for mistakes.
76 changes: 76 additions & 0 deletions src/docker_logs/screens/main_screen.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +66 to +76
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These message classes don't inherit from textual.message.Message. They should inherit from Message to work properly with Textual's message handling system, similar to how they're defined in ControlsWidget.

Copilot uses AI. Check for mistakes.
Empty file.
1 change: 1 addition & 0 deletions src/docker_logs/services/docker_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
""" Docker services """
Empty file.
23 changes: 23 additions & 0 deletions src/docker_logs/utils/colors.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions src/docker_logs/utils/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Config utils
"""
15 changes: 15 additions & 0 deletions src/docker_logs/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace after 'Footer', - this line has inconsistent whitespace compared to other entries in the list.

Suggested change
"Footer",
"Footer",

Copilot uses AI. Check for mistakes.
"MetricsWidget",
"ControlsWidget",
"LogViewerWidget"
]
46 changes: 46 additions & 0 deletions src/docker_logs/widgets/controls_widget.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions src/docker_logs/widgets/footer.py
Original file line number Diff line number Diff line change
@@ -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__()
16 changes: 16 additions & 0 deletions src/docker_logs/widgets/header.py
Original file line number Diff line number Diff line change
@@ -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}"
21 changes: 21 additions & 0 deletions src/docker_logs/widgets/log_viewer_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Log viewer widget."""

from textual.widget import RichLog
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect import path for RichLog. The correct import should be from textual.widgets import RichLog (plural 'widgets', not singular 'widget').

Suggested change
from textual.widget import RichLog
from textual.widgets import RichLog

Copilot uses AI. Check for mistakes.
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:
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after colon in the type annotation for 'color' parameter. Should be 'color: str' to conform to PEP 8 style guidelines.

Suggested change
def write_log(self, container_name: str, message: str, color:str) -> None:
def write_log(self, container_name: str, message: str, color: str) -> None:

Copilot uses AI. Check for mistakes.
"""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()
37 changes: 37 additions & 0 deletions src/docker_logs/widgets/metrics_widget.py
Original file line number Diff line number Diff line change
@@ -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):
Comment on lines +3 to +7
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement is incorrect. Textual's Static widget should be imported with a capital 'S': from textual.widgets import Static. The current lowercase 'static' will cause an ImportError.

Suggested change
from textual.widget import static
from textual.reactive import reactive
from threading import active_count
class MetricsWidget(static):
from textual.widgets import Static
from textual.reactive import reactive
from threading import active_count
class MetricsWidget(Static):

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +7
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class inherits from 'static' (lowercase), but should inherit from 'Static' (capitalized). This is a consequence of the incorrect import on line 3 and will cause a NameError.

Suggested change
from textual.widget import static
from textual.reactive import reactive
from threading import active_count
class MetricsWidget(static):
from textual.widget import Static
from textual.reactive import reactive
from threading import active_count
class MetricsWidget(Static):

Copilot uses AI. Check for mistakes.
"""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"