vpterm-vp 1.0.0__tar.gz
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.
- vpterm_vp-1.0.0/PKG-INFO +10 -0
- vpterm_vp-1.0.0/pyproject.toml +30 -0
- vpterm_vp-1.0.0/setup.cfg +4 -0
- vpterm_vp-1.0.0/vpterm/__init__.py +14 -0
- vpterm_vp-1.0.0/vpterm/kv.py +51 -0
- vpterm_vp-1.0.0/vpterm/panel.py +83 -0
- vpterm_vp-1.0.0/vpterm/progress.py +60 -0
- vpterm_vp-1.0.0/vpterm/py.typed +0 -0
- vpterm_vp-1.0.0/vpterm/style.py +204 -0
- vpterm_vp-1.0.0/vpterm/table.py +61 -0
- vpterm_vp-1.0.0/vpterm/terminal.py +104 -0
- vpterm_vp-1.0.0/vpterm_vp.egg-info/PKG-INFO +10 -0
- vpterm_vp-1.0.0/vpterm_vp.egg-info/SOURCES.txt +13 -0
- vpterm_vp-1.0.0/vpterm_vp.egg-info/dependency_links.txt +1 -0
- vpterm_vp-1.0.0/vpterm_vp.egg-info/top_level.txt +1 -0
vpterm_vp-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vpterm-vp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Terminal rendering: colors, panels, tables, progress bars.
|
|
5
|
+
Author: F000NK, Voluntas Progressus
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.14
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vpterm-vp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "F000NK" },
|
|
10
|
+
{ name = "Voluntas Progressus" },
|
|
11
|
+
]
|
|
12
|
+
description = "Terminal rendering: colors, panels, tables, progress bars."
|
|
13
|
+
license = "MIT"
|
|
14
|
+
requires-python = ">=3.14"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
include = ["vpterm*"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
vpterm = ["py.typed"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
line-length = 120
|
|
30
|
+
target-version = "py314"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""VP Terminal — rich console output library.
|
|
2
|
+
|
|
3
|
+
Provides colors, styled text, panels, key-value formatting,
|
|
4
|
+
progress bars, and tables for terminal applications.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from vpterm.style import Style, style, strip_ansi
|
|
8
|
+
from vpterm.panel import Panel
|
|
9
|
+
from vpterm.table import Table
|
|
10
|
+
from vpterm.kv import KeyValue, kv_line
|
|
11
|
+
from vpterm.progress import ProgressBar
|
|
12
|
+
from vpterm.terminal import Terminal, get_terminal
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Key-value line formatting for structured log output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from vpterm.style import Style
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class KeyValue:
|
|
9
|
+
"""Builder for key=value formatted lines.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
line = KeyValue()
|
|
13
|
+
line.add("epoch", "1/10")
|
|
14
|
+
line.add("loss", 3.1415, precision=4)
|
|
15
|
+
print(line.render())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, separator: str = " ") -> None:
|
|
19
|
+
self.parts: list[str] = []
|
|
20
|
+
self.separator = separator
|
|
21
|
+
|
|
22
|
+
def add(self, key: str, value: str | int | float, precision: int = 4) -> KeyValue:
|
|
23
|
+
"""Add a key=value pair."""
|
|
24
|
+
if isinstance(value, float):
|
|
25
|
+
formatted = f"{value:.{precision}f}"
|
|
26
|
+
else:
|
|
27
|
+
formatted = str(value)
|
|
28
|
+
self.parts.append(
|
|
29
|
+
f"{Style.metric_name(key)}{Style.dim('=')}{Style.metric_value(formatted)}"
|
|
30
|
+
)
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def add_raw(self, text: str) -> KeyValue:
|
|
34
|
+
"""Add pre-formatted text."""
|
|
35
|
+
self.parts.append(text)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def render(self) -> str:
|
|
39
|
+
"""Render all pairs as a single line."""
|
|
40
|
+
return self.separator.join(self.parts)
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return self.render()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def kv_line(**kwargs: str | int | float) -> str:
|
|
47
|
+
"""Quick helper to render key=value pairs from keyword arguments."""
|
|
48
|
+
builder = KeyValue()
|
|
49
|
+
for key, value in kwargs.items():
|
|
50
|
+
builder.add(key, value)
|
|
51
|
+
return builder.render()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Panel rendering — bordered boxes for grouped content."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from vpterm.style import Style, visible_length
|
|
6
|
+
|
|
7
|
+
BOX_TOP_LEFT = "╭"
|
|
8
|
+
BOX_TOP_RIGHT = "╮"
|
|
9
|
+
BOX_BOTTOM_LEFT = "╰"
|
|
10
|
+
BOX_BOTTOM_RIGHT = "╯"
|
|
11
|
+
BOX_HORIZONTAL = "─"
|
|
12
|
+
BOX_VERTICAL = "│"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Panel:
|
|
16
|
+
"""Bordered panel for grouping related output.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
panel = Panel(title="Training")
|
|
20
|
+
panel.add_line("epoch 1/10 batch 50/3139")
|
|
21
|
+
panel.add_line("loss=3.14 lr=0.001")
|
|
22
|
+
print(panel.render())
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, title: str = "", width: int = 0, min_width: int = 60) -> None:
|
|
26
|
+
self.title = title
|
|
27
|
+
self.requested_width = width
|
|
28
|
+
self.min_width = min_width
|
|
29
|
+
self.lines: list[str] = []
|
|
30
|
+
|
|
31
|
+
def add_line(self, text: str) -> Panel:
|
|
32
|
+
"""Add a content line to the panel."""
|
|
33
|
+
self.lines.append(text)
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def add_blank(self) -> Panel:
|
|
37
|
+
"""Add an empty line."""
|
|
38
|
+
self.lines.append("")
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def add_section(self, title: str) -> Panel:
|
|
42
|
+
"""Add a section header inside the panel."""
|
|
43
|
+
self.lines.append(Style.bold(Style.cyan(title)))
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def add_kv(self, key: str, value: str, key_width: int = 14) -> Panel:
|
|
47
|
+
"""Add an indented key-value pair."""
|
|
48
|
+
padded = key.ljust(key_width)
|
|
49
|
+
self.lines.append(f" {Style.label(padded)} {value}")
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def _compute_width(self) -> int:
|
|
53
|
+
if self.requested_width > 0:
|
|
54
|
+
return self.requested_width
|
|
55
|
+
max_content = max((visible_length(line) for line in self.lines), default=0)
|
|
56
|
+
title_len = len(self.title) + 4 if self.title else 0
|
|
57
|
+
return max(self.min_width, max_content + 4, title_len)
|
|
58
|
+
|
|
59
|
+
def render(self) -> str:
|
|
60
|
+
"""Render the panel as a multi-line string."""
|
|
61
|
+
width = self._compute_width()
|
|
62
|
+
inner_width = width - 2
|
|
63
|
+
output_lines: list[str] = []
|
|
64
|
+
|
|
65
|
+
if self.title:
|
|
66
|
+
title_display = f" {self.title} "
|
|
67
|
+
bar_len = max(0, inner_width - len(title_display))
|
|
68
|
+
top = f"{BOX_TOP_LEFT}{BOX_HORIZONTAL}{title_display}{BOX_HORIZONTAL * bar_len}{BOX_TOP_RIGHT}"
|
|
69
|
+
else:
|
|
70
|
+
top = f"{BOX_TOP_LEFT}{BOX_HORIZONTAL * inner_width}{BOX_TOP_RIGHT}"
|
|
71
|
+
output_lines.append(Style.dim(top))
|
|
72
|
+
|
|
73
|
+
for line in self.lines:
|
|
74
|
+
padding = max(0, inner_width - visible_length(line) - 1)
|
|
75
|
+
output_lines.append(f"{Style.dim(BOX_VERTICAL)} {line}{' ' * padding}{Style.dim(BOX_VERTICAL)}")
|
|
76
|
+
|
|
77
|
+
bottom = f"{BOX_BOTTOM_LEFT}{BOX_HORIZONTAL * inner_width}{BOX_BOTTOM_RIGHT}"
|
|
78
|
+
output_lines.append(Style.dim(bottom))
|
|
79
|
+
|
|
80
|
+
return "\n".join(output_lines)
|
|
81
|
+
|
|
82
|
+
def __str__(self) -> str:
|
|
83
|
+
return self.render()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Progress bar and time formatting for training loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from vpterm.style import Style
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_duration(total_seconds: float) -> str:
|
|
9
|
+
"""Format seconds into human-readable HH:MM:SS."""
|
|
10
|
+
rounded = max(0, int(total_seconds))
|
|
11
|
+
hours, remainder = divmod(rounded, 3600)
|
|
12
|
+
minutes, seconds = divmod(remainder, 60)
|
|
13
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def format_number(value: int) -> str:
|
|
17
|
+
"""Format integer with thousands separator."""
|
|
18
|
+
return f"{value:,}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProgressBar:
|
|
22
|
+
"""Text-based progress bar.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
bar = ProgressBar(total=100, width=30)
|
|
26
|
+
print(bar.render(current=42))
|
|
27
|
+
# ████████████░░░░░░░░░░░░░░░░░░ 42%
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
FILL = "█"
|
|
31
|
+
EMPTY = "░"
|
|
32
|
+
|
|
33
|
+
def __init__(self, total: int, width: int = 30) -> None:
|
|
34
|
+
self.total = max(1, total)
|
|
35
|
+
self.width = width
|
|
36
|
+
|
|
37
|
+
def render(self, current: int) -> str:
|
|
38
|
+
"""Render progress bar for the given current value."""
|
|
39
|
+
fraction = min(1.0, max(0.0, current / self.total))
|
|
40
|
+
filled = int(self.width * fraction)
|
|
41
|
+
empty = self.width - filled
|
|
42
|
+
bar = self.FILL * filled + self.EMPTY * empty
|
|
43
|
+
percent = f"{fraction * 100:.0f}%"
|
|
44
|
+
|
|
45
|
+
if fraction < 0.33:
|
|
46
|
+
colored_bar = Style.red(bar)
|
|
47
|
+
elif fraction < 0.66:
|
|
48
|
+
colored_bar = Style.yellow(bar)
|
|
49
|
+
else:
|
|
50
|
+
colored_bar = Style.green(bar)
|
|
51
|
+
|
|
52
|
+
return f"{colored_bar} {Style.bold(percent)}"
|
|
53
|
+
|
|
54
|
+
def render_with_label(self, current: int, label: str = "") -> str:
|
|
55
|
+
"""Render progress bar with optional label and count."""
|
|
56
|
+
bar = self.render(current)
|
|
57
|
+
count = Style.dim(f"{format_number(current)}/{format_number(self.total)}")
|
|
58
|
+
if label:
|
|
59
|
+
return f"{Style.label(label)} {bar} {count}"
|
|
60
|
+
return f"{bar} {count}"
|
|
File without changes
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""ANSI terminal styling — colors, bold, dim, underline.
|
|
2
|
+
|
|
3
|
+
Supports auto-detection of terminal capabilities and graceful
|
|
4
|
+
fallback to plain text when colors are not supported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
RESET = "\033[0m"
|
|
14
|
+
BOLD = "\033[1m"
|
|
15
|
+
DIM = "\033[2m"
|
|
16
|
+
ITALIC = "\033[3m"
|
|
17
|
+
UNDERLINE = "\033[4m"
|
|
18
|
+
|
|
19
|
+
FG_BLACK = "\033[30m"
|
|
20
|
+
FG_RED = "\033[31m"
|
|
21
|
+
FG_GREEN = "\033[32m"
|
|
22
|
+
FG_YELLOW = "\033[33m"
|
|
23
|
+
FG_BLUE = "\033[34m"
|
|
24
|
+
FG_MAGENTA = "\033[35m"
|
|
25
|
+
FG_CYAN = "\033[36m"
|
|
26
|
+
FG_WHITE = "\033[37m"
|
|
27
|
+
|
|
28
|
+
FG_BRIGHT_BLACK = "\033[90m"
|
|
29
|
+
FG_BRIGHT_RED = "\033[91m"
|
|
30
|
+
FG_BRIGHT_GREEN = "\033[92m"
|
|
31
|
+
FG_BRIGHT_YELLOW = "\033[93m"
|
|
32
|
+
FG_BRIGHT_BLUE = "\033[94m"
|
|
33
|
+
FG_BRIGHT_MAGENTA = "\033[95m"
|
|
34
|
+
FG_BRIGHT_CYAN = "\033[96m"
|
|
35
|
+
FG_BRIGHT_WHITE = "\033[97m"
|
|
36
|
+
|
|
37
|
+
BG_RED = "\033[41m"
|
|
38
|
+
BG_GREEN = "\033[42m"
|
|
39
|
+
BG_YELLOW = "\033[43m"
|
|
40
|
+
BG_BLUE = "\033[44m"
|
|
41
|
+
|
|
42
|
+
_STRIP_ANSI = re.compile(r"\033\[[0-9;]*m")
|
|
43
|
+
|
|
44
|
+
_colors_enabled: bool | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _detect_color_support() -> bool:
|
|
48
|
+
"""Detect if terminal supports ANSI colors."""
|
|
49
|
+
if os.environ.get("NO_COLOR"):
|
|
50
|
+
return False
|
|
51
|
+
if os.environ.get("FORCE_COLOR"):
|
|
52
|
+
return True
|
|
53
|
+
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
|
|
54
|
+
return False
|
|
55
|
+
if sys.platform == "win32":
|
|
56
|
+
try:
|
|
57
|
+
import ctypes
|
|
58
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
59
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
return os.environ.get("TERM") is not None
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def colors_enabled() -> bool:
|
|
67
|
+
"""Check if ANSI colors are enabled for this session."""
|
|
68
|
+
global _colors_enabled
|
|
69
|
+
if _colors_enabled is None:
|
|
70
|
+
_colors_enabled = _detect_color_support()
|
|
71
|
+
return _colors_enabled
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def set_colors_enabled(enabled: bool) -> None:
|
|
75
|
+
"""Force enable or disable colors."""
|
|
76
|
+
global _colors_enabled
|
|
77
|
+
_colors_enabled = enabled
|
|
78
|
+
|
|
79
|
+
def strip_ansi(text: str) -> str:
|
|
80
|
+
"""Remove all ANSI escape sequences from text."""
|
|
81
|
+
return _STRIP_ANSI.sub("", text)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def visible_length(text: str) -> int:
|
|
85
|
+
"""Compute visible length of text (excluding ANSI codes)."""
|
|
86
|
+
return len(strip_ansi(text))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Style:
|
|
90
|
+
"""Named style presets for consistent theming."""
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _wrap(codes: str, text: str) -> str:
|
|
94
|
+
if not colors_enabled():
|
|
95
|
+
return text
|
|
96
|
+
return f"{codes}{text}{RESET}"
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def bold(text: str) -> str:
|
|
100
|
+
return Style._wrap(BOLD, text)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def dim(text: str) -> str:
|
|
104
|
+
return Style._wrap(DIM, text)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def red(text: str) -> str:
|
|
108
|
+
return Style._wrap(FG_RED, text)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def green(text: str) -> str:
|
|
112
|
+
return Style._wrap(FG_GREEN, text)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def yellow(text: str) -> str:
|
|
116
|
+
return Style._wrap(FG_YELLOW, text)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def blue(text: str) -> str:
|
|
120
|
+
return Style._wrap(FG_BLUE, text)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def magenta(text: str) -> str:
|
|
124
|
+
return Style._wrap(FG_MAGENTA, text)
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def cyan(text: str) -> str:
|
|
128
|
+
return Style._wrap(FG_CYAN, text)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def white(text: str) -> str:
|
|
132
|
+
return Style._wrap(FG_WHITE, text)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def bright_black(text: str) -> str:
|
|
136
|
+
return Style._wrap(FG_BRIGHT_BLACK, text)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def bright_green(text: str) -> str:
|
|
140
|
+
return Style._wrap(FG_BRIGHT_GREEN, text)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def bright_yellow(text: str) -> str:
|
|
144
|
+
return Style._wrap(FG_BRIGHT_YELLOW, text)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def bright_cyan(text: str) -> str:
|
|
148
|
+
return Style._wrap(FG_BRIGHT_CYAN, text)
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def bright_red(text: str) -> str:
|
|
152
|
+
return Style._wrap(FG_BRIGHT_RED, text)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def success(text: str) -> str:
|
|
156
|
+
return Style._wrap(FG_GREEN + BOLD, text)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def warning(text: str) -> str:
|
|
160
|
+
return Style._wrap(FG_YELLOW + BOLD, text)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def error(text: str) -> str:
|
|
164
|
+
return Style._wrap(FG_RED + BOLD, text)
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def info(text: str) -> str:
|
|
168
|
+
return Style._wrap(FG_CYAN, text)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def label(text: str) -> str:
|
|
172
|
+
return Style._wrap(FG_BRIGHT_BLACK, text)
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def value(text: str) -> str:
|
|
176
|
+
return Style._wrap(FG_WHITE + BOLD, text)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def metric_name(text: str) -> str:
|
|
180
|
+
return Style._wrap(FG_CYAN, text)
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def metric_value(text: str) -> str:
|
|
184
|
+
return Style._wrap(FG_BRIGHT_YELLOW, text)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def improved(text: str) -> str:
|
|
188
|
+
return Style._wrap(FG_GREEN + BOLD, text)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def degraded(text: str) -> str:
|
|
192
|
+
return Style._wrap(FG_RED, text)
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def timestamp(text: str) -> str:
|
|
196
|
+
return Style._wrap(DIM, text)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def style(text: str, *codes: str) -> str:
|
|
200
|
+
"""Apply arbitrary ANSI codes to text."""
|
|
201
|
+
if not colors_enabled():
|
|
202
|
+
return text
|
|
203
|
+
combined = "".join(codes)
|
|
204
|
+
return f"{combined}{text}{RESET}"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Simple table rendering for terminal output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from vpterm.style import Style, visible_length
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Table:
|
|
9
|
+
"""Minimal table renderer with column alignment.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
table = Table(["Component", "Params", "Trainable"])
|
|
13
|
+
table.add_row(["universal", "45,000,000", "yes"])
|
|
14
|
+
table.add_row(["family/slavic", "7,200,000", "no"])
|
|
15
|
+
print(table.render())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, headers: list[str], padding: int = 2) -> None:
|
|
19
|
+
self.headers = headers
|
|
20
|
+
self.rows: list[list[str]] = []
|
|
21
|
+
self.padding = padding
|
|
22
|
+
|
|
23
|
+
def add_row(self, values: list[str]) -> Table:
|
|
24
|
+
"""Add a data row. Must have same number of columns as headers."""
|
|
25
|
+
self.rows.append(values)
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def _compute_widths(self) -> list[int]:
|
|
29
|
+
widths = [visible_length(header) for header in self.headers]
|
|
30
|
+
for row in self.rows:
|
|
31
|
+
for col_idx, cell in enumerate(row):
|
|
32
|
+
if col_idx < len(widths):
|
|
33
|
+
widths[col_idx] = max(widths[col_idx], visible_length(cell))
|
|
34
|
+
return widths
|
|
35
|
+
|
|
36
|
+
def render(self) -> str:
|
|
37
|
+
"""Render the table as a multi-line string."""
|
|
38
|
+
widths = self._compute_widths()
|
|
39
|
+
pad = " " * self.padding
|
|
40
|
+
lines: list[str] = []
|
|
41
|
+
|
|
42
|
+
header_cells = []
|
|
43
|
+
for idx, header in enumerate(self.headers):
|
|
44
|
+
header_cells.append(Style.bold(header.ljust(widths[idx])))
|
|
45
|
+
lines.append(pad.join(header_cells))
|
|
46
|
+
|
|
47
|
+
separator_cells = [Style.dim("─" * width) for width in widths]
|
|
48
|
+
lines.append(pad.join(separator_cells))
|
|
49
|
+
|
|
50
|
+
for row in self.rows:
|
|
51
|
+
cells = []
|
|
52
|
+
for col_idx, cell in enumerate(row):
|
|
53
|
+
width = widths[col_idx] if col_idx < len(widths) else 0
|
|
54
|
+
cell_padding = max(0, width - visible_length(cell))
|
|
55
|
+
cells.append(cell + " " * cell_padding)
|
|
56
|
+
lines.append(pad.join(cells))
|
|
57
|
+
|
|
58
|
+
return "\n".join(lines)
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
return self.render()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Terminal abstraction — write styled output to stdout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from vpterm.style import Style, colors_enabled, visible_length
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Terminal:
|
|
12
|
+
"""Output controller for terminal rendering.
|
|
13
|
+
|
|
14
|
+
Manages writing styled lines, clearing, and detecting terminal width.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, stream: object | None = None) -> None:
|
|
18
|
+
self.stream = stream or sys.stdout
|
|
19
|
+
|
|
20
|
+
def width(self) -> int:
|
|
21
|
+
"""Get terminal width in columns."""
|
|
22
|
+
try:
|
|
23
|
+
return os.get_terminal_size(self.stream.fileno()).columns
|
|
24
|
+
except (AttributeError, ValueError, OSError):
|
|
25
|
+
return 120
|
|
26
|
+
|
|
27
|
+
def write(self, text: str) -> None:
|
|
28
|
+
"""Write text to stream."""
|
|
29
|
+
self.stream.write(text)
|
|
30
|
+
self.stream.flush()
|
|
31
|
+
|
|
32
|
+
def writeln(self, text: str = "") -> None:
|
|
33
|
+
"""Write text followed by newline."""
|
|
34
|
+
self.stream.write(text + "\n")
|
|
35
|
+
self.stream.flush()
|
|
36
|
+
|
|
37
|
+
def blank_line(self) -> None:
|
|
38
|
+
"""Write an empty line."""
|
|
39
|
+
self.writeln()
|
|
40
|
+
|
|
41
|
+
def overwrite_line(self, text: str) -> None:
|
|
42
|
+
"""Overwrite the current line (carriage return)."""
|
|
43
|
+
width = self.width()
|
|
44
|
+
padded = text + " " * max(0, width - visible_length(text))
|
|
45
|
+
self.stream.write(f"\r{padded}")
|
|
46
|
+
self.stream.flush()
|
|
47
|
+
|
|
48
|
+
def separator(self, char: str = "─", label: str = "") -> None:
|
|
49
|
+
"""Draw a horizontal separator line."""
|
|
50
|
+
width = self.width()
|
|
51
|
+
if label:
|
|
52
|
+
styled_label = f" {label} "
|
|
53
|
+
label_len = len(styled_label)
|
|
54
|
+
left = char * 2
|
|
55
|
+
right = char * max(0, width - 4 - label_len)
|
|
56
|
+
line = f"{left}{Style.bold(styled_label)}{right}"
|
|
57
|
+
else:
|
|
58
|
+
line = char * width
|
|
59
|
+
self.writeln(Style.dim(line) if not label else line)
|
|
60
|
+
|
|
61
|
+
def header(self, title: str) -> None:
|
|
62
|
+
"""Print a styled header with separators."""
|
|
63
|
+
self.separator("─", title)
|
|
64
|
+
|
|
65
|
+
def section(self, title: str) -> None:
|
|
66
|
+
"""Print a section header."""
|
|
67
|
+
self.writeln(Style.bold(Style.cyan(f"▸ {title}")))
|
|
68
|
+
|
|
69
|
+
def kv_pair(self, key: str, value: str, key_width: int = 0) -> None:
|
|
70
|
+
"""Print a key=value pair with styling."""
|
|
71
|
+
padded_key = key.ljust(key_width) if key_width > 0 else key
|
|
72
|
+
self.writeln(f" {Style.label(padded_key)} {Style.value(value)}")
|
|
73
|
+
|
|
74
|
+
def metric(self, name: str, value: str | float, precision: int = 4) -> str:
|
|
75
|
+
"""Format a metric as styled string (does not print)."""
|
|
76
|
+
if isinstance(value, float):
|
|
77
|
+
formatted = f"{value:.{precision}f}"
|
|
78
|
+
else:
|
|
79
|
+
formatted = str(value)
|
|
80
|
+
return f"{Style.metric_name(name)}{Style.dim('=')}{Style.metric_value(formatted)}"
|
|
81
|
+
|
|
82
|
+
def status(self, message: str, kind: str = "info") -> None:
|
|
83
|
+
"""Print a status message."""
|
|
84
|
+
prefixes = {
|
|
85
|
+
"info": Style.cyan("ℹ"),
|
|
86
|
+
"success": Style.success("✓"),
|
|
87
|
+
"warning": Style.warning("⚠"),
|
|
88
|
+
"error": Style.error("✗"),
|
|
89
|
+
"start": Style.blue("▶"),
|
|
90
|
+
"stop": Style.yellow("■"),
|
|
91
|
+
}
|
|
92
|
+
prefix = prefixes.get(kind, Style.dim("·"))
|
|
93
|
+
self.writeln(f" {prefix} {message}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_default_terminal: Terminal | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_terminal() -> Terminal:
|
|
100
|
+
"""Get the global Terminal instance."""
|
|
101
|
+
global _default_terminal
|
|
102
|
+
if _default_terminal is None:
|
|
103
|
+
_default_terminal = Terminal()
|
|
104
|
+
return _default_terminal
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vpterm-vp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Terminal rendering: colors, panels, tables, progress bars.
|
|
5
|
+
Author: F000NK, Voluntas Progressus
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.14
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
vpterm/__init__.py
|
|
3
|
+
vpterm/kv.py
|
|
4
|
+
vpterm/panel.py
|
|
5
|
+
vpterm/progress.py
|
|
6
|
+
vpterm/py.typed
|
|
7
|
+
vpterm/style.py
|
|
8
|
+
vpterm/table.py
|
|
9
|
+
vpterm/terminal.py
|
|
10
|
+
vpterm_vp.egg-info/PKG-INFO
|
|
11
|
+
vpterm_vp.egg-info/SOURCES.txt
|
|
12
|
+
vpterm_vp.egg-info/dependency_links.txt
|
|
13
|
+
vpterm_vp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vpterm
|