flexablelib-tui 3.3.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.
@@ -0,0 +1,624 @@
1
+ """
2
+ TERMINAL WIDGETS
3
+
4
+ Small renderables for dashboards (``pip install flexablelib[tui]``), built
5
+ to sit in a :class:`~flexablelib.Grid`
6
+ or a :class:`~flexablelib.Card` next to tables:
7
+
8
+ ``ProgressBar`` ``Ventas ███████████▍░░░░░░ 64%``
9
+ ``Sparkline`` ``▁▂▄▆█▇▅▃``
10
+ ``BarChart`` one labelled horizontal bar per value
11
+ ``StatusBar`` a full-width bar with left / centre / right text
12
+ ``KeyHints`` ``q Salir F1 Ayuda`` key chips
13
+ ``LogView`` the tail of a log, levels coloured
14
+ ``Text`` aligned, wrapped, coloured text
15
+ ``Rule`` a horizontal rule with an optional title
16
+
17
+ Every widget has ``render(width=None, height=None)`` (``None`` width: the
18
+ terminal's), ``natural_width()`` and ``str(widget)``. Widths count visible
19
+ columns: ANSI escapes are zero wide, emoji and CJK two. Colour follows
20
+ ``use_colors``: ``None`` means only on a terminal, honouring ``NO_COLOR`` and
21
+ ``FORCE_COLOR``.
22
+ """
23
+
24
+ import builtins
25
+ import re
26
+
27
+ from flexablelib.colors import RESET, parse_spec, supports_color
28
+ from flexablelib.layout import _align, _fit_line, render_width
29
+ from flexablelib import _frames as _fx
30
+ from flexablelib.text_width import (
31
+ pad_to_width,
32
+ strip_ansi,
33
+ wrap_to_width,
34
+ )
35
+
36
+ __all__ = ['ProgressBar', 'Sparkline', 'BarChart', 'StatusBar', 'KeyHints',
37
+ 'LogView', 'Text', 'Rule']
38
+
39
+ _EIGHTHS = ' ▏▎▍▌▋▊▉'
40
+ _SPARKS = '▁▂▃▄▅▆▇█'
41
+
42
+
43
+ class _Widget(object):
44
+ """Shared colour handling and the render protocol."""
45
+
46
+ use_colors = None
47
+
48
+ def _active(self):
49
+ if self.use_colors is None:
50
+ return supports_color()
51
+ return bool(self.use_colors)
52
+
53
+ def _paint(self, text, spec):
54
+ if not text or not spec or not self._active():
55
+ return text
56
+ prefix = parse_spec(spec)
57
+ if not prefix:
58
+ return text
59
+ return prefix + text.replace(RESET, RESET + prefix) + RESET
60
+
61
+ def _width(self, width):
62
+ if width is None:
63
+ width = getattr(self, 'width', None)
64
+ if width is None:
65
+ return min(self.natural_width(), render_width(None))
66
+ return render_width(width)
67
+
68
+ def _finish(self, lines, width, height):
69
+ """Clip or pad to ``height`` lines; strip colour when it is off."""
70
+ if height is not None:
71
+ height = max(0, int(height))
72
+ lines = lines[:height] + [' ' * width] * (height - len(lines))
73
+ text = '\n'.join(lines)
74
+ if not self._active():
75
+ text = strip_ansi(text)
76
+ return text
77
+
78
+ def __str__(self):
79
+ return self.render()
80
+
81
+
82
+ def _check_number(name, value):
83
+ try:
84
+ return float(value)
85
+ except (TypeError, ValueError):
86
+ raise TypeError('{0} must be a number, got {1!r}'.format(name, value))
87
+
88
+
89
+ def _threshold_color(fraction, color, thresholds):
90
+ """The colour for ``fraction``: the last threshold it reaches."""
91
+ if not thresholds:
92
+ return color
93
+ pairs = sorted(thresholds.items() if isinstance(thresholds, dict)
94
+ else thresholds)
95
+ chosen = color
96
+ for limit, spec in pairs:
97
+ if fraction >= float(limit):
98
+ chosen = spec
99
+ return chosen
100
+
101
+
102
+ class ProgressBar(_Widget):
103
+ """
104
+ A horizontal bar for a value out of a total.
105
+
106
+ >>> print(ProgressBar(64, label='Ventas', use_colors=False).render(30))
107
+ Ventas ██████████████▏░░░░░░░░ 64%
108
+
109
+ ``thresholds`` changes the colour as the bar fills: ``{0.7: 'yellow',
110
+ 0.9: 'red'}``. With the default ``█`` fill the end of the bar is drawn in
111
+ eighths (``▏▎▍▌▋▊▉``) so small changes show. ``value_format`` replaces the
112
+ percentage, e.g. ``'{value:.0f}/{total:.0f} GB'``. ``label_width`` and
113
+ ``value_width`` pad both ends so a stack of bars lines up.
114
+ """
115
+
116
+ def __init__(self, value, total=100, label=None, width=None,
117
+ show_percent=True, value_format=None, color='green',
118
+ empty_color='bright_black', label_color=None,
119
+ thresholds=None, fill_char='█', empty_char='░',
120
+ label_width=None, value_width=None, use_colors=None):
121
+ self.value = _check_number('value', value)
122
+ self.total = _check_number('total', total)
123
+ if self.total <= 0:
124
+ raise ValueError('total must be > 0')
125
+ self.label = label
126
+ self.width = width
127
+ self.show_percent = show_percent
128
+ self.value_format = value_format
129
+ self.color = color
130
+ self.empty_color = empty_color
131
+ self.label_color = label_color
132
+ self.thresholds = thresholds
133
+ if not fill_char or not empty_char:
134
+ raise ValueError('fill_char and empty_char cannot be empty')
135
+ self.fill_char = fill_char
136
+ self.empty_char = empty_char
137
+ self.label_width = label_width
138
+ self.value_width = value_width
139
+ self.use_colors = use_colors
140
+
141
+ @property
142
+ def fraction(self):
143
+ """The filled share, clamped to ``0..1``."""
144
+ return max(0.0, min(1.0, self.value / self.total))
145
+
146
+ def _suffix(self):
147
+ if self.value_format:
148
+ text = self.value_format.format(
149
+ value=self.value, total=self.total,
150
+ percent=self.fraction * 100)
151
+ elif self.show_percent:
152
+ text = '{0:>3.0f}%'.format(self.fraction * 100)
153
+ else:
154
+ text = ''
155
+ if self.value_width is not None:
156
+ text = pad_to_width(text, int(self.value_width), 'r')
157
+ return text
158
+
159
+ def _label(self):
160
+ if self.label is None:
161
+ return ''
162
+ if self.label_width is not None:
163
+ return _fit_line(str(self.label), int(self.label_width))
164
+ return str(self.label)
165
+
166
+ def natural_width(self):
167
+ label, suffix = self._label(), self._suffix()
168
+ return (_fx.visible_width(label) + (1 if label else 0) + 20 +
169
+ (1 + _fx.visible_width(suffix) if suffix else 0))
170
+
171
+ def bar(self, width):
172
+ """Just the bar, ``width`` columns, coloured."""
173
+ width = max(0, int(width))
174
+ cells = self.fraction * width
175
+ full = int(cells)
176
+ rest = cells - full
177
+ partial = ''
178
+ if self.fill_char == '█' and full < width:
179
+ eighth = int(round(rest * 8))
180
+ if eighth == 8:
181
+ full += 1
182
+ elif eighth:
183
+ partial = _EIGHTHS[eighth]
184
+ filled = self.fill_char * full + partial
185
+ empty = self.empty_char * (width - full - (1 if partial else 0))
186
+ color = _threshold_color(self.fraction, self.color, self.thresholds)
187
+ return self._paint(filled, color) + self._paint(empty,
188
+ self.empty_color)
189
+
190
+ def render(self, width=None, height=None):
191
+ width = self._width(width)
192
+ label, suffix = self._label(), self._suffix()
193
+ head = (self._paint(label, self.label_color) + ' ') if label else ''
194
+ tail = (' ' + suffix) if suffix else ''
195
+ room = width - _fx.visible_width(head) - _fx.visible_width(tail)
196
+ if room < 1:
197
+ # Too narrow for label and value: keep the bar.
198
+ head, tail, room = '', '', width
199
+ line = head + self.bar(room) + tail
200
+ return self._finish([line], width, height)
201
+
202
+
203
+ class Sparkline(_Widget):
204
+ """
205
+ A one-line chart of a series: ``▁▂▄▆█▇▅▃``.
206
+
207
+ When the series is longer than the width the most recent values are
208
+ shown. ``low`` / ``high`` fix the scale (default: the data's range).
209
+ ``label`` goes in front and ``show_last`` appends the latest value.
210
+ """
211
+
212
+ def __init__(self, values, width=None, color=None, low=None, high=None,
213
+ label=None, show_last=False, value_format='{0:g}',
214
+ chars=_SPARKS, label_color=None, label_width=None,
215
+ use_colors=None):
216
+ self.values = [_check_number('values', v) for v in values]
217
+ self.width = width
218
+ self.color = color
219
+ self.low = low
220
+ self.high = high
221
+ self.label = label
222
+ self.show_last = show_last
223
+ self.value_format = value_format
224
+ if not chars:
225
+ raise ValueError('chars cannot be empty')
226
+ self.chars = chars
227
+ self.label_color = label_color
228
+ self.label_width = label_width
229
+ self.use_colors = use_colors
230
+
231
+ def _parts(self):
232
+ label = '' if self.label is None else str(self.label)
233
+ if label and self.label_width is not None:
234
+ label = _fit_line(label, int(self.label_width))
235
+ head = (label + ' ') if label else ''
236
+ tail = ''
237
+ if self.show_last and self.values:
238
+ tail = ' ' + self.value_format.format(self.values[-1])
239
+ return head, tail
240
+
241
+ def natural_width(self):
242
+ head, tail = self._parts()
243
+ return _fx.visible_width(head) + max(1, len(self.values)) + \
244
+ _fx.visible_width(tail)
245
+
246
+ def line(self, width):
247
+ """Just the chart, at most ``width`` columns."""
248
+ values = self.values[-width:] if width > 0 else []
249
+ if not values:
250
+ return ''
251
+ low = min(values) if self.low is None else float(self.low)
252
+ high = max(values) if self.high is None else float(self.high)
253
+ steps = len(self.chars) - 1
254
+ out = []
255
+ for v in values:
256
+ if high == low:
257
+ index = steps // 2 if high else 0
258
+ else:
259
+ index = int(round((min(max(v, low), high) - low) /
260
+ (high - low) * steps))
261
+ out.append(self.chars[index])
262
+ return self._paint(''.join(out), self.color)
263
+
264
+ def render(self, width=None, height=None):
265
+ width = self._width(width)
266
+ head, tail = self._parts()
267
+ room = width - _fx.visible_width(head) - _fx.visible_width(tail)
268
+ if room < 1:
269
+ head, tail, room = '', '', width
270
+ line = self._paint(head, self.label_color) + self.line(room) + tail
271
+ return self._finish([line], width, height)
272
+
273
+
274
+ class BarChart(_Widget):
275
+ """
276
+ Labelled horizontal bars, one per value.
277
+
278
+ >>> print(BarChart({'Norte': 120, 'Sur': 80}, use_colors=False).render(30))
279
+ Norte ███████████████████ 120
280
+ Sur ████████████▋ 80
281
+
282
+ ``colors`` is one spec or ``{label: spec}``; ``sort=True`` puts the
283
+ largest first; ``max`` fixes the scale.
284
+ """
285
+
286
+ def __init__(self, data, width=None, color='cyan', colors=None,
287
+ show_values=True, value_format='{0:,.0f}', sort=False,
288
+ max=None, label_color=None, fill_char='█', use_colors=None):
289
+ if isinstance(data, dict):
290
+ pairs = list(data.items())
291
+ else:
292
+ pairs = [tuple(item) for item in data]
293
+ self.data = [(str(k), _check_number('value', v)) for k, v in pairs]
294
+ if any(v < 0 for _, v in self.data):
295
+ raise ValueError('BarChart values must be >= 0')
296
+ self.width = width
297
+ self.color = color
298
+ self.colors = colors or {}
299
+ self.show_values = show_values
300
+ self.value_format = value_format
301
+ self.sort = sort
302
+ self.max = max
303
+ self.label_color = label_color
304
+ self.fill_char = fill_char
305
+ self.use_colors = use_colors
306
+
307
+ def _rows(self):
308
+ rows = list(self.data)
309
+ if self.sort:
310
+ rows.sort(key=lambda kv: kv[1], reverse=True)
311
+ return rows
312
+
313
+ def natural_width(self):
314
+ rows = self._rows()
315
+ if not rows:
316
+ return 1
317
+ label_w = max(_fx.visible_width(k) for k, _ in rows)
318
+ value_w = max(_fx.visible_width(self.value_format.format(v))
319
+ for _, v in rows) if self.show_values else 0
320
+ return label_w + 1 + 24 + (1 + value_w if value_w else 0)
321
+
322
+ def render(self, width=None, height=None):
323
+ width = self._width(width)
324
+ rows = self._rows()
325
+ if not rows:
326
+ return self._finish([''], width, height)
327
+ label_w = min(max(_fx.visible_width(k) for k, _ in rows),
328
+ max(1, width // 3))
329
+ values = [self.value_format.format(v) for _, v in rows]
330
+ value_w = max(_fx.visible_width(v) for v in values) \
331
+ if self.show_values else 0
332
+ bar_w = width - label_w - 1 - (value_w + 1 if value_w else 0)
333
+ if bar_w < 1:
334
+ bar_w, value_w = max(1, width - label_w - 1), 0
335
+ top = float(self.max) if self.max is not None else \
336
+ builtins.max(v for _, v in rows)
337
+ lines = []
338
+ for (label, value), shown in zip(rows, values):
339
+ spec = self.colors.get(label, self.color) \
340
+ if isinstance(self.colors, dict) else self.colors
341
+ bar = ProgressBar(min(value, top) if top else 0, top or 1,
342
+ show_percent=False, color=spec,
343
+ fill_char=self.fill_char, empty_char=' ',
344
+ empty_color=None, use_colors=self._active())
345
+ line = (self._paint(_fit_line(label, label_w), self.label_color) +
346
+ ' ' + bar.bar(bar_w))
347
+ if value_w:
348
+ line += ' ' + pad_to_width(shown, value_w, 'r')
349
+ lines.append(line)
350
+ return self._finish(lines, width, height)
351
+
352
+
353
+ class StatusBar(_Widget):
354
+ """
355
+ One full-width line with ``left``, ``center`` and ``right`` text on a
356
+ background, like an editor's status line.
357
+
358
+ When the parts do not fit, the centre goes first, then the left part is
359
+ cut with ``…``.
360
+ """
361
+
362
+ def __init__(self, left='', center='', right='', color='black on cyan',
363
+ width=None, padding=1, use_colors=None):
364
+ self.left = str(left or '')
365
+ self.center = str(center or '')
366
+ self.right = str(right or '')
367
+ self.color = color
368
+ self.width = width
369
+ self.padding = max(0, int(padding))
370
+ self.use_colors = use_colors
371
+
372
+ def natural_width(self):
373
+ parts = [p for p in (self.left, self.center, self.right) if p]
374
+ return sum(_fx.visible_width(p) for p in parts) + \
375
+ 2 * max(0, len(parts) - 1) + 2 * self.padding
376
+
377
+ def render(self, width=None, height=None):
378
+ width = self._width(width)
379
+ pad = ' ' * min(self.padding, max(0, (width - 1) // 2))
380
+ inner = width - 2 * len(pad)
381
+ left, center, right = self.left, self.center, self.right
382
+ lw, cw, rw = (_fx.visible_width(left), _fx.visible_width(center),
383
+ _fx.visible_width(right))
384
+ if lw + cw + rw + (2 if center else 0) + (2 if left and right else 0)\
385
+ > inner:
386
+ center, cw = '', 0
387
+ if lw + rw + (1 if left and right else 0) > inner:
388
+ rw = min(rw, inner)
389
+ right = _fx.truncate(right, rw, '…') if rw < \
390
+ _fx.visible_width(right) else right
391
+ lw = max(0, inner - rw - (1 if rw else 0))
392
+ left = _fx.truncate(left, lw, '…') if lw else ''
393
+ lw = _fx.visible_width(left)
394
+ body = left
395
+ if center:
396
+ start = max(lw + 1, (inner - cw) // 2)
397
+ start = min(start, inner - rw - 1 - cw) if rw else start
398
+ body = body + ' ' * (start - lw) + center
399
+ used = _fx.visible_width(body)
400
+ body = body + ' ' * max(0, inner - used - rw) + right
401
+ body = _fit_line(body, inner)
402
+ text = pad + body + pad
403
+ return self._finish([self._paint(text, self.color)], width, height)
404
+
405
+
406
+ class KeyHints(_Widget):
407
+ """
408
+ Keyboard shortcuts as chips: ``q Salir F1 Ayuda / Buscar``.
409
+
410
+ ``keys`` is a dict or ``(key, action)`` pairs. Hints wrap onto more lines
411
+ when they do not fit.
412
+ """
413
+
414
+ def __init__(self, keys, key_color='black on white', label_color=None,
415
+ separator=' ', width=None, align='l', use_colors=None):
416
+ pairs = list(keys.items()) if isinstance(keys, dict) else \
417
+ [tuple(k) for k in keys]
418
+ self.keys = [(str(k), str(v)) for k, v in pairs]
419
+ self.key_color = key_color
420
+ self.label_color = label_color
421
+ self.separator = separator
422
+ self.width = width
423
+ self.align = _align(align)
424
+ self.use_colors = use_colors
425
+
426
+ def _chips(self):
427
+ return [(' {0} '.format(k), ' ' + v) for k, v in self.keys]
428
+
429
+ def natural_width(self):
430
+ chips = self._chips()
431
+ return sum(_fx.visible_width(k) + _fx.visible_width(v) for k, v in chips) + \
432
+ _fx.visible_width(self.separator) * max(0, len(chips) - 1)
433
+
434
+ def render(self, width=None, height=None):
435
+ width = self._width(width)
436
+ lines, current, used = [], [], 0
437
+ sep_w = _fx.visible_width(self.separator)
438
+ for key, label in self._chips():
439
+ chip_w = _fx.visible_width(key) + _fx.visible_width(label)
440
+ if chip_w > width:
441
+ key = _fx.truncate(key, width, '')
442
+ label = _fx.truncate(label, max(0, width -
443
+ _fx.visible_width(key)), '…')
444
+ chip_w = _fx.visible_width(key) + _fx.visible_width(label)
445
+ extra = chip_w + (sep_w if current else 0)
446
+ if current and used + extra > width:
447
+ lines.append((current, used))
448
+ current, used = [], 0
449
+ extra = chip_w
450
+ current.append(self._paint(key, self.key_color) +
451
+ self._paint(label, self.label_color))
452
+ used += extra
453
+ if current:
454
+ lines.append((current, used))
455
+ out = []
456
+ for chips, used in lines:
457
+ out.append(pad_to_width(self.separator.join(chips), width,
458
+ self.align))
459
+ return self._finish(out, width, height)
460
+
461
+
462
+ _LEVELS = (
463
+ (re.compile(r'\b(CRITICAL|FATAL|ERROR|ERR)\b'), 'bold red'),
464
+ (re.compile(r'\b(WARNING|WARN)\b'), 'yellow'),
465
+ (re.compile(r'\b(INFO)\b'), 'cyan'),
466
+ (re.compile(r'\b(DEBUG|TRACE)\b'), 'bright_black'),
467
+ (re.compile(r'\b(OK|SUCCESS|DONE)\b'), 'green'),
468
+ )
469
+
470
+
471
+ class LogView(_Widget):
472
+ """
473
+ The last lines of a log, sized to its slot.
474
+
475
+ Level words (``ERROR``, ``WARN``, ``INFO``, ``DEBUG``, ``OK``) are
476
+ coloured; ``level_colors`` overrides them as ``{word: spec}``. Long lines
477
+ are cut with ``…`` unless ``wrap=True``. ``height`` (or the slot's height
478
+ in a grid with a height) is how many lines to show -- the newest ones,
479
+ unless ``tail=False``.
480
+ """
481
+
482
+ def __init__(self, lines=None, height=None, width=None, wrap=False,
483
+ tail=True, level_colors=None, color_lines=False,
484
+ max_lines=1000, use_colors=None):
485
+ self.max_lines = max(1, int(max_lines))
486
+ self.lines = []
487
+ for line in lines or []:
488
+ self.append(line)
489
+ self.height = height
490
+ self.width = width
491
+ self.wrap = wrap
492
+ self.tail = tail
493
+ self.level_colors = level_colors
494
+ self.color_lines = color_lines
495
+ self.use_colors = use_colors
496
+
497
+ def append(self, line):
498
+ """Add one line (or several, split on newlines); returns ``self``."""
499
+ for part in str(line).split('\n'):
500
+ self.lines.append(part)
501
+ del self.lines[:-self.max_lines]
502
+ return self
503
+
504
+ def natural_width(self):
505
+ return max([_fx.visible_width(l) for l in self.lines] or [1])
506
+
507
+ def _spec_for(self, word):
508
+ word = word.upper()
509
+ if self.level_colors:
510
+ for key, spec in self.level_colors.items():
511
+ if key.upper() == word:
512
+ return spec
513
+ for pattern, spec in _LEVELS:
514
+ if pattern.fullmatch(word):
515
+ return spec
516
+ return None
517
+
518
+ def _colour(self, line):
519
+ if not self._active() or '\x1b' in line:
520
+ return line
521
+ words = [p.pattern for p, _ in _LEVELS]
522
+ if self.level_colors:
523
+ words.insert(0, r'(?i:\b(' + '|'.join(
524
+ re.escape(k) for k in self.level_colors) + r')\b)')
525
+ pattern = re.compile('|'.join(words))
526
+ match = pattern.search(line)
527
+ if not match:
528
+ return line
529
+ spec = self._spec_for(match.group(0))
530
+ if self.color_lines:
531
+ return self._paint(line, spec)
532
+ return (line[:match.start()] + self._paint(match.group(0), spec) +
533
+ line[match.end():])
534
+
535
+ def render(self, width=None, height=None):
536
+ width = self._width(width)
537
+ if height is None:
538
+ height = self.height
539
+ rows = []
540
+ for line in self.lines:
541
+ if self.wrap:
542
+ rows.extend(wrap_to_width(line, width) or [''])
543
+ elif _fx.visible_width(line) > width:
544
+ rows.append(_fx.truncate(line, width, '…'))
545
+ else:
546
+ rows.append(line)
547
+ if height is not None:
548
+ height = int(height)
549
+ rows = rows[-height:] if self.tail else rows[:height]
550
+ if height == 0:
551
+ rows = []
552
+ out = [pad_to_width(self._colour(r), width) for r in rows] or \
553
+ [' ' * width]
554
+ return self._finish(out, width, height)
555
+
556
+
557
+ class Text(_Widget):
558
+ """Text that wraps (or is cut with ``…``), aligned and coloured."""
559
+
560
+ def __init__(self, text, align='l', color=None, wrap=True, width=None,
561
+ use_colors=None):
562
+ self.text = str(text)
563
+ self.align = _align(align)
564
+ self.color = color
565
+ self.wrap = wrap
566
+ self.width = width
567
+ self.use_colors = use_colors
568
+
569
+ def natural_width(self):
570
+ return max([_fx.visible_width(l) for l in self.text.split('\n')] or [0])
571
+
572
+ def render(self, width=None, height=None):
573
+ width = self._width(width)
574
+ out = []
575
+ for raw in self.text.split('\n'):
576
+ if self.wrap:
577
+ pieces = wrap_to_width(raw, width) if raw else ['']
578
+ elif _fx.visible_width(raw) > width:
579
+ pieces = [_fx.truncate(raw, width, '…')]
580
+ else:
581
+ pieces = [raw]
582
+ for piece in pieces:
583
+ out.append(pad_to_width(self._paint(piece, self.color), width,
584
+ self.align))
585
+ return self._finish(out, width, height)
586
+
587
+
588
+ class Rule(_Widget):
589
+ """A full-width horizontal rule, optionally titled: ``── Ventas ──``."""
590
+
591
+ def __init__(self, title=None, char='─', align='c', color='bright_black',
592
+ title_color='bold', width=None, use_colors=None):
593
+ if not char:
594
+ raise ValueError('char cannot be empty')
595
+ self.title = None if title is None else str(title)
596
+ self.char = char
597
+ self.align = _align(align, 'c')
598
+ self.color = color
599
+ self.title_color = title_color
600
+ self.width = width
601
+ self.use_colors = use_colors
602
+
603
+ def natural_width(self):
604
+ return (_fx.visible_width(self.title) + 6) if self.title else 1
605
+
606
+ def render(self, width=None, height=None):
607
+ if width is None and self.width is None:
608
+ width = render_width(None)
609
+ width = self._width(width)
610
+ if not self.title or _fx.visible_width(self.title) + 4 > width:
611
+ line = self._paint((self.char * width)[:width], self.color)
612
+ return self._finish([line], width, height)
613
+ label = ' ' + self.title + ' '
614
+ free = width - _fx.visible_width(label)
615
+ if self.align == 'l':
616
+ left = 2
617
+ elif self.align == 'r':
618
+ left = free - 2
619
+ else:
620
+ left = free // 2
621
+ line = (self._paint(self.char * left, self.color) +
622
+ self._paint(label, self.title_color) +
623
+ self._paint(self.char * (free - left), self.color))
624
+ return self._finish([line], width, height)
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: flexablelib-tui
3
+ Version: 3.3.0
4
+ Summary: Terminal interfaces for flexablelib: widgets, full-screen layout, diff redraw, keyboard input, focus and key bindings.
5
+ Home-page: https://github.com/Opus-Perpetuus/flexablelib
6
+ Author: Benjamin Ramirez
7
+ Author-email: chilerito12@gmail.com
8
+ License: MIT
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Environment :: Console :: Curses
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Terminals
16
+ Classifier: Topic :: Software Development :: User Interfaces
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: flexablelib>=3.3.0
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license
28
+ Dynamic: license-file
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # flexablelib-tui
34
+
35
+ Terminal interfaces for [flexablelib](https://github.com/Opus-Perpetuus/flexablelib): widgets, full-screen layout, in-place redraw that writes only the lines that changed, keyboard input on Linux, macOS and Windows, focus, key bindings, scrolling, dialogs and resize.
36
+
37
+ Install it with `pip install flexablelib[tui]`.
38
+
39
+ Documentation: [LAYOUT.md](https://github.com/Opus-Perpetuus/flexablelib/blob/main/docs/LAYOUT.md) and [COMPONENTS.md](https://github.com/Opus-Perpetuus/flexablelib/blob/main/docs/COMPONENTS.md)
40
+
41
+ ![An App driven by the keyboard](https://raw.githubusercontent.com/Opus-Perpetuus/flexablelib/main/docs/img/v33/04_teclado_foco.png)
@@ -0,0 +1,12 @@
1
+ flexablelib_tui/__init__.py,sha256=msYP3_dW0d2RDZ8YPCTPHbMYVikk0Z5IoflvUr5mIj0,1262
2
+ flexablelib_tui/app.py,sha256=C2TKo8c35_M2Aeh6APWtxiCIOeVNc3_phPaWGpFOQFA,23475
3
+ flexablelib_tui/keys.py,sha256=Wto8tL0loFH_8w9zUXFgpnxXxL_pQIRSHPECQE7mPLQ,11246
4
+ flexablelib_tui/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ flexablelib_tui/screen.py,sha256=Vr2QHCId-4oCWcYWtCrPHBfccznrPdINptGuMFlawo0,9766
6
+ flexablelib_tui/terminal.py,sha256=oAWDzILwAqufJKFgW1c3OAkF9qHQknki1Qruqm9Sm_g,7818
7
+ flexablelib_tui/widgets.py,sha256=PJeccvPRWDSsWGjFTy5tTsVioSVbCHnht8bqWF6AMLs,23845
8
+ flexablelib_tui-3.3.0.dist-info/licenses/LICENSE,sha256=TsktDCE3E8N8WasPUHkR5Azor67EyerUJdk54L4wavM,1077
9
+ flexablelib_tui-3.3.0.dist-info/METADATA,sha256=zf1atV4sHoZwjxs30x7_VP0zBgaKMqXsxsleaZd4oJs,1714
10
+ flexablelib_tui-3.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
11
+ flexablelib_tui-3.3.0.dist-info/top_level.txt,sha256=gru30mHT_bpNToyvsy3Qsw6tStNIoaQxVuPxh9o1Pkg,16
12
+ flexablelib_tui-3.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+