refactor stable unthorough
- SHA
057ca7f6921a703ea7844dc73c405f9f08b25b17- Parents
-
9309653 - Tree
6c70249
057ca7f
057ca7f6921a703ea7844dc73c405f9f08b25b179309653
6c70249| Status | File | + | - |
|---|---|---|---|
| A |
settings.sample.toml
|
25 | 0 |
| M |
src/shtick/cli.py
|
32 | 0 |
| M |
src/shtick/commands.py
|
138 | 0 |
| M |
src/shtick/config.py
|
58 | 8 |
| M |
src/shtick/generator.py
|
22 | 2 |
| A |
src/shtick/settings.py
|
200 | 0 |
| M |
src/shtick/shtick.py
|
113 | 7 |
settings.sample.tomladded@@ -0,0 +1,25 @@ | ||
| 1 | +[generation] | |
| 2 | +# Shells to generate files for. Empty list = auto-detect based on current shell | |
| 3 | +shells = ["bash", "dash", "zsh", "nushell", "powershell", "yash", "fish", "oil", "elvish"] | |
| 4 | +# Enable parallel generation (not implemented yet) | |
| 5 | +parallel = false | |
| 6 | +# Consolidate all items into single files per shell | |
| 7 | +consolidate_files = true | |
| 8 | + | |
| 9 | +[behavior] | |
| 10 | +# Prompt to source changes after modifications | |
| 11 | +auto_source_prompt = true | |
| 12 | +# Check for conflicts when adding items | |
| 13 | +check_conflicts = true | |
| 14 | +# Create backups before saving config | |
| 15 | +backup_on_save = false | |
| 16 | +# Enable interactive prompts | |
| 17 | +interactive_mode = true | |
| 18 | + | |
| 19 | +[performance] | |
| 20 | +# Cache time-to-live in seconds | |
| 21 | +cache_ttl = 300 | |
| 22 | +# Enable lazy loading (not implemented yet) | |
| 23 | +lazy_load = false | |
| 24 | +# Enable batch operations for better performance | |
| 25 | +batch_operations = true | |
src/shtick/cli.pymodified@@ -124,6 +124,29 @@ def main(): | ||
| 124 | 124 | "--shell", help="Specify shell type (auto-detected if not provided)" |
| 125 | 125 | ) |
| 126 | 126 | |
| 127 | + # Settings command | |
| 128 | + settings_parser = subparsers.add_parser("settings", help="Manage shtick settings") | |
| 129 | + settings_subparsers = settings_parser.add_subparsers( | |
| 130 | + dest="settings_command", help="Settings commands" | |
| 131 | + ) | |
| 132 | + | |
| 133 | + # Settings subcommands | |
| 134 | + settings_init_parser = settings_subparsers.add_parser( | |
| 135 | + "init", help="Create default settings file" | |
| 136 | + ) | |
| 137 | + | |
| 138 | + settings_show_parser = settings_subparsers.add_parser( | |
| 139 | + "show", help="Show current settings" | |
| 140 | + ) | |
| 141 | + | |
| 142 | + settings_set_parser = settings_subparsers.add_parser( | |
| 143 | + "set", help="Set a setting value" | |
| 144 | + ) | |
| 145 | + settings_set_parser.add_argument( | |
| 146 | + "key", help="Setting key (e.g., generation.shells)" | |
| 147 | + ) | |
| 148 | + settings_set_parser.add_argument("value", help="Setting value") | |
| 149 | + | |
| 127 | 150 | args = parser.parse_args() |
| 128 | 151 | |
| 129 | 152 | # Set up logging first |
@@ -172,6 +195,15 @@ def main(): | ||
| 172 | 195 | display.shells(args.long) |
| 173 | 196 | elif args.command == "source": |
| 174 | 197 | commands.source_command(args.shell) |
| 198 | + elif args.command == "settings": | |
| 199 | + if args.settings_command == "init": | |
| 200 | + commands.settings_init() | |
| 201 | + elif args.settings_command == "show": | |
| 202 | + commands.settings_show() | |
| 203 | + elif args.settings_command == "set": | |
| 204 | + commands.settings_set(args.key, args.value) | |
| 205 | + else: | |
| 206 | + settings_parser.print_help() | |
| 175 | 207 | |
| 176 | 208 | except KeyboardInterrupt: |
| 177 | 209 | logger.debug("Operation cancelled by user") |
src/shtick/commands.pymodified@@ -54,6 +54,13 @@ class ShtickCommands: | ||
| 54 | 54 | |
| 55 | 55 | def offer_auto_source(self): |
| 56 | 56 | """Offer to source shtick in current shell session""" |
| 57 | + # Check settings first | |
| 58 | + from shtick.settings import Settings | |
| 59 | + | |
| 60 | + settings = Settings() | |
| 61 | + if not settings.behavior.auto_source_prompt: | |
| 62 | + return | |
| 63 | + | |
| 57 | 64 | current_shell = self.get_current_shell() |
| 58 | 65 | if not current_shell or current_shell not in ["bash", "zsh", "fish"]: |
| 59 | 66 | return |
@@ -403,3 +410,134 @@ class ShtickCommands: | ||
| 403 | 410 | |
| 404 | 411 | # Output the source command that can be eval'd |
| 405 | 412 | print(f"source {loader_path}") |
| 413 | + | |
| 414 | + # Settings commands | |
| 415 | + def settings_init(self): | |
| 416 | + """Initialize settings file with defaults""" | |
| 417 | + from shtick.settings import Settings | |
| 418 | + | |
| 419 | + settings = Settings() | |
| 420 | + | |
| 421 | + if os.path.exists(settings._settings_path): | |
| 422 | + try: | |
| 423 | + response = ( | |
| 424 | + input("Settings file already exists. Overwrite? [y/N]: ") | |
| 425 | + .strip() | |
| 426 | + .lower() | |
| 427 | + ) | |
| 428 | + if response not in ["y", "yes"]: | |
| 429 | + print("Cancelled") | |
| 430 | + return | |
| 431 | + except (KeyboardInterrupt, EOFError): | |
| 432 | + print("\nCancelled") | |
| 433 | + return | |
| 434 | + | |
| 435 | + settings.create_default_settings_file() | |
| 436 | + print(f"✓ Created settings file at {settings._settings_path}") | |
| 437 | + print("\nYou can now customize your shtick behavior by editing this file.") | |
| 438 | + | |
| 439 | + def settings_show(self): | |
| 440 | + """Show current settings""" | |
| 441 | + from shtick.settings import Settings | |
| 442 | + | |
| 443 | + settings = Settings() | |
| 444 | + | |
| 445 | + print("Shtick Settings") | |
| 446 | + print("=" * 50) | |
| 447 | + | |
| 448 | + print("\n[generation]") | |
| 449 | + print(f" shells = {settings.generation.shells or '[] (auto-detect)'}") | |
| 450 | + print(f" parallel = {settings.generation.parallel}") | |
| 451 | + print(f" consolidate_files = {settings.generation.consolidate_files}") | |
| 452 | + | |
| 453 | + print("\n[behavior]") | |
| 454 | + print(f" auto_source_prompt = {settings.behavior.auto_source_prompt}") | |
| 455 | + print(f" check_conflicts = {settings.behavior.check_conflicts}") | |
| 456 | + print(f" backup_on_save = {settings.behavior.backup_on_save}") | |
| 457 | + print(f" interactive_mode = {settings.behavior.interactive_mode}") | |
| 458 | + | |
| 459 | + print("\n[performance]") | |
| 460 | + print(f" cache_ttl = {settings.performance.cache_ttl}") | |
| 461 | + print(f" lazy_load = {settings.performance.lazy_load}") | |
| 462 | + print(f" batch_operations = {settings.performance.batch_operations}") | |
| 463 | + | |
| 464 | + print(f"\nSettings file: {settings._settings_path}") | |
| 465 | + if not os.path.exists(settings._settings_path): | |
| 466 | + print("(No settings file found - using defaults)") | |
| 467 | + print("Run 'shtick settings init' to create one") | |
| 468 | + | |
| 469 | + def settings_set(self, key: str, value: str): | |
| 470 | + """Set a specific setting value""" | |
| 471 | + from shtick.settings import Settings | |
| 472 | + | |
| 473 | + settings = Settings() | |
| 474 | + | |
| 475 | + # Parse the key (e.g., "generation.shells") | |
| 476 | + parts = key.split(".") | |
| 477 | + if len(parts) != 2: | |
| 478 | + print( | |
| 479 | + f"Error: Invalid key format. Use 'section.key' (e.g., 'generation.shells')" | |
| 480 | + ) | |
| 481 | + sys.exit(1) | |
| 482 | + | |
| 483 | + section, setting_key = parts | |
| 484 | + | |
| 485 | + # Validate section | |
| 486 | + if section not in ["generation", "behavior", "performance"]: | |
| 487 | + print( | |
| 488 | + f"Error: Invalid section '{section}'. Must be one of: generation, behavior, performance" | |
| 489 | + ) | |
| 490 | + sys.exit(1) | |
| 491 | + | |
| 492 | + # Get the section object | |
| 493 | + section_obj = getattr(settings, section) | |
| 494 | + | |
| 495 | + # Check if key exists | |
| 496 | + if not hasattr(section_obj, setting_key): | |
| 497 | + print(f"Error: Invalid key '{setting_key}' for section '{section}'") | |
| 498 | + print(f"Valid keys: {', '.join(vars(section_obj).keys())}") | |
| 499 | + sys.exit(1) | |
| 500 | + | |
| 501 | + # Parse the value based on type | |
| 502 | + current_value = getattr(section_obj, setting_key) | |
| 503 | + try: | |
| 504 | + if isinstance(current_value, bool): | |
| 505 | + # Parse boolean | |
| 506 | + if value.lower() in ["true", "1", "yes", "on"]: | |
| 507 | + parsed_value = True | |
| 508 | + elif value.lower() in ["false", "0", "no", "off"]: | |
| 509 | + parsed_value = False | |
| 510 | + else: | |
| 511 | + raise ValueError(f"Invalid boolean value: {value}") | |
| 512 | + elif isinstance(current_value, int): | |
| 513 | + # Parse integer | |
| 514 | + parsed_value = int(value) | |
| 515 | + elif isinstance(current_value, list): | |
| 516 | + # Parse list (simple eval for now - could be improved) | |
| 517 | + if value == "[]": | |
| 518 | + parsed_value = [] | |
| 519 | + elif value.startswith("[") and value.endswith("]"): | |
| 520 | + # Simple parsing - just split by comma | |
| 521 | + parsed_value = [ | |
| 522 | + s.strip().strip("\"'") | |
| 523 | + for s in value[1:-1].split(",") | |
| 524 | + if s.strip() | |
| 525 | + ] | |
| 526 | + else: | |
| 527 | + # Single value becomes a list | |
| 528 | + parsed_value = [value] | |
| 529 | + else: | |
| 530 | + # String value | |
| 531 | + parsed_value = value | |
| 532 | + except Exception as e: | |
| 533 | + print(f"Error parsing value: {e}") | |
| 534 | + sys.exit(1) | |
| 535 | + | |
| 536 | + # Set the value | |
| 537 | + setattr(section_obj, setting_key, parsed_value) | |
| 538 | + | |
| 539 | + # Save settings | |
| 540 | + settings.save() | |
| 541 | + | |
| 542 | + print(f"✓ Set {key} = {parsed_value}") | |
| 543 | + print(f"Settings saved to {settings._settings_path}") | |
src/shtick/config.pymodified@@ -61,8 +61,11 @@ class GroupData: | ||
| 61 | 61 | class Config: |
| 62 | 62 | """Main configuration handler""" |
| 63 | 63 | |
| 64 | - # Class variable for shell detection caching | |
| 64 | + # Class variables for caching | |
| 65 | 65 | _detected_shell = None |
| 66 | + _active_groups_cache = None | |
| 67 | + _active_groups_mtime = None | |
| 68 | + _active_groups_file_path = None | |
| 66 | 69 | |
| 67 | 70 | def __init__(self, config_path: Optional[str] = None): |
| 68 | 71 | self.config_path = config_path or self.get_default_config_path() |
@@ -96,14 +99,44 @@ class Config: | ||
| 96 | 99 | """Clear the cached shell detection (useful for testing)""" |
| 97 | 100 | cls._detected_shell = None |
| 98 | 101 | |
| 102 | + @classmethod | |
| 103 | + def clear_all_caches(cls): | |
| 104 | + """Clear all caches (useful for testing or forced refresh)""" | |
| 105 | + cls._detected_shell = None | |
| 106 | + cls._active_groups_cache = None | |
| 107 | + cls._active_groups_mtime = None | |
| 108 | + cls._active_groups_file_path = None | |
| 109 | + | |
| 99 | 110 | def load_active_groups(self) -> List[str]: |
| 100 | - """Load list of currently active groups""" | |
| 111 | + """Load list of currently active groups with caching""" | |
| 101 | 112 | active_file = self.get_active_groups_file() |
| 102 | - if not os.path.exists(active_file): | |
| 103 | - return [] | |
| 104 | 113 | |
| 105 | - with open(active_file, "r") as f: | |
| 106 | - return [line.strip() for line in f if line.strip()] | |
| 114 | + # Check if we need to reload from disk | |
| 115 | + if os.path.exists(active_file): | |
| 116 | + current_mtime = os.path.getmtime(active_file) | |
| 117 | + if ( | |
| 118 | + self._active_groups_cache is None | |
| 119 | + or self._active_groups_file_path != active_file | |
| 120 | + or self._active_groups_mtime != current_mtime | |
| 121 | + ): | |
| 122 | + | |
| 123 | + logger.debug(f"Reloading active groups from {active_file}") | |
| 124 | + with open(active_file, "r") as f: | |
| 125 | + self._active_groups_cache = [ | |
| 126 | + line.strip() for line in f if line.strip() | |
| 127 | + ] | |
| 128 | + self._active_groups_mtime = current_mtime | |
| 129 | + self._active_groups_file_path = active_file | |
| 130 | + else: | |
| 131 | + logger.debug("Using cached active groups") | |
| 132 | + else: | |
| 133 | + # File doesn't exist, return empty list | |
| 134 | + self._active_groups_cache = [] | |
| 135 | + self._active_groups_mtime = None | |
| 136 | + | |
| 137 | + return ( | |
| 138 | + self._active_groups_cache.copy() | |
| 139 | + ) # Return a copy to prevent external modification | |
| 107 | 140 | |
| 108 | 141 | def save_active_groups(self, active_groups: List[str]) -> None: |
| 109 | 142 | """Save list of active groups to state file""" |
@@ -114,6 +147,11 @@ class Config: | ||
| 114 | 147 | for group in active_groups: |
| 115 | 148 | f.write(f"{group}\n") |
| 116 | 149 | |
| 150 | + # Invalidate cache by updating mtime | |
| 151 | + self._active_groups_cache = active_groups.copy() | |
| 152 | + self._active_groups_mtime = os.path.getmtime(active_file) | |
| 153 | + self._active_groups_file_path = active_file | |
| 154 | + | |
| 117 | 155 | def activate_group(self, group_name: str) -> bool: |
| 118 | 156 | """Activate a group. Returns True if successful.""" |
| 119 | 157 | # Check if group exists |
@@ -284,10 +322,19 @@ class Config: | ||
| 284 | 322 | |
| 285 | 323 | def get_all_shells_to_generate(self) -> List[str]: |
| 286 | 324 | """Get list of shells to generate files for based on user settings""" |
| 287 | - # For now, just detect current shell and common ones | |
| 288 | - # Later this can read from settings.toml | |
| 325 | + from .settings import Settings | |
| 326 | + | |
| 327 | + settings = Settings() | |
| 328 | + | |
| 329 | + # If shells are explicitly set in settings, use those | |
| 330 | + if settings.generation.shells: | |
| 331 | + logger.debug(f"Using shells from settings: {settings.generation.shells}") | |
| 332 | + return settings.generation.shells | |
| 333 | + | |
| 334 | + # Otherwise auto-detect based on current shell | |
| 289 | 335 | current_shell = self.get_current_shell() |
| 290 | 336 | if not current_shell: |
| 337 | + logger.debug("No current shell detected, using defaults") | |
| 291 | 338 | return ["bash", "zsh", "fish"] # Common defaults |
| 292 | 339 | |
| 293 | 340 | # Include current shell and close relatives |
@@ -300,4 +347,7 @@ class Config: | ||
| 300 | 347 | } |
| 301 | 348 | |
| 302 | 349 | shells = shell_families.get(current_shell, [current_shell]) |
| 350 | + logger.debug( | |
| 351 | + f"Auto-detected shells based on current shell '{current_shell}': {shells}" | |
| 352 | + ) | |
| 303 | 353 | return list(set(shells)) # Remove duplicates |
src/shtick/generator.pymodified@@ -19,16 +19,30 @@ class Generator: | ||
| 19 | 19 | self.output_base_dir = output_base_dir or Config.get_output_dir() |
| 20 | 20 | # Get shells to generate for once |
| 21 | 21 | self._shells_to_generate = None |
| 22 | + self._config_for_shells = None | |
| 22 | 23 | |
| 23 | 24 | @property |
| 24 | 25 | def shells_to_generate(self) -> List[str]: |
| 25 | 26 | """Get list of shells to generate files for""" |
| 26 | 27 | if self._shells_to_generate is None: |
| 27 | - config = Config() | |
| 28 | - self._shells_to_generate = config.get_all_shells_to_generate() | |
| 28 | + # If we have a config instance stored, use it | |
| 29 | + if self._config_for_shells: | |
| 30 | + self._shells_to_generate = ( | |
| 31 | + self._config_for_shells.get_all_shells_to_generate() | |
| 32 | + ) | |
| 33 | + else: | |
| 34 | + # Otherwise create a temporary one just for getting shells | |
| 35 | + config = Config() | |
| 36 | + self._shells_to_generate = config.get_all_shells_to_generate() | |
| 29 | 37 | logger.debug(f"Will generate files for shells: {self._shells_to_generate}") |
| 30 | 38 | return self._shells_to_generate |
| 31 | 39 | |
| 40 | + def set_config_for_shells(self, config: Config) -> None: | |
| 41 | + """Set config instance to use for shell detection""" | |
| 42 | + self._config_for_shells = config | |
| 43 | + # Clear cached shells to force re-read | |
| 44 | + self._shells_to_generate = None | |
| 45 | + | |
| 32 | 46 | def ensure_output_dir(self, group_name: str) -> str: |
| 33 | 47 | """Ensure output directory exists and return the path""" |
| 34 | 48 | output_dir = os.path.join(self.output_base_dir, group_name) |
@@ -118,6 +132,9 @@ class Generator: | ||
| 118 | 132 | print("No groups found in configuration") |
| 119 | 133 | return |
| 120 | 134 | |
| 135 | + # Set config for shell detection FIRST | |
| 136 | + self.set_config_for_shells(config) | |
| 137 | + | |
| 121 | 138 | print(f"Generating shell files for {len(config.groups)} groups...") |
| 122 | 139 | print(f"Target shells: {', '.join(self.shells_to_generate)}") |
| 123 | 140 | |
@@ -136,6 +153,9 @@ class Generator: | ||
| 136 | 153 | """Generate dynamic loader files that source persistent + active groups""" |
| 137 | 154 | logger.info("Generating dynamic loader files...") |
| 138 | 155 | |
| 156 | + # Set config for shell detection | |
| 157 | + self.set_config_for_shells(config) | |
| 158 | + | |
| 139 | 159 | active_groups = config.load_active_groups() |
| 140 | 160 | persistent_group = config.get_persistent_group() |
| 141 | 161 | |
src/shtick/settings.pyadded@@ -0,0 +1,200 @@ | ||
| 1 | +""" | |
| 2 | +Settings management for shtick | |
| 3 | +""" | |
| 4 | + | |
| 5 | +import os | |
| 6 | +import tomllib | |
| 7 | +import logging | |
| 8 | +from pathlib import Path | |
| 9 | +from typing import Dict, List, Optional, Any | |
| 10 | +from dataclasses import dataclass, field | |
| 11 | + | |
| 12 | +logger = logging.getLogger("shtick") | |
| 13 | + | |
| 14 | + | |
| 15 | +@dataclass | |
| 16 | +class GenerationSettings: | |
| 17 | + """Settings for file generation""" | |
| 18 | + | |
| 19 | + shells: List[str] = field(default_factory=list) # Empty = auto-detect | |
| 20 | + parallel: bool = False # Threading not implemented yet | |
| 21 | + consolidate_files: bool = True | |
| 22 | + | |
| 23 | + | |
| 24 | +@dataclass | |
| 25 | +class BehaviorSettings: | |
| 26 | + """Settings for shtick behavior""" | |
| 27 | + | |
| 28 | + auto_source_prompt: bool = True | |
| 29 | + check_conflicts: bool = True | |
| 30 | + backup_on_save: bool = False | |
| 31 | + interactive_mode: bool = True | |
| 32 | + | |
| 33 | + | |
| 34 | +@dataclass | |
| 35 | +class PerformanceSettings: | |
| 36 | + """Settings for performance optimization""" | |
| 37 | + | |
| 38 | + cache_ttl: int = 300 # 5 minutes | |
| 39 | + lazy_load: bool = False # Not implemented yet | |
| 40 | + batch_operations: bool = True | |
| 41 | + | |
| 42 | + | |
| 43 | +class Settings: | |
| 44 | + """Manages shtick settings and preferences""" | |
| 45 | + | |
| 46 | + # Singleton instance | |
| 47 | + _instance = None | |
| 48 | + _loaded = False | |
| 49 | + | |
| 50 | + def __new__(cls): | |
| 51 | + if cls._instance is None: | |
| 52 | + cls._instance = super().__new__(cls) | |
| 53 | + return cls._instance | |
| 54 | + | |
| 55 | + def __init__(self): | |
| 56 | + if not self._loaded: | |
| 57 | + self.generation = GenerationSettings() | |
| 58 | + self.behavior = BehaviorSettings() | |
| 59 | + self.performance = PerformanceSettings() | |
| 60 | + self._settings_path = self.get_settings_path() | |
| 61 | + self._load() | |
| 62 | + Settings._loaded = True | |
| 63 | + | |
| 64 | + @staticmethod | |
| 65 | + def get_settings_path() -> str: | |
| 66 | + """Get the settings file path""" | |
| 67 | + return os.path.expanduser("~/.config/shtick/settings.toml") | |
| 68 | + | |
| 69 | + def _load(self) -> None: | |
| 70 | + """Load settings from file if it exists""" | |
| 71 | + if not os.path.exists(self._settings_path): | |
| 72 | + logger.debug("No settings file found, using defaults") | |
| 73 | + return | |
| 74 | + | |
| 75 | + try: | |
| 76 | + logger.debug(f"Loading settings from {self._settings_path}") | |
| 77 | + with open(self._settings_path, "rb") as f: | |
| 78 | + data = tomllib.load(f) | |
| 79 | + | |
| 80 | + # Load generation settings | |
| 81 | + if "generation" in data: | |
| 82 | + gen_data = data["generation"] | |
| 83 | + self.generation.shells = gen_data.get("shells", []) | |
| 84 | + self.generation.parallel = gen_data.get("parallel", False) | |
| 85 | + self.generation.consolidate_files = gen_data.get( | |
| 86 | + "consolidate_files", True | |
| 87 | + ) | |
| 88 | + | |
| 89 | + # Load behavior settings | |
| 90 | + if "behavior" in data: | |
| 91 | + beh_data = data["behavior"] | |
| 92 | + self.behavior.auto_source_prompt = beh_data.get( | |
| 93 | + "auto_source_prompt", True | |
| 94 | + ) | |
| 95 | + self.behavior.check_conflicts = beh_data.get("check_conflicts", True) | |
| 96 | + self.behavior.backup_on_save = beh_data.get("backup_on_save", False) | |
| 97 | + self.behavior.interactive_mode = beh_data.get("interactive_mode", True) | |
| 98 | + | |
| 99 | + # Load performance settings | |
| 100 | + if "performance" in data: | |
| 101 | + perf_data = data["performance"] | |
| 102 | + self.performance.cache_ttl = perf_data.get("cache_ttl", 300) | |
| 103 | + self.performance.lazy_load = perf_data.get("lazy_load", False) | |
| 104 | + self.performance.batch_operations = perf_data.get( | |
| 105 | + "batch_operations", True | |
| 106 | + ) | |
| 107 | + | |
| 108 | + logger.debug("Settings loaded successfully") | |
| 109 | + | |
| 110 | + except Exception as e: | |
| 111 | + logger.warning(f"Failed to load settings: {e}, using defaults") | |
| 112 | + | |
| 113 | + def save(self) -> None: | |
| 114 | + """Save current settings to file""" | |
| 115 | + # Ensure directory exists | |
| 116 | + os.makedirs(os.path.dirname(self._settings_path), exist_ok=True) | |
| 117 | + | |
| 118 | + # Build settings dictionary | |
| 119 | + settings_dict = { | |
| 120 | + "generation": { | |
| 121 | + "shells": self.generation.shells, | |
| 122 | + "parallel": self.generation.parallel, | |
| 123 | + "consolidate_files": self.generation.consolidate_files, | |
| 124 | + }, | |
| 125 | + "behavior": { | |
| 126 | + "auto_source_prompt": self.behavior.auto_source_prompt, | |
| 127 | + "check_conflicts": self.behavior.check_conflicts, | |
| 128 | + "backup_on_save": self.behavior.backup_on_save, | |
| 129 | + "interactive_mode": self.behavior.interactive_mode, | |
| 130 | + }, | |
| 131 | + "performance": { | |
| 132 | + "cache_ttl": self.performance.cache_ttl, | |
| 133 | + "lazy_load": self.performance.lazy_load, | |
| 134 | + "batch_operations": self.performance.batch_operations, | |
| 135 | + }, | |
| 136 | + } | |
| 137 | + | |
| 138 | + # Write TOML file | |
| 139 | + with open(self._settings_path, "w") as f: | |
| 140 | + f.write("# Shtick settings file\n") | |
| 141 | + f.write("# Generated automatically - edit as needed\n\n") | |
| 142 | + | |
| 143 | + for section, values in settings_dict.items(): | |
| 144 | + f.write(f"[{section}]\n") | |
| 145 | + for key, value in values.items(): | |
| 146 | + if isinstance(value, bool): | |
| 147 | + f.write(f"{key} = {str(value).lower()}\n") | |
| 148 | + elif isinstance(value, list): | |
| 149 | + if value: # Non-empty list | |
| 150 | + f.write(f"{key} = {repr(value)}\n") | |
| 151 | + else: | |
| 152 | + f.write(f"{key} = [] # Empty = auto-detect\n") | |
| 153 | + else: | |
| 154 | + f.write(f"{key} = {value}\n") | |
| 155 | + f.write("\n") | |
| 156 | + | |
| 157 | + def create_default_settings_file(self) -> None: | |
| 158 | + """Create a default settings file with comments""" | |
| 159 | + os.makedirs(os.path.dirname(self._settings_path), exist_ok=True) | |
| 160 | + | |
| 161 | + content = """# Shtick settings file | |
| 162 | +# This file controls various shtick behaviors and optimizations | |
| 163 | + | |
| 164 | +[generation] | |
| 165 | +# Shells to generate files for. Empty list = auto-detect based on current shell | |
| 166 | +shells = [] | |
| 167 | +# Enable parallel generation (not implemented yet) | |
| 168 | +parallel = false | |
| 169 | +# Consolidate all items into single files per shell | |
| 170 | +consolidate_files = true | |
| 171 | + | |
| 172 | +[behavior] | |
| 173 | +# Prompt to source changes after modifications | |
| 174 | +auto_source_prompt = true | |
| 175 | +# Check for conflicts when adding items | |
| 176 | +check_conflicts = true | |
| 177 | +# Create backups before saving config | |
| 178 | +backup_on_save = false | |
| 179 | +# Enable interactive prompts | |
| 180 | +interactive_mode = true | |
| 181 | + | |
| 182 | +[performance] | |
| 183 | +# Cache time-to-live in seconds | |
| 184 | +cache_ttl = 300 | |
| 185 | +# Enable lazy loading (not implemented yet) | |
| 186 | +lazy_load = false | |
| 187 | +# Enable batch operations for better performance | |
| 188 | +batch_operations = true | |
| 189 | +""" | |
| 190 | + | |
| 191 | + with open(self._settings_path, "w") as f: | |
| 192 | + f.write(content) | |
| 193 | + | |
| 194 | + logger.info(f"Created default settings file at {self._settings_path}") | |
| 195 | + | |
| 196 | + @classmethod | |
| 197 | + def reset(cls): | |
| 198 | + """Reset the singleton instance (useful for testing)""" | |
| 199 | + cls._instance = None | |
| 200 | + cls._loaded = False | |
src/shtick/shtick.pymodified@@ -40,14 +40,12 @@ class ShtickManager: | ||
| 40 | 40 | debug: Enable debug output |
| 41 | 41 | """ |
| 42 | 42 | self.config_path = config_path or Config.get_default_config_path() |
| 43 | + self.debug = debug | |
| 43 | 44 | |
| 44 | - # Set up logging | |
| 45 | - if debug: | |
| 46 | - logging.basicConfig( | |
| 47 | - level=logging.DEBUG, format="%(name)s - %(levelname)s - %(message)s" | |
| 48 | - ) | |
| 49 | - else: | |
| 50 | - logging.basicConfig(level=logging.INFO, format="%(message)s") | |
| 45 | + # Set up logging using centralized setup | |
| 46 | + from .logger import setup_logging | |
| 47 | + | |
| 48 | + self.logger = setup_logging(debug=debug) | |
| 51 | 49 | |
| 52 | 50 | self._config = None |
| 53 | 51 | self._generator = Generator() |
@@ -269,6 +267,13 @@ class ShtickManager: | ||
| 269 | 267 | try: |
| 270 | 268 | config = self._get_config() |
| 271 | 269 | |
| 270 | + # Use settings if check_conflicts not explicitly set | |
| 271 | + if check_conflicts is None: | |
| 272 | + from .settings import Settings | |
| 273 | + | |
| 274 | + settings = Settings() | |
| 275 | + check_conflicts = settings.behavior.check_conflicts | |
| 276 | + | |
| 272 | 277 | # Check for conflicts if requested |
| 273 | 278 | if check_conflicts: |
| 274 | 279 | conflicts = self.check_conflicts(item_type, key, group_name) |
@@ -312,6 +317,107 @@ class ShtickManager: | ||
| 312 | 317 | logger.error(f"Error removing {item_type}: {e}") |
| 313 | 318 | return False |
| 314 | 319 | |
| 320 | + # Batch operations | |
| 321 | + def add_items_batch( | |
| 322 | + self, items: List[Dict[str, Union[str, bool]]] | |
| 323 | + ) -> Dict[str, List[str]]: | |
| 324 | + """ | |
| 325 | + Add multiple items in one batch operation. | |
| 326 | + | |
| 327 | + Args: | |
| 328 | + items: List of dicts with keys: type, group, key, value, check_conflicts (optional) | |
| 329 | + Example: [ | |
| 330 | + {'type': 'alias', 'group': 'dev', 'key': 'll', 'value': 'ls -la'}, | |
| 331 | + {'type': 'env', 'group': 'dev', 'key': 'DEBUG', 'value': '1'}, | |
| 332 | + ] | |
| 333 | + | |
| 334 | + Returns: | |
| 335 | + Dictionary with 'success' and 'failed' lists of item keys | |
| 336 | + """ | |
| 337 | + results = {"success": [], "failed": []} | |
| 338 | + config = self._get_config() | |
| 339 | + affected_groups = set() | |
| 340 | + | |
| 341 | + for item in items: | |
| 342 | + try: | |
| 343 | + item_type = item["type"] | |
| 344 | + group_name = item["group"] | |
| 345 | + key = item["key"] | |
| 346 | + value = item["value"] | |
| 347 | + check_conflicts = item.get("check_conflicts", True) | |
| 348 | + | |
| 349 | + # Check for conflicts if requested | |
| 350 | + if check_conflicts: | |
| 351 | + conflicts = self.check_conflicts(item_type, key, group_name) | |
| 352 | + if conflicts: | |
| 353 | + logger.warning( | |
| 354 | + f"Item '{key}' exists in groups: {[c[0] for c in conflicts]}" | |
| 355 | + ) | |
| 356 | + | |
| 357 | + # Add the item | |
| 358 | + config.add_item(item_type, group_name, key, value) | |
| 359 | + results["success"].append(key) | |
| 360 | + | |
| 361 | + # Track affected groups | |
| 362 | + if group_name == "persistent" or config.is_group_active(group_name): | |
| 363 | + affected_groups.add(group_name) | |
| 364 | + | |
| 365 | + except Exception as e: | |
| 366 | + logger.error(f"Failed to add item '{item.get('key', 'unknown')}': {e}") | |
| 367 | + results["failed"].append(item.get("key", "unknown")) | |
| 368 | + | |
| 369 | + # Save and regenerate once for all affected groups | |
| 370 | + if results["success"]: | |
| 371 | + self._save_and_regenerate(list(affected_groups)) | |
| 372 | + | |
| 373 | + return results | |
| 374 | + | |
| 375 | + def remove_items_batch(self, items: List[Dict[str, str]]) -> Dict[str, List[str]]: | |
| 376 | + """ | |
| 377 | + Remove multiple items in one batch operation. | |
| 378 | + | |
| 379 | + Args: | |
| 380 | + items: List of dicts with keys: type, group, key | |
| 381 | + Example: [ | |
| 382 | + {'type': 'alias', 'group': 'dev', 'key': 'll'}, | |
| 383 | + {'type': 'env', 'group': 'dev', 'key': 'DEBUG'}, | |
| 384 | + ] | |
| 385 | + | |
| 386 | + Returns: | |
| 387 | + Dictionary with 'success' and 'failed' lists of item keys | |
| 388 | + """ | |
| 389 | + results = {"success": [], "failed": []} | |
| 390 | + config = self._get_config() | |
| 391 | + affected_groups = set() | |
| 392 | + | |
| 393 | + for item in items: | |
| 394 | + try: | |
| 395 | + item_type = item["type"] | |
| 396 | + group_name = item["group"] | |
| 397 | + key = item["key"] | |
| 398 | + | |
| 399 | + # Remove the item | |
| 400 | + if config.remove_item(item_type, group_name, key): | |
| 401 | + results["success"].append(key) | |
| 402 | + | |
| 403 | + # Track affected groups | |
| 404 | + if group_name == "persistent" or config.is_group_active(group_name): | |
| 405 | + affected_groups.add(group_name) | |
| 406 | + else: | |
| 407 | + results["failed"].append(key) | |
| 408 | + | |
| 409 | + except Exception as e: | |
| 410 | + logger.error( | |
| 411 | + f"Failed to remove item '{item.get('key', 'unknown')}': {e}" | |
| 412 | + ) | |
| 413 | + results["failed"].append(item.get("key", "unknown")) | |
| 414 | + | |
| 415 | + # Save and regenerate once for all affected groups | |
| 416 | + if results["success"]: | |
| 417 | + self._save_and_regenerate(list(affected_groups)) | |
| 418 | + | |
| 419 | + return results | |
| 420 | + | |
| 315 | 421 | # Group management |
| 316 | 422 | def activate_group(self, group: str) -> bool: |
| 317 | 423 | """ |