pw_build: Make pw-wrap-ninja avoid terminal codes if non-interactive

This commit makes the pw-wrap-ninja script avoid the use of ANSI
terminal codes if it detects that it is running in a non-interactive
context.

This follows the same logic as Ninja: a terminal is interactive if
all of the following are true:
1) stdout is a TTY
2) the TERM environment variable is set
3) the TERM environment variable is not "dumb"

In this mode, the script will avoid printing out the live progress
display (as this requires terminal codes). Instead, it'll just print out
whenever an action finishes, along with any output from the action.

Tested:
  Ran pw-wrap-ninja piped to tee, saw that the progress display
  was hidden, no ANSI escapes were visible, and the 'Finished...' lines
  were printed.
Change-Id: I9a6a95599782f57ca422ee9af5bf5e5ad54678c6
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/126172
Reviewed-by: Rob Mohr <mohrr@google.com>
Pigweed-Auto-Submit: Eli Lipsitz <elipsitz@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_build/py/pw_build/wrap_ninja.py b/pw_build/py/pw_build/wrap_ninja.py
index d11f4ef..c9f1c3e 100644
--- a/pw_build/py/pw_build/wrap_ninja.py
+++ b/pw_build/py/pw_build/wrap_ninja.py
@@ -52,6 +52,12 @@
     the terminal once printed. Second, "temporary" printed lines follow the
     most recently printed permanent line, and get rewritten on each call
     to `render`.
+
+    Attributes:
+        smart_terminal: If true, will print a rich TUI using terminal control
+            codes. Otherwise, this class won't print temporary lines, and
+            permanent lines will be printed without any special control codes.
+            Defaults to true if stdout is connected to a TTY.
     """
 
     def __init__(self) -> None:
@@ -60,6 +66,9 @@
         self._temporary_lines: List[str] = []
         self._previous_line_count = 0
 
+        term = os.environ.get('TERM')
+        self.smart_terminal = term and (term != 'dumb') and sys.stdout.isatty()
+
     def print_line(self, line: str) -> None:
         """Queue a permanent line for printing during the next render."""
         self._queued_lines.append(line)
@@ -71,6 +80,14 @@
     def render(self) -> None:
         """Render the current state of the renderer."""
 
+        # If we can't use terminal codes, print out permanent lines and exit.
+        if not self.smart_terminal:
+            for line in self._queued_lines:
+                print(line)
+            self._queued_lines.clear()
+            sys.stdout.flush()
+            return
+
         # Go back to the end of the last permanent lines.
         for _ in range(self._previous_line_count):
             print(_TERM_MOVE_PREVIOUS_LINE, end='')
@@ -336,7 +353,8 @@
 
     def _process_event(self, event: NinjaEvent) -> None:
         """Processes a Ninja Event. Must be called under the Ninja lock."""
-        print_actions = self._args.log_actions
+        show_started = self._args.log_actions
+        show_ended = self._args.log_actions or not self._renderer.smart_terminal
 
         if event.kind == NinjaEventKind.ACTION_LOG:
             if event.action and (event.action != self._last_log_action):
@@ -345,14 +363,14 @@
             assert event.log_message is not None
             self._renderer.print_line(event.log_message)
 
-        if event.kind == NinjaEventKind.ACTION_STARTED and print_actions:
+        if event.kind == NinjaEventKind.ACTION_STARTED and show_started:
             assert event.action
             self._renderer.print_line(
                 f'[{self._ninja.num_finished}/{self._ninja.num_total}] '
                 f'Started  [{event.action.name}]'
             )
 
-        if event.kind == NinjaEventKind.ACTION_FINISHED and print_actions:
+        if event.kind == NinjaEventKind.ACTION_FINISHED and show_ended:
             assert event.action and event.action.end_time is not None
             duration = _format_duration(
                 event.action.end_time - event.action.start_time