Documentation: - Rewrite README.md with current features and git operations - Rewrite docs/ARCHITECTURE.md with layered architecture details - Rewrite docs/tui-ide-spec.md with Alt-key shortcuts - Add docs/code-organization.md for component architecture - Add docs/user-manual.md for end users - Update TODO.md to mark completed items Code fixes: - Fix undefined 'event' variable in diff_pane.py (was _event) - Use ternary operator in editor.py save_file method - Clean up imports in claude_events.py and syntax_service.py - Auto-fix import sorting across multiple files Config: - Add snapshot report path to pyproject.toml pytest options - Exclude clide/vendor from ruff linting - Ignore TCH002/TCH003 type-checking import rules Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
433 lines
11 KiB
Markdown
433 lines
11 KiB
Markdown
# Clide Architecture
|
|
|
|
Technical documentation covering Clide's architecture, the frameworks it builds on, and implementation patterns.
|
|
|
|
## Table of Contents
|
|
|
|
- [Application Architecture](#application-architecture)
|
|
- [Textual TUI Framework](#textual-tui-framework)
|
|
- [Pydantic Data Validation](#pydantic-data-validation)
|
|
- [Extension System](#extension-system)
|
|
- [Testing Strategy](#testing-strategy)
|
|
- [Build and Distribution](#build-and-distribution)
|
|
|
|
---
|
|
|
|
## Application Architecture
|
|
|
|
Clide follows a layered architecture with clear separation between UI, business logic, and data.
|
|
|
|
### Layer Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ ClideApp (app.py) │
|
|
│ Main application, layout, keybindings │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ Widgets Layer │
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ Panels │ │ Components │ │ Themes │ │
|
|
│ │ (layout) │ │ (reusable) │ │ (styling) │ │
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ Controllers Layer │
|
|
│ Business logic, state management │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ Services Layer │
|
|
│ Git, files, scanning, settings, skills │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ Models Layer │
|
|
│ Pydantic data structures │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Data Flow
|
|
|
|
```
|
|
User Action (click, keypress)
|
|
│
|
|
▼
|
|
Widget Event
|
|
│
|
|
▼
|
|
Message Bubbles Up
|
|
│
|
|
▼
|
|
App Event Handler
|
|
│
|
|
▼
|
|
Controller Method
|
|
│
|
|
▼
|
|
Service Call
|
|
│
|
|
▼
|
|
Return Data/Status
|
|
│
|
|
▼
|
|
Update UI State
|
|
│
|
|
▼
|
|
Reactive UI Update
|
|
```
|
|
|
|
### Key Patterns
|
|
|
|
**Message-based communication** — Widgets emit messages that bubble up. Parent widgets or the app handle messages and coordinate responses.
|
|
|
|
**Reactive properties** — UI state uses Textual's `reactive` type. Changes automatically trigger `watch_*` methods.
|
|
|
|
**Background workers** — Long operations use `@work(thread=True)` to avoid blocking the UI.
|
|
|
|
**State preservation** — Hiding panels uses `display: none`, never destroying widgets. All state persists.
|
|
|
|
For detailed code organization, see [Code Organization](code-organization.md).
|
|
|
|
---
|
|
|
|
## Textual TUI Framework
|
|
|
|
Textual provides the foundation for Clide's terminal UI.
|
|
|
|
### Core Concepts
|
|
|
|
**Widgets** — Building blocks of the UI. Everything visible is a widget.
|
|
|
|
**Containers** — Widgets that hold other widgets (Vertical, Horizontal, Container).
|
|
|
|
**Reactive Programming** — State changes trigger automatic UI updates.
|
|
|
|
**CSS Styling** — Layout and appearance defined in CSS, similar to web development.
|
|
|
|
### Layout System
|
|
|
|
Clide uses CSS Grid for the main layout:
|
|
|
|
```css
|
|
Screen {
|
|
layout: grid;
|
|
grid-size: 3 1;
|
|
grid-columns: 20% 1fr 25%;
|
|
}
|
|
```
|
|
|
|
Panels use percentage widths with minimum sizes:
|
|
|
|
```css
|
|
#panel-sidebar {
|
|
width: 20%;
|
|
min-width: 25;
|
|
}
|
|
```
|
|
|
|
### Widget Lifecycle
|
|
|
|
```python
|
|
class MyWidget(Widget):
|
|
def __init__(self):
|
|
super().__init__()
|
|
# Initialize instance variables
|
|
|
|
def compose(self) -> ComposeResult:
|
|
# Yield child widgets
|
|
yield Label("Hello")
|
|
|
|
def on_mount(self) -> None:
|
|
# Called after widget is added to DOM
|
|
# Safe to query other widgets here
|
|
|
|
def on_unmount(self) -> None:
|
|
# Cleanup when removed
|
|
```
|
|
|
|
### Event Handling
|
|
|
|
Events bubble up through the widget tree:
|
|
|
|
```python
|
|
# Define a message
|
|
class FileSelected(Message):
|
|
def __init__(self, path: Path):
|
|
self.path = path
|
|
super().__init__()
|
|
|
|
# Emit the message
|
|
self.post_message(self.FileSelected(path))
|
|
|
|
# Handle in parent (naming convention: on_<widget>_<message>)
|
|
def on_files_view_file_selected(self, event: FilesView.FileSelected):
|
|
self.open_file(event.path)
|
|
```
|
|
|
|
### Background Tasks
|
|
|
|
Use `@work` for operations that shouldn't block the UI:
|
|
|
|
```python
|
|
from textual import work
|
|
|
|
@work(thread=True)
|
|
def fetch_data(self) -> dict:
|
|
"""Runs in thread pool."""
|
|
result = expensive_operation()
|
|
return result
|
|
|
|
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
if event.state == WorkerState.SUCCESS:
|
|
self.update_ui(event.worker.result)
|
|
```
|
|
|
|
### References
|
|
|
|
- [Textual Documentation](https://textual.textualize.io/)
|
|
- [Textual Widgets](https://textual.textualize.io/widgets/)
|
|
- [Textual CSS](https://textual.textualize.io/guide/CSS/)
|
|
|
|
---
|
|
|
|
## Pydantic Data Validation
|
|
|
|
All data models use Pydantic v2 with strict mode.
|
|
|
|
### Model Configuration
|
|
|
|
```python
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
class GitChange(BaseModel):
|
|
model_config = ConfigDict(strict=True, frozen=True)
|
|
|
|
path: str
|
|
status: Literal["added", "modified", "deleted"]
|
|
staged: bool
|
|
```
|
|
|
|
**strict=True** — No type coercion. `"123"` won't become `123`.
|
|
|
|
**frozen=True** — Immutable instances. Enables hashing for use as dict keys.
|
|
|
|
### Settings Management
|
|
|
|
Application settings use `pydantic-settings`:
|
|
|
|
```python
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
class ClideSettings(BaseSettings):
|
|
model_config = SettingsConfigDict(
|
|
env_prefix="CLIDE_",
|
|
env_file=".env",
|
|
)
|
|
|
|
theme: str = "summer-night"
|
|
jira_enabled: bool = False
|
|
```
|
|
|
|
Settings load from (in priority order):
|
|
1. Environment variables (`CLIDE_THEME=dracula`)
|
|
2. `.env` file
|
|
3. Default values
|
|
|
|
### References
|
|
|
|
- [Pydantic Documentation](https://docs.pydantic.dev/latest/)
|
|
- [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
|
|
|
|
---
|
|
|
|
## Extension System
|
|
|
|
Clide uses Pluggy for hook-based extensibility.
|
|
|
|
### Hook Specifications
|
|
|
|
Hooks define extension points:
|
|
|
|
```python
|
|
# clide/extensions/hookspecs.py
|
|
import pluggy
|
|
|
|
hookspec = pluggy.HookspecMarker("clide")
|
|
hookimpl = pluggy.HookimplMarker("clide")
|
|
|
|
class ClideHookSpec:
|
|
@hookspec
|
|
def clide_startup(self, app: App) -> None:
|
|
"""Called when the app starts."""
|
|
|
|
@hookspec
|
|
def clide_on_file_changed(self, event: FileEvent) -> None:
|
|
"""Called when a file changes."""
|
|
```
|
|
|
|
### Implementing Hooks
|
|
|
|
Extensions implement hooks with the `@hookimpl` decorator:
|
|
|
|
```python
|
|
from clide.extensions import hookimpl
|
|
|
|
class MyExtension:
|
|
@hookimpl
|
|
def clide_startup(self, app: App) -> None:
|
|
app.notify("Extension loaded!")
|
|
|
|
@hookimpl
|
|
def clide_on_file_changed(self, event: FileEvent) -> None:
|
|
if event.path.suffix == ".py":
|
|
# React to Python file changes
|
|
pass
|
|
```
|
|
|
|
### Distribution
|
|
|
|
Extensions can be packaged and distributed via entry points:
|
|
|
|
```toml
|
|
# pyproject.toml of extension package
|
|
[project.entry-points."clide.extensions"]
|
|
my_extension = "my_package:MyExtension"
|
|
```
|
|
|
|
### Available Hooks
|
|
|
|
| Hook | When Called |
|
|
|------|-------------|
|
|
| `clide_startup` | App initialization |
|
|
| `clide_shutdown` | App cleanup |
|
|
| `clide_on_file_changed` | File created/modified/deleted |
|
|
| `clide_on_file_saved` | File saved in editor |
|
|
|
|
### References
|
|
|
|
- [Pluggy Documentation](https://pluggy.readthedocs.io/)
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Test Organization
|
|
|
|
```
|
|
tests/
|
|
├── unit/ # Isolated component tests
|
|
├── integration/ # Component interaction tests
|
|
└── snapshots/ # Visual regression tests
|
|
```
|
|
|
|
### Async Testing
|
|
|
|
Configure pytest-asyncio in auto mode:
|
|
|
|
```toml
|
|
[tool.pytest.ini_options]
|
|
asyncio_mode = "auto"
|
|
```
|
|
|
|
Tests can be async without decorators:
|
|
|
|
```python
|
|
async def test_async_operation():
|
|
result = await some_async_function()
|
|
assert result == expected
|
|
```
|
|
|
|
### Snapshot Testing
|
|
|
|
Visual regression testing with pytest-textual-snapshot:
|
|
|
|
```python
|
|
def test_layout(snap_compare):
|
|
assert snap_compare(ClideApp(), terminal_size=(120, 40))
|
|
|
|
def test_with_interaction(snap_compare):
|
|
async def setup(pilot):
|
|
await pilot.press("tab", "enter")
|
|
|
|
assert snap_compare(ClideApp(), run_before=setup)
|
|
```
|
|
|
|
Update snapshots after intentional changes:
|
|
|
|
```bash
|
|
pytest tests/snapshots/ --snapshot-update
|
|
```
|
|
|
|
### Mocking
|
|
|
|
Use `AsyncMock` for async dependencies:
|
|
|
|
```python
|
|
from unittest.mock import AsyncMock
|
|
|
|
async def test_with_mock():
|
|
mock_service = AsyncMock(return_value={"status": "ok"})
|
|
result = await mock_service()
|
|
assert result["status"] == "ok"
|
|
```
|
|
|
|
### References
|
|
|
|
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/)
|
|
- [pytest-textual-snapshot](https://github.com/Textualize/pytest-textual-snapshot)
|
|
- [Textual Testing Guide](https://textual.textualize.io/guide/testing/)
|
|
|
|
---
|
|
|
|
## Build and Distribution
|
|
|
|
### Development
|
|
|
|
```bash
|
|
make setup # Create venv, install deps
|
|
make run # Run application
|
|
make test # Run all tests
|
|
make typecheck # Run mypy
|
|
make lint # Run ruff
|
|
make format # Format code
|
|
```
|
|
|
|
### PyInstaller
|
|
|
|
Build standalone executables:
|
|
|
|
```bash
|
|
pip install -e ".[build]"
|
|
pyinstaller clide.spec --clean
|
|
```
|
|
|
|
**Important**: PyInstaller cannot cross-compile. Build on each target platform.
|
|
|
|
### CI/CD
|
|
|
|
Multi-platform builds via Gitea Actions:
|
|
|
|
```yaml
|
|
jobs:
|
|
build-linux:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.12'
|
|
- run: pip install -e ".[build]"
|
|
- run: pyinstaller clide.spec --clean
|
|
|
|
build-macos:
|
|
runs-on: macos-latest
|
|
# ... same steps
|
|
```
|
|
|
|
### Optimization
|
|
|
|
- Use `--onefile` for single executable
|
|
- Apply `--strip` to reduce size
|
|
- Use UPX compression for further reduction
|
|
- Exclude unused modules with `--exclude-module`
|
|
|
|
### References
|
|
|
|
- [PyInstaller Documentation](https://pyinstaller.org/)
|
|
- [Gitea Actions](https://docs.gitea.com/usage/actions/overview)
|