pw_env_setup: add spinner

Fixed: 160
Change-Id: I2256b1e773117ed749ad6595558d7c1e5c7241b1
diff --git a/pw_env_setup/py/pw_env_setup/env_setup.py b/pw_env_setup/py/pw_env_setup/env_setup.py
index ba90542..1b9f922 100755
--- a/pw_env_setup/py/pw_env_setup/env_setup.py
+++ b/pw_env_setup/py/pw_env_setup/env_setup.py
@@ -72,6 +72,7 @@
 from pw_env_setup.colors import Color, enable_colors
 from pw_env_setup import cargo_setup
 from pw_env_setup import environment
+from pw_env_setup import spinner
 from pw_env_setup import virtualenv_setup
 
 
@@ -183,7 +184,9 @@
                 newline=False,
             )
 
-            result = step()
+            spin = spinner.Spinner()
+            with spin():
+                result = step()
 
             self._env.echo(result.status_str())
             for message in result.messages():
diff --git a/pw_env_setup/py/pw_env_setup/spinner.py b/pw_env_setup/py/pw_env_setup/spinner.py
new file mode 100644
index 0000000..fd6ea9c
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/spinner.py
@@ -0,0 +1,59 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+#     https://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.
+"""Spinner!"""
+
+import contextlib
+import sys
+import threading
+import time
+
+
+class Spinner(object):  # pylint: disable=useless-object-inheritance
+    """Spinner!"""
+    def __init__(self, *args, **kwargs):
+        super(Spinner, self).__init__(*args, **kwargs)
+        self._done = None
+        self._thread = None
+
+    def __del__(self):
+        self._done = True
+
+    def _spin(self):
+        i = 0
+        chars = '|/-\\'
+        while not self._done:
+            sys.stdout.write('[{}]'.format(chars[i]))
+            sys.stdout.flush()
+            time.sleep(0.1)
+            sys.stdout.write('\b\b\b')
+            i = (i + 1) % len(chars)
+
+    def start(self):
+        self._done = False
+        self._thread = threading.Thread(target=self._spin)
+        self._thread.start()
+
+    def stop(self):
+        assert self._thread
+        self._done = True
+        self._thread.join()
+        self._thread = None
+
+    @contextlib.contextmanager
+    def __call__(self):
+        try:
+            self.start()
+            yield self
+        finally:
+            self.stop()