zenus-visualization 0.6.1__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,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zenus-visualization
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Data visualization system for Zenus
|
|
5
|
+
Author: Zeni
|
|
6
|
+
Requires-Python: >=3.10,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: rich (>=13.7.0,<14.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Zenus Visualization
|
|
17
|
+
|
|
18
|
+
Automatic data visualization system for Zenus.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Auto-detection**: Automatically detects data types and chooses the best visualization
|
|
23
|
+
- **Rich tables**: Beautiful tables for structured data
|
|
24
|
+
- **Progress bars**: Visual indicators for percentages and resource usage
|
|
25
|
+
- **File trees**: Hierarchical file and directory displays
|
|
26
|
+
- **Syntax highlighting**: Code and JSON with color coding
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from zenus_visualization import Visualizer
|
|
32
|
+
|
|
33
|
+
# Automatically visualize any data
|
|
34
|
+
Visualizer.visualize(data)
|
|
35
|
+
|
|
36
|
+
# With context hint
|
|
37
|
+
Visualizer.visualize(data, context="process_list")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Supported Data Types
|
|
41
|
+
|
|
42
|
+
- Process lists
|
|
43
|
+
- Disk usage stats
|
|
44
|
+
- System resource summaries
|
|
45
|
+
- File listings
|
|
46
|
+
- JSON/structured data
|
|
47
|
+
- Key-value pairs
|
|
48
|
+
- Plain text (fallback)
|
|
49
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Zenus Visualization
|
|
2
|
+
|
|
3
|
+
Automatic data visualization system for Zenus.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auto-detection**: Automatically detects data types and chooses the best visualization
|
|
8
|
+
- **Rich tables**: Beautiful tables for structured data
|
|
9
|
+
- **Progress bars**: Visual indicators for percentages and resource usage
|
|
10
|
+
- **File trees**: Hierarchical file and directory displays
|
|
11
|
+
- **Syntax highlighting**: Code and JSON with color coding
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from zenus_visualization import Visualizer
|
|
17
|
+
|
|
18
|
+
# Automatically visualize any data
|
|
19
|
+
Visualizer.visualize(data)
|
|
20
|
+
|
|
21
|
+
# With context hint
|
|
22
|
+
Visualizer.visualize(data, context="process_list")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Supported Data Types
|
|
26
|
+
|
|
27
|
+
- Process lists
|
|
28
|
+
- Disk usage stats
|
|
29
|
+
- System resource summaries
|
|
30
|
+
- File listings
|
|
31
|
+
- JSON/structured data
|
|
32
|
+
- Key-value pairs
|
|
33
|
+
- Plain text (fallback)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "zenus-visualization"
|
|
7
|
+
version = "0.6.1"
|
|
8
|
+
description = "Data visualization system for Zenus"
|
|
9
|
+
authors = ["Zeni"]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
packages = [{include = "zenus_visualization", from = "src"}]
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = "^3.10"
|
|
15
|
+
rich = "^13.7.0"
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
pytest = "^7.4.0"
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main Visualizer - Automatic Data Visualization
|
|
3
|
+
|
|
4
|
+
Detects data types and renders beautiful visualizations automatically.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, List, Optional, Union
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
from rich import box
|
|
14
|
+
from rich.tree import Tree
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Visualizer:
|
|
22
|
+
"""
|
|
23
|
+
Automatically visualizes data in the most appropriate format
|
|
24
|
+
|
|
25
|
+
Detects:
|
|
26
|
+
- Tables (lists of dicts, process lists, file listings)
|
|
27
|
+
- Progress/percentages (disk usage, memory usage)
|
|
28
|
+
- JSON/structured data
|
|
29
|
+
- Code diffs
|
|
30
|
+
- File trees
|
|
31
|
+
- Key-value pairs
|
|
32
|
+
- Plain text (fallback)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def visualize(data: Union[str, Dict, List], context: Optional[str] = None) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Main entry point - automatically detects type and visualizes
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data: Data to visualize (string, dict, list)
|
|
42
|
+
context: Optional context (e.g., "file_list", "process_list", "disk_usage")
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(data, dict):
|
|
45
|
+
Visualizer._visualize_dict(data, context)
|
|
46
|
+
elif isinstance(data, list):
|
|
47
|
+
Visualizer._visualize_list(data, context)
|
|
48
|
+
elif isinstance(data, str):
|
|
49
|
+
Visualizer._visualize_string(data, context)
|
|
50
|
+
else:
|
|
51
|
+
# Fallback to string representation
|
|
52
|
+
console.print(f" → {str(data)}", style="dim")
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _visualize_string(data: str, context: Optional[str]) -> None:
|
|
56
|
+
"""Visualize string data with smart detection"""
|
|
57
|
+
|
|
58
|
+
# Check for process list pattern
|
|
59
|
+
if "PID" in data and "%" in data:
|
|
60
|
+
Visualizer._visualize_process_list(data)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Check for disk usage pattern
|
|
64
|
+
if "GB" in data and ("used" in data.lower() or "free" in data.lower()):
|
|
65
|
+
Visualizer._visualize_disk_usage(data)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Check for CPU/Memory/Disk summary
|
|
69
|
+
if "CPU:" in data and "Memory:" in data and "Disk:" in data:
|
|
70
|
+
Visualizer._visualize_system_summary(data)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Check for file list pattern (filenames with sizes)
|
|
74
|
+
if context == "file_list" or re.search(r'\.(py|txt|md|json|yaml)', data):
|
|
75
|
+
Visualizer._visualize_file_list(data)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Check for percentage/progress
|
|
79
|
+
if "%" in data:
|
|
80
|
+
Visualizer._visualize_percentage(data)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Check for JSON
|
|
84
|
+
if data.strip().startswith('{') or data.strip().startswith('['):
|
|
85
|
+
try:
|
|
86
|
+
parsed = json.loads(data)
|
|
87
|
+
Visualizer._visualize_dict(parsed, context)
|
|
88
|
+
return
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
# Check for key-value pairs
|
|
93
|
+
if "\n" in data and ":" in data:
|
|
94
|
+
Visualizer._visualize_key_value(data)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Fallback: plain text with formatting
|
|
98
|
+
console.print(f" → {data}", style="cyan")
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _visualize_process_list(data: str) -> None:
|
|
102
|
+
"""Visualize process list as a rich table"""
|
|
103
|
+
lines = data.strip().split("\n")
|
|
104
|
+
|
|
105
|
+
table = Table(
|
|
106
|
+
title="🖥️ Processes",
|
|
107
|
+
box=box.ROUNDED,
|
|
108
|
+
show_header=True,
|
|
109
|
+
header_style="bold magenta"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
table.add_column("PID", style="cyan", justify="right", width=8)
|
|
113
|
+
table.add_column("Name", style="green")
|
|
114
|
+
table.add_column("Memory", style="yellow", justify="right", width=10)
|
|
115
|
+
table.add_column("Usage Bar", width=20)
|
|
116
|
+
|
|
117
|
+
for line in lines:
|
|
118
|
+
# Parse: "PID 1009: openclaw-gateway (12.6% mem)"
|
|
119
|
+
match = re.match(r'PID\s+(\d+):\s+(.+?)\s+\(([0-9.]+)%\s+mem\)', line)
|
|
120
|
+
if match:
|
|
121
|
+
pid, name, mem_pct = match.groups()
|
|
122
|
+
mem_float = float(mem_pct)
|
|
123
|
+
|
|
124
|
+
# Create visual bar
|
|
125
|
+
bar_width = int(mem_float / 5) # Scale to 20 chars max
|
|
126
|
+
bar = "█" * bar_width + "░" * (20 - bar_width)
|
|
127
|
+
|
|
128
|
+
# Color code by usage
|
|
129
|
+
if mem_float > 10:
|
|
130
|
+
mem_style = "bold red"
|
|
131
|
+
elif mem_float > 5:
|
|
132
|
+
mem_style = "bold yellow"
|
|
133
|
+
else:
|
|
134
|
+
mem_style = "bold green"
|
|
135
|
+
|
|
136
|
+
table.add_row(
|
|
137
|
+
pid,
|
|
138
|
+
name,
|
|
139
|
+
f"[{mem_style}]{mem_pct}%[/{mem_style}]",
|
|
140
|
+
f"[{mem_style}]{bar}[/{mem_style}]"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
console.print(table)
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _visualize_disk_usage(data: str) -> None:
|
|
147
|
+
"""Visualize disk usage with progress bar"""
|
|
148
|
+
# Parse: "Disk /tmp: 110.0GB used / 260.0GB total (42.3% used, 136.8GB free)"
|
|
149
|
+
match = re.search(
|
|
150
|
+
r'Disk\s+(.+?):\s+([0-9.]+)GB\s+used\s+/\s+([0-9.]+)GB\s+total\s+\(([0-9.]+)%\s+used,\s+([0-9.]+)GB\s+free\)',
|
|
151
|
+
data
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if not match:
|
|
155
|
+
console.print(f" → {data}", style="cyan")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
path, used, total, pct, free = match.groups()
|
|
159
|
+
used_f, total_f, pct_f, free_f = float(used), float(total), float(pct), float(free)
|
|
160
|
+
|
|
161
|
+
# Color code based on usage
|
|
162
|
+
if pct_f > 90:
|
|
163
|
+
color = "red"
|
|
164
|
+
emoji = "🔴"
|
|
165
|
+
elif pct_f > 75:
|
|
166
|
+
color = "yellow"
|
|
167
|
+
emoji = "🟡"
|
|
168
|
+
else:
|
|
169
|
+
color = "green"
|
|
170
|
+
emoji = "🟢"
|
|
171
|
+
|
|
172
|
+
# Create visual bar
|
|
173
|
+
bar_width = 40
|
|
174
|
+
filled = int((pct_f / 100) * bar_width)
|
|
175
|
+
bar = "█" * filled + "░" * (bar_width - filled)
|
|
176
|
+
|
|
177
|
+
# Build panel content
|
|
178
|
+
content = f"""
|
|
179
|
+
[bold cyan]Path:[/bold cyan] {path}
|
|
180
|
+
|
|
181
|
+
[bold {color}]{emoji} {pct_f:.1f}% Used[/bold {color}]
|
|
182
|
+
|
|
183
|
+
[{color}]{bar}[/{color}]
|
|
184
|
+
|
|
185
|
+
[bold]Used:[/bold] {used_f:.1f} GB [bold]Free:[/bold] {free_f:.1f} GB [bold]Total:[/bold] {total_f:.1f} GB
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
panel = Panel(
|
|
189
|
+
content.strip(),
|
|
190
|
+
title="💾 Disk Usage",
|
|
191
|
+
border_style=color,
|
|
192
|
+
box=box.ROUNDED
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
console.print(panel)
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _visualize_system_summary(data: str) -> None:
|
|
199
|
+
"""Visualize system resource summary"""
|
|
200
|
+
lines = data.strip().split("\n")
|
|
201
|
+
|
|
202
|
+
table = Table(
|
|
203
|
+
title="🖥️ System Resources",
|
|
204
|
+
box=box.ROUNDED,
|
|
205
|
+
show_header=True,
|
|
206
|
+
header_style="bold magenta"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
table.add_column("Resource", style="cyan", width=12)
|
|
210
|
+
table.add_column("Usage", style="yellow", justify="right", width=15)
|
|
211
|
+
table.add_column("Visual", width=25)
|
|
212
|
+
table.add_column("Details", style="dim")
|
|
213
|
+
|
|
214
|
+
for line in lines:
|
|
215
|
+
if line.startswith("CPU:"):
|
|
216
|
+
# Parse: "CPU: 5.1% used (2 cores)"
|
|
217
|
+
match = re.search(r'CPU:\s+([0-9.]+)%\s+used\s+\((\d+)\s+cores?\)', line)
|
|
218
|
+
if match:
|
|
219
|
+
pct, cores = match.groups()
|
|
220
|
+
pct_f = float(pct)
|
|
221
|
+
bar = "█" * int(pct_f / 4) + "░" * (25 - int(pct_f / 4))
|
|
222
|
+
|
|
223
|
+
color = "red" if pct_f > 80 else "yellow" if pct_f > 50 else "green"
|
|
224
|
+
|
|
225
|
+
table.add_row(
|
|
226
|
+
"CPU",
|
|
227
|
+
f"[bold {color}]{pct}%[/bold {color}]",
|
|
228
|
+
f"[{color}]{bar}[/{color}]",
|
|
229
|
+
f"{cores} cores"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
elif line.startswith("Memory:"):
|
|
233
|
+
# Parse: "Memory: 1.5GB / 3.7GB (52.9% used, 1.7GB free)"
|
|
234
|
+
match = re.search(r'Memory:\s+([0-9.]+)GB\s+/\s+([0-9.]+)GB\s+\(([0-9.]+)%\s+used,\s+([0-9.]+)GB\s+free\)', line)
|
|
235
|
+
if match:
|
|
236
|
+
used, total, pct, free = match.groups()
|
|
237
|
+
pct_f = float(pct)
|
|
238
|
+
bar = "█" * int(pct_f / 4) + "░" * (25 - int(pct_f / 4))
|
|
239
|
+
|
|
240
|
+
color = "red" if pct_f > 90 else "yellow" if pct_f > 70 else "green"
|
|
241
|
+
|
|
242
|
+
table.add_row(
|
|
243
|
+
"Memory",
|
|
244
|
+
f"[bold {color}]{pct}%[/bold {color}]",
|
|
245
|
+
f"[{color}]{bar}[/{color}]",
|
|
246
|
+
f"{used}GB / {total}GB"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
elif line.startswith("Disk:"):
|
|
250
|
+
# Parse: "Disk: 110.0GB / 260.0GB (42.3% used, 136.8GB free)"
|
|
251
|
+
match = re.search(r'Disk:\s+([0-9.]+)GB\s+/\s+([0-9.]+)GB\s+\(([0-9.]+)%\s+used,\s+([0-9.]+)GB\s+free\)', line)
|
|
252
|
+
if match:
|
|
253
|
+
used, total, pct, free = match.groups()
|
|
254
|
+
pct_f = float(pct)
|
|
255
|
+
bar = "█" * int(pct_f / 4) + "░" * (25 - int(pct_f / 4))
|
|
256
|
+
|
|
257
|
+
color = "red" if pct_f > 90 else "yellow" if pct_f > 75 else "green"
|
|
258
|
+
|
|
259
|
+
table.add_row(
|
|
260
|
+
"Disk",
|
|
261
|
+
f"[bold {color}]{pct}%[/bold {color}]",
|
|
262
|
+
f"[{color}]{bar}[/{color}]",
|
|
263
|
+
f"{used}GB / {total}GB"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
console.print(table)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _visualize_file_list(data: str) -> None:
|
|
270
|
+
"""Visualize file listing"""
|
|
271
|
+
# Try to parse as list first
|
|
272
|
+
if data.startswith('[') and data.endswith(']'):
|
|
273
|
+
try:
|
|
274
|
+
files = eval(data) # Safe here since we control the input
|
|
275
|
+
if isinstance(files, list):
|
|
276
|
+
tree = Tree("📁 Files", guide_style="dim")
|
|
277
|
+
|
|
278
|
+
for item in files:
|
|
279
|
+
if isinstance(item, str):
|
|
280
|
+
# Detect file vs directory
|
|
281
|
+
if '.' in item:
|
|
282
|
+
tree.add(f"📄 [cyan]{item}[/cyan]")
|
|
283
|
+
else:
|
|
284
|
+
tree.add(f"📁 [green]{item}/[/green]")
|
|
285
|
+
|
|
286
|
+
console.print(tree)
|
|
287
|
+
return
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
# Fallback to plain display
|
|
292
|
+
console.print(f" → {data}", style="cyan")
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def _visualize_percentage(data: str) -> None:
|
|
296
|
+
"""Visualize percentage values"""
|
|
297
|
+
console.print(f" → {data}", style="cyan")
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _visualize_key_value(data: str) -> None:
|
|
301
|
+
"""Visualize key-value pairs as a table"""
|
|
302
|
+
lines = [line.strip() for line in data.split("\n") if line.strip() and ":" in line]
|
|
303
|
+
|
|
304
|
+
if len(lines) < 2:
|
|
305
|
+
console.print(f" → {data}", style="cyan")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
table = Table(box=box.SIMPLE, show_header=False)
|
|
309
|
+
table.add_column("Key", style="cyan", width=20)
|
|
310
|
+
table.add_column("Value", style="white")
|
|
311
|
+
|
|
312
|
+
for line in lines:
|
|
313
|
+
if ":" in line:
|
|
314
|
+
key, value = line.split(":", 1)
|
|
315
|
+
table.add_row(key.strip(), value.strip())
|
|
316
|
+
|
|
317
|
+
console.print(table)
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _visualize_dict(data: Dict, context: Optional[str]) -> None:
|
|
321
|
+
"""Visualize dictionary as formatted JSON or table"""
|
|
322
|
+
# Try to display as table if simple key-value pairs
|
|
323
|
+
if all(isinstance(v, (str, int, float, bool, type(None))) for v in data.values()):
|
|
324
|
+
table = Table(box=box.SIMPLE, show_header=False)
|
|
325
|
+
table.add_column("Key", style="cyan", width=20)
|
|
326
|
+
table.add_column("Value", style="white")
|
|
327
|
+
|
|
328
|
+
for key, value in data.items():
|
|
329
|
+
table.add_row(str(key), str(value))
|
|
330
|
+
|
|
331
|
+
console.print(table)
|
|
332
|
+
else:
|
|
333
|
+
# Complex dict - show as formatted JSON
|
|
334
|
+
syntax = Syntax(
|
|
335
|
+
json.dumps(data, indent=2),
|
|
336
|
+
"json",
|
|
337
|
+
theme="monokai",
|
|
338
|
+
word_wrap=True
|
|
339
|
+
)
|
|
340
|
+
console.print(syntax)
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def _visualize_list(data: List, context: Optional[str]) -> None:
|
|
344
|
+
"""Visualize list as table or tree"""
|
|
345
|
+
if not data:
|
|
346
|
+
console.print(" → (empty)", style="dim")
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
# If list of dicts, create table
|
|
350
|
+
if all(isinstance(item, dict) for item in data):
|
|
351
|
+
# Get all unique keys
|
|
352
|
+
keys = set()
|
|
353
|
+
for item in data:
|
|
354
|
+
keys.update(item.keys())
|
|
355
|
+
keys = sorted(keys)
|
|
356
|
+
|
|
357
|
+
table = Table(
|
|
358
|
+
box=box.ROUNDED,
|
|
359
|
+
show_header=True,
|
|
360
|
+
header_style="bold magenta"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
for key in keys:
|
|
364
|
+
table.add_column(str(key), style="cyan")
|
|
365
|
+
|
|
366
|
+
for item in data:
|
|
367
|
+
table.add_row(*[str(item.get(key, "")) for key in keys])
|
|
368
|
+
|
|
369
|
+
console.print(table)
|
|
370
|
+
|
|
371
|
+
# If list of strings, show as tree
|
|
372
|
+
elif all(isinstance(item, str) for item in data):
|
|
373
|
+
tree = Tree("📋 Items", guide_style="dim")
|
|
374
|
+
for item in data:
|
|
375
|
+
tree.add(f"• [cyan]{item}[/cyan]")
|
|
376
|
+
console.print(tree)
|
|
377
|
+
|
|
378
|
+
# Otherwise, show as formatted list
|
|
379
|
+
else:
|
|
380
|
+
for i, item in enumerate(data, 1):
|
|
381
|
+
console.print(f" {i}. {item}", style="cyan")
|