Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add composer panel on project summary, and add an application context manager #8

Merged
merged 4 commits into from
Nov 4, 2024
Merged
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
5 changes: 2 additions & 3 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import typer
from dependency_injector import providers

from service_locator import ServiceContainer
from models import Project
Expand All @@ -10,9 +9,9 @@

@app.command()
def tui(project_path: str) -> None:
project = Project.from_json(json_path=project_path)
ServiceContainer()
tui_app = MainApp(project)
ServiceContainer.context().project = Project.from_json(json_path=project_path)
tui_app = MainApp()
tui_app.run()


Expand Down
18 changes: 18 additions & 0 deletions src/models/app_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Optional

from pydantic import BaseModel

from models import Project


class AppContext(BaseModel):
project: Optional[Project] = None

# Composer related context
composer_updatable_packages: Optional[dict[str, str]] = None

@property
def current_project(self) -> Project:
if self.project is None:
raise RuntimeError("No project set in AppContext")
return self.project
2 changes: 2 additions & 0 deletions src/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
from functools import cached_property
from typing import Optional
from uuid import uuid4, UUID

from pydantic import BaseModel, Field, field_validator, model_validator

Expand All @@ -14,6 +15,7 @@ class ProjectAction(BaseModel):


class Project(BaseModel):
id_: UUID = Field(default_factory=uuid4)
path: str
project_name: Optional[str] = None
composer: Optional[bool] = Field(default=False)
Expand Down
40 changes: 18 additions & 22 deletions src/presentation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, TabbedContent, TabPane
from textual.containers import Container
from textual.app import App
from textual.css.query import NoMatches

from models import Project
from service_locator import ServiceContainer
from .component.message import TerminalCommandRequested

from .composer import ComposerContainer, ComposerCommandRequested
from .composer import ComposerCommandRequested
from .composer.composer_screen import ComposerScreen
from .docker import DockerContainer
from .summary import ProjectSummaryContainer
from .component import Sidebar, TerminalModal, NonShellCommand
from .summary.summary_screen import SummaryScreen


class MainApp(App[None]):
"""A Textual app to manage stopwatches."""

TITLE = "DX Companion"
BINDINGS = [
("d", "toggle_dark", "Toggle dark mode"),
Expand All @@ -26,33 +26,29 @@ class MainApp(App[None]):
}
"""
CSS_PATH = "../tcss/layout.tcss"
SCREENS = {"summary": SummaryScreen, "composer": ComposerScreen}

_project: Project

def __init__(self, project: Project):
self._project = project
def __init__(self):
super().__init__()
self.title = f"DX Companion - {project.name}"
self._project = ServiceContainer.context().current_project
self.title = f"DX Companion - {self._project.name}"

def compose(self) -> ComposeResult:
yield Sidebar(project=self._project, classes="-hidden")
yield Header()
with TabbedContent(initial="summary-pan"):
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()
def on_mount(self) -> None:
self.push_screen("summary")

def action_toggle_sidebar(self) -> None:
self.query_one(Sidebar).toggle_class("-hidden")
try:
self.query_one(Sidebar).toggle_class("-hidden")
except NoMatches:
pass

@on(ComposerCommandRequested)
def action_composer_script(self, event: ComposerCommandRequested) -> None:
def refresh_composer(result: bool | None):
if event.refresh_composer_on_success and result:
self.query_one(ComposerContainer).action_refresh()
ServiceContainer.composer_client().reset_updatable_packages()

self.query_one(Sidebar).add_class("-hidden")
self.app.push_screen(
Expand Down
18 changes: 7 additions & 11 deletions src/presentation/component/sidebar.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from textual.app import ComposeResult
from textual.containers import Container
from textual.widgets import OptionList
from textual.widgets.option_list import Option, Separator


from models import Project
from presentation.component.action_option_list import ActionOptionList
from presentation.composer.composer_script_option_list import ComposerScriptOptionList
from service_locator import ServiceContainer
Expand All @@ -25,20 +21,20 @@ class Sidebar(Container):
}
"""

def __init__(self, project: Project, **kwargs):
self.project = project
def __init__(self, **kwargs):
self._project = ServiceContainer.context().current_project
super().__init__(**kwargs)
self.add_class("-hidden")

def compose(self) -> ComposeResult:

if len(ServiceContainer.composer_client().scripts(self.project)) > 0:
yield ComposerScriptOptionList(self.project)
if self.project.actions is None:
if len(ServiceContainer.composer_client().scripts(self._project)) > 0:
yield ComposerScriptOptionList(self._project)
if self._project.actions is None:
return
for action_group, actions in self.project.actions.items():
for action_group, actions in self._project.actions.items():
if len(actions) > 0:
yield ActionOptionList(
project=self.project, actions=actions, group_name=action_group
project=self._project, actions=actions, group_name=action_group
)
# yield ActionOptionList(project=self.project)
16 changes: 8 additions & 8 deletions src/presentation/composer/composer_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from textual.widgets import Button
from textual.worker import Worker, WorkerState

from models import Project
from models.composer import Composer
from service_locator import ServiceContainer

Expand All @@ -29,9 +28,9 @@ class ComposerContainer(Container):
}
"""

def __init__(self, project: Project, **kwargs):
self.project = project
self.composer = Composer.from_json(project.path)
def __init__(self, **kwargs):
self._project = ServiceContainer.context().current_project
self.composer = Composer.from_json(self._project.path)
super().__init__(**kwargs)

def compose(self) -> ComposeResult:
Expand All @@ -58,19 +57,20 @@ def action_refresh(self) -> None:
self._load_composer()

async def on_mount(self):
self.action_refresh()
self.loading = True
self._load_composer()

@work(exclusive=True, thread=True)
async def _load_composer(self) -> dict[str, str]:
return ServiceContainer.composer_client().updatable_packages(self.project)
return ServiceContainer.composer_client().updatable_packages()

@on(Worker.StateChanged)
async def refresh_listview(self, event: Worker.StateChanged) -> None:
async def refresh_packages(self, event: Worker.StateChanged) -> None:
"""Called when the worker state changes."""
if event.state != WorkerState.SUCCESS:
return
packages_updatable = event.worker.result
composer = ServiceContainer.composer_client().composer_json(self.project)
composer = ServiceContainer.composer_client().composer_json(self._project)
package_table: ComposerPackagesTable = self.query_one(
"#composer-packages-table"
)
Expand Down
27 changes: 27 additions & 0 deletions src/presentation/composer/composer_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from textual import on
from textual.app import ComposeResult
from textual.events import ScreenResume
from textual.screen import Screen
from textual.widgets import Header, Footer

from .composer_container import ComposerContainer
from presentation.component.sidebar import Sidebar


class ComposerScreen(Screen):
BINDINGS = {
("escape", "return", "Return to project"),
}

def compose(self) -> ComposeResult:
yield Sidebar(classes="-hidden")
yield Header()
yield ComposerContainer()
yield Footer()

def action_return(self):
self.app.switch_screen("summary")

@on(ScreenResume)
def screen_resume(self):
self.query_one(ComposerContainer).action_refresh()
31 changes: 1 addition & 30 deletions src/presentation/summary/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1 @@
from textual.containers import Container
from textual.widgets import Markdown

from models import Project


# 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;
}
}
"""

def __init__(self, project: Project, **kwargs):
self.project = project
super().__init__(**kwargs)

def compose(self):
yield Markdown(
f"""
# Project : {self.project.project_name}
"""
)
from .summary_container import ProjectSummaryContainer
112 changes: 112 additions & 0 deletions src/presentation/summary/composer_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from typing import Optional

from rich.style import Style
from rich.table import Table
from textual import work, on
from textual.app import ComposeResult
from textual.containers import Container
from textual.widgets import Static, Button
from textual.worker import Worker, WorkerState

from models.composer import Composer
from presentation.composer.composer_screen import ComposerScreen
from service_locator import ServiceContainer


class ComposerCard(Container):
DEFAULT_CSS = """
ComposerCard {
height: auto;
width: 45;
border: $primary-background round;

Button, Button:focus, Button:hover {
height: 1;
border: none;
width: 100%;
margin-top: 1;
}
}
"""

_composer_config: Optional[Composer] = None
_packages_updatable: dict[str, str] = {}

def __init__(self, **kwargs):
super().__init__(**kwargs)
self._project = ServiceContainer.context().current_project
self._composer_panel = Static(id="composer_panel")

def compose(self) -> ComposeResult:
yield self._composer_panel
yield Button("[underline]Manage packages", id="toggle_composer_tab")

def on_mount(self) -> None:
self._composer_config = ServiceContainer.composer_client().composer_json(
self._project
)
self._composer_panel.update(self.get_composer_panel())
self.query_one(Button).loading = True
self._load_composer()

def get_composer_panel(self) -> Table:
table = Table(
show_header=False,
box=None,
title="Composer status",
title_style=Style(color="#bbc8e8", bold=True),
)
table.add_column()
table.add_column(min_width=25, max_width=27)
table.add_row(
"[label]Composer:",
"[green]Enabled" if self._project.composer else "[red]Disabled",
)

if self._project.composer and self._composer_config is not None:
updatable_packages_keys = self._packages_updatable.keys()
updatable_packages = len(
set(updatable_packages_keys) & set(self._packages_updatable.keys())
)
if updatable_packages > 0:
table.add_row(
"[label]Packages:",
f"{len(self._composer_config.required_packages)} ([orange1]{updatable_packages} updates available[/orange1])",
)
else:
table.add_row(
"[label]Packages:",
f"{len(self._composer_config.required_packages)}",
)

updatable_packages_dev = len(
set(updatable_packages_keys) & set(self._packages_updatable.keys())
)
if updatable_packages_dev > 0:
table.add_row(
"[label]Packages-dev:",
f"{len(self._composer_config.required_packages_dev)} "
f"([orange1]{updatable_packages_dev} updates available[/orange1])",
)
else:
table.add_row(
"[label]Packages-dev:",
f"{len(self._composer_config.required_packages_dev)}",
)
return table

@work(exclusive=True, thread=True)
async def _load_composer(self, no_cache: bool = False) -> dict[str, str]:
return ServiceContainer.composer_client().updatable_packages()

@on(Worker.StateChanged)
async def refresh_listview(self, event: Worker.StateChanged) -> None:
if event.state != WorkerState.SUCCESS:
return
self._packages_updatable = event.worker.result
self._composer_panel.update(self.get_composer_panel())
self.query_one(Button).loading = False

@on(Button.Pressed, "#toggle_composer_tab")
def on_composer_manage(self):
self.app.push_screen(ComposerScreen())
Loading
Loading