1{-# language DeriveGeneric #-}
2{-# language GeneralizedNewtypeDeriving #-}
3{-# language OverloadedStrings #-}
4{-# language PackageImports #-}
5{-# language ScopedTypeVariables #-}
6
7{- |
8A progress bar in the terminal.
9
10A progress bar conveys the progress of a task. Use a progress bar to
11provide a visual cue that processing is underway.
12-}
13module System.ProgressBar
14    ( -- * Getting started
15      -- $start
16
17      -- * Example
18      -- $example
19
20      -- * Progress bars
21      ProgressBar
22    , newProgressBar
23    , hNewProgressBar
24    , renderProgressBar
25    , updateProgress
26    , incProgress
27      -- * Options
28    , Style(..)
29    , EscapeCode
30    , OnComplete(..)
31    , defStyle
32    , ProgressBarWidth(..)
33      -- * Progress
34    , Progress(..)
35      -- * Labels
36    , Label(..)
37    , Timing(..)
38    , msg
39    , percentage
40    , exact
41    , elapsedTime
42    , remainingTime
43    , totalTime
44    , renderDuration
45    ) where
46
47import "base" Control.Concurrent.MVar ( MVar, newMVar, modifyMVar_)
48import "base" Control.Monad ( when )
49import "base" Data.Int       ( Int64 )
50import "base" Data.Monoid    ( Monoid, mempty )
51import "base" Data.Ratio     ( Ratio, (%) )
52import "base" Data.Semigroup ( Semigroup, (<>) )
53import "base" Data.String    ( IsString, fromString )
54import "base" GHC.Generics   ( Generic )
55import "base" System.IO      ( Handle, stderr, hFlush )
56import "deepseq" Control.DeepSeq ( NFData, rnf )
57import qualified "terminal-size" System.Console.Terminal.Size as TS
58import qualified "text" Data.Text.Lazy             as TL
59import qualified "text" Data.Text.Lazy.Builder     as TLB
60import qualified "text" Data.Text.Lazy.Builder.Int as TLB
61import qualified "text" Data.Text.Lazy.IO          as TL
62import "time" Data.Time.Clock ( UTCTime, NominalDiffTime, diffUTCTime, getCurrentTime )
63
64--------------------------------------------------------------------------------
65
66-- | A terminal progress bar.
67--
68-- A 'ProgressBar' value contains the state of a progress bar.
69--
70-- Create a progress bar with 'newProgressBar' or 'hNewProgressBar'.
71-- Update a progress bar with 'updateProgress' or 'incProgress'.
72data ProgressBar s
73   = ProgressBar
74     { pbStyle :: !(Style s)
75     , pbStateMv :: !(MVar (State s))
76     , pbRefreshDelay :: !Double
77     , pbStartTime :: !UTCTime
78     , pbHandle :: !Handle
79     }
80
81instance (NFData s) => NFData (ProgressBar s) where
82    rnf pb =  pbStyle pb
83        `seq` pbStateMv pb
84        `seq` pbRefreshDelay pb
85        `seq` pbStartTime pb
86        -- pbHandle is ignored
87        `seq` ()
88
89-- | State of a progress bar.
90data State s
91   = State
92     { stProgress :: !(Progress s)
93       -- ^ Current progress.
94     , stRenderTime :: !UTCTime
95       -- ^ Moment in time of last render.
96     }
97
98-- | An amount of progress.
99data Progress s
100   = Progress
101     { progressDone :: !Int
102       -- ^ Amount of work completed.
103     , progressTodo :: !Int
104       -- ^ Total amount of work.
105     , progressCustom :: !s
106       -- ^ A value which is used by custom labels. The builtin labels
107       -- do not care about this field. You can ignore it by using the
108       -- unit value '()'.
109     }
110
111progressFinished :: Progress s -> Bool
112progressFinished p = progressDone p >= progressTodo p
113
114-- | Creates a progress bar.
115--
116-- The progress bar is drawn immediately. Update the progress bar with
117-- 'updateProgress' or 'incProgress'. Do not output anything to your
118-- terminal between updates. It will mess up the animation.
119--
120-- The progress bar is written to 'stderr'. Write to another handle
121-- with 'hNewProgressBar'.
122newProgressBar
123    :: Style s -- ^ Visual style of the progress bar.
124    -> Double -- ^ Maximum refresh rate in Hertz.
125    -> Progress s -- ^ Initial progress.
126    -> IO (ProgressBar s)
127newProgressBar = hNewProgressBar stderr
128
129-- | Creates a progress bar which outputs to the given handle.
130--
131-- See 'newProgressBar'.
132hNewProgressBar
133    :: Handle
134       -- ^ File handle on which the progress bar is drawn. Usually
135       -- you select a standard stream like 'stderr' or 'stdout'.
136    -> Style s -- ^ Visual style of the progress bar.
137    -> Double -- ^ Maximum refresh rate in Hertz.
138    -> Progress s -- ^ Initial progress.
139    -> IO (ProgressBar s)
140hNewProgressBar hndl style maxRefreshRate initProgress = do
141    style' <- updateWidth style
142
143    startTime <- getCurrentTime
144    hPutProgressBar hndl style' initProgress (Timing startTime startTime)
145
146    stateMv <- newMVar
147      State
148      { stProgress   = initProgress
149      , stRenderTime = startTime
150      }
151    pure ProgressBar
152         { pbStyle = style'
153         , pbStateMv = stateMv
154         , pbRefreshDelay = recip maxRefreshRate
155         , pbStartTime = startTime
156         , pbHandle = hndl
157         }
158
159-- | Update the width based on the current terminal.
160updateWidth :: Style s -> IO (Style s)
161updateWidth style =
162    case styleWidth style of
163      ConstantWidth {} -> pure style
164      TerminalWidth {} -> do
165        mbWindow <- TS.size
166        pure $ case mbWindow of
167          Nothing -> style
168          Just window -> style{ styleWidth = TerminalWidth (TS.width window) }
169
170-- | Change the progress of a progress bar.
171--
172-- This function is thread safe. Multiple threads may update a single
173-- progress bar at the same time.
174--
175-- There is a maximum refresh rate. This means that some updates might not be drawn.
176updateProgress
177    :: forall s
178     . ProgressBar s -- ^ Progress bar to update.
179    -> (Progress s -> Progress s) -- ^ Function to change the progress.
180    -> IO ()
181updateProgress progressBar f = do
182    updateTime <- getCurrentTime
183    modifyMVar_ (pbStateMv progressBar) $ renderAndUpdate updateTime
184  where
185    renderAndUpdate :: UTCTime -> State s -> IO (State s)
186    renderAndUpdate updateTime state = do
187        when shouldRender $
188          hPutProgressBar hndl (pbStyle progressBar) newProgress timing
189        pure State
190             { stProgress = newProgress
191             , stRenderTime = if shouldRender then updateTime else stRenderTime state
192             }
193      where
194        timing = Timing
195                 { timingStart = pbStartTime progressBar
196                 , timingLastUpdate = updateTime
197                 }
198
199        shouldRender = not tooFast || finished
200        tooFast = secSinceLastRender <= pbRefreshDelay progressBar
201        finished = progressFinished newProgress
202
203        newProgress = f $ stProgress state
204
205        -- Amount of time that passed since last render, in seconds.
206        secSinceLastRender :: Double
207        secSinceLastRender = realToFrac $ diffUTCTime updateTime (stRenderTime state)
208
209    hndl = pbHandle progressBar
210
211-- | Increment the progress of an existing progress bar.
212--
213-- See 'updateProgress' for more information.
214incProgress
215    :: ProgressBar s -- ^ Progress bar which needs an update.
216    -> Int -- ^ Amount by which to increment the progress.
217    -> IO ()
218incProgress pb n = updateProgress pb $ \p -> p{ progressDone = progressDone p + n }
219
220hPutProgressBar :: Handle -> Style s -> Progress s -> Timing -> IO ()
221hPutProgressBar hndl style progress timing = do
222    TL.hPutStr hndl $ renderProgressBar style progress timing
223    TL.hPutStr hndl $
224      if progressFinished progress
225      then case styleOnComplete style of
226             WriteNewline -> "\n"
227             -- Move to beginning of line and then clear everything to
228             -- the right of the cursor.
229             Clear -> "\r\ESC[K"
230      else "\r"
231    hFlush hndl
232
233-- | Renders a progress bar.
234--
235-- >>> let t = UTCTime (ModifiedJulianDay 0) 0
236-- >>> renderProgressBar defStyle (Progress 30 100 ()) (Timing t t)
237-- "[============>..............................]  30%"
238--
239-- Note that this function can not use 'TerminalWidth' because it
240-- doesn't use 'IO'. Use 'newProgressBar' or 'hNewProgressBar' to get
241-- automatic width.
242renderProgressBar
243    :: Style s
244    -> Progress s -- ^ Current progress.
245    -> Timing -- ^ Timing information.
246    -> TL.Text -- ^ Textual representation of the 'Progress' in the given 'Style'.
247renderProgressBar style progress timing = TL.concat
248    [ styleEscapePrefix style progress
249    , prefixLabel
250    , prefixPad
251    , styleEscapeOpen style progress
252    , styleOpen style
253    , styleEscapeDone style progress
254    , TL.replicate completed $ TL.singleton $ styleDone style
255    , styleEscapeCurrent style progress
256    , if remaining /= 0 && completed /= 0
257      then TL.singleton $ styleCurrent style
258      else ""
259    , styleEscapeTodo style progress
260    , TL.replicate
261        (remaining - if completed /= 0 then 1 else 0)
262        (TL.singleton $ styleTodo style)
263    , styleEscapeClose style progress
264    , styleClose style
265    , styleEscapePostfix style progress
266    , postfixPad
267    , postfixLabel
268    ]
269  where
270    todo = fromIntegral $ progressTodo progress
271    done = fromIntegral $ progressDone progress
272    -- Amount of (visible) characters that should be used to display to progress bar.
273    width = fromIntegral $ getProgressBarWidth $ styleWidth style
274
275    -- Amount of work completed.
276    fraction :: Ratio Int64
277    fraction | todo /= 0 = done % todo
278             | otherwise = 0 % 1
279
280    -- Amount of characters available to visualize the progress.
281    effectiveWidth = max 0 $ width - usedSpace
282    -- Amount of printing characters needed to visualize everything except the bar .
283    usedSpace =   TL.length (styleOpen  style)
284                + TL.length (styleClose style)
285                + TL.length prefixLabel
286                + TL.length postfixLabel
287                + TL.length prefixPad
288                + TL.length postfixPad
289
290    -- Number of characters needed to represent the amount of work
291    -- that is completed. Note that this can not always be represented
292    -- by an integer.
293    numCompletedChars :: Ratio Int64
294    numCompletedChars = fraction * (effectiveWidth % 1)
295
296    completed, remaining :: Int64
297    completed = min effectiveWidth $ floor numCompletedChars
298    remaining = effectiveWidth - completed
299
300    prefixLabel, postfixLabel :: TL.Text
301    prefixLabel  = runLabel (stylePrefix  style) progress timing
302    postfixLabel = runLabel (stylePostfix style) progress timing
303
304    prefixPad, postfixPad :: TL.Text
305    prefixPad  = pad prefixLabel
306    postfixPad = pad postfixLabel
307
308pad :: TL.Text -> TL.Text
309pad s | TL.null s = TL.empty
310      | otherwise = TL.singleton ' '
311
312-- | Width of progress bar in characters.
313data ProgressBarWidth
314   = ConstantWidth !Int
315     -- ^ A constant width.
316   | TerminalWidth !Int
317     -- ^ Use the entire width of the terminal.
318     --
319     -- Identical to 'ConstantWidth' if the width of the terminal can
320     -- not be determined.
321     deriving (Generic)
322
323instance NFData ProgressBarWidth
324
325getProgressBarWidth :: ProgressBarWidth -> Int
326getProgressBarWidth (ConstantWidth n) = n
327getProgressBarWidth (TerminalWidth n) = n
328
329{- | Visual style of a progress bar.
330
331The style determines how a progress bar is rendered to text.
332
333The textual representation of a progress bar follows the following template:
334
335\<__prefix__>\<__open__>\<__done__>\<__current__>\<__todo__>\<__close__>\<__postfix__>
336
337Where \<__done__> and \<__todo__> are repeated as often as necessary.
338
339Consider the following progress bar
340
341> "Working [=======>.................]  30%"
342
343This bar can be specified using the following style:
344
345@
346'Style'
347{ 'styleOpen'    = \"["
348, 'styleClose'   = \"]"
349, 'styleDone'    = \'='
350, 'styleCurrent' = \'>'
351, 'styleTodo'    = \'.'
352, 'stylePrefix'  = 'msg' \"Working"
353, 'stylePostfix' = 'percentage'
354, 'styleWidth'   = 'ConstantWidth' 40
355, 'styleEscapeOpen'    = const 'TL.empty'
356, 'styleEscapeClose'   = const 'TL.empty'
357, 'styleEscapeDone'    = const 'TL.empty'
358, 'styleEscapeCurrent' = const 'TL.empty'
359, 'styleEscapeTodo'    = const 'TL.empty'
360, 'styleEscapePrefix'  = const 'TL.empty'
361, 'styleEscapePostfix' = const 'TL.empty'
362, 'styleOnComplete' = 'WriteNewline'
363}
364@
365-}
366data Style s
367   = Style
368     { styleOpen :: !TL.Text
369       -- ^ Bar opening symbol.
370     , styleClose :: !TL.Text
371       -- ^ Bar closing symbol
372     , styleDone :: !Char
373       -- ^ Completed work.
374     , styleCurrent :: !Char
375       -- ^ Symbol used to denote the current amount of work that has been done.
376     , styleTodo :: !Char
377       -- ^ Work not yet completed.
378     , stylePrefix :: Label s
379       -- ^ Prefixed label.
380     , stylePostfix :: Label s
381       -- ^ Postfixed label.
382     , styleWidth :: !ProgressBarWidth
383       -- ^ Total width of the progress bar.
384     , styleEscapeOpen :: EscapeCode s
385       -- ^ Escape code printed just before the 'styleOpen' symbol.
386     , styleEscapeClose :: EscapeCode s
387       -- ^ Escape code printed just before the 'styleClose' symbol.
388     , styleEscapeDone :: EscapeCode s
389       -- ^ Escape code printed just before the first 'styleDone' character.
390     , styleEscapeCurrent :: EscapeCode s
391       -- ^ Escape code printed just before the 'styleCurrent' character.
392     , styleEscapeTodo :: EscapeCode s
393       -- ^ Escape code printed just before the first 'styleTodo' character.
394     , styleEscapePrefix :: EscapeCode s
395       -- ^ Escape code printed just before the 'stylePrefix' label.
396     , styleEscapePostfix :: EscapeCode s
397       -- ^ Escape code printed just before the 'stylePostfix' label.
398     , styleOnComplete :: !OnComplete
399       -- ^ What happens when progress is finished.
400     } deriving (Generic)
401
402instance (NFData s) => NFData (Style s)
403
404-- | An escape code is a sequence of bytes which the terminal looks
405-- for and interprets as commands, not as character codes.
406--
407-- It is vital that the output of this function, when send to the
408-- terminal, does not result in characters being drawn.
409type EscapeCode s
410   = Progress s -- ^ Current progress bar state.
411  -> TL.Text -- ^ Resulting escape code. Must be non-printable.
412
413-- | What happens when a progress bar is finished.
414data OnComplete
415   = WriteNewline
416     -- ^ Write a new line when the progress bar is finished. The
417     -- completed progress bar will remain visible.
418   | Clear -- ^ Clear the progress bar once it is finished.
419     deriving (Generic)
420
421instance NFData OnComplete
422
423-- | The default style.
424--
425-- This style shows the progress as a percentage. It does not use any
426-- escape sequences.
427--
428-- Override some fields of the default instead of specifying all the
429-- fields of a 'Style' record.
430defStyle :: Style s
431defStyle =
432    Style
433    { styleOpen          = "["
434    , styleClose         = "]"
435    , styleDone          = '='
436    , styleCurrent       = '>'
437    , styleTodo          = '.'
438    , stylePrefix        = mempty
439    , stylePostfix       = percentage
440    , styleWidth         = TerminalWidth 50
441    , styleEscapeOpen    = const TL.empty
442    , styleEscapeClose   = const TL.empty
443    , styleEscapeDone    = const TL.empty
444    , styleEscapeCurrent = const TL.empty
445    , styleEscapeTodo    = const TL.empty
446    , styleEscapePrefix  = const TL.empty
447    , styleEscapePostfix = const TL.empty
448    , styleOnComplete    = WriteNewline
449    }
450
451-- | A label is a part of a progress bar that changes based on the progress.
452--
453-- Labels can be at the front (prefix) or at the back (postfix) of a progress bar.
454--
455-- Labels can use both the current amount of progress and the timing
456-- information to generate some text.
457newtype Label s = Label{ runLabel :: Progress s -> Timing -> TL.Text } deriving (NFData)
458
459-- | Combining labels combines their output.
460instance Semigroup (Label s) where
461    Label f <> Label g = Label $ \p t -> f p t <> g p t
462
463-- | The mempty label always outputs an empty text.
464instance Monoid (Label s) where
465    mempty = msg TL.empty
466    mappend = (<>)
467
468-- | Every string is a label which ignores its input and just outputs
469-- that string.
470instance IsString (Label s) where
471    fromString = msg . TL.pack
472
473-- | Timing information about a 'ProgressBar'.
474--
475-- This information is used by 'Label's to calculate elapsed time, remaining time, total time, etc.
476--
477-- See 'elapsedTime', 'remainingTime' and 'totalTime'.
478data Timing
479   = Timing
480     { timingStart :: !UTCTime
481       -- ^ Moment in time when a progress bar was created. See
482       -- 'newProgressBar'.
483     , timingLastUpdate :: !UTCTime
484       -- ^ Moment in time of the most recent progress update.
485     }
486
487-- | Static text.
488--
489-- The output does not depend on the input.
490--
491-- >>> msg "foo" st
492-- "foo"
493msg :: TL.Text -> Label s
494msg s = Label $ \_ _ -> s
495
496-- | Progress as a percentage.
497--
498-- >>> runLabel $ percentage (Progress 30 100 ()) someTiming
499-- " 30%"
500--
501-- __Note__: if no work is to be done (todo == 0) the percentage will
502-- be shown as 100%.
503percentage :: Label s
504percentage = Label render
505  where
506    render progress _timing
507      | todo == 0 = "100%"
508      | otherwise = TL.justifyRight 4 ' ' $ TLB.toLazyText $
509                      TLB.decimal (round (done % todo * 100) :: Int)
510                      <> TLB.singleton '%'
511      where
512        done = progressDone progress
513        todo = progressTodo progress
514
515-- | Progress as a fraction of the total amount of work.
516--
517-- >>> runLabel $ exact (Progress 30 100 ()) someTiming
518-- " 30/100"
519exact :: Label s
520exact = Label render
521  where
522    render progress _timing =
523        TL.justifyRight (TL.length todoStr) ' ' doneStr <> "/" <> todoStr
524      where
525        todoStr = TLB.toLazyText $ TLB.decimal todo
526        doneStr = TLB.toLazyText $ TLB.decimal done
527
528        done = progressDone progress
529        todo = progressTodo progress
530
531-- | Amount of time that has elapsed.
532--
533-- Time starts when a progress bar is created.
534--
535-- The user must supply a function which actually renders the amount
536-- of time that has elapsed. You can use 'renderDuration' or
537-- @formatTime@ from time >= 1.9.
538elapsedTime
539    :: (NominalDiffTime -> TL.Text)
540    -> Label s
541elapsedTime formatNDT = Label render
542  where
543    render _progress timing = formatNDT dt
544      where
545        dt :: NominalDiffTime
546        dt = diffUTCTime (timingLastUpdate timing) (timingStart timing)
547
548-- | Estimated remaining time.
549--
550-- Tells you how much longer some task is expected to take.
551--
552-- This label uses a simple estimation algorithm. It assumes progress
553-- is linear. To prevent nonsense results it won't estimate remaining
554-- time until at least 1 second of work has been done.
555--
556-- When it refuses to estimate the remaining time it will show an
557-- alternative message instead.
558--
559-- The user must supply a function which actually renders the amount
560-- of time that has elapsed. Use 'renderDuration' or @formatTime@ from
561-- the time >= 1.9 package.
562remainingTime
563    :: (NominalDiffTime -> TL.Text)
564    -> TL.Text
565       -- ^ Alternative message when remaining time can't be
566       -- calculated (yet).
567    -> Label s
568remainingTime formatNDT altMsg = Label render
569  where
570    render progress timing
571        | dt > 1 = formatNDT estimatedRemainingTime
572        | progressDone progress <= 0 = altMsg
573        | otherwise = altMsg
574      where
575        estimatedRemainingTime = estimatedTotalTime - dt
576        estimatedTotalTime = dt * recip progressFraction
577
578        progressFraction :: NominalDiffTime
579        progressFraction
580          | progressTodo progress <= 0 = 1
581          | otherwise = fromIntegral (progressDone progress)
582                      / fromIntegral (progressTodo progress)
583
584        dt :: NominalDiffTime
585        dt = diffUTCTime (timingLastUpdate timing) (timingStart timing)
586
587-- | Estimated total time.
588--
589-- This label uses a simple estimation algorithm. It assumes progress
590-- is linear. To prevent nonsense results it won't estimate the total
591-- time until at least 1 second of work has been done.
592--
593-- When it refuses to estimate the total time it will show an
594-- alternative message instead.
595--
596-- The user must supply a function which actually renders the total
597-- amount of time that a task will take. You can use 'renderDuration'
598-- or @formatTime@ from the time >= 1.9 package.
599totalTime
600    :: (NominalDiffTime -> TL.Text)
601    -> TL.Text
602       -- ^ Alternative message when total time can't be calculated
603       -- (yet).
604    -> Label s
605totalTime formatNDT altMsg = Label render
606  where
607    render progress timing
608        | dt > 1 = formatNDT estimatedTotalTime
609        | progressDone progress <= 0 = altMsg
610        | otherwise = altMsg
611      where
612        estimatedTotalTime = dt * recip progressFraction
613
614        progressFraction :: NominalDiffTime
615        progressFraction
616          | progressTodo progress <= 0 = 1
617          | otherwise = fromIntegral (progressDone progress)
618                      / fromIntegral (progressTodo progress)
619
620        dt :: NominalDiffTime
621        dt = diffUTCTime (timingLastUpdate timing) (timingStart timing)
622
623-- | Show amount of time.
624--
625-- > renderDuration (fromInteger 42)
626-- 42
627--
628-- > renderDuration (fromInteger $ 5 * 60 + 42)
629-- 05:42
630--
631-- > renderDuration (fromInteger $ 8 * 60 * 60 + 5 * 60 + 42)
632-- 08:05:42
633--
634-- Use the time >= 1.9 package to get a formatTime function which
635-- accepts 'NominalDiffTime'.
636renderDuration :: NominalDiffTime -> TL.Text
637renderDuration dt = hTxt <> mTxt <> sTxt
638  where
639    hTxt | h == 0 = mempty
640         | otherwise = renderDecimal h <> ":"
641    mTxt | m == 0 = mempty
642         | otherwise = renderDecimal m <> ":"
643    sTxt = renderDecimal s
644
645    (h, hRem) = ts   `quotRem` 3600
646    (m, s   ) = hRem `quotRem`   60
647
648    -- Total amount of seconds
649    ts :: Int
650    ts = round dt
651
652    renderDecimal n = TL.justifyRight 2 '0' $ TLB.toLazyText $ TLB.decimal n
653
654{- $start
655
656You want to perform some task which will take some time. You wish to
657show the progress of this task in the terminal.
658
659    1. Determine the total amount of work
660
661    2. Create a progress bar with 'newProgressBar'
662
663    3. For each unit of work:
664
665        3a. Perform the work
666
667        3b. Update the progress bar with 'incProgress'
668
669Explore the 'Style' and the 'Label' types to see various ways in which
670you can customize the progress bar.
671
672You do not have to close the progress bar, or even finish the task. It
673is perfectly fine to stop half way (maybe your task throws an
674exception).
675
676Just remember to avoid outputting text to the terminal while a
677progress bar is active. It will mess up the output a bit.
678-}
679
680{- $example
681
682Write a function which represents a unit of work. This could be a file
683copy operation, a network operation or some other expensive
684calculation. This example simply waits 1 second.
685
686@
687  work :: IO ()
688  work = threadDelay 1000000 -- 1 second
689@
690
691And you define some work to be done. This could be a list of files to
692process or some jobs that need to be processed.
693
694@
695  toBeDone :: [()]
696  toBeDone = replicate 20 ()
697@
698
699Now create the progress bar. Use the default style and choose a
700maximum refresh rate of 10 Hz. The initial progress is 0 work done out
701of 20.
702
703@
704  pb <- 'newProgressBar' 'defStyle' 10 ('Progress' 0 20 ())
705@
706
707Start performing the work while keeping the user informed of the progress:
708
709@
710  for_ toBeDone $ \() -> do
711    work             -- perform 1 unit of work
712    'incProgress' pb 1 -- increment progress by 1
713@
714
715That's it! You get a nice animated progress bar in your terminal. It
716will look like this:
717
718@
719[==========>................................]  25%
720@
721-}
722