Reduce import scopes, remove cycles.
[python_utils.git] / text_utils.py
1 #!/usr/bin/env python3
2
3 """Utilities for dealing with "text"."""
4
5 from collections import defaultdict
6 import math
7 import sys
8 from typing import List, NamedTuple
9
10 from ansi import fg, reset
11
12
13 class RowsColumns(NamedTuple):
14     rows: int
15     columns: int
16
17
18 def get_console_rows_columns() -> RowsColumns:
19     from exec_utils import cmd
20     rows, columns = cmd("stty size").split()
21     return RowsColumns(int(rows), int(columns))
22
23
24 def progress_graph(
25     current: int,
26     total: int,
27     *,
28     width=70,
29     fgcolor=fg("school bus yellow"),
30     left_end="[",
31     right_end="]",
32     redraw=True,
33 ) -> None:
34     percent = current / total
35     ret = "\r" if redraw else "\n"
36     bar = bar_graph(
37         percent,
38         include_text = True,
39         width = width,
40         fgcolor = fgcolor,
41         left_end = left_end,
42         right_end = right_end)
43     print(
44         bar,
45         end=ret,
46         flush=True,
47         file=sys.stderr)
48
49
50 def bar_graph(
51     percentage: float,
52     *,
53     include_text=True,
54     width=70,
55     fgcolor=fg("school bus yellow"),
56     left_end="[",
57     right_end="]",
58 ) -> None:
59     if percentage < 0.0 or percentage > 1.0:
60         raise ValueError(percentage)
61     if include_text:
62         text = f"{percentage*100.0:2.1f}%"
63     else:
64         text = ""
65     whole_width = math.floor(percentage * width)
66     if whole_width == width:
67         whole_width -= 1
68         part_char = "▉"
69     else:
70         remainder_width = (percentage * width) % 1
71         part_width = math.floor(remainder_width * 8)
72         part_char = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"][part_width]
73     return (
74         left_end +
75         fgcolor +
76         "█" * whole_width + part_char +
77         " " * (width - whole_width - 1) +
78         reset() +
79         right_end + " " +
80         text)
81
82
83 def distribute_strings(
84     strings: List[str],
85     *,
86     width: int = 80,
87     alignment: str = "c",
88     padding: str = " ",
89 ) -> str:
90     subwidth = math.floor(width / len(strings))
91     retval = ""
92     for string in strings:
93         string = justify_string(
94             string, width=subwidth, alignment=alignment, padding=padding
95         )
96         retval += string
97     return retval
98
99
100 def justify_string_by_chunk(
101     string: str, width: int = 80, padding: str = " "
102 ) -> str:
103     padding = padding[0]
104     first, *rest, last = string.split()
105     w = width - (len(first) + 1 + len(last) + 1)
106     retval = (
107         first + padding + distribute_strings(rest, width=w, padding=padding)
108     )
109     while len(retval) + len(last) < width:
110         retval += padding
111     retval += last
112     return retval
113
114
115 def justify_string(
116     string: str, *, width: int = 80, alignment: str = "c", padding: str = " "
117 ) -> str:
118     alignment = alignment[0]
119     padding = padding[0]
120     while len(string) < width:
121         if alignment == "l":
122             string += padding
123         elif alignment == "r":
124             string = padding + string
125         elif alignment == "j":
126             return justify_string_by_chunk(
127                 string,
128                 width=width,
129                 padding=padding
130             )
131         elif alignment == "c":
132             if len(string) % 2 == 0:
133                 string += padding
134             else:
135                 string = padding + string
136         else:
137             raise ValueError
138     return string
139
140
141 def justify_text(text: str, *, width: int = 80, alignment: str = "c") -> str:
142     print("-" * width)
143     retval = ""
144     line = ""
145     for word in text.split():
146         if len(line) + len(word) > width:
147             line = line[1:]
148             line = justify_string(line, width=width, alignment=alignment)
149             retval = retval + "\n" + line
150             line = ""
151         line = line + " " + word
152     if len(line) > 0:
153         retval += "\n" + line[1:]
154     return retval[1:]
155
156
157 def generate_padded_columns(text: List[str]) -> str:
158     max_width = defaultdict(int)
159     for line in text:
160         for pos, word in enumerate(line.split()):
161             max_width[pos] = max(max_width[pos], len(word))
162
163     for line in text:
164         out = ""
165         for pos, word in enumerate(line.split()):
166             width = max_width[pos]
167             word = justify_string(word, width=width, alignment='l')
168             out += f'{word} '
169         yield out