content/blog/2010/02/my-extravagant-zsh-prompt.html @ 9c925e190ed9

Add YUI and start the Django blog entry.
author Steve Losh <steve@stevelosh.com>
date Sun, 06 Feb 2011 18:26:27 -0500
parents def464696a83
children 25ec02499fbc
{% extends "_post.html" %}

{% hyde
    title: "My Extravagant Zsh Prompt"
    snip: "It’s big, but my monitor isn’t running out of ink."
    created: 2010-02-01 01:05:00
    flattr: true
%}

{% block article %}

I spend a lot of time in a Terminal window at a command line. Up until about a
month ago I was using [bash][] for my shell. I decided to try switching to
[Zsh][] after hearing a lot of good things about it and I'm very happy with
the change.

A few days ago I [tweeted][] my current Zsh prompt and the general response
was: "Cool, but how did you do it?" I promised to write more about it when I
got some free time, and it looks like that time is now.

**One quick note:** This entry is about the prompt that *I* find useful. You
are not me, so you'll almost certainly have different needs. That's great!
Take this prompt and hack it to make whatever works for you. If you're feeling
generous you should find me on [Twitter][twsl] and show me what you changed --
I might want to steal/appropriate your changes.

**Another quick note:** I've customized the colors of my Terminal. They're
based on the [Monokai][] TextMate theme, and I think they look very nice. The
colors will be different for you. If you want colors like mine you should take
a look at the [entry][candy] I wrote about it.

[bash]: http://en.wikipedia.org/wiki/Bash
[Zsh]: http://www.zsh.org/
[tweeted]: http://twitter.com/stevelosh/status/8259755151
[Monokai]: http://www.monokai.nl/blog/2006/07/15/textmate-color-theme/
[candy]: /blog/2009/03/candy-colored-terminal/
[twsl]: {{links.twsl}}

[TOC]

Why Should You Care?
--------------------

Many people use the command line every day and never bother to customize their
prompts. It's just a bit of text that's printed before every command -- why
should you waste time learning how to customize it?

I feel that the *most* important aspect of my command line work is the prompt.
Your prompt is something you'll see *literally* thousands of times a day. Why
not take 30 minutes and customize it into something that's much more useful?

I firmly believe I'm right in thinking this way. 30 minutes (or even 3 hours)
of customization to make your work easier for the **rest of your life** (or at
least however long you stay with your current shell) *is* worth it.

This Entry is About Zsh Prompts
-------------------------------

As I mentioned earlier I now use Zsh as my command line shell. If you use bash
(the default on most modern systems) the syntax to create the shell will be
different.

Really, though, you should give Zsh a try.

Screenshots
-----------

I can write all day, but in the end I think a screenshot will be more helpful
than anything I write.

Here's a sample of my current Zsh prompt:

![My Zsh Prompt](/media/images{{parent_url}}/zsh-prompt.png "My Zsh Prompt")

And here's a version of that screenshot with some comments added to explain
things:

![My Zsh Prompt with Comments](/media/images{{parent_url}}/zsh-prompt-comments.png "My Zsh Prompt with Comments")

If you want to know *how* I created that prompt, read on!

Oh My Zsh!
----------

The first thing I'd do when starting out with Zsh is to install
[robbyrussell's][robr] [oh-my-zsh][omz]. It's a great collection of very
useful Zsh configurations and aliases which set some sane defaults and make
working with Zsh much nicer.

The instructions for installing it are on the project page.

[robr]: http://github.com/robbyrussell
[omz]: http://github.com/robbyrussell/oh-my-zsh

Define a "Theme"
----------------

oh-my-zsh uses [theme files][themes] to specify how your prompt looks.

An oh-my-zsh theme file is just a Zsh script which sets a few variables that
the oh-my-zsh scripts use to render your prompt.

Go ahead and create a new theme file for your prompt. Call it whatever you
like. I called my theme "prose" because it's more verbose than the others.
Copy the contents of one of the other themes to get started.

The `PROMPT` variable is what you'll be modifying to change how your prompt
looks. That's the `PROMPT=whatever` line of the theme file.

Once you're done making your awesome prompt you should fork oh-my-zsh on
GitHub and commit/push your new theme so other people can see and use your
prompt.

[themes]: http://github.com/robbyrussell/oh-my-zsh/tree/master/themes/

Username and Hostname
---------------------

The first two pieces of my prompt are the simplest: username and hostname. I
SSH between machines pretty frequently so I find it nice to have these in my
prompt to remind me of where I am.

These are things that you'll find in many, many Zsh prompts. For more
information about this kind of stuff check out [this page][zshpr].

After adding in the colors this piece of my `PROMPT` variable looks like this:

    :::text
    %{$fg[magenta]%}%n%{$reset_color%} at %{$fg[yellow]%}%m%{$reset_color%}

[zshpr]: http://www.acm.uiuc.edu/workshops/zsh/prompt/escapes.html

Current Directory
-----------------

I like to have the current working directory displayed in my prompt. Zsh has
two built-in ways to show the current directory:

* **%d** shows the *entire* path.
* **%~** shows the path with any variables replaced.

Unfortunately neither of these work for me.

If I'm in a directory that's under my home directory I want to see `~` instead
of the full path. This would seem to mean that I should use `%~`, but there's
a catch.

The `%~` sequence also replaces any directories that happen to be environment
variables. That means if I'm in something under my `virtualenvs` directory
I'll see `~WORKON_HOME/venv_name` which is extremely ugly.

To get around this I defined a simple function that implements the behavior I
want.

To add this to your prompt you'll first need to add the following function to
your theme:

    :::bash
    function collapse_pwd {
        echo $(pwd | sed -e "s,^$HOME,~,")
    }

Then add `$(collapse_pwd)` to your `PROMPT` variable wherever you'd like the
directory to show up.

After adding in the colors this piece of my `PROMPT` variable looks like this:

    :::text
    in %{$fg_bold[green]%}$(collapse_pwd)%{$reset_color%}

**UPDATE:** In a comment Juliano pointed out a better way to do this. I've
gotten rid of the `collapse_pwd` function and put this into my `PROMPT`
variable instead:

    :::bash
    ${PWD/#$HOME/~}

My Right-Prompt: Battery Capacity
---------------------------------

The next piece of my prompt I'll talk about is the most stand-alone one: the
battery charge.

I work exclusively on laptops. I don't own a desktop machine -- I have a
Macbook, Macbook Pro and Asus EEE PC and my work machine is a Macbook Pro. I
work from coffeeshops and client meetings pretty often, so it's nice to have a
reminder of my remaining battery power to know when I need to plug in.

On the right-hand side of the prompt in the screenshot you can see a series of
green triangles. These represent my current battery charge and they turn to
empty triangles as the battery becomes depleted. Once the battery reaches 60%
they turn yellow and once it reaches 40% they turn red.

To do this I wrote a *very* simple Python script to output my current battery
capacity. The entire script is shown below.

    :::python
    #!/usr/bin/env python
    # coding=UTF-8
    
    import math, subprocess
    
    p = subprocess.Popen(["ioreg", "-rc", "AppleSmartBattery"], stdout=subprocess.PIPE)
    output = p.communicate()[0]
    
    o_max = [l for l in output.splitlines() if 'MaxCapacity' in l][0]
    o_cur = [l for l in output.splitlines() if 'CurrentCapacity' in l][0]
    
    b_max = float(o_max.rpartition('=')[-1].strip())
    b_cur = float(o_cur.rpartition('=')[-1].strip())
    
    charge = b_cur / b_max
    charge_threshold = int(math.ceil(10 * charge))
    
    # Output
    
    total_slots, slots = 10, []
    filled = int(math.ceil(charge_threshold * (total_slots / 10.0))) * u'▸'
    empty = (total_slots - len(filled)) * u'▹'
    
    out = (filled + empty).encode('utf-8')
    import sys
    
    color_green = '%{%}'
    color_yellow = '%{%}'
    color_red = '%{%}'
    color_reset = '%{%}'
    color_out = (
        color_green if len(filled) > 6
        else color_yellow if len(filled) > 4
        else color_red
    )
    
    out = color_out + out + color_reset
    sys.stdout.write(out)

**Note:** The `color_*` variables contain UTF-8 characters which don't show up
in a normal web browser. They *should* be preserved when copying and pasting
from a decent browser. If the colors don't work find me on [Twitter][twsl] and
I'll put the raw script up somewhere so you can download it.

**UPDATE:** [ehamberg][] [sent me][ehbtweet] the beginnings of a
Linux-compatible battery capacity script. If you flesh it out and make it into
a complete script please let me know and I'll add it here.

[ehamberg]: http://twitter.com/ehamberg/
[ehbtweet]: http://twitter.com/ehamberg/status/8503213690

I've put this script in a file named `batcharge.py` in my `~/bin/` directory.
You can of course name it and place it anything/anywhere you like.

No, it's not perfect. No, I don't care. It gets the job done and any
brainpower I could spend on making it more efficient could be better spent on
writing or working on something else.

If you want to improve it, feel free and *please* send me the link to your work on
[Twitter][twsl] so I can use it!

Now that you've got the script, it's time to add it to your prompt.

If you want this to appear on the right-hand side of your screen (like mine
does) you'll need to add two things to your theme file. The first is a
function that calls the script:

    :::bash
    function battery_charge {
        echo `$BAT_CHARGE` 2>/dev/null
    }

After that you'll need to define the Zsh `RPROMPT` variable:

    :::bash
    RPROMPT='$(battery_charge)'

Once those are in your theme file any new Terminal windows you open should
show the battery capacity to the right of your prompt!

Repository Types
----------------

In my personal work and my full-time job I work with both
[Mercurial][mercurial] and [git][] repositories. I find it helpful to have a
visual reminder of what kind of repository I'm working in.

To do this I've replaced the standard `$` character in my prompt with
different Unicode characters depending on what kind of repository I'm
currently in:

* `○` means "I'm not in any repository."
* `☿` means "I'm in a Mercurial repository."
* `±` means "I'm in a git repository."

Those particular characters are meant to reflect the logos of the various
projects. You can of course change them to anything that makes sense to you.

[mercurial]: {{links.mercurial}}
[git]: {{links.git}}

To add this feature to your prompt you'll need to do two things in your theme
file. First, add the following function:

    :::bash
    function prompt_char {
        git branch >/dev/null 2>/dev/null && echo '±' && return
        hg root >/dev/null 2>/dev/null && echo '☿' && return
        echo '○'
    }

Then add `$(prompt_char)` somewhere inside your `PROMPT` variable -- wherever
you want the character to be displayed (probably at the end).

Mercurial Repository Information
--------------------------------

I use Mercurial at work and for my own personal projects and I find it very
handy to have some information about the current repository right in my
prompt. It keeps me "oriented," especially when I'm using [MQ][] and
manipulating the state of a repo.

[MQ]: http://mercurial.selenic.com/wiki/MqExtension

In the past I toyed around with some shell scripts to do this but eventually I
got fed up with the poor performance and horrible readability of that
solution.

To fix this I wrote a small Mercurial extension called [hg-prompt][]. It adds
an `hg prompt` command to Mercurial that will let you output information about
the current repository. It's designed especially for use in shell prompts
(hence the name).

I open sourced the code a while ago and since then a few people have made
feature requests and/or contributed patches. It's grown into a nice, flexible
little tool. You can check out the [documentation][hgpdoc] to see all the
things it can do.

[hg-prompt]: /projects/hg-prompt/
[hgpdoc]: http://sjl.bitbucket.org/hg-prompt/

I only use a few of the features myself:

* The current repository branch.
* The "dirty" state of the repo (whether there are untracked/uncommitted files).
* Any tags pointing at the current revision.
* An "update" character to show whether running `hg update` would do anything.
* A list of MQ patches (if any are present).

To put this into my prompt I've added a function to my theme:

    :::bash
    function hg_prompt_info {
        hg prompt --angle-brackets "\
    < on <branch>>\
    < at <tags|, >>\
    <status|modified|unknown><update><
    patches: <patches|join( → )>>" 2>/dev/null
    }

Inside my `PROMPT` variable I use `$(hg_prompt_info)` to show the information.
The `2>/dev/null` makes sure no output is shown if I don't happen to be in a
Mercurial repository.

Of course this isn't nearly as pretty or readable without color. Unfortunately
adding color codes makes the actual code almost unreadable:

    :::bash
    function hg_prompt_info {
        hg prompt --angle-brackets "\
    < on %{$fg[magenta]%}<branch>%{$reset_color%}>\
    < at %{$fg[yellow]%}<tags|%{$reset_color%}, %{$fg[yellow]%}>%{$reset_color%}>\
    %{$fg[green]%}<status|modified|unknown><update>%{$reset_color%}<
    patches: <patches|join( → )|pre_applied(%{$fg[yellow]%})|post_applied(%{$reset_color%})|pre_unapplied(%{$fg_bold[black]%})|post_unapplied(%{$reset_color%})>>" 2>/dev/null
    }

**Small note:** My favorite part of this prompt is that applied MQ patches are
orange while unapplied patches are grey. It makes it very obvious what's going
on.

Showing all the MQ patches isn't something that will work for everyone. If you
work on repositories that ever have more than 5 or 6 patches at a time the
output is going to be too overwhelming. There are some other MQ-related
hg-prompt keywords that you might find useful though.

Git Repository Information
--------------------------

Unfortunately I can't *always* use Mercurial. Many projects that I contribute
to use git.

I could try to use the [hg-git][] extension to let me use Mercurial to work
with them, but the extension just isn't fast or mature enough for my taste.
I've ended up buckling down and jumping down the rabbit hole that is git's
CLI.

[hg-git]: http://hg-git.github.com/

Git doesn't seem to have any equivalent to hg-prompt, but I still wanted to
have some information in my prompt so I don't get disoriented. oh-my-zsh has a
bit of support for this, but it wasn't quite enough for me so I [hacked
something together][omzgitcommit] to add the last bit of functionality I
wanted.

[omzgitcommit]: http://github.com/sjl/oh-my-zsh/commit/3d22ee248c6bce357c018a93d31f8d292d2cb4cd

When I'm in a git repository my prompt now shows:

* The current git branch.
* Whether there are any changes in the index.
* Whether there are any changes *not* in the index.

Adding this to your prompt is very simple because oh-my-zsh has built-in
support for it. All you need to do is define a few variables in your theme:

    :::bash
    ZSH_THEME_GIT_PROMPT_PREFIX=" on %{$fg[magenta]%}"
    ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%}"
    ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[green]%}!"
    ZSH_THEME_GIT_PROMPT_UNTRACKED="%{$fg[green]%}?"
    ZSH_THEME_GIT_PROMPT_CLEAN=""

Once you do that you can put `$(git_prompt_info)` into your `PROMPT` variable
to make it show up.

**Note:** The `ZSH_THEME_GIT_PROMPT_UNTRACKED` variable is the part I added.
If your want to use that one (to be able to see indexed and unindexed changes
separately) you'll need to use [my fork of oh-my-zsh][omzfork].

[omzfork]: http://github.com/sjl/oh-my-zsh/

The Whole Thing
---------------

Here's what my new "prose" theme file currently looks like in its entirety:

    :::bash
    function collapse_pwd {
        echo $(pwd | sed -e "s,^$HOME,~,")
    }
    
    function prompt_char {
        git branch >/dev/null 2>/dev/null && echo '±' && return
        hg root >/dev/null 2>/dev/null && echo '☿' && return
        echo '○'
    }
    
    function battery_charge {
        echo `$BAT_CHARGE` 2>/dev/null
    }
    
    function virtualenv_info {
        [ $VIRTUAL_ENV ] && echo '('`basename $VIRTUAL_ENV`') '
    }
    
    function hg_prompt_info {
        hg prompt --angle-brackets "\
    < on %{$fg[magenta]%}<branch>%{$reset_color%}>\
    < at %{$fg[yellow]%}<tags|%{$reset_color%}, %{$fg[yellow]%}>%{$reset_color%}>\
    %{$fg[green]%}<status|modified|unknown><update>%{$reset_color%}<
    patches: <patches|join( → )|pre_applied(%{$fg[yellow]%})|post_applied(%{$reset_color%})|pre_unapplied(%{$fg_bold[black]%})|post_unapplied(%{$reset_color%})>>" 2>/dev/null
    }
    
    PROMPT='
    %{$fg[magenta]%}%n%{$reset_color%} at %{$fg[yellow]%}%m%{$reset_color%} in %{$fg_bold[green]%}$(collapse_pwd)%{$reset_color%}$(hg_prompt_info)$(git_prompt_info)
    $(virtualenv_info)$(prompt_char) '
    
    RPROMPT='$(battery_charge)'
    
    ZSH_THEME_GIT_PROMPT_PREFIX=" on %{$fg[magenta]%}"
    ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%}"
    ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[green]%}!"
    ZSH_THEME_GIT_PROMPT_UNTRACKED="%{$fg[green]%}?"
    ZSH_THEME_GIT_PROMPT_CLEAN=""

The file is also [on GitHub][prosetheme]. Remember, you'll need
[my fork of oh-my-zsh][omzfork] to use the `ZSH_THEME_GIT_PROMPT_UNTRACKED`
feature.

[prosetheme]: http://github.com/sjl/oh-my-zsh/blob/master/themes/prose.zsh-theme

A shell prompt is something that we'll each see thousands of times a day, so
it's something that you should take the time to customize into a useful tool.
Feel free to use my prompt as a starting point or just as a source of ideas.

If you have any questions, suggestions, or examples of your own please fine me on
[Twitter][twsl] -- I'd love to hear them!

{% endblock %}