dotman-git 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. dot_man/__init__.py +4 -0
  2. dot_man/backups.py +211 -0
  3. dot_man/branch_ops.py +347 -0
  4. dot_man/cli/__init__.py +113 -0
  5. dot_man/cli/add_cmd.py +167 -0
  6. dot_man/cli/audit_cmd.py +141 -0
  7. dot_man/cli/backup_cmd.py +105 -0
  8. dot_man/cli/branch_cmd.py +103 -0
  9. dot_man/cli/clean_cmd.py +97 -0
  10. dot_man/cli/common.py +548 -0
  11. dot_man/cli/completions_cmd.py +127 -0
  12. dot_man/cli/config_cmd.py +979 -0
  13. dot_man/cli/deploy_cmd.py +169 -0
  14. dot_man/cli/discover_cmd.py +105 -0
  15. dot_man/cli/doctor_cmd.py +229 -0
  16. dot_man/cli/edit_cmd.py +177 -0
  17. dot_man/cli/encrypt_cmd.py +205 -0
  18. dot_man/cli/export_cmd.py +146 -0
  19. dot_man/cli/import_cmd.py +315 -0
  20. dot_man/cli/init_cmd.py +532 -0
  21. dot_man/cli/interface.py +56 -0
  22. dot_man/cli/log_cmd.py +339 -0
  23. dot_man/cli/main.py +36 -0
  24. dot_man/cli/navigate_cmd.py +903 -0
  25. dot_man/cli/onboarding.py +546 -0
  26. dot_man/cli/profile_cmd.py +313 -0
  27. dot_man/cli/remote_cmd.py +454 -0
  28. dot_man/cli/restore_cmd.py +82 -0
  29. dot_man/cli/revert_cmd.py +86 -0
  30. dot_man/cli/show_cmd.py +29 -0
  31. dot_man/cli/status_cmd.py +185 -0
  32. dot_man/cli/switch_cmd.py +387 -0
  33. dot_man/cli/tag_cmd.py +164 -0
  34. dot_man/cli/template_cmd.py +244 -0
  35. dot_man/cli/tui_cmd.py +44 -0
  36. dot_man/cli/verify_cmd.py +156 -0
  37. dot_man/completions/_dot-man.zsh +28 -0
  38. dot_man/completions/dot-man.bash +15 -0
  39. dot_man/completions/dot-man.fish +58 -0
  40. dot_man/completions/install.sh +26 -0
  41. dot_man/config.py +23 -0
  42. dot_man/config_detector.py +426 -0
  43. dot_man/constants.py +109 -0
  44. dot_man/core.py +614 -0
  45. dot_man/dotman_config.py +516 -0
  46. dot_man/encryption.py +173 -0
  47. dot_man/exceptions.py +255 -0
  48. dot_man/files.py +443 -0
  49. dot_man/global_config.py +305 -0
  50. dot_man/hooks.py +232 -0
  51. dot_man/interactive.py +460 -0
  52. dot_man/lock.py +64 -0
  53. dot_man/merge.py +440 -0
  54. dot_man/operations.py +212 -0
  55. dot_man/py.typed +1 -0
  56. dot_man/save_deploy_ops.py +466 -0
  57. dot_man/secrets.py +473 -0
  58. dot_man/section.py +207 -0
  59. dot_man/status_ops.py +229 -0
  60. dot_man/tui_log.py +91 -0
  61. dot_man/ui.py +127 -0
  62. dot_man/utils.py +132 -0
  63. dot_man/vault.py +317 -0
  64. dotman_git-1.0.0.dist-info/METADATA +678 -0
  65. dotman_git-1.0.0.dist-info/RECORD +69 -0
  66. dotman_git-1.0.0.dist-info/WHEEL +5 -0
  67. dotman_git-1.0.0.dist-info/entry_points.txt +3 -0
  68. dotman_git-1.0.0.dist-info/licenses/LICENSE +21 -0
  69. dotman_git-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,979 @@
1
+ """Config command for dot-man CLI."""
2
+
3
+ import json
4
+
5
+ import click
6
+ from rich.table import Table
7
+
8
+ from .. import ui
9
+ from ..config import GlobalConfig
10
+ from ..exceptions import ConfigurationError
11
+ from .common import complete_config_keys, error, require_init, success
12
+ from .interface import cli as main
13
+
14
+
15
+ @main.group()
16
+ def config():
17
+ """Manage global configuration."""
18
+ pass
19
+
20
+
21
+ @config.command("defaults")
22
+ def config_defaults():
23
+ """Show all configurable defaults with descriptions.
24
+
25
+ This displays all the settings that users can customize,
26
+ along with their current default values.
27
+
28
+ Example: dot-man config defaults
29
+ """
30
+ from rich.table import Table
31
+
32
+ table = Table(title="Configurable Defaults", show_header=True)
33
+ table.add_column("Setting", style="cyan", no_wrap=True)
34
+ table.add_column("Default Value", style="green")
35
+ table.add_column("Description")
36
+
37
+ defaults = [
38
+ # Switch/Navigate settings
39
+ (
40
+ "switch.default_behavior",
41
+ "save",
42
+ "What to do with unsaved changes when switching (save/no-save)",
43
+ ),
44
+ (
45
+ "remote.auto_sync",
46
+ "false",
47
+ "Auto push/pull when switching branches (true/false)",
48
+ ),
49
+ ("remote.url", "", "Remote repository URL for sync"),
50
+ # Default section settings
51
+ ("defaults.secrets_filter", "true", "Redact secrets when saving (true/false)"),
52
+ (
53
+ "defaults.update_strategy",
54
+ "replace",
55
+ "How to deploy: replace/rename_old/ignore",
56
+ ),
57
+ (
58
+ "defaults.follow_symlinks",
59
+ "false",
60
+ "Follow symlinks when deploying (true/false)",
61
+ ),
62
+ # Security
63
+ (
64
+ "security.strict_mode",
65
+ "false",
66
+ "Exit with error if secrets detected (true/false)",
67
+ ),
68
+ ("security.audit_on_commit", "true", "Run audit before commits (true/false)"),
69
+ # Other
70
+ ("backup.max_count", "5", "Maximum number of backups to keep"),
71
+ ]
72
+
73
+ for key, default_val, desc in defaults:
74
+ table.add_row(key, str(default_val), desc)
75
+
76
+ ui.console.print(table)
77
+ ui.console.print()
78
+ ui.console.print("[bold]Section-level settings (in dot-man.toml):[/bold]")
79
+ ui.console.print(" [cyan]paths[/cyan] - List of files/dirs to track")
80
+ ui.console.print(" [cyan]secrets_filter[/cyan] - Enable secret detection")
81
+ ui.console.print(" [cyan]update_strategy[/cyan] - How to handle existing files")
82
+ ui.console.print(" [cyan]pre_deploy[/cyan] - Command to run before deploying")
83
+ ui.console.print(" [cyan]post_deploy[/cyan] - Command to run after deploying")
84
+ ui.console.print()
85
+ ui.console.print("[bold]To change a setting:[/bold]")
86
+ ui.console.print(" [cyan]dot-man config set <key> <value>[/cyan]")
87
+ ui.console.print()
88
+ ui.console.print("[dim]Examples:[/dim]")
89
+ ui.console.print(" dot-man config set switch.default_behavior no-save")
90
+ ui.console.print(" dot-man config set remote.auto_sync true")
91
+ ui.console.print(" dot-man config set defaults.update_strategy rename_old")
92
+ ui.console.print()
93
+
94
+
95
+ @config.command("list")
96
+ def config_list():
97
+ """List all global configuration values."""
98
+ try:
99
+ cfg = GlobalConfig()
100
+ cfg.load()
101
+
102
+ def flatten(d, parent_key="", sep="."):
103
+ items = []
104
+ for k, v in d.items():
105
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
106
+ if isinstance(v, dict):
107
+ items.extend(flatten(v, new_key, sep=sep).items())
108
+ else:
109
+ items.append((new_key, v))
110
+ return dict(items)
111
+
112
+ flat_data = flatten(cfg._data)
113
+
114
+ table = Table(title="Global Configuration")
115
+ table.add_column("Key", style="cyan")
116
+ table.add_column("Value")
117
+
118
+ for k, v in sorted(flat_data.items()):
119
+ table.add_row(k, str(v))
120
+
121
+ ui.console.print(table)
122
+
123
+ except Exception as e:
124
+ error(f"Failed to list config: {e}")
125
+
126
+
127
+ @config.command("get")
128
+ @click.argument("key", shell_complete=complete_config_keys)
129
+ def config_get(key: str):
130
+ """Get a configuration value.
131
+
132
+ Example: dot-man config get dot-man.editor
133
+ """
134
+ try:
135
+ cfg = GlobalConfig()
136
+ cfg.load()
137
+
138
+ parts = key.split(".")
139
+ current = cfg._data
140
+
141
+ for part in parts:
142
+ if isinstance(current, dict) and part in current:
143
+ current = current[part]
144
+ else:
145
+ ui.console.print(f"[red]Key not found:[/red] {key}")
146
+ ui.hint("Run 'dot-man config list' to see all available keys")
147
+ raise SystemExit(1)
148
+
149
+ if isinstance(current, dict):
150
+ ui.console.print(f"[dim]Section '{key}' contains:[/dim]")
151
+ ui.console.print(json.dumps(current, indent=2))
152
+ else:
153
+ ui.console.print(str(current))
154
+
155
+ except Exception as e:
156
+ error(f"Failed to get config: {e}")
157
+
158
+
159
+ @config.command("set")
160
+ @click.argument("key", shell_complete=complete_config_keys)
161
+ @click.argument("value")
162
+ def config_set(key: str, value: str):
163
+ """Set a configuration value.
164
+
165
+ Example: dot-man config set dot-man.editor nvim
166
+ """
167
+ try:
168
+ cfg = GlobalConfig()
169
+ try:
170
+ cfg.load()
171
+ except (FileNotFoundError, ConfigurationError):
172
+ cfg.create_default()
173
+
174
+ val: bool | str
175
+ if value.lower() == "true":
176
+ val = True
177
+ elif value.lower() == "false":
178
+ val = False
179
+ else:
180
+ val = value
181
+
182
+ parts = key.split(".")
183
+ current = cfg._data
184
+
185
+ for i, part in enumerate(parts[:-1]):
186
+ if part not in current:
187
+ current[part] = {}
188
+ current = current[part]
189
+ if not isinstance(current, dict):
190
+ error(
191
+ f"Key path conflict: '{'.'.join(parts[: i + 1])}' is not a section"
192
+ )
193
+
194
+ current[parts[-1]] = val
195
+ cfg.save()
196
+
197
+ success(f"Set '{key}' to '{val}'")
198
+ ui.console.print(f"[dim] Verified: {key} = {val}[/dim]")
199
+
200
+ except Exception as e:
201
+ error(f"Failed to set config: {e}")
202
+
203
+
204
+ @config.command("create")
205
+ @click.option(
206
+ "--examples",
207
+ "with_examples",
208
+ is_flag=True,
209
+ default=True,
210
+ help="Include commented examples in the config file (default: True)",
211
+ )
212
+ @click.option(
213
+ "--minimal",
214
+ is_flag=True,
215
+ help="Create a minimal config file without examples or comments",
216
+ )
217
+ @click.option(
218
+ "--force", is_flag=True, help="Overwrite existing config file without prompting"
219
+ )
220
+ @require_init
221
+ def config_create(with_examples: bool, minimal: bool, force: bool):
222
+ """Create or regenerate the dot-man.toml configuration file.
223
+
224
+ This command creates a new dot-man.toml file for the current branch.
225
+ By default, it includes commented examples to help you get started.
226
+
227
+ Examples:
228
+ # Create config with examples (default)
229
+ dot-man config create
230
+
231
+ # Create minimal config without examples
232
+ dot-man config create --minimal
233
+
234
+ # Create config with examples, overwrite existing
235
+ dot-man config create --examples --force
236
+
237
+ # Create minimal config, overwrite existing
238
+ dot-man config create --minimal --force
239
+ """
240
+ try:
241
+ from ..config import DotManConfig
242
+ from ..constants import DOT_MAN_TOML, REPO_DIR
243
+
244
+ config_path = REPO_DIR / DOT_MAN_TOML
245
+
246
+ # Check if file exists
247
+ if config_path.exists() and not force:
248
+ if not ui.confirm(
249
+ f"Config file already exists at {config_path}. Overwrite?"
250
+ ):
251
+ ui.console.print("Cancelled.")
252
+ return
253
+
254
+ # Create the config
255
+ dotman_config = DotManConfig()
256
+
257
+ if minimal:
258
+ # Create minimal config without examples
259
+ dotman_config._data = {}
260
+ dotman_config.save()
261
+ ui.console.print(f"Created minimal config at {config_path}")
262
+ else:
263
+ # Create config with examples (default behavior)
264
+ dotman_config.create_default()
265
+ ui.console.print(f"Created config with examples at {config_path}")
266
+
267
+ ui.console.print("Tip: Use 'dot-man edit' to open the config in your editor")
268
+
269
+ except Exception as e:
270
+ error(f"Failed to create config: {e}")
271
+
272
+
273
+ @config.command("tutorial")
274
+ @click.option("--section", help="Show examples for a specific section type")
275
+ @click.option("--interactive", "-i", is_flag=True, help="Interactive tutorial mode")
276
+ def config_tutorial(section: str | None, interactive: bool):
277
+ """Interactive configuration tutorial with examples.
278
+
279
+ Learn how to configure dot-man with practical examples and explanations.
280
+ Similar to vimtutor, this guides you through configuration options.
281
+
282
+ Examples:
283
+ # Show all examples
284
+ dot-man config tutorial
285
+
286
+ # Show examples for a specific type
287
+ dot-man config tutorial --section basic
288
+ dot-man config tutorial --section advanced
289
+ dot-man config tutorial --section hooks
290
+
291
+ # Interactive mode (step by step)
292
+ dot-man config tutorial --interactive
293
+ """
294
+ from rich.panel import Panel
295
+ from rich.prompt import Prompt
296
+
297
+ if interactive:
298
+ _run_interactive_tutorial()
299
+ return
300
+
301
+ if section:
302
+ _show_section_examples(section)
303
+ return
304
+
305
+ # Show interactive overview with all sections
306
+ ui.console.print()
307
+ ui.console.print(
308
+ Panel.fit(
309
+ "[bold blue]dot-man Configuration Tutorial[/bold blue]\n\n"
310
+ "This tutorial shows you how to configure dot-man to track your dotfiles.\n"
311
+ "Choose from the options below or use --interactive for guided learning.",
312
+ title="🎯 Tutorial Overview",
313
+ )
314
+ )
315
+
316
+ ui.console.print("\n[bold]What would you like to learn about?[/bold]")
317
+ ui.console.print()
318
+
319
+ # Interactive menu options
320
+ menu_options = [
321
+ ("1", "Basic file tracking", "paths, sections, simple examples"),
322
+ ("2", "Directory tracking", "include/exclude patterns, wildcards"),
323
+ ("3", "Update strategies", "replace, rename_old, ignore strategies"),
324
+ ("4", "Hooks & automation", "pre/post deploy commands, aliases"),
325
+ ("5", "Templates & inheritance", "reusable configs, organization"),
326
+ ("6", "Advanced features", "custom paths, overrides, limits"),
327
+ ("7", "Security & secrets", "automatic filtering, best practices"),
328
+ ("8", "Branch activation", "on_activate, on_deactivate hooks"),
329
+ ("9", "Quick presets", "pre-configured for popular dotfiles"),
330
+ ("I", "Interactive tutorial", "step-by-step guided learning"),
331
+ ("C", "Create config", "generate config file with examples"),
332
+ ("Q", "Quit", "exit tutorial"),
333
+ ]
334
+
335
+ for key, title, desc in menu_options:
336
+ if key in ["I", "C", "Q"]:
337
+ ui.console.print(
338
+ f" [yellow]{key}[/yellow] - [bold]{title}[/bold] - {desc}"
339
+ )
340
+ else:
341
+ ui.console.print(f" [cyan]{key}[/cyan] - [bold]{title}[/bold] - {desc}")
342
+
343
+ ui.console.print()
344
+
345
+ # Get user choice
346
+ choice = Prompt.ask(
347
+ "Enter your choice",
348
+ choices=["1", "2", "3", "4", "5", "6", "7", "8", "9", "I", "C", "Q"],
349
+ default="I",
350
+ ).upper()
351
+
352
+ # Handle choice
353
+ if choice == "1":
354
+ _show_section_examples("basic")
355
+ elif choice == "2":
356
+ _show_section_examples("directories")
357
+ elif choice == "3":
358
+ _show_section_examples("hooks") # Update strategies are in hooks section
359
+ elif choice == "4":
360
+ _show_section_examples("hooks")
361
+ elif choice == "5":
362
+ _show_section_examples("templates")
363
+ elif choice == "6":
364
+ _show_section_examples("advanced")
365
+ elif choice == "7":
366
+ _show_section_examples("secrets")
367
+ elif choice == "8":
368
+ _show_section_examples("activate")
369
+ elif choice == "9":
370
+ _show_section_examples("presets")
371
+ elif choice == "I":
372
+ _run_interactive_tutorial()
373
+ elif choice == "C":
374
+ ui.console.print(
375
+ "\n[dim]Tip: Run 'dot-man config create' to generate a config file with examples[/dim]"
376
+ )
377
+ elif choice == "Q":
378
+ ui.console.print(
379
+ "\n[dim]Goodbye! Run 'dot-man config tutorial' anytime to return.[/dim]"
380
+ )
381
+
382
+ return
383
+
384
+
385
+ def _show_section_examples(section: str):
386
+ """Show examples for a specific section."""
387
+ from typing import Any
388
+
389
+ from rich.panel import Panel
390
+ from rich.syntax import Syntax
391
+
392
+ examples: dict[str, dict[str, Any]] = {
393
+ "basic": {
394
+ "title": "Basic File Tracking",
395
+ "description": "Track individual files with smart defaults",
396
+ "examples": [
397
+ {
398
+ "title": "Simple file tracking",
399
+ "config": """[bashrc]
400
+ paths = ["~/.bashrc"]""",
401
+ "explanation": "Tracks your bash configuration. dot-man automatically:\n"
402
+ "• Generates repo_base as 'bashrc'\n"
403
+ "• Uses 'replace' update_strategy\n"
404
+ "• Enables secrets_filter",
405
+ },
406
+ {
407
+ "title": "Multiple files in one section",
408
+ "config": """[shell-files]
409
+ paths = ["~/.bashrc", "~/.zshrc", "~/.profile"]""",
410
+ "explanation": "Group related files together. All files share the same settings.",
411
+ },
412
+ {
413
+ "title": "Custom repository name",
414
+ "config": """[my-config]
415
+ paths = ["~/.myapp/config"]
416
+ repo_base = "my-app-config" """,
417
+ "explanation": "Override the auto-generated repo_base with a custom name.",
418
+ },
419
+ ],
420
+ },
421
+ "directories": {
422
+ "title": "Directory Tracking",
423
+ "description": "Track entire directories with include/exclude patterns",
424
+ "examples": [
425
+ {
426
+ "title": "Basic directory tracking",
427
+ "config": """[nvim]
428
+ paths = ["~/.config/nvim"]""",
429
+ "explanation": "Tracks your entire Neovim config directory.",
430
+ },
431
+ {
432
+ "title": "Directory with exclusions",
433
+ "config": """[nvim]
434
+ paths = ["~/.config/nvim"]
435
+ exclude = ["*.log", "plugin/packer_compiled.lua"]""",
436
+ "explanation": "Exclude temporary files and compiled plugins from tracking.",
437
+ },
438
+ {
439
+ "title": "Include only specific files",
440
+ "config": """[dotfiles]
441
+ paths = ["~/dotfiles"]
442
+ include = ["*.conf", "*.sh", "README.md"]""",
443
+ "explanation": "Only track configuration files, scripts, and documentation.",
444
+ },
445
+ ],
446
+ },
447
+ "hooks": {
448
+ "title": "Pre/Post Deploy Hooks",
449
+ "description": "Run commands before or after file deployment",
450
+ "examples": [
451
+ {
452
+ "title": "Shell reload after config change",
453
+ "config": """[bashrc]
454
+ paths = ["~/.bashrc"]
455
+ post_deploy = "shell_reload" """,
456
+ "explanation": "Reloads your shell after deploying bash config.\n"
457
+ "'shell_reload' is an alias for: source ~/.bashrc || source ~/.zshrc",
458
+ },
459
+ {
460
+ "title": "Neovim plugin sync",
461
+ "config": """[nvim]
462
+ paths = ["~/.config/nvim"]
463
+ post_deploy = "nvim_sync" """,
464
+ "explanation": "Runs PackerSync after deploying Neovim config.\n"
465
+ "'nvim_sync' is an alias for: nvim --headless +PackerSync +qa",
466
+ },
467
+ {
468
+ "title": "Custom command",
469
+ "config": """[custom-app]
470
+ paths = ["~/.config/myapp"]
471
+ post_deploy = "systemctl --user restart myapp" """,
472
+ "explanation": "Restart a user service after config deployment.",
473
+ },
474
+ {
475
+ "title": "Pre-deploy backup",
476
+ "config": """[important-config]
477
+ paths = ["~/.important"]
478
+ pre_deploy = "cp ~/.important ~/.important.backup" """,
479
+ "explanation": "Create a backup before overwriting important files.",
480
+ },
481
+ ],
482
+ },
483
+ "templates": {
484
+ "title": "Reusable Templates",
485
+ "description": "Define shared settings that can be inherited",
486
+ "examples": [
487
+ {
488
+ "title": "Template definition",
489
+ "config": """[templates.linux-desktop]
490
+ post_deploy = "notify-send 'Config updated'"
491
+ update_strategy = "rename_old"
492
+
493
+ [templates.dev-tools]
494
+ secrets_filter = false
495
+ pre_deploy = "echo 'Deploying dev config'" """,
496
+ "explanation": "Define reusable templates with common settings.",
497
+ },
498
+ {
499
+ "title": "Template inheritance",
500
+ "config": """[hyprland]
501
+ paths = ["~/.config/hypr"]
502
+ inherits = ["linux-desktop"]
503
+
504
+ [git]
505
+ paths = ["~/.gitconfig"]
506
+ inherits = ["dev-tools"]""",
507
+ "explanation": "Inherit settings from templates. Child settings override parent settings.",
508
+ },
509
+ ],
510
+ },
511
+ "advanced": {
512
+ "title": "Advanced Options",
513
+ "description": "Fine-tune behavior with advanced configuration options",
514
+ "examples": [
515
+ {
516
+ "title": "Update strategies",
517
+ "config": """[careful-config]
518
+ paths = ["~/.important"]
519
+ update_strategy = "rename_old"
520
+
521
+ [aggressive-config]
522
+ paths = ["~/.cache/myapp"]
523
+ update_strategy = "replace"
524
+
525
+ [readonly-config]
526
+ paths = ["~/.readonly"]
527
+ update_strategy = "ignore" """,
528
+ "explanation": "• 'replace': Overwrite existing files (default)\n"
529
+ "• 'rename_old': Backup existing files\n"
530
+ "• 'ignore': Skip if file exists",
531
+ },
532
+ {
533
+ "title": "Explicit repository paths",
534
+ "config": """[special-file]
535
+ paths = ["~/.config/app/special.conf"]
536
+ repo_path = "configs/special-config.toml" """,
537
+ "explanation": "Override automatic repo path generation with explicit repo_path.",
538
+ },
539
+ ],
540
+ },
541
+ "secrets": {
542
+ "title": "Secret Detection & Filtering",
543
+ "description": "Automatically detect and handle sensitive information",
544
+ "examples": [
545
+ {
546
+ "title": "Automatic secret filtering",
547
+ "config": """[gitconfig]
548
+ paths = ["~/.gitconfig"]
549
+ # secrets_filter = true (enabled by default)""",
550
+ "explanation": "Automatically redacts API keys, passwords, and tokens when saving.",
551
+ },
552
+ {
553
+ "title": "Disable filtering for trusted files",
554
+ "config": """[trusted-config]
555
+ paths = ["~/.config/trusted"]
556
+ secrets_filter = false""",
557
+ "explanation": "Disable secret filtering for files you know are safe.",
558
+ },
559
+ {
560
+ "title": "Check for secrets",
561
+ "command": "dot-man audit",
562
+ "explanation": "Scan your repository for secrets. Use --strict for CI/CD.",
563
+ },
564
+ ],
565
+ },
566
+ "activate": {
567
+ "title": "Branch Activation Hooks",
568
+ "description": "Run commands when entering/leaving branches",
569
+ "examples": [
570
+ {
571
+ "title": "Start app on branch switch",
572
+ "config": """[dots]
573
+ paths = [".config/quickshell"]
574
+ on_activate = "qs -c ii"
575
+ on_deactivate = "pkill qs -9" """,
576
+ "explanation": "Run 'qs -c ii' when switching TO this branch, "
577
+ "and 'pkill qs -9' when leaving. Perfect for launching "
578
+ "config-specific applications.",
579
+ },
580
+ {
581
+ "title": "Reload environment",
582
+ "config": """[work]
583
+ paths = [".config/work"]
584
+ on_activate = "source ~/.config/work/env.sh"
585
+ on_deactivate = "echo 'Leaving work config'" """,
586
+ "explanation": "Load environment variables or run setup commands "
587
+ "when entering a branch.",
588
+ },
589
+ {
590
+ "title": "Multiple hooks",
591
+ "config": """[dev]
592
+ paths = [".config/dev"]
593
+ on_activate = "echo 'Starting dev mode' && alacritty -e tmux"
594
+ on_deactivate = "pkill -f 'alacritty -e tmux'" """,
595
+ "explanation": "Chain multiple commands with && for complex activation.",
596
+ },
597
+ ],
598
+ },
599
+ "presets": {
600
+ "title": "Quick Setup Presets",
601
+ "description": "Pre-configured sections for popular dotfiles",
602
+ "examples": [
603
+ {
604
+ "title": "Quickshell end-4",
605
+ "config": """[qs-end4]
606
+ paths = [".config/quickshell/end-4"]
607
+ on_activate = "qs -c end-4"
608
+ on_deactivate = "pkill qs -9" """,
609
+ "explanation": "Quickshell with config 'end-4'. Auto-detected if exists.",
610
+ },
611
+ {
612
+ "title": "Quickshell caelestia",
613
+ "config": """[qs-caelestia]
614
+ paths = [".config/quickshell/caelestia"]
615
+ on_activate = "qs -c caelestia"
616
+ on_deactivate = "pkill qs -9" """,
617
+ "explanation": "Quickshell with config 'caelestia'. Auto-detected if exists.",
618
+ },
619
+ {
620
+ "title": "Quickshell custom",
621
+ "config": """[qs-my-config]
622
+ paths = [".config/quickshell/my-config"]
623
+ on_activate = "qs -c my-config"
624
+ on_deactivate = "pkill qs -9" """,
625
+ "explanation": "Replace 'my-config' with your quickshell config name.",
626
+ },
627
+ {
628
+ "title": "Full shell setup",
629
+ "config": """[shell]
630
+ paths = [".bashrc", ".zshrc", ".config/fish"]
631
+ post_deploy = "shell_reload"
632
+
633
+ [vim]
634
+ paths = [".config/nvim"]
635
+
636
+ [tmux]
637
+ paths = [".tmux.conf"]
638
+ post_deploy = "tmux source-file ~/.tmux.conf" """,
639
+ "explanation": "Complete shell setup with multiple sections. "
640
+ "Run 'dot-man config detect' to auto-detect what's available.",
641
+ },
642
+ ],
643
+ },
644
+ }
645
+
646
+ if section not in examples:
647
+ ui.error(f"Unknown section: {section}", exit_code=0)
648
+ ui.console.print(f"Available sections: {', '.join(examples.keys())}")
649
+ return
650
+
651
+ data = examples[section]
652
+
653
+ ui.console.print()
654
+ ui.console.print(
655
+ Panel.fit(
656
+ f"[bold blue]{data['title']}[/bold blue]\n\n{data['description']}",
657
+ title=f"📖 {data['title']}",
658
+ )
659
+ )
660
+
661
+ for i, example in enumerate(data["examples"], 1):
662
+ ui.console.print(f"\n[bold cyan]Example {i}: {example['title']}[/bold cyan]")
663
+
664
+ if "config" in example:
665
+ ui.console.print(
666
+ Syntax(example["config"], "toml", theme="monokai", line_numbers=False)
667
+ )
668
+ elif "command" in example:
669
+ ui.console.print(f"[green]$ {example['command']}[/green]")
670
+
671
+ ui.console.print(f"\n[dim]{example['explanation']}[/dim]")
672
+
673
+ ui.console.print(
674
+ "\n[dim]💡 Run 'dot-man config create' to add these examples to your config file[/dim]"
675
+ )
676
+
677
+
678
+ def _run_interactive_tutorial():
679
+ """Run interactive step-by-step tutorial with detailed explanations."""
680
+ from rich.panel import Panel
681
+ from rich.syntax import Syntax
682
+
683
+ ui.console.print()
684
+ ui.console.print(
685
+ Panel.fit(
686
+ "[bold green]🎓 Interactive dot-man Configuration Tutorial[/bold green]\n\n"
687
+ "This interactive tutorial will guide you through configuring dot-man.\n"
688
+ "You'll learn what each configuration option does as we build examples.",
689
+ title="Welcome!",
690
+ )
691
+ )
692
+
693
+ # Track user configurations for final summary
694
+ user_configs = []
695
+
696
+ # Step 1: Basic files
697
+ ui.console.print("\n[bold cyan]📁 Step 1: Basic File Tracking[/bold cyan]")
698
+ ui.console.print(
699
+ "Every configuration section starts with [section-name] and defines what files to track."
700
+ )
701
+
702
+ ui.console.print("\n[bold green]✅ Example: Shell Configuration[/bold green]")
703
+ ui.console.print()
704
+
705
+ # Show the config with explanations
706
+ config_text = """[shell-config]
707
+ paths = ["~/.bashrc", "~/.zshrc"]
708
+ post_deploy = "shell_reload" """
709
+
710
+ ui.console.print(Syntax(config_text, "toml", theme="monokai"))
711
+ ui.console.print()
712
+
713
+ # Explain each part
714
+ ui.console.print(
715
+ "[bold cyan]🔍 [shell-config][/bold cyan] - A unique name for this group of files"
716
+ )
717
+ ui.console.print(
718
+ "[bold cyan]📂 paths[/bold cyan] - List of files/directories to track (supports ~ expansion)"
719
+ )
720
+ ui.console.print(
721
+ "[bold cyan]🚀 post_deploy[/bold cyan] - Command to run AFTER files are deployed"
722
+ )
723
+ ui.console.print(
724
+ "[bold cyan]🔄 shell_reload[/bold cyan] - Built-in alias that reloads bash/zsh"
725
+ )
726
+ ui.console.print(" [dim](runs: source ~/.bashrc || source ~/.zshrc)[/dim]")
727
+
728
+ ui.console.print(
729
+ "\n[dim]💡 Smart defaults apply automatically - you only specify what's different![/dim]"
730
+ )
731
+
732
+ ui.console.print("\n[dim]Press Enter to continue...[/dim]")
733
+ input()
734
+
735
+ ui.console.print(
736
+ "\n[bold green]✅ Git Config with Automatic Secret Protection:[/bold green]"
737
+ )
738
+ ui.console.print()
739
+
740
+ config_text = """[gitconfig]
741
+ paths = ["~/.gitconfig"]"""
742
+
743
+ ui.console.print(Syntax(config_text, "toml", theme="monokai"))
744
+ ui.console.print()
745
+
746
+ ui.console.print(
747
+ "[bold cyan]🔒 Automatic security[/bold cyan] - Git configs get special protection:"
748
+ )
749
+ ui.console.print(
750
+ " • [yellow]secrets_filter = true[/yellow] - Detects and redacts sensitive data"
751
+ )
752
+ ui.console.print(
753
+ " • [yellow]API keys, passwords, tokens[/yellow] - Automatically removed when saving"
754
+ )
755
+ ui.console.print(
756
+ ' • [yellow]update_strategy = "replace"[/yellow] - Safe for most config files'
757
+ )
758
+
759
+ ui.console.print("\n[dim]Press Enter to continue...[/dim]")
760
+ input()
761
+
762
+ # Step 2: Directories with patterns
763
+ ui.console.print(
764
+ "\n[bold cyan]📂 Step 2: Directory Tracking with Patterns[/bold cyan]"
765
+ )
766
+ ui.console.print(
767
+ "When tracking directories, you can include/exclude specific files."
768
+ )
769
+
770
+ ui.console.print(
771
+ "\n[bold green]✅ Neovim Config with Smart Exclusions:[/bold green]"
772
+ )
773
+ ui.console.print()
774
+
775
+ config_text = """[nvim]
776
+ paths = ["~/.config/nvim"]
777
+ exclude = ["*.log", "plugin/packer_compiled.lua"]
778
+ post_deploy = "nvim_sync" """
779
+
780
+ ui.console.print(Syntax(config_text, "toml", theme="monokai"))
781
+ ui.console.print()
782
+
783
+ ui.console.print(
784
+ "[bold cyan]🎯 exclude[/bold cyan] - Patterns of files/directories to SKIP tracking"
785
+ )
786
+ ui.console.print(" • [yellow]*.log[/yellow] - Any .log files")
787
+ ui.console.print(
788
+ " • [yellow]plugin/packer_compiled.lua[/yellow] - Compiled plugin cache"
789
+ )
790
+ ui.console.print(
791
+ "[bold cyan]📝 Pattern syntax[/bold cyan] - Wildcards (*, **, ?) and gitignore-style"
792
+ )
793
+ ui.console.print(
794
+ "[bold cyan]🔄 nvim_sync[/bold cyan] - Alias: nvim --headless +PackerSync +qa"
795
+ )
796
+
797
+ ui.console.print(
798
+ '\n[dim]💡 Use ** for recursive: "**/*.tmp" matches all .tmp files in subdirs[/dim]'
799
+ )
800
+
801
+ user_configs.append(
802
+ (
803
+ "nvim",
804
+ """[nvim]
805
+ paths = ["~/.config/nvim"]
806
+ exclude = ["*.log", "plugin/packer_compiled.lua"]
807
+ post_deploy = "nvim_sync" """,
808
+ )
809
+ )
810
+
811
+ ui.console.print("\n[dim]Press Enter to continue...[/dim]")
812
+ input()
813
+
814
+ # Step 3: Update strategies
815
+ ui.console.print(
816
+ "\n[bold cyan]🔄 Step 3: Update Strategies - How Files Are Deployed[/bold cyan]"
817
+ )
818
+ ui.console.print("Choose how dot-man handles existing files when deploying.")
819
+
820
+ # Show update strategy information
821
+
822
+ ui.console.print("\n[bold green]📋 Update Strategy Options:[/bold green]")
823
+
824
+ strategy_examples = {
825
+ "Safe (rename_old)": {
826
+ "config": 'update_strategy = "rename_old"',
827
+ "explanation": "• Backs up existing file as filename.bak\n• Then overwrites with new version\n• Your original file is safe if something goes wrong",
828
+ },
829
+ "Direct (replace)": {
830
+ "config": 'update_strategy = "replace" # Default',
831
+ "explanation": "• Directly overwrites existing files\n• No backup created\n• Fastest option",
832
+ },
833
+ "Conservative (ignore)": {
834
+ "config": 'update_strategy = "ignore"',
835
+ "explanation": "• Skips files that already exist\n• Never overwrites your changes\n• Good for one-time setup files",
836
+ },
837
+ }
838
+
839
+ for name, details in strategy_examples.items():
840
+ ui.console.print(f"\n[yellow]{name}:[/yellow]")
841
+ ui.console.print(Syntax(details["config"], "toml", theme="monokai"))
842
+ ui.console.print(details["explanation"])
843
+
844
+ ui.console.print("\n[dim]Press Enter to continue...[/dim]")
845
+ input()
846
+
847
+ # Step 4: Pre-deploy hooks
848
+ ui.console.print(
849
+ "\n[bold cyan]⚡ Step 4: Pre-Deploy Hooks - Actions Before Deployment[/bold cyan]"
850
+ )
851
+ ui.console.print("Sometimes you need to prepare before deploying files.")
852
+
853
+ ui.console.print("\n[bold green]🔧 Pre-deploy Hook Examples:[/bold green]")
854
+ ui.console.print()
855
+
856
+ examples = [
857
+ {
858
+ "title": "Backup important files",
859
+ "config": """[important-config]
860
+ paths = ["~/.important/app.conf"]
861
+ pre_deploy = "cp ~/.important/app.conf ~/.important/app.conf.backup" """,
862
+ "explanation": "Creates a backup before dot-man touches the file",
863
+ },
864
+ {
865
+ "title": "Stop services before config change",
866
+ "config": """[service-config]
867
+ paths = ["~/.config/my-service"]
868
+ pre_deploy = "systemctl --user stop my-service" """,
869
+ "explanation": "Stops the service before updating its config files",
870
+ },
871
+ ]
872
+
873
+ for example in examples:
874
+ ui.console.print(f"[cyan]{example['title']}:[/cyan]")
875
+ ui.console.print(Syntax(example["config"], "toml", theme="monokai"))
876
+ ui.console.print(f" {example['explanation']}")
877
+ ui.console.print()
878
+
879
+ ui.console.print("\n[dim]Press Enter to continue...[/dim]")
880
+ input()
881
+
882
+ # Step 5: Templates
883
+ ui.console.print(
884
+ "\n[bold cyan]📋 Step 5: Templates - Reusable Configuration[/bold cyan]"
885
+ )
886
+ ui.console.print("Define shared settings that multiple sections can inherit.")
887
+
888
+ ui.console.print("\n[bold green]🎨 Template Example:[/bold green]")
889
+ ui.console.print()
890
+
891
+ config_text = """# Define a template
892
+ [templates.desktop-apps]
893
+ post_deploy = "notify-send 'Config updated'"
894
+ update_strategy = "rename_old"
895
+
896
+ # Use the template
897
+ [hyprland]
898
+ paths = ["~/.config/hypr"]
899
+ inherits = ["desktop-apps"]
900
+
901
+ [waybar]
902
+ paths = ["~/.config/waybar"]
903
+ inherits = ["desktop-apps"]
904
+ # Override settings if needed
905
+ update_strategy = "replace" """
906
+
907
+ ui.console.print(Syntax(config_text, "toml", theme="monokai"))
908
+ ui.console.print()
909
+
910
+ ui.console.print(
911
+ "[bold cyan]📋 Template definition[/bold cyan] - [templates.name] sections are reusable"
912
+ )
913
+ ui.console.print(
914
+ "[bold cyan]🔗 inherits[/bold cyan] - List of templates to inherit settings from"
915
+ )
916
+ ui.console.print(
917
+ "[bold cyan]⚡ Override behavior[/bold cyan] - Section settings override templates"
918
+ )
919
+ ui.console.print(
920
+ "[bold cyan]🎯 Use case[/bold cyan] - Share notifications, strategies, etc."
921
+ )
922
+
923
+ ui.console.print("\n[dim]Press Enter to continue...[/dim]")
924
+ input()
925
+
926
+ # Step 6: Terminal
927
+ ui.console.print("\n[bold cyan]💻 Step 6: Terminal Configuration[/bold cyan]")
928
+
929
+ ui.console.print("\n[bold green]✅ Kitty Terminal Configuration:[/bold green]")
930
+ ui.console.print()
931
+
932
+ config_text = """[kitty]
933
+ paths = ["~/.config/kitty"]
934
+ post_deploy = "kitty_reload" """
935
+
936
+ ui.console.print(Syntax(config_text, "toml", theme="monokai"))
937
+ ui.console.print()
938
+
939
+ ui.console.print(
940
+ "[bold cyan]🖥️ Kitty[/bold cyan] - Fast, GPU-accelerated terminal emulator"
941
+ )
942
+ ui.console.print("[bold cyan]📂 paths[/bold cyan] - Kitty configuration directory")
943
+ ui.console.print("[bold cyan]🚀 post_deploy[/bold cyan] - Reload command for Kitty")
944
+ ui.console.print(
945
+ "[bold cyan]🔄 kitty_reload[/bold cyan] - Sends SIGUSR1 to reload running instances"
946
+ )
947
+
948
+ user_configs.append(
949
+ (
950
+ "kitty",
951
+ """[kitty]
952
+ paths = ["~/.config/kitty"]
953
+ post_deploy = "kitty_reload" """,
954
+ )
955
+ )
956
+
957
+ ui.console.print("\n[dim]Press Enter to continue...[/dim]")
958
+ input()
959
+
960
+ # Final summary
961
+ ui.console.print("\n[bold green]🎉 Tutorial Complete![/bold green]")
962
+ ui.console.print("\n[dim]You've learned about:[/dim]")
963
+ ui.console.print(" • 📁 Basic file and directory tracking")
964
+ ui.console.print(" • 🎯 Include/exclude patterns for selective tracking")
965
+ ui.console.print(" • 🔄 Update strategies (replace, rename_old, ignore)")
966
+ ui.console.print(" • ⚡ Pre/post deploy hooks for automation")
967
+ ui.console.print(" • 📋 Templates for reusable configuration")
968
+ ui.console.print(" • 🔒 Automatic secret detection and filtering")
969
+
970
+ ui.console.print("\n[dim]Next steps:[/dim]")
971
+ ui.console.print(
972
+ "[green]$ dot-man config create[/green] [dim]- Generate config file with examples[/dim]"
973
+ )
974
+ ui.console.print(
975
+ "[green]$ dot-man edit[/green] [dim]- Customize your configuration[/dim]"
976
+ )
977
+ ui.console.print(
978
+ "[green]$ dot-man config tutorial --section advanced[/green] [dim]- Learn advanced features[/dim]"
979
+ )