all repos — dot @ e01393235ef52fc05f2a4a3768f22e73eb12cd73

Shell, editor and terminal configuration, and the tools that go with them

config/env (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
# shellcheck shell=sh
# config/env — environment for EVERY shell: zsh and bash, login or not,
# interactive or not. This is the file `config/profile` was always meant to be.
#
# How it is reached:
#   zsh   ~/.zshenv                 — every zsh, including `ssh host cmd`,
#                                     `zsh -c ...` and plain scripts. That is the
#                                     ONLY startup file zsh reads in those modes:
#                                     ~/.zprofile and ~/.zshrc are both skipped,
#                                     and on macOS so is /etc/zprofile (path_helper).
#   zsh   ~/.zprofile               — login shells, AFTER /etc/zprofile has run
#                                     path_helper. Loading this file a second
#                                     time is what puts our PATH entries back in
#                                     front of the system ones; see below.
#   bash  ~/.profile                — login shells
#   bash  $BASH_ENV                 — non-interactive shells (exported below)
#   both  config/profile            — sources this first thing, so interactive
#                                     zsh gets it too, via ~/.zshrc
#
# HARD RULES, because this runs for every shell process:
#   * POSIX sh only — bash and zsh both source it. No [[ ]], no arrays.
#   * No forks. No $(...), no pipes, no external commands. Use `case` and
#     parameter expansion. `command -v` is a shell builtin and is allowed.
#   * No side effects: no mkdir, no stty, no tty, no aliases, no history
#     settings, no secret decryption, nothing that wants a terminal.
#   * Idempotent — config/profile sources it again, and nested shells re-run it.
#   * `set -u` safe — always use the ${VAR:-} form.
#
# Anything that breaks a rule belongs in config/profile (interactive) instead.
# See config/profile's header for the split.

# ---------------------------------------------------------------- PATH helpers
# Fork-free equivalents of profile's pathadd/pathapp, with the same semantics:
# an entry already on PATH is MOVED, not left where it is. Non-existent
# directories are skipped, which is what makes one list work on both macOS and
# Linux.
#
# Moving matters, and an earlier version of this file skipping it is what put
# /usr/bin ahead of ~/.bin and /opt/homebrew/bin. macOS runs path_helper from
# /etc/zprofile, which rebuilds PATH with /etc/paths (/usr/bin, /bin, ...) at
# the front and everything that was already there appended behind it. zsh reads
# ~/.zshenv BEFORE /etc/zprofile, so whatever precedence this file establishes
# is demoted moments later. Only a second pass after path_helper can undo that,
# and a second pass only does anything if the helpers move existing entries.
#
# The consequence is that PATH order is decided entirely by the sequence of
# calls below, from any starting PATH — which also makes re-sourcing a fixed
# point. The flip side: never hand _env_append a system directory, or it will
# be demoted to the end of PATH.

# Drops every occurrence of $1 from PATH into $_env_p, without the surrounding
# colons. Callers re-add it at whichever end they want.
_env_drop() {
    _env_p=":${PATH}:"
    while :; do
        case "$_env_p" in
            *":$1:"*) _env_p="${_env_p%%":$1:"*}:${_env_p#*":$1:"}" ;;
            *) break ;;
        esac
    done
    _env_p="${_env_p#:}"
    _env_p="${_env_p%:}"
}

_env_prepend() {
    [ -d "$1" ] || return 0
    _env_drop "$1"
    PATH="$1${_env_p:+:$_env_p}"
}

_env_append() {
    [ -d "$1" ] || return 0
    _env_drop "$1"
    PATH="${_env_p:+$_env_p:}$1"
}

# ------------------------------------------------------------------------- XDG
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" # Config stuff should go here.
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"    # Cache stuff should go here.
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" # Data should go here.

# ------------------------------------------------------------------- core PATH
# Prepended in this order, so the effective precedence ends up:
#   ~/.local/bin, ~/.bin, /opt/homebrew/bin, /usr/local/bin, ...
_env_prepend "/usr/local/bin"
_env_prepend "/opt/homebrew/bin"
_env_prepend "$HOME/.bin"
_env_prepend "$HOME/.local/bin"

# ------------------------------------------------------------------------ less
export LESS='--tabs=4 --ignore-case -RFX'
export LESSKEY="$XDG_CACHE_HOME/less"
export LESSHISTFILE="${LESSHISTFILE:-$XDG_CACHE_HOME/less/history}"
export PAGER=less

# ---------------------------------------------------------------------- editor
# Quoted so shellcheck reads these as strings, not commands to run (SC2209).
if command -v nvim >/dev/null 2>&1; then
    _env_vim="nvim"
elif command -v vim >/dev/null 2>&1; then
    _env_vim="vim"
else
    _env_vim="vi"
fi
export VISUAL="${VISUAL:-$_env_vim}"
export EDITOR="${EDITOR:-$_env_vim}"
unset _env_vim

# ---------------------------------------------------------------------- locale
# Note: mosh refuses to start without a UTF-8 locale, so this genuinely needs to
# be here and not in the interactive layer.
export LANG="en_US.UTF-8"
export LC_COLLATE="en_US.UTF-8"
export LC_CTYPE="en_US.UTF-8"
export LC_MESSAGES="en_US.UTF-8"
export LC_MONETARY="en_US.UTF-8"
export LC_NUMERIC="en_US.UTF-8"
export LC_TIME="en_US.UTF-8"
export LC_ALL=

# ------------------------------------------------------------------------ node
export npm_config_devdir="${npm_config_devdir:-$XDG_CACHE_HOME/node-gyp}"
export npm_config_cache="${npm_config_cache:-$XDG_CACHE_HOME/npm}"
export npm_config_userconfig="$XDG_CONFIG_HOME/npm/config"
export NODE_REPL_HISTORY="${NODE_REPL_HISTORY:-$XDG_CACHE_HOME/node_repl_history}"
export N_PREFIX="$XDG_DATA_HOME/n"
_env_prepend "$N_PREFIX/bin"

# ---------------------------------------------------------------------- python
_env_append "$HOME/Library/Python/3.8/bin"
# uv hardlinks wheels from its cache into venvs, which only works within one
# volume; with src on Sense the default ~/.cache/uv forces a full-copy fallback.
[ -d "/Volumes/Sense" ] && export UV_CACHE_DIR="${UV_CACHE_DIR:-/Volumes/Sense/.cache/uv}"

# -------------------------------------------------------------------------- go
[ -d "/Volumes/Sense/src/go" ] && export GOPATH=/Volumes/Sense/src/go
# Guarded: an unset GOPATH would otherwise make this "/bin".
[ -n "${GOPATH:-}" ] && _env_append "$GOPATH/bin"
_env_append "$HOME/go/bin"
_env_append "/usr/local/opt/go/libexec/bin"
_env_append "/usr/local/go/bin" # for Linux

# ------------------------------------------------------------------------ rust
export CARGO_HOME="${CARGO_HOME:-$XDG_DATA_HOME/cargo}"
export RUSTUP_HOME="${RUSTUP_HOME:-$XDG_DATA_HOME/rustup}"
_env_prepend "$XDG_DATA_HOME/bin"
_env_prepend "/usr/local/opt/rustup/bin"
# Prepending, in case a system-installed rustc needs to be overridden. This is
# all rustup's own $CARGO_HOME/env does, which is why ~/.zshenv can be ours.
_env_prepend "$CARGO_HOME/bin"

# ------------------------------------------ GNU Arm Embedded Toolchain, android
_env_append "/Applications/ARM/bin"
if [ -d "$HOME/Library/Android/sdk" ]; then
    export ANDROID_HOME="$HOME/Library/Android/sdk"
    _env_append "$ANDROID_HOME/emulator"
    _env_append "$ANDROID_HOME/platform-tools"
fi

# ------------------------------------------------- colima / testcontainers
# DOCKER_HOST: Standard Docker env var telling clients where to find the daemon socket.
# TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: Testcontainers mounts the socket inside containers.
#   The host path doesn't exist inside the colima VM; inside the VM, it's at /var/run/docker.sock.
if command -v colima >/dev/null 2>&1; then
    export DOCKER_HOST="unix://$HOME/.config/colima/default/docker.sock"
    export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock
fi

# ---------------------------------------------------------------- agent CLIs
export KIMI_CODE_HOME="$XDG_DATA_HOME/kimi"
export KIMI_INSTALL_DIR="$KIMI_CODE_HOME"
export KIMI_NO_MODIFY_PATH="no"
_env_append "$KIMI_CODE_HOME/bin"

# cursor-agent CLI: settings in XDG config, runtime data in XDG data.
# The Cursor IDE still uses ~/.cursor for editor state (extensions, argv.json, etc.).
export CURSOR_CONFIG_DIR="$XDG_CONFIG_HOME/cursor"
export CURSOR_DATA_DIR="$XDG_DATA_HOME/cursor"

# Claude Code renderer — pinned to the classic one, so scrollback belongs to
# tmux and nothing else. Do not rely on the `tui` key in
# config/claude/settings.json for this: every running `claude` rewrites that
# whole file from the copy it loaded at *its* start, so with several long-lived
# sessions open the last one to write wins and silently reverts the key — and
# this repo syncs that file to every host, so another machine's flip lands here.
#
# The renderer decision is made in this order, first match wins:
#   1. CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN truthy, or NO_FLICKER falsey  → off
#   2. CLAUDE_CODE_NO_FLICKER truthy                                     → on
#   3. settings.tui                                       → whatever it says
#   4. CLAUDE_CODE_TUI_TRIAL / remote feature gates       → anything at all
# The two vars below both land in step 1, above the setting *and* above the
# gates and the fullscreen trial, so fullscreen cannot come back on by itself.
# Setting both rather than one is deliberate: a tmux server started while the
# old NO_FLICKER=1 was exported keeps it in its global environment and hands it
# to every new pane forever, and only an explicit export overrides that.
#
# Accepted cost: the classic renderer repaints into tmux's normal buffer and
# duplicates lines whenever its live region is taller than the pane.
export CLAUDE_CODE_NO_FLICKER=0
export CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1
# Give the wheel to tmux too. CLAUDE_CODE_DISABLE_MOUSE suppresses mouse capture
# altogether and takes precedence over _MOUSE_CLICKS (which is why that var is
# gone from here). The old comment against this said it left no wheel scrolling
# at all — that was only true under fullscreen; off the alternate screen tmux
# keeps the scrollback and the wheel enters copy-mode as in any other pane.
export CLAUDE_CODE_DISABLE_MOUSE=1

# ------------------------------------------------------------------------ misc
_env_append "/Applications/Ghostty.app/Contents/MacOS"
_env_prepend "$HOME/bin"
# Read by the shell/prompt machinery, not by this file. Deliberately not
# exported, as it never was.
# shellcheck disable=SC2034
DISABLE_AUTO_TITLE=true

export PATH

# Make non-interactive bash read this file too. bash consults $BASH_ENV only
# when non-interactive, so this never double-fires in an interactive shell.
# Missing file => bash silently skips it, so this is safe to export anywhere.
[ -r "$HOME/.env" ] && export BASH_ENV="$HOME/.env"

# Environment linked in from elsewhere: every $XDG_CONFIG_HOME/shell/env.d/*.sh,
# in name order. This repo ships none; it is where another layer plugs in. Same
# rules as this file -- POSIX, no forks, cheap.
#
# A function only to scope zsh's null_glob: zsh aborts on a glob that matches
# nothing (and a glob qualifier like (N) breaks under `emulate sh`), while dash
# and bash hand back the literal pattern, which the -r test skips. The files are
# sourced inside it, so their exports and plain assignments are global, but a
# `local` -- or under zsh a `setopt` -- would not outlast it.
_env_source_dir() {
    if [ -n "${ZSH_VERSION-}" ]; then
        setopt local_options null_glob glob
    fi
    for _env_f in "$1"/*.sh; do
        if [ -r "$_env_f" ]; then
            . "$_env_f"
        fi
    done
    unset _env_f
}
_env_source_dir "$XDG_CONFIG_HOME/shell/env.d"

# Host-local environment. Same rules as this file — keep it cheap.
# Interactive-only local setup goes in ~/.localprofile instead.
# Sourced before the helpers are dropped, so it can use them too.
# shellcheck source=/dev/null
[ -r "$HOME/.localenv" ] && . "$HOME/.localenv"

# Don't leak the helpers into every shell. config/profile keeps its own
# pathadd/pathapp for the interactive layer.
unset -f _env_drop _env_prepend _env_append _env_source_dir
unset _env_p

# Ensure a successful status: the last conditional above may have been false,
# and this file is sourced at the top of scripts that check $?.
:

# vim: filetype=sh