CharmPkk 0.4.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.
- charmpkk/__init__.py +12 -0
- charmpkk/console/__init__.py +95 -0
- charmpkk/console/anim.py +120 -0
- charmpkk/console/banner.py +80 -0
- charmpkk/console/boxes.py +116 -0
- charmpkk/console/charts.py +95 -0
- charmpkk/console/colors.py +182 -0
- charmpkk/console/effects.py +166 -0
- charmpkk/console/forms.py +96 -0
- charmpkk/console/layout.py +77 -0
- charmpkk/console/live.py +67 -0
- charmpkk/console/markdown.py +92 -0
- charmpkk/console/menu.py +219 -0
- charmpkk/console/particles.py +426 -0
- charmpkk/console/progress_ext.py +187 -0
- charmpkk/console/terminal.py +127 -0
- charmpkk/console/text.py +72 -0
- charmpkk/console/themes.py +75 -0
- charmpkk/console/widgets.py +470 -0
- charmpkk/console/wizard.py +53 -0
- charmpkk/version.py +1 -0
- charmpkk/window/__init__.py +84 -0
- charmpkk/window/anim.py +233 -0
- charmpkk/window/app.py +125 -0
- charmpkk/window/command_palette.py +140 -0
- charmpkk/window/custom_titlebar.py +156 -0
- charmpkk/window/dialogs.py +119 -0
- charmpkk/window/effects.py +280 -0
- charmpkk/window/glass.py +290 -0
- charmpkk/window/particles.py +345 -0
- charmpkk/window/screens.py +64 -0
- charmpkk/window/shortcuts.py +63 -0
- charmpkk/window/table_view.py +142 -0
- charmpkk/window/theme.py +155 -0
- charmpkk/window/toast_manager.py +129 -0
- charmpkk/window/widgets.py +838 -0
- charmpkk-0.4.0.dist-info/METADATA +77 -0
- charmpkk-0.4.0.dist-info/RECORD +41 -0
- charmpkk-0.4.0.dist-info/WHEEL +5 -0
- charmpkk-0.4.0.dist-info/licenses/LICENSE +0 -0
- charmpkk-0.4.0.dist-info/top_level.txt +1 -0
charmpkk/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""CharmPkk — красивые интерфейсы для Python: консоль и окно."""
|
|
2
|
+
from .version import __version__
|
|
3
|
+
from . import console
|
|
4
|
+
|
|
5
|
+
__all__ = ["console", "__version__"]
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from . import window # noqa: F401
|
|
9
|
+
__all__.append("window")
|
|
10
|
+
except Exception:
|
|
11
|
+
# tkinter может отсутствовать (например, headless-сервер)
|
|
12
|
+
pass
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from . import colors, text, boxes, layout, widgets, charts, menu
|
|
2
|
+
from . import anim, particles, effects, live as _live_mod
|
|
3
|
+
from . import terminal as terminal_mod
|
|
4
|
+
from . import themes as console_themes
|
|
5
|
+
from . import banner as banner_mod
|
|
6
|
+
from . import forms as forms_mod
|
|
7
|
+
from . import wizard as wizard_mod
|
|
8
|
+
from . import markdown as markdown_mod
|
|
9
|
+
from . import progress_ext
|
|
10
|
+
from .colors import (
|
|
11
|
+
colorize, fg_rgb, bg_rgb, fg_hex, bg_hex, fg_256, bg_256,
|
|
12
|
+
gradient_text, rainbow_text, Style,
|
|
13
|
+
enable, disable, is_enabled, strip_ansi,
|
|
14
|
+
BOLD, DIM, ITALIC, UNDERLINE, BLINK, REVERSE, HIDDEN, STRIKE, RESET,
|
|
15
|
+
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE,
|
|
16
|
+
BRIGHT_BLACK, BRIGHT_RED, BRIGHT_GREEN, BRIGHT_YELLOW,
|
|
17
|
+
BRIGHT_BLUE, BRIGHT_MAGENTA, BRIGHT_CYAN, BRIGHT_WHITE,
|
|
18
|
+
)
|
|
19
|
+
from .text import wrap, align, center, pad, truncate, typewriter, indent, visible_len
|
|
20
|
+
from .boxes import box, rule, section, panel, banner, STYLES
|
|
21
|
+
from .layout import columns, rows, grid, sidebar
|
|
22
|
+
from .widgets import (
|
|
23
|
+
ProgressBar, MultiProgress, Spinner, Table,
|
|
24
|
+
bullet_list, numbered_list, key_value, badge, statusline, tree,
|
|
25
|
+
notify, info, success, warning, error, Log,
|
|
26
|
+
ask, ask_password, ask_number, confirm, ask_choice,
|
|
27
|
+
)
|
|
28
|
+
from .charts import sparkline, bar_chart, gauge, histogram, heatmap
|
|
29
|
+
from .menu import select, multiselect, tabs
|
|
30
|
+
|
|
31
|
+
from .anim import (
|
|
32
|
+
ease, tween, lerp, lerp_color, Transition, EASINGS,
|
|
33
|
+
ease_in_quad, ease_out_quad, ease_in_out_quad,
|
|
34
|
+
ease_in_cubic, ease_out_cubic, ease_in_out_cubic,
|
|
35
|
+
ease_in_quart, ease_out_quart, ease_in_out_sine,
|
|
36
|
+
ease_in_expo, ease_out_expo,
|
|
37
|
+
ease_out_back, ease_out_elastic, ease_out_bounce,
|
|
38
|
+
)
|
|
39
|
+
from .particles import (
|
|
40
|
+
Canvas as ParticleCanvas, ParticleSystem,
|
|
41
|
+
Rain, Snow, Stars, Dust, Matrix, Bubbles, Fireflies,
|
|
42
|
+
play as play_particles, play_preset, PRESETS as PARTICLE_PRESETS,
|
|
43
|
+
)
|
|
44
|
+
from .effects import (
|
|
45
|
+
typewriter as typewriter_fx, fade_in, wave, shimmer,
|
|
46
|
+
glitch, marquee, neon,
|
|
47
|
+
)
|
|
48
|
+
from .live import Live, animate_to
|
|
49
|
+
|
|
50
|
+
from .terminal import Terminal, terminal
|
|
51
|
+
from .themes import Palette, REGISTRY as THEMES, get as get_theme, list_names as list_themes
|
|
52
|
+
from .banner import big as big_text
|
|
53
|
+
from .forms import Form, Field
|
|
54
|
+
from .wizard import Wizard, Step
|
|
55
|
+
from .markdown import render as render_markdown, print_md
|
|
56
|
+
from .progress_ext import FancyProgressBar, AnimatedProgressBar, TaskList
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"colors", "colorize", "fg_rgb", "bg_rgb", "fg_hex", "bg_hex", "fg_256", "bg_256",
|
|
60
|
+
"gradient_text", "rainbow_text", "Style", "enable", "disable", "is_enabled",
|
|
61
|
+
"strip_ansi",
|
|
62
|
+
"BOLD", "DIM", "ITALIC", "UNDERLINE", "BLINK", "REVERSE", "HIDDEN", "STRIKE", "RESET",
|
|
63
|
+
"BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE",
|
|
64
|
+
"BRIGHT_BLACK", "BRIGHT_RED", "BRIGHT_GREEN", "BRIGHT_YELLOW",
|
|
65
|
+
"BRIGHT_BLUE", "BRIGHT_MAGENTA", "BRIGHT_CYAN", "BRIGHT_WHITE",
|
|
66
|
+
"text", "wrap", "align", "center", "pad", "truncate", "typewriter", "indent", "visible_len",
|
|
67
|
+
"boxes", "box", "rule", "section", "panel", "banner", "STYLES",
|
|
68
|
+
"layout", "columns", "rows", "grid", "sidebar",
|
|
69
|
+
"widgets", "ProgressBar", "MultiProgress", "Spinner", "Table",
|
|
70
|
+
"bullet_list", "numbered_list", "key_value", "badge", "statusline", "tree",
|
|
71
|
+
"notify", "info", "success", "warning", "error", "Log",
|
|
72
|
+
"ask", "ask_password", "ask_number", "confirm", "ask_choice",
|
|
73
|
+
"charts", "sparkline", "bar_chart", "gauge", "histogram", "heatmap",
|
|
74
|
+
"menu", "select", "multiselect", "tabs",
|
|
75
|
+
"anim", "ease", "tween", "lerp", "lerp_color", "Transition", "EASINGS",
|
|
76
|
+
"ease_in_quad", "ease_out_quad", "ease_in_out_quad",
|
|
77
|
+
"ease_in_cubic", "ease_out_cubic", "ease_in_out_cubic",
|
|
78
|
+
"ease_in_quart", "ease_out_quart", "ease_in_out_sine",
|
|
79
|
+
"ease_in_expo", "ease_out_expo",
|
|
80
|
+
"ease_out_back", "ease_out_elastic", "ease_out_bounce",
|
|
81
|
+
"particles", "ParticleCanvas", "ParticleSystem",
|
|
82
|
+
"Rain", "Snow", "Stars", "Dust", "Matrix", "Bubbles", "Fireflies",
|
|
83
|
+
"play_particles", "play_preset", "PARTICLE_PRESETS",
|
|
84
|
+
"effects", "typewriter_fx", "fade_in", "wave", "shimmer",
|
|
85
|
+
"glitch", "marquee", "neon",
|
|
86
|
+
"Live", "animate_to",
|
|
87
|
+
# 0.4.0
|
|
88
|
+
"terminal_mod", "Terminal", "terminal",
|
|
89
|
+
"console_themes", "Palette", "THEMES", "get_theme", "list_themes",
|
|
90
|
+
"banner_mod", "big_text",
|
|
91
|
+
"forms_mod", "Form", "Field",
|
|
92
|
+
"wizard_mod", "Wizard", "Step",
|
|
93
|
+
"markdown_mod", "render_markdown", "print_md",
|
|
94
|
+
"progress_ext", "FancyProgressBar", "AnimatedProgressBar", "TaskList",
|
|
95
|
+
]
|
charmpkk/console/anim.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Easing, tween, интерполяция — для плавных консольных анимаций."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# ---------- easing ----------
|
|
6
|
+
|
|
7
|
+
def linear(t): return t
|
|
8
|
+
def ease_in_quad(t): return t * t
|
|
9
|
+
def ease_out_quad(t): return 1 - (1 - t) ** 2
|
|
10
|
+
def ease_in_out_quad(t): return 2 * t * t if t < 0.5 else 1 - (-2 * t + 2) ** 2 / 2
|
|
11
|
+
def ease_in_cubic(t): return t ** 3
|
|
12
|
+
def ease_out_cubic(t): return 1 - (1 - t) ** 3
|
|
13
|
+
def ease_in_out_cubic(t): return 4 * t ** 3 if t < 0.5 else 1 - (-2 * t + 2) ** 3 / 2
|
|
14
|
+
def ease_in_quart(t): return t ** 4
|
|
15
|
+
def ease_out_quart(t): return 1 - (1 - t) ** 4
|
|
16
|
+
def ease_in_out_sine(t): return -(math.cos(math.pi * t) - 1) / 2
|
|
17
|
+
def ease_in_expo(t): return 0 if t == 0 else 2 ** (10 * t - 10)
|
|
18
|
+
def ease_out_expo(t): return 1 if t == 1 else 1 - 2 ** (-10 * t)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ease_out_back(t):
|
|
22
|
+
c1, c3 = 1.70158, 2.70158
|
|
23
|
+
return 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ease_out_elastic(t):
|
|
27
|
+
if t in (0.0, 1.0):
|
|
28
|
+
return t
|
|
29
|
+
c4 = (2 * math.pi) / 3
|
|
30
|
+
return 2 ** (-10 * t) * math.sin((t * 10 - 0.75) * c4) + 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ease_out_bounce(t):
|
|
34
|
+
n1, d1 = 7.5625, 2.75
|
|
35
|
+
if t < 1 / d1:
|
|
36
|
+
return n1 * t * t
|
|
37
|
+
if t < 2 / d1:
|
|
38
|
+
t -= 1.5 / d1
|
|
39
|
+
return n1 * t * t + 0.75
|
|
40
|
+
if t < 2.5 / d1:
|
|
41
|
+
t -= 2.25 / d1
|
|
42
|
+
return n1 * t * t + 0.9375
|
|
43
|
+
t -= 2.625 / d1
|
|
44
|
+
return n1 * t * t + 0.984375
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
EASINGS = {
|
|
48
|
+
"linear": linear,
|
|
49
|
+
"in_quad": ease_in_quad, "out_quad": ease_out_quad, "in_out_quad": ease_in_out_quad,
|
|
50
|
+
"in_cubic": ease_in_cubic, "out_cubic": ease_out_cubic, "in_out_cubic": ease_in_out_cubic,
|
|
51
|
+
"in_quart": ease_in_quart, "out_quart": ease_out_quart,
|
|
52
|
+
"in_out_sine": ease_in_out_sine,
|
|
53
|
+
"in_expo": ease_in_expo, "out_expo": ease_out_expo,
|
|
54
|
+
"out_back": ease_out_back, "out_elastic": ease_out_elastic, "out_bounce": ease_out_bounce,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ease(t, kind="out_cubic"):
|
|
59
|
+
return EASINGS.get(kind, ease_out_cubic)(max(0.0, min(1.0, t)))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def tween(duration=1.0, fps=30, kind="out_cubic"):
|
|
63
|
+
"""Генератор значений 0..1 (с плавным easing)."""
|
|
64
|
+
n = max(int(duration * fps), 1)
|
|
65
|
+
for i in range(n + 1):
|
|
66
|
+
yield ease(i / n, kind)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def lerp(a, b, t):
|
|
70
|
+
return a + (b - a) * t
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _hex_to_rgb(h):
|
|
74
|
+
h = h.lstrip("#")
|
|
75
|
+
if len(h) == 3:
|
|
76
|
+
h = "".join(c * 2 for c in h)
|
|
77
|
+
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _rgb_to_hex(rgb):
|
|
81
|
+
return "#{:02x}{:02x}{:02x}".format(*(max(0, min(255, int(c))) for c in rgb))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def lerp_color(c1, c2, t):
|
|
85
|
+
"""c1, c2 — hex-строки, возвращает hex."""
|
|
86
|
+
a, b = _hex_to_rgb(c1), _hex_to_rgb(c2)
|
|
87
|
+
return _rgb_to_hex((a[i] + (b[i] - a[i]) * t for i in range(3)))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Transition:
|
|
91
|
+
"""Плавно переводит число из текущего значения в целевое."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, value=0.0, kind="out_cubic"):
|
|
94
|
+
self.value = value
|
|
95
|
+
self.target = value
|
|
96
|
+
self.start = value
|
|
97
|
+
self.kind = kind
|
|
98
|
+
self._t = 0.0
|
|
99
|
+
self._duration = 0.0
|
|
100
|
+
|
|
101
|
+
def to(self, target, duration=0.5, kind=None):
|
|
102
|
+
self.start = self.value
|
|
103
|
+
self.target = target
|
|
104
|
+
self._duration = max(duration, 1e-6)
|
|
105
|
+
self._t = 0.0
|
|
106
|
+
if kind:
|
|
107
|
+
self.kind = kind
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def step(self, dt):
|
|
111
|
+
if self._duration <= 0:
|
|
112
|
+
self.value = self.target
|
|
113
|
+
return self.value
|
|
114
|
+
self._t = min(self._t + dt / self._duration, 1.0)
|
|
115
|
+
self.value = lerp(self.start, self.target, ease(self._t, self.kind))
|
|
116
|
+
return self.value
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def done(self):
|
|
120
|
+
return self._t >= 1.0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Большие ASCII-заголовки (минимальный FIGlet-подобный рендер)."""
|
|
2
|
+
from .colors import colorize, gradient_text
|
|
3
|
+
|
|
4
|
+
# 5-строчные глифы (только заглавные + цифры + базовые знаки).
|
|
5
|
+
_FONT = {
|
|
6
|
+
"A": [" ▄▀█ ", " █▀█ ", " ▀ ▀ "],
|
|
7
|
+
"B": [" █▀▄ ", " █▀▄ ", " ▀▀ "],
|
|
8
|
+
"C": [" █▀▀ ", " █ ", " ▀▀▀ "],
|
|
9
|
+
"D": [" █▀▄ ", " █ █ ", " ▀▀ "],
|
|
10
|
+
"E": [" █▀▀ ", " █▀ ", " ▀▀▀ "],
|
|
11
|
+
"F": [" █▀▀ ", " █▀ ", " ▀ "],
|
|
12
|
+
"G": [" █▀▀ ", " █ █ ", " ▀▀▀ "],
|
|
13
|
+
"H": [" █ █ ", " █▀█ ", " ▀ ▀ "],
|
|
14
|
+
"I": [" █ ", " █ ", " ▀ "],
|
|
15
|
+
"J": [" █ ", " █ ", " ▀▀ "],
|
|
16
|
+
"K": [" █ █ ", " █▀▄ ", " ▀ ▀ "],
|
|
17
|
+
"L": [" █ ", " █ ", " ▀▀▀ "],
|
|
18
|
+
"M": [" █▀▄▀█ ", " █ ▀ █ ", " ▀ ▀ "],
|
|
19
|
+
"N": [" █▄ █ ", " █ ▀█ ", " ▀ ▀ "],
|
|
20
|
+
"O": [" █▀█ ", " █ █ ", " ▀▀▀ "],
|
|
21
|
+
"P": [" █▀█ ", " █▀▀ ", " ▀ "],
|
|
22
|
+
"Q": [" █▀█ ", " █ █ ", " ▀▀▀▄ "],
|
|
23
|
+
"R": [" █▀▄ ", " █▀▄ ", " ▀ ▀ "],
|
|
24
|
+
"S": [" █▀▀ ", " ▀▀█ ", " ▀▀▀ "],
|
|
25
|
+
"T": [" █▀█ ", " █ ", " ▀ "],
|
|
26
|
+
"U": [" █ █ ", " █ █ ", " ▀▀▀ "],
|
|
27
|
+
"V": [" █ █ ", " █ █ ", " ▀ "],
|
|
28
|
+
"W": [" █ █ █ ", " █ █ █ ", " ▀ ▀ "],
|
|
29
|
+
"X": [" █ █ ", " █ ", " ▀ ▀ "],
|
|
30
|
+
"Y": [" █ █ ", " █ ", " ▀ "],
|
|
31
|
+
"Z": [" ▀▀█ ", " ▄▀ ", " ▀▀▀ "],
|
|
32
|
+
"0": [" █▀█ ", " █ █ ", " ▀▀▀ "],
|
|
33
|
+
"1": [" ▄█ ", " █ ", " ▀▀▀ "],
|
|
34
|
+
"2": [" █▀█ ", " ▄▀ ", " ▀▀▀ "],
|
|
35
|
+
"3": [" ▀▀█ ", " ▀▀█ ", " ▀▀▀ "],
|
|
36
|
+
"4": [" █ █ ", " ▀▀█ ", " ▀ "],
|
|
37
|
+
"5": [" █▀▀ ", " ▀▀▄ ", " ▀▀▀ "],
|
|
38
|
+
"6": [" █▀▀ ", " █▀▄ ", " ▀▀▀ "],
|
|
39
|
+
"7": [" ▀▀█ ", " █ ", " ▀ "],
|
|
40
|
+
"8": [" █▀█ ", " █▀█ ", " ▀▀▀ "],
|
|
41
|
+
"9": [" █▀█ ", " ▀▀█ ", " ▀▀▀ "],
|
|
42
|
+
" ": [" ", " ", " "],
|
|
43
|
+
"!": [" █ ", " █ ", " ▀ "],
|
|
44
|
+
"?": [" █▀█ ", " ▄▀ ", " ▀ "],
|
|
45
|
+
".": [" ", " ", " ▀ "],
|
|
46
|
+
",": [" ", " ", " ▀ "],
|
|
47
|
+
"-": [" ", " ▀▀▀ ", " "],
|
|
48
|
+
"_": [" ", " ", " ▀▀▀ "],
|
|
49
|
+
":": [" ", " ▀ ", " ▀ "],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_TEMPLATES = {
|
|
53
|
+
"block": ["▄▀█", "█▀█", "▀ ▀"], # не используется напрямую, но идея та же
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _render(text):
|
|
58
|
+
text = text.upper()
|
|
59
|
+
lines = ["", "", ""]
|
|
60
|
+
for ch in text:
|
|
61
|
+
glyph = _FONT.get(ch, _FONT["?"])
|
|
62
|
+
w = max(len(g) for g in glyph)
|
|
63
|
+
for i, g in enumerate(glyph):
|
|
64
|
+
lines[i] += g.ljust(w) + " "
|
|
65
|
+
return [l.rstrip() for l in lines]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def big(text, fg=None, gradient=None, align="left", width=None):
|
|
69
|
+
"""Крупный заголовок. Если передан gradient=(start_hex, end_hex) —
|
|
70
|
+
каждая строка окрашивается градиентом."""
|
|
71
|
+
lines = _render(text)
|
|
72
|
+
if width:
|
|
73
|
+
from .text import align as _a
|
|
74
|
+
lines = [_a(l, width, align) for l in lines]
|
|
75
|
+
if gradient:
|
|
76
|
+
return "\n".join(gradient_text(l, gradient[0], gradient[1])
|
|
77
|
+
for l in lines)
|
|
78
|
+
if fg:
|
|
79
|
+
return "\n".join(colorize(l, fg=fg) for l in lines)
|
|
80
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Рамки, панели, секции, баннеры."""
|
|
2
|
+
from .colors import colorize
|
|
3
|
+
from .text import align, wrap, visible_len
|
|
4
|
+
|
|
5
|
+
STYLES = {
|
|
6
|
+
"single": ("┌", "─", "┐", "│", "└", "┘"),
|
|
7
|
+
"double": ("╔", "═", "╗", "║", "╚", "╝"),
|
|
8
|
+
"round": ("╭", "─", "╮", "│", "╰", "╯"),
|
|
9
|
+
"heavy": ("┏", "━", "┓", "┃", "┗", "┛"),
|
|
10
|
+
"dashed": ("┌", "╌", "┐", "╎", "└", "┘"),
|
|
11
|
+
"dotted": ("┌", "┄", "┐", "┆", "└", "┘"),
|
|
12
|
+
"ascii": ("+", "-", "+", "|", "+", "+"),
|
|
13
|
+
"none": ("", "", "", "", "", ""),
|
|
14
|
+
"shadow": ("┌", "─", "┐", "│", "└", "┘"),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def box(content, title=None, style="round", padding=1, width=None,
|
|
19
|
+
border_fg=None, bg=None, align_mode="left",
|
|
20
|
+
title_align="left", subtitle=None) -> str:
|
|
21
|
+
"""Рамка вокруг текста. content — строка или список строк."""
|
|
22
|
+
if isinstance(content, str):
|
|
23
|
+
lines = content.split("\n")
|
|
24
|
+
else:
|
|
25
|
+
lines = [str(x) for x in content]
|
|
26
|
+
|
|
27
|
+
if width:
|
|
28
|
+
content_w = max(width - 2 - padding * 2, 1)
|
|
29
|
+
new_lines = []
|
|
30
|
+
for line in lines:
|
|
31
|
+
new_lines.extend(wrap(line, content_w))
|
|
32
|
+
lines = new_lines
|
|
33
|
+
|
|
34
|
+
inner = max((visible_len(l) for l in lines), default=0)
|
|
35
|
+
if title:
|
|
36
|
+
inner = max(inner, visible_len(title) + 2)
|
|
37
|
+
if subtitle:
|
|
38
|
+
inner = max(inner, visible_len(subtitle) + 2)
|
|
39
|
+
inner += padding * 2
|
|
40
|
+
|
|
41
|
+
tl, h, tr, v, bl, br = STYLES.get(style, STYLES["round"])
|
|
42
|
+
pad = " " * padding
|
|
43
|
+
|
|
44
|
+
if style == "none":
|
|
45
|
+
return "\n".join(pad + align(l, inner - padding * 2, align_mode) + pad
|
|
46
|
+
for l in lines)
|
|
47
|
+
|
|
48
|
+
if title:
|
|
49
|
+
t = f" {title} "
|
|
50
|
+
if title_align == "center":
|
|
51
|
+
side = max((inner - visible_len(t)) // 2, 0)
|
|
52
|
+
top = tl + h * side + t + h * max(inner - side - visible_len(t), 0) + tr
|
|
53
|
+
elif title_align == "right":
|
|
54
|
+
top = tl + h * max(inner - visible_len(t), 0) + t + tr
|
|
55
|
+
else:
|
|
56
|
+
top = tl + h + t + h * max(inner - visible_len(t) - 1, 0) + tr
|
|
57
|
+
else:
|
|
58
|
+
top = tl + h * inner + tr
|
|
59
|
+
|
|
60
|
+
out = [top]
|
|
61
|
+
for line in lines:
|
|
62
|
+
content_aligned = align(line, inner - padding * 2, align_mode)
|
|
63
|
+
out.append(v + pad + content_aligned + pad + v)
|
|
64
|
+
|
|
65
|
+
if subtitle:
|
|
66
|
+
s = f" {subtitle} "
|
|
67
|
+
bot = bl + h * max(inner - visible_len(s), 0) + s + br
|
|
68
|
+
else:
|
|
69
|
+
bot = bl + h * inner + br
|
|
70
|
+
out.append(bot)
|
|
71
|
+
|
|
72
|
+
if border_fg:
|
|
73
|
+
out = [colorize(x, fg=border_fg, bg=bg) for x in out]
|
|
74
|
+
elif bg:
|
|
75
|
+
out = [colorize(x, bg=bg) for x in out]
|
|
76
|
+
return "\n".join(out)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rule(width=60, char="─", fg=None, title=None, title_align="center",
|
|
80
|
+
bg=None) -> str:
|
|
81
|
+
"""Горизонтальный разделитель."""
|
|
82
|
+
if title:
|
|
83
|
+
t = f" {title} "
|
|
84
|
+
tw = visible_len(t)
|
|
85
|
+
if title_align == "left":
|
|
86
|
+
line = char * 2 + t + char * max(width - tw - 2, 0)
|
|
87
|
+
elif title_align == "right":
|
|
88
|
+
line = char * max(width - tw - 2, 0) + t + char * 2
|
|
89
|
+
else:
|
|
90
|
+
side = max((width - tw) // 2, 0)
|
|
91
|
+
line = char * side + t + char * max(width - side - tw, 0)
|
|
92
|
+
else:
|
|
93
|
+
line = char * width
|
|
94
|
+
return colorize(line, fg=fg, bg=bg)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def section(title, width=60, style="round", fg=None, bg=None) -> str:
|
|
98
|
+
"""Заголовок секции."""
|
|
99
|
+
return rule(width=width, title=title, fg=fg, bg=bg, title_align="left")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def panel(title, content, style="round", border_fg=None, bg=None,
|
|
103
|
+
width=None, padding=1) -> str:
|
|
104
|
+
"""Панель с заголовком."""
|
|
105
|
+
return box(content, title=title, style=style, border_fg=border_fg,
|
|
106
|
+
bg=bg, width=width, padding=padding)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def banner(text, subtitle=None, style="double", fg=None, bg=None,
|
|
110
|
+
width=None) -> str:
|
|
111
|
+
"""Крупный центрированный заголовок."""
|
|
112
|
+
lines = [text]
|
|
113
|
+
if subtitle:
|
|
114
|
+
lines.append(subtitle)
|
|
115
|
+
return box(lines, style=style, border_fg=fg, bg=bg, padding=2,
|
|
116
|
+
width=width, align_mode="center")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Простые ASCII-графики."""
|
|
2
|
+
from .colors import colorize
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sparkline(values, chars="▁▂▃▄▅▆▇█", fg=None):
|
|
6
|
+
if not values:
|
|
7
|
+
return ""
|
|
8
|
+
lo, hi = min(values), max(values)
|
|
9
|
+
rng = (hi - lo) or 1
|
|
10
|
+
out = []
|
|
11
|
+
for v in values:
|
|
12
|
+
i = int((v - lo) / rng * (len(chars) - 1))
|
|
13
|
+
ch = chars[i]
|
|
14
|
+
out.append(colorize(ch, fg=fg) if fg else ch)
|
|
15
|
+
return "".join(out)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bar_chart(data, width=40, char="█", fg=None, show_values=True,
|
|
19
|
+
value_fmt="{:.0f}"):
|
|
20
|
+
items = list(data.items()) if isinstance(data, dict) else list(data)
|
|
21
|
+
if not items:
|
|
22
|
+
return ""
|
|
23
|
+
maxv = max(v for _, v in items) or 1
|
|
24
|
+
max_label = max(len(str(k)) for k, _ in items)
|
|
25
|
+
lines = []
|
|
26
|
+
for label, value in items:
|
|
27
|
+
n = int(value / maxv * width)
|
|
28
|
+
bar = char * n
|
|
29
|
+
if fg:
|
|
30
|
+
bar = colorize(bar, fg=fg)
|
|
31
|
+
lbl = str(label).ljust(max_label)
|
|
32
|
+
suffix = f" {value_fmt.format(value)}" if show_values else ""
|
|
33
|
+
lines.append(f"{lbl} │ {bar}{suffix}")
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def gauge(value, total=100, width=30, fg=None, bg=None, label=""):
|
|
38
|
+
ratio = max(0, min(value / (total or 1), 1))
|
|
39
|
+
n = int(ratio * width)
|
|
40
|
+
filled = "█" * n
|
|
41
|
+
empty = "░" * (width - n)
|
|
42
|
+
if fg:
|
|
43
|
+
filled = colorize(filled, fg=fg)
|
|
44
|
+
if bg:
|
|
45
|
+
empty = colorize(empty, fg=bg)
|
|
46
|
+
pre = f"{label} " if label else ""
|
|
47
|
+
return f"{pre}[{filled}{empty}] {ratio * 100:5.1f}%"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def histogram(values, bins=10, width=40, char="█", fg=None,
|
|
51
|
+
show_labels=True):
|
|
52
|
+
if not values:
|
|
53
|
+
return ""
|
|
54
|
+
lo, hi = min(values), max(values)
|
|
55
|
+
if hi == lo:
|
|
56
|
+
hi = lo + 1
|
|
57
|
+
step = (hi - lo) / bins
|
|
58
|
+
counts = [0] * bins
|
|
59
|
+
for v in values:
|
|
60
|
+
i = min(int((v - lo) / step), bins - 1)
|
|
61
|
+
counts[i] += 1
|
|
62
|
+
maxc = max(counts) or 1
|
|
63
|
+
lines = []
|
|
64
|
+
for i, c in enumerate(counts):
|
|
65
|
+
n = int(c / maxc * width)
|
|
66
|
+
bar = char * n
|
|
67
|
+
if fg:
|
|
68
|
+
bar = colorize(bar, fg=fg)
|
|
69
|
+
if show_labels:
|
|
70
|
+
lbl = f"{lo + i * step:>7.2f}"
|
|
71
|
+
lines.append(f"{lbl} │ {bar} {c}")
|
|
72
|
+
else:
|
|
73
|
+
lines.append(bar)
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def heatmap(values, rows, cols, char_ramp=" ░▒▓█", fg=None):
|
|
78
|
+
"""Простая heatmap из 2D-списка (по строкам)."""
|
|
79
|
+
if not values:
|
|
80
|
+
return ""
|
|
81
|
+
flat = [v for row in values for v in row]
|
|
82
|
+
lo, hi = min(flat), max(flat)
|
|
83
|
+
rng = (hi - lo) or 1
|
|
84
|
+
n = len(char_ramp) - 1
|
|
85
|
+
out = []
|
|
86
|
+
for r in range(rows):
|
|
87
|
+
row = values[r] if r < len(values) else []
|
|
88
|
+
line = []
|
|
89
|
+
for c in range(cols):
|
|
90
|
+
v = row[c] if c < len(row) else lo
|
|
91
|
+
i = int((v - lo) / rng * n)
|
|
92
|
+
ch = char_ramp[i]
|
|
93
|
+
line.append(colorize(ch, fg=fg) if fg else ch)
|
|
94
|
+
out.append("".join(line))
|
|
95
|
+
return "\n".join(out)
|