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>
11 KiB
Clide Architecture
Technical documentation covering Clide's architecture, the frameworks it builds on, and implementation patterns.
Table of Contents
- Application Architecture
- Textual TUI Framework
- Pydantic Data Validation
- Extension System
- Testing Strategy
- 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.
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:
Screen {
layout: grid;
grid-size: 3 1;
grid-columns: 20% 1fr 25%;
}
Panels use percentage widths with minimum sizes:
#panel-sidebar {
width: 20%;
min-width: 25;
}
Widget Lifecycle
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:
# 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:
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
Pydantic Data Validation
All data models use Pydantic v2 with strict mode.
Model Configuration
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:
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):
- Environment variables (
CLIDE_THEME=dracula) .envfile- Default values
References
Extension System
Clide uses Pluggy for hook-based extensibility.
Hook Specifications
Hooks define extension points:
# 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:
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:
# 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
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:
[tool.pytest.ini_options]
asyncio_mode = "auto"
Tests can be async without decorators:
async def test_async_operation():
result = await some_async_function()
assert result == expected
Snapshot Testing
Visual regression testing with pytest-textual-snapshot:
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:
pytest tests/snapshots/ --snapshot-update
Mocking
Use AsyncMock for async dependencies:
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
Build and Distribution
Development
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:
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:
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
--onefilefor single executable - Apply
--stripto reduce size - Use UPX compression for further reduction
- Exclude unused modules with
--exclude-module