#!/usr/bin/env bash # This script will take an HTML compatible hex colour string and turn it into # a terminal sequence for true colour and other formats which best approximate # its colour value. # # After that, we can map a few other special named symbols. The idea is to # permit shell variables such as `aaffcc bold italic` to indicate what you # want a format to look like. # # The terminal colour and effect language herein implemented is named # "SGR Name". debug=0 # This is the named color map. Just add new entries and they'll be supported... declare -A map map[white]="FFFFFF" map[silver]="C0C0C0" map[gray]="808080" map[grey]="808080" map[black]="000000" map[red]="FF0000" map[maroon]="800000" map[yellow]="FFFF00" map[olive]="808000" map[lime]="00FF00" map[green]="008000" map[aqua]="00FFFF" map[teal]="008080" map[blue]="0000FF" map[navy]="000080" map[fuchsia]="FF00FF" map[purple]="800080" # Some VGA color names can be promoted: map[brown]="aa5500" map[cyan]="00aaaa" # The VGA names can be prefixed with "vga-" to explicitly avoid the collisions with HTML names, above... # Based upon wikipedia's article on ANSI. See the table on that page for details. map[vga-black]="000000" map[vga-red]="aa0000" map[vga-green]="00aa00" map[vga-brown]="aa5500" map[vga-yellow]="aa5500" map[vga-blue]="0000aa" map[vga-magenta]="aa00aa" map[vga-cyan]="00aaaa" map[vga-white]="aaaaaa" map[vga-brightblack]="555555" map[vga-grey]="555555" map[vga-gray]="555555" map[vga-brightred]="ff5555" map[vga-brightgreen]="55ff55" map[vga-brightyellow]="ffff55" map[vga-brightblue]="5555ff" map[vga-brightmagenta]="ff55ff" map[vga-brightcyan]="55ffff" map[vga-brightwhite]="ffffff" enable_greyscale_cheat=0 nocsi=0 if [[ $1 == "no-csi" ]] then nocsi=1 shift 1 fi function dump_colors() { for key in ${!map[@]} do case $key in *bright*) text_color="000000" ;; *black*) text_color="ffffff" ;; *) text_color="000000" ;; esac printf "%-24s${map[${key}]} |" ${key} printf "`$0 reset reverse bg:${text_color} fg:${key}`native-sample`$0 reset`|" if [[ ! -v CSHENV_TERMINAL_COLORS ]] || (( ${CSHENV_TERMINAL_COLORS} >= 256 )) then printf "`env CSHENV_TERMINAL_COLORS=256 $0 reset reverse bg:${text_color} fg:${key}`8-bit sample`$0 reset`|" fi if [[ ! -v CSHENV_TERMINAL_COLORS ]] || (( ${CSHENV_TERMINAL_COLORS} >= 16 )) then printf "`env CSHENV_TERMINAL_COLORS=16 $0 reset reverse bg:${text_color} fg:${key}`4-bit sample`$0 reset`|" fi if [[ ! -v CSHENV_TERMINAL_COLORS ]] || (( ${CSHENV_TERMINAL_COLORS} >= 8 )) then printf "`env CSHENV_TERMINAL_COLORS=8 $0 reset reverse bg:${text_color} fg:${key}`3-bit sample`$0 reset`|" fi printf "\n" done exit 0 } function setup_color_depth() { use_8_bit=0 use_4_bit=0 use_3_bit=0 # Must only be set if terminal colors are reduced... if [[ -v CSHENV_TERMINAL_COLORS ]] then if (( ${CSHENV_TERMINAL_COLORS} == 256 )) then use_8_bit=1 elif (( ${CSHENV_TERMINAL_COLORS} == 16 )) then use_4_bit=1 elif (( ${CSHENV_TERMINAL_COLORS} == 8 )) then use_3_bit=1 else echo "ERROR!" 1>&2 ; exit -1 fi fi } function render_color() { command_color=30 if (( ${use_3_bit} )) then # Underline colours need 8-bit or more if (( ${background} > 1 )) ; then return; fi basecolor=$(( ${command_color} + ${background}*10)) next="$(( ${basecolor} + ${legacy_3_bit} ))" elif (( ${use_4_bit} )) then # Underline colours need 8-bit or more if (( ${background} > 1 )) ; then return; fi command_color=$(( ${command_color} + ${intensity_1_bit} * 60 )) basecolor=$(( ${command_color} + ${background}*10 )) next="$(( ${basecolor} + ${legacy_3_bit} ))" elif (( ${use_8_bit} )) then basecolor=$(( 8 + ${command_color} + ${background}*10 )) next="${basecolor};5;${ext_8_bit}" else basecolor=$(( 8 + ${command_color} + ${background}*10 )) next="${basecolor};2;${red_dec};${green_dec};${blue_dec}" fi } function ansi_color() { if (( ${1} > 15 )) then echo "ERROR!" 1>&2 ; exit -1 elif (( ${1} >= 8 )) then use_4_bit=1 intensity_1_bit=1 legacy_3_bit=$(( ${1} - 8 )) else use_3_bit=1 intensity_1_bit=0 legacy_3_bit=$(( ${1} )) fi } function ext_grey() { use_8_bit=1 grey_val=${1} if (( ${grey_val} < 0 || ${grey_val} > 23 )) then echo "ERROR!" 1>&2 ; exit -1 fi ext_8_bit=$(( 232 + ${grey_val} )) } function ext_rgb() { use_8_bit=1 red_ext_val=$((6#${1:0:1})) green_ext_val=$((6#${1:1:1})) blue_ext_val=$((6#${1:2:1})) ext_8_bit=$(( 16 + 36 * ${red_ext_val} + 6 * ${green_ext_val} + ${blue_ext_val} )) } function ext_color() { use_8_bit=1 if (( ${1} > 255 )) then echo "ERROR!" 1>&2 ; exit -1 else ext_8_bit=${1} fi } function rgb_color() { color=$1 case $1 in [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) color=${1:0:1}${1:0:1}${1:1:1}${1:1:1}${1:2:1}${1:2:1} ;; esac # First split off the red, green, and blue components... red_hex=${color:0:2} green_hex=${color:2:2} blue_hex=${color:4:2} # Convert to decimal... red_dec=$((16#${red_hex})) green_dec=$((16#${green_hex})) blue_dec=$((16#${blue_hex})) if (( ${debug} != 0 )) then echo "Red: $red_dec" echo "Green: $green_dec" echo "Blue: $blue_dec" fi # Now compute the rest of the stuff... # We probably don't have to compute all the unused color states. We only have to compute # the color state we're signed up for by CSHENV_TERMINAL_COLORS... but whatever... # We round up by 128 points of colour, so that if we're above a certain intensity, we get the top # bit set. red_1_bit=$(( ( ( ${red_dec} + 128 ) >> 8 ) & 1 )) green_1_bit=$(( ( ( ${green_dec} + 128 ) >> 8 ) & 1 )) blue_1_bit=$(( ( ( ${blue_dec} + 128 ) >> 8 ) & 1 )) intensity_1_bit=0 # If we can support an intensity bit, we'll turn that on too... # But we're going to stop using bold to set the colour "intense" # We'll use the 9x and 10x forms... if (( ${red_dec} >= 192 || ${green_dec} >= 192 || ${blue_dec} >= 192 )) then intensity_1_bit=1 fi if (( ${debug} != 0 )) then echo "Red bit: " $red_1_bit echo "Green bit: " $green_1_bit echo "Blue bit: " $blue_1_bit echo "Intensity bit: " $intensity_1_bit fi # Because grey is tricky, I'm going to special case it: if (( 1 && ${red_dec} == ${green_dec} && ${green_dec} == ${blue_dec} && ${red_dec} < 128 && ${red_dec} >= 80 )) then red_1_bit=0 green_1_bit=0 blue_1_bit=0 intensity_1_bit=1 fi # Because brown is tricky, I'm going to special case it: if (( ${red_dec} > 128 && ${green_dec} > 80 && ${blue_dec} < 50 )) then red_1_bit=1 green_1_bit=1 blue_1_bit=0 fi if (( ${debug} != 0 )) then echo "Red bit: " $red_1_bit echo "Green bit: " $green_1_bit echo "Blue bit: " $blue_1_bit echo "Intensity bit: " $intensity_1_bit fi # This lets us combine them for a legacy colour value in the legacy colour space... legacy_3_bit=$(( ( ${blue_1_bit} << 2 ) + ( ${green_1_bit} << 1 ) + ( ${red_1_bit} ) )) if (( ${debug} != 0 )) then echo "Legacy colour: " ${legacy_3_bit} " Intensity: " ${intensity_1_bit} fi # Now compute an extended colour cube placement (216 colours): red_ext_val=$(( ${red_dec} * 6 / 256 )) green_ext_val=$(( ${green_dec} * 6 / 256 )) blue_ext_val=$(( ${blue_dec} * 6 / 256 )) if (( ${debug} != 0 )) then echo "Red ext: " ${red_ext_val} echo "Green ext: " ${green_ext_val} echo "Blue ext: " ${blue_ext_val} fi ext_8_bit=$(( 16 + 36 * ${red_ext_val} + 6 * ${green_ext_val} + ${blue_ext_val} )) if (( ${debug} )) then echo "Computed 216 color cube: " ${ext_8_bit} fi # Check for precise greyscale, which would replace the above: # (Note that precise greyscale is implied by a user explicitly making ALL of RGB the # same value, thus demanding greyscale, rather than a subtle tinting...) if (( ${red_dec} == ${green_dec} && ${green_dec} == ${blue_dec} )) then ext_8_bit=$(( 232 + ${red_dec} * 24 / 256 )) fi if (( ${debug} )) then echo "Computed 256 color choice, after applying greyscale scan: " ${ext_8_bit} fi if (( ${enable_greyscale_cheat} )) then # Check for SPECIFIC white and black and grey: # The half-saturation, no saturation, and full saturation values are mapped # to specific points in the legacy set. (This may be ill-advised as a manually # built reverse video terminal setting through colour mapping may make it so that # we wind up in la-la land. Perhaps the better way is to find the head and tail # points in the 6x6x6 colour cube.) # # TODO: Only map the head and tail of the colour cube. if (( ${red_dec} == ${green_dec} && ${green_dec} == ${blue_dec} )) then if (( ${red_dec} == 0 )) then ext_8_bit=0 elif (( ${red_dec} == 255 )) then ext_8_bit=15 elif (( ${red_dec} == 128 )) then ext_8_bit=8 elif (( ${red_dec} == 192 )) then ext_8_bit=7 fi fi fi if (( ${debug} )) then echo "Computed 256 color choice, after applying white and black scan: " ${ext_8_bit} fi # We do NOT check for the base colours precisely. Since those 16 can be custom mapped by your terminal emulator, you just pass `ansi:0` thru # `ansi:15` to select them by ID. Or use `ext:0` thru `ext:255` to select a specific extended colour. } function make_color() { setup_color_depth background=$1 # $1 is the background bit $2 $3 # $2 is the color function render_color } function named_color() { name=${1,,} if [[ ! -v map[$name] ]] then echo "ERROR!" 1>&2 ; exit -1 fi mapped=${map[$name]} rgb_color ${mapped} } function build_sgr_code() { case $1 in dump-colors) dump_colors ;; reset) next=0 ;; bold) next=1 ;; dim) next=2 ;; italic) next=3 ;; underline|under) next=4 ;; blink) next=5 ;; fastblink) next=6 ;; reverse) next=7 ;; hide|conceal) next=8 ;; strike|strikethrough|strikethru) next=9 ;; doubleunder) next=21 ;; reveal) next=28 ;; ansi:*) make_color 0 ansi_color ${1:5} ;; ext:grey*) make_color 0 ext_grey ${1:8} ;; ext:rgb*) make_color 0 ext_rgb ${1:7} ;; ext:*) make_color 0 ext_color ${1:4} ;; # 12-bit color also supported [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color $1 ;; [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color $1 ;; fg:ansi:*) make_color 0 ansi_color ${1:8} ;; fg:ext:grey*) make_color 0 ext_grey ${1:11} ;; fg:ext:rgb*) make_color 0 ext_rgb ${1:10} ;; fg:ext:*) make_color 0 ext_color ${1:7} ;; # 12-bit color also supported fg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color ${1:3} ;; fg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color ${1:3} ;; bg:ansi:*) make_color 1 ansi_color ${1:8} ;; bg:ext:grey*) make_color 1 ext_grey ${1:11} ;; bg:ext:rgb*) make_color 1 ext_rgb ${1:10} ;; bg:ext:*) make_color 1 ext_color ${1:7} ;; # 12-bit color also supported bg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 1 rgb_color ${1:3} ;; bg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 1 rgb_color ${1:3} ;; ul:ext:*) make_color 2 ext_color ${1:7} ;; # 12-bit color also supported ul:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 2 rgb_color ${1:3} ;; ul:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 2 rgb_color ${1:3} ;; fg:*) make_color 0 named_color ${1:3} ;; bg:*) make_color 1 named_color ${1:3} ;; ul:*) make_color 2 named_color ${1:3} ;; *) make_color 0 named_color $1 ;; esac if [[ ! -z ${output} ]] then output="${output};" fi output="${output}${next}" } output="" while [[ ! -z $1 ]] do build_sgr_code $1 shift 1 done if [[ ! -z ${output} ]] && (( ! ${nocsi} )) then output="[${output}m" fi if [[ ! -z ${output} ]] then printf ${output} fi exit 0