termainer 0.4.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.
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import shutil
5
+ from typing import List
6
+
7
+ from textual.app import ComposeResult
8
+ from textual.binding import Binding
9
+ from textual.containers import Horizontal, Vertical
10
+ from textual.events import Resize
11
+ from textual.screen import Screen
12
+ from textual.widgets import Button, Static
13
+
14
+ from ..locale import _
15
+ from ..config import has_ssh_servers_configured
16
+ from ..server_manager import ServerManager
17
+ from ..version import VERSION
18
+ from .dashboard import Dashboard
19
+
20
+
21
+ _SERVER_ICONS = {
22
+ "docker": "[bold #4ade80]◇[/]",
23
+ "podman": "[bold #22d3ee]◇[/]",
24
+ "kubernetes": "[bold #22d3ee]◈[/]",
25
+ "openshift": "[bold #f87171]◈[/]",
26
+ "swarm": "[bold #fbbf24]⬢[/]",
27
+ }
28
+
29
+
30
+ def _server_icon(provider_name: str) -> str:
31
+ return _SERVER_ICONS.get(provider_name.lower(), "[bold white]◇[/]")
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class TechnologyCard:
36
+ provider: str
37
+ title: str
38
+ subtitle: str
39
+
40
+
41
+ TECHNOLOGY_CARDS: List[TechnologyCard] = [
42
+ TechnologyCard("docker", "Docker", _("environment.subtitle.docker")),
43
+ TechnologyCard("kubernetes", "Kubernetes", _("environment.subtitle.kubernetes")),
44
+ TechnologyCard("podman", "Podman", _("environment.subtitle.podman")),
45
+ TechnologyCard("openshift", "OpenShift", _("environment.subtitle.openshift")),
46
+ TechnologyCard("swarm", "Docker Swarm", _("environment.subtitle.swarm")),
47
+ ]
48
+
49
+
50
+ PROVIDER_CLI_COMMAND = {
51
+ "docker": "docker",
52
+ "kubernetes": "kubectl",
53
+ "podman": "podman",
54
+ "openshift": "oc",
55
+ "swarm": "docker",
56
+ }
57
+
58
+
59
+ class EnvironmentScreen(Screen):
60
+ BINDINGS = [
61
+ Binding("up", "focus_up", _("environment.bind.up"), priority=True),
62
+ Binding("down", "focus_down", _("environment.bind.down"), priority=True),
63
+ Binding("left", "focus_left", _("environment.bind.left"), priority=True),
64
+ Binding("right", "focus_right", _("environment.bind.right"), priority=True),
65
+ Binding("enter", "select_focused", _("environment.bind.select"), priority=True),
66
+ Binding("?", "show_welcome_help", _("environment.bind.help"), priority=True),
67
+ Binding("h", "show_welcome_help", _("environment.bind.help"), priority=True),
68
+ Binding("q", "quit", _("environment.bind.quit"), priority=True),
69
+ ]
70
+
71
+ def __init__(self, server_manager: ServerManager) -> None:
72
+ super().__init__()
73
+ self._server_manager = server_manager
74
+ self._card_ids: List[str] = []
75
+ self._card_actions: List[str] = []
76
+
77
+ def compose(self) -> ComposeResult:
78
+ rows: List[Horizontal] = []
79
+ row_cards: List[Vertical] = []
80
+ cols_per_row = 2
81
+
82
+ for i, tech in enumerate(TECHNOLOGY_CARDS):
83
+ row_cards.append(self._technology_card(tech, i))
84
+ if len(row_cards) == cols_per_row:
85
+ rows.append(Horizontal(*row_cards, id=f"env-row-{len(rows)}"))
86
+ row_cards = []
87
+
88
+ if row_cards:
89
+ rows.append(Horizontal(*row_cards, id=f"env-row-{len(rows)}"))
90
+
91
+ yield Vertical(
92
+ Static(f"[bold #22d3ee]TERMAINER[/] [#808080]v{VERSION}[/]", id="env-brand"),
93
+ Static(_("environment.title"), id="env-title"),
94
+ Static(
95
+ f"[dim]{_('environment.copy')}[/]",
96
+ id="env-copy",
97
+ ),
98
+ *rows,
99
+ Horizontal(
100
+ Static(
101
+ _("environment.footer.full"),
102
+ id="env-footer-full",
103
+ classes="env-footer-text",
104
+ ),
105
+ Static(
106
+ _("environment.footer.compact"),
107
+ id="env-footer-compact",
108
+ classes="env-footer-text",
109
+ ),
110
+ id="env-footer",
111
+ ),
112
+ id="env-root",
113
+ )
114
+
115
+ @property
116
+ def _has_remote_servers(self) -> bool:
117
+ return any(s.ssh is not None for s in self._server_manager.servers)
118
+
119
+ def _servers_for_provider(self, provider_name: str):
120
+ return [s for s in self._server_manager.servers if s.provider.name.lower() == provider_name.lower()]
121
+
122
+ def _provider_cli_available(self, provider_name: str) -> bool:
123
+ cli = PROVIDER_CLI_COMMAND.get(provider_name)
124
+ if not cli:
125
+ return False
126
+ return shutil.which(cli) is not None
127
+
128
+ def _provider_status_text(self, provider_name: str) -> str:
129
+ count = len(self._servers_for_provider(provider_name))
130
+ if count > 0:
131
+ return _("environment.status.connections", count=str(count))
132
+ if self._provider_cli_available(provider_name):
133
+ if provider_name in {"kubernetes", "openshift"}:
134
+ if self._has_remote_servers:
135
+ return _("environment.status.cli_no_context", provider=provider_name.capitalize())
136
+ return _("environment.status.cli_no_cluster", provider=provider_name.capitalize())
137
+ return _("environment.status.cli_no_servers", provider=provider_name.capitalize())
138
+ return _("environment.status.not_available", provider=provider_name.capitalize())
139
+
140
+ def _technology_card(self, tech: TechnologyCard, idx: int) -> Vertical:
141
+ icon = _server_icon(tech.provider)
142
+ card_id = f"env-card-{idx}"
143
+ self._card_ids.append(card_id)
144
+ self._card_actions.append(f"open_provider_{tech.provider}")
145
+
146
+ card = Vertical(
147
+ Static(f"{icon} [bold white]{tech.title}[/]", classes="env-card-title"),
148
+ Static(f"[dim]{tech.subtitle}[/]", classes="env-card-provider"),
149
+ Static(self._provider_status_text(tech.provider), classes="env-card-copy"),
150
+ Button(_("environment.button.open"), id=f"env-btn-{idx}", classes="env-card-button"),
151
+ classes="env-card",
152
+ id=card_id,
153
+ )
154
+ card.can_focus = True
155
+ return card
156
+
157
+ def on_mount(self) -> None:
158
+ self._apply_responsive_mode(self.size.width, self.size.height)
159
+ if self._card_ids:
160
+ self.query_one(f"#{self._card_ids[0]}").focus()
161
+
162
+ if not has_ssh_servers_configured():
163
+ self.notify(
164
+ _("environment.notify.ssh_hint"),
165
+ severity="information",
166
+ timeout=5,
167
+ )
168
+
169
+ def on_resize(self, event: Resize) -> None:
170
+ self._apply_responsive_mode(event.size.width, event.size.height)
171
+
172
+ def _apply_responsive_mode(self, width: int, height: int) -> None:
173
+ compact = width < 120 or height < 34
174
+ ultra_compact = width < 95 or height < 28
175
+ try:
176
+ root = self.query_one("#env-root", Vertical)
177
+ except Exception:
178
+ return
179
+ if compact:
180
+ root.add_class("compact")
181
+ else:
182
+ root.remove_class("compact")
183
+ if ultra_compact:
184
+ root.add_class("ultra-compact")
185
+ else:
186
+ root.remove_class("ultra-compact")
187
+
188
+ def on_button_pressed(self, event: Button.Pressed) -> None:
189
+ btn_id = event.button.id or ""
190
+ try:
191
+ idx = int(btn_id.replace("env-btn-", ""))
192
+ action_name = self._card_actions[idx]
193
+ self._execute_action(action_name)
194
+ except (ValueError, IndexError):
195
+ pass
196
+
197
+ def _execute_action(self, action_name: str) -> None:
198
+ if not action_name.startswith("open_provider_"):
199
+ return
200
+ provider_name = action_name.replace("open_provider_", "")
201
+
202
+ matches = self._servers_for_provider(provider_name)
203
+ if not matches:
204
+ if self._provider_cli_available(provider_name):
205
+ if provider_name in {"kubernetes", "openshift"}:
206
+ if self._has_remote_servers:
207
+ self.notify(
208
+ _("environment.notify.no_context", provider=provider_name.capitalize()),
209
+ severity="warning",
210
+ )
211
+ else:
212
+ self.notify(
213
+ _("environment.notify.no_cluster", provider=provider_name.capitalize()),
214
+ severity="warning",
215
+ )
216
+ return
217
+ self.notify(
218
+ _("environment.notify.no_servers", provider=provider_name.capitalize()),
219
+ severity="warning",
220
+ )
221
+ return
222
+ self.notify(
223
+ _("environment.notify.not_installed", provider=provider_name.capitalize()),
224
+ severity="warning",
225
+ )
226
+ return
227
+
228
+ provider_server_manager = ServerManager(matches)
229
+ self.app.switch_screen(
230
+ Dashboard(
231
+ provider_server_manager,
232
+ server_label=None,
233
+ root_server_manager=self._server_manager,
234
+ )
235
+ )
236
+
237
+ # Navigation
238
+ def action_focus_up(self) -> None:
239
+ self._move_focus(-self._cols())
240
+
241
+ def action_focus_down(self) -> None:
242
+ self._move_focus(self._cols())
243
+
244
+ def action_focus_left(self) -> None:
245
+ self._move_focus(-1)
246
+
247
+ def action_focus_right(self) -> None:
248
+ self._move_focus(1)
249
+
250
+ def _cols(self) -> int:
251
+ return min(2, max(1, len(self._card_ids)))
252
+
253
+ def _move_focus(self, delta: int) -> None:
254
+ if not self._card_ids:
255
+ return
256
+ focused = self.focused
257
+ if focused is None:
258
+ self.query_one(f"#{self._card_ids[0]}").focus()
259
+ return
260
+ focused_id = focused.id or ""
261
+ if focused_id not in self._card_ids:
262
+ parent = focused.parent
263
+ if parent and parent.id in self._card_ids:
264
+ focused_id = parent.id
265
+ else:
266
+ return
267
+ try:
268
+ idx = self._card_ids.index(focused_id)
269
+ except ValueError:
270
+ return
271
+ new_idx = (idx + delta) % len(self._card_ids)
272
+ self.query_one(f"#{self._card_ids[new_idx]}").focus()
273
+
274
+ def action_select_focused(self) -> None:
275
+ if not self._card_ids:
276
+ return
277
+ focused = self.focused
278
+ if focused is None:
279
+ return
280
+ focused_id = focused.id or ""
281
+ if focused_id not in self._card_ids:
282
+ parent = focused.parent
283
+ if parent and parent.id in self._card_ids:
284
+ focused_id = parent.id
285
+ else:
286
+ return
287
+ try:
288
+ idx = self._card_ids.index(focused_id)
289
+ except ValueError:
290
+ return
291
+ action_name = self._card_actions[idx]
292
+ self._execute_action(action_name)
293
+
294
+ def action_quit(self) -> None:
295
+ self.app.exit()
296
+
297
+ def action_show_welcome_help(self) -> None:
298
+ from .home import HomeScreen
299
+
300
+ self.app.switch_screen(HomeScreen(self._server_manager))
termainer/ui/home.py ADDED
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Horizontal, Vertical
5
+ from textual.events import Resize
6
+ from textual.reactive import reactive
7
+ from textual.screen import Screen
8
+ from textual.widgets import Static
9
+
10
+ from ..locale import _
11
+ from ..server_manager import ServerManager
12
+ from ..version import VERSION
13
+
14
+
15
+ # ── ASCII art ─────────────────────────────────────────────────────────────────
16
+
17
+ LOGO_BLOCK = """\
18
+ [bold #66FF33] _______ ______ _____ __ __ _____ _ _ ______ _____ [/]
19
+ [bold #66FF33]|__ __| ____| __ \\| \\/ | /\\ |_ _| \\ | | ____| __ \\ [/]
20
+ [bold #66FF33] | | | |__ | |__) | \\ / | / \\ | | | \\| | |__ | |__) |[/]
21
+ [bold #66FF33] | | | __| | _ /| |\\/| | / /\\ \\ | | | . ` | __| | _ / [/]
22
+ [bold #66FF33] | | | |____| | \\ \\| | | |/ ____ \\ _| |_| |\\ | |____| | \\ \\ [/]
23
+ [bold #66FF33] |_| |______|_| \\_\\_| |_/_/ \\_\\_____|_| \\_|______|_| \\_\\ [/]"""
24
+
25
+ LOGO_COMPACT = "[bold #66FF33][ >_ ] TERMAINER[/]"
26
+
27
+
28
+ # ── Widgets ───────────────────────────────────────────────────────────────────
29
+
30
+ class HeaderWidget(Horizontal):
31
+ """Fixed header: brand (left) · welcome (center) · live status (right)."""
32
+
33
+ _pulse: reactive[bool] = reactive(True)
34
+
35
+ def __init__(self, server_manager: ServerManager) -> None:
36
+ super().__init__(id="home-header")
37
+ self._server_manager = server_manager
38
+
39
+ def compose(self) -> ComposeResult:
40
+ yield Static(
41
+ f"[bold #66FF33][ >_ ][/] [bold #66FF33]TERMAINER[/] [dim #66FF33]v{VERSION}[/]",
42
+ id="home-logo",
43
+ )
44
+ yield Static(f"─── {_('home.welcome')} ───", id="home-welcome")
45
+ yield Static("", id="home-status")
46
+
47
+ def on_mount(self) -> None:
48
+ self._render_status()
49
+ self.set_interval(0.9, self._toggle_pulse)
50
+
51
+ def _toggle_pulse(self) -> None:
52
+ self._pulse = not self._pulse
53
+
54
+ def watch__pulse(self, _: bool) -> None:
55
+ self._render_status()
56
+
57
+ def _render_status(self) -> None:
58
+ count = self._server_manager.server_count
59
+ dot = "[bold #66FF33]●[/]" if self._pulse else "[dim #2a2a2a]●[/]"
60
+ try:
61
+ self.query_one("#home-status", Static).update(
62
+ f"{dot} {_('home.header.status', connected=count, total=count)}"
63
+ )
64
+ except Exception:
65
+ pass
66
+
67
+
68
+ class HeroWidget(Vertical):
69
+ """Full-width hero section: big logo + tagline."""
70
+
71
+ def compose(self) -> ComposeResult:
72
+ yield Static(LOGO_BLOCK, id="hero-logo")
73
+ yield Static(
74
+ f"[bold #00E5FF]{_('home.hero.tagline')}[/]",
75
+ id="hero-tagline",
76
+ )
77
+
78
+ def set_compact(self, compact: bool) -> None:
79
+ try:
80
+ self.query_one("#hero-logo", Static).update(
81
+ LOGO_COMPACT if compact else LOGO_BLOCK
82
+ )
83
+ except Exception:
84
+ pass
85
+
86
+
87
+ class PlatformsStripWidget(Horizontal):
88
+ """Horizontal strip of 6 platform icons — no boxes, icon + name only."""
89
+
90
+ _PLATFORMS = [
91
+ ("🐳", "#3399FF", "home.platform.docker"),
92
+ ("⎈", "#3399FF", "home.platform.k8s"),
93
+ ("⬡", "#FFD400", "home.platform.swarm"),
94
+ ("◇", "#B366FF", "home.platform.podman"),
95
+ ("◈", "#FF3B30", "home.platform.openshift"),
96
+ ("▸_", "#66FF33", "home.platform.ssh"),
97
+ ]
98
+
99
+ def compose(self) -> ComposeResult:
100
+ for icon, color, key in self._PLATFORMS:
101
+ yield Static(
102
+ f"[bold {color}]{icon}[/] [{color}]{_(key)}[/]",
103
+ classes="platform-item",
104
+ )
105
+
106
+
107
+ class AboutWidget(Vertical):
108
+ """Compact description of what Termainer is."""
109
+
110
+ def compose(self) -> ComposeResult:
111
+ yield Static(f"[bold #00E5FF]{_('home.about.title')}[/]", classes="section-label")
112
+ yield Static(
113
+ _("home.about.description"),
114
+ id="about-description",
115
+ )
116
+
117
+
118
+ class FeaturesWidget(Horizontal):
119
+ """Five-column feature breakdown with icons, titles and item lists."""
120
+
121
+ _COLS = [
122
+ ("#66FF33", "▥", "home.features.obs.title", "home.features.obs.items"),
123
+ ("#00E5FF", "⌕", "home.features.inspection.title", "home.features.inspection.items"),
124
+ ("#3399FF", "⚙", "home.features.ops.title", "home.features.ops.items"),
125
+ ("#B366FF", "⬡", "home.features.export.title", "home.features.export.items"),
126
+ ("#FFD400", "⇶", "home.features.unified.title", "home.features.unified.items"),
127
+ ]
128
+
129
+ def compose(self) -> ComposeResult:
130
+ for color, icon, title_key, items_key in self._COLS:
131
+ yield Vertical(
132
+ Static(f"[bold {color}]{icon} {_(title_key)}[/]", classes="feat-title"),
133
+ Static(f"[dim]{_(items_key)}[/]", classes="feat-body"),
134
+ classes="feat-col",
135
+ )
136
+
137
+
138
+ class SupportedPlatformsWidget(Horizontal):
139
+ """Six platform cards, each with its brand-color border."""
140
+
141
+ _CARDS = [
142
+ ("🐳", "#3399FF", "home.platform.docker", "plat-docker"),
143
+ ("⎈", "#3399FF", "home.platform.k8s", "plat-k8s"),
144
+ ("⬡", "#FFD400", "home.platform.swarm", "plat-swarm"),
145
+ ("◇", "#B366FF", "home.platform.podman", "plat-podman"),
146
+ ("◈", "#FF3B30", "home.platform.openshift", "plat-openshift"),
147
+ ("▸_", "#66FF33", "home.platform.ssh", "plat-ssh"),
148
+ ]
149
+
150
+ def compose(self) -> ComposeResult:
151
+ for icon, color, key, css_class in self._CARDS:
152
+ name = _(key)
153
+ parts = name.split(" ", 1)
154
+ display = f"{parts[0]}\n{parts[1]}" if len(parts) > 1 else name
155
+ yield Vertical(
156
+ Static(f"[bold {color}]{icon}[/]", classes="plat-icon"),
157
+ Static(display, classes="plat-name"),
158
+ Static(f"[bold #4ade80]{_('home.platforms.supported')}[/]", classes="plat-status"),
159
+ classes=f"plat-card {css_class}",
160
+ )
161
+
162
+
163
+ class FooterWidget(Vertical):
164
+ """Centered footer: Enter hint + credits."""
165
+
166
+ def compose(self) -> ComposeResult:
167
+ hint = _("home.footer.enter_hint")
168
+ styled = hint.replace("ENTER", "[bold #66FF33]ENTER[/]")
169
+ yield Static(styled, id="footer-enter")
170
+ yield Static(
171
+ _("home.footer.credits"),
172
+ id="footer-credits",
173
+ )
174
+
175
+
176
+ # ── Screen ────────────────────────────────────────────────────────────────────
177
+
178
+ class HomeScreen(Screen):
179
+ """
180
+ Widget-based home/welcome screen.
181
+
182
+ Hierarchy:
183
+ HomeScreen
184
+ ├── HeaderWidget brand · welcome · live pulsing status
185
+ ├── HeroWidget big logo + tagline
186
+ ├── PlatformsStripWidget 6 platform icons, no boxes
187
+ ├── AboutWidget description + ASCII connection diagram
188
+ ├── FeaturesWidget 5-column feature breakdown
189
+ ├── SupportedPlatformsWidget 6 colored-border platform cards
190
+ └── FooterWidget Enter hint + credits (no shortcuts)
191
+ """
192
+
193
+ BINDINGS = [
194
+ ("enter", "continue_to_env", _("home.bind.continue")),
195
+ ("escape", "continue_to_env", _("home.bind.continue")),
196
+ ("q", "quit", _("home.bind.quit")),
197
+ ]
198
+
199
+ def __init__(self, server_manager: ServerManager) -> None:
200
+ super().__init__()
201
+ self._server_manager = server_manager
202
+ self._dismissed = False
203
+ self._hero: HeroWidget | None = None
204
+
205
+ def compose(self) -> ComposeResult:
206
+ hero = HeroWidget(id="home-hero")
207
+ self._hero = hero
208
+ yield Vertical(
209
+ HeaderWidget(self._server_manager),
210
+ hero,
211
+ PlatformsStripWidget(id="home-platforms"),
212
+ AboutWidget(id="home-about"),
213
+ FeaturesWidget(id="home-features"),
214
+ Static(
215
+ f"[bold #00E5FF]{_('home.platforms.title')}[/]",
216
+ id="home-supported-label",
217
+ ),
218
+ SupportedPlatformsWidget(id="home-supported"),
219
+ id="home-root",
220
+ )
221
+ yield FooterWidget(id="home-footer")
222
+
223
+ def on_mount(self) -> None:
224
+ self._apply_responsive(self.size.width, self.size.height)
225
+
226
+ def on_resize(self, event: Resize) -> None:
227
+ self._apply_responsive(event.size.width, event.size.height)
228
+
229
+ def _apply_responsive(self, width: int, height: int) -> None:
230
+ compact = width < 100 or height < 34
231
+ try:
232
+ root = self.query_one("#home-root", Vertical)
233
+ except Exception:
234
+ return
235
+ if compact:
236
+ root.add_class("compact")
237
+ if self._hero:
238
+ self._hero.set_compact(True)
239
+ else:
240
+ root.remove_class("compact")
241
+ if self._hero:
242
+ self._hero.set_compact(False)
243
+
244
+ def on_key(self, event) -> None:
245
+ if event.key in ("enter", "escape"):
246
+ event.stop()
247
+ self._dismiss()
248
+
249
+ def on_click(self) -> None:
250
+ self._dismiss()
251
+
252
+ def action_continue_to_env(self) -> None:
253
+ self._dismiss()
254
+
255
+ def action_quit(self) -> None:
256
+ self.app.exit()
257
+
258
+ def _dismiss(self) -> None:
259
+ if self._dismissed:
260
+ return
261
+ self._dismissed = True
262
+ from .environment import EnvironmentScreen
263
+ self.app.switch_screen(EnvironmentScreen(self._server_manager))
termainer/ui/splash.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Center, Vertical
7
+ from textual.screen import Screen
8
+ from textual.widgets import Static
9
+
10
+ from ..locale import _
11
+ from ..server_manager import ServerManager
12
+ from ..version import VERSION
13
+
14
+
15
+ _BRAND = f"[bold #66FF33] [ >_ ] T · E · R · M · A · I · N · E · R v{VERSION} [/]"
16
+
17
+ _BOOT_STEP_KEYS: list[tuple[str, str]] = [
18
+ ("splash.boot.init", "#66FF33"),
19
+ ("splash.boot.docker", "#3399FF"),
20
+ ("splash.boot.k8s", "#3399FF"),
21
+ ("splash.boot.openshift","#FF3B30"),
22
+ ("splash.boot.discover", "#00E5FF"),
23
+ ("splash.boot.sync", "#66FF33"),
24
+ ]
25
+
26
+
27
+ class BootScreen(Screen):
28
+ """Animated boot sequence. Auto-transitions to HomeScreen when done."""
29
+
30
+ BINDINGS = [
31
+ ("enter", "skip", _("home.bind.continue")),
32
+ ("space", "skip", _("home.bind.continue")),
33
+ ]
34
+
35
+ def __init__(self, server_manager: ServerManager) -> None:
36
+ super().__init__()
37
+ self._server_manager = server_manager
38
+ self._done = False
39
+
40
+ def compose(self) -> ComposeResult:
41
+ yield Center(
42
+ Vertical(
43
+ Static(_BRAND, id="boot-brand"),
44
+ Static(
45
+ "[dim]─────────────────────────────────────────────[/]",
46
+ id="boot-sep",
47
+ ),
48
+ Static("", id="boot-messages"),
49
+ id="boot-panel",
50
+ ),
51
+ id="boot-root",
52
+ )
53
+
54
+ async def on_mount(self) -> None:
55
+ asyncio.create_task(self._run_sequence())
56
+
57
+ async def _run_sequence(self) -> None:
58
+ widget = self.query_one("#boot-messages", Static)
59
+ lines: list[str] = []
60
+ for key, color in _BOOT_STEP_KEYS:
61
+ if self._done:
62
+ break
63
+ text = _(key)
64
+ lines.append(f" [{color}]▶[/] {text}[dim]...[/]")
65
+ widget.update("\n".join(lines))
66
+ await asyncio.sleep(0.11)
67
+ lines[-1] = f" [{color}]✓[/] {text}"
68
+ widget.update("\n".join(lines))
69
+ await asyncio.sleep(0.07)
70
+ if not self._done:
71
+ await asyncio.sleep(0.25)
72
+ self._go_home()
73
+
74
+ def _go_home(self) -> None:
75
+ if self._done:
76
+ return
77
+ self._done = True
78
+ from .home import HomeScreen
79
+ self.app.switch_screen(HomeScreen(self._server_manager))
80
+
81
+ def action_skip(self) -> None:
82
+ self._go_home()
83
+
84
+ def on_click(self) -> None:
85
+ self.action_skip()
86
+
87
+
88
+ # Backward-compat alias
89
+ SplashScreen = BootScreen