ttyengine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ttyengine/region.py ADDED
@@ -0,0 +1,180 @@
1
+ import re
2
+ from collections import namedtuple
3
+
4
+ Region = namedtuple('Region', 'x y width height')
5
+
6
+ _PERCENT = re.compile(r'^\s*(-?\d+(?:\.\d+)?)\s*%\s*$')
7
+ _NUMBER = re.compile(r'^\s*-?\d+(?:\.\d+)?\s*$')
8
+ _ALIGNS = ('start', 'center', 'end')
9
+ _KEYS = {
10
+ 'x': 'x', 'left': 'x', 'y': 'y', 'top': 'y',
11
+ 'width': 'width', 'w': 'width', 'height': 'height', 'h': 'height',
12
+ 'right': 'right', 'bottom': 'bottom',
13
+ }
14
+
15
+
16
+ def _number(value):
17
+ if isinstance(value, bool):
18
+ raise ValueError('region values must be numbers, percentages or alignments, not %r' % (value,))
19
+ if isinstance(value, (int, float)):
20
+ if value != value or value in (float('inf'), float('-inf')):
21
+ raise ValueError('region values must be finite, got %r' % (value,))
22
+ return int(round(value))
23
+ if isinstance(value, str) and _NUMBER.match(value):
24
+ return int(round(float(value)))
25
+ return None
26
+
27
+
28
+ def _clean(value, name, aligned):
29
+ if value is None:
30
+ return None
31
+ if isinstance(value, str):
32
+ text = value.strip().lower()
33
+ if _PERCENT.match(text):
34
+ return text.replace(' ', '')
35
+ if aligned and text in _ALIGNS + ('cursor',):
36
+ return text
37
+ number = _number(value)
38
+ if number is None:
39
+ raise ValueError('invalid region %s: %r' % (name, value))
40
+ return number
41
+
42
+
43
+ def parse(spec):
44
+ if spec is None:
45
+ return Region(0, 0, None, None)
46
+ if isinstance(spec, dict):
47
+ unknown = [k for k in spec if k not in _KEYS]
48
+ if unknown:
49
+ raise ValueError('unknown region key(s): %s' % ', '.join(map(str, unknown)))
50
+ merged = {}
51
+ for key, value in spec.items():
52
+ target = _KEYS[key]
53
+ if target in merged and merged[target] != value:
54
+ raise ValueError('region key %r given twice' % (key,))
55
+ merged[target] = value
56
+ x, y = merged.get('x'), merged.get('y')
57
+ width, height = merged.get('width'), merged.get('height')
58
+ right, bottom = merged.get('right'), merged.get('bottom')
59
+ x, width = _anchor(x, width, right, 'right', 'width')
60
+ y, height = _anchor(y, height, bottom, 'bottom', 'height')
61
+ else:
62
+ try:
63
+ x, y, width, height = spec
64
+ except (TypeError, ValueError):
65
+ raise ValueError('a region is None, a dict or (x, y, width, height), got %r' % (spec,)) from None
66
+ return Region(
67
+ _clean(x, 'x', True) if x is not None else 0,
68
+ _clean(y, 'y', True) if y is not None else 0,
69
+ _clean(width, 'width', False),
70
+ _clean(height, 'height', False),
71
+ )
72
+
73
+
74
+ def _anchor(pos, size, inset, inset_name, size_name):
75
+ if inset is None:
76
+ return pos, size
77
+ inset = _number(inset)
78
+ if inset is None or inset < 0:
79
+ raise ValueError('%s must be a non-negative number of cells' % inset_name)
80
+ if size is None:
81
+ return pos, -inset
82
+ size_cells = _number(size)
83
+ if pos is not None or size_cells is None:
84
+ raise ValueError('%s with %s needs a numeric %s and no explicit start' % (inset_name, size_name, size_name))
85
+ return -(inset + size_cells), size_cells
86
+
87
+
88
+ def _fraction(text, total):
89
+ return int(round(total * float(_PERCENT.match(text).group(1)) / 100.0))
90
+
91
+
92
+ def _axis(pos, size, total, cursor):
93
+ if pos in ('center', 'end'):
94
+ if size is None:
95
+ length = total
96
+ elif isinstance(size, str):
97
+ length = _fraction(size, total)
98
+ elif size < 0:
99
+ length = total + size
100
+ else:
101
+ length = size
102
+ length = max(0, min(length, total))
103
+ start = (total - length) // 2 if pos == 'center' else total - length
104
+ return start, length
105
+ if pos == 'start' or pos is None:
106
+ start = 0
107
+ elif pos == 'cursor':
108
+ start = cursor
109
+ elif isinstance(pos, str):
110
+ start = _fraction(pos, total)
111
+ if start < 0:
112
+ start += total
113
+ else:
114
+ start = pos + total if pos < 0 else pos
115
+ start = max(0, min(start, total))
116
+ room = total - start
117
+ if size is None:
118
+ length = room
119
+ elif isinstance(size, str):
120
+ length = _fraction(size, total)
121
+ elif size < 0:
122
+ length = room + size
123
+ else:
124
+ length = size
125
+ return start, max(0, min(length, room))
126
+
127
+
128
+ def resolve(spec, width, height, cursor=0):
129
+ x, y, w, h = parse(spec)
130
+ rx, rw = _axis(x, w, max(0, int(width)), 0)
131
+ ry, rh = _axis(y, h, max(0, int(height)), cursor)
132
+ return Region(rx, ry, rw, rh)
133
+
134
+
135
+ def is_default(spec):
136
+ return parse(spec) == Region(0, 0, None, None)
137
+
138
+
139
+ def uses_cursor(spec):
140
+ return parse(spec).y == 'cursor'
141
+
142
+
143
+ def full():
144
+ return Region(0, 0, None, None)
145
+
146
+
147
+ def line(row=0):
148
+ return Region(0, row, None, 1)
149
+
150
+
151
+ def column(col=0):
152
+ return Region(col, 0, 1, None)
153
+
154
+
155
+ def top(rows=1):
156
+ return Region(0, 0, None, rows)
157
+
158
+
159
+ def bottom(rows=1):
160
+ return Region(0, -rows, None, rows)
161
+
162
+
163
+ def left(columns=1):
164
+ return Region(0, 0, columns, None)
165
+
166
+
167
+ def right(columns=1):
168
+ return Region(-columns, 0, columns, None)
169
+
170
+
171
+ def box(x, y, width, height):
172
+ return Region(x, y, width, height)
173
+
174
+
175
+ def centered(width, height):
176
+ return Region('center', 'center', width, height)
177
+
178
+
179
+ def inline(rows=1):
180
+ return Region(0, 'cursor', None, rows)