diff options
author | Jonathan Slenders <jonathan@slenders.be> | 2016-06-17 21:29:08 +0200 |
---|---|---|
committer | Jonathan Slenders <jonathan@slenders.be> | 2016-06-17 21:29:08 +0200 |
commit | 329949deb597292271e11179c015c6c21546516f (patch) | |
tree | a3c2c75292940cf2b4c00a5510ef5e3ab575d5cf | |
parent | a6c27e63adef630dc78d4b3ec1c439a244f87813 (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.py | 4 | ||||
-rw-r--r-- | prompt_toolkit/interface.py | 81 | ||||
-rw-r--r-- | prompt_toolkit/key_binding/bindings/completion.py | 58 | ||||
-rw-r--r-- | prompt_toolkit/shortcuts.py | 2 |
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) |