Repository: tmbo/questionary

Issue:
Add prefix filter search in select control
As featured here (when selecting the AWS region)
https://asciinema.org/a/AyujEPdjcDmSPoOK26pTozCiO

@tmbo thanks for pointing me here from PyInquirer. Looks like the more modern/maintained alternatives (latest lib, tests, ...)

I'll try to use questionary soon and if it's all good, I'll contribute the filter that I have built that I find very practical on long lists like the one I'm dealing with.

Discussion:
kiancross: @mario-dg Thanks for this PR. Please can you merge latest changes into this branch, then I will run the CI and review.
mario-dg: @kiancross, done 😄

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0971e85..5146e51 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
-  - repo: https://github.com/humitos/mirrors-autoflake
-    rev: v1.1
+  - repo: https://github.com/PyCQA/autoflake
+    rev: v2.3.1
     hooks:
       - id: autoflake
         args: ["-i", "--remove-all-unused-imports"]
@@ -26,7 +26,7 @@ repos:
           - "--profile=black"
 
   - repo: https://github.com/pycqa/flake8
-    rev: 6.0.0
+    rev: 7.1.0
     hooks:
       - id: flake8
         args: ["--max-line-length", "120", "--max-doc-length", "140"]
diff --git a/1 b/1
new file mode 100644
index 0000000..e69de29
diff --git a/examples/checkbox_search.py b/examples/checkbox_search.py
new file mode 100644
index 0000000..24cf7e3
--- /dev/null
+++ b/examples/checkbox_search.py
@@ -0,0 +1,75 @@
+import questionary
+from examples import custom_style_dope
+
+zoo_animals = [
+    "Lion",
+    "Tiger",
+    "Elephant",
+    "Giraffe",
+    "Zebra",
+    "Panda",
+    "Kangaroo",
+    "Gorilla",
+    "Chimpanzee",
+    "Orangutan",
+    "Hippopotamus",
+    "Rhinoceros",
+    "Leopard",
+    "Cheetah",
+    "Polar Bear",
+    "Grizzly Bear",
+    "Penguin",
+    "Flamingo",
+    "Peacock",
+    "Ostrich",
+    "Emu",
+    "Koala",
+    "Sloth",
+    "Armadillo",
+    "Meerkat",
+    "Lemur",
+    "Red Panda",
+    "Wolf",
+    "Fox",
+    "Otter",
+    "Sea Lion",
+    "Walrus",
+    "Seal",
+    "Crocodile",
+    "Alligator",
+    "Python",
+    "Boa Constrictor",
+    "Iguana",
+    "Komodo Dragon",
+    "Tortoise",
+    "Turtle",
+    "Parrot",
+    "Toucan",
+    "Macaw",
+    "Hyena",
+    "Jaguar",
+    "Anteater",
+    "Capybara",
+    "Bison",
+    "Moose",
+]
+
+
+if __name__ == "__main__":
+    toppings = (
+        questionary.checkbox(
+            "Select animals for your zoo",
+            choices=zoo_animals,
+            validate=lambda a: (
+                True if len(a) > 0 else "You must select at least one zoo animal"
+            ),
+            style=custom_style_dope,
+            use_jk_keys=False,
+            use_search_filter=True,
+        ).ask()
+        or []
+    )
+
+    print(
+        f"Alright let's create our zoo with following animals: {', '.join(toppings)}."
+    )
diff --git a/examples/select_search.py b/examples/select_search.py
new file mode 100644
index 0000000..ac45363
--- /dev/null
+++ b/examples/select_search.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+"""Example for a select question type with search enabled.
+
+Run example by typing `python -m examples.select_search` in your console."""
+from pprint import pprint
+
+import questionary
+from examples import custom_style_dope
+from questionary import Choice
+from questionary import Separator
+from questionary import prompt
+
+
+def ask_pystyle(**kwargs):
+    # create the question object
+    question = questionary.select(
+        "What do you want to do?",
+        qmark="😃",
+        choices=[
+            "Order a pizza",
+            "Make a reservation",
+            "Cancel a reservation",
+            "Modify your order",
+            Separator(),
+            "Ask for opening hours",
+            Choice("Contact support", disabled="Unavailable at this time"),
+            "Talk to the receptionist",
+        ],
+        style=custom_style_dope,
+        use_jk_keys=False,
+        use_search_filter=True,
+        **kwargs,
+    )
+
+    # prompt the user for an answer
+    return question.ask()
+
+
+def ask_dictstyle(**kwargs):
+    questions = [
+        {
+            "type": "select",
+            "name": "theme",
+            "message": "What do you want to do?",
+            "choices": [
+                "Order a pizza",
+                "Make a reservation",
+                "Cancel a reservation",
+                "Modify your order",
+                Separator(),
+                "Ask for opening hours",
+                {"name": "Contact support", "disabled": "Unavailable at this time"},
+                "Talk to the receptionist",
+            ],
+        }
+    ]
+
+    return prompt(questions, style=custom_style_dope, **kwargs)
+
+
+if __name__ == "__main__":
+    pprint(ask_pystyle())
diff --git a/poetry.lock b/poetry.lock
index f8d6b19..76ea62f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -446,44 +446,44 @@ files = [
 
 [[package]]
 name = "mypy"
-version = "1.9.0"
+version = "1.11.0"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"},
-    {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"},
-    {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"},
-    {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"},
-    {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"},
-    {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"},
-    {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"},
-    {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"},
-    {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"},
-    {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"},
-    {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"},
-    {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"},
-    {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"},
-    {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"},
-    {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"},
-    {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"},
-    {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"},
-    {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"},
-    {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"},
-    {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"},
-    {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"},
-    {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"},
-    {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"},
-    {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"},
-    {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"},
-    {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"},
-    {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"},
+    {file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"},
+    {file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"},
+    {file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"},
+    {file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"},
+    {file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"},
+    {file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"},
+    {file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"},
+    {file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"},
+    {file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"},
+    {file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"},
+    {file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"},
+    {file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"},
+    {file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"},
+    {file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"},
+    {file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"},
+    {file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"},
+    {file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"},
+    {file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"},
+    {file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"},
+    {file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"},
+    {file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"},
+    {file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"},
+    {file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"},
+    {file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"},
+    {file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"},
+    {file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"},
+    {file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"},
 ]
 
 [package.dependencies]
 mypy-extensions = ">=1.0.0"
 tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = ">=4.1.0"
+typing-extensions = ">=4.6.0"
 
 [package.extras]
 dmypy = ["psutil (>=4.0)"]
@@ -544,13 +544,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-
 
 [[package]]
 name = "pluggy"
-version = "1.4.0"
+version = "1.5.0"
 description = "plugin and hook calling mechanisms for python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
-    {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
 ]
 
 [package.extras]
@@ -605,13 +605,13 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pytest"
-version = "8.1.1"
+version = "8.3.1"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
-    {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
+    {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"},
+    {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"},
 ]
 
 [package.dependencies]
@@ -619,11 +619,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
 exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=1.4,<2.0"
+pluggy = ">=1.5,<2"
 tomli = {version = ">=1", markers = "python_version < \"3.11\""}
 
 [package.extras]
-testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
 [[package]]
 name = "pytest-cov"
@@ -737,19 +737,18 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "setuptools"
-version = "67.7.2"
+version = "70.0.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"},
-    {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"},
+    {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"},
+    {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "six"
@@ -829,22 +828,22 @@ test = ["pytest", "pytest-cov"]
 
 [[package]]
 name = "sphinx-autodoc-typehints"
-version = "1.25.2"
+version = "2.0.1"
 description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "sphinx_autodoc_typehints-1.25.2-py3-none-any.whl", hash = "sha256:5ed05017d23ad4b937eab3bee9fae9ab0dd63f0b42aa360031f1fad47e47f673"},
-    {file = "sphinx_autodoc_typehints-1.25.2.tar.gz", hash = "sha256:3cabc2537e17989b2f92e64a399425c4c8bf561ed73f087bc7414a5003616a50"},
+    {file = "sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149"},
+    {file = "sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12"},
 ]
 
 [package.dependencies]
 sphinx = ">=7.1.2"
 
 [package.extras]
-docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)"]
+docs = ["furo (>=2024.1.29)"]
 numpy = ["nptyping (>=2.5)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.7.1)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.4.2)", "diff-cover (>=8.0.3)", "pytest (>=8.0.1)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.9)"]
 
 [[package]]
 name = "sphinx-copybutton"
@@ -1030,13 +1029,13 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.5.0"
-description = "Backported and Experimental Type Hints for Python 3.7+"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
-    {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 ]
 
 [[package]]
@@ -1089,20 +1088,20 @@ files = [
 
 [[package]]
 name = "zipp"
-version = "3.15.0"
+version = "3.19.1"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
-    {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
+    {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"},
+    {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.8"
-content-hash = "db61930009e97d1e2fa7b63147a0b523e022bb3dec61a1efca4a01c4d836fa0c"
+content-hash = "89d56b75b6d9ca1303b0561288d900531c89d84142b72a9c0dcbf9dff78da0ae"
diff --git a/pyproject.toml b/pyproject.toml
index 26118ab..fada9db 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,7 +43,7 @@ Sphinx = ">=4.1,<8.0"
 sphinx-rtd-theme = ">=0.5,<2.1"
 sphinx-autobuild = ">=2020.9.1,<2022.0.0"
 sphinx-copybutton = ">=0.3.1,<0.6.0"
-sphinx-autodoc-typehints = "^1.11.1"
+sphinx-autodoc-typehints = ">=1.11.1,<3.0.0"
 
 [tool.poetry.group.dev.dependencies]
 pytest-cov = ">=5.0.0,<6"
diff --git a/questionary/constants.py b/questionary/constants.py
index 94c5ff4..845dd78 100644
--- a/questionary/constants.py
+++ b/questionary/constants.py
@@ -39,6 +39,14 @@ DEFAULT_STYLE = Style(
         ("qmark", "fg:#5f819d"),  # token in front of the question
         ("question", "bold"),  # question text
         ("answer", "fg:#FF9D00 bold"),  # submitted answer text behind the question
+        (
+            "search_success",
+            "noinherit fg:#00FF00 bold",
+        ),  # submitted answer text behind the question
+        (
+            "search_none",
+            "noinherit fg:#FF0000 bold",
+        ),  # submitted answer text behind the question
         ("pointer", ""),  # pointer used in select and checkbox prompts
         ("selected", ""),  # style for a selected item of a checkbox
         ("separator", ""),  # separator in lists
diff --git a/questionary/prompts/checkbox.py b/questionary/prompts/checkbox.py
index 25161e4..465892b 100644
--- a/questionary/prompts/checkbox.py
+++ b/questionary/prompts/checkbox.py
@@ -1,3 +1,4 @@
+import string
 from typing import Any
 from typing import Callable
 from typing import Dict
@@ -37,6 +38,7 @@ def checkbox(
     use_arrow_keys: bool = True,
     use_jk_keys: bool = True,
     use_emacs_keys: bool = True,
+    use_search_filter: Union[str, bool, None] = False,
     instruction: Optional[str] = None,
     show_description: bool = True,
     **kwargs: Any,
@@ -105,6 +107,14 @@ def checkbox(
 
         use_emacs_keys: Allow the user to select items from the list using
                         `Ctrl+N` (down) and `Ctrl+P` (up) keys.
+
+        use_search_filter: Flag to enable search filtering. Typing some string will
+                           filter the choices to keep only the ones that contain the
+                           search string.
+                           Note that activating this option disables "vi-like"
+                           navigation as "j" and "k" can be part of a prefix and
+                           therefore cannot be used for navigation
+
         instruction: A message describing how to navigate the menu.
 
         show_description: Display description of current selection if available.
@@ -119,6 +129,11 @@ def checkbox(
             "Emacs keys."
         )
 
+    if use_jk_keys and use_search_filter:
+        raise ValueError(
+            "Cannot use j/k keys with prefix filter search, since j/k can be part of the prefix."
+        )
+
     merged_style = merge_styles_default(
         [
             # Disable the default inverted colours bottom-toolbar behaviour (for
@@ -179,8 +194,9 @@ def checkbox(
                         "class:instruction",
                         "(Use arrow keys to move, "
                         "<space> to select, "
-                        "<a> to toggle, "
-                        "<i> to invert)",
+                        f"<{'ctrl-a' if use_search_filter else 'a'}> to toggle, "
+                        f"<{'ctrl-a' if use_search_filter else 'i'}> to invert"
+                        f"{', type to filter' if use_search_filter else ''})",
                     )
                 )
         return tokens
@@ -225,7 +241,7 @@ def checkbox(
 
         perform_validation(get_selected_values())
 
-    @bindings.add("i", eager=True)
+    @bindings.add(Keys.ControlI if use_search_filter else "i", eager=True)
     def invert(_event):
         inverted_selection = [
             c.value
@@ -238,7 +254,7 @@ def checkbox(
 
         perform_validation(get_selected_values())
 
-    @bindings.add("a", eager=True)
+    @bindings.add(Keys.ControlA if use_search_filter else "a", eager=True)
     def all(_event):
         all_selected = True  # all choices have been selected
         for c in ic.choices:
@@ -265,6 +281,17 @@ def checkbox(
         while not ic.is_selection_valid():
             ic.select_previous()
 
+    if use_search_filter:
+
+        def search_filter(event):
+            ic.add_search_character(event.key_sequence[0].key)
+
+        for character in string.printable:
+            if character in string.whitespace:
+                continue
+            bindings.add(character, eager=True)(search_filter)
+        bindings.add(Keys.Backspace, eager=True)(search_filter)
+
     if use_arrow_keys:
         bindings.add(Keys.Down, eager=True)(move_cursor_down)
         bindings.add(Keys.Up, eager=True)(move_cursor_up)
diff --git a/questionary/prompts/common.py b/questionary/prompts/common.py
index 827629d..d07d0e6 100644
--- a/questionary/prompts/common.py
+++ b/questionary/prompts/common.py
@@ -12,11 +12,13 @@ from prompt_toolkit import PromptSession
 from prompt_toolkit.filters import Always
 from prompt_toolkit.filters import Condition
 from prompt_toolkit.filters import IsDone
+from prompt_toolkit.keys import Keys
 from prompt_toolkit.layout import ConditionalContainer
 from prompt_toolkit.layout import FormattedTextControl
 from prompt_toolkit.layout import HSplit
 from prompt_toolkit.layout import Layout
 from prompt_toolkit.layout import Window
+from prompt_toolkit.layout.dimension import LayoutDimension
 from prompt_toolkit.styles import Style
 from prompt_toolkit.validation import ValidationError
 from prompt_toolkit.validation import Validator
@@ -69,8 +71,7 @@ class Choice:
     checked: Optional[bool]
     """Whether the choice is initially selected"""
 
-    shortcut_key: Optional[str]
-    """A shortcut key for the choice"""
+    __shortcut_key: Optional[Union[str, bool]]
 
     description: Optional[str]
     """Choice description"""
@@ -86,6 +87,8 @@ class Choice:
     ) -> None:
         self.disabled = disabled
         self.title = title
+        self.shortcut_key = shortcut_key
+        # self.auto_shortcut is set by the self.shortcut_key setter
         self.checked = checked if checked is not None else False
         self.description = description
 
@@ -96,17 +99,6 @@ class Choice:
         else:
             self.value = title
 
-        if shortcut_key is not None:
-            if isinstance(shortcut_key, bool):
-                self.auto_shortcut = shortcut_key
-                self.shortcut_key = None
-            else:
-                self.shortcut_key = str(shortcut_key)
-                self.auto_shortcut = False
-        else:
-            self.shortcut_key = None
-            self.auto_shortcut = True
-
     @staticmethod
     def build(c: Union[str, "Choice", Dict[str, Any]]) -> "Choice":
         """Create a choice object from different representations.
@@ -134,12 +126,54 @@ class Choice:
                 c.get("description", None),
             )
 
+    @property
+    def shortcut_key(self) -> Optional[Union[str, bool]]:
+        """A shortcut key for the choice"""
+        return self.__shortcut_key
+
+    @shortcut_key.setter
+    def shortcut_key(self, key: Optional[Union[str, bool]]):
+        if key is not None:
+            if isinstance(key, bool):
+                self.__auto_shortcut = key
+                self.__shortcut_key = None
+            else:
+                self.__shortcut_key = str(key)
+                self.__auto_shortcut = False
+        else:
+            self.__shortcut_key = None
+            self.__auto_shortcut = True
+
+    @shortcut_key.deleter
+    def shortcut_key(self):
+        self.__shortcut_key = None
+        self.__auto_shortcut = True
+
     def get_shortcut_title(self):
         if self.shortcut_key is None:
             return "-) "
         else:
             return "{}) ".format(self.shortcut_key)
 
+    @property
+    def auto_shortcut(self) -> bool:
+        """Whether to assign a shortcut key to the choice
+
+        Keys are assigned starting with numbers and proceeding
+        through the ASCII alphabet.
+        """
+        return self.__auto_shortcut
+
+    @auto_shortcut.setter
+    def auto_shortcut(self, should_assign: bool):
+        self.__auto_shortcut = should_assign
+        if self.__auto_shortcut:
+            self.__shortcut_key = None
+
+    @auto_shortcut.deleter
+    def auto_shortcut(self):
+        self.__auto_shortcut = False
+
 
 class Separator(Choice):
     """Used to space/separate choices group."""
@@ -204,6 +238,7 @@ class InquirerControl(FormattedTextControl):
     choices: List[Choice]
     default: Optional[Union[str, Choice, Dict[str, Any]]]
     selected_options: List[Any]
+    search_filter: Union[str, None] = None
     use_indicator: bool
     use_shortcuts: bool
     use_arrow_keys: bool
@@ -275,6 +310,7 @@ class InquirerControl(FormattedTextControl):
         self.submission_attempted = False
         self.error_message = None
         self.selected_options = []
+        self.found_in_search = False
 
         self._init_choices(choices, pointed_at)
         self._assign_shortcut_keys()
@@ -343,9 +379,19 @@ class InquirerControl(FormattedTextControl):
 
             self.choices.append(choice)
 
+    @property
+    def filtered_choices(self):
+        if not self.search_filter:
+            return self.choices
+        filtered = [
+            c for c in self.choices if self.search_filter.lower() in c.title.lower()
+        ]
+        self.found_in_search = len(filtered) > 0
+        return filtered if self.found_in_search else self.choices
+
     @property
     def choice_count(self) -> int:
-        return len(self.choices)
+        return len(self.filtered_choices)
 
     def _get_choice_tokens(self):
         tokens = []
@@ -425,7 +471,7 @@ class InquirerControl(FormattedTextControl):
             tokens.append(("", "\n"))
 
         # prepare the select choices
-        for i, c in enumerate(self.choices):
+        for i, c in enumerate(self.filtered_choices):
             append(i, c)
 
         current = self.get_pointed_at()
@@ -467,7 +513,7 @@ class InquirerControl(FormattedTextControl):
         self.pointed_at = (self.pointed_at + 1) % self.choice_count
 
     def get_pointed_at(self) -> Choice:
-        return self.choices[self.pointed_at]
+        return self.filtered_choices[self.pointed_at]
 
     def get_selected_values(self) -> List[Choice]:
         # get values not labels
@@ -477,6 +523,39 @@ class InquirerControl(FormattedTextControl):
             if (not isinstance(c, Separator) and c.value in self.selected_options)
         ]
 
+    def add_search_character(self, char: Keys) -> None:
+        """Adds a character to the search filter"""
+        if char == Keys.Backspace:
+            self.remove_search_character()
+        else:
+            if self.search_filter is None:
+                self.search_filter = str(char)
+            else:
+                self.search_filter += str(char)
+
+        # Make sure that the selection is in the bounds of the filtered list
+        self.pointed_at = 0
+
+    def remove_search_character(self) -> None:
+        if self.search_filter and len(self.search_filter) > 1:
+            self.search_filter = self.search_filter[:-1]
+        else:
+            self.search_filter = None
+
+    def get_search_string_tokens(self):
+        if self.search_filter is None:
+            return None
+
+        return [
+            ("", "\n"),
+            ("class:question-mark", "/ "),
+            (
+                "class:search_success" if self.found_in_search else "class:search_none",
+                self.search_filter,
+            ),
+            ("class:question-mark", "..."),
+        ]
+
 
 def build_validator(validate: Any) -> Optional[Validator]:
     if validate:
@@ -531,6 +610,10 @@ def create_inquirer_layout(
     )
     _fix_unecessary_blank_lines(ps)
 
+    @Condition
+    def has_search_string():
+        return ic.get_search_string_tokens() is not None
+
     validation_prompt: PromptSession = PromptSession(
         bottom_toolbar=lambda: ic.error_message, **kwargs
     )
@@ -540,6 +623,13 @@ def create_inquirer_layout(
             [
                 ps.layout.container,
                 ConditionalContainer(Window(ic), filter=~IsDone()),
+                ConditionalContainer(
+                    Window(
+                        height=LayoutDimension.exact(2),
+                        content=FormattedTextControl(ic.get_search_string_tokens),
+                    ),
+                    filter=has_search_string & ~IsDone(),
+                ),
                 ConditionalContainer(
                     validation_prompt.layout.container,
                     filter=Condition(lambda: ic.error_message is not None),
diff --git a/questionary/prompts/select.py b/questionary/prompts/select.py
index 41121bd..e41dbe5 100644
--- a/questionary/prompts/select.py
+++ b/questionary/prompts/select.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
+import string
 from typing import Any
 from typing import Dict
 from typing import Optional
@@ -34,6 +35,7 @@ def select(
     use_indicator: bool = False,
     use_jk_keys: bool = True,
     use_emacs_keys: bool = True,
+    use_search_filter: bool = False,
     show_selected: bool = False,
     show_description: bool = True,
     instruction: Optional[str] = None,
@@ -109,6 +111,13 @@ def select(
                         `Ctrl+N` (down) and `Ctrl+P` (up) keys. Arrow keys, j/k keys,
                         emacs keys and shortcuts are not mutually exclusive.
 
+        use_search_filter: Flag to enable search filtering. Typing some string will
+                           filter the choices to keep only the ones that contain the
+                           search string.
+                           Note that activating this option disables "vi-like"
+                           navigation as "j" and "k" can be part of a prefix and
+                           therefore cannot be used for navigation
+
         show_selected: Display current selection choice at the bottom of list.
 
         show_description: Display description of current selection if available.
@@ -124,6 +133,11 @@ def select(
             )
         )
 
+    if use_jk_keys and use_search_filter:
+        raise ValueError(
+            "Cannot use j/k keys with prefix filter search, since j/k can be part of the prefix."
+        )
+
     if use_shortcuts and use_jk_keys:
         if any(getattr(c, "shortcut_key", "") in ["j", "k"] for c in choices):
             raise ValueError(
@@ -179,11 +193,11 @@ def select(
                 tokens.append(("class:instruction", instruction))
             else:
                 if use_shortcuts and use_arrow_keys:
-                    instruction_msg = "(Use shortcuts or arrow keys)"
+                    instruction_msg = f"(Use shortcuts or arrow keys{', type to filter' if use_search_filter else ''})"
                 elif use_shortcuts and not use_arrow_keys:
-                    instruction_msg = "(Use shortcuts)"
+                    instruction_msg = f"(Use shortcuts{', type to filter' if use_search_filter else ''})"
                 else:
-                    instruction_msg = "(Use arrow keys)"
+                    instruction_msg = f"(Use arrow keys{', type to filter' if use_search_filter else ''})"
                 tokens.append(("class:instruction", instruction_msg))
 
         return tokens
@@ -229,6 +243,15 @@ def select(
         while not ic.is_selection_valid():
             ic.select_previous()
 
+    if use_search_filter:
+
+        def search_filter(event):
+            ic.add_search_character(event.key_sequence[0].key)
+
+        for character in string.printable:
+            bindings.add(character, eager=True)(search_filter)
+        bindings.add(Keys.Backspace, eager=True)(search_filter)
+
     if use_arrow_keys:
         bindings.add(Keys.Down, eager=True)(move_cursor_down)
         bindings.add(Keys.Up, eager=True)(move_cursor_up)


Test patch:
diff --git a/tests/prompts/test_checkbox.py b/tests/prompts/test_checkbox.py
index 4b7c840..50da9bd 100644
--- a/tests/prompts/test_checkbox.py
+++ b/tests/prompts/test_checkbox.py
@@ -350,3 +350,42 @@ def test_fail_on_no_method_to_move_selection():
 
     with pytest.raises(ValueError):
         feed_cli_with_input("checkbox", message, text, **kwargs)
+
+
+def test_select_filter_first_choice():
+    message = "Foo message"
+    kwargs = {"choices": ["foo", "bar", "bazz"]}
+    text = KeyInputs.SPACE + KeyInputs.ENTER + "\r"
+
+    result, cli = feed_cli_with_input(
+        "checkbox",
+        message,
+        text,
+        use_search_filter=True,
+        use_jk_keys=False,
+        **kwargs,
+    )
+    assert result == ["foo"]
+
+
+def test_select_filter_multiple_after_search():
+    message = "Foo message"
+    kwargs = {"choices": ["foo", "bar", "bazz", "buzz"]}
+    text = (
+        KeyInputs.SPACE
+        + "bu"
+        + KeyInputs.SPACE
+        + KeyInputs.BACK
+        + KeyInputs.BACK
+        + "\r"
+    )
+
+    result, cli = feed_cli_with_input(
+        "checkbox",
+        message,
+        text,
+        use_search_filter=True,
+        use_jk_keys=False,
+        **kwargs,
+    )
+    assert result == ["foo", "buzz"]
diff --git a/tests/prompts/test_select.py b/tests/prompts/test_select.py
index 1d3eaa5..9aa3759 100644
--- a/tests/prompts/test_select.py
+++ b/tests/prompts/test_select.py
@@ -1,4 +1,6 @@
 # -*- coding: utf-8 -*-
+from copy import copy
+
 import pytest
 
 from questionary import Choice
@@ -199,6 +201,29 @@ def test_allow_shortcut_key_with_True():
     assert result == "bazz"
 
 
+def test_auto_shortcut_key_stable_in_loop():
+    message = "Foo message"
+    choices = [
+        Choice("foo"),
+        Choice("bar"),
+    ]
+    kwargs = {
+        "choices": choices,
+        "use_shortcuts": True,
+    }
+    text = "\r"
+
+    result, cli = feed_cli_with_input("select", message, text, **kwargs)
+    assert result == "foo"
+    result_shortcut_keys = [copy(c.shortcut_key) for c in choices]
+    result2, cli = feed_cli_with_input("select", message, text, **kwargs)
+    assert result2 == "foo"
+    result2_shortcut_keys = [copy(c.shortcut_key) for c in choices]
+    assert (
+        result_shortcut_keys == result2_shortcut_keys
+    ), "Shortcut keys changed across two runs of 'select'"
+
+
 def test_select_initial_choice_with_value():
     message = "Foo message"
     choice = Choice(title="bazz", value="bar")
@@ -384,3 +409,102 @@ def test_select_default_has_arrow_keys():
 
     result, cli = feed_cli_with_input("select", message, text, **kwargs)
     assert result == "bazz"
+
+
+def test_select_filter_with_jk_movement_exception():
+    message = "Foo message"
+    kwargs = {
+        "choices": ["foo", "bazz"],
+        "use_arrow_keys": True,
+        "use_shortcuts": False,
+    }
+    text = "2" + KeyInputs.ENTER + "\r"
+    with pytest.raises(ValueError):
+        feed_cli_with_input("select", message, text, use_search_filter=True, **kwargs)
+
+
+def test_filter_prefix_one_letter():
+    message = "Foo message"
+    kwargs = {"choices": ["abc", "def", "ghi", "jkl"]}
+    text = "g" + KeyInputs.ENTER + "\r"
+
+    result, cli = feed_cli_with_input(
+        "select",
+        message,
+        text,
+        use_search_filter=True,
+        use_jk_keys=False,
+        **kwargs,
+    )
+    assert result == "ghi"
+
+
+def test_filter_prefix_multiple_letters():
+    message = "Foo message"
+    kwargs = {"choices": ["abc", "def", "ghi", "jkl", "jag", "jja"]}
+    text = "j" + "j" + KeyInputs.ENTER + "\r"
+
+    result, cli = feed_cli_with_input(
+        "select",
+        message,
+        text,
+        use_search_filter=True,
+        use_jk_keys=False,
+        **kwargs,
+    )
+    assert result == "jja"
+
+
+def test_select_filter_handle_backspace():
+    message = "Foo message"
+    kwargs = {"choices": ["abc", "def", "ghi", "jkl", "jag", "jja"]}
+    text = "j" + "j" + KeyInputs.BACK + KeyInputs.ENTER + "\r"
+
+    result, cli = feed_cli_with_input(
+        "select",
+        message,
+        text,
+        use_search_filter=True,
+        use_jk_keys=False,
+        **kwargs,
+    )
+    assert result == "jkl"
+
+    message = "Foo message"
+    kwargs = {"choices": ["abc", "def", "ghi", "jkl", "jag", "jja"]}
+    text = (
+        "j"
+        + "j"
+        + KeyInputs.BACK
+        + KeyInputs.BACK
+        + KeyInputs.BACK
+        + KeyInputs.BACK
+        + KeyInputs.ENTER
+        + "\r"
+    )
+
+    result, cli = feed_cli_with_input(
+        "select",
+        message,
+        text,
+        use_search_filter=True,
+        use_jk_keys=False,
+        **kwargs,
+    )
+    assert result == "abc"
+
+
+def test_select_goes_back_to_top_after_filtering():
+    message = "Foo message"
+    kwargs = {"choices": ["abc", "def", "ghi", "jkl", "jag", "jja"]}
+    text = KeyInputs.DOWN + KeyInputs.DOWN + "j" + KeyInputs.ENTER + "\r"
+
+    result, cli = feed_cli_with_input(
+        "select",
+        message,
+        text,
+        use_search_filter=True,
+        use_jk_keys=False,
+        **kwargs,
+    )
+    assert result == "jkl"