From 25d055514de104f471c4c36dae83dc835ee006de Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Mon, 9 Mar 2026 20:15:35 -0400 Subject: [PATCH 01/10] initial bash config --- aliases | 15 ++++++ bashrc | 11 +++++ prompt | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 aliases create mode 100644 bashrc create mode 100644 prompt diff --git a/aliases b/aliases new file mode 100644 index 0000000..384e3a9 --- /dev/null +++ b/aliases @@ -0,0 +1,15 @@ +# Navigation +alias cd='cd -L' + +# Search +alias grep='grep --color' + +# Tree (uses eza if available) +if command -v eza &>/dev/null; then + alias tree='eza --tree --icons=never' +fi + +# Open (graphical environment only) +if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then + alias open='xdg-open' +fi diff --git a/bashrc b/bashrc new file mode 100644 index 0000000..41e3289 --- /dev/null +++ b/bashrc @@ -0,0 +1,11 @@ +# Source prompt and aliases +BASH_CONFIG_DIR="${BASH_SOURCE%/*}" +source "$BASH_CONFIG_DIR/prompt" +source "$BASH_CONFIG_DIR/aliases" + +# Vi mode +set -o vi + +# Completion +bind 'set completion-ignore-case on' +bind 'set completion-map-case on' diff --git a/prompt b/prompt new file mode 100644 index 0000000..644be50 --- /dev/null +++ b/prompt @@ -0,0 +1,139 @@ +# Detect graphical environment for icons +_has_graphics() { + [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ] +} + +check_ssh() { + if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then + ssh_PS1="\n\[\033[01;37m\]\u@\h:\[\033[00m\]\n" + return 0 + fi +} + +# Git-related functions (only if git available) +if command -v git &>/dev/null; then + +check_venv() { + add_icon() { + local icon=$1 + if [[ ! $venv_icons =~ $icon ]]; then + venv_icons+="$icon " + fi + } + + remove_icon() { + local icon=$1 + venv_icons=${venv_icons//$icon/} + } + + if _has_graphics; then + py="" + js="󰌞" + nix="" + else + py="py" + js="js" + nix="nix" + fi + + python_icon="\[\033[01;33m\]$py\[\033[00m\]" + node_icon="\[\033[01;93m\]$js\[\033[00m\]" + nix_icon="\[\033[01;34m\]$nix\[\033[00m\]" + + if [ -n "$IN_NIX_SHELL" ]; then + add_icon "$nix_icon" + else + remove_icon "$nix_icon" + fi + + if [ -n "$VIRTUAL_ENV" ]; then + add_icon "$python_icon" + else + remove_icon "$python_icon" + fi + + if [ -d "${git_root}/node_modules" ]; then + add_icon "$node_icon" + else + remove_icon "$node_icon" + fi +} + +set_git_dir() { + if _has_graphics; then + project_icon=" " + else + project_icon="../" + fi + + local superproject_root=$(git rev-parse --show-superproject-working-tree 2>/dev/null) + if [[ -n "$superproject_root" ]]; then + local submodule_name=$(basename "$git_root") + working_dir="\[\033[01;34m\]$project_icon${superproject_root##*/}/$submodule_name$git_curr_dir\[\033[00m\]" + elif [ "$git_curr_dir" == "." ]; then + working_dir="\[\033[01;34m\]$project_icon$git_root_dir\[\033[00m\]" + return 0 + else + working_dir="\[\033[01;34m\]$project_icon$git_root_dir$git_curr_dir\[\033[00m\]" + return 0 + fi +} + +relative_path() { + local absolute_target=$(readlink -f "$1") + local absolute_base=$(readlink -f "$2") + echo "${absolute_target#$absolute_base}" +} + +check_project() { + git_root=$(git rev-parse --show-toplevel 2>/dev/null) + + if [ -n "$git_root" ]; then + git_branch=$(git branch --show-current 2>/dev/null) + + if [ -z "$git_branch" ]; then + git_branch=$(git describe --tags --exact-match 2>/dev/null) + git_branch=${git_branch:-$(git rev-parse --short HEAD 2>/dev/null)} + fi + + git_curr_dir=$(relative_path "." "$git_root") + git_root_dir=$(basename "$git_root") + + if _has_graphics; then + git_branch_PS1="\[\033[01;31m\]$git_branch 󰘬:\[\033[00m\]" + else + git_branch_PS1="\[\033[01;31m\]$git_branch:\[\033[00m\]" + fi + + set_git_dir + check_venv + + return 0 + fi +} + +fi # end git check + +set_prompt() { + local green_arrow="\[\033[01;32m\]>> " + local white_text="\[\033[00m\]" + local working_dir="\[\033[01;34m\]\w\[\033[00m\]" + + local ssh_PS1 + + check_ssh + + if command -v git &>/dev/null; then + local venv_icons + local git_branch_PS1 + + check_project + + PS1="$ssh_PS1\n$working_dir\n$venv_icons$green_arrow$git_branch_PS1$white_text" + else + PS1="$ssh_PS1\n$working_dir\n$green_arrow$white_text" + fi + return 0 +} + +PROMPT_COMMAND="set_prompt" From 930620e2ef10a9b7952b1a936bc686e8b5712a92 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Mon, 9 Mar 2026 20:22:18 -0400 Subject: [PATCH 02/10] fix python and nix icons --- prompt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prompt b/prompt index 644be50..a9af7f7 100644 --- a/prompt +++ b/prompt @@ -27,9 +27,9 @@ check_venv() { } if _has_graphics; then - py="" + py="" js="󰌞" - nix="" + nix="" else py="py" js="js" From 0088c9646a7e6e7c956b2a4a4f8215473b4a16f4 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Mon, 9 Mar 2026 20:24:24 -0400 Subject: [PATCH 03/10] fix project root icon --- prompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prompt b/prompt index a9af7f7..2416a0a 100644 --- a/prompt +++ b/prompt @@ -61,7 +61,7 @@ check_venv() { set_git_dir() { if _has_graphics; then - project_icon=" " + project_icon=" " else project_icon="../" fi From b286fe447b0b3631af37a0506fb7af402a11ed3a Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Mon, 9 Mar 2026 21:43:24 -0400 Subject: [PATCH 04/10] optimize: eliminate git spawns, use utf codes for icons --- prompt | 260 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 142 insertions(+), 118 deletions(-) diff --git a/prompt b/prompt index 2416a0a..7dac80e 100644 --- a/prompt +++ b/prompt @@ -1,139 +1,163 @@ -# Detect graphical environment for icons -_has_graphics() { - [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ] -} +# Detect graphical environment once at source time +if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then + _gfx=1 + _py_sym=$'\ue73c' + _js_sym=$'\ue781' + _nix_sym=$'\ue843' + _proj_sym=$'\ueb45 ' + _branch_sym=$'\uf418' +else + _gfx="" + _py_sym="py" + _js_sym="js" + _nix_sym="nix" + _proj_sym="../" + _branch_sym="" +fi -check_ssh() { - if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then - ssh_PS1="\n\[\033[01;37m\]\u@\h:\[\033[00m\]\n" - return 0 - fi -} +# Pre-compute colored icons +_python_icon="\[\033[01;33m\]$_py_sym\[\033[00m\]" +_node_icon="\[\033[01;93m\]$_js_sym\[\033[00m\]" +_nix_icon="\[\033[01;34m\]$_nix_sym\[\033[00m\]" -# Git-related functions (only if git available) -if command -v git &>/dev/null; then +# SSH check once at source time +if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then + _ssh_PS1="\n\[\033[01;37m\]\u@\h:\[\033[00m\]\n" +else + _ssh_PS1="" +fi -check_venv() { - add_icon() { - local icon=$1 - if [[ ! $venv_icons =~ $icon ]]; then - venv_icons+="$icon " +# Static prompt parts +_green_arrow="\[\033[01;32m\]>> " +_white_text="\[\033[00m\]" + +# Cache: dir -> "git_root|superproject|" or "-" for non-git +declare -A _dir_cache + +# Find git root by walking up (no git spawn) +_find_git_root() { + local dir="$PWD" + while [ -n "$dir" ]; do + if [ -e "$dir/.git" ]; then + _git_root="$dir" + return 0 fi - } - - remove_icon() { - local icon=$1 - venv_icons=${venv_icons//$icon/} - } - - if _has_graphics; then - py="" - js="󰌞" - nix="" - else - py="py" - js="js" - nix="nix" - fi - - python_icon="\[\033[01;33m\]$py\[\033[00m\]" - node_icon="\[\033[01;93m\]$js\[\033[00m\]" - nix_icon="\[\033[01;34m\]$nix\[\033[00m\]" - - if [ -n "$IN_NIX_SHELL" ]; then - add_icon "$nix_icon" - else - remove_icon "$nix_icon" - fi - - if [ -n "$VIRTUAL_ENV" ]; then - add_icon "$python_icon" - else - remove_icon "$python_icon" - fi - - if [ -d "${git_root}/node_modules" ]; then - add_icon "$node_icon" - else - remove_icon "$node_icon" - fi + dir="${dir%/*}" + done + return 1 } -set_git_dir() { - if _has_graphics; then - project_icon=" " - else - project_icon="../" - fi - - local superproject_root=$(git rev-parse --show-superproject-working-tree 2>/dev/null) - if [[ -n "$superproject_root" ]]; then - local submodule_name=$(basename "$git_root") - working_dir="\[\033[01;34m\]$project_icon${superproject_root##*/}/$submodule_name$git_curr_dir\[\033[00m\]" - elif [ "$git_curr_dir" == "." ]; then - working_dir="\[\033[01;34m\]$project_icon$git_root_dir\[\033[00m\]" - return 0 - else - working_dir="\[\033[01;34m\]$project_icon$git_root_dir$git_curr_dir\[\033[00m\]" - return 0 - fi -} - -relative_path() { - local absolute_target=$(readlink -f "$1") - local absolute_base=$(readlink -f "$2") - echo "${absolute_target#$absolute_base}" -} - -check_project() { - git_root=$(git rev-parse --show-toplevel 2>/dev/null) - - if [ -n "$git_root" ]; then - git_branch=$(git branch --show-current 2>/dev/null) - - if [ -z "$git_branch" ]; then - git_branch=$(git describe --tags --exact-match 2>/dev/null) - git_branch=${git_branch:-$(git rev-parse --short HEAD 2>/dev/null)} +# Find superproject by walking up from git root +_find_superproject() { + local dir="${_git_root%/*}" + _superproject="" + while [ -n "$dir" ]; do + if [ -e "$dir/.git" ]; then + _superproject="$dir" + return 0 fi + dir="${dir%/*}" + done + return 1 +} - git_curr_dir=$(relative_path "." "$git_root") - git_root_dir=$(basename "$git_root") +# Read branch from .git/HEAD (no git spawn) +_read_branch() { + local git_dir="$_git_root/.git" + local head_file - if _has_graphics; then - git_branch_PS1="\[\033[01;31m\]$git_branch 󰘬:\[\033[00m\]" + # Submodule: .git is a file with "gitdir: " + if [ -f "$git_dir" ]; then + read -r _ head_file < "$git_dir" + head_file="$head_file/HEAD" + else + head_file="$git_dir/HEAD" + fi + + [ -f "$head_file" ] || return 1 + + local head + read -r head < "$head_file" + + if [[ "$head" == "ref: refs/heads/"* ]]; then + _git_branch="${head#ref: refs/heads/}" + else + # Detached HEAD - short hash + _git_branch="${head:0:7}" + fi +} + +# Build venv icons (must run every prompt - env can change) +_build_venv_icons() { + venv_icons="" + [ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon " + [ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon " + [ -d "$_git_root/node_modules" ] && venv_icons+="$_node_icon " +} + +# Main prompt logic +_set_prompt() { + local cached="${_dir_cache[$PWD]}" + + # Check cache + if [ -z "$cached" ]; then + if _find_git_root; then + _find_superproject + _dir_cache[$PWD]="$_git_root|$_superproject|" else - git_branch_PS1="\[\033[01;31m\]$git_branch:\[\033[00m\]" + _dir_cache[$PWD]="-" + cached="-" fi - - set_git_dir - check_venv - - return 0 fi -} -fi # end git check + # Non-git directory + if [ "$cached" = "-" ] || [ "${_dir_cache[$PWD]}" = "-" ]; then + venv_icons="" + [ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon " + [ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon " + PS1="$_ssh_PS1\n\[\033[01;34m\]\w\[\033[00m\]\n$venv_icons$_green_arrow$_white_text" + return + fi -set_prompt() { - local green_arrow="\[\033[01;32m\]>> " - local white_text="\[\033[00m\]" - local working_dir="\[\033[01;34m\]\w\[\033[00m\]" + # Parse cache + [ -z "$cached" ] && cached="${_dir_cache[$PWD]}" + IFS='|' read -r _git_root _superproject _ <<< "$cached" - local ssh_PS1 + # Get branch (can change without cd) + _read_branch - check_ssh + # Build paths using bash string ops (no readlink spawn) + local git_curr_dir="${PWD#$_git_root}" + local git_root_dir="${_git_root##*/}" - if command -v git &>/dev/null; then - local venv_icons - local git_branch_PS1 - - check_project - - PS1="$ssh_PS1\n$working_dir\n$venv_icons$green_arrow$git_branch_PS1$white_text" + # Build working_dir + local working_dir + if [ -n "$_superproject" ]; then + local super_name="${_superproject##*/}" + working_dir="\[\033[01;34m\]$_proj_sym$super_name/$git_root_dir$git_curr_dir\[\033[00m\]" + elif [ -z "$git_curr_dir" ]; then + working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir\[\033[00m\]" else - PS1="$ssh_PS1\n$working_dir\n$green_arrow$white_text" + working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir$git_curr_dir\[\033[00m\]" fi - return 0 + + # Build branch PS1 + local git_branch_PS1 + if [ -n "$_gfx" ]; then + git_branch_PS1="\[\033[01;31m\]$_git_branch $_branch_sym:\[\033[00m\]" + else + git_branch_PS1="\[\033[01;31m\]$_git_branch:\[\033[00m\]" + fi + + # Build venv icons + _build_venv_icons + + PS1="$_ssh_PS1\n$working_dir\n$venv_icons$_green_arrow$git_branch_PS1$_white_text" } -PROMPT_COMMAND="set_prompt" +# Invalidate cache (call after git clone, init, etc) +_prompt_cache_invalidate() { + unset "_dir_cache[$PWD]" +} + +PROMPT_COMMAND="_set_prompt" From 100b851ee52bd802f09326081c663f8d6157cdf9 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Mon, 9 Mar 2026 22:05:16 -0400 Subject: [PATCH 05/10] added git symbol --- prompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prompt b/prompt index 7dac80e..51dc17c 100644 --- a/prompt +++ b/prompt @@ -12,7 +12,7 @@ else _js_sym="js" _nix_sym="nix" _proj_sym="../" - _branch_sym="" + _branch_sym="git" fi # Pre-compute colored icons From c8d9ca529ab1569fd6622bc229fd0a4c3c370a80 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Mon, 9 Mar 2026 22:16:23 -0400 Subject: [PATCH 06/10] added git wrapper to invalidate cache on repo creating cmds --- prompt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/prompt b/prompt index 51dc17c..84f97b1 100644 --- a/prompt +++ b/prompt @@ -155,9 +155,17 @@ _set_prompt() { PS1="$_ssh_PS1\n$working_dir\n$venv_icons$_green_arrow$git_branch_PS1$_white_text" } -# Invalidate cache (call after git clone, init, etc) +# Invalidate cache for current directory _prompt_cache_invalidate() { unset "_dir_cache[$PWD]" } +# Wrap git to invalidate cache on repo-creating commands +git() { + command git "$@" + local ret=$? + [[ "$1" =~ ^(init|clone)$ ]] && _prompt_cache_invalidate + return $ret +} + PROMPT_COMMAND="_set_prompt" From 79eb823bbb9ff88f284ae055fe1de954df8bf2e0 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Mon, 9 Mar 2026 22:30:14 -0400 Subject: [PATCH 07/10] fixed --- prompt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/prompt b/prompt index 84f97b1..b5afc1d 100644 --- a/prompt +++ b/prompt @@ -123,8 +123,12 @@ _set_prompt() { [ -z "$cached" ] && cached="${_dir_cache[$PWD]}" IFS='|' read -r _git_root _superproject _ <<< "$cached" - # Get branch (can change without cd) - _read_branch + # Get branch (can change without cd) - if fails, git root is gone + if ! _read_branch; then + unset "_dir_cache[$PWD]" + _set_prompt + return + fi # Build paths using bash string ops (no readlink spawn) local git_curr_dir="${PWD#$_git_root}" From f6700648a3186a1a1cc7d004df7ef14953272c81 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Tue, 10 Mar 2026 13:43:28 -0400 Subject: [PATCH 08/10] added gpg auth sock --- aliases | 3 +++ bashrc | 3 +++ 2 files changed, 6 insertions(+) diff --git a/aliases b/aliases index 384e3a9..6a612d9 100644 --- a/aliases +++ b/aliases @@ -13,3 +13,6 @@ fi if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then alias open='xdg-open' fi + +alias vi='/usr/bin/vim' +alias vim='/usr/bin/nvim' diff --git a/bashrc b/bashrc index 41e3289..318ff7e 100644 --- a/bashrc +++ b/bashrc @@ -1,3 +1,6 @@ +# Use gpg-agent for SSH (YubiKey support) +export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket) + # Source prompt and aliases BASH_CONFIG_DIR="${BASH_SOURCE%/*}" source "$BASH_CONFIG_DIR/prompt" From f0fe41e942dfedab23828131372a5545a6d5c4c6 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Wed, 8 Apr 2026 16:14:55 -0400 Subject: [PATCH 09/10] updated --- aliases | 15 +++++++++++---- bashrc | 13 ++++++++++--- prompt | 6 +++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/aliases b/aliases index 6a612d9..ea7697b 100644 --- a/aliases +++ b/aliases @@ -1,11 +1,15 @@ # Navigation alias cd='cd -L' +# Colors +eval "$(dircolors -b)" +alias ls='ls --color=auto' + # Search -alias grep='grep --color' +alias grep='grep --color=auto' # Tree (uses eza if available) -if command -v eza &>/dev/null; then +if command -v eza >/dev/null 2>&1; then alias tree='eza --tree --icons=never' fi @@ -14,5 +18,8 @@ if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then alias open='xdg-open' fi -alias vi='/usr/bin/vim' -alias vim='/usr/bin/nvim' +if command -v nvim >/dev/null 2>&1; then + alias vim='nvim' +fi + +alias cdg='cd "$(git rev-parse --show-toplevel 2>/dev/null)"' diff --git a/bashrc b/bashrc index 318ff7e..4e08b82 100644 --- a/bashrc +++ b/bashrc @@ -1,10 +1,13 @@ # Use gpg-agent for SSH (YubiKey support) -export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket) +if command -v gpgconf >/dev/null 2>&1; then + export SSH_AUTH_SOCK + SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" +fi # Source prompt and aliases BASH_CONFIG_DIR="${BASH_SOURCE%/*}" -source "$BASH_CONFIG_DIR/prompt" -source "$BASH_CONFIG_DIR/aliases" +. "$BASH_CONFIG_DIR/prompt" +. "$BASH_CONFIG_DIR/aliases" # Vi mode set -o vi @@ -12,3 +15,7 @@ set -o vi # Completion bind 'set completion-ignore-case on' bind 'set completion-map-case on' + +if command -v direnv >/dev/null 2>&1; then + eval "$(direnv hook bash)" +fi diff --git a/prompt b/prompt index b5afc1d..9b7a494 100644 --- a/prompt +++ b/prompt @@ -172,4 +172,8 @@ git() { return $ret } -PROMPT_COMMAND="_set_prompt" +if [ -n "$PROMPT_COMMAND" ]; then + PROMPT_COMMAND="_set_prompt;$PROMPT_COMMAND" +else + PROMPT_COMMAND="_set_prompt" +fi From 6667f36a9f83a88b33b691a5a9b701365267bbb3 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Wed, 15 Apr 2026 21:05:05 -0400 Subject: [PATCH 10/10] init --- aliases | 25 ++++++++ bashrc | 21 +++++++ prompt | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 aliases create mode 100644 bashrc create mode 100644 prompt diff --git a/aliases b/aliases new file mode 100644 index 0000000..ea7697b --- /dev/null +++ b/aliases @@ -0,0 +1,25 @@ +# Navigation +alias cd='cd -L' + +# Colors +eval "$(dircolors -b)" +alias ls='ls --color=auto' + +# Search +alias grep='grep --color=auto' + +# Tree (uses eza if available) +if command -v eza >/dev/null 2>&1; then + alias tree='eza --tree --icons=never' +fi + +# Open (graphical environment only) +if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then + alias open='xdg-open' +fi + +if command -v nvim >/dev/null 2>&1; then + alias vim='nvim' +fi + +alias cdg='cd "$(git rev-parse --show-toplevel 2>/dev/null)"' diff --git a/bashrc b/bashrc new file mode 100644 index 0000000..4e08b82 --- /dev/null +++ b/bashrc @@ -0,0 +1,21 @@ +# Use gpg-agent for SSH (YubiKey support) +if command -v gpgconf >/dev/null 2>&1; then + export SSH_AUTH_SOCK + SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" +fi + +# Source prompt and aliases +BASH_CONFIG_DIR="${BASH_SOURCE%/*}" +. "$BASH_CONFIG_DIR/prompt" +. "$BASH_CONFIG_DIR/aliases" + +# Vi mode +set -o vi + +# Completion +bind 'set completion-ignore-case on' +bind 'set completion-map-case on' + +if command -v direnv >/dev/null 2>&1; then + eval "$(direnv hook bash)" +fi diff --git a/prompt b/prompt new file mode 100644 index 0000000..9b7a494 --- /dev/null +++ b/prompt @@ -0,0 +1,179 @@ +# Detect graphical environment once at source time +if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then + _gfx=1 + _py_sym=$'\ue73c' + _js_sym=$'\ue781' + _nix_sym=$'\ue843' + _proj_sym=$'\ueb45 ' + _branch_sym=$'\uf418' +else + _gfx="" + _py_sym="py" + _js_sym="js" + _nix_sym="nix" + _proj_sym="../" + _branch_sym="git" +fi + +# Pre-compute colored icons +_python_icon="\[\033[01;33m\]$_py_sym\[\033[00m\]" +_node_icon="\[\033[01;93m\]$_js_sym\[\033[00m\]" +_nix_icon="\[\033[01;34m\]$_nix_sym\[\033[00m\]" + +# SSH check once at source time +if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then + _ssh_PS1="\n\[\033[01;37m\]\u@\h:\[\033[00m\]\n" +else + _ssh_PS1="" +fi + +# Static prompt parts +_green_arrow="\[\033[01;32m\]>> " +_white_text="\[\033[00m\]" + +# Cache: dir -> "git_root|superproject|" or "-" for non-git +declare -A _dir_cache + +# Find git root by walking up (no git spawn) +_find_git_root() { + local dir="$PWD" + while [ -n "$dir" ]; do + if [ -e "$dir/.git" ]; then + _git_root="$dir" + return 0 + fi + dir="${dir%/*}" + done + return 1 +} + +# Find superproject by walking up from git root +_find_superproject() { + local dir="${_git_root%/*}" + _superproject="" + while [ -n "$dir" ]; do + if [ -e "$dir/.git" ]; then + _superproject="$dir" + return 0 + fi + dir="${dir%/*}" + done + return 1 +} + +# Read branch from .git/HEAD (no git spawn) +_read_branch() { + local git_dir="$_git_root/.git" + local head_file + + # Submodule: .git is a file with "gitdir: " + if [ -f "$git_dir" ]; then + read -r _ head_file < "$git_dir" + head_file="$head_file/HEAD" + else + head_file="$git_dir/HEAD" + fi + + [ -f "$head_file" ] || return 1 + + local head + read -r head < "$head_file" + + if [[ "$head" == "ref: refs/heads/"* ]]; then + _git_branch="${head#ref: refs/heads/}" + else + # Detached HEAD - short hash + _git_branch="${head:0:7}" + fi +} + +# Build venv icons (must run every prompt - env can change) +_build_venv_icons() { + venv_icons="" + [ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon " + [ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon " + [ -d "$_git_root/node_modules" ] && venv_icons+="$_node_icon " +} + +# Main prompt logic +_set_prompt() { + local cached="${_dir_cache[$PWD]}" + + # Check cache + if [ -z "$cached" ]; then + if _find_git_root; then + _find_superproject + _dir_cache[$PWD]="$_git_root|$_superproject|" + else + _dir_cache[$PWD]="-" + cached="-" + fi + fi + + # Non-git directory + if [ "$cached" = "-" ] || [ "${_dir_cache[$PWD]}" = "-" ]; then + venv_icons="" + [ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon " + [ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon " + PS1="$_ssh_PS1\n\[\033[01;34m\]\w\[\033[00m\]\n$venv_icons$_green_arrow$_white_text" + return + fi + + # Parse cache + [ -z "$cached" ] && cached="${_dir_cache[$PWD]}" + IFS='|' read -r _git_root _superproject _ <<< "$cached" + + # Get branch (can change without cd) - if fails, git root is gone + if ! _read_branch; then + unset "_dir_cache[$PWD]" + _set_prompt + return + fi + + # Build paths using bash string ops (no readlink spawn) + local git_curr_dir="${PWD#$_git_root}" + local git_root_dir="${_git_root##*/}" + + # Build working_dir + local working_dir + if [ -n "$_superproject" ]; then + local super_name="${_superproject##*/}" + working_dir="\[\033[01;34m\]$_proj_sym$super_name/$git_root_dir$git_curr_dir\[\033[00m\]" + elif [ -z "$git_curr_dir" ]; then + working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir\[\033[00m\]" + else + working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir$git_curr_dir\[\033[00m\]" + fi + + # Build branch PS1 + local git_branch_PS1 + if [ -n "$_gfx" ]; then + git_branch_PS1="\[\033[01;31m\]$_git_branch $_branch_sym:\[\033[00m\]" + else + git_branch_PS1="\[\033[01;31m\]$_git_branch:\[\033[00m\]" + fi + + # Build venv icons + _build_venv_icons + + PS1="$_ssh_PS1\n$working_dir\n$venv_icons$_green_arrow$git_branch_PS1$_white_text" +} + +# Invalidate cache for current directory +_prompt_cache_invalidate() { + unset "_dir_cache[$PWD]" +} + +# Wrap git to invalidate cache on repo-creating commands +git() { + command git "$@" + local ret=$? + [[ "$1" =~ ^(init|clone)$ ]] && _prompt_cache_invalidate + return $ret +} + +if [ -n "$PROMPT_COMMAND" ]; then + PROMPT_COMMAND="_set_prompt;$PROMPT_COMMAND" +else + PROMPT_COMMAND="_set_prompt" +fi