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)
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) '