Copyright | (c) 2017 Ertugrul Söylemez |
---|---|
License | BSD3 |
Maintainer | Ertugrul Söylemez <esz@posteo.de> |
Safe Haskell | Safe |
Language | Haskell2010 |
This module implements a progress bar with support for multiple individual text chunks that can be updated independently (called meters).
- data Progress a = Progress {
- progressDelay :: Int
- progressHandle :: Handle
- progressInitial :: a
- progressRender :: a -> String
- withProgress :: Progress a -> (Meter' a -> IO r) -> IO r
- withProgress_ :: a -> (a -> String) -> (Meter' a -> IO r) -> IO r
- meterIO :: Meter a b -> (Handle -> IO r) -> IO r
- putMsg :: Meter a b -> String -> IO ()
- putMsgLn :: Meter a b -> String -> IO ()
- data Meter a b
- type Meter' a = Meter a a
- modifyMeter :: Meter a b -> (a -> b) -> IO ()
- setMeter :: Meter a b -> b -> IO ()
- zoomMeter :: ((a -> b) -> s -> t) -> Meter s t -> Meter a b
- zoomMeterL :: ((a -> Identity b) -> s -> Identity t) -> Meter s t -> Meter a b
- modifyMeterSTM :: Meter a b -> (a -> b) -> STM ()
- setMeterSTM :: Meter a b -> b -> STM ()
Tutorial
A progress bar runs concurrently and redraws itself whenever something changes to inform the impatient user that, yes, your application is actually doing stuff. Redraws are throttled to a user-chosen rate in order not to impact performance in the inconceivable case that changes come in too frequently.
The withProgress_
function adds a progress bar to your application for
the duration of the given action:
withProgress_ :: s -> (s -> String) -> (Meter' s -> IO r) -> IO r
Progress bars maintain mutable state of a user-chosen type s
and use a
user-supplied rendering function of type (s -> String)
in order to
display the current state whenever your application brings itself to
update it. The first argument is the initial state, the second argument
is the rendering function.
For example if you would like to display a simple percentage you could
use s =
and an initial state of Int
0
. The rendering function
could turn the plain number into simple text, an ASCII art or any other
single-line entertainment:
render :: Int -> String render x = "Progress: " ++ show x ++ "%"
To change the current state (in this case: the current percentage) you
can use the setMeter
function with the Meter'
value that your
application receives from withProgress_
(simplified type signature):
setMeter :: Meter' s -> s -> IO ()
Here is a full example in the spirit of the last percent challenging your patience much worse than the rest:
import Control.Concurrent import Data.Foldable import System.Progress main :: IO () main = withProgress_ 0 render $ \pm -> do for_ [1..99] $ \p -> do threadDelay 20000 setMeter pm p threadDelay 3000000 setMeter pm 100 threadDelay 1000000 where render :: Int -> String render x = "Progress: " ++ show x ++ "%"
From time to time you might like to perform regular output for
diagnostics, logging or other purposes. However, you can't just write
to stderr
as that would corrupt the progress bar. Instead you should
use the putMsgLn
function (simplified type signature):
putMsgLn :: Meter' s -> String -> IO ()
You can perform arbitrary actions while temporarily hiding the progress
bar by using the meterIO
function, of which putMsgLn
is a special
case.
This library fully supports concurrency. You can use setMeter
and
meterIO
from multiple threads. The latter will also properly sequence
concurrent actions, so you can safely output diagnostics from multiple
threads.
Zooming
A meter of type (
allows you update the current state of
type Meter'
s)s
. However, especially in highly concurrent applications it can
be useful to give a thread a meter that updates only the part of the
state that is relevant to that thread. For those applications you can
use the zoomMeter
function (simplified type signature):
zoomMeter :: ((a -> a) -> s -> s) -> Meter' s -> Meter' a
Given a function that can map a function of type (a -> a)
over values
of type s
, this function converts a (
into a Meter'
s)(
. This is best illustrated with an example. The following function
strictly maps over the left component of a tuple:Meter'
a)
mapLeft :: (a -> b) -> (a, c) -> (b, c) mapLeft f (x', y) = let !x = f x' in (x, y)
You can use this function with zoomMeter
to turn a (
into a Meter'
(a, b))(
:Meter'
a)
zoomMeter mapLeft :: Meter' (a, b) -> Meter' a
This meter can then be used to update only the left component of the state. Zooms can be cascaded as well.
If you are using van Laarhoven lenses as defined by the
lens library you can also use
the zoomMeterL
function (simplified type signature):
zoomMeterL :: ASetter' s a -> Meter' s -> Meter' a
Caveat: Unfortunately most if not all of the predefined lenses are
non-strict. As mentioned earlier the progress bar's rendering loop is
throttled, so a state update may not cause an immediate redraw. For
that reason the setMeter
function updates the state strictly, so that
updates don't cause unevaluated expressions to pile up. However, it's
only WHNF-strict, so if you do any deep updates using a non-strict
function, they will not be evaluated until the next redraw. The
mapLeft
example above is strict in order to avoid that.
The solution is either to write strict lenses by hand, or to make sure the state type is fully strict in all its fields on all layers.
Concurrent updates
The rendering loop waits for updates to the current state. Whenever an
update comes in, it redraws the progress bar and then sleeps for a
user-specified duration (0.1 seconds if you use withProgress_
). If
further state updates have been done in the meantime, it redraws itself
and sleeps again, etc. Otherwise it waits for updates.
Now imagine you need to do two state updates in a row to inform the user of a certain change, for example you have done one step to completion and want to start a new phase:
setMeter statusMeter "Done with foo, now doing bar" setMeter percentMeter 0
If the rendering loop is currently in the waiting phase it is very
likely that the first setMeter
will immediately wake up the rendering
thread and cause a redraw, after which it goes to its throttle sleep.
Users would then observe a partial state update for a brief amount of
time (the new message, but not the new percentage). In order to avoid
that you should use the STM variant of setMeter
called setMeterSTM
:
atomically $ do setMeterSTM statusMeter "Done with foo, now doing bar" setMeterSTM percentMeter 0
This will make sure that the rendering loop never observes a partial update.
Further notes
This library does not do any fancy terminal magic; in particular it doesn't check the terminal width, so if the text is too long, the user may observe some undesired scrolling. You may know this effect from
curl
. However, this keeps the implementation simple and portable (terminfo is not portable to Windows).The author's recommendation is to just ignore this fact. Even if you overdraw the progress bar itself will still work, and it will span multiple lines properly. The scrolling effect is ugly, but doesn't severely impact the user experience.
- The default throttle of 0.1 seconds may seem too low, but it really isn't. Keep in mind that the rendering loop does not draw at all, unless there are actual updates, so even if your application updates very infrequently the default throttle is fine.
Progress bars
Progress bars displaying state information of the given type
Progress | |
|
Display a progress bar for the duration of the given action
Note: If the output handle is not a terminal (as determined by
hIsTerminalDevice
), no progress bar is displayed and no state is
maintained. In this case modifyMeter
and modifyMeterSTM
are
no-ops.
For most applications the simpler variant withProgress_
is
sufficient.
:: a | Initial state value |
-> (a -> String) | State renderer |
-> (Meter' a -> IO r) | Action with a progress bar |
-> IO r |
Simpler variant of withProgress
Uses a delay of 0.1 seconds and displays the progress bar on stderr.
I/O
meterIO :: Meter a b -> (Handle -> IO r) -> IO r Source #
Perform the given action while temporarily hiding the progress bar
The given action is sequenced with concurrent uses of meterIO
, so
it can be used for regular output without artifacts. The function
receives the output handle of the progress bar.
putMsg :: Meter a b -> String -> IO () Source #
Variant of putMsgLn
that omits the final line feed
Note: Use this function only when the given string ends with a line feed, otherwise the progress bar will overwrite its last line when it is redisplayed.
putMsgLn :: Meter a b -> String -> IO () Source #
Print the given string to the output handle of the progress bar
This is implemented in terms of meterIO
, so it does The Right
Thing: it temporarily hides the progress bar, prints the string, then
redisplays it. It also makes sure that concurrent messages are
properly sequenced.
Meters
A value of type Meter a b
can be used to update part of the
current state of the progress bar by supplying a function of type (a
-> b)
, where a
is the type of the current value and b
is the
type of the new value. See the modifyMeter
function for details.
In most cases you can just assume a = b
and use the Meter'
alias.
type Meter' a = Meter a a Source #
Handy type alias for the common case where the current state type and the new state type are the same
modifyMeter :: Meter a b -> (a -> b) -> IO () Source #
Modify the part of the state represented by the given meter using the given function
The function receives the current value of type a
of the meter and
should return the new value of type b
. Note that for most
applications those types will be the same.
Updates are performed strictly, so they don't pile up when updates are throttled, unless the progress bar is disabled (because the output handle is not a terminal), in which case no state is maintained at all.
setMeter :: Meter a b -> b -> IO () Source #
Variant of modifyMeter
: set the given meter to the given new
state
See modifyMeter
for details.
zoomMeter :: ((a -> b) -> s -> t) -> Meter s t -> Meter a b Source #
Zoom into part of the state
This function returns a variant of the given meter that focusses on the value(s) the given setter modifies. You can use this for example to focus on a single key in a map or all the values in a list.
Examples:
-- Zoom into all values of a list (warning: non-strict!): zoomMeter map :: Meter' [a] -> Meter' a -- Zoom into the left component of a tuple: zoomMeter (\f (x', y) -> let x = f x' in x `seq` (x, y)) :: Meter' (a, b) -> Meter' a -- Zoom into the element indexed by the "foo" key, -- where M = Data.Map.Strict: zoomMeter (\f -> M.alter f "foo") :: Meter' (M.Map String a) -> Meter' (Maybe a) -- Variant of the previous example that always -- adds the element if it didn't exist before: zoomMeter (\f -> M.alter (Just . f) "foo") :: Meter' (M.Map String a) -> Meter (Maybe a) a
STM variants
modifyMeterSTM :: Meter a b -> (a -> b) -> STM () Source #
STM variant of modifyMeter
: modify the given meter in a
transaction
You can use this function to modify multiple meters simultaneously. This is useful, if you want to make sure that users don't observe partial updates.
setMeterSTM :: Meter a b -> b -> STM () Source #
Variant of modifyMeterSTM
: set the given meter to the given new
state in a transaction
See modifyMeterSTM
for details.