Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/markdown-link-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
"ignorePatterns": [
{
"pattern": "^http://localhost"
},
{
"pattern": "^https://www.freedesktop.org"
},
{
"pattern": "^https://goreleaser.com"
},
{
"pattern": "^https://wttr.in"
}
],
"replacementPatterns": [
Expand Down
14 changes: 12 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ roles/claude/files/skills/peon-ping-*/
# Crush directory
.crush/

# Claude session snapshots (personal work state)
.claude/snapshots/
# Claude Code personal files (keep agents/skills/commands)
.claude/settings.json
.claude/settings.local.json
.claude/projects/
.claude/notepads/
.claude/*.md

# Ansible cache/runtime directories
.ansible/
Expand Down Expand Up @@ -50,3 +54,9 @@ node_modules/

# Task files
tasks.json

# User overrides (fork customizations)
overrides/roles/
overrides/config/dotfiles.conf
!overrides/config/dotfiles.conf.example
!overrides/README.md
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,35 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/TechDufus/dotfiles/main/
- Customize your setup by editing `~/.dotfiles/group_vars/all.yml`
- Run `dotfiles` anytime to update your environment

## 🍴 Fork This Project

**This repo is designed for forking.** Customize your dotfiles without merge conflicts.

### How It Works

1. **Fork** this repository
2. **Create overrides** in `overrides/` directory (gitignored)
3. **Pull upstream** anytime - your customizations stay separate

### What You Can Override

| Override Type | Location | Effect |
|--------------|----------|--------|
| Variables | `overrides/roles/{role}/vars/main.yml` | Change settings (paths, names, options) |
| Config Files | `overrides/roles/{role}/files/` | Replace config files with your own |
| Tasks | `overrides/roles/{role}/tasks/main.yml` | Replace entire role behavior |

### Common Customizations

| What | Override | Example |
|------|----------|---------|
| Git identity | `overrides/roles/git/vars/main.yml` | Your name and email |
| Shell config | `overrides/roles/zsh/files/.zshrc` | Personal aliases |
| Terminal | `overrides/roles/ghostty/files/config` | Theme, font, colors |
| Editor | `overrides/roles/neovim/files/` | Plugins, keybindings |

See [`overrides/README.md`](overrides/README.md) for the complete guide.

---

## 🎯 Goals
Expand Down
18 changes: 14 additions & 4 deletions bin/dotfiles
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,22 @@ DOTFILES_LOG="$HOME/.dotfiles.log"
DOTFILES_DIR="$HOME/.dotfiles"
IS_FIRST_RUN="$HOME/.dotfiles_run"
DOTFILES_VERSION="1.0.0"

# Fork configuration - forkers change this or use overrides/dotfiles.conf
DOTFILES_GITHUB_USER="TechDufus"
DOTFILES_REPO_NAME="dotfiles"
DOTFILES_CONFIG="$HOME/.dotfiles/overrides/config/dotfiles.conf"
if [[ -f "$DOTFILES_CONFIG" ]]; then
# shellcheck source=/dev/null
source "$DOTFILES_CONFIG"
fi

# Spinner PID tracking
SPINNER_PID=""

# Show usage/help information
show_help() {
echo -e "${CAT_SAPPHIRE}TechDufus Dotfiles${NC} - Ansible-based dotfiles management"
echo -e "${CAT_SAPPHIRE}${DOTFILES_GITHUB_USER} Dotfiles${NC} - Ansible-based dotfiles management"
echo ""
echo -e "${CAT_BLUE}USAGE:${NC}"
echo " dotfiles [OPTIONS] [-- ANSIBLE_ARGS]"
Expand Down Expand Up @@ -148,7 +158,7 @@ show_help() {
echo " All arguments not recognized by dotfiles are passed to ansible-playbook."
echo " Common options: -t <tags>, --check, -vvv, --list-tags"
echo ""
echo -e "${CAT_SUBTEXT0}For more information: https://github.com/TechDufus/dotfiles${NC}"
echo -e "${CAT_SUBTEXT0}For more information: https://github.com/${DOTFILES_GITHUB_USER}/${DOTFILES_REPO_NAME}${NC}"
}

# Show version information
Expand All @@ -157,7 +167,7 @@ show_version() {
if [[ -d "$DOTFILES_DIR/.git" ]]; then
git_hash=$(git -C "$DOTFILES_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")
fi
echo -e "${CAT_SAPPHIRE}TechDufus Dotfiles${NC} version ${CAT_GREEN}${DOTFILES_VERSION}${NC} (${CAT_SUBTEXT1}${git_hash}${NC})"
echo -e "${CAT_SAPPHIRE}${DOTFILES_GITHUB_USER} Dotfiles${NC} version ${CAT_GREEN}${DOTFILES_VERSION}${NC} (${CAT_SUBTEXT1}${git_hash}${NC})"
}

# Spinner function that runs in background
Expand Down Expand Up @@ -452,7 +462,7 @@ esac

if ! [[ -d "$DOTFILES_DIR" ]]; then
__task "Downloading dotfiles repository (This may take a minute)"
_cmd "git clone --quiet https://github.com/TechDufus/dotfiles.git $DOTFILES_DIR"
_cmd "git clone --quiet https://github.com/${DOTFILES_GITHUB_USER}/${DOTFILES_REPO_NAME}.git $DOTFILES_DIR"
_task_done
else
__task "Updating dotfiles repository"
Expand Down
6 changes: 5 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Dotfiles Documentation

## Forking & Customization

See [`../overrides/README.md`](../overrides/README.md) for the fork-friendly override system.

## Role Development Guide

See [example-role/](example-role/) for a complete template demonstrating the standard patterns used across all dotfiles roles.
Expand Down Expand Up @@ -144,4 +148,4 @@ The uninstall process will:
2. **Be idempotent** - Running twice should not change anything
3. **Handle errors gracefully** - Provide clear messages and alternatives
4. **Document non-obvious choices** - Add comments for complex logic
5. **Test on all platforms** - Ensure consistency across OS
5. **Test on all platforms** - Ensure consistency across OS
11 changes: 5 additions & 6 deletions main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@
tags:
- always

- name: Run roles
ansible.builtin.include_role:
apply:
tags:
- "{{ roles_item }}"
name: "{{ roles_item }}"
- name: Run roles with override support
ansible.builtin.include_tasks:
file: "{{ playbook_dir }}/tasks/run_role_with_overrides.yml"
vars:
_role_name: "{{ roles_item }}"
loop_control:
loop_var: roles_item
with_items: "{{ run_roles }}"
Expand Down
Empty file added overrides/.gitkeep
Empty file.
225 changes: 225 additions & 0 deletions overrides/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Overrides Directory

Fork-friendly customization system. Override any role without modifying upstream files.

## Quick Start

1. **Fork** the repository on GitHub
2. **Clone** your fork locally
3. **Create** your override: `mkdir -p overrides/roles/{role}/files`
4. **Customize** by copying and editing files
5. **Run** `dotfiles` - your overrides apply automatically

Your overrides stay local (gitignored), so you can pull upstream updates without conflicts.

## Override Levels

| Level | Path | Behavior |
|-------|------|----------|
| **Vars** | `overrides/roles/{role}/vars/main.yml` | Merged before role runs |
| **Files** | `overrides/roles/{role}/files/` | Role uses these files instead |
| **Tasks** | `overrides/roles/{role}/tasks/main.yml` | **Replaces role entirely** |

## How It Works

The wrapper checks for overrides before running each role:

1. **Vars override** - Loaded first, variables merge/override role defaults
2. **Files override** - Sets `_files_path` so role symlinks your files
3. **Task override** - Original role is **skipped**, your tasks run instead

## Examples

### Override Variables Only

```bash
mkdir -p overrides/roles/git/vars
cat > overrides/roles/git/vars/main.yml << 'EOF'
git_user_name: "My Fork Name"
git_user_email: "fork@example.com"
EOF
```

### Override Config Files Only

```bash
mkdir -p overrides/roles/neovim/files
cp -r roles/neovim/files/* overrides/roles/neovim/files/
# Edit files in overrides/roles/neovim/files/ as needed
```

### Replace Role Entirely

```bash
mkdir -p overrides/roles/custom-tool/tasks
cat > overrides/roles/custom-tool/tasks/main.yml << 'EOF'
---
- name: "custom-tool | My custom implementation"
ansible.builtin.debug:
msg: "Running custom tasks instead of original role"

# Use _files_path for file operations (role_path unavailable in overrides)
- name: "custom-tool | Symlink config"
ansible.builtin.file:
src: "{{ _files_path }}/config"
dest: "{{ ansible_user_dir }}/.config/custom-tool"
state: link
EOF
```

### Add New Custom Roles

Create roles that don't exist upstream - perfect for personal tools or workflows:

```bash
mkdir -p overrides/roles/my-tool/tasks
cat > overrides/roles/my-tool/tasks/main.yml << 'EOF'
---
- name: "my-tool | Install my custom tool"
ansible.builtin.package:
name: my-tool
state: present

- name: "my-tool | Deploy config"
ansible.builtin.copy:
src: "{{ _files_path }}/config"
dest: "{{ ansible_user_dir }}/.config/my-tool/config"
EOF

# Add config files
mkdir -p overrides/roles/my-tool/files
echo "my config" > overrides/roles/my-tool/files/config
```

Run with the `-t` flag:
```bash
dotfiles -t my-tool
```

Custom roles are only run when explicitly tagged - they won't run during a full `dotfiles` execution unless added to `default_roles`.

## Commonly Overridden Files

| What to Customize | Override Path | Key Settings |
|-------------------|---------------|--------------|
| Git identity | `overrides/roles/git/vars/main.yml` | `git_user_name`, `git_user_email` |
| Git config | `overrides/roles/git/files/.gitconfig` | Aliases, merge strategy |
| Shell aliases | `overrides/roles/zsh/files/.zshrc` | Personal shortcuts |
| Terminal colors | `overrides/roles/ghostty/files/config` | Theme, font, opacity |
| Editor config | `overrides/roles/neovim/files/` | Plugins, keybindings |
| Tmux config | `overrides/roles/tmux/files/tmux/` | Prefix key, status bar |

## Partial File Overrides

**Important:** When overriding files, you must provide ALL files the role expects.

The `_files_path` variable points to ONE directory - either your override OR the role's files, not both.

**Example:** If `roles/git/files/` contains:
- `.gitconfig`
- `global.commit.template`

And you only put `.gitconfig` in `overrides/roles/git/files/`, the role will fail looking for `global.commit.template`.

**Solution:** Copy everything, then modify what you need:
```bash
cp -r roles/git/files/* overrides/roles/git/files/
# Now edit only the files you want to change
```

## Important Notes

- **Task overrides skip the original role** - your tasks have full control
- **Use `_files_path`** in override tasks, not `role_path` (magic variable unavailable)
- **This directory is gitignored** - your customizations stay local
- **Pull upstream cleanly** - no merge conflicts with your overrides

## Variable Scope Behavior

**Important:** Variables loaded from `overrides/roles/{role}/vars/main.yml` persist for the entire playbook run.

This is standard Ansible behavior - `include_vars` loads into play scope. After a role runs, its override variables remain set for all subsequent roles.

### Best Practices

1. **Use role-prefixed variable names** - `git_user_email` not `user_email`
2. **Check role defaults first** - `roles/{role}/defaults/main.yml` shows expected names
3. **Avoid generic names** - `config`, `version`, `name` could collide with other roles
4. **Override only what you need** - fewer variables = fewer collision chances

This behavior matches how `group_vars/` works in Ansible - it's not a bug, just something to be aware of.

## Debugging Overrides

### Verify Override Detection

Run with verbose output to see which overrides are detected:
```bash
dotfiles -t git -vvv 2>&1 | grep -E "(override|_files_path)"
```

### Check _files_path Value

In your playbook output, look for:
```
TASK [run_role_with_overrides : Set _files_path]
ok: [localhost] => {"ansible_facts": {"_files_path": "/path/to/overrides/roles/git/files"}}
```

If it shows `roles/git/files` instead, your override wasn't detected.

### Common Issues

| Problem | Cause | Fix |
|---------|-------|-----|
| Override not applied | Wrong directory structure | Check path: `overrides/roles/{role}/files/` not `override/` |
| File not found | Missing file in override | Copy ALL files from `roles/{role}/files/` |
| Variables not merged | Wrong vars path | Use `overrides/roles/{role}/vars/main.yml` |

## Migrating from Modified Roles

If you've already forked and modified role files directly:

1. **Identify changes** - `git diff upstream/main -- roles/`
2. **Create override structure** - `mkdir -p overrides/roles/{role}/files`
3. **Copy your modified files** - Move from `roles/{role}/files/` to `overrides/roles/{role}/files/`
4. **Reset role to upstream** - `git checkout upstream/main -- roles/{role}/files/`
5. **Test** - Run `dotfiles -t {role}` to verify

Your modifications now live in `overrides/`, cleanly separated from upstream.

## Bootstrap Customization

When you fork this repo, the bootstrap script needs to know to clone YOUR fork, not the original.

### Quick Setup

1. Edit `bin/dotfiles` in your fork and change:
```bash
DOTFILES_GITHUB_USER="YourGitHubUsername"
```

2. Commit and push to your fork

3. Run your fork's bootstrap:
```bash
bash -c "$(curl -fsSL https://raw.githubusercontent.com/YourGitHubUsername/dotfiles/main/bin/dotfiles)"
```

### Config File Override

After initial clone, you can also use a config file for subsequent runs:

```bash
cp overrides/config/dotfiles.conf.example overrides/config/dotfiles.conf
# Edit dotfiles.conf with your settings
```

This is useful if you want to keep `bin/dotfiles` unmodified for easier upstream merges.

### Available Settings

| Variable | Default | Purpose |
|----------|---------|---------|
| `DOTFILES_GITHUB_USER` | `TechDufus` | GitHub username for repo URL |
| `DOTFILES_REPO_NAME` | `dotfiles` | Repository name (if renamed) |
Loading
Loading