File: //snap/google-cloud-cli/current/lib/googlecloudsdk/command_lib/interactive/bindings.py
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The gcloud interactive key bindings."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
import sys
from googlecloudsdk.command_lib.interactive import browser
from prompt_toolkit import keys
from prompt_toolkit.key_binding import manager
import six
class _KeyBinding(object):
"""Key binding base info to keep registered bindings and toolbar in sync.
Attributes:
key: The keys.Key.* object.
help_text: The UX help text.
label: The short word label for the bottom toolbar.
metavar: Display this value in GetLabel(markdown=True) instead of the real
value.
status: The bool => string toggle status map.
toggle: The bool toggle state.
"""
def __init__(self, key, help_text=None, label=None, metavar=None,
status=None, toggle=True):
self.key = key
self.help_text = help_text
self.label = label
self.metavar = metavar
self.status = status
self.toggle = toggle
def GetName(self):
"""Returns the binding display name."""
return re.sub('.*<(.*)>.*', r'\1',
six.text_type(self.key)).replace('C-', 'ctrl-')
def GetLabel(self, markdown=False):
"""Returns the key binding display label containing the name and value."""
if self.label is None and self.status is None:
return None
label = []
if markdown:
label.append('*')
label.append(self.GetName())
label.append(':')
if self.label:
label.append(self.label)
if self.status:
label.append(':')
if markdown:
label.append('*')
if self.status:
if markdown:
label.append('_')
label.append(self.metavar or 'STATE')
label.append('_')
else:
label.append(self.status[self.toggle])
return ''.join(label)
def GetHelp(self, markdown=False):
"""Returns the key help text."""
if not self.help_text:
return None
key = self.GetName()
if markdown:
key = '*{}*'.format(key)
return self.help_text.format(key=key)
def SetMode(self, cli):
"""Sets the toggle mode in the cli."""
del cli
def Handle(self, event):
"""Handles a bound key event."""
self.toggle = not self.toggle
self.SetMode(event.cli)
class _WebHelpKeyBinding(_KeyBinding):
"""The web help key binding."""
def __init__(self, key):
super(_WebHelpKeyBinding, self).__init__(
key=key,
label='web-help',
help_text=(
'Opens a web browser tab/window to display the complete man page '
'help for the current command. If there is no active web browser '
'(running in *ssh*(1) for example), then command specific help or '
'*man*(1) help is attempted.'
),
)
def Handle(self, event):
doc = event.cli.current_buffer.document
browser.OpenReferencePage(event.cli, doc.text, doc.cursor_position)
class _ContextKeyBinding(_KeyBinding):
"""set context key binding."""
def __init__(self, key):
super(_ContextKeyBinding, self).__init__(
key=key,
label='context',
help_text=(
'Sets the context for command input, so you won\'t have to re-type '
'common command prefixes at every prompt. The context is the '
'command line from just after the prompt up to the cursor.'
'\n+\n'
'For example, if you are about to work with `gcloud compute` for '
'a while, type *gcloud compute* and hit {key}. This will display '
'*gcloud compute* at subsequent prompts until the context is '
'changed.'
'\n+\n'
'Hit ctrl-c and {key} to clear the context, or edit a command line '
'and/or move the cursor and hit {key} to set a different context.'
),
)
def Handle(self, event):
event.cli.config.context = (
event.cli.current_buffer.document.text_before_cursor)
class _HelpKeyBinding(_KeyBinding):
"""The help key binding."""
def __init__(self, key, toggle=True):
super(_HelpKeyBinding, self).__init__(
key=key,
label='help',
toggle=toggle, status={False: 'OFF', True: 'ON'},
help_text=(
'Toggles the active help section, *ON* when enabled, *OFF* when '
'disabled.'
),
)
class _QuitKeyBinding(_KeyBinding):
"""The quit key binding."""
def __init__(self, key):
super(_QuitKeyBinding, self).__init__(
key=key,
label='quit',
help_text=(
'Exit.'
),
)
def Handle(self, event):
del event
sys.exit(1)
class _InterruptKeyBinding(_KeyBinding):
"""The interrupt (ctrl-c) key binding.
Catches control-C and clears the prompt input buffer and completer.
"""
def __init__(self, key):
super(_InterruptKeyBinding, self).__init__(
key=key,
)
def Handle(self, event):
event.cli.current_buffer.reset()
event.cli.completer.reset()
class _StopKeyBinding(_KeyBinding):
"""The stop (^Z) key binding.
This binding's sole purpose is to ignore ^Z and prevent it from echoing
in the prompt window.
"""
def __init__(self, key):
super(_StopKeyBinding, self).__init__(
key=key,
)
class KeyBindings(object):
"""All key bindings.
Attributes:
bindings: The list of key bindings in left to right order.
help_key: The help visibility key binding. True for ON, false for
OFF.
context_key: The command prefix context key that sets the context to the
command substring from the beginning of the input line to the current
cursor position.
web_help_key: The browse key binding that pops up the full reference
doc in a browser.
quit_key: The key binding that exits the shell.
"""
def __init__(self, help_mode=True):
"""Associates keys with handlers. Toggle states are reachable from here."""
# The actual key bindings. Changing keys.Keys.* here automatically
# propagates to the bottom toolbar labels.
self.help_key = _HelpKeyBinding(keys.Keys.F2, toggle=help_mode)
self.context_key = _ContextKeyBinding(keys.Keys.F7)
self.web_help_key = _WebHelpKeyBinding(keys.Keys.F8)
self.quit_key = _QuitKeyBinding(keys.Keys.F9)
self.interrupt_signal = _InterruptKeyBinding(keys.Keys.ControlC)
self.stop_signal = _StopKeyBinding(keys.Keys.ControlZ)
# This is the order of binding label appearance in the bottom toolbar.
self.bindings = [
self.help_key,
self.context_key,
self.web_help_key,
self.quit_key,
self.interrupt_signal,
self.stop_signal,
]
def MakeRegistry(self):
"""Makes and returns a key binding registry populated with the bindings."""
m = manager.KeyBindingManager(
enable_abort_and_exit_bindings=True,
enable_system_bindings=True,
enable_search=True,
enable_auto_suggest_bindings=True,)
for binding in self.bindings:
m.registry.add_binding(binding.key, eager=True)(binding.Handle)
return m.registry
def Initialize(self, cli):
"""Initialize key binding defaults in the cli."""
for binding in self.bindings:
binding.SetMode(cli)