move python clide to legacy/

Clide is being rebuilt as a Flutter desktop app. The Python Textual
implementation moves wholesale into legacy/ rather than being deleted:
its pane model, panel set, git skills, and panel communication design
are real thought that should remain readable next to the new code
while the rebuild finds its shape. Git's rename tracking preserves
history, so `git log -- legacy/` still works.

The Flutter rebuild lives at the repo root alongside a Go sidecar
(the architecture claudian was heading toward, which folds into
clide as a core component rather than a separate plugin project).
Bootstrap of the new shape lands in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 20:30:51 +02:00
co-authored by Claude Opus 4.7
parent 30b8cf7db9
commit a355751437
163 changed files with 0 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
"""Pydantic models for Clide."""
from clide.models.config import ClideSettings, PanelConfig
from clide.models.db import ConnectionLog, Project, Session, UserPreference
from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine, DiffViewState
from clide.models.editor import CursorPosition, EditorState, FileBuffer, Selection
from clide.models.git import (
ChangeStatus,
GitBranch,
GitChange,
GitCommit,
GitGraph,
GitStatus,
)
from clide.models.problems import Problem, ProblemsState, ProblemsSummary, Severity
from clide.models.theme import ThemeColors, ThemeDefinition, ThemeMetadata
from clide.models.todos import TodoItem, TodosState, TodosSummary, TodoType
from clide.models.workspace import TAB_ICONS, TabInfo, TabType
__all__ = [
# Config
"ClideSettings",
"PanelConfig",
# Git
"ChangeStatus",
"GitBranch",
"GitChange",
"GitCommit",
"GitGraph",
"GitStatus",
# Editor
"CursorPosition",
"EditorState",
"FileBuffer",
"Selection",
# Diff
"ChangeType",
"DiffContent",
"DiffHunk",
"DiffLine",
"DiffViewState",
# Problems
"Problem",
"ProblemsSummary",
"ProblemsState",
"Severity",
# Todos
"TodoItem",
"TodosSummary",
"TodosState",
"TodoType",
# Theme
"ThemeColors",
"ThemeDefinition",
"ThemeMetadata",
# Workspace
"TabInfo",
"TabType",
"TAB_ICONS",
# Database (SQLModel)
"Project",
"Session",
"UserPreference",
"ConnectionLog",
]
+94
View File
@@ -0,0 +1,94 @@
"""Configuration models using Pydantic Settings."""
from pathlib import Path
from pydantic import BaseModel, ConfigDict
from pydantic_settings import BaseSettings, SettingsConfigDict
class ClideSettings(BaseSettings):
"""Main application settings loaded from environment and config files."""
model_config = SettingsConfigDict(
env_prefix="CLIDE_",
env_file=".config/.env",
env_file_encoding="utf-8",
extra="ignore",
)
# Appearance
theme: str = "summer-night" # Default to Summer Night
# Paths
claude_path: str = "claude"
default_workdir: Path = Path.cwd()
jira_cli_path: str = "jira"
# Behavior
auto_save: bool = True
confirm_exit: bool = True
# Integrations
jira_enabled: bool = False
confluence_enabled: bool = False
imagin_enabled: bool = False
# Linters
linters: list[str] = ["ruff"]
class PanelConfig(BaseModel):
"""Panel visibility and layout configuration."""
model_config = ConfigDict(strict=True, frozen=True)
sidebar_visible: bool = True
context_visible: bool = True
workspace_visible: bool = False # Hidden by default
sidebar_width_percent: int = 20
context_width_percent: int = 25
class KeybindingsConfig(BaseModel):
"""Keybinding configuration."""
model_config = ConfigDict(strict=True)
# Global
command_palette: str = "ctrl+shift+p"
quick_open: str = "ctrl+p"
toggle_sidebar: str = "ctrl+b"
toggle_context: str = "ctrl+shift+b"
toggle_terminal: str = "ctrl+`"
toggle_compact: str = "ctrl+shift+c"
fullscreen: str = "f11"
# Navigation
focus_claude: str = "ctrl+1"
focus_editor: str = "ctrl+2"
focus_terminal: str = "ctrl+3"
focus_sidebar: str = "ctrl+0"
next_tab: str = "ctrl+tab"
prev_tab: str = "ctrl+shift+tab"
close_tab: str = "ctrl+w"
# Git
git_panel: str = "ctrl+shift+g"
stage_file: str = "ctrl+enter"
unstage_file: str = "ctrl+backspace"
# Search
find_in_file: str = "ctrl+f"
find_in_project: str = "ctrl+shift+f"
problems_panel: str = "ctrl+shift+m"
next_problem: str = "f8"
prev_problem: str = "shift+f8"
# Editor
save: str = "ctrl+s"
undo: str = "ctrl+z"
redo: str = "ctrl+shift+z"
go_to_line: str = "ctrl+g"
# Theme
select_theme: str = "ctrl+k ctrl+t"
+50
View File
@@ -0,0 +1,50 @@
"""SQLModel table models for persistent storage.
These models serve both the standalone Clide TUI and the clide-web server.
They ARE Pydantic models (SQLModel inherits from BaseModel).
"""
from datetime import datetime
from sqlmodel import Field, SQLModel
class Project(SQLModel, table=True):
"""A project (git repo) that can be opened in Clide."""
id: int | None = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True)
path: str
theme: str = "summer-night"
last_accessed: datetime | None = None
created_at: datetime = Field(default_factory=datetime.utcnow)
class Session(SQLModel, table=True):
"""A tmux session running a Clide instance (used by clide-web)."""
id: int | None = Field(default=None, primary_key=True)
project_name: str = Field(index=True)
tmux_session: str = Field(unique=True)
pid: int | None = None
status: str = "active"
created_at: datetime = Field(default_factory=datetime.utcnow)
last_activity: datetime = Field(default_factory=datetime.utcnow)
class UserPreference(SQLModel, table=True):
"""Key-value user preferences persisted across restarts."""
id: int | None = Field(default=None, primary_key=True)
key: str = Field(unique=True, index=True)
value: str
class ConnectionLog(SQLModel, table=True):
"""Audit log of browser connections (used by clide-web)."""
id: int | None = Field(default=None, primary_key=True)
project_name: str
client_ip: str
connected_at: datetime = Field(default_factory=datetime.utcnow)
disconnected_at: datetime | None = None
+66
View File
@@ -0,0 +1,66 @@
"""Diff-related Pydantic models."""
from enum import Enum
from pydantic import BaseModel, ConfigDict
class ChangeType(str, Enum):
"""Type of change in a diff line."""
ADDED = "added"
REMOVED = "removed"
CONTEXT = "context"
HEADER = "header"
class DiffLine(BaseModel):
"""A single line in a diff."""
model_config = ConfigDict(strict=True, frozen=True)
change_type: ChangeType
content: str
old_line_num: int | None = None
new_line_num: int | None = None
class DiffHunk(BaseModel):
"""A hunk (section) of a diff."""
model_config = ConfigDict(strict=True, frozen=True)
header: str
old_start: int
old_count: int
new_start: int
new_count: int
lines: tuple[DiffLine, ...]
class DiffContent(BaseModel):
"""Complete diff for a file."""
model_config = ConfigDict(strict=True, frozen=True)
file_path: str
old_path: str | None = None # For renames
hunks: tuple[DiffHunk, ...]
is_binary: bool = False
is_new_file: bool = False
is_deleted: bool = False
class DiffViewState(BaseModel):
"""State of the diff viewer."""
model_config = ConfigDict(strict=True)
diff: DiffContent | None = None
scroll_offset: int = 0
selected_hunk_index: int | None = None
side_by_side: bool = True
# For Claude-proposed changes
is_proposal: bool = False
accepted_hunks: set[int] = set()
rejected_hunks: set[int] = set()
+72
View File
@@ -0,0 +1,72 @@
"""Editor-related Pydantic models."""
from pathlib import Path
from pydantic import BaseModel, ConfigDict
class CursorPosition(BaseModel):
"""Cursor position in editor."""
model_config = ConfigDict(strict=True, frozen=True)
line: int
column: int
class Selection(BaseModel):
"""Text selection range."""
model_config = ConfigDict(strict=True, frozen=True)
start: CursorPosition
end: CursorPosition
class FileBuffer(BaseModel):
"""A file buffer in the editor."""
model_config = ConfigDict(strict=True)
path: Path
content: str
language: str | None = None
is_modified: bool = False
cursor: CursorPosition = CursorPosition(line=0, column=0)
selection: Selection | None = None
scroll_offset: int = 0
@property
def filename(self) -> str:
"""Get the filename from path."""
return self.path.name
@property
def display_name(self) -> str:
"""Get display name with modification indicator."""
prefix = "● " if self.is_modified else ""
return f"{prefix}{self.filename}"
class EditorState(BaseModel):
"""State of the editor panel."""
model_config = ConfigDict(strict=True)
buffers: list[FileBuffer] = []
active_buffer_index: int | None = None
recent_files: list[Path] = []
@property
def active_buffer(self) -> FileBuffer | None:
"""Get currently active buffer."""
if self.active_buffer_index is not None and self.buffers:
return self.buffers[self.active_buffer_index]
return None
def get_buffer_by_path(self, path: Path) -> FileBuffer | None:
"""Find a buffer by its file path."""
for buffer in self.buffers:
if buffer.path == path:
return buffer
return None
+80
View File
@@ -0,0 +1,80 @@
"""Git-related Pydantic models."""
from enum import Enum
from pydantic import BaseModel, ConfigDict
class ChangeStatus(str, Enum):
"""Git file change status."""
ADDED = "added"
MODIFIED = "modified"
DELETED = "deleted"
RENAMED = "renamed"
COPIED = "copied"
UNTRACKED = "untracked"
IGNORED = "ignored"
UNMERGED = "unmerged"
class GitChange(BaseModel):
"""A single file change in git."""
model_config = ConfigDict(strict=True, frozen=True)
path: str
status: ChangeStatus
staged: bool
old_path: str | None = None # For renames
class GitStatus(BaseModel):
"""Current git repository status."""
model_config = ConfigDict(strict=True, frozen=True)
branch: str
ahead: int = 0
behind: int = 0
staged: tuple[GitChange, ...]
unstaged: tuple[GitChange, ...]
untracked: tuple[str, ...] = ()
has_conflicts: bool = False
class GitBranch(BaseModel):
"""Git branch information."""
model_config = ConfigDict(strict=True, frozen=True)
name: str
is_current: bool = False
is_remote: bool = False
tracking: str | None = None
commit_hash: str | None = None
commit_message: str | None = None
class GitCommit(BaseModel):
"""Git commit information for graph view."""
model_config = ConfigDict(strict=True, frozen=True)
hash: str
short_hash: str
message: str
author: str
date: str
is_merge: bool = False
refs: tuple[str, ...] = () # branch names, tags
parents: tuple[str, ...] = ()
class GitGraph(BaseModel):
"""Git log graph data."""
model_config = ConfigDict(strict=True, frozen=True)
commits: tuple[GitCommit, ...]
branches: tuple[GitBranch, ...]
+88
View File
@@ -0,0 +1,88 @@
"""Problems (linter errors) Pydantic models."""
from enum import Enum
from pathlib import Path
from pydantic import BaseModel, ConfigDict
class Severity(str, Enum):
"""Problem severity level."""
ERROR = "error"
WARNING = "warning"
INFO = "info"
HINT = "hint"
class Problem(BaseModel):
"""A single linter problem/diagnostic."""
model_config = ConfigDict(strict=True, frozen=True)
file_path: Path
line: int
column: int
end_line: int | None = None
end_column: int | None = None
severity: Severity
message: str
source: str # e.g., "ruff", "mypy", "eslint"
code: str | None = None # e.g., "E501", "W0612"
@property
def location(self) -> str:
"""Human-readable location string."""
return f"{self.file_path}:{self.line}:{self.column}"
@property
def severity_icon(self) -> str:
"""Icon for severity level."""
icons = {
Severity.ERROR: "✖",
Severity.WARNING: "⚠",
Severity.INFO: "ℹ",
Severity.HINT: "💡",
}
return icons[self.severity]
class ProblemsSummary(BaseModel):
"""Summary of problems in the workspace."""
model_config = ConfigDict(strict=True, frozen=True)
errors: int = 0
warnings: int = 0
infos: int = 0
hints: int = 0
@property
def total(self) -> int:
"""Total number of problems."""
return self.errors + self.warnings + self.infos + self.hints
@property
def display_text(self) -> str:
"""Text for tab badge."""
if self.errors:
return f"⚠ {self.errors}"
if self.warnings:
return f"⚠ {self.warnings}"
return f"✓ {self.total}"
class ProblemsState(BaseModel):
"""State of the problems panel."""
model_config = ConfigDict(strict=True)
problems: list[Problem] = []
summary: ProblemsSummary = ProblemsSummary()
filter_severity: Severity | None = None
filter_source: str | None = None
selected_index: int | None = None
def problems_for_file(self, path: Path) -> list[Problem]:
"""Get problems for a specific file."""
return [p for p in self.problems if p.file_path == path]
+102
View File
@@ -0,0 +1,102 @@
"""Theme-related Pydantic models."""
import re
from typing import TYPE_CHECKING
from pydantic import BaseModel, ConfigDict, field_validator
if TYPE_CHECKING:
from textual.theme import Theme
def validate_hex_color(value: str) -> str:
"""Validate hex color format."""
if not re.match(r"^#[0-9A-Fa-f]{6}$", value):
raise ValueError(f"Invalid hex color: {value}")
return value.lower()
class ThemeColors(BaseModel):
"""Color definitions for a theme."""
model_config = ConfigDict(strict=True, frozen=True)
# Core colors
primary: str
secondary: str
accent: str
# Backgrounds
background: str
surface: str
panel: str
# Text
foreground: str
# Status
success: str
warning: str
error: str
@field_validator("*", mode="before")
@classmethod
def validate_colors(cls, v: str) -> str:
"""Validate all color fields are valid hex."""
return validate_hex_color(v)
class ThemeDefinition(BaseModel):
"""Complete theme definition."""
model_config = ConfigDict(strict=True)
name: str # e.g., "summer-night"
display_name: str # e.g., "Summer Night"
dark: bool # True for dark themes
colors: ThemeColors
def to_textual_colors(self) -> dict[str, str]:
"""Convert to Textual theme color dict."""
return {
"primary": self.colors.primary,
"secondary": self.colors.secondary,
"accent": self.colors.accent,
"background": self.colors.background,
"surface": self.colors.surface,
"panel": self.colors.panel,
"foreground": self.colors.foreground,
"success": self.colors.success,
"warning": self.colors.warning,
"error": self.colors.error,
}
def to_textual_theme(self) -> "Theme":
"""Convert to a Textual Theme object."""
from textual.theme import Theme
return Theme(
name=self.name,
primary=self.colors.primary,
secondary=self.colors.secondary,
accent=self.colors.accent,
background=self.colors.background,
surface=self.colors.surface,
panel=self.colors.panel,
foreground=self.colors.foreground,
success=self.colors.success,
warning=self.colors.warning,
error=self.colors.error,
dark=self.dark,
)
class ThemeMetadata(BaseModel):
"""Theme metadata for listing themes."""
model_config = ConfigDict(strict=True, frozen=True)
name: str
display_name: str
dark: bool
category: str = "custom" # e.g., "core", "popular", "seasonal", "custom"
+138
View File
@@ -0,0 +1,138 @@
"""TODO comments Pydantic models."""
from enum import Enum
from pathlib import Path
from pydantic import BaseModel, ConfigDict
class TodoType(str, Enum):
"""Type of TODO comment."""
TODO = "TODO"
FIXME = "FIXME"
HACK = "HACK"
XXX = "XXX"
NOTE = "NOTE"
BUG = "BUG"
OPTIMIZE = "OPTIMIZE"
REVIEW = "REVIEW"
class ProjectTodoItem(BaseModel):
"""A TODO item from TODO.md file."""
model_config = ConfigDict(strict=True, frozen=True)
text: str
section: str # Top-level section (## heading)
subsection: str | None = None # Optional subsection (### heading)
line: int # Line number in TODO.md
checked: bool = False # Whether the checkbox is checked
@property
def category(self) -> str:
"""Get full category path."""
if self.subsection:
return f"{self.section} › {self.subsection}"
return self.section
@property
def icon(self) -> str:
"""Icon for display."""
return "☑" if self.checked else "☐"
class TodoItem(BaseModel):
"""A single TODO comment found in code."""
model_config = ConfigDict(strict=True, frozen=True)
file_path: Path
line: int
column: int
todo_type: TodoType
text: str
context_line: str # The full line containing the TODO
@property
def location(self) -> str:
"""Human-readable location string."""
return f"{self.file_path}:{self.line}"
@property
def type_icon(self) -> str:
"""Icon for TODO type."""
icons = {
TodoType.TODO: "☐",
TodoType.FIXME: "🔧",
TodoType.HACK: "⚡",
TodoType.XXX: "❗",
TodoType.NOTE: "📝",
TodoType.BUG: "🐛",
TodoType.OPTIMIZE: "⚡",
TodoType.REVIEW: "👀",
}
return icons[self.todo_type]
class TodosSummary(BaseModel):
"""Summary of TODOs in the workspace."""
model_config = ConfigDict(strict=True, frozen=True)
todo_count: int = 0
fixme_count: int = 0
hack_count: int = 0
other_count: int = 0
project_todo_count: int = 0 # Count from TODO.md
project_done_count: int = 0 # Checked items in TODO.md
@property
def total(self) -> int:
"""Total number of code TODOs."""
return self.todo_count + self.fixme_count + self.hack_count + self.other_count
@property
def project_total(self) -> int:
"""Total number of project TODOs."""
return self.project_todo_count + self.project_done_count
@property
def display_text(self) -> str:
"""Text for tab badge."""
return f"✓{self.total}"
class TodosState(BaseModel):
"""State of the TODOs panel."""
model_config = ConfigDict(strict=True)
items: list[TodoItem] = []
project_items: list[ProjectTodoItem] = [] # Items from TODO.md
summary: TodosSummary = TodosSummary()
filter_type: TodoType | None = None
selected_index: int | None = None
group_by_file: bool = True
show_completed_project_todos: bool = False # Toggle for checked items
def items_for_file(self, path: Path) -> list[TodoItem]:
"""Get TODO items for a specific file."""
return [item for item in self.items if item.file_path == path]
def items_by_type(self, todo_type: TodoType) -> list[TodoItem]:
"""Get TODO items of a specific type."""
return [item for item in self.items if item.todo_type == todo_type]
def project_items_by_section(self, section: str) -> list[ProjectTodoItem]:
"""Get project TODO items for a specific section."""
return [item for item in self.project_items if item.section == section]
def get_project_sections(self) -> list[str]:
"""Get unique sections from project TODOs."""
sections: list[str] = []
for item in self.project_items:
if item.section not in sections:
sections.append(item.section)
return sections
+35
View File
@@ -0,0 +1,35 @@
"""Workspace tab models."""
from enum import Enum
from pathlib import Path
from pydantic import BaseModel, ConfigDict
class TabType(str, Enum):
"""Types of workspace tabs."""
EDITOR = "editor"
TERMINAL = "terminal"
DIFF = "diff"
# Nerd Font icons for each tab type
TAB_ICONS: dict[TabType, str] = {
TabType.EDITOR: "\uf15c", # nf-fa-file_text_o
TabType.TERMINAL: "\uf120", # nf-fa-terminal
TabType.DIFF: "\uf440", # nf-oct-diff
}
class TabInfo(BaseModel):
"""Metadata for a workspace tab."""
model_config = ConfigDict(strict=True)
tab_id: str
tab_type: TabType
label: str
file_path: Path | None = None
is_proposal: bool = False
diff_file_path: str | None = None