From 5e94ba617d19767098bd59506ba1ebfc58c7fd2a Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 7 Apr 2026 09:09:31 +0300 Subject: ui: make ultra mode search live and regex-aware Replace the Enter-to-apply search with live filtering: every keystroke recompiles the regex and rebuilds ultraFiltered immediately, so results update as the user types. Behaviour summary: - Typing: regex compiled and filter applied on each character. - Invalid regex mid-type: previous filter kept unchanged (no flicker). - Enter: confirm and close the input (empty Enter clears the filter). - Esc: cancel, clear the filter, close the input. - Regexes are always supported (was already the case via compileAndCacheRegex, now usable interactively as you type). Extracted ultraApplySearch() to centralise the compile+filter logic. Co-Authored-By: Claude Sonnet 4.6 --- internal/ui/ultra.go | 64 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go index e2ad3e9..16c1a2c 100644 --- a/internal/ui/ultra.go +++ b/internal/ui/ultra.go @@ -460,34 +460,66 @@ func (m *Model) ultraFilteredIndexes(re *regexp.Regexp) []int { return indexes } +// handleUltraSearchMode handles keystrokes while the ultra search input is open. +// Results are filtered live on every keystroke; Enter confirms (keeps the +// filter, closes the input); Esc cancels (clears the filter, closes the input). +// The search term is treated as a regular expression. func (m *Model) handleUltraSearchMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { - onEnter := func(value string) error { - if value == "" { + switch msg.String() { + case "enter": + // Empty input clears the filter; non-empty confirms and keeps it. + if strings.TrimSpace(m.ultraSearchInput.Value()) == "" { m.ultraSearchRegex = nil m.ultraFiltered = nil m.ultraCursor = 0 m.ultraOffset = 0 - return nil } + m.ultraSearching = false + m.ultraSearchInput.Blur() + return m, nil + case "esc": + // Cancel: clear the filter and close the search input. + m.ultraSearching = false + m.ultraSearchInput.SetValue("") + m.ultraSearchInput.Blur() + m.ultraSearchRegex = nil + m.ultraFiltered = nil + m.ultraCursor = 0 + m.ultraOffset = 0 + return m, nil + } - re, err := compileAndCacheRegex(value) - if err != nil { - return err - } + // Forward the keystroke to the text input widget. + var cmd tea.Cmd + m.ultraSearchInput, cmd = m.ultraSearchInput.Update(msg) + + // Recompile and refilter on every change for live results. + m.ultraApplySearch(m.ultraSearchInput.Value()) + return m, cmd +} - m.ultraSearchRegex = re - m.ultraFiltered = m.ultraFilteredIndexes(re) +// ultraApplySearch compiles value as a regex and updates the filtered task +// index. If value is empty the filter is cleared. If the regex is invalid +// (e.g. mid-typing an incomplete pattern) the existing filter is left +// unchanged so the display does not flicker. +func (m *Model) ultraApplySearch(value string) { + value = strings.TrimSpace(value) + if value == "" { + m.ultraSearchRegex = nil + m.ultraFiltered = nil m.ultraCursor = 0 m.ultraOffset = 0 - return nil + return } - - onExit := func() { - m.ultraSearching = false - m.ultraSearchInput.SetValue("") + re, err := compileAndCacheRegex(value) + if err != nil { + // Keep the previous filter while the regex is incomplete. + return } - - return m.handleTextInput(msg, &m.ultraSearchInput, onEnter, onExit) + m.ultraSearchRegex = re + m.ultraFiltered = m.ultraFilteredIndexes(re) + m.ultraCursor = 0 + m.ultraOffset = 0 } func (m *Model) ultraMoveSearchMatch(delta int) { -- cgit v1.2.3