Have you ever run a destructive command on the wrong server?
The year was 2010; I had 3 servers that I maintained with a few friends. We developed a script that would destroy a given server over SSH – it would write random data to block devices and kmem, after deleting a lot of files.
We ran the script on one of the servers; of course the wrong one by mistake. It wasn’t a big deal – it was hosting an XMPP chat server together with a few other services. No data was lost but it was inconvenient!
Soon after we wanted to prevent the same thing from happening again; so we changed the colour of the bash prompts – green, red and light blue. It made it quite clear; but we forgot one thing – one of us was red/green colour blind; the same thing almost happened again!
Nonetheless, ever since I now vary my prompt colour by hostname to reduce the chance of mistakes – automatically.
Nowadays I have a dotfiles repository which contains code to set up my environment on whichever server I’m on; given I regularly SSH into a dozen or so hosts, it saves me time but more importantly it’s a mental offload – everything is configured exactly how I expect it with hundreds of tweaks to fix annoyances. I store most of my important data in git(hub) and on a NAS, this makes all my machines effectively disposable. I can reprovision a machine at a moment’s notice.
I use dozens of hosts, and the hosts rotate quite a bit; it would be inconvenient to have to pick a prompt colour for every server I use. So, for many years I used a python script to deterministically pick a colour based on the system hostname.
I use a 256-colour terminal1; – this presents a large palette rather than an even spread. Each colour has a number, 0-255.
So in theory, all I had to do is generate a number, 0-255 from the system hostname:
import socket from hashlib import md5 seed = socket.gethostname().encode() m = md5() m.update(seed) digest = m.hexdigest() colour = int(digest, 16) % 256
This generates a large number using the MD5 hash algorithm, then uses the modulo (remainder) operator to pick the number. I know I could have used the python
random module with a deterministic seed, but I’m not sure the behaviour would be stable over python versions.
In reality it wasn’t that simple – a lot of the colours don’t contrast well with a dark background. I manually picked a selection of 35 colours to work around this problem – generating the index 0-34 instead, using a similar algorithm. Only 35 colours due to the manual effort plus the many colours are indistinct anyway.
This worked great for several years – though due to the low number of available colours and the birthday paradox many hosts shared the same colour. It was still useful, and zero effort.
For a while I’ve been meaning to solve this problem by using 2 colours: a foreground colour and a background colour. For this I’d need another contrasting palette – I could pick one but I knew I could come up with something better.
What if the script evaluated contrast automatically?
I needed a way of mapping the terminal colours to an RGB value, as well as a way to calculate the perceived contrast ratio. For the former, thankfully @jonasjacek had provided a JSON DB of mappings on github.
For the latter, I could have used a basic lightness comparison in HSL colour space but I had my doubts when experimenting with different colours that have the same lightness. I then remembered the accessibility contrast checker I used in my browser to pick colours for this site; something well established as the gold standard.
As it happens, w3c, the standards organisation for the world wide web, define the metric used for checking contrast as part of WCAG. It accounts for the non-linear response of the human eye, I believe.
I was fortunate to find a python implementation (by Geoffery Snedders) of the formula used to calculate the metric. With this implementation and the map, I now had a way of measuring contrast between two terminal colours.
So, the core of my new script:
def get_colours(seed): fg = select_by_seed(COLOUR_MAP, seed) bg_candidates = get_contrasting_colours(fg) bg = select_by_seed(bg_candidates, seed) # 50% chance swap to remove bias to light foreground -- palette is # predominately light return select_by_seed([(fg, bg), (bg, fg)], seed)
Here it is: https://github.com/naggie/dotfiles/blob/master/app-configurators/scripts/bin/system-colour – the script outputs a small shell script to set variables. Those variables are then used by the prompts defined in
Thanks for reading! I'd appreciate it if you could submit/upvote/discuss this article somewhere on Hacker news, Twitter, Hackaday, Lobste.rs, Reddit and/or LinkedIn.
Please email me with any corrections or feedback.