Files
clide/docs/ARCHITECTURE.md
T
jpmschweitzerandClaude Opus 4.5 c5f8b2e615 Add Clide project structure and initial implementation
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>
2026-01-31 21:04:44 +01:00

10 KiB

Clide Architecture Documentation

Comprehensive documentation of architecture patterns, best practices, and implementation guidelines.

Table of Contents


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

  1. Use Immutable Objects

    • Prefer tuples, NamedTuples, or frozen dataclasses
    • Easier to reason about, cache, and test
    • Enables side-effect-free code
  2. Separate Styles

    • Keep CSS in .tcss files, not inline
    • Python code stays clean and focused on logic
  3. Async-First

    • Textual is async under the hood
    • Use async/await for 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

  1. Organize Commands

    • Use add_typer() to group commands
    • Avoid giant files with dozens of commands
    • Each command function should orchestrate, not contain all logic
  2. Entry Point Support

    • Add __main__.py for python -m support
    • Define entry points in pyproject.toml for CLI scripts
  3. Standard Exit Codes

    • 0 for success
    • Non-zero for errors
    • Crucial for CI/CD integration
  4. 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

  1. Use frozen=True for Immutability

    • Prevents accidental mutation
    • Enables hashing for use as dict keys
  2. Explicit Strict Types

    • StrictInt, StrictStr for field-level strictness
    • Or use model_config for model-wide strictness
  3. 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

  1. Hook Specifications - Define the interface extensions implement
  2. Hook Implementations - Extension code implementing hooks
  3. 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) or hookimpl(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

  1. Always use @pytest.mark.asyncio (or auto mode)
  2. Use async fixtures for async setup/teardown
  3. Mock external services - don't hit real APIs
  4. Choose appropriate fixture scopes for performance
  5. 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

  1. Use --onefile for single executable
  2. Apply --strip to reduce binary size
  3. Use UPX compression (460 MB → ~130 MB possible)
  4. Exclude unused modules with --exclude-module
  5. Lazy imports for large libraries

Platform-Specific Output

  • Windows: .exe or MSIX installer
  • macOS: .app bundle in .dmg
  • Linux: AppImage or native package

Linux Compatibility

Build on the oldest target distro version. Newer systems may produce incompatible binaries.

References