Restructure project and add user settings persistence
- Move src/clide/ to clide/ (flatten directory structure) - Add SettingsService for persistent user preferences - Settings stored in ~/.clide/settings.json - Persist theme, panel visibility, and compact mode state - Fix terminal rendering glitch with throttled refresh (~60fps) - Update pyproject.toml, Makefile, and tests for new structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
6b7c3ecf9b
commit
1ed21d2a0f
@@ -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
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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")
|
||||
@@ -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:
|
||||
@@ -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",
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Git-related Pydantic models."""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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__ = [
|
||||
@@ -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__ = [
|
||||
@@ -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
|
||||
@@ -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
|
||||
+1
-1
@@ -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)
|
||||
@@ -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
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -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):
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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:
|
||||
+3
-3
@@ -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]
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user