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:
@@ -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",
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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, ...]
|
||||
@@ -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]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user