Skip to content

SearchInput

SearchInput is a single-line search box with local edit mode. By default, pressing s enters input mode. Internally it opens an input layer that cuts off lower handlers, so j/k/q while editing will be written to or handled by the input box instead of continuing to move a background list or quit the page.

search input demo

This recording is generated by docs/tapes/search-input.tape. The example first moves once in the background list, then enters search, types dep, and submits. It then enters edit mode again, appends zz, cancels with Esc, clears the value, and restores background shortcuts.

cargo run --example search_input

Press s to search. While typing, press Enter to submit and Esc to cancel. In the background state, press j/k to move the list and q to quit.

SearchInput(
    value: query.read().to_string(),
    placeholder: "Press s to search commands".to_string(),
    on_change: move |next: String| query.set(next),
    on_submit: move |value: String| {
        submitted.set(format!("submitted {value}"));
        true
    },
)

SearchInput is controlled: the displayed value comes from value, and every input change is reported through on_change. Internally, the component only keeps cursor state, temporary validation state, and whether it is currently editing.

After entering edit mode, the component registers an EventScope::Layer(layer) handler and sets that layer to blocks_lower=true. This means the background page does not receive the same j/k/q keys:

hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
    // Receives j/k/q only while SearchInput is not in edit mode.
})

On Enter, it calls on_submit(value) -> bool. Returning true closes edit mode; returning false keeps the user in the input, which fits validation failures, unfinished remote search, or flows that require confirmation first.

Esc cancels edit mode. If clear_on_escape: true, the component also clears its internal input, calls on_change(String::new()), and calls on_clear(()).

validate is a synchronous validation callback that returns (bool, String):

validate: move |value: String| {
    if value.len() > 18 {
        (false, "too long".to_string())
    } else if value.is_empty() {
        (true, "type to filter".to_string())
    } else {
        (true, format!("{} matches", count_matches(&value)))
    }
},

The validation message is shown in the border title. true uses success_border_style / success_status_style; false uses error_border_style / error_status_style; when there is no validation result, active_border_style is used.

is_editing is an external “editing is allowed” switch, not the component’s current internal edit state. It is useful for page-level search disabling, such as when a modal is open, data is switching, or permission is missing:

SearchInput(
    is_editing: can_search.get(),
    activate_key: KeyCode::Char('/'),
)

When is_editing becomes false, the component exits internal edit mode too, preventing the UI from still looking active after the input layer has closed.

SearchInput accepts width, margin, and offset, and forwards them to its internal Border. Height is fixed at three rows to keep the single-line input box visually stable:

SearchInput(
    width: Constraint::Fill(1),
    border_style: Style::new().fg(Color::Cyan),
    active_border_style: Style::new().fg(Color::Yellow),
    success_border_style: Style::new().fg(Color::Green),
    error_border_style: Style::new().fg(Color::Red),
    cursor_style: Style::new().bg(Color::Yellow),
)

When you need fully custom input behavior, use the low-level Input together with use_input_layer. For modal-style input exclusivity, see Input Layers and the Input Mutex tutorial.