jfelchner / ruby-progressbar

Ruby/ProgressBar is a text progress bar library for Ruby.
MIT License
1.58k stars 132 forks source link

Progressbar as String #131

Closed TheFox closed 7 years ago

TheFox commented 7 years ago

I do not want to print it to STDOUT. I want only one String of the bar.

So I tried this:

bar_options = {
    :total => 10,
    :starting_at => 4,
    :length => 10,
    :format => '%B',
    :progress_mark => '#',
    :remainder_mark => '-',
    :autostart => false,
}
bar = ProgressBar.create(bar_options)

puts
puts "TO STRING"
puts

bar_s = bar.to_s

puts
puts "bar_s '#{bar_s}'"

I want to use bar_s for further actions. But it's at progress 0%. Even if I use :starting_at => 4 the value is ----------.

So I changed it to this:

bar_options = {
    :total => 10,
    :length => 10,
    :format => '%B',
    :progress_mark => '#',
    :remainder_mark => '-',
    :autostart => false,
}
bar = ProgressBar.create(bar_options)
bar.progress = 4

puts
puts "TO STRING"
puts

bar_s = bar.to_s

puts
puts "bar_s '#{bar_s}'"

bar_s is now ####------ but I don't want to print it to STDOUT on executing bar.progress = 4.

So I changed it to this:

sio = StringIO.new

bar_options = {
    :total => 10,
    :length => 10,
    :format => '%B',
    :progress_mark => '#',
    :remainder_mark => '-',
    :autostart => false,
    :output => sio,
}
bar = ProgressBar.create(bar_options)
bar.progress = 4

puts
puts "TO STRING"
puts

bar_s = bar.to_s

puts
puts "bar_s '#{bar_s}'"

As expected it doesn't print anything to STDOUT anymore, but bar_s is now Progress: ||. It ignores all options. Why? It has no progress. Why? It ignores bar.progress = 4. Why?

Even if I read it from sio it's wrong:

sio.seek(0)
bar_s = sio.read

bar_s has now \x0a\x0aProgress: | as value.

Is StringIO the wrong type to just write the bar to a String? How can I get the bar as one single String?

dannypaz commented 7 years ago

@TheFox: A friend of mine (@brucec5) helped me troubleshoot the same exact issue. Since StringIO does not have a window size (not a terminal), the format of the output goes back to a default formatter, which is exactly the output you are seeing!

Relevant code is here: https://github.com/jfelchner/ruby-progressbar/blob/master/lib/ruby-progressbar/outputs/non_tty.rb#L29

One ~Your~ solution would be to override the non_tty file, add the correct formatter, and specify a (hopefully conservative) window size.

Hope this helps!

Props to @brucec5 !!!!

UPDATE:

Additionally, the output is determined on creation of the progress bar here

dannypaz commented 7 years ago

To go a step further, here is a sample implementation that will:

  1. remove ProgressBar from outputting info the STDOUT
  2. output the information we expect through to_s with a specified formatter
module Example
  class ProgressBar
    PROGRESS_FORMAT = "Progress: [%B] [%c/%C]".freeze

    # This is required. This is our fake output class that will handle any
    # output and shove it straight into the void.
    class FakeOutput
      def self.tty?() true end
      def self.print(str) str end
      def self.flush() self end
    end

    def initialize(total)
      @bar ||= ::ProgressBar.create(
        total: total,
        format: PROGRESS_FORMAT,
        # TODO: Specify the length of the progress bar! This is required.
        length: 100,
        output: FakeOutput
      )
    end

    def progress(amount = 1)
      @bar.progress += amount
      puts @bar
    end
  end
end

And the associated usage:

>> bar = Example::ProgressBar.new(100)
=> #<Example::ProgressBar:0x007fea5d6940c0 @bar=#<ProgressBar:0/100>>
>> bar.progress
Progress: [                                                                                ] [1/100]
>> bar.progress
Progress: [=                                                                               ] [2/100]
>> bar.progress
Progress: [==                                                                              ] [3/100]
>> bar.progress(3)
Progress: [====                                                                            ] [6/100]
jfelchner commented 7 years ago

Can you all elaborate on the use case here? I have to admit this looks completely convoluted to me 😂

What is the point of getting the actual string of the progress bar rather than querying the progress bar object itself?

TheFox commented 7 years ago

@dannypaz Thank you for this informations. I have not tested your code yet. I will. Just to be correct, the object is called StringIO instead of String.IO as mentioned in your first comment. ;)

@jfelchner My use case: I'm making a time tracking CLI tool, called Timr. Where you can create tasks and an estimation for each task. When you add an estimation to a task you also get an progressbar on the info page to visualize the time left you have for this task. Since Timr is no interactive programm I do not need a "#{progressbar}\r"-loop rather than one single "#{progressbar}\n" string. It's like (the most) Git commands: you start it, the process prints informations (for example how far the progress is and a progressbar) and then the process ends. I do not need any "\r"s or "\n"s in my progressbar string because I do not want to explicitly show the progressbar. I also want to print other informations to the user, before and after the progressbar.

dannypaz commented 7 years ago

@jfelchner I don't blame your feelings. The use-case is having progress bar/status in a system log with code containing multiple ruby forks/subprocesses.

My reasoning for this code is because non-tty does not support custom formatters, which is what myself and @TheFox were trying to use. We would see the correct output in a terminal window, but once exported, either through StringIO or into a file, it would change due to .detect.

tty vs. non-tty is something that makes sense, but was not the behavior I was expecting.

jfelchner commented 7 years ago

@TheFox @dannypaz your use case is extremely specific and not something I'd generally change the codebase for, but here's what I'm willing to do for you all (because it's very little code change, is fairly general, and has almost zero performance cost).

I can change Output.detect to check to see if options[:output] is a subclass of ProgressBar::Output. If it is, it will instantiate it with all the options that the current output classes receive. In this way, you can create whatever kind of outputter you'd like (as long as it subclasses ProgressBar::Output). If you want to do something extremely specific (as you seem to be doing), all you'll have to do is override the appropriate methods and you're off to the races.

For example:

class MyOutputter < ProgressBar::Output::Tty
  def clear
  end

  # You will be REQUIRED override this if you want something other than the default IO stream
  def stream
    @stream = whatever_your_desired_io_object_is
  end

  def default_format
    'Progress: [%B] [%c/%C]'
  end

  def eol
    ''
  end
end

@bar ||= ::ProgressBar.create(
           total:  total,
           output: MyOutputter
         )

@bar.progress += 1 # this would output the bar to the stream with (I think) no `\r` or `\n`

Or whatever. :)

How's that sound?

jfelchner commented 7 years ago

Technically if you wanted to get super crazy you could override initialize.

I want to be super clear though, I will make no attempt to follow Semver on the outputters. I will change the interface to whatever I want, whenever I want. I take Semver very seriously and I've managed to go years without a backward-incompatible change. I won't tie my hands for this small use case.

That said, the interface for the outputters hasn't changed in a couple years so you're fairly safe.

jfelchner commented 7 years ago

@TheFox @dannypaz I didn't hear back from you all, but it was a minor change so it's going in 1.9 anyway.

Hope this helps.

TheFox commented 7 years ago

@jfelchner Sorry for not responding anymore. Unfortunately, I already hadn't the chance to test it. I implemented an simple version of a progressbar for my project. Thank you for your help.

jfelchner commented 7 years ago

@TheFox no problem. I actually I went ahead and went a step further for you.

I added a Null outputter, which, if passed into the progress bar like so:

require 'ruby-progressbar/outputs/null'

progressbar = ProgressBar.create(output: ProgressBar::Outputs::Null)

will disable all outputting of the progressbar completely (or should anyway 😉). For this class I will make sure it's compatible if there are any changes to the base Output class, so it's safe to use.

If you use the above code, you should be able to still use progressbar.to_s to get the static bar for the purposes you need it for.

Hope this helps.

jfelchner commented 7 years ago

Wiki article is up

TheFox commented 7 years ago

Nice! Thank you.

github-actions[bot] commented 3 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.