├── go.mod ├── doc └── example.gif ├── Makefile ├── doc.go ├── writer_posix.go ├── example_test.go ├── terminal_size_windows.go ├── writer_test.go ├── .travis.yml ├── terminal_size.go ├── example └── main.go ├── LICENSE ├── README.md ├── writer_windows.go └── writer.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gosuri/uilive 2 | 3 | go 1.10 4 | -------------------------------------------------------------------------------- /doc/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosuri/uilive/HEAD/doc/example.gif -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @go test -race . 3 | 4 | examples: 5 | @go run -race ./example 6 | 7 | .PHONY: test examples 8 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package uilive provides a writer that live updates the terminal. It provides a buffered io.Writer that is flushed at a timed interval. 2 | package uilive 3 | -------------------------------------------------------------------------------- /writer_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package uilive 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // clear the line and move the cursor up 11 | var clear = fmt.Sprintf("%c[%dA%c[2K", ESC, 1, ESC) 12 | 13 | func (w *Writer) clearLines() { 14 | _, _ = fmt.Fprint(w.Out, strings.Repeat(clear, w.lineCount)) 15 | } 16 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package uilive_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gosuri/uilive" 8 | ) 9 | 10 | func Example() { 11 | writer := uilive.New() 12 | 13 | // start listening to updates and render 14 | writer.Start() 15 | 16 | for i := 0; i <= 100; i++ { 17 | fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100) 18 | time.Sleep(time.Millisecond * 5) 19 | } 20 | 21 | fmt.Fprintln(writer, "Finished: Downloaded 100GB") 22 | writer.Stop() // flush and stop rendering 23 | } 24 | -------------------------------------------------------------------------------- /terminal_size_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package uilive 4 | 5 | import ( 6 | "os" 7 | "unsafe" 8 | ) 9 | 10 | func getTermSize() (int, int) { 11 | out, err := os.Open("CONOUT$") 12 | if err != nil { 13 | return 0, 0 14 | } 15 | defer out.Close() 16 | 17 | var csbi consoleScreenBufferInfo 18 | ret, _, _ := procGetConsoleScreenBufferInfo.Call(out.Fd(), uintptr(unsafe.Pointer(&csbi))) 19 | if ret == 0 { 20 | return 0, 0 21 | } 22 | 23 | return int(csbi.window.right - csbi.window.left + 1), int(csbi.window.bottom - csbi.window.top + 1) 24 | } 25 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package uilive 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestWriter(t *testing.T) { 10 | w := New() 11 | b := &bytes.Buffer{} 12 | w.Out = b 13 | w.Start() 14 | for i := 0; i < 2; i++ { 15 | _, _ = fmt.Fprintln(w, "foo") 16 | } 17 | w.Stop() 18 | _, _ = fmt.Fprintln(b, "bar") 19 | 20 | want := "foo\nfoo\nbar\n" 21 | if b.String() != want { 22 | t.Fatalf("want %q, got %q", want, b.String()) 23 | } 24 | } 25 | 26 | func TestStartCalledTwice(t *testing.T) { 27 | w := New() 28 | b := &bytes.Buffer{} 29 | w.Out = b 30 | 31 | w.Start() 32 | w.Stop() 33 | w.Start() 34 | w.Stop() 35 | } 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.11.x 7 | os: linux 8 | - go: 1.12.x 9 | os: linux 10 | - go: 1.11.x 11 | os: linux 12 | env: CROSS_COMPILE=true 13 | - go: 1.12.x 14 | os: linux 15 | env: CROSS_COMPILE=true 16 | - go: 1.11.x 17 | os: osx 18 | - go: 1.12.x 19 | os: osx 20 | 21 | install: 22 | - if [ "$TRAVIS_OS_NAME" = "linux" -a "$CROSS_COMPILE" = "true" ]; then go get github.com/mattn/go-isatty ; fi 23 | - go get -t -v ./... 24 | 25 | script: 26 | - go build 27 | - go test 28 | - if [ "$TRAVIS_OS_NAME" = "linux" -a "$CROSS_COMPILE" = "true" ]; then env GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -v; fi 29 | -------------------------------------------------------------------------------- /terminal_size.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package uilive 4 | 5 | import ( 6 | "os" 7 | "runtime" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | type windowSize struct { 13 | rows uint16 14 | cols uint16 15 | } 16 | 17 | var out *os.File 18 | var err error 19 | var sz windowSize 20 | 21 | func getTermSize() (int, int) { 22 | if runtime.GOOS == "openbsd" { 23 | out, err = os.OpenFile("/dev/tty", os.O_RDWR, 0) 24 | if err != nil { 25 | return 0, 0 26 | } 27 | 28 | } else { 29 | out, err = os.OpenFile("/dev/tty", os.O_WRONLY, 0) 30 | if err != nil { 31 | return 0, 0 32 | } 33 | } 34 | _, _, _ = syscall.Syscall(syscall.SYS_IOCTL, 35 | out.Fd(), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz))) 36 | return int(sz.cols), int(sz.rows) 37 | } 38 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gosuri/uilive" 8 | ) 9 | 10 | func main() { 11 | writer := uilive.New() 12 | 13 | // start listening for updates and render 14 | writer.Start() 15 | 16 | for _, f := range [][]string{{"Foo.zip", "Bar.iso"}, {"Baz.tar.gz", "Qux.img"}} { 17 | for i := 0; i <= 50; i++ { 18 | _, _ = fmt.Fprintf(writer, "Downloading %s.. (%d/%d) GB\n", f[0], i, 50) 19 | _, _ = fmt.Fprintf(writer.Newline(), "Downloading %s.. (%d/%d) GB\n", f[1], i, 50) 20 | time.Sleep(time.Millisecond * 25) 21 | } 22 | _, _ = fmt.Fprintf(writer.Bypass(), "Downloaded %s\n", f[0]) 23 | _, _ = fmt.Fprintf(writer.Bypass(), "Downloaded %s\n", f[1]) 24 | } 25 | _, _ = fmt.Fprintln(writer, "Finished: Downloaded 150GB") 26 | writer.Stop() // flush and stop rendering 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (c) 2015, Greg Osuri 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uilive [![GoDoc](https://godoc.org/github.com/gosuri/uilive?status.svg)](https://godoc.org/github.com/gosuri/uilive) [![Build Status](https://travis-ci.org/gosuri/uilive.svg?branch=master)](https://travis-ci.org/gosuri/uilive) 2 | 3 | uilive is a go library for updating terminal output in realtime. It provides a buffered [io.Writer](https://golang.org/pkg/io/#Writer) that is flushed at a timed interval. uilive powers [uiprogress](https://github.com/gosuri/uiprogress). 4 | 5 | ## Usage Example 6 | 7 | Calling `uilive.New()` will create a new writer. To start rendering, simply call `writer.Start()` and update the ui by writing to the `writer`. Full source for the below example is in [example/main.go](example/main.go). 8 | 9 | ```go 10 | writer := uilive.New() 11 | // start listening for updates and render 12 | writer.Start() 13 | 14 | for i := 0; i <= 100; i++ { 15 | fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100) 16 | time.Sleep(time.Millisecond * 5) 17 | } 18 | 19 | fmt.Fprintln(writer, "Finished: Downloaded 100GB") 20 | writer.Stop() // flush and stop rendering 21 | ``` 22 | 23 | The above will render 24 | 25 | ![example](doc/example.gif) 26 | 27 | ## Installation 28 | 29 | ```sh 30 | $ go get -v github.com/gosuri/uilive 31 | ``` 32 | -------------------------------------------------------------------------------- /writer_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package uilive 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | "syscall" 9 | "unsafe" 10 | "github.com/mattn/go-isatty" 11 | ) 12 | 13 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 14 | 15 | var ( 16 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 17 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 18 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") 19 | ) 20 | 21 | // clear the line and move the cursor up 22 | var clear = fmt.Sprintf("%c[%dA%c[2K\r", ESC, 0, ESC) 23 | 24 | type short int16 25 | type dword uint32 26 | type word uint16 27 | 28 | type coord struct { 29 | x short 30 | y short 31 | } 32 | 33 | type smallRect struct { 34 | left short 35 | top short 36 | right short 37 | bottom short 38 | } 39 | 40 | type consoleScreenBufferInfo struct { 41 | size coord 42 | cursorPosition coord 43 | attributes word 44 | window smallRect 45 | maximumWindowSize coord 46 | } 47 | 48 | func (w *Writer) clearLines() { 49 | f, ok := w.Out.(FdWriter) 50 | if ok && !isatty.IsTerminal(f.Fd()) { 51 | ok = false 52 | } 53 | if !ok { 54 | _, _ = fmt.Fprint(w.Out, strings.Repeat(clear, w.lineCount)) 55 | return 56 | } 57 | fd := f.Fd() 58 | var csbi consoleScreenBufferInfo 59 | _, _, _ = procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&csbi))) 60 | 61 | for i := 0; i < w.lineCount; i++ { 62 | // move the cursor up 63 | csbi.cursorPosition.y-- 64 | _, _, _ = procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&csbi.cursorPosition)))) 65 | // clear the line 66 | cursor := coord{ 67 | x: csbi.window.left, 68 | y: csbi.window.top + csbi.cursorPosition.y, 69 | } 70 | var count, w dword 71 | count = dword(csbi.size.x) 72 | _, _, _ = procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w))) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package uilive 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // ESC is the ASCII code for escape character 13 | const ESC = 27 14 | 15 | // RefreshInterval is the default refresh interval to update the ui 16 | var RefreshInterval = time.Millisecond 17 | 18 | var overFlowHandled bool 19 | 20 | var termWidth int 21 | 22 | // Out is the default output writer for the Writer 23 | var Out = io.Writer(os.Stdout) 24 | 25 | // ErrClosedPipe is the error returned when trying to writer is not listening 26 | var ErrClosedPipe = errors.New("uilive: read/write on closed pipe") 27 | 28 | // FdWriter is a writer with a file descriptor. 29 | type FdWriter interface { 30 | io.Writer 31 | Fd() uintptr 32 | } 33 | 34 | // Writer is a buffered the writer that updates the terminal. The contents of writer will be flushed on a timed interval or when Flush is called. 35 | type Writer struct { 36 | // Out is the writer to write to 37 | Out io.Writer 38 | 39 | // RefreshInterval is the time the UI sould refresh 40 | RefreshInterval time.Duration 41 | 42 | ticker *time.Ticker 43 | tdone chan bool 44 | 45 | buf bytes.Buffer 46 | mtx *sync.Mutex 47 | lineCount int 48 | } 49 | 50 | type bypass struct { 51 | writer *Writer 52 | } 53 | 54 | type newline struct { 55 | writer *Writer 56 | } 57 | 58 | // New returns a new Writer with defaults 59 | func New() *Writer { 60 | termWidth, _ = getTermSize() 61 | if termWidth != 0 { 62 | overFlowHandled = true 63 | } 64 | 65 | return &Writer{ 66 | Out: Out, 67 | RefreshInterval: RefreshInterval, 68 | 69 | mtx: &sync.Mutex{}, 70 | } 71 | } 72 | 73 | // Flush writes to the out and resets the buffer. It should be called after the last call to Write to ensure that any data buffered in the Writer is written to output. 74 | // Any incomplete escape sequence at the end is considered complete for formatting purposes. 75 | // An error is returned if the contents of the buffer cannot be written to the underlying output stream 76 | func (w *Writer) Flush() error { 77 | w.mtx.Lock() 78 | defer w.mtx.Unlock() 79 | 80 | // do nothing if buffer is empty 81 | if len(w.buf.Bytes()) == 0 { 82 | return nil 83 | } 84 | w.clearLines() 85 | 86 | lines := 0 87 | var currentLine bytes.Buffer 88 | for _, b := range w.buf.Bytes() { 89 | if b == '\n' { 90 | lines++ 91 | currentLine.Reset() 92 | } else { 93 | currentLine.Write([]byte{b}) 94 | if overFlowHandled && currentLine.Len() > termWidth { 95 | lines++ 96 | currentLine.Reset() 97 | } 98 | } 99 | } 100 | w.lineCount = lines 101 | _, err := w.Out.Write(w.buf.Bytes()) 102 | w.buf.Reset() 103 | return err 104 | } 105 | 106 | // Start starts the listener in a non-blocking manner 107 | func (w *Writer) Start() { 108 | if w.ticker == nil { 109 | w.ticker = time.NewTicker(w.RefreshInterval) 110 | w.tdone = make(chan bool) 111 | } 112 | 113 | go w.Listen() 114 | } 115 | 116 | // Stop stops the listener that updates the terminal 117 | func (w *Writer) Stop() { 118 | w.Flush() 119 | w.tdone <- true 120 | <-w.tdone 121 | } 122 | 123 | // Listen listens for updates to the writer's buffer and flushes to the out provided. It blocks the runtime. 124 | func (w *Writer) Listen() { 125 | for { 126 | select { 127 | case <-w.ticker.C: 128 | if w.ticker != nil { 129 | _ = w.Flush() 130 | } 131 | case <-w.tdone: 132 | w.mtx.Lock() 133 | w.ticker.Stop() 134 | w.ticker = nil 135 | w.mtx.Unlock() 136 | close(w.tdone) 137 | return 138 | } 139 | } 140 | } 141 | 142 | // Write save the contents of buf to the writer b. The only errors returned are ones encountered while writing to the underlying buffer. 143 | func (w *Writer) Write(buf []byte) (n int, err error) { 144 | w.mtx.Lock() 145 | defer w.mtx.Unlock() 146 | return w.buf.Write(buf) 147 | } 148 | 149 | // Bypass creates an io.Writer which allows non-buffered output to be written to the underlying output 150 | func (w *Writer) Bypass() io.Writer { 151 | return &bypass{writer: w} 152 | } 153 | 154 | func (b *bypass) Write(p []byte) (int, error) { 155 | b.writer.mtx.Lock() 156 | defer b.writer.mtx.Unlock() 157 | 158 | b.writer.clearLines() 159 | b.writer.lineCount = 0 160 | return b.writer.Out.Write(p) 161 | } 162 | 163 | // Newline creates an io.Writer which allows buffered output to be written to the underlying output. This enable writing 164 | // to multiple lines at once. 165 | func (w *Writer) Newline() io.Writer { 166 | return &newline{writer: w} 167 | } 168 | 169 | func (n *newline) Write(p []byte) (int, error) { 170 | n.writer.mtx.Lock() 171 | defer n.writer.mtx.Unlock() 172 | return n.writer.buf.Write(p) 173 | } --------------------------------------------------------------------------------