"""Dialog subclasses for Winston.

This module provides specialized dialog types that extend the base Dialog class.

Key Classes:
    Applet: Base class for Winston applets with threading support
    AppletDialog: Combined Applet and Dialog
    TableDialog: Dialog displaying tabular data with row/column navigation
    FormDialog: Dialog containing form elements with tab navigation
    HeteroDialog: FormDialog with mixed element types (deprecated)
    InputDialog: FormDialog for simple text input
    InputPromptDialog: Console-style dialog with output area and input prompt
    StdinDialog: FormDialog that captures stdout/stderr
    RootDialog: Launcher dialog for applets
    CommandDialog: Dialog for command execution

All dialog classes inherit from Dialog (in dialog.py).

Note:
    InputPromptDialog was moved here from debugging.py and InputPromptElement
    is defined in form_elements.py. Both are general-purpose dialog components
    used by multiple applets (console, clauder, inspection), not specifically
    debugging utilities.
"""

from __future__ import annotations
from typing import Any, TYPE_CHECKING, cast
from collections.abc import Callable, Iterable, Sequence
import sys
import os
import bdb
import queue
import threading
from bdb import BdbQuit
import pyperclip  # type: ignore[import]
from .dialog import Dialog
from .key_handler import KeyHandler, ElementTemplate, DialogException
from .elements import Element, ValuesElement
from .form_elements import FormElement, ListElement, ButtonElement, TextElement, NumberElement, InputPromptElement
from . import framework
from .framework import beep, ModifiedKey, WinQuit
from . import punctuator
from . import unspeakables

if TYPE_CHECKING:
    from queue import Queue
    from threading import Thread
    from typing import Generator
    from .framework import Anchor

class IndexedList(list[str]):
    """List with cursor for history navigation.

    Extends list with cursor tracking for navigating through history
    (e.g., command history in console).

    Attributes:
        owner: Dialog or object that owns this list.
        cursor: Current position in list.
    """

    def __init__(self, owner: Any, *args: Any, **kwargs: Any) -> None:
        """Initialize indexed list.

        Args:
            owner: Owner dialog or object.
            *args: Positional arguments passed to list.
            **kwargs: Keyword arguments passed to list.
        """
        self.owner = owner
        self.cursor = 0
        super().__init__(*args, **kwargs)

    def get_previous(self) -> str:
        """Navigate to previous item in history.

        Returns:
            Previous item, or empty string if list is empty.
            Beeps if already at start of list.
        """
        from .framework import beep
        if len(self):
            if self.cursor:
                self.cursor -= 1
            else:
                beep()
            return self[self.cursor]
        else:
            beep()
            return ''

    def get_next(self) -> str:
        """Navigate to next item in history.

        Returns:
            Next item, or empty string if list is empty.
            Beeps if already at end of list.
        """
        from .framework import beep
        if len(self):
            if self.cursor < (len(self) - 1):
                self.cursor += 1
            else:
                beep()
            return self[self.cursor]
        else:
            beep()
            return ''

    def add(self, item: str) -> None:
        """Add item to history and move cursor to end.

        Args:
            item: Item to add to history.
        """
        self.append(item)
        self.cursor = len(self) - 1

class Applet:
    """Base class for Winston applets with threading and lifecycle management.

    Applets are persistent dialogs managed by the Carousel. They handle
    exceptions, cleanup, and integration with the debugger.

    The run() method wraps execution with exception handling and automatic
    removal from the carousel on exit.
    """

    def run(self, speak: list[str] | None = None) -> str:
        """Execute applet with exception handling and cleanup.

        Main function removes applet from carousel on termination and
        cleans up debugger if this applet was being debugged.

        Args:
            speak: Optional list of speech directives for initial announcement.

        Returns:
            "normal" on clean exit, "exception" if exception was caught.
        """
        try:
            try:
                rc = self.inner_run(speak)  # type: ignore[attr-defined]
            except (BdbQuit, WinQuit) as e:
                sys.stdout.flush()
                # Expected forced termination - exit without exc_writer
                raise DialogException(f' BdbQuit/WinQuit exception in {self}')
            except Exception as e:
                from . import debugging
                debugging.exc_writer(e.__class__, e, e.__traceback__, exc_applet=self)  # type: ignore[arg-type]
                raise DialogException(f'exception in {self}')
            finally:
                self.app.applets.remove(self)  # type: ignore[attr-defined]
                if self.app.debugger.applet is self:  # type: ignore[attr-defined]
                    try:
                        self.app.debugger.set_quit()  # type: ignore[attr-defined]
                    except bdb.BdbQuit:
                        pass
                    except Exception as e:
                        raise DialogException(
                            f'exception {e} thrown when closing {self} while referenced from debugger.applet'
                        )
        except DialogException:
            return "exception"
        return "normal"

    def breadcrumbs(self) -> list[str]:
        """Build list of dialog hierarchy from this applet down.

        Returns:
            List of repr() strings for this applet and all child dialogs.
        """
        crumbs = [repr(self)]
        child = self.child  # type: ignore[attr-defined]
        while child:
            crumbs.append(', ' + repr(child))
            child = child.child
        return crumbs

    def get_result(self) -> None:
        """Get result value when applet closes.

        Applets don't close via Return key (use Alt+Left instead).

        Returns:
            Always None to prevent Return from closing applet.
        """
        return None

    @KeyHandler.hotkey("Alt+Left")
    def close_dialog_wrapper(self, modified_name: str) -> bool:
        """Wrapper for close_dialog to register as hotkey.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result from close_dialog (False to exit run loop).
        """
        return self.close_dialog(modified_name)

    def close_dialog(self, modified_name: str) -> bool:
        """Close applet unconditionally.

        Same behavior as cancel but works even if cancel is overridden
        to prevent accidental dismissal.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            False to signal run loop to exit.
        """
        return False 
class AppletDialog(Applet, Dialog):  # type: ignore[misc]
    """Combined Applet and Dialog base class.

    Combines applet lifecycle management with dialog functionality.
    Suppresses Escape key to prevent accidental dismissal (use Alt+Left).

    Note: Applet must be first in MRO to resolve run() correctly.
    """

    def cancel(self, modified_name: str) -> None:
        """Suppress cancel to prevent accidental applet dismissal.

        Alt+Left is the correct user action to close an applet.
        Cancel implemented here rather than Applet to maintain MRO ordering.

        Args:
            modified_name: Full key name including modifiers.
        """
        beep()


class GptDialog(AppletDialog):
    """GPT integration dialog base class.

    Inherits applet and dialog functionality for OpenAI GPT interactions.
    """
    pass
class TableDialog(Dialog):
    """Dialog displaying tabular data with row/column navigation.

    Contains ValuesElements navigated with Control+Left/Right for columns.
    Up/Down navigate rows as usual, maintaining selected column.
    Supports column-based sorting with Control+S.

    Example subclass: ExplorerDialog with name, date_modified, size columns.

    Attributes:
        headers: Column header labels.
        column: Current column index.
        sort_column: Column used for current sort.
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize table dialog.

        Args:
            *args: Positional arguments passed to Dialog.
            **kwargs: Keyword arguments passed to Dialog.
        """
        super().__init__(*args, **kwargs)
        self.headers = self.get_headers()
        self.column = 0
        self.sort_column = 0

    def element_attributes(self) -> set[str]:
        """Get required element attributes including 'values'.

        Returns:
            Set of required attribute names for table elements.
        """
        return super().element_attributes().union({'values'})

    def get_headers(self) -> list[str]:
        """Get column header labels.

        Override in subclasses to provide actual column names.

        Returns:
            List of column header strings.
        """
        return ['column 1', 'column 2', 'column 3']

    def column_count(self) -> int:
        """Get number of columns in table.

        Returns:
            Number of columns (length of headers).
        """
        return len(self.headers)

    def sort_key(self, element: Element) -> str:
        """Get sort key for element based on current sort column.

        Override for complex scenarios like date or size sorting.

        Args:
            element: Element to get sort key from.

        Returns:
            Value from current sort column, or empty string if not ValuesElement.
        """
        if hasattr(element, 'values'):
            sort_value = element.values[self.sort_column]
        else:
            sort_value = ''
        return sort_value

    def sort_by_column(self, column: int) -> None:
        """Sort table by specified column.

        If already sorted by this column, reverses order instead.

        Args:
            column: Column index to sort by.
        """
        if not (self.sort_column == column):
            self.sort_column = column
            self.sort(key=self.sort_key)
        else:
            self.reverse()

    @KeyHandler.hotkey("Control+S")
    def command_sort(self, modified_name: str) -> None:
        """Interactive column sort selection.

        Prompts user to select column for sorting.

        Args:
            modified_name: Full key name including modifiers.
        """
        current = self.headers[self.sort_column]
        column_name = self.message(
            title=f'Currently sorted by {current}. select column to sort or reverse: ',
            buttons=self.headers + ['cancel'],
            select=current
        )
        if column_name in ('cancel', False):
            self.say_strings('cancelled')
            return
        else:
            # mypy doesn't track narrowing, so cast to str
            column = self.headers.index(cast(str, column_name))
            this_element = self.current()
            this_cursor = self.cursor
            self.sort_by_column(column)
            increment = self.index(this_element) - self.cursor
            self.move(self.index(this_element), 0, True)

    def move(  # type: ignore[override]
        self,
        cursor: int,
        caret: int,
        shift: bool,
        caret_only: bool = False,
        silent: bool = False
    ) -> bool:
        """Move cursor with column update support.

        When changing rows, calls update_column on new element if supported.

        Args:
            cursor: Target cursor position.
            caret: Target caret position within element.
            shift: Whether Shift key is pressed (for selection).
            caret_only: If True, only move caret not cursor.
            silent: If True, suppress speech feedback.

        Returns:
            True if move succeeded, False otherwise.
        """
        changing_row = (cursor != self.cursor)
        update_column = changing_row and hasattr(self.current(), 'update_column')
        moved = super().move(cursor, caret, shift, caret_only=caret_only, silent=silent or update_column)
        if update_column:
            if hasattr(self.current(), 'update_column'):
                self.current().update_column(self.column, silent=silent)  # type: ignore[attr-defined]
            elif not silent:
                self.say_current()
        return moved

    def rotate_elements(
        self,
        increment: int,
        wrap: bool = False,
        silent: bool = False,
        interrupt: bool = True
    ) -> None:
        """Rotate through columns in ValuesElements.

        Updates column index and calls set_index on all ValuesElements.

        Args:
            increment: Number of columns to move (negative for left).
            wrap: If True, wrap around at edges.
            silent: If True, suppress speech feedback.
            interrupt: If True, interrupt current speech.
        """
        if len(self.headers):
            if wrap:
                self.column = (self.column + increment) % len(self.headers)
            else:
                self.column = max(0, min(self.column + increment, len(self.headers) - 1))
        for e in [v for v in self if hasattr(v, 'set_index')]:
            e.set_index(self.column)
        if not silent:
            self.say_strings([self.headers[self.column], str(self.current())], interrupt=interrupt)            
class FormDialog(Dialog):
    """Form dialog with tab navigation between fields.

    Implements form-style navigation where Tab moves between fields
    and Up/Down operate within fields (e.g., in ListElement).

    Field values can be accessed by name: dialog.fieldname returns field value.
    Supports validation at both element and dialog level.

    Attributes:
        say_labels: Whether to speak field labels with values.
        empty_value: Speech text for empty fields (default "empty").
    """

    def __init__(
        self,
        *args: Any,
        say_labels: bool = True,
        empty_value: str = 'empty',
        **kwargs: Any
    ) -> None:
        """Initialize form dialog.

        Args:
            *args: Positional arguments passed to Dialog.
            say_labels: Whether to announce field labels with values.
            empty_value: Speech text for empty form fields.
            **kwargs: Keyword arguments passed to Dialog.
        """
        self.say_labels = say_labels
        self.empty_value = empty_value
        super().__init__(*args, **kwargs)
        # Auto-select first field if it has text
        if len(self) and isinstance(self.current().value, str) and len(self.current().value):
            if hasattr(self.current(), 'command_select_all'):
                self.current().command_select_all('Control+A', silent=True)  # type: ignore[attr-defined]

    def __getattr__(self, name: str) -> Any:
        """Get field value by field name.

        Allows accessing field values as dialog attributes:
        dialog.fieldname returns value of element named "fieldname".

        Note: Don't name elements same as dialog attributes to avoid confusion.

        Args:
            name: Field name to look up.

        Returns:
            Value of element with matching name.

        Raises:
            AttributeError: If no element with that name exists.
        """
        for element in self:
            if element.name == name:
                return element.value
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
    def element_attributes(self) -> set[str]:
        """Get required element attributes including 'char_sink'.

        Returns:
            Set of required attribute names for form elements.
        """
        return super().element_attributes().union({'char_sink'})

    def _register_accelerator(self, element: FormElement) -> None:
        """Register accelerator key for form element.

        For ButtonElements with on_press, calls the callback.
        For other elements, moves focus to the element.

        Args:
            element: FormElement with accelerator attribute.

        Raises:
            ValueError: If accelerator is already assigned to another element.

        Note:
            Conflict checking with @KeyHandler.hotkey happens in KeyHandler.__init__().
        """
        if element.accelerator in self._accelerator_keys:
            raise ValueError(
                f"Duplicate accelerator key '{element.accelerator}' on element '{element.name}'. "
                f"Already registered by another element."
            )

        def accelerator_handler(modified_name: str) -> None:
            if isinstance(element, ButtonElement) and hasattr(element, 'on_press'):
                element.on_press(element)
            else:
                self.set_from_name(element.name)

        # element.accelerator checked as not None in caller (line 537)
        if element.accelerator is None:
            return  # Should never happen, but satisfies mypy
        self._key_methods[element.accelerator] = accelerator_handler
        self._accelerator_keys.add(element.accelerator)
    def generate_content(  # type: ignore[override]
        self,
        filler: Iterable[tuple[type[FormElement], ...] | tuple[type[FormElement], dict[str, Any]]],
        element_type: type[FormElement] | None = None
    ) -> Iterable[FormElement]:
        """Generate form elements from element specifications.

        Each element spec is a tuple of (element_class, kwargs_dict) or
        (element_class, *args). Dict format is preferred.

        Args:
            filler: Iterable of element specifications.
            element_type: Unused, kept for compatibility.

        Yields:
            Constructed FormElement instances with is_element=True.

        Raises:
            TypeError: If element missing required attributes.
        """
        for obj in filler:
            # Construct element - prefer kwargs dict format
            if (len(obj) == 2) and isinstance(obj[1], dict):
                e_type, kwargs = obj
                e = e_type(self, **kwargs)
            else:
                e_type, *args = obj
                e = e_type(self, *args)  # type: ignore[arg-type]

            # Validate using duck typing
            required_attrs = self.element_attributes()
            missing_attrs = required_attrs - set(dir(e))
            if missing_attrs:
                raise TypeError(
                    f"Element {type(e).__name__} is missing required attributes: {missing_attrs}"
                )

            # Register accelerator key if element has one
            if hasattr(e, 'accelerator') and e.accelerator:
                self._register_accelerator(e)

            e.is_element = True  # type: ignore[attr-defined]
            yield e 
    def say_current(
        self,
        interrupt: bool = True,
        suppress_indent_speech: bool = False,
        substitutions: dict[str, str] = punctuator.some
    ) -> None:
        """Speak current field with optional label.

        Args:
            interrupt: Whether to interrupt current speech.
            suppress_indent_speech: Unused, kept for compatibility.
            substitutions: Character substitutions for TTS.
        """
        if self.say_labels:
            s = ' '.join([self.current().name, str(self.current())])
        else:
            s = str(self.current())
        return self.say(s, interrupt=interrupt, substitutions=substitutions)

    def index_from_name(
        self,
        match_string: str,
        match_case: bool = False,
        match_type: str = 'startswith'
    ) -> int | None:
        """Find element index by name.

        Searches forward from current position, wrapping around.

        Args:
            match_string: Name pattern to search for.
            match_case: Whether match is case-sensitive.
            match_type: Match type ('startswith', 'contains', etc).

        Returns:
            Index of matching element, or None if not found.
        """
        matches = [e for e in self[self.cursor+1:] + self[:self.cursor+1]
                   if framework.test_match(e.name, match_string, match_case, match_type)]
        if len(matches):
            return self.index(matches[0])
        return None

    def element_from_name(
        self,
        match_string: str,
        match_case: bool = False,
        match_type: str = 'startswith'
    ) -> FormElement | None:
        """Get element by name.

        Args:
            match_string: Name pattern to search for.
            match_case: Whether match is case-sensitive.
            match_type: Match type ('startswith', 'contains', etc).

        Returns:
            Matching FormElement, or None if not found.
        """
        cursor = self.index_from_name(match_string=match_string, match_case=match_case, match_type=match_type)
        if cursor is not None:
            return self[cursor]
        return None

    def set_from_name(
        self,
        match_string: str,
        match_case: bool = False,
        match_type: str = 'startswith',
        interrupt: bool = False,
        silent: bool = False
    ) -> bool | None:
        """Move focus to element by name.

        Args:
            match_string: Name pattern to search for.
            match_case: Whether match is case-sensitive.
            match_type: Match type ('startswith', 'contains', etc).
            interrupt: Whether to interrupt current speech.
            silent: If True, suppress speech feedback.

        Returns:
            True if element found and focus moved, None if not found.
        """
        cursor = self.index_from_name(match_string=match_string, match_case=match_case, match_type=match_type)
        if cursor is None:
            beep()
        else:
            self.cursor = cursor
            self.selection.reset(self.cursor, 0)
            if not silent:
                self.say_current(interrupt=interrupt)
            return True
        return None

    def no_method(self, modified_name: str) -> Any:
        """Handle unrecognized key combination.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result from parent class no_method.
        """
        return super().no_method(modified_name)
    @KeyHandler.hotkey("Tab")
    def move_focus(self, modified_name: str) -> None:
        """Move focus to next/previous field with Tab/Shift+Tab.

        Validates current field before moving. Calls on_focus if field has one.

        Args:
            modified_name: Full key name including modifiers.
        """
        if modified_name.startswith('Shift'):
            increment = -1
        else:
            increment = 1
        validation = True
        element = self.current()
        if hasattr(element, 'validate') and callable(element.validate):
            validation = element.validate(element)
        # Validation is either True or an error string
        if validation is not True:
            self.say_strings(f"{validation}")
        else:
            if len(self):
                self.move((self.cursor + increment) % len(self), self.current().caret, False, silent=True)
            else:
                beep(tag='attempting to move to different element in empty dialog')
            item = self.current()
            if hasattr(item, 'on_focus') and callable(item.on_focus):
                item.on_focus(item)
            self.say_current()

    @KeyHandler.hotkey("Up")
    def up(self, modified_name: str) -> None:
        """Beep on Up (handled by individual elements).

        Args:
            modified_name: Full key name including modifiers.
        """
        beep()
        self.say_current()

    @KeyHandler.hotkey("Down")
    def down(self, modified_name: str) -> None:
        """Beep on Down (handled by individual elements).

        Args:
            modified_name: Full key name including modifiers.
        """
        beep()
        self.say_current()

    @KeyHandler.hotkey("Prior")
    def prior(self, modified_name: str) -> None:
        """Beep on Page Up (not applicable to forms).

        Args:
            modified_name: Full key name including modifiers.
        """
        beep()

    @KeyHandler.hotkey("Next")
    def next(self, modified_name: str) -> None:
        """Beep on Page Down (not applicable to forms).

        Args:
            modified_name: Full key name including modifiers.
        """
        beep()

    @KeyHandler.hotkey("Control+Home")
    def top(self, modified_name: str, silent: bool = False) -> None:
        """Beep on Control+Home (not applicable to forms).

        Args:
            modified_name: Full key name including modifiers.
            silent: If True, suppress speech feedback.
        """
        beep()

    @KeyHandler.hotkey("Control+End")
    def bottom(self, modified_name: str, silent: bool = False) -> None:
        """Beep on Control+End (not applicable to forms).

        Args:
            modified_name: Full key name including modifiers.
            silent: If True, suppress speech feedback.
        """
        beep()

    def validate_dialog(self) -> bool:
        """Perform dialog-level validation.

        Override to add custom validation logic beyond element validation.
        Called after all element validators pass.

        Returns:
            True if dialog is valid, False otherwise.
        """
        return True

    def validate(self) -> bool:
        """Validate all form elements and dialog.

        Returns:
            True if all element and dialog validation passes.
        """
        validation = all([e.validate(e) is True for e in self
                         if (hasattr(e, 'validate') and callable(e.validate))]) and self.validate_dialog()
        return validation

    def ok(self, modified_name: str) -> Any:
        """Submit form if validation passes.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            None if validation fails (keeps dialog open), otherwise parent ok() result.
        """
        return None if not self.validate() else super().ok(modified_name)

    @KeyHandler.hotkey("Control+Return")
    def alternative_ok(self, modified_name: str) -> Any:
        """Alternative submit for forms with multi-line fields.

        Use when Return key is consumed by text fields.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result from ok().
        """
        return self.ok("Return")

    def fields(self) -> dict[str, Any]:
        """Get current field values (synonym for get_result).

        For readability when extracting values while navigating.

        Returns:
            Dictionary of field names to values.
        """
        return self.get_result()

    def get_result(self) -> dict[str, Any]:  # type: ignore[override]
        """Get result dictionary of all form field values.

        Returns:
            Dict mapping field names to values, plus '__focus__' key
            with dict containing name and value of currently focused field.
        """
        results = {e.name: getattr(e, 'get_result', lambda: e.value)() for e in self}
        results['__focus__'] = {'name': self.current().name, 'value': self.current().value}
        return results 
class HeteroDialog(FormDialog):
    """Synonym for FormDialog for backward compatibility.

    Deprecated - use FormDialog instead.
    """
    pass


class InputDialog(FormDialog):
    """Simple input dialog returning first field value.

    Used for single-field input forms with OK/Cancel buttons.
    """

    def get_result(self) -> str | bool:  # type: ignore[override]
        """Get input value or False if cancelled/empty.

        Returns:
            False if cancelled or empty, otherwise value of first field.
        """
        return False if (self.current().value == 'cancel') or (not len(self[0].value)) else self[0].value


class StdinDialog(FormDialog):
    """Form dialog with stdout capture to output field.

    Registers stdout hook to capture printed output into field named 'output'.
    Used by InspectionDialog and ConsoleDialog.
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize stdin dialog with output capture.

        Args:
            *args: Positional arguments passed to FormDialog.
            **kwargs: Keyword arguments passed to FormDialog.
        """
        super().__init__(*args, **kwargs)
        self.app.stdout.register(self.consume_output)

    def consume_output(self, s: str) -> None:
        """Consume stdout output and append to output field.

        Args:
            s: Output string to capture.
        """
        if len(s.strip()):
            index = self.index_from_name('output')
            if index is not None:
                self[index].append(s.strip())

    def closing(self) -> None:
        """Clean up stdout hook on dialog close."""
        self.app.stdout.unregister(self.consume_output)

    def ok(self, modified_name: str) -> Any:
        """Submit dialog and disable speech purging.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result from parent ok().
        """
        self.home_dialog.purge_speech = False
        return super().ok(modified_name)

    @KeyHandler.hotkey("Tab")
    def move_focus(self, modified_name: str) -> None:
        """Move focus and enable speech purging.

        Args:
            modified_name: Full key name including modifiers.
        """
        self.home_dialog.purge_speech = True
        return super().move_focus(modified_name)             
class RootDialog(AppletDialog):
    """Root launcher dialog for Winston application.

    Manages application exit with confirmation, restart capability,
    and dirty file checking. Auto-restarts on exception.
    """

    def on_close(self, close_reason: bool) -> bool | None:
        """Handle application exit with user confirmation.

        Prompts for confirmation and checks for unsaved work.
        Override to customize close behavior.

        Note: Not called during app.quit_now - use closing() for cleanup.

        Args:
            close_reason: True from Alt+Left, False from Escape.

        Returns:
            close_reason to proceed with close, None to cancel.
        """
        buttons = ['Yes', 'No']
        if self.app.restartable:
            buttons.append('Restart')
        user_result = self.message(title=f'Dismiss {self.app.window_titles[0]}?', buttons=buttons)
        if user_result in ('Yes', 'Restart'):
            # Check for unsaved work in applets
            if names := self.app.applets.get_dirty():
                if self.message(
                    title=f'unsaved work in {", ".join(names)}',
                    buttons=['exit anyway', 'cancel']
                ) != 'exit anyway':
                    return None
            self.app.quit_now = True
            self.app.restartable = (user_result == 'Restart')
            return close_reason
        else:
            self.say_current()
            return None

    def run(self) -> str:  # type: ignore[override]
        """Run root dialog, restarting on exception.

        If dialog crashes, automatically creates new root dialog.

        Returns:
            "normal" or "exception" result code.
        """
        result = super().run(speak=['%current'])
        if result == "exception":
            self.app.add_root()
        return result


class CommandDialog(Dialog):
    """Dialog for selecting commands from list.

    Displays available commands with keyboard shortcuts.
    Returns modified key name for execution.
    """

    def get_result(self) -> str:
        """Extract keyboard shortcut from selected command.

        Returns:
            Modified key name (e.g., "Control+S") for command execution.
        """
        return self.current().value.split(': ')[-1] 
class InputPromptDialog(Dialog):
    """Dialog with input prompt and output display.

    Base class for interactive dialogs like console and Python interpreter.
    Contains InputPromptElement at bottom/top for input, with history navigation
    via Control+Up/Down. Captures stdout/stderr via consume_print().

    Override input_element() to customize prompt element and position.
    Override content_elements() to provide body elements.
    Override ingest_input() to handle submitted input.

    Attributes:
        bottom_input: If True, prompt at bottom; if False, at top.
        history: IndexedList for command history navigation.
    """

    def __init__(
        self,
        parent: Dialog,
        name_seed: str,
        element_type: type[Element] = Element,
        filler: list[Any] | None = None,
        revert_to: Dialog | None = None,
        empty_value: str = 'empty element',
        bottom_input: bool = True,
        output_consumption: bool = True
    ) -> None:
        """Initialize input prompt dialog.

        Args:
            parent: Parent dialog.
            name_seed: Base name for dialog.
            element_type: Element type for body content.
            filler: Initial content for body elements.
            revert_to: Dialog to revert to on close.
            empty_value: Speech text for empty elements.
            bottom_input: If True, prompt at bottom; if False, at top.
            output_consumption: If True, capture stdout/stderr.
        """
        super().__init__(
            parent,
            name_seed,
            element_type=element_type,
            filler=filler or [],
            revert_to=revert_to,
            empty_value=empty_value
        )
        self.bottom_input = bottom_input
        self.history = IndexedList(owner=self)
        self.set_output_consumption(output_consumption)
    def generate_content(  # type: ignore[override]
        self,
        filler: list[Any],
        element_type: type[Element] = Element,
        position: int = 0
    ) -> Generator[Element | InputPromptElement, None, None]:
        """Generate dialog content with input prompt at top or bottom.

        Args:
            filler: Content for body elements.
            element_type: Element type for body content.
            position: Unused, kept for compatibility.

        Yields:
            Body elements and input prompt element.
        """
        contents = list(self.content_elements(filler, element_type))
        bottom, prompt_element = self.input_element()
        self.bottom_input = bottom
        index = 0 if not bottom else len(contents)
        contents.insert(index, prompt_element)
        for item in contents:
            yield item

    def content_elements(
        self,
        filler: list[Any],
        element_type: type[Element]
    ) -> list[Element]:
        """Generate body elements from filler.

        Override to customize body content.

        Args:
            filler: Content for elements.
            element_type: Element type to create.

        Returns:
            List of body elements.
        """
        filler = [] if filler is None else filler
        contents = [element_type(self, 'content', f) for f in filler]
        return contents

    def input_element(self) -> tuple[bool, InputPromptElement]:
        """Create input prompt element.

        Override to customize prompt element and position.

        Returns:
            Tuple of (bottom_position, prompt_element).
        """
        return (True, InputPromptElement(self, 'edit', ''))

    def char_sink(self, char: str) -> None:
        """Route character input to prompt element.

        Args:
            char: Single character from keyboard.
        """
        self.input_line()
        self.current().char_sink(char)  # type: ignore[attr-defined]

    def cancel(self, modified_name: str) -> None:
        """Handle Escape - move to input line and speak.

        Args:
            modified_name: Full key name including modifiers.
        """
        self.input_line()
        self.say_current()

    def input_line(self) -> None:
        """Move cursor to input prompt element."""
        i_cursor = len(self) - 1 if self.bottom_input else 0
        i_caret = self[i_cursor].caret
        self.move(i_cursor, i_caret, False, silent=True)

    def ok(self, modified_name: str) -> None:
        """Beep on unhandled Return key.

        Args:
            modified_name: Full key name including modifiers.
        """
        from .framework import beep
        beep()  # Don't let Enter dismiss dialog if not trapped by element  
    def accept_input(self, value: str) -> None:
        """Accept submitted input - ingest and add to history.

        Args:
            value: Input string from prompt element.
        """
        self.ingest_input(value)
        self.add_to_history(value)

    def ingest_input(self, value: str) -> None:
        """Process submitted input.

        Override in subclass for custom handling.
        Default implementation prints to stdout.

        Args:
            value: Input string to process.
        """
        print(value)

    def worth_recording(self, input_string: str) -> bool:
        """Check if input should be added to history.

        Args:
            input_string: Input to check.

        Returns:
            True if non-empty and different from last history entry.
        """
        if input_string:
            if not self.history or (input_string != self.history[-1]):
                return True
        return False

    def add_to_history(self, input_string: str) -> None:
        """Add input to history if worth recording.

        Args:
            input_string: Input to add.
        """
        if self.worth_recording(input_string):
            self.history.add(input_string)

    @KeyHandler.hotkey("Control+Up")
    def get_previous(self, modified_name: str) -> None:
        """Navigate to previous history entry.

        Args:
            modified_name: Full key name including modifiers.
        """
        self[-1].value = self.history.get_previous()
        self.move_and_say_input()

    @KeyHandler.hotkey("Control+Down")
    def get_next(self, modified_name: str) -> None:
        """Navigate to next history entry.

        Args:
            modified_name: Full key name including modifiers.
        """
        self[-1].value = self.history.get_next()
        self.move_and_say_input()

    def move_and_say_input(self) -> None:
        """Move to input line if needed and speak current."""
        if self.cursor != (len(self) - 1):
            self.bottom('')
        else:
            self.say_current()

    def set_output_consumption(self, active: bool) -> None:
        """Enable/disable stdout/stderr consumption.

        Args:
            active: If True, register consume_print; if False, unregister.
        """
        if active:
            self.app.stdout.register(self.consume_print)
        else:
            self.app.stdout.unregister(self.consume_print)

    def consume_print(self, s: str) -> None:
        """Consume print output and insert as elements.

        Args:
            s: Output string to consume (may contain newlines).
        """
        what = None
        for v in s.split('\n'):
            what = v.strip()
            if len(what):
                self.insert(len(self) - 1, Element(self, name='printed output', value=what))
        if what:
            self.say_strings(what)

    @KeyHandler.hotkey("Control+C")
    def command_copy_or_interrupt(self, modified_name: str) -> None:
        """Handle Control+C - interrupt or copy.

        If input_to_q active, send KeyboardInterrupt; otherwise copy.

        Args:
            modified_name: Full key name including modifiers.
        """
        if hasattr(self, 'input_to_q') and self.input_to_q:
            self.input_q.put(KeyboardInterrupt)  # type: ignore[attr-defined]
        else:
            super().command_copy(modified_name)

    @KeyHandler.hotkey("Alt+Left")
    def close_dialog(self, modified_name: str) -> bool:
        """Close dialog with Alt+Left.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            True to exit dialog.
        """
        return True 
        