Skip to content

dotfiles Module

Install and manage dotfiles by creating symlinks from a dotfiles repository to the home directory, with support for hostname-specific overrides.

Import

from fscm.modules import dotfiles

Directory Structure

The dotfiles module expects a specific directory structure:

~/dotfiles/
├── common/                    # Files for all hosts
│   ├── .bashrc
│   ├── .vimrc
│   └── .config/
│       └── nvim/
│           └── init.lua
├── hosts/                     # Host-specific overrides
│   ├── laptop/
│   │   └── .bashrc           # Overrides common/.bashrc on 'laptop'
│   └── server/
│       └── .config/
│           └── nvim/
│               └── init.lua
  • common/ contains files installed on all hosts
  • hosts/<hostname>/ contains host-specific overrides that take precedence
  • Nested directories (e.g., .config/nvim/) are fully supported

Functions

install

Install dotfiles by creating symlinks.

dotfiles.install(
    dotfiles_dir="~/dotfiles",
    target_dir=None,      # Default: $HOME
    host=None,            # Default: current hostname
    backup=True,          # Backup existing files
    force=False,          # Replace symlinks pointing elsewhere
    sudo=False,
)

Parameters:

Name Type Default Description
dotfiles_dir str/Path required Path to dotfiles repository
target_dir str/Path $HOME Where to create symlinks
host str current hostname Hostname for overrides
backup bool True Backup existing files before replacing
force bool False Replace symlinks pointing elsewhere
sudo bool False Use sudo for privileged locations

Returns: ChangeList with installation changes.

Behavior:

  • Creates symlinks for each file in common/
  • Applies host-specific overrides from hosts/<hostname>/
  • Creates parent directories as needed
  • Existing regular files are backed up and replaced
  • Existing correct symlinks are skipped (idempotent)
  • Existing symlinks pointing elsewhere are skipped unless force=True

uninstall

Remove dotfile symlinks that point to the dotfiles repository.

dotfiles.uninstall(
    dotfiles_dir="~/dotfiles",
    target_dir=None,
    host=None,
    sudo=False,
)

Parameters: Same as install().

Returns: ChangeList with removal changes.

Note: Only removes symlinks pointing to files in your dotfiles repository. Regular files and symlinks pointing elsewhere are left untouched.


list_dotfiles

List all dotfiles that would be installed.

dotfiles.list_dotfiles(
    dotfiles_dir="~/dotfiles",
    host=None,
)

Parameters:

Name Type Default Description
dotfiles_dir str/Path required Path to dotfiles repository
host str current hostname Hostname for overrides

Returns: Dict[Path, Path] mapping relative target paths to absolute source paths.


status

Get the current status of all dotfiles.

dotfiles.status(
    dotfiles_dir="~/dotfiles",
    target_dir=None,
    host=None,
)

Returns: List[DotfileStatus] with status for each dotfile.

Each DotfileStatus has:

  • source — Source file in dotfiles repo
  • target — Target path in home directory
  • state — Current state (see below)
  • needs_action — Whether installation is needed

Change Types

The module defines these change types:

  • DotfileLinked — Symlink created
  • DotfileBackedUp — Existing file backed up
  • DotfileSkipped — File skipped (with reason)
  • DotfileUnlinked — Symlink removed

DotfileState Enum

State Description
MISSING Target doesn't exist
CORRECT_LINK Symlink points to our source
WRONG_LINK Symlink points elsewhere
REGULAR_FILE Regular file exists
DIRECTORY Directory exists (not a symlink)

Skipped Files

By default, these files are skipped:

  • .git/ directory and all contents
  • README.md, README, LICENSE, LICENSE.md
  • .gitignore, .gitmodules
  • Files starting with .dotfiles (config files for the dotfiles repo itself)

Examples

Basic Installation

from fscm.modules import dotfiles

# Install all dotfiles
dotfiles.install("~/dotfiles")

Preview Changes

from fscm.modules import dotfiles

# List what would be installed
for target, source in dotfiles.list_dotfiles("~/dotfiles").items():
    print(f"{target} -> {source}")

# Check status
for s in dotfiles.status("~/dotfiles"):
    print(f"{s.target}: {s.state.value}")
from fscm.modules import dotfiles

# Replace even symlinks pointing elsewhere
dotfiles.install("~/dotfiles", force=True)

Install for Different Host

from fscm.modules import dotfiles

# Install using laptop-specific overrides
dotfiles.install("~/dotfiles", host="laptop")

Install to Custom Location

from fscm.modules import dotfiles

# Install to a different target directory
dotfiles.install(
    "~/dotfiles",
    target_dir="/home/deploy",
    sudo=True,
)

Uninstall

from fscm.modules import dotfiles

# Remove all managed symlinks
dotfiles.uninstall("~/dotfiles")

Complete Example

#!/usr/bin/env python3
"""Set up dotfiles on a new machine."""
from fscm.modules import dotfiles

def main():
    # Check what needs to be done
    statuses = dotfiles.status("~/dotfiles")
    needs_install = [s for s in statuses if s.needs_action]

    if not needs_install:
        print("All dotfiles already installed!")
        return

    print(f"Installing {len(needs_install)} dotfiles...")

    # Install with backup
    changes = dotfiles.install("~/dotfiles", backup=True)

    # Report what was done
    for change in changes:
        print(f"  {change.msg.format(**change.__dict__)}")

if __name__ == "__main__":
    main()