Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 7 additions & 16 deletions tui/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,23 @@
class CustomFooter(Horizontal):
"""A custom footer with keyboard shortcuts and render ID."""

NORMAL_TEXT = "ctrl+c: quit/copy/ * ctrl+l: toggle logs"
QUIT_PENDING_TEXT = "press ctrl+c again to quit * esc: cancel"
FOOTER_TEXT = "ctrl+c: copy * ctrl+d: quit * ctrl+l: toggle logs"
RENDER_FINISHED_TEXT = "enter: exit * ctrl+c: copy * ctrl+l: toggle logs"

def __init__(self, render_id: str = "", **kwargs):
super().__init__(**kwargs)
self.render_id = render_id
self._footer_text_widget: Optional[Static] = None

def compose(self):
self._footer_text_widget = Static(self.NORMAL_TEXT, classes="custom-footer-text")
self._footer_text_widget = Static(self.FOOTER_TEXT, classes="custom-footer-text")
yield self._footer_text_widget
if self.render_id:
yield Static(f"render id: {self.render_id}", classes="custom-footer-render-id")

def update_quit_state(self, quit_pending: bool) -> None:
"""Update footer text based on quit-pending state."""
if self._footer_text_widget is None:
return
if quit_pending:
self._footer_text_widget.update(self.QUIT_PENDING_TEXT)
self._footer_text_widget.remove_class("custom-footer-text")
self._footer_text_widget.add_class("custom-footer-quit-pending")
else:
self._footer_text_widget.update(self.NORMAL_TEXT)
self._footer_text_widget.remove_class("custom-footer-quit-pending")
self._footer_text_widget.add_class("custom-footer-text")
def show_render_finished(self) -> None:
"""Update footer text to show render-finished keybindings."""
if self._footer_text_widget is not None:
self._footer_text_widget.update(self.RENDER_FINISHED_TEXT)


class ScriptOutputType(str, Enum):
Expand Down
50 changes: 16 additions & 34 deletions tui/plain2code_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ class Plain2CodeTUI(App):
"""A Textual TUI for plain2code."""

BINDINGS = [
Binding("ctrl+c", "smart_quit", "Copy/Quit", show=False),
Binding("escape", "cancel_quit", "Cancel Quit", show=False),
Binding("ctrl+c", "copy_selection", "Copy", show=False),
Binding("ctrl+d", "quit", "Quit", show=False),
Binding("enter", "enter_exit", "Exit", show=False),
("ctrl+l", "toggle_logs", "Toggle Logs"),
]

Expand All @@ -80,7 +81,7 @@ def __init__(
self.conformance_tests_script: Optional[str] = conformance_tests_script
self.prepare_environment_script: Optional[str] = prepare_environment_script
self.state_machine_version = state_machine_version
self._quit_pending = False
self._render_finished = False

# Initialize state handlers
self._state_handlers: dict[str, StateHandler] = {
Expand Down Expand Up @@ -279,6 +280,12 @@ def _handle_frid_state(
def on_render_completed(self, event: RenderCompleted):
"""Handle successful render completion."""
self._render_success_handler.handle(event.rendered_code_path)
self._render_finished = True
try:
footer = self.screen.query_one(CustomFooter)
footer.show_render_finished()
except NoMatches:
pass

def on_render_failed(self, event: RenderFailed):
"""Handle render failure."""
Expand All @@ -302,47 +309,22 @@ def ensure_exit():
# daemon=True ensures this thread dies with the process if it exits before the timer fires
threading.Thread(target=ensure_exit, daemon=True).start()

@property
def quit_pending(self) -> bool:
"""Whether a quit confirmation is pending."""
return self._quit_pending
async def action_copy_selection(self) -> None:
"""Handle ctrl+c: copy selected text if any.

async def action_smart_quit(self) -> None:
"""Handle ctrl+c: copy selected text if any, otherwise quit.

Copy-first, quit-second design:
- If text is selected -> copy it to clipboard
- If no text is selected -> enter quit-pending state
- If already in quit-pending state -> actually quit
- ESC cancels the quit confirmation
- If no text is selected -> do nothing
"""
selected_text = self.screen.get_selected_text()
if selected_text:
self.copy_to_clipboard(selected_text)
self.screen.clear_selection()
self.notify("Copied to clipboard", timeout=2)
return

if self._quit_pending:
def action_enter_exit(self) -> None:
"""Handle enter: exit the TUI only after rendering has finished."""
if self._render_finished:
self.action_quit()
return

self._quit_pending = True
self._refresh_footer()

def action_cancel_quit(self) -> None:
"""Cancel the quit confirmation when ESC is pressed."""
if self._quit_pending:
self._quit_pending = False
self._refresh_footer()

def _refresh_footer(self) -> None:
"""Refresh the CustomFooter widget to reflect current quit-pending state."""
try:
footer = self.screen.query_one(CustomFooter)
footer.update_quit_state(self._quit_pending)
except NoMatches:
pass

def action_quit(self) -> None:
"""Quit the application immediately.
Expand Down
5 changes: 0 additions & 5 deletions tui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -399,11 +399,6 @@ CustomFooter {
color: #888;
}

.custom-footer-quit-pending {
width: 1fr;
color: #E0FF6E;
}

.custom-footer-render-id {
width: auto;
color: #888;
Expand Down
2 changes: 1 addition & 1 deletion tui/widget_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def display_success_message(tui, rendered_code_path: str):
rendered_code_path: The path to the rendered code
"""

message = f"[#79FC96]✓ Rendering finished![/#79FC96] [#888888](ctrl+c to exit)[/#888888]\n[#888888]Generated code: {rendered_code_path}[/#888888] "
message = f"[#79FC96]✓ Rendering finished![/#79FC96] [#888888](enter to exit)[/#888888]\n[#888888]Generated code: {rendered_code_path}[/#888888] "

widget: Static = tui.query_one(f"#{TUIComponents.RENDER_STATUS_WIDGET.value}", Static)
widget.update(message)
Expand Down