From 353a31f56af96bcc024ed6b254792c9eca6f6b29 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 17 May 2022 13:27:34 +0100 Subject: [PATCH] Basic suggestions --- sandbox/input.py | 22 +++++++++++++++++++++- src/textual/widgets/text_input.py | 27 ++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/sandbox/input.py b/sandbox/input.py index f681cace3..844a0ba99 100644 --- a/sandbox/input.py +++ b/sandbox/input.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from pathlib import Path + from textual.app import App from textual.widget import Widget @@ -12,6 +16,18 @@ def fahrenheit_to_celsius(fahrenheit: float) -> float: return (fahrenheit - 32) / 1.8 +words = set(Path("/usr/share/dict/words").read_text().splitlines()) + + +def word_autocompleter(value: str) -> str | None: + print(value) + for word in words: + if word.startswith(value): + print("autocompleter suggests: ", word) + return word[len(value) :] + return None + + class InputApp(App[str]): def on_mount(self) -> None: self.fahrenheit = TextInput(placeholder="Fahrenheit", id="fahrenheit") @@ -20,7 +36,11 @@ class InputApp(App[str]): text_boxes = Widget(self.fahrenheit, self.celsius) self.mount(inputs=text_boxes) self.mount(spacer=Widget()) - self.mount(footer=TextInput(placeholder="Footer Search Bar")) + self.mount( + footer=TextInput( + placeholder="Footer Search Bar", autocompleter=word_autocompleter + ) + ) self.mount(text_area=TextArea()) def handle_changed(self, event: TextWidgetBase.Changed) -> None: diff --git a/src/textual/widgets/text_input.py b/src/textual/widgets/text_input.py index ed0ed4719..8841aac42 100644 --- a/src/textual/widgets/text_input.py +++ b/src/textual/widgets/text_input.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable + from rich.console import RenderableType from rich.padding import Padding from rich.style import Style @@ -47,6 +49,7 @@ class TextWidgetBase(Widget): self.post_message_no_wait(self.Changed(self, value=self._editor.content)) self.refresh(layout=True) + print("at end of TextWidgetBase.on_key") def _apply_cursor_to_text(self, display_text: Text, index: int): # Either write a cursor character or apply reverse style to cursor location @@ -80,6 +83,17 @@ class TextWidgetBase(Widget): class TextInput(TextWidgetBase, can_focus=True): + """Widget for inputting text + + Args: + placeholder (str): The text that will be displayed when there's no content in the TextInput. + Defaults to an empty string. + initial (str): The initial value. Defaults to an empty string. + autocompleter (Callable[[str], str | None): Function which returns autocomplete suggestion + which will be displayed within the widget any time the content changes. The autocomplete + suggestion will be displayed as dim text similar to suggestion text in the zsh or fish shells. + """ + CSS = """ TextInput { width: auto; @@ -110,6 +124,7 @@ class TextInput(TextWidgetBase, can_focus=True): *, placeholder: str = "", initial: str = "", + autocompleter: Callable[[str], str | None] | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -118,6 +133,8 @@ class TextInput(TextWidgetBase, can_focus=True): self.placeholder = placeholder self._editor = TextEditorBackend(initial, 0) self.visible_range: tuple[int, int] | None = None + self.autocompleter = autocompleter + self._suggestion = "" @property def value(self): @@ -131,6 +148,7 @@ class TextInput(TextWidgetBase, can_focus=True): def render(self, style: Style) -> RenderableType: # First render: Cursor at start of text, visible range goes from cursor to content region width + print("inside render") if not self.visible_range: self.visible_range = (self._editor.cursor_index, self.content_region.width) @@ -141,6 +159,12 @@ class TextInput(TextWidgetBase, can_focus=True): visible_text = self._editor.get_range(start, end) display_text = Text(visible_text, no_wrap=True) + # TODO: This should not be done in renderer + if self.autocompleter is not None: + self._suggestion = self.autocompleter(self.value) + if self._suggestion: + display_text.append(self._suggestion, "dim") + if show_cursor: display_text = self._apply_cursor_to_text( display_text, self._editor.cursor_index - start @@ -169,6 +193,7 @@ class TextInput(TextWidgetBase, can_focus=True): if scrollable and self._editor.query_cursor_right(): self.visible_range = (start + 1, end + 1) else: + # If the user has hit the scroll limit self.app.bell() elif key == "left": if cursor_index == start: @@ -178,7 +203,6 @@ class TextInput(TextWidgetBase, can_focus=True): cursor_index + available_width - 1, ) else: - # If the user has hit the scroll limit self.app.bell() elif key == "ctrl+h": if cursor_index == start and self._editor.query_cursor_left(): @@ -204,6 +228,7 @@ class TextInput(TextWidgetBase, can_focus=True): # We need to clamp the visible range to ensure we don't use negative indexing start, end = self.visible_range self.visible_range = (max(0, start), end) + print("at end of TextInput.on_key") class Submitted(Message, bubble=True): def __init__(self, sender: MessageTarget, value: str) -> None: