# 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
