diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fc20083 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# My personal dotfiles + +To be used with GNU stow. + +Goal is a clean-as-possible home directory. +As much as possible in standard `.config/`, `.local/` and so on. + +## Shell architecture (multi-shell) + +The config supports **both zsh and bash** from one branch. Pick the login shell +by which package you `stow`; `shell/` is always stowed. (There used to be a +separate `bashed` branch — retired; do not recreate it.) + +Split by *role*: + +- **`shell/`** (shared) — POSIX, shell-agnostic. + - `exports`, `path` — environment. `path` is idempotent (safe to source twice). + - `aliases`, `functions` — interactive bits, POSIX-safe. + - `interactive` — sources `aliases`/`functions`/`extra` + pyenv shims. Loaded + by each shell's own rc (brace expansion OK; only zsh/bash source it). + - `profile` — POSIX **login env only** (no brace expansion, no rc source). + `~/.profile` symlinks to it. +- **`zsh/`** — `~/.zshenv` (sets `ZDOTDIR`+env, runs for *every* zsh), + `$ZDOTDIR/.zshrc` (interactive, sources `shell/interactive`), + `$ZDOTDIR/.zprofile` (login-only, pyenv `--path`). +- **`bash/`** — `~/.bash_profile` (login → sources `~/.bashrc` + pyenv `--path`), + `~/.bashrc` (sources `~/.profile` for env, then `~/.config/bash/rc`), + `~/.config/bash/rc` (interactive, sources `shell/interactive` + bash bits), + `prompt`. + +Invariants worth keeping: +- `ZDOTDIR` belongs in `~/.zshenv` only — it must be set before zsh reads any + rc file, so `exports` is too late. +- Env must reach **non-login interactive** shells: zsh gets it via `.zshenv` + (always runs); bash gets it because `.bashrc` re-sources `~/.profile` + (idempotent). Don't move env loading out of those two spots. +- Prompts are necessarily per-shell (`%n@%M` vs `\u@\h`); keep them split. + +## Known issues / TODO (next session) + +Remaining from the 2026-06-26 review (zsh/bash startup items already fixed): + +1. **Clean-home goal leaks:** + - `psql/rc`: `HISTFILE ~/.psql_history-:DBNAME` writes a dotfile to `$HOME`. + Redirect under `~/.cache`. + - `git/config`: `excludesfile = ~/.gitignore`, `attributesfile = ~/.gitattributes`. + Move to `~/.config/git/{ignore,attributes}` (git reads `ignore` there by default). + +2. **History dirs not created.** `HISTFILE` points at `$XDG_CACHE_HOME/shell/history`, + but zsh/bash won't `mkdir` the parent, so history is silently dropped. Need a + bootstrap/`mkdir -p`. (The python startup script already does its own mkdir.) + +3. **Dead config.** alias `f="$FILE"` — `$FILE` is never set (empty alias); + `functions` file is empty; `git/config` carries macOS-only settings + (`trustctime`, `precomposeunicode`, mac comments) — dead weight on Arch. + `bash/.config/bash/rc` also keeps Homebrew/macOS completion blocks (guarded, + harmless, but droppable). diff --git a/bash/.bash_profile b/bash/.bash_profile new file mode 100644 index 0000000..9015737 --- /dev/null +++ b/bash/.bash_profile @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Bash login shell. Everything a non-login shell gets lives in ~/.bashrc, so +# source it, then add login-only bits. +[ -r ~/.bashrc ] && . ~/.bashrc + +# pyenv shims dir (login only); PYENV_ROOT comes from ~/.config/shell/exports. +command -v pyenv >/dev/null 2>&1 && eval "$(pyenv init --path)" diff --git a/bash/.bashrc b/bash/.bashrc new file mode 100644 index 0000000..8c5ff37 --- /dev/null +++ b/bash/.bashrc @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Loaded by every interactive bash, and by login bash via ~/.bash_profile. +# Source env first (idempotent — covers non-login interactive shells that bash +# would otherwise start without it), then the real interactive config. +[ -r ~/.profile ] && . ~/.profile +[ -r ~/.config/bash/rc ] && . ~/.config/bash/rc diff --git a/bash/.config/bash/prompt b/bash/.config/bash/prompt new file mode 100644 index 0000000..42970f5 --- /dev/null +++ b/bash/.config/bash/prompt @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +# Source: https://github.com/mathiasbynens/dotfiles/ + +# Shell prompt based on the Solarized Dark theme. +# Screenshot: http://i.imgur.com/EkEtphC.png +# Heavily inspired by @necolas’s prompt: https://github.com/necolas/dotfiles +# iTerm → Profiles → Text → use 13pt Monaco with 1.1 vertical spacing. + +# If not running interactively, don't do anything +[[ $- != *i* ]] && return + +if [[ $COLORTERM = gnome-* && $TERM = xterm ]] && infocmp gnome-256color >/dev/null 2>&1; then + export TERM='gnome-256color'; +elif infocmp xterm-256color >/dev/null 2>&1; then + export TERM='xterm-256color'; +fi; + +prompt_git() { + local s=''; + local branchName=''; + + # Check if the current directory is in a Git repository. + git rev-parse --is-inside-work-tree &>/dev/null || return; + + # Check for what branch we’re on. + # Get the short symbolic ref. If HEAD isn’t a symbolic ref, get a + # tracking remote branch or tag. Otherwise, get the + # short SHA for the latest commit, or give up. + branchName="$(git symbolic-ref --quiet --short HEAD 2> /dev/null || \ + git describe --all --exact-match HEAD 2> /dev/null || \ + git rev-parse --short HEAD 2> /dev/null || \ + echo '(unknown)')"; + + # Early exit for Chromium & Blink repo, as the dirty check takes too long. + # Thanks, @paulirish! + # https://github.com/paulirish/dotfiles/blob/dd33151f/.bash_prompt#L110-L123 + repoUrl="$(git config --get remote.origin.url)"; + if grep -q 'chromium/src.git' <<< "${repoUrl}"; then + s+='*'; + else + # Check for uncommitted changes in the index. + if ! $(git diff --quiet --ignore-submodules --cached); then + s+='+'; + fi; + # Check for unstaged changes. + if ! $(git diff-files --quiet --ignore-submodules --); then + s+='!'; + fi; + # Check for untracked files. + if [ -n "$(git ls-files --others --exclude-standard)" ]; then + s+='?'; + fi; + # Check for stashed files. + if $(git rev-parse --verify refs/stash &>/dev/null); then + s+='$'; + fi; + fi; + + [ -n "${s}" ] && s=" [${s}]"; + + echo -e "${1}${branchName}${2}${s}"; +} + +if tput setaf 1 &> /dev/null; then + tput sgr0; # reset colors + bold=$(tput bold); + reset=$(tput sgr0); + # Solarized colors, taken from http://git.io/solarized-colors. + black=$(tput setaf 0); + blue=$(tput setaf 33); + cyan=$(tput setaf 37); + green=$(tput setaf 64); + orange=$(tput setaf 166); + purple=$(tput setaf 125); + red=$(tput setaf 124); + violet=$(tput setaf 61); + white=$(tput setaf 15); + yellow=$(tput setaf 136); +else + bold=''; + reset="\e[0m"; + black="\e[1;30m"; + blue="\e[1;34m"; + cyan="\e[1;36m"; + green="\e[1;32m"; + orange="\e[1;33m"; + purple="\e[1;35m"; + red="\e[1;31m"; + violet="\e[1;35m"; + white="\e[1;37m"; + yellow="\e[1;33m"; +fi; + +# Highlight the user name when logged in as root. +if [[ "${USER}" == "root" ]]; then + userStyle="${red}"; +else + userStyle="${orange}"; +fi; + +# Highlight the hostname when connected via SSH. +if [[ "${SSH_TTY}" ]]; then + hostStyle="${bold}${red}"; +else + hostStyle="${yellow}"; +fi; + +# Set the terminal title and prompt. +PS1="\[\033]0;\W\007\]"; # working directory base name +PS1+="\[${bold}\]\n"; # newline +PS1+="\[${userStyle}\]\u"; # username +PS1+="\[${white}\] at "; +PS1+="\[${hostStyle}\]\h"; # host +PS1+="\[${white}\] in "; +PS1+="\[${green}\]\w"; # working directory full path +PS1+="\$(prompt_git \"\[${white}\] on \[${violet}\]\" \"\[${blue}\]\")"; # Git repository details +PS1+="\n"; +PS1+="\[${white}\]\$ \[${reset}\]"; # `$` (and reset color) +export PS1; + +PS2="\[${yellow}\]→ \[${reset}\]"; +export PS2; diff --git a/bash/.config/bash/rc b/bash/.config/bash/rc new file mode 100644 index 0000000..ebe09d2 --- /dev/null +++ b/bash/.config/bash/rc @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Mostly copied from: https://github.com/mathiasbynens/dotfiles/ + +# Shared interactive config (aliases, functions, pyenv shims): +[ -r ~/.config/shell/interactive ] && source ~/.config/shell/interactive + +# Bash-specific: +[ -f ~/.config/bash/prompt ] && source ~/.config/bash/prompt + +# Case-insensitive globbing (used in pathname expansion) +shopt -s nocaseglob; + +# Append to the Bash history file, rather than overwriting it +shopt -s histappend; + +# Autocorrect typos in path names when using `cd` +shopt -s cdspell; + +# Enable some Bash 4 features when possible: +# * `autocd`, e.g. `**/qux` will enter `./foo/bar/baz/qux` +# * Recursive globbing, e.g. `echo **/*.txt` +for option in autocd globstar; do + shopt -s "$option" 2> /dev/null; +done; + +# Add tab completion for many Bash commands +if which brew &> /dev/null && [ -r "$(brew --prefix)/etc/profile.d/bash_completion.sh" ]; then + # Ensure existing Homebrew v1 completions continue to work + export BASH_COMPLETION_COMPAT_DIR="$(brew --prefix)/etc/bash_completion.d"; + source "$(brew --prefix)/etc/profile.d/bash_completion.sh"; +elif [ -f /etc/bash_completion ]; then + source /etc/bash_completion; +fi; + +# Enable tab completion for `g` by marking it as an alias for `git` +if type _git &> /dev/null; then + complete -o default -o nospace -F _git g; +fi; + +# Add tab completion for SSH hostnames based on ~/.ssh/config, ignoring wildcards +[ -e "$HOME/.ssh/config" ] && complete -o "default" -o "nospace" -W "$(grep "^Host" ~/.ssh/config | grep -v "[?*]" | cut -d " " -f2- | tr ' ' '\n')" scp sftp ssh; + +# Add tab completion for `defaults read|write NSGlobalDomain` +# You could just use `-g` instead, but I like being explicit +complete -W "NSGlobalDomain" defaults; diff --git a/shell/.config/shell/exports b/shell/.config/shell/exports index 5585ca2..00e792b 100644 --- a/shell/.config/shell/exports +++ b/shell/.config/shell/exports @@ -33,5 +33,5 @@ export PYTHON_HISTFILE="${XDG_CACHE_HOME:-$HOME/.cache}/python/history" # Wget config file export WGETRC="${XDG_CONFIG_HOME:-$HOME/.config}/wget/rc" -# ZSH directory -export ZDOTDIR="${XDG_CONFIG_HOME:-$HOME/.config}/zsh" +# NOTE: ZDOTDIR lives in ~/.zshenv, not here. It must be set before zsh reads +# any rc file, and this file is sourced too late for that. diff --git a/shell/.config/shell/interactive b/shell/.config/shell/interactive new file mode 100644 index 0000000..e6f2b8c --- /dev/null +++ b/shell/.config/shell/interactive @@ -0,0 +1,12 @@ +#!/bin/sh + +# Shared interactive shell config. Sourced by both zsh ($ZDOTDIR/.zshrc) and +# bash (~/.config/bash/rc). Only ever loaded by zsh/bash, so brace expansion is +# fine here (unlike the POSIX `profile`). +for _file in "${XDG_CONFIG_HOME:-$HOME/.config}"/shell/{aliases,functions,extra}; do + [ -r "$_file" ] && . "$_file" +done +unset _file + +# pyenv interactive shims (pyenv init - autodetects the running shell): +command -v pyenv >/dev/null 2>&1 && eval "$(pyenv init -)" diff --git a/shell/.config/shell/path b/shell/.config/shell/path index 6f6925f..66767c2 100644 --- a/shell/.config/shell/path +++ b/shell/.config/shell/path @@ -1,4 +1,8 @@ #!/bin/sh -# Adds `~/.local/bin` to $PATH -export PATH="$PATH:${$(find ~/.local/bin -type d -printf %p:)%%:}" +# Prepend ~/.local/bin to $PATH (POSIX, idempotent — safe to source twice). +case ":$PATH:" in + *":$HOME/.local/bin:"*) ;; + *) [ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH" ;; +esac +export PATH diff --git a/shell/.config/shell/profile b/shell/.config/shell/profile index 8fa8105..5a113c0 100644 --- a/shell/.config/shell/profile +++ b/shell/.config/shell/profile @@ -1,17 +1,13 @@ #!/bin/sh -# Load the shell dotfiles, and then some: -# * ~/.path can be used to extend `$PATH`. -# * ~/.extra can be used for other settings you don’t want to commit. -for file in ~/.config/shell/{path,exports,aliases,functions,extra}; do - [ -r "$file" ] && [ -f "$file" ] && source "$file"; -done; -unset file; - -source ~/.config/zsh/rc - -# Setup pyenv, if it is installed: -if [ -x "$(command -v pyenv)" ]; then - eval "$(pyenv init --path)" - eval "$(pyenv init -)" -fi +# POSIX login environment. Sourced directly by a /bin/sh login (display +# managers, plain sh) and indirectly by bash via ~/.bashrc. Env only — no +# brace expansion (dash doesn't expand it), no shell-specific rc. +# +# Interactive config (aliases, functions, prompt) is loaded by each shell's +# own rc via ~/.config/shell/interactive, not here. +for _name in path exports; do + _file="${XDG_CONFIG_HOME:-$HOME/.config}/shell/$_name" + [ -r "$_file" ] && . "$_file" +done +unset _name _file diff --git a/shell/.zprofile b/shell/.zprofile deleted file mode 120000 index 8486fca..0000000 --- a/shell/.zprofile +++ /dev/null @@ -1 +0,0 @@ -.config/shell/profile \ No newline at end of file diff --git a/zsh/.config/zsh/.zprofile b/zsh/.config/zsh/.zprofile new file mode 100644 index 0000000..d1bff27 --- /dev/null +++ b/zsh/.config/zsh/.zprofile @@ -0,0 +1,5 @@ +#!/bin/sh + +# zsh login shell only. PATH-level init that belongs to login (pyenv shims dir). +# PYENV_ROOT is exported earlier in ~/.config/shell/exports. +command -v pyenv >/dev/null 2>&1 && eval "$(pyenv init --path)" diff --git a/zsh/.config/zsh/rc b/zsh/.config/zsh/.zshrc similarity index 92% rename from zsh/.config/zsh/rc rename to zsh/.config/zsh/.zshrc index f4a765e..7fa9429 100644 --- a/zsh/.config/zsh/rc +++ b/zsh/.config/zsh/.zshrc @@ -1,6 +1,10 @@ # Luke's config for the Zoomer Shell # Source (mainly): https://github.com/LukeSmithxyz/voidrice/ +# Shared interactive config (aliases, functions, pyenv shims): +[ -r "${XDG_CONFIG_HOME:-$HOME/.config}/shell/interactive" ] && \ + . "${XDG_CONFIG_HOME:-$HOME/.config}/shell/interactive" + # Enable colors and change prompt: autoload -U colors && colors # Load colors PS1="%B%{$fg[red]%}[%{$fg[yellow]%}%n%{$fg[green]%}@%{$fg[blue]%}%M %{$fg[magenta]%}%~%{$fg[red]%}]%{$reset_color%}$%b " diff --git a/zsh/.zshenv b/zsh/.zshenv new file mode 100644 index 0000000..2b40ab8 --- /dev/null +++ b/zsh/.zshenv @@ -0,0 +1,13 @@ +#!/bin/sh + +# Read first by zsh for EVERY shell (login or not), so this is the only place +# ZDOTDIR can be set early enough to redirect where zsh looks for its rc files. +export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" +export ZDOTDIR="$XDG_CONFIG_HOME/zsh" + +# Shared POSIX environment (idempotent): +for _name in path exports; do + _file="$XDG_CONFIG_HOME/shell/$_name" + [ -r "$_file" ] && . "$_file" +done +unset _name _file