DEV Community

Cover image for Using Starship For Terminal Prompt Goodness
Adam Parkin
Adam Parkin

Posted on • Originally published at codependentcodr.com on

Using Starship For Terminal Prompt Goodness

Recently I had the good fortune of attending the 2021 iteration of the Pycascades conference, and there was at least one talk that mentioned Starship and at least one other where the presenter happened to be using it. For those not in the know, Starship is a cross-shell compatible terminal prompt generator written in Rust that is crazy fast and crazy customizable.

I was intrigued, and decided to take my (reasonably sophisticated) Bash prompt and Starship-ify it. In this post I'll outline some of the things I went through, lessons learned, and hopefully impart some advice on how to do things with it.

To begin with, here was my old bash prompt:

My Old Prompt

That prompt has a bunch of things in it, and shows:

  • exit code of the last command (0 in this case)
  • the current day & time, along with the TZ info
  • the time since I last rebooted (2 days in this case)
  • my current directory
  • the name of the current branch I’m on (starship in this case) and the asterisk to indicate there’s uncommitted changes
  • The lambda symbol as my input symbol (not a Half-Life reference, more a reference to my functional programming days)

Not shown in that screenshot is that when I have a Python virtual environment activated it also gets displayed in the prompt.

The "code" for this that resided in my .bashrc file:

function uptimeinfo {
    uptime | perl -ne 'if(/\d\s+up(.\*),\s+\d+\s+users/) { $s = $1; $s =~ s/^\s+|\s+$//g; print $s; }'
}

function proml {
  local BLACK="\[\033[0;30m\]"
  local GRAY="\[\033[1;30m\]"
  local RED="\[\033[0;31m\]"
  local LIGHT\_RED="\[\033[1;31m\]"
  local GREEN="\[\033[0;32m\]"
  local LIGHT\_GREEN="\[\033[1;32m\]"
  local BROWN="\[\033[0;33m\]"
  local YELLOW="\[\033[1;33m\]"
  local BLUE="\[\033[0;34m\]"
  local LIGHT\_BLUE="\[\033[1;34m\]"
  local PURPLE="\[\033[0;35m\]"
  local LIGHT\_PURPLE="\[\033[1;35m\]"
  local CYAN="\[\033[0;36m\]"
  local LIGHT\_CYAN="\[\033[1;36m\]"
  local LIGHT\_GRAY="\[\033[0;37m\]"
  local WHITE="\[\033[1;37m\]"
  case $TERM in
    xterm*)
    TITLEBAR='\[\033]0;\u@\h:\w \D{%a %b %d %Y %l:%M%p (%Z%z)}\007\]'
    ;;
    *)
    TITLEBAR=""
    ;;
  esac

PCOLOR="\[\033[\$(promptcol)\]"

# note that in the following prompt the error code item (\$?) must be the
# first item in the prompt. Otherwise it'll show the errorcode for the last
# command executed in producing the prompt.
PS1="${TITLEBAR}\
$BLUE [$GREEN[\$?] [\D{%a %b %d %Y %l:%M%p (%Z%z)}] [Up: \$(uptimeinfo)] $BROWN\u@\h:\w $LIGHT\_GRAY\$(\_\_git\_ps1)\
$BLUE]\
\n$PCOLOR λ $LIGHT\_GRAY"
PS2='> '
PS4='+ '
}
proml

Enter fullscreen mode Exit fullscreen mode

That's a lot, and to be honest, I might be missing some of the code as my .bashrc file is full of random little snippets.

It's also shell-specific. If I ever wanted to move to Zsh, or Fish, or whatever, I'd have to re-invent that (and this has been an impediment for me to switching shells).

So I took this as a starting point and wanted to recreate it in Starship. Starting point was to install Starship with Brew:

brew install starship

Enter fullscreen mode Exit fullscreen mode

Easy enough. Next is to initialize it in your shell of choice. In my case for Bash this was adding the following to my .bashrc:

eval "$(starship init bash)"
Enter fullscreen mode Exit fullscreen mode

There are equivalent instructions on the Starship website for other prompts

Lastly, you need a ~/.config/starship.toml file where you'll configure your prompt.

touch ~/.config/starship.toml
Enter fullscreen mode Exit fullscreen mode

Now open up a new terminal and you should see the default Starship prompt which is actually quite sophisticated out of the box:

Default Starship Prompt

Your output may vary, as many of the items in a Starship prompt are dynamic depending on your current context. In this you can see:

  • I'm in a directory called dotfiles
  • I'm currently in a Git repo, but not on any branch instead checked out an arbitrary commit with SHA 931e5c4
  • The coffee cup has to do with Java, but I don't have a valid JDK installed so it's not showing what version
  • My current Python environment is Python 3.9.2
  • My AWS environment is configured to communicate with the ca-central-1 region

That's a lot! Note that each of those things is what Starship calls a Module. This is one of the key things about Starship is that each part of your prompt is made up by a distinct module that you configure. So that prompt is currently displaying the Directory Module, the Git Branch Module, the Git Commit Module, the Python Module, and the AWS Module. Technically it's showing a few others, but this gives you an idea of how Starship composes your prompt by stringing together some modules.

The full list of all modules can be found at: https://starship.rs/config/

Ok, lets get started trying to configure this prompt to be like my old one. I opened up my starship.toml and added the contents from the example on the Starship docs:

# Don't print a new line at the start of the prompt
add\_newline = false

# Replace the "❯" symbol in the prompt with "➜"
[character] # The name of the module we are configuring is "character"
success\_symbol = "[➜](bold green)" # The "success\_symbol" segment is being set to "➜" with the color "bold green"

# Disable the package module, hiding it from the prompt completely
[package]
disabled = true
Enter fullscreen mode Exit fullscreen mode

With this you probably won't see much change, aside from the symbol changing from a > to a due to the configuration of the character module. See, everything in a Starship prompt (including the symbol at the end) is a module. Well, mostly. 😜

Ok, so it currently shows the directory, but not like my old prompt where A) it was a yellow-ish colour, and B) showed the full path. Looking at the docs for the Directory module, I gave this a try to configure it:

[directory]
truncation\_length = 100
truncate\_to\_repo = false
style = "yellow"
format = "[:$path]($style)[$read\_only]($read\_only\_style) "
Enter fullscreen mode Exit fullscreen mode

Let's explain this a little bit to give a feel:

  • truncation_length controls how many directories deep you have to be before Starship will abbreviate the directory name in your prompt. I rarely go very deep and to be honest when I do I still want to see the full path so I made the number rediculously high so that it never truncated
  • truncate_to_repo is a special setting that controls if the directory is truncated to the root of the Git repo you are currently in. Again, I don't like this (I want to see the full path), so disabled it
  • style is a common setting on (I believe) every module and controls the colour of the module when rendered. In this case saying "yellow" to match my old prompt. Styles are covered in depth in the docs
  • format is another common setting across every module and controls effectively the "layout" of the module.

It's worth digging into the format directive there, as understanding this goes a long way in understanding how you control Starship's output. The expression:"[:$path]($style)[$read_only]($read_only_style) " says start a text group(this specified by the [ and ] delimiters) and have it output a colon (:) followed by the value of the $path variable. Each module has its own set of variables that get populated with values that are relevant to that module (in this case $path ends up being the full path of the current working directory). The brackets that follow a text group specify a style string. You might wonder "but isn't that what the style setting is for?" And yes, but essentially the style setting defines the "default" style within a module, and style strings within the format can override that. In this case, $stylecorresponds to the style setting defined in the configuration for the module (in this case "yellow"). You can see that later in this definition I have the $read_only variable (which gets displayed when the current working directory is read only) and has a different style defined for that scenario (the default $read_only_style is "red", but you could change that in this configuration by adding a read_only_style="blue" setting to the directory config). In any case the relevant part of the docs on format strings is here.

Clear as mud? Admittedly, this does take a bit to get your head around (or did for me), but basically you override settings in the config as appropriate to tweak each module to your liking.

Ok, that's fine, but Adam how do we control the order of items in the prompt? And that's a good question that took me a little while to figure out. Turns out that the prompt as a whole has a format setting. The default is to show all modules, this is from the docs:

format = "$all"

# Which is equivalent to
format = """
$username\
$hostname\
$shlvl\
$kubernetes\
$directory\
$git\_branch\
$git\_commit\
$git\_state\
$git\_status\
$hg\_branch\
$docker\_context\
$package\
$cmake\
$dart\
$dotnet\
$elixir\
$elm\
$erlang\
$golang\
$helm\
$java\
$julia\
$kotlin\
$nim\
$nodejs\
$ocaml\
$perl\
$php\
$purescript\
$python\
$ruby\
$rust\
$swift\
$terraform\
$vagrant\
$zig\
$nix\_shell\
$conda\
$memory\_usage\
$aws\
$gcloud\
$openstack\
$env\_var\
$crystal\
$custom\
$cmd\_duration\
$line\_break\
$lua\
$jobs\
$battery\
$time\
$status\
$character"""
Enter fullscreen mode Exit fullscreen mode

This is what controls the order of modules. Move an item up, and it'll appear earlier in the prompt, move down to move it later in the prompt. Personally I don't like this, as it means if you want to change the order of items you have to override the entire format string. It'd be nice if there was an "index" value or something on each module that could determine ordering, but oh well.

In any case it also provides a global "completely hide" ability for a module -- if you remove it from the format then it won't be displayed. Note that I don't think hiding from the format is the same thing as disabling a module. Each module has a disabled setting which (if true) disables that module (so won't get displayed, and I believe not evaluated).

Ok, with this I continued on and got most of my old prompt in place:

# Don't print a new line at the start of the prompt
add\_newline = false

[character]
success\_symbol = " [λ](grey)"
error\_symbol = " [λ](bold red)"

[directory]
truncation\_length = 100
truncate\_to\_repo = false
style = " yellow"
format = "[:$path]($style)[$read\_only]($read\_only\_style) "

[git\_branch]
symbol = ""
style = "bold white"
format = '[\($symbol$branch\)]($style) '

[git\_status]
# I don't care about untracked files or that there's a stash present.
untracked = ""
format = '([\[$conflicted$deleted$renamed$modified$staged$behind\]]($style) )'
modified = '\*'

[status]
disabled = false
format = '[\[$status - $common\_meaning\]](green)'
Enter fullscreen mode Exit fullscreen mode

This is close, but is missing the current time and "uptime". And those proved to be wrinkles for me. My previous time showed the full current date & time along with the current timezone info (ex: PST-0800). There's a time module that can recreate all of this except the timezone name. It should, given the docs on format strings for the underlying Chrono library, but turns out there's a bug there that causes that to not work.

But, Starship supports custom commands to be in a module, so I added a custom command to just defer to the standard date command on *-nix type systems:

[custom.tztime]
command = 'date +"%a %b %d %Y %l:%M%p (%Z%z)"'
when = "true"
format = '[\[$symbol($output)\]](green)'
Enter fullscreen mode Exit fullscreen mode

This gives me the time as it was before. The when bit there is true so that this command is always displayed. We'll get to how to control where custom commands show up in a minute, but there was one more custom command I needed for my system uptime. The way I did this in my old prompt was a Bash function:

function uptimeinfo {
    uptime | perl -ne 'if(/\d\s+up(.\*),\s+\d+\s+users/) { $s = $1; $s =~ s/^\s+|\s+$//g; print $s; }'
}
Enter fullscreen mode Exit fullscreen mode

And then I used uptimeinfo in my prompt. But that's Bash-specific, so instead I created a little shell script called uptime.sh with the following contents:

#!/bin/sh

echo "[`uptime | perl -ne 'if(/\d\s+up(.\*),\s+\d+\s+users/) { $s = $1; $s =~ s/^\s+|\s+$//g; print $s; }'`]"
Enter fullscreen mode Exit fullscreen mode

Which just does the same thing, echoes out the system uptime with it filtered through Perl to make it more concise. Now the starship config:

[custom.uptime]
command = "uptime.sh"
when = "true"
format = "[$symbol($output)](green)"
Enter fullscreen mode Exit fullscreen mode

Now back to that layout question: we control the order of things by the global format setting, but how do we refer to custom commands? Like this:

format = """
$status \
${custom.tztime} \
${custom.uptime} \
$username\

.... rest of the file ...
Enter fullscreen mode Exit fullscreen mode

Ie ${custom.<your custom module name>}. Ok with all that, I then had everything I needed, and continued to flesh out my prompt. My final config:

format = """
$status \
${custom.tztime} \
${custom.uptime} \
$username\
$hostname\
$shlvl\
$kubernetes\
$directory\
$git\_branch\
$git\_commit\
$git\_state\
$git\_status\
$docker\_context\
$package\
$cmake\
$nodejs\
$perl\
$python \
$ruby\
$rust\
$terraform\
$vagrant\
$nix\_shell\
$conda\
$aws \
$env\_var\
$cmd\_duration\
$line\_break\
$character"""

# Don't print a new line at the start of the prompt
add\_newline = false

[aws]
format = '\[AWS: [$profile/($region)]($style)\]'
symbol = ''
style = 'bold white'

[character]
success\_symbol = " [λ](grey)"
error\_symbol = " [λ](bold red)"

[cmd\_duration]
min\_time = 1000

[directory]
truncation\_length = 100
truncate\_to\_repo = false
style = " yellow"
format = "[:$path]($style)[$read\_only]($read\_only\_style) "

[git\_branch]
symbol = ""
style = "bold white"
format = '[\($symbol$branch\)]($style) '

[git\_status]
# I don't care about untracked files or that there's a stash present.
untracked = ""
format = '([\[$conflicted$deleted$renamed$modified$staged$behind\]]($style) )'
modified = '\*'

[python]
format = '[${symbol}${pyenv\_prefix}(${version} )(\($virtualenv\))]($style)'

[status]
disabled = false
format = '[\[$status - $common\_meaning\]](green)'

[custom.tztime]
command = 'date +"%a %b %d %Y %l:%M%p (%Z%z)"'
when = "true"
format = '[\[$symbol($output)\]](green)'

[custom.uptime]
command = "uptime.sh"
when = "true"
format = "[$symbol($output)](green)"

[env\_var]
variable = "0"

#### Disabled modules ####

# add these back to format if you want them:
# $time\
# $hg\_branch\
# $dart\
# $dotnet\
# $elixir\
# $elm\
# $erlang\
# $golang\
# $helm\
# $java\
# $julia\
# $kotlin\
# $nim\
# $ocaml\
# $php\
# $purescript\
# $swift\
# $zig\
# $memory\_usage\
# $gcloud\
# $openstack\
# $crystal\
# $lua\
# $jobs\
# $battery\
[hg\_branch]
disabled = true
[dart]
disabled = true
[dotnet]
disabled = true
[elixir]
disabled = true
[elm]
disabled = true
[erlang]
disabled = true
[golang]
disabled = true
[helm]
disabled = true
[java]
disabled = true
[julia]
disabled = true
[kotlin]
disabled = true
[nim]
disabled = true
[ocaml]
disabled = true
[php]
disabled = true
[purescript]
disabled = true
[swift]
disabled = true
[zig]
disabled = true
[memory\_usage]
disabled = true
[gcloud]
disabled = true
[openstack]
disabled = true
[crystal]
disabled = true
[lua]
disabled = true
[jobs]
disabled = true
[battery]
disabled = true

# Until these get resolved, doing my own datetime with date:
# https://github.com/starship/starship/discussions/2360#discussioncomment-391911
# https://github.com/chronotope/chrono/issues/288
[time]
disabled = true
# format = '[\[$time\]](green) '
# time\_format = "%a %b %d %Y %l:%M%p (%z)"
Enter fullscreen mode Exit fullscreen mode

Note current version (in case I revise in the future) is at: https://github.com/pzelnip/dotfiles/blob/mainline/.config/starship.toml)

This gives a prompt like the following:

New Starship-Powered Prompt

Pretty sweet, lots of dynamicism where needed, but still has all the things I liked from before. Definitely took some time to get this just the way I liked it, but am happy with the result, and as a bonus: now I have the same prompt if I'm in Bash, Zsh or whatever.

Top comments (0)