DEV Community

Jane Ori
Jane Ori

Posted on

Customizing the MacOS Terminal with help from AI

After spending a year traveling the world, I've acquired a new Macbook Air and had to set up my terminal interface again.

I do not normally enjoy this process but I loved it this time.
A continuous conversation with AI (Gemini specifically) lead to every random idea I had becoming realized in my .zshrc file.

There are some goodies in here I suspect you'll enjoy too.

Basic settings and imports

# # # # # # # # # # # # #
# import things we need #
# # # # # # # # # # # # #

# load 'live' hooks to execute things every call,
# load TAB completion
autoload -Uz add-zsh-hook compinit

# # # # # # # # # # # # #
# Enable Tab Completion #
# # # # # # # # # # # # #

# 1. Prevent duplicate entries in paths
typeset -U FPATH PATH
# 2. Add Homebrew to FPATH for even better tab completion (if it's not present already)
FPATH="/opt/homebrew/share/zsh/site-functions:${FPATH}"
# 3. Enable TAB completion
compinit

# # # # # # # # # # # # #
# Toggling Misc options #
# # # # # # # # # # # # #

# make ls pretty
export CLICOLOR=1
Enter fullscreen mode Exit fullscreen mode

Basic aliases

# make the current terminal tab aware of things just installed or changed
alias reload='source ~/.zshrc'

# shortcut to go to my main projects folder
alias pj="cd ~/Desktop/PropJockey"

# Launch Gemini in a frameless chrome window
alias gemini="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --app=https://gemini.google.com/app"
Enter fullscreen mode Exit fullscreen mode

Whenever you modify the .zshrc file, which I did about a hundred times during this, you either have to open a new tab or run the command I've aliased with a simple "reload".

I wound up installing Gemini using Chrome's built in "Install as App" feature which had the added benefit of getting Gemini in my dock. 🤙 Sweet.

Chrome menu →

Lock Function

One of the first things I do when setting up a mac is turn sleep into hibernate. When I close the lid, I'm done for the night, no reason to sleep.

sudo pmset -a hibernatemode 25

Unfortunately, no matter how you've configured your lock screen settings, after only a minute idle on the lock screen, the mac gets bored and falls asleep, which in my case hibernates it and I have to sit through memory reloading from the harddrive to log back in.

The fix:

First create a shortcut using the Shortcuts app that ships with mac. It's great

Add a new shortcut, search for the lock screen action, add it, name it "LockMac", and you're ready for the script

Shortcuts app screenshot of the lock mac shortcut

# Lock the screen without sleeping the system and keep it awake until ctrl + c to quit
# alias lock='shortcuts run "LockMac" && caffeinate -i -d -t 3600'
# ^ great but this is better because I don't have to ctrl + c!
lock() {
  # Start caffeinate in the background, disown it, hide its output, and save its Process ID (PID)
  caffeinate -i -d -t 3600 &>/dev/null &!
  local caff_pid=$!

  shortcuts run "LockMac"

  # Give macOS a bit to register the locked state
  sleep 3

  # The Magic Loop: Poll the Mac's hardware registry. 
  # This specific string ONLY exists while the screen is actively locked.
  while ioreg -n Root -d1 | grep -q "CGSSessionScreenIsLocked"; do
    sleep 2
  done

  # You're back! Kill the caffeinate process silently
  kill $caff_pid 2>/dev/null

  # Print a Bifrost-styled welcome back message
  # print -P "%F{cyan}🛸 Welcome back. Sleep prevention disabled.%f"
}
Enter fullscreen mode Exit fullscreen mode

I always have at least one terminal open so I just type lock and I'm off to the bathroom in my hostel. The mac stays wide awake the whole time so I can simply type my password and continue.

I use this often

screenshot of a terminal tab with the lock command ran many times

Automatic Node & NPM version mismatch warning

One of the things any seasoned node developer has done time and time again is accidentally run their project with the wrong version, or worse, installed packages.

I wanted to prevent that as best as I could so I requested a function that alerts the node version and the version specified in .nvmrc or package.json with pass/fail indicators based on a comparison of the values.

# Checks current versions against .nvmrc or package.json requirements
nvi() {
  # 1. Ensure Node is actually installed
  if ! command -v node &> /dev/null; then
    print -P "%F{red}✘ Node.js is not installed or not in PATH.%f"
    return 1
  fi

  local current_node=$(node -v)
  local current_npm=$(npm -v)
  local req_node=""
  local req_npm=""

  # 2. Look for project requirements (.nvmrc takes priority for node)
  if [[ -f ".nvmrc" ]]; then
    req_node=$(cat .nvmrc | tr -d '\n' | tr -d '\r')
  fi

  # Fallback to package.json engines if it exists
  if [[ -f "package.json" ]]; then
    if [[ -z "$req_node" ]]; then
      # Safely extract engines.node using Node itself
      req_node=$(node -e "try { console.log(require('./package.json').engines.node || '') } catch(e) {}" 2>/dev/null)
    fi
    # Extract engines.npm
    req_npm=$(node -e "try { console.log(require('./package.json').engines.npm || '') } catch(e) {}" 2>/dev/null)
  fi

  # 3. Print Current Versions
  print -P "%F{magenta}Current Node:%f %F{cyan}${current_node}%f"
  print -P "%F{magenta}Current NPM:%f  %F{cyan}v${current_npm}%f"

  # 4. Evaluate and Print Requirements (if they exist)
  if [[ -n "$req_node" || -n "$req_npm" ]]; then
    echo ""
    print -P "%F{242}Project Requirements:%f"

    # Node Requirement Check
    if [[ -n "$req_node" ]]; then
      # Extract just the major version number for a reliable comparison
      local clean_current=$(echo "$current_node" | grep -oE '[0-9]+' | head -1)
      local clean_req=$(echo "$req_node" | grep -oE '[0-9]+' | head -1)

      if [[ "$clean_current" == "$clean_req" || "$current_node" == *"$req_node"* ]]; then
        print -P "%F{green}✔ Node:%f ${req_node}"
      else
        print -P "%F{red}✘ Node:%f ${req_node} (Mismatch detected)"
      fi
    fi

    # NPM Requirement Check
    if [[ -n "$req_npm" ]]; then
      local clean_current_npm=$(echo "$current_npm" | grep -oE '[0-9]+' | head -1)
      local clean_req_npm=$(echo "$req_npm" | grep -oE '[0-9]+' | head -1)

      if [[ "$clean_current_npm" == "$clean_req_npm" || "$current_npm" == *"$req_npm"* ]]; then
        print -P "%F{green}✔ NPM:%f  ${req_npm}"
      else
        print -P "%F{red}✘ NPM:%f  ${req_npm} (Mismatch detected)"
      fi
    fi
  fi
}
Enter fullscreen mode Exit fullscreen mode

Just run nvi (Node Version Information) and we're golden...

...but we could be platinum if we called this automatically when you change directory into one with node expected!

# # # # # # # # # # # # #
# Run nvi automatically #
# # # # # # # # # # # # #

auto_check_node_env() {
  # Check if we are in a folder with Node files
  if [[ -f "package.json" || -f ".nvmrc" ]]; then
    echo "" # Add a blank line for visual breathing room
    nvi
  fi
}
# Attach it to the 'Change Directory' hook
add-zsh-hook chpwd auto_check_node_env
Enter fullscreen mode Exit fullscreen mode

🤌 Great success.

Now every time I cd into the root of one of my node projects, it runs this command so I'll be shown without needing to remember to check. I stopped short of letting it nvm into the "right" version automatically hah

The Beautifully Colorful Path Highlighting and Git Info

First, I installed this color theme called Bifrost:

A terminal color theme pallet display with corresponding color codes

Then, for the current directory path, I wanted to grey out the roots of the path, highlight the project name by detecting if it's in a git repository, then highlight everything after the project name in a different color.

The end result is the most significant parts being easy to distinguish and effortlessly identified at a glance. I. Love. It.

Screenshot of my terminal showing the current path dissected into colors

There's all kinds of other stuff in here like basic git status indicators, the branch, separate counts of staged and unstaged files, etc.

The first part of the prompt is something that is slightly redundant indicating complete vs a failed reason, followed by a completion timestamp of the previous command AND the number of seconds the previous command took (if it was more than one).

A couple line breaks after that and the typical PROMPT shows up.

# # # # # # # # # # # # #
# Meta info of last cmd #
# # # # # # # # # # # # #

# 1. Capture the start time of a command
preexec() {
  timer=${timer:-$SECONDS}
}
# 2. Calculate the difference and format it
calculate_timer() {
  if [[ -n "$timer" ]]; then
    timer_show=$(($SECONDS - $timer))
    if [[ $timer_show -ge 1 ]]; then
        export ELAPSED="%F{yellow}${timer_show}s %f"
    else
        export ELAPSED=""
    fi
    unset timer
  else
    # THE FIX: If no command was run, clear the old time!
    export ELAPSED=""
  fi
}
add-zsh-hook precmd calculate_timer


# # # # # # # # # # # # #
# Best git aware prompt #
# # # # # # # # # # # # #

# look inside the variables every time the PROMPT is printed
setopt PROMPT_SUBST

# Fetch the Mac Display Name
DISPLAY_NAME=$(id -F)

# Define the raw escape codes for Dim and Reset
DIM=$'\e[2m'
RESET=$'\e[22m'
# %{${DIM}%}%F{yellow}%~%f%{${RESET}%}

set_surgical_path() {
  local exit_code=$? # capture exit code of previous command

  local repo_root
  # Get the absolute path to the repo root
  repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
  local GIT_INFO="" # Start with a blank slate

  if [[ -n "$repo_root" ]]; then
    local full_path=$(pwd)

    # 1. Everything BEFORE the repo root
    # We take the directory of the repo_root (its parent)
    local parent_dir=$(dirname "$repo_root")
    local prefix="${parent_dir}/"
    prefix="${prefix/#$HOME/~}" # Clean up Home path

    # 2. The Repo folder itself
    local repo_name=$(basename "$repo_root")

    # 3. Everything AFTER the repo root
    # We remove the repo_root path from the full_path
    local suffix="${full_path#$repo_root}"

    # Assemble: Dim Prefix + Bright Repo + Dim Suffix
    DYNAMIC_PATH="%{${DIM}%}%F{white}${prefix}%f%{${RESET}%}"
    DYNAMIC_PATH+="%F{magenta}${repo_name}%f"
    DYNAMIC_PATH+="%F{blue}${suffix}%f"
    # Get the branch name, fallback to short hash if in detached HEAD
    local git_branch=$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null)

    # Check for Action States (Merging / Rebasing)
    local git_action=""
    if [[ -f "$repo_root/.git/MERGE_HEAD" ]]; then
      git_action="%F{yellow}(merge)%f "
    elif [[ -d "$repo_root/.git/rebase-merge" || -d "$repo_root/.git/rebase-apply" ]]; then
      git_action="%F{yellow}(rebase)%f "
    fi

    # Check Clean/Dirty Status
    local git_state=""
    local git_status=$(git status --porcelain 2>/dev/null)

    if [[ -z "$git_status" ]]; then
      git_state="%F{green}✔%f" # Perfectly clean
    else
      # Check for staged changes (+) and unstaged/untracked files (*)# Count lines matching staged (column 1) and unstaged/untracked (column 2)
      local staged_count=$(echo "$git_status" | grep -c '^[AMRCD]')
      local unstaged_count=$(echo "$git_status" | grep -c '^.[MD?]')

      # If there are staged files, add the yellow count
      if [[ "$staged_count" -gt 0 ]]; then
        git_state+="%F{yellow}+${staged_count}%f"
      fi

      # If there are unstaged/untracked files, add the red count
      if [[ "$unstaged_count" -gt 0 ]]; then
        # Add a space for readability if we also have staged files (e.g., +2 *3)
        [[ -n "$git_state" ]] && git_state+=" "
        git_state+="%F{red}*${unstaged_count}%f"
      fi
    fi

    # Assemble the final Git string
    GIT_INFO=" on %F{cyan}${git_branch}%f ${git_action}${git_state}"
  else
    # Not in a repo: Show the whole path dimmed
    DYNAMIC_PATH="%{${DIM}%}%F{magenta}%~%f%{${RESET}%}"
  fi

  # Define the Prompt
  # %D{%H:%M:%S} = Hour:Minute:Second
  # %n = username
  # %~ = current directory (shortened)
  # %# = % for users, # for root (I replaced this with: %(!.#.$))
  # %(!.#.$) = # for root, $ for users
  # The %(?.X.Y) syntax means: If last exit code was 0, show X, else show Y ## %(?.%F{green}Completed.%F{red}Stopped)%f

  local nl=$'\n' # ${nl}
  local status_line=""
  if [[ $HISTCMD -ne $LAST_HISTCMD ]]; then
    local status_text=""

    # 2. The Exit Code Skyscraper
    case $exit_code in
      0) status_text="%F{green}Completed" ;;

      # The "Oh, I meant to do that" code
      130) status_text="%F{yellow}Stopped" ;; # SIGINT (Ctrl+C)

      # The "Wait, what just happened?" codes
      126) status_text="%F{magenta}Denied (126)" ;; # Permission denied / Not executable
      127) status_text="%F{magenta}Not Found (127)" ;; # Typo in command name

      # The "Violent Crash" codes
      137) status_text="%F{red}Killed / Out of Memory (137)" ;; # SIGKILL (Often the Out-Of-Memory killer)
      139) status_text="%F{red}Segfault (139)" ;; # SIGSEGV (Memory access violation)
      143) status_text="%F{yellow}Terminated (143)" ;; # SIGTERM (Graceful kill command)

      # The Catch-All for standard script errors
      *) status_text="%F{red}Exit code: ${exit_code}" ;;
    esac

    status_line="${nl}%{${DIM}%}${status_text}%f%{${RESET}%} %F{242}at %D{%H:%M:%S}%f ${ELAPSED}${nl}${nl}"
  fi
  LAST_HISTCMD=$HISTCMD

  local name_path_git="%F{magenta}${DISPLAY_NAME}%f in ${DYNAMIC_PATH}${GIT_INFO}"
  local input_indicator="${nl}%{${DIM}%}%F{yellow}%(!.#.$)%f%{${RESET}%}"

  PROMPT="${status_line}${name_path_git}${input_indicator} "
}

# 4. Tell the surgical path function to run before every prompt
add-zsh-hook precmd set_surgical_path
Enter fullscreen mode Exit fullscreen mode

The final touch

Place a little 👽 Extra Terrestrial emoji in right prompt so I can scroll up the wall of text and find my inputs a little more easily.

# # # # # # # # # # # # #
# A little splash of me #
# # # # # # # # # # # # #

RPROMPT='👽'


# # # # # # # # # # # # #
# This was a triumph. ♫ #
# # # # # # # # # # # # #

# I'm making a note here: Huge success.


# # # # # # # # # # # # #
# Things added by tools #
# # # # # # # # # # # # #

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
Enter fullscreen mode Exit fullscreen mode

Open Contact 👽

Please do reach out if you need help with any of this, have feature requests, or want to share what you've created!

PropJockey.io CodePen DEV Blog GitHub Mastodon
PropJockey.io CodePen DEV Blog GitHub Mastodon

🦋@JaneOri.PropJockey.io

𝕏@Jane0ri

My heart is open to receive abundance in all forms, flowing to me in many expected and unexpected ways.

Venmo
https://account.venmo.com/u/JaneOri

PayPal Donate to PropJockey
https://www.paypal.com/cgi-bin/webscr...

Ko-Fi
https://ko-fi.com/janeori

ETH
0x674D4191dEBf9793e743D21a4B8c4cf1cC3beF54

BTC
bc1qe2ss8hvmskcxpmk046msrjpmy9qults2yusgn9

XRP (XRPL Mainnet)
X7zmKiqEhMznSXgj9cirEnD5sWo3iZPqbNPChdEKV9sM9WF

XRP (Base Network)
0xb4eBF3Ec089DE7820ac69771b9634C9687e43F70

Top comments (0)