258 lines
5.3 KiB
Go
258 lines
5.3 KiB
Go
// Copyright 2019 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/vbauerster/mpb/v8/cwriter"
|
|
|
|
"github.com/dolthub/dolt/go/libraries/utils/iohelp"
|
|
)
|
|
|
|
var colorOutput = color.Output
|
|
var CliOut = colorOutput
|
|
var CliErr = color.Error
|
|
|
|
var outputClosed uint64
|
|
|
|
var InStream io.ReadCloser = os.Stdin
|
|
var OutStream io.WriteCloser = os.Stdout
|
|
|
|
var ExecuteWithStdioRestored func(userFunc func())
|
|
|
|
func CloseOutput() {
|
|
if atomic.CompareAndSwapUint64(&outputClosed, 0, 1) {
|
|
fmt.Fprintln(CliOut)
|
|
}
|
|
}
|
|
|
|
func SetIOStreams(inStream io.ReadCloser, outStream io.WriteCloser) {
|
|
InStream = inStream
|
|
OutStream = outStream
|
|
}
|
|
|
|
func InitIO() (restoreIO func()) {
|
|
stdOut, stdErr := os.Stdout, os.Stderr
|
|
|
|
f, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
|
|
if err == nil {
|
|
os.Stdout = f
|
|
os.Stderr = f
|
|
SetIOStreams(os.Stdin, iohelp.NopWrCloser(CliOut))
|
|
}
|
|
|
|
restoreIO = func() {
|
|
if f != nil {
|
|
f.Close()
|
|
}
|
|
|
|
os.Stdout = stdOut
|
|
os.Stderr = stdErr
|
|
SetIOStreams(os.Stdin, os.Stdout)
|
|
}
|
|
|
|
ExecuteWithStdioRestored = func(userFunc func()) {
|
|
initialNoColor := color.NoColor
|
|
color.NoColor = true
|
|
os.Stdout = stdOut
|
|
os.Stderr = stdErr
|
|
SetIOStreams(os.Stdin, os.Stdout)
|
|
|
|
userFunc()
|
|
|
|
os.Stdout = f
|
|
os.Stderr = f
|
|
color.NoColor = initialNoColor
|
|
SetIOStreams(os.Stdin, iohelp.NopWrCloser(CliOut))
|
|
}
|
|
|
|
return restoreIO
|
|
}
|
|
|
|
func Println(a ...interface{}) {
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
fmt.Fprintln(CliOut, a...)
|
|
}
|
|
|
|
func Print(a ...interface{}) {
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
fmt.Fprint(CliOut, a...)
|
|
}
|
|
|
|
func Printf(format string, a ...interface{}) {
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(CliOut, format, a...)
|
|
}
|
|
|
|
func PrintErrln(a ...interface{}) {
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
fmt.Fprintln(CliErr, a...)
|
|
}
|
|
|
|
func PrintErr(a ...interface{}) {
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
fmt.Fprint(CliErr, a...)
|
|
}
|
|
|
|
func PrintErrf(format string, a ...interface{}) {
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(CliErr, format, a...)
|
|
}
|
|
|
|
// DeleteAndPrint prints a new message and deletes the old one given the
|
|
// previous messages length. It returns the length of the printed message that
|
|
// should be passed as prevMsgLen on the next call of DeleteAndPrint.
|
|
//
|
|
// DeleteAndPrint does not work for multiline messages.
|
|
func DeleteAndPrint(prevMsgLen int, msg string) int {
|
|
if outputIsClosed() {
|
|
return 0
|
|
}
|
|
|
|
msgLen := len(msg)
|
|
backspacesAndMsg := make([]byte, prevMsgLen+msgLen, 2*prevMsgLen+msgLen)
|
|
for i := 0; i < prevMsgLen; i++ {
|
|
backspacesAndMsg[i] = '\b'
|
|
}
|
|
|
|
for i, c := range []byte(msg) {
|
|
backspacesAndMsg[i+prevMsgLen] = c
|
|
}
|
|
|
|
diff := prevMsgLen - msgLen
|
|
|
|
if diff > 0 {
|
|
for i := 0; i < diff; i++ {
|
|
backspacesAndMsg = append(backspacesAndMsg, ' ')
|
|
}
|
|
|
|
for i := 0; i < diff; i++ {
|
|
backspacesAndMsg = append(backspacesAndMsg, '\b')
|
|
}
|
|
}
|
|
|
|
PrintErr(string(backspacesAndMsg))
|
|
return msgLen
|
|
}
|
|
|
|
// EphemeralPrinter is tool than you can use to print temporary line(s) to the
|
|
// console. Each time Display is called, the output is reset, and you can begin
|
|
// writing anew.
|
|
type EphemeralPrinter struct {
|
|
w *cwriter.Writer
|
|
mu sync.Mutex // protects the following
|
|
lines int
|
|
atLineStart bool
|
|
}
|
|
|
|
// NewEphemeralPrinter creates a new EphemeralPrinter.
|
|
func NewEphemeralPrinter() *EphemeralPrinter {
|
|
var out io.Writer
|
|
// Bypass the colored writer.
|
|
if CliOut == colorOutput {
|
|
if ExecuteWithStdioRestored != nil {
|
|
ExecuteWithStdioRestored(func() {
|
|
out = os.Stdout
|
|
})
|
|
} else {
|
|
out = os.Stdout
|
|
}
|
|
} else {
|
|
out = CliOut
|
|
}
|
|
w := cwriter.New(out)
|
|
e := &EphemeralPrinter{w: w, mu: sync.Mutex{}, atLineStart: true}
|
|
return e
|
|
}
|
|
|
|
// Printf formats and prints a string to the printer. If no newline character is
|
|
// provided, one will be added to ensure that the output can be cleared in the
|
|
// future. You can print multiple lines.
|
|
func (e *EphemeralPrinter) Printf(format string, a ...interface{}) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
str := fmt.Sprintf(format, a...)
|
|
lines := strings.Split(str, "\n")
|
|
for i, line := range lines {
|
|
if i != 0 {
|
|
_, _ = e.w.Write([]byte("\n"))
|
|
e.lines++
|
|
e.atLineStart = true
|
|
}
|
|
|
|
_, _ = e.w.Write([]byte(line))
|
|
if len(line) > 0 {
|
|
e.atLineStart = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display clears the previous output and displays the new text.
|
|
func (e *EphemeralPrinter) Display() {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if outputIsClosed() {
|
|
return
|
|
}
|
|
|
|
if !e.atLineStart {
|
|
// Cursor needs to be at the start of a line. This will ensure that the
|
|
// output is cleared properly.
|
|
_, _ = e.w.Write([]byte("\n"))
|
|
e.lines++
|
|
e.atLineStart = true
|
|
}
|
|
|
|
_ = e.w.Flush(e.lines)
|
|
e.lines = 0
|
|
}
|
|
|
|
func outputIsClosed() bool {
|
|
isClosed := atomic.LoadUint64(&outputClosed)
|
|
return isClosed == 1
|
|
}
|