def __init__(self, pymux): self.pymux = pymux def get_search_state(cli): " Return the currently active SearchState. (The one for the focussed pane.) " return pymux.arrangement.get_active_pane(cli).search_state # Start from this KeyBindingManager from prompt_toolkit, to have basic # editing functionality for the command line. These key binding are # however only active when the following `enable_all` condition is met. self.registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings( enable_auto_suggest_bindings=True, enable_search= False, # We have our own search bindings, that support multiple panes. enable_extra_page_navigation=True, get_search_state=get_search_state), filter=(HasFocus(COMMAND) | HasFocus(PROMPT) | InScrollBuffer(pymux)) & ~HasPrefix(pymux), ), load_mouse_bindings(), self._load_builtins(), _load_search_bindings(pymux), ]) self._prefix = (Keys.ControlB, ) 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 __init__( self, registry=None, # XXX: not used anymore. enable_vi_mode=None, # (`enable_vi_mode` is deprecated.) enable_all=True, # get_search_state=None, enable_abort_and_exit_bindings=False, enable_system_bindings=False, enable_search=False, enable_open_in_editor=False, enable_extra_page_navigation=False, enable_auto_suggest_bindings=False): assert registry is None or isinstance(registry, Registry) assert get_search_state is None or callable(get_search_state) enable_all = to_cli_filter(enable_all) defaults = load_key_bindings( get_search_state=get_search_state, enable_abort_and_exit_bindings=enable_abort_and_exit_bindings, enable_system_bindings=enable_system_bindings, enable_search=enable_search, enable_open_in_editor=enable_open_in_editor, enable_extra_page_navigation=enable_extra_page_navigation, enable_auto_suggest_bindings=enable_auto_suggest_bindings) # Note, we wrap this whole thing again in a MergedRegistry, because we # don't want the `enable_all` settings to apply on items that were # added to the registry as a whole. self.registry = MergedRegistry( [ConditionalRegistry(defaults, enable_all)])
def __init__(self, pymux): self.pymux = pymux def get_search_state(cli): " Return the currently active SearchState. (The one for the focussed pane.) " return pymux.arrangement.get_active_pane(cli).search_state # Start from this KeyBindingManager from prompt_toolkit, to have basic # editing functionality for the command line. These key binding are # however only active when the following `enable_all` condition is met. self.registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings( enable_auto_suggest_bindings=True, enable_search=False, # We have our own search bindings, that support multiple panes. enable_extra_page_navigation=True, get_search_state=get_search_state), filter=(HasFocus(COMMAND) | HasFocus(PROMPT) | InScrollBuffer(pymux)) & ~HasPrefix(pymux), ), load_mouse_bindings(), self._load_builtins(), _load_search_bindings(pymux), ]) self._prefix = (Keys.ControlB, ) 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_key_bindings(*registries): registry = MergedRegistry([ load_abort_and_exit_bindings(), load_basic_system_bindings(), load_mouse_bindings(), load_cpr_bindings(), ] + list(registries)) return registry
def get_key_bindings(tosh): global_registry = KeyBindingManager.for_prompt(enable_all=Condition(_in_prompt_tab)).registry mouse_registry = ConditionalRegistry(load_mouse_bindings(), Condition(_in_interactive_tab)) prompt_registry = ConditionalRegistry(filter=Condition(_in_prompt_tab)) interactive_registry = ConditionalRegistry(filter=Condition(_in_interactive_tab)) @global_registry.add_binding(Keys.BracketedPaste) def _paste(event): tosh.window.active_tab().paste(event) @prompt_registry.add_binding(Keys.ControlW) @interactive_registry.add_binding(Keys.ControlB, 'w') def _close_tab(event): tosh.window.active_tab().close() @prompt_registry.add_binding(Keys.ControlT) @interactive_registry.add_binding(Keys.ControlB, 't') def _new_prompt_tab(event): from .tosh_tab import ToshTab tosh.window.add_tab(ToshTab(tosh)) @interactive_registry.add_binding(Keys.Any) def _forward_to_session(event): tosh.window.active_tab().write_to_ssh(prompt_toolkit_key_to_vt100_key(event.key_sequence[0].key, True)) @global_registry.add_binding(Keys.ControlB, '1') @global_registry.add_binding(Keys.ControlB, '2') @global_registry.add_binding(Keys.ControlB, '3') @global_registry.add_binding(Keys.ControlB, '4') @global_registry.add_binding(Keys.ControlB, '5') @global_registry.add_binding(Keys.ControlB, '6') @global_registry.add_binding(Keys.ControlB, '7') @global_registry.add_binding(Keys.ControlB, '8') @global_registry.add_binding(Keys.ControlB, '9') def _change_tab(event): tosh.window.switch_tab(int(event.key_sequence[-1].key) - 1) @global_registry.add_binding(Keys.ControlB, Keys.Left) def _prev_tab(_): tosh.window.prev_tab() @global_registry.add_binding(Keys.ControlB, Keys.Right) def _next_tab(_): tosh.window.next_tab() return MergedRegistry([global_registry, prompt_registry, interactive_registry, mouse_registry])
def load_key_bindings(get_search_state=None, enable_abort_and_exit_bindings=False, enable_system_bindings=False, enable_search=False, enable_open_in_editor=False, enable_extra_page_navigation=False, enable_auto_suggest_bindings=False): """ Create a Registry object that contains the default key bindings. :param enable_abort_and_exit_bindings: Filter to enable Ctrl-C and Ctrl-D. :param enable_system_bindings: Filter to enable the system bindings (meta-! prompt and Control-Z suspension.) :param enable_search: Filter to enable the search bindings. :param enable_open_in_editor: Filter to enable open-in-editor. :param enable_open_in_editor: Filter to enable open-in-editor. :param enable_extra_page_navigation: Filter for enabling extra page navigation. (Bindings for up/down scrolling through long pages, like in Emacs or Vi.) :param enable_auto_suggest_bindings: Filter to enable fish-style suggestions. """ assert get_search_state is None or callable(get_search_state) # Accept both Filters and booleans as input. enable_abort_and_exit_bindings = to_cli_filter( enable_abort_and_exit_bindings) enable_system_bindings = to_cli_filter(enable_system_bindings) enable_search = to_cli_filter(enable_search) enable_open_in_editor = to_cli_filter(enable_open_in_editor) enable_extra_page_navigation = to_cli_filter(enable_extra_page_navigation) enable_auto_suggest_bindings = to_cli_filter(enable_auto_suggest_bindings) registry = MergedRegistry([ # Load basic bindings. load_basic_bindings(), load_mouse_bindings(), ConditionalRegistry(load_abort_and_exit_bindings(), enable_abort_and_exit_bindings), ConditionalRegistry(load_basic_system_bindings(), enable_system_bindings), # Load emacs bindings. load_emacs_bindings(), ConditionalRegistry(load_emacs_open_in_editor_bindings(), enable_open_in_editor), ConditionalRegistry( load_emacs_search_bindings(get_search_state=get_search_state), enable_search), ConditionalRegistry(load_emacs_system_bindings(), enable_system_bindings), ConditionalRegistry(load_extra_emacs_page_navigation_bindings(), enable_extra_page_navigation), # Load Vi bindings. load_vi_bindings(get_search_state=get_search_state), ConditionalRegistry(load_vi_open_in_editor_bindings(), enable_open_in_editor), ConditionalRegistry( load_vi_search_bindings(get_search_state=get_search_state), enable_search), ConditionalRegistry(load_vi_system_bindings(), enable_system_bindings), ConditionalRegistry(load_extra_vi_page_navigation_bindings(), enable_extra_page_navigation), # Suggestion bindings. # (This has to come at the end, because the Vi bindings also have an # implementation for the "right arrow", but we really want the # suggestion binding when a suggestion is available.) ConditionalRegistry(load_auto_suggestion_bindings(), enable_auto_suggest_bindings), ]) return registry
def __init__( self, get_globals=None, get_locals=None, history_filename=None, vi_mode=False, # For internal use. _completer=None, _validator=None, _lexer=None, _extra_buffers=None, _extra_buffer_processors=None, _on_start=None, _extra_layout_body=None, _extra_toolbars=None, _input_buffer_height=None, _accept_action=AcceptAction.RETURN_DOCUMENT, _on_exit=AbortAction.RAISE_EXCEPTION): self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals self._completer = _completer or PythonCompleter( self.get_globals, self.get_locals) self._validator = _validator or PythonValidator( self.get_compiler_flags) self.history = FileHistory( history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) self._extra_buffers = _extra_buffers self._accept_action = _accept_action self._on_exit = _on_exit self._on_start = _on_start self._input_buffer_height = _input_buffer_height self._extra_layout_body = _extra_layout_body or [] self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] # Settings. self.show_signature = False self.show_docstring = False self.show_meta_enter_message = True self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN self.completion_menu_scroll_offset = 1 self.show_line_numbers = False self.show_status_bar = True self.wrap_lines = True self.complete_while_typing = True self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. # 'None' means that meta-enter is always required. self.enable_open_in_editor = True self.enable_system_bindings = True self.enable_input_validation = True self.enable_auto_suggest = False self.enable_mouse_support = False self.enable_history_search = False # When True, like readline, going # back in history will filter the # history on the records starting # with the current input. self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' self.terminal_title = None # The title to be displayed in the terminal. (None or string.) self.exit_message = 'Do you really want to exit?' self.insert_blank_line_after_output = True # (For the REPL.) # Tokens to be shown at the prompt. self.prompt_style = 'classic' # The currently active style. self.all_prompt_styles = { # Styles selectable from the menu. 'ipython': IPythonPrompt(self), 'classic': ClassicPrompt(), } self.get_input_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].in_tokens(cli) self.get_output_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].out_tokens(cli) #: Load styles. self.code_styles = get_all_code_styles() self.ui_styles = get_all_ui_styles() self._current_code_style_name = 'default' self._current_ui_style_name = 'default' if is_windows(): self._current_code_style_name = 'win32' self._current_style = self._generate_style() self.true_color = False # Options to be configurable from the sidebar. self.options = self._create_options() self.selected_option_index = 0 #: Incremeting integer counting the current statement. self.current_statement_index = 1 # Code signatures. (This is set asynchronously after a timeout.) self.signatures = [] # Create a Registry for the key bindings. self.key_bindings_registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings_for_prompt( enable_abort_and_exit_bindings=True, enable_search=True, enable_open_in_editor=Condition( lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition( lambda cli: self.enable_system_bindings), enable_auto_suggest_bindings=Condition( lambda cli: self.enable_auto_suggest)), # Disable all default key bindings when the sidebar or the exit confirmation # are shown. filter=Condition(lambda cli: not (self.show_sidebar or self. show_exit_confirmation))), load_mouse_bindings(), load_python_bindings(self), load_sidebar_bindings(self), load_confirm_exit_bindings(self), ]) # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) self._get_signatures_thread_running = False
class PythonInput(object): """ Prompt for reading Python input. :: python_input = PythonInput(...) application = python_input.create_application() cli = PythonCommandLineInterface(application=application) python_code = cli.run() """ def __init__( self, get_globals=None, get_locals=None, history_filename=None, vi_mode=False, # For internal use. _completer=None, _validator=None, _lexer=None, _extra_buffers=None, _extra_buffer_processors=None, _on_start=None, _extra_layout_body=None, _extra_toolbars=None, _input_buffer_height=None, _accept_action=AcceptAction.RETURN_DOCUMENT, _on_exit=AbortAction.RAISE_EXCEPTION): self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals self._completer = _completer or PythonCompleter( self.get_globals, self.get_locals) self._validator = _validator or PythonValidator( self.get_compiler_flags) self.history = FileHistory( history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) self._extra_buffers = _extra_buffers self._accept_action = _accept_action self._on_exit = _on_exit self._on_start = _on_start self._input_buffer_height = _input_buffer_height self._extra_layout_body = _extra_layout_body or [] self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] # Settings. self.show_signature = False self.show_docstring = False self.show_meta_enter_message = True self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN self.completion_menu_scroll_offset = 1 self.show_line_numbers = False self.show_status_bar = True self.wrap_lines = True self.complete_while_typing = True self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. # 'None' means that meta-enter is always required. self.enable_open_in_editor = True self.enable_system_bindings = True self.enable_input_validation = True self.enable_auto_suggest = False self.enable_mouse_support = False self.enable_history_search = False # When True, like readline, going # back in history will filter the # history on the records starting # with the current input. self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' self.terminal_title = None # The title to be displayed in the terminal. (None or string.) self.exit_message = 'Do you really want to exit?' self.insert_blank_line_after_output = True # (For the REPL.) # Tokens to be shown at the prompt. self.prompt_style = 'classic' # The currently active style. self.all_prompt_styles = { # Styles selectable from the menu. 'ipython': IPythonPrompt(self), 'classic': ClassicPrompt(), } self.get_input_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].in_tokens(cli) self.get_output_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].out_tokens(cli) #: Load styles. self.code_styles = get_all_code_styles() self.ui_styles = get_all_ui_styles() self._current_code_style_name = 'default' self._current_ui_style_name = 'default' if is_windows(): self._current_code_style_name = 'win32' self._current_style = self._generate_style() self.true_color = False # Options to be configurable from the sidebar. self.options = self._create_options() self.selected_option_index = 0 #: Incremeting integer counting the current statement. self.current_statement_index = 1 # Code signatures. (This is set asynchronously after a timeout.) self.signatures = [] # Create a Registry for the key bindings. self.key_bindings_registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings_for_prompt( enable_abort_and_exit_bindings=True, enable_search=True, enable_open_in_editor=Condition( lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition( lambda cli: self.enable_system_bindings), enable_auto_suggest_bindings=Condition( lambda cli: self.enable_auto_suggest)), # Disable all default key bindings when the sidebar or the exit confirmation # are shown. filter=Condition(lambda cli: not (self.show_sidebar or self. show_exit_confirmation))), load_mouse_bindings(), load_python_bindings(self), load_sidebar_bindings(self), load_confirm_exit_bindings(self), ]) # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) self._get_signatures_thread_running = False @property def option_count(self): " Return the total amount of options. (In all categories together.) " return sum(len(category.options) for category in self.options) @property def selected_option(self): " Return the currently selected option. " i = 0 for category in self.options: for o in category.options: if i == self.selected_option_index: return o else: i += 1 def get_compiler_flags(self): """ Give the current compiler flags by looking for _Feature instances in the globals. """ flags = 0 for value in self.get_globals().values(): if isinstance(value, __future__._Feature): flags |= value.compiler_flag return flags @property def add_key_binding(self): """ Shortcut for adding new key bindings. (Mostly useful for a .ptpython/config.py file, that receives a PythonInput/Repl instance as input.) :: @python_input.add_key_binding(Keys.ControlX, filter=...) def handler(event): ... """ # Extra key bindings should not be active when the sidebar is visible. sidebar_visible = Condition(lambda cli: self.show_sidebar) def add_binding_decorator(*keys, **kw): # Pop default filter keyword argument. filter = kw.pop('filter', Always()) assert not kw return self.key_bindings_registry.add_binding(*keys, filter=filter & ~sidebar_visible) return add_binding_decorator def install_code_colorscheme(self, name, style_dict): """ Install a new code color scheme. """ assert isinstance(name, six.text_type) assert isinstance(style_dict, dict) self.code_styles[name] = style_dict def use_code_colorscheme(self, name): """ Apply new colorscheme. (By name.) """ assert name in self.code_styles self._current_code_style_name = name self._current_style = self._generate_style() def install_ui_colorscheme(self, name, style_dict): """ Install a new UI color scheme. """ assert isinstance(name, six.text_type) assert isinstance(style_dict, dict) self.ui_styles[name] = style_dict def use_ui_colorscheme(self, name): """ Apply new colorscheme. (By name.) """ assert name in self.ui_styles self._current_ui_style_name = name self._current_style = self._generate_style() def _generate_style(self): """ Create new Style instance. (We don't want to do this on every key press, because each time the renderer receives a new style class, he will redraw everything.) """ return generate_style(self.code_styles[self._current_code_style_name], self.ui_styles[self._current_ui_style_name]) def _create_options(self): """ Create a list of `Option` instances for the options sidebar. """ def enable(attribute, value=True): setattr(self, attribute, value) # Return `True`, to be able to chain this in the lambdas below. return True def disable(attribute): setattr(self, attribute, False) return True def simple_option(title, description, field_name, values=None): " Create Simple on/of option. " values = values or ['off', 'on'] def get_current_value(): return values[bool(getattr(self, field_name))] def get_values(): return { values[1]: lambda: enable(field_name), values[0]: lambda: disable(field_name), } return Option(title=title, description=description, get_values=get_values, get_current_value=get_current_value) return [ OptionCategory('Input', [ simple_option(title='Input mode', description='Vi or emacs key bindings.', field_name='vi_mode', values=['emacs', 'vi']), simple_option( title='Paste mode', description="When enabled, don't indent automatically.", field_name='paste_mode'), Option( title='Complete while typing', description= "Generate autocompletions automatically while typing. " 'Don\'t require pressing TAB. (Not compatible with "History search".)', get_current_value=lambda: ['off', 'on'][ self.complete_while_typing], get_values=lambda: { 'on': lambda: enable('complete_while_typing') and disable( 'enable_history_search'), 'off': lambda: disable('complete_while_typing'), }), Option( title='History search', description= 'When pressing the up-arrow, filter the history on input starting ' 'with the current text. (Not compatible with "Complete while typing".)', get_current_value=lambda: ['off', 'on'][ self.enable_history_search], get_values=lambda: { 'on': lambda: enable('enable_history_search') and disable( 'complete_while_typing'), 'off': lambda: disable('enable_history_search'), }), simple_option( title='Mouse support', description= 'Respond to mouse clicks and scrolling for positioning the cursor, ' 'selecting text and scrolling through windows.', field_name='enable_mouse_support'), simple_option(title='Confirm on exit', description='Require confirmation when exiting.', field_name='confirm_exit'), simple_option( title='Input validation', description= 'In case of syntax errors, move the cursor to the error ' 'instead of showing a traceback of a SyntaxError.', field_name='enable_input_validation'), simple_option( title='Auto suggestion', description='Auto suggest inputs by looking at the history. ' 'Pressing right arrow or Ctrl-E will complete the entry.', field_name='enable_auto_suggest'), Option( title='Accept input on enter', description= 'Amount of ENTER presses required to execute input when the cursor ' 'is at the end of the input. (Note that META+ENTER will always execute.)', get_current_value=lambda: str(self.accept_input_on_enter or 'meta-enter'), get_values=lambda: { '2': lambda: enable('accept_input_on_enter', 2), '3': lambda: enable('accept_input_on_enter', 3), '4': lambda: enable('accept_input_on_enter', 4), 'meta-enter': lambda: enable('accept_input_on_enter', None), }), ]), OptionCategory('Display', [ Option( title='Completions', description= 'Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)', get_current_value=lambda: self.completion_visualisation, get_values=lambda: { CompletionVisualisation.NONE: lambda: enable('completion_visualisation', CompletionVisualisation.NONE), CompletionVisualisation.POP_UP: lambda: enable('completion_visualisation', CompletionVisualisation.POP_UP), CompletionVisualisation.MULTI_COLUMN: lambda: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), }), Option(title='Prompt', description= "Visualisation of the prompt. ('>>>' or 'In [1]:')", get_current_value=lambda: self.prompt_style, get_values=lambda: dict( (s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), simple_option( title='Blank line after output', description='Insert a blank line after the output.', field_name='insert_blank_line_after_output'), simple_option(title='Show signature', description='Display function signatures.', field_name='show_signature'), simple_option(title='Show docstring', description='Display function docstrings.', field_name='show_docstring'), simple_option( title='Show line numbers', description= 'Show line numbers when the input consists of multiple lines.', field_name='show_line_numbers'), simple_option( title='Show Meta+Enter message', description= 'Show the [Meta+Enter] message when this key combination is required to execute commands. ' + '(This is the case when a simple [Enter] key press will insert a newline.', field_name='show_meta_enter_message'), simple_option( title='Wrap lines', description='Wrap lines instead of scrolling horizontally.', field_name='wrap_lines'), simple_option( title='Show status bar', description= 'Show the status bar at the bottom of the terminal.', field_name='show_status_bar'), simple_option( title='Show sidebar help', description= 'When the sidebar is visible, also show this help text.', field_name='show_sidebar_help'), simple_option( title='Highlight parenthesis', description= 'Highlight matching parenthesis, when the cursor is on or right after one.', field_name='highlight_matching_parenthesis'), ]), OptionCategory('Colors', [ Option(title='Code', description='Color scheme to use for the Python code.', get_current_value=lambda: self._current_code_style_name, get_values=lambda: dict( (name, partial(self.use_code_colorscheme, name)) for name in self.code_styles)), Option( title='User interface', description='Color scheme to use for the user interface.', get_current_value=lambda: self._current_ui_style_name, get_values=lambda: dict( (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles)), simple_option( title='True color (24 bit)', description='Use 24 bit colors instead of 265 colors', field_name='true_color'), ]), ] def create_application(self): """ Create an `Application` instance for use in a `CommandLineInterface`. """ buffers = { 'docstring': Buffer(read_only=True), } buffers.update(self._extra_buffers or {}) return Application( layout=create_layout( self, lexer=self._lexer, input_buffer_height=self._input_buffer_height, extra_buffer_processors=self._extra_buffer_processors, extra_body=self._extra_layout_body, extra_toolbars=self._extra_toolbars), buffer=self._create_buffer(), buffers=buffers, key_bindings_registry=self.key_bindings_registry, paste_mode=Condition(lambda cli: self.paste_mode), mouse_support=Condition(lambda cli: self.enable_mouse_support), on_abort=AbortAction.RETRY, on_exit=self._on_exit, style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, on_initialize=self._on_cli_initialize, on_start=self._on_start, on_input_timeout=self._on_input_timeout) def _create_buffer(self): """ Create the `Buffer` for the Python input. """ def is_buffer_multiline(): return (self.paste_mode or self.accept_input_on_enter is None or document_is_multiline_python(python_buffer.document)) python_buffer = Buffer( is_multiline=Condition(is_buffer_multiline), complete_while_typing=Condition( lambda: self.complete_while_typing), enable_history_search=Condition( lambda: self.enable_history_search), tempfile_suffix='.py', history=self.history, completer=self._completer, validator=ConditionalValidator( self._validator, Condition(lambda: self.enable_input_validation)), auto_suggest=ConditionalAutoSuggest( AutoSuggestFromHistory(), Condition(lambda cli: self.enable_auto_suggest)), accept_action=self._accept_action) return python_buffer def _on_cli_initialize(self, cli): """ Called when a CommandLineInterface has been created. """ # Synchronize PythonInput state with the CommandLineInterface. def synchronize(_=None): if self.vi_mode: cli.editing_mode = EditingMode.VI else: cli.editing_mode = EditingMode.EMACS cli.input_processor.beforeKeyPress += synchronize cli.input_processor.afterKeyPress += synchronize synchronize() def _on_input_timeout(self, cli): """ When there is no input activity, in another thread, get the signature of the current code. """ if cli.current_buffer_name != DEFAULT_BUFFER: return # Never run multiple get-signature threads. if self._get_signatures_thread_running: return self._get_signatures_thread_running = True buffer = cli.current_buffer document = buffer.document def run(): script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals()) # Show signatures in help text. if script: try: signatures = script.call_signatures() except ValueError: # e.g. in case of an invalid \\x escape. signatures = [] except Exception: # Sometimes we still get an exception (TypeError), because # of probably bugs in jedi. We can silence them. # See: https://github.com/davidhalter/jedi/issues/492 signatures = [] else: # Try to access the params attribute just once. For Jedi # signatures containing the keyword-only argument star, # this will crash when retrieving it the first time with # AttributeError. Every following time it works. # See: https://github.com/jonathanslenders/ptpython/issues/47 # https://github.com/davidhalter/jedi/issues/598 try: if signatures: signatures[0].params except AttributeError: pass else: signatures = [] self._get_signatures_thread_running = False # Set signatures and redraw if the text didn't change in the # meantime. Otherwise request new signatures. if buffer.text == document.text: self.signatures = signatures # Set docstring in docstring buffer. if signatures: string = signatures[0].docstring() if not isinstance(string, six.text_type): string = string.decode('utf-8') cli.buffers['docstring'].reset( initial_document=Document(string, cursor_position=0)) else: cli.buffers['docstring'].reset() cli.request_redraw() else: self._on_input_timeout(cli) cli.eventloop.run_in_executor(run) def on_reset(self, cli): self.signatures = [] def enter_history(self, cli): """ Display the history. """ cli.vi_state.input_mode = InputMode.NAVIGATION def done(result): if result is not None: cli.buffers[DEFAULT_BUFFER].document = result cli.vi_state.input_mode = InputMode.INSERT cli.run_sub_application( create_history_application(self, cli.buffers[DEFAULT_BUFFER].document), done)
def __init__(self, get_globals=None, get_locals=None, history_filename=None, vi_mode=False, # For internal use. _completer=None, _validator=None, _lexer=None, _extra_buffers=None, _extra_buffer_processors=None, _on_start=None, _extra_layout_body=None, _extra_toolbars=None, _input_buffer_height=None, _accept_action=AcceptAction.RETURN_DOCUMENT, _on_exit=AbortAction.RAISE_EXCEPTION): self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) self._validator = _validator or PythonValidator(self.get_compiler_flags) self.history = FileHistory(history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) self._extra_buffers = _extra_buffers self._accept_action = _accept_action self._on_exit = _on_exit self._on_start = _on_start self._input_buffer_height = _input_buffer_height self._extra_layout_body = _extra_layout_body or [] self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] # Settings. self.show_signature = False self.show_docstring = False self.show_meta_enter_message = True self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN self.completion_menu_scroll_offset = 1 self.show_line_numbers = False self.show_status_bar = True self.wrap_lines = True self.complete_while_typing = True self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. # 'None' means that meta-enter is always required. self.enable_open_in_editor = True self.enable_system_bindings = True self.enable_input_validation = True self.enable_auto_suggest = False self.enable_mouse_support = False self.enable_history_search = False # When True, like readline, going # back in history will filter the # history on the records starting # with the current input. self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' self.terminal_title = None # The title to be displayed in the terminal. (None or string.) self.exit_message = 'Do you really want to exit?' self.insert_blank_line_after_output = True # (For the REPL.) # Tokens to be shown at the prompt. self.prompt_style = 'classic' # The currently active style. self.all_prompt_styles = { # Styles selectable from the menu. 'ipython': IPythonPrompt(self), 'classic': ClassicPrompt(), } self.get_input_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].in_tokens(cli) self.get_output_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].out_tokens(cli) #: Load styles. self.code_styles = get_all_code_styles() self.ui_styles = get_all_ui_styles() self._current_code_style_name = 'default' self._current_ui_style_name = 'default' if is_windows(): self._current_code_style_name = 'win32' self._current_style = self._generate_style() self.true_color = False # Options to be configurable from the sidebar. self.options = self._create_options() self.selected_option_index = 0 #: Incremeting integer counting the current statement. self.current_statement_index = 1 # Code signatures. (This is set asynchronously after a timeout.) self.signatures = [] # Create a Registry for the key bindings. self.key_bindings_registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings_for_prompt( enable_abort_and_exit_bindings=True, enable_search=True, enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest)), # Disable all default key bindings when the sidebar or the exit confirmation # are shown. filter=Condition(lambda cli: not (self.show_sidebar or self.show_exit_confirmation)) ), load_mouse_bindings(), load_python_bindings(self), load_sidebar_bindings(self), load_confirm_exit_bindings(self), ]) # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) self._get_signatures_thread_running = False
class PythonInput(object): """ Prompt for reading Python input. :: python_input = PythonInput(...) application = python_input.create_application() cli = PythonCommandLineInterface(application=application) python_code = cli.run() """ def __init__(self, get_globals=None, get_locals=None, history_filename=None, vi_mode=False, # For internal use. _completer=None, _validator=None, _lexer=None, _extra_buffers=None, _extra_buffer_processors=None, _on_start=None, _extra_layout_body=None, _extra_toolbars=None, _input_buffer_height=None, _accept_action=AcceptAction.RETURN_DOCUMENT, _on_exit=AbortAction.RAISE_EXCEPTION): self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) self._validator = _validator or PythonValidator(self.get_compiler_flags) self.history = FileHistory(history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) self._extra_buffers = _extra_buffers self._accept_action = _accept_action self._on_exit = _on_exit self._on_start = _on_start self._input_buffer_height = _input_buffer_height self._extra_layout_body = _extra_layout_body or [] self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] # Settings. self.show_signature = False self.show_docstring = False self.show_meta_enter_message = True self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN self.completion_menu_scroll_offset = 1 self.show_line_numbers = False self.show_status_bar = True self.wrap_lines = True self.complete_while_typing = True self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. # 'None' means that meta-enter is always required. self.enable_open_in_editor = True self.enable_system_bindings = True self.enable_input_validation = True self.enable_auto_suggest = False self.enable_mouse_support = False self.enable_history_search = False # When True, like readline, going # back in history will filter the # history on the records starting # with the current input. self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' self.terminal_title = None # The title to be displayed in the terminal. (None or string.) self.exit_message = 'Do you really want to exit?' self.insert_blank_line_after_output = True # (For the REPL.) # Tokens to be shown at the prompt. self.prompt_style = 'classic' # The currently active style. self.all_prompt_styles = { # Styles selectable from the menu. 'ipython': IPythonPrompt(self), 'classic': ClassicPrompt(), } self.get_input_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].in_tokens(cli) self.get_output_prompt_tokens = lambda cli: \ self.all_prompt_styles[self.prompt_style].out_tokens(cli) #: Load styles. self.code_styles = get_all_code_styles() self.ui_styles = get_all_ui_styles() self._current_code_style_name = 'default' self._current_ui_style_name = 'default' if is_windows(): self._current_code_style_name = 'win32' self._current_style = self._generate_style() self.true_color = False # Options to be configurable from the sidebar. self.options = self._create_options() self.selected_option_index = 0 #: Incremeting integer counting the current statement. self.current_statement_index = 1 # Code signatures. (This is set asynchronously after a timeout.) self.signatures = [] # Create a Registry for the key bindings. self.key_bindings_registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings_for_prompt( enable_abort_and_exit_bindings=True, enable_search=True, enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest)), # Disable all default key bindings when the sidebar or the exit confirmation # are shown. filter=Condition(lambda cli: not (self.show_sidebar or self.show_exit_confirmation)) ), load_mouse_bindings(), load_python_bindings(self), load_sidebar_bindings(self), load_confirm_exit_bindings(self), ]) # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) self._get_signatures_thread_running = False @property def option_count(self): " Return the total amount of options. (In all categories together.) " return sum(len(category.options) for category in self.options) @property def selected_option(self): " Return the currently selected option. " i = 0 for category in self.options: for o in category.options: if i == self.selected_option_index: return o else: i += 1 def get_compiler_flags(self): """ Give the current compiler flags by looking for _Feature instances in the globals. """ flags = 0 for value in self.get_globals().values(): if isinstance(value, __future__._Feature): flags |= value.compiler_flag return flags @property def add_key_binding(self): """ Shortcut for adding new key bindings. (Mostly useful for a .ptpython/config.py file, that receives a PythonInput/Repl instance as input.) :: @python_input.add_key_binding(Keys.ControlX, filter=...) def handler(event): ... """ # Extra key bindings should not be active when the sidebar is visible. sidebar_visible = Condition(lambda cli: self.show_sidebar) def add_binding_decorator(*keys, **kw): # Pop default filter keyword argument. filter = kw.pop('filter', Always()) assert not kw return self.key_bindings_registry.add_binding(*keys, filter=filter & ~sidebar_visible) return add_binding_decorator def install_code_colorscheme(self, name, style_dict): """ Install a new code color scheme. """ assert isinstance(name, six.text_type) assert isinstance(style_dict, dict) self.code_styles[name] = style_dict def use_code_colorscheme(self, name): """ Apply new colorscheme. (By name.) """ assert name in self.code_styles self._current_code_style_name = name self._current_style = self._generate_style() def install_ui_colorscheme(self, name, style_dict): """ Install a new UI color scheme. """ assert isinstance(name, six.text_type) assert isinstance(style_dict, dict) self.ui_styles[name] = style_dict def use_ui_colorscheme(self, name): """ Apply new colorscheme. (By name.) """ assert name in self.ui_styles self._current_ui_style_name = name self._current_style = self._generate_style() def _generate_style(self): """ Create new Style instance. (We don't want to do this on every key press, because each time the renderer receives a new style class, he will redraw everything.) """ return generate_style(self.code_styles[self._current_code_style_name], self.ui_styles[self._current_ui_style_name]) def _create_options(self): """ Create a list of `Option` instances for the options sidebar. """ def enable(attribute, value=True): setattr(self, attribute, value) # Return `True`, to be able to chain this in the lambdas below. return True def disable(attribute): setattr(self, attribute, False) return True def simple_option(title, description, field_name, values=None): " Create Simple on/of option. " values = values or ['off', 'on'] def get_current_value(): return values[bool(getattr(self, field_name))] def get_values(): return { values[1]: lambda: enable(field_name), values[0]: lambda: disable(field_name), } return Option(title=title, description=description, get_values=get_values, get_current_value=get_current_value) return [ OptionCategory('Input', [ simple_option(title='Input mode', description='Vi or emacs key bindings.', field_name='vi_mode', values=['emacs', 'vi']), simple_option(title='Paste mode', description="When enabled, don't indent automatically.", field_name='paste_mode'), Option(title='Complete while typing', description="Generate autocompletions automatically while typing. " 'Don\'t require pressing TAB. (Not compatible with "History search".)', get_current_value=lambda: ['off', 'on'][self.complete_while_typing], get_values=lambda: { 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), 'off': lambda: disable('complete_while_typing'), }), Option(title='History search', description='When pressing the up-arrow, filter the history on input starting ' 'with the current text. (Not compatible with "Complete while typing".)', get_current_value=lambda: ['off', 'on'][self.enable_history_search], get_values=lambda: { 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), 'off': lambda: disable('enable_history_search'), }), simple_option(title='Mouse support', description='Respond to mouse clicks and scrolling for positioning the cursor, ' 'selecting text and scrolling through windows.', field_name='enable_mouse_support'), simple_option(title='Confirm on exit', description='Require confirmation when exiting.', field_name='confirm_exit'), simple_option(title='Input validation', description='In case of syntax errors, move the cursor to the error ' 'instead of showing a traceback of a SyntaxError.', field_name='enable_input_validation'), simple_option(title='Auto suggestion', description='Auto suggest inputs by looking at the history. ' 'Pressing right arrow or Ctrl-E will complete the entry.', field_name='enable_auto_suggest'), Option(title='Accept input on enter', description='Amount of ENTER presses required to execute input when the cursor ' 'is at the end of the input. (Note that META+ENTER will always execute.)', get_current_value=lambda: str(self.accept_input_on_enter or 'meta-enter'), get_values=lambda: { '2': lambda: enable('accept_input_on_enter', 2), '3': lambda: enable('accept_input_on_enter', 3), '4': lambda: enable('accept_input_on_enter', 4), 'meta-enter': lambda: enable('accept_input_on_enter', None), }), ]), OptionCategory('Display', [ Option(title='Completions', description='Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)', get_current_value=lambda: self.completion_visualisation, get_values=lambda: { CompletionVisualisation.NONE: lambda: enable('completion_visualisation', CompletionVisualisation.NONE), CompletionVisualisation.POP_UP: lambda: enable('completion_visualisation', CompletionVisualisation.POP_UP), CompletionVisualisation.MULTI_COLUMN: lambda: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), }), Option(title='Prompt', description="Visualisation of the prompt. ('>>>' or 'In [1]:')", get_current_value=lambda: self.prompt_style, get_values=lambda: dict((s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), simple_option(title='Blank line after output', description='Insert a blank line after the output.', field_name='insert_blank_line_after_output'), simple_option(title='Show signature', description='Display function signatures.', field_name='show_signature'), simple_option(title='Show docstring', description='Display function docstrings.', field_name='show_docstring'), simple_option(title='Show line numbers', description='Show line numbers when the input consists of multiple lines.', field_name='show_line_numbers'), simple_option(title='Show Meta+Enter message', description='Show the [Meta+Enter] message when this key combination is required to execute commands. ' + '(This is the case when a simple [Enter] key press will insert a newline.', field_name='show_meta_enter_message'), simple_option(title='Wrap lines', description='Wrap lines instead of scrolling horizontally.', field_name='wrap_lines'), simple_option(title='Show status bar', description='Show the status bar at the bottom of the terminal.', field_name='show_status_bar'), simple_option(title='Show sidebar help', description='When the sidebar is visible, also show this help text.', field_name='show_sidebar_help'), simple_option(title='Highlight parenthesis', description='Highlight matching parenthesis, when the cursor is on or right after one.', field_name='highlight_matching_parenthesis'), ]), OptionCategory('Colors', [ Option(title='Code', description='Color scheme to use for the Python code.', get_current_value=lambda: self._current_code_style_name, get_values=lambda: dict( (name, partial(self.use_code_colorscheme, name)) for name in self.code_styles) ), Option(title='User interface', description='Color scheme to use for the user interface.', get_current_value=lambda: self._current_ui_style_name, get_values=lambda: dict( (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles) ), simple_option(title='True color (24 bit)', description='Use 24 bit colors instead of 265 colors', field_name='true_color'), ]), ] def create_application(self): """ Create an `Application` instance for use in a `CommandLineInterface`. """ buffers = { 'docstring': Buffer(read_only=True), } buffers.update(self._extra_buffers or {}) return Application( layout=create_layout( self, lexer=self._lexer, input_buffer_height=self._input_buffer_height, extra_buffer_processors=self._extra_buffer_processors, extra_body=self._extra_layout_body, extra_toolbars=self._extra_toolbars), buffer=self._create_buffer(), buffers=buffers, key_bindings_registry=self.key_bindings_registry, paste_mode=Condition(lambda cli: self.paste_mode), mouse_support=Condition(lambda cli: self.enable_mouse_support), on_abort=AbortAction.RETRY, on_exit=self._on_exit, style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, reverse_vi_search_direction=True, on_initialize=self._on_cli_initialize, on_start=self._on_start, on_input_timeout=self._on_input_timeout) def _create_buffer(self): """ Create the `Buffer` for the Python input. """ def is_buffer_multiline(): return (self.paste_mode or self.accept_input_on_enter is None or document_is_multiline_python(python_buffer.document)) python_buffer = Buffer( is_multiline=Condition(is_buffer_multiline), complete_while_typing=Condition(lambda: self.complete_while_typing), enable_history_search=Condition(lambda: self.enable_history_search), tempfile_suffix='.py', history=self.history, completer=self._completer, validator=ConditionalValidator( self._validator, Condition(lambda: self.enable_input_validation)), auto_suggest=ConditionalAutoSuggest( AutoSuggestFromHistory(), Condition(lambda cli: self.enable_auto_suggest)), accept_action=self._accept_action) return python_buffer def _on_cli_initialize(self, cli): """ Called when a CommandLineInterface has been created. """ # Synchronize PythonInput state with the CommandLineInterface. def synchronize(_=None): if self.vi_mode: cli.editing_mode = EditingMode.VI else: cli.editing_mode = EditingMode.EMACS cli.input_processor.beforeKeyPress += synchronize cli.input_processor.afterKeyPress += synchronize synchronize() def _on_input_timeout(self, cli): """ When there is no input activity, in another thread, get the signature of the current code. """ if cli.current_buffer_name != DEFAULT_BUFFER: return # Never run multiple get-signature threads. if self._get_signatures_thread_running: return self._get_signatures_thread_running = True buffer = cli.current_buffer document = buffer.document def run(): script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals()) # Show signatures in help text. if script: try: signatures = script.call_signatures() except ValueError: # e.g. in case of an invalid \\x escape. signatures = [] except Exception: # Sometimes we still get an exception (TypeError), because # of probably bugs in jedi. We can silence them. # See: https://github.com/davidhalter/jedi/issues/492 signatures = [] else: # Try to access the params attribute just once. For Jedi # signatures containing the keyword-only argument star, # this will crash when retrieving it the first time with # AttributeError. Every following time it works. # See: https://github.com/jonathanslenders/ptpython/issues/47 # https://github.com/davidhalter/jedi/issues/598 try: if signatures: signatures[0].params except AttributeError: pass else: signatures = [] self._get_signatures_thread_running = False # Set signatures and redraw if the text didn't change in the # meantime. Otherwise request new signatures. if buffer.text == document.text: self.signatures = signatures # Set docstring in docstring buffer. if signatures: string = signatures[0].docstring() if not isinstance(string, six.text_type): string = string.decode('utf-8') cli.buffers['docstring'].reset( initial_document=Document(string, cursor_position=0)) else: cli.buffers['docstring'].reset() cli.request_redraw() else: self._on_input_timeout(cli) cli.eventloop.run_in_executor(run) def on_reset(self, cli): self.signatures = [] def enter_history(self, cli): """ Display the history. """ cli.vi_state.input_mode = InputMode.NAVIGATION def done(result): if result is not None: cli.buffers[DEFAULT_BUFFER].document = result cli.vi_state.input_mode = InputMode.INSERT cli.run_sub_application(create_history_application( self, cli.buffers[DEFAULT_BUFFER].document), done)
class KeyBindingsManager(object): """ Pymux key binding manager. """ def __init__(self, pymux): self.pymux = pymux def get_search_state(cli): " Return the currently active SearchState. (The one for the focussed pane.) " return pymux.arrangement.get_active_pane(cli).search_state # Start from this KeyBindingManager from prompt_toolkit, to have basic # editing functionality for the command line. These key binding are # however only active when the following `enable_all` condition is met. self.registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings( enable_auto_suggest_bindings=True, enable_search= False, # We have our own search bindings, that support multiple panes. enable_extra_page_navigation=True, get_search_state=get_search_state), filter=(HasFocus(COMMAND) | HasFocus(PROMPT) | InScrollBuffer(pymux)) & ~HasPrefix(pymux), ), load_mouse_bindings(), self._load_builtins(), _load_search_bindings(pymux), ]) self._prefix = (Keys.ControlB, ) 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 registry = self.registry # Remove previous binding. if self._prefix_binding: self.registry.remove_binding(self._prefix_binding) # Create new Python binding. @registry.add_binding( *self._prefix, filter=~(HasPrefix(pymux) | HasFocus(COMMAND) | HasFocus(PROMPT) | WaitsForConfirmation(pymux))) def enter_prefix_handler(event): " Enter prefix mode. " pymux.get_client_state(event.cli).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 registry = Registry() # Create filters. has_prefix = HasPrefix(pymux) waits_for_confirmation = WaitsForConfirmation(pymux) prompt_or_command_focus = HasFocus(COMMAND) | HasFocus(PROMPT) display_pane_numbers = Condition( lambda cli: pymux.display_pane_numbers) in_scroll_buffer_not_searching = InScrollBufferNotSearching(pymux) pane_input_allowed = ~(prompt_or_command_focus | has_prefix | waits_for_confirmation | display_pane_numbers | InScrollBuffer(pymux)) @registry.add_binding(Keys.Any, filter=pane_input_allowed, invalidate_ui=False) def _(event): """ When a pane has the focus, key bindings are redirected to the process running inside the pane. """ # NOTE: we don't invalidate the UI, because for pymux itself, # nothing in the output changes yet. It's the application in # the pane that will probably echo back the typed characters. # When we receive them, they are draw to the UI and it's # invalidated. w = pymux.arrangement.get_active_window(event.cli) pane = w.active_pane if pane.clock_mode: # Leave clock mode on key press. pane.clock_mode = False pymux.invalidate() else: # Write input to pane. If 'synchronize_panes' is on, write # input to all panes in the current window. panes = w.panes if w.synchronize_panes else [pane] for p in panes: p.process.write_key(event.key_sequence[0].key) @registry.add_binding(Keys.BracketedPaste, filter=pane_input_allowed, invalidate_ui=False) def _(event): """ Pasting to the active pane. (Using bracketed paste.) """ w = pymux.arrangement.get_active_window(event.cli) pane = w.active_pane if not pane.clock_mode: # Paste input to pane. If 'synchronize_panes' is on, paste # input to all panes in the current window. panes = w.panes if w.synchronize_panes else [pane] for p in panes: p.process.write_input(event.data, paste=True) @registry.add_binding(Keys.Any, filter=has_prefix) def _(event): " Ignore unknown Ctrl-B prefixed key sequences. " pymux.get_client_state(event.cli).has_prefix = False @registry.add_binding(Keys.ControlC, filter=prompt_or_command_focus & ~has_prefix) @registry.add_binding(Keys.ControlG, filter=prompt_or_command_focus & ~has_prefix) @registry.add_binding( Keys.Backspace, filter=HasFocus(COMMAND) & ~has_prefix & Condition(lambda cli: cli.buffers[COMMAND].text == '')) def _(event): " Leave command mode. " pymux.leave_command_mode(event.cli, append_to_history=False) @registry.add_binding('y', filter=waits_for_confirmation) @registry.add_binding('Y', filter=waits_for_confirmation) def _(event): """ Confirm command. """ client_state = pymux.get_client_state(event.cli) command = client_state.confirm_command client_state.confirm_command = None client_state.confirm_text = None pymux.handle_command(event.cli, command) @registry.add_binding('n', filter=waits_for_confirmation) @registry.add_binding('N', filter=waits_for_confirmation) @registry.add_binding(Keys.ControlC, filter=waits_for_confirmation) def _(event): """ Cancel command. """ client_state = pymux.get_client_state(event.cli) client_state.confirm_command = None client_state.confirm_text = None @registry.add_binding(Keys.ControlC, filter=in_scroll_buffer_not_searching) @registry.add_binding(Keys.ControlJ, filter=in_scroll_buffer_not_searching) @registry.add_binding('q', filter=in_scroll_buffer_not_searching) def _(event): " Exit scroll buffer. " pane = pymux.arrangement.get_active_pane(event.cli) pane.exit_scroll_buffer() @registry.add_binding(' ', 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) @registry.add_binding(Keys.ControlJ, filter=in_scroll_buffer_not_searching & HasSelection()) def _(event): " Copy selection when pressing Enter. " clipboard_data = event.current_buffer.copy_selection() event.cli.clipboard.set_data(clipboard_data) @registry.add_binding('v', filter=in_scroll_buffer_not_searching & HasSelection()) 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)] @registry.add_binding(Keys.Any, filter=display_pane_numbers) def _(event): " When the pane numbers are shown. Any key press should hide them. " pymux.display_pane_numbers = False return registry 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) | HasFocus(COMMAND) | HasFocus(PROMPT)) def key_handler(event): " The actual key handler. " call_command_handler(command, self.pymux, event.cli, arguments) self.pymux.get_client_state(event.cli).has_prefix = False self.registry.add_binding(*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.registry.remove_binding(self.custom_bindings[k].handler) del self.custom_bindings[k]
class KeyBindingsManager(object): """ Pymux key binding manager. """ def __init__(self, pymux): self.pymux = pymux def get_search_state(cli): " Return the currently active SearchState. (The one for the focussed pane.) " return pymux.arrangement.get_active_pane(cli).search_state # Start from this KeyBindingManager from prompt_toolkit, to have basic # editing functionality for the command line. These key binding are # however only active when the following `enable_all` condition is met. self.registry = MergedRegistry([ ConditionalRegistry( registry=load_key_bindings( enable_auto_suggest_bindings=True, enable_search=False, # We have our own search bindings, that support multiple panes. enable_extra_page_navigation=True, get_search_state=get_search_state), filter=(HasFocus(COMMAND) | HasFocus(PROMPT) | InScrollBuffer(pymux)) & ~HasPrefix(pymux), ), load_mouse_bindings(), self._load_builtins(), _load_search_bindings(pymux), ]) self._prefix = (Keys.ControlB, ) 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 registry = self.registry # Remove previous binding. if self._prefix_binding: self.registry.remove_binding(self._prefix_binding) # Create new Python binding. @registry.add_binding(*self._prefix, filter= ~(HasPrefix(pymux) | HasFocus(COMMAND) | HasFocus(PROMPT) | WaitsForConfirmation(pymux))) def enter_prefix_handler(event): " Enter prefix mode. " pymux.get_client_state(event.cli).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 registry = Registry() # Create filters. has_prefix = HasPrefix(pymux) waits_for_confirmation = WaitsForConfirmation(pymux) prompt_or_command_focus = HasFocus(COMMAND) | HasFocus(PROMPT) display_pane_numbers = Condition(lambda cli: pymux.display_pane_numbers) in_scroll_buffer_not_searching = InScrollBufferNotSearching(pymux) pane_input_allowed = ~(prompt_or_command_focus | has_prefix | waits_for_confirmation | display_pane_numbers | InScrollBuffer(pymux)) @registry.add_binding(Keys.Any, filter=pane_input_allowed, invalidate_ui=False) def _(event): """ When a pane has the focus, key bindings are redirected to the process running inside the pane. """ # NOTE: we don't invalidate the UI, because for pymux itself, # nothing in the output changes yet. It's the application in # the pane that will probably echo back the typed characters. # When we receive them, they are draw to the UI and it's # invalidated. w = pymux.arrangement.get_active_window(event.cli) pane = w.active_pane if pane.clock_mode: # Leave clock mode on key press. pane.clock_mode = False pymux.invalidate() else: # Write input to pane. If 'synchronize_panes' is on, write # input to all panes in the current window. panes = w.panes if w.synchronize_panes else [pane] for p in panes: p.process.write_key(event.key_sequence[0].key) @registry.add_binding(Keys.BracketedPaste, filter=pane_input_allowed, invalidate_ui=False) def _(event): """ Pasting to the active pane. (Using bracketed paste.) """ w = pymux.arrangement.get_active_window(event.cli) pane = w.active_pane if not pane.clock_mode: # Paste input to pane. If 'synchronize_panes' is on, paste # input to all panes in the current window. panes = w.panes if w.synchronize_panes else [pane] for p in panes: p.process.write_input(event.data, paste=True) @registry.add_binding(Keys.Any, filter=has_prefix) def _(event): " Ignore unknown Ctrl-B prefixed key sequences. " pymux.get_client_state(event.cli).has_prefix = False @registry.add_binding(Keys.ControlC, filter=prompt_or_command_focus & ~has_prefix) @registry.add_binding(Keys.ControlG, filter=prompt_or_command_focus & ~has_prefix) @registry.add_binding(Keys.Backspace, filter=HasFocus(COMMAND) & ~has_prefix & Condition(lambda cli: cli.buffers[COMMAND].text == '')) def _(event): " Leave command mode. " pymux.leave_command_mode(event.cli, append_to_history=False) @registry.add_binding('y', filter=waits_for_confirmation) @registry.add_binding('Y', filter=waits_for_confirmation) def _(event): """ Confirm command. """ client_state = pymux.get_client_state(event.cli) command = client_state.confirm_command client_state.confirm_command = None client_state.confirm_text = None pymux.handle_command(event.cli, command) @registry.add_binding('n', filter=waits_for_confirmation) @registry.add_binding('N', filter=waits_for_confirmation) @registry.add_binding(Keys.ControlC, filter=waits_for_confirmation) def _(event): """ Cancel command. """ client_state = pymux.get_client_state(event.cli) client_state.confirm_command = None client_state.confirm_text = None @registry.add_binding(Keys.ControlC, filter=in_scroll_buffer_not_searching) @registry.add_binding(Keys.ControlJ, filter=in_scroll_buffer_not_searching) @registry.add_binding('q', filter=in_scroll_buffer_not_searching) def _(event): " Exit scroll buffer. " pane = pymux.arrangement.get_active_pane(event.cli) pane.exit_scroll_buffer() @registry.add_binding(' ', 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) @registry.add_binding(Keys.ControlJ, filter=in_scroll_buffer_not_searching & HasSelection()) def _(event): " Copy selection when pressing Enter. " clipboard_data = event.current_buffer.copy_selection() event.cli.clipboard.set_data(clipboard_data) @registry.add_binding('v', filter=in_scroll_buffer_not_searching & HasSelection()) 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)] @registry.add_binding(Keys.Any, filter=display_pane_numbers) def _(event): " When the pane numbers are shown. Any key press should hide them. " pymux.display_pane_numbers = False return registry 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) | HasFocus(COMMAND) | HasFocus(PROMPT)) def key_handler(event): " The actual key handler. " call_command_handler(command, self.pymux, event.cli, arguments) self.pymux.get_client_state(event.cli).has_prefix = False self.registry.add_binding(*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.registry.remove_binding(self.custom_bindings[k].handler) del self.custom_bindings[k]