refactor cleanup stable
- SHA
96ed1a02229f4686142e8e8c9209e12b1aaaf40a- Parents
-
5a0b93c - Tree
243d149
96ed1a0
96ed1a02229f4686142e8e8c9209e12b1aaaf40a5a0b93c
243d149| Status | File | + | - |
|---|---|---|---|
| M |
src/shtick/cli.py
|
45 | 29 |
| M |
src/shtick/commands.py
|
113 | 251 |
| M |
src/shtick/config.py
|
110 | 92 |
| M |
src/shtick/display.py
|
147 | 163 |
| M |
src/shtick/generator.py
|
144 | 226 |
| A |
src/shtick/logger.py
|
46 | 0 |
| M |
src/shtick/shtick.py
|
156 | 162 |
src/shtick/cli.pymodified@@ -8,6 +8,7 @@ import sys | ||
| 8 | 8 | import argparse |
| 9 | 9 | from shtick.commands import ShtickCommands |
| 10 | 10 | from shtick.display import DisplayCommands |
| 11 | +from shtick.logger import setup_logging | |
| 11 | 12 | |
| 12 | 13 | |
| 13 | 14 | def main(): |
@@ -125,6 +126,9 @@ def main(): | ||
| 125 | 126 | |
| 126 | 127 | args = parser.parse_args() |
| 127 | 128 | |
| 129 | + # Set up logging first | |
| 130 | + logger = setup_logging(debug=args.debug) | |
| 131 | + | |
| 128 | 132 | if not args.command: |
| 129 | 133 | # Show helpful getting started message |
| 130 | 134 | parser.print_help() |
@@ -134,39 +138,51 @@ def main(): | ||
| 134 | 138 | print(" shtick list # List all items") |
| 135 | 139 | sys.exit(1) |
| 136 | 140 | |
| 137 | - # Initialize command handlers | |
| 141 | + # Initialize command handlers with debug flag | |
| 138 | 142 | commands = ShtickCommands(debug=args.debug) |
| 139 | 143 | display = DisplayCommands(debug=args.debug) |
| 140 | 144 | |
| 141 | 145 | # Route commands |
| 142 | - if args.command == "generate": | |
| 143 | - commands.generate(args.config, args.terse) | |
| 144 | - elif args.command == "add": | |
| 145 | - commands.add_item(args.type, args.group, args.assignment) | |
| 146 | - elif args.command == "add-persistent": | |
| 147 | - commands.add_persistent(args.type, args.assignment) | |
| 148 | - elif args.command == "alias": | |
| 149 | - commands.add_persistent("alias", args.assignment) | |
| 150 | - elif args.command == "env": | |
| 151 | - commands.add_persistent("env", args.assignment) | |
| 152 | - elif args.command == "function": | |
| 153 | - commands.add_persistent("function", args.assignment) | |
| 154 | - elif args.command == "remove": | |
| 155 | - commands.remove_item(args.type, args.group, args.search) | |
| 156 | - elif args.command == "remove-persistent": | |
| 157 | - commands.remove_item(args.type, "persistent", args.search) | |
| 158 | - elif args.command == "activate": | |
| 159 | - commands.activate_group(args.group) | |
| 160 | - elif args.command == "deactivate": | |
| 161 | - commands.deactivate_group(args.group) | |
| 162 | - elif args.command == "status": | |
| 163 | - display.status() | |
| 164 | - elif args.command == "list": | |
| 165 | - display.list_config(args.long) | |
| 166 | - elif args.command == "shells": | |
| 167 | - display.shells(args.long) | |
| 168 | - elif args.command == "source": | |
| 169 | - commands.source_command(args.shell) | |
| 146 | + try: | |
| 147 | + if args.command == "generate": | |
| 148 | + commands.generate(args.config, args.terse) | |
| 149 | + elif args.command == "add": | |
| 150 | + commands.add_item(args.type, args.group, args.assignment) | |
| 151 | + elif args.command == "add-persistent": | |
| 152 | + commands.add_persistent(args.type, args.assignment) | |
| 153 | + elif args.command == "alias": | |
| 154 | + commands.add_persistent("alias", args.assignment) | |
| 155 | + elif args.command == "env": | |
| 156 | + commands.add_persistent("env", args.assignment) | |
| 157 | + elif args.command == "function": | |
| 158 | + commands.add_persistent("function", args.assignment) | |
| 159 | + elif args.command == "remove": | |
| 160 | + commands.remove_item(args.type, args.group, args.search) | |
| 161 | + elif args.command == "remove-persistent": | |
| 162 | + commands.remove_item(args.type, "persistent", args.search) | |
| 163 | + elif args.command == "activate": | |
| 164 | + commands.activate_group(args.group) | |
| 165 | + elif args.command == "deactivate": | |
| 166 | + commands.deactivate_group(args.group) | |
| 167 | + elif args.command == "status": | |
| 168 | + display.status() | |
| 169 | + elif args.command == "list": | |
| 170 | + display.list_config(args.long) | |
| 171 | + elif args.command == "shells": | |
| 172 | + display.shells(args.long) | |
| 173 | + elif args.command == "source": | |
| 174 | + commands.source_command(args.shell) | |
| 175 | + | |
| 176 | + except KeyboardInterrupt: | |
| 177 | + logger.debug("Operation cancelled by user") | |
| 178 | + print("\nCancelled") | |
| 179 | + sys.exit(1) | |
| 180 | + except Exception as e: | |
| 181 | + if args.debug: | |
| 182 | + logger.exception("Unhandled exception") | |
| 183 | + else: | |
| 184 | + logger.error(f"Error: {e}") | |
| 185 | + sys.exit(1) | |
| 170 | 186 | |
| 171 | 187 | |
| 172 | 188 | if __name__ == "__main__": |
src/shtick/commands.pymodified@@ -1,27 +1,36 @@ | ||
| 1 | 1 | """ |
| 2 | -Command implementations for shtick CLI | |
| 2 | +Command implementations for shtick CLI - REFACTORED to use ShtickManager | |
| 3 | 3 | """ |
| 4 | 4 | |
| 5 | 5 | import os |
| 6 | 6 | import sys |
| 7 | 7 | import subprocess |
| 8 | +import logging | |
| 8 | 9 | from typing import Optional, List |
| 9 | 10 | |
| 11 | +from shtick.shtick import ShtickManager | |
| 10 | 12 | from shtick.config import Config |
| 11 | -from shtick.generator import Generator | |
| 12 | -from shtick.shells import get_supported_shells | |
| 13 | + | |
| 14 | +logger = logging.getLogger("shtick") | |
| 13 | 15 | |
| 14 | 16 | |
| 15 | 17 | class ShtickCommands: |
| 16 | - """Central command handler for shtick operations""" | |
| 18 | + """Central command handler for shtick operations - now using ShtickManager""" | |
| 17 | 19 | |
| 18 | 20 | def __init__(self, debug: bool = False): |
| 19 | - self.debug = debug | |
| 21 | + # Set up logging based on debug flag | |
| 22 | + if debug: | |
| 23 | + logging.basicConfig( | |
| 24 | + level=logging.DEBUG, format="%(name)s - %(levelname)s - %(message)s" | |
| 25 | + ) | |
| 26 | + else: | |
| 27 | + logging.basicConfig(level=logging.INFO, format="%(message)s") | |
| 28 | + | |
| 29 | + self.manager = ShtickManager(debug=debug) | |
| 20 | 30 | |
| 21 | 31 | def get_current_shell(self) -> Optional[str]: |
| 22 | - """Detect the current shell""" | |
| 23 | - shell_path = os.environ.get("SHELL", "") | |
| 24 | - return os.path.basename(shell_path) if shell_path else None | |
| 32 | + """Use cached shell detection from Config""" | |
| 33 | + return Config.get_current_shell() | |
| 25 | 34 | |
| 26 | 35 | def validate_assignment(self, assignment: str) -> tuple[str, str]: |
| 27 | 36 | """Validate key=value assignment format and return key, value""" |
@@ -43,84 +52,6 @@ class ShtickCommands: | ||
| 43 | 52 | |
| 44 | 53 | return key, value |
| 45 | 54 | |
| 46 | - def check_conflicts( | |
| 47 | - self, config: Config, item_type: str, group_name: str, key: str | |
| 48 | - ) -> List[str]: | |
| 49 | - """Check for potential conflicts and warn user""" | |
| 50 | - warnings = [] | |
| 51 | - | |
| 52 | - # Check if item already exists in same group | |
| 53 | - group = config.get_group(group_name) | |
| 54 | - if group: | |
| 55 | - existing_value = None | |
| 56 | - if item_type == "alias" and key in group.aliases: | |
| 57 | - existing_value = group.aliases[key] | |
| 58 | - elif item_type == "env" and key in group.env_vars: | |
| 59 | - existing_value = group.env_vars[key] | |
| 60 | - elif item_type == "function" and key in group.functions: | |
| 61 | - existing_value = group.functions[key] | |
| 62 | - | |
| 63 | - if existing_value: | |
| 64 | - warnings.append( | |
| 65 | - f"Will overwrite existing {item_type} '{key}' = '{existing_value}' in group '{group_name}'" | |
| 66 | - ) | |
| 67 | - | |
| 68 | - # Check if item exists in other groups | |
| 69 | - for other_group in config.groups: | |
| 70 | - if other_group.name == group_name: | |
| 71 | - continue | |
| 72 | - | |
| 73 | - if item_type == "alias" and key in other_group.aliases: | |
| 74 | - warnings.append( | |
| 75 | - f"Alias '{key}' also exists in group '{other_group.name}' = '{other_group.aliases[key]}'" | |
| 76 | - ) | |
| 77 | - elif item_type == "env" and key in other_group.env_vars: | |
| 78 | - warnings.append( | |
| 79 | - f"Environment variable '{key}' also exists in group '{other_group.name}' = '{other_group.env_vars[key]}'" | |
| 80 | - ) | |
| 81 | - elif item_type == "function" and key in other_group.functions: | |
| 82 | - warnings.append( | |
| 83 | - f"Function '{key}' also exists in group '{other_group.name}' = '{other_group.functions[key]}'" | |
| 84 | - ) | |
| 85 | - | |
| 86 | - return warnings | |
| 87 | - | |
| 88 | - def handle_warnings(self, warnings: List[str]) -> bool: | |
| 89 | - """Handle conflict warnings and return True if user wants to continue""" | |
| 90 | - if not warnings: | |
| 91 | - return True | |
| 92 | - | |
| 93 | - print("Warnings:") | |
| 94 | - for warning in warnings: | |
| 95 | - print(f" - {warning}") | |
| 96 | - try: | |
| 97 | - response = input("Continue anyway? [y/N]: ").strip().lower() | |
| 98 | - return response in ["y", "yes"] | |
| 99 | - except (KeyboardInterrupt, EOFError): | |
| 100 | - print("\nCancelled") | |
| 101 | - return False | |
| 102 | - | |
| 103 | - def regenerate_and_offer_source(self, config: Config, group_name: str = None): | |
| 104 | - """Regenerate shell files and offer auto-sourcing""" | |
| 105 | - try: | |
| 106 | - generator = Generator() | |
| 107 | - | |
| 108 | - if group_name: | |
| 109 | - group = config.get_group(group_name) | |
| 110 | - if group: | |
| 111 | - generator.generate_for_group(group) | |
| 112 | - else: | |
| 113 | - # Regenerate all groups | |
| 114 | - for group in config.groups: | |
| 115 | - generator.generate_for_group(group) | |
| 116 | - | |
| 117 | - generator.generate_loader(config) | |
| 118 | - print("✓ Regenerated shell files") | |
| 119 | - self.offer_auto_source() | |
| 120 | - except Exception as e: | |
| 121 | - print(f"Warning: Failed to regenerate files: {e}") | |
| 122 | - print("Run 'shtick generate' to update shell files") | |
| 123 | - | |
| 124 | 55 | def offer_auto_source(self): |
| 125 | 56 | """Offer to source shtick in current shell session""" |
| 126 | 57 | current_shell = self.get_current_shell() |
@@ -141,43 +72,10 @@ class ShtickCommands: | ||
| 141 | 72 | .lower() |
| 142 | 73 | ) |
| 143 | 74 | if response in ["", "y", "yes"]: |
| 144 | - # Test syntax first | |
| 145 | - if self._test_loader_syntax(current_shell, loader_path): | |
| 146 | - self._show_source_instructions(current_shell) | |
| 75 | + self._show_source_instructions(current_shell) | |
| 147 | 76 | except (KeyboardInterrupt, EOFError): |
| 148 | 77 | print() |
| 149 | 78 | |
| 150 | - def _test_loader_syntax(self, shell: str, loader_path: str) -> bool: | |
| 151 | - """Test loader file syntax and return True if valid""" | |
| 152 | - try: | |
| 153 | - test_response = ( | |
| 154 | - input("Test the loader file for syntax errors? [Y/n]: ").strip().lower() | |
| 155 | - ) | |
| 156 | - if test_response not in ["", "y", "yes"]: | |
| 157 | - return True | |
| 158 | - | |
| 159 | - result = subprocess.run( | |
| 160 | - [shell, "-c", f'source "{loader_path}"'], | |
| 161 | - capture_output=True, | |
| 162 | - text=True, | |
| 163 | - timeout=5, | |
| 164 | - ) | |
| 165 | - | |
| 166 | - if result.returncode == 0: | |
| 167 | - print("✓ Loader file syntax is valid") | |
| 168 | - return True | |
| 169 | - else: | |
| 170 | - print("✗ Loader file has syntax errors:") | |
| 171 | - print(result.stderr) | |
| 172 | - print("Please fix the errors before sourcing.") | |
| 173 | - return False | |
| 174 | - except subprocess.TimeoutExpired: | |
| 175 | - print("✓ Loader file appears to work (test timed out, which is normal)") | |
| 176 | - return True | |
| 177 | - except Exception as e: | |
| 178 | - print(f"Could not test loader file: {e}") | |
| 179 | - return True | |
| 180 | - | |
| 181 | 79 | def _show_source_instructions(self, shell: str): |
| 182 | 80 | """Show instructions for sourcing""" |
| 183 | 81 | print(f"\n🎯 Copy and paste this command to load changes immediately:") |
@@ -275,7 +173,7 @@ class ShtickCommands: | ||
| 275 | 173 | print(f"✓ Added shtick integration to {config_file}") |
| 276 | 174 | return True |
| 277 | 175 | except Exception as e: |
| 278 | - print(f"✗ Failed to modify {config_file}: {e}") | |
| 176 | + logger.error(f"Failed to modify {config_file}: {e}") | |
| 279 | 177 | continue |
| 280 | 178 | |
| 281 | 179 | # If no existing config file found, create the primary one |
@@ -289,27 +187,29 @@ class ShtickCommands: | ||
| 289 | 187 | print(f"✓ Created {primary_config} with shtick integration") |
| 290 | 188 | return True |
| 291 | 189 | except Exception as e: |
| 292 | - print(f"✗ Failed to create {primary_config}: {e}") | |
| 190 | + logger.error(f"Failed to create {primary_config}: {e}") | |
| 293 | 191 | return False |
| 294 | 192 | |
| 295 | - # Command implementations | |
| 193 | + # Command implementations - now using ShtickManager | |
| 296 | 194 | def generate(self, config_path: str = None, terse: bool = False): |
| 297 | 195 | """Generate shell files from config""" |
| 298 | - config_path = config_path or Config.get_default_config_path() | |
| 299 | - | |
| 300 | 196 | try: |
| 301 | - config = Config(config_path, debug=self.debug) | |
| 302 | - config.load() | |
| 303 | - | |
| 304 | - generator = Generator() | |
| 305 | - generator.generate_all(config, interactive=not terse) | |
| 197 | + if config_path: | |
| 198 | + # Create a new manager with custom config path | |
| 199 | + manager = ShtickManager(config_path=config_path) | |
| 200 | + else: | |
| 201 | + manager = self.manager | |
| 306 | 202 | |
| 307 | - if not terse: | |
| 203 | + success = manager.generate_shell_files() | |
| 204 | + if success and not terse: | |
| 308 | 205 | self.check_shell_integration() |
| 206 | + elif not success: | |
| 207 | + print("Error: Failed to generate shell files") | |
| 208 | + sys.exit(1) | |
| 309 | 209 | |
| 310 | 210 | except FileNotFoundError as e: |
| 311 | 211 | print(f"Error: {e}") |
| 312 | - print(f"Create a config file at {config_path} first") | |
| 212 | + print(f"Create a config file first") | |
| 313 | 213 | sys.exit(1) |
| 314 | 214 | except Exception as e: |
| 315 | 215 | print(f"Error: {e}") |
@@ -323,31 +223,27 @@ class ShtickCommands: | ||
| 323 | 223 | print(f"Error: {e}") |
| 324 | 224 | sys.exit(1) |
| 325 | 225 | |
| 326 | - config_path = Config.get_default_config_path() | |
| 327 | - | |
| 328 | - try: | |
| 329 | - config = Config(config_path, debug=self.debug) | |
| 330 | - try: | |
| 331 | - config.load() | |
| 332 | - except FileNotFoundError: | |
| 333 | - print(f"Creating new config file at {config_path}") | |
| 334 | - | |
| 335 | - # Check for conflicts and get user confirmation | |
| 336 | - warnings = self.check_conflicts(config, item_type, group, key) | |
| 337 | - if not self.handle_warnings(warnings): | |
| 338 | - return | |
| 339 | - | |
| 340 | - config.add_item(item_type, group, key, value) | |
| 341 | - config.save() | |
| 226 | + # Dispatch to appropriate manager method | |
| 227 | + if item_type == "alias": | |
| 228 | + success = self.manager.add_alias(key, value, group) | |
| 229 | + elif item_type == "env": | |
| 230 | + success = self.manager.add_env(key, value, group) | |
| 231 | + elif item_type == "function": | |
| 232 | + success = self.manager.add_function(key, value, group) | |
| 233 | + else: | |
| 234 | + print(f"Error: Unknown item type '{item_type}'") | |
| 235 | + sys.exit(1) | |
| 342 | 236 | |
| 237 | + if success: | |
| 343 | 238 | print(f"✓ Added {item_type} '{key}' = '{value}' to group '{group}'") |
| 344 | - | |
| 345 | - # Auto-regenerate if group is active | |
| 346 | - if config.is_group_active(group) or group == "persistent": | |
| 347 | - self.regenerate_and_offer_source(config, group) | |
| 348 | - | |
| 349 | - except Exception as e: | |
| 350 | - print(f"Error: {e}") | |
| 239 | + # Check if group is active and offer to source | |
| 240 | + if ( | |
| 241 | + self.manager.get_active_groups() | |
| 242 | + and group in self.manager.get_active_groups() | |
| 243 | + ): | |
| 244 | + self.offer_auto_source() | |
| 245 | + else: | |
| 246 | + print(f"Error: Failed to add {item_type}") | |
| 351 | 247 | sys.exit(1) |
| 352 | 248 | |
| 353 | 249 | def add_persistent(self, item_type: str, assignment: str): |
@@ -358,50 +254,43 @@ class ShtickCommands: | ||
| 358 | 254 | print(f"Error: {e}") |
| 359 | 255 | sys.exit(1) |
| 360 | 256 | |
| 361 | - config_path = Config.get_default_config_path() | |
| 362 | - is_first_time = not os.path.exists(config_path) | |
| 257 | + is_first_time = not os.path.exists(Config.get_default_config_path()) | |
| 363 | 258 | |
| 364 | - try: | |
| 365 | - config = Config(config_path, debug=self.debug) | |
| 366 | - try: | |
| 367 | - config.load() | |
| 368 | - except FileNotFoundError: | |
| 369 | - print(f"Creating new config file at {config_path}") | |
| 370 | - | |
| 371 | - # Check for conflicts | |
| 372 | - warnings = self.check_conflicts(config, item_type, "persistent", key) | |
| 373 | - if not self.handle_warnings(warnings): | |
| 374 | - return | |
| 375 | - | |
| 376 | - config.add_item(item_type, "persistent", key, value) | |
| 377 | - config.save() | |
| 259 | + # Dispatch to appropriate manager method | |
| 260 | + if item_type == "alias": | |
| 261 | + success = self.manager.add_persistent_alias(key, value) | |
| 262 | + elif item_type == "env": | |
| 263 | + success = self.manager.add_persistent_env(key, value) | |
| 264 | + elif item_type == "function": | |
| 265 | + success = self.manager.add_persistent_function(key, value) | |
| 266 | + else: | |
| 267 | + print(f"Error: Unknown item type '{item_type}'") | |
| 268 | + sys.exit(1) | |
| 378 | 269 | |
| 270 | + if success: | |
| 379 | 271 | print( |
| 380 | 272 | f"✓ Added {item_type} '{key}' = '{value}' to persistent group (always active)" |
| 381 | 273 | ) |
| 382 | - | |
| 383 | - # Auto-regenerate files | |
| 384 | - self.regenerate_and_offer_source(config, "persistent") | |
| 274 | + self.offer_auto_source() | |
| 385 | 275 | |
| 386 | 276 | # First-time setup experience |
| 387 | 277 | if is_first_time: |
| 388 | 278 | print("\n🎉 Welcome to shtick!") |
| 389 | 279 | self.check_shell_integration() |
| 390 | - | |
| 391 | - except Exception as e: | |
| 392 | - print(f"Error: {e}") | |
| 280 | + else: | |
| 281 | + print(f"Error: Failed to add {item_type}") | |
| 393 | 282 | sys.exit(1) |
| 394 | 283 | |
| 395 | 284 | def remove_item(self, item_type: str, group: str, search: str): |
| 396 | 285 | """Remove an item from a group""" |
| 397 | - config_path = Config.get_default_config_path() | |
| 398 | - | |
| 399 | 286 | try: |
| 400 | - config = Config(config_path, debug=self.debug) | |
| 401 | - config.load() | |
| 402 | - | |
| 403 | - # Find matching items | |
| 404 | - matches = config.find_items(item_type, group, search) | |
| 287 | + # Use manager's list_items to find matches | |
| 288 | + all_items = self.manager.list_items(group) | |
| 289 | + matches = [ | |
| 290 | + item["key"] | |
| 291 | + for item in all_items | |
| 292 | + if item["type"] == item_type and search.lower() in item["key"].lower() | |
| 293 | + ] | |
| 405 | 294 | |
| 406 | 295 | if not matches: |
| 407 | 296 | print( |
@@ -414,19 +303,25 @@ class ShtickCommands: | ||
| 414 | 303 | if not item_to_remove: |
| 415 | 304 | return |
| 416 | 305 | |
| 417 | - if config.remove_item(item_type, group, item_to_remove): | |
| 418 | - config.save() | |
| 419 | - print(f"✓ Removed {item_type} '{item_to_remove}' from group '{group}'") | |
| 306 | + # Dispatch to appropriate manager method | |
| 307 | + if item_type == "alias": | |
| 308 | + success = self.manager.remove_alias(item_to_remove, group) | |
| 309 | + elif item_type == "env": | |
| 310 | + success = self.manager.remove_env(item_to_remove, group) | |
| 311 | + elif item_type == "function": | |
| 312 | + success = self.manager.remove_function(item_to_remove, group) | |
| 313 | + else: | |
| 314 | + print(f"Error: Unknown item type '{item_type}'") | |
| 315 | + return | |
| 420 | 316 | |
| 421 | - # Auto-regenerate if group is active | |
| 422 | - if config.is_group_active(group) or group == "persistent": | |
| 423 | - self.regenerate_and_offer_source(config, group) | |
| 317 | + if success: | |
| 318 | + print(f"✓ Removed {item_type} '{item_to_remove}' from group '{group}'") | |
| 319 | + # Offer to source if group is active | |
| 320 | + if group == "persistent" or group in self.manager.get_active_groups(): | |
| 321 | + self.offer_auto_source() | |
| 424 | 322 | else: |
| 425 | 323 | print(f"Failed to remove {item_type} '{item_to_remove}'") |
| 426 | 324 | |
| 427 | - except FileNotFoundError: | |
| 428 | - print(f"Config file not found: {config_path}") | |
| 429 | - sys.exit(1) | |
| 430 | 325 | except Exception as e: |
| 431 | 326 | print(f"Error: {e}") |
| 432 | 327 | sys.exit(1) |
@@ -459,69 +354,36 @@ class ShtickCommands: | ||
| 459 | 354 | |
| 460 | 355 | def activate_group(self, group_name: str): |
| 461 | 356 | """Activate a group""" |
| 462 | - config_path = Config.get_default_config_path() | |
| 463 | - | |
| 464 | - try: | |
| 465 | - config = Config(config_path, debug=self.debug) | |
| 466 | - config.load() | |
| 467 | - | |
| 468 | - if group_name == "persistent": | |
| 469 | - print( | |
| 470 | - "Error: 'persistent' group is always active and cannot be manually activated" | |
| 471 | - ) | |
| 472 | - return | |
| 357 | + if group_name == "persistent": | |
| 358 | + print( | |
| 359 | + "Error: 'persistent' group is always active and cannot be manually activated" | |
| 360 | + ) | |
| 361 | + return | |
| 473 | 362 | |
| 474 | - if config.activate_group(group_name): | |
| 475 | - self.regenerate_and_offer_source(config) | |
| 476 | - print(f"✓ Activated group '{group_name}'") | |
| 477 | - print("Changes are now active in new shell sessions") | |
| 478 | - else: | |
| 479 | - print(f"Error: Group '{group_name}' not found in configuration") | |
| 480 | - available = [g.name for g in config.get_regular_groups()] | |
| 481 | - if available: | |
| 482 | - print(f"Available groups: {', '.join(available)}") | |
| 483 | - | |
| 484 | - except FileNotFoundError: | |
| 485 | - print(f"Config file not found: {config_path}") | |
| 486 | - print("Run 'shtick generate' first or add some items with 'shtick add'") | |
| 487 | - sys.exit(1) | |
| 488 | - except Exception as e: | |
| 489 | - print(f"Error: {e}") | |
| 490 | - sys.exit(1) | |
| 363 | + success = self.manager.activate_group(group_name) | |
| 364 | + if success: | |
| 365 | + print(f"✓ Activated group '{group_name}'") | |
| 366 | + print("Changes are now active in new shell sessions") | |
| 367 | + self.offer_auto_source() | |
| 368 | + else: | |
| 369 | + print(f"Error: Group '{group_name}' not found in configuration") | |
| 370 | + available = self.manager.get_groups() | |
| 371 | + if available: | |
| 372 | + print(f"Available groups: {', '.join(available)}") | |
| 491 | 373 | |
| 492 | 374 | def deactivate_group(self, group_name: str): |
| 493 | 375 | """Deactivate a group""" |
| 494 | - config_path = Config.get_default_config_path() | |
| 495 | - | |
| 496 | - try: | |
| 497 | - config = Config(config_path, debug=self.debug) | |
| 498 | - config.load() | |
| 499 | - | |
| 500 | - if group_name == "persistent": | |
| 501 | - print("Error: 'persistent' group cannot be deactivated") | |
| 502 | - return | |
| 503 | - | |
| 504 | - if config.deactivate_group(group_name): | |
| 505 | - # Only need to regenerate loader for deactivation | |
| 506 | - try: | |
| 507 | - generator = Generator() | |
| 508 | - generator.generate_loader(config) | |
| 509 | - print("✓ Regenerated shell files") | |
| 510 | - except Exception as e: | |
| 511 | - print(f"Warning: Failed to regenerate files: {e}") | |
| 512 | - | |
| 513 | - print(f"✓ Deactivated group '{group_name}'") | |
| 514 | - print("Changes will take effect in new shell sessions") | |
| 515 | - self.offer_auto_source() | |
| 516 | - else: | |
| 517 | - print(f"Group '{group_name}' was not active") | |
| 376 | + if group_name == "persistent": | |
| 377 | + print("Error: 'persistent' group cannot be deactivated") | |
| 378 | + return | |
| 518 | 379 | |
| 519 | - except FileNotFoundError: | |
| 520 | - print(f"Config file not found: {config_path}") | |
| 521 | - sys.exit(1) | |
| 522 | - except Exception as e: | |
| 523 | - print(f"Error: {e}") | |
| 524 | - sys.exit(1) | |
| 380 | + success = self.manager.deactivate_group(group_name) | |
| 381 | + if success: | |
| 382 | + print(f"✓ Deactivated group '{group_name}'") | |
| 383 | + print("Changes will take effect in new shell sessions") | |
| 384 | + self.offer_auto_source() | |
| 385 | + else: | |
| 386 | + print(f"Group '{group_name}' was not active") | |
| 525 | 387 | |
| 526 | 388 | def source_command(self, shell: str = None): |
| 527 | 389 | """Output source command for eval""" |
src/shtick/config.pymodified@@ -1,12 +1,15 @@ | ||
| 1 | 1 | """ |
| 2 | -Configuration parsing for shtick | |
| 2 | +Configuration parsing for shtick - REFACTORED | |
| 3 | 3 | """ |
| 4 | 4 | |
| 5 | 5 | import os |
| 6 | 6 | import tomllib |
| 7 | +import logging | |
| 7 | 8 | from pathlib import Path |
| 8 | 9 | from dataclasses import dataclass |
| 9 | -from typing import Dict, List, Optional | |
| 10 | +from typing import Dict, List, Optional, Union | |
| 11 | + | |
| 12 | +logger = logging.getLogger("shtick") | |
| 10 | 13 | |
| 11 | 14 | |
| 12 | 15 | @dataclass |
@@ -18,13 +21,51 @@ class GroupData: | ||
| 18 | 21 | env_vars: Dict[str, str] |
| 19 | 22 | functions: Dict[str, str] |
| 20 | 23 | |
| 24 | + def get_items(self, item_type: str) -> Dict[str, str]: | |
| 25 | + """Get items dictionary for the specified type""" | |
| 26 | + type_map = {"alias": "aliases", "env": "env_vars", "function": "functions"} | |
| 27 | + attr_name = type_map.get(item_type) | |
| 28 | + if not attr_name: | |
| 29 | + raise ValueError(f"Unknown item type: {item_type}") | |
| 30 | + return getattr(self, attr_name) | |
| 31 | + | |
| 32 | + def set_item(self, item_type: str, key: str, value: str) -> None: | |
| 33 | + """Set an item in the appropriate dictionary""" | |
| 34 | + items = self.get_items(item_type) | |
| 35 | + items[key] = value | |
| 36 | + | |
| 37 | + def remove_item(self, item_type: str, key: str) -> bool: | |
| 38 | + """Remove an item if it exists""" | |
| 39 | + items = self.get_items(item_type) | |
| 40 | + if key in items: | |
| 41 | + del items[key] | |
| 42 | + return True | |
| 43 | + return False | |
| 44 | + | |
| 45 | + def has_item(self, item_type: str, key: str) -> bool: | |
| 46 | + """Check if an item exists""" | |
| 47 | + items = self.get_items(item_type) | |
| 48 | + return key in items | |
| 49 | + | |
| 50 | + def get_item_value(self, item_type: str, key: str) -> Optional[str]: | |
| 51 | + """Get the value of an item""" | |
| 52 | + items = self.get_items(item_type) | |
| 53 | + return items.get(key) | |
| 54 | + | |
| 55 | + @property | |
| 56 | + def total_items(self) -> int: | |
| 57 | + """Get total number of items in this group""" | |
| 58 | + return len(self.aliases) + len(self.env_vars) + len(self.functions) | |
| 59 | + | |
| 21 | 60 | |
| 22 | 61 | class Config: |
| 23 | 62 | """Main configuration handler""" |
| 24 | 63 | |
| 25 | - def __init__(self, config_path: Optional[str] = None, debug: bool = False): | |
| 64 | + # Class variable for shell detection caching | |
| 65 | + _detected_shell = None | |
| 66 | + | |
| 67 | + def __init__(self, config_path: Optional[str] = None): | |
| 26 | 68 | self.config_path = config_path or self.get_default_config_path() |
| 27 | - self.debug = debug | |
| 28 | 69 | self.groups: List[GroupData] = [] |
| 29 | 70 | |
| 30 | 71 | @staticmethod |
@@ -42,6 +83,19 @@ class Config: | ||
| 42 | 83 | """Get the active groups state file path""" |
| 43 | 84 | return os.path.expanduser("~/.config/shtick/active_groups") |
| 44 | 85 | |
| 86 | + @classmethod | |
| 87 | + def get_current_shell(cls) -> Optional[str]: | |
| 88 | + """Detect the current shell with caching""" | |
| 89 | + if cls._detected_shell is None: | |
| 90 | + shell_path = os.environ.get("SHELL", "") | |
| 91 | + cls._detected_shell = os.path.basename(shell_path) if shell_path else "" | |
| 92 | + return cls._detected_shell if cls._detected_shell else None | |
| 93 | + | |
| 94 | + @classmethod | |
| 95 | + def clear_shell_cache(cls): | |
| 96 | + """Clear the cached shell detection (useful for testing)""" | |
| 97 | + cls._detected_shell = None | |
| 98 | + | |
| 45 | 99 | def load_active_groups(self) -> List[str]: |
| 46 | 100 | """Load list of currently active groups""" |
| 47 | 101 | active_file = self.get_active_groups_file() |
@@ -104,28 +158,19 @@ class Config: | ||
| 104 | 158 | if not os.path.exists(self.config_path): |
| 105 | 159 | raise FileNotFoundError(f"Config file not found: {self.config_path}") |
| 106 | 160 | |
| 107 | - if self.debug: | |
| 108 | - print(f"Debug: Loading config from: {self.config_path}") | |
| 161 | + logger.debug(f"Loading config from: {self.config_path}") | |
| 109 | 162 | |
| 110 | 163 | with open(self.config_path, "rb") as f: |
| 111 | 164 | data = tomllib.load(f) |
| 112 | 165 | |
| 113 | - if self.debug: | |
| 114 | - print(f"Debug: Raw TOML data keys: {list(data.keys())}") | |
| 115 | - print(f"Debug: Raw TOML data structure: {data}") | |
| 166 | + logger.debug(f"Raw TOML data keys: {list(data.keys())}") | |
| 116 | 167 | |
| 117 | 168 | self.groups = [] |
| 118 | 169 | |
| 119 | 170 | # Parse groups from nested TOML structure |
| 120 | 171 | for group_name, group_config in data.items(): |
| 121 | - if self.debug: | |
| 122 | - print( | |
| 123 | - f"Debug: Processing group '{group_name}' with config: {group_config}" | |
| 124 | - ) | |
| 125 | - | |
| 126 | 172 | if not isinstance(group_config, dict): |
| 127 | - if self.debug: | |
| 128 | - print(f"Debug: Skipping non-dict value for key '{group_name}'") | |
| 173 | + logger.debug(f"Skipping non-dict value for key '{group_name}'") | |
| 129 | 174 | continue |
| 130 | 175 | |
| 131 | 176 | # Initialize group data |
@@ -133,64 +178,44 @@ class Config: | ||
| 133 | 178 | |
| 134 | 179 | # Extract each section from the group |
| 135 | 180 | for section_name, section_data in group_config.items(): |
| 136 | - if self.debug: | |
| 137 | - print( | |
| 138 | - f"Debug: Processing section '{section_name}' in group '{group_name}': {section_data}" | |
| 139 | - ) | |
| 140 | - | |
| 141 | 181 | if section_name == "aliases" and isinstance(section_data, dict): |
| 142 | 182 | group_data["aliases"] = section_data |
| 143 | - if self.debug: | |
| 144 | - print( | |
| 145 | - f"Debug: Added {len(section_data)} aliases to '{group_name}'" | |
| 146 | - ) | |
| 183 | + logger.debug(f"Added {len(section_data)} aliases to '{group_name}'") | |
| 147 | 184 | elif section_name == "env_vars" and isinstance(section_data, dict): |
| 148 | 185 | group_data["env_vars"] = section_data |
| 149 | - if self.debug: | |
| 150 | - print( | |
| 151 | - f"Debug: Added {len(section_data)} env_vars to '{group_name}'" | |
| 152 | - ) | |
| 186 | + logger.debug( | |
| 187 | + f"Added {len(section_data)} env_vars to '{group_name}'" | |
| 188 | + ) | |
| 153 | 189 | elif section_name == "functions" and isinstance(section_data, dict): |
| 154 | 190 | group_data["functions"] = section_data |
| 155 | - if self.debug: | |
| 156 | - print( | |
| 157 | - f"Debug: Added {len(section_data)} functions to '{group_name}'" | |
| 158 | - ) | |
| 191 | + logger.debug( | |
| 192 | + f"Added {len(section_data)} functions to '{group_name}'" | |
| 193 | + ) | |
| 159 | 194 | else: |
| 160 | - if self.debug: | |
| 161 | - print( | |
| 162 | - f"Debug: Unknown or invalid section '{section_name}' in group '{group_name}'" | |
| 163 | - ) | |
| 195 | + logger.debug( | |
| 196 | + f"Unknown or invalid section '{section_name}' in group '{group_name}'" | |
| 197 | + ) | |
| 164 | 198 | |
| 165 | 199 | # Create GroupData object if group has any items |
| 166 | - total_items = ( | |
| 167 | - len(group_data["aliases"]) | |
| 168 | - + len(group_data["env_vars"]) | |
| 169 | - + len(group_data["functions"]) | |
| 200 | + new_group = GroupData( | |
| 201 | + name=group_name, | |
| 202 | + aliases=group_data["aliases"], | |
| 203 | + env_vars=group_data["env_vars"], | |
| 204 | + functions=group_data["functions"], | |
| 170 | 205 | ) |
| 171 | - if self.debug: | |
| 172 | - print(f"Debug: Group '{group_name}' has {total_items} total items") | |
| 173 | - | |
| 174 | - if total_items > 0: | |
| 175 | - new_group = GroupData( | |
| 176 | - name=group_name, | |
| 177 | - aliases=group_data["aliases"], | |
| 178 | - env_vars=group_data["env_vars"], | |
| 179 | - functions=group_data["functions"], | |
| 180 | - ) | |
| 206 | + | |
| 207 | + if new_group.total_items > 0: | |
| 181 | 208 | self.groups.append(new_group) |
| 182 | - if self.debug: | |
| 183 | - print( | |
| 184 | - f"Debug: Created group '{group_name}' with {len(group_data['aliases'])} aliases, {len(group_data['env_vars'])} env_vars, {len(group_data['functions'])} functions" | |
| 185 | - ) | |
| 209 | + logger.debug( | |
| 210 | + f"Created group '{group_name}' with {len(group_data['aliases'])} aliases, " | |
| 211 | + f"{len(group_data['env_vars'])} env_vars, {len(group_data['functions'])} functions" | |
| 212 | + ) | |
| 186 | 213 | else: |
| 187 | - if self.debug: | |
| 188 | - print(f"Warning: Group '{group_name}' has no items, skipping") | |
| 214 | + logger.warning(f"Group '{group_name}' has no items, skipping") | |
| 189 | 215 | |
| 190 | - if self.debug: | |
| 191 | - print( | |
| 192 | - f"Debug: Final groups loaded: {[g.name for g in self.groups]} (total: {len(self.groups)})" | |
| 193 | - ) | |
| 216 | + logger.debug( | |
| 217 | + f"Final groups loaded: {[g.name for g in self.groups]} (total: {len(self.groups)})" | |
| 218 | + ) | |
| 194 | 219 | |
| 195 | 220 | def save(self) -> None: |
| 196 | 221 | """Save the current configuration back to TOML file""" |
@@ -236,33 +261,14 @@ class Config: | ||
| 236 | 261 | def add_item(self, item_type: str, group_name: str, key: str, value: str) -> None: |
| 237 | 262 | """Add an alias, env var, or function to a group""" |
| 238 | 263 | group = self.add_group(group_name) |
| 239 | - | |
| 240 | - if item_type == "alias": | |
| 241 | - group.aliases[key] = value | |
| 242 | - elif item_type == "env": | |
| 243 | - group.env_vars[key] = value | |
| 244 | - elif item_type == "function": | |
| 245 | - group.functions[key] = value | |
| 246 | - else: | |
| 247 | - raise ValueError(f"Unknown item type: {item_type}") | |
| 264 | + group.set_item(item_type, key, value) | |
| 248 | 265 | |
| 249 | 266 | def remove_item(self, item_type: str, group_name: str, key: str) -> bool: |
| 250 | 267 | """Remove an item from a group. Returns True if found and removed.""" |
| 251 | 268 | group = self.get_group(group_name) |
| 252 | 269 | if not group: |
| 253 | 270 | return False |
| 254 | - | |
| 255 | - if item_type == "alias" and key in group.aliases: | |
| 256 | - del group.aliases[key] | |
| 257 | - return True | |
| 258 | - elif item_type == "env" and key in group.env_vars: | |
| 259 | - del group.env_vars[key] | |
| 260 | - return True | |
| 261 | - elif item_type == "function" and key in group.functions: | |
| 262 | - del group.functions[key] | |
| 263 | - return True | |
| 264 | - | |
| 265 | - return False | |
| 271 | + return group.remove_item(item_type, key) | |
| 266 | 272 | |
| 267 | 273 | def find_items( |
| 268 | 274 | self, item_type: str, group_name: str, search_term: str |
@@ -272,14 +278,26 @@ class Config: | ||
| 272 | 278 | if not group: |
| 273 | 279 | return [] |
| 274 | 280 | |
| 275 | - if item_type == "alias": | |
| 276 | - items = group.aliases.keys() | |
| 277 | - elif item_type == "env": | |
| 278 | - items = group.env_vars.keys() | |
| 279 | - elif item_type == "function": | |
| 280 | - items = group.functions.keys() | |
| 281 | - else: | |
| 282 | - return [] | |
| 283 | - | |
| 281 | + items = group.get_items(item_type) | |
| 284 | 282 | # Simple fuzzy matching - contains search term |
| 285 | - return [item for item in items if search_term.lower() in item.lower()] | |
| 283 | + return [item for item in items.keys() if search_term.lower() in item.lower()] | |
| 284 | + | |
| 285 | + def get_all_shells_to_generate(self) -> List[str]: | |
| 286 | + """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 | |
| 289 | + current_shell = self.get_current_shell() | |
| 290 | + if not current_shell: | |
| 291 | + return ["bash", "zsh", "fish"] # Common defaults | |
| 292 | + | |
| 293 | + # Include current shell and close relatives | |
| 294 | + shell_families = { | |
| 295 | + "bash": ["bash", "sh"], | |
| 296 | + "zsh": ["zsh", "bash"], | |
| 297 | + "fish": ["fish"], | |
| 298 | + "ksh": ["ksh", "bash"], | |
| 299 | + "dash": ["dash", "sh"], | |
| 300 | + } | |
| 301 | + | |
| 302 | + shells = shell_families.get(current_shell, [current_shell]) | |
| 303 | + return list(set(shells)) # Remove duplicates | |
src/shtick/display.pymodified@@ -4,235 +4,219 @@ Display commands for shtick CLI - handles listing, status, and informational out | ||
| 4 | 4 | |
| 5 | 5 | import os |
| 6 | 6 | import sys |
| 7 | +import logging | |
| 7 | 8 | from shtick.config import Config |
| 8 | 9 | from shtick.shells import get_supported_shells |
| 10 | +from shtick.shtick import ShtickManager | |
| 11 | + | |
| 12 | +logger = logging.getLogger("shtick") | |
| 9 | 13 | |
| 10 | 14 | |
| 11 | 15 | class DisplayCommands: |
| 12 | 16 | """Handles all display/listing commands for shtick""" |
| 13 | 17 | |
| 14 | 18 | def __init__(self, debug: bool = False): |
| 15 | - self.debug = debug | |
| 19 | + # Set up logging | |
| 20 | + if debug: | |
| 21 | + logging.basicConfig( | |
| 22 | + level=logging.DEBUG, format="%(name)s - %(levelname)s - %(message)s" | |
| 23 | + ) | |
| 24 | + else: | |
| 25 | + logging.basicConfig(level=logging.INFO, format="%(message)s") | |
| 26 | + | |
| 27 | + self.manager = ShtickManager(debug=debug) | |
| 16 | 28 | |
| 17 | 29 | def get_current_shell(self): |
| 18 | - """Detect the current shell""" | |
| 19 | - shell_path = os.environ.get("SHELL", "") | |
| 20 | - return os.path.basename(shell_path) if shell_path else None | |
| 30 | + """Use cached shell detection from Config""" | |
| 31 | + return Config.get_current_shell() | |
| 21 | 32 | |
| 22 | 33 | def status(self): |
| 23 | 34 | """Show status of groups and active state""" |
| 24 | - config_path = Config.get_default_config_path() | |
| 35 | + status = self.manager.get_status() | |
| 25 | 36 | |
| 26 | - try: | |
| 27 | - config = Config(config_path, debug=self.debug) | |
| 28 | - config.load() | |
| 29 | - | |
| 30 | - persistent_group = config.get_persistent_group() | |
| 31 | - regular_groups = config.get_regular_groups() | |
| 32 | - active_groups = config.load_active_groups() | |
| 33 | - | |
| 34 | - print("Shtick Status") | |
| 35 | - print("=" * 40) | |
| 36 | - | |
| 37 | - # Show current shell integration status | |
| 38 | - current_shell = self.get_current_shell() | |
| 39 | - if current_shell: | |
| 40 | - print(f"Current shell: {current_shell}") | |
| 41 | - loader_path = os.path.expanduser( | |
| 42 | - f"~/.config/shtick/load_active.{current_shell}" | |
| 43 | - ) | |
| 44 | - if os.path.exists(loader_path): | |
| 45 | - print(f"Loader file: ✓ exists") | |
| 46 | - else: | |
| 47 | - print(f"Loader file: ✗ missing (run 'shtick generate')") | |
| 48 | - print() | |
| 37 | + if "error" in status: | |
| 38 | + print(f"Error loading configuration: {status['error']}") | |
| 39 | + print("\nGet started with:") | |
| 40 | + print(" shtick alias ll='ls -la'") | |
| 41 | + return | |
| 49 | 42 | |
| 50 | - # Show persistent group | |
| 51 | - if persistent_group: | |
| 52 | - total_persistent = ( | |
| 53 | - len(persistent_group.aliases) | |
| 54 | - + len(persistent_group.env_vars) | |
| 55 | - + len(persistent_group.functions) | |
| 56 | - ) | |
| 57 | - print(f"Persistent (always active): {total_persistent} items") | |
| 43 | + print("Shtick Status") | |
| 44 | + print("=" * 40) | |
| 45 | + | |
| 46 | + # Show current shell integration status | |
| 47 | + if status["current_shell"]: | |
| 48 | + print(f"Current shell: {status['current_shell']}") | |
| 49 | + if status["loader_exists"]: | |
| 50 | + print(f"Loader file: ✓ exists") | |
| 58 | 51 | else: |
| 59 | - print("Persistent: No items") | |
| 52 | + print(f"Loader file: ✗ missing (run 'shtick generate')") | |
| 53 | + print() | |
| 60 | 54 | |
| 61 | - print() | |
| 55 | + # Show persistent group | |
| 56 | + if status["persistent_items"] > 0: | |
| 57 | + print(f"Persistent (always active): {status['persistent_items']} items") | |
| 58 | + else: | |
| 59 | + print("Persistent: No items") | |
| 62 | 60 | |
| 63 | - # Show regular groups | |
| 64 | - if regular_groups: | |
| 65 | - print("Available Groups:") | |
| 66 | - for group in regular_groups: | |
| 67 | - status = "ACTIVE" if group.name in active_groups else "inactive" | |
| 68 | - total_items = ( | |
| 69 | - len(group.aliases) + len(group.env_vars) + len(group.functions) | |
| 70 | - ) | |
| 71 | - print(f" {group.name}: {total_items} items ({status})") | |
| 72 | - else: | |
| 73 | - print("No regular groups configured") | |
| 61 | + print() | |
| 74 | 62 | |
| 75 | - print() | |
| 63 | + # Show regular groups | |
| 64 | + if status["available_groups"]: | |
| 65 | + print("Available Groups:") | |
| 66 | + active_set = set(status["active_groups"]) | |
| 67 | + for group_name in status["available_groups"]: | |
| 68 | + # Get item count for this group | |
| 69 | + items = self.manager.list_items(group_name) | |
| 70 | + item_count = len(items) | |
| 71 | + status_str = "ACTIVE" if group_name in active_set else "inactive" | |
| 72 | + print(f" {group_name}: {item_count} items ({status_str})") | |
| 73 | + else: | |
| 74 | + print("No regular groups configured") | |
| 76 | 75 | |
| 77 | - # Show summary | |
| 78 | - if active_groups: | |
| 79 | - print(f"Currently active: {', '.join(active_groups)}") | |
| 80 | - else: | |
| 81 | - print("No groups currently active") | |
| 76 | + print() | |
| 82 | 77 | |
| 83 | - print() | |
| 84 | - print("Quick commands:") | |
| 85 | - print(" shtick alias ll='ls -la' # Add persistent alias") | |
| 86 | - print(" shtick activate <group> # Activate group") | |
| 87 | - print(' eval "$(shtick source)" # Load changes now') | |
| 78 | + # Show summary | |
| 79 | + if status["active_groups"]: | |
| 80 | + print(f"Currently active: {', '.join(status['active_groups'])}") | |
| 81 | + else: | |
| 82 | + print("No groups currently active") | |
| 88 | 83 | |
| 89 | - except FileNotFoundError: | |
| 90 | - print(f"Config file not found: {config_path}") | |
| 91 | - print("No configuration exists yet") | |
| 92 | - print("\nGet started with:") | |
| 93 | - print(" shtick alias ll='ls -la'") | |
| 94 | - except Exception as e: | |
| 95 | - print(f"Error: {e}") | |
| 96 | - sys.exit(1) | |
| 84 | + print() | |
| 85 | + print("Quick commands:") | |
| 86 | + print(" shtick alias ll='ls -la' # Add persistent alias") | |
| 87 | + print(" shtick activate <group> # Activate group") | |
| 88 | + print(' eval "$(shtick source)" # Load changes now') | |
| 97 | 89 | |
| 98 | 90 | def list_config(self, long_format: bool = False): |
| 99 | 91 | """List current configuration""" |
| 100 | - config_path = Config.get_default_config_path() | |
| 92 | + items = self.manager.list_items() | |
| 101 | 93 | |
| 102 | - try: | |
| 103 | - config = Config(config_path, debug=self.debug) | |
| 104 | - config.load() | |
| 105 | - | |
| 106 | - if not config.groups: | |
| 107 | - print("No groups configured") | |
| 108 | - print("\nGet started with:") | |
| 109 | - print(" shtick alias ll='ls -la' # Add persistent alias") | |
| 110 | - print(" shtick add alias work ll='ls -la' # Add to 'work' group") | |
| 111 | - print(" shtick activate work # Activate 'work' group") | |
| 112 | - return | |
| 113 | - | |
| 114 | - persistent_group = config.get_persistent_group() | |
| 115 | - regular_groups = config.get_regular_groups() | |
| 116 | - active_groups = config.load_active_groups() | |
| 117 | - | |
| 118 | - if long_format: | |
| 119 | - self._print_detailed_list( | |
| 120 | - persistent_group, regular_groups, active_groups | |
| 121 | - ) | |
| 122 | - else: | |
| 123 | - self._print_tabular_list( | |
| 124 | - persistent_group, regular_groups, active_groups | |
| 125 | - ) | |
| 126 | - | |
| 127 | - except FileNotFoundError: | |
| 128 | - print(f"Config file not found: {config_path}") | |
| 129 | - print("Use 'shtick alias <key>=<value>' to create your first alias") | |
| 130 | - except Exception as e: | |
| 131 | - print(f"Error: {e}") | |
| 132 | - sys.exit(1) | |
| 133 | - | |
| 134 | - def _print_detailed_list(self, persistent_group, regular_groups, active_groups): | |
| 94 | + if not items: | |
| 95 | + print("No items configured") | |
| 96 | + print("\nGet started with:") | |
| 97 | + print(" shtick alias ll='ls -la' # Add persistent alias") | |
| 98 | + print(" shtick add alias work ll='ls -la' # Add to 'work' group") | |
| 99 | + print(" shtick activate work # Activate 'work' group") | |
| 100 | + return | |
| 101 | + | |
| 102 | + # Group items by group name | |
| 103 | + groups_data = {} | |
| 104 | + for item in items: | |
| 105 | + group_name = item["group"] | |
| 106 | + if group_name not in groups_data: | |
| 107 | + groups_data[group_name] = { | |
| 108 | + "aliases": {}, | |
| 109 | + "env_vars": {}, | |
| 110 | + "functions": {}, | |
| 111 | + "active": item["active"], | |
| 112 | + } | |
| 113 | + | |
| 114 | + if item["type"] == "alias": | |
| 115 | + groups_data[group_name]["aliases"][item["key"]] = item["value"] | |
| 116 | + elif item["type"] == "env": | |
| 117 | + groups_data[group_name]["env_vars"][item["key"]] = item["value"] | |
| 118 | + elif item["type"] == "function": | |
| 119 | + groups_data[group_name]["functions"][item["key"]] = item["value"] | |
| 120 | + | |
| 121 | + # Display based on format | |
| 122 | + if long_format: | |
| 123 | + self._print_detailed_list(groups_data) | |
| 124 | + else: | |
| 125 | + self._print_tabular_list(items) | |
| 126 | + | |
| 127 | + def _print_detailed_list(self, groups_data): | |
| 135 | 128 | """Print detailed line-by-line list format""" |
| 136 | - # Show persistent group first | |
| 137 | - if persistent_group: | |
| 129 | + # Show persistent group first if it exists | |
| 130 | + if "persistent" in groups_data: | |
| 138 | 131 | print("Group: persistent (always active)") |
| 139 | - self._print_group_items(persistent_group) | |
| 132 | + self._print_group_items_detailed(groups_data["persistent"]) | |
| 140 | 133 | print() |
| 134 | + del groups_data["persistent"] | |
| 141 | 135 | |
| 142 | 136 | # Show regular groups |
| 143 | - for group in regular_groups: | |
| 144 | - status = " (ACTIVE)" if group.name in active_groups else " (inactive)" | |
| 145 | - print(f"Group: {group.name}{status}") | |
| 146 | - self._print_group_items(group) | |
| 137 | + for group_name, group_data in sorted(groups_data.items()): | |
| 138 | + status = " (ACTIVE)" if group_data["active"] else " (inactive)" | |
| 139 | + print(f"Group: {group_name}{status}") | |
| 140 | + self._print_group_items_detailed(group_data) | |
| 147 | 141 | print() |
| 148 | 142 | |
| 149 | - def _print_group_items(self, group): | |
| 150 | - """Print items for a single group""" | |
| 151 | - if group.aliases: | |
| 152 | - print(f" Aliases ({len(group.aliases)}):") | |
| 153 | - for key, value in group.aliases.items(): | |
| 143 | + def _print_group_items_detailed(self, group_data): | |
| 144 | + """Print items for a single group in detailed format""" | |
| 145 | + if group_data["aliases"]: | |
| 146 | + print(f" Aliases ({len(group_data['aliases'])}):") | |
| 147 | + for key, value in group_data["aliases"].items(): | |
| 154 | 148 | print(f" {key} = {value}") |
| 155 | - if group.env_vars: | |
| 156 | - print(f" Environment Variables ({len(group.env_vars)}):") | |
| 157 | - for key, value in group.env_vars.items(): | |
| 149 | + if group_data["env_vars"]: | |
| 150 | + print(f" Environment Variables ({len(group_data['env_vars'])}):") | |
| 151 | + for key, value in group_data["env_vars"].items(): | |
| 158 | 152 | print(f" {key} = {value}") |
| 159 | - if group.functions: | |
| 160 | - print(f" Functions ({len(group.functions)}):") | |
| 161 | - for key, value in group.functions.items(): | |
| 153 | + if group_data["functions"]: | |
| 154 | + print(f" Functions ({len(group_data['functions'])}):") | |
| 155 | + for key, value in group_data["functions"].items(): | |
| 162 | 156 | print(f" {key} = {value}") |
| 163 | 157 | |
| 164 | - def _print_tabular_list(self, persistent_group, regular_groups, active_groups): | |
| 158 | + def _print_tabular_list(self, items): | |
| 165 | 159 | """Print compact tabular list format""" |
| 166 | - # Collect all items for tabular display | |
| 167 | - items = [] | |
| 168 | - | |
| 169 | - # Add persistent items | |
| 170 | - if persistent_group: | |
| 171 | - items.extend(self._collect_group_items(persistent_group, "PERSISTENT")) | |
| 172 | - | |
| 173 | - # Add regular group items | |
| 174 | - for group in regular_groups: | |
| 175 | - status = "ACTIVE" if group.name in active_groups else "inactive" | |
| 176 | - items.extend(self._collect_group_items(group, status)) | |
| 177 | - | |
| 178 | 160 | if not items: |
| 179 | - print("No items configured") | |
| 180 | 161 | return |
| 181 | 162 | |
| 182 | - self._print_table(items) | |
| 183 | - self._print_summary(items, active_groups) | |
| 184 | - | |
| 185 | - def _collect_group_items(self, group, status): | |
| 186 | - """Collect items from a group for tabular display""" | |
| 187 | - items = [] | |
| 188 | - for key, value in group.aliases.items(): | |
| 189 | - items.append((group.name, "alias", key, value, status)) | |
| 190 | - for key, value in group.env_vars.items(): | |
| 191 | - items.append((group.name, "env", key, value, status)) | |
| 192 | - for key, value in group.functions.items(): | |
| 193 | - items.append((group.name, "function", key, value, status)) | |
| 194 | - return items | |
| 195 | - | |
| 196 | - def _print_table(self, items): | |
| 197 | - """Print items in tabular format""" | |
| 198 | 163 | # Calculate column widths |
| 199 | - max_group = max(max(len(item[0]) for item in items), 5) # "Group" | |
| 200 | - max_type = max(max(len(item[1]) for item in items), 4) # "Type" | |
| 201 | - max_key = max(max(len(item[2]) for item in items), 3) # "Key" | |
| 164 | + max_group = max(max(len(item["group"]) for item in items), 5) # "Group" | |
| 165 | + max_type = max(max(len(item["type"]) for item in items), 4) # "Type" | |
| 166 | + max_key = max(max(len(item["key"]) for item in items), 3) # "Key" | |
| 202 | 167 | max_value = max( |
| 203 | - max(min(len(item[3]), 50) for item in items), 5 | |
| 168 | + max(min(len(item["value"]), 50) for item in items), 5 | |
| 204 | 169 | ) # "Value" (limited) |
| 205 | - max_status = max(max(len(item[4]) for item in items), 6) # "Status" | |
| 170 | + max_status = max( | |
| 171 | + max(len("ACTIVE" if item["active"] else "inactive") for item in items), 6 | |
| 172 | + ) # "Status" | |
| 206 | 173 | |
| 207 | 174 | # Print header |
| 208 | 175 | header = f"{'Group':<{max_group}} {'Type':<{max_type}} {'Key':<{max_key}} {'Value':<{max_value}} {'Status':<{max_status}}" |
| 209 | 176 | print(header) |
| 210 | 177 | print("-" * len(header)) |
| 211 | 178 | |
| 179 | + # Sort items for better display (persistent first, then by group, then by type) | |
| 180 | + def sort_key(item): | |
| 181 | + group_order = 0 if item["group"] == "persistent" else 1 | |
| 182 | + return (group_order, item["group"], item["type"], item["key"]) | |
| 183 | + | |
| 184 | + sorted_items = sorted(items, key=sort_key) | |
| 185 | + | |
| 212 | 186 | # Print items |
| 213 | - for group, item_type, key, value, status in items: | |
| 187 | + for item in sorted_items: | |
| 214 | 188 | # Truncate long values with ellipsis |
| 189 | + value = item["value"] | |
| 215 | 190 | display_value = ( |
| 216 | 191 | value if len(value) <= max_value else value[: max_value - 3] + "..." |
| 217 | 192 | ) |
| 193 | + status = "ACTIVE" if item["active"] else "inactive" | |
| 218 | 194 | print( |
| 219 | - f"{group:<{max_group}} {item_type:<{max_type}} {key:<{max_key}} {display_value:<{max_value}} {status:<{max_status}}" | |
| 195 | + f"{item['group']:<{max_group}} {item['type']:<{max_type}} " | |
| 196 | + f"{item['key']:<{max_key}} {display_value:<{max_value}} {status:<{max_status}}" | |
| 220 | 197 | ) |
| 221 | 198 | |
| 222 | - def _print_summary(self, items, active_groups): | |
| 199 | + # Print summary | |
| 200 | + self._print_summary(items) | |
| 201 | + | |
| 202 | + def _print_summary(self, items): | |
| 223 | 203 | """Print summary information""" |
| 224 | 204 | print() |
| 225 | 205 | total_items = len(items) |
| 226 | - active_items = len( | |
| 227 | - [item for item in items if item[4] in ["ACTIVE", "PERSISTENT"]] | |
| 228 | - ) | |
| 206 | + active_items = len([item for item in items if item["active"]]) | |
| 229 | 207 | print(f"Total: {total_items} items ({active_items} active)") |
| 230 | 208 | |
| 231 | 209 | # Show available commands |
| 232 | 210 | print() |
| 233 | 211 | print("Use 'shtick list -l' for detailed view") |
| 234 | - if any(item[4] == "inactive" for item in items): | |
| 235 | - inactive_groups = set(item[0] for item in items if item[4] == "inactive") | |
| 212 | + | |
| 213 | + # Get inactive groups | |
| 214 | + inactive_groups = set() | |
| 215 | + for item in items: | |
| 216 | + if not item["active"] and item["group"] != "persistent": | |
| 217 | + inactive_groups.add(item["group"]) | |
| 218 | + | |
| 219 | + if inactive_groups: | |
| 236 | 220 | print(f"Activate groups with: shtick activate <group>") |
| 237 | 221 | print(f"Inactive groups: {', '.join(sorted(inactive_groups))}") |
| 238 | 222 | |
src/shtick/generator.pymodified@@ -3,10 +3,13 @@ Shell file generator for shtick | ||
| 3 | 3 | """ |
| 4 | 4 | |
| 5 | 5 | import os |
| 6 | +import logging | |
| 6 | 7 | from pathlib import Path |
| 7 | -from typing import Dict, List | |
| 8 | +from typing import Dict, List, Set | |
| 8 | 9 | from shtick.config import GroupData, Config |
| 9 | -from shtick.shells import get_supported_shells, get_shell_syntax | |
| 10 | +from shtick.shells import get_shell_syntax | |
| 11 | + | |
| 12 | +logger = logging.getLogger("shtick") | |
| 10 | 13 | |
| 11 | 14 | |
| 12 | 15 | class Generator: |
@@ -14,63 +17,100 @@ class Generator: | ||
| 14 | 17 | |
| 15 | 18 | def __init__(self, output_base_dir: str = None): |
| 16 | 19 | self.output_base_dir = output_base_dir or Config.get_output_dir() |
| 17 | - | |
| 18 | - def ensure_output_dir(self, group_name: str, item_type: str) -> str: | |
| 20 | + # Get shells to generate for once | |
| 21 | + self._shells_to_generate = None | |
| 22 | + | |
| 23 | + @property | |
| 24 | + def shells_to_generate(self) -> List[str]: | |
| 25 | + """Get list of shells to generate files for""" | |
| 26 | + if self._shells_to_generate is None: | |
| 27 | + config = Config() | |
| 28 | + self._shells_to_generate = config.get_all_shells_to_generate() | |
| 29 | + logger.debug(f"Will generate files for shells: {self._shells_to_generate}") | |
| 30 | + return self._shells_to_generate | |
| 31 | + | |
| 32 | + def ensure_output_dir(self, group_name: str) -> str: | |
| 19 | 33 | """Ensure output directory exists and return the path""" |
| 20 | - output_dir = os.path.join(self.output_base_dir, group_name, item_type) | |
| 34 | + output_dir = os.path.join(self.output_base_dir, group_name) | |
| 21 | 35 | Path(output_dir).mkdir(parents=True, exist_ok=True) |
| 22 | 36 | return output_dir |
| 23 | 37 | |
| 24 | 38 | def generate_for_group(self, group: GroupData) -> None: |
| 25 | 39 | """Generate all shell files for a single group""" |
| 26 | - print(f"Processing group: {group.name}") | |
| 40 | + logger.info(f"Processing group: {group.name}") | |
| 27 | 41 | |
| 28 | - # Generate aliases | |
| 29 | - if group.aliases: | |
| 30 | - print(f" Generating alias files ({len(group.aliases)} aliases)") | |
| 31 | - self._generate_files(group.name, "alias", group.aliases, "aliases") | |
| 42 | + # Skip if group is empty | |
| 43 | + if group.total_items == 0: | |
| 44 | + logger.debug(f"Skipping empty group: {group.name}") | |
| 45 | + return | |
| 32 | 46 | |
| 33 | - # Generate env vars | |
| 34 | - if group.env_vars: | |
| 35 | - print(f" Generating env var files ({len(group.env_vars)} variables)") | |
| 36 | - self._generate_files(group.name, "env", group.env_vars, "envvars") | |
| 47 | + # Prepare all content for the group | |
| 48 | + group_content = self._prepare_group_content(group) | |
| 37 | 49 | |
| 38 | - # Generate functions | |
| 39 | - if group.functions: | |
| 40 | - print(f" Generating function files ({len(group.functions)} functions)") | |
| 41 | - self._generate_files(group.name, "function", group.functions, "functions") | |
| 50 | + # Generate consolidated files for each shell | |
| 51 | + output_dir = self.ensure_output_dir(group.name) | |
| 52 | + for shell_name in self.shells_to_generate: | |
| 53 | + self._generate_group_file(shell_name, group.name, group_content, output_dir) | |
| 42 | 54 | |
| 43 | - def _generate_files( | |
| 44 | - self, group_name: str, item_type: str, items: Dict[str, str], prefix: str | |
| 55 | + def _prepare_group_content(self, group: GroupData) -> Dict[str, Dict[str, str]]: | |
| 56 | + """Prepare all content for a group organized by type""" | |
| 57 | + return { | |
| 58 | + "alias": group.aliases, | |
| 59 | + "env": group.env_vars, | |
| 60 | + "function": group.functions, | |
| 61 | + } | |
| 62 | + | |
| 63 | + def _generate_group_file( | |
| 64 | + self, | |
| 65 | + shell_name: str, | |
| 66 | + group_name: str, | |
| 67 | + content: Dict[str, Dict[str, str]], | |
| 68 | + output_dir: str, | |
| 45 | 69 | ) -> None: |
| 46 | - """Generate shell files for a specific item type""" | |
| 47 | - output_dir = self.ensure_output_dir(group_name, item_type) | |
| 48 | - | |
| 49 | - # Generate for each supported shell + default | |
| 50 | - all_shells = get_supported_shells() + ["default"] | |
| 51 | - | |
| 52 | - for shell_name in all_shells: | |
| 53 | - shell_syntax = get_shell_syntax(shell_name) | |
| 54 | - filename = f"{prefix}.{shell_name}" | |
| 55 | - filepath = os.path.join(output_dir, filename) | |
| 56 | - | |
| 57 | - with open(filepath, "w") as f: | |
| 58 | - # Write header | |
| 59 | - f.write(f"# {prefix} for {shell_name}\n") | |
| 60 | - f.write("# Generated by shtick\n\n") | |
| 61 | - | |
| 62 | - # Write items using appropriate syntax | |
| 63 | - for key, value in items.items(): | |
| 64 | - if item_type == "alias": | |
| 65 | - line = shell_syntax.alias_fmt.format(key, value) | |
| 66 | - elif item_type == "env": | |
| 67 | - line = shell_syntax.env_fmt.format(key, value) | |
| 68 | - elif item_type == "function": | |
| 69 | - line = shell_syntax.function_fmt.format(key, value) | |
| 70 | - else: | |
| 71 | - continue | |
| 70 | + """Generate a single consolidated file for a shell""" | |
| 71 | + shell_syntax = get_shell_syntax(shell_name) | |
| 72 | + | |
| 73 | + # Count total items | |
| 74 | + total_items = sum(len(items) for items in content.values()) | |
| 75 | + if total_items == 0: | |
| 76 | + return | |
| 77 | + | |
| 78 | + logger.debug( | |
| 79 | + f"Generating {shell_name} file for group {group_name} ({total_items} items)" | |
| 80 | + ) | |
| 81 | + | |
| 82 | + # Create consolidated file | |
| 83 | + filename = f"all.{shell_name}" | |
| 84 | + filepath = os.path.join(output_dir, filename) | |
| 85 | + | |
| 86 | + with open(filepath, "w") as f: | |
| 87 | + # Write header | |
| 88 | + f.write(f"# Shtick configuration for {group_name} - {shell_name}\n") | |
| 89 | + f.write("# Generated by shtick\n\n") | |
| 90 | + | |
| 91 | + # Write aliases | |
| 92 | + if content["alias"]: | |
| 93 | + f.write(f"# Aliases ({len(content['alias'])})\n") | |
| 94 | + for key, value in content["alias"].items(): | |
| 95 | + line = shell_syntax.alias_fmt.format(key, value) | |
| 96 | + f.write(line) | |
| 97 | + f.write("\n") | |
| 98 | + | |
| 99 | + # Write env vars | |
| 100 | + if content["env"]: | |
| 101 | + f.write(f"# Environment Variables ({len(content['env'])})\n") | |
| 102 | + for key, value in content["env"].items(): | |
| 103 | + line = shell_syntax.env_fmt.format(key, value) | |
| 104 | + f.write(line) | |
| 105 | + f.write("\n") | |
| 72 | 106 | |
| 107 | + # Write functions | |
| 108 | + if content["function"]: | |
| 109 | + f.write(f"# Functions ({len(content['function'])})\n") | |
| 110 | + for key, value in content["function"].items(): | |
| 111 | + line = shell_syntax.function_fmt.format(key, value) | |
| 73 | 112 | f.write(line) |
| 113 | + f.write("\n") | |
| 74 | 114 | |
| 75 | 115 | def generate_all(self, config: Config, interactive: bool = True) -> None: |
| 76 | 116 | """Generate shell files for all groups in config""" |
@@ -79,6 +119,7 @@ class Generator: | ||
| 79 | 119 | return |
| 80 | 120 | |
| 81 | 121 | print(f"Generating shell files for {len(config.groups)} groups...") |
| 122 | + print(f"Target shells: {', '.join(self.shells_to_generate)}") | |
| 82 | 123 | |
| 83 | 124 | for group in config.groups: |
| 84 | 125 | self.generate_for_group(group) |
@@ -86,20 +127,19 @@ class Generator: | ||
| 86 | 127 | # Generate the dynamic loader for all shells |
| 87 | 128 | self.generate_loader(config) |
| 88 | 129 | |
| 89 | - print(f"All done! Files generated in {self.output_base_dir}") | |
| 90 | - self._print_usage_instructions(config, interactive) | |
| 130 | + print(f"\n✓ All done! Files generated in {self.output_base_dir}") | |
| 131 | + | |
| 132 | + if interactive: | |
| 133 | + self._print_usage_instructions(config) | |
| 91 | 134 | |
| 92 | 135 | def generate_loader(self, config: Config) -> None: |
| 93 | 136 | """Generate dynamic loader files that source persistent + active groups""" |
| 94 | - print("Generating dynamic loader files...") | |
| 137 | + logger.info("Generating dynamic loader files...") | |
| 95 | 138 | |
| 96 | 139 | active_groups = config.load_active_groups() |
| 97 | 140 | persistent_group = config.get_persistent_group() |
| 98 | 141 | |
| 99 | - # Generate for each supported shell + default | |
| 100 | - all_shells = get_supported_shells() + ["default"] | |
| 101 | - | |
| 102 | - for shell_name in all_shells: | |
| 142 | + for shell_name in self.shells_to_generate: | |
| 103 | 143 | loader_path = os.path.join( |
| 104 | 144 | self.output_base_dir, f"load_active.{shell_name}" |
| 105 | 145 | ) |
@@ -109,58 +149,64 @@ class Generator: | ||
| 109 | 149 | f.write("# This file is auto-generated - do not edit\n\n") |
| 110 | 150 | |
| 111 | 151 | # Source persistent group first (always active) |
| 112 | - if persistent_group: | |
| 152 | + if persistent_group and persistent_group.total_items > 0: | |
| 113 | 153 | f.write("# Load persistent configuration\n") |
| 114 | - for item_type in ["alias", "env", "function"]: | |
| 115 | - # Map item_type to the actual filename prefix | |
| 116 | - prefix_map = { | |
| 117 | - "alias": "aliases", | |
| 118 | - "env": "envvars", | |
| 119 | - "function": "functions", | |
| 120 | - } | |
| 121 | - prefix = prefix_map[item_type] | |
| 122 | - file_path = f"$HOME/.config/shtick/persistent/{item_type}/{prefix}.{shell_name}" | |
| 123 | - f.write(f'[ -f "{file_path}" ] && source "{file_path}"\n') | |
| 124 | - f.write("\n") | |
| 154 | + file_path = f"$HOME/.config/shtick/persistent/all.{shell_name}" | |
| 155 | + f.write(f'[ -f "{file_path}" ] && source "{file_path}"\n\n') | |
| 125 | 156 | |
| 126 | 157 | # Source active groups |
| 127 | 158 | if active_groups: |
| 128 | 159 | f.write("# Load active groups\n") |
| 129 | 160 | for group_name in active_groups: |
| 130 | - f.write(f"# Group: {group_name}\n") | |
| 131 | - for item_type in ["alias", "env", "function"]: | |
| 132 | - # Map item_type to the actual filename prefix | |
| 133 | - prefix_map = { | |
| 134 | - "alias": "aliases", | |
| 135 | - "env": "envvars", | |
| 136 | - "function": "functions", | |
| 137 | - } | |
| 138 | - prefix = prefix_map[item_type] | |
| 139 | - file_path = f"$HOME/.config/shtick/{group_name}/{item_type}/{prefix}.{shell_name}" | |
| 161 | + group = config.get_group(group_name) | |
| 162 | + if group and group.total_items > 0: | |
| 163 | + f.write(f"# Group: {group_name}\n") | |
| 164 | + file_path = ( | |
| 165 | + f"$HOME/.config/shtick/{group_name}/all.{shell_name}" | |
| 166 | + ) | |
| 140 | 167 | f.write(f'[ -f "{file_path}" ] && source "{file_path}"\n') |
| 141 | - f.write("\n") | |
| 168 | + f.write("\n") | |
| 142 | 169 | else: |
| 143 | 170 | f.write("# No active groups\n") |
| 144 | 171 | |
| 145 | - def _print_usage_instructions( | |
| 146 | - self, config: Config, interactive: bool = True | |
| 147 | - ) -> None: | |
| 172 | + def _print_usage_instructions(self, config: Config) -> None: | |
| 148 | 173 | """Print usage instructions for the user""" |
| 149 | 174 | active_groups = config.load_active_groups() |
| 150 | 175 | persistent_group = config.get_persistent_group() |
| 151 | - | |
| 152 | - if interactive: | |
| 153 | - self._interactive_shell_setup() | |
| 176 | + current_shell = Config.get_current_shell() | |
| 177 | + | |
| 178 | + print("\nUsage Instructions:") | |
| 179 | + print("=" * 50) | |
| 180 | + | |
| 181 | + if current_shell and current_shell in self.shells_to_generate: | |
| 182 | + print(f"\nFor {current_shell}, add this to your shell config:") | |
| 183 | + loader_path = f"~/.config/shtick/load_active.{current_shell}" | |
| 184 | + | |
| 185 | + shell_configs = { | |
| 186 | + "bash": "~/.bashrc", | |
| 187 | + "zsh": "~/.zshrc", | |
| 188 | + "fish": "~/.config/fish/config.fish", | |
| 189 | + "ksh": "~/.kshrc", | |
| 190 | + "dash": "~/.dashrc", | |
| 191 | + } | |
| 192 | + | |
| 193 | + config_file = shell_configs.get(current_shell, "your shell config") | |
| 194 | + print(f" # In {config_file}:") | |
| 195 | + print(f" source {loader_path}") | |
| 154 | 196 | else: |
| 155 | - self._print_default_instructions() | |
| 197 | + # Fallback instructions | |
| 198 | + print("\nAdd the appropriate line to your shell config:") | |
| 199 | + for shell in self.shells_to_generate[:3]: # Show top 3 | |
| 200 | + print(f" # For {shell}:") | |
| 201 | + print(f" source ~/.config/shtick/load_active.{shell}") | |
| 202 | + | |
| 203 | + print("\n" + "=" * 50) | |
| 156 | 204 | |
| 205 | + # Status summary | |
| 157 | 206 | if persistent_group: |
| 158 | - total_persistent = ( | |
| 159 | - len(persistent_group.aliases) | |
| 160 | - + len(persistent_group.env_vars) | |
| 161 | - + len(persistent_group.functions) | |
| 207 | + print( | |
| 208 | + f"\nPersistent items (always active): {persistent_group.total_items} total" | |
| 162 | 209 | ) |
| 163 | - print(f"\nPersistent items (always active): {total_persistent} total") | |
| 164 | 210 | |
| 165 | 211 | if active_groups: |
| 166 | 212 | print(f"Active groups: {', '.join(active_groups)}") |
@@ -173,144 +219,16 @@ class Generator: | ||
| 173 | 219 | if available_groups: |
| 174 | 220 | print(f"Available groups: {', '.join(available_groups)}") |
| 175 | 221 | |
| 176 | - def _print_default_instructions(self) -> None: | |
| 177 | - """Print default sourcing instructions""" | |
| 178 | - print("\nTo use these configurations, add this line to your shell config:") | |
| 179 | - print(" # For bash/zsh (~/.bashrc or ~/.zshrc):") | |
| 180 | - print(" source ~/.config/shtick/load_active.bash") | |
| 181 | - print("\n # For fish (~/.config/fish/config.fish):") | |
| 182 | - print(" source ~/.config/shtick/load_active.fish") | |
| 183 | - | |
| 184 | - def _interactive_shell_setup(self) -> None: | |
| 185 | - """Interactive shell selection for sourcing instructions""" | |
| 186 | - from shtick.shells import get_supported_shells | |
| 187 | - | |
| 188 | - print("\nSelect shells to show sourcing instructions for:") | |
| 189 | - print("(You can specify multiple shells by number or name, space-separated)") | |
| 190 | - print() | |
| 191 | - | |
| 192 | - shells = sorted(get_supported_shells()) | |
| 193 | - shell_configs = { | |
| 194 | - "bash": ("~/.bashrc", "source ~/.config/shtick/load_active.bash"), | |
| 195 | - "zsh": ("~/.zshrc", "source ~/.config/shtick/load_active.zsh"), | |
| 196 | - "fish": ( | |
| 197 | - "~/.config/fish/config.fish", | |
| 198 | - "source ~/.config/shtick/load_active.fish", | |
| 199 | - ), | |
| 200 | - "ksh": ("~/.kshrc", "source ~/.config/shtick/load_active.ksh"), | |
| 201 | - "mksh": ("~/.mkshrc", "source ~/.config/shtick/load_active.mksh"), | |
| 202 | - "yash": ("~/.yashrc", "source ~/.config/shtick/load_active.yash"), | |
| 203 | - "dash": ("~/.dashrc", "source ~/.config/shtick/load_active.dash"), | |
| 204 | - "csh": ("~/.cshrc", "source ~/.config/shtick/load_active.csh"), | |
| 205 | - "tcsh": ("~/.tcshrc", "source ~/.config/shtick/load_active.tcsh"), | |
| 206 | - "xonsh": ("~/.xonshrc", "source ~/.config/shtick/load_active.xonsh"), | |
| 207 | - "elvish": ( | |
| 208 | - "~/.elvish/rc.elv", | |
| 209 | - "source ~/.config/shtick/load_active.elvish", | |
| 210 | - ), | |
| 211 | - "nushell": ( | |
| 212 | - "~/.config/nushell/config.nu", | |
| 213 | - "source ~/.config/shtick/load_active.nushell", | |
| 214 | - ), | |
| 215 | - "powershell": ("$PROFILE", ". ~/.config/shtick/load_active.powershell"), | |
| 216 | - "rc": ("~/.rcrc", "source ~/.config/shtick/load_active.rc"), | |
| 217 | - "es": ("~/.esrc", "source ~/.config/shtick/load_active.es"), | |
| 218 | - "oil": ("~/.oilrc", "source ~/.config/shtick/load_active.oil"), | |
| 219 | - } | |
| 220 | - | |
| 221 | - # Display numbered list | |
| 222 | - for i, shell in enumerate(shells, 1): | |
| 223 | - config_file = shell_configs.get( | |
| 224 | - shell, ("~/.profile", f"source ~/.config/shtick/load_active.{shell}") | |
| 225 | - )[0] | |
| 226 | - print(f" {i:2d}. {shell:<12} ({config_file})") | |
| 227 | - | |
| 228 | - print(f" {len(shells)+1:2d}. all (show all shells)") | |
| 229 | - print() | |
| 230 | - | |
| 231 | - try: | |
| 232 | - user_input = input( | |
| 233 | - "Enter shell numbers or names (space-separated): " | |
| 234 | - ).strip() | |
| 235 | - if not user_input: | |
| 236 | - print("No selection made, skipping sourcing instructions.") | |
| 237 | - return | |
| 238 | - | |
| 239 | - selected_shells = self._parse_shell_selection(user_input, shells) | |
| 240 | - | |
| 241 | - if not selected_shells: | |
| 242 | - print("No valid shells selected, skipping sourcing instructions.") | |
| 243 | - return | |
| 244 | - | |
| 245 | - print("\nAdd these lines to your shell configuration files:") | |
| 246 | - print("=" * 60) | |
| 247 | - | |
| 248 | - for shell in selected_shells: | |
| 249 | - config_file, source_line = shell_configs.get( | |
| 250 | - shell, | |
| 251 | - ("~/.profile", f"source ~/.config/shtick/load_active.{shell}"), | |
| 252 | - ) | |
| 253 | - | |
| 254 | - print(f"\n# {shell.upper()}") | |
| 255 | - print(f"# File: {config_file}") | |
| 256 | - print(f"{source_line}") | |
| 257 | - | |
| 258 | - print("\n" + "=" * 60) | |
| 259 | - | |
| 260 | - except (KeyboardInterrupt, EOFError): | |
| 261 | - print("\nCancelled. Skipping sourcing instructions.") | |
| 262 | - | |
| 263 | - def _parse_shell_selection( | |
| 264 | - self, user_input: str, available_shells: List[str] | |
| 265 | - ) -> List[str]: | |
| 266 | - """Parse user input for shell selection (numbers or names)""" | |
| 267 | - selected = set() | |
| 268 | - tokens = user_input.lower().split() | |
| 269 | - | |
| 270 | - for token in tokens: | |
| 271 | - # Check if it's "all" | |
| 272 | - if token == "all": | |
| 273 | - return available_shells | |
| 274 | - | |
| 275 | - # Check if it's a number | |
| 276 | - try: | |
| 277 | - num = int(token) | |
| 278 | - if 1 <= num <= len(available_shells): | |
| 279 | - selected.add(available_shells[num - 1]) | |
| 280 | - elif num == len(available_shells) + 1: # "all" option | |
| 281 | - return available_shells | |
| 282 | - else: | |
| 283 | - print(f"Warning: Invalid number {num}, ignoring") | |
| 284 | - continue | |
| 285 | - except ValueError: | |
| 286 | - pass | |
| 287 | - | |
| 288 | - # Check if it's a shell name (fuzzy match) | |
| 289 | - matches = [shell for shell in available_shells if token in shell.lower()] | |
| 290 | - if len(matches) == 1: | |
| 291 | - selected.add(matches[0]) | |
| 292 | - elif len(matches) > 1: | |
| 293 | - print( | |
| 294 | - f"Warning: '{token}' matches multiple shells: {', '.join(matches)}" | |
| 295 | - ) | |
| 296 | - print(f"Using exact match or first match: {matches[0]}") | |
| 297 | - selected.add(matches[0]) | |
| 298 | - else: | |
| 299 | - print(f"Warning: No shell matches '{token}', ignoring") | |
| 300 | - | |
| 301 | - return sorted(list(selected)) | |
| 302 | - | |
| 303 | - def get_shell_files_for_group(self, group_name: str) -> Dict[str, List[str]]: | |
| 222 | + def get_shell_files_for_group(self, group_name: str) -> List[str]: | |
| 304 | 223 | """Get list of generated shell files for a group""" |
| 305 | - files = {"alias": [], "env": [], "function": []} | |
| 306 | - | |
| 307 | - for item_type in files.keys(): | |
| 308 | - type_dir = os.path.join(self.output_base_dir, group_name, item_type) | |
| 309 | - if os.path.exists(type_dir): | |
| 310 | - files[item_type] = [ | |
| 311 | - os.path.join(type_dir, f) | |
| 312 | - for f in os.listdir(type_dir) | |
| 313 | - if os.path.isfile(os.path.join(type_dir, f)) | |
| 314 | - ] | |
| 224 | + group_dir = os.path.join(self.output_base_dir, group_name) | |
| 225 | + if not os.path.exists(group_dir): | |
| 226 | + return [] | |
| 227 | + | |
| 228 | + files = [] | |
| 229 | + for shell in self.shells_to_generate: | |
| 230 | + file_path = os.path.join(group_dir, f"all.{shell}") | |
| 231 | + if os.path.exists(file_path): | |
| 232 | + files.append(file_path) | |
| 315 | 233 | |
| 316 | 234 | return files |
src/shtick/logger.pyadded@@ -0,0 +1,46 @@ | ||
| 1 | +""" | |
| 2 | +Logging configuration for shtick | |
| 3 | +""" | |
| 4 | + | |
| 5 | +import logging | |
| 6 | +import sys | |
| 7 | + | |
| 8 | + | |
| 9 | +def setup_logging(debug: bool = False, name: str = "shtick") -> logging.Logger: | |
| 10 | + """ | |
| 11 | + Set up logging configuration for shtick. | |
| 12 | + | |
| 13 | + Args: | |
| 14 | + debug: Enable debug logging | |
| 15 | + name: Logger name | |
| 16 | + | |
| 17 | + Returns: | |
| 18 | + Configured logger instance | |
| 19 | + """ | |
| 20 | + logger = logging.getLogger(name) | |
| 21 | + | |
| 22 | + # Clear any existing handlers to avoid duplicates | |
| 23 | + logger.handlers.clear() | |
| 24 | + | |
| 25 | + # Create console handler | |
| 26 | + handler = logging.StreamHandler(sys.stdout) | |
| 27 | + | |
| 28 | + # Set level and format based on debug flag | |
| 29 | + if debug: | |
| 30 | + logger.setLevel(logging.DEBUG) | |
| 31 | + formatter = logging.Formatter( | |
| 32 | + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| 33 | + datefmt="%Y-%m-%d %H:%M:%S", | |
| 34 | + ) | |
| 35 | + else: | |
| 36 | + logger.setLevel(logging.INFO) | |
| 37 | + # Simple format for non-debug - just the message | |
| 38 | + formatter = logging.Formatter("%(message)s") | |
| 39 | + | |
| 40 | + handler.setFormatter(formatter) | |
| 41 | + logger.addHandler(handler) | |
| 42 | + | |
| 43 | + # Prevent propagation to root logger | |
| 44 | + logger.propagate = False | |
| 45 | + | |
| 46 | + return logger | |
src/shtick/shtick.pymodified@@ -3,10 +3,12 @@ High-level API for shtick - provides a clean programmatic interface | ||
| 3 | 3 | """ |
| 4 | 4 | |
| 5 | 5 | import os |
| 6 | -from typing import List, Dict, Optional, Union | |
| 6 | +import logging | |
| 7 | +from typing import List, Dict, Optional, Union, Tuple | |
| 7 | 8 | from .config import Config, GroupData |
| 8 | 9 | from .generator import Generator |
| 9 | -from .commands import ShtickCommands | |
| 10 | + | |
| 11 | +logger = logging.getLogger("shtick") | |
| 10 | 12 | |
| 11 | 13 | |
| 12 | 14 | class ShtickManager: |
@@ -38,21 +40,30 @@ class ShtickManager: | ||
| 38 | 40 | debug: Enable debug output |
| 39 | 41 | """ |
| 40 | 42 | self.config_path = config_path or Config.get_default_config_path() |
| 41 | - self.debug = debug | |
| 43 | + | |
| 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") | |
| 51 | + | |
| 42 | 52 | self._config = None |
| 43 | - self._commands = ShtickCommands(debug=debug) | |
| 53 | + self._generator = Generator() | |
| 44 | 54 | |
| 45 | 55 | def _load_config(self, create_if_missing: bool = True) -> Config: |
| 46 | 56 | """Load or reload the configuration""" |
| 47 | 57 | try: |
| 48 | - config = Config(self.config_path, debug=self.debug) | |
| 58 | + config = Config(self.config_path) | |
| 49 | 59 | config.load() |
| 50 | 60 | self._config = config |
| 51 | 61 | return config |
| 52 | 62 | except FileNotFoundError: |
| 53 | 63 | if create_if_missing: |
| 64 | + logger.debug("Creating new config file") | |
| 54 | 65 | # Create empty config |
| 55 | - config = Config(self.config_path, debug=self.debug) | |
| 66 | + config = Config(self.config_path) | |
| 56 | 67 | config.ensure_config_dir() |
| 57 | 68 | config.save() |
| 58 | 69 | self._config = config |
@@ -66,44 +77,64 @@ class ShtickManager: | ||
| 66 | 77 | return self._load_config() |
| 67 | 78 | return self._config |
| 68 | 79 | |
| 69 | - def _save_and_regenerate(self) -> None: | |
| 80 | + def _save_and_regenerate(self, affected_groups: Optional[List[str]] = None) -> None: | |
| 70 | 81 | """Save config and regenerate shell files""" |
| 71 | 82 | config = self._get_config() |
| 72 | 83 | config.save() |
| 73 | 84 | |
| 74 | - # Regenerate shell files | |
| 75 | - generator = Generator() | |
| 85 | + # Regenerate shell files for affected groups | |
| 86 | + if affected_groups: | |
| 87 | + for group_name in affected_groups: | |
| 88 | + group = config.get_group(group_name) | |
| 89 | + if group: | |
| 90 | + self._generator.generate_for_group(group) | |
| 91 | + else: | |
| 92 | + # Regenerate all | |
| 93 | + for group in config.groups: | |
| 94 | + self._generator.generate_for_group(group) | |
| 95 | + | |
| 96 | + # Always regenerate loader | |
| 97 | + self._generator.generate_loader(config) | |
| 98 | + | |
| 99 | + def check_conflicts( | |
| 100 | + self, item_type: str, key: str, group_name: str | |
| 101 | + ) -> List[Tuple[str, str]]: | |
| 102 | + """ | |
| 103 | + Check for conflicts across all groups. | |
| 104 | + | |
| 105 | + Returns: | |
| 106 | + List of tuples (group_name, existing_value) for conflicts | |
| 107 | + """ | |
| 108 | + config = self._get_config() | |
| 109 | + conflicts = [] | |
| 110 | + | |
| 76 | 111 | for group in config.groups: |
| 77 | - generator.generate_for_group(group) | |
| 78 | - generator.generate_loader(config) | |
| 112 | + if group.has_item(item_type, key): | |
| 113 | + existing_value = group.get_item_value(item_type, key) | |
| 114 | + conflicts.append((group.name, existing_value)) | |
| 115 | + | |
| 116 | + return conflicts | |
| 79 | 117 | |
| 80 | 118 | # Alias management |
| 81 | - def add_persistent_alias(self, key: str, value: str) -> bool: | |
| 119 | + def add_persistent_alias( | |
| 120 | + self, key: str, value: str, check_conflicts: bool = True | |
| 121 | + ) -> bool: | |
| 82 | 122 | """ |
| 83 | 123 | Add a persistent alias (always active). |
| 84 | 124 | |
| 85 | 125 | Args: |
| 86 | 126 | key: Alias name |
| 87 | 127 | value: Alias command |
| 128 | + check_conflicts: Whether to check for conflicts | |
| 88 | 129 | |
| 89 | 130 | Returns: |
| 90 | 131 | True if successful |
| 91 | - | |
| 92 | - Example: | |
| 93 | - >>> manager.add_persistent_alias('ll', 'ls -la') | |
| 94 | - True | |
| 95 | 132 | """ |
| 96 | - try: | |
| 97 | - config = self._get_config() | |
| 98 | - config.add_item("alias", "persistent", key, value) | |
| 99 | - self._save_and_regenerate() | |
| 100 | - return True | |
| 101 | - except Exception as e: | |
| 102 | - if self.debug: | |
| 103 | - print(f"Error adding persistent alias: {e}") | |
| 104 | - return False | |
| 133 | + return self._add_item("alias", "persistent", key, value, check_conflicts) | |
| 105 | 134 | |
| 106 | - def add_alias(self, key: str, value: str, group: str) -> bool: | |
| 135 | + def add_alias( | |
| 136 | + self, key: str, value: str, group: str, check_conflicts: bool = True | |
| 137 | + ) -> bool: | |
| 107 | 138 | """ |
| 108 | 139 | Add an alias to a specific group. |
| 109 | 140 | |
@@ -111,19 +142,12 @@ class ShtickManager: | ||
| 111 | 142 | key: Alias name |
| 112 | 143 | value: Alias command |
| 113 | 144 | group: Group name |
| 145 | + check_conflicts: Whether to check for conflicts | |
| 114 | 146 | |
| 115 | 147 | Returns: |
| 116 | 148 | True if successful |
| 117 | 149 | """ |
| 118 | - try: | |
| 119 | - config = self._get_config() | |
| 120 | - config.add_item("alias", group, key, value) | |
| 121 | - self._save_and_regenerate() | |
| 122 | - return True | |
| 123 | - except Exception as e: | |
| 124 | - if self.debug: | |
| 125 | - print(f"Error adding alias: {e}") | |
| 126 | - return False | |
| 150 | + return self._add_item("alias", group, key, value, check_conflicts) | |
| 127 | 151 | |
| 128 | 152 | def remove_alias(self, key: str, group: str = "persistent") -> bool: |
| 129 | 153 | """ |
@@ -136,40 +160,28 @@ class ShtickManager: | ||
| 136 | 160 | Returns: |
| 137 | 161 | True if successful |
| 138 | 162 | """ |
| 139 | - try: | |
| 140 | - config = self._get_config() | |
| 141 | - success = config.remove_item("alias", group, key) | |
| 142 | - if success: | |
| 143 | - self._save_and_regenerate() | |
| 144 | - return success | |
| 145 | - except Exception as e: | |
| 146 | - if self.debug: | |
| 147 | - print(f"Error removing alias: {e}") | |
| 148 | - return False | |
| 163 | + return self._remove_item("alias", group, key) | |
| 149 | 164 | |
| 150 | 165 | # Environment variable management |
| 151 | - def add_persistent_env(self, key: str, value: str) -> bool: | |
| 166 | + def add_persistent_env( | |
| 167 | + self, key: str, value: str, check_conflicts: bool = True | |
| 168 | + ) -> bool: | |
| 152 | 169 | """ |
| 153 | 170 | Add a persistent environment variable (always active). |
| 154 | 171 | |
| 155 | 172 | Args: |
| 156 | 173 | key: Variable name |
| 157 | 174 | value: Variable value |
| 175 | + check_conflicts: Whether to check for conflicts | |
| 158 | 176 | |
| 159 | 177 | Returns: |
| 160 | 178 | True if successful |
| 161 | 179 | """ |
| 162 | - try: | |
| 163 | - config = self._get_config() | |
| 164 | - config.add_item("env", "persistent", key, value) | |
| 165 | - self._save_and_regenerate() | |
| 166 | - return True | |
| 167 | - except Exception as e: | |
| 168 | - if self.debug: | |
| 169 | - print(f"Error adding persistent env var: {e}") | |
| 170 | - return False | |
| 180 | + return self._add_item("env", "persistent", key, value, check_conflicts) | |
| 171 | 181 | |
| 172 | - def add_env(self, key: str, value: str, group: str) -> bool: | |
| 182 | + def add_env( | |
| 183 | + self, key: str, value: str, group: str, check_conflicts: bool = True | |
| 184 | + ) -> bool: | |
| 173 | 185 | """ |
| 174 | 186 | Add an environment variable to a specific group. |
| 175 | 187 | |
@@ -177,19 +189,12 @@ class ShtickManager: | ||
| 177 | 189 | key: Variable name |
| 178 | 190 | value: Variable value |
| 179 | 191 | group: Group name |
| 192 | + check_conflicts: Whether to check for conflicts | |
| 180 | 193 | |
| 181 | 194 | Returns: |
| 182 | 195 | True if successful |
| 183 | 196 | """ |
| 184 | - try: | |
| 185 | - config = self._get_config() | |
| 186 | - config.add_item("env", group, key, value) | |
| 187 | - self._save_and_regenerate() | |
| 188 | - return True | |
| 189 | - except Exception as e: | |
| 190 | - if self.debug: | |
| 191 | - print(f"Error adding env var: {e}") | |
| 192 | - return False | |
| 197 | + return self._add_item("env", group, key, value, check_conflicts) | |
| 193 | 198 | |
| 194 | 199 | def remove_env(self, key: str, group: str = "persistent") -> bool: |
| 195 | 200 | """ |
@@ -202,40 +207,28 @@ class ShtickManager: | ||
| 202 | 207 | Returns: |
| 203 | 208 | True if successful |
| 204 | 209 | """ |
| 205 | - try: | |
| 206 | - config = self._get_config() | |
| 207 | - success = config.remove_item("env", group, key) | |
| 208 | - if success: | |
| 209 | - self._save_and_regenerate() | |
| 210 | - return success | |
| 211 | - except Exception as e: | |
| 212 | - if self.debug: | |
| 213 | - print(f"Error removing env var: {e}") | |
| 214 | - return False | |
| 210 | + return self._remove_item("env", group, key) | |
| 215 | 211 | |
| 216 | 212 | # Function management |
| 217 | - def add_persistent_function(self, key: str, value: str) -> bool: | |
| 213 | + def add_persistent_function( | |
| 214 | + self, key: str, value: str, check_conflicts: bool = True | |
| 215 | + ) -> bool: | |
| 218 | 216 | """ |
| 219 | 217 | Add a persistent function (always active). |
| 220 | 218 | |
| 221 | 219 | Args: |
| 222 | 220 | key: Function name |
| 223 | 221 | value: Function body |
| 222 | + check_conflicts: Whether to check for conflicts | |
| 224 | 223 | |
| 225 | 224 | Returns: |
| 226 | 225 | True if successful |
| 227 | 226 | """ |
| 228 | - try: | |
| 229 | - config = self._get_config() | |
| 230 | - config.add_item("function", "persistent", key, value) | |
| 231 | - self._save_and_regenerate() | |
| 232 | - return True | |
| 233 | - except Exception as e: | |
| 234 | - if self.debug: | |
| 235 | - print(f"Error adding persistent function: {e}") | |
| 236 | - return False | |
| 227 | + return self._add_item("function", "persistent", key, value, check_conflicts) | |
| 237 | 228 | |
| 238 | - def add_function(self, key: str, value: str, group: str) -> bool: | |
| 229 | + def add_function( | |
| 230 | + self, key: str, value: str, group: str, check_conflicts: bool = True | |
| 231 | + ) -> bool: | |
| 239 | 232 | """ |
| 240 | 233 | Add a function to a specific group. |
| 241 | 234 | |
@@ -243,19 +236,12 @@ class ShtickManager: | ||
| 243 | 236 | key: Function name |
| 244 | 237 | value: Function body |
| 245 | 238 | group: Group name |
| 239 | + check_conflicts: Whether to check for conflicts | |
| 246 | 240 | |
| 247 | 241 | Returns: |
| 248 | 242 | True if successful |
| 249 | 243 | """ |
| 250 | - try: | |
| 251 | - config = self._get_config() | |
| 252 | - config.add_item("function", group, key, value) | |
| 253 | - self._save_and_regenerate() | |
| 254 | - return True | |
| 255 | - except Exception as e: | |
| 256 | - if self.debug: | |
| 257 | - print(f"Error adding function: {e}") | |
| 258 | - return False | |
| 244 | + return self._add_item("function", group, key, value, check_conflicts) | |
| 259 | 245 | |
| 260 | 246 | def remove_function(self, key: str, group: str = "persistent") -> bool: |
| 261 | 247 | """ |
@@ -268,15 +254,62 @@ class ShtickManager: | ||
| 268 | 254 | Returns: |
| 269 | 255 | True if successful |
| 270 | 256 | """ |
| 257 | + return self._remove_item("function", group, key) | |
| 258 | + | |
| 259 | + # Generic item management | |
| 260 | + def _add_item( | |
| 261 | + self, | |
| 262 | + item_type: str, | |
| 263 | + group_name: str, | |
| 264 | + key: str, | |
| 265 | + value: str, | |
| 266 | + check_conflicts: bool, | |
| 267 | + ) -> bool: | |
| 268 | + """Generic method to add any item type""" | |
| 269 | + try: | |
| 270 | + config = self._get_config() | |
| 271 | + | |
| 272 | + # Check for conflicts if requested | |
| 273 | + if check_conflicts: | |
| 274 | + conflicts = self.check_conflicts(item_type, key, group_name) | |
| 275 | + if conflicts: | |
| 276 | + logger.warning( | |
| 277 | + f"Item '{key}' exists in groups: {[c[0] for c in conflicts]}" | |
| 278 | + ) | |
| 279 | + # Still proceed but warn | |
| 280 | + | |
| 281 | + config.add_item(item_type, group_name, key, value) | |
| 282 | + | |
| 283 | + # Only regenerate files for affected group if it's active | |
| 284 | + affected_groups = [] | |
| 285 | + if group_name == "persistent" or config.is_group_active(group_name): | |
| 286 | + affected_groups.append(group_name) | |
| 287 | + | |
| 288 | + self._save_and_regenerate(affected_groups) | |
| 289 | + return True | |
| 290 | + | |
| 291 | + except Exception as e: | |
| 292 | + logger.error(f"Error adding {item_type}: {e}") | |
| 293 | + return False | |
| 294 | + | |
| 295 | + def _remove_item(self, item_type: str, group_name: str, key: str) -> bool: | |
| 296 | + """Generic method to remove any item type""" | |
| 271 | 297 | try: |
| 272 | 298 | config = self._get_config() |
| 273 | - success = config.remove_item("function", group, key) | |
| 299 | + success = config.remove_item(item_type, group_name, key) | |
| 300 | + | |
| 274 | 301 | if success: |
| 275 | - self._save_and_regenerate() | |
| 302 | + # Only regenerate if group is active | |
| 303 | + affected_groups = [] | |
| 304 | + if group_name == "persistent" or config.is_group_active(group_name): | |
| 305 | + affected_groups.append(group_name) | |
| 306 | + | |
| 307 | + self._save_and_regenerate(affected_groups) | |
| 308 | + | |
| 276 | 309 | return success |
| 310 | + | |
| 277 | 311 | except Exception as e: |
| 278 | - if self.debug: | |
| 279 | - print(f"Error removing function: {e}") | |
| 312 | + logger.error(f"Error removing {item_type}: {e}") | |
| 280 | 313 | return False |
| 281 | 314 | |
| 282 | 315 | # Group management |
@@ -294,12 +327,10 @@ class ShtickManager: | ||
| 294 | 327 | config = self._get_config() |
| 295 | 328 | success = config.activate_group(group) |
| 296 | 329 | if success: |
| 297 | - generator = Generator() | |
| 298 | - generator.generate_loader(config) | |
| 330 | + self._generator.generate_loader(config) | |
| 299 | 331 | return success |
| 300 | 332 | except Exception as e: |
| 301 | - if self.debug: | |
| 302 | - print(f"Error activating group: {e}") | |
| 333 | + logger.error(f"Error activating group: {e}") | |
| 303 | 334 | return False |
| 304 | 335 | |
| 305 | 336 | def deactivate_group(self, group: str) -> bool: |
@@ -316,12 +347,10 @@ class ShtickManager: | ||
| 316 | 347 | config = self._get_config() |
| 317 | 348 | success = config.deactivate_group(group) |
| 318 | 349 | if success: |
| 319 | - generator = Generator() | |
| 320 | - generator.generate_loader(config) | |
| 350 | + self._generator.generate_loader(config) | |
| 321 | 351 | return success |
| 322 | 352 | except Exception as e: |
| 323 | - if self.debug: | |
| 324 | - print(f"Error deactivating group: {e}") | |
| 353 | + logger.error(f"Error deactivating group: {e}") | |
| 325 | 354 | return False |
| 326 | 355 | |
| 327 | 356 | def get_active_groups(self) -> List[str]: |
@@ -364,7 +393,7 @@ class ShtickManager: | ||
| 364 | 393 | regular_groups = config.get_regular_groups() |
| 365 | 394 | active_groups = config.load_active_groups() |
| 366 | 395 | |
| 367 | - current_shell = os.path.basename(os.environ.get("SHELL", "")) | |
| 396 | + current_shell = Config.get_current_shell() or "" | |
| 368 | 397 | loader_exists = False |
| 369 | 398 | if current_shell: |
| 370 | 399 | loader_path = os.path.expanduser( |
@@ -372,13 +401,7 @@ class ShtickManager: | ||
| 372 | 401 | ) |
| 373 | 402 | loader_exists = os.path.exists(loader_path) |
| 374 | 403 | |
| 375 | - persistent_count = 0 | |
| 376 | - if persistent_group: | |
| 377 | - persistent_count = ( | |
| 378 | - len(persistent_group.aliases) | |
| 379 | - + len(persistent_group.env_vars) | |
| 380 | - + len(persistent_group.functions) | |
| 381 | - ) | |
| 404 | + persistent_count = persistent_group.total_items if persistent_group else 0 | |
| 382 | 405 | |
| 383 | 406 | return { |
| 384 | 407 | "current_shell": current_shell, |
@@ -419,49 +442,24 @@ class ShtickManager: | ||
| 419 | 442 | groups_to_check = [g for g in groups_to_check if g is not None] |
| 420 | 443 | |
| 421 | 444 | for g in groups_to_check: |
| 422 | - # Add aliases | |
| 423 | - for key, value in g.aliases.items(): | |
| 424 | - items.append( | |
| 425 | - { | |
| 426 | - "group": g.name, | |
| 427 | - "type": "alias", | |
| 428 | - "key": key, | |
| 429 | - "value": value, | |
| 430 | - "active": config.is_group_active(g.name) | |
| 431 | - or g.name == "persistent", | |
| 432 | - } | |
| 433 | - ) | |
| 434 | - | |
| 435 | - # Add env vars | |
| 436 | - for key, value in g.env_vars.items(): | |
| 437 | - items.append( | |
| 438 | - { | |
| 439 | - "group": g.name, | |
| 440 | - "type": "env", | |
| 441 | - "key": key, | |
| 442 | - "value": value, | |
| 443 | - "active": config.is_group_active(g.name) | |
| 444 | - or g.name == "persistent", | |
| 445 | - } | |
| 446 | - ) | |
| 447 | - | |
| 448 | - # Add functions | |
| 449 | - for key, value in g.functions.items(): | |
| 450 | - items.append( | |
| 451 | - { | |
| 452 | - "group": g.name, | |
| 453 | - "type": "function", | |
| 454 | - "key": key, | |
| 455 | - "value": value, | |
| 456 | - "active": config.is_group_active(g.name) | |
| 457 | - or g.name == "persistent", | |
| 458 | - } | |
| 459 | - ) | |
| 445 | + # Process each item type | |
| 446 | + for item_type in ["alias", "env", "function"]: | |
| 447 | + item_dict = g.get_items(item_type) | |
| 448 | + for key, value in item_dict.items(): | |
| 449 | + items.append( | |
| 450 | + { | |
| 451 | + "group": g.name, | |
| 452 | + "type": item_type, | |
| 453 | + "key": key, | |
| 454 | + "value": value, | |
| 455 | + "active": config.is_group_active(g.name) | |
| 456 | + or g.name == "persistent", | |
| 457 | + } | |
| 458 | + ) | |
| 460 | 459 | |
| 461 | 460 | return items |
| 462 | 461 | except Exception as e: |
| 463 | - if self.debug: | |
| 464 | - print(f"Error listing items: {e}") | |
| 462 | + logger.error(f"Error listing items: {e}") | |
| 465 | 463 | return [] |
| 466 | 464 | |
| 467 | 465 | def generate_shell_files(self) -> bool: |
@@ -473,14 +471,10 @@ class ShtickManager: | ||
| 473 | 471 | """ |
| 474 | 472 | try: |
| 475 | 473 | config = self._get_config() |
| 476 | - generator = Generator() | |
| 477 | - for group in config.groups: | |
| 478 | - generator.generate_for_group(group) | |
| 479 | - generator.generate_loader(config) | |
| 474 | + self._generator.generate_all(config, interactive=False) | |
| 480 | 475 | return True |
| 481 | 476 | except Exception as e: |
| 482 | - if self.debug: | |
| 483 | - print(f"Error generating shell files: {e}") | |
| 477 | + logger.error(f"Error generating shell files: {e}") | |
| 484 | 478 | return False |
| 485 | 479 | |
| 486 | 480 | def get_source_command(self, shell: Optional[str] = None) -> Optional[str]: |
@@ -494,7 +488,7 @@ class ShtickManager: | ||
| 494 | 488 | Source command string or None if not available |
| 495 | 489 | """ |
| 496 | 490 | try: |
| 497 | - current_shell = shell or os.path.basename(os.environ.get("SHELL", "")) | |
| 491 | + current_shell = shell or Config.get_current_shell() | |
| 498 | 492 | if not current_shell: |
| 499 | 493 | return None |
| 500 | 494 | |