summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonathan Slenders <jonathan@slenders.be>2016-06-17 21:29:08 +0200
committerJonathan Slenders <jonathan@slenders.be>2016-06-17 21:29:08 +0200
commit329949deb597292271e11179c015c6c21546516f (patch)
treea3c2c75292940cf2b4c00a5510ef5e3ab575d5cf
parenta6c27e63adef630dc78d4b3ec1c439a244f87813 (diff)
A few changes regarding the display of readline-like autocompletions.
- Bugfix for Python2: math.ceil can return a float. (instead of an int.) - Added erase_when_done option to the "Application" class. - Added CommandeLineInterface.run_application_generator function for easier writing interaction with subapplications (without the callback hell.) - Improved the user experience of readline-line completions.
-rw-r--r--prompt_toolkit/application.py4
-rw-r--r--prompt_toolkit/interface.py81
-rw-r--r--prompt_toolkit/key_binding/bindings/completion.py58
-rw-r--r--prompt_toolkit/shortcuts.py2
4 files changed, 108 insertions, 37 deletions
diff --git a/prompt_toolkit/application.py b/prompt_toolkit/application.py
index f47a033e..e2489e37 100644
--- a/prompt_toolkit/application.py
+++ b/prompt_toolkit/application.py
@@ -56,6 +56,7 @@ class Application(object):
:param on_exit: What to do when Control-D is pressed.
:param use_alternate_screen: When True, run the application on the alternate screen buffer.
:param get_title: Callable that returns the current title to be displayed in the terminal.
+ :param erase_when_done: (bool) Clear the application output when it finishes.
Filters:
@@ -88,6 +89,7 @@ class Application(object):
get_title=None,
paste_mode=False, ignore_case=False, editing_mode=EditingMode.EMACS,
+ erase_when_done=False,
on_input_timeout=None, on_start=None, on_stop=None,
on_reset=None, on_initialize=None, on_buffer_changed=None,
@@ -111,6 +113,7 @@ class Application(object):
assert isinstance(editing_mode, six.string_types)
assert on_input_timeout is None or callable(on_input_timeout)
assert style is None or isinstance(style, Style)
+ assert isinstance(erase_when_done, bool)
assert on_start is None or callable(on_start)
assert on_stop is None or callable(on_stop)
@@ -161,6 +164,7 @@ class Application(object):
self.paste_mode = paste_mode
self.ignore_case = ignore_case
self.editing_mode = editing_mode
+ self.erase_when_done = erase_when_done
def dummy_handler(cli):
" Dummy event handler. "
diff --git a/prompt_toolkit/interface.py b/prompt_toolkit/interface.py
index 78993c37..4d803eba 100644
--- a/prompt_toolkit/interface.py
+++ b/prompt_toolkit/interface.py
@@ -3,6 +3,7 @@ The main `CommandLineInterface` class and logic.
"""
from __future__ import unicode_literals
+import datetime
import functools
import os
import signal
@@ -10,8 +11,8 @@ import six
import sys
import textwrap
import threading
+import types
import weakref
-import datetime
from subprocess import Popen
@@ -461,7 +462,9 @@ class CommandLineInterface(object):
"""
raise NotImplementedError
- def run_sub_application(self, application, done_callback=None, erase_when_done=False):
+ def run_sub_application(self, application, done_callback=None, erase_when_done=False,
+ _from_application_generator=False):
+ # `erase_when_done` is deprecated, set Application.erase_when_done instead.
"""
Run a sub :class:`~prompt_toolkit.application.Application`.
@@ -477,10 +480,6 @@ class CommandLineInterface(object):
only a proxy to our main event loop. The reason is that calling
'stop' --which returns the result of an application when it's
done-- is handled differently.
-
- :param erase_when_done: Explicitely erase the sub application when
- done. (This has no effect if the sub application runs in an
- alternate screen.)
"""
assert isinstance(application, Application)
assert done_callback is None or callable(done_callback)
@@ -489,7 +488,8 @@ class CommandLineInterface(object):
raise RuntimeError('Another sub application started already.')
# Erase current application.
- self.renderer.erase()
+ if not _from_application_generator:
+ self.renderer.erase()
# Callback when the sub app is done.
def done():
@@ -497,7 +497,7 @@ class CommandLineInterface(object):
# and reset the renderer. (This reset will also quit the alternate
# screen, if the sub application used that.)
sub_cli._redraw()
- if erase_when_done:
+ if erase_when_done or application.erase_when_done:
sub_cli.renderer.erase()
sub_cli.renderer.reset()
sub_cli._is_running = False # Don't render anymore.
@@ -505,8 +505,9 @@ class CommandLineInterface(object):
self._sub_cli = None
# Restore main application.
- self.renderer.request_absolute_cursor_position()
- self._redraw()
+ if not _from_application_generator:
+ self.renderer.request_absolute_cursor_position()
+ self._redraw()
# Deliver result.
if done_callback:
@@ -607,11 +608,12 @@ class CommandLineInterface(object):
self.renderer.reset() # Make sure to disable mouse mode, etc...
else:
self.renderer.erase()
+ self._return_value = None
# Run system command.
with self.input.cooked_mode():
result = func()
- self._return_value = None
+
# Redraw interface again.
self.renderer.reset()
@@ -620,6 +622,63 @@ class CommandLineInterface(object):
return result
+ def run_application_generator(self, coroutine, render_cli_done=False):
+ """
+ EXPERIMENTAL
+ Like `run_in_terminal`, but takes a generator that can yield Application instances.
+
+ Example:
+
+ def f():
+ yield Application1(...)
+ print('...')
+ yield Application2(...)
+ cli.run_in_terminal_async(f)
+
+ The values which are yielded by the given coroutine are supposed to be
+ `Application` instances that run in the current CLI, all other code is
+ supposed to be CPU bound, so except for yielding the applications,
+ there should not be any user interaction or I/O in the given function.
+ """
+ # Draw interface in 'done' state, or erase.
+ if render_cli_done:
+ self._return_value = True
+ self._redraw()
+ self.renderer.reset() # Make sure to disable mouse mode, etc...
+ else:
+ self.renderer.erase()
+ self._return_value = None
+
+ # Loop through the generator.
+ g = coroutine()
+ assert isinstance(g, types.GeneratorType)
+
+ def step_next(send_value=None):
+ " Execute next step of the coroutine."
+ try:
+ # Run until next yield, in cooked mode.
+ with self.input.cooked_mode():
+ result = g.send(send_value)
+ except StopIteration:
+ done()
+ except:
+ done()
+ raise
+ else:
+ # Process yielded value from coroutine.
+ assert isinstance(result, Application)
+ self.run_sub_application(result, done_callback=step_next,
+ _from_application_generator=True)
+
+ def done():
+ # Redraw interface again.
+ self.renderer.reset()
+ self.renderer.request_absolute_cursor_position()
+ self._redraw()
+
+ # Start processing coroutine.
+ step_next()
+
def run_system_command(self, command):
"""
Run system command (While hiding the prompt. When finished, all the
diff --git a/prompt_toolkit/key_binding/bindings/completion.py b/prompt_toolkit/key_binding/bindings/completion.py
index fb427aa8..6c7a9891 100644
--- a/prompt_toolkit/key_binding/bindings/completion.py
+++ b/prompt_toolkit/key_binding/bindings/completion.py
@@ -93,14 +93,15 @@ def _display_completions_like_readline(cli, completions):
max(get_cwidth(c.text) for c in completions) + 1)
column_count = max(1, term_width // max_compl_width)
completions_per_page = column_count * (term_height - 1)
- page_count = math.ceil(len(completions) / float(completions_per_page))
+ page_count = int(math.ceil(len(completions) / float(completions_per_page)))
+ # Note: math.ceil can return float on Python2.
def display(page):
# Display completions.
page_completions = completions[page * completions_per_page:
(page+1) * completions_per_page]
- page_row_count = math.ceil(len(page_completions) / float(column_count))
+ page_row_count = int(math.ceil(len(page_completions) / float(column_count)))
page_columns = [page_completions[i * page_row_count:(i+1) * page_row_count]
for i in range(column_count)]
@@ -115,29 +116,30 @@ def _display_completions_like_readline(cli, completions):
cli.output.write(''.join(result))
cli.output.flush()
- cli.output.write('\n'); cli.output.flush()
-
- if len(completions) > completions_per_page:
- # Ask confirmation if it doesn't fit on the screen.
- page_counter = [0]
- def display_page(result):
- if result:
- cli.run_in_terminal(lambda: display(page_counter[0]))
-
- # Display --MORE-- and go to the next page.
- page_counter[0] += 1
- if page_counter[0] < page_count:
- cli.run_sub_application(
- _create_more_application(),
- done_callback=display_page, erase_when_done=True)
-
- message = 'Display all {} possibilities? (y on n) '.format(len(completions))
- cli.run_sub_application(
- create_confirm_application(message),
- done_callback=display_page, erase_when_done=True)
- else:
- # Display all completions.
- cli.run_in_terminal(lambda: display(0))
+ # User interaction through an application generator function.
+ def run():
+ if len(completions) > completions_per_page:
+ # Ask confirmation if it doesn't fit on the screen.
+ message = 'Display all {} possibilities? (y on n) '.format(len(completions))
+ confirm = yield create_confirm_application(message)
+
+ if confirm:
+ # Display pages.
+ for page in range(page_count):
+ display(page)
+
+ if page != page_count - 1:
+ # Display --MORE-- and go to the next page.
+ show_more = yield _create_more_application()
+ if not show_more:
+ return
+ else:
+ cli.output.write('\n'); cli.output.flush()
+ else:
+ # Display all completions.
+ display(0)
+
+ cli.run_application_generator(run, render_cli_done=True)
def _create_more_application():
@@ -148,7 +150,10 @@ def _create_more_application():
registry = Registry()
@registry.add_binding(' ')
+ @registry.add_binding('y')
+ @registry.add_binding('Y')
@registry.add_binding(Keys.ControlJ)
+ @registry.add_binding(Keys.ControlI) # Tab.
def _(event):
event.cli.set_return_value(True)
@@ -160,4 +165,5 @@ def _create_more_application():
def _(event):
event.cli.set_return_value(False)
- return create_prompt_application('--MORE--', key_bindings_registry=registry)
+ return create_prompt_application(
+ '--MORE--', key_bindings_registry=registry, erase_when_done=True)
diff --git a/prompt_toolkit/shortcuts.py b/prompt_toolkit/shortcuts.py
index 7f9ae77c..5ca9ecd1 100644
--- a/prompt_toolkit/shortcuts.py
+++ b/prompt_toolkit/shortcuts.py
@@ -384,6 +384,7 @@ def create_prompt_application(
on_abort=AbortAction.RAISE_EXCEPTION,
on_exit=AbortAction.RAISE_EXCEPTION,
accept_action=AcceptAction.RETURN_DOCUMENT,
+ erase_when_done=False,
default=''):
"""
Create an :class:`~Application` instance for a prompt.
@@ -497,6 +498,7 @@ def create_prompt_application(
get_title=get_title,
mouse_support=mouse_support,
editing_mode=editing_mode,
+ erase_when_done=erase_when_done,
on_abort=on_abort,
on_exit=on_exit)