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