Skip to content

Commit b87cf48

Browse files
authored
Handle nested progress bars (#13)
1 parent 69ae6c2 commit b87cf48

File tree

8 files changed

+258
-63
lines changed

8 files changed

+258
-63
lines changed

Project.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ authors = ["Chris Foster <chris42f@gmail.com>"]
44
version = "0.1.0"
55

66
[deps]
7+
LeftChildRightSiblingTrees = "1d6d02ad-be62-4b6b-8a6d-2f90e265016e"
78
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
89
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
10+
ProgressLogging = "33c8b6b6-d38a-422a-b730-caa89a2f386c"
11+
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
912

1013
[compat]
14+
LeftChildRightSiblingTrees = "0.1.1"
15+
ProgressLogging = "0.1.1"
1116
julia = "1"
1217

1318
[extras]

src/ProgressBar.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
Base.@kwdef mutable struct ProgressBar
2+
fraction::Union{Float64,Nothing}
3+
name::String
4+
level::Int = 0
5+
barglyphs::BarGlyphs = BarGlyphs()
6+
tlast::Float64 = time()
7+
tfirst::Float64 = time()
8+
id::UUID
9+
parentid::UUID
10+
end
11+
12+
set_fraction!(bar::ProgressBar, ::Nothing) = bar
13+
function set_fraction!(bar::ProgressBar, fraction::Real)
14+
bar.tlast = time()
15+
bar.fraction = fraction
16+
return bar
17+
end
18+
19+
# This is how `ProgressMeter.printprogress` decides "ETA" vs "Time":
20+
ensure_done!(bar::ProgressBar) = bar.fraction = 1
21+
22+
function eta_seconds(bar)
23+
total = (bar.tlast - bar.tfirst) / something(bar.fraction, NaN)
24+
return total - (time() - bar.tfirst)
25+
end
26+
27+
function printprogress(io::IO, bar::ProgressBar)
28+
if bar.name == ""
29+
desc = "Progress: "
30+
else
31+
desc = bar.name
32+
if !endswith(desc, " ")
33+
desc *= " "
34+
end
35+
end
36+
37+
pad = " "^bar.level
38+
print(io, pad)
39+
40+
lines, columns = displaysize(io)
41+
ProgressMeter.printprogress(
42+
IOContext(io, :displaysize => (lines, max(1, columns - length(pad)))),
43+
bar.barglyphs,
44+
bar.tfirst,
45+
desc,
46+
something(bar.fraction, NaN),
47+
eta_seconds(bar),
48+
)
49+
end

src/ProgressMeter/ProgressMeter.jl

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,43 +21,43 @@ BarGlyphs() = BarGlyphs(
2121
'|',
2222
)
2323

24-
struct ProgressBar
25-
barglyphs::BarGlyphs
26-
tfirst::Float64
27-
end
28-
29-
ProgressBar(barglyphs = BarGlyphs()) = ProgressBar(barglyphs, time())
30-
3124
"""
32-
printprogress(io::IO, p::ProgressBar, desc, progress::Real)
25+
printprogress(io::IO, barglyphs::BarGlyphs, tfirst::Float64, desc, progress::Real)
3326
34-
Print progress bar to `io` with setting `p`.
27+
Print progress bar to `io`.
3528
3629
# Arguments
3730
- `io::IO`
38-
- `p::ProgressBar`
31+
- `barglyphs::BarGlyphs`
32+
- `tfirst::Float64`
3933
- `desc`: description to be printed at left side of progress bar.
4034
- `progress::Real`: a number between 0 and 1 or a `NaN`.
35+
- `eta_seconds::Real`: ETA in seconds
4136
"""
42-
function printprogress(io::IO, p::ProgressBar, desc, progress::Real)
37+
function printprogress(
38+
io::IO,
39+
barglyphs::BarGlyphs,
40+
tfirst::Float64,
41+
desc,
42+
progress::Real,
43+
eta_seconds::Real,
44+
)
4345
t = time()
4446
percentage_complete = 100.0 * (isnan(progress) ? 0.0 : progress)
4547

4648
#...length of percentage and ETA string with days is 29 characters
4749
barlen = max(0, displaysize(io)[2] - (length(desc) + 29))
4850

4951
if progress >= 1
50-
bar = barstring(barlen, percentage_complete, barglyphs=p.barglyphs)
51-
dur = durationstring(t - p.tfirst)
52+
bar = barstring(barlen, percentage_complete, barglyphs=barglyphs)
53+
dur = durationstring(t - tfirst)
5254
@printf io "%s%3u%%%s Time: %s" desc round(Int, percentage_complete) bar dur
5355
return
5456
end
5557

56-
bar = barstring(barlen, percentage_complete, barglyphs=p.barglyphs)
57-
elapsed_time = t - p.tfirst
58-
est_total_time = 100 * elapsed_time / percentage_complete
59-
if 0 <= est_total_time <= typemax(Int)
60-
eta_sec = round(Int, est_total_time - elapsed_time)
58+
bar = barstring(barlen, percentage_complete, barglyphs=barglyphs)
59+
if 0 <= eta_seconds <= typemax(Int)
60+
eta_sec = round(Int, eta_seconds)
6161
eta = durationstring(eta_sec)
6262
else
6363
eta = "N/A"

src/StickyMessages.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ end
9595

9696
function Base.push!(sticky::StickyMessages, message::Pair)
9797
if !sticky.ansi_codes
98-
write(sticky.io, message[2])
98+
println(sticky.io, rstrip(message[2]))
9999
return
100100
end
101101
label,text = message

src/TerminalLogger.jl

Lines changed: 82 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct TerminalLogger <: AbstractLogger
2929
right_justify::Int
3030
message_limits::Dict{Any,Int}
3131
sticky_messages::StickyMessages
32-
bars::Dict{Any,ProgressBar}
32+
bartrees::Vector{Node{ProgressBar}}
3333
end
3434
function TerminalLogger(stream::IO=stderr, min_level=ProgressLevel;
3535
meta_formatter=default_metafmt, show_limited=true,
@@ -42,7 +42,7 @@ function TerminalLogger(stream::IO=stderr, min_level=ProgressLevel;
4242
right_justify,
4343
Dict{Any,Int}(),
4444
StickyMessages(stream),
45-
Dict{Any,ProgressBar}(),
45+
Union{}[],
4646
)
4747
end
4848

@@ -104,61 +104,105 @@ function termlength(str)
104104
return N
105105
end
106106

107-
function handle_progress(logger, message, id, progress)
108-
# Don't do anything when it's already done:
109-
if (progress == "done" || progress >= 1) && !haskey(logger.bars, id)
110-
return
107+
function findbar(bartree, id)
108+
if !(bartree isa AbstractArray)
109+
bartree.data.id === id && return bartree
110+
end
111+
for node in bartree
112+
found = findbar(node, id)
113+
found === nothing || return found
111114
end
115+
return nothing
116+
end
112117

113-
try
114-
bar = get!(ProgressBar, logger.bars, id)
118+
function foldtree(op, acc, tree)
119+
for node in tree
120+
acc = foldtree(op, acc, node)
121+
end
122+
if !(tree isa AbstractArray)
123+
acc = op(acc, tree)
124+
end
125+
return acc
126+
end
115127

116-
if message == ""
117-
message = "Progress: "
118-
else
119-
message = string(message)
120-
if !endswith(message, " ")
121-
message *= " "
122-
end
123-
end
128+
const BAR_MESSAGE_ID = gensym(:BAR_MESSAGE_ID)
124129

125-
bartxt = sprint(
126-
printprogress,
127-
bar,
128-
message,
129-
progress == "done" ? 1.0 : progress;
130-
context = :displaysize => displaysize(logger.stream),
131-
)
130+
function handle_progress(logger, progress)
131+
node = findbar(logger.bartrees, progress.id)
132+
if node === nothing
133+
# Don't do anything when it's already done:
134+
(progress.done || something(progress.fraction, 0.0) >= 1) && return
132135

133-
if progress == "done" || progress >= 1
134-
pop!(logger.sticky_messages, id)
135-
printstyled(logger.stream, bartxt; color=:light_black)
136-
println(logger.stream)
136+
parentnode = findbar(logger.bartrees, progress.parentid)
137+
bar = ProgressBar(
138+
fraction = progress.fraction,
139+
name = progress.name,
140+
id = progress.id,
141+
parentid = progress.parentid,
142+
)
143+
if parentnode === nothing
144+
node = Node(bar)
145+
pushfirst!(logger.bartrees, node)
137146
else
138-
bartxt = sprint(context = logger.stream) do io
139-
printstyled(io, bartxt; color=:green)
140-
end
141-
push!(logger.sticky_messages, id => bartxt)
147+
bar.level = parentnode.data.level + 1
148+
node = addchild(parentnode, bar)
142149
end
143-
finally
144-
if progress == "done" || progress >= 1
145-
pop!(logger.sticky_messages, id) # redundant (but safe) if no error
146-
pop!(logger.bars, id, nothing)
150+
else
151+
bar = node.data
152+
set_fraction!(bar, progress.fraction)
153+
if progress.name != ""
154+
bar.name = progress.name
155+
end
156+
node.data = bar
157+
end
158+
if progress.done
159+
if isroot(node)
160+
deleteat!(logger.bartrees, findfirst(x -> x === node, logger.bartrees))
161+
else
162+
prunebranch!(node)
163+
end
164+
end
165+
166+
bartxt = sprint(context = :displaysize => displaysize(logger.stream)) do io
167+
foldtree(true, logger.bartrees) do isfirst, node
168+
isfirst || println(io)
169+
printprogress(io, node.data)
170+
false # next `isfirst`
171+
end
172+
end
173+
if isempty(bartxt)
174+
pop!(logger.sticky_messages, BAR_MESSAGE_ID)
175+
else
176+
bartxt = sprint(context = logger.stream) do io
177+
printstyled(io, bartxt; color=:green)
178+
end
179+
push!(logger.sticky_messages, BAR_MESSAGE_ID => bartxt)
180+
end
181+
182+
# "Flushing" non-sticky message should be done after the sticky
183+
# message is re-drawn:
184+
if progress.done
185+
ensure_done!(bar)
186+
donetxt = sprint(context = :displaysize => displaysize(logger.stream)) do io
187+
printprogress(io, bar)
147188
end
189+
printstyled(logger.stream, donetxt; color=:light_black)
190+
println(logger.stream)
148191
end
149192
end
150193

151194
function handle_message(logger::TerminalLogger, level, message, _module, group, id,
152-
filepath, line; maxlog=nothing, progress=nothing,
195+
filepath, line; maxlog=nothing,
153196
sticky=nothing, kwargs...)
154197
if maxlog !== nothing && maxlog isa Integer
155198
remaining = get!(logger.message_limits, id, maxlog)
156199
logger.message_limits[id] = remaining - 1
157200
remaining > 0 || return
158201
end
159202

160-
if progress == "done" || progress isa Real
161-
handle_progress(logger, message, id, progress)
203+
progress = asprogress(level, message, _module, group, id, filepath, line; kwargs...)
204+
if progress !== nothing
205+
handle_progress(logger, progress)
162206
return
163207
end
164208

src/TerminalLoggers.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@ using Logging:
77
import Logging:
88
handle_message, shouldlog, min_enabled_level, catch_exceptions
99

10+
using LeftChildRightSiblingTrees: Node, addchild, isroot, prunebranch!
11+
using ProgressLogging: asprogress
12+
using UUIDs: UUID
13+
1014
export TerminalLogger
1115

1216
const ProgressLevel = LogLevel(-1)
1317

1418
include("ProgressMeter/ProgressMeter.jl")
1519
using .ProgressMeter:
16-
ProgressBar, printprogress
20+
BarGlyphs
1721

1822
include("StickyMessages.jl")
23+
include("ProgressBar.jl")
1924
include("TerminalLogger.jl")
2025

2126
end # module

0 commit comments

Comments
 (0)