diff --git a/console_gpt/custom_stdout.py b/console_gpt/custom_stdout.py index 65180ff..40f52e1 100644 --- a/console_gpt/custom_stdout.py +++ b/console_gpt/custom_stdout.py @@ -8,14 +8,14 @@ # Define the specific types for 'ptype' PrintType = Literal["ok", "warn", "info", "error", "sigint", "exit", "changelog"] +HeaderColor = Literal["green", "yellow", "blue", "red", "white", "cyan"] -def markdown_print(data: str, header: Optional[str] = None, end: Optional[str] = ""): +def markdown_print(data: str, header: Optional[str] = None, end: Optional[str] = "", header_color: Optional[HeaderColor] = "blue") -> None: console = Console() - # Print the header if it exists if header: - header_text = Text(f"╰─❯ {header}:", style="blue underline bold") + header_text = Text(f"╰─❯ {header}:", style=f"{header_color} underline bold") console.print(header_text, end=end) # Create a Markdown object diff --git a/console_gpt/menus/command_handler.py b/console_gpt/menus/command_handler.py index 4fcf361..828d439 100644 --- a/console_gpt/menus/command_handler.py +++ b/console_gpt/menus/command_handler.py @@ -1,6 +1,6 @@ from typing import Optional -from console_gpt.custom_stdout import custom_print +from console_gpt.custom_stdout import custom_print, markdown_print from console_gpt.general_utils import help_message from console_gpt.menus.settings_menu import settings_menu from console_gpt.menus.tools_menu import tools_menu @@ -58,6 +58,7 @@ def command_handler(model_title, model_name, user_input, conversation, cached) - else: user_input = multiline_data else: + markdown_print(clarification, header="Prompt clarifications", header_color="yellow", end="\n") if cached is True: user_input = clarification, multiline_data else: diff --git a/console_gpt/prompts/multiline_prompt.py b/console_gpt/prompts/multiline_prompt.py index 9927327..64a8843 100644 --- a/console_gpt/prompts/multiline_prompt.py +++ b/console_gpt/prompts/multiline_prompt.py @@ -1,55 +1,198 @@ -import re -from typing import Dict, Optional - +from textual.app import App, ComposeResult +from textual.containers import Vertical, Horizontal +from textual.reactive import reactive +from textual.widgets import Button, Static, TextArea +from typing import Optional from console_gpt.catch_errors import eof_wrapper -from console_gpt.constants import style -from console_gpt.custom_stdin import custom_input -from console_gpt.custom_stdout import custom_print +from rich.console import Console -# Compile the regex once at the module level -stop_regex = re.compile(r"(\n|\s)+$") +console = Console() +class MultilinePromptApp(App): + """Textual app for handling multiline prompts.""" -def _validate_description(val: str) -> str | bool: - """ - Sub-function to _add_custom_role() which validates - the user input and does not allow empty values - :param val: The STDIN from the user - :return: Either error string or bool to confirm that - the user input is valid + CSS = """ + Screen { + align: center middle; + } + + #dialog { + padding: 1 2; + border: heavy $background 80%; + height: 100%; + width: 80%; + layout: vertical; + } + + #additional_input_area { + width: 100%; + height: 3; + min-height: 1; + max-height: 9; + } + + #multiline_input { + width: 100%; + height: 10; + max-height: 30; + } + + #output_label { + background: $error 20%; + color: $error; + margin: 1; + padding: 1; + border: round $error; + text-align: center; + text-style: bold; + height: auto; + display: none; + } + + #output_label.show { + display: block; + } + + #info_label { + background: $success 20%; + color: $success; + margin: 1; + padding: 1; + border: round $success; + text-align: center; + text-style: bold; + height: auto; + display: none; + } + + #info_label.show { + display: block; + } + + TextArea { + width: 100%; + } + + TextArea:focus { + border: tall $accent !important; + } + + #additional_placeholder, #multiline_placeholder { + color: $text; + padding-left: 1; + text-style: bold; + } + + .buttons { + width: 100%; + height: auto; + align: center bottom; + } """ - if not val or stop_regex.match(val): - return "Empty input not allowed!" - return True + + multiline_data: str = reactive("") + additional_data: Optional[str] = reactive(None) + + def show_error(self, message: str) -> None: + """Display an error message.""" + output_label = self.query_one("#output_label") + output_label.update(f"{message}") + output_label.add_class("show") + + def clear_error(self) -> None: + """Clear the error message.""" + output_label = self.query_one("#output_label") + output_label.update("") + output_label.remove_class("show") + + def clear_info(self) -> None: + """Clear the info message.""" + info_label = self.query_one("#info_label") + info_label.remove_class("show") + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Vertical( + Static("💡 Use to navigate and CTRL+Q or 'Exit' to quit.", id="info_label"), + Static("", id="output_label"), + Static("Instructions or actions to perform:", id="additional_placeholder"), + TextArea(id="additional_input_area"), + + Static("Enter multiline text here:", id="multiline_placeholder"), + TextArea(id="multiline_input"), + + Horizontal( + Button("Submit", id="submit_button", variant="primary"), + Button("Exit", id="exit_button", variant="default"), + classes="buttons" + ), + + id="dialog" + ) + + def on_mount(self) -> None: + # Enable both text areas on mount + self.query_one("#additional_input_area").disabled = False + self.query_one("#multiline_input").disabled = False + self.query_one("#additional_input_area").focus() + self.query_one("#info_label").add_class("show") + + def on_text_area_changed(self, event: TextArea.Changed) -> None: + """Handle text changes in TextArea widgets.""" + self.clear_error() + self.clear_info() + + if event.text_area.id == "multiline_input": + self.multiline_data = event.text_area.text + elif event.text_area.id == "additional_input_area": + self.additional_data = event.text_area.text + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button presses.""" + if event.button.id == "submit_button": + self.submit_data() + elif event.button.id == "exit_button": + self.multiline_data = "" + self.additional_data = None + self.exit((self.additional_data, self.multiline_data)) + + def clean_up_input(self, input_text: str) -> str: + """Clean up the input text by removing leading and trailing whitespaces.""" + return input_text.strip() + + def submit_data(self): + """Submit the data after validation.""" + multiline_input = self.query_one("#multiline_input") + additional_input = self.query_one("#additional_input_area") + + cleaned_multiline_data = self.clean_up_input(multiline_input.text) + cleaned_additional_data = self.clean_up_input(additional_input.text) if additional_input.text else None + + if not cleaned_multiline_data: + self.show_error("Main text field cannot be empty!") + multiline_input.focus() + return + + if cleaned_additional_data == "": + self.show_error("Additional info cannot be just spaces or new lines!") + additional_input.focus() + return + + self.exit((cleaned_additional_data, cleaned_multiline_data)) + @eof_wrapper -def multiline_prompt() -> Optional[str]: - """ - Multiline prompt which allows writing on multiple lines without +def multiline_prompt() -> tuple[Optional[str], str]: + """Multiline prompt which allows writing on multiple lines without "Enter" (Return) interrupting your input. - :return: The content or None (If cancelled) + :return: Tuple containing additional data (Optional[str]) and multiline data (str) """ - multiline_data = custom_input( - is_single_line=False, - auto_exit=False, - message="Multiline input", - style=style, - qmark="❯", - validate=_validate_description, - multiline=True, - ) - if not multiline_data: - custom_print("info", "Cancelled. Continuing normally!") - return None, None - - additional_data = custom_input( - auto_exit=False, - message="Additional clarifications? (Press 'ENTER' to cancel):", - style=style, - qmark="❯", - ) - if additional_data: - return additional_data, f"This is a multiline input:\n{multiline_data}" - else: - return None, f"This is a multiline input:\n{multiline_data}" + + app = MultilinePromptApp() + try: + additional_data, multiline_data = app.run() + except TypeError: + return None, "" + + return additional_data, multiline_data \ No newline at end of file