shotgun-sh 0.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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/__init__.py +5 -0
- shotgun/agents/__init__.py +1 -0
- shotgun/agents/agent_manager.py +651 -0
- shotgun/agents/common.py +549 -0
- shotgun/agents/config/__init__.py +13 -0
- shotgun/agents/config/constants.py +17 -0
- shotgun/agents/config/manager.py +294 -0
- shotgun/agents/config/models.py +185 -0
- shotgun/agents/config/provider.py +206 -0
- shotgun/agents/conversation_history.py +106 -0
- shotgun/agents/conversation_manager.py +105 -0
- shotgun/agents/export.py +96 -0
- shotgun/agents/history/__init__.py +5 -0
- shotgun/agents/history/compaction.py +85 -0
- shotgun/agents/history/constants.py +19 -0
- shotgun/agents/history/context_extraction.py +108 -0
- shotgun/agents/history/history_building.py +104 -0
- shotgun/agents/history/history_processors.py +426 -0
- shotgun/agents/history/message_utils.py +84 -0
- shotgun/agents/history/token_counting.py +429 -0
- shotgun/agents/history/token_estimation.py +138 -0
- shotgun/agents/messages.py +35 -0
- shotgun/agents/models.py +275 -0
- shotgun/agents/plan.py +98 -0
- shotgun/agents/research.py +108 -0
- shotgun/agents/specify.py +98 -0
- shotgun/agents/tasks.py +96 -0
- shotgun/agents/tools/__init__.py +34 -0
- shotgun/agents/tools/codebase/__init__.py +28 -0
- shotgun/agents/tools/codebase/codebase_shell.py +256 -0
- shotgun/agents/tools/codebase/directory_lister.py +141 -0
- shotgun/agents/tools/codebase/file_read.py +144 -0
- shotgun/agents/tools/codebase/models.py +252 -0
- shotgun/agents/tools/codebase/query_graph.py +67 -0
- shotgun/agents/tools/codebase/retrieve_code.py +81 -0
- shotgun/agents/tools/file_management.py +218 -0
- shotgun/agents/tools/user_interaction.py +37 -0
- shotgun/agents/tools/web_search/__init__.py +60 -0
- shotgun/agents/tools/web_search/anthropic.py +144 -0
- shotgun/agents/tools/web_search/gemini.py +85 -0
- shotgun/agents/tools/web_search/openai.py +98 -0
- shotgun/agents/tools/web_search/utils.py +20 -0
- shotgun/build_constants.py +20 -0
- shotgun/cli/__init__.py +1 -0
- shotgun/cli/codebase/__init__.py +5 -0
- shotgun/cli/codebase/commands.py +202 -0
- shotgun/cli/codebase/models.py +21 -0
- shotgun/cli/config.py +275 -0
- shotgun/cli/export.py +81 -0
- shotgun/cli/models.py +10 -0
- shotgun/cli/plan.py +73 -0
- shotgun/cli/research.py +85 -0
- shotgun/cli/specify.py +69 -0
- shotgun/cli/tasks.py +78 -0
- shotgun/cli/update.py +152 -0
- shotgun/cli/utils.py +25 -0
- shotgun/codebase/__init__.py +12 -0
- shotgun/codebase/core/__init__.py +46 -0
- shotgun/codebase/core/change_detector.py +358 -0
- shotgun/codebase/core/code_retrieval.py +243 -0
- shotgun/codebase/core/ingestor.py +1497 -0
- shotgun/codebase/core/language_config.py +297 -0
- shotgun/codebase/core/manager.py +1662 -0
- shotgun/codebase/core/nl_query.py +331 -0
- shotgun/codebase/core/parser_loader.py +128 -0
- shotgun/codebase/models.py +111 -0
- shotgun/codebase/service.py +206 -0
- shotgun/logging_config.py +227 -0
- shotgun/main.py +167 -0
- shotgun/posthog_telemetry.py +158 -0
- shotgun/prompts/__init__.py +5 -0
- shotgun/prompts/agents/__init__.py +1 -0
- shotgun/prompts/agents/export.j2 +350 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +87 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +37 -0
- shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +26 -0
- shotgun/prompts/agents/plan.j2 +144 -0
- shotgun/prompts/agents/research.j2 +69 -0
- shotgun/prompts/agents/specify.j2 +51 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +19 -0
- shotgun/prompts/agents/state/system_state.j2 +31 -0
- shotgun/prompts/agents/tasks.j2 +143 -0
- shotgun/prompts/codebase/__init__.py +1 -0
- shotgun/prompts/codebase/cypher_query_patterns.j2 +223 -0
- shotgun/prompts/codebase/cypher_system.j2 +28 -0
- shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
- shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
- shotgun/prompts/codebase/partials/graph_schema.j2 +30 -0
- shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
- shotgun/prompts/history/__init__.py +1 -0
- shotgun/prompts/history/incremental_summarization.j2 +53 -0
- shotgun/prompts/history/summarization.j2 +46 -0
- shotgun/prompts/loader.py +140 -0
- shotgun/py.typed +0 -0
- shotgun/sdk/__init__.py +13 -0
- shotgun/sdk/codebase.py +219 -0
- shotgun/sdk/exceptions.py +17 -0
- shotgun/sdk/models.py +189 -0
- shotgun/sdk/services.py +23 -0
- shotgun/sentry_telemetry.py +87 -0
- shotgun/telemetry.py +93 -0
- shotgun/tui/__init__.py +0 -0
- shotgun/tui/app.py +116 -0
- shotgun/tui/commands/__init__.py +76 -0
- shotgun/tui/components/prompt_input.py +69 -0
- shotgun/tui/components/spinner.py +86 -0
- shotgun/tui/components/splash.py +25 -0
- shotgun/tui/components/vertical_tail.py +13 -0
- shotgun/tui/screens/chat.py +782 -0
- shotgun/tui/screens/chat.tcss +43 -0
- shotgun/tui/screens/chat_screen/__init__.py +0 -0
- shotgun/tui/screens/chat_screen/command_providers.py +219 -0
- shotgun/tui/screens/chat_screen/hint_message.py +40 -0
- shotgun/tui/screens/chat_screen/history.py +221 -0
- shotgun/tui/screens/directory_setup.py +113 -0
- shotgun/tui/screens/provider_config.py +221 -0
- shotgun/tui/screens/splash.py +31 -0
- shotgun/tui/styles.tcss +10 -0
- shotgun/tui/utils/__init__.py +5 -0
- shotgun/tui/utils/mode_progress.py +257 -0
- shotgun/utils/__init__.py +5 -0
- shotgun/utils/env_utils.py +35 -0
- shotgun/utils/file_system_utils.py +36 -0
- shotgun/utils/update_checker.py +375 -0
- shotgun_sh-0.1.0.dist-info/METADATA +466 -0
- shotgun_sh-0.1.0.dist-info/RECORD +130 -0
- shotgun_sh-0.1.0.dist-info/WHEEL +4 -0
- shotgun_sh-0.1.0.dist-info/entry_points.txt +2 -0
- shotgun_sh-0.1.0.dist-info/licenses/LICENSE +21 -0
shotgun/tui/app.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from textual.app import App, SystemCommand
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
|
|
8
|
+
from shotgun.agents.config import ConfigManager, get_config_manager
|
|
9
|
+
from shotgun.logging_config import get_logger
|
|
10
|
+
from shotgun.tui.screens.splash import SplashScreen
|
|
11
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
|
+
from shotgun.utils.update_checker import check_for_updates_async
|
|
13
|
+
|
|
14
|
+
from .screens.chat import ChatScreen
|
|
15
|
+
from .screens.directory_setup import DirectorySetupScreen
|
|
16
|
+
from .screens.provider_config import ProviderConfigScreen
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShotgunApp(App[None]):
|
|
22
|
+
SCREENS = {
|
|
23
|
+
"chat": ChatScreen,
|
|
24
|
+
"provider_config": ProviderConfigScreen,
|
|
25
|
+
"directory_setup": DirectorySetupScreen,
|
|
26
|
+
}
|
|
27
|
+
BINDINGS = [
|
|
28
|
+
Binding("ctrl+c", "quit", "Quit the app"),
|
|
29
|
+
]
|
|
30
|
+
CSS_PATH = "styles.tcss"
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self, no_update_check: bool = False, continue_session: bool = False
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.config_manager: ConfigManager = get_config_manager()
|
|
37
|
+
self.no_update_check = no_update_check
|
|
38
|
+
self.continue_session = continue_session
|
|
39
|
+
self.update_notification: str | None = None
|
|
40
|
+
|
|
41
|
+
# Start async update check
|
|
42
|
+
if not no_update_check:
|
|
43
|
+
check_for_updates_async(callback=self._update_callback)
|
|
44
|
+
|
|
45
|
+
def _update_callback(self, notification: str) -> None:
|
|
46
|
+
"""Store update notification to show later."""
|
|
47
|
+
self.update_notification = notification
|
|
48
|
+
logger.debug(f"Update notification received: {notification}")
|
|
49
|
+
|
|
50
|
+
def on_mount(self) -> None:
|
|
51
|
+
self.theme = "gruvbox"
|
|
52
|
+
# Track TUI startup
|
|
53
|
+
from shotgun.posthog_telemetry import track_event
|
|
54
|
+
|
|
55
|
+
track_event("tui_started", {})
|
|
56
|
+
|
|
57
|
+
self.push_screen(
|
|
58
|
+
SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def refresh_startup_screen(self) -> None:
|
|
62
|
+
"""Push the appropriate screen based on configured providers."""
|
|
63
|
+
if not self.config_manager.has_any_provider_key():
|
|
64
|
+
if isinstance(self.screen, ProviderConfigScreen):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
self.push_screen(
|
|
68
|
+
"provider_config", callback=lambda _arg: self.refresh_startup_screen()
|
|
69
|
+
)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if not self.check_local_shotgun_directory_exists():
|
|
73
|
+
if isinstance(self.screen, DirectorySetupScreen):
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
self.push_screen(
|
|
77
|
+
"directory_setup", callback=lambda _arg: self.refresh_startup_screen()
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
if isinstance(self.screen, ChatScreen):
|
|
82
|
+
return
|
|
83
|
+
# Pass continue_session flag to ChatScreen
|
|
84
|
+
self.push_screen(ChatScreen(continue_session=self.continue_session))
|
|
85
|
+
|
|
86
|
+
def check_local_shotgun_directory_exists(self) -> bool:
|
|
87
|
+
shotgun_dir = get_shotgun_base_path()
|
|
88
|
+
return shotgun_dir.exists() and shotgun_dir.is_dir()
|
|
89
|
+
|
|
90
|
+
async def action_quit(self) -> None:
|
|
91
|
+
"""Override quit action to show update notification."""
|
|
92
|
+
if self.update_notification:
|
|
93
|
+
# Show notification before quitting
|
|
94
|
+
from rich.console import Console
|
|
95
|
+
|
|
96
|
+
console = Console()
|
|
97
|
+
console.print(f"\n[cyan]{self.update_notification}[/cyan]", style="bold")
|
|
98
|
+
self.exit()
|
|
99
|
+
|
|
100
|
+
def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
|
|
101
|
+
return [] # we don't want any system commands
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
|
105
|
+
"""Run the TUI application.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
no_update_check: If True, disable automatic update checks.
|
|
109
|
+
continue_session: If True, continue from previous conversation.
|
|
110
|
+
"""
|
|
111
|
+
app = ShotgunApp(no_update_check=no_update_check, continue_session=continue_session)
|
|
112
|
+
app.run(inline_no_clear=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
run()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Command handling for the TUI chat interface."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CommandHandler:
|
|
7
|
+
"""Handles slash commands in the TUI chat interface."""
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
"""Initialize the command handler with available commands."""
|
|
11
|
+
self.commands: dict[str, Callable[[], str]] = {
|
|
12
|
+
"help": self.get_help_text,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def is_command(self, text: str) -> bool:
|
|
16
|
+
"""Check if the text is a command (starts with /)."""
|
|
17
|
+
return text.strip().startswith("/")
|
|
18
|
+
|
|
19
|
+
def parse_command(self, text: str) -> str:
|
|
20
|
+
"""Extract the command name from the text."""
|
|
21
|
+
text = text.strip()
|
|
22
|
+
if not text.startswith("/"):
|
|
23
|
+
return ""
|
|
24
|
+
|
|
25
|
+
# Split on whitespace and get the command part
|
|
26
|
+
parts = text[1:].split()
|
|
27
|
+
return parts[0] if parts else ""
|
|
28
|
+
|
|
29
|
+
def handle_command(self, text: str) -> tuple[bool, str]:
|
|
30
|
+
"""
|
|
31
|
+
Handle a command and return success status and response text.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
text: The full command text including the leading /
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (success, response_text)
|
|
38
|
+
"""
|
|
39
|
+
if not self.is_command(text):
|
|
40
|
+
return False, ""
|
|
41
|
+
|
|
42
|
+
command = self.parse_command(text)
|
|
43
|
+
|
|
44
|
+
if command in self.commands:
|
|
45
|
+
response = self.commands[command]()
|
|
46
|
+
return True, response
|
|
47
|
+
else:
|
|
48
|
+
return False, self.get_error_message(command)
|
|
49
|
+
|
|
50
|
+
def get_help_text(self) -> str:
|
|
51
|
+
"""Return the help text for the /help command."""
|
|
52
|
+
return """📚 **Shotgun Help**
|
|
53
|
+
|
|
54
|
+
**Commands:**
|
|
55
|
+
• `/help` - Show this help message
|
|
56
|
+
|
|
57
|
+
**Keyboard Shortcuts:**
|
|
58
|
+
|
|
59
|
+
* `Enter` - Send message
|
|
60
|
+
* `Ctrl+P` - Open command palette
|
|
61
|
+
* `Shift+Tab` - Cycle agent modes
|
|
62
|
+
* `Ctrl+C` - Quit application
|
|
63
|
+
|
|
64
|
+
**Agent Modes:**
|
|
65
|
+
* **Research** - Research topics with web search and synthesize findings
|
|
66
|
+
* **Specify** - Create detailed specifications and requirements documents
|
|
67
|
+
* **Planning** - Create comprehensive, actionable plans with milestones
|
|
68
|
+
* **Tasks** - Generate specific, actionable tasks from research and plans
|
|
69
|
+
* **Export** - Export artifacts and findings to various formats
|
|
70
|
+
|
|
71
|
+
**Usage:**
|
|
72
|
+
Type your message and press Enter to chat with the AI. The AI will respond based on the current mode."""
|
|
73
|
+
|
|
74
|
+
def get_error_message(self, command: str) -> str:
|
|
75
|
+
"""Return a polite error message for unknown commands."""
|
|
76
|
+
return f"⚠️ Sorry, `/{command}` is not a recognized command. Type `/help` to see available commands."
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from textual import events
|
|
2
|
+
from textual.message import Message
|
|
3
|
+
from textual.widgets import TextArea
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PromptInput(TextArea):
|
|
7
|
+
"""A TextArea with a submit binding."""
|
|
8
|
+
|
|
9
|
+
DEFAULT_CSS = """
|
|
10
|
+
PromptInput {
|
|
11
|
+
outline: round $primary;
|
|
12
|
+
background: transparent;
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
|
|
17
|
+
if action != "copy":
|
|
18
|
+
return True
|
|
19
|
+
# run copy action if there is selected text
|
|
20
|
+
# otherwise, do nothing, so global ctrl+c still works.
|
|
21
|
+
return bool(self.selected_text)
|
|
22
|
+
|
|
23
|
+
class Submitted(Message):
|
|
24
|
+
"""A message to indicate that the text has been submitted."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, text: str) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.text = text
|
|
29
|
+
|
|
30
|
+
def action_submit(self) -> None:
|
|
31
|
+
"""An action to submit the text."""
|
|
32
|
+
self.post_message(self.Submitted(self.text))
|
|
33
|
+
|
|
34
|
+
async def _on_key(self, event: events.Key) -> None:
|
|
35
|
+
"""Handle key presses which correspond to document inserts."""
|
|
36
|
+
|
|
37
|
+
# Don't handle Enter key here - let the binding handle it
|
|
38
|
+
if event.key == "enter":
|
|
39
|
+
self.action_submit()
|
|
40
|
+
|
|
41
|
+
self._restart_blink()
|
|
42
|
+
|
|
43
|
+
if self.read_only:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
key = event.key
|
|
47
|
+
insert_values = {
|
|
48
|
+
"ctrl+j": "\n",
|
|
49
|
+
}
|
|
50
|
+
if self.tab_behavior == "indent":
|
|
51
|
+
if key == "escape":
|
|
52
|
+
event.stop()
|
|
53
|
+
event.prevent_default()
|
|
54
|
+
self.screen.focus_next()
|
|
55
|
+
return
|
|
56
|
+
if self.indent_type == "tabs":
|
|
57
|
+
insert_values["tab"] = "\t"
|
|
58
|
+
else:
|
|
59
|
+
insert_values["tab"] = " " * self._find_columns_to_next_tab_stop()
|
|
60
|
+
|
|
61
|
+
if event.is_printable or key in insert_values:
|
|
62
|
+
event.stop()
|
|
63
|
+
event.prevent_default()
|
|
64
|
+
insert = insert_values.get(key, event.character)
|
|
65
|
+
# `insert` is not None because event.character cannot be
|
|
66
|
+
# None because we've checked that it's printable.
|
|
67
|
+
assert insert is not None # noqa: S101
|
|
68
|
+
start, end = self.selection
|
|
69
|
+
self._replace_via_keyboard(insert, start, end)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Spinner component for showing loading/working state."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Container
|
|
5
|
+
from textual.css.query import NoMatches
|
|
6
|
+
from textual.reactive import reactive
|
|
7
|
+
from textual.timer import Timer
|
|
8
|
+
from textual.widget import Widget
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Spinner(Widget):
|
|
13
|
+
"""A spinner widget that shows a rotating animation when working."""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CSS = """
|
|
16
|
+
Spinner {
|
|
17
|
+
width: auto;
|
|
18
|
+
height: 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Spinner > Container {
|
|
22
|
+
width: auto;
|
|
23
|
+
height: 1;
|
|
24
|
+
layout: horizontal;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Spinner .spinner-icon {
|
|
28
|
+
width: 1;
|
|
29
|
+
margin-right: 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Spinner .spinner-text {
|
|
33
|
+
width: auto;
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Animation frames for the spinner
|
|
38
|
+
FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
39
|
+
|
|
40
|
+
text = reactive("Working...")
|
|
41
|
+
_frame_index = reactive(0)
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
text: str = "Working...",
|
|
46
|
+
*,
|
|
47
|
+
name: str | None = None,
|
|
48
|
+
id: str | None = None,
|
|
49
|
+
classes: str | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(name=name, id=id, classes=classes)
|
|
52
|
+
self.text = text
|
|
53
|
+
self._timer: Timer | None = None
|
|
54
|
+
|
|
55
|
+
def compose(self) -> ComposeResult:
|
|
56
|
+
"""Compose the spinner widget."""
|
|
57
|
+
with Container():
|
|
58
|
+
yield Static("", classes="spinner-icon")
|
|
59
|
+
yield Static(self.text, classes="spinner-text")
|
|
60
|
+
|
|
61
|
+
def on_mount(self) -> None:
|
|
62
|
+
"""Set up the animation timer when mounted."""
|
|
63
|
+
self._timer = self.set_interval(0.1, self._advance_frame)
|
|
64
|
+
|
|
65
|
+
def _advance_frame(self) -> None:
|
|
66
|
+
"""Advance to the next animation frame."""
|
|
67
|
+
self._frame_index = (self._frame_index + 1) % len(self.FRAMES)
|
|
68
|
+
self._update_display()
|
|
69
|
+
|
|
70
|
+
def _update_display(self) -> None:
|
|
71
|
+
"""Update the spinner display."""
|
|
72
|
+
try:
|
|
73
|
+
icon_widget = self.query_one(".spinner-icon", Static)
|
|
74
|
+
icon_widget.update(self.FRAMES[self._frame_index])
|
|
75
|
+
except NoMatches:
|
|
76
|
+
# Widget not mounted yet, ignore
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def watch_text(self, text: str) -> None:
|
|
80
|
+
"""React to changes in the text."""
|
|
81
|
+
try:
|
|
82
|
+
text_widget = self.query_one(".spinner-text", Static)
|
|
83
|
+
text_widget.update(text)
|
|
84
|
+
except NoMatches:
|
|
85
|
+
# Widget not mounted yet, ignore
|
|
86
|
+
return
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from textual.app import RenderResult
|
|
2
|
+
from textual.widgets import Static
|
|
3
|
+
|
|
4
|
+
ART = """
|
|
5
|
+
|
|
6
|
+
███████╗██╗ ██╗ ██████╗ ████████╗ ██████╗ ██╗ ██╗███╗ ██╗
|
|
7
|
+
██╔════╝██║ ██║██╔═══██╗╚══██╔══╝██╔════╝ ██║ ██║████╗ ██║
|
|
8
|
+
███████╗███████║██║ ██║ ██║ ██║ ███╗██║ ██║██╔██╗ ██║
|
|
9
|
+
╚════██║██╔══██║██║ ██║ ██║ ██║ ██║██║ ██║██║╚██╗██║
|
|
10
|
+
███████║██║ ██║╚██████╔╝ ██║ ╚██████╔╝╚██████╔╝██║ ╚████║
|
|
11
|
+
╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SplashWidget(Static):
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
SplashWidget {
|
|
19
|
+
text-align: center;
|
|
20
|
+
width: 64;
|
|
21
|
+
}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def render(self) -> RenderResult:
|
|
25
|
+
return ART
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from textual.containers import VerticalScroll
|
|
2
|
+
from textual.reactive import reactive
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class VerticalTail(VerticalScroll):
|
|
6
|
+
"""A vertical scroll container that automatically scrolls to the bottom when content is added."""
|
|
7
|
+
|
|
8
|
+
auto_scroll = reactive(True, layout=False)
|
|
9
|
+
|
|
10
|
+
def watch_auto_scroll(self, value: bool) -> None:
|
|
11
|
+
"""Handle auto_scroll property changes."""
|
|
12
|
+
if value:
|
|
13
|
+
self.scroll_end(animate=False)
|