1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Text formatting functions.""" 15 16import copy 17import re 18from typing import Iterable, List, Tuple 19 20from prompt_toolkit.formatted_text import StyleAndTextTuples 21from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple 22from prompt_toolkit.utils import get_cwidth 23 24_ANSI_SEQUENCE_REGEX = re.compile(r'\x1b[^m]*m') 25 26 27def strip_ansi(text: str): 28 """Strip out ANSI escape sequences.""" 29 return _ANSI_SEQUENCE_REGEX.sub('', text) 30 31 32def split_lines( 33 input_fragments: StyleAndTextTuples) -> List[StyleAndTextTuples]: 34 """Break a flattened list of StyleAndTextTuples into a list of lines. 35 36 Ending line breaks are not preserved.""" 37 lines: List[StyleAndTextTuples] = [] 38 this_line: StyleAndTextTuples = [] 39 for item in input_fragments: 40 if item[1].endswith('\n'): 41 # If there are no elements in this line except for a linebreak add 42 # an empty StyleAndTextTuple so this line isn't an empty list. 43 if len(this_line) == 0 and item[1] == '\n': 44 this_line.append((item[0], item[1][:-1])) 45 lines.append(this_line) 46 this_line = [] 47 else: 48 this_line.append(item) 49 return lines 50 51 52def insert_linebreaks( 53 input_fragments: StyleAndTextTuples, 54 max_line_width: int, 55 truncate_long_lines: bool = True) -> Tuple[StyleAndTextTuples, int]: 56 """Add line breaks at max_line_width if truncate_long_lines is True. 57 58 Returns input_fragments with each character as it's own formatted text 59 tuple.""" 60 fragments: StyleAndTextTuples = [] 61 total_width = 0 62 line_width = 0 63 line_height = 0 64 new_break_inserted = False 65 66 for item in input_fragments: 67 # Check for non-printable fragment; doesn't affect the width. 68 if '[ZeroWidthEscape]' in item[0]: 69 fragments.append(item) 70 continue 71 72 new_item_style = item[0] 73 74 # For each character in the fragment 75 for character in item[1]: 76 # Get the width respecting double width characters 77 width = get_cwidth(character) 78 # Increment counters 79 total_width += width 80 line_width += width 81 # Save this character as it's own fragment 82 if line_width <= max_line_width: 83 if not new_break_inserted or character != '\n': 84 fragments.append((new_item_style, character)) 85 # Was a line break just inserted? 86 if character == '\n': 87 # Increase height 88 line_height += 1 89 new_break_inserted = False 90 91 # Reset width to zero even if we are beyond the max line width. 92 if character == '\n': 93 line_width = 0 94 95 # Are we at the limit for this line? 96 elif line_width == max_line_width: 97 # Insert a new linebreak fragment 98 fragments.append((new_item_style, '\n')) 99 # Increase height 100 line_height += 1 101 # Set a flag for skipping the next character if it is also a 102 # line break. 103 new_break_inserted = True 104 105 if not truncate_long_lines: 106 # Reset line width to zero 107 line_width = 0 108 109 # Check if the string ends in a final line break 110 last_fragment_style = fragments[-1][0] 111 last_fragment_text = fragments[-1][1] 112 if not last_fragment_text.endswith('\n'): 113 # Add a line break if none exists 114 fragments.append((last_fragment_style, '\n')) 115 line_height += 1 116 117 return fragments, line_height 118 119 120def join_adjacent_style_tuples( 121 fragments: StyleAndTextTuples) -> StyleAndTextTuples: 122 """Join adjacent FormattedTextTuples if they have the same style.""" 123 new_fragments: StyleAndTextTuples = [] 124 125 for i, fragment in enumerate(fragments): 126 # Add the first fragment 127 if i == 0: 128 new_fragments.append(fragment) 129 continue 130 131 # Get this style 132 style = fragment[0] 133 # If the previous style matches 134 if style == new_fragments[-1][0]: 135 # Get the previous text 136 new_text = new_fragments[-1][1] 137 # Append this text 138 new_text += fragment[1] 139 # Replace the last fragment 140 new_fragments[-1] = (style, new_text) 141 else: 142 # Styles don't match, just append. 143 new_fragments.append(fragment) 144 145 return new_fragments 146 147 148def fill_character_width(input_fragments: StyleAndTextTuples, 149 fragment_width: int, 150 window_width: int, 151 line_wrapping: bool = False, 152 remaining_width: int = 0, 153 horizontal_scroll_amount: int = 0, 154 add_cursor: bool = False) -> StyleAndTextTuples: 155 """Fill line to the width of the window using spaces.""" 156 # Calculate the number of spaces to add at the end. 157 empty_characters = window_width - fragment_width 158 # If wrapping is on, use remaining_width 159 if line_wrapping and (fragment_width > window_width): 160 empty_characters = remaining_width 161 162 # Add additional spaces for horizontal scrolling. 163 empty_characters += horizontal_scroll_amount 164 165 if empty_characters <= 0: 166 # No additional spaces required 167 return input_fragments 168 169 line_fragments = copy.copy(input_fragments) 170 171 single_space = ('', ' ') 172 line_ends_in_a_break = False 173 # Replace the trailing \n with a space 174 if line_fragments[-1][1] == '\n': 175 line_fragments[-1] = single_space 176 empty_characters -= 1 177 line_ends_in_a_break = True 178 179 # Append remaining spaces 180 for _i in range(empty_characters): 181 line_fragments.append(single_space) 182 183 if line_ends_in_a_break: 184 # Restore the \n 185 line_fragments.append(('', '\n')) 186 187 if add_cursor: 188 # Add a cursor to this line by adding SetCursorPosition fragment. 189 line_fragments_remainder = line_fragments 190 line_fragments = [('[SetCursorPosition]', '')] 191 # Use extend to keep types happy. 192 line_fragments.extend(line_fragments_remainder) 193 194 return line_fragments 195 196 197def flatten_formatted_text_tuples( 198 lines: Iterable[StyleAndTextTuples]) -> StyleAndTextTuples: 199 """Flatten a list of lines of FormattedTextTuples 200 201 This function will also remove trailing newlines to avoid displaying extra 202 empty lines in prompt_toolkit containers. 203 """ 204 fragments: StyleAndTextTuples = [] 205 206 # Return empty list if lines is empty. 207 if not lines: 208 return fragments 209 210 for line_fragments in lines: 211 # Append all FormattedText tuples for this line. 212 for fragment in line_fragments: 213 fragments.append(fragment) 214 215 # Strip off any trailing line breaks 216 last_fragment: OneStyleAndTextTuple = fragments[-1] 217 style = last_fragment[0] 218 text = last_fragment[1].rstrip('\n') 219 fragments[-1] = (style, text) 220 return fragments 221 222 223def remove_formatting(formatted_text: StyleAndTextTuples) -> str: 224 """Throw away style info from prompt_toolkit formatted text tuples.""" 225 return ''.join([formatted_tuple[1] for formatted_tuple in formatted_text]) 226 227 228def get_line_height(text_width, screen_width, prefix_width): 229 """Calculates line height for a string with line wrapping enabled.""" 230 if text_width == 0: 231 return 0 232 233 # If text will fit on the screen without wrapping. 234 if text_width <= screen_width: 235 return 1, screen_width - text_width 236 237 # Assume zero width prefix if it's >= width of the screen. 238 if prefix_width >= screen_width: 239 prefix_width = 0 240 241 # Start with height of 1 row. 242 total_height = 1 243 244 # One screen_width of characters (with no prefix) is displayed first. 245 remaining_width = text_width - screen_width 246 247 # While we have caracters remaining to be displayed 248 while remaining_width > 0: 249 # Add the new indentation prefix 250 remaining_width += prefix_width 251 # Display this line 252 remaining_width -= screen_width 253 # Add a line break 254 total_height += 1 255 256 # Remaining characters is what's left below zero. 257 return (total_height, abs(remaining_width)) 258