Repository: Textualize/textual

Issue:
Changing `Input` value doesn't update cursor position
Changing the `Input` value doesn't update the cursor position, which can result in an 'invalid' cursor position when the new value is shorter.

Here's a quick example to demonstrate:

```python
from textual.app import App, ComposeResult
from textual.geometry import clamp
from textual.widgets import Button, Input, Label


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        yield Label("[b]Enter a number between 1-10:[/]")
        yield Input(
            "999999999999999999999",
            type="integer",
            select_on_focus=False,
        )

        yield Label("[dim]The input value will be clamped after being submitted.[/]")
        yield Label("[dim]Notice that the cursor seems to disappear.[/]")

        yield Button("Check cursor position", variant="warning")

    def on_input_submitted(self, event: Input.Submitted) -> None:
        clamped_value = clamp(int(event.value), 1, 10)
        event.input.value = str(clamped_value)

    def on_button_pressed(self) -> None:
        cursor_position = self.query_one(Input).cursor_position
        severity = "error" if cursor_position > 2 else "information"
        self.notify(
            str(cursor_position),
            title="Cursor position",
            severity=severity,
        )


if __name__ == "__main__":
    app = ExampleApp()
    app.run()
```


Discussion:
We found the following entries in the [FAQ](https://textual.textualize.io/FAQ/) which you may find helpful:


- [How do I center a widget in a screen?](https://textual.textualize.io/FAQ/#how-do-i-center-a-widget-in-a-screen)
- [Why doesn't Textual look good on macOS?](https://textual.textualize.io/FAQ/#why-doesnt-textual-look-good-on-macos)
- [How do I pass arguments to an app?](https://textual.textualize.io/FAQ/#how-do-i-pass-arguments-to-an-app)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
Don't forget to [star](https://github.com/Textualize/textual) the repository!

Follow [@textualizeio](https://twitter.com/textualizeio) for Textual updates.
Don't forget to [star](https://github.com/Textualize/textual) the repository!

Follow [@textualizeio](https://twitter.com/textualizeio) for Textual updates.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 072f92175e..86d28a6776 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 - Fixed `VERTICAL_BREAKPOINTS` doesn't work https://github.com/Textualize/textual/pull/5785
 - Fixed `Button` allowing text selection https://github.com/Textualize/textual/pull/5770
+- Fixed `Input` invalid cursor position after updating the value https://github.com/Textualize/textual/issues/5811
 
 ## [3.2.0] - 2025-05-02
 
diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py
index e70c694073..a9e80c696b 100644
--- a/src/textual/widgets/_input.py
+++ b/src/textual/widgets/_input.py
@@ -536,6 +536,10 @@ def _watch_value(self, value: str) -> None:
         if self._initial_value:
             self.cursor_position = len(self.value)
             self._initial_value = False
+        else:
+            # Force a re-validation of the selection to ensure it accounts for
+            # the length of the new value
+            self.selection = self.selection
 
     def _watch_valid_empty(self) -> None:
         """Repeat validation when valid_empty changes."""


Test patch:
diff --git a/tests/input/test_input_properties.py b/tests/input/test_input_properties.py
index ef518ba69d..7792549327 100644
--- a/tests/input/test_input_properties.py
+++ b/tests/input/test_input_properties.py
@@ -93,3 +93,16 @@ async def test_input_selection_deleted_programmatically():
         input_widget.selection = Selection(4, 0)
         input_widget.delete_selection()
         assert input_widget.value == "o, world!"
+
+
+async def test_input_selection_is_valid_after_updating_value():
+    """Regression test for https://github.com/Textualize/textual/issues/5811"""
+    app = InputApp()
+    async with app.run_test() as pilot:
+        input_widget = pilot.app.query_one(Input)
+        # Sanity check (by default focusing the input selects all text)
+        assert input_widget.selection == (0, len(input_widget.value))
+
+        input_widget.value = "foo"
+
+        assert input_widget.selection == (0, len(input_widget.value))