├── go.sum ├── go.mod ├── examples ├── progressbar │ ├── common │ │ ├── commonbar.gif │ │ └── main.go │ ├── defaultbar │ │ ├── defaultbar.gif │ │ └── main.go │ ├── uncertain │ │ ├── uncertainbar.gif │ │ └── main.go │ ├── writingbytes_bar │ │ ├── writingbytes_bar.gif │ │ └── main.go │ └── plugin │ │ └── main.go ├── window │ └── clear │ │ └── main.go └── main.go ├── pb ├── setup.go ├── option.go ├── bar_token.go ├── progress_bar.go └── progress_bar.go.bak ├── utils └── consolesync.go ├── init └── init.go ├── window ├── console_size_unix.go ├── window.go └── console_size_windows.go ├── cursor └── cursor.go ├── box ├── option.go └── box.go ├── graph └── line.go ├── font └── font.go └── README.md /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gngtwhh/gocui 2 | 3 | go 1.22.1 4 | -------------------------------------------------------------------------------- /examples/progressbar/common/commonbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gngtwhh/gocui/HEAD/examples/progressbar/common/commonbar.gif -------------------------------------------------------------------------------- /examples/progressbar/defaultbar/defaultbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gngtwhh/gocui/HEAD/examples/progressbar/defaultbar/defaultbar.gif -------------------------------------------------------------------------------- /examples/progressbar/uncertain/uncertainbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gngtwhh/gocui/HEAD/examples/progressbar/uncertain/uncertainbar.gif -------------------------------------------------------------------------------- /pb/setup.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | func init() { 4 | InitBarToken() // Register tokens 5 | InitProgressBar() // Set default bar 6 | } 7 | -------------------------------------------------------------------------------- /examples/progressbar/writingbytes_bar/writingbytes_bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gngtwhh/gocui/HEAD/examples/progressbar/writingbytes_bar/writingbytes_bar.gif -------------------------------------------------------------------------------- /utils/consolesync.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "sync" 4 | 5 | var ConsoleMutex sync.Mutex 6 | 7 | func init() { 8 | ConsoleMutex = sync.Mutex{} 9 | } 10 | -------------------------------------------------------------------------------- /init/init.go: -------------------------------------------------------------------------------- 1 | package init 2 | 3 | import "runtime" 4 | 5 | const ( 6 | Windows = iota 7 | Linux 8 | Unix 9 | Mac 10 | ) 11 | 12 | var SysType int 13 | 14 | func init() { 15 | switch runtime.GOOS { 16 | case "windows": 17 | SysType = Windows 18 | case "Linux": 19 | SysType = Linux 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/window/clear/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gngtwhh/gocui/cursor" 8 | "github.com/gngtwhh/gocui/window" 9 | ) 10 | 11 | func main() { 12 | window.ClearScreen() 13 | cursor.GotoXY(0, 0) 14 | fmt.Print("\n1234567") 15 | time.Sleep(time.Second) 16 | window.ClearLine(1) 17 | fmt.Println("abcdefg") 18 | fmt.Print("123456789") 19 | } 20 | -------------------------------------------------------------------------------- /examples/progressbar/defaultbar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gngtwhh/gocui/cursor" 5 | "github.com/gngtwhh/gocui/pb" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | // test Default Bar 11 | cursor.HideCursor() 12 | p := pb.DefaultBar 13 | p.Iter( 14 | 100, func() { 15 | // time.Sleep(time.Second * 5) 16 | time.Sleep(time.Millisecond * 50) // Simulate some time-consuming task 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /window/console_size_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix || darwin 2 | 3 | package window 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | ) 9 | 10 | func GetConsoleSize() (weight, height int) { 11 | var sz struct { 12 | rows uint16 13 | cols uint16 14 | xpixel uint16 15 | ypixel uint16 16 | } 17 | _, _, err := syscall.Syscall(syscall.SYS_IOCTL, 18 | uintptr(syscall.Stdout), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz))) 19 | if err != 0 { 20 | return 0, 0 21 | } 22 | return int(sz.cols), int(sz.rows) 23 | } 24 | -------------------------------------------------------------------------------- /examples/progressbar/uncertain/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gngtwhh/gocui/pb" 8 | ) 9 | 10 | func main() { 11 | //test uncertain progress bar 12 | up, _ := pb.NewProgressBar("[%bar] waiting operation...%spinner", pb.WithUncertain(), 13 | pb.WithStyle(pb.Style{ 14 | Incomplete: " ", 15 | UnCertain: "<===>", 16 | })) 17 | stop := up.Run(time.Millisecond * 500) 18 | // Simulate a 3-second time-consuming task 19 | time.Sleep(time.Second * 20) 20 | close(stop) 21 | fmt.Printf("\ndone\n") 22 | } 23 | -------------------------------------------------------------------------------- /examples/progressbar/common/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gngtwhh/gocui/font" 7 | "github.com/gngtwhh/gocui/pb" 8 | ) 9 | 10 | func main() { 11 | // test progress bar 12 | p, _ := pb.NewProgressBar("%spinner[%bar]%percent %rate[%elapsed]", 13 | pb.WithStyle(pb.Style{ 14 | Complete: "=", 15 | CompleteHead: ">", 16 | Incomplete: "-", 17 | CompleteColor: font.Green, 18 | IncompleteColor: font.LightBlack, 19 | })) 20 | p.Iter(100, func() { 21 | time.Sleep(time.Millisecond * 50) // Simulate some time-consuming task 22 | }) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /examples/progressbar/plugin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gngtwhh/gocui/font" 7 | "github.com/gngtwhh/gocui/pb" 8 | ) 9 | 10 | type Spinnerbar struct { 11 | cur int 12 | } 13 | 14 | func (sb *Spinnerbar) ToString(ctx *pb.Context) string { 15 | res := []string{"⠧⠤⠴", "⠯⠥⠄", "⠯⠍⠁", "⠏⠉⠙", "⠉⠉⠽", "⠀⠭⠽", "⠤⠤⠽"}[sb.cur] 16 | res = font.Decorate(res, font.Red+sb.cur) 17 | sb.cur = (sb.cur + 1) % 7 18 | return res 19 | } 20 | 21 | func main() { 22 | // test plugin mode 23 | pb.RegisterToken("%spinbar", &Spinnerbar{}) 24 | pluginBar, _ := pb.NewProgressBar("%spinbar[%bar]%percent|%elapsed", pb.WithPos(1, 0), 25 | pb.WithStyle(pb.Style{ 26 | Complete: "#", 27 | Incomplete: "-", 28 | CompleteColor: font.Green, 29 | IncompleteColor: font.LightBlack, 30 | })) 31 | pluginBar.Iter(100, func() { 32 | time.Sleep(time.Millisecond * 50) // Simulate some time-consuming task 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /window/window.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gngtwhh/gocui/cursor" 8 | ) 9 | 10 | // ClearArea clears a rectangular area of the screen. 11 | func ClearArea(x, y, width, height int) { 12 | for i := 0; i < height; i++ { 13 | cursor.GotoXY(x+i, y) 14 | fmt.Printf("%s", strings.Repeat(" ", width)) 15 | } 16 | } 17 | 18 | // ClearLine clears the line at the given row, or the current line if row is negative. 19 | func ClearLine(row int) { 20 | if row < 0 { 21 | fmt.Print("\033[2K") 22 | } else { 23 | fmt.Print("\033[s") 24 | cursor.GotoXY(row, 0) 25 | fmt.Print("\033[2K\033[u") 26 | } 27 | } 28 | 29 | // ClearLineAfterCursor clear the content from the cursor position to the end of the line 30 | func ClearLineAfterCursor() { 31 | fmt.Print("\033[0K") 32 | } 33 | 34 | // ClearLineBeforCursor Clear the cursor position to the beginning of the line 35 | func ClearLineBeforCursor() { 36 | fmt.Print("\033[1K") 37 | } 38 | 39 | func ClearScreen() { 40 | fmt.Printf("%s", "\033[H\033[J") 41 | } 42 | -------------------------------------------------------------------------------- /cursor/cursor.go: -------------------------------------------------------------------------------- 1 | package cursor 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | //var ( 8 | // csi = "\033[" 9 | //) 10 | 11 | // GotoXY returns the escape sequence to move the cursor to the given position. 12 | func GotoXY(x, y int) { 13 | fmt.Printf("\033[%d;%dH", x+1, y+1) 14 | } 15 | 16 | // Up returns the escape sequence to move the cursor up by n lines. 17 | func Up(n int) { 18 | fmt.Printf("\033[%dA", n) 19 | } 20 | 21 | // Down returns the escape sequence to move the cursor down by n lines. 22 | func Down(n int) { 23 | fmt.Printf("\033[%dB", n) 24 | } 25 | 26 | // Left returns the escape sequence to move the cursor left by n columns. 27 | func Left(n int) { 28 | fmt.Printf("\033[%dD", n) 29 | } 30 | 31 | // Right returns the escape sequence to move the cursor right by n columns. 32 | func Right(n int) { 33 | fmt.Printf("\033[%dC", n) 34 | } 35 | 36 | // HideCursor returns the escape sequence to hide the cursor. 37 | func HideCursor() { 38 | fmt.Printf("\033[?25l") 39 | } 40 | 41 | // ShowCursor returns the escape sequence to show the cursor. 42 | func ShowCursor() { 43 | fmt.Printf("\033[?25h") 44 | } 45 | -------------------------------------------------------------------------------- /box/option.go: -------------------------------------------------------------------------------- 1 | package box 2 | 3 | type ModFunc func(p *Property) 4 | 5 | // WithDefault sets the default property of the box. 6 | // Should be used before any other ModFunc, otherwise it will overwrite all the other ModFuncs 7 | // that in front of it. 8 | func WithDefault() ModFunc { 9 | return func(p *Property) { 10 | *p = DefaultProperty 11 | } 12 | } 13 | 14 | // WithProperty sets the property of the box to given property. 15 | // NOTE: This will cause the current property of the box to be overwritten. 16 | func WithProperty(p Property) ModFunc { 17 | return func(p2 *Property) { 18 | *p2 = p 19 | } 20 | } 21 | 22 | // WithPos sets the position of the box on the screen. 23 | // param x, y: the position of the TopLeft corner of the box on the screen, must be within [0, screen width/height), 24 | // if x or y is out of range, it will not be set. 25 | func WithPos(x, y int) ModFunc { 26 | return func(p *Property) { 27 | p.PosX = x 28 | p.PosY = y 29 | p.BindPos = true 30 | } 31 | } 32 | 33 | // WithStyle sets the style of the box. 34 | func WithStyle(s Style) ModFunc { 35 | return func(p *Property) { 36 | p.Style = s 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /graph/line.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gngtwhh/gocui/cursor" 6 | "github.com/gngtwhh/gocui/utils" 7 | ) 8 | 9 | // Line Draws a line 10 | // x, y - starting point 11 | // length - length of the line 12 | // ch - character to draw, 0 - horizontal, 1 - vertical 13 | func Line(x, y, length int, ch rune, lineType uint8) { 14 | utils.ConsoleMutex.Lock() 15 | defer utils.ConsoleMutex.Unlock() 16 | 17 | if lineType == 0 { 18 | for i := 0; i < length; i++ { 19 | cursor.GotoXY(x+i, y) 20 | fmt.Print(string(ch)) 21 | } 22 | } else { 23 | for i := 0; i < length; i++ { 24 | cursor.GotoXY(x, y+i) 25 | fmt.Print(string(ch)) 26 | } 27 | } 28 | } 29 | 30 | // Curve Draws a curve 31 | // x, y - starting point 32 | // length - length of the curve 33 | // ch - character to draw 34 | // f - function that returns the y coordinate by the x coordinate 35 | func Curve(x, y, length, sign int, ch rune, f func(int) int) { 36 | utils.ConsoleMutex.Lock() 37 | defer utils.ConsoleMutex.Unlock() 38 | 39 | if sign < 0 { 40 | for i := -length; i < 0; i++ { 41 | if x+i < 0 { 42 | continue 43 | } 44 | cursor.GotoXY(x+i, y+f(i)) 45 | fmt.Print(string(ch)) 46 | } 47 | } else { 48 | for i := 0; i < length; i++ { 49 | cursor.GotoXY(x+i, y+f(i)) 50 | fmt.Print(string(ch)) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/progressbar/writingbytes_bar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "github.com/gngtwhh/gocui/pb" 12 | ) 13 | 14 | func main() { 15 | // The server only supports weak ciphers, which were disabled by default in Go 1.22. 16 | os.Setenv("GODEBUG", "tlsrsakex=1") 17 | 18 | tr := &http.Transport{ 19 | TLSClientConfig: &tls.Config{ 20 | MinVersion: tls.VersionTLS12, 21 | }, 22 | } 23 | client := &http.Client{Transport: tr} 24 | req, _ := http.NewRequest("GET", "https://studygolang.com/dl/golang/go1.23.5.src.tar.gz", nil) 25 | req.Header.Add("Accept-Encoding", "identity") 26 | resp, err := client.Do(req) 27 | if err != nil { 28 | panic(err) 29 | } 30 | defer resp.Body.Close() 31 | 32 | f, _ := os.OpenFile("go1.23.5.src.tar.gz", os.O_CREATE|os.O_WRONLY, 0644) 33 | defer func() { 34 | f.Close() 35 | if err := os.Remove("go1.23.5.src.tar.gz"); err != nil { 36 | panic(err) 37 | } 38 | }() 39 | 40 | fmt.Println("downloading...go1.23.5.src.tar.gz") 41 | // bar, _ := pb.NewProgressBar("[%bar] %percent %bytes", pb.WithWriter(), pb.WithTotal(resp.ContentLength)) 42 | bar, _ := pb.NewProgressBar("[%bar] %percent %bytes", pb.WithWriter()) 43 | barWriter, _ := bar.RunWithWriter(resp.ContentLength) 44 | if _, err := io.Copy(io.MultiWriter(f, barWriter), resp.Body); err != nil { 45 | fmt.Print(err.Error()) 46 | } 47 | time.Sleep(time.Millisecond) 48 | fmt.Print("\ndone\n") 49 | } 50 | -------------------------------------------------------------------------------- /font/font.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Style of font 9 | const ( 10 | Bold = 1 11 | Dim = 2 12 | Italic = 3 13 | Underline = 4 14 | BlinkSlow = 5 15 | BlinkFast = 6 16 | Reverse = 7 17 | Hide = 8 18 | CrossedOut = 9 19 | ) 20 | 21 | // 8/16 colors 22 | const ( 23 | RESET = 0 24 | 25 | // foreground colors 26 | Black = 30 27 | Red = 31 28 | Green = 32 29 | Yellow = 33 30 | Blue = 34 31 | Magenta = 35 32 | Cyan = 36 33 | White = 37 34 | LightBlack = 90 35 | LightRed = 91 36 | LightGreen = 92 37 | LightYellow = 93 38 | LightBlue = 94 39 | LightMagenta = 95 40 | LightCyan = 96 41 | LightWhite = 97 42 | 43 | // background colors 44 | BlackBg = 40 45 | RedBg = 41 46 | GreenBg = 42 47 | YellowBg = 43 48 | BlueBg = 44 49 | MagentaBg = 45 50 | CyanBg = 46 51 | WhiteBg = 47 52 | LightBlackBg = 100 53 | LightRedBg = 101 54 | LightGreenBg = 102 55 | LightYellowBg = 103 56 | LightBlueBg = 104 57 | LightMagentaBg = 105 58 | LightCyanBg = 106 59 | LightWhiteBg = 107 60 | ) 61 | 62 | func SetColor(color int) { 63 | fmt.Printf("\033[%dm", color) 64 | } 65 | 66 | func SetColorRgb(r, g, b int, bg bool) { 67 | if bg { 68 | fmt.Printf("\033[48;2;%d;%d;%dm", r, g, b) 69 | } else { 70 | fmt.Printf("\033[38;2;%d;%d;%dm", r, g, b) 71 | } 72 | } 73 | 74 | func ResetColor() { 75 | fmt.Printf("\033[%dm", RESET) 76 | } 77 | 78 | func SetStyle(style int) string { 79 | return fmt.Sprintf("\033[%dm", style) 80 | } 81 | 82 | func Decorate(text string, style ...int) string { 83 | buf := strings.Builder{} 84 | buf.WriteString("\033[") 85 | l := len(style) 86 | for i, s := range style { 87 | buf.WriteString(fmt.Sprintf("%d", s)) 88 | if i < l-1 { 89 | buf.WriteString(";") 90 | } 91 | } 92 | buf.WriteString("m") 93 | buf.WriteString(text) 94 | buf.WriteString("\033[0m") 95 | return buf.String() 96 | } 97 | 98 | func Splice(ks ...interface{}) string { 99 | buf := strings.Builder{} 100 | for _, k := range ks { 101 | switch v := k.(type) { 102 | case int: 103 | buf.WriteString(fmt.Sprintf("\033[%dm", v)) 104 | case rune: 105 | buf.WriteRune(v) 106 | case string: 107 | buf.WriteString(v) 108 | } 109 | } 110 | return buf.String() 111 | } 112 | -------------------------------------------------------------------------------- /pb/option.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import "github.com/gngtwhh/gocui/window" 4 | 5 | // ModFunc is a function that modifies the Property of the progress bar. 6 | type ModFunc func(p *Property) 7 | 8 | // WithDefault sets the default property of the progress bar. 9 | // Should be used before any other ModFunc, otherwise it will overwrite all the other ModFuncs 10 | // that in front of it. 11 | func WithDefault() ModFunc { 12 | return func(p *Property) { 13 | *p = DefaultProperty 14 | } 15 | } 16 | 17 | // WithProperty sets the property of the progress bar to given property. 18 | // NOTE: This will cause the current property of the progress bar to be overwritten. 19 | func WithProperty(p Property) ModFunc { 20 | return func(p2 *Property) { 21 | *p2 = p 22 | } 23 | } 24 | 25 | // WithPos sets the position of the progress bar on the screen. 26 | // If set, the progress bar will be placed at the specified position, 27 | // otherwise, it will refresh at the current line of the cursor(by default). 28 | // param x, y: the position of the progress bar on the screen, must be within [0, screen width/height), 29 | // if x or y is out of range, it will not be set. 30 | func WithPos(x, y int) ModFunc { 31 | return func(p *Property) { 32 | w, h := window.GetConsoleSize() 33 | if x < 0 || y < 0 || x >= h || y >= w { 34 | return 35 | } 36 | p.BindPos = true 37 | p.PosX = x 38 | p.PosY = y 39 | } 40 | } 41 | 42 | // WithWidth sets the width of the progress bar shown on the screen. 43 | // The supplied width argument w must be a positive integer, 44 | // should be larger than or equal to the actual width of the expected render. 45 | func WithWidth(w int) ModFunc { 46 | return func(p *Property) { 47 | p.Width = w 48 | } 49 | } 50 | 51 | // WithUncertain sets the type of progress bar to uncertain. 52 | func WithUncertain() ModFunc { 53 | return func(p *Property) { 54 | p.Uncertain = true 55 | } 56 | } 57 | 58 | // WithStyle sets the style of the progress bar. 59 | func WithStyle(s Style) ModFunc { 60 | return func(p *Property) { 61 | p.Style = s 62 | } 63 | } 64 | 65 | // WithFormat sets the format of the progress bar. 66 | // The format string will be parsed to tokens before rendering. 67 | func WithFormat(f string) ModFunc { 68 | return func(p *Property) { 69 | p.Format = f 70 | p.formatChanged = true 71 | } 72 | } 73 | 74 | // WithWriter set the bar type as IO bar 75 | func WithWriter() ModFunc { 76 | return func(p *Property) { 77 | p.Bytes = true 78 | } 79 | } 80 | 81 | // WithBarWidth set the width of the bar width 82 | func WithBarWidth(w int) ModFunc { 83 | return func(p *Property) { 84 | p.BarWidth = w 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /window/console_size_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package window 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | ) 9 | 10 | type ( 11 | short int16 12 | word uint16 13 | ulong uint32 14 | smallRect struct { 15 | Left short 16 | Top short 17 | Right short 18 | Bottom short 19 | } 20 | coord struct { 21 | X short 22 | Y short 23 | } 24 | /* 25 | typedef struct _CONSOLE_SCREEN_BUFFER_INFO { 26 | COORD dwSize; 27 | COORD dwCursorPosition; 28 | WORD wAttributes; 29 | SMALL_RECT srWindow; 30 | COORD dwMaximumWindowSize; 31 | } CONSOLE_SCREEN_BUFFER_INFO; 32 | */ 33 | consoleScreenBufferInfo struct { 34 | dwSize coord 35 | dwCursorPosition coord 36 | wAttributes word 37 | srWindow smallRect 38 | dwMaximumWindowSize coord 39 | } 40 | /* 41 | typedef struct _CONSOLE_SCREEN_BUFFER_INFOEX { 42 | ULONG cbSize; 43 | COORD dwSize; 44 | COORD dwCursorPosition; 45 | WORD wAttributes; 46 | SMALL_RECT srWindow; 47 | COORD dwMaximumWindowSize; 48 | WORD wPopupAttributes; 49 | BOOL bFullscreenSupported; 50 | COLORREF ColorTable[16]; 51 | } CONSOLE_SCREEN_BUFFER_INFOEX, *PCONSOLE_SCREEN_BUFFER_INFOEX; 52 | */ 53 | consoleScreenBufferInfoEx struct { 54 | cbSize uint32 55 | dwSize coord 56 | dwCursorPosition coord 57 | wAttributes word 58 | srWindow smallRect 59 | dwMaximumWindowSize coord 60 | wPopupAttributes word 61 | bFullscreenSupported bool 62 | ColorTable [16]uint32 63 | } 64 | ) 65 | 66 | func (c coord) uintptr() uintptr { 67 | // little endian, put x first 68 | return uintptr(c.X) | (uintptr(c.Y) << 16) 69 | } 70 | 71 | var kernel32DLL = syscall.NewLazyDLL("kernel32.dll") 72 | var getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") 73 | 74 | //var setConsoleScreenBufferInfoProc = kernel32DLL.NewProc("SetConsoleScreenBufferInfo") 75 | //var setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo") 76 | 77 | var handle *syscall.Handle 78 | 79 | func getConsoleScreenBufferInfo() (*consoleScreenBufferInfo, error) { 80 | if handle == nil { 81 | h, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) 82 | if err != nil { 83 | return nil, err 84 | } 85 | handle = &h 86 | } 87 | 88 | var info consoleScreenBufferInfo 89 | if err := getError(getConsoleScreenBufferInfoProc.Call(uintptr(*handle), uintptr(unsafe.Pointer(&info)))); err != nil { 90 | return nil, err 91 | } 92 | return &info, nil 93 | } 94 | 95 | func getError(r1, r2 uintptr, lastErr error) error { 96 | // If the function fails, the return value is zero. 97 | if r1 == 0 { 98 | if lastErr != nil { 99 | return lastErr 100 | } 101 | return syscall.EINVAL 102 | } 103 | return nil 104 | } 105 | 106 | func GetConsoleSize() (weight, height int) { 107 | info, err := getConsoleScreenBufferInfo() 108 | if err != nil { 109 | return 0, 0 110 | } 111 | return int(info.srWindow.Right - info.srWindow.Left + 1), int(info.srWindow.Bottom - info.srWindow.Top + 1) 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gocui 2 | 3 | Gocui is a simple command line graphics toolkit for Go.Use it to build simple command line applications easily. 4 | 5 | // At present, this is just a simple small project, welcome to help improve it. 6 | 7 | # Features 8 | - Easy to use. Just create an object and set its style, then call `Run()` to start the application. 9 | - Customizable style. You can choose from different tokens to customize the style of the objects. 10 | - Compatible. It use CSI codes to control the terminal. 11 | 12 | # Functions 13 | - Progress bar: Create a progress bar or an uncertain progress bar. And you can set the style of the progress bar. 14 | - Text box: Create a text box to contain text. 15 | - graph: Draw lines or curves in the terminal. 16 | 17 | # Examples 18 | 19 | ## Progress Bar 20 | bar running by iter. 21 | 22 | ### Use Default Bar Style 23 | gocui provide a default bar style: 24 | ```go 25 | p := pb.DefaultBar 26 | it, _ := p.Iter() 27 | for range it { 28 | //fmt.Printf("i=%d\n", i) 29 | time.Sleep(time.Millisecond * 50) // Simulate some time-consuming task 30 | } 31 | ``` 32 | which looks like: 33 | ![Example of default progress bar](examples/progressbar/defaultbar/defaultbar.gif) 34 | 35 | ### Common usage 36 | You can decorate the bar by format string with tokens supported. 37 | 38 | ```go 39 | // test progress bar 40 | p, _ := pb.NewProgressBar("%spinner[%bar] %percent %rate [%elapsed]", 41 | pb.WithStyle(pb.Style{ 42 | Complete: ">", 43 | Incomplete: "-", 44 | CompleteColor: font.Green, 45 | IncompleteColor: font.LightBlack, 46 | })) 47 | it, _ := p.Iter() 48 | for range it { 49 | time.Sleep(time.Millisecond * 50) // Simulate some time-consuming task 50 | } 51 | ``` 52 | Which looks like: 53 | ![Example of progress bar](examples/progressbar/common/commonbar.gif) 54 | 55 | ### Uncertain progress bar 56 | gocui support uncertain bar, main goroutine can stop it anytime. 57 | 58 | ```go 59 | //test uncertain progress bar 60 | up, _ := pb.NewProgressBar("[%bar] waiting operation...%spinner", pb.WithUncertain(), 61 | pb.WithStyle(pb.Style{ 62 | Incomplete: " ", 63 | UnCertain: "👈🤣👉", 64 | })) 65 | stop := up.Run(time.Millisecond * 100) 66 | // Simulate a 3-second time-consuming task 67 | time.Sleep(time.Second * 3) 68 | close(stop) 69 | fmt.Printf("\ndone") 70 | ``` 71 | which looks like: 72 | ![Example of uncertain progress bar](examples/progressbar/uncertain/uncertainbar.gif) 73 | 74 | ### I/O Progress Bar 75 | Data is synchronously written to the progress bar as a progress update. 76 | 77 | ```go 78 | req, _ := http.NewRequest("GET", "https://studygolang.com/dl/golang/go1.23.5.src.tar.gz", nil) 79 | req.Header.Add("Accept-Encoding", "identity") 80 | resp, err := http.DefaultClient.Do(req) 81 | if err != nil { 82 | panic(err) 83 | } 84 | defer resp.Body.Close() 85 | 86 | f, _ := os.OpenFile("go1.23.5.src.tar.gz", os.O_CREATE|os.O_WRONLY, 0644) 87 | defer func() { 88 | f.Close() 89 | if err := os.Remove("go1.23.5.src.tar.gz"); err != nil { 90 | panic(err) 91 | } 92 | }() 93 | 94 | fmt.Println("downloading...") 95 | bar, _ := pb.NewProgressBar("[%bar] %percent %bytes", pb.WithWriter(), pb.WithTotal(resp.ContentLength)) 96 | barWriter, _ := bar.RunWithWriter() 97 | if _, err := io.Copy(io.MultiWriter(f, barWriter), resp.Body); err != nil { 98 | fmt.Print(err.Error()) 99 | } 100 | fmt.Print("\ndone") 101 | ``` 102 | which looks like: 103 | ![Example of I/O progress bar](examples/progressbar/writingbytes_bar/writingbytes_bar.gif) 104 | 105 | ## Text box 106 | ```go 107 | payload := []string{ 108 | " Books Management System", 109 | "", 110 | " 1.Store new books 2.New user registration", 111 | " 3.Borrow books 4.Return books", 112 | " 5.All books 6.All user", 113 | " 7.Delete database 8.Log out", 114 | "", 115 | " Select operation number:", 116 | } 117 | window.ClearScreen() 118 | aBox, _ := box.GetBox(len(payload)+2, 50+2, "bold", payload) 119 | box.SetBoxAt(aBox, 0, 0) 120 | ``` 121 | 122 | This will create a text box and set it to the top left corner of the screen. 123 | 124 | # Customization 125 | 126 | // Currently, only Progress bar is supported. 127 | 128 | Use a format string to customize the style of the objects,in which you can use the tokens to customize the style of the objects. 129 | 130 | You can think of tokens as verbs in go. 131 | 132 | ## Tokens 133 | 134 | Tokens that users use to customize the style of the objects. 135 | 136 | ### progress bar 137 | - `%bar`: the progress bar 138 | - `%current`: the current value 139 | - `%total`: the total value 140 | - `%percent`: the percentage 141 | - `%elapsed`: the elapsed time 142 | - `%rate`: Interval between two updates 143 | 144 | # TODO 145 | - [ ] Add more examples 146 | - [ ] Add more modules 147 | - [ ] Support more tokens 148 | - [ ] Allow users define their own tokens 149 | - [ ] Expand application scenarios, such as support parameters processing... 150 | -------------------------------------------------------------------------------- /pb/bar_token.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/gngtwhh/gocui/font" 11 | ) 12 | 13 | /************************************************** 14 | * Currently supported tokens: 15 | * %string: Regular string 16 | * %bar: The body of the progress bar 17 | * %current: Current progress value 18 | * %total: Total progress value 19 | * %elapsed: The elapsed time of the progress bar 20 | * %rate: Speed of the progress bar 21 | * %spinner: A rotator character 22 | * %bytes: Progress of writing data 23 | * &percent: Percentage of progress 24 | **************************************************/ 25 | 26 | // var legalTokens []string 27 | var registeredTokens = make(map[string]token) 28 | 29 | func InitBarToken() { 30 | // legalTokens = []string{ 31 | // "%bar", "%current", "%total", "%percent", "%elapsed", "%rate", "%spinner", "%bytes", 32 | // } 33 | registeredTokens["%bar"] = &TokenBar{} 34 | registeredTokens["%current"] = &TokenCurrent{} 35 | registeredTokens["%total"] = &TokenTotal{} 36 | registeredTokens["%percent"] = &TokenPercent{} 37 | registeredTokens["%elapsed"] = &TokenElapsed{} 38 | registeredTokens["%rate"] = &TokenRate{minDelay: time.Millisecond * 100} 39 | registeredTokens["%spinner"] = &TokenSpinner{} 40 | registeredTokens["%bytes"] = &TokenBytes{} 41 | } 42 | 43 | // token is the interface that all tokens must implement. 44 | type token interface { 45 | ToString(ctx *Context) string 46 | } 47 | 48 | // Here are the tokens that can be used in the format string. 49 | // All tokens use the toString method to convert the tokens to a string for print. 50 | 51 | type TokenBar struct{} 52 | type TokenCurrent struct{} 53 | type TokenTotal struct{} 54 | type TokenPercent struct{} 55 | type TokenElapsed struct{} 56 | type TokenRate struct { 57 | minDelay time.Duration 58 | lastRate string 59 | lastTime time.Time 60 | lastProcess int64 61 | } 62 | type TokenString struct{ payload string } 63 | type TokenSpinner struct{ cur int8 } 64 | type TokenBytes struct{} 65 | 66 | // ToString implements the interface 67 | func (b *TokenBar) ToString(ctx *Context) string { 68 | var repeatStr = func(s string, length int) string { 69 | if len(s) == 0 { 70 | return "" 71 | } 72 | return strings.Repeat(s, length/len(s)) + s[:length%len(s)] 73 | } 74 | 75 | p := &ctx.Property 76 | barWidth := p.BarWidth 77 | if barWidth == 0 { 78 | barWidth = ctx.WindowWidth - ctx.WidthWithoutBar 79 | } 80 | if p.Uncertain { 81 | // leftSpace := int(ctx.current) 82 | // rightSpace := barWidth - leftSpace - len(p.Style.UnCertain) 83 | // if leftSpace == 0 && ctx.direction == -1 { 84 | // ctx.direction = 1 85 | // } 86 | // if rightSpace == 0 && ctx.direction == 1 { 87 | // ctx.direction = -1 88 | // } 89 | leftSpace := int(ctx.Current) % barWidth 90 | uncertainWidth := min(barWidth-leftSpace, len(p.Style.UnCertain)) 91 | rightSpace := max(0, barWidth-leftSpace-len(p.Style.UnCertain)) 92 | return font.Decorate(repeatStr(p.Style.Incomplete, leftSpace), p.Style.IncompleteColor) + 93 | font.Decorate(repeatStr(p.Style.UnCertain, uncertainWidth), p.Style.UnCertainColor) + 94 | font.Decorate(repeatStr(p.Style.Incomplete, rightSpace), p.Style.IncompleteColor) 95 | } else { 96 | completeLength := int(float64(ctx.Current)/float64(ctx.Total)*float64(barWidth)) - len(p.Style.CompleteHead) 97 | if completeLength < 0 { 98 | completeLength = 0 99 | } 100 | return font.Decorate(repeatStr(p.Style.Complete, completeLength), p.Style.CompleteColor) + 101 | font.Decorate(p.Style.CompleteHead, p.Style.CompleteHeadColor) + 102 | font.Decorate(repeatStr(p.Style.Incomplete, barWidth-completeLength-len(p.Style.CompleteHead)), p.Style.IncompleteColor) 103 | } 104 | } 105 | 106 | func (c *TokenCurrent) ToString(ctx *Context) string { 107 | return strconv.FormatInt(ctx.Current, 10) 108 | } 109 | 110 | func (t *TokenTotal) ToString(ctx *Context) string { 111 | return strconv.FormatInt(ctx.Total, 10) 112 | } 113 | 114 | func (t *TokenPercent) ToString(ctx *Context) string { 115 | var percent int 116 | if ctx.Current == 0 { 117 | percent = 0 118 | } else { 119 | percent = int(float64(ctx.Current) / float64(ctx.Total) * 100) 120 | } 121 | // 保留2位小数 122 | return fmt.Sprintf("%3d%%", percent) 123 | } 124 | 125 | func (t *TokenElapsed) ToString(ctx *Context) string { 126 | // return fmt.Sprintf("%5.2fs", ctx.property.elapsed.Seconds()) 127 | return fmt.Sprintf("%.1fs", time.Since(ctx.StartTime).Seconds()) 128 | } 129 | 130 | func (t *TokenRate) ToString(ctx *Context) string { 131 | inc := ctx.Current - t.lastProcess 132 | if t.lastTime.IsZero() { 133 | t.lastTime = time.Now() 134 | t.lastRate = "0 it/s" 135 | return "0 it/s" 136 | } 137 | dur := time.Since(t.lastTime) 138 | if dur < t.minDelay { 139 | return t.lastRate 140 | } 141 | // return dur.String() 142 | rate := (inc) * int64(time.Second/dur) 143 | t.lastRate = fmt.Sprintf("%d it/s", rate) 144 | t.lastProcess = ctx.Current 145 | t.lastTime = time.Now() 146 | return t.lastRate 147 | } 148 | 149 | func (s *TokenString) ToString(ctx *Context) string { 150 | return s.payload 151 | } 152 | 153 | func (s *TokenSpinner) ToString(ctx *Context) string { 154 | res := "\\|/-"[s.cur : s.cur+1] 155 | s.cur = (s.cur + 1) % 4 156 | return res 157 | } 158 | 159 | func (s *TokenBytes) ToString(ctx *Context) string { 160 | calStr := func(b int64) string { 161 | if b == 0 { 162 | return "0 B" 163 | } 164 | sizes := []string{" B", " kB", " MB", " GB", " TB", " PB", " EB"} 165 | base := 1024.0 166 | e := math.Floor(math.Log(float64(b)) / math.Log(base)) 167 | unit := sizes[int(e)] 168 | val := math.Floor(float64(b)/math.Pow(base, e)*10+0.5) / 10 169 | return fmt.Sprintf("%.1f%s", val, unit) 170 | } 171 | if ctx.Property.Uncertain { 172 | return calStr(ctx.Current) 173 | } 174 | return calStr(ctx.Current) + "/" + calStr(ctx.Total) 175 | } 176 | 177 | // unmarshalToken converts the token string to a slice of tokens. 178 | func unmarshalToken(format string) (ts []token, barPos []int) { 179 | if len(format) == 0 { 180 | return 181 | } 182 | 183 | ok := false // Whether a valid token is matched 184 | for len(format) > 0 { 185 | ok = false 186 | if format[0] != '%' { 187 | goto commonString 188 | } 189 | for legalToken, legalTokenInstance := range registeredTokens { 190 | if strings.HasPrefix(format, legalToken) { 191 | format = format[len(legalToken):] 192 | var newToken = legalTokenInstance 193 | ts = append(ts, newToken) 194 | ok = true 195 | break 196 | } 197 | } 198 | if ok && len(format) == 0 { 199 | break 200 | } 201 | commonString: 202 | if format[0] != '%' || !ok { 203 | if idx := strings.IndexAny(format[1:], "%"); idx == -1 { 204 | ts = append(ts, &TokenString{payload: format}) 205 | break 206 | } else { 207 | ts = append(ts, &TokenString{payload: format[:idx+1]}) 208 | format = format[idx+1:] 209 | } 210 | } 211 | } 212 | return 213 | } 214 | 215 | // RegisterToken allows user to register a new token to achieve the unique effect they desire. 216 | // This means allowing users to use their own custom tokens with specific behaviors in the format, 217 | // as long as they are registered before use. 218 | // WARNING: Registering a token with a name that already exists will overwrite the existing token. 219 | // WARNING: Ensure no existing token becomes a prefix of any newly registered token. 220 | func RegisterToken(name string, token token) { 221 | registeredTokens[name] = token 222 | } 223 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "math" 7 | "os" 8 | "time" 9 | 10 | "github.com/gngtwhh/gocui/cursor" 11 | "github.com/gngtwhh/gocui/font" 12 | "github.com/gngtwhh/gocui/graph" 13 | "github.com/gngtwhh/gocui/pb" 14 | "github.com/gngtwhh/gocui/window" 15 | ) 16 | 17 | func boxTest() { 18 | // payload := []string{ 19 | // "", 20 | // " 1.Store new books 2.New user registration ", 21 | // " 3.Borrow books 4.Return books", 22 | // " 5.All books 6.All user", 23 | // " 7.Delete database 8.Log out", 24 | // "", 25 | // " Select operation number:", 26 | // } 27 | // window.ClearScreen() 28 | 29 | // aBox, _ := box.NewBox(box.Property{ 30 | // BoxType: "rounded", 31 | // Title: "Books Management System", 32 | // Payload: payload, 33 | // }) 34 | // box.SetBoxAt(aBox, 0, 0) 35 | } 36 | 37 | func barTest() { 38 | cursor.HideCursor() 39 | window.ClearScreen() 40 | 41 | // test Default Bar 42 | p := pb.DefaultBar 43 | p.Iter(100, func() { 44 | //fmt.Printf("i=%d\n", i) 45 | time.Sleep(time.Millisecond * 50) // Simulate some time-consuming task 46 | // time.Sleep(time.Second * 10) // Simulate some time-consuming task 47 | }) 48 | 49 | // test progress bar 50 | p, _ = pb.NewProgressBar("[%bar] %current/%total-%percent %rate", pb.WithPos(1, 0), 51 | pb.WithStyle(pb.Style{ 52 | Complete: "#", 53 | Incomplete: "-", 54 | CompleteColor: font.Green, 55 | IncompleteColor: font.LightBlack, 56 | })) 57 | p.Iter(100, func() { 58 | time.Sleep(time.Millisecond * 30) // Simulate some time-consuming task 59 | }) 60 | 61 | //test uncertain progress bar 62 | up, _ := pb.NewProgressBar("[%bar] testing ubar...%spinner", pb.WithUncertain(), pb.WithPos(2, 0), 63 | pb.WithStyle(pb.Style{ 64 | Incomplete: " ", 65 | UnCertain: "<===>", 66 | })) 67 | stop := up.Run(time.Millisecond * 100) 68 | _, _ = up.UpdateProperty(pb.WithPos(3, 0)) 69 | stop2 := up.Run(time.Millisecond * 200) 70 | // Simelate doing something 71 | time.Sleep(time.Second * 3) // Simulate a 3-second time-consuming task 72 | stop2 <- struct{}{} 73 | time.Sleep(time.Second * 3) // Another 3-second time-consuming task 74 | close(stop) 75 | 76 | //test the Go function using default uncertain progress bar 77 | f := func() { 78 | for range 100 { 79 | time.Sleep(time.Millisecond * 30) // Simulate some time-consuming task 80 | } 81 | } 82 | pb.Go(f) 83 | 84 | // test the Go method 85 | up.Go(f) 86 | 87 | // test the Start method 88 | r, _ := p.Start(100) 89 | for i := range 100 { 90 | time.Sleep(time.Millisecond * 30) // Simulate some time-consuming task 91 | if i > 50 { 92 | r.UpdateAdd(-1) 93 | } else { 94 | r.UpdateAdd(1) 95 | } 96 | } 97 | r.Stop() 98 | 99 | cursor.GotoXY(3, 0) 100 | fmt.Println("time out. exit...") 101 | } 102 | 103 | func lineTest() { 104 | drawCoord := func(x, y, length int) { 105 | graph.Line(x, y, length, '|', 0) 106 | cursor.GotoXY(x+length, y) 107 | fmt.Printf("v") 108 | length = 60 109 | graph.Line(x, y, length, '-', 1) 110 | cursor.GotoXY(x, y+length) 111 | fmt.Printf(">") 112 | cursor.GotoXY(x, y) 113 | fmt.Printf("+") 114 | } 115 | 116 | var x, y, length int 117 | 118 | window.ClearScreen() 119 | 120 | // curve f(x)=x^2 121 | x, y, length = 1, 2, 10 122 | drawCoord(x, y, length) 123 | f := func(x int) int { 124 | return int(math.Pow(float64(x), 2)) 125 | } 126 | x, y, length = 1, 2, 10 127 | graph.Curve(x, y, length, 1, '*', f) 128 | time.Sleep(time.Second * 2) 129 | 130 | window.ClearScreen() 131 | 132 | // curve f(x)=sin(x) 133 | x, y, length = 2, 30, 10 134 | drawCoord(x, y, length) 135 | f = func(x int) int { 136 | return int(math.Sin(float64(x)) * 5) 137 | } 138 | x, y, length = 2, 30, 10 139 | graph.Curve(x, y, length, 1, '*', f) 140 | time.Sleep(time.Second * 2) 141 | 142 | //time.Sleep(time.Second * 2) 143 | window.ClearScreen() 144 | 145 | // curve f(x)=sqrt(3)*x 146 | x, y, length = 10, 30, 20 147 | drawCoord(x, y, length) 148 | f = func(x int) int { 149 | return int(math.Sqrt(3) * float64(x)) 150 | } 151 | x, y, length = 10, 30, 15 152 | graph.Curve(x, y, length, 1, '*', f) 153 | graph.Curve(x, y, length, -1, '*', f) 154 | } 155 | 156 | func windowSizeTest() { 157 | w, h := window.GetConsoleSize() 158 | fmt.Printf("Command info: weight: %d, height: %d\n", w, h) 159 | } 160 | 161 | func FontTest() { 162 | fmt.Println("-----Testing text color-----") 163 | fmt.Println(font.Decorate("Black", font.Black)) 164 | fmt.Println(font.Decorate("Red", font.Red)) 165 | fmt.Println(font.Decorate("Green", font.Green)) 166 | fmt.Println(font.Decorate("Yellow", font.Yellow)) 167 | fmt.Println(font.Decorate("Blue", font.Blue)) 168 | fmt.Println(font.Decorate("Magenta", font.Magenta)) 169 | fmt.Println(font.Decorate("Cyan", font.Cyan)) 170 | fmt.Println(font.Decorate("White", font.White)) 171 | fmt.Println(font.Decorate("LightBlack", font.LightBlack)) 172 | fmt.Println(font.Decorate("LightRed", font.LightRed)) 173 | fmt.Println(font.Decorate("LightGreen", font.LightGreen)) 174 | fmt.Println(font.Decorate("LightYellow", font.LightYellow)) 175 | fmt.Println(font.Decorate("LightBlue", font.LightBlue)) 176 | fmt.Println(font.Decorate("LightMagenta", font.LightMagenta)) 177 | fmt.Println(font.Decorate("LightCyan", font.LightCyan)) 178 | fmt.Println(font.Decorate("LightWhite", font.LightWhite)) 179 | 180 | fmt.Println("-----Testing background style-----") 181 | fmt.Println(font.Decorate("BlackBg", font.BlackBg)) 182 | fmt.Println(font.Decorate("RedBg", font.RedBg)) 183 | fmt.Println(font.Decorate("GreenBg", font.GreenBg)) 184 | fmt.Println(font.Decorate("YellowBg", font.YellowBg)) 185 | fmt.Println(font.Decorate("BlueBg", font.BlueBg)) 186 | fmt.Println(font.Decorate("MagentaBg", font.MagentaBg)) 187 | fmt.Println(font.Decorate("CyanBg", font.CyanBg)) 188 | fmt.Println(font.Decorate("WhiteBg", font.WhiteBg)) 189 | fmt.Println(font.Decorate("LightBlackBg", font.LightBlackBg)) 190 | fmt.Println(font.Decorate("LightRedBg", font.LightRedBg)) 191 | fmt.Println(font.Decorate("LightGreenBg", font.LightGreenBg)) 192 | fmt.Println(font.Decorate("LightYellowBg", font.LightYellowBg)) 193 | fmt.Println(font.Decorate("LightBlueBg", font.LightBlueBg)) 194 | fmt.Println(font.Decorate("LightMagentaBg", font.LightMagentaBg)) 195 | fmt.Println(font.Decorate("LightCyanBg", font.LightCyanBg)) 196 | fmt.Println(font.Decorate("LightWhiteBg", font.LightWhiteBg)) 197 | 198 | fmt.Println("-----Testing font style-----") 199 | fmt.Println(font.Decorate("Bold", font.Bold)) 200 | fmt.Println(font.Decorate("Dim", font.Dim)) 201 | fmt.Println(font.Decorate("Italic", font.Italic)) 202 | fmt.Println(font.Decorate("Underline", font.Underline)) 203 | fmt.Println(font.Decorate("BlinkSlow", font.BlinkSlow)) 204 | fmt.Println(font.Decorate("BlinkFast", font.BlinkFast)) 205 | fmt.Println(font.Decorate("Reverse", font.Reverse)) 206 | fmt.Println(font.Decorate("Hide", font.Hide)) 207 | } 208 | 209 | func main() { 210 | //c := '0' 211 | runList := []string{ 212 | "barTest", 213 | // "boxTest", 214 | // "lineTest", 215 | // "windowSizeTest", 216 | // "FontTest", 217 | } 218 | funcs := map[string]func(){ 219 | "barTest": barTest, 220 | "lineTest": lineTest, 221 | "boxTest": boxTest, 222 | "windowSizeTest": windowSizeTest, 223 | "FontTest": FontTest, 224 | } 225 | 226 | scanner := bufio.NewScanner(os.Stdin) 227 | for _, s := range runList { 228 | if f, ok := funcs[s]; ok { 229 | f() 230 | scanner.Scan() 231 | window.ClearScreen() 232 | } 233 | } 234 | 235 | cursor.ShowCursor() 236 | } 237 | -------------------------------------------------------------------------------- /box/box.go: -------------------------------------------------------------------------------- 1 | package box 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gngtwhh/gocui/cursor" 6 | "github.com/gngtwhh/gocui/font" 7 | "github.com/gngtwhh/gocui/utils" 8 | "github.com/gngtwhh/gocui/window" 9 | "strings" 10 | ) 11 | 12 | /** 13 | * box characters 14 | * 0 1 2 3 4 5 6 7 8 9 A B C D E F 15 | * U+250x ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ 16 | * U+251x ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ 17 | * U+252x ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ 18 | * U+253x ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ 19 | * U+254x ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ 20 | * U+255x ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ 21 | * U+256x ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ 22 | * U+257x ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ 23 | */ 24 | 25 | // Box types 26 | const ( 27 | FINE = iota 28 | BOLD 29 | DOUBLE 30 | ROUNDED 31 | ) 32 | 33 | // Title positions 34 | const ( 35 | TopLeft = iota 36 | TopMid 37 | TopRight 38 | BottomLeft 39 | BottomMid 40 | BottomRight 41 | InsideLeft 42 | InsideMid 43 | InsideRight 44 | ) 45 | 46 | // Content align 47 | const ( 48 | Center = iota 49 | Left 50 | Right 51 | ) 52 | 53 | // Types of box 54 | var ( 55 | fine = []rune{'─', '│', '┌', '┐', '└', '┘', '┬', '┴', '├', '┤', '┼'} 56 | bold = []rune{'━', '┃', '┏', '┓', '┗', '┛', '┳', '┻', '┣', '┫', '╋'} 57 | double = []rune{'═', '║', '╔', '╗', '╚', '╝', '╦', '╩', '╠', '╣', '╬'} 58 | rounded = []rune{'─', '│', '╭', '╮', '╰', '╯', '┬', '┴', '├', '┤', '┼'} 59 | ) 60 | 61 | // default settings 62 | var ( 63 | DefaultBox *Box 64 | defaultType []rune 65 | DefaultProperty Property 66 | ) 67 | 68 | // Char contains characters of the box frame 69 | type Char struct { 70 | TopLeft, TopRight, BottomLeft, BottomRight rune // Characters for box corners 71 | Top, Bottom, Left, Right rune // Characters for box sides 72 | } 73 | 74 | // Color contains colors of the box frame 75 | type Color struct { 76 | TopLeftColor, TopRightColor, BottomLeftColor, BottomRightColor int // color of box corners 77 | TopColor, BottomColor, LeftColor, RightColor int // color of box sides 78 | TitleColor, InnerColor int // color of the title and the text inside the box 79 | } 80 | type Style struct { 81 | Char 82 | Color 83 | } 84 | 85 | type Property struct { 86 | Style // decorate characters and color 87 | PadX, PadY int // padding 88 | Align int // Align of the content (Center/Left/Right), default Center 89 | TitlePos int // title pos (Top/Bottom/Inside x Left/Middle/Right), default TopLeft 90 | PosX, PosY int // default pos to be print 91 | 92 | BindPos bool // Whether bind the absolute pos, PosX and PosY are valid only when BindPos is true 93 | } 94 | 95 | type Box struct { 96 | Property 97 | } 98 | 99 | func init() { 100 | defaultType = fine 101 | DefaultProperty = Property{ 102 | PadX: 1, PadY: 1, 103 | Align: Center, 104 | TitlePos: TopLeft, 105 | Style: Style{ 106 | Char: Char{ 107 | TopLeft: defaultType[2], TopRight: defaultType[3], 108 | BottomLeft: defaultType[4], BottomRight: defaultType[5], 109 | Top: defaultType[0], Bottom: defaultType[0], 110 | Left: defaultType[1], Right: defaultType[1], 111 | }, 112 | Color: Color{ 113 | TopLeftColor: font.White, TopRightColor: font.White, BottomLeftColor: font.White, BottomRightColor: font.White, 114 | TopColor: font.White, BottomColor: font.White, LeftColor: font.White, RightColor: font.White, 115 | TitleColor: font.White, InnerColor: font.White, 116 | }, 117 | }, 118 | } 119 | DefaultBox = &Box{DefaultProperty} 120 | } 121 | 122 | // func NewBox(p Property) (box []string, err error) { 123 | // payloadCnt := len(p.Payload) 124 | // var useChar []rune 125 | // switch p.BoxType { 126 | // case "fine": 127 | // useChar = fine 128 | // case "bold": 129 | // useChar = bold 130 | // case "double": 131 | // useChar = double 132 | // case "rounded": 133 | // useChar = rounded 134 | // default: 135 | // useChar = fine 136 | // } 137 | // // calculate row and col 138 | // row, col := p.Row, p.Col 139 | // if (row < 2 && row != 0) || (col < 2 && col != 0) { 140 | // box = []string{} 141 | // err = fmt.Errorf("row and col must be greater than 2") 142 | // return 143 | // } 144 | // if row == 0 { 145 | // row = len(p.Payload) + 2 146 | // } 147 | // if col == 0 { 148 | // for _, p := range p.Payload { 149 | // col = max(len(p), col) 150 | // } 151 | // col += 2 152 | // } 153 | // if len(p.Title) > col-2 { 154 | // p.Title = p.Title[:col-2] // 标题过长,截断 155 | // } 156 | // // generate title line of box 157 | // { 158 | // aLen := min(2, col-2-len(p.Title)) 159 | // box = append(box, string(useChar[2])+strings.Repeat(string(useChar[0]), aLen)+p.Title+ 160 | // strings.Repeat(string(useChar[0]), col-2-len(p.Title)-aLen)+string(useChar[3])) 161 | // } 162 | // for i := 0; i < row-2; i++ { 163 | // //暂时无法在包含有非ASCII字符时正确对齐 164 | // 165 | // if i < payloadCnt { 166 | // copyLen := min(col-2, len([]byte(p.Payload[i]))) 167 | // a := string([]byte(p.Payload[i])[:copyLen]) 168 | // b := strings.Repeat(" ", col-2-copyLen) 169 | // box = append(box, string(useChar[1])+a+b+string(useChar[1])) 170 | // } else { 171 | // box = append(box, string(useChar[1])+strings.Repeat(" ", col-2)+string(useChar[1])) 172 | // } 173 | // } 174 | // box = append(box, string(useChar[4])+strings.Repeat(string(useChar[0]), col-2)+string(useChar[5])) 175 | // return box, err 176 | // } 177 | 178 | // NewBox create a box template with several modify functions 179 | func NewBox(mfs ...ModFunc) (box *Box, err error) { 180 | var p Property 181 | for _, mf := range mfs { 182 | if mf == nil { 183 | return nil, fmt.Errorf("modify func cannot be nil") 184 | } 185 | mf(&p) 186 | } 187 | // revise the characters 188 | if p.Style.Top == rune(0) { 189 | p.Style.Top = defaultType[0] 190 | } 191 | if p.Style.Bottom == rune(0) { 192 | p.Style.Bottom = defaultType[0] 193 | } 194 | if p.Style.Left == rune(0) { 195 | p.Style.Left = defaultType[1] 196 | } 197 | if p.Style.Right == rune(0) { 198 | p.Style.Right = defaultType[1] 199 | } 200 | if p.Style.TopLeft == rune(0) { 201 | p.Style.TopLeft = defaultType[2] 202 | } 203 | if p.Style.TopRight == rune(0) { 204 | p.Style.TopRight = defaultType[3] 205 | } 206 | if p.Style.BottomLeft == rune(0) { 207 | p.Style.BottomLeft = defaultType[4] 208 | } 209 | if p.Style.BottomRight == rune(0) { 210 | p.Style.BottomRight = defaultType[5] 211 | } 212 | // revise the colors 213 | if p.Style.TopLeftColor == 0 { 214 | p.Style.TopLeftColor = font.White 215 | } 216 | if p.Style.TopRightColor == 0 { 217 | p.Style.TopRightColor = font.White 218 | } 219 | if p.Style.BottomLeftColor == 0 { 220 | p.Style.BottomLeftColor = font.White 221 | } 222 | if p.Style.BottomRightColor == 0 { 223 | p.Style.BottomRightColor = font.White 224 | } 225 | if p.Style.TopColor == 0 { 226 | p.Style.TopColor = font.White 227 | } 228 | if p.Style.BottomColor == 0 { 229 | p.Style.BottomColor = font.White 230 | } 231 | if p.Style.LeftColor == 0 { 232 | p.Style.LeftColor = font.White 233 | } 234 | if p.Style.RightColor == 0 { 235 | p.Style.RightColor = font.White 236 | } 237 | if p.Style.TitleColor == 0 { 238 | p.Style.TitleColor = font.White 239 | } 240 | if p.Style.InnerColor == 0 { 241 | p.Style.InnerColor = font.White 242 | } 243 | // revise other props 244 | if p.PadX < 0 { 245 | p.PadX = 0 246 | } 247 | if p.PadY < 0 { 248 | p.PadY = 0 249 | } 250 | if p.Align < 0 || p.Align > 2 { 251 | p.Align = Center 252 | } 253 | if p.TitlePos < 0 || p.TitlePos > 8 { 254 | p.TitlePos = TopLeft 255 | } 256 | maxY, maxX := window.GetConsoleSize() 257 | if p.PosX < 0 || p.PosX >= maxX { 258 | p.PosX = 0 259 | } 260 | if p.PosY < 0 || p.PosY >= maxY { 261 | p.PosY = 0 262 | } 263 | return &Box{p}, nil 264 | } 265 | 266 | func (box *Box) Print(title string, payload []string) { 267 | p := box.Property 268 | maxLen := 0 269 | for _, str := range payload { 270 | maxLen = max(maxLen, len(str)) 271 | } 272 | x, y := 0, 0 273 | if box.Property.BindPos { 274 | x, y = p.PosX, p.PosY 275 | } 276 | var line string 277 | utils.ConsoleMutex.Lock() 278 | defer utils.ConsoleMutex.Unlock() 279 | // first line 280 | line = font.Splice( 281 | p.TopLeftColor, p.TopLeft, p.TopColor, strings.Repeat(string(p.Top), maxLen+p.PadX*2), p.TopRightColor, 282 | p.TopRight, 283 | ) 284 | cursor.GotoXY(x, y) 285 | fmt.Print(line) 286 | y++ 287 | 288 | } 289 | -------------------------------------------------------------------------------- /pb/progress_bar.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gngtwhh/gocui/cursor" 11 | "github.com/gngtwhh/gocui/font" 12 | "github.com/gngtwhh/gocui/utils" 13 | "github.com/gngtwhh/gocui/window" 14 | ) 15 | 16 | // DefaultBar is a pre-created default progress bar tokens 17 | var ( 18 | DefaultBar *ProgressBar 19 | DefaultBarFormat string 20 | DefaultProperty Property 21 | ) 22 | 23 | var ( 24 | DefaultUncertainBar *ProgressBar 25 | DefaultUncertainBarFormat string 26 | DefaultUncertainBarProperty Property 27 | ) 28 | 29 | func InitProgressBar() { 30 | var err error 31 | 32 | DefaultBarFormat = "%percent|%bar|%current/%total %elapsed %rate" 33 | DefaultProperty = Property{ 34 | Style: Style{ 35 | Complete: " ", 36 | Incomplete: " ", 37 | CompleteColor: font.WhiteBg, 38 | IncompleteColor: font.RESET, 39 | }, 40 | } 41 | DefaultBar, err = NewProgressBar(DefaultBarFormat, WithDefault()) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | DefaultUncertainBarFormat = "[%bar]" 47 | DefaultUncertainBarProperty = Property{ 48 | Style: Style{ 49 | Incomplete: " ", 50 | UnCertain: " ", 51 | UnCertainColor: font.WhiteBg, 52 | }, 53 | } 54 | DefaultUncertainBar, err = NewProgressBar( 55 | DefaultUncertainBarFormat, WithProperty(DefaultUncertainBarProperty), WithUncertain(), 56 | ) 57 | if err != nil { 58 | panic(err) 59 | } 60 | } 61 | 62 | // Property is the default Property of the progress bar. 63 | // A running instance will be initialized with the progress bar's current Property. 64 | type Property struct { 65 | Style // Style of the "bar" token, including characters and color 66 | Format string // Format: The render format of the progress bar(with tokens). 67 | PosX, PosY int // Pos: The position of the progress bar on the screen 68 | BarWidth int // BarWidth: The render width of the token:"%bar" 69 | Width int // Width: The maximum width of the progress bar displayed on the terminal. 70 | 71 | Uncertain bool // Uncertain: Whether the progress bar is Uncertain, default: false 72 | Bytes bool // Type: Whether the progress bar is used for bytes writer, default: false 73 | BindPos bool // Whether bind the absolute pos, PosX and PosY are valid only when BindPos is true 74 | 75 | formatChanged bool // Indicates the change in format when updating property 76 | } 77 | 78 | // Style is the tokens struct in the Property struct, used to decorate the token "bar". 79 | type Style struct { 80 | Complete, CompleteHead, Incomplete, UnCertain string // The "bar" token style 81 | CompleteColor, CompleteHeadColor, IncompleteColor, UnCertainColor int // The color of the bar 82 | } 83 | 84 | // ProgressBar is a simple progress bar implementation. 85 | type ProgressBar struct { 86 | property Property 87 | tokens []token // Parsed tokens tokens, will not be updated 88 | barPos []int // index of "%bar" in tokens 89 | // running int // The number of running instances 90 | rw sync.RWMutex // RWMutex to synchronize access to the progress bar 91 | } 92 | 93 | // Context is a Context created when the progress bar is running. 94 | // Each progress bar instance can create several contexts for reuse. 95 | type Context struct { 96 | Property Property // Copy of static progress bar property 97 | tokens []token // Copy of static progress bar tokens 98 | Total int64 // total: Only available when Uncertain is false or Bytes is true 99 | Current int64 // current progress 100 | WindowWidth int // window width, set by window.GetConsoleSize() 101 | WidthWithoutBar int // accumulated render width without bar 102 | StartTime time.Time // start time 103 | Interrupt chan struct{} // interrupt channel to stop running 104 | // Direction: for UnCertain bar to update, 1(default) for increasing, -1 for decreasing, only available when UnCertain is true 105 | Direction int 106 | } 107 | 108 | // BytesWriter implements io.Writer interface, 109 | // used to receive data written to the bar and trigger render updates 110 | type BytesWriter struct { 111 | // bytesChan is the context pointer to which the BytesWriter belongs 112 | bytesChan chan int 113 | // closeCh indicate that the BytesWriter has been closed 114 | closeCh chan struct{} 115 | } 116 | 117 | // Runner holds the necessary information to run a progress bar. 118 | // For users to manually control the progress of the progress bar. 119 | type Runner struct { 120 | bar *ProgressBar 121 | ctx *Context 122 | } 123 | 124 | // Update updates the progress bar's current value. 125 | func (r *Runner) Update(value int64) { 126 | value = min(max(0, value), r.ctx.Total) 127 | r.ctx.updateCurrentTo(value) 128 | r.ctx.Print() 129 | } 130 | 131 | // UpdateAdd updates the progress bar's current value by adding the given value. 132 | func (r *Runner) UpdateAdd(value int64) { 133 | r.ctx.updateCurrentWithAdd(value) 134 | r.ctx.Print() 135 | } 136 | 137 | // Stop stops the progress bar running instance. 138 | func (r *Runner) Stop() { 139 | // currently doing nothing 140 | } 141 | 142 | func NewContext(p *ProgressBar) Context { 143 | style := make([]token, len(p.tokens)) 144 | copy(style, p.tokens) 145 | 146 | ctx := Context{ 147 | Property: p.property, 148 | tokens: style, 149 | // barPos: DefaultBarPos, 150 | Current: 0, 151 | StartTime: time.Now(), 152 | Interrupt: make(chan struct{}), 153 | Direction: 1, 154 | } 155 | ctx.WindowWidth, _ = window.GetConsoleSize() 156 | return ctx 157 | } 158 | 159 | func NewBytesWriter() *BytesWriter { 160 | return &BytesWriter{ 161 | bytesChan: make(chan int, 1), 162 | closeCh: make(chan struct{}), 163 | } 164 | } 165 | 166 | // Write implement io.Writer interface 167 | func (bw *BytesWriter) Write(b []byte) (n int, err error) { 168 | n = len(b) 169 | return n, bw.update(b) 170 | } 171 | 172 | // update send update current value to the channel to render next status 173 | func (bw *BytesWriter) update(b []byte) error { 174 | select { 175 | case <-bw.closeCh: 176 | return errors.New("channel closed") 177 | default: 178 | } 179 | bw.bytesChan <- len(b) 180 | return nil 181 | } 182 | 183 | // close stop the BytesWriter producer 184 | func (bw *BytesWriter) close() error { 185 | select { 186 | case <-bw.closeCh: 187 | return errors.New("the BytesWriter has been closed") 188 | default: 189 | } 190 | close(bw.closeCh) 191 | close(bw.bytesChan) 192 | return nil 193 | } 194 | 195 | // NewProgressBar creates a new progress bar that with the given tokens and total. 196 | func NewProgressBar(style string, mfs ...ModFunc) (pb *ProgressBar, err error) { 197 | if style == "" { 198 | return nil, fmt.Errorf("tokens cannot be empty") 199 | } 200 | 201 | property := Property{} 202 | for _, mf := range mfs { 203 | if mf == nil { 204 | return nil, fmt.Errorf("modify func cannot be nil") 205 | } 206 | mf(&property) 207 | } 208 | 209 | // revise the properties 210 | if property.BarWidth < 0 { 211 | property.BarWidth = 0 // default 0 means full width 212 | } 213 | x, _ := window.GetConsoleSize() 214 | if property.Width <= 0 && property.Width > x { 215 | property.Width = x 216 | } 217 | if property.Style.Complete == "" { 218 | property.Style.Complete = "=" 219 | } 220 | // if property.Style.CompleteHead == "" { 221 | // property.Style.CompleteHead = ">" 222 | // } 223 | if property.Style.Incomplete == "" { 224 | property.Style.Incomplete = "-" 225 | } 226 | if property.Style.UnCertain == "" { 227 | property.Style.UnCertain = "<->" 228 | } 229 | if property.Style.CompleteColor == font.RESET { 230 | property.Style.CompleteColor = font.White 231 | } 232 | if property.Style.CompleteHeadColor == font.RESET { 233 | property.Style.CompleteHeadColor = font.White 234 | } 235 | if property.Style.IncompleteColor == font.RESET { 236 | property.Style.IncompleteColor = font.LightBlack 237 | } 238 | if property.Style.UnCertainColor == font.RESET { 239 | property.Style.UnCertainColor = font.White 240 | } 241 | // generate tokens tokens 242 | styleTokens, barPos := unmarshalToken(style) 243 | // create progress bar 244 | pb = &ProgressBar{ 245 | tokens: styleTokens, 246 | property: property, 247 | barPos: barPos, // TODO: deprecated 248 | rw: sync.RWMutex{}, 249 | } 250 | return 251 | } 252 | 253 | // UpdateProperty updates Property of the progress bar. 254 | // If the bar has instances running, it will return an error and do nothing. 255 | func (p *ProgressBar) UpdateProperty(mfs ...ModFunc) (NewBar *ProgressBar, err error) { 256 | p.rw.Lock() 257 | defer p.rw.Unlock() 258 | 259 | property := &p.property 260 | for _, mf := range mfs { 261 | if mf == nil { 262 | return p, fmt.Errorf("modify func cannot be nil") 263 | } 264 | mf(property) 265 | } 266 | 267 | // revise the properties 268 | if property.BarWidth < 0 { 269 | property.BarWidth = 0 // default 0 means full width 270 | } 271 | x, _ := window.GetConsoleSize() 272 | if property.Width <= 0 && property.Width > x { 273 | property.Width = x 274 | } 275 | if property.Style.Complete == "" { 276 | property.Style.Complete = "#" 277 | } 278 | if property.Style.Incomplete == "" { 279 | property.Style.Incomplete = "-" 280 | } 281 | if property.Style.UnCertain == "" { 282 | property.Style.UnCertain = "<->" 283 | } 284 | if property.Style.CompleteColor == font.RESET { 285 | property.Style.CompleteColor = font.White 286 | } 287 | if property.Style.IncompleteColor == font.RESET { 288 | property.Style.IncompleteColor = font.LightBlack 289 | } 290 | if property.Style.UnCertainColor == font.RESET { 291 | property.Style.UnCertainColor = font.White 292 | } 293 | // generate tokens tokens 294 | if property.formatChanged || property.Format != "" { 295 | property.formatChanged = false 296 | p.tokens, p.barPos = unmarshalToken(property.Format) 297 | } 298 | return p, nil 299 | } 300 | 301 | // updateCurrent increase the current progress without printing the progress bar. 302 | func (ctx *Context) updateCurrent() { 303 | if ctx.Property.Uncertain { 304 | // ctx.current += int64(ctx.direction) 305 | // if rightSpace := ctx.property.barWidth -ctx.current - len(ctx.property.UnCertain); rightSpace <= 0 && ctx.direction == 1 { 306 | // ctx.current = ctx.total 307 | // ctx.direction = -ctx.direction 308 | // } else if ctx.current <= 0 && ctx.direction == -1 { 309 | // ctx.current = 0 310 | // ctx.direction = -ctx.direction 311 | // } 312 | ctx.Current++ 313 | } else { 314 | // common bar use total to update the current progress 315 | ctx.Current = min(ctx.Current+1, ctx.Total) 316 | } 317 | } 318 | 319 | // updateCurrentWithAdd increase the current progress by add 320 | func (ctx *Context) updateCurrentWithAdd(add int64) { 321 | if ctx.Property.Uncertain { 322 | ctx.updateCurrent() // just add 1 for uncertain progress bar 323 | } else { 324 | ctx.Current = min(ctx.Current+add, ctx.Total) // common bar use total to update the current progress 325 | } 326 | } 327 | 328 | // updateCurrentTo update the current progress to value 329 | func (ctx *Context) updateCurrentTo(value int64) { 330 | if ctx.Property.Uncertain && int64(value) != ctx.Current { 331 | ctx.updateCurrent() // just add 1 for uncertain progress bar 332 | } else { 333 | ctx.Current = min(max(int64(value), 0), ctx.Total) 334 | } 335 | } 336 | 337 | // Print prints the current progress of the progress bar. 338 | func (ctx *Context) Print() { 339 | var payloadBuilder0 strings.Builder 340 | payloadBuilder1 := strings.Builder{} 341 | var barToken *TokenBar 342 | for _, t := range ctx.tokens { 343 | if _, ok := t.(*TokenBar); ok && barToken == nil { 344 | barToken = t.(*TokenBar) 345 | payloadBuilder0 = payloadBuilder1 346 | payloadBuilder1 = strings.Builder{} 347 | } else { 348 | payloadBuilder1.WriteString(t.ToString(ctx)) 349 | } 350 | } 351 | ctx.WidthWithoutBar = payloadBuilder0.Len() + payloadBuilder1.Len() 352 | barStr := barToken.ToString(ctx) 353 | 354 | utils.ConsoleMutex.Lock() // Lock the cursor 355 | defer utils.ConsoleMutex.Unlock() 356 | { 357 | // window.ClearLine(-1) 358 | if ctx.Property.BindPos { 359 | cursor.GotoXY(ctx.Property.PosX, ctx.Property.PosY) 360 | } else { 361 | fmt.Print("\r") 362 | } 363 | fmt.Print(payloadBuilder0.String()) 364 | fmt.Print(barStr) 365 | fmt.Print(payloadBuilder1.String()) 366 | if ctx.Property.BarWidth != 0 { 367 | window.ClearLineAfterCursor() 368 | } 369 | } 370 | } 371 | 372 | // Stop stops the progress bar. 373 | func (ctx *Context) stop() { 374 | // currently do nothing 375 | 376 | // ctx.interrupt <- struct{}{} 377 | // p.rw.Lock() 378 | // p.running-- 379 | // p.rw.Unlock() 380 | } 381 | 382 | // iter starts a progress bar iteration, and returns two channels: 383 | // iter <-chan int: to be used to iterate over the progress bar; 384 | // stop chan<- struct{}: to stop the progress bar. 385 | // This method should not be used if the progress bar is uncertain, 386 | // otherwise, the returned iter channel will be closed, and the stop channel will be nil. 387 | func (p *ProgressBar) iter(n int) (iter <-chan int64, stop chan<- struct{}) { 388 | ch := make(chan int64) 389 | var ctx Context 390 | p.rw.Lock() 391 | { 392 | // p.property.total = int64(n) 393 | if p.property.Uncertain { 394 | close(ch) // Uncertain bar cannot be iterated 395 | p.rw.Unlock() 396 | return ch, nil 397 | } 398 | ctx = NewContext(p) 399 | ctx.Total = int64(n) 400 | // p.running++ 401 | } 402 | p.rw.Unlock() 403 | 404 | ctx.StartTime = time.Now() 405 | go func() { 406 | defer ctx.stop() 407 | defer close(ch) 408 | for i := ctx.Current; i <= ctx.Total; i++ { 409 | ctx.Print() 410 | select { 411 | case ch <- i: 412 | case <-ctx.Interrupt: 413 | return 414 | } 415 | ctx.updateCurrent() 416 | } 417 | }() 418 | return ch, ctx.Interrupt 419 | } 420 | 421 | // Start starts an progress bar, and returns a *Runner instance to control the progress bar. 422 | // This method should not be called if the bar is uncertain. 423 | func (p *ProgressBar) Start(n int) (r *Runner, err error) { 424 | if p.property.Uncertain { 425 | return nil, errors.New("the bar is uncertain") 426 | } 427 | p.rw.Lock() 428 | // p.running++ 429 | ctx := NewContext(p) 430 | p.rw.Unlock() 431 | ctx.Total = int64(n) 432 | r = &Runner{ 433 | bar: p, 434 | ctx: &ctx, 435 | } 436 | return r, nil 437 | } 438 | 439 | // Iter WILL BLOCK, start an default progress bar over the param function and render the bar. 440 | // The bar will iterate n times and call f for each iteration. 441 | func (p *ProgressBar) Iter(n int, f func()) { 442 | if n <= 0 || f == nil { 443 | return 444 | } 445 | it, stop := p.iter(n) 446 | for range it { 447 | f() 448 | } 449 | close(stop) 450 | } 451 | 452 | // Run starts an uncertain progress bar, and returns a channel to stop the progress bar. 453 | // period is the time interval between each update, pass 0 to use the default period(100ms). 454 | // If the progress bar is not uncertain, the returned channel will be nil. 455 | func (p *ProgressBar) Run(period time.Duration) (stop chan<- struct{}) { 456 | var ctx Context 457 | p.rw.Lock() 458 | { 459 | if !p.property.Uncertain { 460 | p.rw.Unlock() 461 | return nil 462 | } 463 | 464 | ctx = NewContext(p) 465 | if period == 0 { 466 | period = time.Millisecond * 100 // default period is 100ms 467 | } 468 | // p.running++ 469 | } 470 | p.rw.Unlock() 471 | 472 | ctx.StartTime = time.Now() 473 | ticker := time.NewTicker(period) 474 | 475 | go func() { 476 | defer ctx.stop() 477 | for { 478 | select { 479 | case <-ticker.C: 480 | ctx.Print() 481 | ctx.updateCurrent() 482 | case <-ctx.Interrupt: // interrupt got a signal or closed 483 | ticker.Stop() 484 | return 485 | } 486 | } 487 | }() 488 | return ctx.Interrupt 489 | } 490 | 491 | // RunWithWriter automatically start a progress bar with writing bytes data. 492 | // param n: the total bytes to write. 493 | // It returns a writer for user to write data and a stop channel indicate exit. 494 | // This method should not be called if the progress bar is not with writer or is uncertain. 495 | func (p *ProgressBar) RunWithWriter(n int64) (writer *BytesWriter, stop chan<- struct{}) { 496 | if p.property.Uncertain { 497 | return 498 | } 499 | var ctx Context 500 | p.rw.Lock() 501 | { 502 | // check if the bar is with writer 503 | if !p.property.Bytes { 504 | p.rw.Unlock() 505 | return 506 | } 507 | ctx = NewContext(p) 508 | ctx.Total = n // n bytes to receive 509 | // p.running++ 510 | } 511 | p.rw.Unlock() 512 | 513 | ctx.StartTime = time.Now() 514 | 515 | bw := NewBytesWriter() 516 | 517 | go func() { 518 | defer ctx.stop() 519 | ctx.Print() // print 0 520 | for { 521 | select { 522 | case add := <-bw.bytesChan: 523 | ctx.updateCurrentWithAdd(int64(add)) 524 | ctx.Print() 525 | if ctx.Current == ctx.Total { 526 | bw.close() 527 | return 528 | } 529 | case <-ctx.Interrupt: // interrupt got a signal or closed 530 | bw.close() 531 | return 532 | } 533 | } 534 | }() 535 | return bw, ctx.Interrupt 536 | } 537 | 538 | // Go WILL BLOCK, start the uncertain bar over the param function and render the bar until f finish. 539 | // This method will panic if the progress bar is not uncertain. 540 | func (p *ProgressBar) Go(f func()) { 541 | stop := p.Run(0) 542 | if stop == nil { 543 | panic("progress bar is not uncertain") 544 | } 545 | f() 546 | close(stop) 547 | } 548 | 549 | // Go WILL BLOCK, start an default uncertain bar over the param function and render the bar until f finish. 550 | // This method will panic if the progress bar is not uncertain. 551 | func Go(f func()) { 552 | pb := DefaultUncertainBar 553 | stop := pb.Run(0) 554 | if stop == nil { 555 | panic("progress bar is not uncertain") 556 | } 557 | f() 558 | close(stop) 559 | } 560 | 561 | // Iter WILL BLOCK, start an default progress bar over the param function and render the bar. 562 | // The bar will iterate n times and call f for each iteration. 563 | func Iter(n int, f func()) { 564 | if n <= 0 || f == nil { 565 | return 566 | } 567 | p := DefaultBar 568 | p.Iter(n, f) 569 | } 570 | -------------------------------------------------------------------------------- /pb/progress_bar.go.bak: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gngtwhh/gocui/cursor" 11 | "github.com/gngtwhh/gocui/font" 12 | "github.com/gngtwhh/gocui/utils" 13 | "github.com/gngtwhh/gocui/window" 14 | ) 15 | 16 | // DefaultBar is a pre-created default progress bar tokens 17 | var ( 18 | DefaultBar *ProgressBar 19 | DefaultBarFormat string 20 | DefaultProperty Property 21 | ) 22 | 23 | var ( 24 | DefaultUncertainBar *ProgressBar 25 | DefaultUncertainBarFormat string 26 | DefaultUncertainBarProperty Property 27 | ) 28 | 29 | func init() { 30 | var err error 31 | 32 | DefaultBarFormat = "%percent|%bar|%current/%total %elapsed %rate" 33 | DefaultProperty = Property{ 34 | Style: Style{ 35 | Complete: " ", 36 | Incomplete: " ", 37 | CompleteColor: font.WhiteBg, 38 | IncompleteColor: font.RESET, 39 | }, 40 | total: 100, 41 | } 42 | DefaultBar, err = NewProgressBar(DefaultBarFormat, WithDefault()) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | DefaultUncertainBarFormat = "[%bar]" 48 | DefaultUncertainBarProperty = Property{ 49 | Style: Style{ 50 | Incomplete: " ", 51 | UnCertain: " ", 52 | UnCertainColor: font.WhiteBg, 53 | }, 54 | } 55 | DefaultUncertainBar, err = NewProgressBar(DefaultUncertainBarFormat, WithProperty(DefaultUncertainBarProperty), WithUncertain()) 56 | if err != nil { 57 | panic(err) 58 | } 59 | } 60 | 61 | // Property is the default Property of the progress bar. 62 | // A running instance will be initialized with the progress bar's current Property. 63 | type Property struct { 64 | Style // Style of the "bar" token, including characters and color 65 | Format string // Format: The render format of the progress bar(with tokens). 66 | PosX, PosY int // Pos: The position of the progress bar on the screen 67 | BarWidth int // BarWidth: The render width of the token:"%bar" 68 | Width int // Width: The maximum width of the progress bar displayed on the terminal. 69 | 70 | total int64 // Total: Only available when Uncertain is false or Bytes is true 71 | 72 | Uncertain bool // Uncertain: Whether the progress bar is Uncertain, default: false 73 | Bytes bool // Type: Whether the progress bar is used for bytes writer, default: false 74 | BindPos bool // Whether bind the absolute pos, PosX and PosY are valid only when BindPos is true 75 | 76 | formatChanged bool // Indicates the change in format when updating property 77 | } 78 | 79 | // Style is the tokens struct in the Property struct, used to decorate the token "bar". 80 | type Style struct { 81 | Complete, CompleteHead, Incomplete, UnCertain string // The "bar" token style 82 | CompleteColor, CompleteHeadColor, IncompleteColor, UnCertainColor int // The color of the bar 83 | } 84 | 85 | // ProgressBar is a simple progress bar implementation. 86 | type ProgressBar struct { 87 | property Property 88 | tokens []token // Parsed tokens tokens, will not be updated 89 | barPos []int // index of "%bar" in tokens 90 | running int // The number of running instances 91 | rw sync.RWMutex // RWMutex to synchronize access to the progress bar 92 | } 93 | 94 | // context is a context created when the progress bar is running. 95 | // Each progress bar instance can create several contexts for reuse. 96 | type context struct { 97 | property Property // Copy of static progress bar property 98 | tokens []token // Copy of static progress bar tokens 99 | // barPos []int // Copy of static progress bar barPos 100 | current int64 // current progress 101 | windowWidth int // window width, set by window.GetConsoleSize() 102 | WidthWithoutBar int // accumulated render width without bar 103 | startTime time.Time // start time 104 | interrupt chan struct{} // interrupt channel to stop running 105 | // direction: for UnCertain bar to update, 1(default) for increasing, -1 for decreasing, only available when UnCertain is true 106 | direction int 107 | } 108 | 109 | // BytesWriter implements io.Writer interface, 110 | // used to receive data written to the bar and trigger render updates 111 | type BytesWriter struct { 112 | // bytesChan is the context pointer to which the BytesWriter belongs 113 | bytesChan chan int 114 | // closeCh indicate that the BytesWriter has been closed 115 | closeCh chan struct{} 116 | } 117 | 118 | // Runner holds the necessary information to run a progress bar. 119 | // For users to manually control the progress of the progress bar. 120 | type Runner struct { 121 | bar *ProgressBar 122 | ctx *context 123 | } 124 | 125 | // Update updates the progress bar's current value. 126 | func (r *Runner) Update(value int64) { 127 | value = min(max(0, value), r.ctx.property.total) 128 | // r.bar.updateCurrentWithAdd(ctx *context,3) 129 | r.bar.Print(r.ctx) 130 | } 131 | 132 | // Stop stops the progress bar running instance. 133 | func (r *Runner) Stop() { 134 | } 135 | 136 | func NewContext(p *ProgressBar) context { 137 | style := make([]token, len(p.tokens)) 138 | copy(style, p.tokens) 139 | 140 | ctx := context{ 141 | property: p.property, 142 | tokens: style, 143 | // barPos: DefaultBarPos, 144 | current: 0, 145 | startTime: time.Now(), 146 | interrupt: make(chan struct{}), 147 | direction: 1, 148 | } 149 | ctx.windowWidth, _ = window.GetConsoleSize() 150 | return ctx 151 | } 152 | 153 | func NewBytesWriter() *BytesWriter { 154 | return &BytesWriter{ 155 | bytesChan: make(chan int, 1), 156 | closeCh: make(chan struct{}), 157 | } 158 | } 159 | 160 | // Write implement io.Writer interface 161 | func (bw *BytesWriter) Write(b []byte) (n int, err error) { 162 | n = len(b) 163 | return n, bw.update(b) 164 | } 165 | 166 | // update send update current value to the channel to render next status 167 | func (bw *BytesWriter) update(b []byte) error { 168 | select { 169 | case <-bw.closeCh: 170 | return errors.New("channel closed") 171 | default: 172 | } 173 | bw.bytesChan <- len(b) 174 | return nil 175 | } 176 | 177 | // close stop the BytesWriter producer 178 | func (bw *BytesWriter) close() error { 179 | select { 180 | case <-bw.closeCh: 181 | return errors.New("the BytesWriter has been closed") 182 | default: 183 | } 184 | close(bw.closeCh) 185 | close(bw.bytesChan) 186 | return nil 187 | } 188 | 189 | // NewProgressBar creates a new progress bar that with the given tokens and total. 190 | func NewProgressBar(style string, mfs ...ModFunc) (pb *ProgressBar, err error) { 191 | if style == "" { 192 | return nil, fmt.Errorf("tokens cannot be empty") 193 | } 194 | 195 | property := Property{} 196 | for _, mf := range mfs { 197 | if mf == nil { 198 | return nil, fmt.Errorf("modify func cannot be nil") 199 | } 200 | mf(&property) 201 | } 202 | 203 | // revise the properties 204 | if property.total == 0 { 205 | property.total = 100 // default total is 100 206 | } 207 | if property.BarWidth < 0 { 208 | property.BarWidth = 0 // default 0 means full width 209 | } 210 | x, _ := window.GetConsoleSize() 211 | if property.Width <= 0 && property.Width > x { 212 | property.Width = x 213 | } 214 | if property.Style.Complete == "" { 215 | property.Style.Complete = "=" 216 | } 217 | // if property.Style.CompleteHead == "" { 218 | // property.Style.CompleteHead = ">" 219 | // } 220 | if property.Style.Incomplete == "" { 221 | property.Style.Incomplete = "-" 222 | } 223 | if property.Style.UnCertain == "" { 224 | property.Style.UnCertain = "<->" 225 | } 226 | if property.Style.CompleteColor == font.RESET { 227 | property.Style.CompleteColor = font.White 228 | } 229 | if property.Style.CompleteHeadColor == font.RESET { 230 | property.Style.CompleteHeadColor = font.White 231 | } 232 | if property.Style.IncompleteColor == font.RESET { 233 | property.Style.IncompleteColor = font.LightBlack 234 | } 235 | if property.Style.UnCertainColor == font.RESET { 236 | property.Style.UnCertainColor = font.White 237 | } 238 | // generate tokens tokens 239 | styleTokens, barPos := unmarshalToken(style) 240 | // create progress bar 241 | pb = &ProgressBar{ 242 | tokens: styleTokens, 243 | property: property, 244 | barPos: barPos, 245 | rw: sync.RWMutex{}, 246 | } 247 | return 248 | } 249 | 250 | // UpdateProperty updates Property of the progress bar. 251 | // If the bar has instances running, it will return an error and do nothing. 252 | func (p *ProgressBar) UpdateProperty(mfs ...ModFunc) (NewBar *ProgressBar, err error) { 253 | p.rw.Lock() 254 | defer p.rw.Unlock() 255 | 256 | property := &p.property 257 | for _, mf := range mfs { 258 | if mf == nil { 259 | return p, fmt.Errorf("modify func cannot be nil") 260 | } 261 | mf(property) 262 | } 263 | 264 | // revise the properties 265 | if property.total == 0 { 266 | property.total = 100 // default total is 100 267 | } 268 | if property.BarWidth < 0 { 269 | property.BarWidth = 0 // default 0 means full width 270 | } 271 | x, _ := window.GetConsoleSize() 272 | if property.Width <= 0 && property.Width > x { 273 | property.Width = x 274 | } 275 | if property.Style.Complete == "" { 276 | property.Style.Complete = "#" 277 | } 278 | if property.Style.Incomplete == "" { 279 | property.Style.Incomplete = "-" 280 | } 281 | if property.Style.UnCertain == "" { 282 | property.Style.UnCertain = "<->" 283 | } 284 | if property.Style.CompleteColor == font.RESET { 285 | property.Style.CompleteColor = font.White 286 | } 287 | if property.Style.IncompleteColor == font.RESET { 288 | property.Style.IncompleteColor = font.LightBlack 289 | } 290 | if property.Style.UnCertainColor == font.RESET { 291 | property.Style.UnCertainColor = font.White 292 | } 293 | // generate tokens tokens 294 | if property.formatChanged || property.Format != "" { 295 | property.formatChanged = false 296 | p.tokens, p.barPos = unmarshalToken(property.Format) 297 | } 298 | return p, nil 299 | } 300 | 301 | // updateCurrent increase the current progress without printing the progress bar. 302 | func (p *ProgressBar) updateCurrent(ctx *context) { 303 | if ctx.property.Uncertain { 304 | // ctx.current = min( 305 | // ctx.current+int64(ctx.direction), int64(ctx.property.BarWidth-len(ctx.property.Style.UnCertain)), 306 | // ) // UnCertain bar use width to update the current progress 307 | // ctx.current = max(ctx.current, 0) 308 | // if ctx.current == int64(ctx.property.BarWidth-len(ctx.property.Style.UnCertain)) || ctx.current == 0 { 309 | // ctx.direction = -ctx.direction 310 | // } 311 | ctx.current = ctx.current + int64(ctx.direction) 312 | } else { 313 | // common bar use total to update the current progress 314 | ctx.current = min(ctx.current+1, ctx.property.total) 315 | } 316 | } 317 | 318 | // updateCurrentWithAdd increase the current progress by add 319 | func (p *ProgressBar) updateCurrentWithAdd(ctx *context, add int) { 320 | if ctx.property.Uncertain { 321 | ctx.current = min( 322 | ctx.current+int64(ctx.direction*add), int64(ctx.property.BarWidth-len(ctx.property.Style.UnCertain)), 323 | ) // UnCertain bar use width to update the current progress 324 | ctx.current = max(ctx.current, 0) 325 | if ctx.current == int64(ctx.property.BarWidth-len(ctx.property.Style.UnCertain)) || ctx.current == 0 { 326 | ctx.direction = -ctx.direction 327 | } 328 | } else { 329 | ctx.current = min( 330 | ctx.current+int64(add), ctx.property.total, 331 | ) // common bar use total to update the current progress 332 | } 333 | } 334 | 335 | // Print prints the current progress of the progress bar. 336 | func (p *ProgressBar) Print(ctx *context) { 337 | var payloadBuilder0 strings.Builder 338 | payloadBuilder1 := strings.Builder{} 339 | var barToken *TokenBar 340 | for _, t := range ctx.tokens { 341 | if _, ok := t.(*TokenBar); ok && barToken == nil { 342 | barToken = t.(*TokenBar) 343 | payloadBuilder0 = payloadBuilder1 344 | payloadBuilder1 = strings.Builder{} 345 | } else { 346 | payloadBuilder1.WriteString(t.toString(ctx)) 347 | } 348 | } 349 | ctx.WidthWithoutBar = payloadBuilder0.Len() + payloadBuilder1.Len() 350 | barStr := barToken.toString(ctx) 351 | 352 | utils.ConsoleMutex.Lock() // Lock the cursor 353 | defer utils.ConsoleMutex.Unlock() 354 | { 355 | // window.ClearLine(-1) 356 | if ctx.property.BindPos { 357 | cursor.GotoXY(ctx.property.PosX, ctx.property.PosY) 358 | } else { 359 | fmt.Print("\r") 360 | } 361 | fmt.Print(payloadBuilder0.String()) 362 | fmt.Print(barStr) 363 | fmt.Print(payloadBuilder1.String()) 364 | window.ClearLineAfterCursor() 365 | } 366 | } 367 | 368 | // Stop stops the progress bar. 369 | func (p *ProgressBar) stop(ctx *context) { 370 | // ctx.interrupt <- struct{}{} 371 | p.rw.Lock() 372 | p.running-- 373 | p.rw.Unlock() 374 | } 375 | 376 | // iter starts a progress bar iteration, and returns two channels: 377 | // iter <-chan int: to be used to iterate over the progress bar; 378 | // stop chan<- struct{}: to stop the progress bar. 379 | // This method should not be used if the progress bar is uncertain, 380 | // otherwise, the returned iter channel will be closed, and the stop channel will be nil. 381 | func (p *ProgressBar) iter(n int) (iter <-chan int64, stop chan<- struct{}) { 382 | ch := make(chan int64) 383 | var ctx context 384 | p.rw.Lock() 385 | { 386 | // p.property.total = int64(n) 387 | if p.property.Uncertain { 388 | close(ch) // Uncertain bar cannot be iterated 389 | p.rw.Unlock() 390 | return ch, nil 391 | } 392 | ctx = NewContext(p) 393 | ctx.property.total = int64(n) 394 | p.running++ 395 | } 396 | p.rw.Unlock() 397 | 398 | ctx.startTime = time.Now() 399 | go func() { 400 | defer p.stop(&ctx) 401 | defer close(ch) 402 | for i := ctx.current; i <= ctx.property.total; i++ { 403 | p.Print(&ctx) 404 | select { 405 | case ch <- i: 406 | case <-ctx.interrupt: 407 | return 408 | } 409 | p.updateCurrent(&ctx) 410 | } 411 | }() 412 | return ch, ctx.interrupt 413 | } 414 | 415 | // Start starts an progress bar, and returns a *Runner instance to control the progress bar. 416 | // This method should not be called if the bar is uncertain. 417 | func (p *ProgressBar) Start(n int) (r *Runner, err error) { 418 | if p.property.Uncertain { 419 | return nil, errors.New("the bar is uncertain") 420 | } 421 | p.rw.Lock() 422 | p.running++ 423 | ctx := NewContext(p) 424 | p.rw.Unlock() 425 | ctx.property.total = int64(n) 426 | r = &Runner{ 427 | bar: p, 428 | ctx: &ctx, 429 | } 430 | return r, nil 431 | } 432 | 433 | // Iter WILL BLOCK, start an default progress bar over the param function and render the bar. 434 | // The bar will iterate n times and call f for each iteration. 435 | func (p *ProgressBar) Iter(n int, f func()) { 436 | if n <= 0 || f == nil { 437 | return 438 | } 439 | it, stop := p.iter(n) 440 | for range it { 441 | f() 442 | } 443 | close(stop) 444 | } 445 | 446 | // Run starts an uncertain progress bar, and returns a channel to stop the progress bar. 447 | // period is the time interval between each update, pass 0 to use the default period(100ms). 448 | // If the progress bar is not uncertain, the returned channel will be nil. 449 | func (p *ProgressBar) Run(period time.Duration) (stop chan<- struct{}) { 450 | var ctx context 451 | p.rw.Lock() 452 | { 453 | if !p.property.Uncertain { 454 | p.rw.Unlock() 455 | return nil 456 | } 457 | 458 | ctx = NewContext(p) 459 | if period == 0 { 460 | period = time.Millisecond * 100 // default period is 100ms 461 | } 462 | p.running++ 463 | } 464 | p.rw.Unlock() 465 | 466 | ctx.startTime = time.Now() 467 | ticker := time.NewTicker(period) 468 | 469 | go func() { 470 | defer p.stop(&ctx) 471 | for { 472 | select { 473 | case <-ticker.C: 474 | p.Print(&ctx) 475 | p.updateCurrent(&ctx) 476 | case <-ctx.interrupt: // interrupt got a signal or closed 477 | ticker.Stop() 478 | return 479 | } 480 | } 481 | }() 482 | return ctx.interrupt 483 | } 484 | 485 | // RunWithWriter automatically start a progress bar with writing bytes data. 486 | // param n: the total bytes to write. 487 | // It returns a writer for user to write data and a stop channel indicate exit. 488 | // This method should not be called if the progress bar is not with writer or is uncertain. 489 | func (p *ProgressBar) RunWithWriter(n int64) (writer *BytesWriter, stop chan<- struct{}) { 490 | if p.property.Uncertain { 491 | return 492 | } 493 | var ctx context 494 | p.rw.Lock() 495 | { 496 | // check if the bar is with writer 497 | if !p.property.Bytes { 498 | p.rw.Unlock() 499 | return 500 | } 501 | p.property.total = n // n bytes to receive 502 | ctx = NewContext(p) 503 | p.running++ 504 | } 505 | p.rw.Unlock() 506 | 507 | ctx.startTime = time.Now() 508 | 509 | bw := NewBytesWriter() 510 | 511 | go func() { 512 | defer p.stop(&ctx) 513 | p.Print(&ctx) // print 0 514 | for { 515 | select { 516 | case add := <-bw.bytesChan: 517 | p.updateCurrentWithAdd(&ctx, add) 518 | p.Print(&ctx) 519 | if ctx.current == ctx.property.total { 520 | bw.close() 521 | return 522 | } 523 | case <-ctx.interrupt: // interrupt got a signal or closed 524 | bw.close() 525 | return 526 | } 527 | } 528 | }() 529 | return bw, ctx.interrupt 530 | } 531 | 532 | // Go WILL BLOCK, start the uncertain bar over the param function and render the bar until f finish. 533 | // This method will panic if the progress bar is not uncertain. 534 | func (p *ProgressBar) Go(f func()) { 535 | stop := p.Run(0) 536 | if stop == nil { 537 | panic("progress bar is not uncertain") 538 | } 539 | f() 540 | close(stop) 541 | } 542 | 543 | // Go WILL BLOCK, start an default uncertain bar over the param function and render the bar until f finish. 544 | // This method will panic if the progress bar is not uncertain. 545 | func Go(f func()) { 546 | pb := DefaultUncertainBar 547 | stop := pb.Run(0) 548 | if stop == nil { 549 | panic("progress bar is not uncertain") 550 | } 551 | f() 552 | close(stop) 553 | } 554 | 555 | // Iter WILL BLOCK, start an default progress bar over the param function and render the bar. 556 | // The bar will iterate n times and call f for each iteration. 557 | func Iter(n int, f func()) { 558 | if n <= 0 || f == nil { 559 | return 560 | } 561 | p := DefaultBar 562 | p.Iter(n, f) 563 | } 564 | --------------------------------------------------------------------------------