Files
clide/docs/ARCHITECTURE.md
T
Jeroen SchweitzerandClaude Opus 4.5 8b1a84e7e2 docs: update documentation and fix lint issues for v1.0.0
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>
2026-02-01 22:02:00 +01:00

11 KiB

Clide Architecture

Technical documentation covering Clide's architecture, the frameworks it builds on, and implementation patterns.

Table of Contents


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):

  1. Environment variables (CLIDE_THEME=dracula)
  2. .env file
  3. 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 --onefile for single executable
  • Apply --strip to reduce size
  • Use UPX compression for further reduction
  • Exclude unused modules with --exclude-module

References