Animated console during a process

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