Skip to content

Commit

Permalink
Project configuration and upgrade Terminal widget (#6)
Browse files Browse the repository at this point in the history
* Use basic project configuration file

* Refactor TabbedContents

* Upgrade Terminal Component

* Update design
  • Loading branch information
jeckel authored Oct 30, 2024
1 parent d78c5bf commit e5b9f38
Show file tree
Hide file tree
Showing 21 changed files with 334 additions and 219 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
3 changes: 3 additions & 0 deletions projects/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!.gitignore
!.project-sample.json
33 changes: 0 additions & 33 deletions src/composer_utils.py

This file was deleted.

31 changes: 5 additions & 26 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import typer
from pydantic_core._pydantic_core import ValidationError
from rich import print
from dependency_injector import providers

from composer_utils import composer_updatable
from service_locator import Container
from models import Project
from presentation import MainApp
Expand All @@ -12,32 +10,13 @@

@app.command()
def tui(project_path: str) -> None:
try:
project = Project(path=project_path)
print(f"Composer present: {project.composer}")
except ValidationError as e:
print("Validation error:", e)
exit(1)

print(f"Launch tui for {project.name} project")
app = MainApp(project)
app.run()


@app.command()
def debug(project_path: str) -> None:
try:
project = Project(path=project_path)
print(f"Composer present: {project.composer}")
except ValidationError as e:
print("Validation error:", e)
exit(1)

print(composer_updatable(project))
project = Project.from_json(json_path=project_path)
Container()
tui_app = MainApp(project)
tui_app.run()


def main() -> None:
Container()
app()


Expand Down
21 changes: 15 additions & 6 deletions src/models/project.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
from functools import cached_property
from typing import Optional
Expand All @@ -9,13 +10,21 @@

class Project(BaseModel):
path: str
project_name: Optional[str] = None
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):
with open(json_path, "r") as file:
data = json.load(file)
return cls(**data)

@cached_property
def name(self) -> str:
return os.path.basename(self.path)
return self.project_name or os.path.basename(self.path)

@field_validator("path", mode="before")
def check_directory_exists(cls, v) -> str:
Expand All @@ -31,8 +40,8 @@ def check_composer_file(self):
self.composer = True
return self

@cached_property
def composer_json(self) -> Optional[Composer]:
if not self.composer:
return None
return Composer.from_json(self.path)
# @cached_property
# def composer_json(self) -> Optional[Composer]:
# if not self.composer:
# return None
# return Composer.from_json(self.path)
18 changes: 11 additions & 7 deletions src/presentation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, TabbedContent
from textual.widgets import Footer, Header, TabbedContent, TabPane

from models import Project

from .composer import ComposerPan
from .docker import DockerPan
from .summary import ProjectSummaryPan
from .composer import ComposerContainer
from .docker import DockerContainer
from .summary import ProjectSummaryContainer


class MainApp(App):
Expand All @@ -21,11 +21,15 @@ class MainApp(App):
def __init__(self, project: Project):
self._project = project
super().__init__()
self.title = f"DX Companion - {project.name}"

def compose(self) -> ComposeResult:
yield Header()
with TabbedContent(initial="summary-pan"):
yield ProjectSummaryPan(project=self._project)
yield ComposerPan(project=self._project)
yield DockerPan(project=self._project)
with TabPane(title="Summary", id="summary-pan"):
yield ProjectSummaryContainer(project=self._project)
with TabPane(title="Composer", id="composer-pan"):
yield ComposerContainer(project=self._project)
with TabPane(title="Docker", id="docker-pan"):
yield DockerContainer(project=self._project)
yield Footer()
1 change: 1 addition & 0 deletions src/presentation/component/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .terminal_modal import TerminalModal
from .terminal import Terminal
90 changes: 90 additions & 0 deletions src/presentation/component/terminal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import subprocess
from time import sleep

from textual.widgets import RichLog
from textual import on
from textual.message import Message
from textual.worker import Worker, WorkerState


class Terminal(RichLog):
DEFAULT_CSS = """
Terminal {
padding: 1 1;
}
"""
command: list[str] = []
current_worker: Worker | None = None

def __init__(self, **kwargs):
super().__init__(highlight=True, markup=True, **kwargs)

def execute(self, command: list[str], path: str) -> None:
self.command = command
self.current_worker = self.run_worker(
self._execute(command, path), exclusive=True, thread=True
)

def is_running(self) -> bool:
return self.current_worker is not None and self.current_worker.is_running

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:
assert process.stdout is not None
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 worker_state_changed(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):
"""
Message sent when terminal execution starts
"""

def __init__(self, command: list[str]) -> None:
self.command = command
super().__init__()

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: 17 additions & 50 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 .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 @@ -42,22 +39,18 @@ class TerminalModal(ModalScreen[bool]):

def __init__(
self,
command: str | list[str],
command: 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,26 @@ 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
5 changes: 1 addition & 4 deletions src/presentation/composer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
# from .composer_packages_table import ComposerPackagesTable
from .composer_pan import ComposerPan

# from .composer_script_button import ComposerScriptButton
from .composer_container import ComposerContainer
Loading

0 comments on commit e5b9f38

Please sign in to comment.