// Copyright 2019 Google Inc. All rights reserved. // // 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 terminal import ( "fmt" "strings" "time" "android/soong/ui/status" ) type formatter struct { colorize bool format string quiet bool start time.Time } // newFormatter returns a formatter for formatting output to // the terminal in a format similar to Ninja. // format takes nearly all the same options as NINJA_STATUS. // %c is currently unsupported. func newFormatter(colorize bool, format string, quiet bool) formatter { return formatter{ colorize: colorize, format: format, quiet: quiet, start: time.Now(), } } func (s formatter) message(level status.MsgLevel, message string) string { if level >= status.ErrorLvl { return fmt.Sprintf("%s %s", s.failedString(), message) } else if level > status.StatusLvl { return fmt.Sprintf("%s%s", level.Prefix(), message) } else if level == status.StatusLvl { return message } return "" } func remainingTimeString(t time.Time) string { now := time.Now() if t.After(now) { return t.Sub(now).Round(time.Duration(time.Second)).String() } return time.Duration(0).Round(time.Duration(time.Second)).String() } func (s formatter) progress(counts status.Counts) string { if s.format == "" { output := fmt.Sprintf("[%3d%% %d/%d", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions) if !counts.EstimatedTime.IsZero() { output += fmt.Sprintf(" %s remaining", remainingTimeString(counts.EstimatedTime)) } output += "] " return output } buf := &strings.Builder{} for i := 0; i < len(s.format); i++ { c := s.format[i] if c != '%' { buf.WriteByte(c) continue } i = i + 1 if i == len(s.format) { buf.WriteByte(c) break } c = s.format[i] switch c { case '%': buf.WriteByte(c) case 's': fmt.Fprintf(buf, "%d", counts.StartedActions) case 't': fmt.Fprintf(buf, "%d", counts.TotalActions) case 'r': fmt.Fprintf(buf, "%d", counts.RunningActions) case 'u': fmt.Fprintf(buf, "%d", counts.TotalActions-counts.StartedActions) case 'f': fmt.Fprintf(buf, "%d", counts.FinishedActions) case 'o': fmt.Fprintf(buf, "%.1f", float64(counts.FinishedActions)/time.Since(s.start).Seconds()) case 'c': // TODO: implement? buf.WriteRune('?') case 'p': fmt.Fprintf(buf, "%3d%%", 100*counts.FinishedActions/counts.TotalActions) case 'e': fmt.Fprintf(buf, "%.3f", time.Since(s.start).Seconds()) case 'l': if counts.EstimatedTime.IsZero() { // No esitimated data buf.WriteRune('?') } else { fmt.Fprintf(buf, "%s", remainingTimeString(counts.EstimatedTime)) } default: buf.WriteString("unknown placeholder '") buf.WriteByte(c) buf.WriteString("'") } } return buf.String() } func (s formatter) result(result status.ActionResult) string { var ret string if result.Error != nil { targets := strings.Join(result.Outputs, " ") if s.quiet || result.Command == "" { ret = fmt.Sprintf("%s %s\n%s", s.failedString(), targets, result.Output) } else { ret = fmt.Sprintf("%s %s\n%s\n%s", s.failedString(), targets, result.Command, result.Output) } } else if result.Output != "" { ret = result.Output } if len(ret) > 0 && ret[len(ret)-1] != '\n' { ret += "\n" } return ret } func (s formatter) failedString() string { failed := "FAILED:" if s.colorize { failed = ansi.red() + ansi.bold() + failed + ansi.regular() } return failed }