htcli 1.1.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.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTCLI panel components.
|
|
3
|
+
Provides enhanced panels for content display.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from rich.layout import Layout
|
|
9
|
+
from rich.live import Live
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
from ..colors import HTCLIColors
|
|
13
|
+
from ..display import get_console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HTCLIPanel:
|
|
17
|
+
"""Enhanced panel for HTCLI operations."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
content: Any,
|
|
22
|
+
title: str = None,
|
|
23
|
+
border_style: str = "htcli.panel.border",
|
|
24
|
+
padding: tuple = (1, 2),
|
|
25
|
+
expand: bool = True,
|
|
26
|
+
highlight: bool = False,
|
|
27
|
+
):
|
|
28
|
+
self.panel = Panel(
|
|
29
|
+
content,
|
|
30
|
+
title=title,
|
|
31
|
+
border_style=border_style,
|
|
32
|
+
padding=padding,
|
|
33
|
+
expand=expand,
|
|
34
|
+
highlight=highlight,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def info_panel(cls, content: Any, title: str = "ℹ️ Information"):
|
|
39
|
+
"""Create an info panel."""
|
|
40
|
+
return cls(
|
|
41
|
+
content,
|
|
42
|
+
title=title,
|
|
43
|
+
border_style="blue",
|
|
44
|
+
padding=(1, 2),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def success_panel(cls, content: Any, title: str = "✅ Success"):
|
|
49
|
+
"""Create a success panel."""
|
|
50
|
+
return cls(
|
|
51
|
+
content,
|
|
52
|
+
title=title,
|
|
53
|
+
border_style="green",
|
|
54
|
+
padding=(1, 2),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def warning_panel(cls, content: Any, title: str = "⚠️ Warning"):
|
|
59
|
+
"""Create a warning panel."""
|
|
60
|
+
return cls(
|
|
61
|
+
content,
|
|
62
|
+
title=title,
|
|
63
|
+
border_style="yellow",
|
|
64
|
+
padding=(1, 2),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def error_panel(cls, content: Any, title: str = "❌ Error"):
|
|
69
|
+
"""Create an error panel."""
|
|
70
|
+
return cls(
|
|
71
|
+
content,
|
|
72
|
+
title=title,
|
|
73
|
+
border_style="red",
|
|
74
|
+
padding=(1, 2),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def render(self, console=None):
|
|
78
|
+
"""Render the panel."""
|
|
79
|
+
if console is None:
|
|
80
|
+
console_instance = get_console()
|
|
81
|
+
# HTCLIConsole exposes `print`
|
|
82
|
+
console_instance.print(self.panel)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Support both HTCLIConsole and raw Rich Console objects
|
|
86
|
+
if hasattr(console, "print"):
|
|
87
|
+
console.print(self.panel)
|
|
88
|
+
elif hasattr(console, "console") and hasattr(console.console, "print"):
|
|
89
|
+
console.console.print(self.panel)
|
|
90
|
+
else:
|
|
91
|
+
# Fallback to global console if provided object is unexpected
|
|
92
|
+
get_console().print(self.panel)
|
|
93
|
+
|
|
94
|
+
def __rich__(self):
|
|
95
|
+
"""Rich renderable."""
|
|
96
|
+
return self.panel
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class HTCLIStatus:
|
|
100
|
+
"""Real-time status display for HTCLI operations."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, title: str = "Status"):
|
|
103
|
+
self.title = title
|
|
104
|
+
self.layout = Layout()
|
|
105
|
+
self.layout.split_column(
|
|
106
|
+
Layout(name="header", size=3),
|
|
107
|
+
Layout(name="main", size=10),
|
|
108
|
+
Layout(name="footer", size=3),
|
|
109
|
+
)
|
|
110
|
+
self.layout["header"].update(Panel(title, style="bold cyan"))
|
|
111
|
+
self.layout["footer"].update(Panel("", style="dim"))
|
|
112
|
+
|
|
113
|
+
def update_main(self, content: Any):
|
|
114
|
+
"""Update the main content area."""
|
|
115
|
+
self.layout["main"].update(content)
|
|
116
|
+
|
|
117
|
+
def update_footer(self, content: Any):
|
|
118
|
+
"""Update the footer area."""
|
|
119
|
+
self.layout["footer"].update(content)
|
|
120
|
+
|
|
121
|
+
def render(self):
|
|
122
|
+
"""Render the status display."""
|
|
123
|
+
console = get_console()
|
|
124
|
+
console.print(self.layout)
|
|
125
|
+
|
|
126
|
+
def __rich__(self):
|
|
127
|
+
"""Rich renderable."""
|
|
128
|
+
return self.layout
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Specialized panel factory functions
|
|
132
|
+
def create_welcome_panel() -> HTCLIPanel:
|
|
133
|
+
"""Create a welcome panel."""
|
|
134
|
+
welcome_text = """
|
|
135
|
+
Welcome to HTCLI - Your Gateway to the Hypertensor Blockchain
|
|
136
|
+
|
|
137
|
+
🌐 Manage subnets and nodes
|
|
138
|
+
💰 Handle wallets and transactions
|
|
139
|
+
📊 Monitor network statistics
|
|
140
|
+
⚡ Execute blockchain operations
|
|
141
|
+
|
|
142
|
+
Use 'htcli --help' to see all available commands.
|
|
143
|
+
"""
|
|
144
|
+
return HTCLIPanel.info_panel(welcome_text, "🚀 HTCLI Welcome")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def create_help_panel() -> HTCLIPanel:
|
|
148
|
+
"""Create a help panel."""
|
|
149
|
+
help_text = """
|
|
150
|
+
Available Commands:
|
|
151
|
+
|
|
152
|
+
📊 subnet - Subnet management operations
|
|
153
|
+
💰 wallet - Wallet and key management
|
|
154
|
+
📡 node - Node registration and management
|
|
155
|
+
⚡ stake - Staking operations
|
|
156
|
+
🔗 chain - Chain information and statistics
|
|
157
|
+
⚙️ config - Configuration management
|
|
158
|
+
|
|
159
|
+
For detailed help on any command, use:
|
|
160
|
+
htcli <command> --help
|
|
161
|
+
"""
|
|
162
|
+
return HTCLIPanel.info_panel(help_text, "📚 HTCLI Help")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_error_summary_panel(errors: list[str]) -> HTCLIPanel:
|
|
166
|
+
"""Create an error summary panel."""
|
|
167
|
+
error_text = "\n".join([f"• {error}" for error in errors])
|
|
168
|
+
return HTCLIPanel.error_panel(error_text, "❌ Errors Found")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def create_success_summary_panel(successes: list[str]) -> HTCLIPanel:
|
|
172
|
+
"""Create a success summary panel."""
|
|
173
|
+
success_text = "\n".join([f"✅ {success}" for success in successes])
|
|
174
|
+
return HTCLIPanel.success_panel(success_text, "🎉 Operations Completed")
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTCLI progress components.
|
|
3
|
+
Provides enhanced progress bars for operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import (
|
|
11
|
+
BarColumn,
|
|
12
|
+
Progress,
|
|
13
|
+
SpinnerColumn,
|
|
14
|
+
TaskID,
|
|
15
|
+
TaskProgressColumn,
|
|
16
|
+
TextColumn,
|
|
17
|
+
TimeRemainingColumn,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ..display import get_console
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HTCLIProgress:
|
|
24
|
+
"""Enhanced progress bar for HTCLI operations."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
console: Console = None,
|
|
29
|
+
refresh_per_second: float = 10,
|
|
30
|
+
transient: bool = False,
|
|
31
|
+
):
|
|
32
|
+
# Use the provided console or get the default one
|
|
33
|
+
# Rich Progress uses Live display internally which handles line updates automatically
|
|
34
|
+
# The SpinnerColumn will automatically animate while the progress bar updates
|
|
35
|
+
if console is None:
|
|
36
|
+
console = get_console().console
|
|
37
|
+
|
|
38
|
+
self.console = console
|
|
39
|
+
# SpinnerColumn animates automatically - refresh_per_second controls animation speed
|
|
40
|
+
# Higher refresh rate = smoother spinner animation
|
|
41
|
+
self.progress = Progress(
|
|
42
|
+
SpinnerColumn(), # Spinner animates automatically while progress updates
|
|
43
|
+
TextColumn("[progress.description]{task.description}"),
|
|
44
|
+
BarColumn(
|
|
45
|
+
bar_width=None,
|
|
46
|
+
style="htcli.progress.bar",
|
|
47
|
+
complete_style="htcli.status.success",
|
|
48
|
+
finished_style="htcli.status.success",
|
|
49
|
+
),
|
|
50
|
+
TaskProgressColumn(),
|
|
51
|
+
TimeRemainingColumn(),
|
|
52
|
+
console=self.console,
|
|
53
|
+
transient=transient, # Allow control over whether progress disappears after completion
|
|
54
|
+
refresh_per_second=refresh_per_second, # Controls spinner animation speed
|
|
55
|
+
# Ensure progress is visible even for quick operations
|
|
56
|
+
auto_refresh=True,
|
|
57
|
+
)
|
|
58
|
+
self._tasks: dict[str, TaskID] = {}
|
|
59
|
+
self._running = False
|
|
60
|
+
|
|
61
|
+
def __enter__(self):
|
|
62
|
+
self._running = True
|
|
63
|
+
self.progress.start()
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
67
|
+
self._running = False
|
|
68
|
+
# Stop the progress display - this cleans up the Live display
|
|
69
|
+
self.progress.stop()
|
|
70
|
+
# Print a newline to ensure clean separation from progress bar output
|
|
71
|
+
# This prevents the next output from appearing on the same line as the progress bar
|
|
72
|
+
self.console.print()
|
|
73
|
+
|
|
74
|
+
def add_task(
|
|
75
|
+
self, description: str, total: Optional[float] = None, task_id: str = None
|
|
76
|
+
) -> TaskID:
|
|
77
|
+
"""Add a new task to the progress bar."""
|
|
78
|
+
if task_id is None:
|
|
79
|
+
task_id = description
|
|
80
|
+
task = self.progress.add_task(description, total=total)
|
|
81
|
+
self._tasks[task_id] = task
|
|
82
|
+
return task
|
|
83
|
+
|
|
84
|
+
def update(
|
|
85
|
+
self,
|
|
86
|
+
task_id: Union[TaskID, str],
|
|
87
|
+
advance: float = 1,
|
|
88
|
+
description: str = None,
|
|
89
|
+
total: float = None,
|
|
90
|
+
):
|
|
91
|
+
"""Update a task's progress."""
|
|
92
|
+
if isinstance(task_id, str):
|
|
93
|
+
task_id = self._tasks.get(task_id)
|
|
94
|
+
if task_id is not None:
|
|
95
|
+
self.progress.update(
|
|
96
|
+
task_id, advance=advance, description=description, total=total
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def set_description(self, task_id: Union[TaskID, str], description: str):
|
|
100
|
+
"""Set task description."""
|
|
101
|
+
if isinstance(task_id, str):
|
|
102
|
+
task_id = self._tasks.get(task_id)
|
|
103
|
+
if task_id is not None:
|
|
104
|
+
self.progress.update(task_id, description=description)
|
|
105
|
+
|
|
106
|
+
def complete_task(self, task_id: Union[TaskID, str]):
|
|
107
|
+
"""Mark a task as completed."""
|
|
108
|
+
if isinstance(task_id, str):
|
|
109
|
+
task_id = self._tasks.get(task_id)
|
|
110
|
+
if task_id is not None:
|
|
111
|
+
self.progress.update(task_id, completed=True)
|
|
112
|
+
|
|
113
|
+
@contextmanager
|
|
114
|
+
def task(
|
|
115
|
+
self, description: str, total: Optional[float] = None, task_id: str = None
|
|
116
|
+
):
|
|
117
|
+
"""Context manager for individual tasks."""
|
|
118
|
+
task = self.add_task(description, total, task_id)
|
|
119
|
+
try:
|
|
120
|
+
yield task
|
|
121
|
+
finally:
|
|
122
|
+
self.complete_task(task)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class HTCLILoadingContext:
|
|
126
|
+
"""Context manager for loading operations with spinner indication.
|
|
127
|
+
|
|
128
|
+
Uses HTCLISpinner for indeterminate loading operations (no known completion).
|
|
129
|
+
This ensures the spinner updates in place on the same line rather than
|
|
130
|
+
printing multiple lines.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
message: str = "Loading...",
|
|
136
|
+
show_progress: bool = True,
|
|
137
|
+
transient: bool = True,
|
|
138
|
+
):
|
|
139
|
+
self.message = message
|
|
140
|
+
self.show_progress = show_progress
|
|
141
|
+
self.transient = transient
|
|
142
|
+
self.spinner = None
|
|
143
|
+
|
|
144
|
+
def __enter__(self):
|
|
145
|
+
if self.show_progress:
|
|
146
|
+
# Use HTCLISpinner for simple loading operations
|
|
147
|
+
# This properly uses Rich's Live display to update in place
|
|
148
|
+
from .spinners import HTCLISpinner
|
|
149
|
+
|
|
150
|
+
self.spinner = HTCLISpinner(
|
|
151
|
+
text=self.message,
|
|
152
|
+
spinner="dots",
|
|
153
|
+
transient=self.transient,
|
|
154
|
+
refresh_per_second=12,
|
|
155
|
+
)
|
|
156
|
+
self.spinner.__enter__()
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
160
|
+
if self.spinner:
|
|
161
|
+
self.spinner.__exit__(exc_type, exc_val, exc_tb)
|
|
162
|
+
|
|
163
|
+
def update_message(self, message: str):
|
|
164
|
+
"""Update the loading message."""
|
|
165
|
+
if self.spinner:
|
|
166
|
+
self.spinner.update(message)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTCLI spinner components.
|
|
3
|
+
Provides enhanced spinners for loading operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
from rich.spinner import Spinner
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from ..display import get_console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTCLISpinner:
|
|
16
|
+
"""Enhanced spinner for HTCLI operations using Rich's Spinner with Live display."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
text: str = "Loading...",
|
|
21
|
+
spinner: str = "dots",
|
|
22
|
+
style: str = "htcli.spinner",
|
|
23
|
+
refresh_per_second: float = 12,
|
|
24
|
+
transient: bool = True,
|
|
25
|
+
):
|
|
26
|
+
# Get console and ensure it's configured for terminal output
|
|
27
|
+
base_console = get_console().console
|
|
28
|
+
# Create a console instance specifically for the Live display
|
|
29
|
+
# This ensures proper terminal detection and in-place updates
|
|
30
|
+
self.console = base_console
|
|
31
|
+
self.text = text
|
|
32
|
+
self.spinner_name = spinner
|
|
33
|
+
self.style = style
|
|
34
|
+
self.refresh_per_second = refresh_per_second
|
|
35
|
+
self.transient = transient
|
|
36
|
+
self._spinner = None
|
|
37
|
+
self._live = None
|
|
38
|
+
self._running = False
|
|
39
|
+
|
|
40
|
+
def __enter__(self):
|
|
41
|
+
# Create Rich Spinner with text
|
|
42
|
+
self._spinner = Spinner(
|
|
43
|
+
name=self.spinner_name,
|
|
44
|
+
text=Text(self.text, style=self.style),
|
|
45
|
+
speed=1.0,
|
|
46
|
+
)
|
|
47
|
+
# Create Live display with the spinner
|
|
48
|
+
# For in-place updates (spinning on the same line), we need:
|
|
49
|
+
# - screen=False: Update on the same line using carriage return
|
|
50
|
+
# - auto_refresh=True: Automatically refresh the display
|
|
51
|
+
# - console must be a terminal (not a file/pipe)
|
|
52
|
+
# If the console is not a terminal, Live will print new lines instead of updating in place
|
|
53
|
+
import sys
|
|
54
|
+
# Only use in-place updates if stdout is a terminal
|
|
55
|
+
# Otherwise, fall back to simple printing (though this shouldn't happen in normal CLI usage)
|
|
56
|
+
is_terminal = sys.stdout.isatty() and self.console.is_terminal
|
|
57
|
+
|
|
58
|
+
self._live = Live(
|
|
59
|
+
self._spinner,
|
|
60
|
+
console=self.console,
|
|
61
|
+
refresh_per_second=self.refresh_per_second,
|
|
62
|
+
transient=self.transient,
|
|
63
|
+
screen=False, # Update in place on same line (uses \r carriage return)
|
|
64
|
+
auto_refresh=True,
|
|
65
|
+
vertical_overflow="visible",
|
|
66
|
+
)
|
|
67
|
+
self._running = True
|
|
68
|
+
self._live.start()
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
72
|
+
self._running = False
|
|
73
|
+
if self._live:
|
|
74
|
+
self._live.stop()
|
|
75
|
+
# Always add a newline after stopping spinner to avoid next output
|
|
76
|
+
# appearing on the same line as the spinner.
|
|
77
|
+
self.console.print()
|
|
78
|
+
|
|
79
|
+
def update(self, text: str):
|
|
80
|
+
"""Update the spinner text and render it."""
|
|
81
|
+
if self._spinner and self._running:
|
|
82
|
+
self._spinner.update(text=Text(text, style=self.style))
|
|
83
|
+
|
|
84
|
+
@contextmanager
|
|
85
|
+
def step(self, step_text: str):
|
|
86
|
+
"""Context manager for individual steps."""
|
|
87
|
+
original_text = self.text
|
|
88
|
+
self.update(f"{original_text} - {step_text}")
|
|
89
|
+
try:
|
|
90
|
+
yield
|
|
91
|
+
finally:
|
|
92
|
+
self.update(original_text)
|