diff --git a/sandbox/darren/file_search.py b/sandbox/darren/file_search.py index 240e2bec1..c008b3d17 100644 --- a/sandbox/darren/file_search.py +++ b/sandbox/darren/file_search.py @@ -62,7 +62,7 @@ class FileSearchApp(App): self.file_table.filter = event.value -app = FileSearchApp(log_path="textual.log", css_path="file_search.scss", watch_css=True) +app = FileSearchApp(css_path="file_search.scss", watch_css=True) if __name__ == "__main__": result = app.run() diff --git a/sandbox/darren/file_search.scss b/sandbox/darren/file_search.scss index 5163bd5b1..6b8839216 100644 --- a/sandbox/darren/file_search.scss +++ b/sandbox/darren/file_search.scss @@ -1,6 +1,6 @@ Screen { - layout: dock; - docks: top=top bottom=bottom; + + } #file_table_wrapper { diff --git a/sandbox/will/input.py b/sandbox/will/input.py new file mode 100644 index 000000000..861ce005a --- /dev/null +++ b/sandbox/will/input.py @@ -0,0 +1,17 @@ +from textual.app import App +from textual.widgets import TextInput + + +class InputApp(App): + + CSS = """ + TextInput { + + } + """ + + def compose(self): + yield TextInput(initial="foo") + + +app = InputApp() diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 55883a7fa..77a8f858e 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -101,7 +101,7 @@ class XTermParser(Parser[events.Event]): key_events = sequence_to_key_events(character) for event in key_events: if event.key == "escape": - event = events.Key(event.sender, key="^") + event = events.Key(event.sender, "^", None) on_token(event) while not self.is_eof: @@ -229,7 +229,21 @@ class XTermParser(Parser[events.Event]): on_token(event) def _sequence_to_key_events(self, sequence: str) -> Iterable[events.Key]: - default = (sequence,) if len(sequence) == 1 else () - keys = ANSI_SEQUENCES_KEYS.get(sequence, default) - for key in keys: - yield events.Key(self.sender, key) + """Map a sequence of code points on to a sequence of keys. + + Args: + sequence (str): Sequence of code points. + + Returns: + Iterable[events.Key]: keys + + """ + + keys = ANSI_SEQUENCES_KEYS.get(sequence) + if keys is not None: + for key in keys: + yield events.Key( + self.sender, key.value, sequence if len(sequence) == 1 else None + ) + elif len(sequence) == 1: + yield events.Key(self.sender, sequence, sequence) diff --git a/src/textual/app.py b/src/textual/app.py index b0695f530..04e92e20c 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -656,7 +656,9 @@ class App(Generic[ReturnType], DOMNode): await asyncio.sleep(0.05) else: print(f"press {key!r}") - driver.send_event(events.Key(self, key)) + driver.send_event( + events.Key(self, key, key if len(key) == 1 else None) + ) await asyncio.sleep(0.01) if screenshot: self._screenshot = self.export_screenshot( diff --git a/src/textual/events.py b/src/textual/events.py index e868eedab..cd4c5301e 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -188,18 +188,26 @@ class Key(InputEvent): """Sent when the user hits a key on the keyboard. Args: - sender (MessageTarget): The sender of the event (the App) - key (str): The pressed key if a single character (or a longer string for special characters) + sender (MessageTarget): The sender of the event (the App). + key (str): A key name (textual.keys.Keys). + char (str | None, optional): A printable character or None if it is not printable. """ - __slots__ = ["key"] + __slots__ = ["key", "char"] - def __init__(self, sender: MessageTarget, key: str) -> None: + def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None: super().__init__(sender) - self.key = key.value if isinstance(key, Keys) else key + self.key = key + self.char = (key if len(key) == 1 else None) if char is None else char def __rich_repr__(self) -> rich.repr.Result: yield "key", self.key + yield "char", self.char, None + + @property + def key_name(self) -> str | None: + """Name of a key suitable for use as a Python identifier.""" + return self.key.replace("+", "_") @property def is_printable(self) -> bool: @@ -209,7 +217,7 @@ class Key(InputEvent): Returns: bool: True if the key is printable. """ - return self.key == Keys.Space or self.key not in KEY_VALUES + return False if self.char is None else self.char.isprintable() @rich.repr.auto diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index b814e1a7d..4700538d8 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -517,7 +517,7 @@ class MessagePump(metaclass=MessagePumpMeta): Args: event (events.Key): A key event. """ - key_method = getattr(self, f"key_{event.key}", None) + key_method = getattr(self, f"key_{event.key_name}", None) if key_method is not None: if await invoke(key_method, event): event.prevent_default() diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 4231127fc..552e5109e 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -19,6 +19,7 @@ __all__ = [ "Placeholder", "Pretty", "Static", + "TextInput", "TreeControl", ] diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index b903300c5..40c986a59 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -1,10 +1,11 @@ # This stub file must re-export every classes exposed in the __init__.py's `__all__` list: from ._button import Button as Button -from ._data_table import DataTable +from ._data_table import DataTable as DataTable from ._directory_tree import DirectoryTree as DirectoryTree from ._footer import Footer as Footer from ._header import Header as Header from ._placeholder import Placeholder as Placeholder from ._pretty import Pretty as Pretty from ._static import Static as Static +from ._text_input import TextInput as TextInput from ._tree_control import TreeControl as TreeControl diff --git a/src/textual/widgets/text_input.py b/src/textual/widgets/_text_input.py similarity index 97% rename from src/textual/widgets/text_input.py rename to src/textual/widgets/_text_input.py index 966faf2a1..9e96dfa4f 100644 --- a/src/textual/widgets/text_input.py +++ b/src/textual/widgets/_text_input.py @@ -40,12 +40,9 @@ class TextWidgetBase(Widget): key = event.key if key == "escape": return - elif key == "space": - key = " " - changed = False - if event.is_printable: - changed = self._editor.insert(key) + if event.char is not None and event.is_printable: + changed = self._editor.insert(event.char) elif key == "ctrl+h": changed = self._editor.delete_back() elif key == "ctrl+d": @@ -59,11 +56,11 @@ class TextWidgetBase(Widget): elif key == "end" or key == "ctrl+e": self._editor.cursor_text_end() + self.refresh(layout=True) + if changed: self.post_message_no_wait(self.Changed(self, value=self._editor.content)) - self.refresh(layout=True) - def _apply_cursor_to_text(self, display_text: Text, index: int) -> Text: if index < 0: return display_text @@ -115,10 +112,9 @@ class TextInput(TextWidgetBase, can_focus=True): DEFAULT_CSS = """ TextInput { width: auto; - background: $surface; height: 3; - padding: 0 1; - content-align: left middle; + padding: 1; + background: $surface; } """ @@ -165,11 +161,15 @@ class TextInput(TextWidgetBase, can_focus=True): self._editor.cursor_text_end() self.refresh() - def on_resize(self, event: events.Resize) -> None: + def get_content_width(self, container: Size, viewport: Size) -> int: + # TODO: Why does this need +2 ? + return min(cell_len(self._editor.content) + 2, container.width) + + def _on_resize(self, event: events.Resize) -> None: # Ensure the cursor remains visible when the widget is resized self._reset_visible_range() - def on_click(self, event: events.Click) -> None: + def _on_click(self, event: events.Click) -> None: """When the user clicks on the text input, the cursor moves to the character that was clicked on. Double-width characters makes this more difficult."""