From 84100a85d1a2a5f3f9fdefeacbf3154f2ce221b3 Mon Sep 17 00:00:00 2001 From: MangoPig Date: Mon, 1 Jun 2026 12:12:41 +0100 Subject: [PATCH] Zsh Prompt --- Commands/Setup/mod.just | 20 ---- Makefile | 5 +- Zsh/.zsh_aliases | 2 +- Zsh/.zsh_prompt | 245 ++++++++++++++++++++++++++++++++++++++++ Zsh/.zshrc | 6 +- 5 files changed, 253 insertions(+), 25 deletions(-) create mode 100644 Zsh/.zsh_prompt diff --git a/Commands/Setup/mod.just b/Commands/Setup/mod.just index 1823590..14487b9 100644 --- a/Commands/Setup/mod.just +++ b/Commands/Setup/mod.just @@ -21,23 +21,3 @@ clean: update: git -C '{{project_root}}' pull origin main just --justfile '{{project_root}}/Justfile' setup all - -# Run distro test containers. -test-ubuntu: - echo "Ubuntu Test" - docker run -it --rm -e TERM=xterm-256color -v '{{project_root}}':/root/dotfiles ubuntu:latest \ - bash -c "export DEBIAN_FRONTEND=noninteractive && \ - apt-get update && \ - apt-get install -y sudo git make curl && \ - cd /root/dotfiles && \ - make setup" - -test-arch: - echo "Spawning Arch Container..." - docker run -it --rm -e TERM=xterm-256color -v '{{project_root}}':/root/dotfiles archlinux:latest \ - bash -c "pacman -Sy --noconfirm base-devel git make sudo && cd /root/dotfiles && make setup" - -test-fedora: - echo "Spawning Fedora Container..." - docker run -it --rm -e TERM=xterm-256color -v '{{project_root}}':/root/dotfiles fedora:latest \ - bash -c "dnf install -y git make sudo curl which passwd procps-ng && cd /root/dotfiles && make setup" diff --git a/Makefile b/Makefile index a609f61..6fde350 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,8 @@ SCRIPTS_DIR := ./Scripts all: setup # Bootstrap entrypoint for first-run setup. +# This intentionally keeps Make lightweight: base packages, then restow dotfiles. setup: - bash $(SCRIPTS_DIR)/setup.sh + bash $(SCRIPTS_DIR)/base.sh + stow --dir=. -D Zsh --target="$$HOME" 2>/dev/null || true + stow --dir=. Zsh --target="$$HOME" diff --git a/Zsh/.zsh_aliases b/Zsh/.zsh_aliases index 8e9cc2b..aeeca29 100644 --- a/Zsh/.zsh_aliases +++ b/Zsh/.zsh_aliases @@ -16,7 +16,7 @@ alias grep="grep --color=auto" alias src="source ~/.zshrc && echo '๐Ÿ”„ Reloaded .zshrc'" -alias cat="bat" +# alias cat="bat" alias fd="fd" alias tree="eza --icons -T --git-ignore" diff --git a/Zsh/.zsh_prompt b/Zsh/.zsh_prompt new file mode 100644 index 0000000..860d383 --- /dev/null +++ b/Zsh/.zsh_prompt @@ -0,0 +1,245 @@ +# Prompt styling and context + +autoload -Uz add-zsh-hook vcs_info + +zstyle ':vcs_info:*' enable git +zstyle ':vcs_info:git:*' formats '๎‚  %b' + +typeset -g PROMPT_SHOW_SPACER=0 +typeset -gA PROMPT_PROJECT_MATCH_CACHE + +prompt_preexec() { + case "$1" in + clear|clear\ *|reset|reset\ *) + PROMPT_SHOW_SPACER=0 + ;; + *) + PROMPT_SHOW_SPACER=1 + ;; + esac +} + +get_command_version() { + local command_name="$1" + local binary_path="" + local raw_version="" + + binary_path="$(whence -p "$command_name" 2>/dev/null || true)" + [ -n "$binary_path" ] && [ -x "$binary_path" ] || return 0 + + case "$command_name" in + python) + raw_version="$($binary_path --version 2>&1 || true)" + raw_version="${raw_version#Python }" + ;; + node) + raw_version="$($binary_path -v 2>/dev/null || true)" + raw_version="${raw_version#v}" + ;; + go) + raw_version="$($binary_path version 2>/dev/null || true)" + raw_version="${raw_version#go version go}" + raw_version="${raw_version%% *}" + ;; + rustc) + raw_version="$($binary_path -V 2>/dev/null || true)" + raw_version="${raw_version#rustc }" + raw_version="${raw_version%% *}" + ;; + *) + return 0 + ;; + esac + + [ -n "$raw_version" ] || return 0 + print -r -- "$raw_version" +} + +get_project_root() { + command git rev-parse --show-toplevel 2>/dev/null || print -r -- "$PWD" +} + +project_has_pattern() { + local project_root="" + local cache_key="" + local pattern + local matches=() + + project_root="$(get_project_root)" + cache_key="${project_root}::${(j:|:)@}" + + if [[ -n ${PROMPT_PROJECT_MATCH_CACHE[$cache_key]+x} ]]; then + [[ ${PROMPT_PROJECT_MATCH_CACHE[$cache_key]} == 1 ]] + return + fi + + for pattern in "$@"; do + matches=("${project_root}"/${~pattern}(N)) + if [ "${#matches[@]}" -gt 0 ]; then + PROMPT_PROJECT_MATCH_CACHE[$cache_key]=1 + return 0 + fi + done + + PROMPT_PROJECT_MATCH_CACHE[$cache_key]=0 + + return 1 +} + +build_git_status_segment() { + local git_status_output="" + local line="" + local staged=0 + local unstaged=0 + local untracked=0 + local status_parts=() + + command git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0 + git_status_output="$(command git status --porcelain 2>/dev/null || true)" + + if [ -z "$git_status_output" ]; then + print -r -- "%F{green}โœ“ clean%f" + return 0 + fi + + while IFS= read -r line; do + [ -n "$line" ] || continue + + if [ "${line:0:2}" = "??" ]; then + ((untracked++)) + continue + fi + + [ "${line:0:1}" = " " ] || ((staged++)) + [ "${line:1:1}" = " " ] || ((unstaged++)) + done <<< "$git_status_output" + + [ "$staged" -eq 0 ] || status_parts+=("+$staged") + [ "$unstaged" -eq 0 ] || status_parts+=("~$unstaged") + [ "$untracked" -eq 0 ] || status_parts+=("?$untracked") + + if [ "${#status_parts[@]}" -eq 0 ]; then + print -r -- "%F{green}โœ“ clean%f" + else + print -r -- "%F{yellow}${(j: :)status_parts}%f" + fi +} + +build_prompt_separator_line() { + local last_status="$1" + local line_width="${COLUMNS:-80}" + local separator_line="" + local color_prefix="" + local color_suffix="" + local i=0 + + if [ "$line_width" -lt 1 ]; then + line_width=1 + fi + + if [ "$last_status" -ne 0 ]; then + color_prefix="%F{red}" + color_suffix="%f" + fi + + while [ "$i" -lt "$line_width" ]; do + separator_line+="โ”€" + ((i++)) + done + + print -r -- "${color_prefix}${separator_line}${color_suffix}" +} + +build_prompt() { + local last_status=$? + local git_segment="" + local git_status_segment="" + local env_segment="" + local project_symbols=() + local tool_versions=() + local python_version="" + local node_version="" + local go_version="" + local rust_version="" + local context_line="" + local newline=$'\n' + local prompt_spacer="" + local separator="::" + + vcs_info + git_segment="${vcs_info_msg_0_}" + git_status_segment="$(build_git_status_segment)" + + if [ -n "${CONDA_DEFAULT_ENV:-}" ]; then + env_segment="๎˜† ${CONDA_DEFAULT_ENV}" + elif [ -n "${DIRENV_DIR:-}" ] || [ -n "${DIRENV_FILE:-}" ]; then + env_segment="๏€ฃ direnv" + fi + + if project_has_pattern '**/docker-compose*.yml' '**/docker-compose*.yaml' '**/compose*.yml' '**/compose*.yaml'; then + project_symbols+=("%F{blue}๏Œˆ%f") + fi + + if project_has_pattern '**/Makefile' '**/makefile' '**/GNUmakefile' '**/Justfile' '**/justfile' '**/.justfile'; then + project_symbols+=("%F{yellow}๏‚ญ%f") + fi + + if project_has_pattern '**/pyproject.toml' '**/requirements.txt' '**/.python-version' '**/setup.py'; then + python_version="$(get_command_version python)" + [ -z "$python_version" ] || tool_versions+=("%F{magenta}๎ˆต ${python_version}%f") + fi + + if project_has_pattern '**/package.json' '**/.nvmrc' '**/pnpm-workspace.yaml' '**/yarn.lock' '**/package-lock.json'; then + node_version="$(get_command_version node)" + [ -z "$node_version" ] || tool_versions+=("%F{green}๎œ˜ ${node_version}%f") + fi + + if project_has_pattern '**/go.mod' '**/go.work'; then + go_version="$(get_command_version go)" + [ -z "$go_version" ] || tool_versions+=("%F{cyan}๎˜ง ${go_version}%f") + fi + + if project_has_pattern '**/Cargo.toml' '**/Cargo.lock' '**/rust-toolchain.toml' '**/rust-toolchain'; then + rust_version="$(get_command_version rustc)" + [ -z "$rust_version" ] || tool_versions+=("%F{yellow}๎žจ ${rust_version}%f") + fi + + if [ -n "$git_segment" ]; then + context_line="%F{cyan}${git_segment}%f" + [ -z "$git_status_segment" ] || context_line+=" ${git_status_segment}" + fi + + if [ -n "$env_segment" ]; then + [ -z "$context_line" ] || context_line+=" %F{240}ยท%f " + context_line+="%F{green}${env_segment}%f" + fi + + if [ "${#project_symbols[@]}" -gt 0 ]; then + [ -z "$context_line" ] || context_line+=" %F{240}ยท%f " + context_line+="${(j: :)project_symbols}" + fi + + if [ "${#tool_versions[@]}" -gt 0 ]; then + [ -z "$context_line" ] || context_line+=" %F{240}ยท%f " + context_line+="${(j: :)tool_versions}" + fi + + if [ -n "$context_line" ]; then + context_line+="${newline}" + fi + + if [ "$last_status" -ne 0 ]; then + separator="%F{red}::%f" + fi + + if [ "$PROMPT_SHOW_SPACER" -eq 1 ]; then + prompt_spacer="$(build_prompt_separator_line "$last_status")${newline}" + fi + + PROMPT="${prompt_spacer}${context_line}%B%~%b ${separator} " + RPROMPT="%n@%m" +} + +add-zsh-hook preexec prompt_preexec +add-zsh-hook precmd build_prompt +build_prompt diff --git a/Zsh/.zshrc b/Zsh/.zshrc index 5067bd8..4db1ba6 100644 --- a/Zsh/.zshrc +++ b/Zsh/.zshrc @@ -6,9 +6,6 @@ export ZSH="$HOME/.oh-my-zsh" # Programming Languages Root export PROG_DIR="$HOME/.programming" -PROMPT="%B%~%b :: " -RPROMPT="%n@%m" - # Plugins plugins=(git zsh-syntax-highlighting zsh-autosuggestions sudo rclone rust nvm golang conda pyenv) @@ -78,3 +75,6 @@ fi if command -v direnv >/dev/null 2>&1; then eval "$(direnv hook zsh)" fi + +# Prompt Styling +[ -f ~/.zsh_prompt ] && source ~/.zsh_prompt