Files
EvoScientist-Multi/EvoScientist/cli/widgets/chat_input.py
T
Xi Zhang cc266dd6b1 Feat/latex onboard setup and tui completion fix (#88)
* feat(onboard): add LaTeX setup steps and TinyTeX installation helpers

* fix(onboard): improve LaTeX status output to a single-line summary

* fix(assets): update wechat_group image
2026-03-23 10:51:42 +00:00

116 lines
3.3 KiB
Python

"""Multi-line chat input widget with Enter-to-submit and modifier+Enter newline."""
from __future__ import annotations
import sys
from collections.abc import Callable
from typing import ClassVar
from textual import events
from textual.binding import Binding
from textual.message import Message
from textual.widgets import TextArea
class ChatTextArea(TextArea):
"""TextArea that submits on Enter and inserts newline on modifier+Enter.
Emits :class:`ChatTextArea.Submitted` when the user presses Enter
with non-empty text. Modifier+Enter (Option+Enter on macOS,
Ctrl+J everywhere) inserts a literal newline instead.
An optional *before_submit* callback can be set to intercept Enter
before submission. If it returns ``True`` the submit is suppressed
(the callback handled the event itself).
"""
BINDINGS: ClassVar[list[Binding]] = [
Binding(
"shift+enter,ctrl+j,alt+enter,ctrl+enter",
"insert_newline",
"New Line",
show=False,
priority=True,
),
]
DEFAULT_CSS = """
ChatTextArea {
height: auto;
min-height: 1;
max-height: 8;
border: none;
padding: 0;
background: transparent;
}
ChatTextArea:focus {
border: none;
}
ChatTextArea .text-area--cursor-line {
background: transparent;
}
"""
class Submitted(Message):
"""Posted when the user presses Enter to submit."""
def __init__(self, value: str) -> None:
super().__init__()
self.value = value
def __init__(
self,
*,
placeholder: str = "",
id: str | None = None,
before_submit: Callable[[], bool] | None = None,
) -> None:
super().__init__(
id=id,
language=None,
show_line_numbers=False,
soft_wrap=True,
)
self._placeholder = placeholder
self.before_submit: Callable[[], bool] | None = before_submit
@property
def value(self) -> str:
"""Get the current text content."""
return self.text
@value.setter
def value(self, new_value: str) -> None:
"""Set the text content (clears and replaces)."""
self.clear()
if new_value:
self.insert(new_value)
def action_insert_newline(self) -> None:
"""Insert a literal newline character."""
self.insert("\n")
async def _on_key(self, event: events.Key) -> None:
"""Handle Enter as submit.
If *before_submit* is set and returns ``True``, the submit is
suppressed (the callback already handled the Enter press, e.g.
to apply a completion selection).
"""
if event.key == "enter":
event.prevent_default()
event.stop()
# Let the host intercept Enter (e.g. for completion selection)
if self.before_submit and self.before_submit():
return
value = self.text.strip()
if value:
self.post_message(self.Submitted(value))
return
await super()._on_key(event)
@staticmethod
def newline_shortcut_label() -> str:
"""Return the platform-native label for the newline shortcut."""
return "Option+Enter" if sys.platform == "darwin" else "Ctrl+J"