From 13cc065ec014b86369dfe4ad60d1a43fa06fd251 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Thu, 17 Sep 2026 19:45:23 +0100 Subject: dotfiles Shell, editor and terminal configuration, the tools that go with them, and szecret, an age-based secret manager whose store lives elsewhere. History restarts here. What came before was not written to be read by anyone else, and carried machine names, addresses and an encrypted secret store; it is kept privately instead. What is left is meant to be safe on any machine, including one whose owner is not me: see CLAUDE.md for the rule it is written to. --- config/env | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 config/env (limited to 'config/env') diff --git a/config/env b/config/env new file mode 100644 index 0000000..5768649 --- /dev/null +++ b/config/env @@ -0,0 +1,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 -- cgit v1.2.3