Creating a Pomodoro in Bash

I originally wanted to create a pomodoro timer that could run in i3status or i3blocks. I haven’t quite made it that far, but it does run in bash. For those of you who haven’t heard about it, see the Wikipedia article on the Pomodoro Technique.

I chose to create a work period of 25 minutes, with 5 minute short breaks and 20 minute long breaks. I decided to measure the time using a date object measured in seconds. Then I figured I needed three functions: 1) a compute function that takes an argument of seconds to start from and prints the number of seconds elapsed from that date object (measured in seconds), 2) a “convert seconds” function that takes a date object in seconds and returns the formated string of minutes and seconds that can be displayed to the user, and 3) a “seconds remaining” function that takes two arguments: the number of seconds when the period started and the “now” time in seconds, and that returns a printed string of remaining minutes and seconds in the period.

Further, I needed a function to run the breaks between work periods. This function would start a period for a certain length, and that length would be this functions only required parameter.

Once all this was working, I would need some kind of audible bell or something to tell the user when the Pomodoro period or break was done, and when to take a break or return to work. So that was another function.

Because I not only wanted this to run in a shell terminal, I wrote a function that would display the work periods (pomodoros) and break periods in a single line that could be displayed on a status bar like i3status or i3blocks for my favorite tiling window manager, i3wm. This required that I actually have two functions for showing a pomodoro, one that displays inside a terminal and one that runs inside a bar. Well, the “bar” function works such that it displays a line of text that could run inside a status bar, but I haven’t gotten it to actually work inside either i3bar or i3blocks. It displays inside a terminal on a single line just fine. And the other functions works fine inside a terminal window; it takes up about 5 lines of text, separated by line spaces in-between, for a total of ten line spaces.

Here’s the script:

     1	#!/usr/bin/env bash
     2	
     3	#################################
     4	#  Easy little pomodoro time mgmt
     5	#  program to help you not waste time.
     6	#  This runs in a terminal, so you can use it 
     7	#  with a tiling window manager if desired.
     8	#  Requires 'play' is installed (from sox)
     9	#################################
    10	
    11	##  Initial variables
    12	short_break=5
    13	long_break=20
    14	pomodoro=25
    15	## Forgot that all terminals are not UTF
    16	#goal="⌚⌚⌚⌚⌚⌚⌚⌚"
    17	goal="********"
    18	completed_pomodoros=""
    19	start_sound=./sounds/Ship_Bell-Mike_Koenig-1911209136.wav
    20	end_sound=./sounds/foghorn-daniel_simon.wav
    21	((durationinmins=${pomodoro}*3600/60))
    22	
    23	## Run the i3bar version or regular version?
    24	[[ "$@" =~ 'b' ]] && runinbar=true || unset runinbar
    25	
    26	##  Set the start time in seconds
    27	start_time=$(date +%s)
    28	
    29	## Play start or end sounds for periods
    30	play_sound(){
    31	    ## make sure 'play' is installed...
    32	    command -v play >/dev/null 2>&1 || { echo >&2 "play (from sox app) is required but not installed. aborting..."; exit 1; } 
    33	    file=${1}
    34	    play -q $file 2>/dev/null
    35	}
    36	
    37	## Play the start sound
    38	play_sound $start_sound
    39	
    40	## Computes seconds elapsed from after 1st argument
    41	## Expects argument in seconds
    42	compute(){
    43	   start_secs=${1}
    44	   now_time=$(date +%s)
    45	   elapsed=$( expr $now_time - $start_secs )
    46	   printf "%d " "${elapsed}"
    47	}
    48	
    49	## Converts seconds to hours, minutes, seconds
    50	## Just displays minutes and seconds
    51	convertsecs(){
    52	    ((h=${1}/3600))
    53	    ((m=(${1}%3600)/60))
    54	    ((s=${1}%60))
    55	    #printf "%02d:%02d:%02d" $h $m $s
    56	    printf "%02d:%02d" $m $s
    57	}
    58	
    59	## Computes minutes and seconds remaining from start time
    60	## and now time,  Expects now_seconds then start_time
    61	remainingsecs(){
    62	    ((m=${2}-1-${1}%3600/60))
    63	    ((s=(60-${1}%60)))
    64	    [ $m -eq -1 ] && m=0
    65	    printf "%02d:%02d" $m $s
    66	}
    67	
    68	## For showing a list of asterisks and completed Pomodoros
    69	completed(){
    70	    echo '⌚' 
    71	}
    72	
    73	##  Runs a break, either a short one or long one
    74	##  Expects an argument in minutes
    75	run_break(){
    76	    play_sound $end_sound
    77	    start=$(date +%s)
    78	    now=$(date +%s)
    79	    length=${1}
    80	    elapsed_secs=$(expr $now - $start)
    81	    minutes=$((elapsed_secs/60))
    82	
    83	
    84	
    85	    while [ $minutes -le $length ]; do
    86	        now=$(date +%s)
    87	        pomo=$(compute $start)
    88	        minutes=$((elapsed_secs/60))
    89	
    90	        ## exit and start a new Pomodoro when break is spent
    91	        if (($minutes >= $length)) ; then 
    92	            return 0
    93	        fi
    94	
    95	        elapsed_secs=$(expr $now - $start)
    96	        remaining=$(remainingsecs $elapsed_secs $length)
    97	        elapsed=$(convertsecs $pomo)
    98	        if [ $length -ne $short_break ]; then
    99	            break_type="Long Break"
   100	        else
   101	            break_type="Short Break"
   102	        fi
   103	
   104	        clear
   105	## Hot pink may not work well on light terminal but it does on dark terminal
   106	cat <<EOBreak
   107	
   108	
   109	        $(echo -e "\033[38;5;205m")
   110	        //////////////////////   BREAK TIME  //////////////////////////////
   111	
   112	        Break: ${break_type}    Elapsed: ${elapsed}   Remaining:  ${remaining}
   113	        $(echo -e "\033[m")
   114	
   115	EOBreak
   116	
   117	        sleep 1
   118	    done
   119	}
   120	
   121	show_bar(){
   122	    count=0
   123	    period=${1}
   124	    length=${2}
   125	    start=$(date +%s)
   126	    spinner=('\' '|' '/' '—' '\' '|' '/' '—')
   127	
   128	
   129	    while true; do
   130	        elapsedsecs=$(compute $start)
   131	        [ ${elapsedsecs} -gt $((length*60)) ] && return
   132	        pomo=$(compute $start)
   133	        elapsed=$(convertsecs $pomo)
   134	        remaining=$(remainingsecs $pomo $length)
   135	        clear
   136	        printf "%s > %s %ss left %d/%d done" ${period} ${spinner[$count]} ${remaining} ${#completed_pomodoros} ${#goal} 
   137	        sleep 1
   138	        if [ $count -lt 7 ]; then
   139	            ((count++))
   140	        else
   141	            count=0
   142	        fi
   143	    done
   144	
   145	}
   146	
   147	##  This is the main pomodoro screen
   148	show_pom(){
   149	    period=${1}
   150	    length=${2}
   151	    start=$(date +%s)
   152	
   153	    while true; do
   154	        pomo=$(compute $start)
   155	        elapsedsecs=$(compute $start)
   156	        [ ${elapsedsecs} -gt $((length*60)) ] && return
   157	        elapsed=$(convertsecs $pomo)
   158	        remaining=$(remainingsecs $pomo $length)
   159	
   160	    clear
   161	
   162	cat <<EOF
   163	
   164	
   165	                    Welcome to MyPomodoro!
   166	
   167	        ///////////////  Work Time  ///////////////////////
   168	
   169	        Short break: ${short_break}     Elapsed: ${elapsed}
   170	
   171	        Long break: ${long_break}     Remaining: ${remaining}
   172	
   173	        Goal: ${goal}     Completed: ${completed_pomodoros}
   174	EOF
   175	    sleep 1
   176	    done
   177	}
   178	    
   179	
   180	##  This is the main loop
   181	while true; do
   182	    [ "$runinbar" ] && show_bar "Work" ${pomodoro} || show_pom "Work" ${pomodoro}
   183	
   184	    completed_pomodoros+=$(completed)
   185	    if  [[  $(( ${#completed_pomodoros} % 4 )) == 0 ]] && [[ ${#completed_pomodoros} -ne 0 ]]; then
   186	        [ "$runinbar" ] && play_sound $end_sound && show_bar "Long_Break" ${long_break} || run_break ${long_break}
   187	    else
   188	        [ "$runinbar" ] && play_sound $end_sound && show_bar "Short_Break" ${short_break} || run_break ${short_break}
   189	    fi
   190	    start_time=$(date +%s)
   191	    play_sound $start_sound
   192	done

Thoughts on Script

I’m not sure this script will stand, but it seems to work for now. The idea was to pass a ‘-b’ argument if it should run in the bar, and pass no arguments if it should just run in the terminal. So far, it just runs in the terminal, either in a single line or on multiple lines.

Also, I thought I could paste in little watch glyphs using Unicode characters, but then those didn’t show up on some of my simpler terminal emulators. So I went back to just using asterisks.

There was also the need to check whether the Sox ‘play’ command was installed and issue a warning if it wasn’t. The command -v command was of use here.

The entire script relies on a while loop at the bottom of the script, which calls the other functions as required.

Pause?

Strictly speaking, I don’t think I need a pause command. The whole idea of Pomodoro is to concentrate on what you’re doing and not get side tracked. If the phone rings, let the caller leave a message. If wifey texts, look at it during a break. If your kid is stuck by the side of the road, then handle it during a break. If your boss interrupts you, well, then it’s up to you how you handle it. But it seems to me that adding a pause function is counter-intuitive to what the Pomodoro Technique is all about. What do you think?

Going Forward

I really want to get this running in i3blocks. I’m not sure whether it’s possible to get it running in i3status. So far no luck. I’ll keep at it.

I’ve still got some comments in the code to remind me to think about things, like the ‘goal’ string variable. Is there a way to display the unicode characters for hourglass or watch in Urxvt for example? Not sure yet.

I think Sox is a pretty universal sound player. It rarely comes pre-packaged with Linux distributions anymore, but I think it’s available in almost all repos, and has few if any dependencies.

This little application has already taken a surprising amount of time to complete thus far. And it still doesn’t run in i3blocks! But I’ve been using it for my productivity in using i3wm, and it’s easy to run inside of a narrow terminal.

Hopefully, I’ll get it running inside i3blocks real soon now!