Skip to main content

A pure python implementation of fzf (fuzzyfinder) with some additional features using the curses library.

Project description

curses_fzf

A pure Python implementation of fzf (fuzzyfinder) using the curses library - no external fzf binary required.

Although there are many good fzf libraries available, they all have one thing in common: They are wrappers to the shell tool fzf.

This is not inherently bad, but has one major downside: It does not integrate well into Python code.

  • What if you want to fuzzy-find over a list of dicts or objects?
  • What if you want to pre-select items (e.g. tags already set for a resource, that could be unset while selecting new ones)?
  • What if you want to display additional information along with the entry to fuzzy-find on?
  • What if you want to customize the fuzzy-finder algorithm?

To all of the above questions this module is the answer.

Features

Multi Select Mode

from curses_fzf import FuzzyFinder, CursesFzfAborted

fzf = FuzzyFinder(multi=True)
try:
    choices = fzf.find(data)
except CursesFzfAborted:
    print("Fuzzy finder aborted by user.")
else:
    for item in choices:
        print(item)

In its simplest form, FuzzyFinder only requires the data list to present to the user, a single item can then be chosen from the list.

Setting multi to True will allow you to select multiple items using the TAB key.

In any case the returned result is a list of strings. It will contain exactly one items in single-selection mode and 0..n in multi-selection mode.

If selection is aborted a CursesFzfAborted exception is raised, so single selection mode can't return an empty list.

Query Pre-Seeding

from curses_fzf import FuzzyFinder

fzf = FuzzyFinder(query="the in")
choices = fzf.find(data)

By default FuzzyFinder will start with an empty query. The unfiltered list will then be presented in its original order.

If the user enters a filter query the list is reduced to the matching items, sorted by match score (see score function).

The query can also be pre-seeded with a given string. The user is still able to fully modify the query, including completely clearing it. The parameter can be given to FuzzyFinder constructor or the object's find method.

image

Title Prompting The User

from curses_fzf import FuzzyFinder

fzf = FuzzyFinder(title="Select an item:")
choices = fzf.find(data)

Instead of "ITEMS", you can provide a custom header for the FuzzyFinder window. The parameter can be given to FuzzyFinder constructor or the object's find method.

Display Function

from curses_fzf import FuzzyFinder

def display_name_property(item: Any) -> str:
    return item.name

fzf = FuzzyFinder(display=display_name_property)
choices = fzf.find(data)

Since curses_fzf allows you to work with lists of any type of items, you may want to define a custom behavior of how it displays your items. In the above example we have a list of objects, using the name property to represent each item in FuzzyFinder listing.

The display function must return a single line of text. A CursesFzfAssertion exception will be raised, if the function returns multi-line text. If you want to present more complex information, have a look at the preview function.

The default behavior is to stringify the item provided:

FuzzyFinder(display=lambda item: str(item))

Preselect Function

from curses_fzf import FuzzyFinder, ScoringResult

def preselect_items(item: Any, scoring_result: ScoringResult) -> bool:
    return item in PREFERRED_ITEMS

fzf = FuzzyFinder(multi=True, preselect=preselect_items)
choices = fzf.find(data)

If you use FuzzyFinder in multi-select mode, you can pre-select some items using the preselect function. This function is expected to return True if the item should be selected.

The default implementation always returns False.

image

Preview Function

import curses
from curses_fzf import FuzzyFinder, ScoringResult, Color, ColorTheme

def my_preview(preview_window: curses.window, color_theme: ColorTheme, item: Any, result: ScoringResult) -> str:
    preview_window.addstr(1, 1, item.description, curses.color_pair(Color.RED))
    return ""

fzf = FuzzyFinder(preview=my_preview)
choices = fzf.find(data)

The preview function (default None), if set, will show a preview window on the right side of the FuzzyFinder window. You can use this window to present additional information about the item. For example you can yaml.dump dict items.

There are two possible ways to use this function:

Either you ignore the provided preview_window and simply return a string, that can also be multi-line. The FuzzyFinder will take care of the text not leaking out of the window boundaries.

Or you return an empty string and use preview_window to modify the curses window manually. If you do so, you should ensure to handle window boundaries correctly to avoid crashes, e.g. on terminal resizing. See ColorTheme section for information on coloring, the selected color_theme is also provided to the preview function.

See examples folder for more detailed code snippets.

Not only the item is provided, but also the ScoringResult. This allows to display scoring related information.

You can use preview_window_percentage parameter of FuzzyFinder to define the width of the preview window. The default value is 40 percent of the terminal window. Don't worry that the preview window might hide portions of your items, you can toggle the preview window any time using Ctrl + P.

image

Scoring Function

from curses_fzf import FuzzyFinder, ScoringResult

def my_scoring(query: str, candidate: str) -> ScoringResult:
    sr = ScoringResult(query, candidate)
    # ... scoring logic
    sr.score = 100
    # ...
    return sr

fzf = FuzzyFinder(score=my_scoring)
choices = fzf.find(data)

The curses_fzf module comes with built-in scoring functions (default scoring_full_words). Scoring determines if an item is considered to match the query the user entered. The higher the score the higher the item gets sorted among the matches. If score is 0 the item is considered to not be a match, it will not be displayed in the list at all.

A scoring function retrieves the user query as its first argument and the candidate to match as the second. The candidate is the display string of the item in question.

The function is supposed to return a ScoringResult.

ScoringResult

The only important thing about the ScoringResult is its score field. Although there are helper functions, you are free to modify this field directly as your scoring function requires. If the value of this field is 0, the candidate will not be displayed in the list of matches. A higher value indicates a better match and will prioritize the item in the sorted list of results.

The second field to notice is matches, which is a list of tuples containing the starting index and length of all matches inside the candidate string. If set, this information will be used by FuzzyFinder to colorize the matched substrings in the list of query results.

The intended way to set those fields is sr.add_match(position: int, length: int, score: int). The first two parameters represent one tuple appended to the matches list. The score parameter is the score associated with the partial match that position and length identifies, it is added to the score field of this ScoringResult.

ScoringResult also assists with tokenization of the query and candidate, providing the fields query, query_lower, query_words_with_index, candidate, candidate_lower and candidate_words_with_index.

ColorTheme Customization

from curses_fzf import FuzzyFinder, ColorTheme, Color

fzf = FuzzyFinder(color_theme=ColorTheme(text=Color.CYAN))
choices = fzf.find(data)

ColorTheme can be used to customize text colors, e.g. to increase readability. Use the indexes defined via Color enum. If you want to register your own color_pairs, the indexes 1 to 29 are safe to use.

Autoreturn

from curses_fzf import FuzzyFinder

fzf = FuzzyFinder(multi=True, query="foo", autoreturn=3)
choices = fzf.find(data)

If the list provided contains exactly the number of entries defined by autoreturn, the FuzzyFinder will return those entries without user interaction.

This is most useful in combination with a pre-seed query, in which case the number of matches is considered.

The default 0 means "don't autoreturn". If multi=True the number given as autoreturn's value is checked against the filter results. If multi=False the number given as autoreturn's value is not relevant, the match will be returned, if there is only one.

Page Size

The page_size parameter (default 10) defines the number of entries that are skipped by the keys PAGE_UP and PAGE_DOWN.

Help

Press F1 to display a help screen with a list of keyboard actions.

image

Exceptions

CursesFzfException is the base exception type that can catch any curses_fzf exceptions.

If the user aborts the selection using ESC or Ctrl + C, a CursesFzfAborted is raised. In single selection mode the returned list will always contain an item, since otherwise this exception would have been raised. In multi selection mode the returned list can be empty, if the user accepts an empty selection with Enter.

CursesFzfAssertion will be raised if some contracts are broken, e.g. if the display function returns multiline text.

A special case of this assertion is CursesFzfIndexOutOfBounds, which is raised e.g. if the calls to query modification functions use invalid indexes.

Keymap And More

from curses_fzf import FuzzyFinder

fzf = FuzzyFinder()
fzf.keymap[curses.KEY_F2] = lambda: fzf.kb_move_items_cursor_relative(2)
choices = fzf.find(data)

FuzzyFinder is designed to allow for deep customization.

See examples folder for more detailed code snippets, e.g. on how to define your own keyboard actions.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

curses_fzf-0.2.0.tar.gz (29.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

curses_fzf-0.2.0-py3-none-any.whl (16.5 kB view details)

Uploaded Python 3

File details

Details for the file curses_fzf-0.2.0.tar.gz.

File metadata

  • Download URL: curses_fzf-0.2.0.tar.gz
  • Upload date:
  • Size: 29.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.20

File hashes

Hashes for curses_fzf-0.2.0.tar.gz
Algorithm Hash digest
SHA256 519d0f4af5e62a2f4f0c900cf2b12c078745863fdfe45fdf453ec0c7ace28750
MD5 77f1e6750ca0f2e00935fcb92f2ec2f3
BLAKE2b-256 a5b2048d9e7321b7f10bafcb223d82ff147feec3ee5b3d93e45f8bc52667d81a

See more details on using hashes here.

File details

Details for the file curses_fzf-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: curses_fzf-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 16.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.20

File hashes

Hashes for curses_fzf-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2188499119a0328d1455c8961cd3d47d0b48132c8c4ab5e33c9724b786aa134e
MD5 1049322ff43aa1ce67568a4bd9363706
BLAKE2b-256 8775d682dd88d9dfa9e1e23d2279891be4b9a9e120506c2f9a239ff2c6246b68

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page