In-place progress output in the terminal or consol

2019-01-21 23:00发布

问题:

When you run git clone, it updates progress in place. For example, the percentage of the objects received changes in place.

user@athena:~/cloj/src$ git clone git://git.boinkor.net/slime.git
Initialized empty Git repository in /home/user/cloj/src/slime/.git/
remote: Counting objects: 15936, done.
remote: Compressing objects: 100% (5500/5500), done.
Receiving objects:  28% (4547/15936), 3.16 MiB | 165 KiB/s

How is this acccomplished? Does it use ncurses or something even simpler, like some combination of backspace characters and regular character output?

I'm especially interested in how this kind of console output could be accomplished from Ruby.

EDIT

My original question is answered. But here's an addendum. When you use MPlayer, for example, it not only updates a line to show current progress, but also the previous line (e.g. when you press pause).

 =====  PAUSE  =====
A:  79.9 (01:19.9) of 4718.0 ( 1:18:38.0)  0.3% 

How would you update two lines of output in-place?

回答1:

Use carriage return. '\r' should usually work.



回答2:

git/progress.c

...
        eol = done ? done : "   \r";
...
                fprintf(stderr, "...%s", ..., eol);
                fflush(stderr);

Git simply emits a carriage return and no line feed, which the terminal interprets as "move to first column".



回答3:

You'll have to use another method (like Curses) to update two lines in-place.

ablogaboutcode.com | web.archive.org

...and...

http://www.ruby-doc.org/stdlib-1.9.3/libdoc/curses/rdoc/Curses.html



回答4:

I wrote little class for multiline output update:

class ConsoleReset
  # Unix
  # Contains a string to clear the line in the shell
  CLR = "\e[0K"
  # ANSI escape sequence for hiding terminal cursor
  ESC_CURS_INVIS = "\e[?25l"
  # ANSI escape sequence for showing terminal cursor
  ESC_CURS_VIS   = "\e[?25h"
  # ANSI escape sequence for clearing line in terminal
  ESC_R_AND_CLR  = "\r#{CLR}"
  # ANSI escape sequence for going up a line in terminal
  ESC_UP_A_LINE = "\e[1A"

  def initialize
    @first_call = true
  end

  def reset_line(text = '')
    # Initialise ANSI escape string
    escape = ""

    # The number of lines the previous message spanned
    lines = text.strip.lines.count - 1

    # Clear and go up a line
    lines.times { escape += "#{ESC_R_AND_CLR}#{ESC_UP_A_LINE}" }

    # Clear the line that is to be printed on
    # escape += "#{ESC_R_AND_CLR}"

    # Console is clear, we can print!
    STDOUT.print escape if !@first_call
    @first_call = false
    print text
  end

  def hide_cursor
    STDOUT.print(ESC_CURS_INVIS)
  end

  def show_cursor
    STDOUT.print(ESC_CURS_VIS)
  end

  def test
    hide_cursor

    5.times do |i|
      line = ['===========================================']
      (1..10).each do |num|
        line << ["#{num}:\t#{rand_num}"]
      end
      line << ['===========================================']
      line = line.join("\n")
      reset_line(line)
      sleep 1
    end

    show_cursor

    puts ''
  end

  private
    def rand_num
      rand(10 ** rand(10))
    end
end

Inspired by prydonius/spinning_cursor. See test method for example usage.



回答5:

There are a number of curses librbaries for Ruby. I believe rbbcurse is the most maintained.