diff --git a/go.mod b/go.mod index 4370284..511e6b4 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,17 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/fumiama/terasu v0.0.0-20240418161858-1c3273a78268 +require ( + github.com/fumiama/terasu v0.0.0-20240418161858-1c3273a78268 + github.com/gizak/termui/v3 v3.1.0 +) require ( github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 // indirect github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 // indirect + github.com/mattn/go-runewidth v0.0.2 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 50b79db..3615ff9 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fumiama/terasu v0.0.0-20240418161858-1c3273a78268 h1:6R8kSGVSIoR3xm2NG8Z4ivkTpAET783RFsJOVKrI7n8= github.com/fumiama/terasu v0.0.0-20240418161858-1c3273a78268/go.mod h1:afchyfKAb7J/zvaENtYzjIEPVbwiEjJaow05zzT4usM= +github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= +github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/log.go b/log.go index 3578bec..99015df 100644 --- a/log.go +++ b/log.go @@ -2,8 +2,11 @@ package main import ( "errors" + "fmt" "io" + "strings" + "github.com/gizak/termui/v3/widgets" "github.com/sirupsen/logrus" ) @@ -11,12 +14,92 @@ var ( errZeroMeterSize = errors.New("zero meter size") ) +func infof(format string, args ...any) { + if notui { + logrus.Infof(format, args...) + } else { + sc.infof(format, args...) + } +} + +func (s *screen) infof(format string, args ...any) { + s.Lock() + defer s.Unlock() + s.logroll.Rows = append(s.logroll.Rows, fmt.Sprintf("[INFO](fg:blue) "+format, args...)) + s.logroll.ScrollDown() +} + +func warnf(format string, args ...any) { + if notui { + logrus.Warnf(format, args...) + } else { + sc.warnf(format, args...) + } +} + +func (s *screen) warnf(format string, args ...any) { + s.Lock() + defer s.Unlock() + s.logroll.Rows = append(s.logroll.Rows, fmt.Sprintf("[WARN](fg:yellow) "+format, args...)) + s.logroll.ScrollDown() +} + +func errorf(format string, args ...any) { + if notui { + logrus.Errorf(format, args...) + } else { + sc.errorf(format, args...) + } +} + +func (s *screen) errorf(format string, args ...any) { + s.Lock() + defer s.Unlock() + s.logroll.Rows = append(s.logroll.Rows, fmt.Sprintf("[ERRO](fg:red) "+format, args...)) + s.logroll.ScrollDown() +} + +func infoln(args ...any) { + if notui { + logrus.Infoln(args...) + } else { + sc.infoln(args...) + } +} + +func (s *screen) infoln(args ...any) { + s.Lock() + defer s.Unlock() + s.logroll.Rows = append(s.logroll.Rows, strings.TrimSuffix( + "[INFO](fg:blue) "+fmt.Sprintln(args...), "\n", + )) + s.logroll.ScrollDown() +} + +func errorln(args ...any) { + if notui { + logrus.Errorln(args...) + } else { + sc.errorln(args...) + } +} + +func (s *screen) errorln(args ...any) { + s.Lock() + defer s.Unlock() + s.logroll.Rows = append(s.logroll.Rows, strings.TrimSuffix( + "[ERRO](fg:red) "+fmt.Sprintln(args...), "\n", + )) + s.logroll.ScrollDown() +} + type progressmeter struct { prefix string name string size int prgs int lstp int + fptr *widgets.Gauge io.Writer } @@ -24,6 +107,9 @@ func newmeter(prefix, name string, size int) (pm progressmeter) { pm.prefix = prefix pm.name = name pm.size = size + if !notui { + pm.fptr = sc.addfile(prefix+" "+name, size) + } return } @@ -32,11 +118,25 @@ func (pm *progressmeter) Write(p []byte) (n int, err error) { return 0, errZeroMeterSize } pm.prgs += len(p) + if !notui { + sc.logwrite(len(p)) + } percent := pm.prgs * 100 / pm.size if percent == pm.lstp { return len(p), nil } - logrus.Infof("%s [%2d%%] %s\t(%d/%dMB)", pm.prefix, percent, pm.name, pm.prgs/1024/1024, pm.size/1024/1024) + if notui { + logrus.Infof("%s [%2d%%] %s\t(%dMB/%dMB)", pm.prefix, percent, pm.name, pm.prgs/1024/1024, pm.size/1024/1024) + } else { + pm.fptr.Percent = percent + pm.fptr.Label = fmt.Sprintf("%d%% (%dMB/%dMB)", percent, pm.prgs/1024/1024, pm.size/1024/1024) + } pm.lstp = percent return len(p), nil } + +func (pm *progressmeter) finish() { + if !notui { + sc.removefile(pm.fptr) + } +} diff --git a/main.go b/main.go index c4e4781..9a27592 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/fumiama/terasu/dns" "github.com/fumiama/terasu/ip" + ui "github.com/gizak/termui/v3" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -19,15 +20,18 @@ import ( const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0" +var ( + notui = false + sc screen +) + func main() { - logrus.Infoln("RVC Models Downloader start at", time.Now().Local().Format(time.DateTime+" (MST)")) - logrus.Infof("operating system: %s, architecture: %s", runtime.GOOS, runtime.GOARCH) - logrus.Infoln("can use ipv6:", ip.IsIPv6Available.Get()) ntrs := flag.Bool("notrs", false, "use standard TLS client") dnsf := flag.String("dns", "", "custom dns.yaml") cust := flag.Bool("c", false, "use custom yaml instruction") force := flag.Bool("f", false, "force download even file exists") wait := flag.Uint("w", 4, "connection waiting seconds") + flag.BoolVar(¬ui, "notui", false, "use plain text instead of TUI") flag.Parse() args := flag.Args() if len(args) != 1 { @@ -38,35 +42,50 @@ func main() { fmt.Println(cmdlst.String()) return } - if *dnsf != "" { - f, err := os.Open(*dnsf) - if err != nil { - logrus.Errorln("open custom dns file", *dnsf, "err:", err) + if notui { + logrus.Infoln("RVC Models Downloader start at", time.Now().Local().Format(time.DateTime+" (MST)")) + logrus.Infof("operating system: %s, architecture: %s", runtime.GOOS, runtime.GOARCH) + logrus.Infoln("is ipv6 available:", ip.IsIPv6Available.Get()) + } else { + if err := ui.Init(); err != nil { + logrus.Errorln("failed to initialize termui:", err) return } - m := map[string][]string{} - err = yaml.NewDecoder(f).Decode(&m) + defer ui.Close() + sc = newscreen() + } + go func() { + if *dnsf != "" { + f, err := os.Open(*dnsf) + if err != nil { + errorln("open custom dns file", *dnsf, "err:", err) + return + } + m := map[string][]string{} + err = yaml.NewDecoder(f).Decode(&m) + if err != nil { + errorln("decode custom dns file", *dnsf, "err:", err) + return + } + _ = f.Close() + if ip.IsIPv6Available.Get() { + dns.IPv6Servers.Add(m) + } else { + dns.IPv4Servers.Add(m) + } + infoln("custom dns file added") + } + usercfg, err := readconfig(args[0], *cust) if err != nil { - logrus.Errorln("decode custom dns file", *dnsf, "err:", err) + errorln(err) return } - _ = f.Close() - if ip.IsIPv6Available.Get() { - dns.IPv6Servers.Add(m) - } else { - dns.IPv4Servers.Add(m) + err = usercfg.download(args[0], "", time.Second*time.Duration(*wait), *cust, !*ntrs, *force) + if err != nil { + errorln(err) + return } - fmt.Println("custom dns file added") - } - usercfg, err := readconfig(args[0], *cust) - if err != nil { - logrus.Errorln(err) - return - } - err = usercfg.download(args[0], "", time.Second*time.Duration(*wait), *cust, !*ntrs, *force) - if err != nil { - logrus.Errorln(err) - return - } - logrus.Info("all download tasks finished.") + infoln("all download tasks finished.") + }() + sc.flushloop(time.Second) } diff --git a/net.go b/net.go index e24cba6..8ffc3f2 100644 --- a/net.go +++ b/net.go @@ -13,14 +13,13 @@ import ( "github.com/fumiama/terasu/http2" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) func (c *config) download(path, prefix string, waits time.Duration, usecust, usetrs, force bool) error { for i, t := range c.Targets { if t.Refer != "" { refp := path[:strings.LastIndex(path, "/")+1] + t.Refer - logrus.Infof("#%s%d refer to target '%s'.", prefix, i+1, refp) + infof("#%s%d refer to target '%s'.", prefix, i+1, refp) refcfg, err := readconfig(refp, usecust) if err != nil { return err @@ -32,25 +31,25 @@ func (c *config) download(path, prefix string, waits time.Duration, usecust, use continue } if t.OS != "" && t.OS != runtime.GOOS { - logrus.Warnf("#%s%d target required OS: %s but you are %s, skip.", prefix, i+1, t.OS, runtime.GOOS) + warnf("#%s%d target required OS: %s but you are %s, skip.", prefix, i+1, t.OS, runtime.GOOS) continue } if t.Arch != "" && t.Arch != runtime.GOARCH { - logrus.Warnf("#%s%d target required Arch: %s but you are %s, skip.", prefix, i+1, t.Arch, runtime.GOARCH) + warnf("#%s%d target required Arch: %s but you are %s, skip.", prefix, i+1, t.Arch, runtime.GOARCH) continue } err := os.MkdirAll(t.Folder, 0755) if err != nil { return errors.Wrap(err, fmt.Sprintf("#%s%d make target folder '%s'", prefix, i+1, t.Folder)) } - logrus.Infof("#%s%d open target folder '%s'.", prefix, i+1, t.Folder) + infof("#%s%d open target folder '%s'.", prefix, i+1, t.Folder) if len(t.Copy) == 0 { - logrus.Warningf("#%s%d empty copy target.", prefix, i+1) + warnf("#%s%d empty copy target.", prefix, i+1) continue } wg := sync.WaitGroup{} wg.Add(len(t.Copy)) - logrus.Infof("#%s%d download copy: '%v'.", prefix, i+1, t.Copy) + infof("#%s%d download copy: '%v'.", prefix, i+1, t.Copy) for j, cp := range t.Copy { go func(i int, cp, prefix string) { defer wg.Done() @@ -61,16 +60,16 @@ func (c *config) download(path, prefix string, waits time.Duration, usecust, use fname := t.Folder + "/" + cp[strings.LastIndex(cp, "/")+1:] if !force { if _, err := os.Stat(fname); err == nil || os.IsExist(err) { - logrus.Warnf("#%s%d skip exist file %s", prefix, i+1, fname) + warnf("#%s%d skip exist file %s", prefix, i+1, fname) return } } req, err := http.NewRequest("GET", c.BaseURL+"/"+cp, nil) if err != nil { - logrus.Errorf("#%s%d new request to %s err: %v", prefix, i+1, cp, err) + errorf("#%s%d new request to %s err: %v", prefix, i+1, cp, err) return } - logrus.Infof("#%s%d get: %s", prefix, i+1, req.URL) + infof("#%s%d get: %s", prefix, i+1, req.URL) req.Header.Add("user-agent", ua) var resp *http.Response if usetrs { @@ -79,29 +78,30 @@ func (c *config) download(path, prefix string, waits time.Duration, usecust, use resp, err = http.DefaultClient.Do(req) } if err != nil { - logrus.Errorf("#%s%d get %s err: %v", prefix, i+1, req.URL, err) + errorf("#%s%d get %s err: %v", prefix, i+1, req.URL, err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err := errors.New(fmt.Sprintf("HTTP %d %s", resp.StatusCode, resp.Status)) - logrus.Errorf("#%s%d get %s err: %v", prefix, i+1, req.URL, err) + errorf("#%s%d get %s err: %v", prefix, i+1, req.URL, err) return } f, err := os.Create(fname) if err != nil { - logrus.Errorf("#%s%d create file %s err: %v", prefix, i+1, fname, err) + errorf("#%s%d create file %s err: %v", prefix, i+1, fname, err) return } defer f.Close() - logrus.Infof("#%s%d writing file %s", prefix, i+1, fname) + infof("#%s%d writing file %s", prefix, i+1, fname) pm := newmeter(fmt.Sprintf("#%s%d", prefix, i+1), fname, int(resp.ContentLength)) + defer pm.finish() _, err = io.Copy(io.MultiWriter(f, &pm), resp.Body) if err != nil { - logrus.Errorf("#%s%d download file %s err: %v", prefix, i+1, fname, err) + errorf("#%s%d download file %s err: %v", prefix, i+1, fname, err) return } - logrus.Infof("#%s%d finished download %s", prefix, i+1, fname) + infof("#%s%d finished download %s", prefix, i+1, fname) }(j, cp, fmt.Sprintf("%s%d.", prefix, i+1)) } wg.Wait() diff --git a/ui.go b/ui.go new file mode 100644 index 0000000..803d42a --- /dev/null +++ b/ui.go @@ -0,0 +1,141 @@ +package main + +import ( + "fmt" + "runtime" + "sync" + "time" + "unsafe" + + "github.com/fumiama/terasu/ip" + ui "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" +) + +type screen struct { + sync.RWMutex + sysinfo *widgets.Paragraph + logroll *widgets.List + speedln *widgets.Plot + prgbars []ui.Drawable + reusepg []*widgets.Gauge + currh, w int + totaldl int + lastclr time.Time +} + +func newscreen() (s screen) { + w, h := ui.TerminalDimensions() + s.w = w + + s.sysinfo = widgets.NewParagraph() + s.sysinfo.Title = "System Info" + s.sysinfo.BorderStyle.Fg = ui.ColorGreen + s.sysinfo.Text = fmt.Sprintf( + "[Time](mod:bold): %s\n[OS](mod:bold): %s, [Architecture](mod:bold): %s\n[Is IPv6 available](mod:bold): %v", + time.Now().Local().Format(time.DateTime+" (MST)"), + runtime.GOOS, runtime.GOARCH, + ip.IsIPv6Available.Get(), + ) + s.sysinfo.SetRect(0, s.currh, w/2, s.currh+5) + s.currh += 5 + + s.logroll = widgets.NewList() + s.logroll.Title = "Logs" + s.logroll.BorderStyle.Fg = ui.ColorBlue + s.logroll.WrapText = false + s.logroll.SetRect(w/2, 0, w, h/2) + + s.speedln = widgets.NewPlot() + s.speedln.Title = "Speed" + s.speedln.Data = make([][]float64, 1) + s.speedln.Data[0] = []float64{0, 0} + s.speedln.AxesColor = ui.ColorWhite + s.speedln.LineColors[0] = ui.ColorYellow + s.speedln.BorderStyle.Fg = ui.ColorYellow + s.speedln.SetRect(w/2, h/2, w, h) + return +} + +func (s *screen) logwrite(sz int) { + s.Lock() + defer s.Unlock() + s.totaldl += sz + tdiff := time.Since(s.lastclr) + if tdiff > time.Second { + s.speedln.Data[0] = append(s.speedln.Data[0], + float64(s.totaldl/1024)/(float64(tdiff)/float64(time.Second)), + ) + s.totaldl = 0 + s.lastclr = time.Now() + } +} + +func (s *screen) flushloop(interval time.Duration) { + t := time.NewTicker(interval) + defer t.Stop() + s.flush() + for { + select { + case e := <-ui.PollEvents(): + s.flush() + if e.Type == ui.KeyboardEvent { + switch e.ID { + case "q", "", "": + return + } + } + case <-t.C: + s.flush() + } + } +} + +func (s *screen) flush() { + s.RLock() + defer s.RUnlock() + ui.Render(s.sysinfo, s.logroll, s.speedln) + if len(s.prgbars) > 0 { + ui.Render(s.prgbars...) + } +} + +func (s *screen) addfile(name string, size int) *widgets.Gauge { + s.Lock() + defer s.Unlock() + var g *widgets.Gauge + if len(s.reusepg) > 0 { + b := len(s.reusepg) - 1 + g = s.reusepg[b] + s.reusepg = s.reusepg[:b] + } else { + g = widgets.NewGauge() + g.SetRect(0, s.currh, s.w/2, s.currh+3) + s.currh += 3 + } + g.Title = name + g.Label = fmt.Sprintf("0%% (0MB/%dMB)", size/1024/1024) + s.prgbars = append(s.prgbars, g) + return g +} + +func (s *screen) removefile(g *widgets.Gauge) { + s.Lock() + defer s.Unlock() + for i, obj := range s.prgbars { + if *(**widgets.Gauge)(unsafe.Add( + unsafe.Pointer(&obj), unsafe.Sizeof(uintptr(0)), + )) == g { + switch i { + case 0: + s.prgbars = s.prgbars[1:] + case len(s.prgbars) - 1: + s.prgbars = s.prgbars[:len(s.prgbars)-1] + default: + s.prgbars = append(s.prgbars[:i], s.prgbars[i+1:]...) + } + s.reusepg = append(s.reusepg, g) + return + } + } +}