Newer
Older
navi-1 / clients / terminal / tui / widgets / input_box.py
"""Input box widget for the TUI.

A multi-line prompt: ``Enter`` sends the message, ``Ctrl+Enter`` inserts a hard
line break, and long lines soft-wrap visually (``TextArea`` defaults to
``soft_wrap=True``). The field grows with content up to ``max-height`` and then
scrolls internally.
"""

from __future__ import annotations

from textual import events
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.widgets import TextArea

from clients.terminal.tui.events import UserSubmitted


class _PromptInput(TextArea):
    """Multi-line text area that submits on Enter and breaks on Ctrl+Enter."""

    async def _on_key(self, event: events.Key) -> None:
        # Intercept before TextArea's own _on_key, which maps "enter" -> "\n".
        if event.key == "enter":
            event.stop()
            event.prevent_default()
            self.action_submit()
            return
        if event.key == "ctrl+enter":
            event.stop()
            event.prevent_default()
            self.insert("\n")
            return
        await super()._on_key(event)

    def action_submit(self) -> None:
        text = self.text
        if text.strip():
            self.post_message(UserSubmitted(text))
        self.text = ""


class InputBox(Vertical):
    """Bottom prompt frame with a multi-line input field."""

    DEFAULT_CSS = """
    InputBox {
        height: auto;
        min-height: 3;
        border: heavy $tui-prompt-border;
        background: $tui-surface;
        color: $tui-text;
        padding: 0 1;
    }
    InputBox > TextArea {
        height: auto;
        min-height: 3;
        max-height: 12;
        width: 100%;
        border: none;
        background: $tui-surface;
        color: $tui-text;
        padding: 0;
    }
    InputBox > TextArea:focus {
        border: none;
        background-tint: transparent;
    }
    """

    def __init__(self) -> None:
        super().__init__()
        self._input = _PromptInput(
            text="",
            placeholder="Ask anything... (Enter to send, Ctrl+Enter for a new line)",
            classes="input-field",
            show_line_numbers=False,
            soft_wrap=True,
        )

    def compose(self) -> ComposeResult:
        yield self._input

    def on_mount(self) -> None:
        self._input.focus()

    def focus_input(self) -> None:
        self._input.focus()

    def set_placeholder(self, text: str) -> None:
        self._input.placeholder = text