def __init__(self) -> None: self._bindings: List[Binding] = [] self._get_bindings_for_keys_cache: SimpleCache[KeysTuple, List[Binding]] = \ SimpleCache(maxsize=10000) self._get_bindings_starting_with_keys_cache: SimpleCache[KeysTuple, List[Binding]] = \ SimpleCache(maxsize=1000) self.__version = 0 # For cache invalidation.
def __init__(self, get_tokens, default_char=None, get_default_char=None, align_right=False, align_center=False, has_focus=False, wrap_lines=True): assert default_char is None or isinstance(default_char, Char) assert get_default_char is None or callable(get_default_char) assert not (default_char and get_default_char) self.align_right = to_cli_filter(align_right) self.align_center = to_cli_filter(align_center) self._has_focus_filter = to_cli_filter(has_focus) self.wrap_lines = to_cli_filter(wrap_lines) self.get_tokens = get_tokens # Construct `get_default_char` callable. if default_char: get_default_char = lambda _: default_char elif not get_default_char: get_default_char = lambda _: Char(' ', Token) self.get_default_char = get_default_char #: Cache for rendered screens. self._screen_cache = SimpleCache(maxsize=18) self._token_cache = SimpleCache(maxsize=1) # Only cache one token list. We don't need the previous item. # Render info for the mouse support. self._tokens = None # The last rendered tokens. self._pos_to_indexes = None # Mapping from mouse positions (x,y) to
def __init__(self, get_tokens, default_char=None, get_default_char=None, align_right=False, align_center=False, has_focus=False): assert default_char is None or isinstance(default_char, Char) assert get_default_char is None or callable(get_default_char) assert not (default_char and get_default_char) self.align_right = to_cli_filter(align_right) self.align_center = to_cli_filter(align_center) self._has_focus_filter = to_cli_filter(has_focus) self.get_tokens = get_tokens # Construct `get_default_char` callable. if default_char: get_default_char = lambda _: default_char elif not get_default_char: get_default_char = lambda _: Char(' ', Token) self.get_default_char = get_default_char #: Cache for the content. self._content_cache = SimpleCache(maxsize=18) self._token_cache = SimpleCache(maxsize=1) # Only cache one token list. We don't need the previous item. # Render info for the mouse support. self._tokens = None
def __init__(self, text='', style='', focusable=False, key_bindings=None, show_cursor=True, modal=False, get_cursor_position=None): from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase assert isinstance(style, six.text_type) assert key_bindings is None or isinstance(key_bindings, KeyBindingsBase) assert isinstance(show_cursor, bool) assert isinstance(modal, bool) assert get_cursor_position is None or callable(get_cursor_position) self.text = text # No type check on 'text'. This is done dynamically. self.style = style self.focusable = to_filter(focusable) # Key bindings. self.key_bindings = key_bindings self.show_cursor = show_cursor self.modal = modal self.get_cursor_position = get_cursor_position #: Cache for the content. self._content_cache = SimpleCache(maxsize=18) self._fragment_cache = SimpleCache(maxsize=1) # Only cache one fragment list. We don't need the previous item. # Render info for the mouse support. self._fragments = None
def __init__(self, get_tokens, default_char=None, get_default_char=None, align_right=False, align_center=False, has_focus=False): assert callable(get_tokens) assert default_char is None or isinstance(default_char, Char) assert get_default_char is None or callable(get_default_char) assert not (default_char and get_default_char) self.align_right = to_cli_filter(align_right) self.align_center = to_cli_filter(align_center) self._has_focus_filter = to_cli_filter(has_focus) self.get_tokens = get_tokens # Construct `get_default_char` callable. if default_char: get_default_char = lambda _: default_char elif not get_default_char: get_default_char = lambda _: Char(' ', Token.Transparent) self.get_default_char = get_default_char #: Cache for the content. self._content_cache = SimpleCache(maxsize=18) self._token_cache = SimpleCache(maxsize=1) # Only cache one token list. We don't need the previous item. # Render info for the mouse support. self._tokens = None
def __init__( self, text: AnyFormattedText = "", style: str = "", focusable: FilterOrBool = False, key_bindings: Optional["KeyBindingsBase"] = None, show_cursor: bool = True, modal: bool = False, get_cursor_position: Optional[Callable[[], Optional[Point]]] = None, ) -> None: self.text = text # No type check on 'text'. This is done dynamically. self.style = style self.focusable = to_filter(focusable) # Key bindings. self.key_bindings = key_bindings self.show_cursor = show_cursor self.modal = modal self.get_cursor_position = get_cursor_position #: Cache for the content. self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18) self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache( maxsize=1) # Only cache one fragment list. We don't need the previous item. # Render info for the mouse support. self._fragments: Optional[StyleAndTextTuples] = None
def __init__(self, buffer=None, input_processors=None, include_default_input_processors=True, lexer=None, preview_search=False, focusable=True, search_buffer_control=None, menu_position=None, focus_on_click=False, key_bindings=None): from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase assert buffer is None or isinstance(buffer, Buffer) assert input_processors is None or isinstance(input_processors, list) assert isinstance(include_default_input_processors, bool) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert (search_buffer_control is None or callable(search_buffer_control) or isinstance(search_buffer_control, SearchBufferControl)) assert key_bindings is None or isinstance(key_bindings, KeyBindingsBase) self.input_processors = input_processors self.include_default_input_processors = include_default_input_processors self.default_input_processors = [ HighlightSearchProcessor(), HighlightIncrementalSearchProcessor(), HighlightSelectionProcessor(), DisplayMultipleCursors(), ] self.preview_search = to_filter(preview_search) self.focusable = to_filter(focusable) self.focus_on_click = to_filter(focus_on_click) self.buffer = buffer or Buffer() self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.key_bindings = key_bindings self._search_buffer_control = search_buffer_control #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a fairly easy way to cache such an expensive operation. self._fragment_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None self._last_get_processed_line = None
def __init__(self, content, width=None, height=None, get_width=None, get_height=None, dont_extend_width=False, dont_extend_height=False, left_margins=None, right_margins=None, scroll_offsets=None, allow_scroll_beyond_bottom=False, get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False): assert isinstance(content, UIControl) assert width is None or isinstance(width, LayoutDimension) assert height is None or isinstance(height, LayoutDimension) assert get_width is None or callable(get_width) assert get_height is None or callable(get_height) assert width is None or get_width is None assert height is None or get_height is None assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets) assert left_margins is None or all( isinstance(m, Margin) for m in left_margins) assert right_margins is None or all( isinstance(m, Margin) for m in right_margins) assert get_vertical_scroll is None or callable(get_vertical_scroll) assert get_horizontal_scroll is None or callable(get_horizontal_scroll) self.allow_scroll_beyond_bottom = to_cli_filter( allow_scroll_beyond_bottom) self.always_hide_cursor = to_cli_filter(always_hide_cursor) self.content = content self.dont_extend_width = dont_extend_width self.dont_extend_height = dont_extend_height self.left_margins = left_margins or [] self.right_margins = right_margins or [] self.scroll_offsets = scroll_offsets or ScrollOffsets() self.get_vertical_scroll = get_vertical_scroll self.get_horizontal_scroll = get_horizontal_scroll self._width = get_width or (lambda cli: width) self._height = get_height or (lambda cli: height) # Cache for the screens generated by the margin. self._margin_cache = SimpleCache(maxsize=8) self.reset()
def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, highlighters=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, wrap_lines=True, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all(isinstance(i, Processor) for i in input_processors) assert highlighters is None or all(isinstance(i, Highlighter) for i in highlighters) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.wrap_lines = to_cli_filter(wrap_lines) self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.highlighters = highlighters or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) #: Keep a similar cache for rendered screens. (when we scroll up/down #: through the screen, or when we change another buffer, we don't want #: to recreate the same screen again.) self._screen_cache = SimpleCache(maxsize=8) #: Highlight Cache. #: When nothing of the buffer content or processors has changed, but #: the highlighting of the selection/search changes, self._highlight_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None
def __init__( self, chars: str = "[](){}<>", max_cursor_distance: int = 1000 ) -> None: self.chars = chars self.max_cursor_distance = max_cursor_distance self._positions_cache: SimpleCache[ Hashable, List[Tuple[int, int]] ] = SimpleCache(maxsize=8)
def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all( isinstance(i, Processor) for i in input_processors) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) assert default_char is None or isinstance(default_char, Char) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None self._last_get_processed_line = None
def __init__( self, buffer: Optional[Buffer] = None, input_processors: Optional[List[Processor]] = None, include_default_input_processors: bool = True, lexer: Optional[Lexer] = None, preview_search: FilterOrBool = False, focusable: FilterOrBool = True, search_buffer_control: Union[None, "SearchBufferControl", Callable[[], "SearchBufferControl"]] = None, menu_position: Optional[Callable] = None, focus_on_click: FilterOrBool = False, key_bindings: Optional["KeyBindingsBase"] = None, ): self.input_processors = input_processors self.include_default_input_processors = include_default_input_processors self.default_input_processors = [ HighlightSearchProcessor(), HighlightIncrementalSearchProcessor(), HighlightSelectionProcessor(), DisplayMultipleCursors(), ] self.preview_search = to_filter(preview_search) self.focusable = to_filter(focusable) self.focus_on_click = to_filter(focus_on_click) self.buffer = buffer or Buffer() self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.key_bindings = key_bindings self._search_buffer_control = search_buffer_control #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a fairly easy way to cache such an expensive operation. self._fragment_cache: SimpleCache[Hashable, Callable[ [int], StyleAndTextTuples]] = SimpleCache(maxsize=8) self._last_click_timestamp: Optional[float] = None self._last_get_processed_line: Optional[Callable[ [int], _ProcessedLine]] = None
def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, highlighters=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, wrap_lines=True, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all( isinstance(i, Processor) for i in input_processors) assert highlighters is None or all( isinstance(i, Highlighter) for i in highlighters) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.wrap_lines = to_cli_filter(wrap_lines) self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.highlighters = highlighters or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) #: Keep a similar cache for rendered screens. (when we scroll up/down #: through the screen, or when we change another buffer, we don't want #: to recreate the same screen again.) self._screen_cache = SimpleCache(maxsize=8) #: Highlight Cache. #: When nothing of the buffer content or processors has changed, but #: the highlighting of the selection/search changes, self._highlight_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None
class _MergedStyle(BaseStyle): """ Merge multiple `Style` objects into one. This is supposed to ensure consistency: if any of the given styles changes, then this style will be updated. """ # NOTE: previously, we used an algorithm where we did not generate the # combined style. Instead this was a proxy that called one style # after the other, passing the outcome of the previous style as the # default for the next one. This did not work, because that way, the # priorities like described in the `Style` class don't work. # 'class:aborted' was for instance never displayed in gray, because # the next style specified a default color for any text. (The # explicit styling of class:aborted should have taken priority, # because it was more precise.) def __init__(self, styles): assert all(isinstance(style, BaseStyle) for style in styles) self.styles = styles self._style = SimpleCache(maxsize=1) @property def _merged_style(self): " The `Style` object that has the other styles merged together. " def get(): return Style(self.style_rules) return self._style.get(self.invalidation_hash(), get) @property def style_rules(self): style_rules = [] for s in self.styles: style_rules.extend(s.style_rules) return style_rules def get_attrs_for_style_str(self, style_str, default=DEFAULT_ATTRS): return self._merged_style.get_attrs_for_style_str(style_str, default) def invalidation_hash(self): return tuple(s.invalidation_hash() for s in self.styles)
def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all(isinstance(i, Processor) for i in input_processors) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) assert default_char is None or isinstance(default_char, Char) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) self._processed_token_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None self._last_get_processed_line = None
def __init__(self, content, width=None, height=None, get_width=None, get_height=None, dont_extend_width=False, dont_extend_height=False, left_margins=None, right_margins=None, scroll_offsets=None, allow_scroll_beyond_bottom=False, get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False): assert isinstance(content, UIControl) assert width is None or isinstance(width, LayoutDimension) assert height is None or isinstance(height, LayoutDimension) assert get_width is None or callable(get_width) assert get_height is None or callable(get_height) assert width is None or get_width is None assert height is None or get_height is None assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets) assert left_margins is None or all(isinstance(m, Margin) for m in left_margins) assert right_margins is None or all(isinstance(m, Margin) for m in right_margins) assert get_vertical_scroll is None or callable(get_vertical_scroll) assert get_horizontal_scroll is None or callable(get_horizontal_scroll) self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom) self.always_hide_cursor = to_cli_filter(always_hide_cursor) self.content = content self.dont_extend_width = dont_extend_width self.dont_extend_height = dont_extend_height self.left_margins = left_margins or [] self.right_margins = right_margins or [] self.scroll_offsets = scroll_offsets or ScrollOffsets() self.get_vertical_scroll = get_vertical_scroll self.get_horizontal_scroll = get_horizontal_scroll self._width = get_width or (lambda cli: width) self._height = get_height or (lambda cli: height) # Cache for the screens generated by the margin. self._margin_cache = SimpleCache(maxsize=8) self.reset()
class FormattedTextControl(UIControl): """ Control that displays formatted text. This can be either plain text, an :class:`~prompt_toolkit.formatted_text.HTML` object an :class:`~prompt_toolkit.formatted_text.ANSI` object or a list of ``(style_str, text)`` tuples, depending on how you prefer to do the formatting. See ``prompt_toolkit.layout.formatted_text`` for more information. (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) When this UI control has the focus, the cursor will be shown in the upper left corner of this control by default. There are two ways for specifying the cursor position: - Pass a `get_cursor_position` function which returns a `Point` instance with the current cursor position. - If the (formatted) text is passed as a list of ``(style, text)`` tuples and there is one that looks like ``('[SetCursorPosition]', '')``, then this will specify the cursor position. Mouse support: The list of fragments can also contain tuples of three items, looking like: (style_str, text, handler). When mouse support is enabled and the user clicks on this fragment, then the given handler is called. That handler should accept two inputs: (Application, MouseEvent) and it should either handle the event or return `NotImplemented` in case we want the containing Window to handle this event. :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. :param text: Text or formatted text to be displayed. :param style: Style string applied to the content. (If you want to style the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the :class:`~prompt_toolkit.layout.Window` instead.) :param key_bindings: a :class:`.KeyBindings` object. :param get_cursor_position: A callable that returns the cursor position as a `Point` instance. """ def __init__(self, text='', style='', focusable=False, key_bindings=None, show_cursor=True, modal=False, get_cursor_position=None): from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase assert isinstance(style, six.text_type) assert key_bindings is None or isinstance(key_bindings, KeyBindingsBase) assert isinstance(show_cursor, bool) assert isinstance(modal, bool) assert get_cursor_position is None or callable(get_cursor_position) self.text = text # No type check on 'text'. This is done dynamically. self.style = style self.focusable = to_filter(focusable) # Key bindings. self.key_bindings = key_bindings self.show_cursor = show_cursor self.modal = modal self.get_cursor_position = get_cursor_position #: Cache for the content. self._content_cache = SimpleCache(maxsize=18) self._fragment_cache = SimpleCache(maxsize=1) # Only cache one fragment list. We don't need the previous item. # Render info for the mouse support. self._fragments = None def reset(self): self._fragments = None def is_focusable(self): return self.focusable() def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.text) def _get_formatted_text_cached(self): """ Get fragments, but only retrieve fragments once during one render run. (This function is called several times during one rendering, because we also need those for calculating the dimensions.) """ return self._fragment_cache.get( get_app().render_counter, lambda: to_formatted_text(self.text, self.style)) def preferred_width(self, max_available_width): """ Return the preferred width for this control. That is the width of the longest line. """ text = fragment_list_to_text(self._get_formatted_text_cached()) line_lengths = [get_cwidth(l) for l in text.split('\n')] return max(line_lengths) def preferred_height(self, width, max_available_height, wrap_lines): content = self.create_content(width, None) return content.line_count def create_content(self, width, height): # Get fragments fragments_with_mouse_handlers = self._get_formatted_text_cached() fragment_lines_with_mouse_handlers = list( split_lines(fragments_with_mouse_handlers)) # Strip mouse handlers from fragments. fragment_lines = [[tuple(item[:2]) for item in line] for line in fragment_lines_with_mouse_handlers] # Keep track of the fragments with mouse handler, for later use in # `mouse_handler`. self._fragments = fragments_with_mouse_handlers # If there is a `[SetCursorPosition]` in the fragment list, set the # cursor position here. def get_cursor_position(fragment='[SetCursorPosition]'): for y, line in enumerate(fragment_lines): x = 0 for style_str, text in line: if fragment in style_str: return Point(x=x, y=y) x += len(text) return None # If there is a `[SetMenuPosition]`, set the menu over here. def get_menu_position(): return get_cursor_position('[SetMenuPosition]') cursor_position = (self.get_cursor_position or get_cursor_position)() # Create content, or take it from the cache. key = (tuple(fragments_with_mouse_handlers), width, cursor_position) def get_content(): return UIContent(get_line=lambda i: fragment_lines[i], line_count=len(fragment_lines), show_cursor=self.show_cursor, cursor_position=cursor_position, menu_position=get_menu_position()) return self._content_cache.get(key, get_content) def mouse_handler(self, mouse_event): """ Handle mouse events. (When the fragment list contained mouse handlers and the user clicked on on any of these, the matching handler is called. This handler can still return `NotImplemented` in case we want the :class:`~prompt_toolkit.layout.Window` to handle this particular event.) """ if self._fragments: # Read the generator. fragments_for_line = list(split_lines(self._fragments)) try: fragments = fragments_for_line[mouse_event.position.y] except IndexError: return NotImplemented else: # Find position in the fragment list. xpos = mouse_event.position.x # Find mouse handler for this character. count = 0 for item in fragments: count += len(item[1]) if count >= xpos: if len(item) >= 3: # Handler found. Call it. # (Handler can return NotImplemented, so return # that result.) handler = item[2] return handler(mouse_event) else: break # Otherwise, don't handle here. return NotImplemented def is_modal(self): return self.modal def get_key_bindings(self): return self.key_bindings
def __init__(self, app: Application[_AppResult]) -> None: self.app = app self._cache: SimpleCache[Tuple[Window, FrozenSet[UIControl]], KeyBindingsBase] = SimpleCache()
class KeyBindings(KeyBindingsBase): """ A container for a set of key bindings. Example usage:: kb = KeyBindings() @kb.add('c-t') def _(event): print('Control-T pressed') @kb.add('c-a', 'c-b') def _(event): print('Control-A pressed, followed by Control-B') @kb.add('c-x', filter=is_searching) def _(event): print('Control-X pressed') # Works only if we are searching. """ def __init__(self): self.bindings = [] self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000) self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000) self.__version = 0 # For cache invalidation. def _clear_cache(self): self.__version += 1 self._get_bindings_for_keys_cache.clear() self._get_bindings_starting_with_keys_cache.clear() @property def _version(self): return self.__version def add(self, *keys, **kwargs): """ Decorator for adding a key bindings. :param filter: :class:`~prompt_toolkit.filters.Filter` to determine when this key binding is active. :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`. When True, ignore potential longer matches when this key binding is hit. E.g. when there is an active eager key binding for Ctrl-X, execute the handler immediately and ignore the key binding for Ctrl-X Ctrl-E of which it is a prefix. :param is_global: When this key bindings is added to a `Container` or `Control`, make it a global (always active) binding. :param save_before: Callable that takes an `Event` and returns True if we should save the current buffer, before handling the event. (That's the default.) :param record_in_macro: Record these key bindings when a macro is being recorded. (True by default.) """ filter = to_filter(kwargs.pop('filter', True)) eager = to_filter(kwargs.pop('eager', False)) is_global = to_filter(kwargs.pop('is_global', False)) save_before = kwargs.pop('save_before', lambda e: True) record_in_macro = to_filter(kwargs.pop('record_in_macro', True)) assert not kwargs assert keys assert callable(save_before) keys = tuple(_check_and_expand_key(k) for k in keys) if isinstance(filter, Never): # When a filter is Never, it will always stay disabled, so in that # case don't bother putting it in the key bindings. It will slow # down every key press otherwise. def decorator(func): return func else: def decorator(func): if isinstance(func, _Binding): # We're adding an existing _Binding object. self.bindings.append( _Binding( keys, func.handler, filter=func.filter & filter, eager=eager | func.eager, is_global=is_global | func.is_global, save_before=func.save_before, record_in_macro=func.record_in_macro)) else: self.bindings.append( _Binding(keys, func, filter=filter, eager=eager, is_global=is_global, save_before=save_before, record_in_macro=record_in_macro)) self._clear_cache() return func return decorator def remove(self, *args): """ Remove a key binding. This expects either a function that was given to `add` method as parameter or a sequence of key bindings. Raises `ValueError` when no bindings was found. Usage:: remove(handler) # Pass handler. remove('c-x', 'c-a') # Or pass the key bindings. """ found = False if callable(args[0]): assert len(args) == 1 function = args[0] # Remove the given function. for b in self.bindings: if b.handler == function: self.bindings.remove(b) found = True else: assert len(args) > 0 # Remove this sequence of key bindings. keys = tuple(_check_and_expand_key(k) for k in args) for b in self.bindings: if b.keys == keys: self.bindings.remove(b) found = True if found: self._clear_cache() else: # No key binding found for this function. Raise ValueError. raise ValueError('Binding not found: %r' % (function,)) # For backwards-compatibility. add_binding = add remove_binding = remove def get_bindings_for_keys(self, keys): """ Return a list of key bindings that can handle this key. (This return also inactive bindings, so the `filter` still has to be called, for checking it.) :param keys: tuple of keys. """ def get(): result = [] for b in self.bindings: if len(keys) == len(b.keys): match = True any_count = 0 for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if i == Keys.Any: any_count += 1 if match: result.append((any_count, b)) # Place bindings that have more 'Any' occurrences in them at the end. result = sorted(result, key=lambda item: -item[0]) return [item[1] for item in result] return self._get_bindings_for_keys_cache.get(keys, get) def get_bindings_starting_with_keys(self, keys): """ Return a list of key bindings that handle a key sequence starting with `keys`. (It does only return bindings for which the sequences are longer than `keys`. And like `get_bindings_for_keys`, it also includes inactive bindings.) :param keys: tuple of keys. """ def get(): result = [] for b in self.bindings: if len(keys) < len(b.keys): match = True for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if match: result.append(b) return result return self._get_bindings_starting_with_keys_cache.get(keys, get)
def __init__(self, buffer=None, input_processor=None, lexer=None, preview_search=False, focusable=True, search_buffer_control=None, get_search_buffer_control=None, get_search_state=None, menu_position=None, focus_on_click=False, key_bindings=None): from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase assert buffer is None or isinstance(buffer, Buffer) assert input_processor is None or isinstance(input_processor, Processor) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert search_buffer_control is None or isinstance( search_buffer_control, BufferControl) assert get_search_buffer_control is None or callable( get_search_buffer_control) assert not (search_buffer_control and get_search_buffer_control) assert get_search_state is None or callable(get_search_state) assert key_bindings is None or isinstance(key_bindings, KeyBindingsBase) # Default search state. if get_search_state is None: search_state = SearchState() def get_search_state(): return search_state # Default input processor (display search and selection by default.) if input_processor is None: input_processor = merge_processors([ HighlightSearchProcessor(), HighlightSelectionProcessor(), DisplayMultipleCursors(), ]) self.preview_search = to_filter(preview_search) self.focusable = to_filter(focusable) self.get_search_state = get_search_state self.focus_on_click = to_filter(focus_on_click) self.input_processor = input_processor self.buffer = buffer or Buffer() self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.get_search_buffer_control = get_search_buffer_control self.key_bindings = key_bindings self._search_buffer_control = search_buffer_control #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a fairly easy way to cache such an expensive operation. self._fragment_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None self._last_get_processed_line = None
class _CombinedRegistry(KeyBindingsBase): """ The `KeyBindings` of key bindings for a `Application`. This merges the global key bindings with the one of the current user control. """ def __init__(self, app): self.app = app self._cache = SimpleCache() @property def _version(self): """ Not needed - this object is not going to be wrapped in another KeyBindings object. """ raise NotImplementedError def _create_key_bindings(self, current_window, other_controls): """ Create a `KeyBindings` object that merges the `KeyBindings` from the `UIControl` with all the parent controls and the global key bindings. """ key_bindings = [] collected_containers = set() # Collect key bindings from currently focused control and all parent # controls. Don't include key bindings of container parent controls. container = current_window while True: collected_containers.add(container) kb = container.get_key_bindings() if kb is not None: key_bindings.append(kb) if container.is_modal(): break parent = self.app.layout.get_parent(container) if parent is None: break else: container = parent # Include global bindings (starting at the top-model container). for c in walk(container): if c not in collected_containers: kb = c.get_key_bindings() if kb is not None: key_bindings.append(GlobalOnlyKeyBindings(kb)) # Add App key bindings if self.app.key_bindings: key_bindings.append(self.app.key_bindings) # Add mouse bindings. key_bindings.append(ConditionalKeyBindings( self.app._page_navigation_bindings, self.app.enable_page_navigation_bindings)) key_bindings.append(self.app._default_bindings) # Reverse this list. The current control's key bindings should come # last. They need priority. key_bindings = key_bindings[::-1] return merge_key_bindings(key_bindings) @property def _key_bindings(self): current_window = self.app.layout.current_window other_controls = list(self.app.layout.find_all_controls()) key = current_window, frozenset(other_controls) return self._cache.get( key, lambda: self._create_key_bindings(current_window, other_controls)) def get_bindings_for_keys(self, keys): return self._key_bindings.get_bindings_for_keys(keys) def get_bindings_starting_with_keys(self, keys): return self._key_bindings.get_bindings_starting_with_keys(keys)
class TokenListControl(UIControl): """ Control that displays a list of (Token, text) tuples. (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) Mouse support: The list of tokens can also contain tuples of three items, looking like: (Token, text, handler). When mouse support is enabled and the user clicks on this token, then the given handler is called. That handler should accept two inputs: (CommandLineInterface, MouseEvent) and it should either handle the event or return `NotImplemented` in case we want the containing Window to handle this event. :param get_tokens: Callable that takes a `CommandLineInterface` instance and returns the list of (Token, text) tuples to be displayed right now. :param default_char: default :class:`.Char` (character and Token) to use for the background when there is more space available than `get_tokens` returns. :param get_default_char: Like `default_char`, but this is a callable that takes a :class:`prompt_toolkit.interface.CommandLineInterface` and returns a :class:`.Char` instance. :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`, this UI control will take the focus. The cursor will be shown in the upper left corner of this control, unless `get_token` returns a ``Token.SetCursorPosition`` token somewhere in the token list, then the cursor will be shown there. """ def __init__(self, get_tokens, default_char=None, get_default_char=None, align_right=False, align_center=False, has_focus=False): assert default_char is None or isinstance(default_char, Char) assert get_default_char is None or callable(get_default_char) assert not (default_char and get_default_char) self.align_right = to_cli_filter(align_right) self.align_center = to_cli_filter(align_center) self._has_focus_filter = to_cli_filter(has_focus) self.get_tokens = get_tokens # Construct `get_default_char` callable. if default_char: get_default_char = lambda _: default_char elif not get_default_char: get_default_char = lambda _: Char(' ', Token) self.get_default_char = get_default_char #: Cache for the content. self._content_cache = SimpleCache(maxsize=18) self._token_cache = SimpleCache(maxsize=1) # Only cache one token list. We don't need the previous item. # Render info for the mouse support. self._tokens = None def reset(self): self._tokens = None def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.get_tokens) def _get_tokens_cached(self, cli): """ Get tokens, but only retrieve tokens once during one render run. (This function is called several times during one rendering, because we also need those for calculating the dimensions.) """ return self._token_cache.get( cli.render_counter, lambda: self.get_tokens(cli)) def has_focus(self, cli): return self._has_focus_filter(cli) def preferred_width(self, cli, max_available_width): """ Return the preferred width for this control. That is the width of the longest line. """ text = token_list_to_text(self._get_tokens_cached(cli)) line_lengths = [get_cwidth(l) for l in text.split('\n')] return max(line_lengths) def preferred_height(self, cli, width, max_available_height, wrap_lines): content = self.create_content(cli, width, None) return content.line_count def create_content(self, cli, width, height): # Get tokens tokens_with_mouse_handlers = self._get_tokens_cached(cli) default_char = self.get_default_char(cli) # Wrap/align right/center parameters. right = self.align_right(cli) center = self.align_center(cli) def process_line(line): " Center or right align a single line. " used_width = token_list_width(line) padding = width - used_width if center: padding = int(padding / 2) return [(default_char.token, default_char.char * padding)] + line if right or center: token_lines_with_mouse_handlers = [] for line in split_lines(tokens_with_mouse_handlers): token_lines_with_mouse_handlers.append(process_line(line)) else: token_lines_with_mouse_handlers = list(split_lines(tokens_with_mouse_handlers)) # Strip mouse handlers from tokens. token_lines = [ [tuple(item[:2]) for item in line] for line in token_lines_with_mouse_handlers ] # Keep track of the tokens with mouse handler, for later use in # `mouse_handler`. self._tokens = tokens_with_mouse_handlers # If there is a `Token.SetCursorPosition` in the token list, set the # cursor position here. def get_cursor_position(): SetCursorPosition = Token.SetCursorPosition for y, line in enumerate(token_lines): x = 0 for token, text in line: if token == SetCursorPosition: return Point(x=x, y=y) x += len(text) return None # Create content, or take it from the cache. key = (default_char.char, default_char.token, tuple(tokens_with_mouse_handlers), width, right, center) def get_content(): return UIContent(get_line=lambda i: token_lines[i], line_count=len(token_lines), default_char=default_char, cursor_position=get_cursor_position()) return self._content_cache.get(key, get_content) @classmethod def static(cls, tokens): def get_static_tokens(cli): return tokens return cls(get_static_tokens) def mouse_handler(self, cli, mouse_event): """ Handle mouse events. (When the token list contained mouse handlers and the user clicked on on any of these, the matching handler is called. This handler can still return `NotImplemented` in case we want the `Window` to handle this particular event.) """ if self._tokens: # Read the generator. tokens_for_line = list(split_lines(self._tokens)) try: tokens = tokens_for_line[mouse_event.position.y] except IndexError: return NotImplemented else: # Find position in the token list. xpos = mouse_event.position.x # Find mouse handler for this character. count = 0 for item in tokens: count += len(item[1]) if count >= xpos: if len(item) >= 3: # Handler found. Call it. # (Handler can return NotImplemented, so return # that result.) handler = item[2] return handler(cli, mouse_event) else: break # Otherwise, don't handle here. return NotImplemented
def __init__(self): self.key_bindings = [] self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000) self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
def __init__(self): self.bindings = [] self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000) self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000) self.__version = 0 # For cache invalidation.
def __init__(self, styles): assert all(isinstance(style, BaseStyle) for style in styles) self.styles = styles self._style = SimpleCache(maxsize=1)
class HighlightMatchingBracketProcessor(Processor): """ When the cursor is on or right after a bracket, it highlights the matching bracket. :param max_cursor_distance: Only highlight matching brackets when the cursor is within this distance. (From inside a `Processor`, we can't know which lines will be visible on the screen. But we also don't want to scan the whole document for matching brackets on each key press, so we limit to this value.) """ _closing_braces = '])}>' def __init__(self, chars='[](){}<>', max_cursor_distance=1000): self.chars = chars self.max_cursor_distance = max_cursor_distance self._positions_cache = SimpleCache(maxsize=8) def _get_positions_to_highlight(self, document): """ Return a list of (row, col) tuples that need to be highlighted. """ # Try for the character under the cursor. if document.current_char and document.current_char in self.chars: pos = document.find_matching_bracket_position( start_pos=document.cursor_position - self.max_cursor_distance, end_pos=document.cursor_position + self.max_cursor_distance) # Try for the character before the cursor. elif (document.char_before_cursor and document.char_before_cursor in self._closing_braces and document.char_before_cursor in self.chars): document = Document(document.text, document.cursor_position - 1) pos = document.find_matching_bracket_position( start_pos=document.cursor_position - self.max_cursor_distance, end_pos=document.cursor_position + self.max_cursor_distance) else: pos = None # Return a list of (row, col) tuples that need to be highlighted. if pos: pos += document.cursor_position # pos is relative. row, col = document.translate_index_to_position(pos) return [(row, col), (document.cursor_position_row, document.cursor_position_col)] else: return [] def apply_transformation(self, cli, document, lineno, source_to_display, tokens): # Get the highlight positions. key = (cli.render_counter, document.text, document.cursor_position) positions = self._positions_cache.get( key, lambda: self._get_positions_to_highlight(document)) # Apply if positions were foun at this line. if positions: for row, col in positions: if row == lineno: col = source_to_display(col) tokens = explode_tokens(tokens) tokens[col] = (Token.MatchingBracket, tokens[col][1]) return Transformation(tokens)
def __init__(self, chars='[](){}<>', max_cursor_distance=1000): self.chars = chars self.max_cursor_distance = max_cursor_distance self._positions_cache = SimpleCache(maxsize=8)
def __init__(self, app): self.app = app self._cache = SimpleCache()
class BufferControl(UIControl): """ Control for visualising the content of a `Buffer`. :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`. :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting. :param preview_search: `bool` or `CLIFilter`: Show search while typing. :param get_search_state: Callable that takes a CommandLineInterface and returns the SearchState to be used. (If not CommandLineInterface.search_state.) :param buffer_name: String representing the name of the buffer to display. :param default_char: :class:`.Char` instance to use to fill the background. This is transparent by default. :param focus_on_click: Focus this buffer when it's click, but not yet focussed. """ def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all(isinstance(i, Processor) for i in input_processors) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) assert default_char is None or isinstance(default_char, Char) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) self._processed_token_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None self._last_get_processed_line = None def _buffer(self, cli): """ The buffer object that contains the 'main' content. """ return cli.buffers[self.buffer_name] def has_focus(self, cli): # This control gets the focussed if the actual `Buffer` instance has the # focus or when any of the `InputProcessor` classes tells us that it # wants the focus. (E.g. in case of a reverse-search, where the actual # search buffer may not be displayed, but the "reverse-i-search" text # should get the focus.) return cli.current_buffer_name == self.buffer_name or \ any(i.has_focus(cli) for i in self.input_processors) def preferred_width(self, cli, max_available_width): """ This should return the preferred width. Note: We don't specify a preferred width according to the content, because it would be too expensive. Calculating the preferred width can be done by calculating the longest line, but this would require applying all the processors to each line. This is unfeasible for a larger document, and doing it for small documents only would result in inconsistent behaviour. """ return None def preferred_height(self, cli, width, max_available_height, wrap_lines): # Calculate the content height, if it was drawn on a screen with the # given width. height = 0 content = self.create_content(cli, width, None) # When line wrapping is off, the height should be equal to the amount # of lines. if not wrap_lines: return content.line_count # When the number of lines exceeds the max_available_height, just # return max_available_height. No need to calculate anything. if content.line_count >= max_available_height: return max_available_height for i in range(content.line_count): height += content.get_height_for_line(i, width) if height >= max_available_height: return max_available_height return height def _get_tokens_for_line_func(self, cli, document): """ Create a function that returns the tokens for a given line. """ # Cache using `document.text`. def get_tokens_for_line(): return self.lexer.lex_document(cli, document) return self._token_cache.get(document.text, get_tokens_for_line) def _create_get_processed_line_func(self, cli, document): """ Create a function that takes a line number of the current document and returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source) tuple. """ def transform(lineno, tokens): " Transform the tokens for a given line number. " source_to_display_functions = [] display_to_source_functions = [] # Get cursor position at this line. if document.cursor_position_row == lineno: cursor_column = document.cursor_position_col else: cursor_column = None def source_to_display(i): """ Translate x position from the buffer to the x position in the processed token list. """ for f in source_to_display_functions: i = f(i) return i # Apply each processor. for p in self.input_processors: transformation = p.apply_transformation( cli, document, lineno, source_to_display, tokens) tokens = transformation.tokens if cursor_column: cursor_column = transformation.source_to_display(cursor_column) display_to_source_functions.append(transformation.display_to_source) source_to_display_functions.append(transformation.source_to_display) def display_to_source(i): for f in reversed(display_to_source_functions): i = f(i) return i return _ProcessedLine(tokens, source_to_display, display_to_source) def create_func(): get_line = self._get_tokens_for_line_func(cli, document) cache = {} def get_processed_line(i): try: return cache[i] except KeyError: processed_line = transform(i, get_line(i)) cache[i] = processed_line return processed_line return get_processed_line return create_func() def create_content(self, cli, width, height): """ Create a UIContent. """ buffer = self._buffer(cli) # Get the document to be shown. If we are currently searching (the # search buffer has focus, and the preview_search filter is enabled), # then use the search document, which has possibly a different # text/cursor position.) def preview_now(): """ True when we should preview a search. """ return bool(self.preview_search(cli) and cli.buffers[self.search_buffer_name].text) if preview_now(): if self.get_search_state: ss = self.get_search_state(cli) else: ss = cli.search_state document = buffer.document_for_search(SearchState( text=cli.current_buffer.text, direction=ss.direction, ignore_case=ss.ignore_case)) else: document = buffer.document get_processed_line = self._create_get_processed_line_func(cli, document) self._last_get_processed_line = get_processed_line def translate_rowcol(row, col): " Return the content column for this coordinate. " return Point(y=row, x=get_processed_line(row).source_to_display(col)) def get_line(i): " Return the tokens for a given line number. " tokens = get_processed_line(i).tokens # Add a space at the end, because that is a possible cursor # position. (When inserting after the input.) We should do this on # all the lines, not just the line containing the cursor. (Because # otherwise, line wrapping/scrolling could change when moving the # cursor around.) tokens = tokens + [(self.default_char.token, ' ')] return tokens content = UIContent( get_line=get_line, line_count=document.line_count, cursor_position=translate_rowcol(document.cursor_position_row, document.cursor_position_col), default_char=self.default_char) # If there is an auto completion going on, use that start point for a # pop-up menu position. (But only when this buffer has the focus -- # there is only one place for a menu, determined by the focussed buffer.) if cli.current_buffer_name == self.buffer_name: menu_position = self.menu_position(cli) if self.menu_position else None if menu_position is not None: assert isinstance(menu_position, int) menu_row, menu_col = buffer.document.translate_index_to_position(menu_position) content.menu_position = translate_rowcol(menu_row, menu_col) elif buffer.complete_state: # Position for completion menu. # Note: We use 'min', because the original cursor position could be # behind the input string when the actual completion is for # some reason shorter than the text we had before. (A completion # can change and shorten the input.) menu_row, menu_col = buffer.document.translate_index_to_position( min(buffer.cursor_position, buffer.complete_state.original_document.cursor_position)) content.menu_position = translate_rowcol(menu_row, menu_col) else: content.menu_position = None return content def mouse_handler(self, cli, mouse_event): """ Mouse handler for this control. """ buffer = self._buffer(cli) position = mouse_event.position # Focus buffer when clicked. if self.has_focus(cli): if self._last_get_processed_line: processed_line = self._last_get_processed_line(position.y) # Translate coordinates back to the cursor position of the # original input. xpos = processed_line.display_to_source(position.x) index = buffer.document.translate_row_col_to_index(position.y, xpos) # Set the cursor position. if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: buffer.exit_selection() buffer.cursor_position = index elif mouse_event.event_type == MouseEventTypes.MOUSE_UP: # When the cursor was moved to another place, select the text. # (The >1 is actually a small but acceptable workaround for # selecting text in Vi navigation mode. In navigation mode, # the cursor can never be after the text, so the cursor # will be repositioned automatically.) if abs(buffer.cursor_position - index) > 1: buffer.start_selection(selection_type=SelectionType.CHARACTERS) buffer.cursor_position = index # Select word around cursor on double click. # Two MOUSE_UP events in a short timespan are considered a double click. double_click = self._last_click_timestamp and time.time() - self._last_click_timestamp < .3 self._last_click_timestamp = time.time() if double_click: start, end = buffer.document.find_boundaries_of_current_word() buffer.cursor_position += start buffer.start_selection(selection_type=SelectionType.CHARACTERS) buffer.cursor_position += end - start else: # Don't handle scroll events here. return NotImplemented # Not focussed, but focussing on click events. else: if self.focus_on_click(cli) and mouse_event.event_type == MouseEventTypes.MOUSE_UP: # Focus happens on mouseup. (If we did this on mousedown, the # up event will be received at the point where this widget is # focussed and be handled anyway.) cli.focus(self.buffer_name) else: return NotImplemented def move_cursor_down(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_down_position() def move_cursor_up(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_up_position()
class Window(Container): """ Container that holds a control. :param content: :class:`~prompt_toolkit.layout.controls.UIControl` instance. :param width: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance. :param height: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance. :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`. :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`. :param dont_extend_width: When `True`, don't take up more width then the preferred width reported by the control. :param dont_extend_height: When `True`, don't take up more width then the preferred height reported by the control. :param left_margins: A list of :class:`~prompt_toolkit.layout.margins.Margin` instance to be displayed on the left. For instance: :class:`~prompt_toolkit.layout.margins.NumberredMargin` can be one of them in order to show line numbers. :param right_margins: Like `left_margins`, but on the other side. :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the preferred amount of lines/columns to be always visible before/after the cursor. When both top and bottom are a very high number, the cursor will be centered vertically most of the time. :param allow_scroll_beyond_bottom: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, allow scrolling so far, that the top part of the content is not visible anymore, while there is still empty space available at the bottom of the window. In the Vi editor for instance, this is possible. You will see tildes while the top part of the body is hidden. :param get_vertical_scroll: Callable that takes this window instance as input and returns a preferred vertical scroll. (When this is `None`, the scroll is only determined by the last and current cursor position.) :param get_horizontal_scroll: Callable that takes this window instance as input and returns a preferred vertical scroll. :param always_hide_cursor: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, never display the cursor, even when the user control specifies a cursor position. """ def __init__(self, content, width=None, height=None, get_width=None, get_height=None, dont_extend_width=False, dont_extend_height=False, left_margins=None, right_margins=None, scroll_offsets=None, allow_scroll_beyond_bottom=False, get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False): assert isinstance(content, UIControl) assert width is None or isinstance(width, LayoutDimension) assert height is None or isinstance(height, LayoutDimension) assert get_width is None or callable(get_width) assert get_height is None or callable(get_height) assert width is None or get_width is None assert height is None or get_height is None assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets) assert left_margins is None or all(isinstance(m, Margin) for m in left_margins) assert right_margins is None or all(isinstance(m, Margin) for m in right_margins) assert get_vertical_scroll is None or callable(get_vertical_scroll) assert get_horizontal_scroll is None or callable(get_horizontal_scroll) self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom) self.always_hide_cursor = to_cli_filter(always_hide_cursor) self.content = content self.dont_extend_width = dont_extend_width self.dont_extend_height = dont_extend_height self.left_margins = left_margins or [] self.right_margins = right_margins or [] self.scroll_offsets = scroll_offsets or ScrollOffsets() self.get_vertical_scroll = get_vertical_scroll self.get_horizontal_scroll = get_horizontal_scroll self._width = get_width or (lambda cli: width) self._height = get_height or (lambda cli: height) # Cache for the screens generated by the margin. self._margin_cache = SimpleCache(maxsize=8) self.reset() def __repr__(self): return 'Window(content=%r)' % self.content def reset(self): self.content.reset() #: Scrolling position of the main content. self.vertical_scroll = 0 self.horizontal_scroll = 0 #: Keep render information (mappings between buffer input and render #: output.) self.render_info = None def preferred_width(self, cli, max_available_width): # Width of the margins. total_margin_width = sum(m.get_width(cli) for m in self.left_margins + self.right_margins) # Window of the content. preferred_width = self.content.preferred_width( cli, max_available_width - total_margin_width) if preferred_width is not None: preferred_width += total_margin_width # Merge. return self._merge_dimensions( dimension=self._width(cli), preferred=preferred_width, dont_extend=self.dont_extend_width) def preferred_height(self, cli, width): return self._merge_dimensions( dimension=self._height(cli), preferred=self.content.preferred_height(cli, width), dont_extend=self.dont_extend_height) @staticmethod def _merge_dimensions(dimension, preferred=None, dont_extend=False): """ Take the LayoutDimension from this `Window` class and the received preferred size from the `UIControl` and return a `LayoutDimension` to report to the parent container. """ dimension = dimension or LayoutDimension() # When a preferred dimension was explicitly given to the Window, # ignore the UIControl. if dimension.preferred_specified: preferred = dimension.preferred # When a 'preferred' dimension is given by the UIControl, make sure # that it stays within the bounds of the Window. if preferred is not None: if dimension.max: preferred = min(preferred, dimension.max) if dimension.min: preferred = max(preferred, dimension.min) # When a `dont_extend` flag has been given, use the preferred dimension # also as the max dimension. if dont_extend and preferred is not None: max_ = min(dimension.max, preferred) else: max_ = dimension.max return LayoutDimension(min=dimension.min, max=max_, preferred=preferred) def write_to_screen(self, cli, screen, mouse_handlers, write_position): """ Write window to screen. This renders the user control, the margins and copies everything over to the absolute position at the given screen. """ # Calculate margin sizes. left_margin_widths = [m.get_width(cli) for m in self.left_margins] right_margin_widths = [m.get_width(cli) for m in self.right_margins] total_margin_width = sum(left_margin_widths + right_margin_widths) # Render UserControl. tpl = self.content.create_screen( cli, write_position.width - total_margin_width, write_position.height) if isinstance(tpl, tuple): temp_screen, highlighting = tpl else: # For backwards, compatibility. temp_screen, highlighting = tpl, defaultdict(lambda: defaultdict(lambda: None)) # Scroll content. applied_scroll_offsets = self._scroll( temp_screen, write_position.width - total_margin_width, write_position.height, cli) # Write body to screen. self._copy_body(cli, temp_screen, highlighting, screen, write_position, sum(left_margin_widths), write_position.width - total_margin_width, applied_scroll_offsets) # Remember render info. (Set before generating the margins. They need this.) self.render_info = WindowRenderInfo( original_screen=temp_screen, horizontal_scroll=self.horizontal_scroll, vertical_scroll=self.vertical_scroll, window_width=write_position.width, window_height=write_position.height, cursor_position=Point(y=temp_screen.cursor_position.y - self.vertical_scroll, x=temp_screen.cursor_position.x - self.horizontal_scroll), configured_scroll_offsets=self.scroll_offsets, applied_scroll_offsets=applied_scroll_offsets) # Set mouse handlers. def mouse_handler(cli, mouse_event): """ Wrapper around the mouse_handler of the `UIControl` that turns absolute coordinates into relative coordinates. """ position = mouse_event.position # Call the mouse handler of the UIControl first. result = self.content.mouse_handler( cli, MouseEvent( position=Point(x=position.x - write_position.xpos - sum(left_margin_widths), y=position.y - write_position.ypos + self.vertical_scroll), event_type=mouse_event.event_type)) # If it returns NotImplemented, handle it here. if result == NotImplemented: return self._mouse_handler(cli, mouse_event) return result mouse_handlers.set_mouse_handler_for_range( x_min=write_position.xpos + sum(left_margin_widths), x_max=write_position.xpos + write_position.width - total_margin_width, y_min=write_position.ypos, y_max=write_position.ypos + write_position.height, handler=mouse_handler) # Render and copy margins. move_x = 0 def render_margin(m, width): " Render margin. Return `Screen`. " # Retrieve margin tokens. tokens = m.create_margin(cli, self.render_info, width, write_position.height) # Turn it into a screen. (Take a screen from the cache if we # already rendered those tokens using this size.) def create_screen(): return TokenListControl.static(tokens).create_screen( cli, width + 1, write_position.height) key = (tuple(tokens), width, write_position.height) return self._margin_cache.get(key, create_screen) for m, width in zip(self.left_margins, left_margin_widths): # Create screen for margin. margin_screen = render_margin(m, width) # Copy and shift X. self._copy_margin(margin_screen, screen, write_position, move_x, width) move_x += width move_x = write_position.width - sum(right_margin_widths) for m, width in zip(self.right_margins, right_margin_widths): # Create screen for margin. margin_screen = render_margin(m, width) # Copy and shift X. self._copy_margin(margin_screen, screen, write_position, move_x, width) move_x += width def _copy_body(self, cli, temp_screen, highlighting, new_screen, write_position, move_x, width, applied_scroll_offsets): """ Copy characters from the temp screen that we got from the `UIControl` to the real screen. """ xpos = write_position.xpos + move_x ypos = write_position.ypos height = write_position.height temp_buffer = temp_screen.data_buffer new_buffer = new_screen.data_buffer temp_screen_height = temp_screen.height vertical_scroll = self.vertical_scroll horizontal_scroll = self.horizontal_scroll y = 0 # Now copy the region we need to the real screen. for y in range(0, height): # We keep local row variables. (Don't look up the row in the dict # for each iteration of the nested loop.) new_row = new_buffer[y + ypos] if y >= temp_screen_height and y >= write_position.height: # Break out of for loop when we pass after the last row of the # temp screen. (We use the 'y' position for calculation of new # screen's height.) break else: temp_row = temp_buffer[y + vertical_scroll] highlighting_row = highlighting[y + vertical_scroll] # Copy row content, except for transparent tokens. # (This is useful in case of floats.) # Also apply highlighting. for x in range(0, width): cell = temp_row[x + horizontal_scroll] highlighting_token = highlighting_row[x] if highlighting_token: new_row[x + xpos] = Char(cell.char, highlighting_token) elif cell.token != Transparent: new_row[x + xpos] = cell if self.content.has_focus(cli): new_screen.cursor_position = Point(y=temp_screen.cursor_position.y + ypos - vertical_scroll, x=temp_screen.cursor_position.x + xpos - horizontal_scroll) if not self.always_hide_cursor(cli): new_screen.show_cursor = temp_screen.show_cursor if not new_screen.menu_position and temp_screen.menu_position: new_screen.menu_position = Point(y=temp_screen.menu_position.y + ypos - vertical_scroll, x=temp_screen.menu_position.x + xpos - horizontal_scroll) # Update height of the output screen. (new_screen.write_data is not # called, so the screen is not aware of its height.) new_screen.height = max(new_screen.height, ypos + y + 1) def _copy_margin(self, temp_screen, new_screen, write_position, move_x, width): """ Copy characters from the margin screen to the real screen. """ xpos = write_position.xpos + move_x ypos = write_position.ypos temp_buffer = temp_screen.data_buffer new_buffer = new_screen.data_buffer # Now copy the region we need to the real screen. for y in range(0, write_position.height): new_row = new_buffer[y + ypos] temp_row = temp_buffer[y] # Copy row content, except for transparent tokens. # (This is useful in case of floats.) for x in range(0, width): cell = temp_row[x] if cell.token != Transparent: new_row[x + xpos] = cell def _scroll(self, temp_screen, width, height, cli): """ Scroll to make sure the cursor position is visible and that we maintain the requested scroll offset. Return the applied scroll offsets. """ def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end, cursor_pos, window_size, content_size): " Scrolling algorithm. Used for both horizontal and vertical scrolling. " # Calculate the scroll offset to apply. # This can obviously never be more than have the screen size. Also, when the # cursor appears at the top or bottom, we don't apply the offset. scroll_offset_start = int(min(scroll_offset_start, window_size / 2, cursor_pos)) scroll_offset_end = int(min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos)) # Prevent negative scroll offsets. if current_scroll < 0: current_scroll = 0 # Scroll back if we scrolled to much and there's still space to show more of the document. if (not self.allow_scroll_beyond_bottom(cli) and current_scroll > content_size - window_size): current_scroll = max(0, content_size - window_size) # Scroll up if cursor is before visible part. if current_scroll > cursor_pos - scroll_offset_start: current_scroll = max(0, cursor_pos - scroll_offset_start) # Scroll down if cursor is after visible part. if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end: current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end # Calculate the applied scroll offset. This value can be lower than what we had. scroll_offset_start = max(0, min(current_scroll, scroll_offset_start)) scroll_offset_end = max(0, min(content_size - current_scroll - window_size, scroll_offset_end)) return current_scroll, scroll_offset_start, scroll_offset_end # When a preferred scroll is given, take that first into account. if self.get_vertical_scroll: self.vertical_scroll = self.get_vertical_scroll(self) assert isinstance(self.vertical_scroll, int) if self.get_horizontal_scroll: self.horizontal_scroll = self.get_horizontal_scroll(self) assert isinstance(self.horizontal_scroll, int) # Update horizontal/vertical scroll to make sure that the cursor # remains visible. offsets = self.scroll_offsets self.vertical_scroll, scroll_offset_top, scroll_offset_bottom = do_scroll( current_scroll=self.vertical_scroll, scroll_offset_start=offsets.top, scroll_offset_end=offsets.bottom, cursor_pos=temp_screen.cursor_position.y, window_size=height, content_size=temp_screen.height) self.horizontal_scroll, scroll_offset_left, scroll_offset_right = do_scroll( current_scroll=self.horizontal_scroll, scroll_offset_start=offsets.left, scroll_offset_end=offsets.right, cursor_pos=temp_screen.cursor_position.x, window_size=width, content_size=temp_screen.width) applied_scroll_offsets = ScrollOffsets( top=scroll_offset_top, bottom=scroll_offset_bottom, left=scroll_offset_left, right=scroll_offset_right) return applied_scroll_offsets def _mouse_handler(self, cli, mouse_event): """ Mouse handler. Called when the UI control doesn't handle this particular event. """ if mouse_event.event_type == MouseEventTypes.SCROLL_DOWN: self._scroll_down(cli) elif mouse_event.event_type == MouseEventTypes.SCROLL_UP: self._scroll_up(cli) def _scroll_down(self, cli): " Scroll window down. " info = self.render_info if self.vertical_scroll < info.content_height - info.window_height: if info.cursor_position.y <= info.configured_scroll_offsets.top: self.content.move_cursor_down(cli) self.vertical_scroll += 1 def _scroll_up(self, cli): " Scroll window up. " info = self.render_info if info.vertical_scroll > 0: if info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom: self.content.move_cursor_up(cli) self.vertical_scroll -= 1 def walk(self, cli): # Only yield self. A window doesn't have children. yield self
class BufferControl(UIControl): """ Control for visualising the content of a :class:`.Buffer`. :param buffer: The :class:`.Buffer` object to be displayed. :param input_processors: A list of :class:`~prompt_toolkit.layout.processors.Processor` objects. :param include_default_input_processors: When True, include the default processors for highlighting of selection, search and displaying of multiple cursors. :param lexer: :class:`.Lexer` instance for syntax highlighting. :param preview_search: `bool` or :class:`.Filter`: Show search while typing. When this is `True`, probably you want to add a ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the cursor position will move, but the text won't be highlighted. :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. :param focus_on_click: Focus this buffer when it's click, but not yet focused. :param key_bindings: a :class:`.KeyBindings` object. """ def __init__(self, buffer=None, input_processors=None, include_default_input_processors=True, lexer=None, preview_search=False, focusable=True, search_buffer_control=None, menu_position=None, focus_on_click=False, key_bindings=None): from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase assert buffer is None or isinstance(buffer, Buffer) assert input_processors is None or isinstance(input_processors, list) assert isinstance(include_default_input_processors, bool) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert (search_buffer_control is None or callable(search_buffer_control) or isinstance(search_buffer_control, SearchBufferControl)) assert key_bindings is None or isinstance(key_bindings, KeyBindingsBase) self.input_processors = input_processors self.include_default_input_processors = include_default_input_processors self.default_input_processors = [ HighlightSearchProcessor(), HighlightIncrementalSearchProcessor(), HighlightSelectionProcessor(), DisplayMultipleCursors(), ] self.preview_search = to_filter(preview_search) self.focusable = to_filter(focusable) self.focus_on_click = to_filter(focus_on_click) self.buffer = buffer or Buffer() self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.key_bindings = key_bindings self._search_buffer_control = search_buffer_control #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a fairly easy way to cache such an expensive operation. self._fragment_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None self._last_get_processed_line = None def __repr__(self): return '<%s(buffer=%r at %r>' % (self.__class__.__name__, self.buffer, id(self)) @property def search_buffer_control(self): if callable(self._search_buffer_control): result = self._search_buffer_control() else: result = self._search_buffer_control assert result is None or isinstance(result, SearchBufferControl) return result @property def search_buffer(self): control = self.search_buffer_control if control is not None: return control.buffer @property def search_state(self): """ Return the `SearchState` for searching this `BufferControl`. This is always associated with the search control. If one search bar is used for searching multiple `BufferControls`, then they share the same `SearchState`. """ search_buffer_control = self.search_buffer_control if search_buffer_control: return search_buffer_control.searcher_search_state else: return SearchState() def is_focusable(self): return self.focusable() def preferred_width(self, max_available_width): """ This should return the preferred width. Note: We don't specify a preferred width according to the content, because it would be too expensive. Calculating the preferred width can be done by calculating the longest line, but this would require applying all the processors to each line. This is unfeasible for a larger document, and doing it for small documents only would result in inconsistent behaviour. """ return None def preferred_height(self, width, max_available_height, wrap_lines): # Calculate the content height, if it was drawn on a screen with the # given width. height = 0 content = self.create_content(width, None) # When line wrapping is off, the height should be equal to the amount # of lines. if not wrap_lines: return content.line_count # When the number of lines exceeds the max_available_height, just # return max_available_height. No need to calculate anything. if content.line_count >= max_available_height: return max_available_height for i in range(content.line_count): height += content.get_height_for_line(i, width) if height >= max_available_height: return max_available_height return height def _get_formatted_text_for_line_func(self, document): """ Create a function that returns the fragments for a given line. """ # Cache using `document.text`. def get_formatted_text_for_line(): return self.lexer.lex_document(document) key = (document.text, self.lexer.invalidation_hash()) return self._fragment_cache.get(key, get_formatted_text_for_line) def _create_get_processed_line_func(self, document, width, height): """ Create a function that takes a line number of the current document and returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) tuple. """ # Merge all input processors together. input_processors = self.input_processors or [] if self.include_default_input_processors: input_processors = self.default_input_processors + input_processors merged_processor = merge_processors(input_processors) def transform(lineno, fragments): " Transform the fragments for a given line number. " # Get cursor position at this line. if document.cursor_position_row == lineno: cursor_column = document.cursor_position_col else: cursor_column = None def source_to_display(i): """ X position from the buffer to the x position in the processed fragment list. By default, we start from the 'identity' operation. """ return i transformation = merged_processor.apply_transformation( TransformationInput(self, document, lineno, source_to_display, fragments, width, height)) if cursor_column: cursor_column = transformation.source_to_display(cursor_column) return _ProcessedLine(transformation.fragments, transformation.source_to_display, transformation.display_to_source) def create_func(): get_line = self._get_formatted_text_for_line_func(document) cache = {} def get_processed_line(i): try: return cache[i] except KeyError: processed_line = transform(i, get_line(i)) cache[i] = processed_line return processed_line return get_processed_line return create_func() def create_content(self, width, height, preview_search=False): """ Create a UIContent. """ buffer = self.buffer # Get the document to be shown. If we are currently searching (the # search buffer has focus, and the preview_search filter is enabled), # then use the search document, which has possibly a different # text/cursor position.) search_control = self.search_buffer_control preview_now = preview_search or bool( # Only if this feature is enabled. self.preview_search() and # And something was typed in the associated search field. search_control and search_control.buffer.text and # And we are searching in this control. (Many controls can point to # the same search field, like in Pyvim.) get_app().layout.search_target_buffer_control == self) if preview_now: ss = self.search_state document = buffer.document_for_search( SearchState(text=search_control.buffer.text, direction=ss.direction, ignore_case=ss.ignore_case)) else: document = buffer.document get_processed_line = self._create_get_processed_line_func( document, width, height) self._last_get_processed_line = get_processed_line def translate_rowcol(row, col): " Return the content column for this coordinate. " return Point(x=get_processed_line(row).source_to_display(col), y=row) def get_line(i): " Return the fragments for a given line number. " fragments = get_processed_line(i).fragments # Add a space at the end, because that is a possible cursor # position. (When inserting after the input.) We should do this on # all the lines, not just the line containing the cursor. (Because # otherwise, line wrapping/scrolling could change when moving the # cursor around.) fragments = fragments + [('', ' ')] return fragments content = UIContent(get_line=get_line, line_count=document.line_count, cursor_position=translate_rowcol( document.cursor_position_row, document.cursor_position_col)) # If there is an auto completion going on, use that start point for a # pop-up menu position. (But only when this buffer has the focus -- # there is only one place for a menu, determined by the focused buffer.) if get_app().layout.current_control == self: menu_position = self.menu_position( ) if self.menu_position else None if menu_position is not None: assert isinstance(menu_position, int) menu_row, menu_col = buffer.document.translate_index_to_position( menu_position) content.menu_position = translate_rowcol(menu_row, menu_col) elif buffer.complete_state: # Position for completion menu. # Note: We use 'min', because the original cursor position could be # behind the input string when the actual completion is for # some reason shorter than the text we had before. (A completion # can change and shorten the input.) menu_row, menu_col = buffer.document.translate_index_to_position( min( buffer.cursor_position, buffer.complete_state. original_document.cursor_position)) content.menu_position = translate_rowcol(menu_row, menu_col) else: content.menu_position = None return content def mouse_handler(self, mouse_event): """ Mouse handler for this control. """ buffer = self.buffer position = mouse_event.position # Focus buffer when clicked. if get_app().layout.current_control == self: if self._last_get_processed_line: processed_line = self._last_get_processed_line(position.y) # Translate coordinates back to the cursor position of the # original input. xpos = processed_line.display_to_source(position.x) index = buffer.document.translate_row_col_to_index( position.y, xpos) # Set the cursor position. if mouse_event.event_type == MouseEventType.MOUSE_DOWN: buffer.exit_selection() buffer.cursor_position = index elif mouse_event.event_type == MouseEventType.MOUSE_UP: # When the cursor was moved to another place, select the text. # (The >1 is actually a small but acceptable workaround for # selecting text in Vi navigation mode. In navigation mode, # the cursor can never be after the text, so the cursor # will be repositioned automatically.) if abs(buffer.cursor_position - index) > 1: buffer.start_selection( selection_type=SelectionType.CHARACTERS) buffer.cursor_position = index # Select word around cursor on double click. # Two MOUSE_UP events in a short timespan are considered a double click. double_click = self._last_click_timestamp and time.time( ) - self._last_click_timestamp < .3 self._last_click_timestamp = time.time() if double_click: start, end = buffer.document.find_boundaries_of_current_word( ) buffer.cursor_position += start buffer.start_selection( selection_type=SelectionType.CHARACTERS) buffer.cursor_position += end - start else: # Don't handle scroll events here. return NotImplemented # Not focused, but focusing on click events. else: if self.focus_on_click( ) and mouse_event.event_type == MouseEventType.MOUSE_UP: # Focus happens on mouseup. (If we did this on mousedown, the # up event will be received at the point where this widget is # focused and be handled anyway.) get_app().layout.current_control = self else: return NotImplemented def move_cursor_down(self): b = self.buffer b.cursor_position += b.document.get_cursor_down_position() def move_cursor_up(self): b = self.buffer b.cursor_position += b.document.get_cursor_up_position() def get_key_bindings(self): """ When additional key bindings are given. Return these. """ return self.key_bindings def get_invalidate_events(self): """ Return the Window invalidate events. """ # Whenever the buffer changes, the UI has to be updated. yield self.buffer.on_text_changed yield self.buffer.on_cursor_position_changed yield self.buffer.on_completions_changed yield self.buffer.on_suggestion_set
class TokenListControl(UIControl): """ Control that displays a list of (Token, text) tuples. (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) Mouse support: The list of tokens can also contain tuples of three items, looking like: (Token, text, handler). When mouse support is enabled and the user clicks on this token, then the given handler is called. That handler should accept two inputs: (CommandLineInterface, MouseEvent) and it should either handle the event or return `NotImplemented` in case we want the containing Window to handle this event. :param get_tokens: Callable that takes a `CommandLineInterface` instance and returns the list of (Token, text) tuples to be displayed right now. :param default_char: default :class:`.Char` (character and Token) to use for the background when there is more space available than `get_tokens` returns. :param get_default_char: Like `default_char`, but this is a callable that takes a :class:`prompt_toolkit.interface.CommandLineInterface` and returns a :class:`.Char` instance. :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`, this UI control will take the focus. The cursor will be shown in the upper left corner of this control, unless `get_token` returns a ``Token.SetCursorPosition`` token somewhere in the token list, then the cursor will be shown there. :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines. """ def __init__(self, get_tokens, default_char=None, get_default_char=None, align_right=False, align_center=False, has_focus=False, wrap_lines=True): assert default_char is None or isinstance(default_char, Char) assert get_default_char is None or callable(get_default_char) assert not (default_char and get_default_char) self.align_right = to_cli_filter(align_right) self.align_center = to_cli_filter(align_center) self._has_focus_filter = to_cli_filter(has_focus) self.wrap_lines = to_cli_filter(wrap_lines) self.get_tokens = get_tokens # Construct `get_default_char` callable. if default_char: get_default_char = lambda _: default_char elif not get_default_char: get_default_char = lambda _: Char(' ', Token) self.get_default_char = get_default_char #: Cache for rendered screens. self._screen_cache = SimpleCache(maxsize=18) self._token_cache = SimpleCache(maxsize=1) # Only cache one token list. We don't need the previous item. # Render info for the mouse support. self._tokens = None # The last rendered tokens. self._pos_to_indexes = None # Mapping from mouse positions (x,y) to # positions in the token list. def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.get_tokens) def _get_tokens_cached(self, cli): """ Get tokens, but only retrieve tokens once during one render run. (This function is called several times during one rendering, because we also need those for calculating the dimensions.) """ return self._token_cache.get(cli.render_counter, lambda: self.get_tokens(cli)) def has_focus(self, cli): return self._has_focus_filter(cli) def preferred_width(self, cli, max_available_width): """ Return the preferred width for this control. That is the width of the longest line. """ text = ''.join(t[1] for t in self._get_tokens_cached(cli)) line_lengths = [get_cwidth(l) for l in text.split('\n')] return max(line_lengths) def preferred_height(self, cli, width): screen = self.create_screen(cli, width, None) return screen.height def create_screen(self, cli, width, height): # Get tokens tokens_with_mouse_handlers = self._get_tokens_cached(cli) default_char = self.get_default_char(cli) # Wrap/align right/center parameters. wrap_lines = self.wrap_lines(cli) right = self.align_right(cli) center = self.align_center(cli) def process_line(line): " Center or right align a single line. " used_width = token_list_width(line) padding = width - used_width if center: padding = int(padding / 2) return [(default_char.token, default_char.char * padding) ] + line + [(Token, '\n')] if right or center: tokens2 = [] for line in split_lines(tokens_with_mouse_handlers): tokens2.extend(process_line(line)) tokens_with_mouse_handlers = tokens2 # Strip mouse handlers from tokens. tokens = [tuple(item[:2]) for item in tokens_with_mouse_handlers] # Create screen, or take it from the cache. key = (default_char.char, default_char.token, tuple(tokens_with_mouse_handlers), width, wrap_lines, right, center) params = (default_char, tokens, width, wrap_lines, right, center) screen, self._pos_to_indexes = self._screen_cache.get( key, lambda: self._get_screen(*params)) self._tokens = tokens_with_mouse_handlers return screen @classmethod def _get_screen(cls, default_char, tokens, width, wrap_lines, right, center): screen = Screen(default_char, initial_width=width) # Only call write_data when we actually have tokens. # (Otherwise the screen height will go up from 0 to 1 while we don't # want that. -- An empty control should not take up any space.) if tokens: write_data_result = screen.write_data( tokens, width=(width if wrap_lines else None)) indexes_to_pos = write_data_result.indexes_to_pos pos_to_indexes = _LazyReverseDict(indexes_to_pos) else: pos_to_indexes = {} return screen, pos_to_indexes @classmethod def static(cls, tokens): def get_static_tokens(cli): return tokens return cls(get_static_tokens) def mouse_handler(self, cli, mouse_event): """ Handle mouse events. (When the token list contained mouse handlers and the user clicked on on any of these, the matching handler is called. This handler can still return `NotImplemented` in case we want the `Window` to handle this particular event.) """ if self._pos_to_indexes: # Find position in the token list. position = mouse_event.position index = self._pos_to_indexes.get((position.x, position.y)) if index is not None: # Find mouse handler for this character. count = 0 for item in self._tokens: count += len(item[1]) if count >= index: if len(item) >= 3: # Handler found. Call it. # (Handler can return NotImplemented, so return # that result.) handler = item[2] return handler(cli, mouse_event) else: break # Otherwise, don't handle here. return NotImplemented
class Registry(object): """ Key binding registry. :: r = Registry() @r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT) def handler(event): # Handle ControlX-ControlC key sequence. pass """ def __init__(self): self.key_bindings = [] self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000) self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000) def _clear_cache(self): self._get_bindings_for_keys_cache.clear() self._get_bindings_starting_with_keys_cache.clear() def add_binding(self, *keys, **kwargs): """ Decorator for annotating key bindings. :param filter: :class:`~prompt_toolkit.filters.CLIFilter` to determine when this key binding is active. :param eager: :class:`~prompt_toolkit.filters.CLIFilter` or `bool`. When True, ignore potential longer matches when this key binding is hit. E.g. when there is an active eager key binding for Ctrl-X, execute the handler immediately and ignore the key binding for Ctrl-X Ctrl-E of which it is a prefix. :param save_before: Callable that takes an `Event` and returns True if we should save the current buffer, before handling the event. (That's the default.) """ filter = to_cli_filter(kwargs.pop('filter', True)) eager = to_cli_filter(kwargs.pop('eager', False)) save_before = kwargs.pop('save_before', lambda e: True) to_cli_filter(kwargs.pop('invalidate_ui', True)) # Deprecated! (ignored.) assert not kwargs assert keys assert all(isinstance(k, (Key, text_type)) for k in keys), \ 'Key bindings should consist of Key and string (unicode) instances.' assert callable(save_before) if isinstance(filter, Never): # When a filter is Never, it will always stay disabled, so in that case # don't bother putting it in the registry. It will slow down every key # press otherwise. def decorator(func): return func else: def decorator(func): self.key_bindings.append( _Binding(keys, func, filter=filter, eager=eager, save_before=save_before)) self._clear_cache() return func return decorator def remove_binding(self, function): """ Remove a key binding. This expects a function that was given to `add_binding` method as parameter. Raises `ValueError` when the given function was not registered before. """ assert callable(function) for b in self.key_bindings: if b.handler == function: self.key_bindings.remove(b) self._clear_cache() return # No key binding found for this function. Raise ValueError. raise ValueError('Binding not found: %r' % (function, )) def get_bindings_for_keys(self, keys): """ Return a list of key bindings that can handle this key. (This return also inactive bindings, so the `filter` still has to be called, for checking it.) :param keys: tuple of keys. """ def get(): result = [] for b in self.key_bindings: if len(keys) == len(b.keys): match = True any_count = 0 for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if i == Keys.Any: any_count += 1 if match: result.append((any_count, b)) # Place bindings that have more 'Any' occurences in them at the end. result = sorted(result, key=lambda item: -item[0]) return [item[1] for item in result] return self._get_bindings_for_keys_cache.get(keys, get) def get_bindings_starting_with_keys(self, keys): """ Return a list of key bindings that handle a key sequence starting with `keys`. (It does only return bindings for which the sequences are longer than `keys`. And like `get_bindings_for_keys`, it also includes inactive bindings.) :param keys: tuple of keys. """ def get(): result = [] for b in self.key_bindings: if len(keys) < len(b.keys): match = True for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if match: result.append(b) return result return self._get_bindings_starting_with_keys_cache.get(keys, get)
class BufferControl(UIControl): """ Control for visualising the content of a `Buffer`. :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`. :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting. :param preview_search: `bool` or `CLIFilter`: Show search while typing. :param get_search_state: Callable that takes a CommandLineInterface and returns the SearchState to be used. (If not CommandLineInterface.search_state.) :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines. :param buffer_name: String representing the name of the buffer to display. :param default_char: :class:`.Char` instance to use to fill the background. This is transparent by default. :param focus_on_click: Focus this buffer when it's click, but not yet focussed. """ def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, highlighters=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, wrap_lines=True, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all( isinstance(i, Processor) for i in input_processors) assert highlighters is None or all( isinstance(i, Highlighter) for i in highlighters) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.wrap_lines = to_cli_filter(wrap_lines) self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.highlighters = highlighters or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) #: Keep a similar cache for rendered screens. (when we scroll up/down #: through the screen, or when we change another buffer, we don't want #: to recreate the same screen again.) self._screen_cache = SimpleCache(maxsize=8) #: Highlight Cache. #: When nothing of the buffer content or processors has changed, but #: the highlighting of the selection/search changes, self._highlight_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None def _buffer(self, cli): """ The buffer object that contains the 'main' content. """ return cli.buffers[self.buffer_name] def has_focus(self, cli): # This control gets the focussed if the actual `Buffer` instance has the # focus or when any of the `InputProcessor` classes tells us that it # wants the focus. (E.g. in case of a reverse-search, where the actual # search buffer may not be displayed, but the "reverse-i-search" text # should get the focus.) return cli.current_buffer_name == self.buffer_name or \ any(i.has_focus(cli) for i in self.input_processors) def preferred_width(self, cli, max_available_width): # Return the length of the longest line. return max(map(len, self._buffer(cli).document.lines)) def preferred_height(self, cli, width): # Draw content on a screen using this width. Measure the height of the # result. screen, highlighters = self.create_screen(cli, width, None) return screen.height def _get_input_tokens(self, cli, document): """ Tokenize input text for highlighting. Return (tokens, source_to_display, display_to_source) tuple. :param document: The document to be shown. This can be `buffer.document` but could as well be a different one, in case we are searching through the history. (Buffer.document_for_search) """ def get(): # Call lexer. tokens = self.lexer.get_tokens(cli, document.text) # 'Explode' tokens in characters. (And turn generator into a list.) # (Some input processors -- like search/selection highlighter -- # rely on that each item in the tokens array only contains one # character.) tokens = [(token, c) for token, text in tokens for c in text] # Run all processors over the input. # (They can transform both the tokens and the cursor position.) source_to_display_functions = [] display_to_source_functions = [] d_ = document # Each processor receives the document of the previous one. for p in self.input_processors: transformation = p.apply_transformation(cli, d_, tokens) d_ = transformation.document assert isinstance(transformation, Transformation) tokens = transformation.tokens source_to_display_functions.append( transformation.source_to_display) display_to_source_functions.append( transformation.display_to_source) # Chain cursor transformation (movement) functions. def source_to_display(cursor_position): " Chained source_to_display. " for f in source_to_display_functions: cursor_position = f(cursor_position) return cursor_position def display_to_source(cursor_position): " Chained display_to_source. " for f in reversed(display_to_source_functions): cursor_position = f(cursor_position) return cursor_position return tokens, source_to_display, display_to_source key = ( document.text, # Include invalidation_hashes from all processors. tuple( p.invalidation_hash(cli, document) for p in self.input_processors), ) return self._token_cache.get(key, get) def create_screen(self, cli, width, height): buffer = self._buffer(cli) # Get the document to be shown. If we are currently searching (the # search buffer has focus, and the preview_search filter is enabled), # then use the search document, which has possibly a different # text/cursor position.) def preview_now(): """ True when we should preview a search. """ return bool( self.preview_search(cli) and cli.buffers[self.search_buffer_name].text) if preview_now(): if self.get_search_state: ss = self.get_search_state(cli) else: ss = cli.search_state document = buffer.document_for_search( SearchState(text=cli.current_buffer.text, direction=ss.direction, ignore_case=ss.ignore_case)) else: document = buffer.document # Wrap. wrap_width = width if self.wrap_lines(cli) else None def _create_screen(): screen = Screen(self.default_char, initial_width=width) # Get tokens # Note: we add the space character at the end, because that's where # the cursor can also be. input_tokens, source_to_display, display_to_source = self._get_input_tokens( cli, document) input_tokens += [(self.default_char.token, ' ')] write_data_result = screen.write_data(input_tokens, width=wrap_width) indexes_to_pos = write_data_result.indexes_to_pos line_lengths = write_data_result.line_lengths pos_to_indexes = _LazyReverseDict(indexes_to_pos) def cursor_position_to_xy(cursor_position): """ Turn a cursor position in the buffer into x/y coordinates on the screen. """ cursor_position = min(len(document.text), cursor_position) # First get the real token position by applying all transformations. cursor_position = source_to_display(cursor_position) # Then look up into the table. try: return indexes_to_pos[cursor_position] except KeyError: # This can fail with KeyError, but only if one of the # processors is returning invalid key locations. raise # return 0, 0 def xy_to_cursor_position(x, y): """ Turn x/y screen coordinates back to the original cursor position in the buffer. """ # Look up reverse in table. while x > 0 or y > 0: try: index = pos_to_indexes[x, y] break except KeyError: # No match found -> mouse click outside of region # containing text. Look to the left or up. if x: x -= 1 elif y: y -= 1 else: # Nobreak. index = 0 # Transform. return display_to_source(index) return screen, cursor_position_to_xy, xy_to_cursor_position, line_lengths # Build a key for the caching. If any of these parameters changes, we # have to recreate a new screen. key = ( # When the text changes, we obviously have to recreate a new screen. document.text, # When the width changes, line wrapping will be different. # (None when disabled.) wrap_width, # Include invalidation_hashes from all processors. tuple( p.invalidation_hash(cli, document) for p in self.input_processors), ) # Get from cache, or create if this doesn't exist yet. screen, cursor_position_to_xy, self._xy_to_cursor_position, line_lengths = \ self._screen_cache.get(key, _create_screen) x, y = cursor_position_to_xy(document.cursor_position) screen.cursor_position = Point(y=y, x=x) # If there is an auto completion going on, use that start point for a # pop-up menu position. (But only when this buffer has the focus -- # there is only one place for a menu, determined by the focussed buffer.) if cli.current_buffer_name == self.buffer_name: menu_position = self.menu_position( cli) if self.menu_position else None if menu_position is not None: assert isinstance(menu_position, int) x, y = cursor_position_to_xy(menu_position) screen.menu_position = Point(y=y, x=x) elif buffer.complete_state: # Position for completion menu. # Note: We use 'min', because the original cursor position could be # behind the input string when the actual completion is for # some reason shorter than the text we had before. (A completion # can change and shorten the input.) x, y = cursor_position_to_xy( min( buffer.cursor_position, buffer.complete_state. original_document.cursor_position)) screen.menu_position = Point(y=y, x=x) else: screen.menu_position = None # Add highlighting. highlight_key = ( key, # Includes everything from the 'key' above. (E.g. when the # document changes, we have to recalculate highlighting.) # Include invalidation_hashes from all highlighters. tuple( h.invalidation_hash(cli, document) for h in self.highlighters)) highlighting = self._highlight_cache.get( highlight_key, lambda: self._get_highlighting( cli, document, cursor_position_to_xy, line_lengths)) return screen, highlighting def _get_highlighting(self, cli, document, cursor_position_to_xy, line_lengths): """ Return a _HighlightDict for the highlighting. (This is a lazy dict of dicts.) The Window class will apply this for the visible regions. - That way, we don't have to recalculate the screen again for each selection/search change. :param line_lengths: Maps line numbers to the length of these lines. """ def get_row_size(y): " Return the max 'x' value for a given row in the screen. " return max(1, line_lengths.get(y, 0)) # Get list of fragments. row_to_fragments = defaultdict(list) for h in self.highlighters: for fragment in h.get_fragments(cli, document): # Expand fragments. start_column, start_row = cursor_position_to_xy(fragment.start) end_column, end_row = cursor_position_to_xy(fragment.end) token = fragment.token if start_row == end_row: # Single line highlighting. row_to_fragments[start_row].append( _HighlightFragment(start_column, end_column, token)) else: # Multi line highlighting. # (First line.) row_to_fragments[start_row].append( _HighlightFragment(start_column, get_row_size(start_row), token)) # (Middle lines.) for y in range(start_row + 1, end_row): row_to_fragments[y].append( _HighlightFragment(0, get_row_size(y), token)) # (Last line.) row_to_fragments[end_row].append( _HighlightFragment(0, end_column, token)) # Create dict to return. return _HighlightDict(row_to_fragments) def mouse_handler(self, cli, mouse_event): """ Mouse handler for this control. """ buffer = self._buffer(cli) position = mouse_event.position # Focus buffer when clicked. if self.has_focus(cli): if self._xy_to_cursor_position: # Translate coordinates back to the cursor position of the # original input. pos = self._xy_to_cursor_position(position.x, position.y) # Set the cursor position. if pos <= len(buffer.text): if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: buffer.exit_selection() buffer.cursor_position = pos elif mouse_event.event_type == MouseEventTypes.MOUSE_UP: # When the cursor was moved to another place, select the text. # (The >1 is actually a small but acceptable workaround for # selecting text in Vi navigation mode. In navigation mode, # the cursor can never be after the text, so the cursor # will be repositioned automatically.) if abs(buffer.cursor_position - pos) > 1: buffer.start_selection( selection_type=SelectionType.CHARACTERS) buffer.cursor_position = pos # Select word around cursor on double click. # Two MOUSE_UP events in a short timespan are considered a double click. double_click = self._last_click_timestamp and time.time( ) - self._last_click_timestamp < .3 self._last_click_timestamp = time.time() if double_click: start, end = buffer.document.find_boundaries_of_current_word( ) buffer.cursor_position += start buffer.start_selection( selection_type=SelectionType.CHARACTERS) buffer.cursor_position += end - start else: # Don't handle scroll events here. return NotImplemented # Not focussed, but focussing on click events. else: if self.focus_on_click( cli ) and mouse_event.event_type == MouseEventTypes.MOUSE_UP: # Focus happens on mouseup. (If we did this on mousedown, the # up event will be received at the point where this widget is # focussed and be handled anyway.) cli.focus(self.buffer_name) else: return NotImplemented def move_cursor_down(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_down_position() def move_cursor_up(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_up_position()
class TokenListControl(UIControl): """ Control that displays a list of (Token, text) tuples. (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) Mouse support: The list of tokens can also contain tuples of three items, looking like: (Token, text, handler). When mouse support is enabled and the user clicks on this token, then the given handler is called. That handler should accept two inputs: (CommandLineInterface, MouseEvent) and it should either handle the event or return `NotImplemented` in case we want the containing Window to handle this event. :param get_tokens: Callable that takes a `CommandLineInterface` instance and returns the list of (Token, text) tuples to be displayed right now. :param default_char: default :class:`.Char` (character and Token) to use for the background when there is more space available than `get_tokens` returns. :param get_default_char: Like `default_char`, but this is a callable that takes a :class:`prompt_toolkit.interface.CommandLineInterface` and returns a :class:`.Char` instance. :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`, this UI control will take the focus. The cursor will be shown in the upper left corner of this control, unless `get_token` returns a ``Token.SetCursorPosition`` token somewhere in the token list, then the cursor will be shown there. """ def __init__(self, get_tokens, default_char=None, get_default_char=None, align_right=False, align_center=False, has_focus=False): assert callable(get_tokens) assert default_char is None or isinstance(default_char, Char) assert get_default_char is None or callable(get_default_char) assert not (default_char and get_default_char) self.align_right = to_cli_filter(align_right) self.align_center = to_cli_filter(align_center) self._has_focus_filter = to_cli_filter(has_focus) self.get_tokens = get_tokens # Construct `get_default_char` callable. if default_char: get_default_char = lambda _: default_char elif not get_default_char: get_default_char = lambda _: Char(' ', Token.Transparent) self.get_default_char = get_default_char #: Cache for the content. self._content_cache = SimpleCache(maxsize=18) self._token_cache = SimpleCache(maxsize=1) # Only cache one token list. We don't need the previous item. # Render info for the mouse support. self._tokens = None def reset(self): self._tokens = None def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.get_tokens) def _get_tokens_cached(self, cli): """ Get tokens, but only retrieve tokens once during one render run. (This function is called several times during one rendering, because we also need those for calculating the dimensions.) """ return self._token_cache.get(cli.render_counter, lambda: self.get_tokens(cli)) def has_focus(self, cli): return self._has_focus_filter(cli) def preferred_width(self, cli, max_available_width): """ Return the preferred width for this control. That is the width of the longest line. """ text = token_list_to_text(self._get_tokens_cached(cli)) line_lengths = [get_cwidth(l) for l in text.split('\n')] return max(line_lengths) def preferred_height(self, cli, width, max_available_height, wrap_lines): content = self.create_content(cli, width, None) return content.line_count def create_content(self, cli, width, height): # Get tokens tokens_with_mouse_handlers = self._get_tokens_cached(cli) default_char = self.get_default_char(cli) # Wrap/align right/center parameters. right = self.align_right(cli) center = self.align_center(cli) def process_line(line): " Center or right align a single line. " used_width = token_list_width(line) padding = width - used_width if center: padding = int(padding / 2) return [(default_char.token, default_char.char * padding)] + line if right or center: token_lines_with_mouse_handlers = [] for line in split_lines(tokens_with_mouse_handlers): token_lines_with_mouse_handlers.append(process_line(line)) else: token_lines_with_mouse_handlers = list( split_lines(tokens_with_mouse_handlers)) # Strip mouse handlers from tokens. token_lines = [[tuple(item[:2]) for item in line] for line in token_lines_with_mouse_handlers] # Keep track of the tokens with mouse handler, for later use in # `mouse_handler`. self._tokens = tokens_with_mouse_handlers # If there is a `Token.SetCursorPosition` in the token list, set the # cursor position here. def get_cursor_position(): SetCursorPosition = Token.SetCursorPosition for y, line in enumerate(token_lines): x = 0 for token, text in line: if token == SetCursorPosition: return Point(x=x, y=y) x += len(text) return None # Create content, or take it from the cache. key = (default_char.char, default_char.token, tuple(tokens_with_mouse_handlers), width, right, center) def get_content(): return UIContent(get_line=lambda i: token_lines[i], line_count=len(token_lines), default_char=default_char, cursor_position=get_cursor_position()) return self._content_cache.get(key, get_content) @classmethod def static(cls, tokens): def get_static_tokens(cli): return tokens return cls(get_static_tokens) def mouse_handler(self, cli, mouse_event): """ Handle mouse events. (When the token list contained mouse handlers and the user clicked on on any of these, the matching handler is called. This handler can still return `NotImplemented` in case we want the `Window` to handle this particular event.) """ if self._tokens: # Read the generator. tokens_for_line = list(split_lines(self._tokens)) try: tokens = tokens_for_line[mouse_event.position.y] except IndexError: return NotImplemented else: # Find position in the token list. xpos = mouse_event.position.x # Find mouse handler for this character. count = 0 for item in tokens: count += len(item[1]) if count >= xpos: if len(item) >= 3: # Handler found. Call it. # (Handler can return NotImplemented, so return # that result.) handler = item[2] return handler(cli, mouse_event) else: break # Otherwise, don't handle here. return NotImplemented
class BufferControl(UIControl): """ Control for visualising the content of a `Buffer`. :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`. :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting. :param preview_search: `bool` or `CLIFilter`: Show search while typing. :param get_search_state: Callable that takes a CommandLineInterface and returns the SearchState to be used. (If not CommandLineInterface.search_state.) :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines. :param buffer_name: String representing the name of the buffer to display. :param default_char: :class:`.Char` instance to use to fill the background. This is transparent by default. :param focus_on_click: Focus this buffer when it's click, but not yet focussed. """ def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, highlighters=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, wrap_lines=True, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all(isinstance(i, Processor) for i in input_processors) assert highlighters is None or all(isinstance(i, Highlighter) for i in highlighters) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.wrap_lines = to_cli_filter(wrap_lines) self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.highlighters = highlighters or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) #: Keep a similar cache for rendered screens. (when we scroll up/down #: through the screen, or when we change another buffer, we don't want #: to recreate the same screen again.) self._screen_cache = SimpleCache(maxsize=8) #: Highlight Cache. #: When nothing of the buffer content or processors has changed, but #: the highlighting of the selection/search changes, self._highlight_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None def _buffer(self, cli): """ The buffer object that contains the 'main' content. """ return cli.buffers[self.buffer_name] def has_focus(self, cli): # This control gets the focussed if the actual `Buffer` instance has the # focus or when any of the `InputProcessor` classes tells us that it # wants the focus. (E.g. in case of a reverse-search, where the actual # search buffer may not be displayed, but the "reverse-i-search" text # should get the focus.) return cli.current_buffer_name == self.buffer_name or \ any(i.has_focus(cli) for i in self.input_processors) def preferred_width(self, cli, max_available_width): # Return the length of the longest line. return max(map(len, self._buffer(cli).document.lines)) def preferred_height(self, cli, width): # Draw content on a screen using this width. Measure the height of the # result. screen, highlighters = self.create_screen(cli, width, None) return screen.height def _get_input_tokens(self, cli, document): """ Tokenize input text for highlighting. Return (tokens, source_to_display, display_to_source) tuple. :param document: The document to be shown. This can be `buffer.document` but could as well be a different one, in case we are searching through the history. (Buffer.document_for_search) """ def get(): # Call lexer. tokens = self.lexer.get_tokens(cli, document.text) # 'Explode' tokens in characters. (And turn generator into a list.) # (Some input processors -- like search/selection highlighter -- # rely on that each item in the tokens array only contains one # character.) tokens = [(token, c) for token, text in tokens for c in text] # Run all processors over the input. # (They can transform both the tokens and the cursor position.) source_to_display_functions = [] display_to_source_functions = [] d_ = document # Each processor receives the document of the previous one. for p in self.input_processors: transformation = p.apply_transformation(cli, d_, tokens) d_ = transformation.document assert isinstance(transformation, Transformation) tokens = transformation.tokens source_to_display_functions.append(transformation.source_to_display) display_to_source_functions.append(transformation.display_to_source) # Chain cursor transformation (movement) functions. def source_to_display(cursor_position): " Chained source_to_display. " for f in source_to_display_functions: cursor_position = f(cursor_position) return cursor_position def display_to_source(cursor_position): " Chained display_to_source. " for f in reversed(display_to_source_functions): cursor_position = f(cursor_position) return cursor_position return tokens, source_to_display, display_to_source key = ( document.text, # Include invalidation_hashes from all processors. tuple(p.invalidation_hash(cli, document) for p in self.input_processors), ) return self._token_cache.get(key, get) def create_screen(self, cli, width, height): buffer = self._buffer(cli) # Get the document to be shown. If we are currently searching (the # search buffer has focus, and the preview_search filter is enabled), # then use the search document, which has possibly a different # text/cursor position.) def preview_now(): """ True when we should preview a search. """ return bool(self.preview_search(cli) and cli.buffers[self.search_buffer_name].text) if preview_now(): if self.get_search_state: ss = self.get_search_state(cli) else: ss = cli.search_state document = buffer.document_for_search(SearchState( text=cli.current_buffer.text, direction=ss.direction, ignore_case=ss.ignore_case)) else: document = buffer.document # Wrap. wrap_width = width if self.wrap_lines(cli) else None def _create_screen(): screen = Screen(self.default_char, initial_width=width) # Get tokens # Note: we add the space character at the end, because that's where # the cursor can also be. input_tokens, source_to_display, display_to_source = self._get_input_tokens(cli, document) input_tokens += [(self.default_char.token, ' ')] write_data_result = screen.write_data(input_tokens, width=wrap_width) indexes_to_pos = write_data_result.indexes_to_pos line_lengths = write_data_result.line_lengths pos_to_indexes = _LazyReverseDict(indexes_to_pos) def cursor_position_to_xy(cursor_position): """ Turn a cursor position in the buffer into x/y coordinates on the screen. """ cursor_position = min(len(document.text), cursor_position) # First get the real token position by applying all transformations. cursor_position = source_to_display(cursor_position) # Then look up into the table. try: return indexes_to_pos[cursor_position] except KeyError: # This can fail with KeyError, but only if one of the # processors is returning invalid key locations. raise # return 0, 0 def xy_to_cursor_position(x, y): """ Turn x/y screen coordinates back to the original cursor position in the buffer. """ # Look up reverse in table. while x > 0 or y > 0: try: index = pos_to_indexes[x, y] break except KeyError: # No match found -> mouse click outside of region # containing text. Look to the left or up. if x: x -= 1 elif y: y -=1 else: # Nobreak. index = 0 # Transform. return display_to_source(index) return screen, cursor_position_to_xy, xy_to_cursor_position, line_lengths # Build a key for the caching. If any of these parameters changes, we # have to recreate a new screen. key = ( # When the text changes, we obviously have to recreate a new screen. document.text, # When the width changes, line wrapping will be different. # (None when disabled.) wrap_width, # Include invalidation_hashes from all processors. tuple(p.invalidation_hash(cli, document) for p in self.input_processors), ) # Get from cache, or create if this doesn't exist yet. screen, cursor_position_to_xy, self._xy_to_cursor_position, line_lengths = \ self._screen_cache.get(key, _create_screen) x, y = cursor_position_to_xy(document.cursor_position) screen.cursor_position = Point(y=y, x=x) # If there is an auto completion going on, use that start point for a # pop-up menu position. (But only when this buffer has the focus -- # there is only one place for a menu, determined by the focussed buffer.) if cli.current_buffer_name == self.buffer_name: menu_position = self.menu_position(cli) if self.menu_position else None if menu_position is not None: assert isinstance(menu_position, int) x, y = cursor_position_to_xy(menu_position) screen.menu_position = Point(y=y, x=x) elif buffer.complete_state: # Position for completion menu. # Note: We use 'min', because the original cursor position could be # behind the input string when the actual completion is for # some reason shorter than the text we had before. (A completion # can change and shorten the input.) x, y = cursor_position_to_xy( min(buffer.cursor_position, buffer.complete_state.original_document.cursor_position)) screen.menu_position = Point(y=y, x=x) else: screen.menu_position = None # Add highlighting. highlight_key = ( key, # Includes everything from the 'key' above. (E.g. when the # document changes, we have to recalculate highlighting.) # Include invalidation_hashes from all highlighters. tuple(h.invalidation_hash(cli, document) for h in self.highlighters) ) highlighting = self._highlight_cache.get(highlight_key, lambda: self._get_highlighting(cli, document, cursor_position_to_xy, line_lengths)) return screen, highlighting def _get_highlighting(self, cli, document, cursor_position_to_xy, line_lengths): """ Return a _HighlightDict for the highlighting. (This is a lazy dict of dicts.) The Window class will apply this for the visible regions. - That way, we don't have to recalculate the screen again for each selection/search change. :param line_lengths: Maps line numbers to the length of these lines. """ def get_row_size(y): " Return the max 'x' value for a given row in the screen. " return max(1, line_lengths.get(y, 0)) # Get list of fragments. row_to_fragments = defaultdict(list) for h in self.highlighters: for fragment in h.get_fragments(cli, document): # Expand fragments. start_column, start_row = cursor_position_to_xy(fragment.start) end_column, end_row = cursor_position_to_xy(fragment.end) token = fragment.token if start_row == end_row: # Single line highlighting. row_to_fragments[start_row].append( _HighlightFragment(start_column, end_column, token)) else: # Multi line highlighting. # (First line.) row_to_fragments[start_row].append( _HighlightFragment(start_column, get_row_size(start_row), token)) # (Middle lines.) for y in range(start_row + 1, end_row): row_to_fragments[y].append(_HighlightFragment(0, get_row_size(y), token)) # (Last line.) row_to_fragments[end_row].append(_HighlightFragment(0, end_column, token)) # Create dict to return. return _HighlightDict(row_to_fragments) def mouse_handler(self, cli, mouse_event): """ Mouse handler for this control. """ buffer = self._buffer(cli) position = mouse_event.position # Focus buffer when clicked. if self.has_focus(cli): if self._xy_to_cursor_position: # Translate coordinates back to the cursor position of the # original input. pos = self._xy_to_cursor_position(position.x, position.y) # Set the cursor position. if pos <= len(buffer.text): if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: buffer.exit_selection() buffer.cursor_position = pos elif mouse_event.event_type == MouseEventTypes.MOUSE_UP: # When the cursor was moved to another place, select the text. # (The >1 is actually a small but acceptable workaround for # selecting text in Vi navigation mode. In navigation mode, # the cursor can never be after the text, so the cursor # will be repositioned automatically.) if abs(buffer.cursor_position - pos) > 1: buffer.start_selection(selection_type=SelectionType.CHARACTERS) buffer.cursor_position = pos # Select word around cursor on double click. # Two MOUSE_UP events in a short timespan are considered a double click. double_click = self._last_click_timestamp and time.time() - self._last_click_timestamp < .3 self._last_click_timestamp = time.time() if double_click: start, end = buffer.document.find_boundaries_of_current_word() buffer.cursor_position += start buffer.start_selection(selection_type=SelectionType.CHARACTERS) buffer.cursor_position += end - start else: # Don't handle scroll events here. return NotImplemented # Not focussed, but focussing on click events. else: if self.focus_on_click(cli) and mouse_event.event_type == MouseEventTypes.MOUSE_UP: # Focus happens on mouseup. (If we did this on mousedown, the # up event will be received at the point where this widget is # focussed and be handled anyway.) cli.focus(self.buffer_name) else: return NotImplemented def move_cursor_down(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_down_position() def move_cursor_up(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_up_position()
class BufferControl(UIControl): """ Control for visualising the content of a `Buffer`. :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`. :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting. :param preview_search: `bool` or `CLIFilter`: Show search while typing. :param get_search_state: Callable that takes a CommandLineInterface and returns the SearchState to be used. (If not CommandLineInterface.search_state.) :param buffer_name: String representing the name of the buffer to display. :param default_char: :class:`.Char` instance to use to fill the background. This is transparent by default. :param focus_on_click: Focus this buffer when it's click, but not yet focussed. """ def __init__(self, buffer_name=DEFAULT_BUFFER, input_processors=None, lexer=None, preview_search=False, search_buffer_name=SEARCH_BUFFER, get_search_state=None, menu_position=None, default_char=None, focus_on_click=False): assert input_processors is None or all( isinstance(i, Processor) for i in input_processors) assert menu_position is None or callable(menu_position) assert lexer is None or isinstance(lexer, Lexer) assert get_search_state is None or callable(get_search_state) assert default_char is None or isinstance(default_char, Char) self.preview_search = to_cli_filter(preview_search) self.get_search_state = get_search_state self.focus_on_click = to_cli_filter(focus_on_click) self.input_processors = input_processors or [] self.buffer_name = buffer_name self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.default_char = default_char or Char(token=Token.Transparent) self.search_buffer_name = search_buffer_name #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a faily easy way to cache such an expensive operation. self._token_cache = SimpleCache(maxsize=8) self._xy_to_cursor_position = None self._last_click_timestamp = None self._last_get_processed_line = None def _buffer(self, cli): """ The buffer object that contains the 'main' content. """ return cli.buffers[self.buffer_name] def has_focus(self, cli): # This control gets the focussed if the actual `Buffer` instance has the # focus or when any of the `InputProcessor` classes tells us that it # wants the focus. (E.g. in case of a reverse-search, where the actual # search buffer may not be displayed, but the "reverse-i-search" text # should get the focus.) return cli.current_buffer_name == self.buffer_name or \ any(i.has_focus(cli) for i in self.input_processors) def preferred_width(self, cli, max_available_width): """ This should return the preferred width. Note: We don't specify a preferred width according to the content, because it would be too expensive. Calculating the preferred width can be done by calculating the longest line, but this would require applying all the processors to each line. This is unfeasible for a larger document, and doing it for small documents only would result in inconsistent behaviour. """ return None def preferred_height(self, cli, width, max_available_height, wrap_lines): # Calculate the content height, if it was drawn on a screen with the # given width. height = 0 content = self.create_content(cli, width, None) # When line wrapping is off, the height should be equal to the amount # of lines. if not wrap_lines: return content.line_count # When the number of lines exceeds the max_available_height, just # return max_available_height. No need to calculate anything. if content.line_count >= max_available_height: return max_available_height for i in range(content.line_count): height += content.get_height_for_line(i, width) if height >= max_available_height: return max_available_height return height def _get_tokens_for_line_func(self, cli, document): """ Create a function that returns the tokens for a given line. """ # Cache using `document.text`. def get_tokens_for_line(): return self.lexer.lex_document(cli, document) return self._token_cache.get(document.text, get_tokens_for_line) def _create_get_processed_line_func(self, cli, document): """ Create a function that takes a line number of the current document and returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source) tuple. """ def transform(lineno, tokens): " Transform the tokens for a given line number. " source_to_display_functions = [] display_to_source_functions = [] # Get cursor position at this line. if document.cursor_position_row == lineno: cursor_column = document.cursor_position_col else: cursor_column = None def source_to_display(i): """ Translate x position from the buffer to the x position in the processed token list. """ for f in source_to_display_functions: i = f(i) return i # Apply each processor. for p in self.input_processors: transformation = p.apply_transformation( cli, document, lineno, source_to_display, tokens) tokens = transformation.tokens if cursor_column: cursor_column = transformation.source_to_display( cursor_column) display_to_source_functions.append( transformation.display_to_source) source_to_display_functions.append( transformation.source_to_display) def display_to_source(i): for f in reversed(display_to_source_functions): i = f(i) return i return _ProcessedLine(tokens, source_to_display, display_to_source) def create_func(): get_line = self._get_tokens_for_line_func(cli, document) cache = {} def get_processed_line(i): try: return cache[i] except KeyError: processed_line = transform(i, get_line(i)) cache[i] = processed_line return processed_line return get_processed_line return create_func() def create_content(self, cli, width, height): """ Create a UIContent. """ buffer = self._buffer(cli) # Get the document to be shown. If we are currently searching (the # search buffer has focus, and the preview_search filter is enabled), # then use the search document, which has possibly a different # text/cursor position.) def preview_now(): """ True when we should preview a search. """ return bool( self.preview_search(cli) and cli.buffers[self.search_buffer_name].text) if preview_now(): if self.get_search_state: ss = self.get_search_state(cli) else: ss = cli.search_state document = buffer.document_for_search( SearchState(text=cli.current_buffer.text, direction=ss.direction, ignore_case=ss.ignore_case)) else: document = buffer.document get_processed_line = self._create_get_processed_line_func( cli, document) self._last_get_processed_line = get_processed_line def translate_rowcol(row, col): " Return the content column for this coordinate. " return Point(y=row, x=get_processed_line(row).source_to_display(col)) def get_line(i): " Return the tokens for a given line number. " tokens = get_processed_line(i).tokens # Add a space at the end, because that is a possible cursor # position. (When inserting after the input.) We should do this on # all the lines, not just the line containing the cursor. (Because # otherwise, line wrapping/scrolling could change when moving the # cursor around.) tokens = tokens + [(self.default_char.token, ' ')] return tokens content = UIContent(get_line=get_line, line_count=document.line_count, cursor_position=translate_rowcol( document.cursor_position_row, document.cursor_position_col), default_char=self.default_char) # If there is an auto completion going on, use that start point for a # pop-up menu position. (But only when this buffer has the focus -- # there is only one place for a menu, determined by the focussed buffer.) if cli.current_buffer_name == self.buffer_name: menu_position = self.menu_position( cli) if self.menu_position else None if menu_position is not None: assert isinstance(menu_position, int) menu_row, menu_col = buffer.document.translate_index_to_position( menu_position) content.menu_position = translate_rowcol(menu_row, menu_col) elif buffer.complete_state: # Position for completion menu. # Note: We use 'min', because the original cursor position could be # behind the input string when the actual completion is for # some reason shorter than the text we had before. (A completion # can change and shorten the input.) menu_row, menu_col = buffer.document.translate_index_to_position( min( buffer.cursor_position, buffer.complete_state. original_document.cursor_position)) content.menu_position = translate_rowcol(menu_row, menu_col) else: content.menu_position = None return content def mouse_handler(self, cli, mouse_event): """ Mouse handler for this control. """ buffer = self._buffer(cli) position = mouse_event.position # Focus buffer when clicked. if self.has_focus(cli): if self._last_get_processed_line: processed_line = self._last_get_processed_line(position.y) # Translate coordinates back to the cursor position of the # original input. xpos = processed_line.display_to_source(position.x) index = buffer.document.translate_row_col_to_index( position.y, xpos) # Set the cursor position. if mouse_event.event_type == MouseEventType.MOUSE_DOWN: buffer.exit_selection() buffer.cursor_position = index elif mouse_event.event_type == MouseEventType.MOUSE_UP: # When the cursor was moved to another place, select the text. # (The >1 is actually a small but acceptable workaround for # selecting text in Vi navigation mode. In navigation mode, # the cursor can never be after the text, so the cursor # will be repositioned automatically.) if abs(buffer.cursor_position - index) > 1: buffer.start_selection( selection_type=SelectionType.CHARACTERS) buffer.cursor_position = index # Select word around cursor on double click. # Two MOUSE_UP events in a short timespan are considered a double click. double_click = self._last_click_timestamp and time.time( ) - self._last_click_timestamp < .3 self._last_click_timestamp = time.time() if double_click: start, end = buffer.document.find_boundaries_of_current_word( ) buffer.cursor_position += start buffer.start_selection( selection_type=SelectionType.CHARACTERS) buffer.cursor_position += end - start else: # Don't handle scroll events here. return NotImplemented # Not focussed, but focussing on click events. else: if self.focus_on_click( cli) and mouse_event.event_type == MouseEventType.MOUSE_UP: # Focus happens on mouseup. (If we did this on mousedown, the # up event will be received at the point where this widget is # focussed and be handled anyway.) cli.focus(self.buffer_name) else: return NotImplemented def move_cursor_down(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_down_position() def move_cursor_up(self, cli): b = self._buffer(cli) b.cursor_position += b.document.get_cursor_up_position()
def __init__(self, styles: List[BaseStyle]) -> None: self.styles = styles self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1)
class Registry(object): """ Key binding registry. :: r = Registry() @r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT) def handler(event): # Handle ControlX-ControlC key sequence. pass """ def __init__(self): self.key_bindings = [] self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000) self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000) def _clear_cache(self): self._get_bindings_for_keys_cache.clear() self._get_bindings_starting_with_keys_cache.clear() def add_binding(self, *keys, **kwargs): """ Decorator for annotating key bindings. :param filter: :class:`~prompt_toolkit.filters.CLIFilter` to determine when this key binding is active. :param eager: :class:`~prompt_toolkit.filters.CLIFilter` or `bool`. When True, ignore potential longer matches when this key binding is hit. E.g. when there is an active eager key binding for Ctrl-X, execute the handler immediately and ignore the key binding for Ctrl-X Ctrl-E of which it is a prefix. :param save_before: Callable that takes an `Event` and returns True if we should save the current buffer, before handling the event. (That's the default.) """ filter = to_cli_filter(kwargs.pop('filter', True)) eager = to_cli_filter(kwargs.pop('eager', False)) save_before = kwargs.pop('save_before', lambda e: True) to_cli_filter(kwargs.pop('invalidate_ui', True)) # Deprecated! (ignored.) assert not kwargs assert keys assert all(isinstance(k, (Key, text_type)) for k in keys), \ 'Key bindings should consist of Key and string (unicode) instances.' assert callable(save_before) def decorator(func): # When a filter is Never, it will always stay disabled, so in that case # don't bother putting it in the registry. It will slow down every key # press otherwise. if not isinstance(filter, Never): self.key_bindings.append( _Binding(keys, func, filter=filter, eager=eager, save_before=save_before)) self._clear_cache() return func return decorator def remove_binding(self, function): """ Remove a key binding. This expects a function that was given to `add_binding` method as parameter. Raises `ValueError` when the given function was not registered before. """ assert callable(function) for b in self.key_bindings: if b.handler == function: self.key_bindings.remove(b) self._clear_cache() return # No key binding found for this function. Raise ValueError. raise ValueError('Binding not found: %r' % (function, )) def get_bindings_for_keys(self, keys): """ Return a list of key bindings that can handle this key. (This return also inactive bindings, so the `filter` still has to be called, for checking it.) :param keys: tuple of keys. """ def get(): result = [] for b in self.key_bindings: if len(keys) == len(b.keys): match = True any_count = 0 for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if i == Keys.Any: any_count += 1 if match: result.append((any_count, b)) # Place bindings that have more 'Any' occurences in them at the end. result = sorted(result, key=lambda item: -item[0]) return [item[1] for item in result] return self._get_bindings_for_keys_cache.get(keys, get) def get_bindings_starting_with_keys(self, keys): """ Return a list of key bindings that handle a key sequence starting with `keys`. (It does only return bindings for which the sequences are longer than `keys`. And like `get_bindings_for_keys`, it also includes inactive bindings.) :param keys: tuple of keys. """ def get(): result = [] for b in self.key_bindings: if len(keys) < len(b.keys): match = True for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if match: result.append(b) return result return self._get_bindings_starting_with_keys_cache.get(keys, get)
class TokenListControl(UIControl): """ Control that displays a list of (Token, text) tuples. (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) Mouse support: The list of tokens can also contain tuples of three items, looking like: (Token, text, handler). When mouse support is enabled and the user clicks on this token, then the given handler is called. That handler should accept two inputs: (CommandLineInterface, MouseEvent) and it should either handle the event or return `NotImplemented` in case we want the containing Window to handle this event. :param get_tokens: Callable that takes a `CommandLineInterface` instance and returns the list of (Token, text) tuples to be displayed right now. :param default_char: default :class:`.Char` (character and Token) to use for the background when there is more space available than `get_tokens` returns. :param get_default_char: Like `default_char`, but this is a callable that takes a :class:`prompt_toolkit.interface.CommandLineInterface` and returns a :class:`.Char` instance. :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`, this UI control will take the focus. The cursor will be shown in the upper left corner of this control, unless `get_token` returns a ``Token.SetCursorPosition`` token somewhere in the token list, then the cursor will be shown there. :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines. """ def __init__(self, get_tokens, default_char=None, get_default_char=None, align_right=False, align_center=False, has_focus=False, wrap_lines=True): assert default_char is None or isinstance(default_char, Char) assert get_default_char is None or callable(get_default_char) assert not (default_char and get_default_char) self.align_right = to_cli_filter(align_right) self.align_center = to_cli_filter(align_center) self._has_focus_filter = to_cli_filter(has_focus) self.wrap_lines = to_cli_filter(wrap_lines) self.get_tokens = get_tokens # Construct `get_default_char` callable. if default_char: get_default_char = lambda _: default_char elif not get_default_char: get_default_char = lambda _: Char(' ', Token) self.get_default_char = get_default_char #: Cache for rendered screens. self._screen_cache = SimpleCache(maxsize=18) self._token_cache = SimpleCache(maxsize=1) # Only cache one token list. We don't need the previous item. # Render info for the mouse support. self._tokens = None # The last rendered tokens. self._pos_to_indexes = None # Mapping from mouse positions (x,y) to # positions in the token list. def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.get_tokens) def _get_tokens_cached(self, cli): """ Get tokens, but only retrieve tokens once during one render run. (This function is called several times during one rendering, because we also need those for calculating the dimensions.) """ return self._token_cache.get( cli.render_counter, lambda: self.get_tokens(cli)) def has_focus(self, cli): return self._has_focus_filter(cli) def preferred_width(self, cli, max_available_width): """ Return the preferred width for this control. That is the width of the longest line. """ text = ''.join(t[1] for t in self._get_tokens_cached(cli)) line_lengths = [get_cwidth(l) for l in text.split('\n')] return max(line_lengths) def preferred_height(self, cli, width): screen = self.create_screen(cli, width, None) return screen.height def create_screen(self, cli, width, height): # Get tokens tokens_with_mouse_handlers = self._get_tokens_cached(cli) default_char = self.get_default_char(cli) # Wrap/align right/center parameters. wrap_lines = self.wrap_lines(cli) right = self.align_right(cli) center = self.align_center(cli) def process_line(line): " Center or right align a single line. " used_width = token_list_width(line) padding = width - used_width if center: padding = int(padding / 2) return [(default_char.token, default_char.char * padding)] + line + [(Token, '\n')] if right or center: tokens2 = [] for line in split_lines(tokens_with_mouse_handlers): tokens2.extend(process_line(line)) tokens_with_mouse_handlers = tokens2 # Strip mouse handlers from tokens. tokens = [tuple(item[:2]) for item in tokens_with_mouse_handlers] # Create screen, or take it from the cache. key = (default_char.char, default_char.token, tuple(tokens_with_mouse_handlers), width, wrap_lines, right, center) params = (default_char, tokens, width, wrap_lines, right, center) screen, self._pos_to_indexes = self._screen_cache.get(key, lambda: self._get_screen(*params)) self._tokens = tokens_with_mouse_handlers return screen @classmethod def _get_screen(cls, default_char, tokens, width, wrap_lines, right, center): screen = Screen(default_char, initial_width=width) # Only call write_data when we actually have tokens. # (Otherwise the screen height will go up from 0 to 1 while we don't # want that. -- An empty control should not take up any space.) if tokens: write_data_result = screen.write_data(tokens, width=(width if wrap_lines else None)) indexes_to_pos = write_data_result.indexes_to_pos pos_to_indexes = _LazyReverseDict(indexes_to_pos) else: pos_to_indexes = {} return screen, pos_to_indexes @classmethod def static(cls, tokens): def get_static_tokens(cli): return tokens return cls(get_static_tokens) def mouse_handler(self, cli, mouse_event): """ Handle mouse events. (When the token list contained mouse handlers and the user clicked on on any of these, the matching handler is called. This handler can still return `NotImplemented` in case we want the `Window` to handle this particular event.) """ if self._pos_to_indexes: # Find position in the token list. position = mouse_event.position index = self._pos_to_indexes.get((position.x, position.y)) if index is not None: # Find mouse handler for this character. count = 0 for item in self._tokens: count += len(item[1]) if count >= index: if len(item) >= 3: # Handler found. Call it. # (Handler can return NotImplemented, so return # that result.) handler = item[2] return handler(cli, mouse_event) else: break # Otherwise, don't handle here. return NotImplemented
class KeyBindings(KeyBindingsBase): """ A container for a set of key bindings. Example usage:: kb = KeyBindings() @kb.add('c-t') def _(event): print('Control-T pressed') @kb.add('c-a', 'c-b') def _(event): print('Control-A pressed, followed by Control-B') @kb.add('c-x', filter=is_searching) def _(event): print('Control-X pressed') # Works only if we are searching. """ def __init__(self): self.bindings = [] self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000) self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000) self.__version = 0 # For cache invalidation. def _clear_cache(self): self.__version += 1 self._get_bindings_for_keys_cache.clear() self._get_bindings_starting_with_keys_cache.clear() @property def _version(self): return self.__version def add(self, *keys, **kwargs): """ Decorator for adding a key bindings. :param filter: :class:`~prompt_toolkit.filters.Filter` to determine when this key binding is active. :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`. When True, ignore potential longer matches when this key binding is hit. E.g. when there is an active eager key binding for Ctrl-X, execute the handler immediately and ignore the key binding for Ctrl-X Ctrl-E of which it is a prefix. :param is_global: When this key bindings is added to a `Container` or `Control`, make it a global (always active) binding. :param save_before: Callable that takes an `Event` and returns True if we should save the current buffer, before handling the event. (That's the default.) :param record_in_macro: Record these key bindings when a macro is being recorded. (True by default.) """ filter = to_filter(kwargs.pop('filter', True)) eager = to_filter(kwargs.pop('eager', False)) is_global = to_filter(kwargs.pop('is_global', False)) save_before = kwargs.pop('save_before', lambda e: True) record_in_macro = to_filter(kwargs.pop('record_in_macro', True)) assert not kwargs assert keys assert callable(save_before) keys = tuple(_check_and_expand_key(k) for k in keys) if isinstance(filter, Never): # When a filter is Never, it will always stay disabled, so in that # case don't bother putting it in the key bindings. It will slow # down every key press otherwise. def decorator(func): return func else: def decorator(func): if isinstance(func, _Binding): # We're adding an existing _Binding object. self.bindings.append( _Binding( keys, func.handler, filter=func.filter & filter, eager=eager | func.eager, is_global = is_global | func.is_global, save_before=func.save_before, record_in_macro=func.record_in_macro)) else: self.bindings.append( _Binding(keys, func, filter=filter, eager=eager, is_global=is_global, save_before=save_before, record_in_macro=record_in_macro)) self._clear_cache() return func return decorator def remove(self, *args): """ Remove a key binding. This expects either a function that was given to `add` method as parameter or a sequence of key bindings. Raises `ValueError` when no bindings was found. Usage:: remove(handler) # Pass handler. remove('c-x', 'c-a') # Or pass the key bindings. """ found = False if callable(args[0]): assert len(args) == 1 function = args[0] # Remove the given function. for b in self.bindings: if b.handler == function: self.bindings.remove(b) found = True else: assert len(args) > 0 # Remove this sequence of key bindings. keys = tuple(_check_and_expand_key(k) for k in args) for b in self.bindings: if b.keys == keys: self.bindings.remove(b) found = True if found: self._clear_cache() else: # No key binding found for this function. Raise ValueError. raise ValueError('Binding not found: %r' % (function, )) # For backwards-compatibility. add_binding = add remove_binding = remove def get_bindings_for_keys(self, keys): """ Return a list of key bindings that can handle this key. (This return also inactive bindings, so the `filter` still has to be called, for checking it.) :param keys: tuple of keys. """ def get(): result = [] for b in self.bindings: if len(keys) == len(b.keys): match = True any_count = 0 for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if i == Keys.Any: any_count += 1 if match: result.append((any_count, b)) # Place bindings that have more 'Any' occurrences in them at the end. result = sorted(result, key=lambda item: -item[0]) return [item[1] for item in result] return self._get_bindings_for_keys_cache.get(keys, get) def get_bindings_starting_with_keys(self, keys): """ Return a list of key bindings that handle a key sequence starting with `keys`. (It does only return bindings for which the sequences are longer than `keys`. And like `get_bindings_for_keys`, it also includes inactive bindings.) :param keys: tuple of keys. """ def get(): result = [] for b in self.bindings: if len(keys) < len(b.keys): match = True for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if match: result.append(b) return result return self._get_bindings_starting_with_keys_cache.get(keys, get)