NcursesDisplay.menu: treat ESC as cancel
diff --git a/certbot/display/util.py b/certbot/display/util.py
index b4004997f60..39486b2bde9 100644
--- a/certbot/display/util.py
+++ b/certbot/display/util.py
@@ -1,4 +1,5 @@
 """Certbot display."""
+import logging
 import os
 import textwrap
 
@@ -9,6 +10,9 @@
 from certbot import errors
 from certbot.display import completer
 
+
+logger = logging.getLogger(__name__)
+
 WIDTH = 72
 HEIGHT = 20
 
@@ -29,6 +33,9 @@
 HELP = "help"
 """Display exit code when for when the user requests more help."""
 
+ESC = "esc"
+"""Display exit code when the user hits Escape"""
+
 
 def _wrap_lines(msg):
     """Format lines nicely to 80 chars.
@@ -51,6 +58,24 @@ def _wrap_lines(msg):
 
     return os.linesep.join(fixed_l)
 
+
+def _clean(dialog_result):
+    """Treat sundy python-dialog return codes as CANCEL
+
+    :param tuple dialog_result: (code, result)
+    :returns: the argument but with unknown codes set to -1 (Error)
+    :rtype: tuple
+    """
+    code, result = dialog_result
+    if code in (OK, HELP):
+        return dialog_result
+    elif code in (CANCEL, ESC):
+        return (CANCEL, result)
+    else:
+        logger.debug("Surprising dialog return code %s", code)
+        return (CANCEL, result)
+
+
 @zope.interface.implementer(interfaces.IDisplay)
 class NcursesDisplay(object):
     """Ncurses-based display."""
@@ -58,6 +83,7 @@ class NcursesDisplay(object):
     def __init__(self, width=WIDTH, height=HEIGHT):
         super(NcursesDisplay, self).__init__()
         self.dialog = dialog.Dialog()
+        assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?"
         self.width = width
         self.height = height
 
@@ -92,7 +118,7 @@ def menu(self, message, choices, ok_label="OK", cancel_label="Cancel",
         :param dict unused_kwargs: absorbs default / cli_args
 
         :returns: tuple of the form (`code`, `index`) where
-            `code` - int display exit code
+            `code` - display exit code
             `int` - index of the selected item
         :rtype: tuple
 
@@ -111,7 +137,7 @@ def menu(self, message, choices, ok_label="OK", cancel_label="Cancel",
         # Can accept either tuples or just the actual choices
         if choices and isinstance(choices[0], tuple):
             # pylint: disable=star-args
-            code, selection = self.dialog.menu(message, **menu_options)
+            code, selection = _clean(self.dialog.menu(message, **menu_options))
 
             # Return the selection index
             for i, choice in enumerate(choices):
@@ -126,9 +152,9 @@ def menu(self, message, choices, ok_label="OK", cancel_label="Cancel",
                 (str(i), choice) for i, choice in enumerate(choices, 1)
             ]
             # pylint: disable=star-args
-            code, index = self.dialog.menu(message, **menu_options)
+            code, index = _clean(self.dialog.menu(message, **menu_options))
 
-            if code == CANCEL:
+            if code == CANCEL or index == "":
                 return code, -1
 
             return code, int(index) - 1
@@ -140,7 +166,7 @@ def input(self, message, **unused_kwargs):
         :param dict _kwargs: absorbs default / cli_args
 
         :returns: tuple of the form (`code`, `string`) where
-            `code` - int display exit code
+            `code` - display exit code
             `string` - input entered by the user
 
         """
@@ -148,7 +174,7 @@ def input(self, message, **unused_kwargs):
         # each section takes at least one line, plus extras if it's longer than self.width
         wordlines = [1 + (len(section) / self.width) for section in sections]
         height = 6 + sum(wordlines) + len(sections)
-        return self.dialog.inputbox(message, width=self.width, height=height)
+        return _clean(self.dialog.inputbox(message, width=self.width, height=height))
 
     def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
         """Display a Yes/No dialog box.
@@ -179,13 +205,13 @@ def checklist(self, message, tags, default_status=True, **unused_kwargs):
 
 
         :returns: tuple of the form (`code`, `list_tags`) where
-            `code` - int display exit code
+            `code` - display exit code
             `list_tags` - list of str tags selected by the user
 
         """
         choices = [(tag, "", default_status) for tag in tags]
-        return self.dialog.checklist(
-            message, width=self.width, height=self.height, choices=choices)
+        return _clean(self.dialog.checklist(
+            message, width=self.width, height=self.height, choices=choices))
 
     def directory_select(self, message, **unused_kwargs):
         """Display a directory selection screen.
@@ -193,14 +219,14 @@ def directory_select(self, message, **unused_kwargs):
         :param str message: prompt to give the user
 
         :returns: tuple of the form (`code`, `string`) where
-            `code` - int display exit code
+            `code` - display exit code
             `string` - input entered by the user
 
         """
         root_directory = os.path.abspath(os.sep)
-        return self.dialog.dselect(
+        return _clean(self.dialog.dselect(
             filepath=root_directory, width=self.width,
-            height=self.height, help_button=True, title=message)
+            height=self.height, help_button=True, title=message))
 
 
 @zope.interface.implementer(interfaces.IDisplay)
@@ -355,7 +381,7 @@ def directory_select(self, message, **unused_kwargs):
         :param str message: prompt to give the user
 
         :returns: tuple of the form (`code`, `string`) where
-            `code` - int display exit code
+            `code` - display exit code
             `string` - input entered by the user
 
         """
diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py
index 4a38803d198..a6ced90ab2e 100644
--- a/certbot/tests/display/util_test.py
+++ b/certbot/tests/display/util_test.py
@@ -96,6 +96,7 @@ def test_menu_desc_only_cancel(self, mock_menu):
     @mock.patch("certbot.display.util."
                 "dialog.Dialog.inputbox")
     def test_input(self, mock_input):
+        mock_input.return_value = (mock.MagicMock(), mock.MagicMock())
         self.displayer.input("message")
         self.assertEqual(mock_input.call_count, 1)
 
@@ -112,6 +113,7 @@ def test_yesno(self, mock_yesno):
     @mock.patch("certbot.display.util."
                 "dialog.Dialog.checklist")
     def test_checklist(self, mock_checklist):
+        mock_checklist.return_value = (mock.MagicMock(), mock.MagicMock())
         self.displayer.checklist("message", TAGS)
 
         choices = [
@@ -125,6 +127,7 @@ def test_checklist(self, mock_checklist):
 
     @mock.patch("certbot.display.util.dialog.Dialog.dselect")
     def test_directory_select(self, mock_dselect):
+        mock_dselect.return_value = (mock.MagicMock(), mock.MagicMock())
         self.displayer.directory_select("message")
         self.assertEqual(mock_dselect.call_count, 1)
 

Currently it will fire a weird traceback like:

```
File "/home/ubuntu/letsencrypt/letsencrypt/plugins/selection.py", line 113, in choose_plugin
    code, index = disp.menu(question, opts, help_label="More Info")
File "/home/ubuntu/letsencrypt/letsencrypt/display/util.py", line 129, in menu
    return code, int(index) - 1
ValueError: invalid literal for int() with base 10: ''
```
