Giter Club home page Giter Club logo

textual-autocomplete's Introduction

textual-autocomplete

textual-autocomplete is a Python library for creating dropdown autocompletion menus in Textual applications, allowing users to quickly select from a list of suggestions as they type. textual-autocomplete supports Textual version 0.14.0 and above.

image

Video example
textual-autocomplete-styles.mov

Warning Textual still has a major version number of 0, meaning there are still significant API changes happening which can sometimes impact this project. I'll do my best to keep it compatible with the latest version of Textual, but there may be a slight delay between Textual releases and this library working with said release.

Quickstart

Simply wrap a Textual Input widget as follows:

from textual.app import ComposeResult
from textual.widgets import Input
from textual_autocomplete import AutoComplete, Dropdown, DropdownItem

def compose(self) -> ComposeResult:
    yield AutoComplete(
        Input(placeholder="Type to search..."),
        Dropdown(items=[
            DropdownItem("Glasgow"),
            DropdownItem("Edinburgh"),
            DropdownItem("Aberdeen"),
            DropdownItem("Dundee"),
        ]),
    )

There are more complete examples here.

Installation

textual-autocomplete can be installed from PyPI using your favourite dependency manager.

Usage

Wrapping your Input

As shown in the quickstart, you can wrap the Textual builtin Input widget with AutoComplete, and supply a Dropdown. The AutoComplete manages communication between the Input and the Dropdown.

The Dropdown is the widget you see on screen, as you type into the input.

The DropdownItems contain up to 3 columns. All must contain a "main" column, which is the central column used in the filtering. They can also optionally contain a left and right metadata column.

Supplying data to Dropdown

You can supply the data for the dropdown via a list or a callback function.

Using a list

The easiest way to use textual-autocomplete is to pass in a list of DropdownItems, as shown in the quickstart.

Using a callable

Instead of passing a list of DropdownItems, you can supply a callback function which will be called with the current input state. From this function, you should return the list of DropdownItems you wish to be displayed.

See here for a usage example.

Keyboard control

  • Press the Up/Down arrow keys to navigate.
  • Press Enter to select the item in the dropdown and fill it in.
  • Press Tab to fill in the selected item, and move focus.
  • Press Esc to hide the dropdown.
  • Press the Up/Down arrow keys to force the dropdown to appear.

Styling

The Dropdown itself can be styled using Textual CSS.

For more fine-grained control over styling, you can target the following CSS classes:

  • .autocomplete--highlight-match: the highlighted portion of a matching item
  • .autocomplete--selection-cursor: the item the selection cursor is on
  • .autocomplete--left-column: the left metadata column, if it exists
  • .autocomplete--right-column: the right metadata column, if it exists

Since the 3 columns in DropdownItem support Rich Text objects, they can be styled dynamically. The custom_meta.py file is an example of this, showing how the rightmost column is coloured dynamically based on the city population.

The examples directory contains multiple examples of custom styling.

Messages

When you select an item in the dropdown, an AutoComplete.Selected event is emitted.

You can declare a handler for this event on_auto_complete_selected(self, event) to respond to an item being selected.

An item is selected when it's highlighted in the dropdown, and you press Enter or Tab.

Pressing Enter simply fills the value in the dropdown, whilst Tab fills the value and then shifts focus from the input.

Other notes

  • textual-autocomplete will create a new layer at runtime on the Screen that the AutoComplete is on. The Dropdown will be rendered on this layer.
  • The position of the dropdown is currently fixed below the value entered into the Input. This means if your Input is at the bottom of the screen, it's probably not going to be much use for now. I'm happy to discuss or look at PRs that offer a flag for having it float above.
  • There's currently no special handling for when the dropdown meets the right-hand side of the screen.
  • Do not apply margin to the Dropdown. The position of the dropdown is updated by applying margin to the top/left of it.
  • There's currently no debouncing support, but I'm happy to discuss or look at PRs for this.
  • There are a few known issues/TODOs in the code, which will later be transferred to GitHub.
  • Test coverage is currently non-existent - sorry!

textual-autocomplete's People

Contributors

darrenburns avatar geoff-va avatar jtyers avatar khughitt avatar umbsublime avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

textual-autocomplete's Issues

textual-autocomplete breaks textual-0.12+

Reproduce:

python3 -mvenv python
source python/bin/activate
pip install textual==0.12.1
python git/textual/examples/calculator.py # it works!
pip install textual-autocomplete
python git/textual/examples/calculator.py # sad face

Looks like it may have broken all widget with the new context manager feature?

Dropdown displays even if input does not have focus

Awesome library, thanks for this. It's s real game changer for turning Textual apps into productivity boosters :-)

I have a screen where 4 or 5 of these are rendered in a container. The screen is a CRUD-style update screen, and when the screen is pushed to the stack each input's value is then updated with the value from the data. The values may or may not match exactly a completion DropdownItem though I think this is irrelevant.

Updating the value of the inputs causes the dropdowns to pop up even when those inputs have no focus, resulting in a screen full of dropdowns.

I think I've found the bug so will open a PR.

Support for textual 0.11.0

Textual 0.11.0 had breaking changes:

  • Removed reactive.watch in favor of DOMNode.watch
  • Removed emit_no_wait, to be replaced with post_message_no_wait

Making those adjustments seems to get it working again - some simple touch testing at least.

Multiple autocomplete items event handling

When there are more than one Autocomplete items in an App, is there a way to handle events only for the item that caused the event?

Currently, from within handler on_auto_complete_selected(self, event) I think there is no way to distinguish from which item the event was caused and take appropriate action, because the event itself does not contain such information.

So, all my autocomplete items will have the same event handling, but that is something that might not be the desired behavior.

Drop-down menu doesn't appear

Greetings!

I'm trying to run the examples for the latest version of textual/textual-autocomplete, but have been unable to get the menus to render as expected.

When attempting to run either of the demos, I get a usable input field and tab completion appears to work, but the autocomplete options are never shown:

textual-autocomplete-bug-2022-12-27_09.39.52.mp4

This is on Arch / kitty / zsh. I tried a few other terminals, just in case (Alacritty, xterm, urxvt), but no luck. Same result in Bash/ZSH.

I also tried installing textual in a clean conda environment and running the examples from there with the same result.

I tried running the tests for textual, and there are a few errors there, but none that are obviously related to the autocomplete issue:

========================================================= short test summary info ==========================================================
ERROR tests/devtools/test_devtools.py::test_devtools_valid_client_log
ERROR tests/devtools/test_devtools.py::test_devtools_string_not_json_message
ERROR tests/devtools/test_devtools.py::test_devtools_invalid_json_message
ERROR tests/devtools/test_devtools.py::test_devtools_spillover_message
ERROR tests/devtools/test_devtools.py::test_devtools_console_size_change
ERROR tests/devtools/test_devtools_client.py::test_devtools_client_is_connected
ERROR tests/devtools/test_devtools_client.py::test_devtools_log_places_encodes_and_queues_message
ERROR tests/devtools/test_devtools_client.py::test_devtools_log_places_encodes_and_queues_many_logs_as_string
ERROR tests/devtools/test_devtools_client.py::test_devtools_log_spillover
ERROR tests/devtools/test_devtools_client.py::test_devtools_client_update_console_dimensions
ERROR tests/devtools/test_redirect_output.py::test_print_redirect_to_devtools_only
ERROR tests/devtools/test_redirect_output.py::test_print_redirect_to_logfile_only
ERROR tests/devtools/test_redirect_output.py::test_print_redirect_to_devtools_and_logfile
ERROR tests/devtools/test_redirect_output.py::test_print_without_flush_not_sent_to_devtools
ERROR tests/devtools/test_redirect_output.py::test_print_forced_flush_sent_to_devtools
ERROR tests/devtools/test_redirect_output.py::test_print_multiple_args_batched_as_one_log
ERROR tests/devtools/test_redirect_output.py::test_print_strings_containing_newline_flushed
ERROR tests/devtools/test_redirect_output.py::test_flush_flushes_buffered_logs
===================================== 699 passed, 1 skipped, 4 xfailed, 1 warning, 18 errors in 18.13s =======

I had a friend check on another machine (also Arch), and the demos worked fine for him, so it's something about my setup that autocomplete is not liking.

Any ideas what the issue might be or how I can get more information to localize the problem?

I'm excited to see your work on this though! A solid autocomplete is something TUI frameworks have been sorely missing.

Cheers,
Keith

System info

  • Arch Linux
  • Python 3.10.9
  • textual 0.8.1
  • textual-autocomplete 3a20f09
  • zsh 5.9
  • kitty 0.26.5

Bug in example: incorrect sorting by match at start of text

# Favour items that start with the Input value, pull them to the top
ordered = sorted(matches, key=lambda v: v.main.plain.startswith(input_state.value.lower()))

This should be:

    # Favour items that start with the Input value, pull them to the top
    ordered = sorted(matches, key=lambda v: v.main.plain.lower().startswith(input_state.value.lower()), reverse=True)

or:

    # Favour items that start with the Input value, pull them to the top
    ordered = sorted(matches, key=lambda v: not v.main.plain.lower().startswith(input_state.value.lower()))

You might not have noticed the negative affect of the sort being backwards because of the missing lowercasing (a bug hiding a bug).


As a side note, I created a bug myself when extending the code to use two sequential sorts:

    # Only keep matches that contain the Input value as a substring
    matches = [c for c in items if input_state.value.lower() in c.main.plain.lower() + c.right_meta.plain.lower()]
    # Favour items that start with the Input value, pull them to the top
    # Keywords starting with the Input value are less important than the main text starting with it,
    # so sort by the more important criteria last.
    ordered = sorted(matches, key=lambda v: v.right_meta.plain.lower().startswith(input_state.value.lower()), reverse=True)
    ordered = sorted(matches, key=lambda v: v.main.plain.lower().startswith(input_state.value.lower()), reverse=True)

    return ordered

The second sort failed because when I copied the line of code I was still passing the original matches but it would need to be passed ordered. If it used just one variable (matches) I wouldn't have run into this problem:

    # Only keep matches that contain the Input value as a substring
    matches = [c for c in items if input_state.value.lower() in c.main.plain.lower() + c.right_meta.plain.lower()]
    # Favour items that start with the Input value, pull them to the top
    # Keywords starting with the Input value are less important than the main text starting with it,
    # so sort by the more important criteria last.
    matches = sorted(matches, key=lambda v: v.right_meta.plain.lower().startswith(input_state.value.lower()), reverse=True)
    matches = sorted(matches, key=lambda v: v.main.plain.lower().startswith(input_state.value.lower()), reverse=True)

    return matches

(And it doesn't make sense to have matches, partially_ordered, ordered; imagine trying to keep that up!)
So it could make the example slightly more malleable. Just a little tweak that I would recommend.

Please add a license

For compliance many organizations are not allowed to use software that is unlicensed

[BUG] `AutoComplete` no longer displays inside `Static` widget (v2.1.0b0)

Is there an existing issue for this?

  • I have searched the open issues

Description of the bug

After upgrading to the latest version of textual-autocomplete 2.1.0b0, the AutoComplete input no longer displays if inside a Static widget.

I think I'm right in thinking that Static is a "base" widget in Textual, so likely other similar issues but I've only just started testing with this new version.

To Reproduce

# location_search.py

from textual.app import ComposeResult
from textual.widgets import Input, Static
from textual_autocomplete import AutoComplete, Dropdown, DropdownItem

ITEMS = [
    DropdownItem("Glasgow"),
    DropdownItem("Edinburgh"),
    DropdownItem("Aberdeen"),
    DropdownItem("Dundee"),
]


class LocationSearch(Static):
    def on_mount(self) -> None:
        self.query_one(Input).focus()

    def compose(self) -> ComposeResult:
        yield AutoComplete(
            Input(placeholder="Search for a location"),
            Dropdown(items=ITEMS),
        )
# locations_app.py

from textual.app import App, ComposeResult
from textual.containers import Container
from textual.widgets import Footer, Input, Markdown

from location_search import LocationSearch


class LocationsApp(App):
    def on_mount(self) -> None:
        self.query_one(Input).focus()

    def compose(self) -> ComposeResult:
        with Container():
            yield Markdown("# Find a location")
            yield LocationSearch()
        yield Footer()

Is it possible to have Grouped Autocomplete?

I'm not sure if "Grouped Autocomplete" is an actual term, it might be something I'm making up.

The end goal would be to have different sets of autocomplete based on the placement in the input box. In my mind I'm thinking of 3 groups: object, verb and query. Each separated by a space character.

For this example, I imagine a Textual input box expecting input in the order: [object] [verb] [query]

Where as you type in the input box you start typing the first few letters of the 'object' and the only AutoComplete options shown are those in the list of 'objects'. You can continue typing or accept one of the autocomplete suggestions.

You add a space character and start typing again to begin the 'verb' and again as you begin the first few characters the only AutoComplete options shown are those from the 'verb' list.

After accepting an AutoComplete suggestion for the 'verb' you input a space character and begin typing the final item a 'query'. Again, as you get a few characters in the only AutoComplete items shown are those in the 'query' list.

I'm not quiet sure how to pull this off or if textual-autocomplete is the right module for the job. What are your thoughts?

[FEATURE] Display a hint above the input (or in a dynamic way)

Hi @darrenburns, first of all I find your plugin very useful and wanted to ask if there are any plans to include textual-autocomplete in the Textual library itself? Unfortunately, I don't see such a widget on the roadmap but maybe you have some thoughts because I know that you contribute to textual itself.

The second thing and the very reason for this issue that I wanted to raise - it's the lack of ability to decide whether the hint will appear at the top or at the bottom. My use case looks like this: at the very bottom of the screen there is a small one-line console to which I would like to attach autocomplete (this console is very similar to one of your applications - kupo).

Unfortunately, due to the lack of a choice to make it appear at the top, I am a bit unable to apply it to this layout.

I saw in the code that you predicted such a possibility. e.g:

# edge: Whether the dropdown should appear above or below.

But I think the best way would be to do it dynamically (check if it could be displayed below and if not, do it above)

I wanted to ask if you anticipate introducing this maybe in the near future? Or do you have any hint where to look to add something like this (maybe via PR)?

[BUG] Dropdown list is not constrained to screen

If the terminal is small enough that the max number of items doesn't fit on screen, the bottom of the dropdown is cut off, making the the bottom of the scrollbar and the last few items inaccessible.

Screencast:
Screencast

Also the scrollbar appears to be half-width, probably cut off horizontally. (Maybe it the scrollbar width should be subtracted?)

I guess this project predates the overlay and constrain styles added in Textual 0.24.0 and could benefit from them, although they're still undocumented.

[FEATURE] Adjust layout

I want to remove the spacing between the left_meta column's content and the main column:

screenshot showing unwanted spacing

(It's also distracting having the columns resize as you type, and on wider terminals, the spacing gets worse.)

This could work something like:

.autocomplete--left-column {
    width: 3;
}

using self.parent.get_component_styles("autocomplete--left-column").width.
What all styles should be supported, and whether this opens up a can of worms, I can't say.

I would also like to position the dropdown flush with the left of the input, since it will be suggesting whole input values, not substrings to be concatenated with the current input value.
I don't have a strong opinion on how this should be specified (CSS or Python API), but the following doesn't work:

Dropdown {
    margin-left: 0 !important;
}

Unlike in the browser, !important can't beat an inline style.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.