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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ vpterm