Hello, I have a question. Actually during a dependencies calculation, I would like to play a simple animation in my terminal.
I know it’s not the good way, but I done that (See code below)
Is it possible to do this animation but outside my loop where I calculate dependencies ? Because this drastically reduce calculation speed, and I can’t accept that for my software.
I know the problem come from sleep (it’s nornal).
Maybe with an index ?
How much iteration I need to have the same result ?
Or maybe with time calculation ?
text = "Making dependencies tree"
print "ISM start to calculate depencies: "
loop do
text.each_char do |char|
print "#{char.colorize(:green)}"
sleep 0.04
end
text.reverse.each_char_with_index do |char,index|
print "\033[1D"
print " "
print "\033[1D"
sleep 0.04
end
currentDependenciesArray.each do |software|
dependencies = software.getDependencies
if !dependencies.empty?
nextDependenciesArray = nextDependenciesArray + dependencies
dependenciesLevelArray = dependenciesLevelArray + dependencies
end
end
dependenciesLevelArray.uniq! { |dependency| [ dependency.name,
dependency.version,
dependency.options] }
if !dependenciesLevelArray.empty?
neededSoftwaresTree << dependenciesLevelArray.dup
end
if nextDependenciesArray.empty?
break
end
if neededSoftwaresTree.size != neededSoftwaresTree.uniq.size
inextricableDependency = true
neededSoftwaresTree = neededSoftwaresTree & neededSoftwaresTree.uniq
break
end
currentDependenciesArray = nextDependenciesArray.uniq
nextDependenciesArray.clear
dependenciesLevelArray.clear
end
1 Like
Sure, you can split the work and the UI into separate fibers:
begin
running = true
spawn { animate(text) { running } }
do_work
ensure
running = false
end
Passing a block to animate
will allow changes in closured variables in the caller to propagate into the method. Then animate
could look like this:
def animate(text : String)
# Keep going as long as the block passed to the method evaluates
# to truthy.
while yield
# print stuff
end
end
Very important to keep in mind that you almost never want two fibers writing to the UI (this is true for both GUI and terminal UI apps) at the same time, especially since you’re using current cursor state with "\033[1D"
, so you probably only want to do this for trivial UI updates.
If your UI is nontrivial you may want to have a dedicated fiber for managing it, and everything communicates with it using a well defined API.
1 Like
I’m not sure I understood 100% your code, specially yield instruction with while.
I done that, the calculation is done, but no animation. What I missed ?
inextricableDependency = false
playAnimation = true
text = "Making dependencies tree"
print "ISM start to calculate depencies: "
begin
spawn { printDependenciesCalculationAnimation(text) { playAnimation } }
loop do
currentDependenciesArray.each do |software|
dependencies = software.getDependencies
if !dependencies.empty?
nextDependenciesArray = nextDependenciesArray + dependencies
dependenciesLevelArray = dependenciesLevelArray + dependencies
end
end
dependenciesLevelArray.uniq! { |dependency| [ dependency.name,
dependency.version,
dependency.options] }
if !dependenciesLevelArray.empty?
neededSoftwaresTree << dependenciesLevelArray.dup
end
if nextDependenciesArray.empty?
playAnimation = false
break
end
if neededSoftwaresTree.size != neededSoftwaresTree.uniq.size
inextricableDependency = true
neededSoftwaresTree = neededSoftwaresTree & neededSoftwaresTree.uniq
playAnimation = false
break
end
currentDependenciesArray = nextDependenciesArray.uniq
nextDependenciesArray.clear
dependenciesLevelArray.clear
end
ensure
playAnimation = false
end
def printDependenciesCalculationAnimation(text : String)
while yield
text.each_char do |char|
print "#{char.colorize(:green)}"
sleep 0.04
end
text.reverse.each_char_with_index do |char,index|
print "\033[1D"
print " "
print "\033[1D"
sleep 0.04
end
end
end
Ah, something I forgot to point out. Inside your work loop, you may want to run a Fiber.yield
every now and then (especially in O(n)
operations like each
or uniq!
and at the end of your loop
block) so that the animation fiber can run. Most of my apps talk to databases or other services, which automatically relinquishes the CPU to other fibers while it waits for a response, so I sometimes forget you need to do that explicitly in compute-bound blocks.
while yield
is a way of saying “do this as long as the block passed into this method returns anything other than false
or nil
”.
Okay now I have a problem, when the dependencies calculation is done, after normally without the animation, my software ask if the user want to install, but when my software print the question, my animation move after my question, and continue to play the animation one time (I guess the animation try to finish completely the task)
Is it because I use sleep instruction ? How can I correct that, because I would like this animation stop to play when the calculation finished.
My full code:
module ISM
module Option
class SoftwareInstall < ISM::CommandLineOption
def initialize
super( ISM::Default::Option::SoftwareInstall::ShortText,
ISM::Default::Option::SoftwareInstall::LongText,
ISM::Default::Option::SoftwareInstall::Description,
Array(ISM::CommandLineOption).new)
end
def start
if ARGV.size == 2
showHelp
else
matchingSoftwaresArray = Array(ISM::SoftwareInformation).new
matching = false
wrongArgument = ""
#####################
#Get wanted softwares
#####################
ARGV[2..-1].uniq.each do |argument|
matching = false
Ism.softwares.each do |software|
if argument == software.name || argument == software.name.downcase
matchingSoftwaresArray << software.versions.last
matching = true
else
software.versions.each do |version|
if argument == version.versionName || argument == version.versionName.downcase
matchingSoftwaresArray << version
matching = true
end
end
end
end
if !matching
wrongArgument = argument
break
end
end
################################
#Get dependencies array by level
################################
currentDependenciesArray = Array(ISM::SoftwareDependency).new
matchingSoftwaresArray.each do |software|
currentDependency = ISM::SoftwareDependency.new
currentDependency.name = software.name
currentDependency.version = software.version
currentDependency.options = software.options
currentDependenciesArray << currentDependency
end
nextDependenciesArray = currentDependenciesArray
dependenciesLevelArray = currentDependenciesArray
dependencies = Array(ISM::SoftwareDependency).new
neededSoftwaresTree = Array(Array(ISM::SoftwareDependency)).new
neededSoftwares = Array(ISM::SoftwareDependency).new
matchingSoftwaresArray.clear
inextricableDependency = false
playAnimation = true
text = "Making dependencies tree"
print "ISM start to calculate depencies: "
begin
spawn { printDependenciesCalculationAnimation(text) { playAnimation } }
loop do
Fiber.yield
currentDependenciesArray.each do |software|
#Fiber.yield
dependencies = software.getDependencies
if !dependencies.empty?
nextDependenciesArray = nextDependenciesArray + dependencies
dependenciesLevelArray = dependenciesLevelArray + dependencies
end
end
dependenciesLevelArray.uniq! { |dependency| [ dependency.name,
dependency.version,
dependency.options] }
if !dependenciesLevelArray.empty?
neededSoftwaresTree << dependenciesLevelArray.dup
end
if nextDependenciesArray.empty?
playAnimation = false
break
end
if neededSoftwaresTree.size != neededSoftwaresTree.uniq.size
inextricableDependency = true
neededSoftwaresTree = neededSoftwaresTree & neededSoftwaresTree.uniq
playAnimation = false
break
end
currentDependenciesArray = nextDependenciesArray.uniq
nextDependenciesArray.clear
dependenciesLevelArray.clear
end
ensure
playAnimation = false
end
#Retirer encore les doublons si il y a des paquets de meme nom ou version differente, ou options differentes
if !matching
puts ISM::Default::Option::SoftwareInstall::NoMatchFound + "#{wrongArgument.colorize(:green)}"
puts ISM::Default::Option::SoftwareInstall::NoMatchFoundAdvice
elsif inextricableDependency
inextricableDependenciesArray = Array(ISM::SoftwareInformation).new
neededSoftwaresTree.each do |level|
level.each do |dependency|
matchingSoftwaresArray << dependency.getInformation
end
end
matchingSoftwaresArray.uniq!
matchingSoftwaresArray.each do |software|
software.dependencies.each do |dependency|
temporaryArray = dependency.getInformation.dependencies + software.dependencies
if temporaryArray.map(&.name).includes?(software.name) &&
temporaryArray.map(&.name).includes?(dependency.name)
inextricableDependenciesArray << software
end
end
end
if inextricableDependenciesArray.empty?
inextricableDependenciesArray = matchingSoftwaresArray
end
puts "#{ISM::Default::Option::SoftwareInstall::InextricableText.colorize(:yellow)}"
puts "\n"
inextricableDependenciesArray.each do |software|
softwareText = "#{software.name.colorize(:magenta)}" + " /" + "#{software.version.colorize(Colorize::ColorRGB.new(255,100,100))}" + "/ "
optionsText = "{ "
software.options.each do |option|
if option.active
optionsText += "#{option.name.colorize(:red)}"
else
optionsText += "#{option.name.colorize(:blue)}"
end
optionsText += " "
end
optionsText += "}"
puts "\t" + softwareText + " " + optionsText + "\n"
end
puts "\n"
else
neededSoftwaresTree.reverse.each do |level|
level.each do |dependency|
matchingSoftwaresArray << dependency.getInformation
end
end
matchingSoftwaresArray.uniq!
puts "\n"
matchingSoftwaresArray.each do |software|
softwareText = "#{software.name.colorize(:green)}" + " /" + "#{software.version.colorize(Colorize::ColorRGB.new(255,100,100))}" + "/ "
optionsText = "{ "
software.options.each do |option|
if option.active
optionsText += "#{option.name.colorize(:red)}"
else
optionsText += "#{option.name.colorize(:blue)}"
end
optionsText += " "
end
optionsText += "}"
puts "\t" + softwareText + " " + optionsText + "\n"
end
puts "\n"
userInput = ""
userAgreement = false
print "#{ISM::Default::Option::SoftwareInstall::InstallQuestion.colorize.mode(:underline)}" +
"[" + "#{ISM::Default::Option::SoftwareInstall::YesReplyOption.colorize(:green)}" +
"/" + "#{ISM::Default::Option::SoftwareInstall::NoReplyOption.colorize(:red)}" + "]"
loop do
userInput = gets
if userInput == ISM::Default::Option::SoftwareInstall::YesReplyOption
userAgreement = true
break
end
if userInput == ISM::Default::Option::SoftwareInstall::NoReplyOption
break
end
end
if userAgreement
matchingSoftwaresArray.each do |software|
file = File.open("ISM.task", "w")
file << "require \"./#{ISM::Default::Path::SoftwaresDirectory + software.name + "/" + software.version + "/" + software.version + ".cr"}\"\n"
file << "target = Target.new\n"
file << "target.download\n"
file << "target.check\n"
file << "target.extract\n"
file << "target.patch\n"
file << "target.prepare\n"
file << "target.configure\n"
file << "target.build\n"
file << "target.install\n"
file << "target.clean\n"
file.close
Process.run("crystal",args: ["ISM.task"],output: :inherit)
end
end
end
end
end
def printDependenciesCalculationAnimation(text : String)
while yield
text.each_char do |char|
print "#{char.colorize(:green)}"
sleep 0.04
end
text.reverse.each_char_with_index do |char,index|
print "\033[1D"
print " "
print "\033[1D"
sleep 0.04
end
end
end
end
end
end
Normally the result is like this if it work properly:
zohran master ~ Documents Programmation ISM 130 crystal Main.cr -so -i libstdc++-pass1 gcc-pass1
ISM start to calculate depencies: Making dependencies tree
Binutils-Pass1 /2.37/ { }
Gcc-Pass1 /11.2.0/ { }
Linux-API-Headers /5.13.12/ { }
Glibc-Pass1 /2.34/ { }
Libstdc++-Pass1 /11.2.0/ { }
Would you like to install these softwares ?[y/n]
Not like that:
crystal Main.cr -so -i libstdc++-pass1 gcc-pass1
ISM start to calculate depencies: M
Binutils-Pass1 /2.37/ { }
Gcc-Pass1 /11.2.0/ { }
Linux-API-Headers /5.13.12/ { }
Glibc-Pass1 /2.34/ { }
Libstdc++-Pass1 /11.2.0/ { }
Would you like to install these softwares ?[y/naking dependencies tree
(It’s just an example because the animation isn’t at the goof place actually)
I solved my problem like this, because it’s exactly what I wanted. I don’t like sleep command, this slowert the performance, even I use Fiber. Thanks you to help me to think about that !
inextricableDependency = false
calculationStartingTime = Time.monotonic
frameIndex = 0
reverseAnimation = false
text = "Making dependencies tree"
print "ISM start to calculate depencies: "
loop do
currentTime = Time.monotonic
if (currentTime - calculationStartingTime).milliseconds > 40
if frameIndex >= text.size
reverseAnimation = true
end
if frameIndex < 1
reverseAnimation = false
end
if reverseAnimation
print "\033[1D"
print " "
print "\033[1D"
frameIndex -= 1
end
if !reverseAnimation
print "#{text[frameIndex].colorize(:green)}"
frameIndex += 1
end
calculationStartingTime = Time.monotonic
end
currentDependenciesArray.each do |software|
dependencies = software.getDependencies
if !dependencies.empty?
nextDependenciesArray = nextDependenciesArray + dependencies
dependenciesLevelArray = dependenciesLevelArray + dependencies
end
end
dependenciesLevelArray.uniq! { |dependency| [ dependency.name,
dependency.version,
dependency.options] }
if !dependenciesLevelArray.empty?
neededSoftwaresTree << dependenciesLevelArray.dup
end
if nextDependenciesArray.empty?
break
end
if neededSoftwaresTree.size != neededSoftwaresTree.uniq.size
inextricableDependency = true
neededSoftwaresTree = neededSoftwaresTree & neededSoftwaresTree.uniq
break
end
currentDependenciesArray = nextDependenciesArray.uniq
nextDependenciesArray.clear
dependenciesLevelArray.clear
end
But because of you now, I understood how to use multi-threads, thanks you so much ! I will use that for something else