Repository: Textualize/textual
Pull request: Input completion suggestions
Adds suggested completions to the `Input`

- `Input` accepts a parameter `suggestions` with a list of suggestions that are shown while the user types.
- `Input.suggestions` reactive can be used to change the possible suggestions for a given input.
- `Input` has new component class `input--suggestion`.
- Keybinding <kbd>right</kbd> in `Input` is also used to accept a suggestion if the cursor is at the end of the input.

Closes #2330.

Problem statement:
Input auto-complete
We need a way to suggest auto-complete for the Input widget.

The auto-complete I'm thinking of is when you start typing and a the rest of the word or phrase appears in front of the cursor. Something like this:

You hit `T` and the text `extualize` appears with a different style in front of the cursor:

T*extualize*

Hitting `e` adds *xtualize*

Te*xtualize*

Pressing escape cancels the auto complete, pressing return accept it.

## The tricky part

The obvious solution would be to have a callback that takes the text in the input, and returns a potential auto-complete. However, we want this to be asynchronous. i.e. the callback should be able to do non-blocking work without making typing slow.

The probably probably requires the use of workers. Please discuss with @willmcgugan re the proposed solution.

Hints from issue discussion:
Usually with this kind of interface pressing the right arrow when the cursor is at the end accepts it. Pressing return won't work because it would be unclear whether the user is trying to submit input or accept the autocomplete.
@darrenburns I do find that annoying. But return to accept the auto-complete seems to be common. If you don't want to accept the auto-complete, you can press escape to dismiss it first.
> The auto-complete I'm thinking of is when you start typing and a the rest of the word or phrase appears in front of the cursor

Hope you don't mind me sticking my nose in... might this also allow a way showing a list of possible completions? This can be  helpful when you might not know _exactly_ what you're looking for.

(I'm aware there's some crossover with darren's personal autocomplete project here, so don't want to step on any toes!)
I think we will have a builtin version of Darren's autocomplete at some point. Which might extend this feature. But its also nice to have this in isolation.
Consider the quick typing (mindless) user looking elsewhere doing data entry.

If it's a closed set of options, it's fine to have `enter` to accept, if it's free text, what if they did try and write 'te'? There should be a canonical way for them to input exactly that, regardless of possible completions, I guess making sure to hit `escape` works for that... I think you mention insipration by vue etc. how do they handle this?
I think we kind of _have_ to accept auto-completion on <kbd>Enter</kbd>. (Independently of also allowing <kbd>right</kbd> or <kbd>Tab</kbd> to accept the suggestion or not.)

Suppose we _didn't_.
Someone opens an app with an input, starts typing, and sees an auto-completion suggestion.
The user presses <kbd>Enter</kbd> to accept the suggestion.
Instead of accepting it, the user unintentionally triggered the message `Input.Submitted` which may have consequences that are difficult to recover from just because the user used the wrong keybinding.

The other side of this story is "the user wanted to submit _exactly_ what was typed, even though there is a suggestion showing, and upon pressing <kbd>Enter</kbd> the suggestion is accepted instead of submitting the input".
This is much easier to recover from because the user only has to delete the unwanted characters.
@rodrigogiraoserrao That's user error. The auto-completion is dimmed out, so it doesn't appear to the user that pressing enter will submit the entire completion suggestion.

This is how it works in both `zsh` and `fish` and many other shells. `prompt-toolkit` also takes the exact same approach.

<img width="572" alt="image" src="https://github.com/Textualize/textual/assets/5740731/31a31fb5-7f60-449c-8c0e-d1f30a1bf70b">

If I press enter in the image above, it submits "env TEX". If I press right or tab it fills in the remainder of the command. Additionally if I press Option+right, it fills a single word.

To me, if we implement it in the way suggested earlier it's just doing it the opposite of how these things generally work in terminals and is really counterintuitive. I've never seen the approach suggested in this thread anywhere before personally and it doesn't feel like it'd be intuitive for terminal users.

**Side note:** I like how prompt-toolkit calls this feature "autosuggest" and reserves "autocomplete" for its autocomplete dropdown feature: https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#auto-suggestion

That makes more sense to me. I've always hated return to auto-complete. Often leads me to entering things I didn't intend to.

Commit message: Add explicit sleep.

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42bf44a47d..d702b2e246 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
 
+## Unreleased
+
+### Added
+
+- `Input` widget now supports showing automatic input suggestions https://github.com/Textualize/textual/issues/2330
+- `Input` accepts a parameter `suggestions` with a list of suggestions that show up while the user types https://github.com/Textualize/textual/pull/2604
+- `Input.suggestions` reactive can be used to change the possible completions for a given input https://github.com/Textualize/textual/pull/2604
+- `Input` has new component class `input--suggestion` https://github.com/Textualize/textual/pull/2604
+
+### Changed
+
+- Keybinding <kbd>right</kbd> in `Input` is also used to accept a suggestion if the cursor is at the end of the input https://github.com/Textualize/textual/pull/2604
+
+
 ## [0.25.0] - 2023-05-17
 
 ### Changed
diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py
index e14dcdf109..d0432871a9 100644
--- a/src/textual/widgets/_input.py
+++ b/src/textual/widgets/_input.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 import re
-from typing import ClassVar
+from typing import ClassVar, Iterable, List, Optional
 
 from rich.cells import cell_len, get_character_cell_size
 from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
@@ -9,7 +9,7 @@
 from rich.segment import Segment
 from rich.text import Text
 
-from .. import events
+from .. import events, work
 from .._segment_tools import line_crop
 from ..binding import Binding, BindingType
 from ..events import Blur, Focus, Mount
@@ -31,13 +31,28 @@ def __rich_console__(
     ) -> "RenderResult":
         input = self.input
         result = input._value
-        if input._cursor_at_end:
-            result.pad_right(1)
-        cursor_style = input.get_component_rich_style("input--cursor")
+        width = input.content_size.width
+
+        # Add the completion with a faded style.
+        value = input.value
+        value_length = len(value)
+        suggestion = input._suggestion
+        show_suggestion = suggestion.startswith(value) and (
+            len(suggestion) > value_length
+        )
+        if show_suggestion:
+            result += Text(
+                suggestion[value_length:],
+                input.get_component_rich_style("input--suggestion"),
+            )
+
         if self.cursor_visible and input.has_focus:
+            if not show_suggestion and input._cursor_at_end:
+                result.pad_right(1)
+            cursor_style = input.get_component_rich_style("input--cursor")
             cursor = input.cursor_position
             result.stylize(cursor_style, cursor, cursor + 1)
-        width = input.content_size.width
+
         segments = list(result.render(console))
         line_length = Segment.get_line_length(segments)
         if line_length < width:
@@ -80,7 +95,7 @@ class Input(Widget, can_focus=True):
     | :- | :- |
     | left | Move the cursor left. |
     | ctrl+left | Move the cursor one word to the left. |
-    | right | Move the cursor right. |
+    | right | Move the cursor right or accept the completion suggestion. |
     | ctrl+right | Move the cursor one word to the right. |
     | backspace | Delete the character to the left of the cursor. |
     | home,ctrl+a | Go to the beginning of the input. |
@@ -93,12 +108,17 @@ class Input(Widget, can_focus=True):
     | ctrl+k | Delete everything to the right of the cursor. |
     """
 
-    COMPONENT_CLASSES: ClassVar[set[str]] = {"input--cursor", "input--placeholder"}
+    COMPONENT_CLASSES: ClassVar[set[str]] = {
+        "input--cursor",
+        "input--placeholder",
+        "input--suggestion",
+    }
     """
     | Class | Description |
     | :- | :- |
     | `input--cursor` | Target the cursor. |
     | `input--placeholder` | Target the placeholder text (when it exists). |
+    | `input--suggestion` | Target the auto-completion suggestion (when it exists). |
     """
 
     DEFAULT_CSS = """
@@ -119,7 +139,7 @@ class Input(Widget, can_focus=True):
         color: $text;
         text-style: reverse;
     }
-    Input>.input--placeholder {
+    Input>.input--placeholder, Input>.input--suggestion {
         color: $text-disabled;
     }
     """
@@ -135,6 +155,14 @@ class Input(Widget, can_focus=True):
     _cursor_visible = reactive(True)
     password = reactive(False)
     max_size: reactive[int | None] = reactive(None)
+    suggestions = reactive[Optional[List[str]]](None)
+    """List of completion suggestions that are shown while the user types.
+
+    The precedence of the suggestions is inferred from the order of the list.
+    Set this to `None` or to an empty list to disable this feature..
+    """
+    _suggestion = reactive("")
+    """A completion suggestion for the current value in the input."""
 
     class Changed(Message, bubble=True):
         """Posted when the value changes.
@@ -184,6 +212,8 @@ def __init__(
         placeholder: str = "",
         highlighter: Highlighter | None = None,
         password: bool = False,
+        *,
+        suggestions: Iterable[str] | None = None,
         name: str | None = None,
         id: str | None = None,
         classes: str | None = None,
@@ -195,7 +225,8 @@ def __init__(
             value: An optional default value for the input.
             placeholder: Optional placeholder text for the input.
             highlighter: An optional highlighter for the input.
-            password: Flag to say if the field should obfuscate its content. Default is `False`.
+            password: Flag to say if the field should obfuscate its content.
+            suggestions: Possible auto-completions for the input field.
             name: Optional name for the input widget.
             id: Optional ID for the widget.
             classes: Optional initial classes for the widget.
@@ -207,6 +238,7 @@ def __init__(
         self.placeholder = placeholder
         self.highlighter = highlighter
         self.password = password
+        self.suggestions = suggestions
 
     def _position_to_cell(self, position: int) -> int:
         """Convert an index within the value to cell position."""
@@ -252,6 +284,9 @@ def watch_cursor_position(self, cursor_position: int) -> None:
             self.view_position = self.view_position
 
     async def watch_value(self, value: str) -> None:
+        self._suggestion = ""
+        if self.suggestions and value:
+            self._get_suggestion()
         if self.styles.auto_dimensions:
             self.refresh(layout=True)
         self.post_message(self.Changed(self, value))
@@ -359,7 +394,7 @@ def insert_text_at_cursor(self, text: str) -> None:
         Args:
             text: New text to insert.
         """
-        if self.cursor_position > len(self.value):
+        if self.cursor_position >= len(self.value):
             self.value += text
             self.cursor_position = len(self.value)
         else:
@@ -374,8 +409,12 @@ def action_cursor_left(self) -> None:
         self.cursor_position -= 1
 
     def action_cursor_right(self) -> None:
-        """Move the cursor one position to the right."""
-        self.cursor_position += 1
+        """Accept an auto-completion or move the cursor one position to the right."""
+        if self._cursor_at_end and self._suggestion:
+            self.value = self._suggestion
+            self.cursor_position = len(self.value)
+        else:
+            self.cursor_position += 1
 
     def action_home(self) -> None:
         """Move the cursor to the start of the input."""
@@ -492,3 +531,23 @@ def action_delete_left_all(self) -> None:
     async def action_submit(self) -> None:
         """Handle a submit action (normally the user hitting Enter in the input)."""
         self.post_message(self.Submitted(self, self.value))
+
+    def validate_suggestions(
+        self, suggestions: Iterable[str] | None
+    ) -> list[str] | None:
+        """Convert suggestions iterable into a list."""
+        if suggestions is None:
+            return None
+        return list(suggestions)
+
+    @work(exclusive=True)
+    def _get_suggestion(self) -> None:
+        """Try to get a suggestion for the user."""
+        if not self.suggestions:
+            return
+
+        value = self.value
+        for suggestion in self.suggestions:
+            if suggestion.startswith(value):
+                self._suggestion = suggestion
+                break
diff --git a/tests/input/test_suggestions.py b/tests/input/test_suggestions.py
new file mode 100644
index 0000000000..ca310d29fb
--- /dev/null
+++ b/tests/input/test_suggestions.py
@@ -0,0 +1,100 @@
+import string
+
+import pytest
+
+from textual.app import App, ComposeResult
+from textual.widgets import Input
+
+
+class SuggestionsApp(App[ComposeResult]):
+    def __init__(self, suggestions=None):
+        self.suggestions = suggestions
+        self.input = Input(suggestions=self.suggestions)
+        super().__init__()
+
+    def compose(self) -> ComposeResult:
+        yield self.input
+
+
+async def test_no_suggestions():
+    app = SuggestionsApp()
+    async with app.run_test() as pilot:
+        assert app.input._suggestion == ""
+        await pilot.press("a")
+        assert app.input._suggestion == ""
+
+
+async def test_suggestion():
+    app = SuggestionsApp(["hello"])
+    async with app.run_test() as pilot:
+        for char in "hello":
+            await pilot.press(char)
+            assert app.input._suggestion == "hello"
+
+
+async def test_accept_suggestion():
+    app = SuggestionsApp(["hello"])
+    async with app.run_test() as pilot:
+        await pilot.press("h")
+        await pilot.press("right")
+        assert app.input.value == "hello"
+
+
+async def test_no_suggestion_on_empty_value():
+    app = SuggestionsApp(["hello"])
+    async with app.run_test():
+        assert app.input._suggestion == ""
+
+
+async def test_no_suggestion_on_empty_value_after_deleting():
+    app = SuggestionsApp(["hello"])
+    async with app.run_test() as pilot:
+        await pilot.press("h", "e", "backspace", "backspace")
+        assert app.input.value == ""  # Sanity check.
+        assert app.input._suggestion == ""
+
+
+async def test_suggestion_shows_up_after_deleting_extra_chars():
+    app = SuggestionsApp(["hello"])
+    async with app.run_test() as pilot:
+        await pilot.press(*"help")
+        assert app.input._suggestion == ""
+        await pilot.press("backspace")
+        assert app.input._suggestion == "hello"
+
+
+async def test_suggestion_shows_up_after_deleting_extra_chars_in_middle_of_word():
+    app = SuggestionsApp(["hello"])
+    async with app.run_test() as pilot:
+        await pilot.press(*"hefl")
+        assert app.input._suggestion == ""
+        await pilot.press("left", "backspace")
+        assert app.input._suggestion == "hello"
+
+
+@pytest.mark.parametrize(
+    ("suggestion", "truncate_at"),
+    [
+        (".......", 3),
+        ("hey there", 3),
+        ("Olá, tudo bem?", 3),
+        ("áàóãõñç", 2),
+        (string.punctuation, 3),
+        (string.punctuation[::-1], 5),
+        (string.punctuation[::3], 5),
+    ],
+)
+async def test_suggestion_with_special_characters(suggestion: str, truncate_at: int):
+    app = SuggestionsApp([suggestion])
+    async with app.run_test() as pilot:
+        await pilot.press(*suggestion[:truncate_at])
+        assert app.input._suggestion == suggestion
+
+
+async def test_suggestion_priority():
+    app = SuggestionsApp(["dog", "dad"])
+    async with app.run_test() as pilot:
+        await pilot.press("d")
+        assert app.input._suggestion == "dog"
+        await pilot.press("a")
+        assert app.input._suggestion == "dad"
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index c0e2e96b98..260107313e 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -16496,135 +16496,295 @@
           font-weight: 700;
       }
   
-      .terminal-4205022328-matrix {
+      .terminal-596216952-matrix {
           font-family: Fira Code, monospace;
           font-size: 20px;
           line-height: 24.4px;
           font-variant-east-asian: full-width;
       }
   
-      .terminal-4205022328-title {
+      .terminal-596216952-title {
           font-size: 18px;
           font-weight: bold;
           font-family: arial;
       }
   
-      .terminal-4205022328-r1 { fill: #1e1e1e }
-  .terminal-4205022328-r2 { fill: #121212 }
-  .terminal-4205022328-r3 { fill: #c5c8c6 }
-  .terminal-4205022328-r4 { fill: #e2e2e2 }
-  .terminal-4205022328-r5 { fill: #0178d4 }
-  .terminal-4205022328-r6 { fill: #e1e1e1 }
+      .terminal-596216952-r1 { fill: #1e1e1e }
+  .terminal-596216952-r2 { fill: #121212 }
+  .terminal-596216952-r3 { fill: #c5c8c6 }
+  .terminal-596216952-r4 { fill: #e2e2e2 }
+  .terminal-596216952-r5 { fill: #0178d4 }
+  .terminal-596216952-r6 { fill: #e1e1e1 }
       </style>
   
       <defs>
-      <clipPath id="terminal-4205022328-clip-terminal">
+      <clipPath id="terminal-596216952-clip-terminal">
         <rect x="0" y="0" width="975.0" height="584.5999999999999" />
       </clipPath>
-      <clipPath id="terminal-4205022328-line-0">
+      <clipPath id="terminal-596216952-line-0">
       <rect x="0" y="1.5" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-1">
+  <clipPath id="terminal-596216952-line-1">
       <rect x="0" y="25.9" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-2">
+  <clipPath id="terminal-596216952-line-2">
       <rect x="0" y="50.3" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-3">
+  <clipPath id="terminal-596216952-line-3">
       <rect x="0" y="74.7" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-4">
+  <clipPath id="terminal-596216952-line-4">
       <rect x="0" y="99.1" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-5">
+  <clipPath id="terminal-596216952-line-5">
       <rect x="0" y="123.5" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-6">
+  <clipPath id="terminal-596216952-line-6">
       <rect x="0" y="147.9" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-7">
+  <clipPath id="terminal-596216952-line-7">
       <rect x="0" y="172.3" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-8">
+  <clipPath id="terminal-596216952-line-8">
       <rect x="0" y="196.7" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-9">
+  <clipPath id="terminal-596216952-line-9">
       <rect x="0" y="221.1" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-10">
+  <clipPath id="terminal-596216952-line-10">
       <rect x="0" y="245.5" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-11">
+  <clipPath id="terminal-596216952-line-11">
       <rect x="0" y="269.9" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-12">
+  <clipPath id="terminal-596216952-line-12">
       <rect x="0" y="294.3" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-13">
+  <clipPath id="terminal-596216952-line-13">
       <rect x="0" y="318.7" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-14">
+  <clipPath id="terminal-596216952-line-14">
       <rect x="0" y="343.1" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-15">
+  <clipPath id="terminal-596216952-line-15">
       <rect x="0" y="367.5" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-16">
+  <clipPath id="terminal-596216952-line-16">
       <rect x="0" y="391.9" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-17">
+  <clipPath id="terminal-596216952-line-17">
       <rect x="0" y="416.3" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-18">
+  <clipPath id="terminal-596216952-line-18">
       <rect x="0" y="440.7" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-19">
+  <clipPath id="terminal-596216952-line-19">
       <rect x="0" y="465.1" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-20">
+  <clipPath id="terminal-596216952-line-20">
       <rect x="0" y="489.5" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-21">
+  <clipPath id="terminal-596216952-line-21">
       <rect x="0" y="513.9" width="976" height="24.65"/>
               </clipPath>
-  <clipPath id="terminal-4205022328-line-22">
+  <clipPath id="terminal-596216952-line-22">
       <rect x="0" y="538.3" width="976" height="24.65"/>
               </clipPath>
       </defs>
   
-      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-4205022328-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">InputApp</text>
+      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-596216952-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">InputApp</text>
               <g transform="translate(26,22)">
               <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
               <circle cx="22" cy="0" r="7" fill="#febc2e"/>
               <circle cx="44" cy="0" r="7" fill="#28c840"/>
               </g>
           
-      <g transform="translate(9, 41)" clip-path="url(#terminal-4205022328-clip-terminal)">
-      <rect fill="#121212" x="0" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="1.5" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="36.6" y="25.9" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="122" y="25.9" width="817.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="939.4" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="50.3" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="74.7" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="36.6" y="99.1" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#e1e1e1" x="97.6" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="109.8" y="99.1" width="829.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="939.4" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="123.5" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
-      <g class="terminal-4205022328-matrix">
-      <text class="terminal-4205022328-r1" x="0" y="20" textLength="12.2" clip-path="url(#terminal-4205022328-line-0)">▊</text><text class="terminal-4205022328-r2" x="12.2" y="20" textLength="951.6" clip-path="url(#terminal-4205022328-line-0)">▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔</text><text class="terminal-4205022328-r2" x="963.8" y="20" textLength="12.2" clip-path="url(#terminal-4205022328-line-0)">▎</text><text class="terminal-4205022328-r3" x="976" y="20" textLength="12.2" clip-path="url(#terminal-4205022328-line-0)">
-  </text><text class="terminal-4205022328-r1" x="0" y="44.4" textLength="12.2" clip-path="url(#terminal-4205022328-line-1)">▊</text><text class="terminal-4205022328-r4" x="36.6" y="44.4" textLength="85.4" clip-path="url(#terminal-4205022328-line-1)">Darren&#160;</text><text class="terminal-4205022328-r2" x="963.8" y="44.4" textLength="12.2" clip-path="url(#terminal-4205022328-line-1)">▎</text><text class="terminal-4205022328-r3" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-4205022328-line-1)">
-  </text><text class="terminal-4205022328-r1" x="0" y="68.8" textLength="12.2" clip-path="url(#terminal-4205022328-line-2)">▊</text><text class="terminal-4205022328-r2" x="12.2" y="68.8" textLength="951.6" clip-path="url(#terminal-4205022328-line-2)">▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁</text><text class="terminal-4205022328-r2" x="963.8" y="68.8" textLength="12.2" clip-path="url(#terminal-4205022328-line-2)">▎</text><text class="terminal-4205022328-r3" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-4205022328-line-2)">
-  </text><text class="terminal-4205022328-r1" x="0" y="93.2" textLength="12.2" clip-path="url(#terminal-4205022328-line-3)">▊</text><text class="terminal-4205022328-r5" x="12.2" y="93.2" textLength="951.6" clip-path="url(#terminal-4205022328-line-3)">▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔</text><text class="terminal-4205022328-r5" x="963.8" y="93.2" textLength="12.2" clip-path="url(#terminal-4205022328-line-3)">▎</text><text class="terminal-4205022328-r3" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-4205022328-line-3)">
-  </text><text class="terminal-4205022328-r1" x="0" y="117.6" textLength="12.2" clip-path="url(#terminal-4205022328-line-4)">▊</text><text class="terminal-4205022328-r4" x="36.6" y="117.6" textLength="61" clip-path="url(#terminal-4205022328-line-4)">Burns</text><text class="terminal-4205022328-r5" x="963.8" y="117.6" textLength="12.2" clip-path="url(#terminal-4205022328-line-4)">▎</text><text class="terminal-4205022328-r3" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-4205022328-line-4)">
-  </text><text class="terminal-4205022328-r1" x="0" y="142" textLength="12.2" clip-path="url(#terminal-4205022328-line-5)">▊</text><text class="terminal-4205022328-r5" x="12.2" y="142" textLength="951.6" clip-path="url(#terminal-4205022328-line-5)">▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁</text><text class="terminal-4205022328-r5" x="963.8" y="142" textLength="12.2" clip-path="url(#terminal-4205022328-line-5)">▎</text><text class="terminal-4205022328-r3" x="976" y="142" textLength="12.2" clip-path="url(#terminal-4205022328-line-5)">
-  </text><text class="terminal-4205022328-r3" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-4205022328-line-6)">
-  </text><text class="terminal-4205022328-r3" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-4205022328-line-7)">
-  </text><text class="terminal-4205022328-r3" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-4205022328-line-8)">
-  </text><text class="terminal-4205022328-r3" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-4205022328-line-9)">
-  </text><text class="terminal-4205022328-r3" x="976" y="264" textLength="12.2" clip-path="url(#terminal-4205022328-line-10)">
-  </text><text class="terminal-4205022328-r3" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-4205022328-line-11)">
-  </text><text class="terminal-4205022328-r3" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-4205022328-line-12)">
-  </text><text class="terminal-4205022328-r3" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-4205022328-line-13)">
-  </text><text class="terminal-4205022328-r3" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-4205022328-line-14)">
-  </text><text class="terminal-4205022328-r3" x="976" y="386" textLength="12.2" clip-path="url(#terminal-4205022328-line-15)">
-  </text><text class="terminal-4205022328-r3" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-4205022328-line-16)">
-  </text><text class="terminal-4205022328-r3" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-4205022328-line-17)">
-  </text><text class="terminal-4205022328-r3" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-4205022328-line-18)">
-  </text><text class="terminal-4205022328-r3" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-4205022328-line-19)">
-  </text><text class="terminal-4205022328-r3" x="976" y="508" textLength="12.2" clip-path="url(#terminal-4205022328-line-20)">
-  </text><text class="terminal-4205022328-r3" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-4205022328-line-21)">
-  </text><text class="terminal-4205022328-r3" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-4205022328-line-22)">
+      <g transform="translate(9, 41)" clip-path="url(#terminal-596216952-clip-terminal)">
+      <rect fill="#121212" x="0" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="1.5" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="36.6" y="25.9" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="109.8" y="25.9" width="829.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="939.4" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="50.3" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="74.7" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="36.6" y="99.1" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#e1e1e1" x="97.6" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="109.8" y="99.1" width="829.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="939.4" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="123.5" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
+      <g class="terminal-596216952-matrix">
+      <text class="terminal-596216952-r1" x="0" y="20" textLength="12.2" clip-path="url(#terminal-596216952-line-0)">▊</text><text class="terminal-596216952-r2" x="12.2" y="20" textLength="951.6" clip-path="url(#terminal-596216952-line-0)">▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔</text><text class="terminal-596216952-r2" x="963.8" y="20" textLength="12.2" clip-path="url(#terminal-596216952-line-0)">▎</text><text class="terminal-596216952-r3" x="976" y="20" textLength="12.2" clip-path="url(#terminal-596216952-line-0)">
+  </text><text class="terminal-596216952-r1" x="0" y="44.4" textLength="12.2" clip-path="url(#terminal-596216952-line-1)">▊</text><text class="terminal-596216952-r4" x="36.6" y="44.4" textLength="73.2" clip-path="url(#terminal-596216952-line-1)">Darren</text><text class="terminal-596216952-r2" x="963.8" y="44.4" textLength="12.2" clip-path="url(#terminal-596216952-line-1)">▎</text><text class="terminal-596216952-r3" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-596216952-line-1)">
+  </text><text class="terminal-596216952-r1" x="0" y="68.8" textLength="12.2" clip-path="url(#terminal-596216952-line-2)">▊</text><text class="terminal-596216952-r2" x="12.2" y="68.8" textLength="951.6" clip-path="url(#terminal-596216952-line-2)">▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁</text><text class="terminal-596216952-r2" x="963.8" y="68.8" textLength="12.2" clip-path="url(#terminal-596216952-line-2)">▎</text><text class="terminal-596216952-r3" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-596216952-line-2)">
+  </text><text class="terminal-596216952-r1" x="0" y="93.2" textLength="12.2" clip-path="url(#terminal-596216952-line-3)">▊</text><text class="terminal-596216952-r5" x="12.2" y="93.2" textLength="951.6" clip-path="url(#terminal-596216952-line-3)">▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔</text><text class="terminal-596216952-r5" x="963.8" y="93.2" textLength="12.2" clip-path="url(#terminal-596216952-line-3)">▎</text><text class="terminal-596216952-r3" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-596216952-line-3)">
+  </text><text class="terminal-596216952-r1" x="0" y="117.6" textLength="12.2" clip-path="url(#terminal-596216952-line-4)">▊</text><text class="terminal-596216952-r4" x="36.6" y="117.6" textLength="61" clip-path="url(#terminal-596216952-line-4)">Burns</text><text class="terminal-596216952-r5" x="963.8" y="117.6" textLength="12.2" clip-path="url(#terminal-596216952-line-4)">▎</text><text class="terminal-596216952-r3" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-596216952-line-4)">
+  </text><text class="terminal-596216952-r1" x="0" y="142" textLength="12.2" clip-path="url(#terminal-596216952-line-5)">▊</text><text class="terminal-596216952-r5" x="12.2" y="142" textLength="951.6" clip-path="url(#terminal-596216952-line-5)">▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁</text><text class="terminal-596216952-r5" x="963.8" y="142" textLength="12.2" clip-path="url(#terminal-596216952-line-5)">▎</text><text class="terminal-596216952-r3" x="976" y="142" textLength="12.2" clip-path="url(#terminal-596216952-line-5)">
+  </text><text class="terminal-596216952-r3" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-596216952-line-6)">
+  </text><text class="terminal-596216952-r3" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-596216952-line-7)">
+  </text><text class="terminal-596216952-r3" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-596216952-line-8)">
+  </text><text class="terminal-596216952-r3" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-596216952-line-9)">
+  </text><text class="terminal-596216952-r3" x="976" y="264" textLength="12.2" clip-path="url(#terminal-596216952-line-10)">
+  </text><text class="terminal-596216952-r3" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-596216952-line-11)">
+  </text><text class="terminal-596216952-r3" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-596216952-line-12)">
+  </text><text class="terminal-596216952-r3" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-596216952-line-13)">
+  </text><text class="terminal-596216952-r3" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-596216952-line-14)">
+  </text><text class="terminal-596216952-r3" x="976" y="386" textLength="12.2" clip-path="url(#terminal-596216952-line-15)">
+  </text><text class="terminal-596216952-r3" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-596216952-line-16)">
+  </text><text class="terminal-596216952-r3" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-596216952-line-17)">
+  </text><text class="terminal-596216952-r3" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-596216952-line-18)">
+  </text><text class="terminal-596216952-r3" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-596216952-line-19)">
+  </text><text class="terminal-596216952-r3" x="976" y="508" textLength="12.2" clip-path="url(#terminal-596216952-line-20)">
+  </text><text class="terminal-596216952-r3" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-596216952-line-21)">
+  </text><text class="terminal-596216952-r3" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-596216952-line-22)">
+  </text>
+      </g>
+      </g>
+  </svg>
+  
+  '''
+# ---
+# name: test_input_suggestions
+  '''
+  <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
+      <!-- Generated with Rich https://www.textualize.io -->
+      <style>
+  
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Regular"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+          font-style: normal;
+          font-weight: 400;
+      }
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Bold"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+          font-style: bold;
+          font-weight: 700;
+      }
+  
+      .terminal-1319604136-matrix {
+          font-family: Fira Code, monospace;
+          font-size: 20px;
+          line-height: 24.4px;
+          font-variant-east-asian: full-width;
+      }
+  
+      .terminal-1319604136-title {
+          font-size: 18px;
+          font-weight: bold;
+          font-family: arial;
+      }
+  
+      .terminal-1319604136-r1 { fill: #1e1e1e }
+  .terminal-1319604136-r2 { fill: #0178d4 }
+  .terminal-1319604136-r3 { fill: #c5c8c6 }
+  .terminal-1319604136-r4 { fill: #e2e2e2 }
+  .terminal-1319604136-r5 { fill: #1e1e1e;font-style: italic; }
+  .terminal-1319604136-r6 { fill: #ff0000;font-style: italic; }
+  .terminal-1319604136-r7 { fill: #e1e1e1 }
+      </style>
+  
+      <defs>
+      <clipPath id="terminal-1319604136-clip-terminal">
+        <rect x="0" y="0" width="975.0" height="584.5999999999999" />
+      </clipPath>
+      <clipPath id="terminal-1319604136-line-0">
+      <rect x="0" y="1.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-1">
+      <rect x="0" y="25.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-2">
+      <rect x="0" y="50.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-3">
+      <rect x="0" y="74.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-4">
+      <rect x="0" y="99.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-5">
+      <rect x="0" y="123.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-6">
+      <rect x="0" y="147.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-7">
+      <rect x="0" y="172.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-8">
+      <rect x="0" y="196.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-9">
+      <rect x="0" y="221.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-10">
+      <rect x="0" y="245.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-11">
+      <rect x="0" y="269.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-12">
+      <rect x="0" y="294.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-13">
+      <rect x="0" y="318.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-14">
+      <rect x="0" y="343.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-15">
+      <rect x="0" y="367.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-16">
+      <rect x="0" y="391.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-17">
+      <rect x="0" y="416.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-18">
+      <rect x="0" y="440.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-19">
+      <rect x="0" y="465.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-20">
+      <rect x="0" y="489.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-21">
+      <rect x="0" y="513.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1319604136-line-22">
+      <rect x="0" y="538.3" width="976" height="24.65"/>
+              </clipPath>
+      </defs>
+  
+      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1319604136-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">FruitsApp</text>
+              <g transform="translate(26,22)">
+              <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+              <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+              <circle cx="44" cy="0" r="7" fill="#28c840"/>
+              </g>
+          
+      <g transform="translate(9, 41)" clip-path="url(#terminal-1319604136-clip-terminal)">
+      <rect fill="#0178d4" x="0" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="1.5" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="36.6" y="25.9" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#e1e1e1" x="97.6" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="109.8" y="25.9" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="158.6" y="25.9" width="780.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="939.4" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#262626" x="12.2" y="50.3" width="951.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
+      <g class="terminal-1319604136-matrix">
+      <text class="terminal-1319604136-r1" x="0" y="20" textLength="12.2" clip-path="url(#terminal-1319604136-line-0)">▊</text><text class="terminal-1319604136-r2" x="12.2" y="20" textLength="951.6" clip-path="url(#terminal-1319604136-line-0)">▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔</text><text class="terminal-1319604136-r2" x="963.8" y="20" textLength="12.2" clip-path="url(#terminal-1319604136-line-0)">▎</text><text class="terminal-1319604136-r3" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1319604136-line-0)">
+  </text><text class="terminal-1319604136-r1" x="0" y="44.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-1)">▊</text><text class="terminal-1319604136-r4" x="36.6" y="44.4" textLength="61" clip-path="url(#terminal-1319604136-line-1)">straw</text><text class="terminal-1319604136-r5" x="97.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-1)">b</text><text class="terminal-1319604136-r6" x="109.8" y="44.4" textLength="48.8" clip-path="url(#terminal-1319604136-line-1)">erry</text><text class="terminal-1319604136-r2" x="963.8" y="44.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-1)">▎</text><text class="terminal-1319604136-r3" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-1)">
+  </text><text class="terminal-1319604136-r1" x="0" y="68.8" textLength="12.2" clip-path="url(#terminal-1319604136-line-2)">▊</text><text class="terminal-1319604136-r2" x="12.2" y="68.8" textLength="951.6" clip-path="url(#terminal-1319604136-line-2)">▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁</text><text class="terminal-1319604136-r2" x="963.8" y="68.8" textLength="12.2" clip-path="url(#terminal-1319604136-line-2)">▎</text><text class="terminal-1319604136-r3" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1319604136-line-2)">
+  </text><text class="terminal-1319604136-r3" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1319604136-line-3)">
+  </text><text class="terminal-1319604136-r3" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1319604136-line-4)">
+  </text><text class="terminal-1319604136-r3" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1319604136-line-5)">
+  </text><text class="terminal-1319604136-r3" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-6)">
+  </text><text class="terminal-1319604136-r3" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1319604136-line-7)">
+  </text><text class="terminal-1319604136-r3" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1319604136-line-8)">
+  </text><text class="terminal-1319604136-r3" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1319604136-line-9)">
+  </text><text class="terminal-1319604136-r3" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1319604136-line-10)">
+  </text><text class="terminal-1319604136-r3" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-11)">
+  </text><text class="terminal-1319604136-r3" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1319604136-line-12)">
+  </text><text class="terminal-1319604136-r3" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1319604136-line-13)">
+  </text><text class="terminal-1319604136-r3" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1319604136-line-14)">
+  </text><text class="terminal-1319604136-r3" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1319604136-line-15)">
+  </text><text class="terminal-1319604136-r3" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-16)">
+  </text><text class="terminal-1319604136-r3" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1319604136-line-17)">
+  </text><text class="terminal-1319604136-r3" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1319604136-line-18)">
+  </text><text class="terminal-1319604136-r3" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1319604136-line-19)">
+  </text><text class="terminal-1319604136-r3" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1319604136-line-20)">
+  </text><text class="terminal-1319604136-r3" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1319604136-line-21)">
+  </text><text class="terminal-1319604136-r3" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1319604136-line-22)">
   </text>
       </g>
       </g>
diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py
index d45cd87ac3..236e527b2c 100644
--- a/tests/snapshot_tests/conftest.py
+++ b/tests/snapshot_tests/conftest.py
@@ -40,7 +40,7 @@ def snap_compare(
 
     def compare(
         app_path: str | PurePath,
-        press: Iterable[str] = ("_",),
+        press: Iterable[str] = (),
         terminal_size: tuple[int, int] = (80, 24),
         run_before: Callable[[Pilot], Awaitable[None] | None] | None = None,
     ) -> bool:
diff --git a/tests/snapshot_tests/snapshot_apps/input_suggestions.py b/tests/snapshot_tests/snapshot_apps/input_suggestions.py
new file mode 100644
index 0000000000..1ddc4ba42b
--- /dev/null
+++ b/tests/snapshot_tests/snapshot_apps/input_suggestions.py
@@ -0,0 +1,22 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Input
+
+
+fruits = ["apple", "pear", "mango", "peach", "strawberry", "blueberry", "banana"]
+
+
+class FruitsApp(App[None]):
+    CSS = """
+    Input > .input--suggestion {
+        color: red;
+        text-style: italic;
+    }
+    """
+
+    def compose(self) -> ComposeResult:
+        yield Input("straw", suggestions=fruits)
+
+
+if __name__ == "__main__":
+    app = FruitsApp()
+    app.run()
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index bdedbced39..6f0a97faf3 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -84,6 +84,10 @@ def test_input_and_focus(snap_compare):
     assert snap_compare(WIDGET_EXAMPLES_DIR / "input.py", press=press)
 
 
+def test_input_suggestions(snap_compare):
+    assert snap_compare(SNAPSHOT_APPS_DIR / "input_suggestions.py", press=[])
+
+
 def test_buttons_render(snap_compare):
     # Testing button rendering. We press tab to focus the first button too.
     assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab"])
@@ -463,13 +467,17 @@ def test_dock_scroll2(snap_compare):
 def test_dock_scroll_off_by_one(snap_compare):
     # https://github.com/Textualize/textual/issues/2525
     assert snap_compare(
-        SNAPSHOT_APPS_DIR / "dock_scroll_off_by_one.py", terminal_size=(80, 25)
+        SNAPSHOT_APPS_DIR / "dock_scroll_off_by_one.py",
+        terminal_size=(80, 25),
+        press=["_"],
     )
 
 
 def test_scroll_to(snap_compare):
     # https://github.com/Textualize/textual/issues/2525
-    assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_to.py", terminal_size=(80, 25))
+    assert snap_compare(
+        SNAPSHOT_APPS_DIR / "scroll_to.py", terminal_size=(80, 25), press=["_"]
+    )
 
 
 def test_auto_fr(snap_compare):


Code review comments:
diff_hunk: @@ -492,3 +531,23 @@ def action_delete_left_all(self) -> None:
     async def action_submit(self) -> None:
         """Handle a submit action (normally the user hitting Enter in the input)."""
         self.post_message(self.Submitted(self, self.value))
+
+    def validate_suggestions(
+        self, suggestions: Iterable[str] | None
+    ) -> list[str] | None:
+        """Convert suggestions iterable into a list."""
+        if suggestions is None:
+            return None
+        return list(suggestions)
+
+    @work(exclusive=True)
path: src/textual/widgets/_input.py
text: @user1:
This worker isn't going to do anything useful...