Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

otk/ui: introduce some sort of ui #272

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

supakeen
Copy link
Member

@supakeen supakeen commented Oct 10, 2024

We've spoken about being nice to users before; @schuellerf is working on adding a lot of annotated data to our data structures, I've spent a morning on writing a tiny bit of UI.

The idea is:

  • output goes to stdout
  • ui and logging goes to stderr
  • when stderr is not a tty (e.g. redirected away) we don't show a ui
  • the ui is everything that isn't logging, logging is still there and will intersperse with the ui

Based on #277.

@supakeen
Copy link
Member Author

Might need better locking and signal handling around the thread but I didn't encounter any issues so far. We can always move all output into a single object and have it lock.

Copy link
Contributor

@mvo5 mvo5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some quick high-level thoughts:

  • would be nice to unify the log/ui concepts, otherwise I see a lot of "ui.print(some-info")";log.info("some-info") in the code (also verbose probably means the same for ui/logs)
  • not print if it's not a tty is unusal IME, most tools seem to not do spinners or color but not printing anything is rare (I think?)
  • tests :)

@supakeen
Copy link
Member Author

Some quick high-level thoughts:

  • would be nice to unify the log/ui concepts, otherwise I see a lot of "ui.print(some-info")";log.info("some-info") in the code (also verbose probably means the same for ui/logs)

Yes, however; see the answer for the second point.

  • not print if it's not a tty is unusal IME, most tools seem to not do spinners or color but not printing anything is rare (I think?)

Yes! So in my case the UI is meant to be not-important, only for human eyes. Normal log messages are always shown (interspersed with the UI) and any redirection turns off everything related to the UI. How do you feel?

  • tests :)

Of course! (UI's always fun to test).

src/otk/ui.py Outdated Show resolved Hide resolved
@schuellerf
Copy link
Contributor

schuellerf commented Oct 11, 2024

A real "spinner" would be nice :-D
but that's probably an overkill

diff --git a/src/otk/ui.py b/src/otk/ui.py
index 3fd3496..410438a 100644
--- a/src/otk/ui.py
+++ b/src/otk/ui.py
@@ -1,3 +1,4 @@
+import os
 import sys
 import time
 import threading
@@ -13,20 +14,36 @@ class _Spinner:
     active: bool
     thread: threading.Thread
 
+    spin_chars = '⠋⠙⠹⠸⢰⣰⣠⣄⣆⡆⠇⠏'
+
     def __init__(self, prompt: str) -> None:
         self.prompt = prompt
 
+    @classmethod
+    def is_ansi_terminal(cls):
+        term = os.getenv('TERM', '')
+        return term.startswith('xterm') or 'ansi' in term or 'vt100' in term
+
     def _loop(self):
-        sys.stderr.write(self.prompt)
+        sys.stderr.write(self.prompt + " ")
         sys.stderr.flush()
 
         # No spinner business if we aren't printing to tty's.
         if sys.stderr.isatty():
+            i = 0
+            ansi = _Spinner.is_ansi_terminal()
             while self.active:
-                sys.stderr.write(".")
+                if ansi:
+                    i = (i + 1) % len(_Spinner.spin_chars)
+                    sys.stderr.write(_Spinner.spin_chars[i] + '\x1b[D')
+                else:
+                    sys.stderr.write(".")
                 sys.stderr.flush()
                 time.sleep(0.1)
+            if ansi:
+                sys.stderr.write("\x1b[D. ")
+                sys.stderr.flush()
+
         # Only write a newline if the prompt was actually there
         if self.prompt:
             sys.stderr.write("\n")

@supakeen
Copy link
Member Author

There are a lot of ways to make the UI prettier, I think the core issue for this right now is my decision to have a UI that is separate from log output and which gets disabled on any redirection. If we decide that that's an OK thing to do then I'll add some tests, we can merge, and we can iterate.

@supakeen supakeen force-pushed the you-spin-me-right-round-baby-right-round branch from f9e1017 to 2c517c7 Compare October 14, 2024 08:34
@supakeen supakeen marked this pull request as ready for review October 14, 2024 10:45
schuellerf
schuellerf previously approved these changes Oct 15, 2024
Copy link
Contributor

@schuellerf schuellerf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with anything a little more verbose "for humans"

@schuellerf
Copy link
Contributor

although some tests are missing, right? :-)

@mvo5
Copy link
Contributor

mvo5 commented Oct 17, 2024

Some quick high-level thoughts:

  • would be nice to unify the log/ui concepts, otherwise I see a lot of "ui.print(some-info")";log.info("some-info") in the code (also verbose probably means the same for ui/logs)

Yes, however; see the answer for the second point.

  • not print if it's not a tty is unusal IME, most tools seem to not do spinners or color but not printing anything is rare (I think?)

Yes! So in my case the UI is meant to be not-important, only for human eyes. Normal log messages are always shown (interspersed with the UI) and any redirection turns off everything related to the UI. How do you feel?
[..]

I think it is preferable to have a single way to do output, I'm not sure there is a thing as "not important output", it depends on context, when doing a bugreport usually all "log" level output is important but when running in "normal" mode it is not. So having a single way to output seems useful, otherwise we would do things like "ui.print("doing something"); logger.info("doing something") or we would have "ui.print("doing something") and no logger but then when asking for a debug log via "otk compile foo.yaml > output.log" we would miss "doing something" in the output which may (or may not) be an important clue).

Also I think building blocks that are orthogonal are important, anytime something is kinda-similar to a different concept we already have I feel uneasy. With this class we have now logging that does output and a new ui that does output. And depending on context (isatty()) some is turned on or off. And depending on verbosity level in logging some is turned on and some is turned off.

In many ways this is very similar to logging, we want to control "debug", "warn" etc and a way to increase or decrease verbosity.
So my suggestion would be to have a UI/Output/Logger class that implements some abstract methods like "Output.Print()", "Output.{Start,Stop}Spinner()" (or WithSpinner() as context manager) etc. Then on non-tty system the spinner would just do nothing. We could set verbosity level with that, we could have a multiplexer to have syslog output and tty output etc.

[edit: sorry, parts where maybe a bit terse/badly phrased, I rewrote the section about "not important output"]

Copy link
Contributor

@mvo5 mvo5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there are some commits in this PR that are not related to the title of the PR, is that intentional?

@supakeen
Copy link
Member Author

It looks like there are some commits in this PR that are not related to the title of the PR, is that intentional?

Yes, it needs rebasing again as it was based on top of #277 which just got merged.

src/otk/ui.py Outdated


def print(text: str) -> None: # pylint: disable=redefined-builtin
"""Write a line of text to stderr if it's attached to a tty."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an intermediate solution to the valuable input of @mvo5 - should we info/debug-log the text here, to avoid duplication and have those interesting messages also in the logs?
( could as well be the "else" of isatty()? )

@mvo5
Copy link
Contributor

mvo5 commented Oct 28, 2024

I was asked to re-review this but afaict nothing since my comments in #272 (comment) has changed so they still apply.

@supakeen
Copy link
Member Author

Indeed nothing changed. I'll be taking this PR in the direction you mentioned in the comment.

@supakeen supakeen force-pushed the you-spin-me-right-round-baby-right-round branch 2 times, most recently from c046ec6 to 3c36474 Compare October 30, 2024 10:20
@supakeen
Copy link
Member Author

supakeen commented Oct 30, 2024

I've adjusted this PR to have a ui.Terminal that wraps a logging.Logger instance. It a spinner and motd methods right now. Spinner does nothing if not isatty().

This allows us to take control of the delegated things, expand functionality, and act as a filter and/or formatter (colors, etc) for output based on the settings of ui.Terminal or isatty(). The global logging output is still configured by the entrypoint, this might/could be moved into the ui at some point.

We want prettier outputs, let's create an ui submodule to hold
primitives for that. This includes the initial implementation of a
spinner (which just prints dots for now).

Signed-off-by: Simon de Vlieger <[email protected]>
Shows a message with dots appearing after it when an external is
running.

Signed-off-by: Simon de Vlieger <[email protected]>
The log level for includes can be reduced from INFO to DEBUG to be
consistent with out log levels.

Signed-off-by: Simon de Vlieger <[email protected]>
Only prints output when stderr is a tty.

Signed-off-by: Simon de Vlieger <[email protected]>
Small MOTD to print version and documentation URL.

Signed-off-by: Simon de Vlieger <[email protected]>
Use a logger instance as the default output for the UI. The logger
instance is still configured by the entrypoint for its levels.

The new `Terminal` instance delegates all `Logger` calls but adds a few
others such as a `spinner` and `motd`.

Signed-off-by: Simon de Vlieger <[email protected]>
Signed-off-by: Simon de Vlieger <[email protected]>
@supakeen supakeen force-pushed the you-spin-me-right-round-baby-right-round branch from 3c36474 to ea5d39a Compare November 11, 2024 10:06
Probably rebased wrongly?

Signed-off-by: Simon de Vlieger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants