Post your handy utility scripts!

A few weeks ago, I shared a simple digital clock, a bash script that made a figlet-like display that animated in the terminal.

Then @freebird54 reacted with a :mantelpiece_clock: emoji and I got an idea…

I quickly dismissed this idea as too complicated, and decided not to bother with it. But two nights ago I had a dream in which I figured out how to do it, so yesterday and last night, I did it.

So, I present to you bashclock2, an animated scalable analogue clock, completely written in bash:

#!/usr/bin/env bash
# An analogue version of bashclock

function cleanup_() {
  printf '\033[?25h' # unhide cursor
  stty echo 2>/dev/null
}
trap cleanup_ EXIT

REFRESH_INTERVAL=0.2  # display the clock every 0.2 seconds, adjust if necessary
CLOCK_RADIUS=23       # default size of the clock, adjust if necessary

# parsing the only option
# bashclock N, where N is an integer, for a clock of a different size.
# minumum radius is 7, maximum radius is 37, adjust if necessary
if (( $# >= 1 )) && (( $1 >= 7 && $1 <= 37 )); then
  CLOCK_RADIUS=$1
fi

# Trigonometric & other mathematical functions -------------------------------
# Table of sines, first quadrant, 1° increment
declare -r sin_table=(
          0   17452406   34899497   52335956   69756474   87155743  104528463 
  121869343  139173101  156434465  173648178  190808995  207911691  224951054 
  241921896  258819045  275637356  292371705  309016994  325568154  342020143 
  358367950  374606593  390731128  406736643  422618262  438371147  453990500 
  469471563  484809620  500000000  515038075  529919264  544639035  559192903 
  573576436  587785252  601815023  615661475  629320391  642787610  656059029 
  669130606  681998360  694658370  707106781  719339800  731353702  743144825 
  754709580  766044443  777145961  788010754  798635510  809016994  819152044 
  829037573  838670568  848048096  857167301  866025404  874619707  882947593 
  891006524  898794046  906307787  913545458  920504853  927183855  933580426 
  939692621  945518576  951056516  956304756  961261696  965925826  970295726 
  974370065  978147601  981627183  984807753  987688341  990268069  992546152 
  994521895  996194698  997564050  998629535  999390827  999847695 1000000000 
)
function cosE9() { # returns 1000000000*cos($1), $1=0..360°
  if   (($1 >=  0 && $1 <=  90)); then printf ${sin_table[90-$1]}
  elif (($1 >  90 && $1 <= 180)); then printf -- -${sin_table[$1-90]}
  elif (($1 > 180 && $1 <= 270)); then printf -- -${sin_table[270-$1]}
  elif (($1 > 270 && $1 <= 360)); then printf ${sin_table[$1-270]}
  fi
}
function sinE9() { # returns 1000000000*sin($1), $1=0..360°
  if   (($1 >=  0 && $1 <=  90)); then printf ${sin_table[$1]}
  elif (($1 >  90 && $1 <= 180)); then printf ${sin_table[180-$1]}
  elif (($1 > 180 && $1 <= 270)); then printf -- -${sin_table[$1-180]}
  elif (($1 > 270 && $1 <= 360)); then printf -- -${sin_table[360-$1]}
  fi
}
function abs() { 
  if (($1 >= 0)); then printf $1
  else printf -- -$1
  fi
}
function signum() { 
  if   (($1 >  0)); then printf 1
  elif (($1 == 0)); then printf 0
  else printf -- -1
  fi
}
#-----------------------------------------------------------------------------
W=$((2*CLOCK_RADIUS+1)) # clock width
H=$((CLOCK_RADIUS+1))   # clock height

declare RASTER_MATRIX   # array of pixels
declare CLOCK_MATRIX    # the clock face without hands

# runs once on startup, draws the clock and saves it as the CLOCK_MATRIX
function generate_CLOCK_MATRIX() {
  # Midpoint circle rasterising algorithm
  local E=-$CLOCK_RADIUS
  local x=$CLOCK_RADIUS
  local y=0
  while ((y <= x)); do
    CLOCK_MATRIX[$(((CLOCK_RADIUS+x)+(CLOCK_RADIUS+y)*W))]=1
    CLOCK_MATRIX[$(((CLOCK_RADIUS+x)+(CLOCK_RADIUS-y)*W))]=1
    CLOCK_MATRIX[$(((CLOCK_RADIUS-x)+(CLOCK_RADIUS+y)*W))]=1
    CLOCK_MATRIX[$(((CLOCK_RADIUS-x)+(CLOCK_RADIUS-y)*W))]=1
    CLOCK_MATRIX[$(((CLOCK_RADIUS+y)+(CLOCK_RADIUS+x)*W))]=1
    CLOCK_MATRIX[$(((CLOCK_RADIUS+y)+(CLOCK_RADIUS-x)*W))]=1
    CLOCK_MATRIX[$(((CLOCK_RADIUS-y)+(CLOCK_RADIUS+x)*W))]=1
    CLOCK_MATRIX[$(((CLOCK_RADIUS-y)+(CLOCK_RADIUS-x)*W))]=1
    E=$((E+2*y+1))
    y=$((y+1))
    if ((E >= 0)); then
      E=$((E-(2*x-1)))
      x=$((x-1))
    fi
  done
  # hour ticks
  for ((phi = 0; phi < 360; phi+=30)) {
    x=$((CLOCK_RADIUS+((CLOCK_RADIUS*$(cosE9 $phi))/1050000000)))
    y=$((CLOCK_RADIUS-((CLOCK_RADIUS*$(sinE9 $phi))/1050000000)))
    CLOCK_MATRIX[x+y*W]=1
  }
  CLOCK_MATRIX[CLOCK_RADIUS+CLOCK_RADIUS*W]=1
}

function reset_RASTER_MATRIX() {
  local l=$((W*(W+1)))
  for ((i = 0; i < l; i++)); do
    RASTER_MATRIX[i]=${CLOCK_MATRIX[i]}
  done
}

function plot_line() {
  # Bresenham's line rasterising algorithm
  local x0=$1
  local y0=$2
  local x1=$3
  local y1=$4
  
  local dx=$((x1-x0))
  local sx=$(signum $dx)
  local dx=$(abs $dx)
  
  local dy=$((y1-y0))
  local sy=$(signum $dy)
  local dy=-$(abs $dy)
  
  local E=$((dx+dy))
  
  while true; do
    local x=$((CLOCK_RADIUS+x0))
    local y=$((CLOCK_RADIUS-y0))
    RASTER_MATRIX[x+y*W]=1
    
    local e2=$((2*E))
    if ((e2 >= dy)); then
      ((x0 == x1)) && return
      E=$((E+dy))
      x0=$((x0+sx))
    fi
    if ((e2 <= dx)); then
      ((y0 == y1)) && return
      E=$((E+dx));
      y0=$((y0+sy))
    fi
  done
}
#-----------------------------------------------------------------------------
function draw_HOURS_hand() {
  local phi=$(((($HOURS*30+$MINUTES*30/60)+270)%360))
  local x=$(((CLOCK_RADIUS*$(cosE9 $phi))/1700000000))
  local y=-$(((CLOCK_RADIUS*$(sinE9 $phi))/1700000000))
  plot_line 0 0 $x $y
}
function draw_MINUTES_hand() {
  local phi=$(((($MINUTES*6+$SECONDS*6/60)+270)%360))
  local x=$(((CLOCK_RADIUS*$(cosE9 $phi))/1200000000))
  local y=-$(((CLOCK_RADIUS*$(sinE9 $phi))/1200000000))
  plot_line 0 0 $x $y
}
function draw_SECONDS_hand() {
  local phi=$(((($SECONDS*6)+270)%360))
  local x=$(((CLOCK_RADIUS*$(cosE9 $phi))/1060000000))
  local y=-$(((CLOCK_RADIUS*$(sinE9 $phi))/1060000000))
  plot_line 0 0 $x $y
}

# converts the pixel array into characters █, ▀, and ▄, prints to stdout
function print_RASTER_MATRIX() {
  for ((r = 0; r < H; r++)); do
    printf '  ' # indentation
    for ((c = 0; c < W; c++)); do
      if   ((RASTER_MATRIX[c+2*r*W] == 1 && RASTER_MATRIX[c+2*r*W+W] == 1))
        then printf '█'
      elif ((RASTER_MATRIX[c+2*r*W] == 1 && RASTER_MATRIX[c+2*r*W+W] == 0))
        then printf '▀'
      elif ((RASTER_MATRIX[c+2*r*W] == 0 && RASTER_MATRIX[c+2*r*W+W] == 1))
        then printf '▄'
      else
        printf ' '
      fi
    done
    printf '\n'
  done
}
#-----------------------------------------------------------------------------
generate_CLOCK_MATRIX
stty -echo 2>/dev/null
printf '\033[?25l\n' # hide cursor
unset EXIT_

while true; do
  HOURS=$((10#$(printf "%(%H)T" $((EPOCHSECONDS)))))
  MINUTES=$((10#$(printf "%(%M)T" $((EPOCHSECONDS)))))
  SECONDS=$((10#$(printf "%(%S)T" $((EPOCHSECONDS)))))

  reset_RASTER_MATRIX
  draw_HOURS_hand
  draw_MINUTES_hand
  draw_SECONDS_hand
  print_RASTER_MATRIX

  [[ -z $EXIT_ ]] || exit 0
  read -t $REFRESH_INTERVAL -n 1 && EXIT_=1;

  printf "\033[${H}A\r" # move cursor to beginning
done

bashclock2

You can change the size of the clock with a command line argument. The default radius is 20, but it can be anything between 7 and 37. The bigger clock looks nicer. The big sizes require a fast terminal emulator. It works very nice on Konsole (with the Hack font). It also looks great in the TTY.


This is a bit more complex than the previous script. It uses the same trick of writing the escape sequence \033[F to move the cursor to the beginning of the last line, so as to overwrite what was written previously. However, this drawing is slightly more complex than just printing out the “figletised” digits. It involves rasterising vector graphics composed of two primitive types: a line and a circle. The algorithms for that are well-known, and used all the time: Midpoint Circle Algorithm and the Bresenham’s Line Algorithm. You can find a very nice description of both (which I used as a guide when writing this), here:

https://members.chello.at/~easyfilter/Bresenham.pdf

After figuring that out, drawing lines and circles using the characters █, ▀, and ▄ was quite easy, however the bigger problem with the script was performance. In order for the animation to be fairly smooth and not skip seconds, the clock has to be updated several times a second. The problem is that drawing the hands of the clock requires trigonometry, and bash has no support for floating point computations. Using integers, it is fairly straightforward to implement fixed point computation in bash, but computing sine and cosine functions in fixed point, at runtime, was out of the question (due to precision and time). The obvious way to use floating point computations in bash is to outsource it and do the calculation in another program like bc or awk, but running a separate process for every frame of the animation was out of the question. The solution, which works rather well, is to hardcode the trigonometric functions. BTW, this was done in Doom (1993), in order to maximise performance. :slight_smile:

Oh, and just for @freebird54, I’m using read instead of sleep, so that one can exit with the press of the Any key (that, and the clock looks ugly when you exit in the middle of the frame).

Three additional tricks: using escape sequences to hide the cursor, turning off stty echo, and using trap to cleanup the mess on exit.


PS. After spending hours working on this, when I close my eyes, all I see is clocks.
frog_clocks

Edit: updated the script to get rid of calling the external date process. Bash has builtin printf which provides the facilities for outputting the time without calling an external process. This should improve performance significantly.

14 Likes