#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>
require "./ansi"
module RemiLib::Console
# A console-based progress bar. This uses ANSI control characters to draw the
# bar on one line.
class ProgressBar(T)
class ProgressBarDoneError < Exception
end
# The current step.
getter step : T = T.zero
# The max number of steps.
getter max : T
# The maximum width for the progress bar label. If this is 0, then the max
# width is computed to be 1/5th of the width of the console.
getter labelWidth : UInt32 = 0u32
# The maximum width for the progress bar's post-bar label. If
# this is 0, then the max width is computed to be 1/5th of the
# width of the console.
getter postLabelWidth : UInt32 = 0u32
# An optional label that can be printed after the percentage at
# the end of the progress bar. If this is `nil`, then no extra
# label is printed. This can be up to `postLabelWidth` characters
# long.
getter postLabel : String? = nil
# When `true`, the progress bar will not be redrawn automatically, and will
# only be redrawn when `#refresh` is called, or when `#done` is called. The
# default is `false`.
property? noAutoRefresh : Bool = false
# When `true` (the default), ellipses ("...") will be added to a clipped
# label if possible.
property? addEllipses : Bool = true
def initialize(@label : String, @max : T, @output : IO = STDOUT)
raise "ProgressBar max cannot be 0 or less" if @max <= 0
end
# When `true`, then the progress bar is finished and will not refresh itself
# anymore or allow changes to its properties.
getter? done : Bool = false
# When `false`, then percentages that are above 100 are capped at 100. The
# default is `true`.
property? allowOver100 : Bool = true
# Refreshes the bar, then prints a newline to the output and sets `#done?`
# to `true`. Calling this more than once is effectively a non-op.
@[AlwaysInline]
def done : Nil
return if @done
printBar
@output << '\n'
@done = true
end
# Re-draws the progress bar. This can be called even when `#done?` is
# `true`.
@[AlwaysInline]
def refresh : Nil
printBar
end
# Increases the step by one, then refreshes the bar. This will
# automatically redraw the bar unless `#noAutoRefresh` is `true`.
def pump : Nil
raise ProgressBarDoneError.new("Bar is finished") if @done
@step += 1
printBar unless @noAutoRefresh
end
# Sets the step to `val`, then refreshes the bar. `val` cannot be less than
# zero. If it is greater than `#max`, then it is clamped to `#max`. This
# will automatically redraw the bar unless `#noAutoRefresh` is `true`.
def step=(val : T) : Nil
raise ProgressBarDoneError.new("Bar is finished") if @done
raise "Cannot set progress bar step to a negative value." if val < 0
@step = val
printBar unless @noAutoRefresh
end
# Changes the label of the progress bar. This will automatically redraw the
# bar unless `#noAutoRefresh` is `true`.
def label=(newLabel : String) : Nil
raise ProgressBarDoneError.new("Bar is finished") if @done
@label = newLabel
printBar unless @noAutoRefresh
end
# Changes the post-bar label of the progress bar. This is printed after the
# percentage. This will automatically redraw the bar unless
# `#noAutoRefresh` is `true`.
def postLabel=(newLabel : String) : Nil
raise ProgressBarDoneError.new("Bar is finished") if @done
@postLabel = newLabel
printBar unless @noAutoRefresh
end
# Changes the maximum value of the progress bar. This cannot be zero or
# negative.
#
# If the new maximum is greater than the current `#step`, then the current
# `#step` is set to the new maximum.
#
# This will automatically redraw the bar unless `#noAutoRefresh` is `true`.
def max=(newMax : T) : Nil
raise "ProgressBar max cannot be 0 or less" if newMax <= 0
@max = newMax
@step = @max if @step > @max
printBar unless @noAutoRefresh
end
# Changes the label width. This cannot be negative. This will
# automatically redraw the bar unless `#noAutoRefresh` is `true`.
def labelWidth=(value : Int) : Nil
raise ProgressBarDoneError.new("Bar is finished") if @done
raise "Label width cannot be negative" if value < 0
@labelWidth = value.to_u32
printBar unless @noAutoRefresh
end
# Changes the post-bar label width. This cannot be negative. This will
# automatically redraw the bar unless `#noAutoRefresh` is `true`.
def postLabelWidth=(value : Int) : Nil
raise ProgressBarDoneError.new("Bar is finished") if @done
raise "postLabelWidth width cannot be negative" if value < 0
@postLabelWidth = value.to_u32
printBar unless @noAutoRefresh
end
private def printBar : Nil
RemiLib::Console.cursor(@output, 1)
#RemiLib::Console.erase(@output) # Likely not needed
# Get console width.
_, conWidth = RemiLib::Console.getWinSize
return if conWidth < 10
width = conWidth.to_i32 - 1
lblWidth = if @labelWidth == 0
ret = width // 5
ret < 5 ? 5 : ret
else
@labelWidth
end
if @label.size < lblWidth
lblWidth = @label.size
end
postLblWidth = if @postLabelWidth == 0
ret = width // 5
ret < 5 ? 5 : ret
else
@postLabelWidth
end
@postLabel.try do |lbl|
if lbl.size < postLblWidth
postLblWidth = lbl.size
end
end
# The -5 is to account for the percent display. The -2 is to account for
# the two vertical bars. The -1 is to account for a space between the
# label and bar.
barWidth = width - lblWidth - 5 - 2 - 1
if @postLabel
# Extra space after the percentage if we have a post-bar label.
barWidth -= 1
# Subtract post-bar label width as well
barWidth -= postLblWidth
end
raise "Bar width went negative" if barWidth < 0
# Write label
if @label.size < lblWidth + 1
@output << @label
else
if @addEllipses && lblWidth >= 4
@output << @label[0...(lblWidth - 3)] << "..."
else
@output << @label[0...lblWidth]
end
end
@output << ": "
# Write bar
percent = (@step / @max) * 100
percent = 100 if percent > 100 && !@allowOver100
progress = (::Math.min(percent, 100.0) * barWidth) / 100_f32
@output << '|' << ("*" * progress.to_i) << ("-" * (barWidth - progress.to_i)) << '|'
# Write percent
@output << "#{sprintf("%4i", percent.to_i)}%"
# Write post-bar label
@postLabel.try do |lbl|
@output << ' '
if lbl.size < postLblWidth + 1
@output << lbl
else
if @addEllipses && postLblWidth >= 4
@output << lbl[0...(postLblWidth - 3)] << "..."
else
@output << lbl[0...postLblWidth]
end
end
end
end
end
# Creates a new `RemiLib::Console::ProgressBar` with the given parameters,
# then yields it to the block. A newline will be automatically printed once
# the block is finished by calling `RemiLib::Console::ProgressBar#done`.
def withProgress(label : String, max : Int|Float, output : IO = STDOUT, &) : Nil
bar = ProgressBar.new(label, max, output)
yield bar
bar.done
end
end
|