tenseleyflow/shtick / 96ed1a0

Browse files

refactor cleanup stable

Authored by espadonne
SHA
96ed1a02229f4686142e8e8c9209e12b1aaaf40a
Parents
5a0b93c
Tree
243d149

7 changed files

StatusFile+-
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
88
 import argparse
99
 from shtick.commands import ShtickCommands
1010
 from shtick.display import DisplayCommands
11
+from shtick.logger import setup_logging
1112
 
1213
 
1314
 def main():
@@ -125,6 +126,9 @@ def main():
125126
 
126127
     args = parser.parse_args()
127128
 
129
+    # Set up logging first
130
+    logger = setup_logging(debug=args.debug)
131
+
128132
     if not args.command:
129133
         # Show helpful getting started message
130134
         parser.print_help()
@@ -134,39 +138,51 @@ def main():
134138
         print("  shtick list                   # List all items")
135139
         sys.exit(1)
136140
 
137
-    # Initialize command handlers
141
+    # Initialize command handlers with debug flag
138142
     commands = ShtickCommands(debug=args.debug)
139143
     display = DisplayCommands(debug=args.debug)
140144
 
141145
     # 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)
170186
 
171187
 
172188
 if __name__ == "__main__":
src/shtick/commands.pymodified
@@ -1,27 +1,36 @@
11
 """
2
-Command implementations for shtick CLI
2
+Command implementations for shtick CLI - REFACTORED to use ShtickManager
33
 """
44
 
55
 import os
66
 import sys
77
 import subprocess
8
+import logging
89
 from typing import Optional, List
910
 
11
+from shtick.shtick import ShtickManager
1012
 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")
1315
 
1416
 
1517
 class ShtickCommands:
16
-    """Central command handler for shtick operations"""
18
+    """Central command handler for shtick operations - now using ShtickManager"""
1719
 
1820
     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)
2030
 
2131
     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()
2534
 
2635
     def validate_assignment(self, assignment: str) -> tuple[str, str]:
2736
         """Validate key=value assignment format and return key, value"""
@@ -43,84 +52,6 @@ class ShtickCommands:
4352
 
4453
         return key, value
4554
 
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
-
12455
     def offer_auto_source(self):
12556
         """Offer to source shtick in current shell session"""
12657
         current_shell = self.get_current_shell()
@@ -141,43 +72,10 @@ class ShtickCommands:
14172
                 .lower()
14273
             )
14374
             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)
14776
         except (KeyboardInterrupt, EOFError):
14877
             print()
14978
 
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
-
18179
     def _show_source_instructions(self, shell: str):
18280
         """Show instructions for sourcing"""
18381
         print(f"\n🎯 Copy and paste this command to load changes immediately:")
@@ -275,7 +173,7 @@ class ShtickCommands:
275173
                     print(f"✓ Added shtick integration to {config_file}")
276174
                     return True
277175
                 except Exception as e:
278
-                    print(f"✗ Failed to modify {config_file}: {e}")
176
+                    logger.error(f"Failed to modify {config_file}: {e}")
279177
                     continue
280178
 
281179
         # If no existing config file found, create the primary one
@@ -289,27 +187,29 @@ class ShtickCommands:
289187
             print(f"✓ Created {primary_config} with shtick integration")
290188
             return True
291189
         except Exception as e:
292
-            print(f"✗ Failed to create {primary_config}: {e}")
190
+            logger.error(f"Failed to create {primary_config}: {e}")
293191
             return False
294192
 
295
-    # Command implementations
193
+    # Command implementations - now using ShtickManager
296194
     def generate(self, config_path: str = None, terse: bool = False):
297195
         """Generate shell files from config"""
298
-        config_path = config_path or Config.get_default_config_path()
299
-
300196
         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
306202
 
307
-            if not terse:
203
+            success = manager.generate_shell_files()
204
+            if success and not terse:
308205
                 self.check_shell_integration()
206
+            elif not success:
207
+                print("Error: Failed to generate shell files")
208
+                sys.exit(1)
309209
 
310210
         except FileNotFoundError as e:
311211
             print(f"Error: {e}")
312
-            print(f"Create a config file at {config_path} first")
212
+            print(f"Create a config file first")
313213
             sys.exit(1)
314214
         except Exception as e:
315215
             print(f"Error: {e}")
@@ -323,31 +223,27 @@ class ShtickCommands:
323223
             print(f"Error: {e}")
324224
             sys.exit(1)
325225
 
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)
342236
 
237
+        if success:
343238
             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}")
351247
             sys.exit(1)
352248
 
353249
     def add_persistent(self, item_type: str, assignment: str):
@@ -358,50 +254,43 @@ class ShtickCommands:
358254
             print(f"Error: {e}")
359255
             sys.exit(1)
360256
 
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())
363258
 
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)
378269
 
270
+        if success:
379271
             print(
380272
                 f"✓ Added {item_type} '{key}' = '{value}' to persistent group (always active)"
381273
             )
382
-
383
-            # Auto-regenerate files
384
-            self.regenerate_and_offer_source(config, "persistent")
274
+            self.offer_auto_source()
385275
 
386276
             # First-time setup experience
387277
             if is_first_time:
388278
                 print("\n🎉 Welcome to shtick!")
389279
                 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}")
393282
             sys.exit(1)
394283
 
395284
     def remove_item(self, item_type: str, group: str, search: str):
396285
         """Remove an item from a group"""
397
-        config_path = Config.get_default_config_path()
398
-
399286
         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
+            ]
405294
 
406295
             if not matches:
407296
                 print(
@@ -414,19 +303,25 @@ class ShtickCommands:
414303
             if not item_to_remove:
415304
                 return
416305
 
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
420316
 
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()
424322
             else:
425323
                 print(f"Failed to remove {item_type} '{item_to_remove}'")
426324
 
427
-        except FileNotFoundError:
428
-            print(f"Config file not found: {config_path}")
429
-            sys.exit(1)
430325
         except Exception as e:
431326
             print(f"Error: {e}")
432327
             sys.exit(1)
@@ -459,69 +354,36 @@ class ShtickCommands:
459354
 
460355
     def activate_group(self, group_name: str):
461356
         """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
473362
 
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)}")
491373
 
492374
     def deactivate_group(self, group_name: str):
493375
         """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
518379
 
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")
525387
 
526388
     def source_command(self, shell: str = None):
527389
         """Output source command for eval"""
src/shtick/config.pymodified
@@ -1,12 +1,15 @@
11
 """
2
-Configuration parsing for shtick
2
+Configuration parsing for shtick - REFACTORED
33
 """
44
 
55
 import os
66
 import tomllib
7
+import logging
78
 from pathlib import Path
89
 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")
1013
 
1114
 
1215
 @dataclass
@@ -18,13 +21,51 @@ class GroupData:
1821
     env_vars: Dict[str, str]
1922
     functions: Dict[str, str]
2023
 
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
+
2160
 
2261
 class Config:
2362
     """Main configuration handler"""
2463
 
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):
2668
         self.config_path = config_path or self.get_default_config_path()
27
-        self.debug = debug
2869
         self.groups: List[GroupData] = []
2970
 
3071
     @staticmethod
@@ -42,6 +83,19 @@ class Config:
4283
         """Get the active groups state file path"""
4384
         return os.path.expanduser("~/.config/shtick/active_groups")
4485
 
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
+
4599
     def load_active_groups(self) -> List[str]:
46100
         """Load list of currently active groups"""
47101
         active_file = self.get_active_groups_file()
@@ -104,28 +158,19 @@ class Config:
104158
         if not os.path.exists(self.config_path):
105159
             raise FileNotFoundError(f"Config file not found: {self.config_path}")
106160
 
107
-        if self.debug:
108
-            print(f"Debug: Loading config from: {self.config_path}")
161
+        logger.debug(f"Loading config from: {self.config_path}")
109162
 
110163
         with open(self.config_path, "rb") as f:
111164
             data = tomllib.load(f)
112165
 
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())}")
116167
 
117168
         self.groups = []
118169
 
119170
         # Parse groups from nested TOML structure
120171
         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
-
126172
             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}'")
129174
                 continue
130175
 
131176
             # Initialize group data
@@ -133,64 +178,44 @@ class Config:
133178
 
134179
             # Extract each section from the group
135180
             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
-
141181
                 if section_name == "aliases" and isinstance(section_data, dict):
142182
                     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}'")
147184
                 elif section_name == "env_vars" and isinstance(section_data, dict):
148185
                     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
+                    )
153189
                 elif section_name == "functions" and isinstance(section_data, dict):
154190
                     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
+                    )
159194
                 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
+                    )
164198
 
165199
             # 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"],
170205
             )
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:
181208
                 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
+                )
186213
             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")
189215
 
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
+        )
194219
 
195220
     def save(self) -> None:
196221
         """Save the current configuration back to TOML file"""
@@ -236,33 +261,14 @@ class Config:
236261
     def add_item(self, item_type: str, group_name: str, key: str, value: str) -> None:
237262
         """Add an alias, env var, or function to a group"""
238263
         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)
248265
 
249266
     def remove_item(self, item_type: str, group_name: str, key: str) -> bool:
250267
         """Remove an item from a group. Returns True if found and removed."""
251268
         group = self.get_group(group_name)
252269
         if not group:
253270
             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)
266272
 
267273
     def find_items(
268274
         self, item_type: str, group_name: str, search_term: str
@@ -272,14 +278,26 @@ class Config:
272278
         if not group:
273279
             return []
274280
 
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)
284282
         # 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
44
 
55
 import os
66
 import sys
7
+import logging
78
 from shtick.config import Config
89
 from shtick.shells import get_supported_shells
10
+from shtick.shtick import ShtickManager
11
+
12
+logger = logging.getLogger("shtick")
913
 
1014
 
1115
 class DisplayCommands:
1216
     """Handles all display/listing commands for shtick"""
1317
 
1418
     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)
1628
 
1729
     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()
2132
 
2233
     def status(self):
2334
         """Show status of groups and active state"""
24
-        config_path = Config.get_default_config_path()
35
+        status = self.manager.get_status()
2536
 
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
4942
 
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")
5851
             else:
59
-                print("Persistent: No items")
52
+                print(f"Loader file: ✗ missing (run 'shtick generate')")
53
+        print()
6054
 
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")
6260
 
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()
7462
 
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")
7675
 
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()
8277
 
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")
8883
 
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')
9789
 
9890
     def list_config(self, long_format: bool = False):
9991
         """List current configuration"""
100
-        config_path = Config.get_default_config_path()
92
+        items = self.manager.list_items()
10193
 
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):
135128
         """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:
138131
             print("Group: persistent (always active)")
139
-            self._print_group_items(persistent_group)
132
+            self._print_group_items_detailed(groups_data["persistent"])
140133
             print()
134
+            del groups_data["persistent"]
141135
 
142136
         # 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)
147141
             print()
148142
 
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():
154148
                 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():
158152
                 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():
162156
                 print(f"    {key} = {value}")
163157
 
164
-    def _print_tabular_list(self, persistent_group, regular_groups, active_groups):
158
+    def _print_tabular_list(self, items):
165159
         """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
-
178160
         if not items:
179
-            print("No items configured")
180161
             return
181162
 
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"""
198163
         # 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"
202167
         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
204169
         )  # "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"
206173
 
207174
         # Print header
208175
         header = f"{'Group':<{max_group}} {'Type':<{max_type}} {'Key':<{max_key}} {'Value':<{max_value}} {'Status':<{max_status}}"
209176
         print(header)
210177
         print("-" * len(header))
211178
 
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
+
212186
         # Print items
213
-        for group, item_type, key, value, status in items:
187
+        for item in sorted_items:
214188
             # Truncate long values with ellipsis
189
+            value = item["value"]
215190
             display_value = (
216191
                 value if len(value) <= max_value else value[: max_value - 3] + "..."
217192
             )
193
+            status = "ACTIVE" if item["active"] else "inactive"
218194
             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}}"
220197
             )
221198
 
222
-    def _print_summary(self, items, active_groups):
199
+        # Print summary
200
+        self._print_summary(items)
201
+
202
+    def _print_summary(self, items):
223203
         """Print summary information"""
224204
         print()
225205
         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"]])
229207
         print(f"Total: {total_items} items ({active_items} active)")
230208
 
231209
         # Show available commands
232210
         print()
233211
         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:
236220
             print(f"Activate groups with: shtick activate <group>")
237221
             print(f"Inactive groups: {', '.join(sorted(inactive_groups))}")
238222
 
src/shtick/generator.pymodified
@@ -3,10 +3,13 @@ Shell file generator for shtick
33
 """
44
 
55
 import os
6
+import logging
67
 from pathlib import Path
7
-from typing import Dict, List
8
+from typing import Dict, List, Set
89
 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")
1013
 
1114
 
1215
 class Generator:
@@ -14,63 +17,100 @@ class Generator:
1417
 
1518
     def __init__(self, output_base_dir: str = None):
1619
         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:
1933
         """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)
2135
         Path(output_dir).mkdir(parents=True, exist_ok=True)
2236
         return output_dir
2337
 
2438
     def generate_for_group(self, group: GroupData) -> None:
2539
         """Generate all shell files for a single group"""
26
-        print(f"Processing group: {group.name}")
40
+        logger.info(f"Processing group: {group.name}")
2741
 
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
3246
 
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)
3749
 
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)
4254
 
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,
4569
     ) -> 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")
72106
 
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)
73112
                     f.write(line)
113
+                f.write("\n")
74114
 
75115
     def generate_all(self, config: Config, interactive: bool = True) -> None:
76116
         """Generate shell files for all groups in config"""
@@ -79,6 +119,7 @@ class Generator:
79119
             return
80120
 
81121
         print(f"Generating shell files for {len(config.groups)} groups...")
122
+        print(f"Target shells: {', '.join(self.shells_to_generate)}")
82123
 
83124
         for group in config.groups:
84125
             self.generate_for_group(group)
@@ -86,20 +127,19 @@ class Generator:
86127
         # Generate the dynamic loader for all shells
87128
         self.generate_loader(config)
88129
 
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)
91134
 
92135
     def generate_loader(self, config: Config) -> None:
93136
         """Generate dynamic loader files that source persistent + active groups"""
94
-        print("Generating dynamic loader files...")
137
+        logger.info("Generating dynamic loader files...")
95138
 
96139
         active_groups = config.load_active_groups()
97140
         persistent_group = config.get_persistent_group()
98141
 
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:
103143
             loader_path = os.path.join(
104144
                 self.output_base_dir, f"load_active.{shell_name}"
105145
             )
@@ -109,58 +149,64 @@ class Generator:
109149
                 f.write("# This file is auto-generated - do not edit\n\n")
110150
 
111151
                 # Source persistent group first (always active)
112
-                if persistent_group:
152
+                if persistent_group and persistent_group.total_items > 0:
113153
                     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')
125156
 
126157
                 # Source active groups
127158
                 if active_groups:
128159
                     f.write("# Load active groups\n")
129160
                     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
+                            )
140167
                             f.write(f'[ -f "{file_path}" ] && source "{file_path}"\n')
141
-                        f.write("\n")
168
+                    f.write("\n")
142169
                 else:
143170
                     f.write("# No active groups\n")
144171
 
145
-    def _print_usage_instructions(
146
-        self, config: Config, interactive: bool = True
147
-    ) -> None:
172
+    def _print_usage_instructions(self, config: Config) -> None:
148173
         """Print usage instructions for the user"""
149174
         active_groups = config.load_active_groups()
150175
         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}")
154196
         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)
156204
 
205
+        # Status summary
157206
         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"
162209
             )
163
-            print(f"\nPersistent items (always active): {total_persistent} total")
164210
 
165211
         if active_groups:
166212
             print(f"Active groups: {', '.join(active_groups)}")
@@ -173,144 +219,16 @@ class Generator:
173219
         if available_groups:
174220
             print(f"Available groups: {', '.join(available_groups)}")
175221
 
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]:
304223
         """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)
315233
 
316234
         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
33
 """
44
 
55
 import os
6
-from typing import List, Dict, Optional, Union
6
+import logging
7
+from typing import List, Dict, Optional, Union, Tuple
78
 from .config import Config, GroupData
89
 from .generator import Generator
9
-from .commands import ShtickCommands
10
+
11
+logger = logging.getLogger("shtick")
1012
 
1113
 
1214
 class ShtickManager:
@@ -38,21 +40,30 @@ class ShtickManager:
3840
             debug: Enable debug output
3941
         """
4042
         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
+
4252
         self._config = None
43
-        self._commands = ShtickCommands(debug=debug)
53
+        self._generator = Generator()
4454
 
4555
     def _load_config(self, create_if_missing: bool = True) -> Config:
4656
         """Load or reload the configuration"""
4757
         try:
48
-            config = Config(self.config_path, debug=self.debug)
58
+            config = Config(self.config_path)
4959
             config.load()
5060
             self._config = config
5161
             return config
5262
         except FileNotFoundError:
5363
             if create_if_missing:
64
+                logger.debug("Creating new config file")
5465
                 # Create empty config
55
-                config = Config(self.config_path, debug=self.debug)
66
+                config = Config(self.config_path)
5667
                 config.ensure_config_dir()
5768
                 config.save()
5869
                 self._config = config
@@ -66,44 +77,64 @@ class ShtickManager:
6677
             return self._load_config()
6778
         return self._config
6879
 
69
-    def _save_and_regenerate(self) -> None:
80
+    def _save_and_regenerate(self, affected_groups: Optional[List[str]] = None) -> None:
7081
         """Save config and regenerate shell files"""
7182
         config = self._get_config()
7283
         config.save()
7384
 
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
+
76111
         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
79117
 
80118
     # 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:
82122
         """
83123
         Add a persistent alias (always active).
84124
 
85125
         Args:
86126
             key: Alias name
87127
             value: Alias command
128
+            check_conflicts: Whether to check for conflicts
88129
 
89130
         Returns:
90131
             True if successful
91
-
92
-        Example:
93
-            >>> manager.add_persistent_alias('ll', 'ls -la')
94
-            True
95132
         """
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)
105134
 
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:
107138
         """
108139
         Add an alias to a specific group.
109140
 
@@ -111,19 +142,12 @@ class ShtickManager:
111142
             key: Alias name
112143
             value: Alias command
113144
             group: Group name
145
+            check_conflicts: Whether to check for conflicts
114146
 
115147
         Returns:
116148
             True if successful
117149
         """
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)
127151
 
128152
     def remove_alias(self, key: str, group: str = "persistent") -> bool:
129153
         """
@@ -136,40 +160,28 @@ class ShtickManager:
136160
         Returns:
137161
             True if successful
138162
         """
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)
149164
 
150165
     # 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:
152169
         """
153170
         Add a persistent environment variable (always active).
154171
 
155172
         Args:
156173
             key: Variable name
157174
             value: Variable value
175
+            check_conflicts: Whether to check for conflicts
158176
 
159177
         Returns:
160178
             True if successful
161179
         """
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)
171181
 
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:
173185
         """
174186
         Add an environment variable to a specific group.
175187
 
@@ -177,19 +189,12 @@ class ShtickManager:
177189
             key: Variable name
178190
             value: Variable value
179191
             group: Group name
192
+            check_conflicts: Whether to check for conflicts
180193
 
181194
         Returns:
182195
             True if successful
183196
         """
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)
193198
 
194199
     def remove_env(self, key: str, group: str = "persistent") -> bool:
195200
         """
@@ -202,40 +207,28 @@ class ShtickManager:
202207
         Returns:
203208
             True if successful
204209
         """
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)
215211
 
216212
     # 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:
218216
         """
219217
         Add a persistent function (always active).
220218
 
221219
         Args:
222220
             key: Function name
223221
             value: Function body
222
+            check_conflicts: Whether to check for conflicts
224223
 
225224
         Returns:
226225
             True if successful
227226
         """
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)
237228
 
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:
239232
         """
240233
         Add a function to a specific group.
241234
 
@@ -243,19 +236,12 @@ class ShtickManager:
243236
             key: Function name
244237
             value: Function body
245238
             group: Group name
239
+            check_conflicts: Whether to check for conflicts
246240
 
247241
         Returns:
248242
             True if successful
249243
         """
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)
259245
 
260246
     def remove_function(self, key: str, group: str = "persistent") -> bool:
261247
         """
@@ -268,15 +254,62 @@ class ShtickManager:
268254
         Returns:
269255
             True if successful
270256
         """
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"""
271297
         try:
272298
             config = self._get_config()
273
-            success = config.remove_item("function", group, key)
299
+            success = config.remove_item(item_type, group_name, key)
300
+
274301
             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
+
276309
             return success
310
+
277311
         except Exception as e:
278
-            if self.debug:
279
-                print(f"Error removing function: {e}")
312
+            logger.error(f"Error removing {item_type}: {e}")
280313
             return False
281314
 
282315
     # Group management
@@ -294,12 +327,10 @@ class ShtickManager:
294327
             config = self._get_config()
295328
             success = config.activate_group(group)
296329
             if success:
297
-                generator = Generator()
298
-                generator.generate_loader(config)
330
+                self._generator.generate_loader(config)
299331
             return success
300332
         except Exception as e:
301
-            if self.debug:
302
-                print(f"Error activating group: {e}")
333
+            logger.error(f"Error activating group: {e}")
303334
             return False
304335
 
305336
     def deactivate_group(self, group: str) -> bool:
@@ -316,12 +347,10 @@ class ShtickManager:
316347
             config = self._get_config()
317348
             success = config.deactivate_group(group)
318349
             if success:
319
-                generator = Generator()
320
-                generator.generate_loader(config)
350
+                self._generator.generate_loader(config)
321351
             return success
322352
         except Exception as e:
323
-            if self.debug:
324
-                print(f"Error deactivating group: {e}")
353
+            logger.error(f"Error deactivating group: {e}")
325354
             return False
326355
 
327356
     def get_active_groups(self) -> List[str]:
@@ -364,7 +393,7 @@ class ShtickManager:
364393
             regular_groups = config.get_regular_groups()
365394
             active_groups = config.load_active_groups()
366395
 
367
-            current_shell = os.path.basename(os.environ.get("SHELL", ""))
396
+            current_shell = Config.get_current_shell() or ""
368397
             loader_exists = False
369398
             if current_shell:
370399
                 loader_path = os.path.expanduser(
@@ -372,13 +401,7 @@ class ShtickManager:
372401
                 )
373402
                 loader_exists = os.path.exists(loader_path)
374403
 
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
382405
 
383406
             return {
384407
                 "current_shell": current_shell,
@@ -419,49 +442,24 @@ class ShtickManager:
419442
             groups_to_check = [g for g in groups_to_check if g is not None]
420443
 
421444
             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
+                        )
460459
 
461460
             return items
462461
         except Exception as e:
463
-            if self.debug:
464
-                print(f"Error listing items: {e}")
462
+            logger.error(f"Error listing items: {e}")
465463
             return []
466464
 
467465
     def generate_shell_files(self) -> bool:
@@ -473,14 +471,10 @@ class ShtickManager:
473471
         """
474472
         try:
475473
             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)
480475
             return True
481476
         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}")
484478
             return False
485479
 
486480
     def get_source_command(self, shell: Optional[str] = None) -> Optional[str]:
@@ -494,7 +488,7 @@ class ShtickManager:
494488
             Source command string or None if not available
495489
         """
496490
         try:
497
-            current_shell = shell or os.path.basename(os.environ.get("SHELL", ""))
491
+            current_shell = shell or Config.get_current_shell()
498492
             if not current_shell:
499493
                 return None
500494