Improve multiproduct_kati output

It now uses the same output style as ninja, overwriting status lines in
smart terminals.

Test: multiproduct_kati
Test: multiproduct_kati | cat
Change-Id: I8db5198ffdc5ebc5503241ac492379753d92978e
diff --git a/cmd/multiproduct_kati/main.go b/cmd/multiproduct_kati/main.go
index 3aa5a87..97d4cfa 100644
--- a/cmd/multiproduct_kati/main.go
+++ b/cmd/multiproduct_kati/main.go
@@ -56,6 +56,79 @@
 	config build.Config
 }
 
+type Status struct {
+	cur    int
+	total  int
+	failed int
+
+	ctx           build.Context
+	haveBlankLine bool
+	smartTerminal bool
+
+	lock sync.Mutex
+}
+
+func NewStatus(ctx build.Context) *Status {
+	return &Status{
+		ctx:           ctx,
+		haveBlankLine: true,
+		smartTerminal: ctx.IsTerminal(),
+	}
+}
+
+func (s *Status) SetTotal(total int) {
+	s.total = total
+}
+
+func (s *Status) Fail(product string, err error) {
+	s.Finish(product)
+
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	if s.smartTerminal && !s.haveBlankLine {
+		fmt.Fprintln(s.ctx.Stdout())
+		s.haveBlankLine = true
+	}
+
+	s.failed++
+	fmt.Fprintln(s.ctx.Stderr(), "FAILED:", product)
+	s.ctx.Verboseln("FAILED:", product)
+	s.ctx.Println(err)
+}
+
+func (s *Status) Finish(product string) {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	s.cur++
+	line := fmt.Sprintf("[%d/%d] %s", s.cur, s.total, product)
+
+	if s.smartTerminal {
+		if max, ok := s.ctx.TermWidth(); ok {
+			if len(line) > max {
+				line = line[:max]
+			}
+		}
+
+		fmt.Fprint(s.ctx.Stdout(), "\r", line, "\x1b[K")
+		s.haveBlankLine = false
+	} else {
+		s.ctx.Println(line)
+	}
+}
+
+func (s *Status) Finished() int {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	if !s.haveBlankLine {
+		fmt.Fprintln(s.ctx.Stdout())
+		s.haveBlankLine = true
+	}
+	return s.failed
+}
+
 func main() {
 	log := logger.New(os.Stderr)
 	defer log.Cleanup()
@@ -80,7 +153,7 @@
 		StdioInterface: build.StdioImpl{},
 	}}
 
-	failed := false
+	status := NewStatus(buildCtx)
 
 	config := build.NewConfig(buildCtx)
 	if *outDir == "" {
@@ -94,7 +167,7 @@
 
 		if !*keep {
 			defer func() {
-				if !failed {
+				if status.Finished() == 0 {
 					os.RemoveAll(*outDir)
 				}
 			}()
@@ -114,8 +187,9 @@
 	products := strings.Fields(vars["all_named_products"])
 	log.Verbose("Got product list:", products)
 
+	status.SetTotal(len(products))
+
 	var wg sync.WaitGroup
-	errs := make(chan error, len(products))
 	productConfigs := make(chan Product, len(products))
 
 	// Run the product config for every product in parallel
@@ -124,7 +198,7 @@
 		go func(product string) {
 			defer wg.Done()
 			defer logger.Recover(func(err error) {
-				errs <- fmt.Errorf("Error building %s: %v", product, err)
+				status.Fail(product, err)
 			})
 
 			productOutDir := filepath.Join(config.OutDir(), product)
@@ -171,7 +245,7 @@
 			for product := range productConfigs {
 				func() {
 					defer logger.Recover(func(err error) {
-						errs <- fmt.Errorf("Error building %s: %v", product.config.TargetProduct(), err)
+						status.Fail(product.config.TargetProduct(), err)
 					})
 
 					buildWhat := 0
@@ -185,22 +259,14 @@
 					if !*keep {
 						os.RemoveAll(product.config.OutDir())
 					}
-					log.Println("Finished running for", product.config.TargetProduct())
+					status.Finish(product.config.TargetProduct())
 				}()
 			}
 		}()
 	}
-	go func() {
-		wg2.Wait()
-		close(errs)
-	}()
+	wg2.Wait()
 
-	for err := range errs {
-		failed = true
-		log.Print(err)
-	}
-
-	if failed {
-		log.Fatalln("Failed")
+	if count := status.Finished(); count > 0 {
+		log.Fatalln(count, "products failed")
 	}
 }
diff --git a/ui/build/context.go b/ui/build/context.go
index f85bb6c..52a337d 100644
--- a/ui/build/context.go
+++ b/ui/build/context.go
@@ -102,3 +102,7 @@
 	}
 	return false
 }
+
+func (c ContextImpl) TermWidth() (int, bool) {
+	return termWidth(c.Stdout())
+}