# shellcheck shell=bash # profiling helpers __profile_enabled=false __timestamp() { # date +%s perl -MTime::HiRes=time -e 'printf "%d\n", time * 1000' } if [ $__profile_enabled = true ]; then __previous_timestamp=$(__timestamp) fi _profile() { if [ $__profile_enabled = true ]; then _new_timestamp=$(__timestamp) _diff=$((_new_timestamp - __previous_timestamp)) echo "$_diff $1" __previous_timestamp=$_new_timestamp fi } _profile entry # The INTERACTIVE layer. # # Plain environment (PATH, XDG, locale, tool homes) lives in config/env, which # is POSIX sh and is loaded by *every* shell — see its header. This file holds # only what must NOT run in every shell: aliases, history, terminal setup, and # anything expensive (theme, jenv, and whatever profile.d brings in). # # Reached via ~/.zshrc (interactive zsh) and ~/.profile (login bash). Sourcing # config/env here too, because a login bash reaches ~/.profile without ever # passing through ~/.zshenv. It is idempotent, so the double load is harmless. if [ -r "$HOME/.env" ]; then # shellcheck source=/dev/null . "$HOME/.env" else # No ~/.env yet. A pull that does not run link.sh (by hand, or a sync without # --link) can leave this file updated and the ~/.env symlink still absent # - which would silently cost it PATH, XDG, locale and EDITOR. Fall back to # the copy next to this file, found through the ~/.profile symlink that got # us here. Harmless once link.sh has run. __dotfiles_self=$(readlink "$HOME/.profile" 2>/dev/null) if [ -n "$__dotfiles_self" ] && [ -r "${__dotfiles_self%/*}/env" ]; then # shellcheck source=/dev/null . "${__dotfiles_self%/*}/env" fi unset __dotfiles_self fi _profile env # Everything below this point is bash/zsh syntax, and this file is not read # only by bash and zsh: Ubuntu's /bin/sh is dash, and `sh -l` sources # ~/.profile. dash cannot parse `[[ ]]` (noisy but survivable) and refuses a # function named `exit`, which is a POSIX special builtin - that one is a fatal # parse error, so dash dies here and never runs the command it was given. That # silently broke every `sh -lc '...'` on every Linux host; it surfaced as a # background daemon spawned that way failing before it could even create the # log file its error message pointed at. macOS is unaffected: its /bin/sh is # bash, so BASH_VERSION is set and the rest of the file still runs there. # # config/env above is POSIX and has already been sourced, so a plain sh keeps # PATH, XDG, locale and EDITOR. It just stops before the interactive layer, # which it had no use for anyway. if [ -z "${BASH_VERSION-}" ] && [ -z "${ZSH_VERSION-}" ]; then return 0 2>/dev/null || exit 0 fi # Marks that this file is already running, so ~/.bashrc (which we may source at # the very end, for login bash) doesn't source us straight back. See the header # of config/bashrc for the full two-way handshake. __dotfiles_profile=1 if [[ "$(uname)" == "Darwin" ]] then # Sometimes, when invoked via Karabiner, the HOME variable # will not be defined, even with a login shell : "${HOME:="/Users/$(whoami)"}" elif [[ "$(uname)" == "Linux" ]] then if command -v wl-copy >/dev/null && command -v wl-paste >/dev/null; then alias pbcopy='wl-copy' alias pbpaste='wl-paste' else alias pbcopy='xclip -selection clipboard' alias pbpaste='xclip -selection clipboard -o' fi fi _profile clipboard_tools # ripgrep # Remind to use rg instead of ag # ag() { # >&2 echo "ERROR: You decided to switch to 'rg' (ripgrep)" # return 1 # # command ag "$@" # } # if [[ -r "$HOME/.config/ripgrep/config" ]]; then # export RIPGREP_CONFIG_PATH="$HOME/.config/ripgrep/config" # fi # XDG_{CONFIG,CACHE,DATA}_HOME are set in config/env. # Set history file location and size. SHELL_NAME="$(basename "$SHELL")" SHELL_CACHE="$XDG_CACHE_HOME/$SHELL_NAME" mkdir -p "$SHELL_CACHE" HISTFILE="$SHELL_CACHE/history" HISTSIZE=1000000 # SAVEHIST is read by zsh itself, which shellcheck can't lint for. # shellcheck disable=SC2034 SAVEHIST=1000000 exists() { command -v "$1" >/dev/null 2>&1; } # Check if command exists (is in path). lspath() { echo "$PATH" | tr ':' '\n'; } # list PATH entries pathadd() { [[ -d "$1" ]] && PATH="$1"$(echo ":$PATH:" | sed "s|:$1:|:|" | sed 's|:$||'); } pathapp() { [[ -d "$1" ]] && PATH=$(echo ":$PATH:" | sed "s|:$1:|:|" | sed 's|^:||')"$1"; } # GPG GPG_TTY=$(tty) export GPG_TTY pbsign() { pbpaste | gpg --clearsign | pbcopy; } pbverify() { pbpaste | gpg --verify; } # Show processes listening on TCP sockets listening() { lsof -n -P -i4TCP -sTCP:LISTEN \ | awk '{ print $9,$1,$2 }' \ | awk -F '\:' '{ print $2 }' \ | grep -E -v '^$' \ | column -t \ | sort -n } # https://sites.google.com/site/jimmyxu101/testing/use-tcpdump-to-monitor-http-traffic httpdump() { sudo tcpdump -A -i "${2:-lo0}" -s 0 -q "tcp port $1 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)" } alias clc="fc -ln -1 | sed -e 's/\\\\n/\\n/g' -e 's/\\\\t/\\t/g' | pbcopy" # bash # DISABLE_AUTO_TITLE, the core PATH entries and the less/PAGER settings are all # in config/env. # use bash like vi ! # set -o vi _profile bash # when using sudo, use alias expansion (otherwise sudo ignores your aliases) alias sudo='sudo ' # color export CLICOLOR=1 THEME_COLORS="$(theme --colors)" export THEME_COLORS if [ "$(theme --mode)" = "light" ]; then export LSCOLORS=GxFxCxDxBxegedabagaced else export LSCOLORS=GxFxBxDxCxegedabagacad fi if exists bat ; then # best for light: Coldark-Cold, Github, Monokai Extended Light, OneHalfLight, gruvbox-light # best for dark: 1337, Coldark-Dark, Monokai Extended, OneHalfDark, gruvbox-dark # config file in $XDG_CONFIG_HOME/bat/config takes precedence to this env var export BAT_THEME='ansi' # works well with both light and dark themes export BAT_STYLE='plain' export MANPAGER="sh -c 'col -bx | bat -l man -p'" export FZF_COMPLETION_OPTS='--preview '\''bat --color=always --style=numbers --line-range=:500 {}'\' fi if exists lsd then alias ls='lsd' fi _profile color # VISUAL/EDITOR and the node environment are set in config/env. # # ruby # _profile ruby-start # export RBENV_ROOT="${RBENV_ROOT:-"$XDG_CACHE_HOME/rbenv"}" # Set rbenv location. # activate-rbenv() { # if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi # } # activate-rbenv # if [ -d "$HOME/.rbenv/bin" ] ; then # pathapp "$HOME/.rbenv/bin" # eval "$(rbenv init -)" # fi # # add user installs bin to path # if which ruby >/dev/null && which gem >/dev/null; then # pathapp "$(ruby -rubygems -e 'puts Gem.user_dir')/bin" # fi # _profile ruby-done # python # export VIRTUAL_ENV_DISABLE_PROMPT=1 # Add the virtualenv prompt myself. # LANG/LC_* are set in config/env — mosh needs a UTF-8 locale in shells that # never reach this file. # If you want to use pyenv # if [ -x "$(command -v pyenv)" ] ; then # export PYENV_ROOT="$XDG_DATA_HOME/pyenv" # pathadd "$PYENV_ROOT/bin" # eval "$(pyenv init --path)" # fi # Instead of virtualenvwrapper's: # mkvirtualenv -p # I prefer to use: # virtualenv --python= .venv # If you want to use virtualenvwrapper, uncomment this: # if [ -r "$HOME/.local/pipx/venvs" ] ; then # export WORKON_HOME="$XDG_DATA_HOME/virtualenvs" # export VIRTUALENVWRAPPER_PYTHON="$HOME/.local/pipx/venvs/virtualenvwrapper/bin/python3.9" # source "$HOME/.local/bin/virtualenvwrapper.sh" # fi export PYENV_VIRTUALENV_DISABLE_PROMPT=1 # The Python user bin is on PATH via config/env. _profile python # go — GOPATH and the go bin dirs are in config/env. godocserver() { godoc "-http=localhost:6060"; } _profile go # rust — CARGO_HOME/RUSTUP_HOME and the cargo bin dir are in config/env, which # also replaces what rustup's own $CARGO_HOME/env does (it only prepends PATH). # GNU Arm Embedded Toolchain, used to build joeycastillo/Sensor-Watch, is on # PATH via config/env. # java if exists jenv > /dev/null && [[ -z "$JENV_LOADED" ]]; then eval "$(jenv init - --no-rehash)"; # export JAVA_HOME="${JAVA_HOME:-"$(jenv javahome)"}" # export IDEA_JDK=$JAVA_HOME # to make sure JAVA_HOME is set, enable this plugin - just need to run once: # jenv enable-plugin export fi _profile jenv-init # A gradlew / gradle wrapper. # https://github.com/gdubw/gng # This would be nice but it breaks existing IntelliJ projects # export GRADLE_USER_HOME="$XDG_CACHE_HOME/gradle" # Also contains gradle.properties (symlink from XDG_CONFIG_HOME). if exists gw > /dev/null; then alias gradle=gw fi _profile java # ANDROID_HOME and the colima/testcontainers docker vars are in config/env. # Interactive setup linked in from elsewhere, one file per concern, in name # order. This repo ships none: it is where a machine's own layer plugs in, with # whatever needs a secret or names a host -- a token wrapper, API keys. # ~/.localprofile, below, is for hand-written per-host setup and still has the # last word. # # A function only to scope nullglob: zsh aborts on a glob matching nothing, and # bash would hand the loop the literal pattern. The files are sourced inside it, # so their plain assignments, exports and functions are global, but `local` or # `typeset` in them would not be -- and under zsh neither would a `setopt`, # which local_options reverts when the function returns. __dotfiles_source_dir() { if [ -n "${ZSH_VERSION-}" ]; then setopt local_options null_glob else # Not `$(shopt -p nullglob)`: that exits 1 when the option is off, and a # caller that sources this under `set -e` would die here without a word. local __dotfiles_nullglob='' shopt -q nullglob || __dotfiles_nullglob='shopt -u nullglob' shopt -s nullglob fi local __dotfiles_f for __dotfiles_f in "$1"/*.sh; do # shellcheck source=/dev/null . "$__dotfiles_f" done if [ -z "${ZSH_VERSION-}" ] && [ -n "$__dotfiles_nullglob" ]; then eval "$__dotfiles_nullglob" fi } __dotfiles_source_dir "${XDG_CONFIG_HOME:-$HOME/.config}/shell/profile.d" unset -f __dotfiles_source_dir _profile profile.d macbook_chassis_regulatory_model() { ioreg -l \ | grep '"regulatory-model-number"' \ | sed -E 's/.*<([^>]+).*/\1/' \ | cut -c1-10 \ | xxd -r -p \ | tr -d '\000' } _profile misc if [[ -r /proc/cpuinfo ]] && \ grep -qi 'raspberry pi' /proc/cpuinfo then system_temperature() { paste \ <(cat /sys/class/thermal/thermal_zone*/type) \ <(cat /sys/class/thermal/thermal_zone*/temp | sed 's/\(.\)..$/.\1°C/' ) \ | column -s $'\t' -t if [[ -f /sys/class/thermal/cooling_device0/cur_state ]] then echo "Fan state: $(cat /sys/class/thermal/cooling_device0/cur_state)" else echo "No fan" fi } fi _profile raspberry_pi # The kimi / cursor-agent / claude env vars are all in config/env. if [ -n "$ZSH_VERSION" ]; then [[ -d /opt/homebrew/share/zsh/site-functions/ ]] && fpath+=(/opt/homebrew/share/zsh/site-functions/) fi _profile zsh_specific # Disable XON/XOFF flow control so Ctrl-Q/Ctrl-S work in apps like nvim stty -ixon 2>/dev/null # The Ghostty bin dir is on PATH via config/env. _profile misc # Interactive host-local setup. Plain host-local *environment* goes in # ~/.localenv, which config/env sources for every shell. # shellcheck source=/dev/null [ -r "$HOME/.localprofile" ] && source "$HOME/.localprofile" _profile local # A *login* bash reads this file but never ~/.bashrc, so login and non-login # interactive bash would otherwise diverge. Source it here to converge them. # Skipped when ~/.bashrc is what sourced us, and for zsh, which has no such gap # (a login interactive zsh reads both ~/.zprofile and ~/.zshrc). if [ -n "${BASH_VERSION:-}" ] && [ -z "${__dotfiles_bashrc:-}" ] && [ -r "$HOME/.bashrc" ]; then # shellcheck source=/dev/null . "$HOME/.bashrc" fi _profile bashrc _profile exit # ~/bin is prepended to PATH in config/env. # vim: foldmethod=marker filetype=sh tw=100