Set up the TUI IDE wrapper for Claude Code CLI with: - Core app structure using Textual framework - Panel architecture (sidebar, workspace, claude, context) - Theme system with 22 built-in themes (Summer Night default) - Pydantic models for configuration and data - Makefile for development commands - Project documentation and specs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
10 KiB
Clide Architecture Documentation
Comprehensive documentation of architecture patterns, best practices, and implementation guidelines.
Table of Contents
- Textual TUI Framework
- Typer CLI Framework
- Pydantic Data Validation
- Extension System
- Testing Strategy
- Build and Distribution
Textual TUI Framework
Textual models TUIs as a reactive tree of widgets, similar to React's component tree but grid-based on character cells.
Key Concepts
Widgets and Containers
- Widgets are the building blocks of the UI
- Containers are widgets that hold other widgets
- Default layout stacks widgets vertically from top of screen
Reactive Programming
- State changes trigger automatic UI updates
- No manual refresh loops needed
- Use reactive attributes for state management
Event-Driven Model
- Define callbacks for key presses, mouse clicks, timer ticks
- Actions are functions callable via keystroke or text link
Best Practices
-
Use Immutable Objects
- Prefer tuples, NamedTuples, or frozen dataclasses
- Easier to reason about, cache, and test
- Enables side-effect-free code
-
Separate Styles
- Keep CSS in
.tcssfiles, not inline - Python code stays clean and focused on logic
- Keep CSS in
-
Async-First
- Textual is async under the hood
- Use
async/awaitfor I/O operations - Can integrate with async libraries if needed
Layout Management
# Grid layout example
CSS = """
Screen {
layout: grid;
grid-size: 3 1;
grid-columns: 1fr 2fr 1fr;
}
"""
References
Typer CLI Framework
Typer is built on Click with Python type hints for automatic argument parsing.
Project Structure Pattern
app/
├── __init__.py
├── main.py # Root Typer app
├── commands/ # Subcommand modules
│ ├── users.py
│ └── tasks.py
└── helpers/ # Shared utilities
└── validate.py
Best Practices
-
Organize Commands
- Use
add_typer()to group commands - Avoid giant files with dozens of commands
- Each command function should orchestrate, not contain all logic
- Use
-
Entry Point Support
- Add
__main__.pyforpython -msupport - Define entry points in pyproject.toml for CLI scripts
- Add
-
Standard Exit Codes
0for success- Non-zero for errors
- Crucial for CI/CD integration
-
Type Hints for Validation
- Use Enum for dropdown-style restrictions
- Type hints provide editor autocompletion
Subcommand Example
# commands/users.py
import typer
app = typer.Typer()
@app.command()
def create(name: str):
"""Create a new user."""
...
# main.py
from commands import users
main_app = typer.Typer()
main_app.add_typer(users.app, name="users")
References
Pydantic Data Validation
Pydantic v2 with strict mode ensures type safety and validation.
Strict Mode Configuration
from pydantic import BaseModel, ConfigDict
class MyModel(BaseModel):
model_config = ConfigDict(strict=True, frozen=True)
name: str
count: int # Will reject "123" string
Settings Management
Settings have moved to pydantic-settings package:
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
env_file=".env",
env_nested_delimiter="__",
)
database_url: str
debug: bool = False
Best Practices
-
Use
frozen=Truefor Immutability- Prevents accidental mutation
- Enables hashing for use as dict keys
-
Explicit Strict Types
StrictInt,StrictStrfor field-level strictness- Or use
model_configfor model-wide strictness
-
Validation vs Parsing
- Strict mode rejects type coercion
- JSON parsing allows some conversion (ISO8601 → datetime)
References
Extension System
The plugin system uses Pluggy for hook-based extensibility.
Pluggy Concepts
- Hook Specifications - Define the interface extensions implement
- Hook Implementations - Extension code implementing hooks
- Plugin Manager - Discovers and calls implementations
Architecture
# hookspecs.py - Define hooks
import pluggy
hookspec = pluggy.HookspecMarker("clide")
hookimpl = pluggy.HookimplMarker("clide")
class ClideHookSpec:
@hookspec
def register_panel(self) -> dict: ...
# extension.py - Implement hooks
class MyExtension:
@hookimpl
def register_panel(self) -> dict:
return {"name": "custom", "widget": CustomWidget}
Distribution
Extensions can be distributed as packages using entry points:
# pyproject.toml of extension package
[project.entry-points."clide.extensions"]
my_extension = "my_package:MyExtension"
Hook Execution Order
- Multiple implementations called in LIFO (Last In, First Out) order
- Use
hookimpl(tryfirst=True)orhookimpl(trylast=True)for ordering
Alternatives
- Stevedore - Better for driver/extension patterns, uses entry points
- Choose Pluggy for hook-based systems (like pytest uses)
References
Testing Strategy
pytest-asyncio
Configure auto mode for automatic async test discovery:
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
Async Test Patterns
import pytest
# Auto mode - no decorator needed
async def test_async_operation():
result = await some_async_function()
assert result == expected
# Async fixtures
@pytest.fixture
async def database_connection():
conn = await create_connection()
yield conn
await conn.close()
Async Mocking
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"
Snapshot Testing
Visual regression with pytest-textual-snapshot:
def test_layout(snap_compare):
assert snap_compare("app.py", terminal_size=(120, 40))
def test_with_interaction(snap_compare):
async def setup(pilot):
await pilot.press("tab", "enter")
assert snap_compare("app.py", run_before=setup)
Update snapshots after intentional changes:
pytest tests/snapshots/ --snapshot-update
Test Harness Pattern
Harnesses provide isolated test environments:
class AppHarness:
async def start(self) -> tuple[App, Pilot]:
"""Start app with mocked dependencies."""
async def stop(self) -> None:
"""Clean shutdown."""
Best Practices
- Always use
@pytest.mark.asyncio(or auto mode) - Use async fixtures for async setup/teardown
- Mock external services - don't hit real APIs
- Choose appropriate fixture scopes for performance
- Avoid blocking the event loop in async tests
References
Build and Distribution
PyInstaller Limitations
Critical: PyInstaller cannot cross-compile.
- Build on the target OS
- Use CI/CD for multi-platform builds
CI/CD Multi-Platform Build
Use Gitea Actions (or compatible CI) for multi-platform builds:
# .gitea/workflows/build.yml
name: Build
on: [push, tag]
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
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-windows:
runs-on: windows-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
Optimization Tips
- Use
--onefilefor single executable - Apply
--stripto reduce binary size - Use UPX compression (460 MB → ~130 MB possible)
- Exclude unused modules with
--exclude-module - Lazy imports for large libraries
Platform-Specific Output
- Windows:
.exeor MSIX installer - macOS:
.appbundle in.dmg - Linux: AppImage or native package
Linux Compatibility
Build on the oldest target distro version. Newer systems may produce incompatible binaries.