gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/progress/ansimeter.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package progress
    21  
    22  import (
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"time"
    27  	"unicode"
    28  
    29  	"golang.org/x/crypto/ssh/terminal"
    30  
    31  	"github.com/snapcore/snapd/strutil/quantity"
    32  )
    33  
    34  var stdout io.Writer = os.Stdout
    35  
    36  // ANSIMeter is a progress.Meter that uses ANSI escape codes to make
    37  // better use of the available horizontal space.
    38  type ANSIMeter struct {
    39  	label   []rune
    40  	total   float64
    41  	written float64
    42  	spin    int
    43  	t0      time.Time
    44  }
    45  
    46  // these are the bits of the ANSI escapes (beyond \r) that we use
    47  // (names of the terminfo capabilities, see terminfo(5))
    48  var (
    49  	// clear to end of line
    50  	clrEOL = "\033[K"
    51  	// make cursor invisible
    52  	cursorInvisible = "\033[?25l"
    53  	// make cursor visible
    54  	cursorVisible = "\033[?25h"
    55  	// turn on reverse video
    56  	enterReverseMode = "\033[7m"
    57  	// go back to normal video
    58  	exitAttributeMode = "\033[0m"
    59  )
    60  
    61  var termWidth = func() int {
    62  	col, _, _ := terminal.GetSize(0)
    63  	if col <= 0 {
    64  		// give up
    65  		col = 80
    66  	}
    67  	return col
    68  }
    69  
    70  func (p *ANSIMeter) Start(label string, total float64) {
    71  	p.label = []rune(label)
    72  	p.total = total
    73  	p.t0 = time.Now().UTC()
    74  	fmt.Fprint(stdout, cursorInvisible)
    75  }
    76  
    77  func norm(col int, msg []rune) []rune {
    78  	if col <= 0 {
    79  		return []rune{}
    80  	}
    81  	out := make([]rune, col)
    82  	copy(out, msg)
    83  	d := col - len(msg)
    84  	if d < 0 {
    85  		out[col-1] = '…'
    86  	} else {
    87  		for i := len(msg); i < col; i++ {
    88  			out[i] = ' '
    89  		}
    90  	}
    91  	return out
    92  }
    93  
    94  func (p *ANSIMeter) SetTotal(total float64) {
    95  	p.total = total
    96  }
    97  
    98  func (p *ANSIMeter) percent() string {
    99  	if p.total == 0. {
   100  		return "---%"
   101  	}
   102  	q := p.written * 100 / p.total
   103  	if q > 999.4 || q < 0. {
   104  		return "???%"
   105  	}
   106  	return fmt.Sprintf("%3.0f%%", q)
   107  }
   108  
   109  var formatDuration = quantity.FormatDuration
   110  
   111  func (p *ANSIMeter) Set(current float64) {
   112  	if current < 0 {
   113  		current = 0
   114  	}
   115  	if current > p.total {
   116  		current = p.total
   117  	}
   118  
   119  	p.written = current
   120  	col := termWidth()
   121  	// time left: 5
   122  	//    gutter: 1
   123  	//     speed: 8
   124  	//    gutter: 1
   125  	//   percent: 4
   126  	//    gutter: 1
   127  	//          =====
   128  	//           20
   129  	// and we want to leave at least 10 for the label, so:
   130  	//  * if      width <= 15, don't show any of this (progress bar is good enough)
   131  	//  * if 15 < width <= 20, only show time left (time left + gutter = 6)
   132  	//  * if 20 < width <= 29, also show percentage (percent + gutter = 5
   133  	//  * if 29 < width      , also show speed (speed+gutter = 9)
   134  	var percent, speed, timeleft string
   135  	if col > 15 {
   136  		since := time.Now().UTC().Sub(p.t0).Seconds()
   137  		per := since / p.written
   138  		left := (p.total - p.written) * per
   139  		// XXX: duration unit string is controlled by translations, and
   140  		// may carry a multibyte unit suffix
   141  		timeleft = " " + formatDuration(left)
   142  		if col > 20 {
   143  			percent = " " + p.percent()
   144  			if col > 29 {
   145  				speed = " " + quantity.FormatBPS(p.written, since, -1)
   146  			}
   147  		}
   148  	}
   149  
   150  	rpercent := []rune(percent)
   151  	rspeed := []rune(speed)
   152  	rtimeleft := []rune(timeleft)
   153  	msg := make([]rune, 0, col)
   154  	// XXX: assuming terminal can display `col` number of runes
   155  	msg = append(msg, norm(col-len(rpercent)-len(rspeed)-len(rtimeleft), p.label)...)
   156  	msg = append(msg, rpercent...)
   157  	msg = append(msg, rspeed...)
   158  	msg = append(msg, rtimeleft...)
   159  	i := int(current * float64(col) / p.total)
   160  	fmt.Fprint(stdout, "\r", enterReverseMode, string(msg[:i]), exitAttributeMode, string(msg[i:]))
   161  }
   162  
   163  var spinner = []string{"/", "-", "\\", "|"}
   164  
   165  func (p *ANSIMeter) Spin(msgstr string) {
   166  	msg := []rune(msgstr)
   167  	col := termWidth()
   168  	if col-2 >= len(msg) {
   169  		fmt.Fprint(stdout, "\r", string(norm(col-2, msg)), " ", spinner[p.spin])
   170  		p.spin++
   171  		if p.spin >= len(spinner) {
   172  			p.spin = 0
   173  		}
   174  	} else {
   175  		fmt.Fprint(stdout, "\r", string(norm(col, msg)))
   176  	}
   177  }
   178  
   179  func (*ANSIMeter) Finished() {
   180  	fmt.Fprint(stdout, "\r", exitAttributeMode, cursorVisible, clrEOL)
   181  }
   182  
   183  func (*ANSIMeter) Notify(msgstr string) {
   184  	col := termWidth()
   185  	fmt.Fprint(stdout, "\r", exitAttributeMode, clrEOL)
   186  
   187  	msg := []rune(msgstr)
   188  	var i int
   189  	for len(msg) > col {
   190  		for i = col; i >= 0; i-- {
   191  			if unicode.IsSpace(msg[i]) {
   192  				break
   193  			}
   194  		}
   195  		if i < 1 {
   196  			// didn't find anything; print the whole thing and try again
   197  			fmt.Fprintln(stdout, string(msg[:col]))
   198  			msg = msg[col:]
   199  		} else {
   200  			// found a space; print up to but not including it, and skip it
   201  			fmt.Fprintln(stdout, string(msg[:i]))
   202  			msg = msg[i+1:]
   203  		}
   204  	}
   205  	fmt.Fprintln(stdout, string(msg))
   206  }
   207  
   208  func (p *ANSIMeter) Write(bs []byte) (n int, err error) {
   209  	n = len(bs)
   210  	p.Set(p.written + float64(n))
   211  
   212  	return
   213  }