// Copyright 2016-2018, Pulumi Corporation. // // 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 display // forked from: https://github.com/moby/moby/blob/master/pkg/jsonmessage/jsonmessage.go // so we can customize parts of the display of our progress messages import ( "fmt" "io" "os" gotty "github.com/ijc/Gotty" "github.com/pulumi/pulumi/sdk/v2/go/common/util/contract" ) /* Satisfied by gotty.TermInfo as well as noTermInfo from below */ type termInfo interface { Parse(attr string, params ...interface{}) (string, error) } type noTermInfo struct{} // canary used when no terminfo. func (ti *noTermInfo) Parse(attr string, params ...interface{}) (string, error) { return "", fmt.Errorf("noTermInfo") } func clearLine(out io.Writer, ti termInfo) { // el2 (clear whole line) is not exposed by terminfo. // First clear line from beginning to cursor if attr, err := ti.Parse("el1"); err == nil { fmt.Fprintf(out, "%s", attr) } else { fmt.Fprintf(out, "\x1b[1K") } // Then clear line from cursor to end if attr, err := ti.Parse("el"); err == nil { fmt.Fprintf(out, "%s", attr) } else { fmt.Fprintf(out, "\x1b[K") } } func cursorUp(out io.Writer, ti termInfo, l int) { if l == 0 { // Should never be the case, but be tolerant return } if attr, err := ti.Parse("cuu", l); err == nil { fmt.Fprintf(out, "%s", attr) } else { fmt.Fprintf(out, "\x1b[%dA", l) } } func cursorDown(out io.Writer, ti termInfo, l int) { if l == 0 { // Should never be the case, but be tolerant return } if attr, err := ti.Parse("cud", l); err == nil { fmt.Fprintf(out, "%s", attr) } else { fmt.Fprintf(out, "\x1b[%dB", l) } } // Display displays the Progress to `out`. `termInfo` is non-nil if `out` is a terminal. func (jm *Progress) Display(out io.Writer, termInfo termInfo) { var endl string if termInfo != nil && /*jm.Stream == "" &&*/ jm.Action != "" { clearLine(out, termInfo) endl = "\r" fmt.Fprint(out, endl) } if jm.Action != "" && termInfo != nil { fmt.Fprintf(out, "%s%s", jm.Action, endl) } else { var msg string if jm.Action != "" { msg = jm.Action } else { msg = jm.Message } fmt.Fprintf(out, "%s%s\n", msg, endl) } } // ShowProgressOutput displays a progress stream from `in` to `out`, `isTerminal` describes if // `out` is a terminal. If this is the case, it will print `\n` at the end of each line and move the // cursor while displaying. func ShowProgressOutput(in <-chan Progress, out io.Writer, isTerminal bool) { var ( ids = make(map[string]int) ) var info termInfo if isTerminal { term := os.Getenv("TERM") if term == "" { term = "vt102" } var err error if info, err = gotty.OpenTermInfo(term); err != nil { info = &noTermInfo{} } } for jm := range in { diff := 0 if jm.Action != "" { if jm.ID == "" { contract.Failf("Must have an ID if we have an action! %s", jm.Action) } line, ok := ids[jm.ID] if !ok { // NOTE: This approach of using len(id) to // figure out the number of lines of history // only works as long as we clear the history // when we output something that's not // accounted for in the map, such as a line // with no ID. line = len(ids) ids[jm.ID] = line if info != nil { fmt.Fprintf(out, "\n") } } diff = len(ids) - line if info != nil { cursorUp(out, info, diff) } } else { // When outputting something that isn't progress // output, clear the history of previous lines. We // don't want progress entries from some previous // operation to be updated (for example, pull -a // with multiple tags). ids = make(map[string]int) } jm.Display(out, info) if jm.Action != "" && info != nil { cursorDown(out, info, diff) } } }