Last Updated: February 25, 2016
·
581
· alexlafroscia

Use `name` from Package.json to Abbreviate ZSH Path Name

I'm a big fan of ZSH and oh-my-zsh, espectially with the gorgeous Agnoster theme. Since some of the screenshots seems to be broken, here's a screenshot of my set up. (Shown here: iTerm 2, ZSH, the Agnoster theme, and Tmux)

iTerm 2, ZSH, Agnoster theme, Tmux

One of the things that really bothered me about Agnoster, however, is that the prompt can end up getting really long since it contains the full path to the current directory. I started to fix that using ZSH's named directories feature, but really didn't want to have to set up an entry for every project that I'm working on. I realized that many, if not most, of the projects I work on have a package.json file (I write a lot of JavaScript), and that this file has a project name that could easily replace a large piece of the directory name. So, I started working on it and came up with the custom Agnoster theme below.

As you can see in the above screenshot, instead of the blue part of the prompt having the full path name, it instead just contains the name field from the package.json file. When moving into a subdirectory, the path from the root of the project (determined by the location of the .git directory) is appended to the name, so that you don't lose the context of where you are in your project.

One thing to note is that this solution depends on jq being installed, which is a command line utility for parsing JSON files. I will try to move away from this in the future, but this is working for now.

More information can also be found at this gist

My Changes

# Dir: current working directory
# Prints name of the NPM Package first, if possible
prompt_dir() {
  local name repo_path package_path extention_dirs current_dir zero

  # Get the path of the Git repo, which should have the package.json file
  if repo_path=$(git rev-parse --git-dir 2>/dev/null); then
    if [[ "$repo_path" == ".git" ]]; then
      # If the current path is the root of the project, then the package path is
      # the current directory and we don't want to append anything to represent
      # the path to a subdirectory
      package_path="."
      subdirectory_path=""
    else
      # If the current path is something else, get the path to the package.json
      # file by finding the repo path and removing the '.git` from the path
      package_path=${repo_path:0:-4}
      zero='%([BSUbfksu]|([FB]|){*})'
      current_dir=$(pwd)
      # Then, find the length of the package_path string, and save the
      # subdirectory path as a substring of the current directory's path from 0
      # to the length of the package path's string
      subdirectory_path="/${current_dir:${#${(S%%)package_path//$~zero/}}}"
    fi
  fi

  # Parse the 'name' from the package.json; if there are any problems, just
  # print the file path
  if name=$( jq -e '.name' < "$package_path/package.json" ) 2> /dev/null; then
    # Instead of printing out the full path, print out the name of the package
    # from the package.json and append the current subdirectory
    prompt_segment blue black "`echo $name | tr -d "[:punct:]"`$subdirectory_path"
  else
    prompt_segment blue black '%~'
  fi
}

Whole File

# vim:ft=zsh ts=2 sw=2 sts=2
#
# agnoster's Theme - https://gist.github.com/3712874
# A Powerline-inspired theme for ZSH
#
# # README
#
# In order for this theme to render correctly, you will need a
# [Powerline-patched font](https://github.com/Lokaltog/powerline-fonts).
#
# In addition, I recommend the
# [Solarized theme](https://github.com/altercation/solarized/) and, if you're
# using it on Mac OS X, [iTerm 2](http://www.iterm2.com/) over Terminal.app -
# it has significantly better color fidelity.
#
# # Goals
#
# The aim of this theme is to only show you *relevant* information. Like most
# prompts, it will only show git information when in a git working directory.
# However, it goes a step further: everything from the current user and
# hostname to whether the last call exited with an error to whether background
# jobs are running in this shell will all be displayed automatically when
# appropriate.

### Segment drawing
# A few utility functions to make it easy and re-usable to draw segmented prompts

CURRENT_BG='NONE'
SEGMENT_SEPARATOR='î‚°'

# Begin a segment
# Takes two arguments, background and foreground. Both can be omitted,
# rendering default background/foreground.
prompt_segment() {
  local bg fg
  [[ -n $1 ]] && bg="%K{$1}" || bg="%k"
  [[ -n $2 ]] && fg="%F{$2}" || fg="%f"
  if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then
    echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} "
  else
    echo -n "%{$bg%}%{$fg%} "
  fi
  CURRENT_BG=$1
  [[ -n $3 ]] && echo -n $3
}

# End the prompt, closing any open segments
prompt_end() {
  if [[ -n $CURRENT_BG ]]; then
    echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR"
  else
    echo -n "%{%k%}"
  fi
  echo -n "%{%f%}"
  CURRENT_BG=''
}

### Prompt components
# Each component will draw itself, and hide itself if no information needs to be shown

# Context: user@hostname (who am I and where am I)
prompt_context() {
  if [[ "$USER" != "$DEFAULT_USER" || -n "$SSH_CLIENT" ]]; then
    prompt_segment black default "%(!.%{%F{yellow}%}.)$USER@%m"
  fi
}

# Git: branch/detached head, dirty status
prompt_git() {
  local ref dirty mode repo_path
  repo_path=$(git rev-parse --git-dir 2>/dev/null)

  if $(git rev-parse --is-inside-work-tree >/dev/null 2>&1); then
    dirty=$(parse_git_dirty)
    ref=$(git symbolic-ref HEAD 2> /dev/null) || ref="➦ $(git show-ref --head -s --abbrev |head -n1 2> /dev/null)"
    if [[ -n $dirty ]]; then
      prompt_segment yellow black
    else
      prompt_segment green black
    fi

    if [[ -e "${repo_path}/BISECT_LOG" ]]; then
      mode=" <B>"
    elif [[ -e "${repo_path}/MERGE_HEAD" ]]; then
      mode=" >M<"
    elif [[ -e "${repo_path}/rebase" || -e "${repo_path}/rebase-apply" || -e "${repo_path}/rebase-merge" || -e "${repo_path}/../.dotest" ]]; then
      mode=" >R>"
    fi

    setopt promptsubst
    autoload -Uz vcs_info

    zstyle ':vcs_info:*' enable git
    zstyle ':vcs_info:*' get-revision true
    zstyle ':vcs_info:*' check-for-changes true
    zstyle ':vcs_info:*' stagedstr '✚'
    zstyle ':vcs_info:git:*' unstagedstr '●'
    zstyle ':vcs_info:*' formats ' %u%c'
    zstyle ':vcs_info:*' actionformats ' %u%c'
    vcs_info
    echo -n "${ref/refs\/heads\//î‚  }${vcs_info_msg_0_%% }${mode}"
  fi
}

prompt_hg() {
  local rev status
  if $(hg id >/dev/null 2>&1); then
    if $(hg prompt >/dev/null 2>&1); then
      if [[ $(hg prompt "{status|unknown}") = "?" ]]; then
        # if files are not added
        prompt_segment red white
        st='±'
      elif [[ -n $(hg prompt "{status|modified}") ]]; then
        # if any modification
        prompt_segment yellow black
        st='±'
      else
        # if working copy is clean
        prompt_segment green black
      fi
      echo -n $(hg prompt "☿ {rev}@{branch}") $st
    else
      st=""
      rev=$(hg id -n 2>/dev/null | sed 's/[^-0-9]//g')
      branch=$(hg id -b 2>/dev/null)
      if `hg st | grep -q "^\?"`; then
        prompt_segment red black
        st='±'
      elif `hg st | grep -q "^(M|A)"`; then
        prompt_segment yellow black
        st='±'
      else
        prompt_segment green black
      fi
      echo -n "☿ $rev@$branch" $st
    fi
  fi
}

# Dir: current working directory
# Prints name of the NPM Package first, if possible
prompt_dir() {
  local name repo_path package_path extention_dirs current_dir zero

  # Get the path of the Git repo, which should have the package.json file
  if repo_path=$(git rev-parse --git-dir 2>/dev/null); then
    if [[ "$repo_path" == ".git" ]]; then
      # If the current path is the root of the project, then the package path is
      # the current directory and we don't want to append anything to represent
      # the path to a subdirectory
      package_path="."
      subdirectory_path=""
    else
      # If the current path is something else, get the path to the package.json
      # file by finding the repo path and removing the '.git` from the path
      package_path=${repo_path:0:-4}
      zero='%([BSUbfksu]|([FB]|){*})'
      current_dir=$(pwd)
      # Then, find the length of the package_path string, and save the
      # subdirectory path as a substring of the current directory's path from 0
      # to the length of the package path's string
      subdirectory_path="/${current_dir:${#${(S%%)package_path//$~zero/}}}"
    fi
  fi

  # Parse the 'name' from the package.json; if there are any problems, just
  # print the file path
  if name=$( jq -e '.name' < "$package_path/package.json" ) 2> /dev/null; then
    # Instead of printing out the full path, print out the name of the package
    # from the package.json and append the current subdirectory
    prompt_segment blue black "`echo $name | tr -d "[:punct:]"`$subdirectory_path"
  else
    prompt_segment blue black '%~'
  fi
}

# Virtualenv: current working virtualenv
prompt_virtualenv() {
  local virtualenv_path="$VIRTUAL_ENV"
  if [[ -n $virtualenv_path && -n $VIRTUAL_ENV_DISABLE_PROMPT ]]; then
    prompt_segment blue black "(`basename $virtualenv_path`)"
  fi
}

# Status:
# - was there an error
# - am I root
# - are there background jobs?
prompt_status() {
  local symbols
  symbols=()
  [[ $RETVAL -ne 0 ]] && symbols+="%{%F{red}%}✘"
  [[ $UID -eq 0 ]] && symbols+="%{%F{yellow}%}âš¡"
  [[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{cyan}%}âš™"

  [[ -n "$symbols" ]] && prompt_segment black default "$symbols"
}

## Main prompt
build_prompt() {
  RETVAL=$?
  prompt_status
  prompt_virtualenv
  prompt_context
  prompt_dir
  prompt_git
  prompt_hg
  prompt_end
}

PROMPT='%{%f%b%k%}$(build_prompt) '