• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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