1- import os
2- import threading
3- import time
41from typing import Callable , Optional
52
63from textual .app import App , ComposeResult
74from textual .binding import Binding
85from textual .containers import Vertical , VerticalScroll
96from textual .css .query import NoMatches
107from textual .widgets import ContentSwitcher , Static
11- from textual .worker import Worker , WorkerFailed , WorkerState
128
139from event_bus import EventBus
14- from plain2code_console import console
1510from plain2code_events import (
1611 LogMessageEmitted ,
1712 RenderCompleted ,
4843 UnitTestsHandler ,
4944)
5045
51- FORCE_EXIT_DELAY = 0.5 # seconds
52-
5346
5447class Plain2CodeTUI (App ):
5548 """A Textual TUI for plain2code."""
@@ -64,7 +57,7 @@ class Plain2CodeTUI(App):
6457 def __init__ (
6558 self ,
6659 event_bus : EventBus ,
67- worker_fun : Callable [[], None ],
60+ on_ready : Callable [[], None ],
6861 render_id : str ,
6962 unittests_script : str ,
7063 conformance_tests_script : str ,
@@ -75,7 +68,7 @@ def __init__(
7568 super ().__init__ (** kwargs )
7669 self .dark = True # Set dark mode as default
7770 self .event_bus = event_bus
78- self .worker_fun = worker_fun
71+ self ._on_ready = on_ready
7972 self .render_id = render_id
8073 self .unittests_script : Optional [str ] = unittests_script
8174 self .conformance_tests_script : Optional [str ] = conformance_tests_script
@@ -134,39 +127,7 @@ def on_mount(self) -> None:
134127 self .event_bus .subscribe (RenderModuleCompleted , self .on_render_module_completed )
135128 self .event_bus .subscribe (LogMessageEmitted , self .on_log_message_emitted )
136129
137- self .render_worker = self .run_worker (self .worker_fun , thread = True )
138-
139- def on_worker_state_changed (self , event : Worker .StateChanged ) -> None :
140- """Handle worker state changes."""
141- if event .worker .state == WorkerState .ERROR :
142- # Extract the original exception from WorkerFailed wrapper
143- error = event .worker .error
144- original_error = error .__cause__ if isinstance (error , WorkerFailed ) and error .__cause__ else error
145- self .exit (result = original_error )
146-
147- def _handle_exception (self , error : Exception ) -> None :
148- """Override Textual's exception handler to suppress console tracebacks for worker errors.
149-
150- Worker exceptions are logged to file via the logging system (configured in file handler in plain2code.py),
151- but the verbose Textual/Rich traceback is suppressed from the terminal. The clean error message
152- is displayed via the exception handlers in main().
153- """
154- # Because TUI is running in main thread and code renderer is running in a worker thread, textual models this
155- # by raising a WorkerFailed exception in this case
156- if isinstance (error , WorkerFailed ):
157- # Here, we still print the error to get some additional information to the file, but it's probably
158- # not necessary because we either way print the entire traceback to the console. Here, we could in the future
159- # print more information if we would want to.
160- original_error = error .__cause__ if error .__cause__ else error
161- console .error (
162- f"Worker failed with exception: { type (original_error ).__name__ } : { original_error } " ,
163- exc_info = (type (original_error ), original_error , original_error .__traceback__ ),
164- stack_info = False ,
165- )
166- return
167-
168- # For non-worker exceptions, use the default Textual behavior
169- super ()._handle_exception (error )
130+ self ._on_ready ()
170131
171132 def compose (self ) -> ComposeResult :
172133 """Create child widgets for the app."""
@@ -290,24 +251,12 @@ def on_render_completed(self, event: RenderCompleted):
290251 def on_render_failed (self , event : RenderFailed ):
291252 """Handle render failure."""
292253 self ._render_error_handler .handle (event .error_message )
293-
294- def _ensure_exit (self ) -> None :
295- """Ensure exit the application immediately."""
296-
297- def ensure_exit ():
298- time .sleep (FORCE_EXIT_DELAY )
299- try :
300- if hasattr (self , "_driver" ) and self ._driver is not None :
301- # suspend_application_mode() calls stop_application_mode() and close()
302- # It ensures terminal reset sequences are flushed before exiting.
303- self ._driver .suspend_application_mode ()
304- except Exception as e :
305- log_to_widget (self , "WARNING" , f"Error suspending application mode: { type (e ).__name__ } : { e } " )
306- finally :
307- os ._exit (0 ) # Kill process immediately, no cleanup - terminates all threads
308-
309- # daemon=True ensures this thread dies with the process if it exits before the timer fires
310- threading .Thread (target = ensure_exit , daemon = True ).start ()
254+ self ._render_finished = True
255+ try :
256+ footer = self .screen .query_one (CustomFooter )
257+ footer .show_render_finished ()
258+ except NoMatches :
259+ pass
311260
312261 async def action_copy_selection (self ) -> None :
313262 """Handle ctrl+c: copy selected text if any.
@@ -332,5 +281,4 @@ def action_quit(self) -> None:
332281 Note: Force exit may leave files partially written if interrupted during file I/O operations.
333282 This is acceptable since the folders in which we are writing are git versioned and are reset in the next render.
334283 """
335- self .render_worker .cancel ()
336284 self .exit ()
0 commit comments