diff --git a/README.md b/README.md index 2512791..3807486 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ -- composer install -- composer update -- composer outdated -- composer actions -- +# DxCompanion, enhance your Developer eXperience + +DxCompanion is a Terminal UI application used to provide usefull information about you're project status and easy access +to tools ot manage and monitor your project under development environment. + +## Installation + +- install uv +- git clone + +## Your `.json` file \ No newline at end of file diff --git a/src/models/project.py b/src/models/project.py index c7e0ba6..e976814 100644 --- a/src/models/project.py +++ b/src/models/project.py @@ -14,6 +14,7 @@ class Project(BaseModel): composer: Optional[bool] = Field(default=False) composer_cmd: list[str] = ["composer"] docker_composer_cmd: list[str] = ["docker", "compose"] + actions: dict[str, str] = {} @classmethod def from_json(cls, json_path: str): diff --git a/src/presentation/component/terminal.py b/src/presentation/component/terminal.py new file mode 100644 index 0000000..586dc3c --- /dev/null +++ b/src/presentation/component/terminal.py @@ -0,0 +1,83 @@ +import subprocess +from time import sleep + +from textual.widgets import RichLog +from textual import work, on +from textual.message import Message +from textual.worker import Worker, WorkerState + +class Terminal(RichLog): + DEFAULT_CSS = """ + Terminal { + padding: 1 1; + } + """ + command: list[str] = () + + + def __init__(self, **kwargs): + super().__init__( + highlight=True, + markup=True, + **kwargs) + + + @work(exclusive=True, thread=True) + async def execute(self, command: list[str], path: str) -> None: + self.command = command + self.clear() + self.write(f"Path: [bold blue]{path}[/bold blue]") + self.write(f"Command: [bold blue]{" ".join(command)}[/bold blue]") + self.write( + "----------------------------------------------------------------", + shrink=True, + ) + self.log(f"Running: {command} in {path}") + with subprocess.Popen( + command, + cwd=path, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) as process: + for line in iter(process.stdout.readline, ""): + self.write(line.strip(), shrink=True) + sleep(0.01) + process.wait() + + self.write( + "----------------------------------------------------------------", + shrink=True, + ) + self.write(f"Return code [bold]{process.returncode}[/bold]") + if process.returncode == 0: + self.write("[bold green]Completed![/bold green]") + else: + self.write("[bold red]Completed with errors![/bold red]") + + + @on(Worker.StateChanged) + async def refresh_listview(self, event: Worker.StateChanged) -> None: + if event.state == WorkerState.RUNNING: + self.post_message(self.TerminalStarted(self.command)) + if event.state == WorkerState.SUCCESS: + self.post_message(self.TerminalCompleted(self.command)) + if event.state == WorkerState.CANCELLED or event.state == WorkerState.ERROR: + self.post_message(self.TerminalCompleted(self.command, False)) + + + class TerminalStarted(Message): + def __init__(self, command: list[str]) -> None: + self.command = command + super().__init__() + pass + + + class TerminalCompleted(Message): + """ + Message sent when terminal execution completes + """ + def __init__(self, command: list[str], success: bool = True) -> None: + self.command = command + self.success = success + super().__init__() diff --git a/src/presentation/component/terminal_modal.py b/src/presentation/component/terminal_modal.py index 1c6c635..2a50892 100644 --- a/src/presentation/component/terminal_modal.py +++ b/src/presentation/component/terminal_modal.py @@ -1,10 +1,10 @@ -import subprocess - -from textual import on, work +from textual import on from textual.app import ComposeResult from textual.containers import Container, Horizontal from textual.screen import ModalScreen -from textual.widgets import Button, RichLog, Static +from textual.widgets import Button, Static + +from presentation.component.terminal import Terminal class TerminalModal(ModalScreen[bool]): @@ -17,9 +17,6 @@ class TerminalModal(ModalScreen[bool]): Horizontal > Label { padding: 0 1; } - RichLog { - padding: 1 1; - } } .modal_title { @@ -44,7 +41,6 @@ def __init__( self, command: str | list[str], path: str, - use_stderr: bool = False, allow_rerun: bool = False, **kwargs, ): @@ -52,12 +48,9 @@ def __init__( self.command = command self.path = path self.modal_title = f"Running: {" ".join(self.command)}" - self.use_stderr = use_stderr self.allow_rerun = allow_rerun - self.terminal = RichLog( + self.terminal = Terminal( id="terminal_command", - highlight=True, - markup=True, classes="modal_container", ) self._result = False @@ -67,46 +60,13 @@ def compose(self) -> ComposeResult: with Horizontal(classes="modal_title"): yield Static(self.modal_title) yield self.terminal - with Horizontal(classes="button_container"): + with Horizontal(classes="button_container", id="modal_button_container"): yield Button("Close", id="modal_close") if self.allow_rerun: yield Button.success(" Rerun", id="modal_rerun") def on_mount(self) -> None: - self._start() - - @work(exclusive=True, thread=True) - async def _start(self) -> None: - self.terminal.write(f"Path: [bold blue]{self.path}[/bold blue]") - self.terminal.write(f"Command: [bold blue]{" ".join(self.command)}[/bold blue]") - self.terminal.write( - "----------------------------------------------------------------", - shrink=True, - ) - self.log(f"Running: {self.command} in {self.path}") - with subprocess.Popen( - self.command, - cwd=self.path, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) as process: - stdout, stderr = process.communicate() - if stderr and self.use_stderr: - self.terminal.write(f"[italic]{stderr}[/italic]") - self.terminal.write(stdout) - - self.terminal.write( - "----------------------------------------------------------------", - shrink=True, - ) - self.terminal.write(f"Return code [bold]{process.returncode}[/bold]") - if process.returncode == 0: - self.terminal.write("[bold green]Completed![/bold green]") - self._result = True - else: - self.terminal.write("[bold red]Completed with errors![/bold red]") - self._result = False + self.terminal.execute(command=self.command, path=self.path) @on(Button.Pressed, "#modal_close") def on_close(self, event: Button.Pressed) -> None: @@ -114,5 +74,14 @@ def on_close(self, event: Button.Pressed) -> None: @on(Button.Pressed, "#modal_rerun") def on_rerun(self, event: Button.Pressed) -> None: - self.terminal.clear() - self._start() + self.terminal.execute(command=self.command, path=self.path) + + + @on(Terminal.TerminalCompleted) + def on_terminal_completed(self, event: Terminal.TerminalCompleted) -> None: + self.query_one("#modal_button_container").loading = False + + + @on(Terminal.TerminalStarted) + def on_terminal_started(self, event: Terminal.TerminalStarted) -> None: + self.query_one("#modal_button_container").loading = True diff --git a/src/presentation/composer/composer_container.py b/src/presentation/composer/composer_container.py index c572b28..0af0ecb 100644 --- a/src/presentation/composer/composer_container.py +++ b/src/presentation/composer/composer_container.py @@ -82,7 +82,6 @@ def on_pressed(self, event: Button.Pressed) -> None: TerminalModal( command=["composer", "--no-ansi", event.button.script_name], path=self.project.path, - use_stderr=True, allow_rerun=True, ) ) diff --git a/src/presentation/summary/__init__.py b/src/presentation/summary/__init__.py index dfbe105..fc1df0c 100644 --- a/src/presentation/summary/__init__.py +++ b/src/presentation/summary/__init__.py @@ -1,12 +1,28 @@ -from textual.containers import Container -from textual.widgets import Markdown +from textual.containers import Container, Horizontal +from textual.widgets import Markdown, Button +from textual import on from models import Project +from presentation.component import TerminalModal # service: Service = Provide[Container.service] class ProjectSummaryContainer(Container): BORDER_TITLE = "Project's summary" + DEFAULT_CSS = """ + ProjectSummaryContainer { + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; + Markdown { + height: auto; + } + + #summary-actions { + height: 3; + } + } + """ def __init__(self, project: Project, **kwargs): self.project = project super().__init__(**kwargs) @@ -17,3 +33,18 @@ def compose(self): # Project : {self.project.project_name} """ ) + if len(self.project.actions) > 0: + with Horizontal(id="summary-actions"): + for label in self.project.actions.keys(): + yield Button(label, name=label) + + + @on(Button.Pressed) + def on_pressed(self, event: Button.Pressed) -> None: + self.app.push_screen( + TerminalModal( + command=self.project.actions[event.button.name].split(' '), + path=self.project.path, + allow_rerun=True, + ) + ) \ No newline at end of file diff --git a/src/services/docker_client.py b/src/services/docker_client.py index 757523a..64d753e 100644 --- a/src/services/docker_client.py +++ b/src/services/docker_client.py @@ -14,4 +14,5 @@ def get_running_containers(self) -> list: def get_container_logs(self, container_id): """Fetches logs from a specific container.""" container = self.client.containers.get(container_id) + # return container.logs(follow=True, tail=1000) return container.logs(stream=True, follow=True) diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss index a7480d7..174c86d 100644 --- a/src/tcss/layout.tcss +++ b/src/tcss/layout.tcss @@ -1,14 +1,14 @@ -ProjectSummaryContainer { +# ProjectSummaryContainer { # layout: grid; # grid-size: 2 2; # grid-gutter: 1; - border-title-color: $accent; - border: $primary-background round; - content-align: center middle; - Label { - width: 100%; - } -} +# border-title-color: $accent; +# border: $primary-background round; +# content-align: center middle; +# Label { +# width: 100%; +# } +# } ComposerContainer { Container {