Skip to content

Commit dca3384

Browse files
committed
Merge branch 'feature/26-toolbar'
* feature/26-toolbar: Fix merge conflict with package_data. Set separators above and below the help pane. Refactor getting and setting of config options code. Add styles to menu and toolbar. Add toolbar and hotkey support. Hook up config options. Add config and unit test. Add build_config_file_path method to refactor config file path code. Implement awslabs#28: Add auto suggest completions. Update to prompt-toolkit 0.52.
2 parents 1a8806d + 654c93a commit dca3384

21 files changed

+905
-37
lines changed

awsshell/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ def main():
5959
t = threading.Thread(target=write_doc_index, args=(doc_index_file,))
6060
t.daemon = True
6161
t.start()
62-
completer = shellcomplete.AWSShellCompleter(
63-
autocomplete.AWSCLIModelCompleter(index_data))
62+
model_completer = autocomplete.AWSCLIModelCompleter(index_data)
63+
completer = shellcomplete.AWSShellCompleter(model_completer)
6464
history = InMemoryHistory()
65-
shell = app.create_aws_shell(completer, history, doc_data)
65+
shell = app.create_aws_shell(completer, model_completer, history, doc_data)
6666
shell.run()
6767

6868

awsshell/app.py

Lines changed: 177 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,116 @@
1616
from prompt_toolkit.interface import AbortAction, AcceptAction
1717
from prompt_toolkit.key_binding.manager import KeyBindingManager
1818
from prompt_toolkit.utils import Callback
19+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
1920

2021
from awsshell.ui import create_default_layout
22+
from awsshell.config import Config
23+
from awsshell.keys import KeyManager
24+
from awsshell.style import StyleFactory
25+
from awsshell.toolbar import Toolbar
2126

2227

2328
LOG = logging.getLogger(__name__)
2429

2530

26-
def create_aws_shell(completer, history, docs):
27-
return AWSShell(completer, history, docs)
31+
def create_aws_shell(completer, model_completer, history, docs):
32+
return AWSShell(completer, model_completer, history, docs)
33+
34+
35+
class InputInterrupt(Exception):
36+
"""Stops the input of commands.
37+
38+
Raising `InputInterrupt` is useful to force a cli rebuild, which is
39+
sometimes necessary in order for config changes to take effect.
40+
"""
41+
pass
2842

2943

3044
class AWSShell(object):
31-
def __init__(self, completer, history, docs):
45+
"""Encapsulates the ui, completer, command history, docs, and config.
46+
47+
Runs the input event loop and delegates the command execution to either
48+
the `awscli` or the underlying shell.
49+
50+
:type refresh_cli: bool
51+
:param refresh_cli: Flag to refresh the cli.
52+
53+
:type config_obj: :class:`configobj.ConfigObj`
54+
:param config_obj: Contains the config information for reading and writing.
55+
56+
:type config_section: :class:`configobj.Section`
57+
:param config_section: Convenience attribute to access the main section
58+
of the config.
59+
60+
:type model_completer: :class:`AWSCLIModelCompleter`
61+
:param model_completer: Matches input with completions. `AWSShell` sets
62+
and gets the attribute `AWSCLIModelCompleter.match_fuzzy`.
63+
64+
:type enable_vi_bindings: bool
65+
:param enable_vi_bindings: If True, enables Vi key bindings. Else, Emacs
66+
key bindings are enabled.
67+
68+
:type show_completion_columns: bool
69+
param show_completion_columns: If True, completions are shown in multiple
70+
columns. Else, completions are shown in a single scrollable column.
71+
72+
:type show_help: bool
73+
:param show_help: If True, shows the help pane. Else, hides the help pane.
74+
75+
:type theme: str
76+
:param theme: The pygments theme.
77+
"""
78+
79+
def __init__(self, completer, model_completer, history, docs):
3280
self.completer = completer
81+
self.model_completer = model_completer
3382
self.history = history
3483
self._cli = None
3584
self._docs = docs
3685
self.current_docs = u''
86+
self.refresh_cli = False
87+
self.load_config()
88+
89+
def load_config(self):
90+
"""Loads the config from the config file or template."""
91+
config = Config()
92+
self.config_obj = config.load('awsshellrc')
93+
self.config_section = self.config_obj['aws-shell']
94+
self.model_completer.match_fuzzy = self.config_section.as_bool(
95+
'match_fuzzy')
96+
self.enable_vi_bindings = self.config_section.as_bool(
97+
'enable_vi_bindings')
98+
self.show_completion_columns = self.config_section.as_bool(
99+
'show_completion_columns')
100+
self.show_help = self.config_section.as_bool('show_help')
101+
self.theme = self.config_section['theme']
102+
103+
def save_config(self):
104+
"""Saves the config to the config file."""
105+
self.config_section['match_fuzzy'] = self.model_completer.match_fuzzy
106+
self.config_section['enable_vi_bindings'] = self.enable_vi_bindings
107+
self.config_section['show_completion_columns'] = \
108+
self.show_completion_columns
109+
self.config_section['show_help'] = self.show_help
110+
self.config_section['theme'] = self.theme
111+
self.config_obj.write()
37112

38113
@property
39114
def cli(self):
40-
if self._cli is None:
41-
self._cli = self.create_cli_interface()
115+
if self._cli is None or self.refresh_cli:
116+
self._cli = self.create_cli_interface(self.show_completion_columns)
117+
self.refresh_cli = False
42118
return self._cli
43119

44120
def run(self):
45121
while True:
46122
try:
47123
document = self.cli.run()
48124
text = document.text
125+
except InputInterrupt:
126+
pass
49127
except (KeyboardInterrupt, EOFError):
128+
self.save_config()
50129
break
51130
else:
52131
if text.strip() in ['quit', 'exit']:
@@ -78,38 +157,118 @@ def run(self):
78157
p = subprocess.Popen(full_cmd, shell=True)
79158
p.communicate()
80159

81-
def create_layout(self):
160+
def stop_input_and_refresh_cli(self):
161+
"""Stops input by raising an `InputInterrupt`, forces a cli refresh.
162+
163+
The cli refresh is necessary because changing options such as key
164+
bindings, single vs multi column menu completions, and the help pane
165+
all require a rebuild.
166+
167+
:raises: :class:`InputInterrupt <exceptions.InputInterrupt>`.
168+
"""
169+
self.refresh_cli = True
170+
self.cli.request_redraw()
171+
raise InputInterrupt
172+
173+
def create_layout(self, display_completions_in_columns, toolbar):
82174
return create_default_layout(
83175
self, u'aws> ', reserve_space_for_menu=True,
84-
display_completions_in_columns=True)
176+
display_completions_in_columns=display_completions_in_columns,
177+
get_bottom_toolbar_tokens=toolbar.handler)
85178

86179
def create_buffer(self, completer, history):
87180
return Buffer(
88181
history=history,
182+
auto_suggest=AutoSuggestFromHistory(),
183+
enable_history_search=True,
89184
completer=completer,
90185
complete_while_typing=Always(),
91186
accept_action=AcceptAction.RETURN_DOCUMENT)
92187

93-
def create_application(self, completer, history):
94-
key_bindings_registry = KeyBindingManager(
95-
enable_vi_mode=True,
96-
enable_system_bindings=False,
97-
enable_open_in_editor=False).registry
188+
def create_key_manager(self):
189+
"""Creates the :class:`KeyManager`.
190+
191+
The inputs to KeyManager are expected to be callable, so we can't
192+
use the standard @property and @attrib.setter for these attributes.
193+
Lambdas cannot contain assignments so we're forced to define setters.
194+
195+
:rtype: :class:`KeyManager`
196+
:return: A KeyManager with callables to set the toolbar options. Also
197+
includes the method stop_input_and_refresh_cli to ensure certain
198+
options take effect within the current session.
199+
"""
200+
201+
def set_match_fuzzy(match_fuzzy):
202+
"""Setter for fuzzy matching mode.
203+
204+
:type match_fuzzy: bool
205+
:param match_fuzzy: The match fuzzy flag.
206+
"""
207+
self.model_completer.match_fuzzy = match_fuzzy
208+
209+
def set_enable_vi_bindings(enable_vi_bindings):
210+
"""Setter for vi mode keybindings.
211+
212+
If vi mode is off, emacs mode is enabled by default by
213+
`prompt_toolkit`.
214+
215+
:type enable_vi_bindings: bool
216+
:param enable_vi_bindings: The enable Vi bindings flag.
217+
"""
218+
self.enable_vi_bindings = enable_vi_bindings
219+
220+
def set_show_completion_columns(show_completion_columns):
221+
"""Setter for showing the completions in columns flag.
222+
223+
:type show_completion_columns: bool
224+
:param show_completion_columns: The show completions in
225+
multiple columns flag.
226+
"""
227+
self.show_completion_columns = show_completion_columns
228+
229+
def set_show_help(show_help):
230+
"""Setter for showing the help container flag.
231+
232+
:type show_help: bool
233+
:param show_help: The show help flag.
234+
"""
235+
self.show_help = show_help
236+
237+
return KeyManager(
238+
lambda: self.model_completer.match_fuzzy, set_match_fuzzy,
239+
lambda: self.enable_vi_bindings, set_enable_vi_bindings,
240+
lambda: self.show_completion_columns, set_show_completion_columns,
241+
lambda: self.show_help, set_show_help,
242+
self.stop_input_and_refresh_cli)
243+
244+
def create_application(self, completer, history,
245+
display_completions_in_columns):
246+
self.key_manager = self.create_key_manager()
247+
toolbar = Toolbar(
248+
lambda: self.model_completer.match_fuzzy,
249+
lambda: self.enable_vi_bindings,
250+
lambda: self.show_completion_columns,
251+
lambda: self.show_help)
252+
style_factory = StyleFactory(self.theme)
98253
buffers = {
99254
'clidocs': Buffer(read_only=True)
100255
}
101256

102257
return Application(
103-
layout=self.create_layout(),
258+
layout=self.create_layout(display_completions_in_columns, toolbar),
259+
mouse_support=False,
260+
style=style_factory.style,
104261
buffers=buffers,
105262
buffer=self.create_buffer(completer, history),
106263
on_abort=AbortAction.RAISE_EXCEPTION,
107264
on_exit=AbortAction.RAISE_EXCEPTION,
108265
on_input_timeout=Callback(self.on_input_timeout),
109-
key_bindings_registry=key_bindings_registry,
266+
key_bindings_registry=self.key_manager.manager.registry,
110267
)
111268

112269
def on_input_timeout(self, cli):
270+
if not self.show_help:
271+
return
113272
document = cli.current_buffer.document
114273
text = document.text
115274
LOG.debug("document.text = %s", text)
@@ -129,11 +288,13 @@ def on_input_timeout(self, cli):
129288
initial_document=Document(self.current_docs, cursor_position=0))
130289
cli.request_redraw()
131290

132-
def create_cli_interface(self):
291+
def create_cli_interface(self, display_completions_in_columns):
133292
# A CommandLineInterface from prompt_toolkit
134293
# accepts two things: an application and an
135294
# event loop.
136295
loop = create_eventloop()
137-
app = self.create_application(self.completer, self.history)
296+
app = self.create_application(self.completer,
297+
self.history,
298+
display_completions_in_columns)
138299
cli = CommandLineInterface(application=app, eventloop=loop)
139300
return cli

awsshell/autocomplete.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import print_function
22
from awsshell.fuzzy import fuzzy_search
3+
from awsshell.substring import substring_search
34

45

56
class AWSCLIModelCompleter(object):
@@ -9,7 +10,7 @@ class AWSCLIModelCompleter(object):
910
AWS service (which we pull through botocore's data loaders).
1011
1112
"""
12-
def __init__(self, index_data):
13+
def __init__(self, index_data, match_fuzzy=True):
1314
self._index = index_data
1415
self._root_name = 'aws'
1516
self._global_options = index_data[self._root_name]['arguments']
@@ -22,6 +23,7 @@ def __init__(self, index_data):
2223
self.last_option = ''
2324
# This will get populated as a command is completed.
2425
self.cmd_path = [self._current_name]
26+
self.match_fuzzy = match_fuzzy
2527

2628
@property
2729
def arg_metadata(self):
@@ -99,8 +101,14 @@ def autocomplete(self, line):
99101
# We don't need to recompute this until the args are
100102
# different.
101103
all_args = self._get_all_args()
102-
return fuzzy_search(last_word, all_args)
103-
return fuzzy_search(last_word, self._current['commands'])
104+
if self.match_fuzzy:
105+
return fuzzy_search(last_word, all_args)
106+
else:
107+
return substring_search(last_word, all_args)
108+
if self.match_fuzzy:
109+
return fuzzy_search(last_word, self._current['commands'])
110+
else:
111+
return substring_search(last_word, self._current['commands'])
104112

105113
def _get_all_args(self):
106114
if self._current['arguments'] != self._global_options:

awsshell/awsshellrc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[aws-shell]
2+
3+
# fuzzy or substring match.
4+
match_fuzzy = True
5+
6+
# vi or emacs key bindings.
7+
enable_vi_bindings = False
8+
9+
# multi or single column completion menu.
10+
show_completion_columns = False
11+
12+
# show or hide the help pane.
13+
show_help = True
14+
15+
# visual theme. possible values: manni, igor, xcode, vim,
16+
# autumn,vs, rrt, native, perldoc, borland, tango, emacs,
17+
# friendly, monokai, paraiso-dark, colorful, murphy, bw,
18+
# pastie, paraiso-light, trac, default, fruity.
19+
# to disable themes, set theme = none
20+
theme = vim

0 commit comments

Comments
 (0)