diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-07 09:09:31 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-07 09:24:18 +0300 |
| commit | 5e94ba617d19767098bd59506ba1ebfc58c7fd2a (patch) | |
| tree | fb8dc198f3c687514e248ed27dc62cb5bfbac36a | |
| parent | f2b75267a6085f28a415bc8464cf2a70b650e65f (diff) | |
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 <noreply@anthropic.com>
| -rw-r--r-- | internal/ui/ultra.go | 64 |
1 files 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) { |
