Repository: mitmproxy/mitmproxy

Issue:
console: notifications cope very poorly with long data
##### Steps to reproduce the problem:

When we send a long notification to the statusbar - e.g. a traceback from some internal error - the statusbar gets messed up. We should snip this to the first line, and invite the user to view the rest in the event log. 

This behaviour can be seen in, e.g. #1431



Discussion:
Working on it.

Resolving patch:
diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py
--- a/mitmproxy/tools/console/master.py
+++ b/mitmproxy/tools/console/master.py
@@ -94,7 +94,9 @@ def sig_add_log(self, event_store, entry: log.LogEntry):
                 self.start_err = entry
             else:
                 signals.status_message.send(
-                    message=(entry.level, "{}: {}".format(entry.level.title(), entry.msg)),
+                    message=(entry.level,
+                             "{}: {}".format(entry.level.title(),
+                                             str(entry.msg).lstrip())),
                     expire=5
                 )
 
diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py
--- a/mitmproxy/tools/console/statusbar.py
+++ b/mitmproxy/tools/console/statusbar.py
@@ -49,7 +49,8 @@ def __init__(self, master):
     def sig_message(self, sender, message, expire=1):
         if self.prompting:
             return
-        w = urwid.Text(message)
+        cols, _ = self.master.ui.get_cols_rows()
+        w = urwid.Text(self.shorten_message(message, cols))
         self._w = w
         if expire:
             def cb(*args):
@@ -60,6 +61,36 @@ def cb(*args):
     def prep_prompt(self, p):
         return p.strip() + ": "
 
+    def shorten_message(self, msg, max_width):
+        """
+        Shorten message so that it fits into a single line in the statusbar.
+        """
+        if isinstance(msg, tuple):
+            disp_attr, msg_text = msg
+        elif isinstance(msg, str):
+            disp_attr, msg_text = None, msg
+        else:
+            return msg
+        msg_end = "\u2026"  # unicode ellipsis for the end of shortened message
+        prompt = "(more in eventlog)"
+
+        msg_lines = msg_text.split("\n")
+        first_line = msg_lines[0]
+        if len(msg_lines) > 1:
+            # First line of messages with a few lines must end with prompt.
+            line_length = len(first_line) + len(prompt)
+        else:
+            line_length = len(first_line)
+
+        if line_length > max_width:
+            shortening_index = max(0, max_width - len(prompt) - len(msg_end))
+            first_line = first_line[:shortening_index] + msg_end
+        else:
+            if len(msg_lines) == 1:
+                prompt = ""
+
+        return [(disp_attr, first_line), ("warn", prompt)]
+
     def sig_prompt(self, sender, prompt, text, callback, args=()):
         signals.focus.send(self, section="footer")
         self._w = urwid.Edit(self.prep_prompt(prompt), text or "")


Test patch:
diff --git a/test/mitmproxy/tools/console/test_statusbar.py b/test/mitmproxy/tools/console/test_statusbar.py
--- a/test/mitmproxy/tools/console/test_statusbar.py
+++ b/test/mitmproxy/tools/console/test_statusbar.py
@@ -1,3 +1,5 @@
+import pytest
+
 from mitmproxy import options
 from mitmproxy.tools.console import statusbar, master
 
@@ -31,3 +33,29 @@ def test_statusbar(monkeypatch):
 
     bar = statusbar.StatusBar(m)  # this already causes a redraw
     assert bar.ib._w
+
+
+@pytest.mark.parametrize("message,ready_message", [
+    ("", [(None, ""), ("warn", "")]),
+    (("info", "Line fits into statusbar"), [("info", "Line fits into statusbar"),
+                                            ("warn", "")]),
+    ("Line doesn't fit into statusbar", [(None, "Line doesn'\u2026"),
+                                         ("warn", "(more in eventlog)")]),
+    (("alert", "Two lines.\nFirst fits"), [("alert", "Two lines."),
+                                           ("warn", "(more in eventlog)")]),
+    ("Two long lines\nFirst doesn't fit", [(None, "Two long li\u2026"),
+                                           ("warn", "(more in eventlog)")])
+])
+def test_shorten_message(message, ready_message):
+    o = options.Options()
+    m = master.ConsoleMaster(o)
+    ab = statusbar.ActionBar(m)
+    assert ab.shorten_message(message, max_width=30) == ready_message
+
+
+def test_shorten_message_narrow():
+    o = options.Options()
+    m = master.ConsoleMaster(o)
+    ab = statusbar.ActionBar(m)
+    shorten_msg = ab.shorten_message("error", max_width=4)
+    assert shorten_msg == [(None, "\u2026"), ("warn", "(more in eventlog)")]