"""
Unified Path Cache System
Provides shared path caching functionality for both TUI and PyQt GUI implementations.
Persists last used paths across application runs for improved user experience.
"""
import json
import logging
from pathlib import Path
from typing import Dict, Optional
from enum import Enum
logger = logging.getLogger(__name__)
[docs]
class PathCacheKey(Enum):
"""
Enumeration of path cache keys for different UI contexts.
Used to maintain separate cached paths for different file operations
across both TUI and PyQt GUI implementations.
"""
# Original keys from both implementations
FILE_SELECTION = "file_selection"
DIRECTORY_SELECTION = "directory_selection"
PLATE_IMPORT = "plate_import"
CONFIG_EXPORT = "config_export"
GENERAL = "general"
# Specific file type contexts
FUNCTION_PATTERNS = "function_patterns" # .func files
PIPELINE_FILES = "pipeline_files" # .pipeline files
STEP_SETTINGS = "step_settings" # .step files
DEBUG_FILES = "debug_files" # .pkl debug files
CODE_EDITOR = "code_editor" # .py files from code editor
# Additional contexts for future use
PLATE_BROWSER = "plate_browser"
FUNCTION_BROWSER = "function_browser"
PIPELINE_BROWSER = "pipeline_browser"
EXPORT_BROWSER = "export_browser"
CONFIG_BROWSER = "config_browser"
ANALYSIS_BROWSER = "analysis_browser"
[docs]
class UnifiedPathCache:
"""
Unified path cache for persisting directory paths across application sessions.
Provides consistent caching behavior for both TUI browser widgets and
PyQt GUI file dialogs.
"""
[docs]
def __init__(self, cache_file: Optional[Path] = None):
"""
Initialize path cache.
Args:
cache_file: Optional custom cache file location
"""
if cache_file is None:
from pyqt_reactive.protocols import get_form_config
config = get_form_config()
if getattr(config, "path_cache_file", None):
cache_file = Path(config.path_cache_file)
else:
cache_file = Path.home() / ".cache" / "pyqt_reactive" / "path_cache.json"
self.cache_file = cache_file
self._cache: Dict[str, str] = {}
self._load_cache()
logger.debug(f"UnifiedPathCache initialized with cache file: {self.cache_file}")
def _load_cache(self) -> None:
"""Load cache from disk."""
try:
if self.cache_file.exists():
with open(self.cache_file, 'r') as f:
self._cache = json.load(f)
logger.debug(f"Loaded path cache with {len(self._cache)} entries")
else:
logger.debug("No existing path cache found, starting fresh")
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"Failed to load path cache: {e}")
self._cache = {}
def _save_cache(self) -> None:
"""Save cache to disk."""
try:
# Ensure cache directory exists
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.cache_file, 'w') as f:
json.dump(self._cache, f, indent=2)
logger.debug(f"Saved path cache with {len(self._cache)} entries")
except OSError as e:
logger.warning(f"Failed to save path cache: {e}")
[docs]
def get_cached_path(self, key: PathCacheKey) -> Optional[Path]:
"""
Get cached path for a specific key.
Args:
key: PathCacheKey identifying the context
Returns:
Cached Path if exists and valid, None otherwise
"""
cached_str = self._cache.get(key.value)
if cached_str:
cached_path = Path(cached_str)
if cached_path.exists():
logger.debug(f"Retrieved cached path for {key.value}: {cached_path}")
return cached_path
else:
# Remove invalid cached path
logger.debug(f"Removing invalid cached path for {key.value}: {cached_path}")
del self._cache[key.value]
self._save_cache()
return None
[docs]
def set_cached_path(self, key: PathCacheKey, path: Path) -> None:
"""
Set cached path for a specific key.
Args:
key: PathCacheKey identifying the context
path: Path to cache
"""
if path and path.exists():
self._cache[key.value] = str(path)
self._save_cache()
logger.debug(f"Cached path for {key.value}: {path}")
else:
logger.warning(f"Attempted to cache non-existent path for {key.value}: {path}")
[docs]
def get_initial_path(self, key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
"""
Get initial path with intelligent fallback hierarchy.
Args:
key: PathCacheKey identifying the context
fallback: Optional fallback path if cached path unavailable
Returns:
Best available path (cached > fallback > home directory)
"""
# Try cached path first
cached = self.get_cached_path(key)
if cached:
return cached
# Try fallback
if fallback and fallback.exists():
return fallback
# Ultimate fallback to home directory
return Path.home()
[docs]
def clear_cache(self) -> None:
"""Clear all cached paths."""
self._cache.clear()
self._save_cache()
logger.info("Cleared all cached paths")
[docs]
def remove_cached_path(self, key: PathCacheKey) -> None:
"""
Remove specific cached path.
Args:
key: PathCacheKey to remove
"""
if key.value in self._cache:
del self._cache[key.value]
self._save_cache()
logger.debug(f"Removed cached path for {key.value}")
# Global cache instance
_global_path_cache: Optional[UnifiedPathCache] = None
[docs]
def get_path_cache() -> UnifiedPathCache:
"""Get global path cache instance."""
global _global_path_cache
if _global_path_cache is None:
_global_path_cache = UnifiedPathCache()
return _global_path_cache
[docs]
def cache_path(key: PathCacheKey, path: Path) -> None:
"""
Convenience function to cache a path.
Args:
key: PathCacheKey identifying the context
path: Path to cache
"""
get_path_cache().set_cached_path(key, path)
[docs]
def get_cached_path(key: PathCacheKey) -> Optional[Path]:
"""
Convenience function to get cached path.
Args:
key: PathCacheKey identifying the context
Returns:
Cached Path if exists and valid, None otherwise
"""
return get_path_cache().get_cached_path(key)
[docs]
def get_initial_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
"""
Convenience function to get initial path with fallback.
Args:
key: PathCacheKey identifying the context
fallback: Optional fallback path
Returns:
Best available path (cached > fallback > home directory)
"""
return get_path_cache().get_initial_path(key, fallback)
# Backward compatibility aliases for existing code
[docs]
def cache_browser_path(key: PathCacheKey, path: Path) -> None:
"""Backward compatibility alias for TUI code."""
cache_path(key, path)
[docs]
def cache_dialog_path(key: PathCacheKey, path: Path) -> None:
"""Backward compatibility alias for PyQt code."""
cache_path(key, path)
[docs]
def get_cached_browser_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
"""Backward compatibility alias for TUI code."""
return get_initial_path(key, fallback)
[docs]
def get_cached_dialog_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
"""Backward compatibility alias for PyQt code."""
return get_initial_path(key, fallback)