diff --git a/CLAUDE.md b/CLAUDE.md index 440d1f5e..7428c627 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,80 +59,20 @@ make build # Build for current platform ``` clide/ -├── src/clide/ +├── clide/ # Package source │ ├── __init__.py │ ├── __main__.py │ ├── app.py # Main App, layout, keybindings │ ├── cli.py # Typer entry point -│ │ │ ├── controllers/ # Domain logic (no UI) -│ │ ├── base.py # BaseController ABC -│ │ ├── git.py # Git status, staging, branches -│ │ ├── editor.py # Open files, cursor state -│ │ ├── diff.py # Diff generation, accept/reject -│ │ ├── problems.py # Linter aggregation -│ │ ├── todos.py # TODO/FIXME scanning -│ │ └── jira.py # Jira CLI integration -│ │ -│ ├── widgets/ +│ ├── widgets/ # UI components │ │ ├── panels/ # Main layout containers -│ │ │ ├── sidebar.py # Left sidebar with tabs -│ │ │ ├── workspace.py # Editor/Diff/Terminal tabs -│ │ │ ├── claude.py # Claude interaction panel -│ │ │ └── context.py # Right context panel -│ │ │ │ │ └── components/ # Reusable UI pieces -│ │ ├── files_view.py # DirectoryTree wrapper -│ │ ├── git_changes.py # Staged/unstaged file list -│ │ ├── git_graph.py # Branch visualization -│ │ ├── branch_status.py # Current branch + popout -│ │ ├── editor_pane.py # TextArea with syntax -│ │ ├── diff_pane.py # Side-by-side/unified diff -│ │ ├── terminal_pane.py # PTY terminal -│ │ ├── problems_view.py # Linter errors list -│ │ ├── todos_view.py # TODO comments list -│ │ └── jira_view.py # Jira output display -│ │ │ ├── models/ # Pydantic data models -│ │ ├── config.py # App settings (ClideSettings) -│ │ ├── git.py # GitStatus, GitChange, GitBranch -│ │ ├── editor.py # EditorState, FileBuffer -│ │ ├── diff.py # DiffContent, DiffHunk -│ │ ├── problems.py # Problem, Severity -│ │ ├── todos.py # TodoItem, TodoType -│ │ └── theme.py # ThemeColors, ThemeDefinition -│ │ │ ├── services/ # Background task logic -│ │ ├── process_service.py # Generic subprocess mgmt -│ │ ├── git_service.py # Git command execution -│ │ ├── file_service.py # File I/O, language detection -│ │ ├── linter_service.py # Run linters, parse output -│ │ └── todo_scanner.py # Grep for TODOs -│ │ │ ├── themes/ # Theme system -│ │ ├── __init__.py -│ │ ├── registry.py # Theme registry, get_theme() -│ │ ├── loader.py # Load custom themes from .config -│ │ └── builtin/ # 22 built-in themes -│ │ ├── __init__.py # Auto-register all themes -│ │ ├── summer_night.py # DEFAULT - Summer Night dark -│ │ ├── summer_day.py # Summer Day light -│ │ ├── one_dark.py # Atom One Dark -│ │ ├── dracula.py # Dracula theme -│ │ ├── nord.py # Nord color palette -│ │ ├── gruvbox_dark.py # Gruvbox dark -│ │ ├── gruvbox_light.py # Gruvbox light -│ │ └── ... # More themes -│ │ │ ├── extensions/ # Plugin system -│ │ ├── hookspecs.py -│ │ ├── manager.py -│ │ └── builtin/ -│ │ -│ └── helpers/ -│ ├── async_utils.py -│ ├── path_utils.py -│ └── terminal_utils.py +│ └── helpers/ # Utility functions │ ├── tests/ │ ├── conftest.py diff --git a/Makefile b/Makefile index 16f5f143..671f52be 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ test-single: $(BIN)/pytest $(TEST) -v test-cov: - $(BIN)/pytest --cov=src/clide --cov-report=html --cov-report=term + $(BIN)/pytest --cov=clide/clide --cov-report=html --cov-report=term test-snapshots: $(BIN)/pytest tests/snapshots/ @@ -52,14 +52,14 @@ test-snapshots-update: $(BIN)/pytest tests/snapshots/ --snapshot-update typecheck: - $(BIN)/mypy src/ + $(BIN)/mypy clide/ lint: - $(BIN)/ruff check src/ tests/ + $(BIN)/ruff check clide/ tests/ format: - $(BIN)/ruff format src/ tests/ - $(BIN)/ruff check --fix src/ tests/ + $(BIN)/ruff format clide/ tests/ + $(BIN)/ruff check --fix clide/ tests/ build: $(BIN)/pyinstaller clide.spec --clean @@ -70,7 +70,7 @@ build-onefile: --onefile \ --clean \ --noconfirm \ - src/clide/__main__.py + clide/clide/__main__.py clean: rm -rf $(VENV) @@ -83,12 +83,12 @@ clean: # CI targets (for GitHub Actions) ci-lint: pip install ruff mypy - ruff check src/ tests/ - mypy src/ + ruff check clide/ tests/ + mypy clide/ ci-test: pip install -e ".[dev]" - pytest --cov=src/clide --cov-report=xml + pytest --cov=clide/clide --cov-report=xml ci-build: pip install -e ".[build]" diff --git a/src/clide/__init__.py b/clide/__init__.py similarity index 100% rename from src/clide/__init__.py rename to clide/__init__.py diff --git a/src/clide/__main__.py b/clide/__main__.py similarity index 100% rename from src/clide/__main__.py rename to clide/__main__.py diff --git a/src/clide/app.py b/clide/app.py similarity index 86% rename from src/clide/app.py rename to clide/app.py index 84e42e0c..38d03bc7 100644 --- a/src/clide/app.py +++ b/clide/app.py @@ -5,7 +5,7 @@ from typing import ClassVar from textual.app import App, ComposeResult from textual.binding import Binding -from textual.containers import Container, Horizontal, Vertical +from textual.containers import Horizontal, Vertical from textual.reactive import reactive from textual.widgets import Footer, Header @@ -17,6 +17,7 @@ from clide.controllers.problems import ProblemsController from clide.controllers.todos import TodosController from clide.extensions.manager import ExtensionManager from clide.models.config import ClideSettings +from clide.services.settings_service import SettingsService, get_settings_service from clide.themes.registry import get_all_themes, get_theme from clide.widgets.panels.claude import ClaudePanel from clide.widgets.panels.context import ContextPanel @@ -151,6 +152,10 @@ class ClideApp(App[None]): self.settings = settings or ClideSettings() self._test_mode = test_mode + # User settings persistence + self._settings_service = get_settings_service() + self._user_settings = self._settings_service.load() + # Extension manager self.extension_manager = ExtensionManager() @@ -161,7 +166,7 @@ class ClideApp(App[None]): self.problems_controller = ProblemsController(self.workdir) self.todos_controller = TodosController(self.workdir) self.jira_controller = JiraController( - enabled=self.settings.jira_enabled, + enabled=self._user_settings.jira_enabled, ) # Register themes @@ -174,8 +179,28 @@ class ClideApp(App[None]): if theme_def: self.register_theme(theme_def.to_textual_theme()) - # Set initial theme - self.theme = self.settings.theme + # Set initial theme from user settings + self.theme = self._user_settings.theme + + def set_theme(self, theme_name: str, *, save: bool = True) -> None: + """Set the application theme. + + Args: + theme_name: Name of the theme to apply + save: Whether to persist the setting (default: True) + """ + self.theme = theme_name + if save: + self._settings_service.set("theme", theme_name) + + def save_user_settings(self) -> None: + """Save current user settings to disk.""" + self._settings_service.update( + theme=self.theme, + compact_mode=self.compact_mode, + sidebar_visible=self.query_one(SidebarPanel).display, + context_visible=self.query_one(ContextPanel).display, + ) def compose(self) -> ComposeResult: """Create the main layout.""" @@ -202,6 +227,9 @@ class ClideApp(App[None]): async def on_mount(self) -> None: """Initialize application on mount.""" + # Apply saved user settings + self._apply_user_settings() + # Load extensions self.extension_manager.load_extensions() await self.extension_manager.trigger_app_startup(self) @@ -210,12 +238,24 @@ class ClideApp(App[None]): await self._refresh_git() await self._refresh_problems() await self._refresh_todos() - if self.settings.jira_enabled: + if self._user_settings.jira_enabled: await self._refresh_jira() # Focus Claude panel self.action_focus_claude() + def _apply_user_settings(self) -> None: + """Apply saved user settings on startup.""" + # Panel visibility + sidebar = self.query_one(SidebarPanel) + sidebar.visible = self._user_settings.sidebar_visible + + context = self.query_one(ContextPanel) + context.visible = self._user_settings.context_visible + + # Compact mode + self.compact_mode = self._user_settings.compact_mode + # Reactive watchers def watch_workspace_visible(self, visible: bool) -> None: """Update panels when workspace visibility changes.""" @@ -279,11 +319,13 @@ class ClideApp(App[None]): """Toggle sidebar visibility.""" sidebar = self.query_one(SidebarPanel) sidebar.visible = not sidebar.visible + self._settings_service.set("sidebar_visible", sidebar.visible) def action_toggle_context(self) -> None: """Toggle context panel visibility.""" context = self.query_one(ContextPanel) context.visible = not context.visible + self._settings_service.set("context_visible", context.visible) def action_toggle_terminal(self) -> None: """Toggle terminal (shows workspace with terminal tab).""" @@ -297,6 +339,7 @@ class ClideApp(App[None]): def action_toggle_compact(self) -> None: """Toggle compact mode.""" self.compact_mode = not self.compact_mode + self._settings_service.set("compact_mode", self.compact_mode) def action_toggle_fullscreen(self) -> None: """Toggle fullscreen for focused panel.""" @@ -364,7 +407,6 @@ class ClideApp(App[None]): def action_save_file(self) -> None: """Save current file.""" - workspace = self.query_one(WorkspacePanel) # EditorPane handles save internally pass @@ -378,10 +420,19 @@ class ClideApp(App[None]): # TODO: Implement quick open pass - def action_select_theme(self) -> None: + async def action_select_theme(self) -> None: """Open theme selector.""" - # TODO: Implement theme selector via command palette - pass + from clide.themes.registry import get_all_themes + + # Get all available themes + themes = get_all_themes() + theme_names = [t.name for t in themes] + + # Use Textual's built-in selection if available, otherwise cycle + # For now, simple cycle through themes + current_idx = theme_names.index(self.theme) if self.theme in theme_names else 0 + next_idx = (current_idx + 1) % len(theme_names) + self.set_theme(theme_names[next_idx]) # Event handlers for panel messages async def on_sidebar_panel_file_selected( @@ -439,14 +490,14 @@ class ClideApp(App[None]): async def on_context_panel_jira_refresh_requested( self, - event: ContextPanel.JiraRefreshRequested, + _event: ContextPanel.JiraRefreshRequested, ) -> None: """Handle Jira refresh request.""" await self._refresh_jira() async def on_workspace_panel_file_saved( self, - event: WorkspacePanel.FileSaved, + _event: WorkspacePanel.FileSaved, ) -> None: """Handle file save - refresh problems and git.""" await self._refresh_git() @@ -493,7 +544,7 @@ class ClideApp(App[None]): def on_claude_panel_claude_started( self, - event: ClaudePanel.ClaudeStarted, + _event: ClaudePanel.ClaudeStarted, ) -> None: """Handle Claude Code started.""" self.notify("Claude Code started", severity="information") diff --git a/src/clide/cli.py b/clide/cli.py similarity index 91% rename from src/clide/cli.py rename to clide/cli.py index 6859a722..b0c85eba 100644 --- a/src/clide/cli.py +++ b/clide/cli.py @@ -1,7 +1,7 @@ """Typer CLI entry point for Clide.""" from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer @@ -25,12 +25,12 @@ def version_callback(value: bool) -> None: @app.callback(invoke_without_command=True) def main( ctx: typer.Context, - version: Annotated[ - Optional[bool], + _version: Annotated[ + bool | None, typer.Option("--version", "-v", callback=version_callback, is_eager=True), ] = None, workdir: Annotated[ - Optional[Path], + Path | None, typer.Option("--workdir", "-w", help="Working directory to open"), ] = None, ) -> None: diff --git a/src/clide/controllers/__init__.py b/clide/controllers/__init__.py similarity index 89% rename from src/clide/controllers/__init__.py rename to clide/controllers/__init__.py index ff216259..e7cb3f3a 100644 --- a/src/clide/controllers/__init__.py +++ b/clide/controllers/__init__.py @@ -1,12 +1,12 @@ """Domain controllers for Clide.""" -from clide.controllers.base import controller, ControllerMixin -from clide.controllers.git import GitController -from clide.controllers.editor import EditorController +from clide.controllers.base import ControllerMixin, controller from clide.controllers.diff import DiffController +from clide.controllers.editor import EditorController +from clide.controllers.git import GitController +from clide.controllers.jira import JiraController from clide.controllers.problems import ProblemsController from clide.controllers.todos import TodosController -from clide.controllers.jira import JiraController __all__ = [ "controller", diff --git a/src/clide/controllers/base.py b/clide/controllers/base.py similarity index 96% rename from src/clide/controllers/base.py rename to clide/controllers/base.py index b4c99c95..ab393178 100644 --- a/src/clide/controllers/base.py +++ b/clide/controllers/base.py @@ -1,17 +1,15 @@ """Base controller utilities using decorator pattern.""" from functools import wraps -from typing import TYPE_CHECKING, Callable, TypeVar +from typing import TYPE_CHECKING from textual.message import Message if TYPE_CHECKING: from textual.app import App -T = TypeVar("T") - -def controller(cls: type[T]) -> type[T]: +def controller[T](cls: type[T]) -> type[T]: """Decorator to add controller capabilities to a class. Adds: diff --git a/src/clide/controllers/diff.py b/clide/controllers/diff.py similarity index 94% rename from src/clide/controllers/diff.py rename to clide/controllers/diff.py index 6ac82706..0c411fe6 100644 --- a/src/clide/controllers/diff.py +++ b/clide/controllers/diff.py @@ -186,17 +186,17 @@ class DiffController: current_hunk_lines.append(DiffLine( change_type=ChangeType.ADDED, content=line[1:], - new_line_num=new_start + len([l for l in current_hunk_lines if l.change_type != ChangeType.REMOVED]), + new_line_num=new_start + len([ln for ln in current_hunk_lines if ln.change_type != ChangeType.REMOVED]), )) elif line.startswith("-") and not line.startswith("---"): current_hunk_lines.append(DiffLine( change_type=ChangeType.REMOVED, content=line[1:], - old_line_num=old_start + len([l for l in current_hunk_lines if l.change_type != ChangeType.ADDED]), + old_line_num=old_start + len([ln for ln in current_hunk_lines if ln.change_type != ChangeType.ADDED]), )) elif line.startswith(" "): - old_num = old_start + len([l for l in current_hunk_lines if l.change_type != ChangeType.ADDED]) - new_num = new_start + len([l for l in current_hunk_lines if l.change_type != ChangeType.REMOVED]) + old_num = old_start + len([ln for ln in current_hunk_lines if ln.change_type != ChangeType.ADDED]) + new_num = new_start + len([ln for ln in current_hunk_lines if ln.change_type != ChangeType.REMOVED]) current_hunk_lines.append(DiffLine( change_type=ChangeType.CONTEXT, content=line[1:], @@ -232,7 +232,7 @@ class DiffController: """ return await self.load_git_diff(path, staged) - async def accept_proposal(self, file_path: str) -> bool: + async def accept_proposal(self, _file_path: str) -> bool: """Accept a proposed change and apply it. Args: @@ -249,7 +249,7 @@ class DiffController: self.clear() return True - async def reject_proposal(self, file_path: str) -> bool: + async def reject_proposal(self, _file_path: str) -> bool: """Reject a proposed change. Args: diff --git a/src/clide/controllers/editor.py b/clide/controllers/editor.py similarity index 94% rename from src/clide/controllers/editor.py rename to clide/controllers/editor.py index 0c2f3646..c3e7e0db 100644 --- a/src/clide/controllers/editor.py +++ b/clide/controllers/editor.py @@ -6,7 +6,6 @@ from textual.message import Message from clide.controllers.base import controller from clide.models.editor import CursorPosition, EditorState, FileBuffer -from clide.services.file_service import FileService @controller @@ -131,11 +130,10 @@ class EditorController: self._state.buffers.remove(buffer) # Update active buffer - if self._state.active_buffer_index is not None: - if self._state.active_buffer_index >= len(self._state.buffers): - self._state.active_buffer_index = ( - len(self._state.buffers) - 1 if self._state.buffers else None - ) + if self._state.active_buffer_index is not None and self._state.active_buffer_index >= len(self._state.buffers): + self._state.active_buffer_index = ( + len(self._state.buffers) - 1 if self._state.buffers else None + ) return True diff --git a/src/clide/controllers/git.py b/clide/controllers/git.py similarity index 100% rename from src/clide/controllers/git.py rename to clide/controllers/git.py diff --git a/src/clide/controllers/jira.py b/clide/controllers/jira.py similarity index 100% rename from src/clide/controllers/jira.py rename to clide/controllers/jira.py diff --git a/src/clide/controllers/problems.py b/clide/controllers/problems.py similarity index 98% rename from src/clide/controllers/problems.py rename to clide/controllers/problems.py index 7414a3d6..449fe406 100644 --- a/src/clide/controllers/problems.py +++ b/clide/controllers/problems.py @@ -5,7 +5,7 @@ from pathlib import Path from textual.message import Message from clide.controllers.base import controller -from clide.models.problems import Problem, ProblemsSummary, ProblemsState, Severity +from clide.models.problems import Problem, ProblemsState, ProblemsSummary, Severity from clide.services.linter_service import LinterService diff --git a/src/clide/controllers/todos.py b/clide/controllers/todos.py similarity index 97% rename from src/clide/controllers/todos.py rename to clide/controllers/todos.py index fc4eec96..4465838c 100644 --- a/src/clide/controllers/todos.py +++ b/clide/controllers/todos.py @@ -5,7 +5,7 @@ from pathlib import Path from textual.message import Message from clide.controllers.base import controller -from clide.models.todos import TodoItem, TodosSummary, TodosState, TodoType +from clide.models.todos import TodoItem, TodosState, TodosSummary, TodoType from clide.services.todo_scanner import TodoScanner diff --git a/src/clide/extensions/__init__.py b/clide/extensions/__init__.py similarity index 100% rename from src/clide/extensions/__init__.py rename to clide/extensions/__init__.py diff --git a/src/clide/extensions/builtin/__init__.py b/clide/extensions/builtin/__init__.py similarity index 100% rename from src/clide/extensions/builtin/__init__.py rename to clide/extensions/builtin/__init__.py diff --git a/src/clide/extensions/hookspecs.py b/clide/extensions/hookspecs.py similarity index 100% rename from src/clide/extensions/hookspecs.py rename to clide/extensions/hookspecs.py diff --git a/src/clide/extensions/manager.py b/clide/extensions/manager.py similarity index 100% rename from src/clide/extensions/manager.py rename to clide/extensions/manager.py diff --git a/src/clide/helpers/__init__.py b/clide/helpers/__init__.py similarity index 100% rename from src/clide/helpers/__init__.py rename to clide/helpers/__init__.py diff --git a/src/clide/models/__init__.py b/clide/models/__init__.py similarity index 87% rename from src/clide/models/__init__.py rename to clide/models/__init__.py index cd578ad5..5669aeaf 100644 --- a/src/clide/models/__init__.py +++ b/clide/models/__init__.py @@ -11,9 +11,9 @@ from clide.models.git import ( GitGraph, GitStatus, ) -from clide.models.problems import Problem, ProblemsSummary, ProblemsState, Severity +from clide.models.problems import Problem, ProblemsState, ProblemsSummary, Severity from clide.models.theme import ThemeColors, ThemeDefinition, ThemeMetadata -from clide.models.todos import TodoItem, TodosSummary, TodosState, TodoType +from clide.models.todos import TodoItem, TodosState, TodosSummary, TodoType __all__ = [ # Config diff --git a/src/clide/models/config.py b/clide/models/config.py similarity index 100% rename from src/clide/models/config.py rename to clide/models/config.py diff --git a/src/clide/models/diff.py b/clide/models/diff.py similarity index 100% rename from src/clide/models/diff.py rename to clide/models/diff.py diff --git a/src/clide/models/editor.py b/clide/models/editor.py similarity index 100% rename from src/clide/models/editor.py rename to clide/models/editor.py diff --git a/src/clide/models/git.py b/clide/models/git.py similarity index 98% rename from src/clide/models/git.py rename to clide/models/git.py index b5f4c4d5..de177a0f 100644 --- a/src/clide/models/git.py +++ b/clide/models/git.py @@ -1,7 +1,6 @@ """Git-related Pydantic models.""" from enum import Enum -from typing import Literal from pydantic import BaseModel, ConfigDict diff --git a/src/clide/models/problems.py b/clide/models/problems.py similarity index 100% rename from src/clide/models/problems.py rename to clide/models/problems.py diff --git a/src/clide/models/theme.py b/clide/models/theme.py similarity index 100% rename from src/clide/models/theme.py rename to clide/models/theme.py diff --git a/src/clide/models/todos.py b/clide/models/todos.py similarity index 100% rename from src/clide/models/todos.py rename to clide/models/todos.py diff --git a/src/clide/services/__init__.py b/clide/services/__init__.py similarity index 67% rename from src/clide/services/__init__.py rename to clide/services/__init__.py index cbf64496..f32e3291 100644 --- a/src/clide/services/__init__.py +++ b/clide/services/__init__.py @@ -3,11 +3,15 @@ from clide.services.git_service import GitService from clide.services.linter_service import LinterService from clide.services.process_service import ProcessService +from clide.services.settings_service import SettingsService, UserSettings, get_settings_service from clide.services.todo_scanner import TodoScanner __all__ = [ "GitService", "LinterService", "ProcessService", + "SettingsService", "TodoScanner", + "UserSettings", + "get_settings_service", ] diff --git a/src/clide/services/file_service.py b/clide/services/file_service.py similarity index 100% rename from src/clide/services/file_service.py rename to clide/services/file_service.py diff --git a/src/clide/services/git_service.py b/clide/services/git_service.py similarity index 100% rename from src/clide/services/git_service.py rename to clide/services/git_service.py diff --git a/src/clide/services/linter_service.py b/clide/services/linter_service.py similarity index 100% rename from src/clide/services/linter_service.py rename to clide/services/linter_service.py diff --git a/src/clide/services/process_service.py b/clide/services/process_service.py similarity index 98% rename from src/clide/services/process_service.py rename to clide/services/process_service.py index 26480ea7..0d7200d1 100644 --- a/src/clide/services/process_service.py +++ b/clide/services/process_service.py @@ -65,7 +65,7 @@ class ProcessService: stdout=stdout.decode("utf-8", errors="replace"), stderr=stderr.decode("utf-8", errors="replace"), ) - except asyncio.TimeoutError: + except TimeoutError: process.kill() return CommandResult( returncode=-1, diff --git a/clide/services/settings_service.py b/clide/services/settings_service.py new file mode 100644 index 00000000..31ac6269 --- /dev/null +++ b/clide/services/settings_service.py @@ -0,0 +1,159 @@ +"""Settings persistence service for user preferences.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from pydantic import BaseModel + + +class UserSettings(BaseModel): + """User settings that persist across sessions. + + Stored in ~/.clide/settings.json + """ + + # Appearance + theme: str = "summer-night" + + # Panel state + sidebar_visible: bool = True + context_visible: bool = True + + # Window + compact_mode: bool = False + + # Behavior + auto_save: bool = True + confirm_exit: bool = True + + # Integrations + jira_enabled: bool = False + + +class SettingsService: + """Service for loading and saving user settings. + + Settings are stored in ~/.clide/settings.json + """ + + def __init__(self, settings_dir: Path | None = None) -> None: + """Initialize the settings service. + + Args: + settings_dir: Override the settings directory (default: ~/.clide) + """ + self._settings_dir = settings_dir or Path.home() / ".clide" + self._settings_file = self._settings_dir / "settings.json" + self._settings: UserSettings | None = None + + @property + def settings_dir(self) -> Path: + """Get the settings directory path.""" + return self._settings_dir + + @property + def settings_file(self) -> Path: + """Get the settings file path.""" + return self._settings_file + + def load(self) -> UserSettings: + """Load settings from disk, creating defaults if needed. + + Returns: + The loaded or default UserSettings + """ + if self._settings is not None: + return self._settings + + if self._settings_file.exists(): + try: + data = json.loads(self._settings_file.read_text()) + self._settings = UserSettings.model_validate(data) + except (json.JSONDecodeError, ValueError): + # Invalid JSON or schema, use defaults + self._settings = UserSettings() + else: + self._settings = UserSettings() + + return self._settings + + def save(self) -> None: + """Save current settings to disk.""" + if self._settings is None: + return + + # Ensure directory exists + self._settings_dir.mkdir(parents=True, exist_ok=True) + + # Write settings as formatted JSON + data = self._settings.model_dump(mode="json") + self._settings_file.write_text( + json.dumps(data, indent=2, sort_keys=True) + "\n" + ) + + def get(self, key: str, default: Any = None) -> Any: + """Get a setting value. + + Args: + key: The setting key (attribute name) + default: Default value if key doesn't exist + + Returns: + The setting value or default + """ + settings = self.load() + return getattr(settings, key, default) + + def set(self, key: str, value: Any, *, save: bool = True) -> None: + """Set a setting value. + + Args: + key: The setting key (attribute name) + value: The value to set + save: Whether to save immediately (default: True) + """ + settings = self.load() + if hasattr(settings, key): + # Create new settings with updated value + data = settings.model_dump() + data[key] = value + self._settings = UserSettings.model_validate(data) + + if save: + self.save() + + def update(self, **kwargs: Any) -> None: + """Update multiple settings at once. + + Args: + **kwargs: Key-value pairs to update + """ + settings = self.load() + data = settings.model_dump() + + for key, value in kwargs.items(): + if hasattr(settings, key): + data[key] = value + + self._settings = UserSettings.model_validate(data) + self.save() + + def reset(self) -> None: + """Reset settings to defaults.""" + self._settings = UserSettings() + self.save() + + +# Global instance for convenience +_settings_service: SettingsService | None = None + + +def get_settings_service() -> SettingsService: + """Get the global settings service instance.""" + global _settings_service + if _settings_service is None: + _settings_service = SettingsService() + return _settings_service diff --git a/src/clide/services/todo_scanner.py b/clide/services/todo_scanner.py similarity index 100% rename from src/clide/services/todo_scanner.py rename to clide/services/todo_scanner.py diff --git a/src/clide/themes/__init__.py b/clide/themes/__init__.py similarity index 100% rename from src/clide/themes/__init__.py rename to clide/themes/__init__.py index 773eb7f9..792497e1 100644 --- a/src/clide/themes/__init__.py +++ b/clide/themes/__init__.py @@ -1,11 +1,11 @@ """Theme system for Clide.""" from clide.themes.registry import ( - get_theme, + DEFAULT_THEME, get_all_themes, + get_theme, get_themes_by_category, register_theme, - DEFAULT_THEME, ) __all__ = [ diff --git a/src/clide/themes/builtin/__init__.py b/clide/themes/builtin/__init__.py similarity index 100% rename from src/clide/themes/builtin/__init__.py rename to clide/themes/builtin/__init__.py index dc47763c..532481db 100644 --- a/src/clide/themes/builtin/__init__.py +++ b/clide/themes/builtin/__init__.py @@ -1,28 +1,28 @@ """Built-in themes for Clide.""" from clide.themes.builtin import ( - summer_night, - summer_day, - one_dark, - one_dark_pro, - one_light, + all_hallows_eve, + christmas, + dark_autumn, dracula, - nord, + fall, + gamma, gruvbox_dark, gruvbox_light, - one_dark_teal, - gamma, - winter_is_coming, - monokai_winter, - fall, - dark_autumn, - all_hallows_eve, - halloween, - christmas, - santa_baby, - pro_hacker, hacker_style, + halloween, houston, + monokai_winter, + nord, + one_dark, + one_dark_pro, + one_dark_teal, + one_light, + pro_hacker, + santa_baby, + summer_day, + summer_night, + winter_is_coming, ) __all__ = [ diff --git a/src/clide/themes/builtin/all_hallows_eve.py b/clide/themes/builtin/all_hallows_eve.py similarity index 100% rename from src/clide/themes/builtin/all_hallows_eve.py rename to clide/themes/builtin/all_hallows_eve.py diff --git a/src/clide/themes/builtin/christmas.py b/clide/themes/builtin/christmas.py similarity index 100% rename from src/clide/themes/builtin/christmas.py rename to clide/themes/builtin/christmas.py diff --git a/src/clide/themes/builtin/dark_autumn.py b/clide/themes/builtin/dark_autumn.py similarity index 100% rename from src/clide/themes/builtin/dark_autumn.py rename to clide/themes/builtin/dark_autumn.py diff --git a/src/clide/themes/builtin/dracula.py b/clide/themes/builtin/dracula.py similarity index 100% rename from src/clide/themes/builtin/dracula.py rename to clide/themes/builtin/dracula.py diff --git a/src/clide/themes/builtin/fall.py b/clide/themes/builtin/fall.py similarity index 100% rename from src/clide/themes/builtin/fall.py rename to clide/themes/builtin/fall.py diff --git a/src/clide/themes/builtin/gamma.py b/clide/themes/builtin/gamma.py similarity index 100% rename from src/clide/themes/builtin/gamma.py rename to clide/themes/builtin/gamma.py diff --git a/src/clide/themes/builtin/gruvbox_dark.py b/clide/themes/builtin/gruvbox_dark.py similarity index 100% rename from src/clide/themes/builtin/gruvbox_dark.py rename to clide/themes/builtin/gruvbox_dark.py diff --git a/src/clide/themes/builtin/gruvbox_light.py b/clide/themes/builtin/gruvbox_light.py similarity index 100% rename from src/clide/themes/builtin/gruvbox_light.py rename to clide/themes/builtin/gruvbox_light.py diff --git a/src/clide/themes/builtin/hacker_style.py b/clide/themes/builtin/hacker_style.py similarity index 100% rename from src/clide/themes/builtin/hacker_style.py rename to clide/themes/builtin/hacker_style.py diff --git a/src/clide/themes/builtin/halloween.py b/clide/themes/builtin/halloween.py similarity index 100% rename from src/clide/themes/builtin/halloween.py rename to clide/themes/builtin/halloween.py diff --git a/src/clide/themes/builtin/houston.py b/clide/themes/builtin/houston.py similarity index 100% rename from src/clide/themes/builtin/houston.py rename to clide/themes/builtin/houston.py diff --git a/src/clide/themes/builtin/monokai_winter.py b/clide/themes/builtin/monokai_winter.py similarity index 100% rename from src/clide/themes/builtin/monokai_winter.py rename to clide/themes/builtin/monokai_winter.py diff --git a/src/clide/themes/builtin/nord.py b/clide/themes/builtin/nord.py similarity index 100% rename from src/clide/themes/builtin/nord.py rename to clide/themes/builtin/nord.py diff --git a/src/clide/themes/builtin/one_dark.py b/clide/themes/builtin/one_dark.py similarity index 100% rename from src/clide/themes/builtin/one_dark.py rename to clide/themes/builtin/one_dark.py diff --git a/src/clide/themes/builtin/one_dark_pro.py b/clide/themes/builtin/one_dark_pro.py similarity index 100% rename from src/clide/themes/builtin/one_dark_pro.py rename to clide/themes/builtin/one_dark_pro.py diff --git a/src/clide/themes/builtin/one_dark_teal.py b/clide/themes/builtin/one_dark_teal.py similarity index 100% rename from src/clide/themes/builtin/one_dark_teal.py rename to clide/themes/builtin/one_dark_teal.py diff --git a/src/clide/themes/builtin/one_light.py b/clide/themes/builtin/one_light.py similarity index 100% rename from src/clide/themes/builtin/one_light.py rename to clide/themes/builtin/one_light.py diff --git a/src/clide/themes/builtin/pro_hacker.py b/clide/themes/builtin/pro_hacker.py similarity index 100% rename from src/clide/themes/builtin/pro_hacker.py rename to clide/themes/builtin/pro_hacker.py diff --git a/src/clide/themes/builtin/santa_baby.py b/clide/themes/builtin/santa_baby.py similarity index 100% rename from src/clide/themes/builtin/santa_baby.py rename to clide/themes/builtin/santa_baby.py diff --git a/src/clide/themes/builtin/summer_day.py b/clide/themes/builtin/summer_day.py similarity index 100% rename from src/clide/themes/builtin/summer_day.py rename to clide/themes/builtin/summer_day.py diff --git a/src/clide/themes/builtin/summer_night.py b/clide/themes/builtin/summer_night.py similarity index 100% rename from src/clide/themes/builtin/summer_night.py rename to clide/themes/builtin/summer_night.py diff --git a/src/clide/themes/builtin/winter_is_coming.py b/clide/themes/builtin/winter_is_coming.py similarity index 100% rename from src/clide/themes/builtin/winter_is_coming.py rename to clide/themes/builtin/winter_is_coming.py diff --git a/src/clide/themes/loader.py b/clide/themes/loader.py similarity index 99% rename from src/clide/themes/loader.py rename to clide/themes/loader.py index ed6d7fdd..5c3cf14a 100644 --- a/src/clide/themes/loader.py +++ b/clide/themes/loader.py @@ -1,8 +1,7 @@ """Custom theme loader for user-defined themes.""" -from pathlib import Path - import tomllib +from pathlib import Path from clide.models.theme import ThemeColors, ThemeDefinition from clide.themes.registry import register_theme diff --git a/src/clide/themes/registry.py b/clide/themes/registry.py similarity index 99% rename from src/clide/themes/registry.py rename to clide/themes/registry.py index 80198b6c..b20d86bf 100644 --- a/src/clide/themes/registry.py +++ b/clide/themes/registry.py @@ -1,6 +1,5 @@ """Theme registry for managing available themes.""" -from typing import Callable from clide.models.theme import ThemeDefinition, ThemeMetadata @@ -71,28 +70,28 @@ def _load_builtin_themes() -> None: """Load all built-in themes.""" # Import here to avoid circular imports from clide.themes.builtin import ( - summer_night, - summer_day, - one_dark, - one_dark_pro, - one_light, + all_hallows_eve, + christmas, + dark_autumn, dracula, - nord, + fall, + gamma, gruvbox_dark, gruvbox_light, - one_dark_teal, - gamma, - winter_is_coming, - monokai_winter, - fall, - dark_autumn, - all_hallows_eve, - halloween, - christmas, - santa_baby, - pro_hacker, hacker_style, + halloween, houston, + monokai_winter, + nord, + one_dark, + one_dark_pro, + one_dark_teal, + one_light, + pro_hacker, + santa_baby, + summer_day, + summer_night, + winter_is_coming, ) # Core themes diff --git a/src/clide/widgets/__init__.py b/clide/widgets/__init__.py similarity index 100% rename from src/clide/widgets/__init__.py rename to clide/widgets/__init__.py diff --git a/src/clide/widgets/components/__init__.py b/clide/widgets/components/__init__.py similarity index 100% rename from src/clide/widgets/components/__init__.py rename to clide/widgets/components/__init__.py diff --git a/src/clide/widgets/components/branch_status.py b/clide/widgets/components/branch_status.py similarity index 98% rename from src/clide/widgets/components/branch_status.py rename to clide/widgets/components/branch_status.py index 82592c3c..97066b4f 100644 --- a/src/clide/widgets/components/branch_status.py +++ b/clide/widgets/components/branch_status.py @@ -146,7 +146,7 @@ class BranchStatus(Vertical): """Handle click on status bar.""" self.toggle_popout() - def on_button_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self, _event: Button.Pressed) -> None: """Handle button presses.""" if event.button.id == "btn-checkout": branch_list = self.query_one("#branch-list", ListView) diff --git a/src/clide/widgets/components/diff_pane.py b/clide/widgets/components/diff_pane.py similarity index 96% rename from src/clide/widgets/components/diff_pane.py rename to clide/widgets/components/diff_pane.py index a21d4854..a7ebcb54 100644 --- a/src/clide/widgets/components/diff_pane.py +++ b/clide/widgets/components/diff_pane.py @@ -5,7 +5,7 @@ from textual.containers import Horizontal, Vertical from textual.message import Message from textual.widgets import Button, RichLog, Static -from clide.models.diff import ChangeType, DiffContent, DiffHunk +from clide.models.diff import ChangeType, DiffContent class DiffPane(Vertical): @@ -137,7 +137,7 @@ class DiffPane(Vertical): header = self.query_one(".diff-header", Static) header.update("No diff loaded") - def on_button_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self, _event: Button.Pressed) -> None: """Handle button presses.""" if not self._diff: return diff --git a/src/clide/widgets/components/editor_pane.py b/clide/widgets/components/editor_pane.py similarity index 98% rename from src/clide/widgets/components/editor_pane.py rename to clide/widgets/components/editor_pane.py index 6dca84e1..52e549cb 100644 --- a/src/clide/widgets/components/editor_pane.py +++ b/clide/widgets/components/editor_pane.py @@ -5,7 +5,7 @@ from pathlib import Path from textual.app import ComposeResult from textual.containers import Vertical from textual.message import Message -from textual.widgets import Label, Static, TextArea +from textual.widgets import Static, TextArea from clide.models.editor import CursorPosition, FileBuffer @@ -92,7 +92,7 @@ class EditorPane(Vertical): yield TextArea(id="editor-textarea", show_line_numbers=True) yield Static("", classes="editor-status", id="editor-status") - def load_buffer(self, buffer: FileBuffer) -> None: + def load_buffer(self, buffer: FileBuffer) -> None: # noqa: ARG002 """Load a file buffer into the editor.""" self._buffer = buffer diff --git a/src/clide/widgets/components/files_view.py b/clide/widgets/components/files_view.py similarity index 100% rename from src/clide/widgets/components/files_view.py rename to clide/widgets/components/files_view.py diff --git a/src/clide/widgets/components/git_changes.py b/clide/widgets/components/git_changes.py similarity index 100% rename from src/clide/widgets/components/git_changes.py rename to clide/widgets/components/git_changes.py diff --git a/src/clide/widgets/components/git_graph.py b/clide/widgets/components/git_graph.py similarity index 98% rename from src/clide/widgets/components/git_graph.py rename to clide/widgets/components/git_graph.py index 8434162d..842b3cfa 100644 --- a/src/clide/widgets/components/git_graph.py +++ b/clide/widgets/components/git_graph.py @@ -1,9 +1,9 @@ """Git graph visualization component.""" from textual.app import ComposeResult -from textual.message import Message -from textual.widgets import RichLog, Static from textual.containers import Vertical +from textual.message import Message +from textual.widgets import RichLog from clide.models.git import GitCommit diff --git a/src/clide/widgets/components/jira_view.py b/clide/widgets/components/jira_view.py similarity index 100% rename from src/clide/widgets/components/jira_view.py rename to clide/widgets/components/jira_view.py diff --git a/src/clide/widgets/components/problems_view.py b/clide/widgets/components/problems_view.py similarity index 96% rename from src/clide/widgets/components/problems_view.py rename to clide/widgets/components/problems_view.py index af9f89a5..c97e269a 100644 --- a/src/clide/widgets/components/problems_view.py +++ b/clide/widgets/components/problems_view.py @@ -3,11 +3,11 @@ from pathlib import Path from textual.app import ComposeResult -from textual.message import Message -from textual.widgets import Label, ListItem, ListView, Static from textual.containers import Vertical +from textual.message import Message +from textual.widgets import ListItem, ListView, Static -from clide.models.problems import Problem, Severity +from clide.models.problems import Problem class ProblemItem(ListItem): diff --git a/src/clide/widgets/components/terminal_pane.py b/clide/widgets/components/terminal_pane.py similarity index 100% rename from src/clide/widgets/components/terminal_pane.py rename to clide/widgets/components/terminal_pane.py diff --git a/src/clide/widgets/components/todos_view.py b/clide/widgets/components/todos_view.py similarity index 97% rename from src/clide/widgets/components/todos_view.py rename to clide/widgets/components/todos_view.py index 453ac669..db29a1e5 100644 --- a/src/clide/widgets/components/todos_view.py +++ b/clide/widgets/components/todos_view.py @@ -1,11 +1,10 @@ """TODOs view component.""" -from pathlib import Path from textual.app import ComposeResult -from textual.message import Message -from textual.widgets import Label, ListItem, ListView, Static from textual.containers import Vertical +from textual.message import Message +from textual.widgets import ListItem, ListView, Static from clide.models.todos import TodoItem, TodoType diff --git a/src/clide/widgets/panels/__init__.py b/clide/widgets/panels/__init__.py similarity index 100% rename from src/clide/widgets/panels/__init__.py rename to clide/widgets/panels/__init__.py diff --git a/src/clide/widgets/panels/claude.py b/clide/widgets/panels/claude.py similarity index 91% rename from src/clide/widgets/panels/claude.py rename to clide/widgets/panels/claude.py index c4636b13..e06307f7 100644 --- a/src/clide/widgets/panels/claude.py +++ b/clide/widgets/panels/claude.py @@ -9,6 +9,7 @@ import pty import shutil import struct import termios +import time from pathlib import Path from typing import TYPE_CHECKING @@ -49,6 +50,9 @@ class TerminalDisplay(Widget, can_focus=True): self._master_fd: int | None = None self._pid: int | None = None self._read_task: asyncio.Task | None = None + self._refresh_task: asyncio.Task | None = None + self._needs_refresh: bool = False + self._last_refresh: float = 0 def on_resize(self, event) -> None: """Handle terminal resize.""" @@ -99,17 +103,35 @@ class TerminalDisplay(Widget, can_focus=True): # Start reading self._read_task = asyncio.create_task(self._read_output()) + def _schedule_refresh(self) -> None: + """Schedule a throttled refresh to avoid glitching.""" + self._needs_refresh = True + if self._refresh_task is None or self._refresh_task.done(): + self._refresh_task = asyncio.create_task(self._throttled_refresh()) + + async def _throttled_refresh(self) -> None: + """Refresh at most every 16ms (~60fps) to avoid glitching.""" + min_interval = 0.016 # ~60fps + while self._needs_refresh: + now = time.monotonic() + elapsed = now - self._last_refresh + if elapsed < min_interval: + await asyncio.sleep(min_interval - elapsed) + self._needs_refresh = False + self._last_refresh = time.monotonic() + self.refresh() + # Small delay to batch rapid updates + await asyncio.sleep(0.008) + async def _read_output(self) -> None: """Read output from the PTY.""" if self._master_fd is None: return - loop = asyncio.get_event_loop() - while True: try: # Wait for data to be available - await asyncio.sleep(0.01) + await asyncio.sleep(0.005) try: data = os.read(self._master_fd, 65536) @@ -118,7 +140,7 @@ class TerminalDisplay(Widget, can_focus=True): # Feed data to pyte self._stream.feed(data.decode("utf-8", errors="replace")) - self.refresh() + self._schedule_refresh() except BlockingIOError: # No data available @@ -136,6 +158,10 @@ class TerminalDisplay(Widget, can_focus=True): self._read_task.cancel() self._read_task = None + if self._refresh_task: + self._refresh_task.cancel() + self._refresh_task = None + if self._master_fd is not None: try: os.close(self._master_fd) diff --git a/src/clide/widgets/panels/context.py b/clide/widgets/panels/context.py similarity index 99% rename from src/clide/widgets/panels/context.py rename to clide/widgets/panels/context.py index a05d6dbe..453d8329 100644 --- a/src/clide/widgets/panels/context.py +++ b/clide/widgets/panels/context.py @@ -1,6 +1,5 @@ """Context panel with Problems, TODOs, and Jira tabs.""" -from pathlib import Path from textual.app import ComposeResult from textual.containers import Horizontal, Vertical diff --git a/src/clide/widgets/panels/sidebar.py b/clide/widgets/panels/sidebar.py similarity index 90% rename from src/clide/widgets/panels/sidebar.py rename to clide/widgets/panels/sidebar.py index 9d2b8184..c84705be 100644 --- a/src/clide/widgets/panels/sidebar.py +++ b/clide/widgets/panels/sidebar.py @@ -6,7 +6,7 @@ from textual.app import ComposeResult from textual.containers import Container, Vertical from textual.message import Message from textual.reactive import reactive -from textual.widgets import Static, TabbedContent, TabPane +from textual.widgets import TabbedContent, TabPane from clide.widgets.components.branch_status import BranchStatus from clide.widgets.components.files_view import FilesView @@ -75,14 +75,13 @@ class SidebarPanel(Vertical): self.id = "panel-sidebar" def compose(self) -> ComposeResult: - with Container(classes="sidebar-content"): - with TabbedContent(id="sidebar-tabs"): - with TabPane("Files", id="sidebar-files"): - yield FilesView(path=self._workdir) - with TabPane("Git", id="sidebar-git"): - yield GitChangesView() - with TabPane("Tree", id="sidebar-tree"): - yield GitGraphView() + with Container(classes="sidebar-content"), TabbedContent(id="sidebar-tabs"): + with TabPane("Files", id="sidebar-files"): + yield FilesView(path=self._workdir) + with TabPane("Git", id="sidebar-git"): + yield GitChangesView() + with TabPane("Tree", id="sidebar-tree"): + yield GitGraphView() yield BranchStatus(current_branch=self.current_branch) def watch_visible(self, visible: bool) -> None: diff --git a/src/clide/widgets/panels/workspace.py b/clide/widgets/panels/workspace.py similarity index 100% rename from src/clide/widgets/panels/workspace.py rename to clide/widgets/panels/workspace.py diff --git a/pyproject.toml b/pyproject.toml index 6a99718d..4b56d7f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ clide = "clide.cli:app" # example = "clide.extensions.builtin.example:ExampleExtension" [tool.hatch.build.targets.wheel] -packages = ["src/clide"] +packages = ["clide"] [tool.pytest.ini_options] testpaths = ["tests"] @@ -75,7 +75,7 @@ exclude = ["tests", "dist", "build"] [tool.ruff] target-version = "py312" line-length = 100 -src = ["src", "tests"] +src = ["clide", "tests"] [tool.ruff.lint] select = [ @@ -100,7 +100,7 @@ ignore = [ known-first-party = ["clide"] [tool.coverage.run] -source = ["src/clide"] +source = ["clide"] branch = true [tool.coverage.report] diff --git a/tests/conftest.py b/tests/conftest.py index add8dac9..2be03a24 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,7 @@ """Shared pytest fixtures for Clide tests.""" +from collections.abc import AsyncGenerator, Generator from pathlib import Path -from typing import AsyncGenerator, Generator -from unittest.mock import MagicMock import pytest from textual.pilot import Pilot diff --git a/tests/harnesses/app_harness.py b/tests/harnesses/app_harness.py index 2d81d078..55754226 100644 --- a/tests/harnesses/app_harness.py +++ b/tests/harnesses/app_harness.py @@ -1,7 +1,6 @@ """Full application test harness for Clide.""" from pathlib import Path -from typing import Optional from textual.pilot import Pilot @@ -26,14 +25,14 @@ class AppHarness: def __init__( self, workdir: Path, - settings: Optional[ClideSettings] = None, - extension_manager: Optional[ExtensionManager] = None, + settings: ClideSettings | None = None, + extension_manager: ExtensionManager | None = None, ) -> None: self.workdir = workdir self.settings = settings or ClideSettings() self.extension_manager = extension_manager or ExtensionManager() - self._app: Optional[ClideApp] = None - self._pilot: Optional[Pilot] = None + self._app: ClideApp | None = None + self._pilot: Pilot | None = None async def start(self) -> tuple[ClideApp, Pilot]: """Start the application and return app and pilot for testing. diff --git a/tests/harnesses/controller_harness.py b/tests/harnesses/controller_harness.py index 700a4100..39b04126 100644 --- a/tests/harnesses/controller_harness.py +++ b/tests/harnesses/controller_harness.py @@ -1,6 +1,6 @@ """Controller isolation test harness for Clide.""" -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock from textual.message import Message @@ -19,7 +19,7 @@ class MockApp: def _capture_message(self, message: Message) -> None: self.messages.append(message) - def get_messages(self, message_type: Optional[type] = None) -> list[Message]: + def get_messages(self, message_type: type | None = None) -> list[Message]: """Get captured messages, optionally filtered by type.""" if message_type is None: return self.messages.copy() @@ -46,7 +46,7 @@ class ControllerHarness: def __init__(self) -> None: self._mock_app = MockApp() - self._controllers: list["BaseController"] = [] + self._controllers: list[BaseController] = [] self._mocks: dict[str, Any] = {} @property @@ -68,7 +68,7 @@ class ControllerHarness: for controller in self._controllers: await controller.shutdown() - def get_messages(self, message_type: Optional[type] = None) -> list[Message]: + def get_messages(self, message_type: type | None = None) -> list[Message]: """Get messages posted to the mock app.""" return self._mock_app.get_messages(message_type) diff --git a/tests/integration/test_files_view.py b/tests/integration/test_files_view.py index 446717d8..f6553ef2 100644 --- a/tests/integration/test_files_view.py +++ b/tests/integration/test_files_view.py @@ -4,7 +4,6 @@ from pathlib import Path import pytest from textual.app import App, ComposeResult -from textual.widgets import Static from clide.widgets.components.files_view import FilesView diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index cb617e29..e1769e02 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -1,9 +1,6 @@ """Tests for ClideApp.""" from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest from clide.models.config import ClideSettings diff --git a/tests/unit/test_controllers.py b/tests/unit/test_controllers.py index 7018be1a..a072f49c 100644 --- a/tests/unit/test_controllers.py +++ b/tests/unit/test_controllers.py @@ -1,7 +1,7 @@ """Tests for controller classes.""" from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import patch import pytest @@ -11,13 +11,11 @@ from clide.controllers.git import GitController from clide.controllers.jira import JiraController from clide.controllers.problems import ProblemsController from clide.controllers.todos import TodosController -from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine -from clide.models.editor import CursorPosition, FileBuffer +from clide.models.diff import DiffContent, DiffHunk +from clide.models.editor import FileBuffer from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitStatus -from clide.models.problems import Problem, ProblemsSummary, Severity -from clide.models.todos import TodoItem, TodosSummary, TodoType -from clide.services.git_service import GitService -from clide.services.process_service import CommandResult +from clide.models.problems import Problem, Severity +from clide.models.todos import TodoItem, TodoType class TestGitController: diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 62e8865b..e6bafc61 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -6,12 +6,12 @@ import pytest from pydantic import ValidationError from clide.models.config import ClideSettings, KeybindingsConfig, PanelConfig -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, ProblemsSummary, ProblemsState, Severity +from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine +from clide.models.editor import CursorPosition, EditorState, FileBuffer +from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitStatus +from clide.models.problems import Problem, ProblemsSummary, Severity from clide.models.theme import ThemeColors, ThemeDefinition, ThemeMetadata -from clide.models.todos import TodoItem, TodosSummary, TodosState, TodoType +from clide.models.todos import TodoItem, TodosSummary, TodoType class TestCursorPosition: diff --git a/tests/unit/test_panels.py b/tests/unit/test_panels.py index 13a2ebce..499339f1 100644 --- a/tests/unit/test_panels.py +++ b/tests/unit/test_panels.py @@ -1,12 +1,7 @@ """Tests for panel widgets.""" from pathlib import Path -from unittest.mock import MagicMock, patch -import pytest - -from clide.models.diff import DiffContent -from clide.models.git import GitBranch, GitChange from clide.models.problems import Problem, Severity from clide.models.todos import TodoItem, TodoType diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py index 478558b5..d127c7d5 100644 --- a/tests/unit/test_services.py +++ b/tests/unit/test_services.py @@ -1,14 +1,11 @@ """Tests for service classes.""" -import asyncio from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import patch import pytest -from clide.models.git import GitBranch, GitChange, GitCommit, GitStatus -from clide.models.problems import Problem, Severity -from clide.models.todos import TodoItem, TodoType +from clide.models.todos import TodoType from clide.services.file_service import FileService from clide.services.git_service import GitService from clide.services.linter_service import LinterService diff --git a/tests/unit/test_widgets.py b/tests/unit/test_widgets.py index be6fcb5f..a034e6f8 100644 --- a/tests/unit/test_widgets.py +++ b/tests/unit/test_widgets.py @@ -1,13 +1,11 @@ """Tests for widget components.""" from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock -import pytest - -from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine -from clide.models.editor import CursorPosition, FileBuffer -from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit +from clide.models.diff import DiffContent +from clide.models.editor import FileBuffer +from clide.models.git import ChangeStatus, GitChange, GitCommit from clide.models.problems import Problem, Severity from clide.models.todos import TodoItem, TodoType