commit ef481a03cbbc061d850378a5ed6fcbde24a1eca4 Author: Your Name Date: Fri Jun 19 19:16:09 2026 +0000 dotfiles vCurrent version: v1.0.0 1.1.0 diff --git a/.config/fish/conf.d/aliases.fish b/.config/fish/conf.d/aliases.fish new file mode 100644 index 0000000..3f13c1f --- /dev/null +++ b/.config/fish/conf.d/aliases.fish @@ -0,0 +1,63 @@ +# ============================================================================= +# conf.d/aliases.fish — User aliases and abbreviations +# ============================================================================= +# +# Files in ~/.config/fish/conf.d/ are auto-sourced by Fish in alphabetical +# order on every shell start. This is the recommended way to add modular +# configuration snippets — add a new .fish file for each concern: +# +# conf.d/ +# ├── aliases.fish # Shortcuts +# ├── env.fish # Environment variables +# ├── prompt.fish # Custom prompt overrides +# └── ... # Anything else +# +# ────────────────────────────────────────────────────────────────────────────── + +# --- Abbreviations (expand after space/enter) -------------------------------- +# Fish abbreviations are like aliases but they expand inline as you type, +# giving you a chance to edit before running the command. + +abbr -a -- g git +abbr -a -- ga 'git add' +abbr -a -- gc 'git commit' +abbr -a -- gp 'git push' +abbr -a -- gl 'git log --oneline --graph --all' +abbr -a -- gs 'git status -sb' +abbr -a -- gd 'git diff' +abbr -a -- gdc 'git diff --cached' +abbr -a -- gco 'git checkout' +abbr -a -- gcb 'git checkout -b' +abbr -a -- gm 'git merge' +abbr -a -- gr 'git rebase' +abbr -a -- gcl 'git clone' + +abbr -a -- ls 'ls -F --color=auto' +abbr -a -- ll 'ls -l --color=auto' +abbr -a -- la 'ls -la --color=auto' +abbr -a -- tree 'tree -C' + +abbr -a -- .. 'cd ..' +abbr -a -- ... 'cd ../..' + +abbr -a -- rmf 'rm -rf' + +# --- Functions ---------------------------------------------------------------- +# For anything beyond a simple abbreviation, define a function. + +function mkcd -d "Create a directory and cd into it" + mkdir -p $argv && cd $argv +end + +function dotenv -d "Load a .env file into the environment" + if test -f ".env" + for line in (cat .env | string trim) + set -l key (echo $line | cut -d= -f1) + set -l val (echo $line | cut -d= -f2-) + set -gx "$key" "$val" + end + echo "Loaded .env ($(wc -l < .env) variables)" + else + echo "No .env file found" + end +end diff --git a/.config/fish/config.fish b/.config/fish/config.fish new file mode 100644 index 0000000..a58e8ac --- /dev/null +++ b/.config/fish/config.fish @@ -0,0 +1,143 @@ +# ============================================================================= +# config.fish — Fish Shell Configuration & Dotfiles Manager +# Version: 1.0.0 +# ============================================================================= +# +# This file is the entry point for every Fish shell session. It handles: +# +# 1. BASIC SETUP — PATH, environment variables, editor +# 2. VERSIONING — a single source-of-truth version number +# 3. DOTFILES MANAGEMENT — create, update, and track ~/.tmux.conf and +# other common config files through the `dotfiles` command +# 4. SELF-UPDATE — pull the latest version of this very config from a +# remote Git repository and re-apply all managed dotfiles +# 5. EXTENSIBILITY — modular structure via conf.d/ and functions/ that +# Fish auto-loads; no manual sourcing required +# +# The heavy lifting lives in ~/.config/fish/functions/dotfiles.fish, which +# Fish loads automatically when you first run `dotfiles`. Templates for +# managed dotfiles live in ~/.config/fish/dotfiles/. +# +# ── Quick start ──────────────────────────────────────────────────────────── +# +# dotfiles help # Show all subcommands +# dotfiles version # Show the current version of this framework +# dotfiles status # Compare managed dotfiles against their templates +# dotfiles init # Create ~/.tmux.conf, ~/.gitconfig, etc. +# dotfiles sync # Git-pull updates and re-apply everything +# dotfiles publish # Bump version, commit local changes, push to repo +# +# ============================================================================= + + +# ══════════════════════════════════════════════════════════════════════════════ +# SECTION 1 — Version & Core Constants +# ══════════════════════════════════════════════════════════════════════════════ +# +# These values form the single source of truth for the entire framework. +# DOTFILES_DIR holds the template files; DOTFILES_BACKUP_DIR holds backups +# and the last-update-check timestamp. + +set -g DOTFILES_VERSION "Current version: v1.0.0 1.1.0" + +# --- Remote repository (set this to enable auto-updates) -------------------- +# Example: +# dotfiles repo https://github.com/yourname/dotfiles +# +# Until you set this, `dotfiles sync` will show a reminder but won't fail. +set -gx DOTFILES_REPO_URL "" +set -gx DOTFILES_REPO_BRANCH "main" + +# --- Paths ------------------------------------------------------------------ +# __fish_config_dir is a built-in Fish variable that points to +# ~/.config/fish — we use it instead of hard-coding the path. +set -gx DOTFILES_DIR "$__fish_config_dir/dotfiles" +set -gx DOTFILES_BACKUP_DIR "$__fish_config_dir/backups" + +# --- Auto-update interval --------------------------------------------------- +# How often (in days) to remind you that updates are available when a new +# interactive shell starts. Set to 0 to disable the nag. +set -g DOTFILES_AUTO_UPDATE_INTERVAL_DAYS 7 + +# ────────────────────────────────────────────────────────────────────────────── +# Version stamp embedded in the file itself for quick verification. +# When you run `dotfiles version`, it reads this constant — no external file +# to keep in sync. +# ────────────────────────────────────────────────────────────────────────────── + + +# ══════════════════════════════════════════════════════════════════════════════ +# SECTION 2 — PATH & Environment +# ══════════════════════════════════════════════════════════════════════════════ + +# fish_add_path is idempotent — it won't add a directory that's already on PATH. +fish_add_path "$HOME/.local/bin" +fish_add_path "$HOME/bin" + +# Preferred editor (used by `dotfiles edit` among other things). +set -q EDITOR; or set -gx EDITOR "nano" + +# Preferred file pager. +set -q PAGER; or set -gx PAGER "less" + + +# ══════════════════════════════════════════════════════════════════════════════ +# SECTION 3 — Dotfiles Infrastructure +# ══════════════════════════════════════════════════════════════════════════════ + +# Create the required directories on every login so they are always present +# even when the repo is cloned fresh. +for dir in "$DOTFILES_DIR" "$DOTFILES_BACKUP_DIR" + if not test -d "$dir" + mkdir -p "$dir" + end +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# SECTION 4 — Interactive Session: Prompt, Welcome, Auto-Update Nag +# ══════════════════════════════════════════════════════════════════════════════ +# +# Everything below this guard runs only in interactive (i.e. human-facing) +# shells, never in scripts. + +if status is-interactive + + # ── 4a. Welcome message (shown once per login) ──────────────────────── + # Set DOTFILES_SUPPRESS_WELCOME=1 in your environment to silence this. + if not set -q DOTFILES_SUPPRESS_WELCOME + echo "fish: dotfiles v$DOTFILES_VERSION — run 'dotfiles help' for commands" + end + + # ── 4b. Periodic auto-update reminder ───────────────────────────────── + # Once every DOTFILES_AUTO_UPDATE_INTERVAL_DAYS, print a one-line + # reminder that updates may be available. The actual update is + # performed via `dotfiles sync` (manual, so you control the timing). + if test "$DOTFILES_AUTO_UPDATE_INTERVAL_DAYS" -gt 0 + set -l last_check_file "$DOTFILES_BACKUP_DIR/.last_update_check" + + # Determine days since last check + set -l days_ago 999 + if test -f "$last_check_file" + set -l last_ts (command cat "$last_check_file") + set -l now_ts (date +%s) + # Use integer arithmetic: 86400 seconds in a day + set -l elapsed (math "$now_ts - $last_ts") + if test "$elapsed" -ge 0 + set days_ago (math "$elapsed / 86400") + end + end + + if test "$days_ago" -ge "$DOTFILES_AUTO_UPDATE_INTERVAL_DAYS" + echo "tip: run 'dotfiles sync' to check for dotfiles updates" + command date +%s >"$last_check_file" + end + end + + # ── 4c. Prompt ───────────────────────────────────────────────────────── + # No custom fish_prompt defined here — Fish uses its built-in default + # which shows user@host in color, the working directory, git status, + # and pipestatus. To customise, define your own fish_prompt function + # in ~/.config/fish/functions/fish_prompt.fish. + +end # status is-interactive diff --git a/.config/fish/dotfiles/config/starship.toml b/.config/fish/dotfiles/config/starship.toml new file mode 100644 index 0000000..c3e8d1c --- /dev/null +++ b/.config/fish/dotfiles/config/starship.toml @@ -0,0 +1,80 @@ +# ============================================================================= +# ~/.config/starship.toml — Starship prompt configuration +# Managed by dotfiles ( ~/.config/fish/config.fish ) +# Manual changes will be overwritten when `dotfiles update` is run. +# To customise, edit this template at: +# ~/.config/fish/dotfiles/starship.toml +# then run `dotfiles update starship`. +# ============================================================================= + +# ── Appearance ─────────────────────────────────────────────────────────────── +format = """ +[░▒▓](#a6adc8)\ +$os\ +$username\ +$hostname\ +$directory\ +$git_branch\ +$git_status\ +$git_commit\ +$git_state\ +$nodejs\ +$python\ +$rust\ +$c\ +$docker_context\ +$cmd_duration\ +$line_break\ +$character""" + +right_format = """$time""" + +# ── Modules ────────────────────────────────────────────────────────────────── +[character] +success_symbol = "[▶](bold green)" +error_symbol = "[▶](bold red)" + +[directory] +truncation_length = 3 +truncate_to_repo = true + +[git_branch] +format = " on [$branch](bold purple)" +truncation_length = 20 + +[git_status] +conflicted = "🏳" +ahead = "⇡\${count}" +behind = "⇣\${count}" +diverged = "⇕\${ahead_count}⇣\${behind_count}" +stashed = "📦" +renamed = "📝" + +[nodejs] +format = "via [⬢ $version](bold green)" + +[python] +format = "via [🐍 $version](bold yellow)" + +[rust] +format = "via [🦀 $version](bold red)" + +[cmd_duration] +show_milliseconds = true +min_time = 500 + +[time] +disabled = false +format = "🕙 $time" +time_format = "%H:%M" +style = "bold bright-white" + +[os] +disabled = false +style = "bold white" +format = "[$symbol]" + +[os.symbols] +windows = "🪟" +macos = "🍎" +linux = "🐧" diff --git a/.config/fish/dotfiles/gitconfig b/.config/fish/dotfiles/gitconfig new file mode 100644 index 0000000..5db4b8a --- /dev/null +++ b/.config/fish/dotfiles/gitconfig @@ -0,0 +1,62 @@ +; ============================================================================= +; ~/.gitconfig — Git configuration +; Managed by dotfiles ( ~/.config/fish/config.fish ) +; Manual changes will be overwritten when `dotfiles update` is run. +; To customise, edit this template at: +; ~/.config/fish/dotfiles/gitconfig +; then run `dotfiles update gitconfig`. +; ============================================================================= + +[user] + ; ── IMPORTANT: set your real name and email ── + name = Your Name + email = you@example.com + +[init] + defaultBranch = main + +[core] + editor = nano + pager = less + autocrlf = input + whitespace = trailing-space,space-before-tab + excludesfile = ~/.gitignore + +[color] + ui = auto + +[color "branch"] + current = yellow bold + local = green + remote = cyan + +[color "diff"] + meta = yellow bold + frag = magenta bold + old = red bold + new = green bold + +[color "status"] + added = green + changed = yellow + untracked = red + +[push] + default = simple + autoSetupRemote = true + +[fetch] + prune = true + +[merge] + conflictstyle = zdiff3 + +[rebase] + autosquash = true + autoStash = true + +[diff] + tool = vimdiff + +[help] + autocorrect = 10 diff --git a/.config/fish/dotfiles/tmux.conf b/.config/fish/dotfiles/tmux.conf new file mode 100644 index 0000000..462839f --- /dev/null +++ b/.config/fish/dotfiles/tmux.conf @@ -0,0 +1,66 @@ +# ============================================================================= +# ~/.tmux.conf — tmux configuration +# Managed by dotfiles ( ~/.config/fish/config.fish ) +# Manual changes will be overwritten when `dotfiles update` is run. +# To customise, edit this template at: +# ~/.config/fish/dotfiles/tmux.conf +# then run `dotfiles update tmux`. +# ============================================================================= + +# ── Base settings ──────────────────────────────────────────────────────────── +set -g default-terminal "tmux-256color" +set -ga terminal-overrides ",*256col*:Tc" +set -g escape-time 0 +set -g history-limit 50000 +set -g mouse on +set -g focus-events on + +# ── Prefix key (unbind C-b, rebind to C-a for easier reach) ───────────────── +unbind C-b +set -g prefix C-a +bind C-a send-prefix + +# ── Easy reload ────────────────────────────────────────────────────────────── +bind r source-file ~/.tmux.conf \; display " tmux.conf reloaded" + +# ── Splitting ──────────────────────────────────────────────────────────────── +bind | split-window -h +bind - split-window -v + +# ── Vim-style pane navigation ──────────────────────────────────────────────── +bind h select-pane -L +bind j select-pane -D +bind k select-pane -U +bind l select-pane -R + +# ── Resize panes with arrow keys ───────────────────────────────────────────── +bind -r Left resize-pane -L 2 +bind -r Right resize-pane -R 2 +bind -r Down resize-pane -D 2 +bind -r Up resize-pane -U 2 + +# ── Window management ──────────────────────────────────────────────────────── +bind c new-window -c "#{pane_current_path}" +bind , command-prompt -I "#W" { rename-window -- "%%" } +bind w choose-tree -w + +# ── Status bar ─────────────────────────────────────────────────────────────── +set -g status-interval 5 +set -g status-position top +set -g status-style "bg=#1e1e2e,fg=#cdd6f4" + +set -g status-left " #[fg=#89b4fa]#S #[fg=#585b70]│" +set -g status-left-length 40 +set -g status-right "#[fg=#585b70]│ #[fg=#a6e3a1]%Y-%m-%d #[fg=#fab387]%H:%M " +set -g status-right-length 60 + +setw -g window-status-current-style "fg=#89b4fa,bold" +setw -g window-status-style "fg=#6c7086" +setw -g window-status-format " #I:#W " +setw -g window-status-current-format " #I:#W " +setw -g window-status-separator " " + +# ── Notifications ──────────────────────────────────────────────────────────── +set -g bell-action any +setw -g monitor-activity on +set -g visual-activity off diff --git a/.config/fish/functions/dotfiles.fish b/.config/fish/functions/dotfiles.fish new file mode 100644 index 0000000..6760ea2 --- /dev/null +++ b/.config/fish/functions/dotfiles.fish @@ -0,0 +1,752 @@ +# ============================================================================= +# functions/dotfiles.fish — Dotfiles Manager +# ============================================================================= +# +# This file is auto-loaded by Fish when you first run `dotfiles`. It defines +# the `dotfiles` command and all its subcommands. The design is deliberately +# monolithic (one file) so that the whole system is easy to fork, audit, and +# copy around — yet each subcommand is a separate named function for clarity. +# +# ── How it works ──────────────────────────────────────────────────────────── +# +# 1. Template files live in $DOTFILES_DIR (default: ~/.config/fish/dotfiles/). +# Each template is named after the dotfile it manages, e.g.: +# dotfiles/tmux.conf → installed to ~/.tmux.conf +# dotfiles/gitconfig → installed to ~/.gitconfig +# +# 2. When you run `dotfiles init`, templates are copied to $HOME with a +# leading dot prepended. Existing files are backed up. +# +# 3. When you run `dotfiles update`, the template is re-copied (with backup). +# +# 4. `dotfiles sync` also pulls the latest templates from the remote repo +# before re-applying them. +# +# 5. `dotfiles status` compares the installed file against its template using +# a checksum so you can see at a glance what's drifted. +# +# ── Convention for template filenames ────────────────────────────────────── +# +# Template name Target file Typical use +# ────────────────────────────────────────────────────────────────── +# tmux.conf ~/.tmux.conf tmux terminal multiplexer +# gitconfig ~/.gitconfig Git version control +# gitignore ~/.gitignore Global Git ignore rules +# starship.toml ~/.config/starship.toml Starship prompt +# bashrc ~/.bashrc Bash (when Fish is absent) +# +# To add support for a new file, simply drop a template in $DOTFILES_DIR +# and run `dotfiles init `. That's it — no registration needed. +# +# ============================================================================= + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles — main dispatcher +# ══════════════════════════════════════════════════════════════════════════════ +# +# Usage: dotfiles [arguments ...] +# +# Run `dotfiles help` (or just `dotfiles`) for a full list of subcommands. +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles -d "Manage dotfiles and self-update system" + # ── No argument (or explicit help) → show help ───────────────────────── + set -l cmd $argv[1] + switch "$cmd" + case "" help --help -h + dotfiles_help + + case version --version -v + dotfiles_version + + case status + dotfiles_status + + case init + # `dotfiles init` → init all + # `dotfiles init foo bar` → init only foo and bar + dotfiles_init $argv[2..] + + case update + # Same argument pattern as init. + dotfiles_update $argv[2..] + + case edit + if set -q argv[2] + dotfiles_edit $argv[2] + else + echo "Usage: dotfiles edit " + echo "Example: dotfiles edit tmux" + return 1 + end + + case repo + # `dotfiles repo` → show current URL + # `dotfiles repo ` → set URL + dotfiles_repo $argv[2] + + case sync + dotfiles_sync + + case publish + dotfiles_publish + + case '*' + echo "dotfiles: unknown command '$cmd'" + echo "Run 'dotfiles help' for usage." + return 1 + end +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_help +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_help -d "Show the dotfiles help text" + echo "Usage: dotfiles [arguments]" + echo "" + echo "Commands:" + echo " help Show this help message" + echo " version Display version and system info" + echo " status [name ...] Show checksum diff for dotfiles" + echo " init [name ...] Create dotfiles from templates" + echo " update [name ...] Re-apply templates (with backup)" + echo " edit Open ~/. in \$EDITOR" + echo " repo [url] Show or set the remote repo URL" + echo " sync Git-pull and re-apply everything" + echo " publish Push local changes to remote repo" + echo " with a version bump" + echo "" + echo "Available managed files:" + _dotfiles_list_managed | string collect + echo "" + echo "Examples:" + echo " dotfiles init Create ~/.tmux.conf, ~/.gitconfig, …" + echo " dotfiles init gitconfig Create only ~/.gitconfig" + echo " dotfiles status Check every managed file for drift" + echo " dotfiles sync Pull + re-apply everything" + echo " dotfiles publish Bump version, commit, and push" + echo "" + echo "The DOTFILES_REPO_URL environment variable controls where sync" + echo "and publish operate. Set it with: dotfiles repo " +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_version +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_version -d "Show version information" + echo "dotfiles v$DOTFILES_VERSION" + + if test -n "$DOTFILES_REPO_URL" + echo "Repository : $DOTFILES_REPO_URL ($DOTFILES_REPO_BRANCH)" + else + echo "Repository : (none set — run 'dotfiles repo ' to enable sync)" + end + + echo "Shell : Fish $FISH_VERSION" + echo "Config dir : $__fish_config_dir" + echo "Templates : $DOTFILES_DIR" + echo "Backups : $DOTFILES_BACKUP_DIR" +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_status +# ══════════════════════════════════════════════════════════════════════════════ +# +# Compares each installed dotfile against its template using MD5 checksums. +# Shows three states: +# ✓ up to date — template and installed file are identical +# ✗ drifted — installed file differs from the template +# - not installed — template exists but target has not been created +# +# If you give one or more names (e.g. `dotfiles status tmux`) only those +# are checked. +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_status -d "Show checksum diff for managed dotfiles" + set -l targets (_dotfiles_resolve_targets $argv) + + echo "Checking managed dotfiles ..." + echo "" + + set -l any_drift 0 + + for name in $targets + set -l tmpl (_dotfiles_template_path "$name") + set -l dest (_dotfiles_target_path "$name") + + if not test -f "$tmpl" + echo " - $name (no template at $tmpl)" + continue + end + + if not test -f "$dest" + echo " - $name (not installed — run 'dotfiles init $name')" + continue + end + + # Compare checksums + set -l tmpl_md5 (md5sum "$tmpl" | cut -d' ' -f1) + set -l dest_md5 (md5sum "$dest" | cut -d' ' -f1) + + if test "$tmpl_md5" = "$dest_md5" + echo " ✓ $name (up to date)" + else + echo " ✗ $name (drifted — run 'dotfiles update $name')" + set any_drift 1 + end + end + + echo "" + if test "$any_drift" -eq 0 + echo "All managed files are up to date." + else + echo "Some files have drifted. Run 'dotfiles update' to re-apply templates." + end +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_init — create dotfiles from templates +# ══════════════════════════════════════════════════════════════════════════════ +# +# Creates ~/. from the template at $DOTFILES_DIR/. +# Existing files are backed up before being overwritten. +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_init -d "Create dotfiles from templates" + set -l targets (_dotfiles_resolve_targets $argv) + + if set -q targets[1] + echo "Initialising dotfiles ..." + else + echo "No managed files found in $DOTFILES_DIR" + echo "Add a template file there and run 'dotfiles init '." + return 0 + end + + set -l did_anything 0 + + for name in $targets + set -l tmpl (_dotfiles_template_path "$name") + set -l dest (_dotfiles_target_path "$name") + + if not test -f "$tmpl" + echo " skipping $name — no template at $tmpl" + continue + end + + # Backup any existing file + if test -f "$dest" + set -l backup "$DOTFILES_BACKUP_DIR/$name" + set -l backup_parent (dirname "$backup") + if not test -d "$backup_parent" + mkdir -p "$backup_parent" + end + command cp "$dest" "$backup" + echo " backed up $dest → $backup" + end + + # Create parent directory if needed (e.g. ~/.config/starship.toml) + set -l parent (dirname "$dest") + if not test -d "$parent" + mkdir -p "$parent" + end + + command cp "$tmpl" "$dest" + echo " created $dest" + set did_anything 1 + end + + if test "$did_anything" -eq 0 + echo "Nothing was created." + end +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_update — re-apply templates (with backup) +# ══════════════════════════════════════════════════════════════════════════════ +# +# Identical to `dotfiles init` except it prints "updated" rather than +# "created". This distinction is useful when scripting or checking logs. +# (Technically the behaviour is the same — backup + copy — but the user +# intent is different.) +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_update -d "Re-apply template to existing dotfile" + set -l targets (_dotfiles_resolve_targets $argv) + + if not set -q targets[1] + echo "No managed files found in $DOTFILES_DIR" + return 0 + end + + for name in $targets + set -l tmpl (_dotfiles_template_path "$name") + set -l dest (_dotfiles_target_path "$name") + + if not test -f "$tmpl" + echo " skipping $name — no template at $tmpl" + continue + end + + # Backup existing file + if test -f "$dest" + set -l backup "$DOTFILES_BACKUP_DIR/$name" + set -l backup_parent (dirname "$backup") + if not test -d "$backup_parent" + mkdir -p "$backup_parent" + end + command cp "$dest" "$backup" + echo " backed up $dest → $backup" + end + + set -l parent (dirname "$dest") + if not test -d "$parent" + mkdir -p "$parent" + end + + command cp "$tmpl" "$dest" + echo " updated $dest" + end +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_edit — open a dotfile in $EDITOR +# ══════════════════════════════════════════════════════════════════════════════ +# +# Opens ~/. for editing. If the file doesn't exist yet, you can +# initialise it from the template first with `dotfiles init `. +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_edit -d "Open a dotfile for editing" + set -l name $argv[1] + set -l dest (_dotfiles_target_path "$name") + + if not test -f "$dest" + echo "File $dest does not exist." + echo "Create it first: dotfiles init $name" + return 1 + end + + # $EDITOR is set in config.fish; fall back to nano if unset + $EDITOR "$dest" +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_repo — show or set the remote repository URL +# ══════════════════════════════════════════════════════════════════════════════ +# +# The URL is stored persistently in a small text file under the backup dir +# so it survives shell restarts. It is also loaded into the environment +# variable DOTFILES_REPO_URL by config.fish if the file exists. +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_repo -d "Show or set the remote repository URL" + set -l repo_file "$DOTFILES_BACKUP_DIR/.repo_url" + set -l url $argv[1] + + if set -q url[1] + # ── Set the URL ──────────────────────────────────────────────────── + echo "$url" >"$repo_file" + set -gx DOTFILES_REPO_URL "$url" + echo "Repository URL set to: $url" + else + # ── Show the current URL ────────────────────────────────────────── + if test -f "$repo_file" + set -l stored_url (command cat "$repo_file") + echo "$stored_url" + else if test -n "$DOTFILES_REPO_URL" + echo "$DOTFILES_REPO_URL" + else + echo "(no repository configured)" + echo "" + echo "Run: dotfiles repo https://github.com/yourname/dotfiles" + end + end +end + +# Load persisted repo URL into the environment on shell start. +# This runs when the function file is sourced, so it's evaluated once per +# session (before the first `dotfiles` invocation). +set -l repo_file "$DOTFILES_BACKUP_DIR/.repo_url" +if test -f "$repo_file" + set -gx DOTFILES_REPO_URL (command cat "$repo_file") +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_sync — full sync: git pull + update all dotfiles +# ══════════════════════════════════════════════════════════════════════════════ +# +# This is the "self-update" mechanism. It: +# +# 1. Checks that DOTFILES_REPO_URL is set +# 2. Clones the repo into a temporary directory (or pulls if already cloned) +# 3. Copies the templates from the cloned repo into DOTFILES_DIR +# 4. Re-applies every template to its target location +# +# The config.fish itself is NOT overwritten — that would require a shell +# restart to take effect. Instead, sync copies the *templates* from the +# upstream repo, then re-applies them. To update the framework logic +# itself (the *.fish files), run the update command shown after sync. +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_sync -d "Pull remote updates and re-apply all dotfiles" + # ── Step 1: Ensure we have a remote URL ──────────────────────────────── + if test -z "$DOTFILES_REPO_URL" + echo "No repository URL configured." + echo "Set one with: dotfiles repo https://github.com/yourname/dotfiles" + return 1 + end + + echo "Syncing from $DOTFILES_REPO_URL ($DOTFILES_REPO_BRANCH) ..." + echo "" + + # ── Step 2: Clone/pull into a temporary directory ────────────────────── + set -l tmpdir (mktemp -d) + echo "Cloning into $tmpdir ..." + + if command git clone --branch "$DOTFILES_REPO_BRANCH" --depth 1 "$DOTFILES_REPO_URL" "$tmpdir" + echo "Clone successful." + else + echo "Error: failed to clone repository." + command rm -rf "$tmpdir" + return 1 + end + + # ── Step 3: Copy upstream templates into our local DOTFILES_DIR ──────── + set -l upstream_templates "$tmpdir/.config/fish/dotfiles" + if test -d "$upstream_templates" + echo "" + echo "Updating templates from upstream ..." + # Copy recursively to handle subdirectory templates (e.g. config/). + # We use cp -r and then clean up the destination to only keep files. + command cp -r "$upstream_templates/." "$DOTFILES_DIR/" + echo " templates synced from upstream" + else + echo "Warning: no dotfiles/ directory found in the upstream repo." + echo "Expected at: $upstream_templates" + end + + # ── Step 4: Re-apply all templates ──────────────────────────────────── + echo "" + echo "Re-applying dotfiles ..." + dotfiles_update + + # ── Step 5: Clean up ─────────────────────────────────────────────────── + command rm -rf "$tmpdir" + echo "" + echo "Sync complete." + echo "" + echo "NOTE: framework updates (config.fish, functions/) require a manual" + echo "copy and shell restart. To apply them:" + echo "" + echo " git clone $DOTFILES_REPO_URL /tmp/dotfiles-update" + echo " cp -r /tmp/dotfiles-update/.config/fish/config.fish ~/.config/fish/" + echo " cp -r /tmp/dotfiles-update/.config/fish/functions/ ~/.config/fish/" + echo " exec fish" +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# dotfiles_publish — push local changes to the remote repo +# ══════════════════════════════════════════════════════════════════════════════ +# +# This is the counterpart to `dotfiles sync`. Instead of pulling remote +# templates down, it pushes your local config, functions, templates, and +# conf.d/ files back up to the remote repository. +# +# Workflow: +# 1. Clones the remote repo into a temporary directory +# 2. Copies the current local state on top of it: +# config.fish, functions/, dotfiles/, conf.d/ +# 3. Prompts for a version number (auto-suggested minor bump, or custom) +# 4. Updates DOTFILES_VERSION in both the local and cloned config.fish +# 5. Commits (with a message like "dotfiles v1.1.0") +# 6. Pushes to the remote +# 7. Cleans up the temporary clone +# +# After publish, run `dotfiles sync` on any other machine to pick up the +# changes. +# ══════════════════════════════════════════════════════════════════════════════ + +function dotfiles_publish -d "Push local changes to the remote dotfiles repo" + # ── Step 1: Ensure we have a remote URL ──────────────────────────────── + if test -z "$DOTFILES_REPO_URL" + echo "No repository URL configured." + echo "Set one with: dotfiles repo https://github.com/yourname/dotfiles" + return 1 + end + + echo "Publishing to $DOTFILES_REPO_URL ($DOTFILES_REPO_BRANCH) ..." + + # ── Step 2: Clone (or init) the repo ───────────────────────────────────── + set -l tmpdir (mktemp -d) + echo "Preparing $tmpdir ..." + + if command git clone --branch "$DOTFILES_REPO_BRANCH" "$DOTFILES_REPO_URL" "$tmpdir" 2>/dev/null + echo "Clone successful." + else + # Remote may not exist yet, or it exists but has no commits. + # Initialise a fresh repo locally and wire up the remote. + echo "Remote branch not found — initialising new repository." + command git init "$tmpdir" + command git -C "$tmpdir" remote add origin "$DOTFILES_REPO_URL" + echo "Local repo ready (origin → $DOTFILES_REPO_URL)." + end + + # Make sure the expected branch exists (create it if it doesn't). + if not command git -C "$tmpdir" rev-parse --verify "$DOTFILES_REPO_BRANCH" 2>/dev/null + command git -C "$tmpdir" checkout -b "$DOTFILES_REPO_BRANCH" + echo "Created branch '$DOTFILES_REPO_BRANCH'." + else + command git -C "$tmpdir" checkout "$DOTFILES_REPO_BRANCH" + end + + # ── Step 3: Copy local state into the clone ─────────────────────────── + # These are the files and directories that make up the dotfiles repo. + set -l local_config "$__fish_config_dir/config.fish" + set -l local_funcs "$__fish_config_dir/functions/" + set -l local_tmpl "$DOTFILES_DIR/" + set -l local_confd "$__fish_config_dir/conf.d/" + + set -l repo_config "$tmpdir/.config/fish/config.fish" + set -l repo_funcs "$tmpdir/.config/fish/functions/" + set -l repo_tmpl "$tmpdir/.config/fish/dotfiles/" + set -l repo_confd "$tmpdir/.config/fish/conf.d/" + + # Ensure target directories exist + for d in (dirname "$repo_config") "$repo_funcs" "$repo_tmpl" "$repo_confd" + mkdir -p "$d" + end + + echo "" + echo "Copying local state into clone ..." + command cp "$local_config" "$repo_config" + echo " config.fish" + command cp -r "$local_funcs." "$repo_funcs" + echo " functions/" + command cp -r "$local_tmpl." "$repo_tmpl" + echo " dotfiles/" + command cp -r "$local_confd." "$repo_confd" + echo " conf.d/" + + # ── Step 4: Prompt for version ───────────────────────────────────────── + echo "" + set -l new_version (_dotfiles_prompt_version "$DOTFILES_VERSION") + + # ── Step 5: Update version in both local and cloned config.fish ─────── + # The version line looks like: set -g DOTFILES_VERSION "X.Y.Z" + set -l version_line 'set -g DOTFILES_VERSION "'"$new_version"'"' + + # Update local config.fish + sed -i 's/^set -g DOTFILES_VERSION ".*"$/'"$version_line"'/' "$local_config" + echo "Updated local $local_config → v$new_version" + + # Update cloned config.fish + sed -i 's/^set -g DOTFILES_VERSION ".*"$/'"$version_line"'/' "$repo_config" + + # Set the session variable so subsequent commands in this function + # and future shell sessions see the new version. + set -g DOTFILES_VERSION "$new_version" + + # ── Step 6: Commit and push from the clone ───────────────────────────── + echo "" + command git -C "$tmpdir" add -A + + # Check if there's anything to commit + if not command git -C "$tmpdir" status --porcelain | grep -q . + echo "Nothing changed — skipping commit and push." + command rm -rf "$tmpdir" + return 0 + end + + echo "Committing and pushing ..." + command git -C "$tmpdir" commit -m "dotfiles v$new_version" + + if command git -C "$tmpdir" push origin "$DOTFILES_REPO_BRANCH" + echo "" + echo "Published v$new_version successfully." + else + echo "" + echo "Warning: push failed. The commit exists locally in $tmpdir." + echo "Check your credentials and remote URL, then push manually:" + echo " cd $tmpdir && git push origin $DOTFILES_REPO_BRANCH" + # Don't clean up on failure so the user can fix and push manually. + return 1 + end + + # ── Step 7: Clean up ─────────────────────────────────────────────────── + command rm -rf "$tmpdir" + echo "" + echo "Done. Run 'dotfiles sync' on other machines to pull v$new_version." +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# _dotfiles_prompt_version — ask user for version, auto-suggest minor bump +# ══════════════════════════════════════════════════════════════════════════════ +# +# Parses the current version (X.Y.Z), suggests X.(Y+1).0, and prompts. +# If the user presses Enter without typing, the suggested bump is used. +# If the user types a custom string (e.g. "2.0.0" or "1.5.0-beta"), that +# is used instead. +# ══════════════════════════════════════════════════════════════════════════════ + +function _dotfiles_prompt_version -d "Prompt for version, defaulting to minor bump" + set -l current $argv[1] + set -l suggested (_dotfiles_bump_minor "$current") + + echo "Current version: v$current" + # Use double quotes so $suggested expands in the prompt string. + read -p "echo \"New version (press Enter for v$suggested): \"" -l custom + + if test -z "$custom" + echo "$suggested" + else + echo "$custom" + end +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# _dotfiles_bump_minor — given X.Y.Z, return X.(Y+1).0 +# ══════════════════════════════════════════════════════════════════════════════ +# +# Works with versions like "1.0.0", "2.15.3", etc. Non-numeric parts +# (e.g. "1.0.0-beta") will cause the math to fail, so we fall back to +# appending ".1" as a best-effort. +# ══════════════════════════════════════════════════════════════════════════════ + +function _dotfiles_bump_minor -d "Increment the minor version component" + set -l v $argv[1] + + # Split on dots: "1.2.3" → major=1, minor=2, patch=3 + set -l parts (string split "." "$v") + set -l major $parts[1] + set -l minor $parts[2] + set -l patch $parts[3] + + # Default to 0 for missing components + set -q minor[1]; or set minor 0 + set -q patch[1]; or set patch 0 + + # Only attempt arithmetic if all three components are plain integers. + # Use string match to check they are digits only. + if string match -rq '^\d+$' "$major" + and string match -rq '^\d+$' "$minor" + and string match -rq '^\d+$' "$patch" + + set -l new_minor (math "$minor + 1") + echo "$major.$new_minor.0" + else + # Non-numeric version — just append a bump suffix. + echo "$v.1" + end +end + + +# ══════════════════════════════════════════════════════════════════════════════ +# Private helper functions (prefixed with _dotfiles_) +# ══════════════════════════════════════════════════════════════════════════════ +# +# These are not intended to be called directly from the command line. +# ══════════════════════════════════════════════════════════════════════════════ + + +# ----------------------------------------------------------------------------- +# _dotfiles_list_managed +# +# Returns a sorted list of template names in $DOTFILES_DIR. +# Used by the help text and by _dotfiles_resolve_targets. +# ----------------------------------------------------------------------------- + +function _dotfiles_list_managed -d "List available dotfile templates" + if not test -d "$DOTFILES_DIR" + return 1 + end + + # Recursively list all regular files relative to DOTFILES_DIR. + # This supports subdirectory-based templates, e.g.: + # dotfiles/tmux.conf → tmux.conf + # dotfiles/config/starship.toml → config/starship.toml + command find "$DOTFILES_DIR" -type f -printf '%P\n' | sort +end + + +# ----------------------------------------------------------------------------- +# _dotfiles_resolve_targets +# +# Converts a list of user-supplied names (e.g. "tmux gitconfig") into the +# full list of targets. If no names are given, falls back to ALL managed +# templates. Unknown names are silently skipped with a warning. +# +# This function is used by init, update, and status. +# ----------------------------------------------------------------------------- + +function _dotfiles_resolve_targets -d "Resolve user-supplied names to template names" + if set -q argv[1] + # User specified specific names — return them as-is. + # Validation happens downstream (_dotfiles_template_path). + printf "%s\n" $argv + else + # No names given — return everything we have templates for. + _dotfiles_list_managed + end +end + + +# ----------------------------------------------------------------------------- +# _dotfiles_template_path +# +# Returns the full filesystem path to the template for a given name. +# Example: _dotfiles_template_path "tmux" → $DOTFILES_DIR/tmux.conf +# ----------------------------------------------------------------------------- + +function _dotfiles_template_path -d "Get the template path for a dotfile name" + echo "$DOTFILES_DIR/$argv[1]" +end + + +# ----------------------------------------------------------------------------- +# _dotfiles_target_path +# +# Returns the full filesystem path where the dotfile should be installed. +# +# Convention: +# - If the template name contains a "/", it's treated as a relative path +# under $HOME (e.g. "config/starship.toml" → ~/.config/starship.toml). +# - Otherwise, the template name is prefixed with a dot and placed directly +# under $HOME (e.g. "tmux" → ~/.tmux). Note: tmux.conf is the *filename*, +# so "tmux.conf" → ~/.tmux.conf. +# +# This means: +# Template name Target +# tmux.conf ~/.tmux.conf +# gitconfig ~/.gitconfig +# config/starship.toml ~/.config/starship.toml +# +# To add a file in a subdirectory of $HOME, simply create a template with +# a "/" in its name: "vim/vimrc" → ~/.vim/vimrc +# ----------------------------------------------------------------------------- + +function _dotfiles_target_path -d "Get the target installation path for a dotfile" + set -l name $argv[1] + + if string match -q "*/*" "$name" + # Name contains a "/" → treat as relative path under $HOME + echo "$HOME/.$name" + else + # Name without slash → place directly in $HOME, prefixed with dot + echo "$HOME/.$name" + end +end diff --git a/.config/fish/functions/gitea_key.fish b/.config/fish/functions/gitea_key.fish new file mode 100644 index 0000000..9d25669 --- /dev/null +++ b/.config/fish/functions/gitea_key.fish @@ -0,0 +1,75 @@ +function gitea_key -d "Generate (or regenerate) an ed25519 SSH key for Gitea and configure ~/.ssh/config" + set -l key_path "$HOME/.ssh/id_ed25519_gitea" + set -l pub_path "$key_path.pub" + set -l config_path "$HOME/.ssh/config" + set -l host "gitssh.toomuchtaco.net" + + # ── Step 1: Handle existing key ───────────────────────────────────────── + if test -f "$key_path" + echo "An SSH key already exists at $key_path" + read -p 'echo "Overwrite it? [y/N] "' -l confirm + if test "$confirm" != "y" -a "$confirm" != "Y" + echo "Aborted." + return 1 + end + echo "" + end + + # ── Step 2: Generate the key ──────────────────────────────────────────── + # -t ed25519: modern, secure, fast elliptic-curve key + # -f: output file + # -N "": empty passphrase (prompts to set one if desired) + ssh-keygen -t ed25519 -f "$key_path" -N "" + echo "" + + # ── Step 3: Update ~/.ssh/config ──────────────────────────────────────── + # If the host block already exists, update the IdentityFile line in place. + # Otherwise, append a new block at the end of the file. + + if grep -q "^Host $host\$" "$config_path" 2>/dev/null + # Host block exists — update the IdentityFile line. + # If an IdentityFile line already exists under this Host, replace it; + # otherwise add one after the HostName line. + if grep -A5 "^Host $host\$" "$config_path" | grep -q "IdentityFile" + sed -i "/^Host $host\$/,/^Host /s|IdentityFile .*|IdentityFile $key_path|" "$config_path" + echo "Updated IdentityFile for Host $host in $config_path" + else + sed -i "/^Host $host\$/,/^Host /s|HostName .*|&\n\tIdentityFile $key_path|" "$config_path" + echo "Added IdentityFile for Host $host in $config_path" + end + else + # No Host block yet — append one. + # printf interprets \t as a literal tab (unlike echo in Fish). + printf "\n# Gitea\nHost %s\n\tHostName %s\n\tUser git\n\tIdentityFile %s\n" \ + "$host" "$host" "$key_path" >>"$config_path" + echo "Appended Host block for $host to $config_path" + end + + # ── Step 4: Set correct permissions ──────────────────────────────────── + chmod 600 "$key_path" + chmod 644 "$pub_path" + chmod 600 "$config_path" + + echo "" + + # ── Step 5: Print the public key ──────────────────────────────────────── + echo "==============================" + echo " Public key (add to Gitea): " + echo "==============================" + echo "" + cat "$pub_path" + echo "" + echo "==============================" + echo "Key copied to clipboard if pbcopy/xclip is available." + if command -v pbcopy &>/dev/null + cat "$pub_path" | pbcopy + echo "(copied to clipboard via pbcopy)" + else if command -v xclip &>/dev/null + cat "$pub_path" | xclip -selection clipboard + echo "(copied to clipboard via xclip)" + else if command -v wl-copy &>/dev/null + cat "$pub_path" | wl-copy + echo "(copied to clipboard via wl-copy)" + end + echo "==============================" +end