Skip to content

Commit

Permalink
Upgrade Terminal Component
Browse files Browse the repository at this point in the history
  • Loading branch information
jeckel committed Oct 30, 2024
1 parent 5e0ef63 commit 226878a
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 65 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<project-name>.json` file
1 change: 1 addition & 0 deletions src/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
83 changes: 83 additions & 0 deletions src/presentation/component/terminal.py
Original file line number Diff line number Diff line change
@@ -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__()
67 changes: 18 additions & 49 deletions src/presentation/component/terminal_modal.py
Original file line number Diff line number Diff line change
@@ -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]):
Expand All @@ -17,9 +17,6 @@ class TerminalModal(ModalScreen[bool]):
Horizontal > Label {
padding: 0 1;
}
RichLog {
padding: 1 1;
}
}
.modal_title {
Expand All @@ -44,20 +41,16 @@ def __init__(
self,
command: str | list[str],
path: str,
use_stderr: bool = False,
allow_rerun: bool = False,
**kwargs,
):
super().__init__(**kwargs)
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
Expand All @@ -67,52 +60,28 @@ 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:
self.dismiss(self._result)

@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
1 change: 0 additions & 1 deletion src/presentation/composer/composer_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
)
Expand Down
35 changes: 33 additions & 2 deletions src/presentation/summary/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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,
)
)
1 change: 1 addition & 0 deletions src/services/docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
16 changes: 8 additions & 8 deletions src/tcss/layout.tcss
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 226878a

Please sign in to comment.