11from __future__ import annotations
22
33import platform
4- from typing import TYPE_CHECKING , Optional , Union , cast
4+ from typing import TYPE_CHECKING , Optional , Union
55
66if TYPE_CHECKING :
77 from textual .app import ComposeResult
8+ from textual .worker import Worker
89
910 from datashuttle .tui .interface import Interface
1011 from datashuttle .tui .screens .project_manager import ProjectManagerScreen
1112
1213from pathlib import Path
1314
15+ from textual import work
1416from textual .containers import Container , Horizontal
1517from textual .widgets import (
1618 Button ,
@@ -64,6 +66,9 @@ def __init__(
6466
6567 self .parent_class = parent_class
6668 self .interface = interface
69+ self .validating_central_popup : (
70+ modal_dialogs .CentralWaitingScreen | None
71+ ) = None
6772
6873 def compose (self ) -> ComposeResult :
6974 """Set up the widgets for the container."""
@@ -108,7 +113,6 @@ def compose(self) -> ComposeResult:
108113 id = "validate_arguments_horizontal" ,
109114 ),
110115 RichLog (highlight = True , markup = True , id = "validate_richlog" ),
111- Label ("" , id = "validate_logs_label" ),
112116 Button ("Validate" , id = "validate_validate_button" ),
113117 ]
114118
@@ -150,6 +154,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
150154 )
151155
152156 elif event .button .id == "validate_validate_button" :
157+ # Get settings from widgets
153158 select_value = self .query_one (
154159 "#validate_top_level_folder_select"
155160 ).value
@@ -165,28 +170,33 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
165170 ).value
166171
167172 if self .interface :
173+ # If we are in a project, and it has a central storage we
174+ # will connect and, if it's a slow connection, show a waiting screen
168175 if self .interface .project .is_local_project ():
169176 include_central = False
170177 else :
171178 include_central = self .query_one (
172179 "#validate_include_central_checkbox"
173180 ).value
174181
175- success , output = self .interface .validate_project (
182+ if include_central and self .interface .project .cfg [
183+ "connection_method"
184+ ] in ["aws" , "gdrive" , "ssh" ]:
185+ self .validating_central_popup = (
186+ modal_dialogs .CentralWaitingScreen (
187+ "Validating central project..."
188+ )
189+ )
190+ self .parent_class .mainwindow .push_screen (
191+ self .validating_central_popup
192+ )
193+
194+ self .run_validate_and_dismiss_popup (
176195 top_level_folder = top_level_folder ,
177196 include_central = include_central ,
178197 strict_mode = strict_mode ,
179198 allow_letters_in_sub_ses_values = allow_letters_in_sub_ses_values ,
180199 )
181- if not success :
182- self .parent_class .mainwindow .show_modal_error_dialog (
183- cast ("str" , output )
184- )
185- else :
186- self .write_results_to_richlog (output )
187- self .query_one (
188- "#validate_logs_label"
189- ).value = f"Logs output to: { self .interface .project .get_logging_path ()} "
190200 else :
191201 path_ = self .query_one ("#validate_path_input" ).value
192202
@@ -215,6 +225,65 @@ def set_select_path(self, path_):
215225 if path_ :
216226 self .query_one ("#validate_path_input" ).value = path_ .as_posix ()
217227
228+ @work (group = "validate_async" , exclusive = True )
229+ async def run_validate_and_dismiss_popup (
230+ self ,
231+ top_level_folder ,
232+ include_central ,
233+ strict_mode ,
234+ allow_letters_in_sub_ses_values ,
235+ ) -> None :
236+ """Run validation in a worker thread and dismiss the waiting popup when done.
237+
238+ Decorated with ``@work`` so Textual owns the task lifecycle (no need to
239+ hold an ``asyncio.Task`` reference to prevent premature GC), and so a
240+ repeat button-press cancels the previous in-flight invocation via
241+ ``exclusive=True``.
242+ """
243+ worker = self .validate_project_worker (
244+ top_level_folder = top_level_folder ,
245+ include_central = include_central ,
246+ strict_mode = strict_mode ,
247+ allow_letters_in_sub_ses_values = allow_letters_in_sub_ses_values ,
248+ )
249+ await worker .wait ()
250+ if self .validating_central_popup :
251+ self ._hide_validating_central_popup ()
252+
253+ @work (exclusive = True , thread = True )
254+ def validate_project_worker (
255+ self ,
256+ top_level_folder ,
257+ include_central ,
258+ strict_mode ,
259+ allow_letters_in_sub_ses_values ,
260+ ) -> Worker [None ]:
261+ """Run validation in a separate thread to avoid freezing the TUI."""
262+ assert self .interface is not None
263+
264+ success , output = self .interface .validate_project (
265+ top_level_folder = top_level_folder ,
266+ include_central = include_central ,
267+ strict_mode = strict_mode ,
268+ allow_letters_in_sub_ses_values = allow_letters_in_sub_ses_values ,
269+ )
270+
271+ if not success :
272+ if self .validating_central_popup :
273+ self .app .call_from_thread (
274+ self ._hide_validating_central_popup ,
275+ )
276+ self .app .call_from_thread (
277+ self .parent_class .mainwindow .show_modal_error_dialog ,
278+ output ,
279+ )
280+ else :
281+ self .app .call_from_thread (self .write_results_to_richlog , output )
282+
283+ def _hide_validating_central_popup (self ):
284+ self .validating_central_popup .dismiss ()
285+ self .validating_central_popup = None
286+
218287 def write_results_to_richlog (self , results ):
219288 """Display the validation results on the Rich Log widget."""
220289 text_log = self .query_one ("#validate_richlog" )
0 commit comments