github.com/wtfutil/wtf@v0.43.0/modules/progress/widget.go (about)

     1  package progress
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/charmbracelet/bubbles/progress"
    12  	"github.com/muesli/reflow/ansi"
    13  	"github.com/rivo/tview"
    14  	"github.com/wtfutil/wtf/utils"
    15  	"github.com/wtfutil/wtf/view"
    16  )
    17  
    18  var errShellUndefined = errors.New("command shell not defined in $SHELL environment variable")
    19  
    20  // Widget is the container for your module's data
    21  type Widget struct {
    22  	view.TextWidget
    23  
    24  	settings *Settings
    25  
    26  	minimum float64
    27  	maximum float64
    28  	current float64
    29  	percent float64
    30  
    31  	padding string
    32  
    33  	shell string
    34  
    35  	err error
    36  }
    37  
    38  // NewWidget creates and returns an instance of Widget
    39  func NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {
    40  	widget := Widget{
    41  		TextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.common),
    42  
    43  		settings: settings,
    44  
    45  		minimum: settings.minimum,
    46  		maximum: settings.maximum,
    47  		current: settings.current,
    48  
    49  		shell: os.Getenv("SHELL"),
    50  
    51  		padding: strings.Repeat(" ", settings.padding),
    52  	}
    53  
    54  	return &widget
    55  }
    56  
    57  /* -------------------- Exported Functions -------------------- */
    58  
    59  // Refresh updates the onscreen contents of the widget
    60  func (widget *Widget) Refresh() {
    61  	var err error
    62  
    63  	if cmd := widget.settings.minimumCmd; cmd != "" {
    64  		widget.minimum, err = widget.execValueCmd(cmd)
    65  		if err != nil {
    66  			widget.err = fmt.Errorf("minimumCmd execution failed: %w", err)
    67  			widget.display()
    68  			return
    69  		}
    70  	}
    71  
    72  	if cmd := widget.settings.maximumCmd; cmd != "" {
    73  		widget.maximum, err = widget.execValueCmd(cmd)
    74  		if err != nil {
    75  			widget.err = fmt.Errorf("maximumCmd execution failed: %w", err)
    76  			widget.display()
    77  			return
    78  		}
    79  	}
    80  
    81  	if cmd := widget.settings.currentCmd; cmd != "" {
    82  		widget.current, err = widget.execValueCmd(cmd)
    83  		if err != nil {
    84  			widget.err = fmt.Errorf("currentCmd execution failed: %w", err)
    85  			widget.display()
    86  			return
    87  		}
    88  	}
    89  
    90  	widget.calcPercent()
    91  
    92  	widget.display()
    93  }
    94  
    95  /* -------------------- Unexported Functions -------------------- */
    96  
    97  func (widget *Widget) content() string {
    98  	if widget.err != nil {
    99  		return "[red]Error: " + widget.err.Error()
   100  	}
   101  
   102  	percent := widget.formatPercent(widget.percent)
   103  	bar := widget.buildProgressBar(percent)
   104  	barView := tview.TranslateANSI(bar.ViewAs(widget.percent))
   105  
   106  	var sb strings.Builder
   107  
   108  	switch widget.settings.showPercentage {
   109  	case "left":
   110  		sb.WriteString(widget.padding + percent + barView + widget.padding)
   111  	case "right":
   112  		sb.WriteString(widget.padding + barView + percent + widget.padding)
   113  	case "above":
   114  		centered := utils.CenterText(percent, bar.Width+widget.settings.padding*2)
   115  		sb.WriteString(centered + "\n" + widget.padding + barView + widget.padding)
   116  	case "below":
   117  		centered := utils.CenterText(percent, bar.Width+widget.settings.padding*2)
   118  		sb.WriteString(widget.padding + barView + widget.padding + "\n" + centered)
   119  	default:
   120  		sb.WriteString(widget.padding + barView + widget.padding)
   121  	}
   122  
   123  	return sb.String()
   124  }
   125  
   126  func (widget *Widget) display() {
   127  	title := widget.CommonSettings().Title
   128  
   129  	if widget.settings.showPercentage == "titleLeft" {
   130  		title = widget.formatPercent(widget.percent) + " " + title
   131  	} else if widget.settings.showPercentage == "titleRight" {
   132  		title = title + " " + widget.formatPercent(widget.percent)
   133  	}
   134  
   135  	widget.Redraw(func() (string, string, bool) {
   136  		return title, widget.content(), false
   137  	})
   138  }
   139  
   140  func (widget *Widget) execValueCmd(cmd string) (float64, error) {
   141  	if widget.shell == "" {
   142  		return -1, errShellUndefined
   143  	}
   144  
   145  	out, err := exec.Command(widget.shell, "-c", cmd).Output()
   146  	if err != nil {
   147  		return -1, err
   148  	}
   149  
   150  	outStr := strings.TrimSpace(string(out))
   151  
   152  	val, err := strconv.ParseFloat(outStr, 64)
   153  	if err != nil {
   154  		return -1, fmt.Errorf("failed to parse command output '%s' as float64: %w", outStr, err)
   155  	}
   156  
   157  	return val, nil
   158  }
   159  
   160  func (widget *Widget) buildProgressBar(percent string) *progress.Model {
   161  	pOpts := []progress.Option{
   162  		progress.WithWidth(widget.calcBarWidth(percent)),
   163  		progress.WithoutPercentage(),
   164  	}
   165  
   166  	if widget.settings.colors.solid != "" {
   167  		pOpts = append(pOpts, progress.WithSolidFill(widget.settings.colors.solid))
   168  	} else {
   169  		pOpts = append(pOpts, progress.WithGradient(
   170  			widget.settings.colors.gradientA,
   171  			widget.settings.colors.gradientB,
   172  		))
   173  	}
   174  
   175  	pb := progress.New(pOpts...)
   176  	return &pb
   177  }
   178  
   179  func (widget *Widget) calcPercent() {
   180  	if widget.maximum == 0 {
   181  		if widget.current > 100 {
   182  			widget.percent = 1
   183  		}
   184  
   185  		if widget.current < 0 {
   186  			widget.percent = 0
   187  		}
   188  
   189  		widget.percent = widget.current / 100
   190  		return
   191  	}
   192  
   193  	if widget.current > widget.maximum {
   194  		widget.percent = 1
   195  		return
   196  	}
   197  
   198  	if widget.current < widget.minimum {
   199  		widget.percent = 0
   200  		return
   201  	}
   202  
   203  	widget.percent = (widget.current - widget.minimum) / (widget.maximum - widget.minimum)
   204  }
   205  
   206  func (widget *Widget) formatPercent(p float64) string {
   207  	switch widget.settings.showPercentage {
   208  	case "left":
   209  		return fmt.Sprintf("%.0f%% ", p*100)
   210  	case "right":
   211  		return fmt.Sprintf(" %.0f%%", p*100)
   212  	case "none":
   213  		return ""
   214  	default:
   215  		return fmt.Sprintf("%.0f%%", p*100)
   216  	}
   217  }
   218  
   219  func (widget *Widget) calcBarWidth(percent string) int {
   220  	_, _, width, _ := widget.View.GetInnerRect()
   221  	width -= widget.settings.padding * 2
   222  
   223  	if widget.settings.showPercentage == "left" || widget.settings.showPercentage == "right" {
   224  		width -= ansi.PrintableRuneWidth(percent)
   225  	}
   226  
   227  	return width
   228  }