dr1p_pymux/pymux/key_bindings.py
2022-08-15 19:23:49 -05:00

261 lines
8.7 KiB
Python

"""
Key bindings.
"""
from __future__ import unicode_literals
from prompt_toolkit.filters import has_focus, Condition, has_selection
from prompt_toolkit.keys import Keys
from prompt_toolkit.selection import SelectionType
from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
from .enums import COMMAND, PROMPT
from .filters import WaitsForConfirmation, HasPrefix, InScrollBufferNotSearching
from .key_mappings import pymux_key_to_prompt_toolkit_key_sequence
from .commands.commands import call_command_handler
import six
__all__ = (
'PymuxKeyBindings',
)
class PymuxKeyBindings(object):
"""
Pymux key binding manager.
"""
def __init__(self, pymux):
self.pymux = pymux
def get_search_state():
" Return the currently active SearchState. (The one for the focused pane.) "
return pymux.arrangement.get_active_pane().search_state
self.custom_key_bindings = KeyBindings()
self.key_bindings = merge_key_bindings([
self._load_builtins(),
self.custom_key_bindings,
])
self._prefix = ('c-b', )
self._prefix_binding = None
# Load initial bindings.
self._load_prefix_binding()
# Custom user configured key bindings.
# { (needs_prefix, key) -> (command, handler) }
self.custom_bindings = {}
def _load_prefix_binding(self):
"""
Load the prefix key binding.
"""
pymux = self.pymux
# Remove previous binding.
if self._prefix_binding:
self.custom_key_bindings.remove_binding(self._prefix_binding)
# Create new Python binding.
@self.custom_key_bindings.add(*self._prefix, filter=
~(HasPrefix(pymux) | has_focus(COMMAND) | has_focus(PROMPT) |
WaitsForConfirmation(pymux)))
def enter_prefix_handler(event):
" Enter prefix mode. "
pymux.get_client_state().has_prefix = True
self._prefix_binding = enter_prefix_handler
@property
def prefix(self):
" Get the prefix key. "
return self._prefix
@prefix.setter
def prefix(self, keys):
"""
Set a new prefix key.
"""
assert isinstance(keys, tuple)
self._prefix = keys
self._load_prefix_binding()
def _load_builtins(self):
"""
Fill the Registry with the hard coded key bindings.
"""
pymux = self.pymux
kb = KeyBindings()
# Create filters.
has_prefix = HasPrefix(pymux)
waits_for_confirmation = WaitsForConfirmation(pymux)
prompt_or_command_focus = has_focus(COMMAND) | has_focus(PROMPT)
display_pane_numbers = Condition(lambda: pymux.display_pane_numbers)
in_scroll_buffer_not_searching = InScrollBufferNotSearching(pymux)
@kb.add(Keys.Any, filter=has_prefix)
def _(event):
" Ignore unknown Ctrl-B prefixed key sequences. "
pymux.get_client_state().has_prefix = False
@kb.add('c-c', filter=prompt_or_command_focus & ~has_prefix)
@kb.add('c-g', filter=prompt_or_command_focus & ~has_prefix)
# @kb.add('backspace', filter=has_focus(COMMAND) & ~has_prefix &
# Condition(lambda: cli.buffers[COMMAND].text == ''))
def _(event):
" Leave command mode. "
pymux.leave_command_mode(append_to_history=False)
@kb.add('y', filter=waits_for_confirmation)
@kb.add('Y', filter=waits_for_confirmation)
def _(event):
"""
Confirm command.
"""
client_state = pymux.get_client_state()
command = client_state.confirm_command
client_state.confirm_command = None
client_state.confirm_text = None
pymux.handle_command(command)
@kb.add('n', filter=waits_for_confirmation)
@kb.add('N', filter=waits_for_confirmation)
@kb.add('c-c' , filter=waits_for_confirmation)
def _(event):
"""
Cancel command.
"""
client_state = pymux.get_client_state()
client_state.confirm_command = None
client_state.confirm_text = None
@kb.add('c-c', filter=in_scroll_buffer_not_searching)
@kb.add('enter', filter=in_scroll_buffer_not_searching)
@kb.add('q', filter=in_scroll_buffer_not_searching)
def _(event):
" Exit scroll buffer. "
pane = pymux.arrangement.get_active_pane()
pane.exit_scroll_buffer()
@kb.add(' ', filter=in_scroll_buffer_not_searching)
def _(event):
" Enter selection mode when pressing space in copy mode. "
event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS)
@kb.add('enter', filter=in_scroll_buffer_not_searching & has_selection)
def _(event):
" Copy selection when pressing Enter. "
clipboard_data = event.current_buffer.copy_selection()
event.app.clipboard.set_data(clipboard_data)
@kb.add('v', filter=in_scroll_buffer_not_searching & has_selection)
def _(event):
" Toggle between selection types. "
types = [SelectionType.LINES, SelectionType.BLOCK, SelectionType.CHARACTERS]
selection_state = event.current_buffer.selection_state
try:
index = types.index(selection_state.type)
except ValueError: # Not in list.
index = 0
selection_state.type = types[(index + 1) % len(types)]
@Condition
def popup_displayed():
return self.pymux.get_client_state().display_popup
@kb.add('q', filter=popup_displayed, eager=True)
def _(event):
" Quit pop-up dialog. "
self.pymux.get_client_state().display_popup = False
@kb.add(Keys.Any, eager=True, filter=display_pane_numbers)
def _(event):
" When the pane numbers are shown. Any key press should hide them. "
pymux.display_pane_numbers = False
@Condition
def clock_displayed():
" "
pane = pymux.arrangement.get_active_pane()
return pane.clock_mode
@kb.add(Keys.Any, eager=True, filter=clock_displayed)
def _(event):
" When the clock is displayed. Any key press should hide it. "
pane = pymux.arrangement.get_active_pane()
pane.clock_mode = False
return kb
def add_custom_binding(self, key_name, command, arguments, needs_prefix=False):
"""
Add custom binding (for the "bind-key" command.)
Raises ValueError if the give `key_name` is an invalid name.
:param key_name: Pymux key name, for instance "C-a" or "M-x".
"""
assert isinstance(key_name, six.text_type)
assert isinstance(command, six.text_type)
assert isinstance(arguments, list)
# Unbind previous key.
self.remove_custom_binding(key_name, needs_prefix=needs_prefix)
# Translate the pymux key name into a prompt_toolkit key sequence.
# (Can raise ValueError.)
keys_sequence = pymux_key_to_prompt_toolkit_key_sequence(key_name)
# Create handler and add to Registry.
if needs_prefix:
filter = HasPrefix(self.pymux)
else:
filter = ~HasPrefix(self.pymux)
filter = filter & ~(WaitsForConfirmation(self.pymux) |
has_focus(COMMAND) | has_focus(PROMPT))
def key_handler(event):
" The actual key handler. "
call_command_handler(command, self.pymux, arguments)
self.pymux.get_client_state().has_prefix = False
self.custom_key_bindings.add(*keys_sequence, filter=filter)(key_handler)
# Store key in `custom_bindings` in order to be able to call
# "unbind-key" later on.
k = (needs_prefix, key_name)
self.custom_bindings[k] = CustomBinding(key_handler, command, arguments)
def remove_custom_binding(self, key_name, needs_prefix=False):
"""
Remove custom key binding for a key.
:param key_name: Pymux key name, for instance "C-A".
"""
k = (needs_prefix, key_name)
if k in self.custom_bindings:
self.custom_key_bindings.remove(self.custom_bindings[k].handler)
del self.custom_bindings[k]
class CustomBinding(object):
"""
Record for storing a single custom key binding.
"""
def __init__(self, handler, command, arguments):
assert callable(handler)
assert isinstance(command, six.text_type)
assert isinstance(arguments, list)
self.handler = handler
self.command = command
self.arguments = arguments