sbt: Customize the Shell prompt in sbt

Posted on Tuesday, March 15, 2016





Did you know you can customize the sbt command prompt?  Neither did I

I have started getting up to speed on using the sbt (Simple Build Tool).  The de facto build tool for Scala projects… although in truth I think I can be used to build other types of languages.

At any rate… I want to learn sbt inside and out, every corner, every capability.  To do this means I have a lot of reading, watching, and poking to do.

This is one of the Pokes.





Why?


Why would you want to customize your prompt?   Well for me it’s the same reason I customize my Linux prompt… more Information.  I use color to indicate where I am, local or on a server, and what folder I am in.  

In sbt I am not sure what kind of information I am going to want to display.   I do know I want to color it, if I can, so that it will be obvious I am running sbt on a terminal.

I found some notes on shellPrompt at http://www.scala-sbt.org/0.13/docs/Global-Settings.html [1]

Let me start with something simple.   Here is my very simple build.sbt file


name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state => " >> " }




There you go!  It's as simple as that.





Complex


But of course I want some more details in my output. 

I want

  • the name of the project
  • Colors!
  • Unicode Character in prompt

Name of the project


This one is pretty easy.  Here is my updated code.


name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state =>    
    s"[${Project.extract(state).currentProject.id}] >> "
}




Hey that worked… Kinda

I am still new to sbt I thought setting name would set my project name.  But currentProject.id is "kitten" not "preowned-kittens"




If I run projectId within sbt I get "preowned-kittens" the name I set.  So what is wrong with my prompt?

I still have a long way to go on learning sbt.  I am guessing the state is not updated before the shellPrompt is set?

Anyway I found a way around it here is my updated code.


name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state =>
   s"[${Project.extract(state).getOpt(sbt.Keys.name).
     getOrElse(Project.extract(state).currentProject.id)}] >> "
}


This will just grab the value of name and if it can't find it will default the currentProject.id.



If I reload sbt…




I get what I want.

Seems like I should be able to do this simpler…


name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state => s"[${name.value}] >> "}


Ahh that is much simpler.  Let me reload.




Wahoo that worked.


But what do I get if I comment out the name variable?


//name  :="preowned-kittens"
version := "1.0"

shellPrompt := { state => s"[${name.value}] >> "}


Now reload




Good it goes to the default nothing special to do.







Colors


How do I get color text, and color background in the sbt shellprompt.?
You can use linux bash prompt colors.  Here is a good site that goes over that http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html [2]

Colors codes (for text)
Color
Code
Black
0;30
Blue
0;34
Green
0;32
Cyan
0;36
Red
0;31
Purple
0;35
Brown
0;33
Light Gray
0;37
Dark Gray
1;30
Light Blue
1;34
Light Green
1;32
Light Cyan
1;36
Light Red
1;31
Light Purple
1;35
Yellow
1;33
White
1;37



Colors codes (for background)

Color
Code
Default
0
Black
40
Red
41
Green
42
Yellow
43
Blue
44
Magenta
45
Cyan
46
White
47

0 is unique as it sets you back to the default

Here is the format

\[\033[1;33m\]

This would give you Yellow Text

\[\033[42m\]

This would give you a Green background



Let me see if I can get that working.

Let me try the yellow prompt first


name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state =>
  s"\[\033[1;33m\]${name.value}] >> "
}


This causes a crash





Looks like I can lose some of the formatting.

I only need this

\033[1;33m





name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state =>
  s"\033[1;33m[${name.value}] >> "
}



Hey a yellow prompt!

Now let me add a green background!




name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state =>
  s"\033[1;33m\033[42m[${name.value}] >> "
}





Now I have a green background.

The only problem with this is the green background continues.




Lucky for us there is a reset button you can do on the command line background

\033[0m



name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state =>
  s"\033[1;33m\033[42m[${name.value}]\033[0m >> "
}




It resets the background and the text color.



These are the colors I am used to.  Turns out there are some more you can use.  I found this post http://unix.stackexchange.com/questions/124407/what-color-codes-can-i-use-in-my-ps1-prompt [5]

Create and run this script to get a list of color numbers that work on your system.


#!/bin/bash

color=16;

while [ $color -lt 245 ]; do
    echo -e "$color: \\033[38;5;${color}mhello\\033[48;5;${color}mworld\\033[0m"
    ((color++));
done 


Or here is a more simple one liner you can run on the command line.



   > for color  in {0..255}; do echo -e "$color:\\033[38;5;${color}mhello\\033[48;5;${color}mworld\\033[0m"; done







You should get something like this.   The first 0-15 seem to be a default colors.  If you look further down you can see shades.





So how do you use these colors?

For text color use 38;5;(color number)

\033[38;5;208m

For background use 48;5;(color number)

\033[48;5;208m




name    :="preowned-kittens"
version := "1.0"

shellPrompt := { state =>
  s"\033[38;5;18m\033[48;5;171m[${name.value}]\033[0m >> "
}

Pink and Blue.




Let me clean up my code a little bit.



name    :="preowned-kittens"
version := "1.0"



shellPrompt := { state =>
  def textColor(color: Int)             = { s"\033[38;5;${color}m" }
  def backgroundColor(color:Int) = { s"\033[48;5;${color}m" }
  def reset                                      = { s"\033[0m" }

  def formatText(str: String)(txtColor: Int, backColor: Int) = {
    s"${textColor(txtColor)}${backgroundColor(backColor)}${str}${reset}"
  }
  val white  = 15
  val orange = 166

  formatText(s"[${name.value}]")(white, orange) + " >> "
}


With this I get



Getting closer to what I want.


Let me fiddle a little more.


name    :="preowned-kittens"
version := "1.0"



shellPrompt := { state =>
  def textColor(color: Int)      = { s"\033[38;5;${color}m" }
  def backgroundColor(color:Int) = { s"\033[48;5;${color}m" }
  def reset                      = { s"\033[0m" }

  def formatText(str: String)(txtColor: Int, backColor: Int) = {
    s"${textColor(txtColor)}${backgroundColor(backColor)}${str}${reset}"
  }
  val white  = 15
  val orange = 166

  formatText(s"[${name.value}]")(white, orange) + "\n > > > "
}




Getting closer to what I want
Let me fiddle a little more…


name    :="preowned-kittens"
version := "1.0"



shellPrompt := { state =>
  def textColor(color: Int)             = { s"\033[38;5;${color}m" }
  def backgroundColor(color:Int) = { s"\033[48;5;${color}m" }
  def reset                                      = { s"\033[0m" }

  def formatText(str: String)(txtColor: Int, backColor: Int) = {
    s"${textColor(txtColor)}${backgroundColor(backColor)}${str}${reset}"
  }
  val red    = 1
  val green  = 2
  val yellow = 11
  val white  = 15
  val black  = 16
  val orange = 166

  formatText(s"[${name.value}]")(white, orange) +
   "\n " +
   formatText("> ")(green, black) +
   formatText("> ")(yellow, black) +
   formatText("> ")(red, black)
}




Getting closer to what I want.

Last step for what I want involves Unicode characters.






Unicode Characters


I use Unicode Character \u276f   on my command line.  In fact to get it working I had to tweak a font on my machine see http://www.whiteboardcoder.com/2015/02/snazzier-command-line.html [5] for more information on that.

Now if I edit my code to



  formatText(s"[${name.value}]")(white, orange) +
   "\n " +
   formatText("\u276f ")(green, black) +
   formatText("\u276f ")(yellow, black) +
   formatText("\u276f ")(red, black)
}




I get this  ?  ?  ?

..... why?


The font I am using has this Unicode character defined so what is going on?

After doing some research I think I found out it is a Java thing….
But, it's a thing that can be fixed.


Looks like the simplest way to fix this problem is to set an environment variable.

On Cygwin I edited ~/.bash_profile


   > vi ~/.bash_profile


Adding a JAVA_OPTS to the end.


#Scala/Java command line will now output unicode characters correctly
export JAVA_OPTS="-Dfile.encoding=UTF-8"




Now reload the .bash_profile


   > source ~/.bash_profile




Now try it.



Unicode!!

But I have an extra space between them I do not want.







Perfect


Here is my final code (thus far).


name    :="preowned-kittens"
version := "1.0"



shellPrompt := { state =>
  def textColor(color: Int)      = { s"\033[38;5;${color}m" }
  def backgroundColor(color:Int) = { s"\033[48;5;${color}m" }
  def reset                      = { s"\033[0m" }

  def formatText(str: String)(txtColor: Int, backColor: Int) = {
    s"${textColor(txtColor)}${backgroundColor(backColor)}${str}${reset}"
  }
  val red    = 1
  val green  = 2
  val yellow = 11
  val white  = 15
  val black  = 16
  val orange = 166

  formatText(s"[${name.value}]")(white, orange) +
   "\n " +
   formatText("\u276f")(green, black) +
   formatText("\u276f")(yellow, black) +
   formatText("\u276f ")(red, black)
}








Global Configurations


Shell Prompts can get pretty personal.  Having the shell prompt for sbt defined in the build.sbt file may not be the right place to put it.   It may be… it depends on your team dynamic.

What do you do to allow people to do their own personal shellPrompts?


Any settings in the ~/.sbt/0.13/global.sbt will be applied to all projects. (I think it used to be ~/.sbt/global.sbt but now that we only have a guarantee of binary compatibility between major versions it makes since to include the 0.13 folder)

Normally you would create the global.sbt file in ~/.sbt/0.13/ , but my set up in Cygwin is not normal.  (In other words I tried and it did not work)





The .sbt folder needs to be in your user.home directory according to JAVA.  How do you check this?  Well start up a scala REPL and run this command.


   > scala
   > println(System.getProperty("user.home"))




There is my home directory.



To make my life easier I am going to make a symlink from my Cygwin home directory,  .sbt folder, to here.


   > ln -fs /cygdrive/c/Users/patman/.sbt ~/.sbt





Now I can create the ~/.sbt/0.13/global.sbt file


   > vi ~/.sbt/0.13/global.sbt




And  place the following in it.


shellPrompt := { state =>
  def textColor(color: Int)      = { s"\033[38;5;${color}m" }
  def backgroundColor(color:Int) = { s"\033[48;5;${color}m" }
  def reset                      = { s"\033[0m" }

  def formatText(str: String)(txtColor: Int, backColor: Int) = {
    s"${textColor(txtColor)}${backgroundColor(backColor)}${str}${reset}"
  }
  val red    = 1
  val green  = 2
  val yellow = 11
  val white  = 15
  val black  = 16
  val orange = 166

  formatText(s"[${name.value}]")(white, orange) +
   "\n " +
   formatText("\u276f")(green, black) +
   formatText("\u276f")(yellow, black) +
   formatText("\u276f ")(red, black)
}



Run reload in sbt and….




It works!

Here is my build.sbt


name    :="preowned-kittens"
version := "1.0"


Now I don't have to impose my command line preferences on the rest of the team.





Future


I am not done with my command line the part I highlight in orange should contain more information.  At this point I do not know what information I may want to add there.

Git sha?  
Last successful Jenkins build?  
Current Sensu alerts?  

It's Scala I could put lots of interesting information here…





References

[1]        sbt Global settings
                Accessed 03/2016
[2]        Bash Colours
                Accessed 03/2016
[3]        [sbt] How to color ShellPrompt
                Accessed 03/2016
[4]        What color codes can I use in my PS1 prompt?
                Accessed 03/2016
[5]        Snazzier command line prompt
                Accessed 03/2016
[6]        Printing Unicode from Scala interpreter
                Accessed 03/2016
[7]        Printing Unicode from Scala interpreter
                Accessed 03/2016
[8]        Sbt Launcher Configuration
                Accessed 03/2016



2 comments:

  1. Hi Patrick. Nice post.
    Did you know that there is a plugin that comes pretty close to what you describe?

    Check out my sbt-prompt plugin: https://github.com/agemooij/sbt-prompt/

    All comments, issues and pull requests are very welcome!

    ReplyDelete
    Replies
    1. Cool I will have to check it out!

      I am an SBT newb so I have not yet even fiddled with plugins yet.
      But my plan is to dive deep into sbt and figure it all out over the next year+ (well at least enough to be good at it :) )

      Delete