Merge "Fix typos in a comment."
diff --git a/android/module.go b/android/module.go
index 03993e5..87e2ca7 100644
--- a/android/module.go
+++ b/android/module.go
@@ -966,10 +966,12 @@
 
 func (m *moduleContext) ninjaError(params BuildParams, err error) (PackageContext, BuildParams) {
 	return pctx, BuildParams{
-		Rule:        ErrorRule,
-		Description: params.Description,
-		Output:      params.Output,
-		Outputs:     params.Outputs,
+		Rule:            ErrorRule,
+		Description:     params.Description,
+		Output:          params.Output,
+		Outputs:         params.Outputs,
+		ImplicitOutput:  params.ImplicitOutput,
+		ImplicitOutputs: params.ImplicitOutputs,
 		Args: map[string]string{
 			"error": err.Error(),
 		},
diff --git a/android/neverallow.go b/android/neverallow.go
index ee3bf4a..ecff62d 100644
--- a/android/neverallow.go
+++ b/android/neverallow.go
@@ -52,6 +52,7 @@
 	rules = append(rules, createTrebleRules()...)
 	rules = append(rules, createLibcoreRules()...)
 	rules = append(rules, createJavaDeviceForHostRules()...)
+	rules = append(rules, createJavaLibraryHostRules()...)
 	return rules
 }
 
@@ -127,6 +128,15 @@
 	}
 }
 
+func createJavaLibraryHostRules() []*rule {
+	return []*rule{
+		neverallow().
+			moduleType("java_library_host").
+			with("no_standard_libs", "true").
+			because("no_standard_libs makes no sense with java_library_host"),
+	}
+}
+
 func neverallowMutator(ctx BottomUpMutatorContext) {
 	m, ok := ctx.Module().(Module)
 	if !ok {
diff --git a/android/neverallow_test.go b/android/neverallow_test.go
index 40ccf14..9c43d53 100644
--- a/android/neverallow_test.go
+++ b/android/neverallow_test.go
@@ -178,6 +178,18 @@
 				}`),
 		},
 	},
+	// java_library_host rule tests
+	{
+		name: "java_library_host with no_standard_libs: true",
+		fs: map[string][]byte{
+			"libcore/Blueprints": []byte(`
+				java_library_host {
+					name: "inside_core_libraries",
+					no_standard_libs: true,
+				}`),
+		},
+		expectedError: "module \"inside_core_libraries\": violates neverallow",
+	},
 }
 
 func TestNeverallow(t *testing.T) {
@@ -200,6 +212,7 @@
 	ctx := NewTestContext()
 	ctx.RegisterModuleType("cc_library", ModuleFactoryAdaptor(newMockCcLibraryModule))
 	ctx.RegisterModuleType("java_library", ModuleFactoryAdaptor(newMockJavaLibraryModule))
+	ctx.RegisterModuleType("java_library_host", ModuleFactoryAdaptor(newMockJavaLibraryModule))
 	ctx.RegisterModuleType("java_device_for_host", ModuleFactoryAdaptor(newMockJavaLibraryModule))
 	ctx.PostDepsMutators(registerNeverallowMutator)
 	ctx.Register()
diff --git a/apex/apex.go b/apex/apex.go
index 11b433c..84e5497 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -61,7 +61,7 @@
 			`--key ${key} ${opt_flags} ${image_dir} ${out} `,
 		CommandDeps: []string{"${apexer}", "${avbtool}", "${e2fsdroid}", "${merge_zips}",
 			"${mke2fs}", "${resize2fs}", "${sefcontext_compile}",
-			"${soong_zip}", "${zipalign}", "${aapt2}"},
+			"${soong_zip}", "${zipalign}", "${aapt2}", "prebuilts/sdk/current/public/android.jar"},
 		Description: "APEX ${image_dir} => ${out}",
 	}, "tool_path", "image_dir", "copy_commands", "manifest", "file_contexts", "canned_fs_config", "key", "opt_flags")
 
@@ -1062,6 +1062,10 @@
 		Description: "signapk",
 		Output:      a.outputFiles[apexType],
 		Input:       unsignedOutputFile,
+		Implicits: []android.Path{
+			a.container_certificate_file,
+			a.container_private_key_file,
+		},
 		Args: map[string]string{
 			"certificates": a.container_certificate_file.String() + " " + a.container_private_key_file.String(),
 			"flags":        "-a 4096", //alignment
diff --git a/cc/builder.go b/cc/builder.go
index 3a8afc0..1e12361 100644
--- a/cc/builder.go
+++ b/cc/builder.go
@@ -22,7 +22,6 @@
 	"fmt"
 	"path/filepath"
 	"runtime"
-	"strconv"
 	"strings"
 
 	"github.com/google/blueprint"
@@ -93,20 +92,6 @@
 		},
 		"arCmd", "arFlags")
 
-	darwinAr = pctx.AndroidStaticRule("darwinAr",
-		blueprint.RuleParams{
-			Command:     "rm -f ${out} && ${config.MacArPath} $arFlags $out $in",
-			CommandDeps: []string{"${config.MacArPath}"},
-		},
-		"arFlags")
-
-	darwinAppendAr = pctx.AndroidStaticRule("darwinAppendAr",
-		blueprint.RuleParams{
-			Command:     "cp -f ${inAr} ${out}.tmp && ${config.MacArPath} $arFlags ${out}.tmp $in && mv ${out}.tmp ${out}",
-			CommandDeps: []string{"${config.MacArPath}", "${inAr}"},
-		},
-		"arFlags", "inAr")
-
 	darwinStrip = pctx.AndroidStaticRule("darwinStrip",
 		blueprint.RuleParams{
 			Command:     "${config.MacStripPath} -u -r -o $out $in",
@@ -515,11 +500,6 @@
 func TransformObjToStaticLib(ctx android.ModuleContext, objFiles android.Paths,
 	flags builderFlags, outputFile android.ModuleOutPath, deps android.Paths) {
 
-	if ctx.Darwin() {
-		transformDarwinObjToStaticLib(ctx, objFiles, flags, outputFile, deps)
-		return
-	}
-
 	arCmd := "${config.ClangBin}/llvm-ar"
 	arFlags := "crsD"
 	if !ctx.Darwin() {
@@ -542,82 +522,6 @@
 	})
 }
 
-// Generate a rule for compiling multiple .o files to a static library (.a) on
-// darwin.  The darwin ar tool doesn't support @file for list files, and has a
-// very small command line length limit, so we have to split the ar into multiple
-// steps, each appending to the previous one.
-func transformDarwinObjToStaticLib(ctx android.ModuleContext, objFiles android.Paths,
-	flags builderFlags, outputFile android.ModuleOutPath, deps android.Paths) {
-
-	arFlags := "cqs"
-
-	if len(objFiles) == 0 {
-		dummy := android.PathForModuleOut(ctx, "dummy"+objectExtension)
-		dummyAr := android.PathForModuleOut(ctx, "dummy"+staticLibraryExtension)
-
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        emptyFile,
-			Description: "empty object file",
-			Output:      dummy,
-			Implicits:   deps,
-		})
-
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        darwinAr,
-			Description: "empty static archive",
-			Output:      dummyAr,
-			Input:       dummy,
-			Args: map[string]string{
-				"arFlags": arFlags,
-			},
-		})
-
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        darwinAppendAr,
-			Description: "static link " + outputFile.Base(),
-			Output:      outputFile,
-			Input:       dummy,
-			Args: map[string]string{
-				"arFlags": "d",
-				"inAr":    dummyAr.String(),
-			},
-		})
-
-		return
-	}
-
-	// ARG_MAX on darwin is 262144, use half that to be safe
-	objFilesLists, err := splitListForSize(objFiles, 131072)
-	if err != nil {
-		ctx.ModuleErrorf("%s", err.Error())
-	}
-
-	var in, out android.WritablePath
-	for i, l := range objFilesLists {
-		in = out
-		out = outputFile
-		if i != len(objFilesLists)-1 {
-			out = android.PathForModuleOut(ctx, outputFile.Base()+strconv.Itoa(i))
-		}
-
-		build := android.BuildParams{
-			Rule:        darwinAr,
-			Description: "static link " + out.Base(),
-			Output:      out,
-			Inputs:      l,
-			Implicits:   deps,
-			Args: map[string]string{
-				"arFlags": arFlags,
-			},
-		}
-		if i != 0 {
-			build.Rule = darwinAppendAr
-			build.Args["inAr"] = in.String()
-		}
-		ctx.Build(pctx, build)
-	}
-}
-
 // Generate a rule for compiling multiple .o files, plus static libraries, whole static libraries,
 // and shared libraries, to a shared library (.so) or dynamic executable
 func TransformObjToDynamicBinary(ctx android.ModuleContext,
diff --git a/cc/rs.go b/cc/rs.go
index 5421b92..fbc6bfb 100644
--- a/cc/rs.go
+++ b/cc/rs.go
@@ -72,11 +72,12 @@
 	stampFile := android.PathForModuleGen(ctx, "rs", "rs.stamp")
 	depFiles := make(android.WritablePaths, 0, len(rsFiles))
 	genFiles := make(android.WritablePaths, 0, 2*len(rsFiles))
+	headers := make(android.Paths, 0, len(rsFiles))
 	for _, rsFile := range rsFiles {
 		depFiles = append(depFiles, rsGeneratedDepFile(ctx, rsFile))
-		genFiles = append(genFiles,
-			rsGeneratedCppFile(ctx, rsFile),
-			rsGeneratedHFile(ctx, rsFile))
+		headerFile := rsGeneratedHFile(ctx, rsFile)
+		genFiles = append(genFiles, rsGeneratedCppFile(ctx, rsFile), headerFile)
+		headers = append(headers, headerFile)
 	}
 
 	ctx.Build(pctx, android.BuildParams{
@@ -92,7 +93,7 @@
 		},
 	})
 
-	return android.Paths{stampFile}
+	return headers
 }
 
 func rsFlags(ctx ModuleContext, flags Flags, properties *BaseCompilerProperties) Flags {
diff --git a/cmd/multiproduct_kati/main.go b/cmd/multiproduct_kati/main.go
index 330c5dd..1171a65 100644
--- a/cmd/multiproduct_kati/main.go
+++ b/cmd/multiproduct_kati/main.go
@@ -156,10 +156,12 @@
 }
 
 func main() {
-	writer := terminal.NewWriter(terminal.StdioImpl{})
-	defer writer.Finish()
+	stdio := terminal.StdioImpl{}
 
-	log := logger.New(writer)
+	output := terminal.NewStatusOutput(stdio.Stdout(), "",
+		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
+
+	log := logger.New(output)
 	defer log.Cleanup()
 
 	flag.Parse()
@@ -172,8 +174,7 @@
 
 	stat := &status.Status{}
 	defer stat.Finish()
-	stat.AddOutput(terminal.NewStatusOutput(writer, "",
-		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
+	stat.AddOutput(output)
 
 	var failures failureCount
 	stat.AddOutput(&failures)
@@ -188,7 +189,7 @@
 		Context: ctx,
 		Logger:  log,
 		Tracer:  trace,
-		Writer:  writer,
+		Writer:  output,
 		Status:  stat,
 	}}
 
@@ -341,7 +342,7 @@
 	} else if failures > 1 {
 		log.Fatalf("%d failures", failures)
 	} else {
-		writer.Print("Success")
+		fmt.Fprintln(output, "Success")
 	}
 }
 
@@ -386,7 +387,7 @@
 		Context: mpctx.Context,
 		Logger:  log,
 		Tracer:  mpctx.Tracer,
-		Writer:  terminal.NewWriter(terminal.NewCustomStdio(nil, f, f)),
+		Writer:  f,
 		Thread:  mpctx.Tracer.NewThread(product),
 		Status:  &status.Status{},
 	}}
@@ -466,3 +467,8 @@
 }
 
 func (f *failureCount) Flush() {}
+
+func (f *failureCount) Write(p []byte) (int, error) {
+	// discard writes
+	return len(p), nil
+}
diff --git a/cmd/soong_ui/main.go b/cmd/soong_ui/main.go
index 5f9bd01..f5276c3 100644
--- a/cmd/soong_ui/main.go
+++ b/cmd/soong_ui/main.go
@@ -109,10 +109,10 @@
 		os.Exit(1)
 	}
 
-	writer := terminal.NewWriter(c.stdio())
-	defer writer.Finish()
+	output := terminal.NewStatusOutput(c.stdio().Stdout(), os.Getenv("NINJA_STATUS"),
+		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
 
-	log := logger.New(writer)
+	log := logger.New(output)
 	defer log.Cleanup()
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -125,8 +125,7 @@
 
 	stat := &status.Status{}
 	defer stat.Finish()
-	stat.AddOutput(terminal.NewStatusOutput(writer, os.Getenv("NINJA_STATUS"),
-		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
+	stat.AddOutput(output)
 	stat.AddOutput(trace.StatusTracer())
 
 	build.SetupSignals(log, cancel, func() {
@@ -140,7 +139,7 @@
 		Logger:  log,
 		Metrics: met,
 		Tracer:  trace,
-		Writer:  writer,
+		Writer:  output,
 		Status:  stat,
 	}}
 
@@ -312,13 +311,13 @@
 func make(ctx build.Context, config build.Config, _ []string, logsDir string) {
 	if config.IsVerbose() {
 		writer := ctx.Writer
-		writer.Print("! The argument `showcommands` is no longer supported.")
-		writer.Print("! Instead, the verbose log is always written to a compressed file in the output dir:")
-		writer.Print("!")
-		writer.Print(fmt.Sprintf("!   gzip -cd %s/verbose.log.gz | less -R", logsDir))
-		writer.Print("!")
-		writer.Print("! Older versions are saved in verbose.log.#.gz files")
-		writer.Print("")
+		fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
+		fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
+		fmt.Fprintln(writer, "!")
+		fmt.Fprintf(writer, "!   gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
+		fmt.Fprintln(writer, "!")
+		fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
+		fmt.Fprintln(writer, "")
 		time.Sleep(5 * time.Second)
 	}
 
diff --git a/java/app.go b/java/app.go
index cf9354f..78e0501 100644
--- a/java/app.go
+++ b/java/app.go
@@ -938,16 +938,18 @@
 }
 
 func (u *usesLibrary) deps(ctx android.BottomUpMutatorContext, noFrameworkLibs bool) {
-	ctx.AddVariationDependencies(nil, usesLibTag, u.usesLibraryProperties.Uses_libs...)
-	ctx.AddVariationDependencies(nil, usesLibTag, u.presentOptionalUsesLibs(ctx)...)
-	if !noFrameworkLibs {
-		// dexpreopt/dexpreopt.go needs the paths to the dex jars of these libraries in case construct_context.sh needs
-		// to pass them to dex2oat.  Add them as a dependency so we can determine the path to the dex jar of each
-		// library to dexpreopt.
-		ctx.AddVariationDependencies(nil, usesLibTag,
-			"org.apache.http.legacy",
-			"android.hidl.base-V1.0-java",
-			"android.hidl.manager-V1.0-java")
+	if !ctx.Config().UnbundledBuild() {
+		ctx.AddVariationDependencies(nil, usesLibTag, u.usesLibraryProperties.Uses_libs...)
+		ctx.AddVariationDependencies(nil, usesLibTag, u.presentOptionalUsesLibs(ctx)...)
+		if !noFrameworkLibs {
+			// dexpreopt/dexpreopt.go needs the paths to the dex jars of these libraries in case construct_context.sh needs
+			// to pass them to dex2oat.  Add them as a dependency so we can determine the path to the dex jar of each
+			// library to dexpreopt.
+			ctx.AddVariationDependencies(nil, usesLibTag,
+				"org.apache.http.legacy",
+				"android.hidl.base-V1.0-java",
+				"android.hidl.manager-V1.0-java")
+		}
 	}
 }
 
diff --git a/java/config/kotlin.go b/java/config/kotlin.go
index 7cea042..fd8e3db 100644
--- a/java/config/kotlin.go
+++ b/java/config/kotlin.go
@@ -32,6 +32,7 @@
 	pctx.SourcePathVariable("KotlinScriptRuntimeJar", "external/kotlinc/lib/kotlin-script-runtime.jar")
 	pctx.SourcePathVariable("KotlinTrove4jJar", "external/kotlinc/lib/trove4j.jar")
 	pctx.SourcePathVariable("KotlinKaptJar", "external/kotlinc/lib/kotlin-annotation-processing.jar")
+	pctx.SourcePathVariable("KotlinAnnotationJar", "external/kotlinc/lib/annotations-13.0.jar")
 	pctx.SourcePathVariable("KotlinStdlibJar", KotlinStdlibJar)
 
 	// These flags silence "Illegal reflective access" warnings when running kotlinc in OpenJDK9
diff --git a/java/hiddenapi_singleton.go b/java/hiddenapi_singleton.go
index b1ddab4..cf9f492 100644
--- a/java/hiddenapi_singleton.go
+++ b/java/hiddenapi_singleton.go
@@ -15,11 +15,14 @@
 package java
 
 import (
+	"fmt"
+
 	"android/soong/android"
 )
 
 func init() {
 	android.RegisterSingletonType("hiddenapi", hiddenAPISingletonFactory)
+	android.RegisterModuleType("hiddenapi_flags", hiddenAPIFlagsFactory)
 }
 
 type hiddenAPISingletonPathsStruct struct {
@@ -307,3 +310,48 @@
 		Text("fi").
 		Text(")")
 }
+
+type hiddenAPIFlagsProperties struct {
+	// name of the file into which the flags will be copied.
+	Filename *string
+}
+
+type hiddenAPIFlags struct {
+	android.ModuleBase
+
+	properties hiddenAPIFlagsProperties
+
+	outputFilePath android.OutputPath
+}
+
+func (h *hiddenAPIFlags) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	filename := String(h.properties.Filename)
+
+	inputPath := hiddenAPISingletonPaths(ctx).flags
+	h.outputFilePath = android.PathForModuleOut(ctx, filename).OutputPath
+
+	// This ensures that outputFilePath has the correct name for others to
+	// use, as the source file may have a different name.
+	ctx.Build(pctx, android.BuildParams{
+		Rule:   android.Cp,
+		Output: h.outputFilePath,
+		Input:  inputPath,
+	})
+}
+
+func (h *hiddenAPIFlags) OutputFiles(tag string) (android.Paths, error) {
+	switch tag {
+	case "":
+		return android.Paths{h.outputFilePath}, nil
+	default:
+		return nil, fmt.Errorf("unsupported module reference tag %q", tag)
+	}
+}
+
+// hiddenapi-flags provides access to the hiddenapi-flags.csv file generated during the build.
+func hiddenAPIFlagsFactory() android.Module {
+	module := &hiddenAPIFlags{}
+	module.AddProperties(&module.properties)
+	android.InitAndroidArchModule(module, android.HostAndDeviceSupported, android.MultilibCommon)
+	return module
+}
diff --git a/java/java_test.go b/java/java_test.go
index 4d161c5..4c8367b 100644
--- a/java/java_test.go
+++ b/java/java_test.go
@@ -212,7 +212,7 @@
 	setDexpreoptTestGlobalConfig(config, dexpreopt.GlobalConfigForTests(pathCtx))
 
 	ctx.Register()
-	_, errs := ctx.ParseFileList(".", []string{"Android.bp", "prebuilts/sdk/Android.bp"})
+	_, errs := ctx.ParseBlueprintsFiles("Android.bp")
 	android.FailIfErrored(t, errs)
 	_, errs = ctx.PrepareBuildActions(config)
 	android.FailIfErrored(t, errs)
diff --git a/java/kotlin.go b/java/kotlin.go
index 33167ba..8306907 100644
--- a/java/kotlin.go
+++ b/java/kotlin.go
@@ -44,6 +44,7 @@
 			"${config.KotlinScriptRuntimeJar}",
 			"${config.KotlinStdlibJar}",
 			"${config.KotlinTrove4jJar}",
+			"${config.KotlinAnnotationJar}",
 			"${config.GenKotlinBuildFileCmd}",
 			"${config.SoongZipCmd}",
 			"${config.ZipSyncCmd}",
diff --git a/java/sdk_test.go b/java/sdk_test.go
index cc4da2e..23d7a98 100644
--- a/java/sdk_test.go
+++ b/java/sdk_test.go
@@ -47,6 +47,14 @@
 			aidl:          "-Iframework/aidl",
 		},
 		{
+			name:          "no_framework_libs:true",
+			properties:    `no_framework_libs:true`,
+			bootclasspath: config.DefaultBootclasspathLibraries,
+			system:        config.DefaultSystemModules,
+			classpath:     []string{},
+			aidl:          "",
+		},
+		{
 			name:          "blank sdk version",
 			properties:    `sdk_version: "",`,
 			bootclasspath: config.DefaultBootclasspathLibraries,
@@ -129,13 +137,6 @@
 			classpath:     []string{},
 		},
 		{
-			name:       "host nostdlib",
-			moduleType: "java_library_host",
-			host:       android.Host,
-			properties: `no_standard_libs: true`,
-			classpath:  []string{},
-		},
-		{
 
 			name:          "host supported default",
 			host:          android.Host,
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index 242e3af..1d23fec 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -22,14 +22,13 @@
 	"testing"
 
 	"android/soong/ui/logger"
-	"android/soong/ui/terminal"
 )
 
 func testContext() Context {
 	return Context{&ContextImpl{
 		Context: context.Background(),
 		Logger:  logger.New(&bytes.Buffer{}),
-		Writer:  terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})),
+		Writer:  &bytes.Buffer{},
 	}}
 }
 
diff --git a/ui/build/context.go b/ui/build/context.go
index 249e898..7ff98ef 100644
--- a/ui/build/context.go
+++ b/ui/build/context.go
@@ -16,12 +16,12 @@
 
 import (
 	"context"
+	"io"
 
 	"android/soong/ui/logger"
 	"android/soong/ui/metrics"
 	"android/soong/ui/metrics/metrics_proto"
 	"android/soong/ui/status"
-	"android/soong/ui/terminal"
 	"android/soong/ui/tracer"
 )
 
@@ -35,7 +35,7 @@
 
 	Metrics *metrics.Metrics
 
-	Writer terminal.Writer
+	Writer io.Writer
 	Status *status.Status
 
 	Thread tracer.Thread
diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go
index 4335667..266130f 100644
--- a/ui/build/dumpvars.go
+++ b/ui/build/dumpvars.go
@@ -249,7 +249,7 @@
 	env := config.Environment()
 	// Print the banner like make does
 	if !env.IsEnvTrue("ANDROID_QUIET_BUILD") {
-		ctx.Writer.Print(Banner(make_vars))
+		fmt.Fprintln(ctx.Writer, Banner(make_vars))
 	}
 
 	// Populate the environment
diff --git a/ui/status/log.go b/ui/status/log.go
index 921aa44..7badac7 100644
--- a/ui/status/log.go
+++ b/ui/status/log.go
@@ -71,6 +71,11 @@
 	fmt.Fprintf(v.w, "%s%s\n", level.Prefix(), message)
 }
 
+func (v *verboseLog) Write(p []byte) (int, error) {
+	fmt.Fprint(v.w, string(p))
+	return len(p), nil
+}
+
 type errorLog struct {
 	w io.WriteCloser
 
@@ -134,3 +139,8 @@
 
 	fmt.Fprintf(e.w, "error: %s\n", message)
 }
+
+func (e *errorLog) Write(p []byte) (int, error) {
+	fmt.Fprint(e.w, string(p))
+	return len(p), nil
+}
diff --git a/ui/status/status.go b/ui/status/status.go
index 46ec72e..3d8cd7a 100644
--- a/ui/status/status.go
+++ b/ui/status/status.go
@@ -173,6 +173,9 @@
 	// Flush is called when your outputs should be flushed / closed. No
 	// output is expected after this call.
 	Flush()
+
+	// Write lets StatusOutput implement io.Writer
+	Write(p []byte) (n int, err error)
 }
 
 // Status is the multiplexer / accumulator between ToolStatus instances (via
diff --git a/ui/status/status_test.go b/ui/status/status_test.go
index e62785f..9494582 100644
--- a/ui/status/status_test.go
+++ b/ui/status/status_test.go
@@ -27,6 +27,11 @@
 func (c counterOutput) Message(level MsgLevel, msg string) {}
 func (c counterOutput) Flush()                             {}
 
+func (c counterOutput) Write(p []byte) (int, error) {
+	// Discard writes
+	return len(p), nil
+}
+
 func (c counterOutput) Expect(t *testing.T, counts Counts) {
 	if Counts(c) == counts {
 		return
diff --git a/ui/terminal/Android.bp b/ui/terminal/Android.bp
index 7104a50..b533b0d 100644
--- a/ui/terminal/Android.bp
+++ b/ui/terminal/Android.bp
@@ -17,11 +17,15 @@
     pkgPath: "android/soong/ui/terminal",
     deps: ["soong-ui-status"],
     srcs: [
+        "dumb_status.go",
+        "format.go",
+        "smart_status.go",
         "status.go",
-        "writer.go",
+        "stdio.go",
         "util.go",
     ],
     testSrcs: [
+        "status_test.go",
         "util_test.go",
     ],
     darwin: {
diff --git a/ui/terminal/dumb_status.go b/ui/terminal/dumb_status.go
new file mode 100644
index 0000000..201770f
--- /dev/null
+++ b/ui/terminal/dumb_status.go
@@ -0,0 +1,71 @@
+// 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"
+	"io"
+
+	"android/soong/ui/status"
+)
+
+type dumbStatusOutput struct {
+	writer    io.Writer
+	formatter formatter
+}
+
+// NewDumbStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewDumbStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+	return &dumbStatusOutput{
+		writer:    w,
+		formatter: formatter,
+	}
+}
+
+func (s *dumbStatusOutput) Message(level status.MsgLevel, message string) {
+	if level >= status.StatusLvl {
+		fmt.Fprintln(s.writer, s.formatter.message(level, message))
+	}
+}
+
+func (s *dumbStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+}
+
+func (s *dumbStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+	str := result.Description
+	if str == "" {
+		str = result.Command
+	}
+
+	progress := s.formatter.progress(counts) + str
+
+	output := s.formatter.result(result)
+	output = string(stripAnsiEscapes([]byte(output)))
+
+	if output != "" {
+		fmt.Fprint(s.writer, progress, "\n", output)
+	} else {
+		fmt.Fprintln(s.writer, progress)
+	}
+}
+
+func (s *dumbStatusOutput) Flush() {}
+
+func (s *dumbStatusOutput) Write(p []byte) (int, error) {
+	fmt.Fprint(s.writer, string(p))
+	return len(p), nil
+}
diff --git a/ui/terminal/format.go b/ui/terminal/format.go
new file mode 100644
index 0000000..4205bdc
--- /dev/null
+++ b/ui/terminal/format.go
@@ -0,0 +1,123 @@
+// 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 {
+	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(format string, quiet bool) formatter {
+	return formatter{
+		format: format,
+		quiet:  quiet,
+		start:  time.Now(),
+	}
+}
+
+func (s formatter) message(level status.MsgLevel, message string) string {
+	if level >= status.ErrorLvl {
+		return fmt.Sprintf("FAILED: %s", message)
+	} else if level > status.StatusLvl {
+		return fmt.Sprintf("%s%s", level.Prefix(), message)
+	} else if level == status.StatusLvl {
+		return message
+	}
+	return ""
+}
+
+func (s formatter) progress(counts status.Counts) string {
+	if s.format == "" {
+		return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions)
+	}
+
+	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())
+		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("FAILED: %s\n%s", targets, result.Output)
+		} else {
+			ret = fmt.Sprintf("FAILED: %s\n%s\n%s", 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
+}
diff --git a/ui/terminal/smart_status.go b/ui/terminal/smart_status.go
new file mode 100644
index 0000000..999a2d0
--- /dev/null
+++ b/ui/terminal/smart_status.go
@@ -0,0 +1,198 @@
+// 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"
+	"io"
+	"os"
+	"os/signal"
+	"strings"
+	"sync"
+	"syscall"
+
+	"android/soong/ui/status"
+)
+
+type smartStatusOutput struct {
+	writer    io.Writer
+	formatter formatter
+
+	lock sync.Mutex
+
+	haveBlankLine bool
+
+	termWidth int
+	sigwinch  chan os.Signal
+}
+
+// NewSmartStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+	s := &smartStatusOutput{
+		writer:    w,
+		formatter: formatter,
+
+		haveBlankLine: true,
+
+		sigwinch: make(chan os.Signal),
+	}
+
+	s.updateTermSize()
+
+	s.startSigwinch()
+
+	return s
+}
+
+func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
+	if level < status.StatusLvl {
+		return
+	}
+
+	str := s.formatter.message(level, message)
+
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	if level > status.StatusLvl {
+		s.print(str)
+	} else {
+		s.statusLine(str)
+	}
+}
+
+func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+	str := action.Description
+	if str == "" {
+		str = action.Command
+	}
+
+	progress := s.formatter.progress(counts)
+
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	s.statusLine(progress + str)
+}
+
+func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+	str := result.Description
+	if str == "" {
+		str = result.Command
+	}
+
+	progress := s.formatter.progress(counts) + str
+
+	output := s.formatter.result(result)
+
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	if output != "" {
+		s.statusLine(progress)
+		s.requestLine()
+		s.print(output)
+	} else {
+		s.statusLine(progress)
+	}
+}
+
+func (s *smartStatusOutput) Flush() {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	s.stopSigwinch()
+
+	s.requestLine()
+}
+
+func (s *smartStatusOutput) Write(p []byte) (int, error) {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+	s.print(string(p))
+	return len(p), nil
+}
+
+func (s *smartStatusOutput) requestLine() {
+	if !s.haveBlankLine {
+		fmt.Fprintln(s.writer)
+		s.haveBlankLine = true
+	}
+}
+
+func (s *smartStatusOutput) print(str string) {
+	if !s.haveBlankLine {
+		fmt.Fprint(s.writer, "\r", "\x1b[K")
+		s.haveBlankLine = true
+	}
+	fmt.Fprint(s.writer, str)
+	if len(str) == 0 || str[len(str)-1] != '\n' {
+		fmt.Fprint(s.writer, "\n")
+	}
+}
+
+func (s *smartStatusOutput) statusLine(str string) {
+	idx := strings.IndexRune(str, '\n')
+	if idx != -1 {
+		str = str[0:idx]
+	}
+
+	// Limit line width to the terminal width, otherwise we'll wrap onto
+	// another line and we won't delete the previous line.
+	if s.termWidth > 0 {
+		str = s.elide(str)
+	}
+
+	// Move to the beginning on the line, turn on bold, print the output,
+	// turn off bold, then clear the rest of the line.
+	start := "\r\x1b[1m"
+	end := "\x1b[0m\x1b[K"
+	fmt.Fprint(s.writer, start, str, end)
+	s.haveBlankLine = false
+}
+
+func (s *smartStatusOutput) elide(str string) string {
+	if len(str) > s.termWidth {
+		// TODO: Just do a max. Ninja elides the middle, but that's
+		// more complicated and these lines aren't that important.
+		str = str[:s.termWidth]
+	}
+
+	return str
+}
+
+func (s *smartStatusOutput) startSigwinch() {
+	signal.Notify(s.sigwinch, syscall.SIGWINCH)
+	go func() {
+		for _ = range s.sigwinch {
+			s.lock.Lock()
+			s.updateTermSize()
+			s.lock.Unlock()
+		}
+	}()
+}
+
+func (s *smartStatusOutput) stopSigwinch() {
+	signal.Stop(s.sigwinch)
+	close(s.sigwinch)
+}
+
+func (s *smartStatusOutput) updateTermSize() {
+	if w, ok := termWidth(s.writer); ok {
+		s.termWidth = w
+	}
+}
diff --git a/ui/terminal/status.go b/ui/terminal/status.go
index 2445c5b..69a2a09 100644
--- a/ui/terminal/status.go
+++ b/ui/terminal/status.go
@@ -15,131 +15,23 @@
 package terminal
 
 import (
-	"fmt"
-	"strings"
-	"time"
+	"io"
 
 	"android/soong/ui/status"
 )
 
-type statusOutput struct {
-	writer Writer
-	format string
-
-	start time.Time
-	quiet bool
-}
-
 // NewStatusOutput returns a StatusOutput that represents the
 // current build status similarly to Ninja's built-in terminal
 // output.
 //
 // statusFormat takes nearly all the same options as NINJA_STATUS.
 // %c is currently unsupported.
-func NewStatusOutput(w Writer, statusFormat string, quietBuild bool) status.StatusOutput {
-	return &statusOutput{
-		writer: w,
-		format: statusFormat,
+func NewStatusOutput(w io.Writer, statusFormat string, quietBuild bool) status.StatusOutput {
+	formatter := newFormatter(statusFormat, quietBuild)
 
-		start: time.Now(),
-		quiet: quietBuild,
-	}
-}
-
-func (s *statusOutput) Message(level status.MsgLevel, message string) {
-	if level >= status.ErrorLvl {
-		s.writer.Print(fmt.Sprintf("FAILED: %s", message))
-	} else if level > status.StatusLvl {
-		s.writer.Print(fmt.Sprintf("%s%s", level.Prefix(), message))
-	} else if level == status.StatusLvl {
-		s.writer.StatusLine(message)
-	}
-}
-
-func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) {
-	if !s.writer.isSmartTerminal() {
-		return
-	}
-
-	str := action.Description
-	if str == "" {
-		str = action.Command
-	}
-
-	s.writer.StatusLine(s.progress(counts) + str)
-}
-
-func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
-	str := result.Description
-	if str == "" {
-		str = result.Command
-	}
-
-	progress := s.progress(counts) + str
-
-	if result.Error != nil {
-		targets := strings.Join(result.Outputs, " ")
-		if s.quiet || result.Command == "" {
-			s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s", targets, result.Output))
-		} else {
-			s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s\n%s", targets, result.Command, result.Output))
-		}
-	} else if result.Output != "" {
-		s.writer.StatusAndMessage(progress, result.Output)
+	if isSmartTerminal(w) {
+		return NewSmartStatusOutput(w, formatter)
 	} else {
-		s.writer.StatusLine(progress)
+		return NewDumbStatusOutput(w, formatter)
 	}
 }
-
-func (s *statusOutput) Flush() {}
-
-func (s *statusOutput) progress(counts status.Counts) string {
-	if s.format == "" {
-		return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions)
-	}
-
-	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())
-		default:
-			buf.WriteString("unknown placeholder '")
-			buf.WriteByte(c)
-			buf.WriteString("'")
-		}
-	}
-	return buf.String()
-}
diff --git a/ui/terminal/status_test.go b/ui/terminal/status_test.go
new file mode 100644
index 0000000..fc9315b
--- /dev/null
+++ b/ui/terminal/status_test.go
@@ -0,0 +1,275 @@
+// Copyright 2018 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 (
+	"bytes"
+	"fmt"
+	"syscall"
+	"testing"
+
+	"android/soong/ui/status"
+)
+
+func TestStatusOutput(t *testing.T) {
+	tests := []struct {
+		name  string
+		calls func(stat status.StatusOutput)
+		smart string
+		dumb  string
+	}{
+		{
+			name:  "two actions",
+			calls: twoActions,
+			smart: "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action1\n[100% 2/2] action2\n",
+		},
+		{
+			name:  "two parallel actions",
+			calls: twoParallelActions,
+			smart: "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action1\n[100% 2/2] action2\n",
+		},
+		{
+			name:  "action with output",
+			calls: actionsWithOutput,
+			smart: "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+			dumb:  "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
+		},
+		{
+			name:  "action with output without newline",
+			calls: actionsWithOutputWithoutNewline,
+			smart: "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+			dumb:  "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
+		},
+		{
+			name:  "action with error",
+			calls: actionsWithError,
+			smart: "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+			dumb:  "[ 33% 1/3] action1\n[ 66% 2/3] action2\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n[100% 3/3] action3\n",
+		},
+		{
+			name:  "action with empty description",
+			calls: actionWithEmptyDescription,
+			smart: "\r\x1b[1m[  0% 0/1] command1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] command1\x1b[0m\x1b[K\n",
+			dumb:  "[100% 1/1] command1\n",
+		},
+		{
+			name:  "messages",
+			calls: actionsWithMessages,
+			smart: "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1mstatus\x1b[0m\x1b[K\r\x1b[Kprint\nFAILED: error\n\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action1\nstatus\nprint\nFAILED: error\n[100% 2/2] action2\n",
+		},
+		{
+			name:  "action with long description",
+			calls: actionWithLongDescription,
+			smart: "\r\x1b[1m[  0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very long descrip\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action with very long description to test eliding\n",
+		},
+		{
+			name:  "action with output with ansi codes",
+			calls: actionWithOuptutWithAnsiCodes,
+			smart: "\r\x1b[1m[  0% 0/1] action1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] action1\x1b[0m\x1b[K\n\x1b[31mcolor\x1b[0m\n",
+			dumb:  "[100% 1/1] action1\ncolor\n",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Run("smart", func(t *testing.T) {
+				smart := &fakeSmartTerminal{termWidth: 40}
+				stat := NewStatusOutput(smart, "", false)
+				tt.calls(stat)
+				stat.Flush()
+
+				if g, w := smart.String(), tt.smart; g != w {
+					t.Errorf("want:\n%q\ngot:\n%q", w, g)
+				}
+			})
+
+			t.Run("dumb", func(t *testing.T) {
+				dumb := &bytes.Buffer{}
+				stat := NewStatusOutput(dumb, "", false)
+				tt.calls(stat)
+				stat.Flush()
+
+				if g, w := dumb.String(), tt.dumb; g != w {
+					t.Errorf("want:\n%q\ngot:\n%q", w, g)
+				}
+			})
+		})
+	}
+}
+
+type runner struct {
+	counts status.Counts
+	stat   status.StatusOutput
+}
+
+func newRunner(stat status.StatusOutput, totalActions int) *runner {
+	return &runner{
+		counts: status.Counts{TotalActions: totalActions},
+		stat:   stat,
+	}
+}
+
+func (r *runner) startAction(action *status.Action) {
+	r.counts.StartedActions++
+	r.counts.RunningActions++
+	r.stat.StartAction(action, r.counts)
+}
+
+func (r *runner) finishAction(result status.ActionResult) {
+	r.counts.FinishedActions++
+	r.counts.RunningActions--
+	r.stat.FinishAction(result, r.counts)
+}
+
+func (r *runner) finishAndStartAction(result status.ActionResult, action *status.Action) {
+	r.counts.FinishedActions++
+	r.stat.FinishAction(result, r.counts)
+
+	r.counts.StartedActions++
+	r.stat.StartAction(action, r.counts)
+}
+
+var (
+	action1 = &status.Action{Description: "action1"}
+	result1 = status.ActionResult{Action: action1}
+	action2 = &status.Action{Description: "action2"}
+	result2 = status.ActionResult{Action: action2}
+	action3 = &status.Action{Description: "action3"}
+	result3 = status.ActionResult{Action: action3}
+)
+
+func twoActions(stat status.StatusOutput) {
+	runner := newRunner(stat, 2)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2)
+	runner.finishAction(result2)
+}
+
+func twoParallelActions(stat status.StatusOutput) {
+	runner := newRunner(stat, 2)
+	runner.startAction(action1)
+	runner.startAction(action2)
+	runner.finishAction(result1)
+	runner.finishAction(result2)
+}
+
+func actionsWithOutput(stat status.StatusOutput) {
+	result2WithOutput := status.ActionResult{Action: action2, Output: "output1\noutput2\n"}
+
+	runner := newRunner(stat, 3)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2)
+	runner.finishAction(result2WithOutput)
+	runner.startAction(action3)
+	runner.finishAction(result3)
+}
+
+func actionsWithOutputWithoutNewline(stat status.StatusOutput) {
+	result2WithOutputWithoutNewline := status.ActionResult{Action: action2, Output: "output1\noutput2"}
+
+	runner := newRunner(stat, 3)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2)
+	runner.finishAction(result2WithOutputWithoutNewline)
+	runner.startAction(action3)
+	runner.finishAction(result3)
+}
+
+func actionsWithError(stat status.StatusOutput) {
+	action2WithError := &status.Action{Description: "action2", Outputs: []string{"f1", "f2"}, Command: "touch f1 f2"}
+	result2WithError := status.ActionResult{Action: action2WithError, Output: "error1\nerror2\n", Error: fmt.Errorf("error1")}
+
+	runner := newRunner(stat, 3)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2WithError)
+	runner.finishAction(result2WithError)
+	runner.startAction(action3)
+	runner.finishAction(result3)
+}
+
+func actionWithEmptyDescription(stat status.StatusOutput) {
+	action1 := &status.Action{Command: "command1"}
+	result1 := status.ActionResult{Action: action1}
+
+	runner := newRunner(stat, 1)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+}
+
+func actionsWithMessages(stat status.StatusOutput) {
+	runner := newRunner(stat, 2)
+
+	runner.startAction(action1)
+	runner.finishAction(result1)
+
+	stat.Message(status.VerboseLvl, "verbose")
+	stat.Message(status.StatusLvl, "status")
+	stat.Message(status.PrintLvl, "print")
+	stat.Message(status.ErrorLvl, "error")
+
+	runner.startAction(action2)
+	runner.finishAction(result2)
+}
+
+func actionWithLongDescription(stat status.StatusOutput) {
+	action1 := &status.Action{Description: "action with very long description to test eliding"}
+	result1 := status.ActionResult{Action: action1}
+
+	runner := newRunner(stat, 2)
+
+	runner.startAction(action1)
+
+	runner.finishAction(result1)
+}
+
+func actionWithOuptutWithAnsiCodes(stat status.StatusOutput) {
+	result1WithOutputWithAnsiCodes := status.ActionResult{Action: action1, Output: "\x1b[31mcolor\x1b[0m"}
+
+	runner := newRunner(stat, 1)
+	runner.startAction(action1)
+	runner.finishAction(result1WithOutputWithAnsiCodes)
+}
+
+func TestSmartStatusOutputWidthChange(t *testing.T) {
+	smart := &fakeSmartTerminal{termWidth: 40}
+	stat := NewStatusOutput(smart, "", false)
+
+	runner := newRunner(stat, 2)
+
+	action := &status.Action{Description: "action with very long description to test eliding"}
+	result := status.ActionResult{Action: action}
+
+	runner.startAction(action)
+	smart.termWidth = 30
+	// Fake a SIGWINCH
+	stat.(*smartStatusOutput).sigwinch <- syscall.SIGWINCH
+	runner.finishAction(result)
+
+	stat.Flush()
+
+	w := "\r\x1b[1m[  0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very lo\x1b[0m\x1b[K\n"
+
+	if g := smart.String(); g != w {
+		t.Errorf("want:\n%q\ngot:\n%q", w, g)
+	}
+}
diff --git a/ui/terminal/stdio.go b/ui/terminal/stdio.go
new file mode 100644
index 0000000..dec2963
--- /dev/null
+++ b/ui/terminal/stdio.go
@@ -0,0 +1,55 @@
+// Copyright 2018 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 provides a set of interfaces that can be used to interact
+// with the terminal (including falling back when the terminal is detected to
+// be a redirect or other dumb terminal)
+package terminal
+
+import (
+	"io"
+	"os"
+)
+
+// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
+type StdioInterface interface {
+	Stdin() io.Reader
+	Stdout() io.Writer
+	Stderr() io.Writer
+}
+
+// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
+type StdioImpl struct{}
+
+func (StdioImpl) Stdin() io.Reader  { return os.Stdin }
+func (StdioImpl) Stdout() io.Writer { return os.Stdout }
+func (StdioImpl) Stderr() io.Writer { return os.Stderr }
+
+var _ StdioInterface = StdioImpl{}
+
+type customStdio struct {
+	stdin  io.Reader
+	stdout io.Writer
+	stderr io.Writer
+}
+
+func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
+	return customStdio{stdin, stdout, stderr}
+}
+
+func (c customStdio) Stdin() io.Reader  { return c.stdin }
+func (c customStdio) Stdout() io.Writer { return c.stdout }
+func (c customStdio) Stderr() io.Writer { return c.stderr }
+
+var _ StdioInterface = customStdio{}
diff --git a/ui/terminal/util.go b/ui/terminal/util.go
index a85a517..3a11b79 100644
--- a/ui/terminal/util.go
+++ b/ui/terminal/util.go
@@ -22,13 +22,15 @@
 	"unsafe"
 )
 
-func isTerminal(w io.Writer) bool {
+func isSmartTerminal(w io.Writer) bool {
 	if f, ok := w.(*os.File); ok {
 		var termios syscall.Termios
 		_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
 			ioctlGetTermios, uintptr(unsafe.Pointer(&termios)),
 			0, 0, 0)
 		return err == 0
+	} else if _, ok := w.(*fakeSmartTerminal); ok {
+		return true
 	}
 	return false
 }
@@ -43,6 +45,8 @@
 			syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
 			0, 0, 0)
 		return int(winsize.ws_column), err == 0
+	} else if f, ok := w.(*fakeSmartTerminal); ok {
+		return f.termWidth, true
 	}
 	return 0, false
 }
@@ -99,3 +103,8 @@
 
 	return input
 }
+
+type fakeSmartTerminal struct {
+	bytes.Buffer
+	termWidth int
+}
diff --git a/ui/terminal/writer.go b/ui/terminal/writer.go
deleted file mode 100644
index ebe4b2a..0000000
--- a/ui/terminal/writer.go
+++ /dev/null
@@ -1,229 +0,0 @@
-// Copyright 2018 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 provides a set of interfaces that can be used to interact
-// with the terminal (including falling back when the terminal is detected to
-// be a redirect or other dumb terminal)
-package terminal
-
-import (
-	"fmt"
-	"io"
-	"os"
-	"strings"
-	"sync"
-)
-
-// Writer provides an interface to write temporary and permanent messages to
-// the terminal.
-//
-// The terminal is considered to be a dumb terminal if TERM==dumb, or if a
-// terminal isn't detected on stdout/stderr (generally because it's a pipe or
-// file). Dumb terminals will strip out all ANSI escape sequences, including
-// colors.
-type Writer interface {
-	// Print prints the string to the terminal, overwriting any current
-	// status being displayed.
-	//
-	// On a dumb terminal, the status messages will be kept.
-	Print(str string)
-
-	// Status prints the first line of the string to the terminal,
-	// overwriting any previous status line. Strings longer than the width
-	// of the terminal will be cut off.
-	//
-	// On a dumb terminal, previous status messages will remain, and the
-	// entire first line of the string will be printed.
-	StatusLine(str string)
-
-	// StatusAndMessage prints the first line of status to the terminal,
-	// similarly to StatusLine(), then prints the full msg below that. The
-	// status line is retained.
-	//
-	// There is guaranteed to be no other output in between the status and
-	// message.
-	StatusAndMessage(status, msg string)
-
-	// Finish ensures that the output ends with a newline (preserving any
-	// current status line that is current displayed).
-	//
-	// This does nothing on dumb terminals.
-	Finish()
-
-	// Write implements the io.Writer interface. This is primarily so that
-	// the logger can use this interface to print to stderr without
-	// breaking the other semantics of this interface.
-	//
-	// Try to use any of the other functions if possible.
-	Write(p []byte) (n int, err error)
-
-	isSmartTerminal() bool
-}
-
-// NewWriter creates a new Writer based on the stdio and the TERM
-// environment variable.
-func NewWriter(stdio StdioInterface) Writer {
-	w := &writerImpl{
-		stdio: stdio,
-
-		haveBlankLine: true,
-	}
-
-	if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" {
-		w.smartTerminal = isTerminal(stdio.Stdout())
-	}
-	w.stripEscapes = !w.smartTerminal
-
-	return w
-}
-
-type writerImpl struct {
-	stdio StdioInterface
-
-	haveBlankLine bool
-
-	// Protecting the above, we assume that smartTerminal and stripEscapes
-	// does not change after initial setup.
-	lock sync.Mutex
-
-	smartTerminal bool
-	stripEscapes  bool
-}
-
-func (w *writerImpl) isSmartTerminal() bool {
-	return w.smartTerminal
-}
-
-func (w *writerImpl) requestLine() {
-	if !w.haveBlankLine {
-		fmt.Fprintln(w.stdio.Stdout())
-		w.haveBlankLine = true
-	}
-}
-
-func (w *writerImpl) Print(str string) {
-	if w.stripEscapes {
-		str = string(stripAnsiEscapes([]byte(str)))
-	}
-
-	w.lock.Lock()
-	defer w.lock.Unlock()
-	w.print(str)
-}
-
-func (w *writerImpl) print(str string) {
-	if !w.haveBlankLine {
-		fmt.Fprint(w.stdio.Stdout(), "\r", "\x1b[K")
-		w.haveBlankLine = true
-	}
-	fmt.Fprint(w.stdio.Stdout(), str)
-	if len(str) == 0 || str[len(str)-1] != '\n' {
-		fmt.Fprint(w.stdio.Stdout(), "\n")
-	}
-}
-
-func (w *writerImpl) StatusLine(str string) {
-	w.lock.Lock()
-	defer w.lock.Unlock()
-
-	w.statusLine(str)
-}
-
-func (w *writerImpl) statusLine(str string) {
-	if !w.smartTerminal {
-		fmt.Fprintln(w.stdio.Stdout(), str)
-		return
-	}
-
-	idx := strings.IndexRune(str, '\n')
-	if idx != -1 {
-		str = str[0:idx]
-	}
-
-	// Limit line width to the terminal width, otherwise we'll wrap onto
-	// another line and we won't delete the previous line.
-	//
-	// Run this on every line in case the window has been resized while
-	// we're printing. This could be optimized to only re-run when we get
-	// SIGWINCH if it ever becomes too time consuming.
-	if max, ok := termWidth(w.stdio.Stdout()); ok {
-		if len(str) > max {
-			// TODO: Just do a max. Ninja elides the middle, but that's
-			// more complicated and these lines aren't that important.
-			str = str[:max]
-		}
-	}
-
-	// Move to the beginning on the line, print the output, then clear
-	// the rest of the line.
-	fmt.Fprint(w.stdio.Stdout(), "\r", str, "\x1b[K")
-	w.haveBlankLine = false
-}
-
-func (w *writerImpl) StatusAndMessage(status, msg string) {
-	if w.stripEscapes {
-		msg = string(stripAnsiEscapes([]byte(msg)))
-	}
-
-	w.lock.Lock()
-	defer w.lock.Unlock()
-
-	w.statusLine(status)
-	w.requestLine()
-	w.print(msg)
-}
-
-func (w *writerImpl) Finish() {
-	w.lock.Lock()
-	defer w.lock.Unlock()
-
-	w.requestLine()
-}
-
-func (w *writerImpl) Write(p []byte) (n int, err error) {
-	w.Print(string(p))
-	return len(p), nil
-}
-
-// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
-type StdioInterface interface {
-	Stdin() io.Reader
-	Stdout() io.Writer
-	Stderr() io.Writer
-}
-
-// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
-type StdioImpl struct{}
-
-func (StdioImpl) Stdin() io.Reader  { return os.Stdin }
-func (StdioImpl) Stdout() io.Writer { return os.Stdout }
-func (StdioImpl) Stderr() io.Writer { return os.Stderr }
-
-var _ StdioInterface = StdioImpl{}
-
-type customStdio struct {
-	stdin  io.Reader
-	stdout io.Writer
-	stderr io.Writer
-}
-
-func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
-	return customStdio{stdin, stdout, stderr}
-}
-
-func (c customStdio) Stdin() io.Reader  { return c.stdin }
-func (c customStdio) Stdout() io.Writer { return c.stdout }
-func (c customStdio) Stderr() io.Writer { return c.stderr }
-
-var _ StdioInterface = customStdio{}
diff --git a/ui/tracer/status.go b/ui/tracer/status.go
index af50e2d..c831255 100644
--- a/ui/tracer/status.go
+++ b/ui/tracer/status.go
@@ -85,3 +85,8 @@
 
 func (s *statusOutput) Flush()                                        {}
 func (s *statusOutput) Message(level status.MsgLevel, message string) {}
+
+func (s *statusOutput) Write(p []byte) (int, error) {
+	// Discard writes
+	return len(p), nil
+}