"""Core App class for clitic applications.
This module provides the main App class that extends Textual's App,
adding plugin management and input handling capabilities.
"""
from __future__ import annotations
from collections.abc import Callable
from importlib.resources import files
from typing import TYPE_CHECKING, Any
from textual.app import App as TextualApp
if TYPE_CHECKING:
from clitic.plugins import ContentPlugin
# Type alias for submit handlers
SubmitHandler = Callable[[str], None]
[docs]
class App(TextualApp[Any]):
"""Main application class for clitic.
Extends Textual's App with plugin management and input submission handling.
This is the foundation for building rich CLI applications with clitic.
Args:
title: The title of the application (displayed in title bar).
theme_name: The theme to use for styling (default: "dark").
Example:
```python
from clitic import App
app = App(title="My App")
@app.on_submit
def handle_input(text: str):
print(f"Got: {text}")
app.run()
```
"""
# Load base styles from package
CSS_PATH = str(files("clitic.styles").joinpath("base.tcss"))
[docs]
def __init__(self, title: str = "clitic", theme_name: str = "dark") -> None:
"""Initialize the App.
Args:
title: The title of the application.
theme_name: The theme to use (default: "dark").
"""
super().__init__()
self.title = title
self._theme_name: str = theme_name
self._plugins: list[ContentPlugin] = []
self._submit_handlers: list[SubmitHandler] = []
@property
def theme_name(self) -> str:
"""Return the current theme name."""
return self._theme_name
[docs]
def register_plugin(self, plugin: ContentPlugin) -> None:
"""Register a content plugin with the application.
The plugin's on_register hook is called after registration,
allowing it to initialize with access to the app instance.
Args:
plugin: The ContentPlugin instance to register.
Example:
```python
app = App(title="My App")
plugin = MyCustomPlugin()
app.register_plugin(plugin)
```
"""
self._plugins.append(plugin)
plugin.on_register(self)
[docs]
def unregister_plugin(self, plugin: ContentPlugin) -> None:
"""Unregister a content plugin from the application.
The plugin's on_unregister hook is called before removal,
allowing it to perform cleanup.
Args:
plugin: The ContentPlugin instance to unregister.
"""
plugin.on_unregister(self)
if plugin in self._plugins:
self._plugins.remove(plugin)
[docs]
def on_submit(self, handler: SubmitHandler) -> SubmitHandler:
"""Decorator to register a handler for input submission.
Multiple handlers can be registered; they will be called in
the order they were registered.
Args:
handler: Function to call when input is submitted.
Returns:
The handler function (allows chaining decorators).
Example:
```python
app = App(title="My App")
@app.on_submit
def handle_input(text: str):
print(f"Received: {text}")
```
"""
self._submit_handlers.append(handler)
return handler
def _trigger_submit(self, text: str) -> None:
"""Trigger all registered submit handlers.
This method is called internally when input is submitted.
Args:
text: The submitted input text.
"""
for handler in self._submit_handlers:
handler(text)
[docs]
def get_plugins(self) -> list[ContentPlugin]:
"""Return a copy of the registered plugins list.
Returns:
List of registered ContentPlugin instances.
"""
return self._plugins.copy()
[docs]
def get_plugin_for_content(self, content_type: str, content: str) -> ContentPlugin | None:
"""Get the best plugin for rendering content.
Returns highest-priority plugin matching content_type, or None.
Args:
content_type: MIME type or identifier for the content.
content: The content to potentially render.
Returns:
The matching ContentPlugin with highest priority, or None if no match.
"""
matching_plugins = [
plugin for plugin in self._plugins if plugin.can_render(content_type, content)
]
matching_plugins.sort(key=lambda p: p.priority, reverse=True)
return matching_plugins[0] if matching_plugins else None