-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtextdiagram.py
266 lines (231 loc) · 7.24 KB
/
textdiagram.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/usr/bin/python333
"""
Simple ASCII art
Copyright (C) Arm Ltd. 2024. All rights reserved.
SPDX-License-Identifier: Apache 2.0
Coordinates are the usual math orientation, i.e. (1,1) is north-east of (0,0).
"""
from __future__ import print_function
import os, sys
import atexit # if we hide the cursor, restore it on exit
RED = 1
GREEN = 2
YELLOW = 3
BLUE = 4 # generally too dark to read
MAGENTA = 5
CYAN = 6
_color_map = {
"none": 0, "": 0, "white": 0,
"red": 1,
"green": 2,
"yellow": 3,
"blue": 4,
"magenta": 5,
"cyan": 6
}
_no_color = os.environ.get("NO_COLOR")
_ANSI_RESET = "\x1b[0m"
# Turn an integer into a printable character
_alphameric = " 123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def _color_key(s):
"""
Given a format string:
<color>[*!]
return a single printable character key to encode the color and format.
Space means default color and format.
Color can be specified either by name or by number.
"""
is_intense = False
is_inverted = False
if s is None:
return 0
try:
x = int(s)
except ValueError:
s = s.lower()
while s and s[-1] in "*!":
if s[-1] == "*":
is_intense = True
elif s[-1] == "!":
is_inverted = True
s = s[:-1]
x = _color_map[s]
if _no_color:
x = 0
if is_intense:
x |= 8
if is_inverted:
x |= 16
key = _alphameric[x]
return key
def _key_ansi(c):
"""
Given a format key, return the ANSI escape sequence.
"""
code = _alphameric.index(c) # by construction, it must be an alphameric character
a = []
if code & 8:
code -= 8
a.append("1") # intense
if code & 16:
code -= 16
if False:
a.append("7") # invert
a.append("43") # yellow, make text stand out more
a.append("3" + str(code))
else:
a.append("4" + str(code)) # background color
else:
a.append("3" + str(code))
return "\x1b[" + ";".join(a) + "m"
class TextDiagram():
"""
ASCII art
The diagram is maintained as an array of arrays of characters.
A 'shadow' diagram (itself an instance of TextDiagram) may also be
present containing formatting codes.
"""
def __init__(self, width=80, height=10):
# Width and height are, for the moment, ignored - instead the
# diagram is expanded elastically
self._hid_cursor = False
self.minX = 0
self.clear()
def clear(self):
self.lines = []
self.colors = None
def max_Y(self):
return len(self.lines)
def at(self, x, y, s, color=None):
"""
Put a text string at the selected coordinates.
"""
if x < self.minX:
adj = self.minX - x
spc = " " * adj
for i in range(len(self.lines)):
self.lines[i] = spc + self.lines[i]
self.minX = x
x -= self.minX
if y >= len(self.lines):
for i in range(len(self.lines), y+1):
self.lines.append("")
ln = self.lines[y]
if len(ln) < x:
ln += " " * (x-len(ln))
self.lines[y] = ln[:x] + s + ln[x+len(s):]
if color is not None:
if self.colors is None:
# Create a color map - which is itself a (monochrome) diagram
self.colors = TextDiagram()
self.colors.at(x, y, (_color_key(color) * len(s)))
elif self.colors is not None:
# make sure any previous color at this position doesn't persist
self.colors.at(x, y, " "*len(s))
return self
def peek(self, x, y):
"""
Get the character at a given position.
"""
x -= self.minX
if y >= len(self.lines):
return " "
if x >= len(self.lines[y]):
return " "
return self.lines[y][x]
def cursor_up(self):
"""
Return a string that, after printing the diagram, repositions the cursor to
redraw it.
"""
return "\x1b[%uA" % self.max_Y()
def str_mono(self):
"""
Return the entire diagram as a multi-line string suitable for printing.
"""
return '\n'.join(reversed(self.lines)) + '\n'
def str_color(self, for_file=None, no_color=False, force_color=False, restore=False):
"""
Return the entire diagram as a multi-line string suitable for printing,
on output that might understand ANSI escape codes.
no_color=True - disable ANSI escape codes
force_color=True - forces ANSI escape codes even if output is non-tty
"""
with_colors = True
if self.colors is None:
# No color annotations have yet been defined for this diagram
with_colors = False
elif no_color:
with_colors = False
elif force_color:
with_colors = True
elif for_file is not None:
try:
with_colors = os.isatty(for_file.fileno())
except AttributeError:
# environments like DS Jython without fileno()
with_colors = None
if not with_colors:
s = self.str_mono()
else:
s = ""
for (yy, ln) in enumerate(reversed(self.lines)):
y = self.max_Y() - 1 - yy
ccol = " "
for (x, c) in enumerate(ln):
col = self.colors.peek(x, y)
if col != ccol:
if ccol != " ":
s += _ANSI_RESET
if col != " ":
s += _key_ansi(col)
ccol = col
s += c
if ccol != " ":
s += _ANSI_RESET
s += "\n"
if restore:
s += self.cursor_up()
return s
def hide_cursor(self):
hide_cursor()
self._hid_cursor = True
def show_cursor(self):
show_cursor()
self._hid_cursor = False
def __del__(self):
if self._hid_cursor:
self.show_cursor()
def __str__(self):
return self.str_mono()
def show_cursor(file=None):
if file is None:
file = sys.stdout
print("\x1b[?25h", end="", file=file)
def hide_cursor(file=None):
if file is None:
file = sys.stdout
print("\x1b[?25l", end="", file=file)
atexit.register(show_cursor, file)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="ASCII art diagram")
parser.add_argument("--color", type=str)
opts = parser.parse_args()
cs = ""
for col in _color_map.keys():
for suf in ["","!","*","*!"]:
st = col + suf
cs += _key_ansi(_color_key(st)) + st + _ANSI_RESET
print(cs)
D = TextDiagram()
D.at(10,10,"center",color=opts.color)
D.at(10,20,"N",color=1)
D.at(17,17,"NE",color=2)
D.at(20,10,"E",color=3)
D.at(17,3,"SE",color="BLUE")
D.at(0,10,"W",color=5)
D.at(3,17,"NW",color=6)
D.at(10,0,"S",color=7)
D.at(3,3,"SW",color=8)
print(D.str_color(for_file=sys.stdout))