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

Implement #26: Add toolbar menu. #45

Merged
merged 12 commits into from
Dec 15, 2015
6 changes: 3 additions & 3 deletions awsshell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ def main():
t = threading.Thread(target=write_doc_index, args=(doc_index_file,))
t.daemon = True
t.start()
completer = shellcomplete.AWSShellCompleter(
autocomplete.AWSCLIModelCompleter(index_data))
model_completer = autocomplete.AWSCLIModelCompleter(index_data)
completer = shellcomplete.AWSShellCompleter(model_completer)
history = InMemoryHistory()
shell = app.create_aws_shell(completer, history, doc_data)
shell = app.create_aws_shell(completer, model_completer, history, doc_data)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Pass AWSCLIModelCompleter to AWSShell: AWSShell now sets the new AWSCLIModelCompleter.match_fuzzy attribute.

Copy link
Member

Choose a reason for hiding this comment

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

The model completer is also available via the .completer property on shellcomplete.AWSShellCompleter. I think I still have some refactoring to do to get all the various autocompleters more coherent.

My line of thinking was that the ShellCompleter is the only thing the app needs to interact with for anything completion related, and the ShellCompleter can proxy to the model completer and server side completer as needed. I don't think I have the abstractions 100% right yet, but ideally I'd like to be able to just say completer.match_fuzzy and then internally, the ShellCompleter can set the match_fuzzy attribute on the model completer.

We don't have to do this now, but I think it would simplify things by not requiring the AWSShell class to take in an additional param in its __init__.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, I see, thanks. I'll add that to my TODO list.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Created #66 to track this work.

shell.run()


Expand Down
193 changes: 177 additions & 16 deletions awsshell/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,116 @@
from prompt_toolkit.interface import AbortAction, AcceptAction
from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.utils import Callback
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory

from awsshell.ui import create_default_layout
from awsshell.config import Config
from awsshell.keys import KeyManager
from awsshell.style import StyleFactory
from awsshell.toolbar import Toolbar


LOG = logging.getLogger(__name__)


def create_aws_shell(completer, history, docs):
return AWSShell(completer, history, docs)
def create_aws_shell(completer, model_completer, history, docs):
return AWSShell(completer, model_completer, history, docs)


class InputInterrupt(Exception):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We use a while True: loop to continue processing input from the user in the run method of app.py.

Some config option changes require us to kick out of this loop and refresh the AWSShell.cli:

  • enable_vi_bindings
  • show_completion_columns
  • show_help

"""Stops the input of commands.

Raising `InputInterrupt` is useful to force a cli rebuild, which is
sometimes necessary in order for config changes to take effect.
"""
pass


class AWSShell(object):
def __init__(self, completer, history, docs):
"""Encapsulates the ui, completer, command history, docs, and config.

Runs the input event loop and delegates the command execution to either
the `awscli` or the underlying shell.

:type refresh_cli: bool
:param refresh_cli: Flag to refresh the cli.

:type config_obj: :class:`configobj.ConfigObj`
:param config_obj: Contains the config information for reading and writing.

:type config_section: :class:`configobj.Section`
:param config_section: Convenience attribute to access the main section
of the config.

:type model_completer: :class:`AWSCLIModelCompleter`
:param model_completer: Matches input with completions. `AWSShell` sets
and gets the attribute `AWSCLIModelCompleter.match_fuzzy`.

:type enable_vi_bindings: bool
:param enable_vi_bindings: If True, enables Vi key bindings. Else, Emacs
key bindings are enabled.

:type show_completion_columns: bool
param show_completion_columns: If True, completions are shown in multiple
columns. Else, completions are shown in a single scrollable column.

:type show_help: bool
:param show_help: If True, shows the help pane. Else, hides the help pane.

:type theme: str
:param theme: The pygments theme.
"""

def __init__(self, completer, model_completer, history, docs):
self.completer = completer
self.model_completer = model_completer
self.history = history
self._cli = None
self._docs = docs
self.current_docs = u''
self.refresh_cli = False
self.load_config()

def load_config(self):
"""Loads the config from the config file or template."""
config = Config()
self.config_obj = config.load('awsshellrc')
self.config_section = self.config_obj['aws-shell']
self.model_completer.match_fuzzy = self.config_section.as_bool(
'match_fuzzy')
self.enable_vi_bindings = self.config_section.as_bool(
'enable_vi_bindings')
self.show_completion_columns = self.config_section.as_bool(
'show_completion_columns')
self.show_help = self.config_section.as_bool('show_help')
self.theme = self.config_section['theme']

def save_config(self):
"""Saves the config to the config file."""
self.config_section['match_fuzzy'] = self.model_completer.match_fuzzy
self.config_section['enable_vi_bindings'] = self.enable_vi_bindings
self.config_section['show_completion_columns'] = \
self.show_completion_columns
self.config_section['show_help'] = self.show_help
self.config_section['theme'] = self.theme
self.config_obj.write()

@property
def cli(self):
if self._cli is None:
self._cli = self.create_cli_interface()
if self._cli is None or self.refresh_cli:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We sometimes have to refresh the cli in order for certain option changes to kick in. Check out the comment on stop_input_and_refresh_cli.

self._cli = self.create_cli_interface(self.show_completion_columns)
self.refresh_cli = False
return self._cli

def run(self):
while True:
try:
document = self.cli.run()
text = document.text
except InputInterrupt:
pass
except (KeyboardInterrupt, EOFError):
self.save_config()
break
else:
if text.strip() in ['quit', 'exit']:
Expand Down Expand Up @@ -78,38 +157,118 @@ def run(self):
p = subprocess.Popen(full_cmd, shell=True)
p.communicate()

def create_layout(self):
def stop_input_and_refresh_cli(self):
"""Stops input by raising an `InputInterrupt`, forces a cli refresh.

The cli refresh is necessary because changing options such as key
bindings, single vs multi column menu completions, and the help pane
all require a rebuild.

:raises: :class:`InputInterrupt <exceptions.InputInterrupt>`.
"""
self.refresh_cli = True
self.cli.request_redraw()
raise InputInterrupt
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

stop_input_and_refresh_cli is called whenever the user toggles one of the following options:

  • enable_vi_bindings
  • show_completion_columns
  • show_help


def create_layout(self, display_completions_in_columns, toolbar):
return create_default_layout(
self, u'aws> ', reserve_space_for_menu=True,
display_completions_in_columns=True)
display_completions_in_columns=display_completions_in_columns,
get_bottom_toolbar_tokens=toolbar.handler)

def create_buffer(self, completer, history):
return Buffer(
history=history,
auto_suggest=AutoSuggestFromHistory(),
enable_history_search=True,
completer=completer,
complete_while_typing=Always(),
accept_action=AcceptAction.RETURN_DOCUMENT)

def create_application(self, completer, history):
key_bindings_registry = KeyBindingManager(
enable_vi_mode=True,
enable_system_bindings=False,
enable_open_in_editor=False).registry
def create_key_manager(self):
"""Creates the :class:`KeyManager`.

The inputs to KeyManager are expected to be callable, so we can't
use the standard @property and @attrib.setter for these attributes.
Lambdas cannot contain assignments so we're forced to define setters.

:rtype: :class:`KeyManager`
:return: A KeyManager with callables to set the toolbar options. Also
includes the method stop_input_and_refresh_cli to ensure certain
options take effect within the current session.
"""

def set_match_fuzzy(match_fuzzy):
"""Setter for fuzzy matching mode.

:type match_fuzzy: bool
:param match_fuzzy: The match fuzzy flag.
"""
self.model_completer.match_fuzzy = match_fuzzy

def set_enable_vi_bindings(enable_vi_bindings):
"""Setter for vi mode keybindings.

If vi mode is off, emacs mode is enabled by default by
`prompt_toolkit`.

:type enable_vi_bindings: bool
:param enable_vi_bindings: The enable Vi bindings flag.
"""
self.enable_vi_bindings = enable_vi_bindings

def set_show_completion_columns(show_completion_columns):
"""Setter for showing the completions in columns flag.

:type show_completion_columns: bool
:param show_completion_columns: The show completions in
multiple columns flag.
"""
self.show_completion_columns = show_completion_columns

def set_show_help(show_help):
"""Setter for showing the help container flag.

:type show_help: bool
:param show_help: The show help flag.
"""
self.show_help = show_help

return KeyManager(
lambda: self.model_completer.match_fuzzy, set_match_fuzzy,
lambda: self.enable_vi_bindings, set_enable_vi_bindings,
lambda: self.show_completion_columns, set_show_completion_columns,
lambda: self.show_help, set_show_help,
self.stop_input_and_refresh_cli)

def create_application(self, completer, history,
display_completions_in_columns):
self.key_manager = self.create_key_manager()
toolbar = Toolbar(
lambda: self.model_completer.match_fuzzy,
lambda: self.enable_vi_bindings,
lambda: self.show_completion_columns,
lambda: self.show_help)
style_factory = StyleFactory(self.theme)
buffers = {
'clidocs': Buffer(read_only=True)
}

return Application(
layout=self.create_layout(),
layout=self.create_layout(display_completions_in_columns, toolbar),
mouse_support=False,
style=style_factory.style,
buffers=buffers,
buffer=self.create_buffer(completer, history),
on_abort=AbortAction.RAISE_EXCEPTION,
on_exit=AbortAction.RAISE_EXCEPTION,
on_input_timeout=Callback(self.on_input_timeout),
key_bindings_registry=key_bindings_registry,
key_bindings_registry=self.key_manager.manager.registry,
)

def on_input_timeout(self, cli):
if not self.show_help:
return
document = cli.current_buffer.document
text = document.text
LOG.debug("document.text = %s", text)
Expand All @@ -129,11 +288,13 @@ def on_input_timeout(self, cli):
initial_document=Document(self.current_docs, cursor_position=0))
cli.request_redraw()

def create_cli_interface(self):
def create_cli_interface(self, display_completions_in_columns):
# A CommandLineInterface from prompt_toolkit
# accepts two things: an application and an
# event loop.
loop = create_eventloop()
app = self.create_application(self.completer, self.history)
app = self.create_application(self.completer,
self.history,
display_completions_in_columns)
cli = CommandLineInterface(application=app, eventloop=loop)
return cli
14 changes: 11 additions & 3 deletions awsshell/autocomplete.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import print_function
from awsshell.fuzzy import fuzzy_search
from awsshell.substring import substring_search


class AWSCLIModelCompleter(object):
Expand All @@ -9,7 +10,7 @@ class AWSCLIModelCompleter(object):
AWS service (which we pull through botocore's data loaders).

"""
def __init__(self, index_data):
def __init__(self, index_data, match_fuzzy=True):
self._index = index_data
self._root_name = 'aws'
self._global_options = index_data[self._root_name]['arguments']
Expand All @@ -22,6 +23,7 @@ def __init__(self, index_data):
self.last_option = ''
# This will get populated as a command is completed.
self.cmd_path = [self._current_name]
self.match_fuzzy = match_fuzzy

@property
def arg_metadata(self):
Expand Down Expand Up @@ -99,8 +101,14 @@ def autocomplete(self, line):
# We don't need to recompute this until the args are
# different.
all_args = self._get_all_args()
return fuzzy_search(last_word, all_args)
return fuzzy_search(last_word, self._current['commands'])
if self.match_fuzzy:
return fuzzy_search(last_word, all_args)
else:
return substring_search(last_word, all_args)
if self.match_fuzzy:
return fuzzy_search(last_word, self._current['commands'])
else:
return substring_search(last_word, self._current['commands'])

def _get_all_args(self):
if self._current['arguments'] != self._global_options:
Expand Down
20 changes: 20 additions & 0 deletions awsshell/awsshellrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[aws-shell]

# fuzzy or substring match.
match_fuzzy = True

# vi or emacs key bindings.
enable_vi_bindings = False

# multi or single column completion menu.
show_completion_columns = False

# show or hide the help pane.
show_help = True

# visual theme. possible values: manni, igor, xcode, vim,
# autumn,vs, rrt, native, perldoc, borland, tango, emacs,
# friendly, monokai, paraiso-dark, colorful, murphy, bw,
# pastie, paraiso-light, trac, default, fruity.
# to disable themes, set theme = none
theme = vim
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This theme will also be used by the lexer when we hook up #27.

Loading