tenseleyflow/shtick / ec1f957

Browse files

stable, interactive mode

Authored by espadonne
SHA
ec1f957251882b1af459d5219023bf48eefcc628
Parents
b2cc963
Tree
899fce8

3 changed files

StatusFile+-
M src/shtick/cli.py 196 138
M src/shtick/config.py 45 27
M src/shtick/generator.py 157 16
src/shtick/cli.pymodified
@@ -9,22 +9,23 @@ import argparse
99
 import os
1010
 from typing import Optional
1111
 
12
-# Package imports  
12
+# Package imports
1313
 from shtick.config import Config
1414
 from shtick.generator import Generator
1515
 from shtick.shells import get_supported_shells
1616
 
17
+
1718
 def cmd_generate(args) -> None:
1819
     """Generate shell files from config"""
1920
     config_path = args.config or Config.get_default_config_path()
20
-    
21
+
2122
     try:
22
-        config = Config(config_path)
23
+        config = Config(config_path, debug=args.debug)
2324
         config.load()
24
-        
25
+
2526
         generator = Generator()
26
-        generator.generate_all(config)
27
-        
27
+        generator.generate_all(config, interactive=args.interactive)
28
+
2829
     except FileNotFoundError as e:
2930
         print(f"Error: {e}")
3031
         print(f"Create a config file at {config_path} first")
@@ -33,54 +34,58 @@ def cmd_generate(args) -> None:
3334
         print(f"Error: {e}")
3435
         sys.exit(1)
3536
 
37
+
3638
 def cmd_add(args) -> None:
3739
     """Add an item to the config"""
38
-    if '=' not in args.assignment:
40
+    if "=" not in args.assignment:
3941
         print("Error: Assignment must be in format key=value")
4042
         sys.exit(1)
41
-    
42
-    key, value = args.assignment.split('=', 1)
43
+
44
+    key, value = args.assignment.split("=", 1)
4345
     key = key.strip()
4446
     value = value.strip()
45
-    
47
+
4648
     if not key or not value:
4749
         print("Error: Both key and value must be non-empty")
4850
         sys.exit(1)
49
-    
51
+
5052
     config_path = Config.get_default_config_path()
51
-    
53
+
5254
     try:
53
-        config = Config(config_path)
55
+        config = Config(config_path, debug=getattr(args, "debug", False))
5456
         # Try to load existing config, create empty if doesn't exist
5557
         try:
5658
             config.load()
5759
         except FileNotFoundError:
5860
             print(f"Creating new config file at {config_path}")
59
-        
61
+
6062
         config.add_item(args.type, args.group, key, value)
6163
         config.save()
62
-        
64
+
6365
         print(f"Added {args.type} '{key}' = '{value}' to group '{args.group}'")
64
-        
66
+
6567
     except Exception as e:
6668
         print(f"Error: {e}")
6769
         sys.exit(1)
6870
 
71
+
6972
 def cmd_remove(args) -> None:
7073
     """Remove an item from the config"""
7174
     config_path = Config.get_default_config_path()
72
-    
75
+
7376
     try:
74
-        config = Config(config_path)
77
+        config = Config(config_path, debug=getattr(args, "debug", False))
7578
         config.load()
76
-        
79
+
7780
         # Find matching items
7881
         matches = config.find_items(args.type, args.group, args.search)
79
-        
82
+
8083
         if not matches:
81
-            print(f"No {args.type} items matching '{args.search}' found in group '{args.group}'")
84
+            print(
85
+                f"No {args.type} items matching '{args.search}' found in group '{args.group}'"
86
+            )
8287
             return
83
-        
88
+
8489
         if len(matches) == 1:
8590
             # Exact match, remove it
8691
             item = matches[0]
@@ -94,13 +99,13 @@ def cmd_remove(args) -> None:
9499
             print(f"Found {len(matches)} matches:")
95100
             for i, item in enumerate(matches, 1):
96101
                 print(f"  {i}. {item}")
97
-            
102
+
98103
             try:
99104
                 choice = input("Enter number to remove (or 'q' to quit): ").strip()
100
-                if choice.lower() == 'q':
105
+                if choice.lower() == "q":
101106
                     print("Cancelled")
102107
                     return
103
-                
108
+
104109
                 idx = int(choice) - 1
105110
                 if 0 <= idx < len(matches):
106111
                     item = matches[idx]
@@ -113,7 +118,7 @@ def cmd_remove(args) -> None:
113118
                     print("Invalid choice")
114119
             except (ValueError, KeyboardInterrupt):
115120
                 print("\nCancelled")
116
-    
121
+
117122
     except FileNotFoundError:
118123
         print(f"Config file not found: {config_path}")
119124
         sys.exit(1)
@@ -121,24 +126,32 @@ def cmd_remove(args) -> None:
121126
         print(f"Error: {e}")
122127
         sys.exit(1)
123128
 
129
+
124130
 def cmd_activate(args) -> None:
125131
     """Activate a group"""
126132
     config_path = Config.get_default_config_path()
127
-    
133
+
128134
     try:
129
-        config = Config(config_path)
135
+        config = Config(config_path, debug=getattr(args, "debug", False))
130136
         config.load()
131
-        
132
-        if args.group == 'persistent':
133
-            print("Error: 'persistent' group is always active and cannot be manually activated")
137
+
138
+        if args.group == "persistent":
139
+            print(
140
+                "Error: 'persistent' group is always active and cannot be manually activated"
141
+            )
134142
             return
135
-        
143
+
136144
         if config.activate_group(args.group):
137
-            # Regenerate loader to include newly activated group
145
+            # Regenerate all files to ensure they exist and are up to date
138146
             from shtick.generator import Generator
147
+
139148
             generator = Generator()
149
+            # Generate shell files for all groups (not just the activated one)
150
+            for group in config.groups:
151
+                generator.generate_for_group(group)
152
+            # Then regenerate the loader to include newly activated group
140153
             generator.generate_loader(config)
141
-            
154
+
142155
             print(f"Activated group '{args.group}'")
143156
             print("Changes are now active in new shell sessions")
144157
         else:
@@ -146,7 +159,7 @@ def cmd_activate(args) -> None:
146159
             available = [g.name for g in config.get_regular_groups()]
147160
             if available:
148161
                 print(f"Available groups: {', '.join(available)}")
149
-    
162
+
150163
     except FileNotFoundError:
151164
         print(f"Config file not found: {config_path}")
152165
         print("Run 'shtick generate' first or add some items with 'shtick add'")
@@ -155,29 +168,32 @@ def cmd_activate(args) -> None:
155168
         print(f"Error: {e}")
156169
         sys.exit(1)
157170
 
171
+
158172
 def cmd_deactivate(args) -> None:
159173
     """Deactivate a group"""
160174
     config_path = Config.get_default_config_path()
161
-    
175
+
162176
     try:
163
-        config = Config(config_path)
177
+        config = Config(config_path, debug=getattr(args, "debug", False))
164178
         config.load()
165
-        
166
-        if args.group == 'persistent':
179
+
180
+        if args.group == "persistent":
167181
             print("Error: 'persistent' group cannot be deactivated")
168182
             return
169
-        
183
+
170184
         if config.deactivate_group(args.group):
171185
             # Regenerate loader to exclude deactivated group
172186
             from shtick.generator import Generator
187
+
173188
             generator = Generator()
189
+            # Regenerate loader (no need to regenerate all files for deactivation)
174190
             generator.generate_loader(config)
175
-            
191
+
176192
             print(f"Deactivated group '{args.group}'")
177193
             print("Changes will take effect in new shell sessions")
178194
         else:
179195
             print(f"Group '{args.group}' was not active")
180
-    
196
+
181197
     except FileNotFoundError:
182198
         print(f"Config file not found: {config_path}")
183199
         sys.exit(1)
@@ -185,36 +201,40 @@ def cmd_deactivate(args) -> None:
185201
         print(f"Error: {e}")
186202
         sys.exit(1)
187203
 
204
+
188205
 def cmd_list(args) -> None:
189206
     """List current configuration"""
190207
     config_path = Config.get_default_config_path()
191
-    
208
+
192209
     try:
193
-        config = Config(config_path)
210
+        config = Config(config_path, debug=getattr(args, "debug", False))
194211
         config.load()
195
-        
212
+
196213
         if not config.groups:
197214
             print("No groups configured")
198215
             return
199
-        
216
+
200217
         persistent_group = config.get_persistent_group()
201218
         regular_groups = config.get_regular_groups()
202219
         active_groups = config.load_active_groups()
203
-        
220
+
204221
         if args.long:
205222
             # Long format - detailed line-by-line
206223
             _print_detailed_list(persistent_group, regular_groups, active_groups)
207224
         else:
208225
             # Default tabular format
209226
             _print_tabular_list(persistent_group, regular_groups, active_groups)
210
-    
227
+
211228
     except FileNotFoundError:
212229
         print(f"Config file not found: {config_path}")
213
-        print("Use 'shtick add' to create entries or 'shtick generate' with a config file")
230
+        print(
231
+            "Use 'shtick add' to create entries or 'shtick generate' with a config file"
232
+        )
214233
     except Exception as e:
215234
         print(f"Error: {e}")
216235
         sys.exit(1)
217236
 
237
+
218238
 def _print_detailed_list(persistent_group, regular_groups, active_groups) -> None:
219239
     """Print detailed line-by-line list format"""
220240
     # Show persistent group first
@@ -233,33 +253,34 @@ def _print_detailed_list(persistent_group, regular_groups, active_groups) -> Non
233253
             for key, value in persistent_group.functions.items():
234254
                 print(f"    {key} = {value}")
235255
         print()
236
-    
256
+
237257
     # Show regular groups
238258
     for group in regular_groups:
239259
         status = " (ACTIVE)" if group.name in active_groups else " (inactive)"
240260
         print(f"Group: {group.name}{status}")
241
-        
261
+
242262
         if group.aliases:
243263
             print(f"  Aliases ({len(group.aliases)}):")
244264
             for key, value in group.aliases.items():
245265
                 print(f"    {key} = {value}")
246
-        
266
+
247267
         if group.env_vars:
248268
             print(f"  Environment Variables ({len(group.env_vars)}):")
249269
             for key, value in group.env_vars.items():
250270
                 print(f"    {key} = {value}")
251
-        
271
+
252272
         if group.functions:
253273
             print(f"  Functions ({len(group.functions)}):")
254274
             for key, value in group.functions.items():
255275
                 print(f"    {key} = {value}")
256276
         print()
257277
 
278
+
258279
 def _print_tabular_list(persistent_group, regular_groups, active_groups) -> None:
259280
     """Print compact tabular list format"""
260281
     # Collect all items for tabular display
261282
     items = []
262
-    
283
+
263284
     # Add persistent items
264285
     if persistent_group:
265286
         for key, value in persistent_group.aliases.items():
@@ -268,54 +289,58 @@ def _print_tabular_list(persistent_group, regular_groups, active_groups) -> None
268289
             items.append(("persistent", "env", key, value, "PERSISTENT"))
269290
         for key, value in persistent_group.functions.items():
270291
             items.append(("persistent", "function", key, value, "PERSISTENT"))
271
-    
292
+
272293
     # Add regular group items
273294
     for group in regular_groups:
274295
         status = "ACTIVE" if group.name in active_groups else "inactive"
275
-        
296
+
276297
         for key, value in group.aliases.items():
277298
             items.append((group.name, "alias", key, value, status))
278299
         for key, value in group.env_vars.items():
279300
             items.append((group.name, "env", key, value, status))
280301
         for key, value in group.functions.items():
281302
             items.append((group.name, "function", key, value, status))
282
-    
303
+
283304
     if not items:
284305
         print("No items configured")
285306
         return
286
-    
307
+
287308
     # Calculate column widths
288309
     max_group = max(len(item[0]) for item in items)
289310
     max_type = max(len(item[1]) for item in items)
290311
     max_key = max(len(item[2]) for item in items)
291312
     max_value = max(min(len(item[3]), 50) for item in items)  # Limit value column width
292313
     max_status = max(len(item[4]) for item in items)
293
-    
314
+
294315
     # Ensure minimum widths
295316
     max_group = max(max_group, 5)  # "Group"
296
-    max_type = max(max_type, 4)    # "Type"
297
-    max_key = max(max_key, 3)      # "Key"
317
+    max_type = max(max_type, 4)  # "Type"
318
+    max_key = max(max_key, 3)  # "Key"
298319
     max_value = max(max_value, 5)  # "Value"
299
-    max_status = max(max_status, 6) # "Status"
300
-    
320
+    max_status = max(max_status, 6)  # "Status"
321
+
301322
     # Print header
302323
     header = f"{'Group':<{max_group}} {'Type':<{max_type}} {'Key':<{max_key}} {'Value':<{max_value}} {'Status':<{max_status}}"
303324
     print(header)
304325
     print("-" * len(header))
305
-    
326
+
306327
     # Print items
307328
     for group, item_type, key, value, status in items:
308329
         # Truncate long values with ellipsis
309
-        display_value = value if len(value) <= max_value else value[:max_value-3] + "..."
310
-        
311
-        print(f"{group:<{max_group}} {item_type:<{max_type}} {key:<{max_key}} {display_value:<{max_value}} {status:<{max_status}}")
312
-    
330
+        display_value = (
331
+            value if len(value) <= max_value else value[: max_value - 3] + "..."
332
+        )
333
+
334
+        print(
335
+            f"{group:<{max_group}} {item_type:<{max_type}} {key:<{max_key}} {display_value:<{max_value}} {status:<{max_status}}"
336
+        )
337
+
313338
     # Print summary
314339
     print()
315340
     total_items = len(items)
316341
     active_items = len([item for item in items if item[4] in ["ACTIVE", "PERSISTENT"]])
317342
     print(f"Total: {total_items} items ({active_items} active)")
318
-    
343
+
319344
     # Show available commands
320345
     print()
321346
     print("Use 'shtick list -l' for detailed view")
@@ -324,10 +349,11 @@ def _print_tabular_list(persistent_group, regular_groups, active_groups) -> None
324349
         print(f"Activate groups with: shtick activate <group>")
325350
         print(f"Inactive groups: {', '.join(sorted(inactive_groups))}")
326351
 
352
+
327353
 def cmd_shells(args) -> None:
328354
     """List supported shells"""
329355
     shells = sorted(get_supported_shells())
330
-    
356
+
331357
     if args.long:
332358
         # Long format - one per line with descriptions
333359
         print("Supported shells:")
@@ -337,34 +363,36 @@ def cmd_shells(args) -> None:
337363
         # Default columnar format (like ls)
338364
         _print_shells_columns(shells)
339365
 
366
+
340367
 def _print_shells_columns(shells) -> None:
341368
     """Print shells in columns like ls output"""
342369
     if not shells:
343370
         print("No shells configured")
344371
         return
345
-    
372
+
346373
     # Try to get terminal width, fallback to 80
347374
     try:
348375
         import shutil
376
+
349377
         terminal_width = shutil.get_terminal_size().columns
350378
     except:
351379
         terminal_width = 80
352
-    
380
+
353381
     # Find the longest shell name
354382
     max_shell_length = max(len(shell) for shell in shells)
355
-    
383
+
356384
     # Add some padding
357385
     column_width = max_shell_length + 2
358
-    
386
+
359387
     # Calculate how many columns we can fit
360388
     columns = max(1, terminal_width // column_width)
361
-    
389
+
362390
     # Calculate number of rows needed
363391
     rows = (len(shells) + columns - 1) // columns
364
-    
392
+
365393
     print(f"Supported shells ({len(shells)} total):")
366394
     print()
367
-    
395
+
368396
     # Print shells in columns
369397
     for row in range(rows):
370398
         line = ""
@@ -375,52 +403,59 @@ def _print_shells_columns(shells) -> None:
375403
                 line += f"{shell:<{column_width}}"
376404
         print(line.rstrip())
377405
 
406
+
378407
 def cmd_status(args) -> None:
379408
     """Show status of groups and active state"""
380409
     config_path = Config.get_default_config_path()
381
-    
410
+
382411
     try:
383
-        config = Config(config_path)
412
+        config = Config(config_path, debug=getattr(args, "debug", False))
384413
         config.load()
385
-        
414
+
386415
         persistent_group = config.get_persistent_group()
387416
         regular_groups = config.get_regular_groups()
388417
         active_groups = config.load_active_groups()
389
-        
418
+
390419
         print("Shtick Status")
391420
         print("=" * 40)
392
-        
421
+
393422
         # Show persistent group
394423
         if persistent_group:
395
-            total_persistent = len(persistent_group.aliases) + len(persistent_group.env_vars) + len(persistent_group.functions)
424
+            total_persistent = (
425
+                len(persistent_group.aliases)
426
+                + len(persistent_group.env_vars)
427
+                + len(persistent_group.functions)
428
+            )
396429
             print(f"Persistent (always active): {total_persistent} items")
397430
         else:
398431
             print("Persistent: No items")
399
-        
432
+
400433
         print()
401
-        
434
+
402435
         # Show regular groups
403436
         if regular_groups:
404437
             print("Available Groups:")
405438
             for group in regular_groups:
406439
                 status = "ACTIVE" if group.name in active_groups else "inactive"
407
-                total_items = len(group.aliases) + len(group.env_vars) + len(group.functions)
440
+                total_items = (
441
+                    len(group.aliases) + len(group.env_vars) + len(group.functions)
442
+                )
408443
                 print(f"  {group.name}: {total_items} items ({status})")
409444
         else:
410445
             print("No regular groups configured")
411
-        
446
+
412447
         print()
413
-        
448
+
414449
         # Show summary
415450
         if active_groups:
416451
             print(f"Currently active: {', '.join(active_groups)}")
417452
         else:
418453
             print("No groups currently active")
419
-        
454
+
420455
         print()
421456
         print("To activate a group: shtick activate <group>")
422457
         print("To deactivate a group: shtick deactivate <group>")
423
-    
458
+
424459
     except FileNotFoundError:
425460
         print(f"Config file not found: {config_path}")
426461
         print("No configuration exists yet")
@@ -428,75 +463,98 @@ def cmd_status(args) -> None:
428463
         print(f"Error: {e}")
429464
         sys.exit(1)
430465
 
466
+
431467
 def main():
432468
     """Main CLI entry point"""
433469
     parser = argparse.ArgumentParser(
434470
         description="shtick - Generate shell configuration files from TOML"
435471
     )
436
-    
437
-    subparsers = parser.add_subparsers(dest='command', help='Available commands')
438
-    
472
+
473
+    # Global flags
474
+    parser.add_argument("--debug", action="store_true", help="Enable debug output")
475
+
476
+    subparsers = parser.add_subparsers(dest="command", help="Available commands")
477
+
439478
     # Generate command
440
-    gen_parser = subparsers.add_parser('generate', help='Generate shell files from config')
441
-    gen_parser.add_argument('config', nargs='?', help='Path to config TOML file')
442
-    
443
-    # Add command  
444
-    add_parser = subparsers.add_parser('add', help='Add an item to config')
445
-    add_parser.add_argument('type', choices=['alias', 'env', 'function'], 
446
-                           help='Type of item to add')
447
-    add_parser.add_argument('group', help='Group name')
448
-    add_parser.add_argument('assignment', help='Assignment in format key=value')
449
-    
479
+    gen_parser = subparsers.add_parser(
480
+        "generate", help="Generate shell files from config"
481
+    )
482
+    gen_parser.add_argument("config", nargs="?", help="Path to config TOML file")
483
+    gen_parser.add_argument(
484
+        "-i",
485
+        "--interactive",
486
+        action="store_true",
487
+        help="Interactive shell selection for sourcing instructions",
488
+    )
489
+
490
+    # Add command
491
+    add_parser = subparsers.add_parser("add", help="Add an item to config")
492
+    add_parser.add_argument(
493
+        "type", choices=["alias", "env", "function"], help="Type of item to add"
494
+    )
495
+    add_parser.add_argument("group", help="Group name")
496
+    add_parser.add_argument("assignment", help="Assignment in format key=value")
497
+
450498
     # Remove command
451
-    rm_parser = subparsers.add_parser('remove', help='Remove an item from config')
452
-    rm_parser.add_argument('type', choices=['alias', 'env', 'function'],
453
-                          help='Type of item to remove')
454
-    rm_parser.add_argument('group', help='Group name')
455
-    rm_parser.add_argument('search', help='Search term (fuzzy match)')
456
-    
499
+    rm_parser = subparsers.add_parser("remove", help="Remove an item from config")
500
+    rm_parser.add_argument(
501
+        "type", choices=["alias", "env", "function"], help="Type of item to remove"
502
+    )
503
+    rm_parser.add_argument("group", help="Group name")
504
+    rm_parser.add_argument("search", help="Search term (fuzzy match)")
505
+
457506
     # Activate command
458
-    activate_parser = subparsers.add_parser('activate', help='Activate a group')
459
-    activate_parser.add_argument('group', help='Group name to activate')
460
-    
461
-    # Deactivate command  
462
-    deactivate_parser = subparsers.add_parser('deactivate', help='Deactivate a group')
463
-    deactivate_parser.add_argument('group', help='Group name to deactivate')
464
-    
507
+    activate_parser = subparsers.add_parser("activate", help="Activate a group")
508
+    activate_parser.add_argument("group", help="Group name to activate")
509
+
510
+    # Deactivate command
511
+    deactivate_parser = subparsers.add_parser("deactivate", help="Deactivate a group")
512
+    deactivate_parser.add_argument("group", help="Group name to deactivate")
513
+
465514
     # Status command
466
-    status_parser = subparsers.add_parser('status', help='Show status of groups')
467
-    
515
+    status_parser = subparsers.add_parser("status", help="Show status of groups")
516
+
468517
     # List command
469
-    list_parser = subparsers.add_parser('list', help='List current configuration')
470
-    list_parser.add_argument('-l', '--long', action='store_true', 
471
-                            help='Show detailed line-by-line format instead of table')
472
-    
518
+    list_parser = subparsers.add_parser("list", help="List current configuration")
519
+    list_parser.add_argument(
520
+        "-l",
521
+        "--long",
522
+        action="store_true",
523
+        help="Show detailed line-by-line format instead of table",
524
+    )
525
+
473526
     # Shells command
474
-    shells_parser = subparsers.add_parser('shells', help='List supported shells')
475
-    shells_parser.add_argument('-l', '--long', action='store_true',
476
-                              help='Show one shell per line instead of columns')
477
-    
527
+    shells_parser = subparsers.add_parser("shells", help="List supported shells")
528
+    shells_parser.add_argument(
529
+        "-l",
530
+        "--long",
531
+        action="store_true",
532
+        help="Show one shell per line instead of columns",
533
+    )
534
+
478535
     args = parser.parse_args()
479
-    
536
+
480537
     if not args.command:
481538
         parser.print_help()
482539
         sys.exit(1)
483
-    
484
-    if args.command == 'generate':
540
+
541
+    if args.command == "generate":
485542
         cmd_generate(args)
486
-    elif args.command == 'add':
543
+    elif args.command == "add":
487544
         cmd_add(args)
488
-    elif args.command == 'remove':
545
+    elif args.command == "remove":
489546
         cmd_remove(args)
490
-    elif args.command == 'activate':
547
+    elif args.command == "activate":
491548
         cmd_activate(args)
492
-    elif args.command == 'deactivate':
549
+    elif args.command == "deactivate":
493550
         cmd_deactivate(args)
494
-    elif args.command == 'status':
551
+    elif args.command == "status":
495552
         cmd_status(args)
496
-    elif args.command == 'list':
553
+    elif args.command == "list":
497554
         cmd_list(args)
498
-    elif args.command == 'shells':
555
+    elif args.command == "shells":
499556
         cmd_shells(args)
500557
 
501
-if __name__ == '__main__':
502
-    main()
558
+
559
+if __name__ == "__main__":
560
+    main()
src/shtick/config.pymodified
@@ -22,8 +22,9 @@ class GroupData:
2222
 class Config:
2323
     """Main configuration handler"""
2424
 
25
-    def __init__(self, config_path: Optional[str] = None):
25
+    def __init__(self, config_path: Optional[str] = None, debug: bool = False):
2626
         self.config_path = config_path or self.get_default_config_path()
27
+        self.debug = debug
2728
         self.groups: List[GroupData] = []
2829
 
2930
     @staticmethod
@@ -103,22 +104,28 @@ class Config:
103104
         if not os.path.exists(self.config_path):
104105
             raise FileNotFoundError(f"Config file not found: {self.config_path}")
105106
 
106
-        print(f"Debug: Loading config from: {self.config_path}")
107
+        if self.debug:
108
+            print(f"Debug: Loading config from: {self.config_path}")
107109
 
108110
         with open(self.config_path, "rb") as f:
109111
             data = tomllib.load(f)
110112
 
111
-        print(f"Debug: Raw TOML data keys: {list(data.keys())}")
112
-        print(f"Debug: Raw TOML data structure: {data}")
113
+        if self.debug:
114
+            print(f"Debug: Raw TOML data keys: {list(data.keys())}")
115
+            print(f"Debug: Raw TOML data structure: {data}")
113116
 
114117
         self.groups = []
115118
 
116119
         # Parse groups from nested TOML structure
117120
         for group_name, group_config in data.items():
118
-            print(f"Debug: Processing group '{group_name}' with config: {group_config}")
121
+            if self.debug:
122
+                print(
123
+                    f"Debug: Processing group '{group_name}' with config: {group_config}"
124
+                )
119125
 
120126
             if not isinstance(group_config, dict):
121
-                print(f"Debug: Skipping non-dict value for key '{group_name}'")
127
+                if self.debug:
128
+                    print(f"Debug: Skipping non-dict value for key '{group_name}'")
122129
                 continue
123130
 
124131
             # Initialize group data
@@ -126,27 +133,34 @@ class Config:
126133
 
127134
             # Extract each section from the group
128135
             for section_name, section_data in group_config.items():
129
-                print(
130
-                    f"Debug: Processing section '{section_name}' in group '{group_name}': {section_data}"
131
-                )
136
+                if self.debug:
137
+                    print(
138
+                        f"Debug: Processing section '{section_name}' in group '{group_name}': {section_data}"
139
+                    )
132140
 
133141
                 if section_name == "aliases" and isinstance(section_data, dict):
134142
                     group_data["aliases"] = section_data
135
-                    print(f"Debug: Added {len(section_data)} aliases to '{group_name}'")
143
+                    if self.debug:
144
+                        print(
145
+                            f"Debug: Added {len(section_data)} aliases to '{group_name}'"
146
+                        )
136147
                 elif section_name == "env_vars" and isinstance(section_data, dict):
137148
                     group_data["env_vars"] = section_data
138
-                    print(
139
-                        f"Debug: Added {len(section_data)} env_vars to '{group_name}'"
140
-                    )
149
+                    if self.debug:
150
+                        print(
151
+                            f"Debug: Added {len(section_data)} env_vars to '{group_name}'"
152
+                        )
141153
                 elif section_name == "functions" and isinstance(section_data, dict):
142154
                     group_data["functions"] = section_data
143
-                    print(
144
-                        f"Debug: Added {len(section_data)} functions to '{group_name}'"
145
-                    )
155
+                    if self.debug:
156
+                        print(
157
+                            f"Debug: Added {len(section_data)} functions to '{group_name}'"
158
+                        )
146159
                 else:
147
-                    print(
148
-                        f"Debug: Unknown or invalid section '{section_name}' in group '{group_name}'"
149
-                    )
160
+                    if self.debug:
161
+                        print(
162
+                            f"Debug: Unknown or invalid section '{section_name}' in group '{group_name}'"
163
+                        )
150164
 
151165
             # Create GroupData object if group has any items
152166
             total_items = (
@@ -154,7 +168,8 @@ class Config:
154168
                 + len(group_data["env_vars"])
155169
                 + len(group_data["functions"])
156170
             )
157
-            print(f"Debug: Group '{group_name}' has {total_items} total items")
171
+            if self.debug:
172
+                print(f"Debug: Group '{group_name}' has {total_items} total items")
158173
 
159174
             if total_items > 0:
160175
                 new_group = GroupData(
@@ -164,15 +179,18 @@ class Config:
164179
                     functions=group_data["functions"],
165180
                 )
166181
                 self.groups.append(new_group)
167
-                print(
168
-                    f"Debug: Created group '{group_name}' with {len(group_data['aliases'])} aliases, {len(group_data['env_vars'])} env_vars, {len(group_data['functions'])} functions"
169
-                )
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
+                    )
170186
             else:
171
-                print(f"Warning: Group '{group_name}' has no items, skipping")
187
+                if self.debug:
188
+                    print(f"Warning: Group '{group_name}' has no items, skipping")
172189
 
173
-        print(
174
-            f"Debug: Final groups loaded: {[g.name for g in self.groups]} (total: {len(self.groups)})"
175
-        )
190
+        if self.debug:
191
+            print(
192
+                f"Debug: Final groups loaded: {[g.name for g in self.groups]} (total: {len(self.groups)})"
193
+            )
176194
 
177195
     def save(self) -> None:
178196
         """Save the current configuration back to TOML file"""
src/shtick/generator.pymodified
@@ -72,7 +72,7 @@ class Generator:
7272
 
7373
                     f.write(line)
7474
 
75
-    def generate_all(self, config: Config) -> None:
75
+    def generate_all(self, config: Config, interactive: bool = False) -> None:
7676
         """Generate shell files for all groups in config"""
7777
         if not config.groups:
7878
             print("No groups found in configuration")
@@ -87,7 +87,7 @@ class Generator:
8787
         self.generate_loader(config)
8888
 
8989
         print(f"All done! Files generated in {self.output_base_dir}")
90
-        self._print_usage_instructions(config)
90
+        self._print_usage_instructions(config, interactive)
9191
 
9292
     def generate_loader(self, config: Config) -> None:
9393
         """Generate dynamic loader files that source persistent + active groups"""
@@ -112,9 +112,15 @@ class Generator:
112112
                 if persistent_group:
113113
                     f.write("# Load persistent configuration\n")
114114
                     for item_type in ["alias", "env", "function"]:
115
-                        file_path = f"$HOME/.config/shtick/persistent/{item_type}"
116
-                        f.write(f'[ -f "{file_path}/{item_type}s.{shell_name}" ] && ')
117
-                        f.write(f'source "{file_path}/{item_type}s.{shell_name}"\n')
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')
118124
                     f.write("\n")
119125
 
120126
                 # Source active groups
@@ -123,25 +129,30 @@ class Generator:
123129
                     for group_name in active_groups:
124130
                         f.write(f"# Group: {group_name}\n")
125131
                         for item_type in ["alias", "env", "function"]:
126
-                            file_path = f"$HOME/.config/shtick/{group_name}/{item_type}"
127
-                            f.write(
128
-                                f'[ -f "{file_path}/{item_type}s.{shell_name}" ] && '
129
-                            )
130
-                            f.write(f'source "{file_path}/{item_type}s.{shell_name}"\n')
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}"
140
+                            f.write(f'[ -f "{file_path}" ] && source "{file_path}"\n')
131141
                         f.write("\n")
132142
                 else:
133143
                     f.write("# No active groups\n")
134144
 
135
-    def _print_usage_instructions(self, config: Config) -> None:
145
+    def _print_usage_instructions(
146
+        self, config: Config, interactive: bool = False
147
+    ) -> None:
136148
         """Print usage instructions for the user"""
137149
         active_groups = config.load_active_groups()
138150
         persistent_group = config.get_persistent_group()
139151
 
140
-        print("\nTo use these configurations, add this line to your shell config:")
141
-        print("  # For bash/zsh (~/.bashrc or ~/.zshrc):")
142
-        print("  source ~/.config/shtick/load_active.bash")
143
-        print("\n  # For fish (~/.config/fish/config.fish):")
144
-        print("  source ~/.config/shtick/load_active.fish")
152
+        if interactive:
153
+            self._interactive_shell_setup()
154
+        else:
155
+            self._print_default_instructions()
145156
 
146157
         if persistent_group:
147158
             total_persistent = (
@@ -162,6 +173,136 @@ class Generator:
162173
         if available_groups:
163174
             print(f"Available groups: {', '.join(available_groups)}")
164175
 
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, showing default instructions.")
237
+                self._print_default_instructions()
238
+                return
239
+
240
+            selected_shells = self._parse_shell_selection(user_input, shells)
241
+
242
+            if not selected_shells:
243
+                print("No valid shells selected, showing default instructions.")
244
+                self._print_default_instructions()
245
+                return
246
+
247
+            print("\nAdd these lines to your shell configuration files:")
248
+            print("=" * 60)
249
+
250
+            for shell in selected_shells:
251
+                config_file, source_line = shell_configs.get(
252
+                    shell,
253
+                    ("~/.profile", f"source ~/.config/shtick/load_active.{shell}"),
254
+                )
255
+
256
+                print(f"\n# {shell.upper()}")
257
+                print(f"# File: {config_file}")
258
+                print(f"{source_line}")
259
+
260
+            print("\n" + "=" * 60)
261
+
262
+        except (KeyboardInterrupt, EOFError):
263
+            print("\nCancelled. Showing default instructions.")
264
+            self._print_default_instructions()
265
+
266
+    def _parse_shell_selection(
267
+        self, user_input: str, available_shells: List[str]
268
+    ) -> List[str]:
269
+        """Parse user input for shell selection (numbers or names)"""
270
+        selected = set()
271
+        tokens = user_input.lower().split()
272
+
273
+        for token in tokens:
274
+            # Check if it's "all"
275
+            if token == "all":
276
+                return available_shells
277
+
278
+            # Check if it's a number
279
+            try:
280
+                num = int(token)
281
+                if 1 <= num <= len(available_shells):
282
+                    selected.add(available_shells[num - 1])
283
+                elif num == len(available_shells) + 1:  # "all" option
284
+                    return available_shells
285
+                else:
286
+                    print(f"Warning: Invalid number {num}, ignoring")
287
+                continue
288
+            except ValueError:
289
+                pass
290
+
291
+            # Check if it's a shell name (fuzzy match)
292
+            matches = [shell for shell in available_shells if token in shell.lower()]
293
+            if len(matches) == 1:
294
+                selected.add(matches[0])
295
+            elif len(matches) > 1:
296
+                print(
297
+                    f"Warning: '{token}' matches multiple shells: {', '.join(matches)}"
298
+                )
299
+                print(f"Using exact match or first match: {matches[0]}")
300
+                selected.add(matches[0])
301
+            else:
302
+                print(f"Warning: No shell matches '{token}', ignoring")
303
+
304
+        return sorted(list(selected))
305
+
165306
     def get_shell_files_for_group(self, group_name: str) -> Dict[str, List[str]]:
166307
         """Get list of generated shell files for a group"""
167308
         files = {"alias": [], "env": [], "function": []}