shell-init: record the first command of every shell in atuin

bash-preexec arms its DEBUG trap from __bp_interactive_mode at the end of
PROMPT_COMMAND, but its lazy installer runs mid-way through the first prompt
because zoxide and ghostty append their hooks after it. Those hooks disarm the
trap again, so the first interactive command of every session never reached
preexec and atuin silently dropped it.

Arm from PS0 instead, which is expanded once per interactive command before the
DEBUG trap and is therefore independent of PROMPT_COMMAND ordering.

Also guard the script against running twice: it is installed as both
loginShellInit and interactiveShellInit, and /etc/profile sources /etc/bashrc,
which registered starship's precmd/preexec hooks twice in every login shell.
This commit is contained in:
randogoth 2026-08-28 16:05:43 +03:00
parent 3f29ad6cec
commit 9f28569fef

View file

@ -37,18 +37,43 @@ fi
(( INTERACTIVE )) || return 0 (( INTERACTIVE )) || return 0
# ---- run-once guard --------------------------------------------------------
# Installed as both loginShellInit (/etc/profile) and interactiveShellInit
# (/etc/bashrc), and /etc/profile sources /etc/bashrc, so a login shell would run
# this twice and register starship's hooks twice. Not exported: child shells must
# initialise on their own.
if [[ -n ${NANUQSAURUS_SHELL_INIT_DONE:-} ]]; then
log_shell_init "already initialised, skipping"
return 0
fi
NANUQSAURUS_SHELL_INIT_DONE=1
# ---- bash-preexec ---------------------------------------------------------- # ---- bash-preexec ----------------------------------------------------------
if [[ -n ${BASH_VERSION:-} && -z ${bash_preexec_imported:-} && -r "@BASHPREEXEC@" ]]; then if [[ -n ${BASH_VERSION:-} && -z ${bash_preexec_imported:-} && -r "@BASHPREEXEC@" ]]; then
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "@BASHPREEXEC@" . "@BASHPREEXEC@"
# Do NOT call __bp_install here. bash-preexec must install on the FIRST prompt,
# after starship/atuin/zoxide/ghostty have modified PROMPT_COMMAND, so that
# __bp_interactive_mode stays the LAST PROMPT_COMMAND entry. Installing eagerly
# lets later hooks land after it and silently breaks preexec (atuin recording).
log_shell_init "bash-preexec loaded" log_shell_init "bash-preexec loaded"
fi fi
# bash-preexec arms its DEBUG trap from __bp_interactive_mode, which it appends to
# the END of PROMPT_COMMAND -- but its lazy installer runs in the MIDDLE of the
# first prompt, because zoxide (below) and ghostty (~/.bashrc) append their hooks
# after it. Those hooks then disarm the trap again, so the first command of every
# session never reaches preexec and atuin never records it. PS0 is expanded once
# per interactive command, before the DEBUG trap, so arming there is independent of
# PROMPT_COMMAND ordering. Needs the non-forking ${ ...; } funsub (bash >= 5.3);
# older bash keeps the previous behaviour.
if [[ -n ${bash_preexec_imported:-} ]] &&
((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3))); then
# shellcheck disable=SC2016
__nanuq_bp_arm='${ __bp_preexec_interactive_mode=on; }'
[[ ${PS0-} == *"$__nanuq_bp_arm"* ]] || PS0=$__nanuq_bp_arm${PS0-}
unset __nanuq_bp_arm
log_shell_init "bash-preexec PS0 arming enabled"
fi
# ---- starship -------------------------------------------------------------- # ---- starship --------------------------------------------------------------
if have @STARSHIP@; then if have @STARSHIP@; then
@ -57,8 +82,7 @@ if have @STARSHIP@; then
*/bash) eval "$(@STARSHIP@ init bash)" ;; */bash) eval "$(@STARSHIP@ init bash)" ;;
*/zsh) eval "$(@STARSHIP@ init zsh)" ;; */zsh) eval "$(@STARSHIP@ init zsh)" ;;
esac esac
# starship's bash init registers starship_precmd/starship_preexec_all with # registers starship_precmd/starship_preexec_all with bash-preexec directly
# bash-preexec directly; no manual PROMPT_COMMAND wiring needed.
else else
log_shell_init "starship skipped" log_shell_init "starship skipped"
fi fi
@ -77,8 +101,7 @@ if have @ATUIN@; then
*/bash) eval "$(@ATUIN@ init bash)" ;; */bash) eval "$(@ATUIN@ init bash)" ;;
*/zsh) eval "$(@ATUIN@ init zsh)" ;; */zsh) eval "$(@ATUIN@ init zsh)" ;;
esac esac
# atuin's init registers __atuin_preexec/__atuin_precmd with bash-preexec # registers __atuin_preexec/__atuin_precmd with bash-preexec itself
# itself; manual array wiring only caused duplicate hook entries.
else else
log_shell_init "atuin skipped" log_shell_init "atuin skipped"
fi fi
@ -91,9 +114,7 @@ if have zoxide; then
*/bash) eval "$(zoxide init bash)" ;; */bash) eval "$(zoxide init bash)" ;;
*/zsh) eval "$(zoxide init zsh)" ;; */zsh) eval "$(zoxide init zsh)" ;;
esac esac
# zoxide's bash init hooks into bash-preexec (precmd_functions) when it is # appends __zoxide_hook to PROMPT_COMMAND; see the PS0 note above
# present; manual PROMPT_COMMAND wiring only pushed hooks past
# __bp_interactive_mode and broke preexec ordering.
else else
log_shell_init "zoxide skipped" log_shell_init "zoxide skipped"
fi fi