def test_ansi_formatting(): value = ANSI('\x1b[32mHe\x1b[45mllo') assert to_formatted_text(value) == [ ('ansigreen', 'H'), ('ansigreen', 'e'), ('ansigreen bg:ansimagenta', 'l'), ('ansigreen bg:ansimagenta', 'l'), ('ansigreen bg:ansimagenta', 'o'), ] # Bold and italic. value = ANSI('\x1b[1mhe\x1b[0mllo') assert to_formatted_text(value) == [ ('bold', 'h'), ('bold', 'e'), ('', 'l'), ('', 'l'), ('', 'o'), ] # Zero width escapes. value = ANSI('ab\001cd\002ef') assert to_formatted_text(value) == [ ('', 'a'), ('', 'b'), ('[ZeroWidthEscape]', 'cd'), ('', 'e'), ('', 'f'), ] assert isinstance(to_formatted_text(value), FormattedText)
def test_html_interpolation(): # %-style interpolation. value = HTML('<b>%s</b>') % 'hello' assert to_formatted_text(value) == [ ('class:b', 'hello') ] value = HTML('<b>%s</b>') % ('hello', ) assert to_formatted_text(value) == [ ('class:b', 'hello') ] value = HTML('<b>%s</b><u>%s</u>') % ('hello', 'world') assert to_formatted_text(value) == [ ('class:b', 'hello'), ('class:u', 'world') ] # Format function. value = HTML('<b>{0}</b><u>{1}</u>').format('hello', 'world') assert to_formatted_text(value) == [ ('class:b', 'hello'), ('class:u', 'world') ] value = HTML('<b>{a}</b><u>{b}</u>').format(a='hello', b='world') assert to_formatted_text(value) == [ ('class:b', 'hello'), ('class:u', 'world') ]
def test_html_with_fg_bg(): html = HTML('<style bg="ansired">hello</style>') assert to_formatted_text(html) == [ ('bg:ansired', 'hello'), ] html = HTML('<style bg="ansired" fg="#ff0000">hello</style>') assert to_formatted_text(html) == [ ('fg:#ff0000 bg:ansired', 'hello'), ] html = HTML('<style bg="ansired" fg="#ff0000">hello <world fg="ansiblue">world</world></style>') assert to_formatted_text(html) == [ ('fg:#ff0000 bg:ansired', 'hello '), ('class:world fg:ansiblue bg:ansired', 'world'), ]
def send_above_prompt(self, formatted_text: AnyFormattedText) -> None: """ Send text to the client. This is asynchronous, returns a `Future`. """ formatted_text = to_formatted_text(formatted_text) return self._run_in_terminal(lambda: self.send(formatted_text))
def apply_transformation(self, ti: TransformationInput) -> Transformation: # Insert fragments after the last line. if ti.lineno == ti.document.line_count - 1: # Get fragments. fragments_after = to_formatted_text(self.text, self.style) return Transformation(fragments=ti.fragments + fragments_after) else: return Transformation(fragments=ti.fragments)
def test_merge_formatted_text(): html1 = HTML('<u>hello</u>') html2 = HTML('<b>world</b>') result = merge_formatted_text([html1, html2]) assert to_formatted_text(result) == [ ('class:u', 'hello'), ('class:b', 'world'), ]
def _get_formatted_text_cached(self) -> StyleAndTextTuples: """ 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 test_interpolation(): value = Template(' {} ').format(HTML('<b>hello</b>')) assert to_formatted_text(value) == [ ('', ' '), ('class:b', 'hello'), ('', ' '), ] value = Template('a{}b{}c').format(HTML('<b>hello</b>'), 'world') assert to_formatted_text(value) == [ ('', 'a'), ('class:b', 'hello'), ('', 'b'), ('', 'world'), ('', 'c'), ]
def create_margin(self, window_render_info: 'WindowRenderInfo', width: int, height: int) -> StyleAndTextTuples: get_continuation = self.get_continuation result: StyleAndTextTuples = [] # First line. result.extend(to_formatted_text(self.get_prompt())) # Next lines. if get_continuation: last_y = None for y in window_render_info.displayed_lines[1:]: result.append(('', '\n')) result.extend(to_formatted_text(get_continuation(width, y, y == last_y))) last_y = y return result
def test_basic_html(): html = HTML('<i>hello</i>') assert to_formatted_text(html) == [('class:i', 'hello')] html = HTML('<i><b>hello</b></i>') assert to_formatted_text(html) == [('class:i,b', 'hello')] html = HTML('<i><b>hello</b>world<strong>test</strong></i>after') assert to_formatted_text(html) == [ ('class:i,b', 'hello'), ('class:i', 'world'), ('class:i,strong', 'test'), ('', 'after'), ] # It's important that `to_formatted_text` returns a `FormattedText` # instance. Otherwise, `print_formatted_text` won't recognise it and will # print a list literal instead. assert isinstance(to_formatted_text(html), FormattedText)
def test_with_style(): """ Text `print_formatted_text` with `HTML` wrapped in `to_formatted_text`. """ f = _Capture() html = HTML('<ansigreen>hello</ansigreen> <b>world</b>') formatted_text = to_formatted_text(html, style='class:myhtml') pt_print(formatted_text, file=f) assert f.data == \ b'\x1b[0m\x1b[?7h\x1b[0;32mhello\x1b[0m \x1b[0;1mworld\x1b[0m\r\n\x1b[0m'
def _get_text_fragments(self) -> StyleAndTextTuples: style = 'class:completion-menu.multi-column-meta' state = get_app().current_buffer.complete_state if state and state.current_completion and state.current_completion.display_meta_text: return to_formatted_text( cast(StyleAndTextTuples, [('', ' ')]) + state.current_completion.display_meta + [('', ' ')], style=style) return []
def get_width() -> AnyDimension: if width is None: text_fragments = to_formatted_text(self.text) text = fragment_list_to_text(text_fragments) if text: longest_line = max( get_cwidth(line) for line in text.splitlines()) else: return D(preferred=0) return D(preferred=longest_line) else: return width
def test_pygments_tokens(): text = [ (('A', 'B'), 'hello'), # Token.A.B (('C', 'D', 'E'), 'hello'), # Token.C.D.E ((), 'world'), # Token ] assert to_formatted_text(PygmentsTokens(text)) == [ ('class:pygments.a.b', 'hello'), ('class:pygments.c.d.e', 'hello'), ('class:pygments', 'world'), ]
def display(page: int) -> None: # Display completions. page_completions = completions[page * completions_per_page:(page + 1) * completions_per_page] page_row_count = int( math.ceil(len(page_completions) / float(column_count))) page_columns = [ page_completions[i * page_row_count:(i + 1) * page_row_count] for i in range(column_count) ] result: StyleAndTextTuples = [] for r in range(page_row_count): for c in range(column_count): try: completion = page_columns[c][r] style = 'class:readline-like-completions.completion ' + ( completion.style or '') result.extend( to_formatted_text(completion.display, style=style)) # Add padding. padding = max_compl_width - get_cwidth( completion.display_text) result.append(( completion.style, ' ' * padding, )) except IndexError: pass result.append(('', '\n')) app.print_text( to_formatted_text(result, 'class:readline-like-completions'))
def format(self, progress_bar: 'ProgressBar', progress: 'ProgressBarCounter[object]', width: int) -> AnyFormattedText: # Get formatted text from nested formatter, and explode it in # text/style tuples. result = self.formatter.format(progress_bar, progress, width) result = explode_text_fragments(to_formatted_text(result)) # Insert colors. result2: StyleAndTextTuples = [] shift = int(time.time() * 3) % len(self.colors) for i, (style, text, *_) in enumerate(result): result2.append((style + ' ' + self.colors[(i + shift) % len(self.colors)], text)) return result2
def _get_text_fragments(self) -> StyleAndTextTuples: def mouse_handler(mouse_event: MouseEvent) -> None: """ Set `_selected_index` and `current_value` according to the y position of the mouse click event. """ if mouse_event.event_type == MouseEventType.MOUSE_UP: self._selected_index = mouse_event.position.y self._handle_enter() result: StyleAndTextTuples = [] for i, value in enumerate(self.values): if self.multiple_selection: checked = (value[0] in self.current_values) else: checked = (value[0] == self.current_value) selected = (i == self._selected_index) style = '' if checked: style += ' ' + self.checked_style if selected: style += ' ' + self.selected_style result.append((style, self.open_character)) if selected: result.append(('[SetCursorPosition]', '')) if checked: result.append((style, '*')) else: result.append((style, ' ')) result.append((style, self.close_character)) result.append((self.default_style, ' ')) result.extend(to_formatted_text(value[1], style=self.default_style)) result.append(('', '\n')) # Add mouse handler to all fragments. for i in range(len(result)): result[i] = (result[i][0], result[i][1], mouse_handler) result.pop() # Remove last newline. return result
def _get_menu_item_meta_fragments( self, completion: Completion, is_current_completion: bool, width: int) -> StyleAndTextTuples: if is_current_completion: style_str = 'class:completion-menu.meta.completion.current' else: style_str = 'class:completion-menu.meta.completion' text, tw = _trim_formatted_text(completion.display_meta, width - 2) padding = ' ' * (width - 1 - tw) return to_formatted_text( cast(StyleAndTextTuples, []) + [('', ' ')] + text + [('', padding)], style=style_str)
def create_content(self, width: int, height: int) -> UIContent: items: List[StyleAndTextTuples] = [] for pr in self.progress_bar.counters: try: text = self.formatter.format(self.progress_bar, pr, width) except BaseException: traceback.print_exc() text = 'ERROR' items.append(to_formatted_text(text)) def get_line(i: int) -> StyleAndTextTuples: return items[i] return UIContent( get_line=get_line, line_count=len(items), show_cursor=False)
def apply_transformation(self, ti: TransformationInput) -> Transformation: source_to_display: Optional[SourceToDisplay] display_to_source: Optional[DisplayToSource] if ti.lineno == 0: # Get fragments. fragments_before = to_formatted_text(self.text, self.style) fragments = fragments_before + ti.fragments shift_position = fragment_list_len(fragments_before) source_to_display = lambda i: i + shift_position display_to_source = lambda i: i - shift_position else: fragments = ti.fragments source_to_display = None display_to_source = None return Transformation(fragments, source_to_display=source_to_display, display_to_source=display_to_source)
def print_formatted_text( output: Output, formatted_text: AnyFormattedText, style: BaseStyle, style_transformation: Optional[StyleTransformation] = None, color_depth: Optional[ColorDepth] = None) -> None: """ Print a list of (style_str, text) tuples in the given style to the output. """ fragments = to_formatted_text(formatted_text) style_transformation = style_transformation or DummyStyleTransformation() color_depth = color_depth or ColorDepth.default() # Reset first. output.reset_attributes() output.enable_autowrap() # Print all (style_str, text) tuples. attrs_for_style_string = _StyleStringToAttrsCache( style.get_attrs_for_style_str, style_transformation) for style_str, text, *_ in fragments: attrs = attrs_for_style_string[style_str] if attrs: output.set_attributes(attrs, color_depth) else: output.reset_attributes() # Eliminate carriage returns text = text.replace('\r', '') # Assume that the output is raw, and insert a carriage return before # every newline. (Also important when the front-end is a telnet client.) output.write(text.replace('\n', '\r\n')) # Reset again. output.reset_attributes() output.flush()
def __init__(self, text: str, start_position: int = 0, display: Optional[AnyFormattedText] = None, display_meta: Optional[AnyFormattedText] = None, style: str = '', selected_style: str = '') -> None: from prompt_toolkit_dev.formatted_text import to_formatted_text self.text = text self.start_position = start_position self._display_meta = display_meta if display is None: display = text self.display = to_formatted_text(display) self.style = style self.selected_style = selected_style assert self.start_position <= 0
def _get_menu_item_fragments( completion: Completion, is_current_completion: bool, width: int, space_after: bool = False) -> StyleAndTextTuples: """ Get the style/text tuples for a menu item, styled and trimmed to the given width. """ if is_current_completion: style_str = 'class:completion-menu.completion.current %s %s' % ( completion.style, completion.selected_style) else: style_str = 'class:completion-menu.completion ' + completion.style text, tw = _trim_formatted_text( completion.display, (width - 2 if space_after else width - 1)) padding = ' ' * (width - 1 - tw) return to_formatted_text( cast(StyleAndTextTuples, []) + [('', ' ')] + text + [('', padding)], style=style_str)
def _add_suffix(self, label: AnyFormattedText) -> StyleAndTextTuples: label = to_formatted_text(label, style='class:label') return label + [('', self.suffix)]
def __init__(self, text: AnyFormattedText, style: str = '') -> None: self.text = to_formatted_text(text, style=style)
def create_content(self, width: int, height: int) -> UIContent: """ Create a UIContent object for this menu. """ complete_state = get_app().current_buffer.complete_state if complete_state is None: return UIContent() column_width = self._get_column_width(complete_state) self._render_pos_to_completion = {} _T = TypeVar('_T') def grouper(n: int, iterable: Iterable[_T], fillvalue: Optional[_T] = None) -> Iterable[List[_T]]: " grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx " args = [iter(iterable)] * n return zip_longest(fillvalue=fillvalue, *args) def is_current_completion(completion: Completion) -> bool: " Returns True when this completion is the currently selected one. " return (complete_state is not None and complete_state.complete_index is not None and c == complete_state.current_completion) # Space required outside of the regular columns, for displaying the # left and right arrow. HORIZONTAL_MARGIN_REQUIRED = 3 # There should be at least one column, but it cannot be wider than # the available width. column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width) # However, when the columns tend to be very wide, because there are # some very wide entries, shrink it anyway. if column_width > self.suggested_max_column_width: # `column_width` can still be bigger that `suggested_max_column_width`, # but if there is place for two columns, we divide by two. column_width //= (column_width // self.suggested_max_column_width) visible_columns = max(1, (width - self._required_margin) // column_width) columns_ = list(grouper(height, complete_state.completions)) rows_ = list(zip(*columns_)) # Make sure the current completion is always visible: update scroll offset. selected_column = (complete_state.complete_index or 0) // height self.scroll = min(selected_column, max(self.scroll, selected_column - visible_columns + 1)) render_left_arrow = self.scroll > 0 render_right_arrow = self.scroll < len(rows_[0]) - visible_columns # Write completions to screen. fragments_for_line = [] for row_index, row in enumerate(rows_): fragments: StyleAndTextTuples = [] middle_row = row_index == len(rows_) // 2 # Draw left arrow if we have hidden completions on the left. if render_left_arrow: fragments.append(('class:scrollbar', '<' if middle_row else ' ')) elif render_right_arrow: # Reserve one column empty space. (If there is a right # arrow right now, there can be a left arrow as well.) fragments.append(('', ' ')) # Draw row content. for column_index, c in enumerate(row[self.scroll:][:visible_columns]): if c is not None: fragments += _get_menu_item_fragments( c, is_current_completion(c), column_width, space_after=False) # Remember render position for mouse click handler. for x in range(column_width): self._render_pos_to_completion[(column_index * column_width + x, row_index)] = c else: fragments.append(('class:completion', ' ' * column_width)) # Draw trailing padding for this row. # (_get_menu_item_fragments only returns padding on the left.) if render_left_arrow or render_right_arrow: fragments.append(('class:completion', ' ')) # Draw right arrow if we have hidden completions on the right. if render_right_arrow: fragments.append(('class:scrollbar', '>' if middle_row else ' ')) elif render_left_arrow: fragments.append(('class:completion', ' ')) # Add line. fragments_for_line.append(to_formatted_text( fragments, style='class:completion-menu')) self._rendered_rows = height self._rendered_columns = visible_columns self._total_columns = len(columns_) self._render_left_arrow = render_left_arrow self._render_right_arrow = render_right_arrow self._render_width = column_width * visible_columns + render_left_arrow + render_right_arrow + 1 def get_line(i: int) -> StyleAndTextTuples: return fragments_for_line[i] return UIContent(get_line=get_line, line_count=len(rows_))
def get_height_for_line(self, lineno: int, width: int, get_line_prefix: Optional[GetLinePrefixCallable], slice_stop: Optional[int] = None) -> int: """ Return the height that a given line would need if it is rendered in a space with the given width (using line wrapping). :param get_line_prefix: None or a `Window.get_line_prefix` callable that returns the prefix to be inserted before this line. :param slice_stop: Wrap only "line[:slice_stop]" and return that partial result. This is needed for scrolling the window correctly when line wrapping. :returns: The computed height. """ # Instead of using `get_line_prefix` as key, we use render_counter # instead. This is more reliable, because this function could still be # the same, while the content would change over time. key = get_app().render_counter, lineno, width, slice_stop try: return self._line_heights_cache[key] except KeyError: if width == 0: height = 10**8 else: # Calculate line width first. line = fragment_list_to_text( self.get_line(lineno))[:slice_stop] text_width = get_cwidth(line) if get_line_prefix: # Add prefix width. text_width += fragment_list_width( to_formatted_text(get_line_prefix(lineno, 0))) # Slower path: compute path when there's a line prefix. height = 1 # Keep wrapping as long as the line doesn't fit. # Keep adding new prefixes for every wrapped line. while text_width > width: height += 1 text_width -= width fragments2 = to_formatted_text( get_line_prefix(lineno, height - 1)) prefix_width = get_cwidth( fragment_list_to_text(fragments2)) if prefix_width >= width: # Prefix doesn't fit. height = 10**8 break text_width += prefix_width else: # Fast path: compute height when there's no line prefix. try: quotient, remainder = divmod(text_width, width) except ZeroDivisionError: height = 10**8 else: if remainder: quotient += 1 # Like math.ceil. height = max(1, quotient) # Cache and return self._line_heights_cache[key] = height return height
def display_meta(self) -> StyleAndTextTuples: " Return meta-text. (This is lazy when using a callable). " from prompt_toolkit_dev.formatted_text import to_formatted_text return to_formatted_text(self._display_meta or '')
def to_text(val: Any) -> StyleAndTextTuples: # Normal lists which are not instances of `FormattedText` are # considered plain text. if isinstance(val, list) and not isinstance(val, FormattedText): return to_formatted_text('{0}'.format(val)) return to_formatted_text(val, auto_convert=True)
def create_content(self, width: int, height: int) -> UIContent: all_fragments: StyleAndTextTuples = [] complete_state = get_app().current_buffer.complete_state if complete_state: completions = complete_state.completions index = complete_state.complete_index # Can be None! # Width of the completions without the left/right arrows in the margins. content_width = width - 6 # Booleans indicating whether we stripped from the left/right cut_left = False cut_right = False # Create Menu content. fragments: StyleAndTextTuples = [] for i, c in enumerate(completions): # When there is no more place for the next completion if fragment_list_len(fragments) + len( c.display_text) >= content_width: # If the current one was not yet displayed, page to the next sequence. if i <= (index or 0): fragments = [] cut_left = True # If the current one is visible, stop here. else: cut_right = True break fragments.extend( to_formatted_text( c.display_text, style=('class:completion-toolbar.completion.current' if i == index else 'class:completion-toolbar.completion'))) fragments.append(('', ' ')) # Extend/strip until the content width. fragments.append( ('', ' ' * (content_width - fragment_list_len(fragments)))) fragments = fragments[:content_width] # Return fragments all_fragments.append(('', ' ')) all_fragments.append( ('class:completion-toolbar.arrow', '<' if cut_left else ' ')) all_fragments.append(('', ' ')) all_fragments.extend(fragments) all_fragments.append(('', ' ')) all_fragments.append( ('class:completion-toolbar.arrow', '>' if cut_right else ' ')) all_fragments.append(('', ' ')) def get_line(i: int) -> StyleAndTextTuples: return all_fragments return UIContent(get_line=get_line, line_count=1)