diff --git a/progressbar.go b/progressbar.go index 6d73304..0ccec7d 100644 --- a/progressbar.go +++ b/progressbar.go @@ -2,10 +2,13 @@ package progressbar import ( "bytes" + "encoding/json" "errors" "fmt" "io" + "log" "math" + "net/http" "os" "regexp" "strings" @@ -1009,6 +1012,31 @@ func (p *ProgressBar) State() State { return s } +// StartHTTPServer starts an HTTP server dedicated to serving progress bar updates. This allows you to +// display the status in various UI elements, such as an OS status bar with an `xbar` extension. +// It is recommended to run this function in a separate goroutine to avoid blocking the main thread. +// +// hostPort specifies the address and port to bind the server to, for example, "0.0.0.0:19999". +func (p *ProgressBar) StartHTTPServer(hostPort string) { + // for advanced users, we can return the data as json + http.HandleFunc("/state", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/json") + // since the state is a simple struct, we can just ignore the error + bs, _ := json.Marshal(p.State()) + w.Write(bs) + }) + // for others, we just return the description in a plain text format + http.HandleFunc("/desc", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintf(w, + "%d/%d, %.2f%%, %s left", + p.State().CurrentNum, p.State().Max, p.State().CurrentPercent*100, + (time.Second * time.Duration(p.State().SecondsLeft)).String(), + ) + }) + log.Fatal(http.ListenAndServe(hostPort, nil)) +} + // regex matching ansi escape codes var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) diff --git a/progressbar_test.go b/progressbar_test.go index 90d2f3f..1a4aad7 100644 --- a/progressbar_test.go +++ b/progressbar_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/md5" "encoding/hex" + "encoding/json" "fmt" "io" "log" @@ -1123,3 +1124,42 @@ func TestOptionShowTotalTrueIndeterminate(t *testing.T) { t.Errorf("wrong string: %s", buf.String()) } } + +func TestStartHTTPServer(t *testing.T) { + bar := Default(10, "test") + bar.Add(1) + + hostPort := "localhost:9696" + go bar.StartHTTPServer(hostPort) + + // check plain text + resp, err := http.Get(fmt.Sprintf("http://%s/desc", hostPort)) + if err != nil { + t.Error(err) + } + got, err := io.ReadAll(resp.Body) + if err != nil { + t.Error(err) + } + if string(got) != "1/10, 10.00%, 0s left" { + t.Errorf("wrong string: %s", string(got)) + } + + // check json + resp, err = http.Get(fmt.Sprintf("http://%s/state", hostPort)) + if err != nil { + t.Error(err) + } + got, err = io.ReadAll(resp.Body) + if err != nil { + t.Error(err) + } + var result State + err = json.Unmarshal(got, &result) + if err != nil { + t.Error(err) + } + if result.Max != bar.State().Max || result.CurrentNum != bar.State().CurrentNum { + t.Errorf("wrong state: %v", result) + } +}