QueryParser and Query

Alfred passes the Script Filter a single raw string — whatever the user has typed after the keyword trigger. For simple handlers this string can be used directly. For multi-step interactions, where the meaning of the input depends on how many words have been typed, you need to tokenise it first.

QueryParser and Query handle this tokenisation.

Basic Usage

The quickest way to parse a query string is from_str(), which uses the default space-delimited parser:

import afwf.api as afwf

q = afwf.Query.from_str("  hello   world  ")
q.parts           # ["", "", "hello", "", "", "world", "", ""]  (raw split)
q.trimmed_parts   # ["hello", "world"]                          (empty parts removed)
q.n_trimmed_parts # 2

parts vs trimmed_parts

parts is the direct result of splitting on the delimiter — it preserves empty strings produced by leading, trailing, or consecutive delimiters.

trimmed_parts strips whitespace from each part and removes empty strings. This is what you almost always want when branching on the number of tokens:

q = afwf.Query.from_str("")
q.n_trimmed_parts   # 0  → show default list

q = afwf.Query.from_str("username")
q.n_trimmed_parts   # 1  → fuzzy-filter by key

q = afwf.Query.from_str("username alice")
q.n_trimmed_parts   # 2  → show confirmation item

Custom Delimiters

Use from_delimiter() when you need to split on something other than a space, or on multiple delimiters at once:

parser = afwf.QueryParser.from_delimiter("/")
q = parser.parse("2026/04/08")
q.trimmed_parts   # ["2026", "04", "08"]

parser = afwf.QueryParser.from_delimiter([" ", ","])
q = parser.parse("a, b, c")
q.trimmed_parts   # ["a", "b", "c"]

The Multi-Step Interaction Pattern

The most common use of Query is branching a Script Filter’s behaviour on n_trimmed_parts. The set_settings example demonstrates the three states a two-word handler typically has:

@afwf.log_error(log_file=afwf.path_enum.dir_afwf / "set_settings.log")
def main(query: str) -> afwf.ScriptFilter:
    sf = afwf.ScriptFilter()
    q = afwf.Query.from_str(query)

    if q.n_trimmed_parts == 0:
        # No input yet — show all keys.
        sf.items.extend(_all_key_items())

    elif q.n_trimmed_parts == 1:
        # One word typed — fuzzy-filter the key list.
        key = q.trimmed_parts[0]
        all_items = _all_key_items()
        matcher = fuzzy_item.FuzzyItemMatcher.from_items(all_items)
        matched = matcher.match(key, threshold=0)
        sf.items.extend(matched if matched else all_items)

    else:
        # Two+ words — treat first as key, rest joined as value.
        key = q.trimmed_parts[0]
        value = " ".join(q.trimmed_parts[1:])
        if key in SettingsKeyEnum.__members__:
            item = afwf.Item(
                title=f"Set settings.{key} = {value!r}",
            )
            item.run_script(_build_cmd(key, value))
            item.send_notification(title=f"Set settings.{key} = {value!r}")
            sf.items.append(item)
        else:
            item = afwf.Item(title=f"{key!r} is not a valid settings key")
            item.set_icon(afwf.IconFileEnum.error)
            sf.items.append(item)

    return sf

The branch logic reads naturally:

  • 0 words — the user has not typed anything; show all available options.

  • 1 word — the user is narrowing down; fuzzy-filter the option list.

  • 2+ words — the user has selected a key and is entering a value; show a confirmation item with the action pre-built.

This pattern keeps each Script Filter invocation stateless: the full context (key and value) is re-parsed from the query string on every keystroke.

trimmed_parts Index Access

After checking n_trimmed_parts, index into trimmed_parts directly:

q = afwf.Query.from_str("username alice bob")

key   = q.trimmed_parts[0]          # "username"
value = " ".join(q.trimmed_parts[1:])  # "alice bob"

This is safe because you have already verified n_trimmed_parts >= 2 before reaching this branch.