Skip to content

Cookbook ​

Short answers to "how do I ...?". Each recipe shows the code and its real output; the reference has the details. The outputs show the terminal at the end of the run, or while running when the recipe says so.

The smallest bar ​

f90
program minimal
!< The smallest progress bar: the default range is [0, 1].
use, intrinsic :: iso_fortran_env, only : R8P=>real64
use forbear, only : bar_object
implicit none
type(bar_object) :: bar ! the progress bar
real(R8P)        :: x   ! progress, in [0, 1]
integer          :: i   ! counter
integer          :: j   ! counter
real(R8P)        :: y   ! work result

x = 0._R8P
call bar%initialize(filled_char_string='+', prefix_string='progress |', suffix_string='|',  &
                    add_progress_percent=.true.)
call bar%start
do i = 1, 20
   x = x + 0.05_R8P
   do j = 1, 1000000
      y = sqrt(x) ! just spend some time
   enddo
   call bar%update(current=x)
enddo
if (y < 0._R8P) print *, y
endprogram minimal
$ minimal
progress |++++++++++++++++++++++++++++++++| 100%

The default range is [0, 1]: pass the fraction done.

A bar over the steps of a loop ​

f90
call bar%initialize(max_value=real(steps, R8P))    ! 1. initialize
call bar%start                                     ! 2. start
do step = 1, steps
   call advance                                    ! the real work
   call bar%update(current=real(step, R8P))        ! 3. update
enddo
print '(A,I0,A)', 'march: ', steps, ' time steps done'
$ march
********************************
march: 50 time steps done

max_value is the number of steps, current the step: both real(real64).

A loop of many iterations ​

f90
call bar%initialize(prefix_string='records ', add_progress_percent=.true., frequency=5, &
                    min_value=real(first - 1, R8P), max_value=real(last, R8P))
call bar%start
do i = first, last
   call bar%update(current=real(i, R8P))
enddo
$ many
records ******************************** 100%

The range starts one before the first record, so that the bar starts at 0%. Updating at every iteration is cheap: the bar is drawn at most ten times a second (min_interval), whatever the number of iterations. frequency=5 draws it only at every 5%, which also keeps the log of a batch job to 21 lines.

Fewer drawings, without recompiling ​

export FORBEAR_MIN_INTERVAL=1 makes every bar of a program wait one second between two drawings on a terminal, unless the program passes min_interval itself; 0 draws at every update. It is the knob for a slow remote terminal, and the docs set it to 0 so that their outputs do not depend on the speed of the machine. See How often the bar is drawn.

A smooth bar ​

f90
call bar%initialize(prefix_string='march ', bracket_left_string='▕', bracket_right_string='▏', &
                    partial_blocks=.true., filled_char_color_fg='cyan',                     &
                    width=40, max_value=real(steps, R8P))
$ march
march ▕██████████████████▍                     ▏

While running. Eight steps per character. For a visible track, give the empty part a background colour, empty_char_color_bg.

A Unicode bar ​

f90
call bar%initialize(prefix_string='march ', bracket_left_string='▕', bracket_right_string='▏', &
                    filled_char_string='█', empty_char_string='░',                         &
                    width=40, max_value=real(steps, R8P))
$ march
march ▕████████████████████░░░░░░░░░░░░░░░░░░░░▏

While running. Save the source as UTF-8; one character for filled_char_string and empty_char_string.

A coloured, solid bar ​

f90
call bar%initialize(prefix_string='march ', prefix_color_fg='cyan', prefix_style='bold_on',             &
                    bracket_left_string='[', bracket_right_string=']',                                  &
                    filled_char_string='█', filled_char_color_fg='green',                               &
                    empty_char_string='█', empty_char_color_fg='black_intense',                         &
                    suffix_string=' time steps', suffix_color_fg='yellow', suffix_style='italics_on', &
                    width=40, max_value=real(steps, R8P))
$ march
march [████████████████████████████████████████] time steps

While running. Full blocks in two foreground colours. The colour and style names are in Colours and styles.

Exact colours ​

f90
call bar%initialize(prefix_string='march ', prefix_color_fg='#89B4FA', width=40,                  &
                    filled_char_string='█', filled_char_color_fg='#A6E3A1',                       &
                    empty_char_string='█', empty_char_color_fg='#313244',                         &
                    add_progress_percent=.true., progress_percent_color_fg='#F9E2AF', max_value=real(steps, R8P))
$ march
march ████████████████████████████████████████  50%

While running. #rrggbb in any colour keyword: the same colour on every terminal palette (here Catppuccin Mocha's blue, green, yellow and surface). The terminal must have true colour, as most modern ones do. See 24-bit colours.

A redline: colours by position ​

f90
call bar%initialize(prefix_string='RPM ', prefix_color_fg='#2EF5C0', prefix_style='bold_on',               &
                    filled_char_string='▌', empty_char_string='▌', empty_char_color_fg='#0E342C',          &
                    bar_zones='0.7:#2EF5C0 0.88:#FFB000 1:#FF3B30',                                        &
                    add_progress_percent=.true., progress_percent_color_fg='#FFB000',                      &
                    width=40, max_value=real(steps, R8P))
$ tach
RPM ▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌  80%

While running. Each limit:colour of bar_zones colours the filled cells up to that fraction of the bar; the dark empty cells are the unlit segments of a display. See Zones.

A rising ramp ​

f90
call bar%initialize(prefix_string='RPM ', prefix_color_fg='#2EF5C0', prefix_style='bold_on',               &
                    bar_profile='ramp', empty_char_color_fg='#0E342C',                                     &
                    bar_zones='0.7:#2EF5C0 0.88:#FFB000 1:#FF3B30',                                        &
                    add_progress_percent=.true., progress_percent_color_fg='#FFB000',                      &
                    width=40, max_value=real(steps, R8P))
$ tach
RPM ▁▁▁▁▁▁▂▂▂▂▂▂▃▃▃▃▃▄▄▄▄▄▄▅▅▅▅▅▆▆▆▆▆▆▇▇▇▇▇█  80%

While running. bar_profile='ramp': blocks rising from one eighth to the full height, lit or unlit; with zones, a tachometer. See Profiles.

Seven-segment numbers ​

f90
call bar%initialize(prefix_string='march ', prefix_color_fg='#2EF5C0', width=30,                              &
                    filled_char_string='▌', filled_char_color_fg='#2EF5C0',                                     &
                    empty_char_string='▌', empty_char_color_fg='#0E342C',                                       &
                    add_progress_percent=.true., progress_percent_color_fg='#FFB000',                           &
                    add_progress_count=.true., progress_count_color_fg='#FFB000',                               &
                    digits='segment', digits_unlit_color='#3A2800', max_value=real(steps, R8P))
$ march
march ▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌ 🯸🯸🯵% 🯸🯸🯷/🯱🯲🯰

While running. digits='segment' for the percent, count, ETA and elapsed time; digits_unlit_color pads them with unlit 8s. Check that the font of your terminal has these characters (Cascadia Code, Iosevka, JuliaMono do; most defaults do not): see Segment digits.

A dashboard look in one keyword ​

f90
call vfd%initialize(theme='vfd', prefix_string='vfd   ', add_progress_percent=.true., width=30, &
                    max_value=real(steps, R8P))
call amber%initialize(theme='amber', prefix_string='amber ', bar_profile='ramp', add_progress_percent=.true., &
                      width=30, max_value=real(steps, R8P), position=1)
call kitt%initialize(theme='kitt', prefix_string='kitt  ', indeterminate=.true., add_progress_count=.true., &
                     width=30, position=2)
$ march
vfd   ▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌  58%
amber ▁▁▁▁▁▂▂▂▂▃▃▃▃▄▄▄▄▅▅▅▅▆▆▆▆▇▇▇▇█  58%
kitt  ▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌ 29

While running. theme='vfd', 'amber' or 'kitt' sets the segments and the colours of every element; anything passed explicitly wins, and zones, a ramp or segment digits go on top. See Themes.

Percent, count, speed, ETA, scale, times, summary ​

f90
call bar%initialize(prefix_string='march ', bracket_left_string='[', bracket_right_string=']',  &
                    filled_char_string='#', empty_char_string='.',                             &
                    add_progress_percent=.true., progress_percent_color_fg='yellow',           &
                    add_progress_count=.true., progress_count_color_fg='cyan',                 &
                    add_progress_speed=.true., progress_speed_color_fg='green',                &
                    add_eta=.true., eta_color_fg='blue',                                       &
                    add_scale_bar=.true., scale_bar_color_fg='blue',                           &
                    add_date_time=.true., date_time_color_fg='magenta',                        &
                    add_summary=.true., summary_color_fg='green',                              &
                    width=30, max_value=real(steps, R8P))
$ march
      [ 0.00 (min)        (max) 50.00]
march [##############################] 100% 50/50 ( nn.nn%/s) ETA hh:mm:ss
[yyyy/mm/dd hh:mm:ss - yyyy/mm/dd hh:mm:ss]
[done in n.nn s, nn.nn/s]

The scale needs width of at least 22 (see Number formats). With a narrower bar the program stops:

f90
program scale_narrow
!< The scale needs a bar at least 22 characters wide.
use forbear, only : bar_object
implicit none
type(bar_object) :: bar ! the progress bar

call bar%initialize(width=20, add_scale_bar=.true.)
endprogram scale_narrow
$ scale_narrow
ERROR STOP error: for adding scale bar the bar width must be at least 22 chars
[exit status 1]

The speed of now, not the average ​

f90
call bar%initialize(add_progress_speed=.true., add_eta=.true., smoothing=1._R8P, max_value=real(steps, R8P)) ! momentary

The speed and the ETA follow an exponential moving average, with weight smoothing (0.3) on the last speed: 1 shows the momentary speed, 0 the average since the start. See update.

A prefix that names the phase ​

f90
call bar%start
do step = 1, steps
   if (step == 10) bar%prefix%string = 'assemble '
   if (step == 30) bar%prefix%string = 'solve    '
   call advance
   call bar%update(current=real(step, R8P))
enddo
$ march
solve    [***********************-------]  78%

While running, in the third phase. Assign bar%prefix%string (or bar%suffix%string) while the bar runs: the next drawing shows it, in the colours of initialize. Keep the phases the same width, or the line jumps.

A spinner next to the bar ​

f90
call bar%initialize(prefix_string='march ', bracket_left_string='[', bracket_right_string='] ', &
                    filled_char_string='#', empty_char_string='.',                              &
                    spinner_string='⠋', spinner_color_fg='red',                                 &
                    width=40, max_value=real(steps, R8P))
$ march
march [####################....................] ⠴

While running. Pick the key of a spinner from the catalogue.

A spinner alone ​

f90
call bar%initialize(prefix_string='march ', spinner_string='⠋', spinner_color_fg='red', &
                    width=0, max_value=real(steps, R8P))
$ march
march [########################################] ⠋
march ⠴

While running. width=0: no bar, only the prefix and the spinner.

A percentage alone ​

f90
call bar%initialize(prefix_string='march ', add_progress_percent=.true., &
                    width=0, max_value=real(steps, R8P))
$ march
march [########################################] ⠋
march ⠋
march   50%

Lines above the bar, a message at its end ​

f90
call bar%start
residual = 1._R8P
do step = 1, steps
   call advance
   residual = residual / 2._R8P
   if (mod(step, 10) == 0) then
      write(text, '(A,I0,A)') 'step ', step, ': solution saved'
      call bar%write(trim(text))                            ! a line above the bar
   endif
   write(text, '(A,ES9.2)') 'residual', residual
   call bar%update(current=real(step, R8P), message=trim(text)) ! the end of the bar line
enddo
$ march
step 10: solution saved
step 20: solution saved
step 30: solution saved
step 40: solution saved
step 50: solution saved
march [##############################] 100% residual 8.88E-16

bar%write prints above the running bar; message= shows the text at the end of the bar line until the next one. A plain print would be drawn over the bar.

Output of a library while the bar runs ​

f90
call bar%start
do step = 1, steps
   call advance
   call bar%update(current=real(step, R8P))
   if (mod(step, 20) == 0) then
      call bar%suspend           ! the bar leaves the terminal...
      call save_solution(step)   ! ...to a library that prints on its own...
      call bar%resume            ! ...and comes back below its lines
   endif
enddo
$ march
io_library: writing solution_0020.h5
io_library: done, 2.1 MB
io_library: writing solution_0040.h5
io_library: done, 2.1 MB
march [##############################] 100%

suspend clears the bar and frees the terminal, resume draws it again below what was printed: see suspend and resume.

A bar for each loop of a nest ​

f90
call steps_bar%initialize(prefix_string='time step ', bracket_left_string='[', bracket_right_string=']',  &
                          partial_blocks=.true., filled_char_color_fg='cyan', add_progress_count=.true., &
                          width=30, max_value=real(steps, R8P))
call iterations_bar%initialize(prefix_string='  newton  ', bracket_left_string='[', bracket_right_string=']',  &
                               partial_blocks=.true., filled_char_color_fg='yellow', add_progress_count=.true., &
                               width=30, max_value=real(iterations, R8P), position=1)
f90
call steps_bar%start
do step = 1, steps
   call iterations_bar%start
   do iteration = 1, iterations
      call advance
      call iterations_bar%update(current=real(iteration, R8P))
   enddo
   call steps_bar%update(current=real(step, R8P))
enddo
print '(A)', 'march: done'
$ march
time step [████████████                  ] 2/5
  newton  [██████████████████            ]  6/10

While running. The inner bar, at position=1, is cleared when it completes and drawn again by its next start.

The bar on standard error ​

f90
call bar%initialize(prefix_string='march ', bracket_left_string='[', bracket_right_string=']',  &
                    filled_char_string='#', empty_char_string='.', add_progress_percent=.true., &
                    frequency=10, output_unit=error_unit,                                      &
                    width=40, max_value=real(steps, R8P))

Standard output keeps only the results of the program,

$ stderr 2> /dev/null
step  10, residual  9.77E-04
step  20, residual  9.54E-07
step  30, residual  9.31E-10
step  40, residual  9.09E-13
step  50, residual  8.88E-16

and standard error only the bar:

$ stderr > results.txt
march [########################################] 100%

A clean log in a batch job ​

Nothing to do: when the output of the bar is not a terminal, the bar writes a plain line every 10%.

$ march > run.log; cat run.log
march [..............................]   0%  0/50 ETA --:--:--
march [###...........................]  10%  5/50 ETA hh:mm:ss
march [######........................]  20% 10/50 ETA hh:mm:ss
march [#########.....................]  30% 15/50 ETA hh:mm:ss
march [############..................]  40% 20/50 ETA hh:mm:ss
march [###############...............]  50% 25/50 ETA hh:mm:ss
march [##################............]  60% 30/50 ETA hh:mm:ss
march [#####################.........]  70% 35/50 ETA hh:mm:ss
march [########################......]  80% 40/50 ETA hh:mm:ss
march [###########################...]  90% 45/50 ETA hh:mm:ss
march [##############################] 100% 50/50 ETA hh:mm:ss
[done in n.nn s, nn.nn/s]
march: done

interactive=.false. (or FORBEAR_INTERACTIVE=0) forces this mode on a terminal too; see Terminals and logs.

A log line every ten minutes ​

fortran
call bar%initialize(max_value=real(steps, R8P), log_interval=600._R8P)

A solver of unknown length, logging a line every so often (here every few iterations, from chapter 8):

$ march > run.log; cat run.log
solve 0 iterations, hh:mm:ss elapsed
solve 2 iterations, hh:mm:ss elapsed
solve 4 iterations, hh:mm:ss elapsed
solve 6 iterations, hh:mm:ss elapsed
solve 8 iterations, hh:mm:ss elapsed
solve 10 iterations, hh:mm:ss elapsed

Or, for every bar, without recompiling: export FORBEAR_LOG_INTERVAL=600 in the job script. See Signs of life in long jobs.

No bars at all ​

Turn every bar of a program off from its environment, without recompiling; write still prints:

$ FORBEAR_DISABLE=1 march
march: done

In code, disabled=.true.; under MPI, draw the bar of one process only:

fortran
call bar%initialize(max_value=real(steps, R8P), disabled=(rank /= 0)) ! rank from MPI_Comm_rank

A layout of your own ​

f90
call bar%initialize(template='{prefix} {percent:yellow} [{bar:20}] {count} files, {eta:blue} left', &
                    prefix_string='load', max_value=real(n, R8P))
$ layout
load  50% [**********----------]  6/12 files, hh:mm:ss left

While running. Any order, any words between the fields: see Layout templates. At the end:

$ layout
load 100% [********************] 12/12 files, hh:mm:ss left

A field of your own ​

f90
module residual_fields
!< A field of the program: the residual of the solution, read at every drawing.
use, intrinsic :: iso_fortran_env, only : R8P=>real64
use forbear, only : field_object, progress_object
implicit none
private
public :: residual_field

type, extends(field_object) :: residual_field
   real(R8P), pointer :: value => null() ! the residual, a variable of the program
   contains
      procedure, pass(self) :: render
endtype residual_field

contains
   function render(self, progress) result(text)
   !< Return the residual, always 7 characters wide.
   class(residual_field), intent(in) :: self
   type(progress_object), intent(in) :: progress
   character(len=:), allocatable     :: text
   character(len=7)                  :: buffer
   write(buffer, '(ES7.1)') self%value
   text = buffer
   endfunction render
endmodule residual_fields
f90
call bar%initialize(template='march {bar:30} {percent:yellow} step {count:cyan} ETA {eta:blue} res {residual:magenta}', &
                    partial_blocks=.true., filled_char_color_fg='cyan', empty_char_color_bg='black_intense', &
                    max_value=real(steps, R8P))
field%value => residual
call bar%add_field('residual', field) ! after initialize, before start
$ march
march ██████████████████████████████ 100% step 50/50 ETA hh:mm:ss res 8.9E-16

The field reads the residual of the program through a pointer, at every drawing.

A loop of unknown length ​

f90
call bar%initialize(template='solve {spinner:cyan} [{bar:20}] {count:yellow} iterations, {elapsed:blue}', &
                    indeterminate=.true., spinner_string='⠋', filled_char_string='#', empty_char_string='.')
residual = 1._R8P
iterations = 0
call bar%start
do while (residual > 1.e-6_R8P) ! how many iterations: unknown until done
   call advance
   residual = residual / 2._R8P
   iterations = iterations + 1
   call bar%update(current=real(iterations, R8P))
enddo
call bar%finish
$ march
solve ⠇ [........#####.......] 8 iterations, hh:mm:ss

While running. current counts the iterations; finish ends the bar. No percent, ETA or scale: see chapter 10.

A scanner for a loop of unknown length ​

f90
call bar%initialize(prefix_string='solve ', prefix_color_fg='#FF3B30',                                       &
                    indeterminate=.true., width=24, add_progress_count=.true., progress_count_color_fg='#FFB000', &
                    filled_char_string='▬', filled_char_color_fg='#FF3B30',                                   &
                    empty_char_string='▬', empty_char_color_fg='#3C0C0A',                                     &
                    pulse_trail='#C0281E #7A1912 #4A0F0B')
$ solve
solve ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 31

While running. pulse_trail turns the pulse of an indeterminate bar into a one-cell head with a fading trail; with #rrggbb shades, as smooth as you like. See Pulse trail.

Leaving a loop early ​

f90
steps = 50
call bar%initialize(prefix_string='march ', bracket_left_string='[', bracket_right_string=']', &
                    filled_char_string='#', empty_char_string='.', add_progress_percent=.true., &
                    progress_percent_color_fg='yellow', message_color_fg='green', width=30,     &
                    max_value=real(steps, R8P))
residual = 1._R8P
call bar%start
do step = 1, steps
   call advance
   residual = residual / 2._R8P
   call bar%update(current=real(step, R8P))
   if (residual < 1.e-9_R8P) exit ! steady state: the remaining steps are not needed
enddo
call bar%finish(message='steady state')

finish ends the bar where the loop left it: the line ended, the cursor shown, the summary written if asked.

Several bars one after another ​

f90
do p = 1, size(phases)
   call bar%initialize(prefix_string=phases(p), bracket_left_string='[', bracket_right_string=']',  &
                       filled_char_string='=', empty_char_string=' ', add_progress_percent=.true., width=30)
   call run_phase
enddo
$ sequence
mesh     ********************************
solve    ********************************
write    ********************************
mesh     [==============================] 100%
solve    [==============================] 100%
write    [==============================] 100%

Every initialize resets the bar, so pass every setting each time. The first three lines of the output come from the other loop of the program, which copies a configured bar (bar = style) and then calls initialize to change the prefix: the copy is lost, and those bars have the defaults.

f90
call style%initialize(bracket_left_string='[', bracket_right_string=']',  filled_char_string='=', &
                      empty_char_string=' ', add_progress_percent=.true., width=30)
do p = 1, size(phases)
   bar = style                                       ! copy the configuration
   call bar%initialize(prefix_string=phases(p))      ! re-initialize: everything else is reset!
   call run_phase
enddo

bar%destroy frees a bar and resets every default, as initialize does first.

Two bars running at the same time need their own lines: see A bar for each loop of a nest.

A cursor that stays visible ​

f90
call bar%initialize(hide_cursor=.false., max_value=real(steps, R8P)) ! the cursor stays visible, even if the program stops

On a terminal the bar hides the cursor while it runs and shows it again at 100%. If the program may stop before (an error stop, a crash, Ctrl-C), hide_cursor=.false. leaves it visible. See If the program stops.

Printing only when no bar is running ​

f90
call bar%start
do step = 1, steps
   call bar%update(current=real(step, R8P))
   if (.not.bar%is_stdout_locked()) print '(A)', 'the bar is done: the terminal is free again'
enddo

is_stdout_locked is true while a bar runs on a terminal: anything printed then would be drawn over the bar (use bar%write instead). Once the bar completes, the terminal is free. See is_stdout_locked.

Released under the GPL v3, BSD 2-Clause, BSD 3-Clause or MIT License.