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 }