Skip to content
This repository was archived by the owner on Mar 27, 2021. It is now read-only.

Commit 122aaca

Browse files
authored
Merge pull request #81 from dangle/display
Display
2 parents 37f543f + d64977e commit 122aaca

File tree

9 files changed

+691
-56
lines changed

9 files changed

+691
-56
lines changed

rcli/dispatcher.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
from docopt import docopt
1717
import colorama
18-
import six
1918

2019
from . import ( # noqa: F401 pylint: disable=unused-import
2120
exceptions as exc,
@@ -36,7 +35,7 @@ def main():
3635
If the command is 'help' then print the help message for the subcommand; if
3736
no subcommand is given, print the standard help message.
3837
"""
39-
colorama.init(wrap=six.PY3)
38+
colorama.init(strip=not sys.stdout.isatty())
4039
doc = usage.get_primary_command_usage()
4140
allow_subcommands = "<command>" in doc
4241
args = docopt(

rcli/display.py renamed to rcli/display/__init__.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from colorama import Cursor, Fore, Style
3131
from tqdm import tqdm
3232

33-
from .backports.get_terminal_size import get_terminal_size
33+
from .terminal import cols as _ncols
3434

3535

3636
_LOGGER = logging.getLogger(__name__)
@@ -186,12 +186,3 @@ def run_tasks(header, tasks):
186186
task[1]()
187187
finally:
188188
pbar.update(task[2] if len(task) > 2 else 1)
189-
190-
191-
def _ncols():
192-
"""Get the current number of columns on the terminal.
193-
194-
Returns:
195-
The current number of columns in the terminal or 80 if there is no tty.
196-
"""
197-
return get_terminal_size().columns or 80

rcli/display/box.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import contextlib
2+
import functools
3+
4+
from . import terminal
5+
from .io import AppendIOBase
6+
from .util import remove_invisible_characters, visible_len
7+
from .style import Alignment, Style
8+
9+
10+
class _BoxIO(AppendIOBase):
11+
def __init__(self, box_):
12+
super().__init__()
13+
self._box = box_
14+
self._style = Style.current()
15+
self._sep = remove_invisible_characters(self._box._get_sep())
16+
17+
def write(self, s):
18+
super().write(
19+
f"{self._style if self._is_sep(s) else Style.current()}{s}"
20+
)
21+
22+
def update_line(self, s):
23+
stack = Box._stack
24+
current_style = Style.current()
25+
if self._is_sep(s):
26+
stack = Box._stack[:-1]
27+
current_style = self._style
28+
left = " ".join(f"{box[1]}{box[0]._vertical}" for box in stack)
29+
left += " " if left else ""
30+
return (
31+
functools.reduce(
32+
lambda r, b: self._get_right_append(r, b[0], *b[1]),
33+
zip(range(len(stack) - 1, -1, -1), reversed(stack)),
34+
f"{self._style}{left}{current_style}{s}{self._style}",
35+
)
36+
+ Style.reset
37+
)
38+
39+
def _is_sep(self, s):
40+
cleaned_s = remove_invisible_characters(s)
41+
return (
42+
cleaned_s[:2] == self._sep[:2] and cleaned_s[-2:] == self._sep[-2:]
43+
)
44+
45+
def _get_right_append(self, current, i, box_, style):
46+
num_spaces = (
47+
(box_._size or terminal.cols())
48+
- visible_len(current)
49+
- visible_len(box_._vertical)
50+
- i * 2
51+
)
52+
return f"{current}{style}{' ' * num_spaces}{box_._vertical}"
53+
54+
55+
class Box:
56+
_depth = 0
57+
_stack = []
58+
59+
def __init__(
60+
self,
61+
upper_left="\u250C",
62+
upper_right="\u2510",
63+
lower_left="\u2514",
64+
lower_right="\u2518",
65+
horizontal="\u2500",
66+
vertical="\u2502",
67+
sep_left="\u251C",
68+
sep_horizontal="\u2500",
69+
sep_right="\u2524",
70+
size=None,
71+
header="",
72+
footer="",
73+
align=Alignment.LEFT,
74+
header_align=None,
75+
footer_align=None,
76+
sep_align=None,
77+
header_style=None,
78+
footer_style=None,
79+
sep_style=None,
80+
):
81+
self._upper_left = upper_left
82+
self._upper_right = upper_right
83+
self._lower_left = lower_left
84+
self._lower_right = lower_right
85+
self._horizontal = horizontal
86+
self._vertical = vertical
87+
self._sep_left = sep_left
88+
self._sep_horizontal = sep_horizontal
89+
self._sep_right = sep_right
90+
self._size = size
91+
self._header = header
92+
self._footer = footer
93+
self._header_align = header_align or align
94+
self._footer_align = footer_align or align
95+
self._sep_align = sep_align or align
96+
self._header_style = header_style
97+
self._footer_style = footer_style
98+
self._sep_style = sep_style
99+
100+
def top(self, text="", align=None):
101+
with Style.current():
102+
print(
103+
self._line(
104+
self._horizontal,
105+
self._upper_left,
106+
f"{self._upper_right}{Style.reset}",
107+
self._header_style(text) if self._header_style else text,
108+
align,
109+
),
110+
flush=True,
111+
)
112+
113+
def sep(self, text="", align=None):
114+
print(
115+
self._get_sep(text, align or self._sep_align), sep="", flush=True
116+
)
117+
118+
def bottom(self, text="", align=None):
119+
with Style.current():
120+
print(
121+
self._line(
122+
self._horizontal,
123+
self._lower_left,
124+
f"{self._lower_right}{Style.reset}",
125+
self._footer_style(text) if self._footer_style else text,
126+
align,
127+
),
128+
flush=True,
129+
)
130+
131+
def _line(self, char, start, end, text="", align=None):
132+
size = self._size or terminal.cols()
133+
vislen = visible_len(text)
134+
if vislen:
135+
text = f" {text} "
136+
vislen += 2
137+
width = size - 4 * (Box._depth - 1) - vislen - 4
138+
if align == Alignment.CENTER:
139+
return f"{start}{char}{char * int(width / 2 + .5)}{text}{char * int(width / 2)}{char}{end}"
140+
if align == Alignment.RIGHT:
141+
return f"{start}{char}{char * width}{text}{char}{end}"
142+
return f"{start}{char}{text}{char * width}{char}{end}"
143+
144+
def _create_buffer(self):
145+
return _BoxIO(self)
146+
147+
def _get_sep(self, text="", align=None):
148+
return self._line(
149+
self._sep_horizontal,
150+
self._sep_left,
151+
self._sep_right,
152+
self._sep_style(text) if self._sep_style else text,
153+
align,
154+
)
155+
156+
def __enter__(self):
157+
Box._depth += 1
158+
self.top(self._header, self._header_align)
159+
Box._stack.append((self, Style.current()))
160+
return self
161+
162+
def __exit__(self, *args, **kwargs):
163+
Box._stack.pop()
164+
self.bottom(self._footer, self._footer_align)
165+
Box._depth -= 1
166+
167+
@staticmethod
168+
def new_style(*args, **kwargs):
169+
@contextlib.contextmanager
170+
def inner(**kw):
171+
impl = Box(*args, **kwargs)
172+
if Box._stack:
173+
impl._size = Box._stack[-1][0]._size
174+
if "size" in kw:
175+
impl._size = kw["size"]
176+
impl._header = kw.get("header", "")
177+
impl._header_align = kw.get(
178+
"header_align", kw.get("align", impl._header_align)
179+
)
180+
impl._footer = kw.get("footer", "")
181+
impl._footer_align = kw.get(
182+
"footer_align", kw.get("align", impl._footer_align)
183+
)
184+
impl._sep_align = kw.get(
185+
"sep_align", kw.get("align", impl._sep_align)
186+
)
187+
with impl, contextlib.redirect_stdout(impl._create_buffer()):
188+
yield impl
189+
190+
return inner
191+
192+
193+
Box.simple = Box.new_style()
194+
Box.thick = Box.new_style(
195+
"\u250F",
196+
"\u2513",
197+
"\u2517",
198+
"\u251B",
199+
"\u2501",
200+
"\u2503",
201+
"\u2523",
202+
"\u2501",
203+
"\u252B",
204+
header_style=Style.bold,
205+
footer_style=Style.bold,
206+
sep_style=Style.bold,
207+
)
208+
Box.info = Box.new_style(
209+
"\u250F",
210+
"\u2513",
211+
"\u2517",
212+
"\u251B",
213+
"\u2501",
214+
"\u2503",
215+
"\u2520",
216+
"\u2500",
217+
"\u2528",
218+
)
219+
Box.ascii = Box.new_style("+", "+", "+", "+", "=", "|", "+", "-", "+")
220+
Box.star = Box.new_style("*", "*", "*", "*", "*", "*", "*", "*", "*")
221+
Box.double = Box.new_style(
222+
"\u2554",
223+
"\u2557",
224+
"\u255A",
225+
"\u255D",
226+
"\u2550",
227+
"\u2551",
228+
"\u2560",
229+
"\u2550",
230+
"\u2563",
231+
)
232+
Box.fancy = Box.new_style("\u2552", "\u2555", "\u2558", "\u255B", "\u2550")
233+
Box.round = Box.new_style("\u256D", "\u256E", "\u2570", "\u256F")
234+
235+
box = Box.simple

rcli/display/io.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import io
2+
import sys
3+
4+
from .util import remove_invisible_characters
5+
6+
7+
class AppendIOBase(io.StringIO):
8+
def __init__(self, stdout=sys.stdout):
9+
super().__init__("", None)
10+
self._stdout = stdout
11+
12+
def flush(self):
13+
buffer = self.getvalue()
14+
lines = buffer.split("\n")
15+
nl = "\n".join(
16+
self.update_line(line)
17+
if remove_invisible_characters(line)
18+
else line
19+
for line in lines
20+
)
21+
self._stdout.write(nl)
22+
self.clear_buffer()
23+
self._stdout.flush()
24+
25+
def update_line(self, s):
26+
return s
27+
28+
def clear_buffer(self):
29+
self.truncate(0)
30+
self.seek(0)
31+
32+
def close(self):
33+
self.flush()
34+
super().close()

0 commit comments

Comments
 (0)