44from typing import Callable , Optional
55
66from textual .app import App , ComposeResult
7+ from textual .binding import Binding
78from textual .containers import Vertical , VerticalScroll
9+ from textual .css .query import NoMatches
810from textual .widgets import ContentSwitcher , Static
911from textual .worker import Worker , WorkerFailed , WorkerState
1012
@@ -53,7 +55,8 @@ class Plain2CodeTUI(App):
5355 """A Textual TUI for plain2code."""
5456
5557 BINDINGS = [
56- ("ctrl+c" , "quit" , "Quit" ),
58+ Binding ("ctrl+c" , "smart_quit" , "Copy/Quit" , show = False ),
59+ Binding ("escape" , "cancel_quit" , "Cancel Quit" , show = False ),
5760 ("ctrl+l" , "toggle_logs" , "Toggle Logs" ),
5861 ]
5962
@@ -77,6 +80,7 @@ def __init__(
7780 self .conformance_tests_script : Optional [str ] = conformance_tests_script
7881 self .prepare_environment_script : Optional [str ] = prepare_environment_script
7982 self .state_machine_version = state_machine_version
83+ self ._quit_pending = False
8084
8185 # Initialize state handlers
8286 self ._state_handlers : dict [str , StateHandler ] = {
@@ -298,12 +302,53 @@ def ensure_exit():
298302 # daemon=True ensures this thread dies with the process if it exits before the timer fires
299303 threading .Thread (target = ensure_exit , daemon = True ).start ()
300304
305+ @property
306+ def quit_pending (self ) -> bool :
307+ """Whether a quit confirmation is pending."""
308+ return self ._quit_pending
309+
310+ async def action_smart_quit (self ) -> None :
311+ """Handle ctrl+c: copy selected text if any, otherwise quit.
312+
313+ Copy-first, quit-second design:
314+ - If text is selected -> copy it to clipboard
315+ - If no text is selected -> enter quit-pending state
316+ - If already in quit-pending state -> actually quit
317+ - ESC cancels the quit confirmation
318+ """
319+ selected_text = self .screen .get_selected_text ()
320+ if selected_text :
321+ self .copy_to_clipboard (selected_text )
322+ self .screen .clear_selection ()
323+ self .notify ("Copied to clipboard" , timeout = 2 )
324+ return
325+
326+ if self ._quit_pending :
327+ self .action_quit ()
328+ return
329+
330+ self ._quit_pending = True
331+ self ._refresh_footer ()
332+
333+ def action_cancel_quit (self ) -> None :
334+ """Cancel the quit confirmation when ESC is pressed."""
335+ if self ._quit_pending :
336+ self ._quit_pending = False
337+ self ._refresh_footer ()
338+
339+ def _refresh_footer (self ) -> None :
340+ """Refresh the CustomFooter widget to reflect current quit-pending state."""
341+ try :
342+ footer = self .screen .query_one (CustomFooter )
343+ footer .update_quit_state (self ._quit_pending )
344+ except NoMatches :
345+ pass
346+
301347 def action_quit (self ) -> None :
302- """An action to quit the application immediately when user presses 'q' .
348+ """Quit the application immediately.
303349
304350 Note: Force exit may leave files partially written if interrupted during file I/O operations.
305351 This is acceptable since the folders in which we are writing are git versioned and are reset in the next render.
306352 """
307- # Show stopping message to user
308353 self .render_worker .cancel ()
309354 self .exit ()
0 commit comments