1from contextlib import contextmanager 2 3from ._compat import term_len 4from .parser import split_opt 5from .termui import get_terminal_size 6 7# Can force a width. This is used by the test system 8FORCED_WIDTH = None 9 10 11def measure_table(rows): 12 widths = {} 13 for row in rows: 14 for idx, col in enumerate(row): 15 widths[idx] = max(widths.get(idx, 0), term_len(col)) 16 return tuple(y for x, y in sorted(widths.items())) 17 18 19def iter_rows(rows, col_count): 20 for row in rows: 21 row = tuple(row) 22 yield row + ("",) * (col_count - len(row)) 23 24 25def wrap_text( 26 text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False 27): 28 """A helper function that intelligently wraps text. By default, it 29 assumes that it operates on a single paragraph of text but if the 30 `preserve_paragraphs` parameter is provided it will intelligently 31 handle paragraphs (defined by two empty lines). 32 33 If paragraphs are handled, a paragraph can be prefixed with an empty 34 line containing the ``\\b`` character (``\\x08``) to indicate that 35 no rewrapping should happen in that block. 36 37 :param text: the text that should be rewrapped. 38 :param width: the maximum width for the text. 39 :param initial_indent: the initial indent that should be placed on the 40 first line as a string. 41 :param subsequent_indent: the indent string that should be placed on 42 each consecutive line. 43 :param preserve_paragraphs: if this flag is set then the wrapping will 44 intelligently handle paragraphs. 45 """ 46 from ._textwrap import TextWrapper 47 48 text = text.expandtabs() 49 wrapper = TextWrapper( 50 width, 51 initial_indent=initial_indent, 52 subsequent_indent=subsequent_indent, 53 replace_whitespace=False, 54 ) 55 if not preserve_paragraphs: 56 return wrapper.fill(text) 57 58 p = [] 59 buf = [] 60 indent = None 61 62 def _flush_par(): 63 if not buf: 64 return 65 if buf[0].strip() == "\b": 66 p.append((indent or 0, True, "\n".join(buf[1:]))) 67 else: 68 p.append((indent or 0, False, " ".join(buf))) 69 del buf[:] 70 71 for line in text.splitlines(): 72 if not line: 73 _flush_par() 74 indent = None 75 else: 76 if indent is None: 77 orig_len = term_len(line) 78 line = line.lstrip() 79 indent = orig_len - term_len(line) 80 buf.append(line) 81 _flush_par() 82 83 rv = [] 84 for indent, raw, text in p: 85 with wrapper.extra_indent(" " * indent): 86 if raw: 87 rv.append(wrapper.indent_only(text)) 88 else: 89 rv.append(wrapper.fill(text)) 90 91 return "\n\n".join(rv) 92 93 94class HelpFormatter(object): 95 """This class helps with formatting text-based help pages. It's 96 usually just needed for very special internal cases, but it's also 97 exposed so that developers can write their own fancy outputs. 98 99 At present, it always writes into memory. 100 101 :param indent_increment: the additional increment for each level. 102 :param width: the width for the text. This defaults to the terminal 103 width clamped to a maximum of 78. 104 """ 105 106 def __init__(self, indent_increment=2, width=None, max_width=None): 107 self.indent_increment = indent_increment 108 if max_width is None: 109 max_width = 80 110 if width is None: 111 width = FORCED_WIDTH 112 if width is None: 113 width = max(min(get_terminal_size()[0], max_width) - 2, 50) 114 self.width = width 115 self.current_indent = 0 116 self.buffer = [] 117 118 def write(self, string): 119 """Writes a unicode string into the internal buffer.""" 120 self.buffer.append(string) 121 122 def indent(self): 123 """Increases the indentation.""" 124 self.current_indent += self.indent_increment 125 126 def dedent(self): 127 """Decreases the indentation.""" 128 self.current_indent -= self.indent_increment 129 130 def write_usage(self, prog, args="", prefix="Usage: "): 131 """Writes a usage line into the buffer. 132 133 :param prog: the program name. 134 :param args: whitespace separated list of arguments. 135 :param prefix: the prefix for the first line. 136 """ 137 usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent) 138 text_width = self.width - self.current_indent 139 140 if text_width >= (term_len(usage_prefix) + 20): 141 # The arguments will fit to the right of the prefix. 142 indent = " " * term_len(usage_prefix) 143 self.write( 144 wrap_text( 145 args, 146 text_width, 147 initial_indent=usage_prefix, 148 subsequent_indent=indent, 149 ) 150 ) 151 else: 152 # The prefix is too long, put the arguments on the next line. 153 self.write(usage_prefix) 154 self.write("\n") 155 indent = " " * (max(self.current_indent, term_len(prefix)) + 4) 156 self.write( 157 wrap_text( 158 args, text_width, initial_indent=indent, subsequent_indent=indent 159 ) 160 ) 161 162 self.write("\n") 163 164 def write_heading(self, heading): 165 """Writes a heading into the buffer.""" 166 self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent)) 167 168 def write_paragraph(self): 169 """Writes a paragraph into the buffer.""" 170 if self.buffer: 171 self.write("\n") 172 173 def write_text(self, text): 174 """Writes re-indented text into the buffer. This rewraps and 175 preserves paragraphs. 176 """ 177 text_width = max(self.width - self.current_indent, 11) 178 indent = " " * self.current_indent 179 self.write( 180 wrap_text( 181 text, 182 text_width, 183 initial_indent=indent, 184 subsequent_indent=indent, 185 preserve_paragraphs=True, 186 ) 187 ) 188 self.write("\n") 189 190 def write_dl(self, rows, col_max=30, col_spacing=2): 191 """Writes a definition list into the buffer. This is how options 192 and commands are usually formatted. 193 194 :param rows: a list of two item tuples for the terms and values. 195 :param col_max: the maximum width of the first column. 196 :param col_spacing: the number of spaces between the first and 197 second column. 198 """ 199 rows = list(rows) 200 widths = measure_table(rows) 201 if len(widths) != 2: 202 raise TypeError("Expected two columns for definition list") 203 204 first_col = min(widths[0], col_max) + col_spacing 205 206 for first, second in iter_rows(rows, len(widths)): 207 self.write("{:>{w}}{}".format("", first, w=self.current_indent)) 208 if not second: 209 self.write("\n") 210 continue 211 if term_len(first) <= first_col - col_spacing: 212 self.write(" " * (first_col - term_len(first))) 213 else: 214 self.write("\n") 215 self.write(" " * (first_col + self.current_indent)) 216 217 text_width = max(self.width - first_col - 2, 10) 218 wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) 219 lines = wrapped_text.splitlines() 220 221 if lines: 222 self.write("{}\n".format(lines[0])) 223 224 for line in lines[1:]: 225 self.write( 226 "{:>{w}}{}\n".format( 227 "", line, w=first_col + self.current_indent 228 ) 229 ) 230 231 if len(lines) > 1: 232 # separate long help from next option 233 self.write("\n") 234 else: 235 self.write("\n") 236 237 @contextmanager 238 def section(self, name): 239 """Helpful context manager that writes a paragraph, a heading, 240 and the indents. 241 242 :param name: the section name that is written as heading. 243 """ 244 self.write_paragraph() 245 self.write_heading(name) 246 self.indent() 247 try: 248 yield 249 finally: 250 self.dedent() 251 252 @contextmanager 253 def indentation(self): 254 """A context manager that increases the indentation.""" 255 self.indent() 256 try: 257 yield 258 finally: 259 self.dedent() 260 261 def getvalue(self): 262 """Returns the buffer contents.""" 263 return "".join(self.buffer) 264 265 266def join_options(options): 267 """Given a list of option strings this joins them in the most appropriate 268 way and returns them in the form ``(formatted_string, 269 any_prefix_is_slash)`` where the second item in the tuple is a flag that 270 indicates if any of the option prefixes was a slash. 271 """ 272 rv = [] 273 any_prefix_is_slash = False 274 for opt in options: 275 prefix = split_opt(opt)[0] 276 if prefix == "/": 277 any_prefix_is_slash = True 278 rv.append((len(prefix), opt)) 279 280 rv.sort(key=lambda x: x[0]) 281 282 rv = ", ".join(x[1] for x in rv) 283 return rv, any_prefix_is_slash 284